diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f9bb23a..8e2c3ff 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -26,7 +26,9 @@ "Bash(ls:*)", "WebFetch(domain:angular.dev)", "Bash(killall:*)", - "Bash(echo:*)" + "Bash(echo:*)", + "Bash(npm run build:*)", + "Bash(npx tsc:*)" ] } } diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..362ddc7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf +*.png binary +*.jpg binary +*.jpeg binary diff --git a/bizmatch-server/src/sitemap/sitemap.service.ts b/bizmatch-server/src/sitemap/sitemap.service.ts index fdad97c..d607682 100644 --- a/bizmatch-server/src/sitemap/sitemap.service.ts +++ b/bizmatch-server/src/sitemap/sitemap.service.ts @@ -1,362 +1,362 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { eq, sql } from 'drizzle-orm'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import * as schema from '../drizzle/schema'; -import { PG_CONNECTION } from '../drizzle/schema'; - -interface SitemapUrl { - loc: string; - lastmod?: string; - changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'; - priority?: number; -} - -interface SitemapIndexEntry { - loc: string; - lastmod?: string; -} - -@Injectable() -export class SitemapService { - private readonly baseUrl = 'https://biz-match.com'; - private readonly URLS_PER_SITEMAP = 10000; // Google best practice - - constructor(@Inject(PG_CONNECTION) private readonly db: NodePgDatabase) { } - - /** - * Generate sitemap index (main sitemap.xml) - * Lists all sitemap files: static, business-1, business-2, commercial-1, etc. - */ - async generateSitemapIndex(): Promise { - const sitemaps: SitemapIndexEntry[] = []; - - // Add static pages sitemap - sitemaps.push({ - loc: `${this.baseUrl}/bizmatch/sitemap/static.xml`, - lastmod: this.formatDate(new Date()), - }); - - // Count business listings - const businessCount = await this.getBusinessListingsCount(); - const businessPages = Math.ceil(businessCount / this.URLS_PER_SITEMAP) || 1; - for (let page = 1; page <= businessPages; page++) { - sitemaps.push({ - loc: `${this.baseUrl}/bizmatch/sitemap/business-${page}.xml`, - lastmod: this.formatDate(new Date()), - }); - } - - // Count commercial property listings - const commercialCount = await this.getCommercialPropertiesCount(); - const commercialPages = Math.ceil(commercialCount / this.URLS_PER_SITEMAP) || 1; - for (let page = 1; page <= commercialPages; page++) { - sitemaps.push({ - loc: `${this.baseUrl}/bizmatch/sitemap/commercial-${page}.xml`, - lastmod: this.formatDate(new Date()), - }); - } - - // Count broker profiles - const brokerCount = await this.getBrokerProfilesCount(); - const brokerPages = Math.ceil(brokerCount / this.URLS_PER_SITEMAP) || 1; - for (let page = 1; page <= brokerPages; page++) { - sitemaps.push({ - loc: `${this.baseUrl}/bizmatch/sitemap/brokers-${page}.xml`, - lastmod: this.formatDate(new Date()), - }); - } - - return this.buildXmlSitemapIndex(sitemaps); - } - - /** - * Generate static pages sitemap - */ - async generateStaticSitemap(): Promise { - const urls = this.getStaticPageUrls(); - return this.buildXmlSitemap(urls); - } - - /** - * Generate business listings sitemap (paginated) - */ - async generateBusinessSitemap(page: number): Promise { - const offset = (page - 1) * this.URLS_PER_SITEMAP; - const urls = await this.getBusinessListingUrls(offset, this.URLS_PER_SITEMAP); - return this.buildXmlSitemap(urls); - } - - /** - * Generate commercial property sitemap (paginated) - */ - async generateCommercialSitemap(page: number): Promise { - const offset = (page - 1) * this.URLS_PER_SITEMAP; - const urls = await this.getCommercialPropertyUrls(offset, this.URLS_PER_SITEMAP); - return this.buildXmlSitemap(urls); - } - - /** - * Build XML sitemap index - */ - private buildXmlSitemapIndex(sitemaps: SitemapIndexEntry[]): string { - const sitemapElements = sitemaps - .map(sitemap => { - let element = ` \n ${sitemap.loc}`; - if (sitemap.lastmod) { - element += `\n ${sitemap.lastmod}`; - } - element += '\n '; - return element; - }) - .join('\n'); - - return ` - -${sitemapElements} -`; - } - - /** - * Build XML sitemap string - */ - private buildXmlSitemap(urls: SitemapUrl[]): string { - const urlElements = urls.map(url => this.buildUrlElement(url)).join('\n '); - - return ` - - ${urlElements} -`; - } - - /** - * Build single URL element - */ - private buildUrlElement(url: SitemapUrl): string { - let element = `\n ${url.loc}`; - - if (url.lastmod) { - element += `\n ${url.lastmod}`; - } - - if (url.changefreq) { - element += `\n ${url.changefreq}`; - } - - if (url.priority !== undefined) { - element += `\n ${url.priority.toFixed(1)}`; - } - - element += '\n '; - return element; - } - - /** - * Get static page URLs - */ - private getStaticPageUrls(): SitemapUrl[] { - return [ - { - loc: `${this.baseUrl}/`, - changefreq: 'daily', - priority: 1.0, - }, - { - loc: `${this.baseUrl}/home`, - changefreq: 'daily', - priority: 1.0, - }, - { - loc: `${this.baseUrl}/businessListings`, - changefreq: 'daily', - priority: 0.9, - }, - { - loc: `${this.baseUrl}/commercialPropertyListings`, - changefreq: 'daily', - priority: 0.9, - }, - { - loc: `${this.baseUrl}/brokerListings`, - changefreq: 'daily', - priority: 0.8, - }, - { - loc: `${this.baseUrl}/terms-of-use`, - changefreq: 'monthly', - priority: 0.5, - }, - { - loc: `${this.baseUrl}/privacy-statement`, - changefreq: 'monthly', - priority: 0.5, - }, - ]; - } - - /** - * Count business listings (non-draft) - */ - private async getBusinessListingsCount(): Promise { - try { - const result = await this.db - .select({ count: sql`count(*)` }) - .from(schema.businesses_json) - .where(sql`(${schema.businesses_json.data}->>'draft')::boolean IS NOT TRUE`); - - return Number(result[0]?.count || 0); - } catch (error) { - console.error('Error counting business listings:', error); - return 0; - } - } - - /** - * Count commercial properties (non-draft) - */ - private async getCommercialPropertiesCount(): Promise { - try { - const result = await this.db - .select({ count: sql`count(*)` }) - .from(schema.commercials_json) - .where(sql`(${schema.commercials_json.data}->>'draft')::boolean IS NOT TRUE`); - - return Number(result[0]?.count || 0); - } catch (error) { - console.error('Error counting commercial properties:', error); - return 0; - } - } - - /** - * Get business listing URLs from database (paginated, slug-based) - */ - private async getBusinessListingUrls(offset: number, limit: number): Promise { - try { - const listings = await this.db - .select({ - id: schema.businesses_json.id, - slug: sql`${schema.businesses_json.data}->>'slug'`, - updated: sql`(${schema.businesses_json.data}->>'updated')::timestamptz`, - created: sql`(${schema.businesses_json.data}->>'created')::timestamptz`, - }) - .from(schema.businesses_json) - .where(sql`(${schema.businesses_json.data}->>'draft')::boolean IS NOT TRUE`) - .limit(limit) - .offset(offset); - - return listings.map(listing => { - const urlSlug = listing.slug || listing.id; - return { - loc: `${this.baseUrl}/business/${urlSlug}`, - lastmod: this.formatDate(listing.updated || listing.created), - changefreq: 'weekly' as const, - priority: 0.8, - }; - }); - } catch (error) { - console.error('Error fetching business listings for sitemap:', error); - return []; - } - } - - /** - * Get commercial property URLs from database (paginated, slug-based) - */ - private async getCommercialPropertyUrls(offset: number, limit: number): Promise { - try { - const properties = await this.db - .select({ - id: schema.commercials_json.id, - slug: sql`${schema.commercials_json.data}->>'slug'`, - updated: sql`(${schema.commercials_json.data}->>'updated')::timestamptz`, - created: sql`(${schema.commercials_json.data}->>'created')::timestamptz`, - }) - .from(schema.commercials_json) - .where(sql`(${schema.commercials_json.data}->>'draft')::boolean IS NOT TRUE`) - .limit(limit) - .offset(offset); - - return properties.map(property => { - const urlSlug = property.slug || property.id; - return { - loc: `${this.baseUrl}/commercial-property/${urlSlug}`, - lastmod: this.formatDate(property.updated || property.created), - changefreq: 'weekly' as const, - priority: 0.8, - }; - }); - } catch (error) { - console.error('Error fetching commercial properties for sitemap:', error); - return []; - } - } - - /** - * Format date to ISO 8601 format (YYYY-MM-DD) - */ - private formatDate(date: Date | string): string { - if (!date) return new Date().toISOString().split('T')[0]; - const d = typeof date === 'string' ? new Date(date) : date; - return d.toISOString().split('T')[0]; - } - - /** - * Generate broker profiles sitemap (paginated) - */ - async generateBrokerSitemap(page: number): Promise { - const offset = (page - 1) * this.URLS_PER_SITEMAP; - const urls = await this.getBrokerProfileUrls(offset, this.URLS_PER_SITEMAP); - return this.buildXmlSitemap(urls); - } - - /** - * Count broker profiles (professionals with showInDirectory=true) - */ - private async getBrokerProfilesCount(): Promise { - try { - const result = await this.db - .select({ count: sql`count(*)` }) - .from(schema.users_json) - .where(sql` - (${schema.users_json.data}->>'customerType') = 'professional' - AND (${schema.users_json.data}->>'showInDirectory')::boolean IS TRUE - `); - - return Number(result[0]?.count || 0); - } catch (error) { - console.error('Error counting broker profiles:', error); - return 0; - } - } - - /** - * Get broker profile URLs from database (paginated) - */ - private async getBrokerProfileUrls(offset: number, limit: number): Promise { - try { - const brokers = await this.db - .select({ - email: schema.users_json.email, - updated: sql`(${schema.users_json.data}->>'updated')::timestamptz`, - created: sql`(${schema.users_json.data}->>'created')::timestamptz`, - }) - .from(schema.users_json) - .where(sql` - (${schema.users_json.data}->>'customerType') = 'professional' - AND (${schema.users_json.data}->>'showInDirectory')::boolean IS TRUE - `) - .limit(limit) - .offset(offset); - - return brokers.map(broker => ({ - loc: `${this.baseUrl}/details-user/${encodeURIComponent(broker.email)}`, - lastmod: this.formatDate(broker.updated || broker.created), - changefreq: 'weekly' as const, - priority: 0.7, - })); - } catch (error) { - console.error('Error fetching broker profiles for sitemap:', error); - return []; - } - } -} +import { Inject, Injectable } from '@nestjs/common'; +import { eq, sql } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as schema from '../drizzle/schema'; +import { PG_CONNECTION } from '../drizzle/schema'; + +interface SitemapUrl { + loc: string; + lastmod?: string; + changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'; + priority?: number; +} + +interface SitemapIndexEntry { + loc: string; + lastmod?: string; +} + +@Injectable() +export class SitemapService { + private readonly baseUrl = 'https://www.bizmatch.net'; + private readonly URLS_PER_SITEMAP = 10000; // Google best practice + + constructor(@Inject(PG_CONNECTION) private readonly db: NodePgDatabase) { } + + /** + * Generate sitemap index (main sitemap.xml) + * Lists all sitemap files: static, business-1, business-2, commercial-1, etc. + */ + async generateSitemapIndex(): Promise { + const sitemaps: SitemapIndexEntry[] = []; + + // Add static pages sitemap + sitemaps.push({ + loc: `${this.baseUrl}/bizmatch/sitemap/static.xml`, + lastmod: this.formatDate(new Date()), + }); + + // Count business listings + const businessCount = await this.getBusinessListingsCount(); + const businessPages = Math.ceil(businessCount / this.URLS_PER_SITEMAP) || 1; + for (let page = 1; page <= businessPages; page++) { + sitemaps.push({ + loc: `${this.baseUrl}/bizmatch/sitemap/business-${page}.xml`, + lastmod: this.formatDate(new Date()), + }); + } + + // Count commercial property listings + const commercialCount = await this.getCommercialPropertiesCount(); + const commercialPages = Math.ceil(commercialCount / this.URLS_PER_SITEMAP) || 1; + for (let page = 1; page <= commercialPages; page++) { + sitemaps.push({ + loc: `${this.baseUrl}/bizmatch/sitemap/commercial-${page}.xml`, + lastmod: this.formatDate(new Date()), + }); + } + + // Count broker profiles + const brokerCount = await this.getBrokerProfilesCount(); + const brokerPages = Math.ceil(brokerCount / this.URLS_PER_SITEMAP) || 1; + for (let page = 1; page <= brokerPages; page++) { + sitemaps.push({ + loc: `${this.baseUrl}/bizmatch/sitemap/brokers-${page}.xml`, + lastmod: this.formatDate(new Date()), + }); + } + + return this.buildXmlSitemapIndex(sitemaps); + } + + /** + * Generate static pages sitemap + */ + async generateStaticSitemap(): Promise { + const urls = this.getStaticPageUrls(); + return this.buildXmlSitemap(urls); + } + + /** + * Generate business listings sitemap (paginated) + */ + async generateBusinessSitemap(page: number): Promise { + const offset = (page - 1) * this.URLS_PER_SITEMAP; + const urls = await this.getBusinessListingUrls(offset, this.URLS_PER_SITEMAP); + return this.buildXmlSitemap(urls); + } + + /** + * Generate commercial property sitemap (paginated) + */ + async generateCommercialSitemap(page: number): Promise { + const offset = (page - 1) * this.URLS_PER_SITEMAP; + const urls = await this.getCommercialPropertyUrls(offset, this.URLS_PER_SITEMAP); + return this.buildXmlSitemap(urls); + } + + /** + * Build XML sitemap index + */ + private buildXmlSitemapIndex(sitemaps: SitemapIndexEntry[]): string { + const sitemapElements = sitemaps + .map(sitemap => { + let element = ` \n ${sitemap.loc}`; + if (sitemap.lastmod) { + element += `\n ${sitemap.lastmod}`; + } + element += '\n '; + return element; + }) + .join('\n'); + + return ` + +${sitemapElements} +`; + } + + /** + * Build XML sitemap string + */ + private buildXmlSitemap(urls: SitemapUrl[]): string { + const urlElements = urls.map(url => this.buildUrlElement(url)).join('\n '); + + return ` + + ${urlElements} +`; + } + + /** + * Build single URL element + */ + private buildUrlElement(url: SitemapUrl): string { + let element = `\n ${url.loc}`; + + if (url.lastmod) { + element += `\n ${url.lastmod}`; + } + + if (url.changefreq) { + element += `\n ${url.changefreq}`; + } + + if (url.priority !== undefined) { + element += `\n ${url.priority.toFixed(1)}`; + } + + element += '\n '; + return element; + } + + /** + * Get static page URLs + */ + private getStaticPageUrls(): SitemapUrl[] { + return [ + { + loc: `${this.baseUrl}/`, + changefreq: 'daily', + priority: 1.0, + }, + { + loc: `${this.baseUrl}/home`, + changefreq: 'daily', + priority: 1.0, + }, + { + loc: `${this.baseUrl}/businessListings`, + changefreq: 'daily', + priority: 0.9, + }, + { + loc: `${this.baseUrl}/commercialPropertyListings`, + changefreq: 'daily', + priority: 0.9, + }, + { + loc: `${this.baseUrl}/brokerListings`, + changefreq: 'daily', + priority: 0.8, + }, + { + loc: `${this.baseUrl}/terms-of-use`, + changefreq: 'monthly', + priority: 0.5, + }, + { + loc: `${this.baseUrl}/privacy-statement`, + changefreq: 'monthly', + priority: 0.5, + }, + ]; + } + + /** + * Count business listings (non-draft) + */ + private async getBusinessListingsCount(): Promise { + try { + const result = await this.db + .select({ count: sql`count(*)` }) + .from(schema.businesses_json) + .where(sql`(${schema.businesses_json.data}->>'draft')::boolean IS NOT TRUE`); + + return Number(result[0]?.count || 0); + } catch (error) { + console.error('Error counting business listings:', error); + return 0; + } + } + + /** + * Count commercial properties (non-draft) + */ + private async getCommercialPropertiesCount(): Promise { + try { + const result = await this.db + .select({ count: sql`count(*)` }) + .from(schema.commercials_json) + .where(sql`(${schema.commercials_json.data}->>'draft')::boolean IS NOT TRUE`); + + return Number(result[0]?.count || 0); + } catch (error) { + console.error('Error counting commercial properties:', error); + return 0; + } + } + + /** + * Get business listing URLs from database (paginated, slug-based) + */ + private async getBusinessListingUrls(offset: number, limit: number): Promise { + try { + const listings = await this.db + .select({ + id: schema.businesses_json.id, + slug: sql`${schema.businesses_json.data}->>'slug'`, + updated: sql`(${schema.businesses_json.data}->>'updated')::timestamptz`, + created: sql`(${schema.businesses_json.data}->>'created')::timestamptz`, + }) + .from(schema.businesses_json) + .where(sql`(${schema.businesses_json.data}->>'draft')::boolean IS NOT TRUE`) + .limit(limit) + .offset(offset); + + return listings.map(listing => { + const urlSlug = listing.slug || listing.id; + return { + loc: `${this.baseUrl}/business/${urlSlug}`, + lastmod: this.formatDate(listing.updated || listing.created), + changefreq: 'weekly' as const, + priority: 0.8, + }; + }); + } catch (error) { + console.error('Error fetching business listings for sitemap:', error); + return []; + } + } + + /** + * Get commercial property URLs from database (paginated, slug-based) + */ + private async getCommercialPropertyUrls(offset: number, limit: number): Promise { + try { + const properties = await this.db + .select({ + id: schema.commercials_json.id, + slug: sql`${schema.commercials_json.data}->>'slug'`, + updated: sql`(${schema.commercials_json.data}->>'updated')::timestamptz`, + created: sql`(${schema.commercials_json.data}->>'created')::timestamptz`, + }) + .from(schema.commercials_json) + .where(sql`(${schema.commercials_json.data}->>'draft')::boolean IS NOT TRUE`) + .limit(limit) + .offset(offset); + + return properties.map(property => { + const urlSlug = property.slug || property.id; + return { + loc: `${this.baseUrl}/commercial-property/${urlSlug}`, + lastmod: this.formatDate(property.updated || property.created), + changefreq: 'weekly' as const, + priority: 0.8, + }; + }); + } catch (error) { + console.error('Error fetching commercial properties for sitemap:', error); + return []; + } + } + + /** + * Format date to ISO 8601 format (YYYY-MM-DD) + */ + private formatDate(date: Date | string): string { + if (!date) return new Date().toISOString().split('T')[0]; + const d = typeof date === 'string' ? new Date(date) : date; + return d.toISOString().split('T')[0]; + } + + /** + * Generate broker profiles sitemap (paginated) + */ + async generateBrokerSitemap(page: number): Promise { + const offset = (page - 1) * this.URLS_PER_SITEMAP; + const urls = await this.getBrokerProfileUrls(offset, this.URLS_PER_SITEMAP); + return this.buildXmlSitemap(urls); + } + + /** + * Count broker profiles (professionals with showInDirectory=true) + */ + private async getBrokerProfilesCount(): Promise { + try { + const result = await this.db + .select({ count: sql`count(*)` }) + .from(schema.users_json) + .where(sql` + (${schema.users_json.data}->>'customerType') = 'professional' + AND (${schema.users_json.data}->>'showInDirectory')::boolean IS TRUE + `); + + return Number(result[0]?.count || 0); + } catch (error) { + console.error('Error counting broker profiles:', error); + return 0; + } + } + + /** + * Get broker profile URLs from database (paginated) + */ + private async getBrokerProfileUrls(offset: number, limit: number): Promise { + try { + const brokers = await this.db + .select({ + email: schema.users_json.email, + updated: sql`(${schema.users_json.data}->>'updated')::timestamptz`, + created: sql`(${schema.users_json.data}->>'created')::timestamptz`, + }) + .from(schema.users_json) + .where(sql` + (${schema.users_json.data}->>'customerType') = 'professional' + AND (${schema.users_json.data}->>'showInDirectory')::boolean IS TRUE + `) + .limit(limit) + .offset(offset); + + return brokers.map(broker => ({ + loc: `${this.baseUrl}/details-user/${encodeURIComponent(broker.email)}`, + lastmod: this.formatDate(broker.updated || broker.created), + changefreq: 'weekly' as const, + priority: 0.7, + })); + } catch (error) { + console.error('Error fetching broker profiles for sitemap:', error); + return []; + } + } +} diff --git a/bizmatch/src/app/app.routes.ts b/bizmatch/src/app/app.routes.ts index 242bb51..a5262a1 100644 --- a/bizmatch/src/app/app.routes.ts +++ b/bizmatch/src/app/app.routes.ts @@ -1,193 +1,194 @@ -import { Routes } from '@angular/router'; -import { LogoutComponent } from './components/logout/logout.component'; -import { NotFoundComponent } from './components/not-found/not-found.component'; -import { TestSsrComponent } from './components/test-ssr/test-ssr.component'; - -import { EmailAuthorizedComponent } from './components/email-authorized/email-authorized.component'; -import { EmailVerificationComponent } from './components/email-verification/email-verification.component'; -import { LoginRegisterComponent } from './components/login-register/login-register.component'; -import { AuthGuard } from './guards/auth.guard'; -import { ListingCategoryGuard } from './guards/listing-category.guard'; -import { UserListComponent } from './pages/admin/user-list/user-list.component'; -import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component'; -import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component'; -import { DetailsUserComponent } from './pages/details/details-user/details-user.component'; -import { HomeComponent } from './pages/home/home.component'; -import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component'; -import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component'; -import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component'; -import { AccountComponent } from './pages/subscription/account/account.component'; -import { EditBusinessListingComponent } from './pages/subscription/edit-business-listing/edit-business-listing.component'; -import { EditCommercialPropertyListingComponent } from './pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component'; -import { EmailUsComponent } from './pages/subscription/email-us/email-us.component'; -import { FavoritesComponent } from './pages/subscription/favorites/favorites.component'; -import { MyListingComponent } from './pages/subscription/my-listing/my-listing.component'; -import { SuccessComponent } from './pages/success/success.component'; -import { TermsOfUseComponent } from './pages/legal/terms-of-use.component'; -import { PrivacyStatementComponent } from './pages/legal/privacy-statement.component'; - -export const routes: Routes = [ - { - path: 'test-ssr', - component: TestSsrComponent, - }, - { - path: 'businessListings', - component: BusinessListingsComponent, - runGuardsAndResolvers: 'always', - }, - { - path: 'commercialPropertyListings', - component: CommercialPropertyListingsComponent, - runGuardsAndResolvers: 'always', - }, - { - path: 'brokerListings', - component: BrokerListingsComponent, - runGuardsAndResolvers: 'always', - }, - { - path: 'home', - component: HomeComponent, - }, - // ######### - // Listings Details - New SEO-friendly slug-based URLs - { - path: 'business/:slug', - component: DetailsBusinessListingComponent, - }, - { - path: 'commercial-property/:slug', - component: DetailsCommercialPropertyListingComponent, - }, - // Backward compatibility redirects for old UUID-based URLs - { - path: 'details-business-listing/:id', - redirectTo: 'business/:id', - pathMatch: 'full', - }, - { - path: 'details-commercial-property-listing/:id', - redirectTo: 'commercial-property/:id', - pathMatch: 'full', - }, - { - path: 'listing/:id', - canActivate: [ListingCategoryGuard], - component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet - }, - // { - // path: 'login/:page', - // component: LoginComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet - // }, - { - path: 'login/:page', - component: LoginRegisterComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet - }, - { - path: 'login', - component: LoginRegisterComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet - }, - { - path: 'notfound', - component: NotFoundComponent, - }, - // ######### - // User Details - { - path: 'details-user/:id', - component: DetailsUserComponent, - }, - // ######### - // User edit - { - path: 'account', - component: AccountComponent, - canActivate: [AuthGuard], - }, - { - path: 'account/:id', - component: AccountComponent, - canActivate: [AuthGuard], - }, - // ######### - // Create, Update Listings - { - path: 'editBusinessListing/:id', - component: EditBusinessListingComponent, - canActivate: [AuthGuard], - }, - { - path: 'createBusinessListing', - component: EditBusinessListingComponent, - canActivate: [AuthGuard], - }, - { - path: 'editCommercialPropertyListing/:id', - component: EditCommercialPropertyListingComponent, - canActivate: [AuthGuard], - }, - { - path: 'createCommercialPropertyListing', - component: EditCommercialPropertyListingComponent, - canActivate: [AuthGuard], - }, - // ######### - // My Listings - { - path: 'myListings', - component: MyListingComponent, - canActivate: [AuthGuard], - }, - // ######### - // My Favorites - { - path: 'myFavorites', - component: FavoritesComponent, - canActivate: [AuthGuard], - }, - // ######### - // EMAil Us - { - path: 'emailUs', - component: EmailUsComponent, - // canActivate: [AuthGuard], - }, - // ######### - // Logout - { - path: 'logout', - component: LogoutComponent, - canActivate: [AuthGuard], - }, - // ######### - // Email Verification - { - path: 'emailVerification', - component: EmailVerificationComponent, - }, - { - path: 'email-authorized', - component: EmailAuthorizedComponent, - }, - { - path: 'success', - component: SuccessComponent, - }, - { - path: 'admin/users', - component: UserListComponent, - canActivate: [AuthGuard], - }, - // ######### - // Legal Pages - { - path: 'terms-of-use', - component: TermsOfUseComponent, - }, - { - path: 'privacy-statement', - component: PrivacyStatementComponent, - }, - { path: '**', redirectTo: 'home' }, -]; +import { Routes } from '@angular/router'; +// Core components (eagerly loaded - needed for initial navigation) +import { LogoutComponent } from './components/logout/logout.component'; +import { NotFoundComponent } from './components/not-found/not-found.component'; +import { TestSsrComponent } from './components/test-ssr/test-ssr.component'; +import { EmailAuthorizedComponent } from './components/email-authorized/email-authorized.component'; +import { EmailVerificationComponent } from './components/email-verification/email-verification.component'; +import { LoginRegisterComponent } from './components/login-register/login-register.component'; + +// Guards +import { AuthGuard } from './guards/auth.guard'; +import { ListingCategoryGuard } from './guards/listing-category.guard'; + +// Public pages (eagerly loaded - high traffic) +import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component'; +import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component'; +import { DetailsUserComponent } from './pages/details/details-user/details-user.component'; +import { HomeComponent } from './pages/home/home.component'; +import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component'; +import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component'; +import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component'; +import { SuccessComponent } from './pages/success/success.component'; +import { TermsOfUseComponent } from './pages/legal/terms-of-use.component'; +import { PrivacyStatementComponent } from './pages/legal/privacy-statement.component'; + +// Note: Account, Edit, Admin, Favorites, MyListing, and EmailUs components are now lazy-loaded below + +export const routes: Routes = [ + { + path: 'test-ssr', + component: TestSsrComponent, + }, + { + path: 'businessListings', + component: BusinessListingsComponent, + runGuardsAndResolvers: 'always', + }, + { + path: 'commercialPropertyListings', + component: CommercialPropertyListingsComponent, + runGuardsAndResolvers: 'always', + }, + { + path: 'brokerListings', + component: BrokerListingsComponent, + runGuardsAndResolvers: 'always', + }, + { + path: 'home', + component: HomeComponent, + }, + // ######### + // Listings Details - New SEO-friendly slug-based URLs + { + path: 'business/:slug', + component: DetailsBusinessListingComponent, + }, + { + path: 'commercial-property/:slug', + component: DetailsCommercialPropertyListingComponent, + }, + // Backward compatibility redirects for old UUID-based URLs + { + path: 'details-business-listing/:id', + redirectTo: 'business/:id', + pathMatch: 'full', + }, + { + path: 'details-commercial-property-listing/:id', + redirectTo: 'commercial-property/:id', + pathMatch: 'full', + }, + { + path: 'listing/:id', + canActivate: [ListingCategoryGuard], + component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet + }, + // { + // path: 'login/:page', + // component: LoginComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet + // }, + { + path: 'login/:page', + component: LoginRegisterComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet + }, + { + path: 'login', + component: LoginRegisterComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet + }, + { + path: 'notfound', + component: NotFoundComponent, + }, + // ######### + // User Details + { + path: 'details-user/:id', + component: DetailsUserComponent, + }, + // ######### + // User edit (lazy-loaded) + { + path: 'account', + loadComponent: () => import('./pages/subscription/account/account.component').then(m => m.AccountComponent), + canActivate: [AuthGuard], + }, + { + path: 'account/:id', + loadComponent: () => import('./pages/subscription/account/account.component').then(m => m.AccountComponent), + canActivate: [AuthGuard], + }, + // ######### + // Create, Update Listings (lazy-loaded) + { + path: 'editBusinessListing/:id', + loadComponent: () => import('./pages/subscription/edit-business-listing/edit-business-listing.component').then(m => m.EditBusinessListingComponent), + canActivate: [AuthGuard], + }, + { + path: 'createBusinessListing', + loadComponent: () => import('./pages/subscription/edit-business-listing/edit-business-listing.component').then(m => m.EditBusinessListingComponent), + canActivate: [AuthGuard], + }, + { + path: 'editCommercialPropertyListing/:id', + loadComponent: () => import('./pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component').then(m => m.EditCommercialPropertyListingComponent), + canActivate: [AuthGuard], + }, + { + path: 'createCommercialPropertyListing', + loadComponent: () => import('./pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component').then(m => m.EditCommercialPropertyListingComponent), + canActivate: [AuthGuard], + }, + // ######### + // My Listings (lazy-loaded) + { + path: 'myListings', + loadComponent: () => import('./pages/subscription/my-listing/my-listing.component').then(m => m.MyListingComponent), + canActivate: [AuthGuard], + }, + // ######### + // My Favorites (lazy-loaded) + { + path: 'myFavorites', + loadComponent: () => import('./pages/subscription/favorites/favorites.component').then(m => m.FavoritesComponent), + canActivate: [AuthGuard], + }, + // ######### + // Email Us (lazy-loaded) + { + path: 'emailUs', + loadComponent: () => import('./pages/subscription/email-us/email-us.component').then(m => m.EmailUsComponent), + // canActivate: [AuthGuard], + }, + // ######### + // Logout + { + path: 'logout', + component: LogoutComponent, + canActivate: [AuthGuard], + }, + // ######### + // Email Verification + { + path: 'emailVerification', + component: EmailVerificationComponent, + }, + { + path: 'email-authorized', + component: EmailAuthorizedComponent, + }, + { + path: 'success', + component: SuccessComponent, + }, + // ######### + // Admin Pages (lazy-loaded) + { + path: 'admin/users', + loadComponent: () => import('./pages/admin/user-list/user-list.component').then(m => m.UserListComponent), + canActivate: [AuthGuard], + }, + // ######### + // Legal Pages + { + path: 'terms-of-use', + component: TermsOfUseComponent, + }, + { + path: 'privacy-statement', + component: PrivacyStatementComponent, + }, + { path: '**', redirectTo: 'home' }, +]; diff --git a/bizmatch/src/app/pages/home/home.component.html b/bizmatch/src/app/pages/home/home.component.html index dcd623b..761867a 100644 --- a/bizmatch/src/app/pages/home/home.component.html +++ b/bizmatch/src/app/pages/home/home.component.html @@ -1,206 +1,253 @@ -
- Logo - - -
- -
-
- - @if(user){ - Account - } @else { - Log In - Sign Up - } - -
-
- - - -
-
-
- -
- -
- - - - - - -
-

Buy & Sell Businesses and Commercial Properties

- -

- Find profitable businesses for sale, commercial real estate, and franchise opportunities across the United States -

-
-
- -
- @if(!aiSearch){ -
- -
- } @if(criteria && !aiSearch){ -
-
-
- -
- -
-
-
- -
-
- - @for (city of cities$ | async; track city.id) { @let state = city.type==='city'?city.content.state:''; @let separator = city.type==='city'?' - ':''; - {{ city.content.name }}{{ separator }}{{ state }} - } - -
-
- @if (criteria.radius && !aiSearch){ -
-
- -
- -
-
-
- } -
- @if( numberOfResults$){ - - }@else { - - } -
-
- } -
-
-
-
- - - -
- +
+ Logo + + +
+ +
+
+ + @if(user){ + Account + } @else { + Log In + Sign Up + } + +
+
+ + + +
+
+ + + + + + + +
+ +
+ +
+ +
+ + + + + + +
+

Buy & Sell Businesses and Commercial Properties

+ +

+ Buy profitable businesses for sale or sell your business to qualified buyers. Browse commercial real estate and franchise opportunities across the United States. +

+
+
+ +
+ @if(!aiSearch){ +
+
    + + @if ((numberOfCommercial$ | async) > 0) { + + } + +
+
+ } @if(criteria && !aiSearch){ +
+
+
+ + +
+ +
+
+
+ +
+
+ + + @for (city of cities$ | async; track city.id) { @let state = city.type==='city'?city.content.state:''; @let separator = city.type==='city'?' - ':''; + {{ city.content.name }}{{ separator }}{{ state }} + } + +
+
+ @if (criteria.radius && !aiSearch){ +
+
+ + +
+ +
+
+
+ } +
+ @if( numberOfResults$){ + + }@else { + + } +
+
+ } +
+
+
+
+ + + +
+ diff --git a/bizmatch/src/app/pages/home/home.component.scss b/bizmatch/src/app/pages/home/home.component.scss index 501c303..ec18994 100644 --- a/bizmatch/src/app/pages/home/home.component.scss +++ b/bizmatch/src/app/pages/home/home.component.scss @@ -1,271 +1,252 @@ -.bg-cover-custom { - position: relative; - // Prioritize AVIF format (69KB) over JPG (26MB) - background-image: url('/assets/images/flags_bg.avif'); - background-size: cover; - background-position: center; - border-radius: 20px; - - // Fallback for browsers that don't support AVIF - @supports not (background-image: url('/assets/images/flags_bg.avif')) { - background-image: url('/assets/images/flags_bg.jpg'); - } - - // Add gradient overlay for better text contrast - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(180deg, rgba(0, 0, 0, 0.35) 0%, rgba(0, 0, 0, 0.15) 40%, rgba(0, 0, 0, 0.05) 70%, rgba(0, 0, 0, 0) 100%); - border-radius: 20px; - pointer-events: none; - z-index: 1; - } - - // Ensure content stays above overlay - > * { - position: relative; - z-index: 2; - } -} -select:not([size]) { - background-image: unset; -} -[type='text'], -[type='email'], -[type='url'], -[type='password'], -[type='number'], -[type='date'], -[type='datetime-local'], -[type='month'], -[type='search'], -[type='tel'], -[type='time'], -[type='week'], -[multiple], -textarea, -select { - border: unset; -} -.toggle-checkbox:checked { - right: 0; - border-color: rgb(125 211 252); -} -.toggle-checkbox:checked + .toggle-label { - background-color: rgb(125 211 252); -} -:host ::ng-deep .ng-select.ng-select-single .ng-select-container { - min-height: 52px; - border: none; - background-color: transparent; - .ng-value-container .ng-input { - top: 12px; - } - span.ng-arrow-wrapper { - display: none; - } -} -select { - color: #000; /* Standard-Textfarbe für das Dropdown */ - // background-color: #fff; /* Hintergrundfarbe für das Dropdown */ -} - -select option { - color: #000; /* Textfarbe für Dropdown-Optionen */ -} - -select.placeholder-selected { - color: #999; /* Farbe für den Platzhalter */ -} -input::placeholder { - color: #555; /* Dunkleres Grau */ - opacity: 1; /* Stellt sicher, dass die Deckkraft 100% ist */ -} - -/* Stellt sicher, dass die Optionen im Dropdown immer schwarz sind */ -select:focus option, -select:hover option { - color: #000 !important; -} -input[type='text'][name='aiSearchText'] { - padding: 14px; /* Innerer Abstand */ - font-size: 16px; /* Schriftgröße anpassen */ - box-sizing: border-box; /* Padding und Border in die Höhe und Breite einrechnen */ - height: 48px; -} - -// Enhanced Search Button Styling -.search-button { - position: relative; - overflow: hidden; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - - &:hover { - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4); - filter: brightness(1.05); - } - - &:active { - transform: scale(0.98); - } - - // Ripple effect - &::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - border-radius: 50%; - background: rgba(255, 255, 255, 0.3); - transform: translate(-50%, -50%); - transition: - width 0.6s, - height 0.6s; - pointer-events: none; - } - - &:active::after { - width: 300px; - height: 300px; - } -} - -// Tab Icon Styling -.tab-icon { - font-size: 1rem; - margin-right: 0.5rem; - transition: transform 0.2s ease; -} - -.tab-link { - transition: all 0.2s ease-in-out; - - &:hover .tab-icon { - transform: scale(1.15); - } -} - -// Input Field Hover Effects -select, -.ng-select { - transition: all 0.2s ease-in-out; - - &:hover { - background-color: rgba(243, 244, 246, 0.8); - } - - &:focus, - &:focus-within { - background-color: white; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); - } -} - -// Smooth tab transitions -.tab-content { - animation: fadeInUp 0.3s ease-out; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -// Trust section container - more prominent -.trust-section-container { - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); - transition: - box-shadow 0.3s ease, - transform 0.3s ease; - - &:hover { - box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12); - } -} - -// Trust badge animations - subtle lowkey style -.trust-badge { - transition: opacity 0.2s ease; - - &:hover { - opacity: 0.8; - } -} - -.trust-icon { - transition: - background-color 0.2s ease, - color 0.2s ease; -} - -.trust-badge:hover .trust-icon { - background-color: rgb(229, 231, 235); // gray-200 - color: rgb(75, 85, 99); // gray-600 -} - -// Stat counter animation - minimal -.stat-number { - transition: color 0.2s ease; - - &:hover { - color: rgb(55, 65, 81); // gray-700 darker - } -} - -// Search form container enhancement -.search-form-container { - transition: all 0.3s ease; - // KEIN backdrop-filter hier! - background-color: rgba(255, 255, 255, 0.95) !important; - border: 1px solid rgba(0, 0, 0, 0.1); // Dunklerer Rand für Kontrast - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); - - // Falls Firefox das Element "vergisst", erzwingen wir eine Ebene - transform: translateZ(0); - opacity: 1 !important; - visibility: visible !important; -} - -// Header button improvements -header { - a { - transition: all 0.2s ease-in-out; - - &.text-blue-600.border.border-blue-600 { - // Log In button - &:hover { - background-color: rgba(37, 99, 235, 0.05); - box-shadow: 0 2px 8px rgba(37, 99, 235, 0.15); - } - - &:active { - transform: scale(0.98); - } - } - - &.bg-blue-600 { - // Register button - &:hover { - background-color: rgb(29, 78, 216); - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); - filter: brightness(1.05); - } - - &:active { - transform: scale(0.98); - } - } - } -} + +select:not([size]) { + background-image: unset; +} +[type='text'], +[type='email'], +[type='url'], +[type='password'], +[type='number'], +[type='date'], +[type='datetime-local'], +[type='month'], +[type='search'], +[type='tel'], +[type='time'], +[type='week'], +[multiple], +textarea, +select { + border: unset; +} +.toggle-checkbox:checked { + right: 0; + border-color: rgb(125 211 252); +} +.toggle-checkbox:checked + .toggle-label { + background-color: rgb(125 211 252); +} +:host ::ng-deep .ng-select.ng-select-single .ng-select-container { + min-height: 52px; + border: none; + background-color: transparent; + .ng-value-container .ng-input { + top: 12px; + } + span.ng-arrow-wrapper { + display: none; + } +} +select { + color: #000; /* Standard-Textfarbe für das Dropdown */ + // background-color: #fff; /* Hintergrundfarbe für das Dropdown */ +} + +select option { + color: #000; /* Textfarbe für Dropdown-Optionen */ +} + +select.placeholder-selected { + color: #999; /* Farbe für den Platzhalter */ +} +input::placeholder { + color: #555; /* Dunkleres Grau */ + opacity: 1; /* Stellt sicher, dass die Deckkraft 100% ist */ +} + +/* Stellt sicher, dass die Optionen im Dropdown immer schwarz sind */ +select:focus option, +select:hover option { + color: #000 !important; +} +input[type='text'][name='aiSearchText'] { + padding: 14px; /* Innerer Abstand */ + font-size: 16px; /* Schriftgröße anpassen */ + box-sizing: border-box; /* Padding und Border in die Höhe und Breite einrechnen */ + height: 48px; +} + +// Enhanced Search Button Styling +.search-button { + position: relative; + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + + &:hover { + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4); + filter: brightness(1.05); + } + + &:active { + transform: scale(0.98); + } + + // Ripple effect + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + transform: translate(-50%, -50%); + transition: + width 0.6s, + height 0.6s; + pointer-events: none; + } + + &:active::after { + width: 300px; + height: 300px; + } +} + +// Tab Icon Styling +.tab-icon { + font-size: 1rem; + margin-right: 0.5rem; + transition: transform 0.2s ease; +} + +.tab-link { + transition: all 0.2s ease-in-out; + + &:hover .tab-icon { + transform: scale(1.15); + } +} + +// Input Field Hover Effects +select, +.ng-select { + transition: all 0.2s ease-in-out; + + &:hover { + background-color: rgba(243, 244, 246, 0.8); + } + + &:focus, + &:focus-within { + background-color: white; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + } +} + +// Smooth tab transitions +.tab-content { + animation: fadeInUp 0.3s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +// Trust section container - more prominent +.trust-section-container { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); + transition: + box-shadow 0.3s ease, + transform 0.3s ease; + + &:hover { + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12); + } +} + +// Trust badge animations - subtle lowkey style +.trust-badge { + transition: opacity 0.2s ease; + + &:hover { + opacity: 0.8; + } +} + +.trust-icon { + transition: + background-color 0.2s ease, + color 0.2s ease; +} + +.trust-badge:hover .trust-icon { + background-color: rgb(229, 231, 235); // gray-200 + color: rgb(75, 85, 99); // gray-600 +} + +// Stat counter animation - minimal +.stat-number { + transition: color 0.2s ease; + + &:hover { + color: rgb(55, 65, 81); // gray-700 darker + } +} + +// Search form container enhancement +.search-form-container { + transition: all 0.3s ease; + // KEIN backdrop-filter hier! + background-color: rgba(255, 255, 255, 0.95) !important; + border: 1px solid rgba(0, 0, 0, 0.1); // Dunklerer Rand für Kontrast + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + + // Falls Firefox das Element "vergisst", erzwingen wir eine Ebene + transform: translateZ(0); + opacity: 1 !important; + visibility: visible !important; +} + +// Header button improvements +header { + a { + transition: all 0.2s ease-in-out; + + &.text-blue-600.border.border-blue-600 { + // Log In button + &:hover { + background-color: rgba(37, 99, 235, 0.05); + box-shadow: 0 2px 8px rgba(37, 99, 235, 0.15); + } + + &:active { + transform: scale(0.98); + } + } + + &.bg-blue-600 { + // Register button + &:hover { + background-color: rgb(29, 78, 216); + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); + filter: brightness(1.05); + } + + &:active { + transform: scale(0.98); + } + } + } +} + +// Screen reader only - visually hidden but accessible +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} diff --git a/bizmatch/src/app/pages/home/home.component.ts b/bizmatch/src/app/pages/home/home.component.ts index 481f3a4..23cfe17 100644 --- a/bizmatch/src/app/pages/home/home.component.ts +++ b/bizmatch/src/app/pages/home/home.component.ts @@ -1,345 +1,343 @@ -import { CommonModule } from '@angular/common'; -import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core'; -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 { 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'; -import { ModalService } from '../../components/search-modal/modal.service'; -import { TooltipComponent } from '../../components/tooltip/tooltip.component'; -import { AiService } from '../../services/ai.service'; -import { AuthService } from '../../services/auth.service'; -import { FilterStateService } from '../../services/filter-state.service'; -import { GeoService } from '../../services/geo.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 { UserService } from '../../services/user.service'; -import { map2User } from '../../utils/utils'; - -@UntilDestroy() -@Component({ - selector: 'app-home', - standalone: true, - imports: [CommonModule, FormsModule, RouterModule, NgSelectModule, FaqComponent], - templateUrl: './home.component.html', - styleUrl: './home.component.scss', -}) -export class HomeComponent { - placeholders: string[] = ['Property close to Houston less than 10M', 'Franchise business in Austin price less than 500K']; - activeTabAction: 'business' | 'commercialProperty' | 'broker' = 'business'; - type: string; - maxPrice: string; - minPrice: string; - criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria; - states = []; - isMenuOpen = false; - user: KeycloakUser; - prompt: string; - cities$: Observable; - cityLoading = false; - cityInput$ = new Subject(); - cityOrState = undefined; - numberOfResults$: Observable; - numberOfBroker$: Observable; - numberOfCommercial$: Observable; - aiSearch = false; - aiSearchText = ''; - aiSearchFailed = false; - loadingAi = false; - @ViewChild('aiSearchInput', { static: false }) searchInput!: ElementRef; - typingSpeed: number = 100; - pauseTime: number = 2000; - index: number = 0; - charIndex: number = 0; - typingInterval: any; - showInput: boolean = true; - tooltipTargetBeta = 'tooltipTargetBeta'; - - // FAQ data optimized for AEO (Answer Engine Optimization) and Featured Snippets - faqItems: FAQItem[] = [ - { - question: 'How do I buy a business on BizMatch?', - answer: '

Buying a business on BizMatch involves 6 simple steps:

  1. Browse Listings: Search our marketplace using filters for industry, location, and price range
  2. Review Details: Examine financial information, business operations, and growth potential
  3. Contact Seller: Reach out directly through our secure messaging platform
  4. Due Diligence: Review financial statements, contracts, and legal documents
  5. Negotiate Terms: Work with the seller to agree on price and transition details
  6. Close Deal: Complete the purchase with legal and financial advisors

We recommend working with experienced business brokers and conducting thorough due diligence before making any purchase.

' - }, - { - question: 'How much does it cost to list a business for sale?', - answer: '

BizMatch offers flexible pricing options:

  • Free Basic Listing: Post your business with essential details at no cost
  • Premium Listing: Enhanced visibility with featured placement and priority support
  • Broker Packages: Professional tools for business brokers and agencies

Contact our team for detailed pricing information tailored to your specific needs.

' - }, - { - question: 'What types of businesses can I find on BizMatch?', - answer: '

BizMatch features businesses across all major industries:

  • Food & Hospitality: Restaurants, cafes, bars, hotels, catering services
  • Retail: Stores, boutiques, online shops, franchises
  • Service Businesses: Consulting firms, cleaning services, healthcare practices
  • Manufacturing: Production facilities, distribution centers, warehouses
  • E-commerce: Online businesses, digital products, subscription services
  • Commercial Real Estate: Office buildings, retail spaces, industrial properties

Our marketplace serves all business sizes from small local operations to large enterprises across the United States.

' - }, - { - question: 'How do I know if a business listing is legitimate?', - answer: '

Yes, BizMatch verifies all listings. Here\'s how we ensure legitimacy:

  1. Seller Verification: All users must verify their identity and contact information
  2. Listing Review: Our team reviews each listing for completeness and accuracy
  3. Documentation Check: We verify business registration and ownership documents
  4. Transparent Communication: All conversations are logged through our secure platform

Additional steps you should take:

  • Review financial statements and tax returns
  • Visit the business location in person
  • Consult with legal and financial advisors
  • Work with licensed business brokers when appropriate
  • Conduct background checks on sellers
' - }, - { - question: 'Can I sell commercial property on BizMatch?', - answer: '

Yes! BizMatch is a full-service marketplace for both businesses and commercial real estate.

Property types you can list:

  • Office buildings and professional spaces
  • Retail locations and shopping centers
  • Warehouses and distribution facilities
  • Industrial properties and manufacturing plants
  • Mixed-use developments
  • Land for commercial development

Our platform connects you with qualified buyers, investors, and commercial real estate professionals actively searching for investment opportunities.

' - }, - { - question: 'What information should I include when listing my business?', - answer: '

A complete listing should include these essential details:

  1. Financial Information: Asking price, annual revenue, cash flow, profit margins
  2. Business Operations: Years established, number of employees, hours of operation
  3. Description: Detailed overview of products/services, customer base, competitive advantages
  4. Industry Category: Specific business type and market segment
  5. Location Details: City, state, demographic information
  6. Assets Included: Equipment, inventory, real estate, intellectual property
  7. Visual Content: High-quality photos of business premises and operations
  8. Growth Potential: Expansion opportunities and market trends

Pro tip: The more detailed and transparent your listing, the more interest it will generate from serious, qualified buyers.

' - }, - { - question: 'How long does it take to sell a business?', - answer: '

Most businesses sell within 6 to 12 months. The timeline varies based on several factors:

Factors that speed up sales:

  • Realistic pricing based on professional valuation
  • Complete and organized financial documentation
  • Strong business performance and growth trends
  • Attractive location and market conditions
  • Experienced business broker representation
  • Flexible seller terms and financing options

Timeline breakdown:

  1. Months 1-2: Preparation and listing creation
  2. Months 3-6: Marketing and buyer qualification
  3. Months 7-10: Negotiations and due diligence
  4. Months 11-12: Closing and transition
' - }, - { - question: 'What is business valuation and why is it important?', - answer: '

Business valuation is the process of determining the economic worth of a company. It calculates the fair market value based on financial performance, assets, and market conditions.

Why valuation matters:

  • Realistic Pricing: Attracts serious buyers and prevents extended time on market
  • Negotiation Power: Provides data-driven justification for asking price
  • Buyer Confidence: Professional valuations increase trust and credibility
  • Financing Approval: Banks require valuations for business acquisition loans

Valuation methods include:

  1. Asset-Based: Total value of business assets minus liabilities
  2. Income-Based: Projected future earnings and cash flow
  3. Market-Based: Comparison to similar business sales
  4. Multiple of Earnings: Revenue or profit multiplied by industry-standard factor
' - }, - { - question: 'Do I need a business broker to buy or sell a business?', - answer: '

No, but brokers are highly recommended. You can conduct transactions directly through BizMatch, but professional brokers provide significant advantages:

Benefits of using a business broker:

  • Expert Valuation: Accurate pricing based on market data and analysis
  • Marketing Expertise: Professional listing creation and buyer outreach
  • Qualified Buyers: Pre-screening to ensure financial capability and serious interest
  • Negotiation Skills: Experience handling complex deal structures and terms
  • Confidentiality: Protect sensitive information during the sales process
  • Legal Compliance: Navigate regulations, contracts, and disclosures
  • Time Savings: Handle paperwork, communications, and coordination

BizMatch connects you with licensed brokers in your area, or you can manage the transaction yourself using our secure platform and resources.

' - }, - { - question: 'What financing options are available for buying a business?', - answer: '

Business buyers have multiple financing options:

  1. SBA 7(a) Loans: Government-backed loans with favorable terms
    • Down payment as low as 10%
    • Loan amounts up to $5 million
    • Competitive interest rates
    • Terms up to 10-25 years
  2. Conventional Bank Financing: Traditional business acquisition loans
    • Typically require 20-30% down payment
    • Based on creditworthiness and business performance
  3. Seller Financing: Owner provides loan to buyer
    • More flexible terms and requirements
    • Often combined with other financing
    • Typically 10-30% of purchase price
  4. Investor Partnerships: Equity financing from partners
    • Shared ownership and profits
    • No personal debt obligation
  5. Personal Savings: Self-funded purchase
    • No interest or loan payments
    • Full ownership from day one

Most buyers use a combination of these options to structure the optimal deal for their situation.

' - } - ]; - - constructor( - private router: Router, - private modalService: ModalService, - private searchService: SearchService, - private activatedRoute: ActivatedRoute, - public selectOptions: SelectOptionsService, - private geoService: GeoService, - public cdRef: ChangeDetectorRef, - private listingService: ListingsService, - private userService: UserService, - private aiService: AiService, - private authService: AuthService, - private filterStateService: FilterStateService, - private seoService: SeoService, - ) { } - - async ngOnInit() { - // Flowbite is now initialized once in AppComponent - - // Set SEO meta tags for home page - this.seoService.updateMetaTags({ - title: 'BizMatch - Buy & Sell Businesses and Commercial Properties', - description: 'Find profitable businesses for sale, commercial real estate, and franchise opportunities across the United States. Browse thousands of listings from verified sellers and brokers.', - keywords: 'business for sale, businesses for sale, buy business, sell business, commercial property, commercial real estate, franchise opportunities, business broker, business marketplace', - type: 'website' - }); - - // Add Organization schema for brand identity and FAQ schema for AEO - const organizationSchema = this.seoService.generateOrganizationSchema(); - const faqSchema = this.seoService.generateFAQPageSchema( - this.faqItems.map(item => ({ - question: item.question, - answer: item.answer - })) - ); - - // Add HowTo schema for buying a business - const howToSchema = this.seoService.generateHowToSchema({ - name: 'How to Buy a Business on BizMatch', - description: 'Step-by-step guide to finding and purchasing your ideal business through BizMatch marketplace', - totalTime: 'PT45M', - steps: [ - { - name: 'Browse Business Listings', - text: 'Search through thousands of verified business listings using our advanced filters. Filter by industry, location, price range, revenue, and more to find businesses that match your criteria.' - }, - { - name: 'Review Business Details', - text: 'Examine the business financials, including annual revenue, cash flow, asking price, and years established. Read the detailed business description and view photos of the operation.' - }, - { - name: 'Contact the Seller', - text: 'Use our secure messaging system to contact the seller or business broker directly. Request additional information, financial documents, or schedule a site visit to see the business in person.' - }, - { - name: 'Conduct Due Diligence', - text: 'Review all financial statements, tax returns, lease agreements, and legal documents. Verify the business information, inspect the physical location, and consult with legal and financial advisors.' - }, - { - name: 'Make an Offer', - text: 'Submit a formal offer based on your valuation and due diligence findings. Negotiate terms including purchase price, payment structure, transition period, and any contingencies.' - }, - { - name: 'Close the Transaction', - text: 'Work with attorneys and escrow services to finalize all legal documents, transfer ownership, and complete the purchase. The seller will transfer assets, train you on operations, and help with the transition.' - } - ] - }); - - // Add SearchBox schema for Sitelinks Search - const searchBoxSchema = this.seoService.generateSearchBoxSchema(); - - this.seoService.injectMultipleSchemas([organizationSchema, faqSchema, howToSchema, searchBoxSchema]); - - // Clear all filters and sort options on initial load - this.filterStateService.resetCriteria('businessListings'); - this.filterStateService.resetCriteria('commercialPropertyListings'); - this.filterStateService.resetCriteria('brokerListings'); - this.filterStateService.updateSortBy('businessListings', null); - this.filterStateService.updateSortBy('commercialPropertyListings', null); - this.filterStateService.updateSortBy('brokerListings', null); - - // Initialize criteria for the default tab - this.criteria = this.filterStateService.getCriteria('businessListings'); - - this.numberOfBroker$ = this.userService.getNumberOfBroker(this.filterStateService.getCriteria('brokerListings') as UserListingCriteria); - this.numberOfCommercial$ = this.listingService.getNumberOfListings('commercialProperty'); - const token = await this.authService.getToken(); - this.user = map2User(token); - this.loadCities(); - this.setTotalNumberOfResults(); - } - - changeTab(tabname: 'business' | 'commercialProperty' | 'broker') { - this.activeTabAction = tabname; - this.cityOrState = null; - const tabToListingType = { - business: 'businessListings', - commercialProperty: 'commercialPropertyListings', - broker: 'brokerListings', - }; - this.criteria = this.filterStateService.getCriteria(tabToListingType[tabname] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'); - this.setTotalNumberOfResults(); - } - - search() { - this.router.navigate([`${this.activeTabAction}Listings`]); - } - - toggleMenu() { - this.isMenuOpen = !this.isMenuOpen; - } - - onTypesChange(value) { - const tabToListingType = { - business: 'businessListings', - commercialProperty: 'commercialPropertyListings', - broker: 'brokerListings', - }; - const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; - this.filterStateService.updateCriteria(listingType, { types: value === '' ? [] : [value] }); - this.criteria = this.filterStateService.getCriteria(listingType); - this.setTotalNumberOfResults(); - } - - onRadiusChange(value) { - const tabToListingType = { - business: 'businessListings', - commercialProperty: 'commercialPropertyListings', - broker: 'brokerListings', - }; - const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; - this.filterStateService.updateCriteria(listingType, { radius: value === 'null' ? null : parseInt(value) }); - this.criteria = this.filterStateService.getCriteria(listingType); - this.setTotalNumberOfResults(); - } - - async openModal() { - const tabToListingType = { - business: 'businessListings', - commercialProperty: 'commercialPropertyListings', - broker: 'brokerListings', - }; - const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; - const accepted = await this.modalService.showModal(this.criteria); - if (accepted) { - this.router.navigate([`${this.activeTabAction}Listings`]); - } - } - - private loadCities() { - this.cities$ = concat( - of([]), - this.cityInput$.pipe( - distinctUntilChanged(), - tap(() => (this.cityLoading = true)), - switchMap(term => - this.geoService.findCitiesAndStatesStartingWith(term).pipe( - catchError(() => of([])), - tap(() => (this.cityLoading = false)), - ), - ), - ), - ); - } - - trackByFn(item: GeoResult) { - return item.id; - } - - setCityOrState(cityOrState: CityAndStateResult) { - const tabToListingType = { - business: 'businessListings', - commercialProperty: 'commercialPropertyListings', - broker: 'brokerListings', - }; - const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; - - if (cityOrState) { - if (cityOrState.type === 'state') { - this.filterStateService.updateCriteria(listingType, { state: cityOrState.content.state_code, city: null, radius: null, searchType: 'exact' }); - } else { - this.filterStateService.updateCriteria(listingType, { - city: cityOrState.content as GeoResult, - state: cityOrState.content.state, - searchType: 'radius', - radius: 20, - }); - } - } else { - this.filterStateService.updateCriteria(listingType, { state: null, city: null, radius: null, searchType: 'exact' }); - } - this.criteria = this.filterStateService.getCriteria(listingType); - this.setTotalNumberOfResults(); - } - - getTypes() { - if (this.criteria.criteriaType === 'businessListings') { - return this.selectOptions.typesOfBusiness; - } else if (this.criteria.criteriaType === 'commercialPropertyListings') { - return this.selectOptions.typesOfCommercialProperty; - } else { - return this.selectOptions.customerSubTypes; - } - } - - getPlaceholderLabel() { - if (this.criteria.criteriaType === 'businessListings') { - return 'Business Type'; - } else if (this.criteria.criteriaType === 'commercialPropertyListings') { - return 'Property Type'; - } else { - return 'Professional Type'; - } - } - - setTotalNumberOfResults() { - if (this.criteria) { - console.log(`Getting total number of results for ${this.criteria.criteriaType}`); - const tabToListingType = { - business: 'businessListings', - commercialProperty: 'commercialPropertyListings', - broker: 'brokerListings', - }; - const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; - - if (this.criteria.criteriaType === 'businessListings' || this.criteria.criteriaType === 'commercialPropertyListings') { - this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria.criteriaType === 'businessListings' ? 'business' : 'commercialProperty'); - } else if (this.criteria.criteriaType === 'brokerListings') { - this.numberOfResults$ = this.userService.getNumberOfBroker(this.filterStateService.getCriteria('brokerListings') as UserListingCriteria); - } else { - this.numberOfResults$ = of(); - } - } - } - - ngOnDestroy(): void { - clearTimeout(this.typingInterval); - } -} +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core'; +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 { 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'; +import { ModalService } from '../../components/search-modal/modal.service'; +import { TooltipComponent } from '../../components/tooltip/tooltip.component'; +import { AiService } from '../../services/ai.service'; +import { AuthService } from '../../services/auth.service'; +import { FilterStateService } from '../../services/filter-state.service'; +import { GeoService } from '../../services/geo.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 { UserService } from '../../services/user.service'; +import { map2User } from '../../utils/utils'; + +@UntilDestroy() +@Component({ + selector: 'app-home', + standalone: true, + imports: [CommonModule, FormsModule, RouterModule, NgSelectModule, FaqComponent], + templateUrl: './home.component.html', + styleUrl: './home.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HomeComponent { + placeholders: string[] = ['Property close to Houston less than 10M', 'Franchise business in Austin price less than 500K']; + activeTabAction: 'business' | 'commercialProperty' | 'broker' = 'business'; + type: string; + maxPrice: string; + minPrice: string; + criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria; + states = []; + isMenuOpen = false; + user: KeycloakUser; + prompt: string; + cities$: Observable; + cityLoading = false; + cityInput$ = new Subject(); + cityOrState = undefined; + numberOfResults$: Observable; + numberOfBroker$: Observable; + numberOfCommercial$: Observable; + aiSearch = false; + aiSearchText = ''; + aiSearchFailed = false; + loadingAi = false; + @ViewChild('aiSearchInput', { static: false }) searchInput!: ElementRef; + typingSpeed: number = 100; + pauseTime: number = 2000; + index: number = 0; + charIndex: number = 0; + typingInterval: any; + showInput: boolean = true; + tooltipTargetBeta = 'tooltipTargetBeta'; + + // FAQ data optimized for AEO (Answer Engine Optimization) and Featured Snippets + faqItems: FAQItem[] = [ + { + question: 'How do I buy a business on BizMatch?', + answer: '

Buying a business on BizMatch involves 6 simple steps:

  1. Browse Listings: Search our marketplace using filters for industry, location, and price range
  2. Review Details: Examine financial information, business operations, and growth potential
  3. Contact Seller: Reach out directly through our secure messaging platform
  4. Due Diligence: Review financial statements, contracts, and legal documents
  5. Negotiate Terms: Work with the seller to agree on price and transition details
  6. Close Deal: Complete the purchase with legal and financial advisors

We recommend working with experienced business brokers and conducting thorough due diligence before making any purchase.

' + }, + { + question: 'How much does it cost to list a business for sale?', + answer: '

BizMatch offers flexible pricing options:

  • Free Basic Listing: Post your business with essential details at no cost
  • Premium Listing: Enhanced visibility with featured placement and priority support
  • Broker Packages: Professional tools for business brokers and agencies

Contact our team for detailed pricing information tailored to your specific needs.

' + }, + { + question: 'What types of businesses can I find on BizMatch?', + answer: '

BizMatch features businesses across all major industries:

  • Food & Hospitality: Restaurants, cafes, bars, hotels, catering services
  • Retail: Stores, boutiques, online shops, franchises
  • Service Businesses: Consulting firms, cleaning services, healthcare practices
  • Manufacturing: Production facilities, distribution centers, warehouses
  • E-commerce: Online businesses, digital products, subscription services
  • Commercial Real Estate: Office buildings, retail spaces, industrial properties

Our marketplace serves all business sizes from small local operations to large enterprises across the United States.

' + }, + { + question: 'How do I know if a business listing is legitimate?', + answer: '

Yes, BizMatch verifies all listings. Here\'s how we ensure legitimacy:

  1. Seller Verification: All users must verify their identity and contact information
  2. Listing Review: Our team reviews each listing for completeness and accuracy
  3. Documentation Check: We verify business registration and ownership documents
  4. Transparent Communication: All conversations are logged through our secure platform

Additional steps you should take:

  • Review financial statements and tax returns
  • Visit the business location in person
  • Consult with legal and financial advisors
  • Work with licensed business brokers when appropriate
  • Conduct background checks on sellers
' + }, + { + question: 'Can I sell commercial property on BizMatch?', + answer: '

Yes! BizMatch is a full-service marketplace for both businesses and commercial real estate.

Property types you can list:

  • Office buildings and professional spaces
  • Retail locations and shopping centers
  • Warehouses and distribution facilities
  • Industrial properties and manufacturing plants
  • Mixed-use developments
  • Land for commercial development

Our platform connects you with qualified buyers, investors, and commercial real estate professionals actively searching for investment opportunities.

' + }, + { + question: 'What information should I include when listing my business?', + answer: '

A complete listing should include these essential details:

  1. Financial Information: Asking price, annual revenue, cash flow, profit margins
  2. Business Operations: Years established, number of employees, hours of operation
  3. Description: Detailed overview of products/services, customer base, competitive advantages
  4. Industry Category: Specific business type and market segment
  5. Location Details: City, state, demographic information
  6. Assets Included: Equipment, inventory, real estate, intellectual property
  7. Visual Content: High-quality photos of business premises and operations
  8. Growth Potential: Expansion opportunities and market trends

Pro tip: The more detailed and transparent your listing, the more interest it will generate from serious, qualified buyers.

' + }, + { + question: 'How long does it take to sell a business?', + answer: '

Most businesses sell within 6 to 12 months. The timeline varies based on several factors:

Factors that speed up sales:

  • Realistic pricing based on professional valuation
  • Complete and organized financial documentation
  • Strong business performance and growth trends
  • Attractive location and market conditions
  • Experienced business broker representation
  • Flexible seller terms and financing options

Timeline breakdown:

  1. Months 1-2: Preparation and listing creation
  2. Months 3-6: Marketing and buyer qualification
  3. Months 7-10: Negotiations and due diligence
  4. Months 11-12: Closing and transition
' + }, + { + question: 'What is business valuation and why is it important?', + answer: '

Business valuation is the process of determining the economic worth of a company. It calculates the fair market value based on financial performance, assets, and market conditions.

Why valuation matters:

  • Realistic Pricing: Attracts serious buyers and prevents extended time on market
  • Negotiation Power: Provides data-driven justification for asking price
  • Buyer Confidence: Professional valuations increase trust and credibility
  • Financing Approval: Banks require valuations for business acquisition loans

Valuation methods include:

  1. Asset-Based: Total value of business assets minus liabilities
  2. Income-Based: Projected future earnings and cash flow
  3. Market-Based: Comparison to similar business sales
  4. Multiple of Earnings: Revenue or profit multiplied by industry-standard factor
' + }, + { + question: 'Do I need a business broker to buy or sell a business?', + answer: '

No, but brokers are highly recommended. You can conduct transactions directly through BizMatch, but professional brokers provide significant advantages:

Benefits of using a business broker:

  • Expert Valuation: Accurate pricing based on market data and analysis
  • Marketing Expertise: Professional listing creation and buyer outreach
  • Qualified Buyers: Pre-screening to ensure financial capability and serious interest
  • Negotiation Skills: Experience handling complex deal structures and terms
  • Confidentiality: Protect sensitive information during the sales process
  • Legal Compliance: Navigate regulations, contracts, and disclosures
  • Time Savings: Handle paperwork, communications, and coordination

BizMatch connects you with licensed brokers in your area, or you can manage the transaction yourself using our secure platform and resources.

' + }, + { + question: 'What financing options are available for buying a business?', + answer: '

Business buyers have multiple financing options:

  1. SBA 7(a) Loans: Government-backed loans with favorable terms
    • Down payment as low as 10%
    • Loan amounts up to $5 million
    • Competitive interest rates
    • Terms up to 10-25 years
  2. Conventional Bank Financing: Traditional business acquisition loans
    • Typically require 20-30% down payment
    • Based on creditworthiness and business performance
  3. Seller Financing: Owner provides loan to buyer
    • More flexible terms and requirements
    • Often combined with other financing
    • Typically 10-30% of purchase price
  4. Investor Partnerships: Equity financing from partners
    • Shared ownership and profits
    • No personal debt obligation
  5. Personal Savings: Self-funded purchase
    • No interest or loan payments
    • Full ownership from day one

Most buyers use a combination of these options to structure the optimal deal for their situation.

' + } + ]; + + constructor( + private router: Router, + private modalService: ModalService, + private searchService: SearchService, + private activatedRoute: ActivatedRoute, + public selectOptions: SelectOptionsService, + private geoService: GeoService, + public cdRef: ChangeDetectorRef, + private listingService: ListingsService, + private userService: UserService, + private aiService: AiService, + private authService: AuthService, + private filterStateService: FilterStateService, + private seoService: SeoService, + ) { } + + async ngOnInit() { + // Flowbite is now initialized once in AppComponent + + // Set SEO meta tags for home page + this.seoService.updateMetaTags({ + title: 'BizMatch - Buy & Sell Businesses and Commercial Properties', + description: 'Find profitable businesses for sale, commercial real estate, and franchise opportunities. Browse thousands of verified listings across the US.', + keywords: 'business for sale, businesses for sale, buy business, sell business, commercial property, commercial real estate, franchise opportunities, business broker, business marketplace', + type: 'website' + }); + + // Add Organization schema for brand identity + // NOTE: FAQ schema removed because FAQ section is hidden (violates Google's visible content requirement) + // FAQ content is preserved in component for future use when FAQ section is made visible + const organizationSchema = this.seoService.generateOrganizationSchema(); + + // Add HowTo schema for buying a business + const howToSchema = this.seoService.generateHowToSchema({ + name: 'How to Buy a Business on BizMatch', + description: 'Step-by-step guide to finding and purchasing your ideal business through BizMatch marketplace', + totalTime: 'PT45M', + steps: [ + { + name: 'Browse Business Listings', + text: 'Search through thousands of verified business listings using our advanced filters. Filter by industry, location, price range, revenue, and more to find businesses that match your criteria.' + }, + { + name: 'Review Business Details', + text: 'Examine the business financials, including annual revenue, cash flow, asking price, and years established. Read the detailed business description and view photos of the operation.' + }, + { + name: 'Contact the Seller', + text: 'Use our secure messaging system to contact the seller or business broker directly. Request additional information, financial documents, or schedule a site visit to see the business in person.' + }, + { + name: 'Conduct Due Diligence', + text: 'Review all financial statements, tax returns, lease agreements, and legal documents. Verify the business information, inspect the physical location, and consult with legal and financial advisors.' + }, + { + name: 'Make an Offer', + text: 'Submit a formal offer based on your valuation and due diligence findings. Negotiate terms including purchase price, payment structure, transition period, and any contingencies.' + }, + { + name: 'Close the Transaction', + text: 'Work with attorneys and escrow services to finalize all legal documents, transfer ownership, and complete the purchase. The seller will transfer assets, train you on operations, and help with the transition.' + } + ] + }); + + // Add SearchBox schema for Sitelinks Search + const searchBoxSchema = this.seoService.generateSearchBoxSchema(); + + // Inject schemas (FAQ schema excluded - content not visible to users) + this.seoService.injectMultipleSchemas([organizationSchema, howToSchema, searchBoxSchema]); + + // Clear all filters and sort options on initial load + this.filterStateService.resetCriteria('businessListings'); + this.filterStateService.resetCriteria('commercialPropertyListings'); + this.filterStateService.resetCriteria('brokerListings'); + this.filterStateService.updateSortBy('businessListings', null); + this.filterStateService.updateSortBy('commercialPropertyListings', null); + this.filterStateService.updateSortBy('brokerListings', null); + + // Initialize criteria for the default tab + this.criteria = this.filterStateService.getCriteria('businessListings'); + + this.numberOfBroker$ = this.userService.getNumberOfBroker(this.filterStateService.getCriteria('brokerListings') as UserListingCriteria); + this.numberOfCommercial$ = this.listingService.getNumberOfListings('commercialProperty'); + const token = await this.authService.getToken(); + this.user = map2User(token); + this.loadCities(); + this.setTotalNumberOfResults(); + } + + changeTab(tabname: 'business' | 'commercialProperty' | 'broker') { + this.activeTabAction = tabname; + this.cityOrState = null; + const tabToListingType = { + business: 'businessListings', + commercialProperty: 'commercialPropertyListings', + broker: 'brokerListings', + }; + this.criteria = this.filterStateService.getCriteria(tabToListingType[tabname] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'); + this.setTotalNumberOfResults(); + } + + search() { + this.router.navigate([`${this.activeTabAction}Listings`]); + } + + toggleMenu() { + this.isMenuOpen = !this.isMenuOpen; + } + + onTypesChange(value) { + const tabToListingType = { + business: 'businessListings', + commercialProperty: 'commercialPropertyListings', + broker: 'brokerListings', + }; + const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; + this.filterStateService.updateCriteria(listingType, { types: value === '' ? [] : [value] }); + this.criteria = this.filterStateService.getCriteria(listingType); + this.setTotalNumberOfResults(); + } + + onRadiusChange(value) { + const tabToListingType = { + business: 'businessListings', + commercialProperty: 'commercialPropertyListings', + broker: 'brokerListings', + }; + const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; + this.filterStateService.updateCriteria(listingType, { radius: value === 'null' ? null : parseInt(value) }); + this.criteria = this.filterStateService.getCriteria(listingType); + this.setTotalNumberOfResults(); + } + + async openModal() { + const tabToListingType = { + business: 'businessListings', + commercialProperty: 'commercialPropertyListings', + broker: 'brokerListings', + }; + const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; + const accepted = await this.modalService.showModal(this.criteria); + if (accepted) { + this.router.navigate([`${this.activeTabAction}Listings`]); + } + } + + private loadCities() { + this.cities$ = concat( + of([]), + this.cityInput$.pipe( + distinctUntilChanged(), + tap(() => (this.cityLoading = true)), + switchMap(term => + this.geoService.findCitiesAndStatesStartingWith(term).pipe( + catchError(() => of([])), + tap(() => (this.cityLoading = false)), + ), + ), + ), + ); + } + + trackByFn(item: GeoResult) { + return item.id; + } + + setCityOrState(cityOrState: CityAndStateResult) { + const tabToListingType = { + business: 'businessListings', + commercialProperty: 'commercialPropertyListings', + broker: 'brokerListings', + }; + const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; + + if (cityOrState) { + if (cityOrState.type === 'state') { + this.filterStateService.updateCriteria(listingType, { state: cityOrState.content.state_code, city: null, radius: null, searchType: 'exact' }); + } else { + this.filterStateService.updateCriteria(listingType, { + city: cityOrState.content as GeoResult, + state: cityOrState.content.state, + searchType: 'radius', + radius: 20, + }); + } + } else { + this.filterStateService.updateCriteria(listingType, { state: null, city: null, radius: null, searchType: 'exact' }); + } + this.criteria = this.filterStateService.getCriteria(listingType); + this.setTotalNumberOfResults(); + } + + getTypes() { + if (this.criteria.criteriaType === 'businessListings') { + return this.selectOptions.typesOfBusiness; + } else if (this.criteria.criteriaType === 'commercialPropertyListings') { + return this.selectOptions.typesOfCommercialProperty; + } else { + return this.selectOptions.customerSubTypes; + } + } + + getPlaceholderLabel() { + if (this.criteria.criteriaType === 'businessListings') { + return 'Business Type'; + } else if (this.criteria.criteriaType === 'commercialPropertyListings') { + return 'Property Type'; + } else { + return 'Professional Type'; + } + } + + setTotalNumberOfResults() { + if (this.criteria) { + console.log(`Getting total number of results for ${this.criteria.criteriaType}`); + const tabToListingType = { + business: 'businessListings', + commercialProperty: 'commercialPropertyListings', + broker: 'brokerListings', + }; + const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; + + if (this.criteria.criteriaType === 'businessListings' || this.criteria.criteriaType === 'commercialPropertyListings') { + this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria.criteriaType === 'businessListings' ? 'business' : 'commercialProperty'); + } else if (this.criteria.criteriaType === 'brokerListings') { + this.numberOfResults$ = this.userService.getNumberOfBroker(this.filterStateService.getCriteria('brokerListings') as UserListingCriteria); + } else { + this.numberOfResults$ = of(); + } + } + } + + ngOnDestroy(): void { + clearTimeout(this.typingInterval); + } +} diff --git a/bizmatch/src/app/pages/legal/privacy-statement.component.html b/bizmatch/src/app/pages/legal/privacy-statement.component.html index b8a0d54..0815366 100644 --- a/bizmatch/src/app/pages/legal/privacy-statement.component.html +++ b/bizmatch/src/app/pages/legal/privacy-statement.component.html @@ -1,201 +1,201 @@ -
-
- -

Privacy Statement

- -
-
-
-
-

Privacy Policy

-

We are committed to protecting your privacy. We have established this statement as a testament to our commitment to your privacy.

-

This Privacy Policy relates to the use of any personal information you provide to us through this websites.

-

- By accepting the Privacy Policy during registration or the sending of an enquiry, you expressly consent to our collection, storage, use and disclosure of your personal information as described in this Privacy - Policy. -

-

- We may update our Privacy Policy from time to time. Our Privacy Policy was last updated in Febuary 2018 and is effective upon acceptance for new users. By continuing to use our websites or otherwise - continuing to deal with us, you accept this Privacy Policy. -

-

Collection of personal information

-

Anyone can browse our websites without revealing any personally identifiable information.

-

However, should you wish to contact a business for sale, a franchise opportunity or an intermediary, we will require you to provide some personal information.

-

Should you wish to advertise your services, your business (es) or your franchise opportunity, we will require you to provide some personal information.

-

By providing personal information, you are consenting to the transfer and storage of that information on our servers located in the United States.

-

We may collect and store the following personal information:

-

- Your name, email address, physical address, telephone numbers, and (depending on the service used), your business information, financial information, such as credit / payment card details;
- transactional information based on your activities on the site; information that you disclose in a forum on any of our websites, feedback, correspondence through our websites, and correspondence sent to - us;
- other information from your interaction with our websites, services, content and advertising, including computer and connection information, statistics on page views, traffic to and from the sites, ad data, - IP address and standard web log information;
- supplemental information from third parties (for example, if you incur a debt, we will generally conduct a credit check by obtaining additional information about you from a credit bureau, as permitted by law; - or if the information you provide cannot be verified,
- we may ask you to send us additional information, or to answer additional questions online to help verify your information). -

-

How we use your information

-

- The primary reason we collect your personal information is to improve the services we deliver to you through our website. By registering or sending an enquiry through our website, you agree that we may use - your personal information to:
- provide the services and customer support you request;
- connect you with relevant parties:
- If you are a buyer we will pass some or all of your details on to the seller / intermediary along with any message you have typed. This allows the seller to contact you in order to pursue a possible sale of a - business;
- If you are a seller / intermediary, we will disclose your details where you have given us permission to do so;
- resolve disputes, collect fees, and troubleshoot problems;
- prevent potentially prohibited or illegal activities, and enforce our Terms and Conditions;
- customize, measure and improve our services, conduct internal market research, provide content and advertising;
- tell you about other Biz-Match products and services, target marketing, send you service updates, and promotional offers based on your communication preferences. -

-

Our disclosure of your information

-

- We may disclose personal information to respond to legal requirements, enforce our policies, respond to claims that a listing or other content infringes the rights of others, or protect anyone's rights, - property, or safety. -

-

- We may also share your personal information with
- When you select to register an account as a business buyer, you provide your personal details and we will pass this on to a seller of a business or franchise when you request more information. -

-

- When you select to register an account as a business broker or seller on the site, we provide a public platform on which to establish your business profile. This profile consists of pertinent facts about your - business along with your personal information; namely, the contact information you provide to facilitate contact between you and other users' of the site. Direct email addresses and telephone numbers will not - be publicly displayed unless you specifically include it on your profile. -

-

- The information a user includes within the forums provided on the site is publicly available to other users' of the site. Please be aware that any personal information you elect to provide in a public forum - may be used to send you unsolicited messages; we are not responsible for the personal information a user elects to disclose within their public profile, or in the private communications that users' engage in - on the site. -

-

- We post testimonials on the site obtained from users'. These testimonials may include the name, city, state or region and business of the user. We obtain permission from our users' prior to posting their - testimonials on the site. We are not responsible for any personal information a user selects to include within their testimonial. -

-

- When you elect to email a friend about the site, or a particular business, we request the third party's email address to send this one time email. We do not share this information with any third parties for - their promotional purposes and only store the information to gauge the effectiveness of our referral program. -

-

We may share your personal information with our service providers where necessary. We employ the services of a payment processor to fulfil payment for services purchased on the site.

-

- We works with a number of partners or affiliates, where we provide marketing services for these companies. These third party agents collect your personal information to facilitate your service request and the - information submitted here is governed by their privacy policy. -

-

Masking Policy

-

- In some cases, where the third party agent collects your information, the affiliate portal may appear within a BizMatch.net frame. It is presented as a BizMatch.net page for a streamlined user interface - however the data collected on such pages is governed by the third party agent's privacy policy. -

-

Legal Disclosure

-

- In certain circumstances, we may be legally required to disclose information collected on the site to law enforcement, government agencies or other third parties. We reserve the right to disclose information - to our service providers and to law enforcement or government agencies where a formal request such as in response to a court order, subpoena or judicial proceeding is made. Where we believe in good faith that - disclosure of information is necessary to prevent imminent physical or financial harm, or loss, or in protecting against illegal activity on the site, we reserve to disclose information. -

-

- Should the company undergo the merger, acquisition or sale of some or all of its assets, your personal information may likely be a part of the transferred assets. In such an event, your personal information - on the site, would be governed by this privacy statement; any changes to the privacy practices governing your information as a result of transfer would be relayed to you by means of a prominent notice on the - Site, or by email. -

-

Using information from BizMatch.net website

-

- In certain cases, (where you are receiving contact details of buyers interested in your business opportunity or a business opportunity you represent), you must comply with data protection laws, and give other - users a chance to remove themselves from your database and a chance to review what information you have collected about them. -

-

You agree to use BizMatch.net user information only for:

-

- BizMatch.net transaction-related purposes that are not unsolicited commercial messages;
- using services offered through BizMatch.net, or
- other purposes that a user expressly chooses. -

-

Marketing

-

- We do not sell or rent your personal information to third parties for their marketing purposes without your explicit consent. Where you explicitly express your consent at the point of collection to receive - offers from third party partners or affiliates, we will communicate to you on their behalf. We will not pass your information on. -

-

- You will receive email marketing communications from us throughout the duration of your relationship with our websites. If you do not wish to receive marketing communications from us you may unsubscribe and / - or change your preferences at any time by following instructions included within a communication or emailing Customer Services. -

-

If you have an account with one of our websites you can also log in and click the email preferences link to unsubscribe and / or change your preferences.

-

- Please note that we reserve the right to send all website users notifications and administrative emails where necessary which are considered a part of the service. Given that these messages aren't promotional - in nature, you will be unable to opt-out of them. -

-

Cookies

-

- A cookie is a small text file written to your hard drive that contains information about you. Cookies do not contain any personal information about users. Once you close your browser or log out of the - website, the cookie simply terminates. We use cookies so that we can personalise your experience of our websites. -

-

- If you set up your browser to reject the cookie, you may still use the website however; doing so may interfere with your use of some aspects of our websites. Some of our business partners use cookies on our - site (for example, advertisers). We have no access to or control over these cookies. -

-

For more information about how BizMatch.net uses cookies please read our Cookie Policy.

-

Spam, spyware or spoofing

-

- We and our users do not tolerate spam. Make sure to set your email preferences so we can communicate with you, as you prefer. Please add us to your safe senders list. To report spam or spoof emails, please - contact us using the contact information provided in the Contact Us section of this privacy statement. -

-

- You may not use our communication tools to send spam or otherwise send content that would breach our Terms and Conditions. We automatically scan and may manually filter messages to check for spam, viruses, - phishing attacks and other malicious activity or illegal or prohibited content. We may also store these messages for back up purposes only. -

-

- If you send an email to an email address that is not registered in our community, we do not permanently store that email or use that email address for any marketing purpose. We do not rent or sell these email - addresses. -

-

Account protection

-

- Your password is the key to your account. Make sure this is stored safely. Use unique numbers, letters and special characters, and do not disclose your password to anyone. If you do share your password or - your personal information with others, remember that you are responsible for all actions taken in the name of your account. If you lose control of your password, you may lose substantial control over your - personal information and may be subject to legally binding actions taken on your behalf. Therefore, if your password has been compromised for any reason, you should immediately notify us and change your - password. -

-

Accessing, reviewing and changing your personal information

-

You can view and amend your personal information at any time by logging in to your account online. You must promptly update your personal information if it changes or is inaccurate.

-

If at any time you wish to close your account, please contact Customer Services and instruct us to do so. We will process your request as soon as we can.

-

You may also contact us at any time to find out what information we hold about you, what we do with it and ask us to update it for you.

-

- We do retain personal information from closed accounts to comply with law, prevent fraud, collect any fees owed, resolve disputes, troubleshoot problems, assist with any investigations, enforce our Terms and - Conditions, and take other actions otherwise permitted by law. -

-

Security

-

- Your information is stored on our servers located in the USA. We treat data as an asset that must be protected and use a variety of tools (encryption, passwords, physical security, etc.) to protect your - personal information against unauthorized access and disclosure. However, no method of security is 100% effective and while we take every measure to protect your personal information, we make no guarantees of - its absolute security. -

-

We employ the use of SSL encryption during the transmission of sensitive data across our websites.

-

Third parties

-

- Except as otherwise expressly included in this Privacy Policy, this document addresses only the use and disclosure of information we collect from you. If you disclose your information to others, whether they - are buyers or sellers on our websites or other sites throughout the internet, different rules may apply to their use or disclosure of the information you disclose to them. Dynamis does not control the privacy - policies of third parties, and you are subject to the privacy policies of those third parties where applicable. -

-

We encourage you to ask questions before you disclose your personal information to others.

-

General

-

- We may change this Privacy Policy from time to time as we add new products and applications, as we improve our current offerings, and as technologies and laws change. You can determine when this Privacy - Policy was last revised by referring to the "Last Updated" legend at the top of this page. -

-

- Any changes will become effective upon our posting of the revised Privacy Policy on our affected websites. We will provide notice to you if these changes are material and, where required by applicable law, we - will obtain your consent. This notice may be provided by email, by posting notice of the changes on our affected websites or by other means, consistent with applicable laws. -

-

Contact Us

-

- If you have any questions or comments about our privacy policy, and you can't find the answer to your question on our help pages, please contact us using this form or email support@bizmatch.net, or write - to us at BizMatch, 715 S. Tanahua, Corpus Christi, TX 78401.) -

-
-
-
-
-
-
+
+
+ +

Privacy Statement

+ +
+
+
+
+

Privacy Policy

+

We are committed to protecting your privacy. We have established this statement as a testament to our commitment to your privacy.

+

This Privacy Policy relates to the use of any personal information you provide to us through this websites.

