11 Commits

55 changed files with 471 additions and 179 deletions

View File

@@ -47,6 +47,7 @@
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"groq-sdk": "^0.5.0", "groq-sdk": "^0.5.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"helmet": "^8.1.0",
"nest-winston": "^1.9.4", "nest-winston": "^1.9.4",
"nestjs-cls": "^5.4.0", "nestjs-cls": "^5.4.0",
"nodemailer": "^7.0.12", "nodemailer": "^7.0.12",

View File

@@ -1,6 +1,7 @@
import { LoggerService } from '@nestjs/common'; import { LoggerService } from '@nestjs/common';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import express from 'express'; import express from 'express';
import helmet from 'helmet';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
@@ -22,6 +23,37 @@ async function bootstrap() {
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
allowedHeaders: 'Content-Type, Accept, Authorization, x-hide-loading', 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); await app.listen(process.env.PORT || 3001);
} }
bootstrap(); bootstrap();

View File

@@ -69,8 +69,8 @@
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "2kb", "maximumWarning": "30kb",
"maximumError": "4kb" "maximumError": "30kb"
} }
], ],
"outputHashing": "all" "outputHashing": "all"

View File

@@ -0,0 +1 @@
google-site-verification: googleccd5315437d68a49.html

View File

@@ -26,6 +26,74 @@ export async function app(): Promise<express.Express> {
server.set('view engine', 'html'); server.set('view engine', 'html');
server.set('views', browserDistFolder); 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 // Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { }); // server.get('/api/**', (req, res) => { });
// Serve static files from /browser // 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 { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { getAuth, provideAuth } from '@angular/fire/auth'; import { getAuth, provideAuth } from '@angular/fire/auth';
import { provideAnimations } from '@angular/platform-browser/animations'; 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 { environment } from '../environments/environment';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { AuthInterceptor } from './interceptors/auth.interceptor'; import { AuthInterceptor } from './interceptors/auth.interceptor';
@@ -48,13 +44,6 @@ export const appConfig: ApplicationConfig = {
provide: 'TIMEOUT_DURATION', provide: 'TIMEOUT_DURATION',
useValue: 5000, // Standard-Timeout von 5 Sekunden 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: ErrorHandler, useClass: GlobalErrorHandler }, // Registriere den globalen ErrorHandler
{ {
provide: IMAGE_CONFIG, provide: IMAGE_CONFIG,
@@ -62,13 +51,6 @@ export const appConfig: ApplicationConfig = {
disableImageSizeWarning: true, disableImageSizeWarning: true,
}, },
}, },
provideShareButtonsOptions(
shareIcons(),
withConfig({
debug: true,
sharerMethod: SharerMethods.Anchor,
}),
),
provideRouter( provideRouter(
routes, routes,
withEnabledBlockingInitialNavigation(), withEnabledBlockingInitialNavigation(),
@@ -79,18 +61,6 @@ export const appConfig: ApplicationConfig = {
), ),
...(environment.production ? [POSTHOG_INIT_PROVIDER] : []), ...(environment.production ? [POSTHOG_INIT_PROVIDER] : []),
provideAnimations(), 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)), provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
provideAuth(() => getAuth()), provideAuth(() => getAuth()),
], ],

View File

