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 { Subject, takeUntil } from 'rxjs'; import dayjs from 'dayjs'; import { BusinessListing, SortByOptions } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListingCriteria, KeycloakUser, LISTINGS_PER_PAGE, ListingType, emailToDirName } 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 { SearchModalComponent } from '../../../components/search-modal/search-modal.component'; import { LazyLoadImageDirective } from '../../../directives/lazy-load-image.directive'; import { AltTextService } from '../../../services/alt-text.service'; import { AuthService } from '../../../services/auth.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 { map2User } from '../../../utils/utils'; @UntilDestroy() @Component({ selector: 'app-business-listings', standalone: true, imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent, SearchModalComponent, LazyLoadImageDirective, BreadcrumbsComponent], templateUrl: './business-listings.component.html', styleUrls: ['./business-listings.component.scss', '../../pages.scss'], }) export class BusinessListingsComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); // Component properties environment = environment; env = environment; listings: Array = []; filteredListings: Array = []; criteria: BusinessListingCriteria; sortBy: SortByOptions | null = null; // Pagination totalRecords = 0; page = 1; pageCount = 1; first = 0; rows = LISTINGS_PER_PAGE; // UI state ts = new Date().getTime(); emailToDirName = emailToDirName; isLoading = false; // Breadcrumbs breadcrumbs: BreadcrumbItem[] = [ { label: 'Home', url: '/', icon: 'fas fa-home' }, { label: 'Business Listings' } ]; // 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 business listings page this.seoService.updateMetaTags({ title: 'Businesses for Sale - Find Profitable Business Opportunities | BizMatch', description: 'Browse thousands of businesses for sale across the United States. Find restaurants, franchises, retail stores, and more. Verified listings from business owners and brokers.', keywords: 'businesses for sale, buy a business, business opportunities, franchise for sale, restaurant for sale, retail business for sale, business broker listings', type: 'website' }); // Subscribe to state changes this.filterStateService .getState$('businessListings') .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 === 'businessListings') { this.search(); } }); } async search(): Promise { try { // Show loading state this.isLoading = true; // Get current criteria from service this.criteria = this.filterStateService.getCriteria('businessListings') as BusinessListingCriteria; // Add sortBy if available const searchCriteria = { ...this.criteria, sortBy: this.sortBy, }; // Perform search const listingsResponse = await this.listingsService.getListings('business'); this.listings = listingsResponse.results; this.totalRecords = listingsResponse.totalCount; this.pageCount = Math.ceil(this.totalRecords / LISTINGS_PER_PAGE); this.page = this.criteria.page || 1; // Hide loading state this.isLoading = false; // 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.isLoading = false; this.cdRef.markForCheck(); } } onPageChange(page: number): void { // Update only pagination properties this.filterStateService.updateCriteria('businessListings', { 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('businessListings'); // Search will be triggered automatically through state subscription } async openFilterModal(): Promise { // Open modal with current criteria const currentCriteria = this.filterStateService.getCriteria('businessListings'); 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 } } getListingPrice(listing: BusinessListing): string { if (!listing.price) return 'Price on Request'; return `$${listing.price.toLocaleString()}`; } getListingLocation(listing: BusinessListing): string { if (!listing.location) return 'Location not specified'; return `${listing.location.name}, ${listing.location.state}`; } private isWithinDays(date: Date | string | undefined | null, days: number): boolean { if (!date) return false; return dayjs().diff(dayjs(date), 'day') < days; } getListingBadge(listing: BusinessListing): 'NEW' | 'UPDATED' | null { if (this.isWithinDays(listing.created, 14)) return 'NEW'; // Priorität if (this.isWithinDays(listing.updated, 14)) return 'UPDATED'; return null; } navigateToDetails(listingId: string): void { this.router.navigate(['/details-business', listingId]); } getDaysListed(listing: BusinessListing) { return dayjs().diff(listing.created, 'day'); } /** * Filter by popular category */ filterByCategory(category: string): void { this.filterStateService.updateCriteria('businessListings', { types: [category], page: 1, start: 0, length: LISTINGS_PER_PAGE, }); // Search will be triggered automatically through state subscription } /** * Check if listing is already in user's favorites */ isFavorite(listing: BusinessListing): 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: BusinessListing): Promise { event.stopPropagation(); event.preventDefault(); if (!this.user?.email) { // User not logged in - redirect to login or show message this.router.navigate(['/login']); return; } try { if (this.isFavorite(listing)) { // Remove from favorites await this.listingsService.removeFavorite(listing.id, 'business'); listing.favoritesForUser = listing.favoritesForUser.filter(email => email !== this.user!.email); } else { // Add to favorites await this.listingsService.addToFavorites(listing.id, 'business'); if (!listing.favoritesForUser) { listing.favoritesForUser = []; } listing.favoritesForUser.push(this.user.email); } this.cdRef.detectChanges(); } catch (error) { console.error('Error toggling favorite:', error); } } /** * Share a listing - opens native share dialog or copies to clipboard */ async shareListing(event: Event, listing: BusinessListing): Promise { event.stopPropagation(); event.preventDefault(); const url = `${window.location.origin}/business/${listing.slug || listing.id}`; const title = listing.title || 'Business 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 business: ${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(() => { // Could add a toast notification here console.log('Link copied to clipboard!'); }).catch(err => { console.error('Failed to copy link:', err); }); } 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()}/businessListings`; // 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: 'Businesses for Sale', description: 'Browse thousands of businesses for sale across the United States. Find restaurants, franchises, retail stores, and more.', totalItems: this.totalRecords, itemsPerPage: LISTINGS_PER_PAGE, currentPage: this.page, baseUrl: baseUrl }); this.seoService.injectStructuredData(collectionSchema); } }