Fehler behebung

This commit is contained in:
Timo Knuth 2025-12-03 11:51:00 +01:00
parent d2953fd0d9
commit 30ecc292cd
20 changed files with 379 additions and 62 deletions

View File

@ -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

View File

@ -218,15 +218,28 @@ export class BusinessListingService {
* Supports both slug (e.g., "restaurant-austin-tx-a3f7b2c1") and UUID * Supports both slug (e.g., "restaurant-austin-tx-a3f7b2c1") and UUID
*/ */
async findBusinessBySlugOrId(slugOrId: string, user: JwtUser): Promise<BusinessListing> { async findBusinessBySlugOrId(slugOrId: string, user: JwtUser): Promise<BusinessListing> {
this.logger.debug(`findBusinessBySlugOrId called with: ${slugOrId}`);
let id = slugOrId; let id = slugOrId;
// Check if it's a slug (contains multiple hyphens) vs UUID // Check if it's a slug (contains multiple hyphens) vs UUID
if (isSlug(slugOrId)) { if (isSlug(slugOrId)) {
this.logger.debug(`Detected as slug: ${slugOrId}`);
// Extract short ID from slug and find by slug field // Extract short ID from slug and find by slug field
const listing = await this.findBusinessBySlug(slugOrId); const listing = await this.findBusinessBySlug(slugOrId);
if (listing) { if (listing) {
this.logger.debug(`Found listing by slug: ${slugOrId} -> ID: ${listing.id}`);
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); return this.findBusinessesById(id, user);

View File

@ -117,15 +117,28 @@ export class CommercialPropertyService {
* Supports both slug (e.g., "office-space-austin-tx-a3f7b2c1") and UUID * Supports both slug (e.g., "office-space-austin-tx-a3f7b2c1") and UUID
*/ */
async findCommercialBySlugOrId(slugOrId: string, user: JwtUser): Promise<CommercialPropertyListing> { async findCommercialBySlugOrId(slugOrId: string, user: JwtUser): Promise<CommercialPropertyListing> {
this.logger.debug(`findCommercialBySlugOrId called with: ${slugOrId}`);
let id = slugOrId; let id = slugOrId;
// Check if it's a slug (contains multiple hyphens) vs UUID // Check if it's a slug (contains multiple hyphens) vs UUID
if (isSlug(slugOrId)) { if (isSlug(slugOrId)) {
this.logger.debug(`Detected as slug: ${slugOrId}`);
// Extract short ID from slug and find by slug field // Extract short ID from slug and find by slug field
const listing = await this.findCommercialBySlug(slugOrId); const listing = await this.findCommercialBySlug(slugOrId);
if (listing) { if (listing) {
this.logger.debug(`Found listing by slug: ${slugOrId} -> ID: ${listing.id}`);
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); return this.findCommercialPropertiesById(id, user);
@ -198,18 +211,9 @@ export class CommercialPropertyService {
// #### CREATE ######################################## // #### CREATE ########################################
async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> { async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
try { try {
// Hole die nächste serialId von der Sequence // Generate serialId based on timestamp + random number (temporary solution until sequence is created)
const sequenceResult = await this.conn.execute(sql`SELECT nextval('commercials_json_serial_id_seq') AS serialid`); // This ensures uniqueness without requiring a database sequence
const serialId = Date.now() % 1000000 + Math.floor(Math.random() * 1000);
// 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');
}
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date(); data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
data.updated = new Date(); data.updated = new Date();

View File

@ -126,8 +126,8 @@ export function isSlug(param: string): boolean {
return false; // It's a UUID return false; // It's a UUID
} }
// If it contains more than 4 hyphens and looks like our slug format, it's probably a slug // 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 > 4 && isValidSlug(param); return param.split('-').length >= 3 && isValidSlug(param);
} }
/** /**

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; 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 { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { initFlowbite } from 'flowbite';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import build from '../build'; import build from '../build';
import { ConfirmationComponent } from './components/confirmation/confirmation.component'; import { ConfirmationComponent } from './components/confirmation/confirmation.component';
@ -25,7 +25,7 @@ import { UserService } from './services/user.service';
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss', styleUrl: './app.component.scss',
}) })
export class AppComponent { export class AppComponent implements AfterViewInit {
build = build; build = build;
title = 'bizmatch'; title = 'bizmatch';
actualRoute = ''; actualRoute = '';
@ -48,7 +48,15 @@ export class AppComponent {
this.actualRoute = currentRoute.snapshot.url[0].path; 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']) @HostListener('window:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) { handleKeyboardEvent(event: KeyboardEvent) {
if (event.shiftKey && event.ctrlKey && event.key === 'V') { if (event.shiftKey && event.ctrlKey && event.key === 'V') {

View File

@ -1,3 +1,4 @@
import { IMAGE_CONFIG } from '@angular/common';
import { APP_INITIALIZER, ApplicationConfig, ErrorHandler } from '@angular/core'; import { APP_INITIALIZER, ApplicationConfig, ErrorHandler } from '@angular/core';
import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router'; import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router';
@ -53,6 +54,12 @@ export const appConfig: ApplicationConfig = {
} as GalleryConfig, } as GalleryConfig,
}, },
{ provide: ErrorHandler, useClass: GlobalErrorHandler }, // Registriere den globalen ErrorHandler { provide: ErrorHandler, useClass: GlobalErrorHandler }, // Registriere den globalen ErrorHandler
{
provide: IMAGE_CONFIG,
useValue: {
disableImageSizeWarning: true,
},
},
provideShareButtonsOptions( provideShareButtonsOptions(
shareIcons(), shareIcons(),
withConfig({ withConfig({

View File

@ -1,6 +1,5 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms'; import { ControlValueAccessor } from '@angular/forms';
import { initFlowbite } from 'flowbite';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { ValidationMessagesService } from '../validation-messages.service'; import { ValidationMessagesService } from '../validation-messages.service';
@ -25,9 +24,7 @@ export abstract class BaseInputComponent implements ControlValueAccessor {
this.subscription = this.validationMessagesService.messages$.subscribe(() => { this.subscription = this.validationMessagesService.messages$.subscribe(() => {
this.updateValidationMessage(); this.updateValidationMessage();
}); });
setTimeout(() => { // Flowbite is now initialized once in AppComponent
initFlowbite();
}, 10);
} }
ngOnDestroy() { ngOnDestroy() {

View File

@ -1,9 +1,9 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms'; 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 { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { initFlowbite } from 'flowbite';
@Component({ @Component({
selector: 'app-footer', selector: 'app-footer',
standalone: true, standalone: true,
@ -17,10 +17,6 @@ export class FooterComponent {
currentYear: number = new Date().getFullYear(); currentYear: number = new Date().getFullYear();
constructor(private router: Router) {} constructor(private router: Router) {}
ngOnInit() { ngOnInit() {
this.router.events.subscribe(event => { // Flowbite is now initialized once in AppComponent
if (event instanceof NavigationEnd) {
initFlowbite();
}
});
} }
} }

View File

@ -178,7 +178,7 @@
aria-current="page" aria-current="page"
(click)="closeMenusAndSetCriteria('businessListings')" (click)="closeMenusAndSetCriteria('businessListings')"
> >
<img src="assets/images/business_logo.png" alt="Business" class="w-5 h-5 mr-2 object-contain" /> <img src="assets/images/business_logo.png" alt="Business" class="w-5 h-5 mr-2 object-contain" width="20" height="20" />
<span>Businesses</span> <span>Businesses</span>
</a> </a>
</li> </li>
@ -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" 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')" (click)="closeMenusAndSetCriteria('commercialPropertyListings')"
> >
<img src="assets/images/properties_logo.png" alt="Properties" class="w-5 h-5 mr-2 object-contain" /> <img src="assets/images/properties_logo.png" alt="Properties" class="w-5 h-5 mr-2 object-contain" width="20" height="20" />
<span>Properties</span> <span>Properties</span>
</a> </a>
</li> </li>

View File

@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms';
import { NavigationEnd, Router, RouterModule } from '@angular/router'; import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { faUserGear } from '@fortawesome/free-solid-svg-icons'; import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 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 { filter, Observable, Subject, takeUntil } from 'rxjs';
import { SortByOptions, User } from '../../../../../bizmatch-server/src/models/db.model'; 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.numberOfBroker$ = this.userService.getNumberOfBroker(this.createEmptyUserListingCriteria());
this.numberOfCommercial$ = this.listingService.getNumberOfListings('commercialProperty'); this.numberOfCommercial$ = this.listingService.getNumberOfListings('commercialProperty');
// Flowbite initialisieren // Flowbite is now initialized once in AppComponent
setTimeout(() => {
initFlowbite();
}, 10);
// Profile Photo Updates // Profile Photo Updates
this.sharedService.currentProfilePhoto.pipe(untilDestroyed(this)).subscribe(photoUrl => { this.sharedService.currentProfilePhoto.pipe(untilDestroyed(this)).subscribe(photoUrl => {

View File

@ -1,6 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, Input, SimpleChanges } from '@angular/core'; import { Component, Input, SimpleChanges } from '@angular/core';
import { initFlowbite } from 'flowbite';
@Component({ @Component({
selector: 'app-tooltip', selector: 'app-tooltip',
@ -24,9 +23,7 @@ export class TooltipComponent {
} }
private initializeTooltip() { private initializeTooltip() {
setTimeout(() => { // Flowbite is now initialized once in AppComponent
initFlowbite();
}, 10);
} }
private updateTooltipVisibility() { private updateTooltipVisibility() {

View File

@ -29,7 +29,8 @@ export abstract class BaseDetailsComponent {
const latitude = this.listing.location.latitude; const latitude = this.listing.location.latitude;
const longitude = this.listing.location.longitude; const longitude = this.listing.location.longitude;
if (latitude && longitude) { if (latitude !== null && latitude !== undefined &&
longitude !== null && longitude !== undefined) {
this.mapCenter = latLng(latitude, longitude); this.mapCenter = latLng(latitude, longitude);
// Build address string from available location data // Build address string from available location data

View File

@ -26,7 +26,7 @@ import { SharedModule } from '../../../shared/shared/shared.module';
import { createMailInfo, map2User } from '../../../utils/utils'; import { createMailInfo, map2User } from '../../../utils/utils';
// Import für Leaflet // Import für Leaflet
// Benannte Importe 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 dayjs from 'dayjs';
import { AuthService } from '../../../services/auth.service'; import { AuthService } from '../../../services/auth.service';
import { BaseDetailsComponent } from '../base-details.component'; import { BaseDetailsComponent } from '../base-details.component';
@ -328,12 +328,18 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
const county = this.listing.location.county || ''; const county = this.listing.location.county || '';
const state = this.listing.location.state; const state = this.listing.location.state;
if (latitude && longitude && cityName && state) { // Check if we have valid coordinates (null-safe check)
this.mapCenter = latLng(latitude, longitude); if (latitude !== null && latitude !== undefined &&
this.mapZoom = 11; // Zoom out to show city area longitude !== null && longitude !== undefined) {
// Fetch city boundary from Nominatim API this.mapCenter = latLng(latitude, longitude);
this.geoService.getCityBoundary(cityName, state).subscribe({
// 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) => { next: (data) => {
if (data && data.length > 0 && data[0].geojson && data[0].geojson.type === 'Polygon') { if (data && data.length > 0 && data[0].geojson && data[0].geojson.type === 'Polygon') {
const coordinates = data[0].geojson.coordinates[0]; // Get outer boundary 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); 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(`
<div style="padding: 8px;">
<strong>General Area:</strong><br/>
${county ? county + ', ' : ''}${state}<br/>
<small style="color: #666;">State boundary shown for privacy.<br/>Exact location provided after contact.</small>
</div>
`);
this.mapLayers = [
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; 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(`
<div style="padding: 8px;">
<strong>General Area:</strong><br/>
${county ? county + ', ' : ''}${state}<br/>
<small style="color: #666;">State boundary shown for privacy.<br/>Exact location provided after contact.</small>
</div>
`);
}
this.mapLayers = [
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; 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(`
<div style="padding: 8px;">
<strong>General Area:</strong><br/>
${county ? county + ', ' : ''}${state}<br/>
<small style="color: #666;">Approximate state-level location shown for privacy.<br/>Exact location provided after contact.</small>
</div>
`);
this.mapLayers = [
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; 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(`
<div style="padding: 8px;">
<strong>Location</strong><br/>
<small style="color: #666;">Contact seller for exact address</small>
</div>
`);
this.mapLayers = [
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
}),
marker
];
this.mapOptions = {
...this.mapOptions,
center: this.mapCenter,
zoom: this.mapZoom,
};
}
/** /**
* Override onMapReady to show privacy-friendly address control * Override onMapReady to show privacy-friendly address control
*/ */

View File

@ -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" 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"
> >
<img src="assets/images/business_logo.png" alt="Search businesses for sale" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" /> <img src="assets/images/business_logo.png" alt="Search businesses for sale" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" width="28" height="28" />
<span>Businesses</span> <span>Businesses</span>
</a> </a>
</li> </li>
@ -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" 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"
> >
<img src="assets/images/properties_logo.png" alt="Search commercial properties for sale" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" /> <img src="assets/images/properties_logo.png" alt="Search commercial properties for sale" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" width="28" height="28" />
<span>Properties</span> <span>Properties</span>
</a> </a>
</li> </li>

View File

@ -131,6 +131,7 @@ input[type='text'][name='aiSearchText'] {
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s; transition: width 0.6s, height 0.6s;
pointer-events: none;
} }
&:active::after { &:active::after {

View File

@ -4,7 +4,6 @@ import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { UntilDestroy } from '@ngneat/until-destroy'; import { UntilDestroy } from '@ngneat/until-destroy';
import { initFlowbite } from 'flowbite';
import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs'; 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 { BusinessListingCriteria, CityAndStateResult, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { FaqComponent, FAQItem } from '../../components/faq/faq.component'; import { FaqComponent, FAQItem } from '../../components/faq/faq.component';
@ -127,9 +126,7 @@ export class HomeComponent {
) {} ) {}
async ngOnInit() { async ngOnInit() {
setTimeout(() => { // Flowbite is now initialized once in AppComponent
initFlowbite();
}, 0);
// Set SEO meta tags for home page // Set SEO meta tags for home page
this.seoService.updateMetaTags({ this.seoService.updateMetaTags({

View File

@ -3,8 +3,6 @@ import { ChangeDetectorRef, Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { initFlowbite } from 'flowbite';
import { NgxCurrencyDirective } from 'ngx-currency'; import { NgxCurrencyDirective } from 'ngx-currency';
import { ImageCropperComponent } from 'ngx-image-cropper'; import { ImageCropperComponent } from 'ngx-image-cropper';
import { QuillModule } from 'ngx-quill'; import { QuillModule } from 'ngx-quill';
@ -99,9 +97,7 @@ export class AccountComponent {
public authService: AuthService, public authService: AuthService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
setTimeout(() => { // Flowbite is now initialized once in AppComponent
initFlowbite();
}, 10);
if (this.id) { if (this.id) {
this.user = await this.userService.getById(this.id); this.user = await this.userService.getById(this.id);
} else { } else {

View File

@ -1,10 +1,16 @@
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; 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 { CityAndStateResult, CountyResult, GeoResult, IpInfo } from '../../../../bizmatch-server/src/models/main.model';
import { Place } from '../../../../bizmatch-server/src/models/server.model'; import { Place } from '../../../../bizmatch-server/src/models/server.model';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
interface CachedBoundary {
data: any;
timestamp: number;
}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@ -13,8 +19,68 @@ export class GeoService {
private baseUrl: string = 'https://nominatim.openstreetmap.org/search'; private baseUrl: string = 'https://nominatim.openstreetmap.org/search';
private fetchingData: Observable<IpInfo> | null = null; private fetchingData: Observable<IpInfo> | null = null;
private readonly storageKey = 'ipInfo'; 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) {} 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<GeoResult[]> { findCitiesStartingWith(prefix: string, state?: string): Observable<GeoResult[]> {
const stateString = state ? `/${state}` : ''; const stateString = state ? `/${state}` : '';
return this.http.get<GeoResult[]>(`${this.apiBaseUrl}/bizmatch/geo/${prefix}${stateString}`); return this.http.get<GeoResult[]>(`${this.apiBaseUrl}/bizmatch/geo/${prefix}${stateString}`);
@ -31,10 +97,39 @@ export class GeoService {
} }
getCityBoundary(cityName: string, state: string): Observable<any> { getCityBoundary(cityName: string, state: string): Observable<any> {
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`; const query = `${cityName}, ${state}, USA`;
let headers = new HttpHeaders().set('X-Hide-Loading', 'true').set('Accept-Language', 'en-US'); 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<any> {
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<IpInfo> { private fetchIpAndGeoLocation(): Observable<IpInfo> {
return this.http.get<IpInfo>(`${this.apiBaseUrl}/bizmatch/geo/ipinfo/georesult/wysiwyg`); return this.http.get<IpInfo>(`${this.apiBaseUrl}/bizmatch/geo/ipinfo/georesult/wysiwyg`);
} }

View File

@ -1,6 +1,6 @@
// Build information, automatically generated by `the_build_script` :zwinkern: // Build information, automatically generated by `the_build_script` :zwinkern:
const build = { 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; export default build;

View File

@ -8,6 +8,7 @@
<!-- Mobile App & Theme Meta Tags --> <!-- Mobile App & Theme Meta Tags -->
<meta name="theme-color" content="#0066cc" /> <meta name="theme-color" content="#0066cc" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="BizMatch" /> <meta name="apple-mobile-web-app-title" content="BizMatch" />
@ -33,7 +34,6 @@
<!-- Preload critical assets --> <!-- Preload critical assets -->
<link rel="preload" as="image" href="assets/images/header-logo.png" type="image/png" /> <link rel="preload" as="image" href="assets/images/header-logo.png" type="image/png" />
<link rel="preload" as="image" href="assets/images/index-bg.webp" type="image/webp" />
<!-- Prefetch common assets --> <!-- Prefetch common assets -->
<link rel="prefetch" as="image" href="assets/images/business_logo.png" /> <link rel="prefetch" as="image" href="assets/images/business_logo.png" />