npm run serve:ssr funktioniert und Hamburger Menu bug fix
This commit is contained in:
parent
43027a54f7
commit
4f8fd77f7d
|
|
@ -26,6 +26,12 @@
|
||||||
"ssr": {
|
"ssr": {
|
||||||
"entry": "server.ts"
|
"entry": "server.ts"
|
||||||
},
|
},
|
||||||
|
"allowedCommonJsDependencies": [
|
||||||
|
"quill-delta",
|
||||||
|
"leaflet",
|
||||||
|
"dayjs",
|
||||||
|
"qs"
|
||||||
|
],
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
"zone.js"
|
"zone.js"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||||
|
|
||||||
|
export const serverRoutes: ServerRoute[] = [
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
renderMode: RenderMode.Server
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 }}"
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue