Started with about page and my cv

This commit is contained in:
2025-11-09 12:21:34 +01:00
parent b80d70d4df
commit 5969e66872
36 changed files with 637 additions and 169 deletions

View File

@@ -6,7 +6,7 @@ import {provideAnimations} from '@angular/platform-browser/animations';
import {provideHttpClient} from '@angular/common/http';
import {provideTranslateService} from '@ngx-translate/core';
import {provideTranslateHttpLoader} from '@ngx-translate/http-loader';
import {Constants} from './constants/Constants';
import {LocalStoreConstants} from './constants/LocalStoreConstants';
const INITIAL_LANG = getInitialLang();
@@ -30,7 +30,7 @@ export const appConfig: ApplicationConfig = {
};
function getInitialLang(): string {
const saved = localStorage.getItem(Constants.LANGUAGE_KEY);
const saved = localStorage.getItem(LocalStoreConstants.LANGUAGE_KEY);
if (saved) return saved;
const nav = typeof navigator !== 'undefined' ? navigator.language?.toLowerCase() : 'en';
return nav?.startsWith('de') ? 'de' : 'en';

View File

@@ -1,36 +0,0 @@
<mat-toolbar color="primary">
<span>{{ 'APP.TITLE' | translate }}</span>
<span class="spacer"></span>
<!-- Language -->
<mat-form-field appearance="outline" style="width: 170px; margin-right: 8px;">
<mat-select [value]="lang.lang()" (selectionChange)="lang.use($event.value)">
<mat-select-trigger>
<img class="flag-icon" [src]="lang.lang() === 'de' ? '/assets/flags/de.svg' : '/assets/flags/gb.svg'"
alt="" aria-hidden="true">
<span style="margin-left: 8px;">
{{ lang.lang() === 'de' ? ('LANG.DE' | translate) : ('LANG.EN' | translate) }}
</span>
</mat-select-trigger>
<mat-option value="de">
<img class="flag-icon" src="/assets/flags/de.svg" alt="" aria-hidden="true">
<span> {{ 'LANG.DE' | translate }} </span>
</mat-option>
<mat-option value="en">
<img class="flag-icon" src="/assets/flags/gb.svg" alt="" aria-hidden="true">
<span> {{ 'LANG.EN' | translate }} </span>
</mat-option>
</mat-select>
</mat-form-field>
<!-- Theme -->
<button mat-icon-button (click)="theme.toggle()">
<mat-icon>{{ themeIcon() }}</mat-icon>
</button>
</mat-toolbar>
<main class="container app-surface">
<router-outlet />
</main>

View File

@@ -1,6 +1,6 @@
import { Routes } from '@angular/router';
import {WelcomeComponent} from './features/welcome/welcome';
import {AboutComponent} from './pages/about/about.component';
export const routes: Routes = [
{ path: '', component: WelcomeComponent },
{ path: '', component: AboutComponent },
];

View File

@@ -1,13 +0,0 @@
.spacer { flex: 1 1 auto; }
.container { max-width: 960px; margin: 24px auto; padding: 0 16px; }
mat-form-field { --mdc-outlined-text-field-container-shape: 20px; }
.flag-icon {
width: 20px;
height: 14px;
object-fit: cover;
border-radius: 2px;
box-shadow: 0 0 0 1px rgba(0,0,0,.08) inset;
vertical-align: -2px;
}
mat-option .flag-icon { margin-right: 8px; }

View File

@@ -1,32 +0,0 @@
import {Component, computed, effect, inject} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { FormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { ThemeService } from './service/theme.service';
import {LanguageService} from './service/language.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [
RouterOutlet,
MatToolbarModule, MatIconModule, MatButtonModule,
MatFormFieldModule, MatSelectModule,
FormsModule,
TranslateModule
],
templateUrl: './app.html',
styleUrl: './app.scss',
})
export class App {
readonly theme = inject(ThemeService);
readonly lang = inject(LanguageService);
readonly themeIcon = computed(() => this.theme.theme() === 'dark' ? 'light_mode' : 'dark_mode');
}

View File

@@ -0,0 +1,7 @@
export class AssetsConstants {
static readonly ME = '/assets/me.webp';
static readonly LOGO = '/assets/favicon.ico';
static readonly FLAG_DE = '/assets/flags/de.svg';
static readonly FLAG_EN = '/assets/flags/gb.svg';
}

