import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { UntilDestroy } from '@ngneat/until-destroy'; import dayjs from 'dayjs'; import { Subject, takeUntil } from 'rxjs'; import { CommercialPropertyListing, SortByOptions } from '../../../../../../bizmatch-server/src/models/db.model'; import { CommercialPropertyListingCriteria, KeycloakUser, LISTINGS_PER_PAGE, ResponseCommercialPropertyListingArray } from '../../../../../../bizmatch-server/src/models/main.model'; import { environment } from '../../../../environments/environment'; import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component'; import { PaginatorComponent } from '../../../components/paginator/paginator.component'; import { ModalService } from '../../../components/search-modal/modal.service'; import { SearchModalCommercialComponent } from '../../../components/search-modal/search-modal-commercial.component'; import { LazyLoadImageDirective } from '../../../directives/lazy-load-image.directive'; import { AltTextService } from '../../../services/alt-text.service'; import { FilterStateService } from '../../../services/filter-state.service'; import { ImageService } from '../../../services/image.service'; import { ListingsService } from '../../../services/listings.service'; import { SearchService } from '../../../services/search.service'; import { SelectOptionsService } from '../../../services/select-options.service'; import { SeoService } from '../../../services/seo.service'; import { AuthService } from '../../../services/auth.service'; import { map2User } from '../../../utils/utils'; @UntilDestroy() @Component({ selector: 'app-commercial-property-listings', standalone: true, imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent, SearchModalCommercialComponent, LazyLoadImageDirective, BreadcrumbsComponent], templateUrl: './commercial-property-listings.component.html', styleUrls: ['./commercial-property-listings.component.scss', '../../pages.scss'], }) export class CommercialPropertyListingsComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); // Component properties environment = environment; env = environment; listings: Array = []; filteredListings: Array = []; criteria: CommercialPropertyListingCriteria; sortBy: SortByOptions | null = null; // Pagination totalRecords = 0; page = 1; pageCount = 1; first = 0; rows = LISTINGS_PER_PAGE; // UI state ts = new Date().getTime(); // Breadcrumbs breadcrumbs: BreadcrumbItem[] = [ { label: 'Home', url: '/home', icon: 'fas fa-home' }, { label: 'Commercial Properties' } ]; // User for favorites user: KeycloakUser | null = null; constructor( public altText: AltTextService, public selectOptions: SelectOptionsService, private listingsService: ListingsService, private router: Router, private cdRef: ChangeDetectorRef, private imageService: ImageService, private searchService: SearchService, private modalService: ModalService, private filterStateService: FilterStateService, private route: ActivatedRoute, private seoService: SeoService, private authService: AuthService, ) {} async ngOnInit(): Promise { // Load user for favorites functionality const token = await this.authService.getToken(); this.user = map2User(token); // Set SEO meta tags for commercial property listings page this.seoService.updateMetaTags({ title: 'Commercial Properties for Sale - Office, Retail, Industrial Real Estate | BizMatch', description: 'Browse commercial real estate listings including office buildings, retail spaces, warehouses, and industrial properties. Investment opportunities from verified sellers and brokers across the United States.', keywords: 'commercial property for sale, commercial real estate, office building for sale, retail space for sale, warehouse for sale, industrial property, investment property, commercial property listings', type: 'website' }); // Subscribe to state changes this.filterStateService .getState$('commercialPropertyListings') .pipe(takeUntil(this.destroy$)) .subscribe(state => { this.criteria = state.criteria; this.sortBy = state.sortBy; // Automatically search when state changes this.search(); }); // Subscribe to search triggers (if triggered from other components) this.searchService.searchTrigger$.pipe(takeUntil(this.destroy$)).subscribe(type => { if (type === 'commercialPropertyListings') { this.search(); } }); } async search(): Promise { try { // Perform search const listingResponse = await this.listingsService.getListings('commercialProperty'); this.listings = (listingResponse as ResponseCommercialPropertyListingArray).results; this.totalRecords = (listingResponse as ResponseCommercialPropertyListingArray).totalCount; this.pageCount = Math.ceil(this.totalRecords / LISTINGS_PER_PAGE); this.page = this.criteria.page || 1; // Update pagination SEO links this.updatePaginationSEO(); // Update view this.cdRef.markForCheck(); this.cdRef.detectChanges(); } catch (error) { console.error('Search error:', error); // Handle error appropriately this.listings = []; this.totalRecords = 0; this.cdRef.markForCheck(); } } onPageChange(page: number): void { // Update only pagination properties this.filterStateService.updateCriteria('commercialPropertyListings', { page: page, start: (page - 1) * LISTINGS_PER_PAGE, length: LISTINGS_PER_PAGE, }); // Search will be triggered automatically through state subscription } clearAllFilters(): void { // Reset criteria but keep sortBy this.filterStateService.clearFilters('commercialPropertyListings'); // Search will be triggered automatically through state subscription } async openFilterModal(): Promise { // Open modal with current criteria const currentCriteria = this.filterStateService.getCriteria('commercialPropertyListings'); const modalResult = await this.modalService.showModal(currentCriteria); if (modalResult.accepted) { // Modal accepted changes - state is updated by modal // Search will be triggered automatically through state subscription } else { // Modal was cancelled - no action needed } } // Helper methods for template getTS(): number { return new Date().getTime(); } getDaysListed(listing: CommercialPropertyListing): number { return dayjs().diff(listing.created, 'day'); } getListingImage(listing: CommercialPropertyListing): string { if (listing.imageOrder?.length > 0) { return `${this.env.imageBaseUrl}/pictures/property/${listing.imagePath}/${listing.serialId}/${listing.imageOrder[0]}`; } return 'assets/images/placeholder_properties.jpg'; } getListingPrice(listing: CommercialPropertyListing): string { if (!listing.price) return 'Price on Request'; return `$${listing.price.toLocaleString()}`; } getListingLocation(listing: CommercialPropertyListing): string { if (!listing.location) return 'Location not specified'; return listing.location.name || listing.location.county || 'Location not specified'; } navigateToDetails(listingId: string): void { this.router.navigate(['/details-commercial-property-listing', listingId]); } /** * Check if listing is already in user's favorites */ isFavorite(listing: CommercialPropertyListing): boolean { if (!this.user?.email || !listing.favoritesForUser) return false; return listing.favoritesForUser.includes(this.user.email); } /** * Toggle favorite status for a listing */ async toggleFavorite(event: Event, listing: CommercialPropertyListing): Promise { event.stopPropagation(); event.preventDefault(); if (!this.user?.email) { // User not logged in - redirect to login this.router.navigate(['/login']); return; } try { if (this.isFavorite(listing)) { // Remove from favorites await this.listingsService.removeFavorite(listing.id, 'commercialProperty'); listing.favoritesForUser = listing.favoritesForUser.filter(email => email !== this.user!.email); } else { // Add to favorites await this.listingsService.addToFavorites(listing.id, 'commercialProperty'); if (!listing.favoritesForUser) { listing.favoritesForUser = []; } listing.favoritesForUser.push(this.user.email); } this.cdRef.detectChanges(); } catch (error) { console.error('Error toggling favorite:', error); } } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); // Clean up pagination links when leaving the page this.seoService.clearPaginationLinks(); } /** * Update pagination SEO links (rel="next/prev") and CollectionPage schema */ private updatePaginationSEO(): void { const baseUrl = `${this.seoService.getBaseUrl()}/commercialPropertyListings`; // Inject rel="next" and rel="prev" links this.seoService.injectPaginationLinks(baseUrl, this.page, this.pageCount); // Inject CollectionPage schema for paginated results const collectionSchema = this.seoService.generateCollectionPageSchema({ name: 'Commercial Properties for Sale', description: 'Browse commercial real estate listings including office buildings, retail spaces, warehouses, and industrial properties across the United States.', totalItems: this.totalRecords, itemsPerPage: LISTINGS_PER_PAGE, currentPage: this.page, baseUrl: baseUrl }); this.seoService.injectStructuredData(collectionSchema); } /** * Share property listing */ async shareProperty(event: Event, listing: CommercialPropertyListing): Promise { event.stopPropagation(); event.preventDefault(); const url = `${window.location.origin}/commercial-property/${listing.slug || listing.id}`; const title = listing.title || 'Commercial Property Listing'; // Try native share API first (works on mobile and some desktop browsers) if (navigator.share) { try { await navigator.share({ title: title, text: `Check out this property: ${title}`, url: url, }); } catch (err) { // User cancelled or share failed - fall back to clipboard this.copyToClipboard(url); } } else { // Fallback: open Facebook share dialog const encodedUrl = encodeURIComponent(url); window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`, '_blank', 'width=600,height=400'); } } /** * Copy URL to clipboard and show feedback */ private copyToClipboard(url: string): void { navigator.clipboard.writeText(url).then(() => { console.log('Link copied to clipboard!'); }).catch(err => { console.error('Failed to copy link:', err); }); } }