npm run serve:ssr funktioniert und Hamburger Menu bug fix

This commit is contained in:
knuthtimo-lab 2026-01-06 22:36:14 +01:00
parent 43027a54f7
commit 4f8fd77f7d
21 changed files with 371 additions and 111 deletions

View File

@ -26,6 +26,12 @@
"ssr": { "ssr": {
"entry": "server.ts" "entry": "server.ts"
}, },
"allowedCommonJsDependencies": [
"quill-delta",
"leaflet",
"dayjs",
"qs"
],
"polyfills": [ "polyfills": [
"zone.js" "zone.js"
], ],

View File

@ -3,17 +3,24 @@ import './src/ssr-dom-polyfill';
import { APP_BASE_HREF } from '@angular/common'; import { APP_BASE_HREF } from '@angular/common';
import { AngularNodeAppEngine, createNodeRequestHandler, writeResponseToNodeResponse } from '@angular/ssr/node'; import { AngularNodeAppEngine, createNodeRequestHandler, writeResponseToNodeResponse } from '@angular/ssr/node';
import { ɵsetAngularAppEngineManifest as setAngularAppEngineManifest } from '@angular/ssr';
import express from 'express'; import express from 'express';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path'; import { dirname, join, resolve } from 'node:path';
// The Express app is exported so that it can be used by serverless Functions. // The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express { export async function app(): Promise<express.Express> {
const server = express(); const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url)); const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser'); const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html'); const indexHtml = join(serverDistFolder, 'index.server.html');
// Explicitly load and set the Angular app engine manifest
// This is required for environments where the manifest is not auto-loaded
const manifestPath = join(serverDistFolder, 'angular-app-engine-manifest.mjs');
const manifest = await import(manifestPath);
setAngularAppEngineManifest(manifest.default);
const angularApp = new AngularNodeAppEngine(); const angularApp = new AngularNodeAppEngine();
server.set('view engine', 'html'); server.set('view engine', 'html');
@ -27,27 +34,46 @@ export function app(): express.Express {
})); }));
// All regular routes use the Angular engine // All regular routes use the Angular engine
server.get('*', (req, res, next) => { server.get('*', async (req, res, next) => {
angularApp console.log(`[SSR] Handling request: ${req.method} ${req.url}`);
.handle(req) try {
.then((response) => { const response = await angularApp.handle(req);
if (response) { if (response) {
console.log(`[SSR] Response received for ${req.url}, status: ${response.status}`);
writeResponseToNodeResponse(response, res); writeResponseToNodeResponse(response, res);
} else { } else {
console.log(`[SSR] No response for ${req.url} - Angular engine returned null`);
console.log(`[SSR] This usually means the route couldn't be rendered. Check for:
1. Browser API usage in components
2. Missing platform checks
3. Errors during component initialization`);
res.sendStatus(404); res.sendStatus(404);
} }
}) } catch (err) {
.catch((err) => next(err)); console.error(`[SSR] Error handling ${req.url}:`, err);
console.error(`[SSR] Stack trace:`, err.stack);
next(err);
}
}); });
return server; return server;
} }
function run(): void { // Global error handlers for debugging
process.on('unhandledRejection', (reason, promise) => {
console.error('[SSR] Unhandled Rejection at:', promise, 'reason:', reason);
});
process.on('uncaughtException', (error) => {
console.error('[SSR] Uncaught Exception:', error);
console.error('[SSR] Stack:', error.stack);
});
async function run(): Promise<void> {
const port = process.env['PORT'] || 4200; const port = process.env['PORT'] || 4200;
// Start up the Node server // Start up the Node server
const server = app(); const server = await app();
server.listen(port, () => { server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`); console.log(`Node Express server listening on http://localhost:${port}`);
}); });

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule, isPlatformBrowser } from '@angular/common';
import { AfterViewInit, Component, HostListener } from '@angular/core'; import { AfterViewInit, Component, HostListener, PLATFORM_ID, inject } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { initFlowbite } from 'flowbite'; import { initFlowbite } from 'flowbite';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
@ -29,6 +29,8 @@ export class AppComponent implements AfterViewInit {
build = build; build = build;
title = 'bizmatch'; title = 'bizmatch';
actualRoute = ''; actualRoute = '';
private platformId = inject(PLATFORM_ID);
private isBrowser = isPlatformBrowser(this.platformId);
public constructor( public constructor(
public loadingService: LoadingService, public loadingService: LoadingService,
@ -48,9 +50,11 @@ export class AppComponent implements AfterViewInit {
this.actualRoute = currentRoute.snapshot.url[0].path; this.actualRoute = currentRoute.snapshot.url[0].path;
// Re-initialize Flowbite after navigation to ensure all components are ready // Re-initialize Flowbite after navigation to ensure all components are ready
if (this.isBrowser) {
setTimeout(() => { setTimeout(() => {
initFlowbite(); initFlowbite();
}, 50); }, 50);
}
}); });
} }
ngOnInit() { ngOnInit() {
@ -60,8 +64,10 @@ export class AppComponent implements AfterViewInit {
ngAfterViewInit() { ngAfterViewInit() {
// Initialize Flowbite for dropdowns, modals, and other interactive components // Initialize Flowbite for dropdowns, modals, and other interactive components
// Note: Drawers work automatically with data-drawer-target attributes // Note: Drawers work automatically with data-drawer-target attributes
if (this.isBrowser) {
initFlowbite(); initFlowbite();
} }
}
@HostListener('window:keydown', ['$event']) @HostListener('window:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) { handleKeyboardEvent(event: KeyboardEvent) {

View File

@ -1,11 +1,15 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server'; import { provideServerRendering } from '@angular/platform-server';
import { provideServerRouting } from '@angular/ssr';
import { appConfig } from './app.config'; import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
const serverConfig: ApplicationConfig = { const serverConfig: ApplicationConfig = {
providers: [ providers: [
provideServerRendering() provideServerRendering(),
provideServerRouting(serverRoutes)
] ]
}; };
export const config = mergeApplicationConfig(appConfig, serverConfig); export const config = mergeApplicationConfig(appConfig, serverConfig);

View File

@ -1,5 +1,6 @@
import { IMAGE_CONFIG } from '@angular/common'; import { IMAGE_CONFIG, isPlatformBrowser } from '@angular/common';
import { APP_INITIALIZER, ApplicationConfig, ErrorHandler } from '@angular/core'; import { APP_INITIALIZER, ApplicationConfig, ErrorHandler, PLATFORM_ID, inject } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router'; import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
@ -19,10 +20,12 @@ import { GlobalErrorHandler } from './services/globalErrorHandler';
import { POSTHOG_INIT_PROVIDER } from './services/posthog.factory'; import { POSTHOG_INIT_PROVIDER } from './services/posthog.factory';
import { SelectOptionsService } from './services/select-options.service'; import { SelectOptionsService } from './services/select-options.service';
import { createLogger } from './utils/utils'; import { createLogger } from './utils/utils';
// provideClientHydration()
const logger = createLogger('ApplicationConfig'); const logger = createLogger('ApplicationConfig');
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
// Temporarily disabled for SSR debugging
// provideClientHydration(),
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
@ -90,7 +93,6 @@ export const appConfig: ApplicationConfig = {
}), }),
provideFirebaseApp(() => initializeApp(environment.firebaseConfig)), provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
provideAuth(() => getAuth()), provideAuth(() => getAuth()),
// provideFirestore(() => getFirestore()),
], ],
}; };
function initServices(selectOptions: SelectOptionsService) { function initServices(selectOptions: SelectOptionsService) {

View File

@ -0,0 +1,8 @@
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '**',
renderMode: RenderMode.Server
}
];

