Compare commits

...

5 Commits

39 changed files with 407 additions and 155 deletions

View File

@ -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",
@ -109,4 +110,4 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
}

View File

@ -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';
@ -13,7 +14,7 @@ async function bootstrap() {
app.useLogger(logger);
//app.use('/bizmatch/payment/webhook', bodyParser.raw({ type: 'application/json' }));
// Serve static files from pictures directory
app.use('/pictures', express.static('pictures'));
// app.use('/pictures', express.static('pictures'));
app.setGlobalPrefix('bizmatch');
@ -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();

View File

@ -21,11 +21,6 @@
"outputPath": "dist/bizmatch",
"index": "src/index.html",
"browser": "src/main.ts",
"server": "src/main.server.ts",
"prerender": false,
"ssr": {
"entry": "server.ts"
},
"allowedCommonJsDependencies": [
"quill-delta",
"leaflet",
@ -53,10 +48,7 @@
],
"styles": [
"src/styles.scss",
"src/styles/lazy-load.css",
"node_modules/quill/dist/quill.snow.css",
"node_modules/leaflet/dist/leaflet.css",
"node_modules/ngx-sharebuttons/themes/default.scss"
"src/styles/lazy-load.css"
]
},
"configurations": {
@ -69,8 +61,8 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
"maximumWarning": "10kb",
"maximumError": "30kb"
}
],
"outputHashing": "all"
@ -78,8 +70,7 @@
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"ssr": false
"sourceMap": true
},
"dev": {
"fileReplacements": [

View File

@ -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

View File

@ -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()),
],

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>;

View File

@ -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[] = [];

View File

@ -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;

View File

@ -6,7 +6,7 @@ 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,7 +24,7 @@ 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,

View File

@ -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']],
};

View File

@ -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 = {

View File

@ -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

View File

@ -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;

View File

@ -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,11 +22,17 @@ 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://www.bizmatch.net/assets/images/bizmatch-og-image.jpg';
private readonly siteName = 'BizMatch';
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();
}
/**
@ -516,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);
});
}

View 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,
};

View 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;

View File

@ -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, '');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

View File

@ -1,6 +1,6 @@
// Build information, automatically generated by `the_build_script` :zwinkern:
const build = {
timestamp: "GER: 04.02.2026 15:43 | TX: 02/04/2026 8:43 AM"
timestamp: "GER: 05.02.2026 13:06 | TX: 02/05/2026 6:06 AM"
};
export default build;

View File

@ -61,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">

View File

@ -129,13 +129,8 @@ Disallow: /
# ===========================================
# Sitemap locations
# ===========================================
# Main sitemap index (dynamically generated, contains all sub-sitemaps)
Sitemap: https://www.bizmatch.net/bizmatch/sitemap.xml
# Individual sitemaps (auto-listed in sitemap index)
# - https://www.bizmatch.net/bizmatch/sitemap/static.xml
# - https://www.bizmatch.net/bizmatch/sitemap/business-1.xml
# - https://www.bizmatch.net/bizmatch/sitemap/commercial-1.xml
# Main sitemap index
Sitemap: https://www.bizmatch.net/sitemap.xml
# ===========================================
# Host directive (for Yandex)

View File

@ -7,7 +7,6 @@
// External CSS imports - these URL imports don't trigger deprecation warnings
// 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');
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css');
// 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,
@ -122,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 */

View File

@ -8,9 +8,7 @@
]
},
"files": [
"src/main.ts",
"src/main.server.ts",
"server.ts"
"src/main.ts"
],
"include": [
"src/**/*.d.ts"