feat: Add comprehensive user authentication, listing management, and core UI components.

This commit is contained in:
Timo Knuth 2026-02-04 21:32:25 +01:00
parent 737329794c
commit 23f7caedeb
34 changed files with 223 additions and 107 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

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

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

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

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

@ -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,12 @@
import { environment } from '../../environments/environment';
// Development: use browser-bunyan for rich logging
// Production: use lightweight console wrapper to avoid loading 50KB+ library
export const createLogger = environment.production
? (name: string) => ({
info: (...args: any[]) => console.log(`[${name}]`, ...args),
warn: (...args: any[]) => console.warn(`[${name}]`, ...args),
error: (...args: any[]) => console.error(`[${name}]`, ...args),
debug: () => {}, // no-op in production
})
: require('browser-bunyan').createLogger;

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: 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: // Build information, automatically generated by `the_build_script` :zwinkern:
const build = { const build = {
timestamp: "GER: 04.02.2026 15:43 | TX: 02/04/2026 8:43 AM" timestamp: "GER: 04.02.2026 21:20 | TX: 02/04/2026 2:20 PM"
}; };
export default build; export default build;

View File

@ -7,7 +7,6 @@
// External CSS imports - these URL imports don't trigger deprecation warnings // External CSS imports - these URL imports don't trigger deprecation warnings
// Using css2 API with specific weights for better performance // 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://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 // 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,
@ -122,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 */

View File

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