diff --git a/public/icons/filter.svg b/public/icons/filter.svg new file mode 100644 index 0000000..c3708ec --- /dev/null +++ b/public/icons/filter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/menu.svg b/public/icons/menu.svg new file mode 100644 index 0000000..a104aea --- /dev/null +++ b/public/icons/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/plus.svg b/public/icons/plus.svg new file mode 100644 index 0000000..7a1e404 --- /dev/null +++ b/public/icons/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/core/components/cookie-consent/cookie-consent.component.ts b/src/app/core/components/cookie-consent/cookie-consent.component.ts index f6ffca7..14e7a3b 100644 --- a/src/app/core/components/cookie-consent/cookie-consent.component.ts +++ b/src/app/core/components/cookie-consent/cookie-consent.component.ts @@ -1,53 +1,53 @@ -import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core'; -import { isPlatformBrowser, CommonModule } from '@angular/common'; // isPlatformBrowser importieren +// /src/app/core/components/cookie-consent/cookie-consent.component.ts + +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { StorageService } from '../../services/storage.service'; // <-- NEUER IMPORT + +// Ein Enum für die verschiedenen Zustände ist typsicherer als reine Strings +type ConsentStatus = 'accepted' | 'declined'; @Component({ selector: 'app-cookie-consent', + standalone: true, // <-- standalone: true hinzufügen, falls es fehlt imports: [CommonModule], templateUrl: './cookie-consent.component.html', styleUrl: './cookie-consent.component.css' }) export class CookieConsentComponent implements OnInit { + // --- Abhängigkeiten mit moderner inject()-Syntax --- + private storageService = inject(StorageService); isVisible = false; private readonly consentKey = 'cookie_consent_status'; - private isBrowser: boolean; - // Wir injizieren PLATFORM_ID, um die aktuelle Plattform (Browser/Server) zu ermitteln. - constructor(@Inject(PLATFORM_ID) private platformId: Object) { - // Speichere das Ergebnis in einer Eigenschaft, um es wiederverwenden zu können. - this.isBrowser = isPlatformBrowser(this.platformId); - } + // Der Konstruktor wird viel sauberer oder kann ganz entfallen + constructor() {} ngOnInit(): void { - // Wir führen die Prüfung nur aus, wenn der Code im Browser läuft. - if (this.isBrowser) { - this.checkConsent(); - } + // Der Service kümmert sich um die Browser-Prüfung. + // Wir können ihn einfach immer aufrufen. + this.checkConsent(); } private checkConsent(): void { - const consent = localStorage.getItem(this.consentKey); + const consent = this.storageService.getItem(this.consentKey); + // Das Banner wird nur angezeigt, wenn noch gar nichts gespeichert wurde (consent ist null) if (!consent) { this.isVisible = true; } } accept(): void { - // Wir stellen sicher, dass wir localStorage nur im Browser schreiben. - if (this.isBrowser) { - localStorage.setItem(this.consentKey, 'accepted'); - this.isVisible = false; - console.log('Cookies wurden akzeptiert.'); - } + // Der Service kümmert sich um die Browser-Prüfung und Serialisierung. + this.storageService.setItem(this.consentKey, 'accepted'); + this.isVisible = false; + console.log('Cookies wurden akzeptiert.'); } decline(): void { - // Wir stellen sicher, dass wir localStorage nur im Browser schreiben. - if (this.isBrowser) { - localStorage.setItem(this.consentKey, 'declined'); - this.isVisible = false; - console.log('Cookies wurden abgelehnt.'); - } + this.storageService.setItem(this.consentKey, 'declined'); + this.isVisible = false; + console.log('Cookies wurden abgelehnt.'); } } \ No newline at end of file diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index 17ce50e..d5faf32 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -1,17 +1,18 @@ // /src/app/core/services/auth.service.ts -import { Injectable, PLATFORM_ID, inject } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { isPlatformBrowser } from '@angular/common'; import { Router } from '@angular/router'; import { Observable, BehaviorSubject, of } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; import { jwtDecode } from 'jwt-decode'; +// Eigene Imports import { LoginRequest, AuthResponse, RegisterRequest } from '../models/auth.models'; import { API_URL } from '../tokens/api-url.token'; +import { StorageService } from './storage.service'; -// Ein Hilfs-Interface, um die Struktur des dekodierten Tokens typsicher zu machen. +// Hilfs-Interface für die dekodierten Token-Daten. interface DecodedToken { exp: number; // Expiration time als UNIX-Timestamp in Sekunden // Hier könnten weitere Claims wie 'email', 'sub', 'role' etc. stehen @@ -21,24 +22,22 @@ interface DecodedToken { providedIn: 'root' }) export class AuthService { - // --- Injizierte Abhängigkeiten mit moderner Syntax --- + // --- Injizierte Abhängigkeiten --- private http = inject(HttpClient); private router = inject(Router); - private platformId = inject(PLATFORM_ID); + private storageService = inject(StorageService); // <-- NEU private apiUrl = inject(API_URL); private readonly endpoint = '/Auth'; private readonly TOKEN_KEY = 'auth-token'; private readonly ROLES_KEY = 'auth-user-roles'; - private loggedInStatus = new BehaviorSubject(this.isBrowser() && !!this.getToken()); + private loggedInStatus = new BehaviorSubject(!!this.getToken()); public isLoggedIn$ = this.loggedInStatus.asObservable(); private tokenExpirationTimer: any; constructor() { - // Beim Initialisieren des Services prüfen, ob bereits ein Token vorhanden ist - // und ggf. den Logout-Timer dafür starten (z.B. nach einem Neuladen der Seite). this.initTokenCheck(); } @@ -47,7 +46,7 @@ export class AuthService { tap(response => { if (response?.isAuthSuccessful) { this.setSession(response); - this.startTokenExpirationTimer(); // Timer nach erfolgreichem Login starten + this.startTokenExpirationTimer(); } }), catchError(() => { @@ -62,7 +61,7 @@ export class AuthService { tap(response => { if (response?.isAuthSuccessful) { this.setSession(response); - this.startTokenExpirationTimer(); // Timer nach erfolgreichem Login starten + this.startTokenExpirationTimer(); } }), catchError(() => { @@ -80,7 +79,6 @@ export class AuthService { logout(): void { this.clearSession(); - // Den proaktiven Timer stoppen, da der Logout manuell erfolgt. if (this.tokenExpirationTimer) { clearTimeout(this.tokenExpirationTimer); } @@ -88,13 +86,11 @@ export class AuthService { } getToken(): string | null { - return this.isBrowser() ? localStorage.getItem(this.TOKEN_KEY) : null; + return this.storageService.getItem(this.TOKEN_KEY); } getUserRoles(): string[] { - if (!this.isBrowser()) return []; - const roles = localStorage.getItem(this.ROLES_KEY); - return roles ? JSON.parse(roles) : []; + return this.storageService.getItem(this.ROLES_KEY) || []; } hasRole(requiredRole: string): boolean { @@ -102,25 +98,19 @@ export class AuthService { } private setSession(authResponse: AuthResponse): void { - if (this.isBrowser() && authResponse?.token && authResponse?.roles) { - localStorage.setItem(this.TOKEN_KEY, authResponse.token); - localStorage.setItem(this.ROLES_KEY, JSON.stringify(authResponse.roles)); + if (authResponse?.token && authResponse?.roles) { + this.storageService.setItem(this.TOKEN_KEY, authResponse.token); + this.storageService.setItem(this.ROLES_KEY, authResponse.roles); this.loggedInStatus.next(true); } } private clearSession(): void { - if (this.isBrowser()) { - localStorage.removeItem(this.TOKEN_KEY); - localStorage.removeItem(this.ROLES_KEY); - } + this.storageService.removeItem(this.TOKEN_KEY); + this.storageService.removeItem(this.ROLES_KEY); this.loggedInStatus.next(false); } - private isBrowser(): boolean { - return isPlatformBrowser(this.platformId); - } - private startTokenExpirationTimer(): void { if (this.tokenExpirationTimer) { clearTimeout(this.tokenExpirationTimer); @@ -133,18 +123,21 @@ export class AuthService { try { const decodedToken = jwtDecode(token); - const expirationDate = new Date(decodedToken.exp * 1000); // JWT 'exp' ist in Sekunden, Date() braucht Millisekunden + const expirationDate = new Date(decodedToken.exp * 1000); const timeoutDuration = expirationDate.getTime() - new Date().getTime(); - if (timeoutDuration > 0) { + // Puffer, um Clock-Skew-Probleme zu vermeiden + const clockSkewBuffer = 5000; // 5 Sekunden + + if (timeoutDuration > clockSkewBuffer) { this.tokenExpirationTimer = setTimeout(() => { console.warn('Sitzung proaktiv beendet, da das Token abgelaufen ist.'); this.logout(); - // Hier könnte man eine Snackbar-Nachricht anzeigen }, timeoutDuration); } else { - // Das gespeicherte Token ist bereits abgelaufen - this.clearSession(); + if (this.getToken()) { + this.logout(); + } } } catch (error) { console.error('Fehler beim Dekodieren des Tokens. Session wird bereinigt.', error); @@ -153,8 +146,7 @@ export class AuthService { } private initTokenCheck(): void { - if (this.isBrowser()) { - this.startTokenExpirationTimer(); - } + // Der StorageService ist bereits SSR-sicher, wir brauchen hier keine extra Prüfung. + this.startTokenExpirationTimer(); } } \ No newline at end of file diff --git a/src/app/core/services/storage.service.ts b/src/app/core/services/storage.service.ts new file mode 100644 index 0000000..1206a82 --- /dev/null +++ b/src/app/core/services/storage.service.ts @@ -0,0 +1,87 @@ +// /src/app/core/services/storage.service.ts + +import { Injectable, PLATFORM_ID, inject } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; + +// Definiert die verfügbaren Speichertypen +export type StorageType = 'localStorage' | 'sessionStorage'; + +@Injectable({ + providedIn: 'root' +}) +export class StorageService { + private platformId = inject(PLATFORM_ID); + + /** + * Speichert einen Wert im angegebenen Web Storage. + * Serialisiert Objekte automatisch zu JSON. + * @param key Der Schlüssel, unter dem der Wert gespeichert wird. + * @param value Der zu speichernde Wert. + * @param storageType Der zu verwendende Speicher ('localStorage' oder 'sessionStorage'). Standard ist 'localStorage'. + */ + setItem(key: string, value: T, storageType: StorageType = 'localStorage'): void { + if (isPlatformBrowser(this.platformId)) { + try { + const storage = this.getStorage(storageType); + const serializedValue = JSON.stringify(value); + storage.setItem(key, serializedValue); + } catch (e) { + console.error(`Error saving to ${storageType} with key "${key}"`, e); + } + } + } + + /** + * Ruft einen Wert aus dem angegebenen Web Storage ab. + * Deserialisiert JSON-Strings automatisch in Objekte. + * @param key Der Schlüssel des abzurufenden Wertes. + * @param storageType Der zu verwendende Speicher ('localStorage' oder 'sessionStorage'). Standard ist 'localStorage'. + * @returns Der abgerufene Wert (typsicher) oder null, wenn der Schlüssel nicht existiert oder ein Fehler auftritt. + */ + getItem(key: string, storageType: StorageType = 'localStorage'): T | null { + if (isPlatformBrowser(this.platformId)) { + try { + const storage = this.getStorage(storageType); + const serializedValue = storage.getItem(key); + if (serializedValue === null) { + return null; + } + return JSON.parse(serializedValue) as T; + } catch (e) { + console.error(`Error reading from ${storageType} with key "${key}"`, e); + return null; + } + } + return null; + } + + /** + * Entfernt einen Wert aus dem angegebenen Web Storage. + * @param key Der Schlüssel des zu entfernenden Wertes. + * @param storageType Der zu verwendende Speicher ('localStorage' oder 'sessionStorage'). Standard ist 'localStorage'. + */ + removeItem(key: string, storageType: StorageType = 'localStorage'): void { + if (isPlatformBrowser(this.platformId)) { + const storage = this.getStorage(storageType); + storage.removeItem(key); + } + } + + /** + * Löscht alle Einträge im angegebenen Web Storage. + * @param storageType Der zu leerende Speicher ('localStorage' oder 'sessionStorage'). Standard ist 'localStorage'. + */ + clear(storageType: StorageType = 'localStorage'): void { + if (isPlatformBrowser(this.platformId)) { + const storage = this.getStorage(storageType); + storage.clear(); + } + } + + /** + * Private Hilfsfunktion, um das korrekte Storage-Objekt zurückzugeben. + */ + private getStorage(storageType: StorageType): Storage { + return storageType === 'localStorage' ? window.localStorage : window.sessionStorage; + } +} \ No newline at end of file diff --git a/src/app/features/components/dashboard/dashboard-page/dashboard-page.component.ts b/src/app/features/components/dashboard/dashboard-page/dashboard-page.component.ts index cdfc937..4b681c5 100644 --- a/src/app/features/components/dashboard/dashboard-page/dashboard-page.component.ts +++ b/src/app/features/components/dashboard/dashboard-page/dashboard-page.component.ts @@ -66,6 +66,30 @@ export class DashboardPageComponent { amount: '€87.00', status: 'info', // NEU: Sprechender Status }, + { + id: 'a2d4b', + user: { name: 'Max Mustermann', email: 'max@test.de', avatarUrl: 'https://i.pravatar.cc/150?u=max' }, + amount: '€129.99', + status: 'completed', // NEU: Sprechender Status + }, + { + id: 'f8e9c', + user: { name: 'Erika Musterfrau', email: 'erika@test.de', avatarUrl: 'https://i.pravatar.cc/150?u=erika' }, + amount: '€49.50', + status: 'processing', // NEU: Sprechender Status + }, + { + id: 'h1g3j', + user: { name: 'John Doe', email: 'john.d@test.com', avatarUrl: 'https://i.pravatar.cc/150?u=john' }, + amount: '€87.00', + status: 'cancelled', // NEU: Sprechender Status + }, + { + id: 'h1g3j', + user: { name: 'John Doe', email: 'john.d@test.com', avatarUrl: 'https://i.pravatar.cc/150?u=john' }, + amount: '€87.00', + status: 'info', // NEU: Sprechender Status + }, ]; handleDeleteOrder(orderId: string): void { diff --git a/src/app/shared/components/layout/sidebar/sidebar.component.ts b/src/app/shared/components/layout/sidebar/sidebar.component.ts index e83d45d..29bae14 100644 --- a/src/app/shared/components/layout/sidebar/sidebar.component.ts +++ b/src/app/shared/components/layout/sidebar/sidebar.component.ts @@ -1,76 +1,51 @@ -import { Component, Inject, PLATFORM_ID, OnInit } from '@angular/core'; -import { CommonModule, isPlatformBrowser } from '@angular/common'; +// /src/app/core/components/default-layout/sidebar/sidebar.component.ts + +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink, RouterLinkActive } from '@angular/router'; // RouterLink und RouterLinkActive importieren import { IconComponent } from '../../ui/icon/icon.component'; +import { StorageService } from '../../../../core/services/storage.service'; import { Router } from '@angular/router'; @Component({ selector: 'app-sidebar', standalone: true, - imports: [CommonModule, IconComponent], + imports: [CommonModule, IconComponent], // RouterLink und RouterLinkActive hier hinzufügen templateUrl: './sidebar.component.html', styleUrl: './sidebar.component.css', }) export class SidebarComponent implements OnInit { - // 1. OnInit implementieren - // Key für localStorage, genau wie beim Dark Mode - private readonly sidebarCollapsedKey = 'app-sidebar-collapsed-setting'; + // --- Abhängigkeiten mit moderner inject()-Syntax --- + private storageService = inject(StorageService); - // Dummy-Eigenschaft für die aktive Route - activeRoute = 'dashboard'; - - // Der Standardwert ist 'false' (aufgeklappt), wird aber sofort überschrieben + private readonly sidebarCollapsedKey = 'app-sidebar-collapsed'; public isCollapsed = false; - // 2. PLATFORM_ID injizieren, um localStorage sicher zu verwenden - constructor( - @Inject(PLATFORM_ID) private platformId: Object, - private router: Router - ) {} + activeRoute = 'dashboard'; + constructor(private router: Router) {} - // 3. Beim Start der Komponente den gespeicherten Zustand laden ngOnInit(): void { this.loadCollapsedState(); } - // Dummy-Methode setActive(route: string): void { this.activeRoute = route; this.router.navigateByUrl('/shop/' + route); - } - // 4. Die Umschalt-Methode aktualisieren, damit sie den neuen Zustand speichert toggleSidebar(): void { - // Zuerst den Zustand ändern this.isCollapsed = !this.isCollapsed; - // Dann den neuen Zustand speichern this.saveCollapsedState(); } - // 5. Methode zum Laden des Zustands (kopiert vom Dark-Mode-Muster) private loadCollapsedState(): void { - if (isPlatformBrowser(this.platformId)) { - try { - const storedValue = localStorage.getItem(this.sidebarCollapsedKey); - // Setze den Zustand der Komponente basierend auf dem gespeicherten Wert - this.isCollapsed = storedValue === 'true'; - } catch (e) { - console.error('Could not access localStorage for sidebar state:', e); - } - } + // Der Service kümmert sich um die Browser-Prüfung und gibt boolean oder null zurück + this.isCollapsed = + this.storageService.getItem(this.sidebarCollapsedKey) ?? false; } - // 6. Methode zum Speichern des Zustands (kopiert vom Dark-Mode-Muster) private saveCollapsedState(): void { - if (isPlatformBrowser(this.platformId)) { - try { - localStorage.setItem( - this.sidebarCollapsedKey, - String(this.isCollapsed) - ); - } catch (e) { - console.error('Could not write to localStorage for sidebar state:', e); - } - } + // Der Service kümmert sich um die Serialisierung des booleans + this.storageService.setItem(this.sidebarCollapsedKey, this.isCollapsed); } }