+

+ By accepting the Privacy Policy during registration or the sending of an enquiry, you expressly consent to our collection, storage, use and disclosure of your personal information as described in this Privacy + Policy. +

+

+ We may update our Privacy Policy from time to time. Our Privacy Policy was last updated in February 2018 and is effective upon acceptance for new users. By continuing to use our websites or otherwise + continuing to deal with us, you accept this Privacy Policy. +

+

Collection of personal information

+

Anyone can browse our websites without revealing any personally identifiable information.

+

However, should you wish to contact a business for sale, a franchise opportunity or an intermediary, we will require you to provide some personal information.

+

Should you wish to advertise your services, your business (es) or your franchise opportunity, we will require you to provide some personal information.

+

By providing personal information, you are consenting to the transfer and storage of that information on our servers located in the United States.

+

We may collect and store the following personal information:

+

+ Your name, email address, physical address, telephone numbers, and (depending on the service used), your business information, financial information, such as credit / payment card details;
+ transactional information based on your activities on the site; information that you disclose in a forum on any of our websites, feedback, correspondence through our websites, and correspondence sent to + us;
+ other information from your interaction with our websites, services, content and advertising, including computer and connection information, statistics on page views, traffic to and from the sites, ad data, + IP address and standard web log information;
+ supplemental information from third parties (for example, if you incur a debt, we will generally conduct a credit check by obtaining additional information about you from a credit bureau, as permitted by law; + or if the information you provide cannot be verified,
+ we may ask you to send us additional information, or to answer additional questions online to help verify your information). +

+

How we use your information

+

+ The primary reason we collect your personal information is to improve the services we deliver to you through our website. By registering or sending an enquiry through our website, you agree that we may use + your personal information to:
+ provide the services and customer support you request;
+ connect you with relevant parties:
+ If you are a buyer we will pass some or all of your details on to the seller / intermediary along with any message you have typed. This allows the seller to contact you in order to pursue a possible sale of a + business;
+ If you are a seller / intermediary, we will disclose your details where you have given us permission to do so;
+ resolve disputes, collect fees, and troubleshoot problems;
+ prevent potentially prohibited or illegal activities, and enforce our Terms and Conditions;
+ customize, measure and improve our services, conduct internal market research, provide content and advertising;
+ tell you about other Biz-Match products and services, target marketing, send you service updates, and promotional offers based on your communication preferences. +

+

Our disclosure of your information

+

+ We may disclose personal information to respond to legal requirements, enforce our policies, respond to claims that a listing or other content infringes the rights of others, or protect anyone's rights, + property, or safety. +

+

+ We may also share your personal information with
+ When you select to register an account as a business buyer, you provide your personal details and we will pass this on to a seller of a business or franchise when you request more information. +

+

+ When you select to register an account as a business broker or seller on the site, we provide a public platform on which to establish your business profile. This profile consists of pertinent facts about your + business along with your personal information; namely, the contact information you provide to facilitate contact between you and other users' of the site. Direct email addresses and telephone numbers will not + be publicly displayed unless you specifically include it on your profile. +

+

+ The information a user includes within the forums provided on the site is publicly available to other users' of the site. Please be aware that any personal information you elect to provide in a public forum + may be used to send you unsolicited messages; we are not responsible for the personal information a user elects to disclose within their public profile, or in the private communications that users' engage in + on the site. +

+

+ We post testimonials on the site obtained from users'. These testimonials may include the name, city, state or region and business of the user. We obtain permission from our users' prior to posting their + testimonials on the site. We are not responsible for any personal information a user selects to include within their testimonial. +

+

+ When you elect to email a friend about the site, or a particular business, we request the third party's email address to send this one time email. We do not share this information with any third parties for + their promotional purposes and only store the information to gauge the effectiveness of our referral program. +

+

We may share your personal information with our service providers where necessary. We employ the services of a payment processor to fulfil payment for services purchased on the site.

+

+ We works with a number of partners or affiliates, where we provide marketing services for these companies. These third party agents collect your personal information to facilitate your service request and the + information submitted here is governed by their privacy policy. +

+

Masking Policy

+

+ In some cases, where the third party agent collects your information, the affiliate portal may appear within a BizMatch.net frame. It is presented as a BizMatch.net page for a streamlined user interface + however the data collected on such pages is governed by the third party agent's privacy policy. +

+

Legal Disclosure

+

+ In certain circumstances, we may be legally required to disclose information collected on the site to law enforcement, government agencies or other third parties. We reserve the right to disclose information + to our service providers and to law enforcement or government agencies where a formal request such as in response to a court order, subpoena or judicial proceeding is made. Where we believe in good faith that + disclosure of information is necessary to prevent imminent physical or financial harm, or loss, or in protecting against illegal activity on the site, we reserve to disclose information. +

+

+ Should the company undergo the merger, acquisition or sale of some or all of its assets, your personal information may likely be a part of the transferred assets. In such an event, your personal information + on the site, would be governed by this privacy statement; any changes to the privacy practices governing your information as a result of transfer would be relayed to you by means of a prominent notice on the + Site, or by email. +

+

Using information from BizMatch.net website

+

+ In certain cases, (where you are receiving contact details of buyers interested in your business opportunity or a business opportunity you represent), you must comply with data protection laws, and give other + users a chance to remove themselves from your database and a chance to review what information you have collected about them. +

+

You agree to use BizMatch.net user information only for:

+

+ BizMatch.net transaction-related purposes that are not unsolicited commercial messages;
+ using services offered through BizMatch.net, or
+ other purposes that a user expressly chooses. +

+

Marketing

+

+ We do not sell or rent your personal information to third parties for their marketing purposes without your explicit consent. Where you explicitly express your consent at the point of collection to receive + offers from third party partners or affiliates, we will communicate to you on their behalf. We will not pass your information on. +

+

+ You will receive email marketing communications from us throughout the duration of your relationship with our websites. If you do not wish to receive marketing communications from us you may unsubscribe and / + or change your preferences at any time by following instructions included within a communication or emailing Customer Services. +

+

If you have an account with one of our websites you can also log in and click the email preferences link to unsubscribe and / or change your preferences.

+

+ Please note that we reserve the right to send all website users notifications and administrative emails where necessary which are considered a part of the service. Given that these messages aren't promotional + in nature, you will be unable to opt-out of them. +

+

Cookies

+

+ A cookie is a small text file written to your hard drive that contains information about you. Cookies do not contain any personal information about users. Once you close your browser or log out of the + website, the cookie simply terminates. We use cookies so that we can personalise your experience of our websites. +

+

+ If you set up your browser to reject the cookie, you may still use the website however; doing so may interfere with your use of some aspects of our websites. Some of our business partners use cookies on our + site (for example, advertisers). We have no access to or control over these cookies. +

+

For more information about how BizMatch.net uses cookies please read our Cookie Policy.

+

Spam, spyware or spoofing

+

+ We and our users do not tolerate spam. Make sure to set your email preferences so we can communicate with you, as you prefer. Please add us to your safe senders list. To report spam or spoof emails, please + contact us using the contact information provided in the Contact Us section of this privacy statement. +

+

+ You may not use our communication tools to send spam or otherwise send content that would breach our Terms and Conditions. We automatically scan and may manually filter messages to check for spam, viruses, + phishing attacks and other malicious activity or illegal or prohibited content. We may also store these messages for back up purposes only. +

+

+ If you send an email to an email address that is not registered in our community, we do not permanently store that email or use that email address for any marketing purpose. We do not rent or sell these email + addresses. +

+

Account protection

+

+ Your password is the key to your account. Make sure this is stored safely. Use unique numbers, letters and special characters, and do not disclose your password to anyone. If you do share your password or + your personal information with others, remember that you are responsible for all actions taken in the name of your account. If you lose control of your password, you may lose substantial control over your + personal information and may be subject to legally binding actions taken on your behalf. Therefore, if your password has been compromised for any reason, you should immediately notify us and change your + password. +

+

Accessing, reviewing and changing your personal information

+

You can view and amend your personal information at any time by logging in to your account online. You must promptly update your personal information if it changes or is inaccurate.

+

If at any time you wish to close your account, please contact Customer Services and instruct us to do so. We will process your request as soon as we can.

+

You may also contact us at any time to find out what information we hold about you, what we do with it and ask us to update it for you.

+

+ We do retain personal information from closed accounts to comply with law, prevent fraud, collect any fees owed, resolve disputes, troubleshoot problems, assist with any investigations, enforce our Terms and + Conditions, and take other actions otherwise permitted by law. +

+

Security

+

+ Your information is stored on our servers located in the USA. We treat data as an asset that must be protected and use a variety of tools (encryption, passwords, physical security, etc.) to protect your + personal information against unauthorized access and disclosure. However, no method of security is 100% effective and while we take every measure to protect your personal information, we make no guarantees of + its absolute security. +

+

We employ the use of SSL encryption during the transmission of sensitive data across our websites.

+

Third parties

+

+ Except as otherwise expressly included in this Privacy Policy, this document addresses only the use and disclosure of information we collect from you. If you disclose your information to others, whether they + are buyers or sellers on our websites or other sites throughout the internet, different rules may apply to their use or disclosure of the information you disclose to them. Dynamis does not control the privacy + policies of third parties, and you are subject to the privacy policies of those third parties where applicable. +

+

We encourage you to ask questions before you disclose your personal information to others.

+

General

+

+ We may change this Privacy Policy from time to time as we add new products and applications, as we improve our current offerings, and as technologies and laws change. You can determine when this Privacy + Policy was last revised by referring to the "Last Updated" legend at the top of this page. +

+

+ Any changes will become effective upon our posting of the revised Privacy Policy on our affected websites. We will provide notice to you if these changes are material and, where required by applicable law, we + will obtain your consent. This notice may be provided by email, by posting notice of the changes on our affected websites or by other means, consistent with applicable laws. +

+

Contact Us

+

+ If you have any questions or comments about our privacy policy, and you can't find the answer to your question on our help pages, please contact us using this form or email support@bizmatch.net, or write + to us at BizMatch, 715 S. Tanahua, Corpus Christi, TX 78401.) +

+
+
+
+
+
+
diff --git a/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.html b/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.html index ac2f2c8..a770e23 100644 --- a/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.html +++ b/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.html @@ -1,159 +1,162 @@ -
- - - - -
-
- -
- -
- - -
-

Professional Business Brokers & Advisors

-

Connect with licensed business brokers, CPAs, attorneys, and other - professionals across the United States.

-
- - -
- -
- - @if(users?.length>0){ -

Professional Listings

-
- - @for (user of users; track user) { -
- -
- @if(currentUser) { - - } - -
-
- @if(user.hasProfile){ - - } @else { - Default business broker placeholder profile photo - } -
-

{{ user.description }}

-

- {{ user.firstname }} {{ user.lastname }}{{ - user.location?.name }} - {{ user.location?.state }} -

-
- -

{{ user.companyName }}

-
-
-
-
-
- @if(user.hasCompanyLogo){ - - } @else { - Default company logo placeholder - } - -
-
- } -
- } @else if (users?.length===0){ - -
-
- - - - - - - - - - - - - - - - - -
-

There're no professionals here -

-

Try changing your filters to -
see professionals -

-
- -
-
-
-
- } - - - @if(pageCount>1){ -
- -
- } -
-
+
+ + + + +
+
+ +
+ +
+ + +
+

Professional Business Brokers & Advisors

+

Connect with licensed business brokers, CPAs, attorneys, and other + professionals across the United States.

+
+

BizMatch connects business buyers and sellers with experienced professionals. Find qualified business brokers to help with your business sale or acquisition. Our platform features verified professionals including business brokers, M&A advisors, CPAs, and attorneys specializing in business transactions across the United States. Whether you're looking to buy or sell a business, our network of professionals can guide you through the process.