@@ -11,19 +11,14 @@ import { LoginRegisterComponent } from './components/login-register/login-regist
import { AuthGuard } from './guards/auth.guard'; import { AuthGuard } from './guards/auth.guard';
import { ListingCategoryGuard } from './guards/listing-category.guard'; import { ListingCategoryGuard } from './guards/listing-category.guard';
// Public pages (eagerly loaded - high traffic) // Public pages - HomeComponent stays eagerly loaded as landing page
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
import { HomeComponent } from './pages/home/home.component'; import { HomeComponent } from './pages/home/home.component';
import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component';
import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component';
import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component';
import { SuccessComponent } from './pages/success/success.component'; import { SuccessComponent } from './pages/success/success.component';
import { TermsOfUseComponent } from './pages/legal/terms-of-use.component'; import { TermsOfUseComponent } from './pages/legal/terms-of-use.component';
import { PrivacyStatementComponent } from './pages/legal/privacy-statement.component'; import { PrivacyStatementComponent } from './pages/legal/privacy-statement.component';
// Note: Account, Edit, Admin, Favorites, MyListing, and EmailUs components are now lazy-loaded below // Note: All listing and details components are now lazy-loaded for better initial bundle size
export const routes: Routes = [ export const routes: Routes = [
{ {
@@ -32,17 +27,17 @@ export const routes: Routes = [
}, },
{ {
path: 'businessListings', path: 'businessListings',
component: BusinessListingsComponent, loadComponent: () => import('./pages/listings/business-listings/business-listings.component').then(m => m.BusinessListingsComponent),
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
{ {
path: 'commercialPropertyListings', path: 'commercialPropertyListings',
component: CommercialPropertyListingsComponent, loadComponent: () => import('./pages/listings/commercial-property-listings/commercial-property-listings.component').then(m => m.CommercialPropertyListingsComponent),
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
{ {
path: 'brokerListings', path: 'brokerListings',
component: BrokerListingsComponent, loadComponent: () => import('./pages/listings/broker-listings/broker-listings.component').then(m => m.BrokerListingsComponent),
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
{ {
@@ -53,11 +48,11 @@ export const routes: Routes = [
// Listings Details - New SEO-friendly slug-based URLs // Listings Details - New SEO-friendly slug-based URLs
{ {
path: 'business/:slug', path: 'business/:slug',
component: DetailsBusinessListingComponent, loadComponent: () => import('./pages/details/details-business-listing/details-business-listing.component').then(m => m.DetailsBusinessListingComponent),
}, },
{ {
path: 'commercial-property/:slug', 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 // Backward compatibility redirects for old UUID-based URLs
{ {
@@ -95,7 +90,7 @@ export const routes: Routes = [
// User Details // User Details
{ {
path: 'details-user/:id', path: 'details-user/:id',
component: DetailsUserComponent, loadComponent: () => import('./pages/details/details-user/details-user.component').then(m => m.DetailsUserComponent),
}, },
// ######### // #########
// User edit (lazy-loaded) // User edit (lazy-loaded)

View File

@@ -13,7 +13,7 @@ import { ConfirmationService } from './confirmation.service';
<button <button
(click)="confirmationService.reject()" (click)="confirmationService.reject()"
type="button" 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"> <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" /> <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> <span class="sr-only">Close modal</span>
</button> </button>
<div class="p-4 md:p-5 text-center"> <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" /> <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> </svg>
@let confirmation = (confirmationService.confirmation$ | async); @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'){ @if(confirmation?.buttons==='both'){
<button <button
(click)="confirmationService.accept()" (click)="confirmationService.accept()"
@@ -37,7 +37,7 @@ import { ConfirmationService } from './confirmation.service';
<button <button
(click)="confirmationService.reject()" (click)="confirmationService.reject()"
type="button" 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 No, cancel
</button> </button>

View File

@@ -9,7 +9,7 @@
<button <button
(click)="eMailService.reject()" (click)="eMailService.reject()"
type="button" 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"> <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" /> <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"> <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" /> --> <!-- <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"> <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> </a>
<p class="text-sm text-neutral-600 text-center lg:text-left">© {{ currentYear }} Bizmatch All rights reserved.</p> <p class="text-sm text-neutral-600 text-center lg:text-left">© {{ currentYear }} Bizmatch All rights reserved.</p>
</div> </div>
@@ -23,9 +23,9 @@
</div> </div>
<div class="mb-4 lg:mb-0 flex flex-col items-center lg:items-end"> <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> 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&#64;bizmatch.net </a> class="fas fa-envelope mr-2"></i>info&#64;bizmatch.net </a>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,7 @@
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4"> <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"> <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" <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> </a>
<div class="flex items-center md:order-2 space-x-3 rtl:space-x-reverse"> <div class="flex items-center md:order-2 space-x-3 rtl:space-x-reverse">
<!-- Filter button --> <!-- 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" 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')"> (click)="closeMenusAndSetCriteria('brokerListings')">
<img src="/assets/images/icon_professionals.png" alt="Professionals" <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> <span>Professionals</span>
</a> </a>
</li> </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 { Component, HostListener, OnDestroy, OnInit, AfterViewInit, PLATFORM_ID, inject } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NavigationEnd, Router, RouterModule } from '@angular/router'; 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 { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Collapse, Dropdown, initFlowbite } from 'flowbite'; import { Collapse, Dropdown, initFlowbite } from 'flowbite';
import { filter, Observable, Subject, takeUntil } from 'rxjs'; import { filter, Observable, Subject, takeUntil } from 'rxjs';
@@ -35,7 +35,7 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
keycloakUser: KeycloakUser; keycloakUser: KeycloakUser;
user: User; user: User;
activeItem; activeItem;
faUserGear = faUserGear; faUserGear = APP_ICONS.faUserGear;
profileUrl: string; profileUrl: string;
env = environment; env = environment;
private filterDropdown: Dropdown | null = null; private filterDropdown: Dropdown | null = null;

View File

@@ -35,7 +35,7 @@
placeholder="Please enter E-Mail Address" 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" 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>
</div> </div>
@@ -50,7 +50,7 @@
placeholder="Please enter Password" 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" 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>
</div> </div>
@@ -65,7 +65,7 @@
placeholder="Repeat Password" 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" 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>
</div> </div>

View File

@@ -3,7 +3,7 @@ import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 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 { AuthService } from '../../services/auth.service';
import { LoadingService } from '../../services/loading.service'; import { LoadingService } from '../../services/loading.service';
@Component({ @Component({
@@ -18,10 +18,10 @@ export class LoginRegisterComponent {
confirmPassword: string = ''; confirmPassword: string = '';
isLoginMode: boolean = true; // true: Login, false: Registration isLoginMode: boolean = true; // true: Login, false: Registration
errorMessage: string = ''; errorMessage: string = '';
envelope = faEnvelope; envelope = APP_ICONS.faEnvelope;
lock = faLock; lock = APP_ICONS.faLock;
arrowRight = faArrowRight; arrowRight = APP_ICONS.faArrowRight;
userplus = faUserPlus; userplus = APP_ICONS.faUserPlus;
constructor(private authService: AuthService, private route: ActivatedRoute, private router: Router, private loadingService: LoadingService) {} constructor(private authService: AuthService, private route: ActivatedRoute, private router: Router, private loadingService: LoadingService) {}
ngOnInit(): void { ngOnInit(): void {

View File

@@ -48,7 +48,7 @@
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label> <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>
<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> <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 class="space-y-4">
<div> <div>
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label> <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>
<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> <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 class="space-y-4">
<div> <div>
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label> <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>
<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> <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 class="space-y-4">
<div> <div>
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label> <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>
<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> <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="grid grid-cols-1 gap-6">
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label> <label for="state-select" 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" 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>
<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> <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 class="space-y-4">
<div> <div>
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label> <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>
<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> <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 { AuthService } from '../../../services/auth.service';
import { BaseDetailsComponent } from '../base-details.component'; import { BaseDetailsComponent } from '../base-details.component';
import { ShareButton } from 'ngx-sharebuttons/button'; import { ShareButton } from 'ngx-sharebuttons/button';
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
import { shareIcons } from 'ngx-sharebuttons/icons';
@Component({ @Component({
selector: 'app-details-business-listing', selector: 'app-details-business-listing',
standalone: true, standalone: true,
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, LeafletModule, BreadcrumbsComponent, ShareButton, NgOptimizedImage], 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', 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 { export class DetailsBusinessListingComponent extends BaseDetailsComponent {
// listings: Array<BusinessListing>; // listings: Array<BusinessListing>;

View File

@@ -3,9 +3,9 @@ import { NgOptimizedImage } from '@angular/common';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { LeafletModule } from '@bluehalo/ngx-leaflet'; 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 dayjs from 'dayjs';
import { GalleryModule, ImageItem } from 'ng-gallery'; import { GALLERY_CONFIG, GalleryConfig, GalleryModule, ImageItem } from 'ng-gallery';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { CommercialPropertyListing, EventTypeEnum, ShareByEMail, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { CommercialPropertyListing, EventTypeEnum, ShareByEMail, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { CommercialPropertyListingCriteria, ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.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 { BaseDetailsComponent } from '../base-details.component';
import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component'; import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component';
import { ShareButton } from 'ngx-sharebuttons/button'; import { ShareButton } from 'ngx-sharebuttons/button';
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
import { shareIcons } from 'ngx-sharebuttons/icons';
@Component({ @Component({
selector: 'app-details-commercial-property-listing', selector: 'app-details-commercial-property-listing',
standalone: true, standalone: true,
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ValidatedNgSelectComponent, GalleryModule, LeafletModule, BreadcrumbsComponent, ShareButton, NgOptimizedImage], 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', 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 { export class DetailsCommercialPropertyListingComponent extends BaseDetailsComponent {
responsiveOptions = [ responsiveOptions = [
@@ -69,7 +90,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
ts = new Date().getTime(); ts = new Date().getTime();
env = environment; env = environment;
errorResponse: ErrorResponse; errorResponse: ErrorResponse;
faTimes = faTimes; faTimes = APP_ICONS.faTimes;
propertyDetails = []; propertyDetails = [];
images: Array<ImageItem> = []; images: Array<ImageItem> = [];
relatedListings: CommercialPropertyListing[] = []; relatedListings: CommercialPropertyListing[] = [];

View File

@@ -19,13 +19,27 @@ import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { formatPhoneNumber, map2User } from '../../../utils/utils'; import { formatPhoneNumber, map2User } from '../../../utils/utils';
import { ShareButton } from 'ngx-sharebuttons/button'; import { ShareButton } from 'ngx-sharebuttons/button';
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
import { shareIcons } from 'ngx-sharebuttons/icons';
@Component({ @Component({
selector: 'app-details-user', selector: 'app-details-user',
standalone: true, standalone: true,
imports: [SharedModule, BreadcrumbsComponent, NgOptimizedImage, ShareButton], imports: [SharedModule, BreadcrumbsComponent, NgOptimizedImage, ShareButton],
providers: [
provideShareButtonsOptions(
shareIcons(),
withConfig({
debug: false,
sharerMethod: SharerMethods.Anchor,
}),
),
],
templateUrl: './details-user.component.html', templateUrl: './details-user.component.html',
styleUrl: '../details.scss', styleUrls: [
'../details.scss',
'../../../../../node_modules/ngx-sharebuttons/themes/default.scss'
],
}) })
export class DetailsUserComponent { export class DetailsUserComponent {
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;

View File

@@ -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"> <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"> <div class="hidden md:flex items-center space-x-4">
@if(user){ @if(user){
<a routerLink="/account" class="text-primary-600 border border-primary-600 px-3 py-2 rounded">Account</a> <a routerLink="/account" class="text-primary-600 border border-primary-600 px-3 py-2 rounded">Account</a>
@@ -12,7 +12,7 @@
</div> </div>
<button <button
(click)="toggleMenu()" (click)="toggleMenu()"
class="md:hidden text-neutral-600" class="md:hidden text-neutral-700 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center"
aria-label="Open navigation menu" aria-label="Open navigation menu"
[attr.aria-expanded]="isMenuOpen" [attr.aria-expanded]="isMenuOpen"
> >
@@ -88,8 +88,8 @@
<!-- Restliche Anpassungen (Innenabstände, Button-Paddings etc.) bleiben wie im vorherigen Schritt --> <!-- 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 }"> <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){ @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"> <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"> <ul class="flex flex-wrap -mb-px w-full" role="tablist" aria-label="Search categories">
<li class="w-[33%]" role="presentation"> <li class="w-[33%]" role="presentation">
<button <button
type="button" type="button"
@@ -99,9 +99,11 @@
[ngClass]=" [ngClass]="
activeTabAction === 'business' activeTabAction === 'business'
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500'] ? ['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 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" 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="" 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" /> <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> <span>Businesses</span>
@@ -117,9 +119,11 @@
[ngClass]=" [ngClass]="
activeTabAction === 'commercialProperty' activeTabAction === 'commercialProperty'
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500'] ? ['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 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" 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="" 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" /> <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> <span>Properties</span>
@@ -135,9 +139,11 @@
[ngClass]=" [ngClass]="
activeTabAction === 'broker' activeTabAction === 'broker'
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500'] ? ['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 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" 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 <img
src="/assets/images/icon_professionals.png" src="/assets/images/icon_professionals.png"
@@ -153,7 +159,7 @@
</ul> </ul>
</div> </div>
} @if(criteria && !aiSearch){ } @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="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"> <div class="relative max-sm:border border-neutral-300 rounded-md">
<label for="type-filter" class="sr-only">Filter by type</label> <label for="type-filter" class="sr-only">Filter by type</label>
@@ -195,7 +201,11 @@
groupBy="type" groupBy="type"
labelForId="location-search" labelForId="location-search"
aria-label="Search by city or state" 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'?' - ':''; @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> <ng-option [value]="city">{{ city.content.name }}{{ separator }}{{ state }}</ng-option>
} }

View File

@@ -1,7 +1,7 @@
select:not([size]) { select:not([size]) {
background-image: unset; background-image: unset;
} }
[type='text'], [type='text'],
[type='email'], [type='email'],
[type='url'], [type='url'],
@@ -19,39 +19,51 @@ textarea,
select { select {
border: unset; border: unset;
} }
.toggle-checkbox:checked { .toggle-checkbox:checked {
right: 0; right: 0;
border-color: rgb(125 211 252); border-color: rgb(125 211 252);
} }
.toggle-checkbox:checked+.toggle-label { .toggle-checkbox:checked+.toggle-label {
background-color: rgb(125 211 252); background-color: rgb(125 211 252);
} }
:host ::ng-deep .ng-select.ng-select-single .ng-select-container { :host ::ng-deep .ng-select.ng-select-single .ng-select-container {
min-height: 52px; min-height: 52px;
border: none; border: none;
background-color: transparent; background-color: transparent;
.ng-value-container .ng-input { .ng-value-container .ng-input {
top: 12px; top: 12px;
} }
span.ng-arrow-wrapper { span.ng-arrow-wrapper {
display: none; display: none;
} }
} }
select { 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 */ // background-color: #fff; /* Hintergrundfarbe für das Dropdown */
} }
select option { select option {
color: #000; /* Textfarbe für Dropdown-Optionen */ color: #000;
/* Textfarbe für Dropdown-Optionen */
} }
select.placeholder-selected { select.placeholder-selected {
color: #999; /* Farbe für den Platzhalter */ color: #6b7280;
/* gray-500 - besserer Kontrast für WCAG AA */
} }
input::placeholder { input::placeholder {
color: #555; /* Dunkleres Grau */ color: #555;
opacity: 1; /* Stellt sicher, dass die Deckkraft 100% ist */ /* Dunkleres Grau */
opacity: 1;
/* Stellt sicher, dass die Deckkraft 100% ist */
} }
/* Stellt sicher, dass die Optionen im Dropdown immer schwarz sind */ /* Stellt sicher, dass die Optionen im Dropdown immer schwarz sind */
@@ -59,10 +71,14 @@ select:focus option,
select:hover option { select:hover option {
color: #000 !important; color: #000 !important;
} }
input[type='text'][name='aiSearchText'] { input[type='text'][name='aiSearchText'] {
padding: 14px; /* Innerer Abstand */ padding: 14px;
font-size: 16px; /* Schriftgröße anpassen */ /* Innerer Abstand */
box-sizing: border-box; /* Padding und Border in die Höhe und Breite einrechnen */ font-size: 16px;
/* Schriftgröße anpassen */
box-sizing: border-box;
/* Padding und Border in die Höhe und Breite einrechnen */
height: 48px; height: 48px;
} }
@@ -145,6 +161,7 @@ select,
opacity: 0; opacity: 0;
transform: translateY(10px); transform: translateY(10px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@@ -212,6 +229,7 @@ header {
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
&.text-blue-600.border.border-blue-600 { &.text-blue-600.border.border-blue-600 {
// Log In button // Log In button
&:hover { &:hover {
background-color: rgba(37, 99, 235, 0.05); background-color: rgba(37, 99, 235, 0.05);
@@ -224,6 +242,7 @@ header {
} }
&.bg-blue-600 { &.bg-blue-600 {
// Register button // Register button
&:hover { &:hover {
background-color: rgb(29, 78, 216); background-color: rgb(29, 78, 216);

View File

@@ -6,7 +6,7 @@ import { NgSelectModule } from '@ng-select/ng-select';
import { UntilDestroy } from '@ngneat/until-destroy'; import { UntilDestroy } from '@ngneat/until-destroy';
import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs'; 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 { 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 { ModalService } from '../../components/search-modal/modal.service';
import { TooltipComponent } from '../../components/tooltip/tooltip.component'; import { TooltipComponent } from '../../components/tooltip/tooltip.component';
import { AiService } from '../../services/ai.service'; import { AiService } from '../../services/ai.service';
@@ -24,7 +24,7 @@ import { map2User } from '../../utils/utils';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
standalone: true, standalone: true,
imports: [CommonModule, FormsModule, RouterModule, NgSelectModule, FaqComponent], imports: [CommonModule, FormsModule, RouterModule, NgSelectModule],
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.scss', styleUrl: './home.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -1,9 +1,9 @@
import { DatePipe, TitleCasePipe } from '@angular/common'; import { DatePipe, TitleCasePipe } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; 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 { NgSelectModule } from '@ng-select/ng-select';
import { QuillModule } from 'ngx-quill'; import { QuillModule, provideQuillConfig } from 'ngx-quill';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { User } from '../../../../../../bizmatch-server/src/models/db.model'; import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, Invoice, UploadParams, ValidationMessage, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.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, ValidatedCountyComponent,
ValidatedLocationComponent, 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', templateUrl: './account.component.html',
styleUrl: './account.component.scss', styleUrls: [
'./account.component.scss',
'../../../../../node_modules/quill/dist/quill.snow.css'
],
}) })
export class AccountComponent { export class AccountComponent {
id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
@@ -58,7 +76,7 @@ export class AccountComponent {
environment = environment; environment = environment;
editorModules = TOOLBAR_OPTIONS; editorModules = TOOLBAR_OPTIONS;
env = environment; env = environment;
faTrash = faTrash; faTrash = APP_ICONS.faTrash;
quillModules = { quillModules = {
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']], 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 { map2User, routeListingWithState } from '../../../utils/utils';
import { DragDropModule } from '@angular/cdk/drag-drop'; 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 { QuillModule } from 'ngx-quill';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { NgxCurrencyDirective } from 'ngx-currency'; import { NgxCurrencyDirective } from 'ngx-currency';
import { provideQuillConfig } from 'ngx-quill';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty, createDefaultBusinessListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model'; import { AutoCompleteCompleteEvent, ImageProperty, createDefaultBusinessListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
@@ -47,9 +48,25 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
ValidatedTextareaComponent, ValidatedTextareaComponent,
ValidatedLocationComponent, 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', 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 { export class EditBusinessListingComponent {
listingsCategory = 'business'; listingsCategory = 'business';
@@ -64,7 +81,7 @@ export class EditBusinessListingComponent {
config = { aspectRatio: 16 / 9 }; config = { aspectRatio: 16 / 9 };
editorModules = TOOLBAR_OPTIONS; editorModules = TOOLBAR_OPTIONS;
draggedImage: ImageProperty; draggedImage: ImageProperty;
faTrash = faTrash; faTrash = APP_ICONS.faTrash;
data: CommercialPropertyListing; data: CommercialPropertyListing;
typesOfBusiness = []; typesOfBusiness = [];
quillModules = { quillModules = {

View File

@@ -58,7 +58,7 @@
(click)="uploadPropertyPicture()" (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" 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> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg> </svg>
Upload Upload

View File

@@ -7,11 +7,11 @@ import { map2User, routeListingWithState } from '../../../utils/utils';
import { DragDropModule } from '@angular/cdk/drag-drop'; import { DragDropModule } from '@angular/cdk/drag-drop';
import { ViewportRuler } from '@angular/cdk/scrolling'; 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 { NgSelectModule } from '@ng-select/ng-select';
import { NgxCurrencyDirective } from 'ngx-currency'; import { NgxCurrencyDirective } from 'ngx-currency';
import { ImageCropperComponent } from 'ngx-image-cropper'; 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 { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty, UploadParams, createDefaultCommercialPropertyListing, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.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, ValidatedLocationComponent,
ImageCropAndUploadComponent, 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', 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 { export class EditCommercialPropertyListingComponent {
@ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>; @ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
@@ -91,7 +107,7 @@ export class EditCommercialPropertyListingComponent {
editorModules = TOOLBAR_OPTIONS; editorModules = TOOLBAR_OPTIONS;
draggedImage: ImageProperty; draggedImage: ImageProperty;
faTrash = faTrash; faTrash = APP_ICONS.faTrash;
suggestions: string[] | undefined; suggestions: string[] | undefined;
data: BusinessListing; data: BusinessListing;
userId: string; userId: string;

View File

@@ -1,5 +1,5 @@
import { Injectable, inject, PLATFORM_ID } from '@angular/core'; import { Injectable, inject, PLATFORM_ID, Renderer2, RendererFactory2 } from '@angular/core';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser, DOCUMENT } from '@angular/common';
import { Meta, Title } from '@angular/platform-browser'; import { Meta, Title } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@@ -22,11 +22,17 @@ export class SeoService {
private router = inject(Router); private router = inject(Router);
private platformId = inject(PLATFORM_ID); private platformId = inject(PLATFORM_ID);
private isBrowser = isPlatformBrowser(this.platformId); 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 defaultImage = 'https://www.bizmatch.net/assets/images/bizmatch-og-image.jpg';
private readonly siteName = 'BizMatch'; private readonly siteName = 'BizMatch';
private readonly baseUrl = 'https://www.bizmatch.net'; private readonly baseUrl = 'https://www.bizmatch.net';
constructor(rendererFactory: RendererFactory2) {
this.renderer = rendererFactory.createRenderer(null, null);
}
/** /**
* Get the base URL for SEO purposes * 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 { private updateCanonicalUrl(url: string): void {
if (!this.isBrowser) return; let link: HTMLLinkElement | null = this.document.querySelector('link[rel="canonical"]');
let link: HTMLLinkElement | null = document.querySelector('link[rel="canonical"]');
if (link) { if (link) {
link.setAttribute('href', url); this.renderer.setAttribute(link, 'href', url);
} else { } else {
link = document.createElement('link'); link = this.renderer.createElement('link');
link.setAttribute('rel', 'canonical'); this.renderer.setAttribute(link, 'rel', 'canonical');
link.setAttribute('href', url); this.renderer.setAttribute(link, 'href', url);
document.head.appendChild(link); 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 { injectStructuredData(schema: object): void {
if (!this.isBrowser) return; // Clear existing schema scripts with the same type
this.removeAllSchemas();
// Remove existing schema script // Create new script element using Renderer2 (works in both SSR and browser)
const existingScript = document.querySelector('script[type="application/ld+json"]'); const script = this.renderer.createElement('script');
if (existingScript) { this.renderer.setAttribute(script, 'type', 'application/ld+json');
existingScript.remove(); this.renderer.setAttribute(script, 'data-schema', 'true');
}
// Add new schema script // Create text node with schema JSON
const script = document.createElement('script'); const schemaText = this.renderer.createText(JSON.stringify(schema));
script.type = 'application/ld+json'; this.renderer.appendChild(script, schemaText);
script.text = JSON.stringify(schema);
document.head.appendChild(script); // 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 { clearStructuredData(): void {
if (!this.isBrowser) return; this.removeAllSchemas();
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
scripts.forEach(script => script.remove());
} }
/** /**
@@ -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 { injectMultipleSchemas(schemas: object[]): void {
if (!this.isBrowser) return; // Clear existing schema scripts
this.removeAllSchemas();
// Remove existing schema scripts // Add new schema scripts using Renderer2
this.clearStructuredData();
// Add new schema scripts
schemas.forEach(schema => { schemas.forEach(schema => {
const script = document.createElement('script'); const script = this.renderer.createElement('script');
script.type = 'application/ld+json'; this.renderer.setAttribute(script, 'type', 'application/ld+json');
script.text = JSON.stringify(schema); this.renderer.setAttribute(script, 'data-schema', 'true');
document.head.appendChild(script);
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 { 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 { jwtDecode } from 'jwt-decode';
import onChange from 'on-change'; import onChange from 'on-change';
import { SortByOptions, User } from '../../../../bizmatch-server/src/models/db.model'; import { SortByOptions, User } from '../../../../bizmatch-server/src/models/db.model';
@@ -141,14 +141,8 @@ export function createMailInfo(user?: User): MailInfo {
listing: null, listing: null,
}; };
} }
export function createLogger(name: string, level: number = INFO, options: any = {}) { export function createLogger(name: string, level?: number, options?: any) {
return _createLogger({ return _createLogger(name);
name,
streams: [{ level, stream: new ConsoleFormattedStream() }],
serializers: stdSerializers,
src: true,
...options,
});
} }
export function formatPhoneNumber(phone: string): string { export function formatPhoneNumber(phone: string): string {
const cleaned = ('' + phone).replace(/\D/g, ''); 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: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -1,6 +1,6 @@
// Build information, automatically generated by `the_build_script` :zwinkern: // Build information, automatically generated by `the_build_script` :zwinkern:
const build = { const build = {
timestamp: "GER: 03.02.2026 12:44 | TX: 02/03/2026 5:44 AM" timestamp: "GER: 05.02.2026 13:06 | TX: 02/05/2026 6:06 AM"
}; };
export default build; export default build;

View File

@@ -35,6 +35,9 @@
<!-- Preload critical assets --> <!-- Preload critical assets -->
<link rel="preload" as="image" href="/assets/images/header-logo.png" type="image/png" /> <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 --> <!-- Prefetch common assets -->
<link rel="prefetch" as="image" href="/assets/images/business_logo.png" /> <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-32x32.png" sizes="32x32" />
<link rel="icon" href="/assets/cropped-Favicon-192x192.png" sizes="192x192" /> <link rel="icon" href="/assets/cropped-Favicon-192x192.png" sizes="192x192" />
<link rel="apple-touch-icon" href="/assets/cropped-Favicon-180x180.png" /> <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> </head>
<body class="flex flex-col min-h-screen"> <body class="flex flex-col min-h-screen">

View File

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

View File

@@ -5,8 +5,8 @@
@tailwind utilities; @tailwind utilities;
// External CSS imports - these URL imports don't trigger deprecation warnings // External CSS imports - these URL imports don't trigger deprecation warnings
@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap'); // Using css2 API with specific weights for better performance
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css'); @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 // 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, // Note: These are loaded via angular.json styles array is the preferred approach,
@@ -121,7 +121,7 @@ p-menubarsub ul {
input::placeholder, input::placeholder,
textarea::placeholder { textarea::placeholder {
color: #999 !important; color: #757575 !important; /* 4.54:1 contrast - WCAG AA compliant */
} }
/* Fix für Marker-Icons in Leaflet */ /* Fix für Marker-Icons in Leaflet */