View File

@@ -1,4 +1,4 @@
export class Constants{
export class LocalStoreConstants {
static readonly THEME_KEY = 'theme';
static readonly LANGUAGE_KEY = 'lang';

View File

@@ -0,0 +1,4 @@
export class UrlConstants {
static readonly LINKED_IN = 'https://www.linkedin.com/in/andreas-dahm-2395991ba';
static readonly GIT_HUB = 'https://github.com/LoboTheDark';
}

View File

@@ -1,28 +0,0 @@
<mat-card>
<mat-card-header>
<mat-card-title>{{ `WELCOME.TITLE` | translate }} </mat-card-title>
<mat-card-subtitle>{{ `WELCOME.SUB` | translate }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>{{ `WELCOME.TEXT` | translate }}</p>
<p style="margin-top: 8px;">
{{ `WELCOME.COUNTER` | translate }}: <strong data-testid="counter">{{ count() }}</strong>
</p>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary"
(click)="inc()"
data-testid="inc"
>
<mat-icon>add</mat-icon> {{ `WELCOME.INC` | translate }}
</button>
<button mat-stroked-button
(click)="reset()"
data-testid="reset"
>
<mat-icon>restart_alt</mat-icon> {{ `WELCOME.RESET` | translate }}
</button>
</mat-card-actions>
</mat-card>

View File

@@ -1,5 +0,0 @@
mat-card {
margin-top: 2rem;
display: block;
background-color: var(--app-card-background);
}

View File

@@ -1,18 +0,0 @@
import { Component, signal } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import {TranslatePipe} from '@ngx-translate/core';
@Component({
selector: 'app-welcome',
standalone: true,
imports: [MatCardModule, MatButtonModule, MatIconModule, TranslatePipe],
templateUrl: './welcome.html',
styleUrl: './welcome.scss',
})
export class WelcomeComponent {
readonly count = signal(0);
inc() { this.count.update(v => v + 1); }
reset() { this.count.set(0); }
}

View File

@@ -0,0 +1,9 @@
<app-topbar />
<main class="container app-surface">
<router-outlet />
</main>
<footer class="foot">
<small>© {{ currentYear }} Andreas Dahm - {{ `APP.COPYRIGHT` | translate }}</small>
</footer>

View File

@@ -0,0 +1,10 @@
.container { max-width: 1100px; margin: 0 auto; padding: 1rem; }
.app-surface {
background: var(--app-bg);
color: var(--app-fg);
transition: background-color 220ms ease, color 220ms ease;
}
.foot {
border-top: 1px solid rgba(0,0,0,.08);
padding: 1rem; text-align: center; opacity: .8;
}

View File

@@ -0,0 +1,16 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {TopbarComponent} from '../topbar/topbar.component';
import {TranslatePipe} from '@ngx-translate/core';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, TopbarComponent, TranslatePipe],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
currentYear = new Date().getFullYear();
}

View File

@@ -0,0 +1,58 @@
<mat-toolbar class="topbar" color="primary" (keydown)="onKeydown($event)">
<a class="brand" routerLink="/">
<img class="logo-dot"
src="{{AssetsConstants.LOGO}}"
alt="" aria-hidden="true"
draggable="false"
oncontextmenu="return false;"
>
<span class="brand-text">{{ 'APP.TITLE' | translate }}</span>
</a>
<nav class="nav">
<a routerLink="/about" mat-button>{{ 'TOPBAR.ABOUT' | translate }}</a>
<a routerLink="/projects" mat-button>{{ 'TOPBAR.PROJECTS' | translate }}</a>
<a routerLink="/hobbys" mat-button>{{ 'TOPBAR.HOBBY' | translate }}</a>
<a routerLink="/contact" mat-button>{{ 'TOPBAR.CONTACT' | translate }}</a>
</nav>
<span class="spacer"></span>
<!-- Settings: Sprache + Theme -->
<button mat-icon-button [matMenuTriggerFor]="settingsMenu" aria-label="Open settings" matTooltip="{{ 'TOPBAR.SETTINGS' | translate }}">
<mat-icon>tune</mat-icon>
</button>
<mat-menu #settingsMenu="matMenu" xPosition="before">
<div class="menu-section">
<div class="menu-title">{{ 'TOPBAR.LANGUAGE' | translate }}</div>
<button mat-menu-item (click)="setLang('de')">
<img class="flag-icon" src="{{AssetsConstants.FLAG_DE}}" alt="" aria-hidden="true">
<span>{{ 'LANG.DE' | translate }}</span>
@if (lang.lang() === 'de')
{
<mat-icon >check</mat-icon>
}
</button>
<button mat-menu-item (click)="setLang('en')">
<img class="flag-icon" src="{{AssetsConstants.FLAG_EN}}" alt="" aria-hidden="true">
<span>{{ 'LANG.EN' | translate }}</span>
@if (lang.lang() === 'en')
{
<mat-icon>check</mat-icon>
}
</button>
</div>
<mat-divider></mat-divider>
<div class="menu-section">
<div class="menu-title">{{ 'TOPBAR.APPEARANCE' | translate }}</div>
<button mat-menu-item (click)="theme.toggle()">
<mat-icon>{{ themeIcon() }}</mat-icon>
<span>
{{ theme.theme() === 'dark' ? ('THEME.LIGHT' | translate) : ('THEME.DARK' | translate) }}
</span>
<span class="kbd">Ctrl/⌘ + J</span>
</button>
</div>
</mat-menu>
</mat-toolbar>

View File

@@ -0,0 +1,73 @@
.topbar {
position: sticky; top: 0; z-index: 100;
backdrop-filter: saturate(1.1) blur(8px);
background:
color-mix(in oklab, var(--app-bg) 80%, transparent);
border-bottom: 1px solid rgba(0,0,0,.08);
.brand {
display:flex; align-items:center; gap:.6rem;
color: inherit; text-decoration: none;
.logo-dot {
width: 48px; height: 48px; border-radius: 50%;
}
.brand-text { font-weight: 600; letter-spacing:.2px; }
}
.nav { display:flex; gap:.25rem; margin-left:.5rem; }
.spacer { flex: 1; }
.flag-icon { width: 18px; height: 18px; border-radius: 2px; margin-right:.5rem; }
.menu-section { padding:.25rem .5rem .5rem; }
.menu-title { font-size:.75rem; opacity:.75; padding:.25rem .75rem .5rem; }
.kbd {
margin-left:auto; font-size:.7rem; opacity:.65; border:1px solid currentColor;
border-radius:4px; padding:0 .35rem;
}
}
::ng-deep .mat-mdc-menu-item .mdc-list-item__primary-text {
display: flex;
align-items: center;
gap: .5rem;
}
::ng-deep .mat-mdc-menu-item .kbd {
margin-left: auto;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-size: 11px;
line-height: 1.6;
padding: 0 .35rem;
border: 0px solid currentColor;
border-radius: 4px;
opacity: .65;
}
::ng-deep .mat-mdc-menu-item .mat-icon {
width: 20px; height: 20px; font-size: 20px;
}
::ng-deep .mat-mdc-menu-item .flag-icon {
width: 20px !important;
height: 14px !important;
object-fit: cover;
border-radius: 2px;
margin-right: .5rem;
vertical-align: middle;
}
::ng-deep .mat-mdc-menu-panel {
border-radius: 10px !important;
border: 1px solid rgba(0,0,0,.14);
}
.dark ::ng-deep .mat-mdc-menu-panel {
border-color: rgba(255,255,255,.06);
}
/* Responsive: Collapse navigation to icon if width is smaller than 760px */
@media (max-width: 760px) {
.topbar .nav { display:none; }
}

View File

@@ -0,0 +1,44 @@
import { Component, computed, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { ThemeService } from '../../service/theme.service';
import { LanguageService } from '../../service/language.service';
import { MatDivider } from '@angular/material/divider';
import {AssetsConstants} from '../../constants/AssetsConstants';
@Component({
selector: 'app-topbar',
standalone: true,
imports: [
RouterLink,
MatToolbarModule, MatIconModule, MatButtonModule, MatMenuModule, MatTooltipModule,
TranslateModule, MatDivider
],
templateUrl: './topbar.component.html',
styleUrl: './topbar.component.scss'
})
export class TopbarComponent {
readonly theme = inject(ThemeService);
readonly lang = inject(LanguageService);
readonly themeIcon = computed(() =>
this.theme.theme() === 'dark' ? 'light_mode' : 'dark_mode'
);
onKeydown(e: KeyboardEvent) {
const metaOrCtrl = e.metaKey || e.ctrlKey;
if (metaOrCtrl && (e.key.toLowerCase() === 'j')) {
e.preventDefault();
this.theme.toggle();
}
}
setLang(code: 'de' | 'en') { this.lang.use(code); }
protected readonly AssetsConstants = AssetsConstants;
}

View File

@@ -0,0 +1,110 @@
<section class="about">
<mat-card class="hero">
<div class="photo">
<img
[ngSrc]="AssetsConstants.ME"
width="320" height="400"
alt="{{ 'ABOUT.ALT.PROFILE' | translate }}"
draggable="false"
oncontextmenu="return false;"
priority />
</div>
<div class="intro">
<h1>{{ 'ABOUT.HELLO' | translate }}</h1>
<p class="lead">
{{ 'ABOUT.LEAD' | translate }}
</p>
<div class="meta">
<div class="row">
<mat-icon aria-hidden="true">work</mat-icon>
<span>{{ 'ABOUT.ROLE' | translate }}</span>
</div>
<div class="row">
<mat-icon aria-hidden="true">location_on</mat-icon>
<span>{{ 'ABOUT.LOCATION' | translate }}</span>
</div>
<div class="row">
<mat-icon aria-hidden="true">email</mat-icon>
<a href="" (click)="openMail($event)">
{{ 'ABOUT.CONTACT_ME' | translate }}
</a>
</div>
<div class="row">
<mat-icon aria-hidden="true">link</mat-icon>
<a href="{{UrlConstants.GIT_HUB}}" target="_blank" rel="noopener">GitHub</a>
<span>·</span>
<a href="{{UrlConstants.LINKED_IN}}" target="_blank" rel="noopener">LinkedIn</a>
</div>
</div>
<div class="actions">
<a mat-flat-button color="primary" [href]="cvHref" target="_blank" rel="noopener">
<mat-icon>picture_as_pdf</mat-icon>
{{ 'ABOUT.DOWNLOAD_CV' | translate }}
</a>
<a mat-stroked-button routerLink="/projects">
<mat-icon>work_outline</mat-icon>
{{ 'ABOUT.VIEW_PROJECTS' | translate }}
</a>
</div>
</div>
</mat-card>
<mat-card class="skills">
<h2>{{ 'ABOUT.SECTION.SKILLS' | translate }}</h2>
<div class="chip-groups">
<div>
<h3>{{ 'ABOUT.SECTION.PRIMARY' | translate }}</h3>
<mat-chip-set aria-label="Primary skills">
@for (s of primarySkills; track primarySkills) {
<mat-chip >{{ s | translate }}</mat-chip>
}
</mat-chip-set>
</div>
<div>
<h3>{{ 'ABOUT.SECTION.TOOLSET' | translate }}</h3>
<mat-chip-set aria-label="Toolset">
@for (t of toolset; track toolset) {
<mat-chip>{{ t | translate }}</mat-chip>
}
</mat-chip-set>
</div>
</div>
</mat-card>
<mat-card class="experience">
<h2>{{ 'ABOUT.SECTION.EXPERIENCE' | translate }}</h2>
<div class="xp-list">
<div class="xp-item">
<div class="xp-head">
<strong>{{ 'ABOUT.XP.T1.ROLE' | translate }}</strong>
<span class="time">{{ 'ABOUT.XP.T1.TIME' | translate }}</span>
</div>
<div class="xp-sub">{{ 'ABOUT.XP.T1.COMPANY' | translate }}</div>
<ul>
<li>{{ 'ABOUT.XP.T1.P1' | translate }}</li>
<li>{{ 'ABOUT.XP.T1.P2' | translate }}</li>
<li>{{ 'ABOUT.XP.T1.P3' | translate }}</li>
</ul>
</div>
<mat-divider></mat-divider>
<div class="xp-item">
<div class="xp-head">
<strong>{{ 'ABOUT.XP.T2.ROLE' | translate }}</strong>
<span class="time">{{ 'ABOUT.XP.T2.TIME' | translate }}</span>
</div>
<div class="xp-sub">{{ 'ABOUT.XP.T2.COMPANY' | translate }}</div>
<ul>
<li>{{ 'ABOUT.XP.T2.P1' | translate }}</li>
<li>{{ 'ABOUT.XP.T2.P2' | translate }}</li>
</ul>
</div>
</div>
</mat-card>
</section>

View File

@@ -0,0 +1,86 @@
.about {
display: grid;
gap: 1rem;
}
/* Hero block: Photo + Intro */
.hero {
display: grid;
grid-template-columns: 240px 1fr;
gap: 1.25rem;
border-radius: 16px;
background: var(--app-card-background);
.photo {
align-items:flex-start; justify-content:center;
img {
display:block;
width: 100%; height: auto;
max-width: 220px;
border-radius: 12px;
box-shadow: 0 6px 24px rgba(0,0,0,.25);
object-fit: cover;
}
}
.intro {
display:flex; flex-direction:column; gap:.5rem;
h1 { margin-top: .25rem }
.lead { opacity:.9; margin: .25rem 0 0.5rem; }
.meta {
display:flex; flex-direction:column; gap:.25rem;
.row {
display:flex; align-items:center; gap:.4rem;
a { color: inherit; }
}
}
.actions {
display:flex; gap:.5rem; flex-wrap:wrap; margin-top:.5rem; margin-bottom: .25rem;
.mat-icon { margin-right:.25rem; }
}
}
}
/* Skills block */
.skills {
border-radius: 16px;
background: var(--app-card-background);
h2 { margin-top: .25rem; margin-left: .25rem; }
.chip-groups {
margin-left: .25rem;
display:grid; gap:1rem;
grid-template-columns: 1fr 1fr;
h3 { margin: .2rem 0 .4rem; font-size: .95rem; opacity:.85; }
mat-chip-set {
display:flex; flex-wrap:wrap; gap:.4rem;
}
}
}
/* Experience block */
.experience {
border-radius: 16px;
background: var(--app-card-background);
h2 { margin-top: .25rem;margin-left: .25rem; }
.xp-list {
margin-left: .25rem;
display: grid; gap: .75rem;
}
.xp-item {
.xp-head {
display:flex; align-items:baseline; gap:.5rem;
.time { opacity:.75; font-size:.9rem; }
}
.xp-sub { opacity:.9; margin-bottom:.25rem; }
ul { margin: .25rem 0 .5rem 1.15rem; }
}
}
/* Responsive */
@media (max-width: 900px) {
.hero { grid-template-columns: 1fr; }
.hero .photo { justify-content: flex-start; }
.skills .chip-groups { grid-template-columns: 1fr; }
}

View File

@@ -0,0 +1,60 @@
import { Component, inject } from '@angular/core';
import { CommonModule, NgOptimizedImage } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { TranslateModule } from '@ngx-translate/core';
import {RouterLink} from '@angular/router';
import {UrlConstants} from '../../constants/UrlConstants';
import {AssetsConstants} from '../../constants/AssetsConstants';
@Component({
selector: 'app-about',
standalone: true,
imports: [
CommonModule, NgOptimizedImage,
MatCardModule, MatChipsModule, MatIconModule, MatButtonModule, MatDividerModule,
TranslateModule, RouterLink
],
templateUrl: './about.component.html',
styleUrl: './about.component.scss'
})
export class AboutComponent {
cvHref = 'assets/cv/andreas-dahm-cv.pdf';
primarySkills = [
'ABOUT.SKILLS.JAVA',
'ABOUT.SKILLS.SPRING',
'ABOUT.SKILLS.ANGULAR',
'ABOUT.SKILLS.DOCKER',
'ABOUT.SKILLS.UNITY',
'ABOUT.SKILLS.PYTHON',
'ABOUT.SKILLS.CSHARP',
'ABOUT.SKILLS.TYPESCRIPT'
];
toolset = [
'ABOUT.TOOLS.GIT',
'ABOUT.TOOLS.GITHUB',
'ABOUT.TOOLS.JENKINS',
'ABOUT.TOOLS.K8S',
'ABOUT.TOOLS.POSTGRES',
'ABOUT.TOOLS.MONGO',
'ABOUT.TOOLS.GRAFANA',
];
openMail(event: Event) {
event.preventDefault();
const user = 'andreas.dahm';
const domain = 'gmail.com';
globalThis.location.href = `mailto:${user}@${domain}`;
}
protected readonly UrlConstants = UrlConstants;
protected readonly AssetsConstants = AssetsConstants;
}

View File

@@ -0,0 +1 @@
<p>project-details works!</p>

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-project-details',
imports: [],
templateUrl: './project-details.component.html',
styleUrl: './project-details.component.scss',
})
export class ProjectDetailsComponent {
}

