Fehler behebung
This commit is contained in:
parent
d2953fd0d9
commit
30ecc292cd
|
|
@ -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
|
||||
|
|
@ -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<BusinessListing> {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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<CommercialPropertyListing> {
|
||||
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<CommercialPropertyListing> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@
|
|||
aria-current="page"
|
||||
(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>
|
||||
</a>
|
||||
</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"
|
||||
(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>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(`
|
||||
<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: '© 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: '© 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: '© 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: '© OpenStreetMap contributors',
|
||||
}),
|
||||
marker
|
||||
];
|
||||
|
||||
this.mapOptions = {
|
||||
...this.mapOptions,
|
||||
center: this.mapCenter,
|
||||
zoom: this.mapZoom,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Override onMapReady to show privacy-friendly address control
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<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>
|
||||
</a>
|
||||
</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"
|
||||
>
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<IpInfo> | 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<GeoResult[]> {
|
||||
const stateString = state ? `/${state}` : '';
|
||||
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> {
|
||||
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<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> {
|
||||
return this.http.get<IpInfo>(`${this.apiBaseUrl}/bizmatch/geo/ipinfo/georesult/wysiwyg`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
<!-- Mobile App & Theme Meta Tags -->
|
||||
<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-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="BizMatch" />
|
||||
|
|
@ -33,7 +34,6 @@
|
|||
|
||||
<!-- 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/index-bg.webp" type="image/webp" />
|
||||
|
||||
<!-- Prefetch common assets -->
|
||||
<link rel="prefetch" as="image" href="assets/images/business_logo.png" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue