import { ChangeDetectorRef, Component } from '@angular/core'; import { NgOptimizedImage } from '@angular/common'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { LeafletModule } from '@bluehalo/ngx-leaflet'; import { lastValueFrom } from 'rxjs'; import { BusinessListing, EventTypeEnum, ShareByEMail, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model'; import { environment } from '../../../../environments/environment'; import { EMailService } from '../../../components/email/email.service'; import { MessageService } from '../../../components/message/message.service'; import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component'; import { ValidatedNgSelectComponent } from '../../../components/validated-ng-select/validated-ng-select.component'; import { ValidatedTextareaComponent } from '../../../components/validated-textarea/validated-textarea.component'; import { ValidationMessagesService } from '../../../components/validation-messages.service'; import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component'; import { AuditService } from '../../../services/audit.service'; import { GeoService } from '../../../services/geo.service'; import { HistoryService } from '../../../services/history.service'; import { ListingsService } from '../../../services/listings.service'; import { MailService } from '../../../services/mail.service'; import { SelectOptionsService } from '../../../services/select-options.service'; import { SeoService } from '../../../services/seo.service'; import { UserService } from '../../../services/user.service'; import { SharedModule } from '../../../shared/shared/shared.module'; import { createMailInfo, map2User } from '../../../utils/utils'; // Import für Leaflet // Note: Leaflet requires browser environment - protected by isBrowser checks in base class 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'; import { ShareButton } from 'ngx-sharebuttons/button'; @Component({ selector: 'app-details-business-listing', standalone: true, imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, LeafletModule, BreadcrumbsComponent, ShareButton, NgOptimizedImage], providers: [], templateUrl: './details-business-listing.component.html', styleUrl: '../details.scss', }) export class DetailsBusinessListingComponent extends BaseDetailsComponent { // listings: Array; responsiveOptions = [ { breakpoint: '1199px', numVisible: 1, numScroll: 1, }, { breakpoint: '991px', numVisible: 2, numScroll: 1, }, { breakpoint: '767px', numVisible: 1, numScroll: 1, }, ]; private id: string | undefined = this.activatedRoute.snapshot.params['slug'] as string | undefined; override listing: BusinessListing; mailinfo: MailInfo; environment = environment; keycloakUser: KeycloakUser; user: User; listingUser: User; description: SafeHtml; private history: string[] = []; ts = new Date().getTime(); env = environment; breadcrumbs: BreadcrumbItem[] = []; relatedListings: BusinessListing[] = []; businessFAQs: Array<{ question: string; answer: string }> = []; constructor( private activatedRoute: ActivatedRoute, private listingsService: ListingsService, private router: Router, private userService: UserService, public selectOptions: SelectOptionsService, private mailService: MailService, private sanitizer: DomSanitizer, public historyService: HistoryService, private validationMessagesService: ValidationMessagesService, private messageService: MessageService, private auditService: AuditService, public emailService: EMailService, private geoService: GeoService, public authService: AuthService, private cdref: ChangeDetectorRef, private seoService: SeoService, ) { super(); this.router.events.subscribe(event => { if (event instanceof NavigationEnd) { this.history.push(event.urlAfterRedirects); } }); this.mailinfo = { sender: { name: '', email: '', phoneNumber: '', state: '', comments: '' }, email: '', url: environment.mailinfoUrl }; // Initialisiere die Map-Optionen } async ngOnInit() { // Initialize default breadcrumbs first this.breadcrumbs = [ { label: 'Home', url: '/home', icon: 'fas fa-home' }, { label: 'Business Listings', url: '/businessListings' } ]; const token = await this.authService.getToken(); this.keycloakUser = map2User(token); if (this.keycloakUser) { this.user = await this.userService.getByMail(this.keycloakUser.email); this.mailinfo = createMailInfo(this.user); } try { this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business')); this.auditService.createEvent(this.listing.id, 'view', this.user?.email); this.listingUser = await this.userService.getByMail(this.listing.email); this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description); if (this.listing.location.latitude && this.listing.location.longitude) { this.configureMap(); } // Update SEO meta tags for this business listing const seoData = { businessName: this.listing.title, description: this.listing.description?.replace(/<[^>]*>/g, '').substring(0, 200) || '', askingPrice: this.listing.price, city: this.listing.location.name || this.listing.location.county || '', state: this.listing.location.state, industry: this.selectOptions.getBusiness(this.listing.type), images: this.listing.imageName ? [this.listing.imageName] : [], id: this.listing.id }; this.seoService.updateBusinessListingMeta(seoData); // Inject structured data (Schema.org JSON-LD) - Using Product schema for better SEO const productSchema = this.seoService.generateProductSchema({ businessName: this.listing.title, description: this.listing.description?.replace(/<[^>]*>/g, '') || '', images: this.listing.imageName ? [this.listing.imageName] : [], address: this.listing.location.street, city: this.listing.location.name, state: this.listing.location.state, zip: this.listing.location.zipCode, askingPrice: this.listing.price, annualRevenue: this.listing.salesRevenue, yearEstablished: this.listing.established, category: this.selectOptions.getBusiness(this.listing.type), id: this.listing.id, slug: this.listing.slug }); const breadcrumbSchema = this.seoService.generateBreadcrumbSchema([ { name: 'Home', url: '/' }, { name: 'Business Listings', url: '/businessListings' }, { name: this.selectOptions.getBusiness(this.listing.type), url: `/business/${this.listing.slug || this.listing.id}` } ]); // Generate FAQ for AEO (Answer Engine Optimization) this.businessFAQs = this.generateBusinessFAQ(); const faqSchema = this.seoService.generateFAQPageSchema(this.businessFAQs); // Inject all schemas including FAQ this.seoService.injectMultipleSchemas([productSchema, breadcrumbSchema, faqSchema]); // Generate breadcrumbs this.breadcrumbs = [ { label: 'Home', url: '/home', icon: 'fas fa-home' }, { label: 'Business Listings', url: '/businessListings' }, { label: this.selectOptions.getBusiness(this.listing.type), url: '/businessListings' }, { label: this.listing.title } ]; // Load related listings for internal linking (SEO improvement) this.loadRelatedListings(); } catch (error) { // Set default breadcrumbs even on error this.breadcrumbs = [ { label: 'Home', url: '/home', icon: 'fas fa-home' }, { label: 'Business Listings', url: '/businessListings' } ]; const errorMessage = error?.error?.message || error?.message || 'An error occurred while loading the listing'; this.auditService.log({ severity: 'error', text: errorMessage }); this.router.navigate(['notfound']); } } /** * Load related business listings based on same category, location, and price range * Improves SEO through internal linking */ private async loadRelatedListings() { try { this.relatedListings = (await this.listingsService.getRelatedListings(this.listing, 'business', 3)) as BusinessListing[]; } catch (error) { console.error('Error loading related listings:', error); this.relatedListings = []; } } /** * Generate dynamic FAQ based on business listing data fields * Provides AEO (Answer Engine Optimization) content */ private generateBusinessFAQ(): Array<{ question: string; answer: string }> { const faqs: Array<{ question: string; answer: string }> = []; // FAQ 1: When was this business established? if (this.listing.established) { faqs.push({ question: 'When was this business established?', answer: `This business was established ${this.listing.established} years ago${this.listing.established >= 10 ? ', demonstrating a proven track record and market stability' : ''}.` }); } // FAQ 2: What is the asking price? if (this.listing.price) { faqs.push({ question: 'What is the asking price for this business?', answer: `The asking price for this business is $${this.listing.price.toLocaleString()}.${this.listing.salesRevenue ? ` With an annual revenue of $${this.listing.salesRevenue.toLocaleString()}, this represents a competitive valuation.` : ''}` }); } else { faqs.push({ question: 'What is the asking price for this business?', answer: 'The asking price is available upon request. Please contact the seller for detailed pricing information.' }); } // FAQ 3: What is included in the sale? const includedItems: string[] = []; if (this.listing.realEstateIncluded) includedItems.push('real estate property'); if (this.listing.ffe) includedItems.push(`furniture, fixtures, and equipment valued at $${this.listing.ffe.toLocaleString()}`); if (this.listing.inventory) includedItems.push(`inventory worth $${this.listing.inventory.toLocaleString()}`); if (includedItems.length > 0) { faqs.push({ question: 'What is included in the sale?', answer: `The sale includes: ${includedItems.join(', ')}.${this.listing.leasedLocation ? ' The business operates from a leased location.' : ''}${this.listing.franchiseResale ? ' This is a franchise resale opportunity.' : ''}` }); } // FAQ 4: How many employees does the business have? if (this.listing.employees) { faqs.push({ question: 'How many employees does this business have?', answer: `The business currently employs ${this.listing.employees} ${this.listing.employees === 1 ? 'person' : 'people'}.${this.listing.supportAndTraining ? ' The seller offers support and training to ensure smooth transition.' : ''}` }); } // FAQ 5: What is the annual revenue and cash flow? if (this.listing.salesRevenue || this.listing.cashFlow) { let answer = ''; if (this.listing.salesRevenue) { answer += `The business generates an annual revenue of $${this.listing.salesRevenue.toLocaleString()}.`; } if (this.listing.cashFlow) { answer += ` The annual cash flow is $${this.listing.cashFlow.toLocaleString()}.`; } faqs.push({ question: 'What is the financial performance of this business?', answer: answer.trim() }); } // FAQ 6: Why is the business for sale? if (this.listing.reasonForSale) { faqs.push({ question: 'Why is this business for sale?', answer: this.listing.reasonForSale }); } // FAQ 7: Where is the business located? faqs.push({ question: 'Where is this business located?', answer: `This ${this.selectOptions.getBusiness(this.listing.type)} business is located in ${this.listing.location.name || this.listing.location.county}, ${this.selectOptions.getState(this.listing.location.state)}.` }); // FAQ 8: Is broker licensing required? if (this.listing.brokerLicencing) { faqs.push({ question: 'Is a broker license required for this business?', answer: this.listing.brokerLicencing }); } // FAQ 9: What type of business is this? faqs.push({ question: 'What type of business is this?', answer: `This is a ${this.selectOptions.getBusiness(this.listing.type)} business${this.listing.established ? ` that has been operating for ${this.listing.established} years` : ''}.` }); return faqs; } ngOnDestroy() { this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten this.seoService.clearStructuredData(); // Clean up SEO structured data } async mail() { try { this.mailinfo.email = this.listingUser.email; this.mailinfo.listing = this.listing; await this.mailService.mail(this.mailinfo); this.validationMessagesService.clearMessages(); this.auditService.createEvent(this.listing.id, 'contact', this.user?.email, this.mailinfo.sender); this.messageService.addMessage({ severity: 'success', text: 'Your message has been sent to the creator of the listing', duration: 3000 }); this.mailinfo = createMailInfo(this.user); } catch (error) { this.messageService.addMessage({ severity: 'danger', text: 'An error occurred while sending the request - Please check your inputs', duration: 5000, }); if (error.error && Array.isArray(error.error?.message)) { this.validationMessagesService.updateMessages(error.error.message); } } } get listingDetails() { let typeOfRealEstate = ''; if (this.listing.realEstateIncluded) { typeOfRealEstate = 'Real Estate Included'; } else if (this.listing.leasedLocation) { typeOfRealEstate = 'Leased Location'; } else if (this.listing.franchiseResale) { typeOfRealEstate = 'Franchise Re-Sale'; } const result = [ { label: 'Category', value: this.selectOptions.getBusiness(this.listing.type) }, { label: 'Located in', value: `${this.listing.location.name ? this.listing.location.name : this.listing.location.county ? this.listing.location.county : ''} ${this.listing.location.name || this.listing.location.county ? ', ' : '' }${this.selectOptions.getState(this.listing.location.state)}`, }, { label: 'Asking Price', value: `${this.listing.price ? `$${this.listing.price.toLocaleString()}` : 'undisclosed '}` }, { label: 'Sales revenue', value: `${this.listing.salesRevenue ? `$${this.listing.salesRevenue.toLocaleString()}` : 'undisclosed '}` }, { label: 'Cash flow', value: `${this.listing.cashFlow ? `$${this.listing.cashFlow.toLocaleString()}` : 'undisclosed '}` }, ...(this.listing.ffe ? [ { label: 'Furniture, Fixtures / Equipment Value (FFE)', value: `$${this.listing.ffe.toLocaleString()}`, }, ] : []), ...(this.listing.inventory ? [ { label: 'Inventory at Cost Value', value: `$${this.listing.inventory.toLocaleString()}`, }, ] : []), { label: 'Type of Real Estate', value: typeOfRealEstate }, { label: 'Employees', value: this.listing.employees }, { label: 'Years established', value: this.listing.established }, { label: 'Support & Training', value: this.listing.supportAndTraining }, { label: 'Reason for Sale', value: this.listing.reasonForSale }, { label: 'Broker licensing', value: this.listing.brokerLicencing }, { label: 'Listed since', value: `${this.dateInserted()} - ${this.getDaysListed()} days` }, { label: 'Listing by', value: null, // Wird nicht verwendet isHtml: true, isListingBy: true, // Flag für den speziellen Fall user: this.listingUser, // Übergebe das User-Objekt imagePath: this.listing.imageName, imageBaseUrl: this.env.imageBaseUrl, ts: this.ts, }, ]; if (this.listing.draft) { result.push({ label: 'Draft', value: this.listing.draft ? 'Yes' : 'No' }); } return result; } async save() { await this.listingsService.addToFavorites(this.listing.id, 'business'); this.listing.favoritesForUser.push(this.user.email); this.auditService.createEvent(this.listing.id, 'favorite', this.user?.email); } isAlreadyFavorite() { return this.listing.favoritesForUser.includes(this.user.email); } async showShareByEMail() { const result = await this.emailService.showShareByEMail({ yourEmail: this.user ? this.user.email : '', yourName: this.user ? `${this.user.firstname} ${this.user.lastname}` : '', recipientEmail: '', url: environment.mailinfoUrl, listingTitle: this.listing.title, id: this.listing.id, type: 'business', }); if (result) { this.auditService.createEvent(this.listing.id, 'email', this.user?.email, result); this.messageService.addMessage({ severity: 'success', text: 'Your Email has beend sent', duration: 5000, }); } } createEvent(eventType: EventTypeEnum) { this.auditService.createEvent(this.listing.id, eventType, this.user?.email); } shareToFacebook() { const url = encodeURIComponent(window.location.href); window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`, '_blank', 'width=600,height=400'); this.createEvent('facebook'); } shareToTwitter() { const url = encodeURIComponent(window.location.href); const text = encodeURIComponent(this.listing?.title || 'Check out this business listing'); window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank', 'width=600,height=400'); this.createEvent('x'); } shareToLinkedIn() { const url = encodeURIComponent(window.location.href); window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`, '_blank', 'width=600,height=400'); this.createEvent('linkedin'); } getDaysListed() { return dayjs().diff(this.listing.created, 'day'); } dateInserted() { return dayjs(this.listing.created).format('DD/MM/YYYY'); } /** * Override configureMap to show city boundary polygon for privacy * Business listings show city boundary instead of exact address */ protected override configureMap() { // For business listings, show city boundary polygon instead of exact location // This protects seller privacy (competition, employees, customers) const latitude = this.listing.location.latitude; const longitude = this.listing.location.longitude; const cityName = this.listing.location.name; const county = this.listing.location.county || ''; const state = this.listing.location.state; // Check if we have valid coordinates (null-safe check) if (latitude !== null && latitude !== undefined && longitude !== null && longitude !== undefined) { 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 // Convert GeoJSON coordinates [lon, lat] to Leaflet LatLng [lat, lon] const latlngs = coordinates.map((coord: number[]) => latLng(coord[1], coord[0])); // Create red outlined polygon for city boundary const cityPolygon = polygon(latlngs, { color: '#ef4444', // Red color (like Google Maps) fillColor: '#ef4444', fillOpacity: 0.1, weight: 2 }); // Add popup to polygon cityPolygon.bindPopup(`
General Area:
${cityName}, ${county ? county + ', ' : ''}${state}
City boundary shown for privacy.
Exact location provided after contact.
`); this.mapLayers = [ tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', }), cityPolygon ]; // Fit map to polygon bounds const bounds = cityPolygon.getBounds(); this.mapOptions = { ...this.mapOptions, center: bounds.getCenter(), zoom: this.mapZoom, }; } else if (data && data.length > 0 && data[0].geojson && data[0].geojson.type === 'MultiPolygon') { // Handle MultiPolygon case (cities with multiple areas) const allPolygons: Polygon[] = []; data[0].geojson.coordinates.forEach((polygonCoords: number[][][]) => { const latlngs = polygonCoords[0].map((coord: number[]) => latLng(coord[1], coord[0])); const cityPolygon = polygon(latlngs, { color: '#ef4444', fillColor: '#ef4444', fillOpacity: 0.1, weight: 2 }); allPolygons.push(cityPolygon); }); // Add popup to first polygon if (allPolygons.length > 0) { allPolygons[0].bindPopup(`
General Area:
${cityName}, ${county ? county + ', ' : ''}${state}
City 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: Use circle if no polygon data available this.useFallbackCircle(latitude, longitude, cityName, county, state); } }, error: (err) => { console.error('Error fetching city boundary:', err); // Fallback: Use circle on error 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); } } } private useFallbackCircle(latitude: number, longitude: number, cityName: string, county: string, state: string) { this.mapCenter = latLng(latitude, longitude); this.mapZoom = 11; const locationCircle = circle([latitude, longitude], { color: '#ef4444', // Red to match polygon style fillColor: '#ef4444', fillOpacity: 0.1, radius: 8000, // 8km radius circle as fallback weight: 2 }); locationCircle.bindPopup(`
General Area:
${cityName}, ${county ? county + ', ' : ''}${state}
Approximate area shown for privacy.
Exact location provided after contact.
`); this.mapLayers = [ tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', }), locationCircle ]; this.mapOptions = { ...this.mapOptions, center: this.mapCenter, zoom: this.mapZoom, }; } /** * 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 */ override onMapReady(map: any) { // Show only city, county, state - no street address const cityName = this.listing.location.name || ''; const county = this.listing.location.county || ''; const state = this.listing.location.state || ''; if (cityName && state) { const addressControl = new Control({ position: 'topright' }); addressControl.onAdd = () => { const container = DomUtil.create('div', 'address-control bg-white p-2 rounded shadow'); const locationText = county ? `${cityName}, ${county}, ${state}` : `${cityName}, ${state}`; container.innerHTML = `
General Area:
${locationText}
Approximate location shown for privacy
`; // Prevent map dragging when clicking the control DomEvent.disableClickPropagation(container); return container; }; addressControl.addTo(map); } } /** * Override openFullMap to open city-area map instead of exact location */ override openFullMap() { const latitude = this.listing.location.latitude; const longitude = this.listing.location.longitude; if (latitude && longitude) { // Open map with zoom level 11 to show large city area, not exact location const url = `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=11/${latitude}/${longitude}`; window.open(url, '_blank'); } } }