+
+
+ + +
+ +
+ + @if(users?.length>0){ +

Professional Listings

+
+ + @for (user of users; track user) { +
+ +
+ @if(currentUser) { + + } + +
+
+ @if(user.hasProfile){ + + } @else { + Default business broker placeholder profile photo + } +
+

{{ user.description }}

+

+ {{ user.firstname }} {{ user.lastname }}{{ + user.location?.name }} - {{ user.location?.state }} +

+
+ +

{{ user.companyName }}

+
+
+
+
+
+ @if(user.hasCompanyLogo){ + + } @else { + Default company logo placeholder + } + +
+
+ } +
+ } @else if (users?.length===0){ + +
+
+ + + + + + + + + + + + + + + + + +
+

There're no professionals here +

+

Try changing your filters to +
see professionals +

+
+ +
+
+
+
+ } + + + @if(pageCount>1){ +
+ +
+ } +
+
\ No newline at end of file diff --git a/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.ts b/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.ts index 138e1c0..ab4118d 100644 --- a/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.ts +++ b/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.ts @@ -1,243 +1,244 @@ -import { CommonModule, NgOptimizedImage } 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 { BusinessListing, SortByOptions, User } from '../../../../../../bizmatch-server/src/models/db.model'; -import { LISTINGS_PER_PAGE, ListingType, UserListingCriteria, emailToDirName, KeycloakUser } from '../../../../../../bizmatch-server/src/models/main.model'; -import { environment } from '../../../../environments/environment'; -import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component'; -import { CustomerSubTypeComponent } from '../../../components/customer-sub-type/customer-sub-type.component'; -import { PaginatorComponent } from '../../../components/paginator/paginator.component'; -import { SearchModalBrokerComponent } from '../../../components/search-modal/search-modal-broker.component'; -import { ModalService } from '../../../components/search-modal/modal.service'; -import { AltTextService } from '../../../services/alt-text.service'; -import { CriteriaChangeService } from '../../../services/criteria-change.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 { UserService } from '../../../services/user.service'; -import { AuthService } from '../../../services/auth.service'; -import { assignProperties, resetUserListingCriteria, map2User } from '../../../utils/utils'; -@UntilDestroy() -@Component({ - selector: 'app-broker-listings', - standalone: true, - imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent, CustomerSubTypeComponent, BreadcrumbsComponent, SearchModalBrokerComponent], - templateUrl: './broker-listings.component.html', - styleUrls: ['./broker-listings.component.scss', '../../pages.scss'], -}) -export class BrokerListingsComponent implements OnInit, OnDestroy { - private destroy$ = new Subject(); - breadcrumbs: BreadcrumbItem[] = [ - { label: 'Home', url: '/home', icon: 'fas fa-home' }, - { label: 'Professionals', url: '/brokerListings' } - ]; - environment = environment; - listings: Array; - users: Array; - filteredListings: Array; - criteria: UserListingCriteria; - realEstateChecked: boolean; - maxPrice: string; - minPrice: string; - type: string; - statesSet = new Set(); - state: string; - first: number = 0; - rows: number = 12; - totalRecords: number = 0; - ts = new Date().getTime(); - env = environment; - public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined; - emailToDirName = emailToDirName; - page = 1; - pageCount = 1; - sortBy: SortByOptions = null; // Neu: Separate Property - currentUser: KeycloakUser | null = null; // Current logged-in user - constructor( - public altText: AltTextService, - public selectOptions: SelectOptionsService, - private listingsService: ListingsService, - private userService: UserService, - private activatedRoute: ActivatedRoute, - private router: Router, - private cdRef: ChangeDetectorRef, - private imageService: ImageService, - private route: ActivatedRoute, - private searchService: SearchService, - private modalService: ModalService, - private criteriaChangeService: CriteriaChangeService, - private filterStateService: FilterStateService, - private authService: AuthService, - ) { - this.loadSortBy(); - } - private loadSortBy() { - const storedSortBy = sessionStorage.getItem('professionalsSortBy'); - this.sortBy = storedSortBy && storedSortBy !== 'null' ? (storedSortBy as SortByOptions) : null; - } - async ngOnInit(): Promise { - // Get current logged-in user - const token = await this.authService.getToken(); - this.currentUser = map2User(token); - - // Subscribe to FilterStateService for criteria changes - this.filterStateService - .getState$('brokerListings') - .pipe(takeUntil(this.destroy$)) - .subscribe(state => { - this.criteria = state.criteria as UserListingCriteria; - this.sortBy = state.sortBy; - this.search(); - }); - - // Subscribe to SearchService for search triggers - this.searchService.searchTrigger$ - .pipe(takeUntil(this.destroy$)) - .subscribe(type => { - if (type === 'brokerListings') { - this.search(); - } - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - async search() { - const usersReponse = await this.userService.search(this.criteria); - this.users = usersReponse.results; - this.totalRecords = usersReponse.totalCount; - this.pageCount = this.totalRecords % LISTINGS_PER_PAGE === 0 ? this.totalRecords / LISTINGS_PER_PAGE : Math.floor(this.totalRecords / LISTINGS_PER_PAGE) + 1; - this.page = this.criteria.page ? this.criteria.page : 1; - this.cdRef.markForCheck(); - this.cdRef.detectChanges(); - } - onPageChange(page: any) { - this.criteria.start = (page - 1) * LISTINGS_PER_PAGE; - this.criteria.length = LISTINGS_PER_PAGE; - this.criteria.page = page; - this.search(); - } - - reset() { } - - // New methods for filter actions - clearAllFilters() { - // Reset criteria to default values - resetUserListingCriteria(this.criteria); - - // Reset pagination - this.criteria.page = 1; - this.criteria.start = 0; - - this.criteriaChangeService.notifyCriteriaChange(); - - // Search with cleared filters - this.searchService.search('brokerListings'); - } - - async openFilterModal() { - // Open the search modal with current criteria - const modalResult = await this.modalService.showModal(this.criteria); - if (modalResult.accepted) { - this.searchService.search('brokerListings'); - } else { - this.criteria = assignProperties(this.criteria, modalResult.criteria); - } - } - - /** - * Check if professional/user is already in current user's favorites - */ - isFavorite(professional: User): boolean { - if (!this.currentUser?.email || !professional.favoritesForUser) return false; - return professional.favoritesForUser.includes(this.currentUser.email); - } - - /** - * Toggle favorite status for a professional - */ - async toggleFavorite(event: Event, professional: User): Promise { - event.stopPropagation(); - event.preventDefault(); - - if (!this.currentUser?.email) { - // User not logged in - redirect to login - this.router.navigate(['/login']); - return; - } - - try { - console.log('Toggling favorite for:', professional.email, 'Current user:', this.currentUser.email); - console.log('Before update, favorites:', professional.favoritesForUser); - - if (this.isFavorite(professional)) { - // Remove from favorites - await this.listingsService.removeFavorite(professional.id, 'user'); - professional.favoritesForUser = professional.favoritesForUser.filter( - email => email !== this.currentUser!.email - ); - } else { - // Add to favorites - await this.listingsService.addToFavorites(professional.id, 'user'); - if (!professional.favoritesForUser) { - professional.favoritesForUser = []; - } - // Use spread to create new array reference - professional.favoritesForUser = [...professional.favoritesForUser, this.currentUser.email]; - } - - console.log('After update, favorites:', professional.favoritesForUser); - this.cdRef.markForCheck(); - this.cdRef.detectChanges(); - } catch (error) { - console.error('Error toggling favorite:', error); - } - } - - /** - * Share professional profile - */ - async shareProfessional(event: Event, user: User): Promise { - event.stopPropagation(); - event.preventDefault(); - - const url = `${window.location.origin}/details-user/${user.id}`; - const title = `${user.firstname} ${user.lastname} - ${user.companyName}`; - - // 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 professional: ${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); - }); - } -} +import { CommonModule, NgOptimizedImage } from '@angular/common'; +import { ChangeDetectionStrategy, 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 { BusinessListing, SortByOptions, User } from '../../../../../../bizmatch-server/src/models/db.model'; +import { LISTINGS_PER_PAGE, ListingType, UserListingCriteria, emailToDirName, KeycloakUser } from '../../../../../../bizmatch-server/src/models/main.model'; +import { environment } from '../../../../environments/environment'; +import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component'; +import { CustomerSubTypeComponent } from '../../../components/customer-sub-type/customer-sub-type.component'; +import { PaginatorComponent } from '../../../components/paginator/paginator.component'; +import { SearchModalBrokerComponent } from '../../../components/search-modal/search-modal-broker.component'; +import { ModalService } from '../../../components/search-modal/modal.service'; +import { AltTextService } from '../../../services/alt-text.service'; +import { CriteriaChangeService } from '../../../services/criteria-change.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 { UserService } from '../../../services/user.service'; +import { AuthService } from '../../../services/auth.service'; +import { assignProperties, resetUserListingCriteria, map2User } from '../../../utils/utils'; +@UntilDestroy() +@Component({ + selector: 'app-broker-listings', + standalone: true, + imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent, CustomerSubTypeComponent, BreadcrumbsComponent, SearchModalBrokerComponent], + templateUrl: './broker-listings.component.html', + styleUrls: ['./broker-listings.component.scss', '../../pages.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BrokerListingsComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); + breadcrumbs: BreadcrumbItem[] = [ + { label: 'Home', url: '/home', icon: 'fas fa-home' }, + { label: 'Professionals', url: '/brokerListings' } + ]; + environment = environment; + listings: Array; + users: Array; + filteredListings: Array; + criteria: UserListingCriteria; + realEstateChecked: boolean; + maxPrice: string; + minPrice: string; + type: string; + statesSet = new Set(); + state: string; + first: number = 0; + rows: number = 12; + totalRecords: number = 0; + ts = new Date().getTime(); + env = environment; + public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined; + emailToDirName = emailToDirName; + page = 1; + pageCount = 1; + sortBy: SortByOptions = null; // Neu: Separate Property + currentUser: KeycloakUser | null = null; // Current logged-in user + constructor( + public altText: AltTextService, + public selectOptions: SelectOptionsService, + private listingsService: ListingsService, + private userService: UserService, + private activatedRoute: ActivatedRoute, + private router: Router, + private cdRef: ChangeDetectorRef, + private imageService: ImageService, + private route: ActivatedRoute, + private searchService: SearchService, + private modalService: ModalService, + private criteriaChangeService: CriteriaChangeService, + private filterStateService: FilterStateService, + private authService: AuthService, + ) { + this.loadSortBy(); + } + private loadSortBy() { + const storedSortBy = sessionStorage.getItem('professionalsSortBy'); + this.sortBy = storedSortBy && storedSortBy !== 'null' ? (storedSortBy as SortByOptions) : null; + } + async ngOnInit(): Promise { + // Get current logged-in user + const token = await this.authService.getToken(); + this.currentUser = map2User(token); + + // Subscribe to FilterStateService for criteria changes + this.filterStateService + .getState$('brokerListings') + .pipe(takeUntil(this.destroy$)) + .subscribe(state => { + this.criteria = state.criteria as UserListingCriteria; + this.sortBy = state.sortBy; + this.search(); + }); + + // Subscribe to SearchService for search triggers + this.searchService.searchTrigger$ + .pipe(takeUntil(this.destroy$)) + .subscribe(type => { + if (type === 'brokerListings') { + this.search(); + } + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + async search() { + const usersReponse = await this.userService.search(this.criteria); + this.users = usersReponse.results; + this.totalRecords = usersReponse.totalCount; + this.pageCount = this.totalRecords % LISTINGS_PER_PAGE === 0 ? this.totalRecords / LISTINGS_PER_PAGE : Math.floor(this.totalRecords / LISTINGS_PER_PAGE) + 1; + this.page = this.criteria.page ? this.criteria.page : 1; + this.cdRef.markForCheck(); + this.cdRef.detectChanges(); + } + onPageChange(page: any) { + this.criteria.start = (page - 1) * LISTINGS_PER_PAGE; + this.criteria.length = LISTINGS_PER_PAGE; + this.criteria.page = page; + this.search(); + } + + reset() { } + + // New methods for filter actions + clearAllFilters() { + // Reset criteria to default values + resetUserListingCriteria(this.criteria); + + // Reset pagination + this.criteria.page = 1; + this.criteria.start = 0; + + this.criteriaChangeService.notifyCriteriaChange(); + + // Search with cleared filters + this.searchService.search('brokerListings'); + } + + async openFilterModal() { + // Open the search modal with current criteria + const modalResult = await this.modalService.showModal(this.criteria); + if (modalResult.accepted) { + this.searchService.search('brokerListings'); + } else { + this.criteria = assignProperties(this.criteria, modalResult.criteria); + } + } + + /** + * Check if professional/user is already in current user's favorites + */ + isFavorite(professional: User): boolean { + if (!this.currentUser?.email || !professional.favoritesForUser) return false; + return professional.favoritesForUser.includes(this.currentUser.email); + } + + /** + * Toggle favorite status for a professional + */ + async toggleFavorite(event: Event, professional: User): Promise { + event.stopPropagation(); + event.preventDefault(); + + if (!this.currentUser?.email) { + // User not logged in - redirect to login + this.router.navigate(['/login']); + return; + } + + try { + console.log('Toggling favorite for:', professional.email, 'Current user:', this.currentUser.email); + console.log('Before update, favorites:', professional.favoritesForUser); + + if (this.isFavorite(professional)) { + // Remove from favorites + await this.listingsService.removeFavorite(professional.id, 'user'); + professional.favoritesForUser = professional.favoritesForUser.filter( + email => email !== this.currentUser!.email + ); + } else { + // Add to favorites + await this.listingsService.addToFavorites(professional.id, 'user'); + if (!professional.favoritesForUser) { + professional.favoritesForUser = []; + } + // Use spread to create new array reference + professional.favoritesForUser = [...professional.favoritesForUser, this.currentUser.email]; + } + + console.log('After update, favorites:', professional.favoritesForUser); + this.cdRef.markForCheck(); + this.cdRef.detectChanges(); + } catch (error) { + console.error('Error toggling favorite:', error); + } + } + + /** + * Share professional profile + */ + async shareProfessional(event: Event, user: User): Promise { + event.stopPropagation(); + event.preventDefault(); + + const url = `${window.location.origin}/details-user/${user.id}`; + const title = `${user.firstname} ${user.lastname} - ${user.companyName}`; + + // 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 professional: ${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); + }); + } +} diff --git a/bizmatch/src/app/pages/listings/business-listings/business-listings.component.html b/bizmatch/src/app/pages/listings/business-listings/business-listings.component.html index 9ac3926..aea60a0 100644 --- a/bizmatch/src/app/pages/listings/business-listings/business-listings.component.html +++ b/bizmatch/src/app/pages/listings/business-listings/business-listings.component.html @@ -1,259 +1,262 @@ -
- - - - -
-
- -
- -
- - -
-

Businesses for Sale

-

Discover profitable business opportunities across the United States. Browse - verified listings from business owners and brokers.

-
- - - @if(isLoading) { -

Loading Business Listings...

-
- @for (item of [1,2,3,4,5,6]; track item) { -
-
- -
-
-
-
- -
- -
-
-
-
- -
-
-
-
-
-
-
- -
-
-
- } -
- } @else if(listings?.length > 0) { -

Available Business Listings

-
- @for (listing of listings; track listing.id) { -
-
- -
- @if(user) { - - } - -
- -
- - {{ - selectOptions.getBusiness(listing.type) }} -
-

- {{ listing.title }} - @if(listing.draft) { - Draft - } -

-
- - {{ selectOptions.getState(listing.location.state) }} - - - @if (getListingBadge(listing); as badge) { - - {{ badge }} - - } -
- -

- Asking price: - - {{ listing?.price != null ? (listing.price | currency : 'USD' : 'symbol' : '1.0-0') : 'undisclosed' }} - -

-

- Sales revenue: - {{ listing?.salesRevenue != null ? (listing.salesRevenue | currency : 'USD' : 'symbol' : '1.0-0') : - 'undisclosed' }} -

-

- Net profit: - {{ listing?.cashFlow != null ? (listing.cashFlow | currency : 'USD' : 'symbol' : '1.0-0') : 'undisclosed' - }} -

-

- Location: {{ listing.location.name ? listing.location.name : listing.location.county ? - listing.location.county : this.selectOptions.getState(listing.location.state) }} -

-

Years established: {{ listing.established }}

- @if(listing.imageName) { - - } -
- -
-
- } -
- } @else if (listings?.length === 0) { -
-
- - - - - - - - - - - - - - - - - -
-

No listings found

-

We couldn't find any businesses - matching your criteria.
Try adjusting your filters or explore popular categories below.

- - -
- - -
- - -
-

- Popular Categories -

-
- - - - - - -
-
- - -
-

- Search Tips -

-
    -
  • • Try expanding your search radius
  • -
  • • Consider adjusting your price range
  • -
  • • Browse all categories to discover opportunities
  • -
-
-
-
-
- } -
- @if(pageCount > 1) { - - } -
- - - +
+ + + + +
+
+ +
+ +
+ + +
+

Businesses for Sale

+

Discover profitable business opportunities across the United States. Browse + verified listings from business owners and brokers.

+
+

BizMatch features thousands of businesses for sale across all industries and price ranges. Browse restaurants, retail stores, franchises, service businesses, e-commerce operations, and manufacturing companies. Each listing includes financial details, years established, location information, and seller contact details. Our marketplace connects business buyers with sellers and brokers nationwide, making it easy to find your next business opportunity.

+
+
+ + + @if(isLoading) { +

Loading Business Listings...

+
+ @for (item of [1,2,3,4,5,6]; track item) { +
+
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+ } +
+ } @else if(listings?.length > 0) { +

Available Business Listings

+
+ @for (listing of listings; track listing.id) { +
+
+ +
+ @if(user) { + + } + +
+ +
+ + {{ + selectOptions.getBusiness(listing.type) }} +
+

+ {{ listing.title }} + @if(listing.draft) { + Draft + } +

+
+ + {{ selectOptions.getState(listing.location.state) }} + + + @if (getListingBadge(listing); as badge) { + + {{ badge }} + + } +
+ +

+ Asking price: + + {{ listing?.price != null ? (listing.price | currency : 'USD' : 'symbol' : '1.0-0') : 'undisclosed' }} + +

+

+ Sales revenue: + {{ listing?.salesRevenue != null ? (listing.salesRevenue | currency : 'USD' : 'symbol' : '1.0-0') : + 'undisclosed' }} +

+

+ Net profit: + {{ listing?.cashFlow != null ? (listing.cashFlow | currency : 'USD' : 'symbol' : '1.0-0') : 'undisclosed' + }} +

+

+ Location: {{ listing.location.name ? listing.location.name : listing.location.county ? + listing.location.county : this.selectOptions.getState(listing.location.state) }} +

+

Years established: {{ listing.established }}

+ @if(listing.imageName) { + + } +
+ +
+
+ } +
+ } @else if (listings?.length === 0) { +
+
+ + + + + + + + + + + + + + + + + +
+

No listings found

+

We couldn't find any businesses + matching your criteria.
Try adjusting your filters or explore popular categories below.

+ + +
+ + +
+ + +
+

+ Popular Categories +

+
+ + + + + + +
+
+ + +
+

+ Search Tips +

+
    +
  • • Try expanding your search radius
  • +
  • • Consider adjusting your price range
  • +
  • • Browse all categories to discover opportunities
  • +
+
+
+
+
+ } +
+ @if(pageCount > 1) { + + } +
+ + +
\ No newline at end of file diff --git a/bizmatch/src/app/pages/listings/business-listings/business-listings.component.ts b/bizmatch/src/app/pages/listings/business-listings/business-listings.component.ts index 06aaf53..a4cdac0 100644 --- a/bizmatch/src/app/pages/listings/business-listings/business-listings.component.ts +++ b/bizmatch/src/app/pages/listings/business-listings/business-listings.component.ts @@ -1,330 +1,331 @@ -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); - } -} +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, 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'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +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 - Profitable Opportunities | BizMatch', + description: 'Browse thousands of businesses for sale. Find restaurants, franchises, retail stores, and more. Verified listings from 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); + } +} diff --git a/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html b/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html index 37689ef..ad76023 100644 --- a/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html +++ b/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html @@ -1,143 +1,146 @@ -
- - - - -
-
- -
- -
- - -
-

Commercial Properties for Sale

-

Find office buildings, retail spaces, warehouses, and industrial properties across the United States. Investment opportunities from verified sellers and commercial real estate brokers.

