diff --git a/frontend/angular.json b/frontend/angular.json index 12f699d..5e08111 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -30,7 +30,6 @@ } ], "styles": [ - "node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "src/styles.scss" ] }, diff --git a/frontend/src/app/app.html b/frontend/src/app/app.html index d73d60c..81168fa 100644 --- a/frontend/src/app/app.html +++ b/frontend/src/app/app.html @@ -5,6 +5,11 @@ + + + edit diff --git a/frontend/src/app/app.ts b/frontend/src/app/app.ts index 0f0b125..562ea99 100644 --- a/frontend/src/app/app.ts +++ b/frontend/src/app/app.ts @@ -1,8 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { RouterModule, RouterOutlet, Router, NavigationEnd } from '@angular/router'; import { MaterialModule } from './material.module'; +import { ThemeService } from './services/theme.service'; import { filter } from 'rxjs/operators'; import { CommonModule } from '@angular/common'; +import { Observable } from 'rxjs'; @Component({ selector: 'app-root', @@ -14,8 +16,14 @@ import { CommonModule } from '@angular/common'; export class AppComponent implements OnInit { title = 'frontend'; isAdminView = false; + darkMode$: Observable; - constructor(private router: Router) {} + constructor( + private router: Router, + private themeService: ThemeService + ) { + this.darkMode$ = this.themeService.darkMode$; + } ngOnInit() { this.router.events.pipe( @@ -24,4 +32,8 @@ export class AppComponent implements OnInit { this.isAdminView = event.urlAfterRedirects === '/admin'; }); } + + toggleTheme(): void { + this.themeService.toggleTheme(); + } } diff --git a/frontend/src/app/components/item-form/item-form.scss b/frontend/src/app/components/item-form/item-form.scss index 7179a9a..34f447c 100644 --- a/frontend/src/app/components/item-form/item-form.scss +++ b/frontend/src/app/components/item-form/item-form.scss @@ -8,6 +8,20 @@ mat-card { width: 100%; } +::ng-deep .mat-mdc-card-title { + display: block !important; + margin-bottom: 24px !important; + padding: 16px 0 !important; + border-bottom: 1px solid rgba(0, 0, 0, 0.12) !important; + font-size: 24px !important; + font-weight: 500 !important; + text-align: center !important; +} + +:host-context(.dark-theme) ::ng-deep .mat-mdc-card-title { + border-bottom-color: rgba(255, 255, 255, 0.12) !important; +} + mat-form-field { width: 100%; margin-bottom: 10px; @@ -20,6 +34,26 @@ mat-form-field { margin-top: 20px; } +:host-context(.dark-theme) .actions button[mat-stroked-button] { + border-color: rgba(255, 255, 255, 0.3) !important; + color: #ffffff !important; + + &:hover { + background-color: rgba(255, 255, 255, 0.08) !important; + border-color: rgba(255, 255, 255, 0.5) !important; + } +} + +::ng-deep .dark-theme .mat-mdc-stroked-button { + border-color: rgba(255, 255, 255, 0.3) !important; + color: #ffffff !important; + + &:hover { + background-color: rgba(255, 255, 255, 0.08) !important; + border-color: rgba(255, 255, 255, 0.5) !important; + } +} + .icon-mode-toggle { margin: 16px 0; } diff --git a/frontend/src/app/components/item-list/item-list.scss b/frontend/src/app/components/item-list/item-list.scss index 0b5d146..9c1e73b 100644 --- a/frontend/src/app/components/item-list/item-list.scss +++ b/frontend/src/app/components/item-list/item-list.scss @@ -2,6 +2,22 @@ padding: 20px; } +mat-card { + ::ng-deep .mat-mdc-card-title { + display: block !important; + margin-bottom: 24px !important; + padding: 16px 0 !important; + border-bottom: 1px solid rgba(0, 0, 0, 0.12) !important; + font-size: 24px !important; + font-weight: 500 !important; + text-align: center !important; + } +} + +:host-context(.dark-theme) mat-card ::ng-deep .mat-mdc-card-title { + border-bottom-color: rgba(255, 255, 255, 0.12) !important; +} + .fab-button { position: fixed; bottom: 20px; diff --git a/frontend/src/app/services/theme.service.ts b/frontend/src/app/services/theme.service.ts new file mode 100644 index 0000000..9d70744 --- /dev/null +++ b/frontend/src/app/services/theme.service.ts @@ -0,0 +1,93 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class ThemeService { + private readonly THEME_COOKIE = 'theme-preference'; + private darkModeSubject = new BehaviorSubject(this.getInitialTheme()); + + darkMode$ = this.darkModeSubject.asObservable(); + + constructor() { + // Theme beim Start anwenden + this.applyTheme(this.darkModeSubject.value); + } + + /** + * Ermittelt das initiale Theme: + * 1. Cookie-Präferenz (falls vorhanden) + * 2. Browser-System-Einstellung (prefers-color-scheme) + */ + private getInitialTheme(): boolean { + const cookieTheme = this.getCookie(this.THEME_COOKIE); + + if (cookieTheme !== null) { + return cookieTheme === 'dark'; + } + + // Fallback auf System-Präferenz + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + } + + /** + * Schaltet zwischen Light und Dark Mode um + */ + toggleTheme(): void { + const newTheme = !this.darkModeSubject.value; + this.setTheme(newTheme); + } + + /** + * Setzt ein spezifisches Theme + */ + setTheme(isDark: boolean): void { + this.darkModeSubject.next(isDark); + this.applyTheme(isDark); + this.setCookie(this.THEME_COOKIE, isDark ? 'dark' : 'light', 365); + } + + /** + * Gibt das aktuelle Theme zurück + */ + isDarkMode(): boolean { + return this.darkModeSubject.value; + } + + /** + * Wendet das Theme auf das Document an + */ + private applyTheme(isDark: boolean): void { + if (isDark) { + document.body.classList.add('dark-theme'); + } else { + document.body.classList.remove('dark-theme'); + } + } + + /** + * Setzt ein Cookie + */ + private setCookie(name: string, value: string, days: number): void { + const expires = new Date(); + expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000); + document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`; + } + + /** + * Liest ein Cookie aus + */ + private getCookie(name: string): string | null { + const nameEQ = name + '='; + const ca = document.cookie.split(';'); + + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + + return null; + } +} diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 90d4ee0..897a7ed 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1 +1,231 @@ /* You can add global styles to this file, and also import other style files */ + +/* Import Material Design Prebuilt Themes */ +@import '@angular/material/prebuilt-themes/indigo-pink.css'; + +/* Light Theme (Default) */ +:root { + --background-color: #fafafa; + --card-background: #ffffff; + --text-color: #000000; + --text-secondary: #666666; + --border-color: #e0e0e0; +} + +/* Dark Theme */ +body.dark-theme { + --background-color: #121212; + --card-background: #1e1e1e; + --text-color: #ffffff; + --text-secondary: #b0b0b0; + --border-color: #333333; + + /* Material Components Dark Theme Override */ + background-color: var(--background-color); + color: var(--text-color); + + /* Material Card Styling */ + .mat-mdc-card { + background-color: var(--card-background) !important; + color: var(--text-color) !important; + } + + /* Material Form Fields */ + .mat-mdc-form-field { + color: var(--text-color); + + .mat-mdc-form-field-focus-overlay { + background-color: rgba(255, 255, 255, 0.05); + } + } + + .mat-mdc-text-field-wrapper { + background-color: #2a2a2a !important; + } + + .mat-mdc-form-field-flex { + background-color: #2a2a2a !important; + } + + .mdc-text-field--filled { + background-color: #2a2a2a !important; + } + + .mat-mdc-input-element { + color: var(--text-color) !important; + caret-color: var(--text-color); + } + + .mat-mdc-form-field-label { + color: var(--text-secondary) !important; + } + + .mat-mdc-floating-label { + color: var(--text-secondary) !important; + } + + .mdc-floating-label { + color: var(--text-secondary) !important; + } + + /* Material Slide Toggle */ + .mat-mdc-slide-toggle { + color: var(--text-color); + + .mdc-switch__track { + background-color: #424242 !important; + border-color: #424242 !important; + } + + .mdc-switch--selected .mdc-switch__track { + background-color: #3f51b5 !important; + border-color: #3f51b5 !important; + } + } + + /* Material Autocomplete */ + .mat-mdc-autocomplete-panel { + background-color: #2a2a2a !important; + color: var(--text-color); + } + + .mat-mdc-option { + color: var(--text-color) !important; + + &:hover { + background-color: rgba(255, 255, 255, 0.08) !important; + } + + &.mat-mdc-option-active { + background-color: rgba(255, 255, 255, 0.08) !important; + } + } + + /* Material Table */ + .mat-mdc-table { + background-color: var(--card-background); + color: var(--text-color); + } + + .mat-mdc-header-cell { + color: var(--text-color); + } + + .mat-mdc-cell { + color: var(--text-color); + } + + /* Material Buttons */ + .mat-mdc-raised-button, .mat-mdc-stroked-button, .mat-mdc-button { + color: var(--text-color); + } + + .mat-mdc-stroked-button { + border-color: var(--border-color); + } + + /* Material Icon Colors */ + .mat-icon { + color: var(--text-color); + } + + /* Card Titles and Content */ + .mat-mdc-card-title { + color: var(--text-color) !important; + } + + .mat-mdc-card-content { + color: var(--text-color) !important; + } + + mat-card-title { + color: var(--text-color) !important; + border-bottom-color: rgba(255, 255, 255, 0.12) !important; + } + + mat-card-content { + color: var(--text-color) !important; + } + + /* Dark theme border for card titles */ + mat-card mat-card-title { + border-bottom-color: rgba(255, 255, 255, 0.12) !important; + } + + /* Slide Toggle Label */ + .mat-mdc-slide-toggle-label { + color: var(--text-color) !important; + } + + .mdc-label { + color: var(--text-color) !important; + } + + /* Button Text */ + .mat-mdc-button, .mat-mdc-stroked-button, .mat-mdc-raised-button { + color: var(--text-color) !important; + + .mdc-button__label { + color: var(--text-color) !important; + } + } + + .mat-mdc-stroked-button { + border-color: rgba(255, 255, 255, 0.3) !important; + + &:hover { + background-color: rgba(255, 255, 255, 0.08) !important; + border-color: rgba(255, 255, 255, 0.5) !important; + } + } + + .mat-mdc-button.mat-primary { + color: #90caf9 !important; + } + + /* Links */ + a { + color: #90caf9; + + &:hover { + color: #64b5f6; + } + } + + /* Specific styling for icon picker */ + .icon-browse-link a { + color: #90caf9 !important; + + mat-icon { + color: #90caf9 !important; + } + } + + .icon-preview { + background-color: #2a2a2a !important; + color: var(--text-color) !important; + + .preview-label { + color: var(--text-secondary) !important; + } + } + + /* Icon category in autocomplete */ + .icon-category { + color: var(--text-secondary) !important; + } + + .icon-option-text { + color: var(--text-color) !important; + } +} + +/* Global Body Styling */ +body { + margin: 0; + padding: 0; + background-color: var(--background-color); + color: var(--text-color); + font-family: Roboto, "Helvetica Neue", sans-serif; + transition: background-color 0.3s ease, color 0.3s ease; +}