import { ChangeDetectorRef, Component, NgZone } from '@angular/core'; import { NgOptimizedImage } from '@angular/common'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { LeafletModule } from '@bluehalo/ngx-leaflet'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs'; import { GalleryModule, ImageItem } from 'ng-gallery'; import { lastValueFrom } from 'rxjs'; import { CommercialPropertyListing, EventTypeEnum, ShareByEMail, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { CommercialPropertyListingCriteria, ErrorResponse, 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 { AuditService } from '../../../services/audit.service'; import { AuthService } from '../../../services/auth.service'; import { HistoryService } from '../../../services/history.service'; import { ImageService } from '../../../services/image.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 { BaseDetailsComponent } from '../base-details.component'; import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component'; import { ShareButton } from 'ngx-sharebuttons/button'; @Component({ selector: 'app-details-commercial-property-listing', standalone: true, imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, GalleryModule, LeafletModule, BreadcrumbsComponent, ShareButton, NgOptimizedImage], providers: [], templateUrl: './details-commercial-property-listing.component.html', styleUrl: '../details.scss', }) export class DetailsCommercialPropertyListingComponent extends BaseDetailsComponent { 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: CommercialPropertyListing; criteria: CommercialPropertyListingCriteria; mailinfo: MailInfo; environment = environment; keycloakUser: KeycloakUser; user: User; listingUser: User; description: SafeHtml; ts = new Date().getTime(); env = environment; errorResponse: ErrorResponse; faTimes = faTimes; propertyDetails = []; images: Array = []; relatedListings: CommercialPropertyListing[] = []; breadcrumbs: BreadcrumbItem[] = []; propertyFAQs: 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 imageService: ImageService, private ngZone: NgZone, private validationMessagesService: ValidationMessagesService, private messageService: MessageService, private auditService: AuditService, private emailService: EMailService, public authService: AuthService, private seoService: SeoService, private cdref: ChangeDetectorRef, ) { super(); this.mailinfo = { sender: { name: '', email: '', phoneNumber: '', state: '', comments: '' }, email: '', url: environment.mailinfoUrl }; } async ngOnInit() { // Initialize default breadcrumbs first this.breadcrumbs = [ { label: 'Home', url: '/home', icon: 'fas fa-home' }, { label: 'Commercial Properties', url: '/commercialPropertyListings' } ]; 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, 'commercialProperty'))) as CommercialPropertyListing; 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); import('flowbite').then(flowbite => { flowbite.initCarousels(); }); this.propertyDetails = [ { label: 'Property Category', value: this.selectOptions.getCommercialProperty(this.listing.type) }, { label: 'Located in', value: this.selectOptions.getState(this.listing.location.state) }, { label: this.listing.location.name ? 'City' : 'County', value: this.listing.location.name ? this.listing.location.name : this.listing.location.county }, { label: 'Asking Price:', value: `$${this.listing.price?.toLocaleString()}` }, { 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.imagePath, imageBaseUrl: this.env.imageBaseUrl, ts: this.ts, }, ]; if (this.listing.draft) { this.propertyDetails.push({ label: 'Draft', value: this.listing.draft ? 'Yes' : 'No' }); } if (this.listing.imageOrder && Array.isArray(this.listing.imageOrder)) { this.listing.imageOrder.forEach(image => { const imageURL = `${this.env.imageBaseUrl}/pictures/property/${this.listing.imagePath}/${this.listing.serialId}/${image}`; this.images.push(new ImageItem({ src: imageURL, thumb: imageURL })); }); } if (this.listing.location.latitude && this.listing.location.longitude) { this.configureMap(); } // Update SEO meta tags for commercial property const propertyData = { id: this.listing.id, propertyType: this.selectOptions.getCommercialProperty(this.listing.type), propertyDescription: 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, address: this.listing.location.street || '', zip: this.listing.location.zipCode || '', latitude: this.listing.location.latitude, longitude: this.listing.location.longitude, squareFootage: (this.listing as any).squareFeet, yearBuilt: (this.listing as any).yearBuilt, images: this.listing.imageOrder?.length > 0 ? this.listing.imageOrder.map(img => `${this.env.imageBaseUrl}/pictures/property/${this.listing.imagePath}/${this.listing.serialId}/${img}`) : [] }; this.seoService.updateCommercialPropertyMeta(propertyData); // Add RealEstateListing structured data const realEstateSchema = this.seoService.generateRealEstateListingSchema(propertyData); const breadcrumbSchema = this.seoService.generateBreadcrumbSchema([ { name: 'Home', url: '/' }, { name: 'Commercial Properties', url: '/commercialPropertyListings' }, { name: propertyData.propertyType, url: `/details-commercial-property/${this.listing.id}` } ]); // Generate FAQ for AEO (Answer Engine Optimization) this.propertyFAQs = this.generatePropertyFAQ(); const faqSchema = this.seoService.generateFAQPageSchema(this.propertyFAQs); // Inject all schemas including FAQ this.seoService.injectMultipleSchemas([realEstateSchema, breadcrumbSchema, faqSchema]); // Generate breadcrumbs for navigation this.breadcrumbs = [ { label: 'Home', url: '/home', icon: 'fas fa-home' }, { label: 'Commercial Properties', url: '/commercialPropertyListings' }, { label: propertyData.propertyType, url: '/commercialPropertyListings' }, { 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: 'Commercial Properties', url: '/commercialPropertyListings' } ]; 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']); } //this.initFlowbite(); } /** * Load related commercial property 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, 'commercialProperty', 3)) as CommercialPropertyListing[]; } catch (error) { console.error('Error loading related listings:', error); this.relatedListings = []; } } /** * Generate dynamic FAQ based on commercial property listing data * Provides AEO (Answer Engine Optimization) content */ private generatePropertyFAQ(): Array<{ question: string; answer: string }> { const faqs: Array<{ question: string; answer: string }> = []; // FAQ 1: What type of property is this? faqs.push({ question: 'What type of commercial property is this?', answer: `This is a ${this.selectOptions.getCommercialProperty(this.listing.type)} property located in ${this.listing.location.name || this.listing.location.county}, ${this.selectOptions.getState(this.listing.location.state)}.` }); // FAQ 2: What is the asking price? if (this.listing.price) { faqs.push({ question: 'What is the asking price for this property?', answer: `The asking price for this commercial property is $${this.listing.price.toLocaleString()}.` }); } else { faqs.push({ question: 'What is the asking price for this property?', answer: 'The asking price is available upon request. Please contact the seller for detailed pricing information.' }); } // FAQ 3: Where is the property located? faqs.push({ question: 'Where is this commercial property located?', answer: `The property is located in ${this.listing.location.name || this.listing.location.county}, ${this.selectOptions.getState(this.listing.location.state)}.${this.listing.location.street ? ' The exact address will be provided after initial contact.' : ''}` }); // FAQ 4: How long has the property been listed? const daysListed = this.getDaysListed(); faqs.push({ question: 'How long has this property been on the market?', answer: `This property was listed on ${this.dateInserted()} and has been on the market for ${daysListed} ${daysListed === 1 ? 'day' : 'days'}.` }); // FAQ 5: How can I schedule a viewing? faqs.push({ question: 'How can I schedule a property viewing?', answer: 'To schedule a viewing of this commercial property, please use the contact form above to get in touch with the listing agent. They will coordinate a convenient time for you to visit the property.' }); // FAQ 6: What is the zoning for this property? faqs.push({ question: 'What is this property suitable for?', answer: `This ${this.selectOptions.getCommercialProperty(this.listing.type)} property is ideal for various commercial uses. Contact the seller for specific zoning information and permitted use details.` }); return faqs; } ngOnDestroy() { this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten this.seoService.clearStructuredData(); // Clean up SEO structured data } private initFlowbite() { this.ngZone.runOutsideAngular(() => { import('flowbite') .then(flowbite => { flowbite.initCarousels(); }) .catch(error => console.error('Error initializing Flowbite:', error)); }); } 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); } } } containsError(fieldname: string) { return this.errorResponse?.fields.map(f => f.fieldname).includes(fieldname); } getImageIndices(): number[] { return this.listing && this.listing.imageOrder ? this.listing.imageOrder.slice(1).map((e, i) => i + 1) : []; } async toggleFavorite() { try { const isFavorited = this.listing.favoritesForUser.includes(this.user.email); if (isFavorited) { // Remove from favorites await this.listingsService.removeFavorite(this.listing.id, 'commercialProperty'); this.listing.favoritesForUser = this.listing.favoritesForUser.filter( email => email !== this.user.email ); } else { // Add to favorites await this.listingsService.addToFavorites(this.listing.id, 'commercialProperty'); this.listing.favoritesForUser.push(this.user.email); this.auditService.createEvent(this.listing.id, 'favorite', this.user?.email); } this.cdref.detectChanges(); } catch (error) { console.error('Error toggling favorite:', error); } } 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: 'commercialProperty', }); 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 commercial property'); 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'); } }