-
- - @if(listings?.length > 0) { -

Available Commercial Property Listings

-
- @for (listing of listings; track listing.id) { -
- -
- @if(user) { - - } - -
- @if (listing.imageOrder?.length>0){ - - } @else { - - } -
- {{ selectOptions.getCommercialProperty(listing.type) }} -
- {{ selectOptions.getState(listing.location.state) }} -

- {{ getDaysListed(listing) }} days listed -

-
-

- {{ listing.title }} - @if(listing.draft){ - Draft - } -

-

{{ listing.location.name ? listing.location.name : listing.location.county }}

-

{{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}

-
- -
-
- } -
- } @else if (listings?.length === 0){ -
-
- - - - - - - - - - - - - - - - - -
-

There’s no listing here

-

Try changing your filters to
see listings

-
- -
-
-
-
- } -
- @if(pageCount > 1) { - - } -
- - - -
+
+ + + + +
+
+ +
+ +
+ + +
+

Commercial Properties for Sale

+

Find office buildings, retail spaces, warehouses, and industrial properties across the United States. Investment opportunities from verified sellers and commercial real estate brokers.

+
+

BizMatch showcases commercial real estate listings including office buildings, retail spaces, warehouses, and industrial properties for sale or lease. Browse investment properties across the United States with detailed information on square footage, zoning, pricing, and location. Our platform connects property buyers and investors with sellers and commercial real estate brokers. Find shopping centers, medical buildings, land parcels, and mixed-use developments in your target market.

+
+
+ + @if(listings?.length > 0) { +

Available Commercial Property Listings

+
+ @for (listing of listings; track listing.id) { +
+ +
+ @if(user) { + + } + +
+ @if (listing.imageOrder?.length>0){ + + } @else { + + } +
+ {{ selectOptions.getCommercialProperty(listing.type) }} +
+ {{ selectOptions.getState(listing.location.state) }} +

+ {{ getDaysListed(listing) }} days listed +

+
+

+ {{ listing.title }} + @if(listing.draft){ + Draft + } +

+

{{ listing.location.name ? listing.location.name : listing.location.county }}

+

{{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}

+
+ +
+
+ } +
+ } @else if (listings?.length === 0){ +
+
+ + + + + + + + + + + + + + + + + +
+

There’s no listing here

+

Try changing your filters to
see listings

+
+ +
+
+
+
+ } +
+ @if(pageCount > 1) { + + } +
+ + + +
diff --git a/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.ts b/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.ts index f7e38d0..6312721 100644 --- a/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.ts +++ b/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.ts @@ -1,301 +1,302 @@ -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); - }); - } -} +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, 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'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +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 | BizMatch', + description: 'Browse commercial real estate: office buildings, retail spaces, warehouses, and industrial properties. Verified investment opportunities.', + 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); + }); + } +} diff --git a/bizmatch/src/app/services/seo.service.ts b/bizmatch/src/app/services/seo.service.ts index 10e74cc..31f92f4 100644 --- a/bizmatch/src/app/services/seo.service.ts +++ b/bizmatch/src/app/services/seo.service.ts @@ -1,635 +1,665 @@ -import { Injectable, inject, PLATFORM_ID } from '@angular/core'; -import { isPlatformBrowser } from '@angular/common'; -import { Meta, Title } from '@angular/platform-browser'; -import { Router } from '@angular/router'; - -export interface SEOData { - title: string; - description: string; - image?: string; - url?: string; - keywords?: string; - type?: string; - author?: string; -} - -@Injectable({ - providedIn: 'root' -}) -export class SeoService { - private meta = inject(Meta); - private title = inject(Title); - private router = inject(Router); - private platformId = inject(PLATFORM_ID); - private isBrowser = isPlatformBrowser(this.platformId); - - private readonly defaultImage = 'https://biz-match.com/assets/images/bizmatch-og-image.jpg'; - private readonly siteName = 'BizMatch'; - private readonly baseUrl = 'https://biz-match.com'; - - /** - * Get the base URL for SEO purposes - */ - getBaseUrl(): string { - return this.baseUrl; - } - - /** - * Update all SEO meta tags for a page - */ - updateMetaTags(data: SEOData): void { - const url = data.url || `${this.baseUrl}${this.router.url}`; - const image = data.image || this.defaultImage; - const type = data.type || 'website'; - - // Update page title - this.title.setTitle(data.title); - - // Standard meta tags - this.meta.updateTag({ name: 'description', content: data.description }); - if (data.keywords) { - this.meta.updateTag({ name: 'keywords', content: data.keywords }); - } - if (data.author) { - this.meta.updateTag({ name: 'author', content: data.author }); - } - - // Open Graph tags (Facebook, LinkedIn, etc.) - this.meta.updateTag({ property: 'og:title', content: data.title }); - this.meta.updateTag({ property: 'og:description', content: data.description }); - this.meta.updateTag({ property: 'og:image', content: image }); - this.meta.updateTag({ property: 'og:url', content: url }); - this.meta.updateTag({ property: 'og:type', content: type }); - this.meta.updateTag({ property: 'og:site_name', content: this.siteName }); - - // Twitter Card tags - this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' }); - this.meta.updateTag({ name: 'twitter:title', content: data.title }); - this.meta.updateTag({ name: 'twitter:description', content: data.description }); - this.meta.updateTag({ name: 'twitter:image', content: image }); - - // Canonical URL - this.updateCanonicalUrl(url); - } - - /** - * Update meta tags for a business listing - */ - updateBusinessListingMeta(listing: any): void { - const title = `${listing.businessName} - Business for Sale in ${listing.city}, ${listing.state} | BizMatch`; - const description = `${listing.businessName} for sale in ${listing.city}, ${listing.state}. ${listing.askingPrice ? `Price: $${listing.askingPrice.toLocaleString()}` : 'Contact for price'}. ${listing.description?.substring(0, 100)}...`; - const keywords = `business for sale, ${listing.industry || 'business'}, ${listing.city} ${listing.state}, buy business, ${listing.businessName}`; - const image = listing.images?.[0] || this.defaultImage; - - this.updateMetaTags({ - title, - description, - keywords, - image, - type: 'product' - }); - } - - /** - * Update meta tags for commercial property listing - */ - updateCommercialPropertyMeta(property: any): void { - const title = `${property.propertyType || 'Commercial Property'} for Sale in ${property.city}, ${property.state} | BizMatch`; - const description = `Commercial property for sale in ${property.city}, ${property.state}. ${property.askingPrice ? `Price: $${property.askingPrice.toLocaleString()}` : 'Contact for price'}. ${property.propertyDescription?.substring(0, 100)}...`; - const keywords = `commercial property, real estate, ${property.propertyType || 'property'}, ${property.city} ${property.state}, buy property`; - const image = property.images?.[0] || this.defaultImage; - - this.updateMetaTags({ - title, - description, - keywords, - image, - type: 'product' - }); - } - - /** - * Update canonical URL - */ - private updateCanonicalUrl(url: string): void { - if (!this.isBrowser) return; - - let link: HTMLLinkElement | null = document.querySelector('link[rel="canonical"]'); - - if (link) { - link.setAttribute('href', url); - } else { - link = document.createElement('link'); - link.setAttribute('rel', 'canonical'); - link.setAttribute('href', url); - document.head.appendChild(link); - } - } - - /** - * Generate Product schema for business listing (better than LocalBusiness for items for sale) - */ - generateProductSchema(listing: any): object { - const urlSlug = listing.slug || listing.id; - const schema: any = { - '@context': 'https://schema.org', - '@type': 'Product', - 'name': listing.businessName, - 'description': listing.description, - 'image': listing.images || [], - 'url': `${this.baseUrl}/business/${urlSlug}`, - 'brand': { - '@type': 'Brand', - 'name': listing.businessName - }, - 'category': listing.category || 'Business' - }; - - // Only include offers if askingPrice is available - if (listing.askingPrice && listing.askingPrice > 0) { - schema['offers'] = { - '@type': 'Offer', - 'price': listing.askingPrice.toString(), - 'priceCurrency': 'USD', - 'availability': 'https://schema.org/InStock', - 'url': `${this.baseUrl}/business/${urlSlug}`, - 'priceValidUntil': new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString().split('T')[0], - 'seller': { - '@type': 'Organization', - 'name': this.siteName, - 'url': this.baseUrl - } - }; - } else { - // For listings without a price, use PriceSpecification with "Contact for price" - schema['offers'] = { - '@type': 'Offer', - 'priceCurrency': 'USD', - 'availability': 'https://schema.org/InStock', - 'url': `${this.baseUrl}/business/${urlSlug}`, - 'priceSpecification': { - '@type': 'PriceSpecification', - 'priceCurrency': 'USD' - }, - 'seller': { - '@type': 'Organization', - 'name': this.siteName, - 'url': this.baseUrl - } - }; - } - - // Add aggregateRating with placeholder data - schema['aggregateRating'] = { - '@type': 'AggregateRating', - 'ratingValue': '4.5', - 'reviewCount': '127' - }; - - // Add address information if available - if (listing.address || listing.city || listing.state) { - schema['location'] = { - '@type': 'Place', - 'address': { - '@type': 'PostalAddress', - 'streetAddress': listing.address, - 'addressLocality': listing.city, - 'addressRegion': listing.state, - 'postalCode': listing.zip, - 'addressCountry': 'US' - } - }; - } - - // Add additional product details - if (listing.annualRevenue) { - schema['additionalProperty'] = schema['additionalProperty'] || []; - schema['additionalProperty'].push({ - '@type': 'PropertyValue', - 'name': 'Annual Revenue', - 'value': listing.annualRevenue, - 'unitText': 'USD' - }); - } - - if (listing.yearEstablished) { - schema['additionalProperty'] = schema['additionalProperty'] || []; - schema['additionalProperty'].push({ - '@type': 'PropertyValue', - 'name': 'Year Established', - 'value': listing.yearEstablished - }); - } - - return schema; - } - - /** - * Generate rich snippet JSON-LD for business listing - * @deprecated Use generateProductSchema instead for better SEO - */ - generateBusinessListingSchema(listing: any): object { - const urlSlug = listing.slug || listing.id; - const schema = { - '@context': 'https://schema.org', - '@type': 'LocalBusiness', - 'name': listing.businessName, - 'description': listing.description, - 'image': listing.images || [], - 'address': { - '@type': 'PostalAddress', - 'streetAddress': listing.address, - 'addressLocality': listing.city, - 'addressRegion': listing.state, - 'postalCode': listing.zip, - 'addressCountry': 'US' - }, - 'offers': { - '@type': 'Offer', - 'price': listing.askingPrice, - 'priceCurrency': 'USD', - 'availability': 'https://schema.org/InStock', - 'url': `${this.baseUrl}/business/${urlSlug}` - } - }; - - if (listing.annualRevenue) { - schema['revenue'] = { - '@type': 'MonetaryAmount', - 'value': listing.annualRevenue, - 'currency': 'USD' - }; - } - - if (listing.yearEstablished) { - schema['foundingDate'] = listing.yearEstablished.toString(); - } - - return schema; - } - - /** - * Inject JSON-LD structured data into page - */ - injectStructuredData(schema: object): void { - if (!this.isBrowser) return; - - // Remove existing schema script - const existingScript = document.querySelector('script[type="application/ld+json"]'); - if (existingScript) { - existingScript.remove(); - } - - // Add new schema script - const script = document.createElement('script'); - script.type = 'application/ld+json'; - script.text = JSON.stringify(schema); - document.head.appendChild(script); - } - - /** - * Clear all structured data - */ - clearStructuredData(): void { - if (!this.isBrowser) return; - - const scripts = document.querySelectorAll('script[type="application/ld+json"]'); - scripts.forEach(script => script.remove()); - } - - /** - * Generate RealEstateListing schema for commercial property - */ - generateRealEstateListingSchema(property: any): object { - const schema: any = { - '@context': 'https://schema.org', - '@type': 'RealEstateListing', - 'name': property.propertyName || `${property.propertyType} in ${property.city}`, - 'description': property.propertyDescription, - 'url': `${this.baseUrl}/details-commercial-property/${property.id}`, - 'image': property.images || [], - 'address': { - '@type': 'PostalAddress', - 'streetAddress': property.address, - 'addressLocality': property.city, - 'addressRegion': property.state, - 'postalCode': property.zip, - 'addressCountry': 'US' - }, - 'geo': property.latitude && property.longitude ? { - '@type': 'GeoCoordinates', - 'latitude': property.latitude, - 'longitude': property.longitude - } : undefined - }; - - // Only include offers with price if askingPrice is available - if (property.askingPrice && property.askingPrice > 0) { - schema['offers'] = { - '@type': 'Offer', - 'price': property.askingPrice.toString(), - 'priceCurrency': 'USD', - 'availability': 'https://schema.org/InStock', - 'url': `${this.baseUrl}/details-commercial-property/${property.id}`, - 'priceSpecification': { - '@type': 'PriceSpecification', - 'price': property.askingPrice.toString(), - 'priceCurrency': 'USD' - } - }; - } else { - // For listings without a price, provide minimal offer information - schema['offers'] = { - '@type': 'Offer', - 'priceCurrency': 'USD', - 'availability': 'https://schema.org/InStock', - 'url': `${this.baseUrl}/details-commercial-property/${property.id}`, - 'priceSpecification': { - '@type': 'PriceSpecification', - 'priceCurrency': 'USD' - } - }; - } - - // Add property-specific details - if (property.squareFootage) { - schema['floorSize'] = { - '@type': 'QuantitativeValue', - 'value': property.squareFootage, - 'unitCode': 'SQF' - }; - } - - if (property.yearBuilt) { - schema['yearBuilt'] = property.yearBuilt; - } - - if (property.propertyType) { - schema['additionalType'] = property.propertyType; - } - - return schema; - } - - /** - * Generate RealEstateAgent schema for broker profiles - */ - generateRealEstateAgentSchema(broker: any): object { - return { - '@context': 'https://schema.org', - '@type': 'RealEstateAgent', - 'name': broker.name || `${broker.firstName} ${broker.lastName}`, - 'description': broker.description || broker.bio, - 'url': `${this.baseUrl}/broker/${broker.id}`, - 'image': broker.profileImage || broker.avatar, - 'email': broker.email, - 'telephone': broker.phone, - 'address': broker.address ? { - '@type': 'PostalAddress', - 'streetAddress': broker.address, - 'addressLocality': broker.city, - 'addressRegion': broker.state, - 'postalCode': broker.zip, - 'addressCountry': 'US' - } : undefined, - 'knowsAbout': broker.specialties || ['Business Brokerage', 'Commercial Real Estate'], - 'memberOf': broker.brokerage ? { - '@type': 'Organization', - 'name': broker.brokerage - } : undefined - }; - } - - /** - * Generate BreadcrumbList schema for navigation - */ - generateBreadcrumbSchema(breadcrumbs: Array<{ name: string; url: string }>): object { - return { - '@context': 'https://schema.org', - '@type': 'BreadcrumbList', - 'itemListElement': breadcrumbs.map((crumb, index) => ({ - '@type': 'ListItem', - 'position': index + 1, - 'name': crumb.name, - 'item': `${this.baseUrl}${crumb.url}` - })) - }; - } - - /** - * Generate Organization schema for the company - */ - generateOrganizationSchema(): object { - return { - '@context': 'https://schema.org', - '@type': 'Organization', - 'name': this.siteName, - 'url': this.baseUrl, - 'logo': `${this.baseUrl}/assets/images/bizmatch-logo.png`, - 'description': 'BizMatch is the leading marketplace for buying and selling businesses and commercial properties across the United States.', - 'sameAs': [ - 'https://www.facebook.com/bizmatch', - 'https://www.linkedin.com/company/bizmatch', - 'https://twitter.com/bizmatch' - ], - 'contactPoint': { - '@type': 'ContactPoint', - 'telephone': '+1-800-BIZ-MATCH', - 'contactType': 'Customer Service', - 'areaServed': 'US', - 'availableLanguage': 'English' - } - }; - } - - /** - * Generate HowTo schema for step-by-step guides - */ - generateHowToSchema(data: { - name: string; - description: string; - totalTime?: string; - steps: Array<{ name: string; text: string; image?: string }>; - }): object { - return { - '@context': 'https://schema.org', - '@type': 'HowTo', - 'name': data.name, - 'description': data.description, - 'totalTime': data.totalTime || 'PT30M', - 'step': data.steps.map((step, index) => ({ - '@type': 'HowToStep', - 'position': index + 1, - 'name': step.name, - 'text': step.text, - 'image': step.image || undefined - })) - }; - } - - /** - * Generate FAQPage schema for frequently asked questions - */ - generateFAQPageSchema(questions: Array<{ question: string; answer: string }>): object { - return { - '@context': 'https://schema.org', - '@type': 'FAQPage', - 'mainEntity': questions.map(q => ({ - '@type': 'Question', - 'name': q.question, - 'acceptedAnswer': { - '@type': 'Answer', - 'text': q.answer - } - })) - }; - } - - /** - * Inject multiple structured data schemas - */ - injectMultipleSchemas(schemas: object[]): void { - if (!this.isBrowser) return; - - // Remove existing schema scripts - this.clearStructuredData(); - - // Add new schema scripts - schemas.forEach(schema => { - const script = document.createElement('script'); - script.type = 'application/ld+json'; - script.text = JSON.stringify(schema); - document.head.appendChild(script); - }); - } - - /** - * Set noindex meta tag to prevent indexing of 404 pages - */ - setNoIndex(): void { - this.meta.updateTag({ name: 'robots', content: 'noindex, follow' }); - this.meta.updateTag({ name: 'googlebot', content: 'noindex, follow' }); - this.meta.updateTag({ name: 'bingbot', content: 'noindex, follow' }); - } - - /** - * Reset to default index/follow directive - */ - setIndexFollow(): void { - this.meta.updateTag({ name: 'robots', content: 'index, follow' }); - this.meta.updateTag({ name: 'googlebot', content: 'index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1' }); - this.meta.updateTag({ name: 'bingbot', content: 'index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1' }); - } - - /** - * Generate Sitelinks SearchBox schema for Google SERP - */ - generateSearchBoxSchema(): object { - return { - '@context': 'https://schema.org', - '@type': 'WebSite', - 'url': this.baseUrl, - 'name': this.siteName, - 'description': 'BizMatch is the leading marketplace for buying and selling businesses and commercial properties across the United States.', - 'potentialAction': [ - { - '@type': 'SearchAction', - 'target': { - '@type': 'EntryPoint', - 'urlTemplate': `${this.baseUrl}/businessListings?search={search_term_string}` - }, - 'query-input': 'required name=search_term_string' - }, - { - '@type': 'SearchAction', - 'target': { - '@type': 'EntryPoint', - 'urlTemplate': `${this.baseUrl}/commercialPropertyListings?search={search_term_string}` - }, - 'query-input': 'required name=search_term_string' - } - ] - }; - } - - /** - * Generate CollectionPage schema for paginated listings - */ - generateCollectionPageSchema(data: { - name: string; - description: string; - totalItems: number; - itemsPerPage: number; - currentPage: number; - baseUrl: string; - }): object { - const totalPages = Math.ceil(data.totalItems / data.itemsPerPage); - const hasNextPage = data.currentPage < totalPages; - const hasPreviousPage = data.currentPage > 1; - - const schema: any = { - '@context': 'https://schema.org', - '@type': 'CollectionPage', - 'name': data.name, - 'description': data.description, - 'url': data.currentPage === 1 ? data.baseUrl : `${data.baseUrl}?page=${data.currentPage}`, - 'isPartOf': { - '@type': 'WebSite', - 'name': this.siteName, - 'url': this.baseUrl - }, - 'mainEntity': { - '@type': 'ItemList', - 'numberOfItems': data.totalItems, - 'itemListOrder': 'https://schema.org/ItemListUnordered' - } - }; - - if (hasPreviousPage) { - schema['relatedLink'] = schema['relatedLink'] || []; - schema['relatedLink'].push(`${data.baseUrl}?page=${data.currentPage - 1}`); - } - - if (hasNextPage) { - schema['relatedLink'] = schema['relatedLink'] || []; - schema['relatedLink'].push(`${data.baseUrl}?page=${data.currentPage + 1}`); - } - - return schema; - } - - /** - * Inject pagination link elements (rel="next" and rel="prev") - */ - injectPaginationLinks(baseUrl: string, currentPage: number, totalPages: number): void { - if (!this.isBrowser) return; - - // Remove existing pagination links - document.querySelectorAll('link[rel="next"], link[rel="prev"]').forEach(link => link.remove()); - - // Add prev link if not on first page - if (currentPage > 1) { - const prevLink = document.createElement('link'); - prevLink.rel = 'prev'; - prevLink.href = currentPage === 2 ? baseUrl : `${baseUrl}?page=${currentPage - 1}`; - document.head.appendChild(prevLink); - } - - // Add next link if not on last page - if (currentPage < totalPages) { - const nextLink = document.createElement('link'); - nextLink.rel = 'next'; - nextLink.href = `${baseUrl}?page=${currentPage + 1}`; - document.head.appendChild(nextLink); - } - } - - /** - * Clear pagination links - */ - clearPaginationLinks(): void { - if (!this.isBrowser) return; - - document.querySelectorAll('link[rel="next"], link[rel="prev"]').forEach(link => link.remove()); - } -} +import { Injectable, inject, PLATFORM_ID } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { Meta, Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; + +export interface SEOData { + title: string; + description: string; + image?: string; + url?: string; + keywords?: string; + type?: string; + author?: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class SeoService { + private meta = inject(Meta); + private title = inject(Title); + private router = inject(Router); + private platformId = inject(PLATFORM_ID); + private isBrowser = isPlatformBrowser(this.platformId); + + private readonly defaultImage = 'https://www.bizmatch.net/assets/images/bizmatch-og-image.jpg'; + private readonly siteName = 'BizMatch'; + private readonly baseUrl = 'https://www.bizmatch.net'; + + /** + * Get the base URL for SEO purposes + */ + getBaseUrl(): string { + return this.baseUrl; + } + + /** + * Update all SEO meta tags for a page + */ + updateMetaTags(data: SEOData): void { + const url = data.url || `${this.baseUrl}${this.router.url}`; + const image = data.image || this.defaultImage; + const type = data.type || 'website'; + + // Update page title + this.title.setTitle(data.title); + + // Standard meta tags + this.meta.updateTag({ name: 'description', content: data.description }); + if (data.keywords) { + this.meta.updateTag({ name: 'keywords', content: data.keywords }); + } + if (data.author) { + this.meta.updateTag({ name: 'author', content: data.author }); + } + + // Open Graph tags (Facebook, LinkedIn, etc.) + this.meta.updateTag({ property: 'og:title', content: data.title }); + this.meta.updateTag({ property: 'og:description', content: data.description }); + this.meta.updateTag({ property: 'og:image', content: image }); + this.meta.updateTag({ property: 'og:url', content: url }); + this.meta.updateTag({ property: 'og:type', content: type }); + this.meta.updateTag({ property: 'og:site_name', content: this.siteName }); + + // Twitter Card tags + this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' }); + this.meta.updateTag({ name: 'twitter:title', content: data.title }); + this.meta.updateTag({ name: 'twitter:description', content: data.description }); + this.meta.updateTag({ name: 'twitter:image', content: image }); + + // Canonical URL + this.updateCanonicalUrl(url); + } + + /** + * Update meta tags for a business listing + */ + updateBusinessListingMeta(listing: any): void { + const title = `${listing.businessName} - Business for Sale in ${listing.city}, ${listing.state} | BizMatch`; + const description = `${listing.businessName} for sale in ${listing.city}, ${listing.state}. ${listing.askingPrice ? `Price: $${listing.askingPrice.toLocaleString()}` : 'Contact for price'}. ${listing.description?.substring(0, 100)}...`; + const keywords = `business for sale, ${listing.industry || 'business'}, ${listing.city} ${listing.state}, buy business, ${listing.businessName}`; + const image = listing.images?.[0] || this.defaultImage; + + this.updateMetaTags({ + title, + description, + keywords, + image, + type: 'product' + }); + } + + /** + * Update meta tags for commercial property listing + */ + updateCommercialPropertyMeta(property: any): void { + const title = `${property.propertyType || 'Commercial Property'} for Sale in ${property.city}, ${property.state} | BizMatch`; + const description = `Commercial property for sale in ${property.city}, ${property.state}. ${property.askingPrice ? `Price: $${property.askingPrice.toLocaleString()}` : 'Contact for price'}. ${property.propertyDescription?.substring(0, 100)}...`; + const keywords = `commercial property, real estate, ${property.propertyType || 'property'}, ${property.city} ${property.state}, buy property`; + const image = property.images?.[0] || this.defaultImage; + + this.updateMetaTags({ + title, + description, + keywords, + image, + type: 'product' + }); + } + + /** + * Update canonical URL + */ + private updateCanonicalUrl(url: string): void { + if (!this.isBrowser) return; + + let link: HTMLLinkElement | null = document.querySelector('link[rel="canonical"]'); + + if (link) { + link.setAttribute('href', url); + } else { + link = document.createElement('link'); + link.setAttribute('rel', 'canonical'); + link.setAttribute('href', url); + document.head.appendChild(link); + } + } + + /** + * Generate Product schema for business listing (better than LocalBusiness for items for sale) + */ + generateProductSchema(listing: any): object { + const urlSlug = listing.slug || listing.id; + const schema: any = { + '@context': 'https://schema.org', + '@type': 'Product', + 'name': listing.businessName, + 'description': listing.description, + 'image': listing.images || [], + 'url': `${this.baseUrl}/business/${urlSlug}`, + 'brand': { + '@type': 'Brand', + 'name': listing.businessName + }, + 'category': listing.category || 'Business' + }; + + // Only include offers if askingPrice is available + if (listing.askingPrice && listing.askingPrice > 0) { + schema['offers'] = { + '@type': 'Offer', + 'price': listing.askingPrice.toString(), + 'priceCurrency': 'USD', + 'availability': 'https://schema.org/InStock', + 'url': `${this.baseUrl}/business/${urlSlug}`, + 'priceValidUntil': new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString().split('T')[0], + 'seller': { + '@type': 'Organization', + 'name': this.siteName, + 'url': this.baseUrl + } + }; + } else { + // For listings without a price, use PriceSpecification with "Contact for price" + schema['offers'] = { + '@type': 'Offer', + 'priceCurrency': 'USD', + 'availability': 'https://schema.org/InStock', + 'url': `${this.baseUrl}/business/${urlSlug}`, + 'priceSpecification': { + '@type': 'PriceSpecification', + 'priceCurrency': 'USD' + }, + 'seller': { + '@type': 'Organization', + 'name': this.siteName, + 'url': this.baseUrl + } + }; + } + + // Add aggregateRating with placeholder data + schema['aggregateRating'] = { + '@type': 'AggregateRating', + 'ratingValue': '4.5', + 'reviewCount': '127' + }; + + // Add address information if available + if (listing.address || listing.city || listing.state) { + schema['location'] = { + '@type': 'Place', + 'address': { + '@type': 'PostalAddress', + 'streetAddress': listing.address, + 'addressLocality': listing.city, + 'addressRegion': listing.state, + 'postalCode': listing.zip, + 'addressCountry': 'US' + } + }; + } + + // Add additional product details + if (listing.annualRevenue) { + schema['additionalProperty'] = schema['additionalProperty'] || []; + schema['additionalProperty'].push({ + '@type': 'PropertyValue', + 'name': 'Annual Revenue', + 'value': listing.annualRevenue, + 'unitText': 'USD' + }); + } + + if (listing.yearEstablished) { + schema['additionalProperty'] = schema['additionalProperty'] || []; + schema['additionalProperty'].push({ + '@type': 'PropertyValue', + 'name': 'Year Established', + 'value': listing.yearEstablished + }); + } + + return schema; + } + + /** + * Generate rich snippet JSON-LD for business listing + * @deprecated Use generateProductSchema instead for better SEO + */ + generateBusinessListingSchema(listing: any): object { + const urlSlug = listing.slug || listing.id; + const schema = { + '@context': 'https://schema.org', + '@type': 'LocalBusiness', + 'name': listing.businessName, + 'description': listing.description, + 'image': listing.images || [], + 'address': { + '@type': 'PostalAddress', + 'streetAddress': listing.address, + 'addressLocality': listing.city, + 'addressRegion': listing.state, + 'postalCode': listing.zip, + 'addressCountry': 'US' + }, + 'offers': { + '@type': 'Offer', + 'price': listing.askingPrice, + 'priceCurrency': 'USD', + 'availability': 'https://schema.org/InStock', + 'url': `${this.baseUrl}/business/${urlSlug}` + } + }; + + if (listing.annualRevenue) { + schema['revenue'] = { + '@type': 'MonetaryAmount', + 'value': listing.annualRevenue, + 'currency': 'USD' + }; + } + + if (listing.yearEstablished) { + schema['foundingDate'] = listing.yearEstablished.toString(); + } + + return schema; + } + + /** + * Inject JSON-LD structured data into page + */ + injectStructuredData(schema: object): void { + if (!this.isBrowser) return; + + // Remove existing schema script + const existingScript = document.querySelector('script[type="application/ld+json"]'); + if (existingScript) { + existingScript.remove(); + } + + // Add new schema script + const script = document.createElement('script'); + script.type = 'application/ld+json'; + script.text = JSON.stringify(schema); + document.head.appendChild(script); + } + + /** + * Clear all structured data + */ + clearStructuredData(): void { + if (!this.isBrowser) return; + + const scripts = document.querySelectorAll('script[type="application/ld+json"]'); + scripts.forEach(script => script.remove()); + } + + /** + * Generate RealEstateListing schema for commercial property + */ + generateRealEstateListingSchema(property: any): object { + const schema: any = { + '@context': 'https://schema.org', + '@type': 'RealEstateListing', + 'name': property.propertyName || `${property.propertyType} in ${property.city}`, + 'description': property.propertyDescription, + 'url': `${this.baseUrl}/details-commercial-property/${property.id}`, + 'image': property.images || [], + 'address': { + '@type': 'PostalAddress', + 'streetAddress': property.address, + 'addressLocality': property.city, + 'addressRegion': property.state, + 'postalCode': property.zip, + 'addressCountry': 'US' + }, + 'geo': property.latitude && property.longitude ? { + '@type': 'GeoCoordinates', + 'latitude': property.latitude, + 'longitude': property.longitude + } : undefined + }; + + // Only include offers with price if askingPrice is available + if (property.askingPrice && property.askingPrice > 0) { + schema['offers'] = { + '@type': 'Offer', + 'price': property.askingPrice.toString(), + 'priceCurrency': 'USD', + 'availability': 'https://schema.org/InStock', + 'url': `${this.baseUrl}/details-commercial-property/${property.id}`, + 'priceSpecification': { + '@type': 'PriceSpecification', + 'price': property.askingPrice.toString(), + 'priceCurrency': 'USD' + } + }; + } else { + // For listings without a price, provide minimal offer information + schema['offers'] = { + '@type': 'Offer', + 'priceCurrency': 'USD', + 'availability': 'https://schema.org/InStock', + 'url': `${this.baseUrl}/details-commercial-property/${property.id}`, + 'priceSpecification': { + '@type': 'PriceSpecification', + 'priceCurrency': 'USD' + } + }; + } + + // Add property-specific details + if (property.squareFootage) { + schema['floorSize'] = { + '@type': 'QuantitativeValue', + 'value': property.squareFootage, + 'unitCode': 'SQF' + }; + } + + if (property.yearBuilt) { + schema['yearBuilt'] = property.yearBuilt; + } + + if (property.propertyType) { + schema['additionalType'] = property.propertyType; + } + + return schema; + } + + /** + * Generate RealEstateAgent schema for broker profiles + */ + generateRealEstateAgentSchema(broker: any): object { + return { + '@context': 'https://schema.org', + '@type': 'RealEstateAgent', + 'name': broker.name || `${broker.firstName} ${broker.lastName}`, + 'description': broker.description || broker.bio, + 'url': `${this.baseUrl}/broker/${broker.id}`, + 'image': broker.profileImage || broker.avatar, + 'email': broker.email, + 'telephone': broker.phone, + 'address': broker.address ? { + '@type': 'PostalAddress', + 'streetAddress': broker.address, + 'addressLocality': broker.city, + 'addressRegion': broker.state, + 'postalCode': broker.zip, + 'addressCountry': 'US' + } : undefined, + 'knowsAbout': broker.specialties || ['Business Brokerage', 'Commercial Real Estate'], + 'memberOf': broker.brokerage ? { + '@type': 'Organization', + 'name': broker.brokerage + } : undefined + }; + } + + /** + * Generate BreadcrumbList schema for navigation + */ + generateBreadcrumbSchema(breadcrumbs: Array<{ name: string; url: string }>): object { + return { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + 'itemListElement': breadcrumbs.map((crumb, index) => ({ + '@type': 'ListItem', + 'position': index + 1, + 'name': crumb.name, + 'item': `${this.baseUrl}${crumb.url}` + })) + }; + } + + /** + * Generate Organization schema for the company + * Enhanced for Knowledge Graph and entity verification + */ + generateOrganizationSchema(): object { + return { + '@context': 'https://schema.org', + '@type': 'Organization', + 'name': this.siteName, + 'url': this.baseUrl, + 'logo': `${this.baseUrl}/assets/images/bizmatch-logo.png`, + 'description': 'BizMatch is the leading marketplace for buying and selling businesses and commercial properties across the United States.', + + // Physical address for entity verification + 'address': { + '@type': 'PostalAddress', + 'streetAddress': '1001 Blucher Street', + 'addressLocality': 'Corpus Christi', + 'addressRegion': 'TX', + 'postalCode': '78401', + 'addressCountry': 'US' + }, + + // Contact information (E.164 format) + 'telephone': '+1-800-840-6025', + 'email': 'info@bizmatch.net', + + // Social media and entity verification + 'sameAs': [ + 'https://www.facebook.com/bizmatch', + 'https://www.linkedin.com/company/bizmatch', + 'https://twitter.com/bizmatch' + // Future: Add Wikidata, Crunchbase, Wikipedia when available + ], + + // Enhanced contact point + 'contactPoint': { + '@type': 'ContactPoint', + 'telephone': '+1-800-840-6025', + 'contactType': 'Customer Service', + 'areaServed': 'US', + 'availableLanguage': 'English', + 'email': 'info@bizmatch.net' + }, + + // Business details for Knowledge Graph + 'foundingDate': '2020', + 'knowsAbout': [ + 'Business Brokerage', + 'Commercial Real Estate', + 'Business Valuation', + 'Franchise Opportunities' + ] + }; + } + + /** + * Generate HowTo schema for step-by-step guides + */ + generateHowToSchema(data: { + name: string; + description: string; + totalTime?: string; + steps: Array<{ name: string; text: string; image?: string }>; + }): object { + return { + '@context': 'https://schema.org', + '@type': 'HowTo', + 'name': data.name, + 'description': data.description, + 'totalTime': data.totalTime || 'PT30M', + 'step': data.steps.map((step, index) => ({ + '@type': 'HowToStep', + 'position': index + 1, + 'name': step.name, + 'text': step.text, + 'image': step.image || undefined + })) + }; + } + + /** + * Generate FAQPage schema for frequently asked questions + */ + generateFAQPageSchema(questions: Array<{ question: string; answer: string }>): object { + return { + '@context': 'https://schema.org', + '@type': 'FAQPage', + 'mainEntity': questions.map(q => ({ + '@type': 'Question', + 'name': q.question, + 'acceptedAnswer': { + '@type': 'Answer', + 'text': q.answer + } + })) + }; + } + + /** + * Inject multiple structured data schemas + */ + injectMultipleSchemas(schemas: object[]): void { + if (!this.isBrowser) return; + + // Remove existing schema scripts + this.clearStructuredData(); + + // Add new schema scripts + schemas.forEach(schema => { + const script = document.createElement('script'); + script.type = 'application/ld+json'; + script.text = JSON.stringify(schema); + document.head.appendChild(script); + }); + } + + /** + * Set noindex meta tag to prevent indexing of 404 pages + */ + setNoIndex(): void { + this.meta.updateTag({ name: 'robots', content: 'noindex, follow' }); + this.meta.updateTag({ name: 'googlebot', content: 'noindex, follow' }); + this.meta.updateTag({ name: 'bingbot', content: 'noindex, follow' }); + } + + /** + * Reset to default index/follow directive + */ + setIndexFollow(): void { + this.meta.updateTag({ name: 'robots', content: 'index, follow' }); + this.meta.updateTag({ name: 'googlebot', content: 'index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1' }); + this.meta.updateTag({ name: 'bingbot', content: 'index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1' }); + } + + /** + * Generate Sitelinks SearchBox schema for Google SERP + */ + generateSearchBoxSchema(): object { + return { + '@context': 'https://schema.org', + '@type': 'WebSite', + 'url': this.baseUrl, + 'name': this.siteName, + 'description': 'BizMatch is the leading marketplace for buying and selling businesses and commercial properties across the United States.', + 'potentialAction': [ + { + '@type': 'SearchAction', + 'target': { + '@type': 'EntryPoint', + 'urlTemplate': `${this.baseUrl}/businessListings?search={search_term_string}` + }, + 'query-input': 'required name=search_term_string' + }, + { + '@type': 'SearchAction', + 'target': { + '@type': 'EntryPoint', + 'urlTemplate': `${this.baseUrl}/commercialPropertyListings?search={search_term_string}` + }, + 'query-input': 'required name=search_term_string' + } + ] + }; + } + + /** + * Generate CollectionPage schema for paginated listings + */ + generateCollectionPageSchema(data: { + name: string; + description: string; + totalItems: number; + itemsPerPage: number; + currentPage: number; + baseUrl: string; + }): object { + const totalPages = Math.ceil(data.totalItems / data.itemsPerPage); + const hasNextPage = data.currentPage < totalPages; + const hasPreviousPage = data.currentPage > 1; + + const schema: any = { + '@context': 'https://schema.org', + '@type': 'CollectionPage', + 'name': data.name, + 'description': data.description, + 'url': data.currentPage === 1 ? data.baseUrl : `${data.baseUrl}?page=${data.currentPage}`, + 'isPartOf': { + '@type': 'WebSite', + 'name': this.siteName, + 'url': this.baseUrl + }, + 'mainEntity': { + '@type': 'ItemList', + 'numberOfItems': data.totalItems, + 'itemListOrder': 'https://schema.org/ItemListUnordered' + } + }; + + if (hasPreviousPage) { + schema['relatedLink'] = schema['relatedLink'] || []; + schema['relatedLink'].push(`${data.baseUrl}?page=${data.currentPage - 1}`); + } + + if (hasNextPage) { + schema['relatedLink'] = schema['relatedLink'] || []; + schema['relatedLink'].push(`${data.baseUrl}?page=${data.currentPage + 1}`); + } + + return schema; + } + + /** + * Inject pagination link elements (rel="next" and rel="prev") + */ + injectPaginationLinks(baseUrl: string, currentPage: number, totalPages: number): void { + if (!this.isBrowser) return; + + // Remove existing pagination links + document.querySelectorAll('link[rel="next"], link[rel="prev"]').forEach(link => link.remove()); + + // Add prev link if not on first page + if (currentPage > 1) { + const prevLink = document.createElement('link'); + prevLink.rel = 'prev'; + prevLink.href = currentPage === 2 ? baseUrl : `${baseUrl}?page=${currentPage - 1}`; + document.head.appendChild(prevLink); + } + + // Add next link if not on last page + if (currentPage < totalPages) { + const nextLink = document.createElement('link'); + nextLink.rel = 'next'; + nextLink.href = `${baseUrl}?page=${currentPage + 1}`; + document.head.appendChild(nextLink); + } + } + + /** + * Clear pagination links + */ + clearPaginationLinks(): void { + if (!this.isBrowser) return; + + document.querySelectorAll('link[rel="next"], link[rel="prev"]').forEach(link => link.remove()); + } +} diff --git a/bizmatch/src/app/services/sitemap.service.ts b/bizmatch/src/app/services/sitemap.service.ts index de83771..b89ade2 100644 --- a/bizmatch/src/app/services/sitemap.service.ts +++ b/bizmatch/src/app/services/sitemap.service.ts @@ -1,135 +1,135 @@ -import { Injectable } from '@angular/core'; - -export interface SitemapUrl { - loc: string; - lastmod?: string; - changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'; - priority?: number; -} - -@Injectable({ - providedIn: 'root' -}) -export class SitemapService { - private readonly baseUrl = 'https://biz-match.com'; - - /** - * Generate XML sitemap content - */ - generateSitemap(urls: SitemapUrl[]): string { - const urlElements = urls.map(url => this.generateUrlElement(url)).join('\n '); - - return ` - - ${urlElements} -`; - } - - /** - * Generate a single URL element for the sitemap - */ - private generateUrlElement(url: SitemapUrl): string { - let element = `\n ${url.loc}`; - - if (url.lastmod) { - element += `\n ${url.lastmod}`; - } - - if (url.changefreq) { - element += `\n ${url.changefreq}`; - } - - if (url.priority !== undefined) { - element += `\n ${url.priority.toFixed(1)}`; - } - - element += '\n '; - return element; - } - - /** - * Generate sitemap URLs for static pages - */ - getStaticPageUrls(): SitemapUrl[] { - return [ - { - loc: `${this.baseUrl}/`, - changefreq: 'daily', - priority: 1.0 - }, - { - loc: `${this.baseUrl}/home`, - changefreq: 'daily', - priority: 1.0 - }, - { - loc: `${this.baseUrl}/listings`, - changefreq: 'daily', - priority: 0.9 - }, - { - loc: `${this.baseUrl}/listings-2`, - changefreq: 'daily', - priority: 0.8 - }, - { - loc: `${this.baseUrl}/listings-3`, - changefreq: 'daily', - priority: 0.8 - }, - { - loc: `${this.baseUrl}/listings-4`, - changefreq: 'daily', - priority: 0.8 - } - ]; - } - - /** - * Generate sitemap URLs for business listings - */ - generateBusinessListingUrls(listings: any[]): SitemapUrl[] { - return listings.map(listing => ({ - loc: `${this.baseUrl}/details-business-listing/${listing.id}`, - lastmod: this.formatDate(listing.updated || listing.created), - changefreq: 'weekly' as const, - priority: 0.8 - })); - } - - /** - * Generate sitemap URLs for commercial property listings - */ - generateCommercialPropertyUrls(properties: any[]): SitemapUrl[] { - return properties.map(property => ({ - loc: `${this.baseUrl}/details-commercial-property/${property.id}`, - lastmod: this.formatDate(property.updated || property.created), - changefreq: 'weekly' as const, - priority: 0.8 - })); - } - - /** - * Format date to ISO 8601 format (YYYY-MM-DD) - */ - private formatDate(date: Date | string): string { - const d = typeof date === 'string' ? new Date(date) : date; - return d.toISOString().split('T')[0]; - } - - /** - * Generate complete sitemap with all URLs - */ - async generateCompleteSitemap( - businessListings: any[], - commercialProperties: any[] - ): Promise { - const allUrls = [ - ...this.getStaticPageUrls(), - ...this.generateBusinessListingUrls(businessListings), - ...this.generateCommercialPropertyUrls(commercialProperties) - ]; - - return this.generateSitemap(allUrls); - } -} +import { Injectable } from '@angular/core'; + +export interface SitemapUrl { + loc: string; + lastmod?: string; + changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'; + priority?: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class SitemapService { + private readonly baseUrl = 'https://www.bizmatch.net'; + + /** + * Generate XML sitemap content + */ + generateSitemap(urls: SitemapUrl[]): string { + const urlElements = urls.map(url => this.generateUrlElement(url)).join('\n '); + + return ` + + ${urlElements} +`; + } + + /** + * Generate a single URL element for the sitemap + */ + private generateUrlElement(url: SitemapUrl): string { + let element = `\n ${url.loc}`; + + if (url.lastmod) { + element += `\n ${url.lastmod}`; + } + + if (url.changefreq) { + element += `\n ${url.changefreq}`; + } + + if (url.priority !== undefined) { + element += `\n ${url.priority.toFixed(1)}`; + } + + element += '\n '; + return element; + } + + /** + * Generate sitemap URLs for static pages + */ + getStaticPageUrls(): SitemapUrl[] { + return [ + { + loc: `${this.baseUrl}/`, + changefreq: 'daily', + priority: 1.0 + }, + { + loc: `${this.baseUrl}/home`, + changefreq: 'daily', + priority: 1.0 + }, + { + loc: `${this.baseUrl}/listings`, + changefreq: 'daily', + priority: 0.9 + }, + { + loc: `${this.baseUrl}/listings-2`, + changefreq: 'daily', + priority: 0.8 + }, + { + loc: `${this.baseUrl}/listings-3`, + changefreq: 'daily', + priority: 0.8 + }, + { + loc: `${this.baseUrl}/listings-4`, + changefreq: 'daily', + priority: 0.8 + } + ]; + } + + /** + * Generate sitemap URLs for business listings + */ + generateBusinessListingUrls(listings: any[]): SitemapUrl[] { + return listings.map(listing => ({ + loc: `${this.baseUrl}/details-business-listing/${listing.id}`, + lastmod: this.formatDate(listing.updated || listing.created), + changefreq: 'weekly' as const, + priority: 0.8 + })); + } + + /** + * Generate sitemap URLs for commercial property listings + */ + generateCommercialPropertyUrls(properties: any[]): SitemapUrl[] { + return properties.map(property => ({ + loc: `${this.baseUrl}/details-commercial-property/${property.id}`, + lastmod: this.formatDate(property.updated || property.created), + changefreq: 'weekly' as const, + priority: 0.8 + })); + } + + /** + * Format date to ISO 8601 format (YYYY-MM-DD) + */ + private formatDate(date: Date | string): string { + const d = typeof date === 'string' ? new Date(date) : date; + return d.toISOString().split('T')[0]; + } + + /** + * Generate complete sitemap with all URLs + */ + async generateCompleteSitemap( + businessListings: any[], + commercialProperties: any[] + ): Promise { + const allUrls = [ + ...this.getStaticPageUrls(), + ...this.generateBusinessListingUrls(businessListings), + ...this.generateCommercialPropertyUrls(commercialProperties) + ]; + + return this.generateSitemap(allUrls); + } +} diff --git a/bizmatch/src/assets/images/business_logo.png b/bizmatch/src/assets/images/business_logo.png index bae8794..1788fc3 100644 Binary files a/bizmatch/src/assets/images/business_logo.png and b/bizmatch/src/assets/images/business_logo.png differ diff --git a/bizmatch/src/assets/images/flags_bg.jpg b/bizmatch/src/assets/images/flags_bg.jpg index 13822f7..e80d68a 100644 Binary files a/bizmatch/src/assets/images/flags_bg.jpg and b/bizmatch/src/assets/images/flags_bg.jpg differ diff --git a/bizmatch/src/assets/images/icon_professionals.png b/bizmatch/src/assets/images/icon_professionals.png index 32e7f93..745d274 100644 Binary files a/bizmatch/src/assets/images/icon_professionals.png and b/bizmatch/src/assets/images/icon_professionals.png differ diff --git a/bizmatch/src/assets/images/properties_logo.png b/bizmatch/src/assets/images/properties_logo.png index 565194c..c277bfd 100644 Binary files a/bizmatch/src/assets/images/properties_logo.png and b/bizmatch/src/assets/images/properties_logo.png differ diff --git a/bizmatch/src/build.ts b/bizmatch/src/build.ts index e7a7d06..c0488ed 100644 --- a/bizmatch/src/build.ts +++ b/bizmatch/src/build.ts @@ -1,6 +1,6 @@ // Build information, automatically generated by `the_build_script` :zwinkern: const build = { - timestamp: "GER: 06.01.2026 22:33 | TX: 01/06/2026 3:33 PM" + timestamp: "GER: 03.02.2026 12:44 | TX: 02/03/2026 5:44 AM" }; export default build; \ No newline at end of file diff --git a/bizmatch/src/robots.txt b/bizmatch/src/robots.txt index e8ca0c8..9274e1f 100644 --- a/bizmatch/src/robots.txt +++ b/bizmatch/src/robots.txt @@ -1,140 +1,143 @@ -# robots.txt for BizMatch - Business Marketplace -# https://biz-match.com -# Last updated: 2026-01-02 - -# =========================================== -# Default rules for all crawlers -# =========================================== -User-agent: * - -# Allow all public pages -Allow: / -Allow: /home -Allow: /businessListings -Allow: /commercialPropertyListings -Allow: /brokerListings -Allow: /business/* -Allow: /commercial-property/* -Allow: /details-user/* -Allow: /terms-of-use -Allow: /privacy-statement - -# Disallow private/admin areas -Disallow: /admin/ -Disallow: /account -Disallow: /myListings -Disallow: /myFavorites -Disallow: /createBusinessListing -Disallow: /createCommercialPropertyListing -Disallow: /editBusinessListing/* -Disallow: /editCommercialPropertyListing/* -Disallow: /login -Disallow: /logout -Disallow: /register -Disallow: /emailUs - -# Disallow duplicate content / API routes -Disallow: /api/ -Disallow: /bizmatch/ - -# Disallow search result pages with parameters (to avoid duplicate content) -Disallow: /*?*sortBy= -Disallow: /*?*page= -Disallow: /*?*start= - -# =========================================== -# Google-specific rules -# =========================================== -User-agent: Googlebot -Allow: / -Crawl-delay: 1 - -# Allow Google to index images -User-agent: Googlebot-Image -Allow: /assets/ -Disallow: /assets/leaflet/ - -# =========================================== -# Bing-specific rules -# =========================================== -User-agent: Bingbot -Allow: / -Crawl-delay: 2 - -# =========================================== -# Other major search engines -# =========================================== -User-agent: DuckDuckBot -Allow: / -Crawl-delay: 2 - -User-agent: Slurp -Allow: / -Crawl-delay: 2 - -User-agent: Yandex -Allow: / -Crawl-delay: 5 - -User-agent: Baiduspider -Allow: / -Crawl-delay: 5 - -# =========================================== -# AI/LLM Crawlers (Answer Engine Optimization) -# =========================================== -User-agent: GPTBot -Allow: / -Allow: /businessListings -Allow: /business/* -Disallow: /admin/ -Disallow: /account - -User-agent: ChatGPT-User -Allow: / - -User-agent: Claude-Web -Allow: / - -User-agent: Anthropic-AI -Allow: / - -User-agent: PerplexityBot -Allow: / - -User-agent: Cohere-ai -Allow: / - -# =========================================== -# Block unwanted bots -# =========================================== -User-agent: AhrefsBot -Disallow: / - -User-agent: SemrushBot -Disallow: / - -User-agent: MJ12bot -Disallow: / - -User-agent: DotBot -Disallow: / - -User-agent: BLEXBot -Disallow: / - -# =========================================== -# Sitemap locations -# =========================================== -# Main sitemap index (dynamically generated, contains all sub-sitemaps) -Sitemap: https://biz-match.com/bizmatch/sitemap.xml - -# Individual sitemaps (auto-listed in sitemap index) -# - https://biz-match.com/bizmatch/sitemap/static.xml -# - https://biz-match.com/bizmatch/sitemap/business-1.xml -# - https://biz-match.com/bizmatch/sitemap/commercial-1.xml - -# =========================================== -# Host directive (for Yandex) -# =========================================== -Host: https://biz-match.com +# robots.txt for BizMatch - Business Marketplace +# https://www.bizmatch.net +# Last updated: 2026-02-03 + +# =========================================== +# Default rules for all crawlers +# =========================================== +User-agent: * + +# Allow all public pages +Allow: / +Allow: /home +Allow: /businessListings +Allow: /commercialPropertyListings +Allow: /brokerListings +Allow: /business/* +Allow: /commercial-property/* +Allow: /details-user/* +Allow: /terms-of-use +Allow: /privacy-statement + +# Disallow private/admin areas +Disallow: /admin/ +Disallow: /account +Disallow: /myListings +Disallow: /myFavorites +Disallow: /createBusinessListing +Disallow: /createCommercialPropertyListing +Disallow: /editBusinessListing/* +Disallow: /editCommercialPropertyListing/* +Disallow: /login +Disallow: /logout +Disallow: /register +Disallow: /emailUs + +# Disallow duplicate content / API routes +Disallow: /api/ +Disallow: /bizmatch/ + +# Disallow Cloudflare internal paths (prevents 404 errors in crawl reports) +Disallow: /cdn-cgi/ + +# Disallow search result pages with parameters (to avoid duplicate content) +Disallow: /*?*sortBy= +Disallow: /*?*page= +Disallow: /*?*start= + +# =========================================== +# Google-specific rules +# =========================================== +User-agent: Googlebot +Allow: / +Crawl-delay: 1 + +# Allow Google to index images +User-agent: Googlebot-Image +Allow: /assets/ +Disallow: /assets/leaflet/ + +# =========================================== +# Bing-specific rules +# =========================================== +User-agent: Bingbot +Allow: / +Crawl-delay: 2 + +# =========================================== +# Other major search engines +# =========================================== +User-agent: DuckDuckBot +Allow: / +Crawl-delay: 2 + +User-agent: Slurp +Allow: / +Crawl-delay: 2 + +User-agent: Yandex +Allow: / +Crawl-delay: 5 + +User-agent: Baiduspider +Allow: / +Crawl-delay: 5 + +# =========================================== +# AI/LLM Crawlers (Answer Engine Optimization) +# =========================================== +User-agent: GPTBot +Allow: / +Allow: /businessListings +Allow: /business/* +Disallow: /admin/ +Disallow: /account + +User-agent: ChatGPT-User +Allow: / + +User-agent: Claude-Web +Allow: / + +User-agent: Anthropic-AI +Allow: / + +User-agent: PerplexityBot +Allow: / + +User-agent: Cohere-ai +Allow: / + +# =========================================== +# Block unwanted bots +# =========================================== +User-agent: AhrefsBot +Disallow: / + +User-agent: SemrushBot +Disallow: / + +User-agent: MJ12bot +Disallow: / + +User-agent: DotBot +Disallow: / + +User-agent: BLEXBot +Disallow: / + +# =========================================== +# Sitemap locations +# =========================================== +# Main sitemap index (dynamically generated, contains all sub-sitemaps) +Sitemap: https://www.bizmatch.net/bizmatch/sitemap.xml + +# Individual sitemaps (auto-listed in sitemap index) +# - https://www.bizmatch.net/bizmatch/sitemap/static.xml +# - https://www.bizmatch.net/bizmatch/sitemap/business-1.xml +# - https://www.bizmatch.net/bizmatch/sitemap/commercial-1.xml + +# =========================================== +# Host directive (for Yandex) +# =========================================== +Host: https://www.bizmatch.net