Compare commits
13 Commits
timo
...
00597a796a
| Author | SHA1 | Date | |
|---|---|---|---|
| 00597a796a | |||
| 8b3c79b5ff | |||
| a7d3d2d958 | |||
| 49528a5c37 | |||
| 047c723364 | |||
| 39c93e7178 | |||
| 6f1109d593 | |||
| 70a50e0ff6 | |||
| 23f7caedeb | |||
| 737329794c | |||
|
|
ff7ef0f423 | ||
|
|
e25722d806 | ||
|
|
bf735ed60f |
@@ -26,7 +26,9 @@
|
||||
"Bash(ls:*)",
|
||||
"WebFetch(domain:angular.dev)",
|
||||
"Bash(killall:*)",
|
||||
"Bash(echo:*)"
|
||||
"Bash(echo:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(npx tsc:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
* text=auto eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
@@ -47,6 +47,7 @@
|
||||
"fs-extra": "^11.2.0",
|
||||
"groq-sdk": "^0.5.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.1.0",
|
||||
"nest-winston": "^1.9.4",
|
||||
"nestjs-cls": "^5.4.0",
|
||||
"nodemailer": "^7.0.12",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LoggerService } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
@@ -22,6 +23,37 @@ async function bootstrap() {
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
allowedHeaders: 'Content-Type, Accept, Authorization, x-hide-loading',
|
||||
});
|
||||
|
||||
// Security Headers with helmet
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://fonts.googleapis.com"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
connectSrc: ["'self'", "https://api.bizmatch.net", "https://*.firebaseapp.com", "https://*.googleapis.com"],
|
||||
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
|
||||
objectSrc: ["'none'"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'self'"],
|
||||
},
|
||||
},
|
||||
crossOriginEmbedderPolicy: false, // Disable for now to avoid breaking existing functionality
|
||||
hsts: {
|
||||
maxAge: 31536000,
|
||||
includeSubDomains: true,
|
||||
preload: true,
|
||||
},
|
||||
frameguard: {
|
||||
action: 'sameorigin', // Allow same-origin framing
|
||||
},
|
||||
crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' }, // Allow popups for OAuth
|
||||
crossOriginResourcePolicy: { policy: 'cross-origin' }, // Allow cross-origin resources
|
||||
}),
|
||||
);
|
||||
|
||||
await app.listen(process.env.PORT || 3001);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@@ -18,7 +18,7 @@ interface SitemapIndexEntry {
|
||||
|
||||
@Injectable()
|
||||
export class SitemapService {
|
||||
private readonly baseUrl = 'https://biz-match.com';
|
||||
private readonly baseUrl = 'https://www.bizmatch.net';
|
||||
private readonly URLS_PER_SITEMAP = 10000; // Google best practice
|
||||
|
||||
constructor(@Inject(PG_CONNECTION) private readonly db: NodePgDatabase<typeof schema>) { }
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
"maximumWarning": "30kb",
|
||||
"maximumError": "30kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
|
||||
1
bizmatch/public/googleccd5315437d68a49.html
Normal file
@@ -0,0 +1 @@
|
||||
google-site-verification: googleccd5315437d68a49.html
|
||||
@@ -26,6 +26,74 @@ export async function app(): Promise<express.Express> {
|
||||
server.set('view engine', 'html');
|
||||
server.set('views', browserDistFolder);
|
||||
|
||||
// Sitemap XML endpoints - MUST be before static files middleware
|
||||
server.get('/sitemap.xml', async (req, res) => {
|
||||
try {
|
||||
const sitemapIndexXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap>
|
||||
<loc>https://www.bizmatch.net/sitemap-static.xml</loc>
|
||||
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
|
||||
</sitemap>
|
||||
</sitemapindex>`;
|
||||
|
||||
res.header('Content-Type', 'application/xml; charset=utf-8');
|
||||
res.send(sitemapIndexXml);
|
||||
} catch (error) {
|
||||
console.error('[SSR] Error generating sitemap index:', error);
|
||||
res.status(500).send('Error generating sitemap');
|
||||
}
|
||||
});
|
||||
|
||||
server.get('/sitemap-static.xml', async (req, res) => {
|
||||
try {
|
||||
const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/home</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/businessListings</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/commercialPropertyListings</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/brokerListings</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/terms-of-use</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.bizmatch.net/privacy-statement</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
</urlset>`;
|
||||
|
||||
res.header('Content-Type', 'application/xml; charset=utf-8');
|
||||
res.send(sitemapXml);
|
||||
} catch (error) {
|
||||
console.error('[SSR] Error generating static sitemap:', error);
|
||||
res.status(500).send('Error generating sitemap');
|
||||
}
|
||||
});
|
||||
|
||||
// Example Express Rest API endpoints
|
||||
// server.get('/api/**', (req, res) => { });
|
||||
// Serve static files from /browser
|
||||
|
||||
@@ -7,10 +7,6 @@ import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@a
|
||||
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
|
||||
import { getAuth, provideAuth } from '@angular/fire/auth';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { GALLERY_CONFIG, GalleryConfig } from 'ng-gallery';
|
||||
import { provideQuillConfig } from 'ngx-quill';
|
||||
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
|
||||
import { shareIcons } from 'ngx-sharebuttons/icons';
|
||||
import { environment } from '../environments/environment';
|
||||
import { routes } from './app.routes';
|
||||
import { AuthInterceptor } from './interceptors/auth.interceptor';
|
||||
@@ -48,13 +44,6 @@ export const appConfig: ApplicationConfig = {
|
||||
provide: 'TIMEOUT_DURATION',
|
||||
useValue: 5000, // Standard-Timeout von 5 Sekunden
|
||||
},
|
||||
{
|
||||
provide: GALLERY_CONFIG,
|
||||
useValue: {
|
||||
autoHeight: true,
|
||||
imageSize: 'cover',
|
||||
} as GalleryConfig,
|
||||
},
|
||||
{ provide: ErrorHandler, useClass: GlobalErrorHandler }, // Registriere den globalen ErrorHandler
|
||||
{
|
||||
provide: IMAGE_CONFIG,
|
||||
@@ -62,13 +51,6 @@ export const appConfig: ApplicationConfig = {
|
||||
disableImageSizeWarning: true,
|
||||
},
|
||||
},
|
||||
provideShareButtonsOptions(
|
||||
shareIcons(),
|
||||
withConfig({
|
||||
debug: true,
|
||||
sharerMethod: SharerMethods.Anchor,
|
||||
}),
|
||||
),
|
||||
provideRouter(
|
||||
routes,
|
||||
withEnabledBlockingInitialNavigation(),
|
||||
@@ -79,18 +61,6 @@ export const appConfig: ApplicationConfig = {
|
||||
),
|
||||
...(environment.production ? [POSTHOG_INIT_PROVIDER] : []),
|
||||
provideAnimations(),
|
||||
provideQuillConfig({
|
||||
modules: {
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'], // Einige Standardoptionen
|
||||
[{ header: [1, 2, 3, false] }], // Benutzerdefinierte Header
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ color: [] }], // Dropdown mit Standardfarben
|
||||
['clean'], // Entfernt Formatierungen
|
||||
],
|
||||
},
|
||||
}),
|
||||
provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
|
||||
provideAuth(() => getAuth()),
|
||||
],
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
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';
|
||||
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';
|
||||
|
||||
// Public pages - HomeComponent stays eagerly loaded as landing page
|
||||
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';
|
||||
|
||||
// Note: All listing and details components are now lazy-loaded for better initial bundle size
|
||||
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'test-ssr',
|
||||
@@ -33,17 +27,17 @@ export const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'businessListings',
|
||||
component: BusinessListingsComponent,
|
||||
loadComponent: () => import('./pages/listings/business-listings/business-listings.component').then(m => m.BusinessListingsComponent),
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'commercialPropertyListings',
|
||||
component: CommercialPropertyListingsComponent,
|
||||
loadComponent: () => import('./pages/listings/commercial-property-listings/commercial-property-listings.component').then(m => m.CommercialPropertyListingsComponent),
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'brokerListings',
|
||||
component: BrokerListingsComponent,
|
||||
loadComponent: () => import('./pages/listings/broker-listings/broker-listings.component').then(m => m.BrokerListingsComponent),
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -54,11 +48,11 @@ export const routes: Routes = [
|
||||
// Listings Details - New SEO-friendly slug-based URLs
|
||||
{
|
||||
path: 'business/:slug',
|
||||
component: DetailsBusinessListingComponent,
|
||||
loadComponent: () => import('./pages/details/details-business-listing/details-business-listing.component').then(m => m.DetailsBusinessListingComponent),
|
||||
},
|
||||
{
|
||||
path: 'commercial-property/:slug',
|
||||
component: DetailsCommercialPropertyListingComponent,
|
||||
loadComponent: () => import('./pages/details/details-commercial-property-listing/details-commercial-property-listing.component').then(m => m.DetailsCommercialPropertyListingComponent),
|
||||
},
|
||||
// Backward compatibility redirects for old UUID-based URLs
|
||||
{
|
||||
@@ -96,61 +90,61 @@ export const routes: Routes = [
|
||||
// User Details
|
||||
{
|
||||
path: 'details-user/:id',
|
||||
component: DetailsUserComponent,
|
||||
loadComponent: () => import('./pages/details/details-user/details-user.component').then(m => m.DetailsUserComponent),
|
||||
},
|
||||
// #########
|
||||
// User edit
|
||||
// User edit (lazy-loaded)
|
||||
{
|
||||
path: 'account',
|
||||
component: AccountComponent,
|
||||
loadComponent: () => import('./pages/subscription/account/account.component').then(m => m.AccountComponent),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'account/:id',
|
||||
component: AccountComponent,
|
||||
loadComponent: () => import('./pages/subscription/account/account.component').then(m => m.AccountComponent),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// Create, Update Listings
|
||||
// Create, Update Listings (lazy-loaded)
|
||||
{
|
||||
path: 'editBusinessListing/:id',
|
||||
component: EditBusinessListingComponent,
|
||||
loadComponent: () => import('./pages/subscription/edit-business-listing/edit-business-listing.component').then(m => m.EditBusinessListingComponent),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'createBusinessListing',
|
||||
component: EditBusinessListingComponent,
|
||||
loadComponent: () => import('./pages/subscription/edit-business-listing/edit-business-listing.component').then(m => m.EditBusinessListingComponent),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'editCommercialPropertyListing/:id',
|
||||
component: EditCommercialPropertyListingComponent,
|
||||
loadComponent: () => import('./pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component').then(m => m.EditCommercialPropertyListingComponent),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'createCommercialPropertyListing',
|
||||
component: EditCommercialPropertyListingComponent,
|
||||
loadComponent: () => import('./pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component').then(m => m.EditCommercialPropertyListingComponent),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// My Listings
|
||||
// My Listings (lazy-loaded)
|
||||
{
|
||||
path: 'myListings',
|
||||
component: MyListingComponent,
|
||||
loadComponent: () => import('./pages/subscription/my-listing/my-listing.component').then(m => m.MyListingComponent),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// My Favorites
|
||||
// My Favorites (lazy-loaded)
|
||||
{
|
||||
path: 'myFavorites',
|
||||
component: FavoritesComponent,
|
||||
loadComponent: () => import('./pages/subscription/favorites/favorites.component').then(m => m.FavoritesComponent),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// EMAil Us
|
||||
// Email Us (lazy-loaded)
|
||||
{
|
||||
path: 'emailUs',
|
||||
component: EmailUsComponent,
|
||||
loadComponent: () => import('./pages/subscription/email-us/email-us.component').then(m => m.EmailUsComponent),
|
||||
// canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
@@ -174,9 +168,11 @@ export const routes: Routes = [
|
||||
path: 'success',
|
||||
component: SuccessComponent,
|
||||
},
|
||||
// #########
|
||||
// Admin Pages (lazy-loaded)
|
||||
{
|
||||
path: 'admin/users',
|
||||
component: UserListComponent,
|
||||
loadComponent: () => import('./pages/admin/user-list/user-list.component').then(m => m.UserListComponent),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ConfirmationService } from './confirmation.service';
|
||||
<button
|
||||
(click)="confirmationService.reject()"
|
||||
type="button"
|
||||
class="absolute top-3 end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
class="absolute top-3 end-2.5 text-gray-600 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
@@ -21,11 +21,11 @@ import { ConfirmationService } from './confirmation.service';
|
||||
<span class="sr-only">Close modal</span>
|
||||
</button>
|
||||
<div class="p-4 md:p-5 text-center">
|
||||
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<svg class="mx-auto mb-4 text-gray-600 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
@let confirmation = (confirmationService.confirmation$ | async);
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmation?.message }}</h3>
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-600">{{ confirmation?.message }}</h3>
|
||||
@if(confirmation?.buttons==='both'){
|
||||
<button
|
||||
(click)="confirmationService.accept()"
|
||||
@@ -37,7 +37,7 @@ import { ConfirmationService } from './confirmation.service';
|
||||
<button
|
||||
(click)="confirmationService.reject()"
|
||||
type="button"
|
||||
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-600 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
>
|
||||
No, cancel
|
||||
</button>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<button
|
||||
(click)="eMailService.reject()"
|
||||
type="button"
|
||||
class="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
class="end-2.5 text-gray-600 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="flex flex-col lg:flex-row items-center mb-4 lg:mb-0">
|
||||
<!-- <img src="/assets/images/header-logo.png" alt="BizMatch Logo" class="h-8 mb-2 lg:mb-0 lg:mr-4" /> -->
|
||||
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
<img src="/assets/images/header-logo.png" class="h-8 mb-2 lg:mb-0 lg:mr-4" alt="BizMatch Logo" />
|
||||
<img src="/assets/images/header-logo.png" class="h-8 mb-2 lg:mb-0 lg:mr-4" alt="BizMatch Logo" width="120" height="32" />
|
||||
</a>
|
||||
<p class="text-sm text-neutral-600 text-center lg:text-left">© {{ currentYear }} Bizmatch All rights reserved.</p>
|
||||
</div>
|
||||
@@ -23,9 +23,9 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-4 lg:mb-0 flex flex-col items-center lg:items-end">
|
||||
<a class="text-sm text-neutral-600 mb-1 lg:mb-2 hover:text-primary-600 w-full"> <i
|
||||
<a href="tel:+1-800-840-6025" class="text-sm text-neutral-600 mb-1 lg:mb-2 hover:text-primary-600 w-full"> <i
|
||||
class="fas fa-phone-alt mr-2"></i>1-800-840-6025 </a>
|
||||
<a class="text-sm text-neutral-600 hover:text-primary-600"> <i
|
||||
<a href="mailto:info@bizmatch.net" class="text-sm text-neutral-600 hover:text-primary-600"> <i
|
||||
class="fas fa-envelope mr-2"></i>info@bizmatch.net </a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
<img src="/assets/images/header-logo.png" class="h-10 w-auto"
|
||||
alt="BizMatch - Business Marketplace for Buying and Selling Businesses" />
|
||||
alt="BizMatch - Business Marketplace for Buying and Selling Businesses" width="150" height="40" />
|
||||
</a>
|
||||
<div class="flex items-center md:order-2 space-x-3 rtl:space-x-reverse">
|
||||
<!-- Filter button -->
|
||||
@@ -192,7 +192,7 @@
|
||||
class="inline-flex items-center py-2 px-3 rounded hover:bg-neutral-100 md:hover:bg-transparent md:hover:text-primary-600 md:p-0 dark:text-white md:dark:hover:text-primary-500 dark:hover:bg-neutral-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-neutral-700"
|
||||
(click)="closeMenusAndSetCriteria('brokerListings')">
|
||||
<img src="/assets/images/icon_professionals.png" alt="Professionals"
|
||||
class="w-5 h-5 mr-2 object-contain bg-transparent" style="mix-blend-mode: darken;" />
|
||||
class="w-5 h-5 mr-2 object-contain bg-transparent" style="mix-blend-mode: darken;" width="20" height="20" />
|
||||
<span>Professionals</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||
import { Component, HostListener, OnDestroy, OnInit, AfterViewInit, PLATFORM_ID, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
||||
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
|
||||
import { APP_ICONS } from '../../utils/fontawesome-icons';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
|
||||
import { filter, Observable, Subject, takeUntil } from 'rxjs';
|
||||
@@ -35,7 +35,7 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
keycloakUser: KeycloakUser;
|
||||
user: User;
|
||||
activeItem;
|
||||
faUserGear = faUserGear;
|
||||
faUserGear = APP_ICONS.faUserGear;
|
||||
profileUrl: string;
|
||||
env = environment;
|
||||
private filterDropdown: Dropdown | null = null;
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
placeholder="Please enter E-Mail Address"
|
||||
class="w-full px-3 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<fa-icon [icon]="envelope" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
|
||||
<fa-icon [icon]="envelope" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
placeholder="Please enter Password"
|
||||
class="w-full px-3 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
|
||||
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
placeholder="Repeat Password"
|
||||
class="w-full px-3 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
|
||||
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faArrowRight, faEnvelope, faLock, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { APP_ICONS } from '../../utils/fontawesome-icons';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { LoadingService } from '../../services/loading.service';
|
||||
@Component({
|
||||
@@ -18,10 +18,10 @@ export class LoginRegisterComponent {
|
||||
confirmPassword: string = '';
|
||||
isLoginMode: boolean = true; // true: Login, false: Registration
|
||||
errorMessage: string = '';
|
||||
envelope = faEnvelope;
|
||||
lock = faLock;
|
||||
arrowRight = faArrowRight;
|
||||
userplus = faUserPlus;
|
||||
envelope = APP_ICONS.faEnvelope;
|
||||
lock = APP_ICONS.faLock;
|
||||
arrowRight = APP_ICONS.faArrowRight;
|
||||
userplus = APP_ICONS.faUserPlus;
|
||||
constructor(private authService: AuthService, private route: ActivatedRoute, private router: Router, private loadingService: LoadingService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
<ng-select id="state-select" aria-label="Location - State" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
@@ -172,7 +172,7 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
<ng-select id="state-select" aria-label="Location - State" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
<ng-select id="state-select" aria-label="Location - State" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
@@ -168,7 +168,7 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
<ng-select id="state-select" aria-label="Location - State" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
<label for="state-select" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select id="state-select" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state" aria-label="Location - State"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
@@ -266,7 +266,7 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
<ng-select id="state-select" aria-label="Location - State" class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
|
||||
@@ -31,13 +31,27 @@ import dayjs from 'dayjs';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
import { BaseDetailsComponent } from '../base-details.component';
|
||||
import { ShareButton } from 'ngx-sharebuttons/button';
|
||||
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
|
||||
import { shareIcons } from 'ngx-sharebuttons/icons';
|
||||
@Component({
|
||||
selector: 'app-details-business-listing',
|
||||
standalone: true,
|
||||
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, LeafletModule, BreadcrumbsComponent, ShareButton, NgOptimizedImage],
|
||||
providers: [],
|
||||
providers: [
|
||||
provideShareButtonsOptions(
|
||||
shareIcons(),
|
||||
withConfig({
|
||||
debug: false,
|
||||
sharerMethod: SharerMethods.Anchor,
|
||||
}),
|
||||
),
|
||||
],
|
||||
templateUrl: './details-business-listing.component.html',
|
||||
styleUrl: '../details.scss',
|
||||
styleUrls: [
|
||||
'../details.scss',
|
||||
'../../../../../node_modules/leaflet/dist/leaflet.css',
|
||||
'../../../../../node_modules/ngx-sharebuttons/themes/default.scss'
|
||||
],
|
||||
})
|
||||
export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
||||
// listings: Array<BusinessListing>;
|
||||
|
||||
@@ -3,9 +3,9 @@ import { NgOptimizedImage } from '@angular/common';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LeafletModule } from '@bluehalo/ngx-leaflet';
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { APP_ICONS } from '../../../utils/fontawesome-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { GalleryModule, ImageItem } from 'ng-gallery';
|
||||
import { GALLERY_CONFIG, GalleryConfig, GalleryModule, ImageItem } from 'ng-gallery';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { CommercialPropertyListing, EventTypeEnum, ShareByEMail, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { CommercialPropertyListingCriteria, ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
@@ -30,14 +30,35 @@ import { createMailInfo, map2User } from '../../../utils/utils';
|
||||
import { BaseDetailsComponent } from '../base-details.component';
|
||||
import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component';
|
||||
import { ShareButton } from 'ngx-sharebuttons/button';
|
||||
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
|
||||
import { shareIcons } from 'ngx-sharebuttons/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details-commercial-property-listing',
|
||||
standalone: true,
|
||||
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, GalleryModule, LeafletModule, BreadcrumbsComponent, ShareButton, NgOptimizedImage],
|
||||
providers: [],
|
||||
providers: [
|
||||
provideShareButtonsOptions(
|
||||
shareIcons(),
|
||||
withConfig({
|
||||
debug: false,
|
||||
sharerMethod: SharerMethods.Anchor,
|
||||
}),
|
||||
),
|
||||
{
|
||||
provide: GALLERY_CONFIG,
|
||||
useValue: {
|
||||
autoHeight: true,
|
||||
imageSize: 'cover',
|
||||
} as GalleryConfig,
|
||||
},
|
||||
],
|
||||
templateUrl: './details-commercial-property-listing.component.html',
|
||||
styleUrl: '../details.scss',
|
||||
styleUrls: [
|
||||
'../details.scss',
|
||||
'../../../../../node_modules/leaflet/dist/leaflet.css',
|
||||
'../../../../../node_modules/ngx-sharebuttons/themes/default.scss'
|
||||
],
|
||||
})
|
||||
export class DetailsCommercialPropertyListingComponent extends BaseDetailsComponent {
|
||||
responsiveOptions = [
|
||||
@@ -69,7 +90,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
ts = new Date().getTime();
|
||||
env = environment;
|
||||
errorResponse: ErrorResponse;
|
||||
faTimes = faTimes;
|
||||
faTimes = APP_ICONS.faTimes;
|
||||
propertyDetails = [];
|
||||
images: Array<ImageItem> = [];
|
||||
relatedListings: CommercialPropertyListing[] = [];
|
||||
|
||||
@@ -19,13 +19,27 @@ import { UserService } from '../../../services/user.service';
|
||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||
import { formatPhoneNumber, map2User } from '../../../utils/utils';
|
||||
import { ShareButton } from 'ngx-sharebuttons/button';
|
||||
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
|
||||
import { shareIcons } from 'ngx-sharebuttons/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details-user',
|
||||
standalone: true,
|
||||
imports: [SharedModule, BreadcrumbsComponent, NgOptimizedImage, ShareButton],
|
||||
providers: [
|
||||
provideShareButtonsOptions(
|
||||
shareIcons(),
|
||||
withConfig({
|
||||
debug: false,
|
||||
sharerMethod: SharerMethods.Anchor,
|
||||
}),
|
||||
),
|
||||
],
|
||||
templateUrl: './details-user.component.html',
|
||||
styleUrl: '../details.scss',
|
||||
styleUrls: [
|
||||
'../details.scss',
|
||||
'../../../../../node_modules/ngx-sharebuttons/themes/default.scss'
|
||||
],
|
||||
})
|
||||
export class DetailsUserComponent {
|
||||
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<header class="w-full flex justify-between items-center p-4 bg-white top-0 z-10 h-16 md:h-20">
|
||||
<img src="/assets/images/header-logo.png" alt="Logo" class="h-8 md:h-10 w-auto" />
|
||||
<img src="/assets/images/header-logo.png" alt="BizMatch - Business & Property Marketplace" class="h-8 md:h-10 w-auto" width="120" height="40" />
|
||||
<div class="hidden md:flex items-center space-x-4">
|
||||
@if(user){
|
||||
<a routerLink="/account" class="text-primary-600 border border-primary-600 px-3 py-2 rounded">Account</a>
|
||||
@@ -10,7 +10,12 @@
|
||||
<!-- <a routerLink="/login" class="text-primary-500 hover:underline">Login/Register</a> -->
|
||||
}
|
||||
</div>
|
||||
<button (click)="toggleMenu()" class="md:hidden text-neutral-600">
|
||||
<button
|
||||
(click)="toggleMenu()"
|
||||
class="md:hidden text-neutral-700 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center"
|
||||
aria-label="Open navigation menu"
|
||||
[attr.aria-expanded]="isMenuOpen"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16m-7 6h7"></path>
|
||||
</svg>
|
||||
@@ -26,7 +31,11 @@
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'login' }" class="text-white text-xl py-2">Log In</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'register' }" class="text-white text-xl py-2">Sign Up</a>
|
||||
}
|
||||
<button (click)="toggleMenu()" class="text-white mt-4">
|
||||
<button
|
||||
(click)="toggleMenu()"
|
||||
class="text-white mt-4"
|
||||
aria-label="Close navigation menu"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
@@ -38,88 +47,126 @@
|
||||
<!-- 1. px-4 für <main> (vorher px-2 sm:px-4) -->
|
||||
<main class="flex flex-col items-center justify-center px-4 w-full flex-grow">
|
||||
<div
|
||||
class="bg-cover-custom pb-12 md:py-20 flex flex-col w-full rounded-xl lg:rounded-2xl md:drop-shadow-custom-md lg:drop-shadow-custom-lg min-h-[calc(100vh_-_20rem)] lg:min-h-[calc(100vh_-_10rem)] max-sm:bg-contain max-sm:bg-bottom max-sm:bg-no-repeat max-sm:min-h-[60vh] max-sm:bg-primary-600"
|
||||
class="relative overflow-hidden pb-12 md:py-20 flex flex-col w-full rounded-xl lg:rounded-2xl md:drop-shadow-custom-md lg:drop-shadow-custom-lg min-h-[calc(100vh_-_20rem)] lg:min-h-[calc(100vh_-_10rem)] max-sm:min-h-[60vh] max-sm:bg-primary-600"
|
||||
>
|
||||
<div class="flex justify-center w-full">
|
||||
<!-- Optimized Background Image -->
|
||||
<picture class="absolute inset-0 w-full h-full z-0 pointer-events-none">
|
||||
<source srcset="/assets/images/flags_bg.avif" type="image/avif">
|
||||
<img
|
||||
width="2500"
|
||||
height="1285"
|
||||
fetchpriority="high"
|
||||
loading="eager"
|
||||
src="/assets/images/flags_bg.jpg"
|
||||
alt=""
|
||||
class="w-full h-full object-cover"
|
||||
>
|
||||
</picture>
|
||||
|
||||
<!-- Gradient Overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/35 via-black/15 to-transparent z-0 pointer-events-none"></div>
|
||||
|
||||
<div class="flex justify-center w-full relative z-10">
|
||||
<!-- 3. Für Mobile: m-2 statt max-w-xs; ab sm: wieder max-width und kein Margin -->
|
||||
<div class="w-full m-2 sm:m-0 sm:max-w-md md:max-w-xl lg:max-w-2xl xl:max-w-3xl">
|
||||
<!-- Hero-Container -->
|
||||
<section class="relative">
|
||||
<!-- Dein Hintergrundbild liegt hier per CSS oder absolutem <img> -->
|
||||
|
||||
<!-- 1) Overlay: sorgt für Kontrast auf hellem Himmel -->
|
||||
<div aria-hidden="true" class="pointer-events-none absolute inset-0"></div>
|
||||
<!-- 1) Overlay: sorgt für Kontrast auf hellem Himmel (Previous overlay removed, using new global overlay) -->
|
||||
<!-- <div aria-hidden="true" class="pointer-events-none absolute inset-0"></div> -->
|
||||
|
||||
<!-- 2) Textblock -->
|
||||
<div class="relative z-10 mx-auto max-w-4xl px-6 sm:px-6 py-4 sm:py-16 text-center text-white">
|
||||
<h1 class="text-[1.55rem] sm:text-4xl md:text-5xl lg:text-6xl font-extrabold tracking-tight leading-tight drop-shadow-[0_2px_6px_rgba(0,0,0,0.55)]">Buy & Sell Businesses and Commercial Properties</h1>
|
||||
|
||||
<p class="mt-3 sm:mt-4 text-base sm:text-lg md:text-xl lg:text-2xl font-medium text-white/90 drop-shadow-[0_1.5px_4px_rgba(0,0,0,0.6)]">
|
||||
Find profitable businesses for sale, commercial real estate, and franchise opportunities across the United States
|
||||
Buy profitable businesses for sale or sell your business to qualified buyers. Browse commercial real estate and franchise opportunities across the United States.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Restliche Anpassungen (Innenabstände, Button-Paddings etc.) bleiben wie im vorherigen Schritt -->
|
||||
<div class="search-form-container bg-white bg-opacity-80 pb-4 md:pb-6 pt-2 px-3 sm:px-4 md:px-6 rounded-lg shadow-lg w-full" [ngClass]="{ 'pt-6': aiSearch }">
|
||||
@if(!aiSearch){
|
||||
<div class="text-sm lg:text-base mb-1 text-center text-neutral-500 border-neutral-200 dark:text-neutral-400 dark:border-neutral-700 flex justify-between">
|
||||
<ul class="flex flex-wrap -mb-px w-full">
|
||||
<li class="w-[33%]">
|
||||
<a
|
||||
<div class="text-sm lg:text-base mb-1 text-center text-neutral-700 border-neutral-200 dark:text-neutral-300 dark:border-neutral-700 flex justify-between">
|
||||
<ul class="flex flex-wrap -mb-px w-full" role="tablist" aria-label="Search categories">
|
||||
<li class="w-[33%]" role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
[attr.aria-selected]="activeTabAction === 'business'"
|
||||
(click)="changeTab('business')"
|
||||
[ngClass]="
|
||||
activeTabAction === 'business'
|
||||
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500']
|
||||
: ['border-transparent', 'hover:text-neutral-600', 'hover:border-neutral-300', 'dark:hover:text-neutral-300']
|
||||
: ['border-transparent', 'text-neutral-700', 'hover:text-neutral-900', 'hover:border-neutral-400', 'dark:hover:text-neutral-300']
|
||||
"
|
||||
class="tab-link hover:cursor-pointer inline-flex items-center justify-center px-1 py-2 md:p-4 border-b-2 rounded-t-lg"
|
||||
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent min-h-[44px]"
|
||||
[attr.aria-controls]="'tabpanel-search'"
|
||||
id="tab-business"
|
||||
>
|
||||
<img src="/assets/images/business_logo.png" alt="Search businesses for sale" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" width="28" height="28" />
|
||||
<img src="/assets/images/business_logo.png" alt="" aria-hidden="true" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" width="28" height="28" />
|
||||
<span>Businesses</span>
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
@if ((numberOfCommercial$ | async) > 0) {
|
||||
<li class="w-[33%]">
|
||||
<a
|
||||
<li class="w-[33%]" role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
[attr.aria-selected]="activeTabAction === 'commercialProperty'"
|
||||
(click)="changeTab('commercialProperty')"
|
||||
[ngClass]="
|
||||
activeTabAction === 'commercialProperty'
|
||||
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500']
|
||||
: ['border-transparent', 'hover:text-neutral-600', 'hover:border-neutral-300', 'dark:hover:text-neutral-300']
|
||||
: ['border-transparent', 'text-neutral-700', 'hover:text-neutral-900', 'hover:border-neutral-400', 'dark:hover:text-neutral-300']
|
||||
"
|
||||
class="tab-link hover:cursor-pointer inline-flex items-center justify-center px-1 py-2 md:p-4 border-b-2 rounded-t-lg"
|
||||
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent min-h-[44px]"
|
||||
[attr.aria-controls]="'tabpanel-search'"
|
||||
id="tab-properties"
|
||||
>
|
||||
<img src="/assets/images/properties_logo.png" alt="Search commercial properties for sale" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" width="28" height="28" />
|
||||
<img src="/assets/images/properties_logo.png" alt="" aria-hidden="true" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" width="28" height="28" />
|
||||
<span>Properties</span>
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
<li class="w-[33%]">
|
||||
<a
|
||||
<li class="w-[33%]" role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
[attr.aria-selected]="activeTabAction === 'broker'"
|
||||
(click)="changeTab('broker')"
|
||||
[ngClass]="
|
||||
activeTabAction === 'broker'
|
||||
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500']
|
||||
: ['border-transparent', 'hover:text-neutral-600', 'hover:border-neutral-300', 'dark:hover:text-neutral-300']
|
||||
: ['border-transparent', 'text-neutral-700', 'hover:text-neutral-900', 'hover:border-neutral-400', 'dark:hover:text-neutral-300']
|
||||
"
|
||||
class="tab-link hover:cursor-pointer inline-flex items-center justify-center px-1 py-2 md:p-4 border-b-2 rounded-t-lg"
|
||||
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent min-h-[44px]"
|
||||
[attr.aria-controls]="'tabpanel-search'"
|
||||
id="tab-professionals"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/icon_professionals.png"
|
||||
alt="Search business professionals and brokers"
|
||||
class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain bg-transparent"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain"
|
||||
style="mix-blend-mode: darken"
|
||||
width="28" height="28"
|
||||
/>
|
||||
<span>Professionals</span>
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
} @if(criteria && !aiSearch){
|
||||
<div class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-neutral-300">
|
||||
<div id="tabpanel-search" role="tabpanel" aria-labelledby="tab-business" class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-neutral-300">
|
||||
<div class="md:flex-none md:w-48 flex-1 md:border-r border-neutral-300 overflow-hidden mb-2 md:mb-0">
|
||||
<div class="relative max-sm:border border-neutral-300 rounded-md">
|
||||
<label for="type-filter" class="sr-only">Filter by type</label>
|
||||
<select
|
||||
id="type-filter"
|
||||
aria-label="Filter by type"
|
||||
|
||||
class="appearance-none bg-transparent w-full py-4 px-4 pr-8 focus:outline-none md:border-none rounded-md md:rounded-none min-h-[52px]"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onTypesChange($event)"
|
||||
@@ -138,6 +185,7 @@
|
||||
|
||||
<div class="md:flex-auto md:w-36 flex-grow md:border-r border-neutral-300 mb-2 md:mb-0">
|
||||
<div class="relative max-sm:border border-neutral-300 rounded-md">
|
||||
<label for="location-search" class="sr-only">Search by city or state</label>
|
||||
<ng-select
|
||||
class="custom md:border-none rounded-md md:rounded-none"
|
||||
[multiple]="false"
|
||||
@@ -151,7 +199,13 @@
|
||||
(ngModelChange)="setCityOrState($event)"
|
||||
placeholder="Enter City or State ..."
|
||||
groupBy="type"
|
||||
labelForId="location-search"
|
||||
aria-label="Search by city or state"
|
||||
[inputAttrs]="{'aria-describedby': 'location-search-hint'}"
|
||||
>
|
||||
<ng-template ng-footer-tmp>
|
||||
<span id="location-search-hint" class="sr-only">Enter at least 2 characters to search for a city or state</span>
|
||||
</ng-template>
|
||||
@for (city of cities$ | async; track city.id) { @let state = city.type==='city'?city.content.state:''; @let separator = city.type==='city'?' - ':'';
|
||||
<ng-option [value]="city">{{ city.content.name }}{{ separator }}{{ state }}</ng-option>
|
||||
}
|
||||
@@ -161,7 +215,10 @@
|
||||
@if (criteria.radius && !aiSearch){
|
||||
<div class="md:flex-none md:w-36 flex-1 md:border-r border-neutral-300 mb-2 md:mb-0">
|
||||
<div class="relative max-sm:border border-neutral-300 rounded-md">
|
||||
<label for="radius-filter" class="sr-only">Filter by radius</label>
|
||||
<select
|
||||
id="radius-filter"
|
||||
aria-label="Filter by radius"
|
||||
class="appearance-none bg-transparent w-full py-4 px-4 pr-8 focus:outline-none md:border-none rounded-md md:rounded-none min-h-[52px]"
|
||||
(ngModelChange)="onRadiusChange($event)"
|
||||
[ngModel]="criteria.radius"
|
||||
@@ -180,13 +237,13 @@
|
||||
}
|
||||
<div class="bg-primary-500 hover:bg-primary-600 max-sm:rounded-md search-button">
|
||||
@if( numberOfResults$){
|
||||
<button class="w-full h-full text-white font-bold py-2 px-4 md:py-3 md:px-6 focus:outline-none rounded-md md:rounded-none min-h-[52px] flex items-center justify-center gap-2" (click)="search()">
|
||||
<i class="fas fa-search"></i>
|
||||
<button aria-label="Search listings" class="w-full h-full text-white font-bold py-2 px-4 md:py-3 md:px-6 focus:outline-none rounded-md md:rounded-none min-h-[52px] flex items-center justify-center gap-2" (click)="search()">
|
||||
<i class="fas fa-search" aria-hidden="true"></i>
|
||||
<span>Search {{ numberOfResults$ | async }}</span>
|
||||
</button>
|
||||
}@else {
|
||||
<button class="w-full h-full text-white font-bold py-2 px-4 md:py-3 md:px-6 focus:outline-none rounded-md md:rounded-none min-h-[52px] flex items-center justify-center gap-2" (click)="search()">
|
||||
<i class="fas fa-search"></i>
|
||||
<button aria-label="Search listings" class="w-full h-full text-white font-bold py-2 px-4 md:py-3 md:px-6 focus:outline-none rounded-md md:rounded-none min-h-[52px] flex items-center justify-center gap-2" (click)="search()">
|
||||
<i class="fas fa-search" aria-hidden="true"></i>
|
||||
<span>Search</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -1,39 +1,7 @@
|
||||
.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'],
|
||||
@@ -51,39 +19,51 @@ 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 */
|
||||
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 */
|
||||
color: #000;
|
||||
/* Textfarbe für Dropdown-Optionen */
|
||||
}
|
||||
|
||||
select.placeholder-selected {
|
||||
color: #999; /* Farbe für den Platzhalter */
|
||||
color: #6b7280;
|
||||
/* gray-500 - besserer Kontrast für WCAG AA */
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #555; /* Dunkleres Grau */
|
||||
opacity: 1; /* Stellt sicher, dass die Deckkraft 100% ist */
|
||||
color: #555;
|
||||
/* Dunkleres Grau */
|
||||
opacity: 1;
|
||||
/* Stellt sicher, dass die Deckkraft 100% ist */
|
||||
}
|
||||
|
||||
/* Stellt sicher, dass die Optionen im Dropdown immer schwarz sind */
|
||||
@@ -91,10 +71,14 @@ 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 */
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -177,6 +161,7 @@ select,
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -244,6 +229,7 @@ header {
|
||||
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);
|
||||
@@ -256,6 +242,7 @@ header {
|
||||
}
|
||||
|
||||
&.bg-blue-600 {
|
||||
|
||||
// Register button
|
||||
&:hover {
|
||||
background-color: rgb(29, 78, 216);
|
||||
@@ -269,3 +256,16 @@ header {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
|
||||
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 { 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';
|
||||
@@ -24,9 +24,10 @@ import { map2User } from '../../utils/utils';
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, RouterModule, NgSelectModule, FaqComponent],
|
||||
imports: [CommonModule, FormsModule, RouterModule, NgSelectModule],
|
||||
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'];
|
||||
@@ -125,19 +126,15 @@ export class HomeComponent {
|
||||
// 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.',
|
||||
description: 'Buy and sell businesses, commercial properties, and franchises. Browse thousands of verified listings across the United States.',
|
||||
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
|
||||
// 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();
|
||||
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({
|
||||
@@ -175,7 +172,8 @@ export class HomeComponent {
|
||||
// Add SearchBox schema for Sitelinks Search
|
||||
const searchBoxSchema = this.seoService.generateSearchBoxSchema();
|
||||
|
||||
this.seoService.injectMultipleSchemas([organizationSchema, faqSchema, howToSchema, searchBoxSchema]);
|
||||
// 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');
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<i class="fas fa-arrow-left text-lg"></i>
|
||||
</button>
|
||||
<h1 class="text-3xl font-bold text-neutral-900 mb-6 pr-14">Privacy Statement</h1>
|
||||
<h1 class="text-3xl font-bold text-neutral-900 mb-6 pr-14">BizMatch Privacy Policy and Data Protection</h1>
|
||||
|
||||
<section id="content" role="main">
|
||||
<article class="post page">
|
||||
@@ -21,7 +21,7 @@
|
||||
Policy.
|
||||
</p>
|
||||
<p class="mb-4">
|
||||
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
|
||||
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.
|
||||
</p>
|
||||
<p class="font-bold mb-4 mt-6">Collection of personal information</p>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<i class="fas fa-arrow-left text-lg"></i>
|
||||
</button>
|
||||
<h1 class="text-3xl font-bold text-neutral-900 mb-6 pr-14">Terms of Use</h1>
|
||||
<h1 class="text-3xl font-bold text-neutral-900 mb-6 pr-14">BizMatch Terms of Use and User Agreement</h1>
|
||||
|
||||
<section id="content" role="main">
|
||||
<article class="post page">
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-neutral-900 mb-2">Professional Business Brokers & Advisors</h1>
|
||||
<p class="text-lg text-neutral-600">Connect with licensed business brokers, CPAs, attorneys, and other
|
||||
professionals across the United States.</p>
|
||||
<div class="mt-4 text-base text-neutral-700 max-w-4xl">
|
||||
<p>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.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Filter Button -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule, NgOptimizedImage } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
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';
|
||||
@@ -29,6 +29,7 @@ import { assignProperties, resetUserListingCriteria, map2User } from '../../../u
|
||||
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<void>();
|
||||
|
||||
@@ -14,9 +14,12 @@
|
||||
|
||||
<!-- SEO-optimized heading -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-neutral-900 mb-2">Businesses for Sale</h1>
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-neutral-900 mb-2">Businesses for Sale - Find Your Next Business Opportunity</h1>
|
||||
<p class="text-lg text-neutral-600">Discover profitable business opportunities across the United States. Browse
|
||||
verified listings from business owners and brokers.</p>
|
||||
<div class="mt-4 text-base text-neutral-700 max-w-4xl">
|
||||
<p>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.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Skeleton -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
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';
|
||||
@@ -31,6 +31,7 @@ import { map2User } from '../../../utils/utils';
|
||||
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<void>();
|
||||
@@ -86,8 +87,8 @@ export class BusinessListingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
// 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.',
|
||||
title: 'Businesses for Sale | BizMatch',
|
||||
description: 'Browse thousands of businesses for sale including restaurants, franchises, and retail stores. Verified listings nationwide.',
|
||||
keywords: 'businesses for sale, buy a business, business opportunities, franchise for sale, restaurant for sale, retail business for sale, business broker listings',
|
||||
type: 'website'
|
||||
});
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-neutral-900 mb-2">Commercial Properties for Sale</h1>
|
||||
<p class="text-lg text-neutral-600">Find office buildings, retail spaces, warehouses, and industrial properties across the United States. Investment opportunities from verified sellers and commercial real estate brokers.</p>
|
||||
<div class="mt-4 text-base text-neutral-700 max-w-4xl">
|
||||
<p>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.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(listings?.length > 0) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
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';
|
||||
@@ -30,6 +30,7 @@ import { map2User } from '../../../utils/utils';
|
||||
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<void>();
|
||||
@@ -83,8 +84,8 @@ export class CommercialPropertyListingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
// 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.',
|
||||
title: 'Commercial Properties for Sale | BizMatch',
|
||||
description: 'Browse commercial real estate including offices, retail, 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'
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { DatePipe, TitleCasePipe } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { APP_ICONS } from '../../../utils/fontawesome-icons';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
import { QuillModule, provideQuillConfig } from 'ngx-quill';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { AutoCompleteCompleteEvent, Invoice, UploadParams, ValidationMessage, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
@@ -45,9 +45,27 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||
ValidatedCountyComponent,
|
||||
ValidatedLocationComponent,
|
||||
],
|
||||
providers: [TitleCasePipe, DatePipe],
|
||||
providers: [
|
||||
TitleCasePipe,
|
||||
DatePipe,
|
||||
provideQuillConfig({
|
||||
modules: {
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ color: [] }],
|
||||
['clean'],
|
||||
],
|
||||
},
|
||||
}) as any,
|
||||
],
|
||||
templateUrl: './account.component.html',
|
||||
styleUrl: './account.component.scss',
|
||||
styleUrls: [
|
||||
'./account.component.scss',
|
||||
'../../../../../node_modules/quill/dist/quill.snow.css'
|
||||
],
|
||||
})
|
||||
export class AccountComponent {
|
||||
id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||
@@ -58,7 +76,7 @@ export class AccountComponent {
|
||||
environment = environment;
|
||||
editorModules = TOOLBAR_OPTIONS;
|
||||
env = environment;
|
||||
faTrash = faTrash;
|
||||
faTrash = APP_ICONS.faTrash;
|
||||
quillModules = {
|
||||
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']],
|
||||
};
|
||||
|
||||
@@ -6,11 +6,12 @@ import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { map2User, routeListingWithState } from '../../../utils/utils';
|
||||
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { APP_ICONS } from '../../../utils/fontawesome-icons';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { NgxCurrencyDirective } from 'ngx-currency';
|
||||
import { provideQuillConfig } from 'ngx-quill';
|
||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { AutoCompleteCompleteEvent, ImageProperty, createDefaultBusinessListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
|
||||
@@ -47,9 +48,25 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||
ValidatedTextareaComponent,
|
||||
ValidatedLocationComponent,
|
||||
],
|
||||
providers: [],
|
||||
providers: [
|
||||
provideQuillConfig({
|
||||
modules: {
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ color: [] }],
|
||||
['clean'],
|
||||
],
|
||||
},
|
||||
}) as any,
|
||||
],
|
||||
templateUrl: './edit-business-listing.component.html',
|
||||
styleUrl: './edit-business-listing.component.scss',
|
||||
styleUrls: [
|
||||
'./edit-business-listing.component.scss',
|
||||
'../../../../../node_modules/quill/dist/quill.snow.css'
|
||||
],
|
||||
})
|
||||
export class EditBusinessListingComponent {
|
||||
listingsCategory = 'business';
|
||||
@@ -64,7 +81,7 @@ export class EditBusinessListingComponent {
|
||||
config = { aspectRatio: 16 / 9 };
|
||||
editorModules = TOOLBAR_OPTIONS;
|
||||
draggedImage: ImageProperty;
|
||||
faTrash = faTrash;
|
||||
faTrash = APP_ICONS.faTrash;
|
||||
data: CommercialPropertyListing;
|
||||
typesOfBusiness = [];
|
||||
quillModules = {
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
(click)="uploadPropertyPicture()"
|
||||
class="flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<svg class="mr-2 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="mr-2 h-5 w-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
Upload
|
||||
|
||||
@@ -7,11 +7,11 @@ import { map2User, routeListingWithState } from '../../../utils/utils';
|
||||
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ViewportRuler } from '@angular/cdk/scrolling';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { APP_ICONS } from '../../../utils/fontawesome-icons';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { NgxCurrencyDirective } from 'ngx-currency';
|
||||
import { ImageCropperComponent } from 'ngx-image-cropper';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
import { QuillModule, provideQuillConfig } from 'ngx-quill';
|
||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { AutoCompleteCompleteEvent, ImageProperty, UploadParams, createDefaultCommercialPropertyListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
|
||||
@@ -53,9 +53,25 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||
ValidatedLocationComponent,
|
||||
ImageCropAndUploadComponent,
|
||||
],
|
||||
providers: [],
|
||||
providers: [
|
||||
provideQuillConfig({
|
||||
modules: {
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ color: [] }],
|
||||
['clean'],
|
||||
],
|
||||
},
|
||||
}) as any,
|
||||
],
|
||||
templateUrl: './edit-commercial-property-listing.component.html',
|
||||
styleUrl: './edit-commercial-property-listing.component.scss',
|
||||
styleUrls: [
|
||||
'./edit-commercial-property-listing.component.scss',
|
||||
'../../../../../node_modules/quill/dist/quill.snow.css'
|
||||
],
|
||||
})
|
||||
export class EditCommercialPropertyListingComponent {
|
||||
@ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
|
||||
@@ -91,7 +107,7 @@ export class EditCommercialPropertyListingComponent {
|
||||
editorModules = TOOLBAR_OPTIONS;
|
||||
|
||||
draggedImage: ImageProperty;
|
||||
faTrash = faTrash;
|
||||
faTrash = APP_ICONS.faTrash;
|
||||
suggestions: string[] | undefined;
|
||||
data: BusinessListing;
|
||||
userId: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { Injectable, inject, PLATFORM_ID, Renderer2, RendererFactory2 } from '@angular/core';
|
||||
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
||||
import { Meta, Title } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@@ -22,10 +22,16 @@ export class SeoService {
|
||||
private router = inject(Router);
|
||||
private platformId = inject(PLATFORM_ID);
|
||||
private isBrowser = isPlatformBrowser(this.platformId);
|
||||
private document = inject(DOCUMENT);
|
||||
private renderer: Renderer2;
|
||||
|
||||
private readonly defaultImage = 'https://biz-match.com/assets/images/bizmatch-og-image.jpg';
|
||||
private readonly defaultImage = 'https://www.bizmatch.net/assets/images/bizmatch-og-image.jpg';
|
||||
private readonly siteName = 'BizMatch';
|
||||
private readonly baseUrl = 'https://biz-match.com';
|
||||
private readonly baseUrl = 'https://www.bizmatch.net';
|
||||
|
||||
constructor(rendererFactory: RendererFactory2) {
|
||||
this.renderer = rendererFactory.createRenderer(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL for SEO purposes
|
||||
@@ -109,20 +115,18 @@ export class SeoService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update canonical URL
|
||||
* Update canonical URL (SSR-compatible using Renderer2)
|
||||
*/
|
||||
private updateCanonicalUrl(url: string): void {
|
||||
if (!this.isBrowser) return;
|
||||
|
||||
let link: HTMLLinkElement | null = document.querySelector('link[rel="canonical"]');
|
||||
let link: HTMLLinkElement | null = this.document.querySelector('link[rel="canonical"]');
|
||||
|
||||
if (link) {
|
||||
link.setAttribute('href', url);
|
||||
this.renderer.setAttribute(link, 'href', url);
|
||||
} else {
|
||||
link = document.createElement('link');
|
||||
link.setAttribute('rel', 'canonical');
|
||||
link.setAttribute('href', url);
|
||||
document.head.appendChild(link);
|
||||
link = this.renderer.createElement('link');
|
||||
this.renderer.setAttribute(link, 'rel', 'canonical');
|
||||
this.renderer.setAttribute(link, 'href', url);
|
||||
this.renderer.appendChild(this.document.head, link);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,32 +273,40 @@ export class SeoService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject JSON-LD structured data into page
|
||||
* Inject JSON-LD structured data into page (SSR-compatible using Renderer2)
|
||||
*/
|
||||
injectStructuredData(schema: object): void {
|
||||
if (!this.isBrowser) return;
|
||||
// Clear existing schema scripts with the same type
|
||||
this.removeAllSchemas();
|
||||
|
||||
// Remove existing schema script
|
||||
const existingScript = document.querySelector('script[type="application/ld+json"]');
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
// Create new script element using Renderer2 (works in both SSR and browser)
|
||||
const script = this.renderer.createElement('script');
|
||||
this.renderer.setAttribute(script, 'type', 'application/ld+json');
|
||||
this.renderer.setAttribute(script, 'data-schema', 'true');
|
||||
|
||||
// Add new schema script
|
||||
const script = document.createElement('script');
|
||||
script.type = 'application/ld+json';
|
||||
script.text = JSON.stringify(schema);
|
||||
document.head.appendChild(script);
|
||||
// Create text node with schema JSON
|
||||
const schemaText = this.renderer.createText(JSON.stringify(schema));
|
||||
this.renderer.appendChild(script, schemaText);
|
||||
|
||||
// Append to document head
|
||||
this.renderer.appendChild(this.document.head, script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all structured data
|
||||
* Remove all schema scripts (internal helper, SSR-compatible)
|
||||
*/
|
||||
private removeAllSchemas(): void {
|
||||
const existingScripts = this.document.querySelectorAll('script[data-schema="true"]');
|
||||
existingScripts.forEach(script => {
|
||||
this.renderer.removeChild(this.document.head, script);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all structured data (SSR-compatible)
|
||||
*/
|
||||
clearStructuredData(): void {
|
||||
if (!this.isBrowser) return;
|
||||
|
||||
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
|
||||
scripts.forEach(script => script.remove());
|
||||
this.removeAllSchemas();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,6 +430,7 @@ export class SeoService {
|
||||
|
||||
/**
|
||||
* Generate Organization schema for the company
|
||||
* Enhanced for Knowledge Graph and entity verification
|
||||
*/
|
||||
generateOrganizationSchema(): object {
|
||||
return {
|
||||
@@ -427,18 +440,47 @@ export class SeoService {
|
||||
'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-BIZ-MATCH',
|
||||
'telephone': '+1-800-840-6025',
|
||||
'contactType': 'Customer Service',
|
||||
'areaServed': 'US',
|
||||
'availableLanguage': 'English'
|
||||
}
|
||||
'availableLanguage': 'English',
|
||||
'email': 'info@bizmatch.net'
|
||||
},
|
||||
|
||||
// Business details for Knowledge Graph
|
||||
'foundingDate': '2020',
|
||||
'knowsAbout': [
|
||||
'Business Brokerage',
|
||||
'Commercial Real Estate',
|
||||
'Business Valuation',
|
||||
'Franchise Opportunities'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -486,20 +528,21 @@ export class SeoService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject multiple structured data schemas
|
||||
* Inject multiple structured data schemas (SSR-compatible using Renderer2)
|
||||
*/
|
||||
injectMultipleSchemas(schemas: object[]): void {
|
||||
if (!this.isBrowser) return;
|
||||
// Clear existing schema scripts
|
||||
this.removeAllSchemas();
|
||||
|
||||
// Remove existing schema scripts
|
||||
this.clearStructuredData();
|
||||
|
||||
// Add new schema scripts
|
||||
// Add new schema scripts using Renderer2
|
||||
schemas.forEach(schema => {
|
||||
const script = document.createElement('script');
|
||||
script.type = 'application/ld+json';
|
||||
script.text = JSON.stringify(schema);
|
||||
document.head.appendChild(script);
|
||||
const script = this.renderer.createElement('script');
|
||||
this.renderer.setAttribute(script, 'type', 'application/ld+json');
|
||||
this.renderer.setAttribute(script, 'data-schema', 'true');
|
||||
|
||||
const schemaText = this.renderer.createText(JSON.stringify(schema));
|
||||
this.renderer.appendChild(script, schemaText);
|
||||
this.renderer.appendChild(this.document.head, script);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface SitemapUrl {
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SitemapService {
|
||||
private readonly baseUrl = 'https://biz-match.com';
|
||||
private readonly baseUrl = 'https://www.bizmatch.net';
|
||||
|
||||
/**
|
||||
* Generate XML sitemap content
|
||||
|
||||
19
bizmatch/src/app/utils/fontawesome-icons.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
faArrowRight,
|
||||
faEnvelope,
|
||||
faLock,
|
||||
faTimes,
|
||||
faTrash,
|
||||
faUserGear,
|
||||
faUserPlus
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export const APP_ICONS = {
|
||||
faArrowRight,
|
||||
faEnvelope,
|
||||
faLock,
|
||||
faTimes,
|
||||
faTrash,
|
||||
faUserGear,
|
||||
faUserPlus,
|
||||
};
|
||||
25
bizmatch/src/app/utils/logger.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
// Lightweight logger implementation for both dev and production
|
||||
// Avoids dynamic require() which causes build issues
|
||||
const createLoggerImpl = (name: string) => ({
|
||||
info: (...args: any[]) => {
|
||||
if (!environment.production) {
|
||||
console.log(`[${name}]`, ...args);
|
||||
}
|
||||
},
|
||||
warn: (...args: any[]) => console.warn(`[${name}]`, ...args),
|
||||
error: (...args: any[]) => console.error(`[${name}]`, ...args),
|
||||
debug: (...args: any[]) => {
|
||||
if (!environment.production) {
|
||||
console.debug(`[${name}]`, ...args);
|
||||
}
|
||||
},
|
||||
trace: (...args: any[]) => {
|
||||
if (!environment.production) {
|
||||
console.trace(`[${name}]`, ...args);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const createLogger = createLoggerImpl;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router } from '@angular/router';
|
||||
import { ConsoleFormattedStream, INFO, createLogger as _createLogger, stdSerializers } from 'browser-bunyan';
|
||||
import { createLogger as _createLogger } from './logger';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import onChange from 'on-change';
|
||||
import { SortByOptions, User } from '../../../../bizmatch-server/src/models/db.model';
|
||||
@@ -141,14 +141,8 @@ export function createMailInfo(user?: User): MailInfo {
|
||||
listing: null,
|
||||
};
|
||||
}
|
||||
export function createLogger(name: string, level: number = INFO, options: any = {}) {
|
||||
return _createLogger({
|
||||
name,
|
||||
streams: [{ level, stream: new ConsoleFormattedStream() }],
|
||||
serializers: stdSerializers,
|
||||
src: true,
|
||||
...options,
|
||||
});
|
||||
export function createLogger(name: string, level?: number, options?: any) {
|
||||
return _createLogger(name);
|
||||
}
|
||||
export function formatPhoneNumber(phone: string): string {
|
||||
const cleaned = ('' + phone).replace(/\D/g, '');
|
||||
|
||||
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 8.7 MiB |
|
Before Width: | Height: | Size: 26 MiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 662 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 662 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 667 B |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
@@ -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: 05.02.2026 13:06 | TX: 02/05/2026 6:06 AM"
|
||||
};
|
||||
|
||||
export default build;
|
||||
@@ -35,6 +35,9 @@
|
||||
|
||||
<!-- Preload critical assets -->
|
||||
<link rel="preload" as="image" href="/assets/images/header-logo.png" type="image/png" />
|
||||
<!-- Hero background is LCP element - preload with high priority -->
|
||||
<link rel="preload" as="image" href="/assets/images/flags_bg.avif" type="image/avif" fetchpriority="high" />
|
||||
<link rel="preload" as="image" href="/assets/images/flags_bg.jpg" imagesrcset="/assets/images/flags_bg.jpg" type="image/jpeg" />
|
||||
|
||||
<!-- Prefetch common assets -->
|
||||
<link rel="prefetch" as="image" href="/assets/images/business_logo.png" />
|
||||
@@ -58,6 +61,53 @@
|
||||
<link rel="icon" href="/assets/cropped-Favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" href="/assets/cropped-Favicon-192x192.png" sizes="192x192" />
|
||||
<link rel="apple-touch-icon" href="/assets/cropped-Favicon-180x180.png" />
|
||||
|
||||
<!-- Schema.org Structured Data (Static) -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "BizMatch",
|
||||
"url": "https://www.bizmatch.net",
|
||||
"logo": "https://www.bizmatch.net/assets/images/bizmatch-logo.png",
|
||||
"description": "Buy and sell businesses, commercial properties, and franchises. Browse thousands of verified listings across the United States.",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "1001 Blucher Street",
|
||||
"addressLocality": "Corpus Christi",
|
||||
"addressRegion": "TX",
|
||||
"postalCode": "78401",
|
||||
"addressCountry": "US"
|
||||
},
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"contactType": "Customer Service",
|
||||
"email": "info@bizmatch.net"
|
||||
},
|
||||
"sameAs": [
|
||||
"https://www.facebook.com/bizmatch",
|
||||
"https://www.linkedin.com/company/bizmatch",
|
||||
"https://twitter.com/bizmatch"
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "BizMatch",
|
||||
"url": "https://www.bizmatch.net",
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": {
|
||||
"@type": "EntryPoint",
|
||||
"urlTemplate": "https://www.bizmatch.net/businessListings?search={search_term_string}"
|
||||
},
|
||||
"query-input": "required name=search_term_string"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col min-h-screen">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# robots.txt for BizMatch - Business Marketplace
|
||||
# https://biz-match.com
|
||||
# Last updated: 2026-01-02
|
||||
# https://www.bizmatch.net
|
||||
# Last updated: 2026-02-03
|
||||
|
||||
# ===========================================
|
||||
# Default rules for all crawlers
|
||||
@@ -37,6 +37,9 @@ Disallow: /emailUs
|
||||
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=
|
||||
@@ -126,15 +129,10 @@ 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
|
||||
# Main sitemap index
|
||||
Sitemap: https://www.bizmatch.net/sitemap.xml
|
||||
|
||||
# ===========================================
|
||||
# Host directive (for Yandex)
|
||||
# ===========================================
|
||||
Host: https://biz-match.com
|
||||
Host: https://www.bizmatch.net
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
@tailwind utilities;
|
||||
|
||||
// External CSS imports - these URL imports don't trigger deprecation warnings
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');
|
||||
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css');
|
||||
// Using css2 API with specific weights for better performance
|
||||
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap');
|
||||
|
||||
// Local CSS files loaded as CSS (not SCSS) to avoid @import deprecation
|
||||
// Note: These are loaded via angular.json styles array is the preferred approach,
|
||||
@@ -121,7 +121,7 @@ p-menubarsub ul {
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: #999 !important;
|
||||
color: #757575 !important; /* 4.54:1 contrast - WCAG AA compliant */
|
||||
}
|
||||
|
||||
/* Fix für Marker-Icons in Leaflet */
|
||||
|
||||