View File

@ -1,6 +1,7 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { LogoutComponent } from './components/logout/logout.component'; import { LogoutComponent } from './components/logout/logout.component';
import { NotFoundComponent } from './components/not-found/not-found.component'; import { NotFoundComponent } from './components/not-found/not-found.component';
import { TestSsrComponent } from './components/test-ssr/test-ssr.component';
import { EmailAuthorizedComponent } from './components/email-authorized/email-authorized.component'; import { EmailAuthorizedComponent } from './components/email-authorized/email-authorized.component';
import { EmailVerificationComponent } from './components/email-verification/email-verification.component'; import { EmailVerificationComponent } from './components/email-verification/email-verification.component';
@ -26,6 +27,10 @@ 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';
export const routes: Routes = [ export const routes: Routes = [
{
path: 'test-ssr',
component: TestSsrComponent,
},
{ {
path: 'businessListings', path: 'businessListings',
component: BusinessListingsComponent, component: BusinessListingsComponent,

View File

@ -1,4 +1,5 @@
import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { createPopper, Instance as PopperInstance } from '@popperjs/core'; import { createPopper, Instance as PopperInstance } from '@popperjs/core';
@Component({ @Component({
@ -23,6 +24,8 @@ export class DropdownComponent implements AfterViewInit, OnDestroy {
@HostBinding('class.hidden') isHidden: boolean = true; @HostBinding('class.hidden') isHidden: boolean = true;
private platformId = inject(PLATFORM_ID);
private isBrowser = isPlatformBrowser(this.platformId);
private popperInstance: PopperInstance | null = null; private popperInstance: PopperInstance | null = null;
isVisible: boolean = false; isVisible: boolean = false;
private clickOutsideListener: any; private clickOutsideListener: any;
@ -30,6 +33,8 @@ export class DropdownComponent implements AfterViewInit, OnDestroy {
private hoverHideListener: any; private hoverHideListener: any;
ngAfterViewInit() { ngAfterViewInit() {
if (!this.isBrowser) return;
if (!this.triggerEl) { if (!this.triggerEl) {
console.error('Trigger element is not provided to the dropdown component.'); console.error('Trigger element is not provided to the dropdown component.');
return; return;
@ -58,6 +63,8 @@ export class DropdownComponent implements AfterViewInit, OnDestroy {
} }
private setupEventListeners() { private setupEventListeners() {
if (!this.isBrowser) return;
if (this.triggerType === 'click') { if (this.triggerType === 'click') {
this.triggerEl.addEventListener('click', () => this.toggle()); this.triggerEl.addEventListener('click', () => this.toggle());
} else if (this.triggerType === 'hover') { } else if (this.triggerType === 'hover') {
@ -74,6 +81,8 @@ export class DropdownComponent implements AfterViewInit, OnDestroy {
} }
private removeEventListeners() { private removeEventListeners() {
if (!this.isBrowser) return;
if (this.triggerType === 'click') { if (this.triggerType === 'click') {
this.triggerEl.removeEventListener('click', () => this.toggle()); this.triggerEl.removeEventListener('click', () => this.toggle());
} else if (this.triggerType === 'hover') { } else if (this.triggerType === 'hover') {
@ -104,7 +113,7 @@ export class DropdownComponent implements AfterViewInit, OnDestroy {
} }
private handleClickOutside(event: MouseEvent) { private handleClickOutside(event: MouseEvent) {
if (!this.isVisible) return; if (!this.isVisible || !this.isBrowser) return;
const clickedElement = event.target as HTMLElement; const clickedElement = event.target as HTMLElement;
if (this.ignoreClickOutsideClass) { if (this.ignoreClickOutsideClass) {

View File

@ -31,8 +31,7 @@
} }
<button type="button" <button type="button"
class="flex text-sm bg-neutral-400 rounded-full md:me-0 focus:ring-4 focus:ring-neutral-300 dark:focus:ring-neutral-600" class="flex text-sm bg-neutral-400 rounded-full md:me-0 focus:ring-4 focus:ring-neutral-300 dark:focus:ring-neutral-600"
id="user-menu-button" aria-expanded="false" [attr.data-dropdown-toggle]="user ? 'user-login' : 'user-unknown'" id="user-menu-button" aria-expanded="false" [attr.data-dropdown-toggle]="user ? 'user-login' : 'user-unknown'">
data-dropdown-placement="bottom">
<span class="sr-only">Open user menu</span> <span class="sr-only">Open user menu</span>
@if(isProfessional || (authService.isAdmin() | async) && user?.hasProfile){ @if(isProfessional || (authService.isAdmin() | async) && user?.hasProfile){
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}" <img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}"

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule, isPlatformBrowser } from '@angular/common';
import { Component, HostListener, OnDestroy, OnInit, AfterViewInit } 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 { faUserGear } from '@fortawesome/free-solid-svg-icons';
@ -42,6 +42,8 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
isMobile: boolean = false; isMobile: boolean = false;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
prompt: string; prompt: string;
private platformId = inject(PLATFORM_ID);
private isBrowser = isPlatformBrowser(this.platformId);
// Aktueller Listing-Typ basierend auf Route // Aktueller Listing-Typ basierend auf Route
currentListingType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings' | null = null; currentListingType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings' | null = null;
@ -74,6 +76,16 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
const excludedIds = ['sortDropdownButton', 'sortDropdownMobileButton', 'user-menu-button']; const excludedIds = ['sortDropdownButton', 'sortDropdownMobileButton', 'user-menu-button'];
if (!excludedIds.includes(target.id) && !target.closest('#user-menu-button')) { if (!excludedIds.includes(target.id) && !target.closest('#user-menu-button')) {
this.sortDropdownVisible = false; this.sortDropdownVisible = false;
// Close User Menu if clicked outside
// We check if the click was inside the menu containers
const userLogin = document.getElementById('user-login');
const userUnknown = document.getElementById('user-unknown');
const clickedInsideMenu = (userLogin && userLogin.contains(target)) || (userUnknown && userUnknown.contains(target));
if (!clickedInsideMenu) {
this.closeDropdown();
}
} }
} }
@ -103,7 +115,7 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
const previousUser = this.user; const previousUser = this.user;
this.user = u; this.user = u;
// Re-initialize Flowbite if user logged in/out state changed // Re-initialize Flowbite if user logged in/out state changed
if ((previousUser === null) !== (u === null)) { if ((previousUser === null) !== (u === null) && this.isBrowser) {
setTimeout(() => initFlowbite(), 50); setTimeout(() => initFlowbite(), 50);
} }
}); });
@ -223,6 +235,8 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
} }
closeDropdown() { closeDropdown() {
if (!this.isBrowser) return;
const dropdownButton = document.getElementById('user-menu-button'); const dropdownButton = document.getElementById('user-menu-button');
const dropdownMenu = this.user ? document.getElementById('user-login') : document.getElementById('user-unknown'); const dropdownMenu = this.user ? document.getElementById('user-login') : document.getElementById('user-unknown');
@ -233,6 +247,8 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
} }
closeMobileMenu() { closeMobileMenu() {
if (!this.isBrowser) return;
const targetElement = document.getElementById('navbar-user'); const targetElement = document.getElementById('navbar-user');
const triggerElement = document.querySelector('[data-collapse-toggle="navbar-user"]'); const triggerElement = document.querySelector('[data-collapse-toggle="navbar-user"]');
@ -293,12 +309,10 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewInit {
}; };
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
// Initialize Flowbite after header DOM is fully rendered // Flowbite initialization is now handled manually or via AppComponent
// This ensures all dropdown elements exist before initialization
setTimeout(() => {
initFlowbite();
}, 0);
} }
ngOnDestroy() { ngOnDestroy() {

View File

@ -0,0 +1,24 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-test-ssr',
standalone: true,
template: `
<div>
<h1>SSR Test Component</h1>
<p>If you see this, SSR is working!</p>
</div>
`,
styles: [`
div {
padding: 20px;
background: #f0f0f0;
}
h1 { color: green; }
`]
})
export class TestSsrComponent {
constructor() {
console.log('[SSR] TestSsrComponent constructor called');
}
}

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule, isPlatformBrowser } from '@angular/common';
import { Component, Input, SimpleChanges } from '@angular/core'; import { Component, Input, SimpleChanges, PLATFORM_ID, inject } from '@angular/core';
@Component({ @Component({
selector: 'app-tooltip', selector: 'app-tooltip',
@ -12,6 +12,9 @@ export class TooltipComponent {
@Input() text: string; @Input() text: string;
@Input() isVisible: boolean = false; @Input() isVisible: boolean = false;
private platformId = inject(PLATFORM_ID);
private isBrowser = isPlatformBrowser(this.platformId);
ngOnInit() { ngOnInit() {
this.initializeTooltip(); this.initializeTooltip();
} }
@ -27,6 +30,8 @@ export class TooltipComponent {
} }
private updateTooltipVisibility() { private updateTooltipVisibility() {
if (!this.isBrowser) return;
const tooltipElement = document.getElementById(this.id); const tooltipElement = document.getElementById(this.id);
if (tooltipElement) { if (tooltipElement) {
if (this.isVisible) { if (this.isVisible) {

View File

@ -1,11 +1,14 @@
import { inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common';
import { inject, PLATFORM_ID } from '@angular/core';
import { ResolveFn } from '@angular/router'; import { ResolveFn } from '@angular/router';
import { KeycloakService } from '../services/keycloak.service'; import { KeycloakService } from '../services/keycloak.service';
export const authResolver: ResolveFn<boolean> = async (route, state) => { export const authResolver: ResolveFn<boolean> = async (route, state) => {
const keycloakService: KeycloakService = inject(KeycloakService); const keycloakService: KeycloakService = inject(KeycloakService);
const platformId = inject(PLATFORM_ID);
const isBrowser = isPlatformBrowser(platformId);
if (!keycloakService.isLoggedIn()) { if (!keycloakService.isLoggedIn() && isBrowser) {
await keycloakService.login({ await keycloakService.login({
redirectUri: window.location.href, redirectUri: window.location.href,
}); });

View File

@ -1,6 +1,7 @@
// auth.service.ts // auth.service.ts
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { isPlatformBrowser } from '@angular/common';
import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpBackend, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
import { FirebaseApp } from '@angular/fire/app'; import { FirebaseApp } from '@angular/fire/app';
import { GoogleAuthProvider, UserCredential, createUserWithEmailAndPassword, getAuth, signInWithCustomToken, signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth'; import { GoogleAuthProvider, UserCredential, createUserWithEmailAndPassword, getAuth, signInWithCustomToken, signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth';
import { BehaviorSubject, Observable, catchError, firstValueFrom, map, of, shareReplay, take, tap } from 'rxjs'; import { BehaviorSubject, Observable, catchError, firstValueFrom, map, of, shareReplay, take, tap } from 'rxjs';
@ -14,8 +15,10 @@ export type UserRole = 'admin' | 'pro' | 'guest';
}) })
export class AuthService { export class AuthService {
private app = inject(FirebaseApp); private app = inject(FirebaseApp);
private auth = getAuth(this.app); private platformId = inject(PLATFORM_ID);
private http = inject(HttpClient); private isBrowser = isPlatformBrowser(this.platformId);
private auth = this.isBrowser ? getAuth(this.app) : null;
private http = new HttpClient(inject(HttpBackend));
private mailService = inject(MailService); private mailService = inject(MailService);
// Add a BehaviorSubject to track the current user role // Add a BehaviorSubject to track the current user role
private userRoleSubject = new BehaviorSubject<UserRole | null>(null); private userRoleSubject = new BehaviorSubject<UserRole | null>(null);
@ -31,6 +34,26 @@ export class AuthService {
this.loadRoleFromToken(); this.loadRoleFromToken();
} }
// Helper methods for localStorage access (only in browser)
private setLocalStorageItem(key: string, value: string): void {
if (this.isBrowser) {
localStorage.setItem(key, value);
}
}
private getLocalStorageItem(key: string): string | null {
if (this.isBrowser) {
return localStorage.getItem(key);
}
return null;
}
private removeLocalStorageItem(key: string): void {
if (this.isBrowser) {
localStorage.removeItem(key);
}
}
private loadRoleFromToken(): void { private loadRoleFromToken(): void {
this.getToken().then(token => { this.getToken().then(token => {
if (token) { if (token) {
@ -54,10 +77,15 @@ export class AuthService {
} }
// Registrierung mit Email und Passwort // Registrierung mit Email und Passwort
async registerWithEmail(email: string, password: string): Promise<UserCredential> { async registerWithEmail(email: string, password: string): Promise<UserCredential> {
// Bestimmen der aktuellen Umgebung/Domain für die Verifizierungs-URL if (!this.isBrowser || !this.auth) {
let verificationUrl = ''; throw new Error('Auth is only available in browser context');
}
// Prüfen der aktuellen Umgebung basierend auf dem Host // Bestimmen der aktuellen Umgebung/Domain für die Verifizierungs-URL
let verificationUrl = 'https://www.bizmatch.net/email-authorized';
// Prüfen der aktuellen Umgebung basierend auf dem Host (nur im Browser)
if (this.isBrowser) {
const currentHost = window.location.hostname; const currentHost = window.location.hostname;
if (currentHost.includes('localhost')) { if (currentHost.includes('localhost')) {
@ -67,6 +95,7 @@ export class AuthService {
} else { } else {
verificationUrl = 'https://www.bizmatch.net/email-authorized'; verificationUrl = 'https://www.bizmatch.net/email-authorized';
} }
}
// ActionCode-Einstellungen mit der dynamischen URL // ActionCode-Einstellungen mit der dynamischen URL
const actionCodeSettings = { const actionCodeSettings = {
@ -93,10 +122,10 @@ export class AuthService {
} }
// const token = await userCredential.user.getIdToken(); // const token = await userCredential.user.getIdToken();
// localStorage.setItem('authToken', token); // this.setLocalStorageItem('authToken', token);
// localStorage.setItem('refreshToken', userCredential.user.refreshToken); // this.setLocalStorageItem('refreshToken', userCredential.user.refreshToken);
// if (userCredential.user.photoURL) { // if (userCredential.user.photoURL) {
// localStorage.setItem('photoURL', userCredential.user.photoURL); // this.setLocalStorageItem('photoURL', userCredential.user.photoURL);
// } // }
return userCredential; return userCredential;
@ -104,13 +133,16 @@ export class AuthService {
// Login mit Email und Passwort // Login mit Email und Passwort
loginWithEmail(email: string, password: string): Promise<UserCredential> { loginWithEmail(email: string, password: string): Promise<UserCredential> {
if (!this.isBrowser || !this.auth) {
throw new Error('Auth is only available in browser context');
}
return signInWithEmailAndPassword(this.auth, email, password).then(async userCredential => { return signInWithEmailAndPassword(this.auth, email, password).then(async userCredential => {
if (userCredential.user) { if (userCredential.user) {
const token = await userCredential.user.getIdToken(); const token = await userCredential.user.getIdToken();
localStorage.setItem('authToken', token); this.setLocalStorageItem('authToken', token);
localStorage.setItem('refreshToken', userCredential.user.refreshToken); this.setLocalStorageItem('refreshToken', userCredential.user.refreshToken);
if (userCredential.user.photoURL) { if (userCredential.user.photoURL) {
localStorage.setItem('photoURL', userCredential.user.photoURL); this.setLocalStorageItem('photoURL', userCredential.user.photoURL);
} }
this.loadRoleFromToken(); this.loadRoleFromToken();
} }
@ -120,14 +152,17 @@ export class AuthService {
// Login mit Google // Login mit Google
loginWithGoogle(): Promise<UserCredential> { loginWithGoogle(): Promise<UserCredential> {
if (!this.isBrowser || !this.auth) {
throw new Error('Auth is only available in browser context');
}
const provider = new GoogleAuthProvider(); const provider = new GoogleAuthProvider();
return signInWithPopup(this.auth, provider).then(async userCredential => { return signInWithPopup(this.auth, provider).then(async userCredential => {
if (userCredential.user) { if (userCredential.user) {
const token = await userCredential.user.getIdToken(); const token = await userCredential.user.getIdToken();
localStorage.setItem('authToken', token); this.setLocalStorageItem('authToken', token);
localStorage.setItem('refreshToken', userCredential.user.refreshToken); this.setLocalStorageItem('refreshToken', userCredential.user.refreshToken);
if (userCredential.user.photoURL) { if (userCredential.user.photoURL) {
localStorage.setItem('photoURL', userCredential.user.photoURL); this.setLocalStorageItem('photoURL', userCredential.user.photoURL);
} }
this.loadRoleFromToken(); this.loadRoleFromToken();
} }
@ -137,13 +172,16 @@ export class AuthService {
// Logout: Token, RefreshToken und photoURL entfernen // Logout: Token, RefreshToken und photoURL entfernen
logout(): Promise<void> { logout(): Promise<void> {
localStorage.removeItem('authToken'); this.removeLocalStorageItem('authToken');
localStorage.removeItem('refreshToken'); this.removeLocalStorageItem('refreshToken');
localStorage.removeItem('photoURL'); this.removeLocalStorageItem('photoURL');
this.clearRoleCache(); this.clearRoleCache();
this.userRoleSubject.next(null); this.userRoleSubject.next(null);
if (this.auth) {
return this.auth.signOut(); return this.auth.signOut();
} }
return Promise.resolve();
}
isAdmin(): Observable<boolean> { isAdmin(): Observable<boolean> {
return this.getUserRole().pipe( return this.getUserRole().pipe(
map(role => role === 'admin'), map(role => role === 'admin'),
@ -202,10 +240,10 @@ export class AuthService {
// Force refresh the token to get updated custom claims // Force refresh the token to get updated custom claims
async refreshUserClaims(): Promise<void> { async refreshUserClaims(): Promise<void> {
this.clearRoleCache(); this.clearRoleCache();
if (this.auth.currentUser) { if (this.auth && this.auth.currentUser) {
await this.auth.currentUser.getIdToken(true); await this.auth.currentUser.getIdToken(true);
const token = await this.auth.currentUser.getIdToken(); const token = await this.auth.currentUser.getIdToken();
localStorage.setItem('authToken', token); this.setLocalStorageItem('authToken', token);
this.loadRoleFromToken(); this.loadRoleFromToken();
} }
} }
@ -234,7 +272,12 @@ export class AuthService {
} }
// Versucht, mit dem RefreshToken einen neuen Access Token zu erhalten // Versucht, mit dem RefreshToken einen neuen Access Token zu erhalten
async refreshToken(): Promise<string | null> { async refreshToken(): Promise<string | null> {
const storedRefreshToken = localStorage.getItem('refreshToken'); const storedRefreshToken = this.getLocalStorageItem('refreshToken');
// SSR protection: refreshToken should only run in browser
if (!this.isBrowser) {
return null;
}
if (!storedRefreshToken) { if (!storedRefreshToken) {
return null; return null;
} }
@ -250,8 +293,8 @@ export class AuthService {
// response enthält z.B. id_token, refresh_token, expires_in etc. // response enthält z.B. id_token, refresh_token, expires_in etc.
const newToken = response.id_token; const newToken = response.id_token;
const newRefreshToken = response.refresh_token; const newRefreshToken = response.refresh_token;
localStorage.setItem('authToken', newToken); this.setLocalStorageItem('authToken', newToken);
localStorage.setItem('refreshToken', newRefreshToken); this.setLocalStorageItem('refreshToken', newRefreshToken);
return newToken; return newToken;
} catch (error) { } catch (error) {
console.error('Error refreshing token:', error); console.error('Error refreshing token:', error);
@ -266,7 +309,12 @@ export class AuthService {
* Ist auch das nicht möglich, wird null zurückgegeben. * Ist auch das nicht möglich, wird null zurückgegeben.
*/ */
async getToken(): Promise<string | null> { async getToken(): Promise<string | null> {
const token = localStorage.getItem('authToken'); const token = this.getLocalStorageItem('authToken');
// SSR protection: return null on server
if (!this.isBrowser) {
return null;
}
if (token && !this.isEMailVerified(token)) { if (token && !this.isEMailVerified(token)) {
return null; return null;
} else if (token && this.isTokenValid(token) && this.isEMailVerified(token)) { } else if (token && this.isTokenValid(token) && this.isEMailVerified(token)) {
@ -278,6 +326,9 @@ export class AuthService {
// Add this new method to sign in with a custom token // Add this new method to sign in with a custom token
async signInWithCustomToken(token: string): Promise<void> { async signInWithCustomToken(token: string): Promise<void> {
if (!this.isBrowser || !this.auth) {
throw new Error('Auth is only available in browser context');
}
try { try {
// Sign in to Firebase with the custom token // Sign in to Firebase with the custom token
const userCredential = await signInWithCustomToken(this.auth, token); const userCredential = await signInWithCustomToken(this.auth, token);
@ -285,11 +336,11 @@ export class AuthService {
// Store the authentication token // Store the authentication token
if (userCredential.user) { if (userCredential.user) {
const idToken = await userCredential.user.getIdToken(); const idToken = await userCredential.user.getIdToken();
localStorage.setItem('authToken', idToken); this.setLocalStorageItem('authToken', idToken);
localStorage.setItem('refreshToken', userCredential.user.refreshToken); this.setLocalStorageItem('refreshToken', userCredential.user.refreshToken);
if (userCredential.user.photoURL) { if (userCredential.user.photoURL) {
localStorage.setItem('photoURL', userCredential.user.photoURL); this.setLocalStorageItem('photoURL', userCredential.user.photoURL);
} }
// Load user role from the token // Load user role from the token

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core'; import { isPlatformBrowser } from '@angular/common';
import { inject, Injectable, PLATFORM_ID } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { SortByOptions } from '../../../../bizmatch-server/src/models/db.model'; import { SortByOptions } from '../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model'; import { BusinessListingCriteria, CommercialPropertyListingCriteria, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
@ -27,6 +28,8 @@ interface FilterState {
export class FilterStateService { export class FilterStateService {
private state: FilterState; private state: FilterState;
private stateSubjects: Map<ListingType, BehaviorSubject<any>> = new Map(); private stateSubjects: Map<ListingType, BehaviorSubject<any>> = new Map();
private platformId = inject(PLATFORM_ID);
private isBrowser = isPlatformBrowser(this.platformId);
constructor() { constructor() {
// Initialize state from sessionStorage or with defaults // Initialize state from sessionStorage or with defaults
@ -125,10 +128,12 @@ export class FilterStateService {
} }
private saveToStorage(type: ListingType): void { private saveToStorage(type: ListingType): void {
if (!this.isBrowser) return;
sessionStorage.setItem(type, JSON.stringify(this.state[type].criteria)); sessionStorage.setItem(type, JSON.stringify(this.state[type].criteria));
} }
private saveSortByToStorage(type: ListingType, sortBy: SortByOptions | null): void { private saveSortByToStorage(type: ListingType, sortBy: SortByOptions | null): void {
if (!this.isBrowser) return;
const sortByKey = type === 'businessListings' ? 'businessSortBy' : type === 'commercialPropertyListings' ? 'commercialSortBy' : 'professionalsSortBy'; const sortByKey = type === 'businessListings' ? 'businessSortBy' : type === 'commercialPropertyListings' ? 'commercialSortBy' : 'professionalsSortBy';
if (sortBy) { if (sortBy) {
@ -156,10 +161,12 @@ export class FilterStateService {
} }
private loadCriteriaFromStorage(key: ListingType): CriteriaType { private loadCriteriaFromStorage(key: ListingType): CriteriaType {
if (this.isBrowser) {
const stored = sessionStorage.getItem(key); const stored = sessionStorage.getItem(key);
if (stored) { if (stored) {
return JSON.parse(stored); return JSON.parse(stored);
} }
}
switch (key) { switch (key) {
case 'businessListings': case 'businessListings':
@ -172,6 +179,7 @@ export class FilterStateService {
} }
private loadSortByFromStorage(key: string): SortByOptions | null { private loadSortByFromStorage(key: string): SortByOptions | null {
if (!this.isBrowser) return null;
const stored = sessionStorage.getItem(key); const stored = sessionStorage.getItem(key);
return stored && stored !== 'null' ? (stored as SortByOptions) : null; return stored && stored !== 'null' ? (stored as SortByOptions) : null;
} }

View File

@ -1,5 +1,6 @@
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { lastValueFrom, Observable, of } from 'rxjs'; import { lastValueFrom, Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { CityAndStateResult, CountyResult, GeoResult, IpInfo } from '../../../../bizmatch-server/src/models/main.model'; import { CityAndStateResult, CountyResult, GeoResult, IpInfo } from '../../../../bizmatch-server/src/models/main.model';
@ -21,6 +22,8 @@ export class GeoService {
private readonly storageKey = 'ipInfo'; private readonly storageKey = 'ipInfo';
private readonly boundaryStoragePrefix = 'nominatim_boundary_'; private readonly boundaryStoragePrefix = 'nominatim_boundary_';
private readonly cacheExpiration = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds private readonly cacheExpiration = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
private platformId = inject(PLATFORM_ID);
private isBrowser = isPlatformBrowser(this.platformId);
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
@ -28,6 +31,8 @@ export class GeoService {
* Get cached boundary data from localStorage * Get cached boundary data from localStorage
*/ */
private getCachedBoundary(cacheKey: string): any | null { private getCachedBoundary(cacheKey: string): any | null {
if (!this.isBrowser) return null;
try { try {
const cached = localStorage.getItem(this.boundaryStoragePrefix + cacheKey); const cached = localStorage.getItem(this.boundaryStoragePrefix + cacheKey);
if (!cached) { if (!cached) {
@ -54,6 +59,8 @@ export class GeoService {
* Save boundary data to localStorage * Save boundary data to localStorage
*/ */
private setCachedBoundary(cacheKey: string, data: any): void { private setCachedBoundary(cacheKey: string, data: any): void {
if (!this.isBrowser) return;
try { try {
const cachedData: CachedBoundary = { const cachedData: CachedBoundary = {
data: data, data: data,
@ -69,6 +76,8 @@ export class GeoService {
* Clear all cached boundary data * Clear all cached boundary data
*/ */
clearBoundaryCache(): void { clearBoundaryCache(): void {
if (!this.isBrowser) return;
try { try {
const keys = Object.keys(localStorage); const keys = Object.keys(localStorage);
keys.forEach(key => { keys.forEach(key => {
@ -136,17 +145,21 @@ export class GeoService {
async getIpInfo(): Promise<IpInfo | null> { async getIpInfo(): Promise<IpInfo | null> {
// Versuche zuerst, die Daten aus dem sessionStorage zu holen // Versuche zuerst, die Daten aus dem sessionStorage zu holen
if (this.isBrowser) {
const storedData = sessionStorage.getItem(this.storageKey); const storedData = sessionStorage.getItem(this.storageKey);
if (storedData) { if (storedData) {
return JSON.parse(storedData); return JSON.parse(storedData);
} }
}
try { try {
// Wenn keine Daten im Storage, hole sie vom Server // Wenn keine Daten im Storage, hole sie vom Server
const data = await lastValueFrom(this.http.get<IpInfo>(`${this.apiBaseUrl}/bizmatch/geo/ipinfo/georesult/wysiwyg`)); const data = await lastValueFrom(this.http.get<IpInfo>(`${this.apiBaseUrl}/bizmatch/geo/ipinfo/georesult/wysiwyg`));
// Speichere die Daten im sessionStorage // Speichere die Daten im sessionStorage
if (this.isBrowser) {
sessionStorage.setItem(this.storageKey, JSON.stringify(data)); sessionStorage.setItem(this.storageKey, JSON.stringify(data));
}
return data; return data;
} catch (error) { } catch (error) {

View File

@ -1,5 +1,6 @@
import { isPlatformBrowser } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { inject, Injectable, PLATFORM_ID } from '@angular/core';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { KeyValue, KeyValueAsSortBy, KeyValueStyle } from '../../../../bizmatch-server/src/models/main.model'; import { KeyValue, KeyValueAsSortBy, KeyValueStyle } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
@ -9,9 +10,28 @@ import { environment } from '../../environments/environment';
}) })
export class SelectOptionsService { export class SelectOptionsService {
private apiBaseUrl = environment.apiBaseUrl; private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) {} private platformId = inject(PLATFORM_ID);
constructor(private http: HttpClient) { }
async init() { async init() {
// Skip HTTP call on server-side to avoid blocking SSR
if (!isPlatformBrowser(this.platformId)) {
console.log('[SSR] SelectOptionsService.init() - Skipping HTTP call on server');
// Initialize with empty arrays - client will hydrate with real data
this.typesOfBusiness = [];
this.prices = [];
this.listingCategories = [];
this.customerTypes = [];
this.customerSubTypes = [];
this.states = [];
this.gender = [];
this.typesOfCommercialProperty = [];
this.distances = [];
this.sortByOptions = [];
return;
}
try {
const allSelectOptions = await lastValueFrom(this.http.get<any>(`${this.apiBaseUrl}/bizmatch/select-options`)); const allSelectOptions = await lastValueFrom(this.http.get<any>(`${this.apiBaseUrl}/bizmatch/select-options`));
this.typesOfBusiness = allSelectOptions.typesOfBusiness; this.typesOfBusiness = allSelectOptions.typesOfBusiness;
this.prices = allSelectOptions.prices; this.prices = allSelectOptions.prices;
@ -23,6 +43,20 @@ export class SelectOptionsService {
this.typesOfCommercialProperty = allSelectOptions.typesOfCommercialProperty; this.typesOfCommercialProperty = allSelectOptions.typesOfCommercialProperty;
this.distances = allSelectOptions.distances; this.distances = allSelectOptions.distances;
this.sortByOptions = allSelectOptions.sortByOptions; this.sortByOptions = allSelectOptions.sortByOptions;
} catch (error) {
console.error('[SelectOptionsService] Failed to load select options:', error);
// Initialize with empty arrays as fallback
this.typesOfBusiness = this.typesOfBusiness || [];
this.prices = this.prices || [];
this.listingCategories = this.listingCategories || [];
this.customerTypes = this.customerTypes || [];
this.customerSubTypes = this.customerSubTypes || [];
this.states = this.states || [];
this.gender = this.gender || [];
this.typesOfCommercialProperty = this.typesOfCommercialProperty || [];
this.distances = this.distances || [];
this.sortByOptions = this.sortByOptions || [];
}
} }
public typesOfBusiness: Array<KeyValueStyle>; public typesOfBusiness: Array<KeyValueStyle>;

View File

@ -1,4 +1,5 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } 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';
@ -19,6 +20,8 @@ export class SeoService {
private meta = inject(Meta); private meta = inject(Meta);
private title = inject(Title); private title = inject(Title);
private router = inject(Router); private router = inject(Router);
private platformId = inject(PLATFORM_ID);
private isBrowser = isPlatformBrowser(this.platformId);
private readonly defaultImage = 'https://biz-match.com/assets/images/bizmatch-og-image.jpg'; private readonly defaultImage = 'https://biz-match.com/assets/images/bizmatch-og-image.jpg';
private readonly siteName = 'BizMatch'; private readonly siteName = 'BizMatch';
@ -109,6 +112,8 @@ export class SeoService {
* Update canonical URL * Update canonical URL
*/ */
private updateCanonicalUrl(url: string): void { private updateCanonicalUrl(url: string): void {
if (!this.isBrowser) return;
let link: HTMLLinkElement | null = document.querySelector('link[rel="canonical"]'); let link: HTMLLinkElement | null = document.querySelector('link[rel="canonical"]');
if (link) { if (link) {
@ -267,6 +272,8 @@ export class SeoService {
* Inject JSON-LD structured data into page * Inject JSON-LD structured data into page
*/ */
injectStructuredData(schema: object): void { injectStructuredData(schema: object): void {
if (!this.isBrowser) return;
// Remove existing schema script // Remove existing schema script
const existingScript = document.querySelector('script[type="application/ld+json"]'); const existingScript = document.querySelector('script[type="application/ld+json"]');
if (existingScript) { if (existingScript) {
@ -284,6 +291,8 @@ export class SeoService {
* Clear all structured data * Clear all structured data
*/ */
clearStructuredData(): void { clearStructuredData(): void {
if (!this.isBrowser) return;
const scripts = document.querySelectorAll('script[type="application/ld+json"]'); const scripts = document.querySelectorAll('script[type="application/ld+json"]');
scripts.forEach(script => script.remove()); scripts.forEach(script => script.remove());
} }
@ -480,6 +489,8 @@ export class SeoService {
* Inject multiple structured data schemas * Inject multiple structured data schemas
*/ */
injectMultipleSchemas(schemas: object[]): void { injectMultipleSchemas(schemas: object[]): void {
if (!this.isBrowser) return;
// Remove existing schema scripts // Remove existing schema scripts
this.clearStructuredData(); this.clearStructuredData();
@ -591,6 +602,8 @@ export class SeoService {
* Inject pagination link elements (rel="next" and rel="prev") * Inject pagination link elements (rel="next" and rel="prev")
*/ */
injectPaginationLinks(baseUrl: string, currentPage: number, totalPages: number): void { injectPaginationLinks(baseUrl: string, currentPage: number, totalPages: number): void {
if (!this.isBrowser) return;
// Remove existing pagination links // Remove existing pagination links
document.querySelectorAll('link[rel="next"], link[rel="prev"]').forEach(link => link.remove()); document.querySelectorAll('link[rel="next"], link[rel="prev"]').forEach(link => link.remove());
@ -615,6 +628,8 @@ export class SeoService {
* Clear pagination links * Clear pagination links
*/ */
clearPaginationLinks(): void { clearPaginationLinks(): void {
if (!this.isBrowser) return;
document.querySelectorAll('link[rel="next"], link[rel="prev"]').forEach(link => link.remove()); document.querySelectorAll('link[rel="next"], link[rel="prev"]').forEach(link => link.remove());
} }
} }

View File

@ -159,8 +159,10 @@ export function formatPhoneNumber(phone: string): string {
} }
export const getSessionStorageHandler = function (criteriaType, path, value, previous, applyData) { export const getSessionStorageHandler = function (criteriaType, path, value, previous, applyData) {
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem(`${criteriaType}_criteria`, JSON.stringify(this)); sessionStorage.setItem(`${criteriaType}_criteria`, JSON.stringify(this));
console.log('Zusätzlicher Parameter:', criteriaType); console.log('Zusätzlicher Parameter:', criteriaType);
}
}; };
export const getSessionStorageHandlerWrapper = param => { export const getSessionStorageHandlerWrapper = param => {
return function (path, value, previous, applyData) { return function (path, value, previous, applyData) {
@ -191,6 +193,11 @@ export function map2User(jwt: string | null): KeycloakUser {
} }
export function getImageDimensions(imageUrl: string): Promise<{ width: number; height: number }> { export function getImageDimensions(imageUrl: string): Promise<{ width: number; height: number }> {
return new Promise(resolve => { return new Promise(resolve => {
// Only use Image in browser context
if (typeof Image === 'undefined') {
resolve({ width: 0, height: 0 });
return;
}
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
resolve({ width: img.width, height: img.height }); resolve({ width: img.width, height: img.height });
@ -295,9 +302,11 @@ export function checkAndUpdate(changed: boolean, condition: boolean, assignment:
return changed || condition; return changed || condition;
} }
export function removeSortByStorage() { export function removeSortByStorage() {
if (typeof sessionStorage !== 'undefined') {
sessionStorage.removeItem('businessSortBy'); sessionStorage.removeItem('businessSortBy');
sessionStorage.removeItem('commercialSortBy'); sessionStorage.removeItem('commercialSortBy');
sessionStorage.removeItem('professionalsSortBy'); sessionStorage.removeItem('professionalsSortBy');
}
} }
// ----------------------------- // -----------------------------
// Criteria Proxy // Criteria Proxy
@ -311,8 +320,11 @@ export function getCriteriaStateObject(criteriaType: 'businessListings' | 'comme
} else { } else {
initialState = createEmptyUserListingCriteria(); initialState = createEmptyUserListingCriteria();
} }
if (typeof sessionStorage !== 'undefined') {
const storedState = sessionStorage.getItem(`${criteriaType}`); const storedState = sessionStorage.getItem(`${criteriaType}`);
return storedState ? JSON.parse(storedState) : initialState; return storedState ? JSON.parse(storedState) : initialState;
}
return initialState;
} }
export function getCriteriaProxy(path: string, component: any): BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria { export function getCriteriaProxy(path: string, component: any): BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria {
if ('businessListings' === path) { if ('businessListings' === path) {
@ -327,7 +339,9 @@ export function getCriteriaProxy(path: string, component: any): BusinessListingC
} }
export function createEnhancedProxy(obj: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria, component: any) { export function createEnhancedProxy(obj: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria, component: any) {
const sessionStorageHandler = function (path, value, previous, applyData) { const sessionStorageHandler = function (path, value, previous, applyData) {
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem(`${obj.criteriaType}`, JSON.stringify(this)); sessionStorage.setItem(`${obj.criteriaType}`, JSON.stringify(this));
}
}; };
return onChange(obj, function (path, value, previous, applyData) { return onChange(obj, function (path, value, previous, applyData) {
@ -341,16 +355,20 @@ export function createEnhancedProxy(obj: BusinessListingCriteria | CommercialPro
}); });
} }
export function getCriteriaByListingCategory(listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') { export function getCriteriaByListingCategory(listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
if (typeof sessionStorage === 'undefined') return null;
const storedState = const storedState =
listingsCategory === 'business' listingsCategory === 'business'
? sessionStorage.getItem('businessListings') ? sessionStorage.getItem('businessListings')
: listingsCategory === 'commercialProperty' : listingsCategory === 'commercialProperty'
? sessionStorage.getItem('commercialPropertyListings') ? sessionStorage.getItem('commercialPropertyListings')
: sessionStorage.getItem('brokerListings'); : sessionStorage.getItem('brokerListings');
return JSON.parse(storedState); return storedState ? JSON.parse(storedState) : null;
} }
export function getSortByListingCategory(listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') { export function getSortByListingCategory(listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
if (typeof sessionStorage === 'undefined') return null;
const storedSortBy = const storedSortBy =
listingsCategory === 'business' ? sessionStorage.getItem('businessSortBy') : listingsCategory === 'commercialProperty' ? sessionStorage.getItem('commercialSortBy') : sessionStorage.getItem('professionalsSortBy'); listingsCategory === 'business' ? sessionStorage.getItem('businessSortBy') : listingsCategory === 'commercialProperty' ? sessionStorage.getItem('commercialSortBy') : sessionStorage.getItem('professionalsSortBy');
const sortBy = storedSortBy && storedSortBy !== 'null' ? (storedSortBy as SortByOptions) : null; const sortBy = storedSortBy && storedSortBy !== 'null' ? (storedSortBy as SortByOptions) : null;

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.01.2026 12:21 | TX: 01/04/2026 5:21 AM" timestamp: "GER: 06.01.2026 22:33 | TX: 01/06/2026 3:33 PM"
}; };
export default build; export default build;

View File

@ -1,10 +1,20 @@
// IMPORTANT: DOM polyfill must be imported FIRST, before any browser-dependent libraries // IMPORTANT: DOM polyfill must be imported FIRST, before any browser-dependent libraries
import './ssr-dom-polyfill'; import './ssr-dom-polyfill';
import { bootstrapApplication } from '@angular/platform-browser'; import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser';
import { AppComponent } from './app/app.component'; import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server'; import { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(AppComponent, config); const bootstrap = (context: BootstrapContext) => {
console.log('[SSR] Bootstrap function called');
const appRef = bootstrapApplication(AppComponent, config, context);
appRef.then(() => {
console.log('[SSR] Application bootstrapped successfully');
}).catch((err) => {
console.error('[SSR] Bootstrap error:', err);
console.error('[SSR] Error stack:', err.stack);
});
return appRef;
};
export default bootstrap; export default bootstrap;