View File

@@ -0,0 +1 @@
<p>projects works!</p>

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-projects',
imports: [],
templateUrl: './projects.component.html',
styleUrl: './projects.component.scss',
})
export class ProjectsComponent {
}

View File

@@ -1,6 +1,6 @@
import { Injectable, inject, signal } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {Constants} from '../constants/Constants';
import {LocalStoreConstants} from '../constants/LocalStoreConstants';
type Lang = 'de' | 'en';
@@ -19,12 +19,12 @@ export class LanguageService {
use(l: Lang) {
this.lang.set(l);
this.translate.use(l);
try { localStorage.setItem(Constants.LANGUAGE_KEY, l); } catch {}
try { localStorage.setItem(LocalStoreConstants.LANGUAGE_KEY, l); } catch {}
}
private getInitial(): Lang {
try {
const stored = localStorage.getItem(Constants.LANGUAGE_KEY) as Lang | null;
const stored = localStorage.getItem(LocalStoreConstants.LANGUAGE_KEY) as Lang | null;
if (stored === 'de' || stored === 'en') return stored;
} catch {}
const browser = (navigator.language || 'en').toLowerCase();

View File

@@ -1,5 +1,5 @@
import { Injectable, NgZone, signal } from '@angular/core';
import {Constants} from '../constants/Constants';
import {LocalStoreConstants} from '../constants/LocalStoreConstants';
@Injectable({ providedIn: 'root' })
@@ -20,13 +20,13 @@ export class ReloadService {
private informListeners(e: StorageEvent, zone: NgZone) {
if (e.key === Constants.LANGUAGE_KEY) {
if (e.key === LocalStoreConstants.LANGUAGE_KEY) {
zone.run(() => this._languageChangedTick.update(v => v + 1));
}
}
bumpLanguageChanged(): void {
this._reloadTick.update(v => v + 1);
localStorage.setItem(Constants.RELOAD_ALL_LANG_LISTENER_KEY, String(Date.now()));
localStorage.setItem(LocalStoreConstants.RELOAD_ALL_LANG_LISTENER_KEY, String(Date.now()));
}
}

View File

@@ -1,7 +1,7 @@
import { Injectable, effect, inject, signal } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { OverlayContainer } from '@angular/cdk/overlay';
import {Constants} from '../constants/Constants';
import {LocalStoreConstants} from '../constants/LocalStoreConstants';
type Theme = 'light' | 'dark';
@@ -20,13 +20,13 @@ export class ThemeService {
body.classList.toggle('dark', isDark);
overlayEl.classList.toggle('dark', isDark);
try { localStorage.setItem(Constants.THEME_KEY, this.theme()); } catch {}
try { localStorage.setItem(LocalStoreConstants.THEME_KEY, this.theme()); } catch {}
});
try {
const mm = globalThis.matchMedia('(prefers-color-scheme: dark)');
mm.addEventListener('change', e => {
const stored = localStorage.getItem(Constants.THEME_KEY) as Theme | null;
const stored = localStorage.getItem(LocalStoreConstants.THEME_KEY) as Theme | null;
if (!stored) this.setTheme(e.matches ? 'dark' : 'light');
});
} catch {}
@@ -37,7 +37,7 @@ export class ThemeService {
private getInitialTheme(): Theme {
try {
const stored = localStorage.getItem(Constants.THEME_KEY) as Theme | null;
const stored = localStorage.getItem(LocalStoreConstants.THEME_KEY) as Theme | null;
if (stored === 'dark' || stored === 'light') return stored;
} catch {}
try {