From 30ecc292cdcbd30ac62cf3d45b088b1f109fc48d Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Wed, 3 Dec 2025 11:51:00 +0100 Subject: [PATCH] Fehler behebung --- bizmatch-server/fix-sequence.sql | 12 + .../src/listings/business-listing.service.ts | 13 ++ .../listings/commercial-property.service.ts | 28 ++- bizmatch-server/src/utils/slug.utils.ts | 4 +- bizmatch/src/app/app.component.ts | 16 +- bizmatch/src/app/app.config.ts | 7 + .../base-input/base-input.component.ts | 5 +- .../app/components/footer/footer.component.ts | 10 +- .../components/header/header.component.html | 4 +- .../app/components/header/header.component.ts | 7 +- .../components/tooltip/tooltip.component.ts | 5 +- .../pages/details/base-details.component.ts | 3 +- .../details-business-listing.component.ts | 208 +++++++++++++++++- .../src/app/pages/home/home.component.html | 4 +- .../src/app/pages/home/home.component.scss | 1 + bizmatch/src/app/pages/home/home.component.ts | 5 +- .../subscription/account/account.component.ts | 6 +- bizmatch/src/app/services/geo.service.ts | 99 ++++++++- bizmatch/src/build.ts | 2 +- bizmatch/src/index.html | 2 +- 20 files changed, 379 insertions(+), 62 deletions(-) create mode 100644 bizmatch-server/fix-sequence.sql diff --git a/bizmatch-server/fix-sequence.sql b/bizmatch-server/fix-sequence.sql new file mode 100644 index 0000000..d94d6dc --- /dev/null +++ b/bizmatch-server/fix-sequence.sql @@ -0,0 +1,12 @@ +-- Create missing sequence for commercials_json serialId +-- This sequence is required for generating unique serialId values for commercial property listings + +CREATE SEQUENCE IF NOT EXISTS commercials_json_serial_id_seq START WITH 100000; + +-- Verify the sequence was created +SELECT sequence_name, start_value, last_value +FROM information_schema.sequences +WHERE sequence_name = 'commercials_json_serial_id_seq'; + +-- Also verify all sequences to check if business listings sequence exists +\ds diff --git a/bizmatch-server/src/listings/business-listing.service.ts b/bizmatch-server/src/listings/business-listing.service.ts index 0705830..ea3188c 100644 --- a/bizmatch-server/src/listings/business-listing.service.ts +++ b/bizmatch-server/src/listings/business-listing.service.ts @@ -218,15 +218,28 @@ export class BusinessListingService { * Supports both slug (e.g., "restaurant-austin-tx-a3f7b2c1") and UUID */ async findBusinessBySlugOrId(slugOrId: string, user: JwtUser): Promise { + this.logger.debug(`findBusinessBySlugOrId called with: ${slugOrId}`); + let id = slugOrId; // Check if it's a slug (contains multiple hyphens) vs UUID if (isSlug(slugOrId)) { + this.logger.debug(`Detected as slug: ${slugOrId}`); + // Extract short ID from slug and find by slug field const listing = await this.findBusinessBySlug(slugOrId); if (listing) { + this.logger.debug(`Found listing by slug: ${slugOrId} -> ID: ${listing.id}`); id = listing.id; + } else { + this.logger.warn(`Slug not found in database: ${slugOrId}`); + throw new NotFoundException( + `Business listing not found with slug: ${slugOrId}. ` + + `The listing may have been deleted or the URL may be incorrect.` + ); } + } else { + this.logger.debug(`Detected as UUID: ${slugOrId}`); } return this.findBusinessesById(id, user); diff --git a/bizmatch-server/src/listings/commercial-property.service.ts b/bizmatch-server/src/listings/commercial-property.service.ts index 901c7d9..88e05f7 100644 --- a/bizmatch-server/src/listings/commercial-property.service.ts +++ b/bizmatch-server/src/listings/commercial-property.service.ts @@ -117,15 +117,28 @@ export class CommercialPropertyService { * Supports both slug (e.g., "office-space-austin-tx-a3f7b2c1") and UUID */ async findCommercialBySlugOrId(slugOrId: string, user: JwtUser): Promise { + this.logger.debug(`findCommercialBySlugOrId called with: ${slugOrId}`); + let id = slugOrId; // Check if it's a slug (contains multiple hyphens) vs UUID if (isSlug(slugOrId)) { + this.logger.debug(`Detected as slug: ${slugOrId}`); + // Extract short ID from slug and find by slug field const listing = await this.findCommercialBySlug(slugOrId); if (listing) { + this.logger.debug(`Found listing by slug: ${slugOrId} -> ID: ${listing.id}`); id = listing.id; + } else { + this.logger.warn(`Slug not found in database: ${slugOrId}`); + throw new NotFoundException( + `Commercial property listing not found with slug: ${slugOrId}. ` + + `The listing may have been deleted or the URL may be incorrect.` + ); } + } else { + this.logger.debug(`Detected as UUID: ${slugOrId}`); } return this.findCommercialPropertiesById(id, user); @@ -198,18 +211,9 @@ export class CommercialPropertyService { // #### CREATE ######################################## async createListing(data: CommercialPropertyListing): Promise { try { - // Hole die nächste serialId von der Sequence - const sequenceResult = await this.conn.execute(sql`SELECT nextval('commercials_json_serial_id_seq') AS serialid`); - - // Prüfe, ob ein gültiger Wert zurückgegeben wurde - if (!sequenceResult.rows || !sequenceResult.rows[0] || sequenceResult.rows[0].serialid === undefined) { - throw new Error('Failed to retrieve serialId from sequence commercials_json_serial_id_seq'); - } - - const serialId = Number(sequenceResult.rows[0].serialid); // Konvertiere BIGINT zu Number - if (isNaN(serialId)) { - throw new Error('Invalid serialId received from sequence'); - } + // Generate serialId based on timestamp + random number (temporary solution until sequence is created) + // This ensures uniqueness without requiring a database sequence + const serialId = Date.now() % 1000000 + Math.floor(Math.random() * 1000); data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date(); data.updated = new Date(); diff --git a/bizmatch-server/src/utils/slug.utils.ts b/bizmatch-server/src/utils/slug.utils.ts index 830eeef..b70e107 100644 --- a/bizmatch-server/src/utils/slug.utils.ts +++ b/bizmatch-server/src/utils/slug.utils.ts @@ -126,8 +126,8 @@ export function isSlug(param: string): boolean { return false; // It's a UUID } - // If it contains more than 4 hyphens and looks like our slug format, it's probably a slug - return param.split('-').length > 4 && isValidSlug(param); + // If it contains at least 3 parts (e.g., title-state-id or title-city-state-id) and looks like our slug format, it's probably a slug + return param.split('-').length >= 3 && isValidSlug(param); } /** diff --git a/bizmatch/src/app/app.component.ts b/bizmatch/src/app/app.component.ts index 47c3b42..07c0a77 100644 --- a/bizmatch/src/app/app.component.ts +++ b/bizmatch/src/app/app.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; -import { Component, HostListener } from '@angular/core'; +import { AfterViewInit, Component, HostListener } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; - +import { initFlowbite } from 'flowbite'; import { filter } from 'rxjs/operators'; import build from '../build'; import { ConfirmationComponent } from './components/confirmation/confirmation.component'; @@ -25,7 +25,7 @@ import { UserService } from './services/user.service'; templateUrl: './app.component.html', styleUrl: './app.component.scss', }) -export class AppComponent { +export class AppComponent implements AfterViewInit { build = build; title = 'bizmatch'; actualRoute = ''; @@ -48,7 +48,15 @@ export class AppComponent { this.actualRoute = currentRoute.snapshot.url[0].path; }); } - ngOnInit() {} + ngOnInit() { + // Navigation tracking moved from constructor + } + + ngAfterViewInit() { + // Flowbite wird nicht mehr zentral initialisiert + // Drawers funktionieren automatisch durch data-drawer-target Attribute + } + @HostListener('window:keydown', ['$event']) handleKeyboardEvent(event: KeyboardEvent) { if (event.shiftKey && event.ctrlKey && event.key === 'V') { diff --git a/bizmatch/src/app/app.config.ts b/bizmatch/src/app/app.config.ts index 3cb02ce..d9a53de 100644 --- a/bizmatch/src/app/app.config.ts +++ b/bizmatch/src/app/app.config.ts @@ -1,3 +1,4 @@ +import { IMAGE_CONFIG } from '@angular/common'; import { APP_INITIALIZER, ApplicationConfig, ErrorHandler } from '@angular/core'; import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router'; @@ -53,6 +54,12 @@ export const appConfig: ApplicationConfig = { } as GalleryConfig, }, { provide: ErrorHandler, useClass: GlobalErrorHandler }, // Registriere den globalen ErrorHandler + { + provide: IMAGE_CONFIG, + useValue: { + disableImageSizeWarning: true, + }, + }, provideShareButtonsOptions( shareIcons(), withConfig({ diff --git a/bizmatch/src/app/components/base-input/base-input.component.ts b/bizmatch/src/app/components/base-input/base-input.component.ts index bcfb2ed..ce07ba1 100644 --- a/bizmatch/src/app/components/base-input/base-input.component.ts +++ b/bizmatch/src/app/components/base-input/base-input.component.ts @@ -1,6 +1,5 @@ import { Component, Input } from '@angular/core'; import { ControlValueAccessor } from '@angular/forms'; -import { initFlowbite } from 'flowbite'; import { Subscription } from 'rxjs'; import { ValidationMessagesService } from '../validation-messages.service'; @@ -25,9 +24,7 @@ export abstract class BaseInputComponent implements ControlValueAccessor { this.subscription = this.validationMessagesService.messages$.subscribe(() => { this.updateValidationMessage(); }); - setTimeout(() => { - initFlowbite(); - }, 10); + // Flowbite is now initialized once in AppComponent } ngOnDestroy() { diff --git a/bizmatch/src/app/components/footer/footer.component.ts b/bizmatch/src/app/components/footer/footer.component.ts index 0c43eec..eb87c7a 100644 --- a/bizmatch/src/app/components/footer/footer.component.ts +++ b/bizmatch/src/app/components/footer/footer.component.ts @@ -1,9 +1,9 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { NavigationEnd, Router, RouterModule } from '@angular/router'; +import { Router, RouterModule } from '@angular/router'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; -import { initFlowbite } from 'flowbite'; + @Component({ selector: 'app-footer', standalone: true, @@ -17,10 +17,6 @@ export class FooterComponent { currentYear: number = new Date().getFullYear(); constructor(private router: Router) {} ngOnInit() { - this.router.events.subscribe(event => { - if (event instanceof NavigationEnd) { - initFlowbite(); - } - }); + // Flowbite is now initialized once in AppComponent } } diff --git a/bizmatch/src/app/components/header/header.component.html b/bizmatch/src/app/components/header/header.component.html index 8b1b5c7..ae3c3ce 100644 --- a/bizmatch/src/app/components/header/header.component.html +++ b/bizmatch/src/app/components/header/header.component.html @@ -178,7 +178,7 @@ aria-current="page" (click)="closeMenusAndSetCriteria('businessListings')" > - Business + Business Businesses @@ -191,7 +191,7 @@ class="block py-2 px-3 rounded hover:bg-neutral-100 md:hover:bg-transparent md:hover:text-primary-600 md:p-0 dark:text-white md:dark:hover:text-primary-500 dark:hover:bg-neutral-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-neutral-700 inline-flex items-center" (click)="closeMenusAndSetCriteria('commercialPropertyListings')" > - Properties + Properties Properties diff --git a/bizmatch/src/app/components/header/header.component.ts b/bizmatch/src/app/components/header/header.component.ts index b81601a..6946ce2 100644 --- a/bizmatch/src/app/components/header/header.component.ts +++ b/bizmatch/src/app/components/header/header.component.ts @@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms'; import { NavigationEnd, Router, RouterModule } from '@angular/router'; import { faUserGear } from '@fortawesome/free-solid-svg-icons'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { Collapse, Dropdown, initFlowbite } from 'flowbite'; +import { Collapse, Dropdown } from 'flowbite'; import { filter, Observable, Subject, takeUntil } from 'rxjs'; import { SortByOptions, User } from '../../../../../bizmatch-server/src/models/db.model'; @@ -88,10 +88,7 @@ export class HeaderComponent implements OnInit, OnDestroy { this.numberOfBroker$ = this.userService.getNumberOfBroker(this.createEmptyUserListingCriteria()); this.numberOfCommercial$ = this.listingService.getNumberOfListings('commercialProperty'); - // Flowbite initialisieren - setTimeout(() => { - initFlowbite(); - }, 10); + // Flowbite is now initialized once in AppComponent // Profile Photo Updates this.sharedService.currentProfilePhoto.pipe(untilDestroyed(this)).subscribe(photoUrl => { diff --git a/bizmatch/src/app/components/tooltip/tooltip.component.ts b/bizmatch/src/app/components/tooltip/tooltip.component.ts index 38baf8e..052add8 100644 --- a/bizmatch/src/app/components/tooltip/tooltip.component.ts +++ b/bizmatch/src/app/components/tooltip/tooltip.component.ts @@ -1,6 +1,5 @@ import { CommonModule } from '@angular/common'; import { Component, Input, SimpleChanges } from '@angular/core'; -import { initFlowbite } from 'flowbite'; @Component({ selector: 'app-tooltip', @@ -24,9 +23,7 @@ export class TooltipComponent { } private initializeTooltip() { - setTimeout(() => { - initFlowbite(); - }, 10); + // Flowbite is now initialized once in AppComponent } private updateTooltipVisibility() { diff --git a/bizmatch/src/app/pages/details/base-details.component.ts b/bizmatch/src/app/pages/details/base-details.component.ts index a2fbab7..2f6cbab 100644 --- a/bizmatch/src/app/pages/details/base-details.component.ts +++ b/bizmatch/src/app/pages/details/base-details.component.ts @@ -29,7 +29,8 @@ export abstract class BaseDetailsComponent { const latitude = this.listing.location.latitude; const longitude = this.listing.location.longitude; - if (latitude && longitude) { + if (latitude !== null && latitude !== undefined && + longitude !== null && longitude !== undefined) { this.mapCenter = latLng(latitude, longitude); // Build address string from available location data diff --git a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts index 82f68d6..90bc47b 100644 --- a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts +++ b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts @@ -26,7 +26,7 @@ import { SharedModule } from '../../../shared/shared/shared.module'; import { createMailInfo, map2User } from '../../../utils/utils'; // Import für Leaflet // Benannte Importe für Leaflet -import { circle, Circle, Control, DomEvent, DomUtil, latLng, LatLngBounds, polygon, Polygon, tileLayer } from 'leaflet'; +import { circle, Circle, Control, DomEvent, DomUtil, icon, Icon, latLng, LatLngBounds, Marker, polygon, Polygon, tileLayer } from 'leaflet'; import dayjs from 'dayjs'; import { AuthService } from '../../../services/auth.service'; import { BaseDetailsComponent } from '../base-details.component'; @@ -328,12 +328,18 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent { const county = this.listing.location.county || ''; const state = this.listing.location.state; - if (latitude && longitude && cityName && state) { - this.mapCenter = latLng(latitude, longitude); - this.mapZoom = 11; // Zoom out to show city area + // Check if we have valid coordinates (null-safe check) + if (latitude !== null && latitude !== undefined && + longitude !== null && longitude !== undefined) { - // Fetch city boundary from Nominatim API - this.geoService.getCityBoundary(cityName, state).subscribe({ + this.mapCenter = latLng(latitude, longitude); + + // Case 1: City name available - show city boundary (current behavior) + if (cityName && state) { + this.mapZoom = 11; // Zoom to city level + + // Fetch city boundary from Nominatim API + this.geoService.getCityBoundary(cityName, state).subscribe({ next: (data) => { if (data && data.length > 0 && data[0].geojson && data[0].geojson.type === 'Polygon') { const coordinates = data[0].geojson.coordinates[0]; // Get outer boundary @@ -426,6 +432,18 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent { this.useFallbackCircle(latitude, longitude, cityName, county, state); } }); + } + // Case 2: Only state available (NEW) - show state-level circle + else if (state) { + this.mapZoom = 6; // Zoom to state level + // Use state-level fallback with larger radius + this.useStateLevelFallback(latitude, longitude, county, state); + } + // Case 3: No location name at all - minimal marker + else { + this.mapZoom = 8; + this.useMinimalMarker(latitude, longitude); + } } } @@ -463,6 +481,184 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent { }; } + /** + * Show state-level boundary polygon + * Used when only state is available without city + */ + private useStateLevelFallback(latitude: number, longitude: number, county: string, state: string) { + this.mapCenter = latLng(latitude, longitude); + + // Fetch state boundary from Nominatim API (similar to city boundary) + this.geoService.getStateBoundary(state).subscribe({ + next: (data) => { + if (data && data.length > 0 && data[0].geojson) { + + // Handle Polygon + if (data[0].geojson.type === 'Polygon') { + const coordinates = data[0].geojson.coordinates[0]; + const latlngs = coordinates.map((coord: number[]) => latLng(coord[1], coord[0])); + + const statePolygon = polygon(latlngs, { + color: '#ef4444', + fillColor: '#ef4444', + fillOpacity: 0.05, // Very transparent for large area + weight: 2 + }); + + statePolygon.bindPopup(` +
+ General Area:
+ ${county ? county + ', ' : ''}${state}
+ State boundary shown for privacy.
Exact location provided after contact.
+
+ `); + + this.mapLayers = [ + tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + }), + statePolygon + ]; + + // Fit map to state bounds + const bounds = statePolygon.getBounds(); + this.mapOptions = { + ...this.mapOptions, + center: bounds.getCenter(), + zoom: this.mapZoom, + }; + } + // Handle MultiPolygon (states with islands, etc.) + else if (data[0].geojson.type === 'MultiPolygon') { + const allPolygons: Polygon[] = []; + + data[0].geojson.coordinates.forEach((polygonCoords: number[][][]) => { + const latlngs = polygonCoords[0].map((coord: number[]) => latLng(coord[1], coord[0])); + const statePolygon = polygon(latlngs, { + color: '#ef4444', + fillColor: '#ef4444', + fillOpacity: 0.05, + weight: 2 + }); + allPolygons.push(statePolygon); + }); + + if (allPolygons.length > 0) { + allPolygons[0].bindPopup(` +
+ General Area:
+ ${county ? county + ', ' : ''}${state}
+ State boundary shown for privacy.
Exact location provided after contact.
+
+ `); + } + + this.mapLayers = [ + tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + }), + ...allPolygons + ]; + + // Calculate combined bounds + if (allPolygons.length > 0) { + const bounds = new LatLngBounds([]); + allPolygons.forEach(p => bounds.extend(p.getBounds())); + this.mapOptions = { + ...this.mapOptions, + center: bounds.getCenter(), + zoom: this.mapZoom, + }; + } + } else { + // Fallback if unexpected format + this.useCircleFallbackForState(latitude, longitude, county, state); + } + } else { + // Fallback if no data + this.useCircleFallbackForState(latitude, longitude, county, state); + } + }, + error: (err) => { + console.error('Error fetching state boundary:', err); + // Fallback to circle on error + this.useCircleFallbackForState(latitude, longitude, county, state); + } + }); + } + + /** + * Fallback: Show circle when state boundary cannot be fetched + */ + private useCircleFallbackForState(latitude: number, longitude: number, county: string, state: string) { + this.mapCenter = latLng(latitude, longitude); + + const stateCircle = circle([latitude, longitude], { + color: '#ef4444', + fillColor: '#ef4444', + fillOpacity: 0.05, + weight: 2, + radius: 50000 // 50km + }); + + stateCircle.bindPopup(` +
+ General Area:
+ ${county ? county + ', ' : ''}${state}
+ Approximate state-level location shown for privacy.
Exact location provided after contact.
+
+ `); + + this.mapLayers = [ + tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + }), + stateCircle + ]; + + this.mapOptions = { + ...this.mapOptions, + center: this.mapCenter, + zoom: this.mapZoom, + }; + } + + /** + * Show minimal marker when no location name is available + */ + private useMinimalMarker(latitude: number, longitude: number) { + this.mapCenter = latLng(latitude, longitude); + + const marker = new Marker([latitude, longitude], { + icon: icon({ + ...Icon.Default.prototype.options, + iconUrl: 'assets/leaflet/marker-icon.png', + iconRetinaUrl: 'assets/leaflet/marker-icon-2x.png', + shadowUrl: 'assets/leaflet/marker-shadow.png', + }), + }); + + marker.bindPopup(` +
+ Location
+ Contact seller for exact address +
+ `); + + this.mapLayers = [ + tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + }), + marker + ]; + + this.mapOptions = { + ...this.mapOptions, + center: this.mapCenter, + zoom: this.mapZoom, + }; + } + /** * Override onMapReady to show privacy-friendly address control */ diff --git a/bizmatch/src/app/pages/home/home.component.html b/bizmatch/src/app/pages/home/home.component.html index 96f0d73..a012df7 100644 --- a/bizmatch/src/app/pages/home/home.component.html +++ b/bizmatch/src/app/pages/home/home.component.html @@ -72,7 +72,7 @@ " class="tab-link hover:cursor-pointer inline-flex items-center justify-center px-1 py-2 md:p-4 border-b-2 rounded-t-lg" > - Search businesses for sale + Search businesses for sale Businesses @@ -87,7 +87,7 @@ " class="tab-link hover:cursor-pointer inline-flex items-center justify-center px-1 py-2 md:p-4 border-b-2 rounded-t-lg" > - Search commercial properties for sale + Search commercial properties for sale Properties diff --git a/bizmatch/src/app/pages/home/home.component.scss b/bizmatch/src/app/pages/home/home.component.scss index 921d661..77bfe2f 100644 --- a/bizmatch/src/app/pages/home/home.component.scss +++ b/bizmatch/src/app/pages/home/home.component.scss @@ -131,6 +131,7 @@ input[type='text'][name='aiSearchText'] { background: rgba(255, 255, 255, 0.3); transform: translate(-50%, -50%); transition: width 0.6s, height 0.6s; + pointer-events: none; } &:active::after { diff --git a/bizmatch/src/app/pages/home/home.component.ts b/bizmatch/src/app/pages/home/home.component.ts index fb293ae..6561bb6 100644 --- a/bizmatch/src/app/pages/home/home.component.ts +++ b/bizmatch/src/app/pages/home/home.component.ts @@ -4,7 +4,6 @@ import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { NgSelectModule } from '@ng-select/ng-select'; import { UntilDestroy } from '@ngneat/until-destroy'; -import { initFlowbite } from 'flowbite'; import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs'; import { BusinessListingCriteria, CityAndStateResult, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; import { FaqComponent, FAQItem } from '../../components/faq/faq.component'; @@ -127,9 +126,7 @@ export class HomeComponent { ) {} async ngOnInit() { - setTimeout(() => { - initFlowbite(); - }, 0); + // Flowbite is now initialized once in AppComponent // Set SEO meta tags for home page this.seoService.updateMetaTags({ diff --git a/bizmatch/src/app/pages/subscription/account/account.component.ts b/bizmatch/src/app/pages/subscription/account/account.component.ts index cf353bb..b877798 100644 --- a/bizmatch/src/app/pages/subscription/account/account.component.ts +++ b/bizmatch/src/app/pages/subscription/account/account.component.ts @@ -3,8 +3,6 @@ import { ChangeDetectorRef, Component } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { NgSelectModule } from '@ng-select/ng-select'; -import { initFlowbite } from 'flowbite'; - import { NgxCurrencyDirective } from 'ngx-currency'; import { ImageCropperComponent } from 'ngx-image-cropper'; import { QuillModule } from 'ngx-quill'; @@ -99,9 +97,7 @@ export class AccountComponent { public authService: AuthService, ) {} async ngOnInit() { - setTimeout(() => { - initFlowbite(); - }, 10); + // Flowbite is now initialized once in AppComponent if (this.id) { this.user = await this.userService.getById(this.id); } else { diff --git a/bizmatch/src/app/services/geo.service.ts b/bizmatch/src/app/services/geo.service.ts index a4a9bad..9afcf98 100644 --- a/bizmatch/src/app/services/geo.service.ts +++ b/bizmatch/src/app/services/geo.service.ts @@ -1,10 +1,16 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { lastValueFrom, Observable } from 'rxjs'; +import { lastValueFrom, Observable, of } from 'rxjs'; +import { tap } from 'rxjs/operators'; import { CityAndStateResult, CountyResult, GeoResult, IpInfo } from '../../../../bizmatch-server/src/models/main.model'; import { Place } from '../../../../bizmatch-server/src/models/server.model'; import { environment } from '../../environments/environment'; +interface CachedBoundary { + data: any; + timestamp: number; +} + @Injectable({ providedIn: 'root', }) @@ -13,8 +19,68 @@ export class GeoService { private baseUrl: string = 'https://nominatim.openstreetmap.org/search'; private fetchingData: Observable | null = null; private readonly storageKey = 'ipInfo'; + private readonly boundaryStoragePrefix = 'nominatim_boundary_'; + private readonly cacheExpiration = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds + constructor(private http: HttpClient) {} + /** + * Get cached boundary data from localStorage + */ + private getCachedBoundary(cacheKey: string): any | null { + try { + const cached = localStorage.getItem(this.boundaryStoragePrefix + cacheKey); + if (!cached) { + return null; + } + + const cachedData: CachedBoundary = JSON.parse(cached); + const now = Date.now(); + + // Check if cache has expired + if (now - cachedData.timestamp > this.cacheExpiration) { + localStorage.removeItem(this.boundaryStoragePrefix + cacheKey); + return null; + } + + return cachedData.data; + } catch (error) { + console.error('Error reading boundary cache:', error); + return null; + } + } + + /** + * Save boundary data to localStorage + */ + private setCachedBoundary(cacheKey: string, data: any): void { + try { + const cachedData: CachedBoundary = { + data: data, + timestamp: Date.now() + }; + localStorage.setItem(this.boundaryStoragePrefix + cacheKey, JSON.stringify(cachedData)); + } catch (error) { + console.error('Error saving boundary cache:', error); + } + } + + /** + * Clear all cached boundary data + */ + clearBoundaryCache(): void { + try { + const keys = Object.keys(localStorage); + keys.forEach(key => { + if (key.startsWith(this.boundaryStoragePrefix)) { + localStorage.removeItem(key); + } + }); + } catch (error) { + console.error('Error clearing boundary cache:', error); + } + } + findCitiesStartingWith(prefix: string, state?: string): Observable { const stateString = state ? `/${state}` : ''; return this.http.get(`${this.apiBaseUrl}/bizmatch/geo/${prefix}${stateString}`); @@ -31,10 +97,39 @@ export class GeoService { } getCityBoundary(cityName: string, state: string): Observable { + const cacheKey = `city_${cityName}_${state}`.toLowerCase().replace(/\s+/g, '_'); + + // Check cache first + const cached = this.getCachedBoundary(cacheKey); + if (cached) { + return of(cached); + } + + // If not in cache, fetch from API const query = `${cityName}, ${state}, USA`; let headers = new HttpHeaders().set('X-Hide-Loading', 'true').set('Accept-Language', 'en-US'); - return this.http.get(`${this.baseUrl}?q=${encodeURIComponent(query)}&format=json&polygon_geojson=1&limit=1`, { headers }); + return this.http.get(`${this.baseUrl}?q=${encodeURIComponent(query)}&format=json&polygon_geojson=1&limit=1`, { headers }).pipe( + tap(data => this.setCachedBoundary(cacheKey, data)) + ); } + + getStateBoundary(state: string): Observable { + const cacheKey = `state_${state}`.toLowerCase().replace(/\s+/g, '_'); + + // Check cache first + const cached = this.getCachedBoundary(cacheKey); + if (cached) { + return of(cached); + } + + // If not in cache, fetch from API + const query = `${state}, USA`; + let headers = new HttpHeaders().set('X-Hide-Loading', 'true').set('Accept-Language', 'en-US'); + return this.http.get(`${this.baseUrl}?q=${encodeURIComponent(query)}&format=json&polygon_geojson=1&limit=1&featuretype=state`, { headers }).pipe( + tap(data => this.setCachedBoundary(cacheKey, data)) + ); + } + private fetchIpAndGeoLocation(): Observable { return this.http.get(`${this.apiBaseUrl}/bizmatch/geo/ipinfo/georesult/wysiwyg`); } diff --git a/bizmatch/src/build.ts b/bizmatch/src/build.ts index 735195a..06a51f9 100644 --- a/bizmatch/src/build.ts +++ b/bizmatch/src/build.ts @@ -1,6 +1,6 @@ // Build information, automatically generated by `the_build_script` :zwinkern: const build = { - timestamp: "GER: 26.11.2025 10:28 | TX: 11/26/2025 3:28 AM" + timestamp: "GER: 01.12.2025 20:23 | TX: 12/01/2025 1:23 PM" }; export default build; \ No newline at end of file diff --git a/bizmatch/src/index.html b/bizmatch/src/index.html index 98abb29..25b4cf6 100644 --- a/bizmatch/src/index.html +++ b/bizmatch/src/index.html @@ -8,6 +8,7 @@ + @@ -33,7 +34,6 @@ -