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

@@ -25,7 +25,7 @@
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets/favicon.ico",
{
"glob": "**/*",
"input": "public"

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 {

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,13 +1,63 @@
{
"APP": { "TITLE": "Playground" },
"WELCOME": {
"TITLE": "Willkommen 👋",
"SUB": "Angular 20 + Material",
"TEXT": "Das ist eine einfache Start-Komponente als Basis.",
"COUNTER": "Zähler",
"INC": "Inkrementieren",
"RESET": "Zurücksetzen"
"APP": {
"TITLE": "Playground",
"COPYRIGHT": "Bilder urheberrechtlich geschützt, keine Nutzung ohne Zustimmung!"
},
"TOPBAR": {
"ABOUT": "Über mich",
"CONTACT": "Kontakt",
"PROJECTS": "Projekte",
"HOBBY": "Hobbies",
"SETTINGS": "Einstellungen",
"LANGUAGE": "Sprache",
"APPEARANCE": "Darstellung"
},
"THEME": { "LIGHT": "Hell", "DARK": "Dunkel" },
"LANG": { "LABEL": "Sprache", "EN": "Englisch", "DE": "Deutsch" }
"LANG": { "LABEL": "Sprache", "EN": "Englisch", "DE": "Deutsch" },
"ABOUT": {
"ALT": { "PROFILE": "Profilfoto von Andreas Dahm" },
"HELLO": "Hallo, ich bin Andreas Dahm.",
"LEAD": "Bereits in meiner Ausbildung zum Fachinformatiker hat mich fasziniert, wie man durch Code komplexe Probleme elegant lösen kann. Nach meinem Studium der Angewandten Informatik an der FH Bingen konnte ich diese Begeisterung in verschiedensten Projekten vertiefen von 3D-Simulationen bis zu modernen Web-Applikationen. Heute arbeite ich als Senior Software Developer und Architekt mit Fokus auf Java, Angular und DevOps. Danke, dass du vorbeischaust!",
"ROLE": "Senior Software Entwickler / Full-Stack Entwickler / Softwarearchitekt",
"LOCATION": "München · Remote",
"DOWNLOAD_CV": "Lebenslauf herunterladen",
"VIEW_PROJECTS": "Projekte ansehen",
"CONTACT_ME": "Kontaktiere mich",
"SECTION": {
"SKILLS": "Fähigkeiten & Stack",
"PRIMARY": "Schwerpunkte",
"TOOLSET": "Toolset",
"EXPERIENCE": "Erfahrung"
},
"SKILLS": {
"JAVA": "Java 8/Java 21+",
"SPRING": "Spring Boot 2/3",
"ANGULAR": "Angular 20+",
"DOCKER": "Docker",
"UNITY": "Unity",
"PYTHON": "Python",
"CSHARP": "C#",
"TYPESCRIPT": "TypeScript"
},
"TOOLS": {
"GIT": "Git",
"GITHUB": "Github",
"JENKINS": "Jenkins",
"K8S": "Kubernetes / k3d",
"POSTGRES": "PostgreSQL",
"MONGO": "MongoDB",
"GRAFANA": "Grafana/Prometheus"
},
"XP": {
"T1": {
"COMPANY": "Teraport GmbH",
"ROLE": "Senior Software Developer / Architect",
"TIME": "Feb. 2024 heute",
"HIGHLIGHTS": {
"P1": "Architecture and implementation of database connectivity using Hibernate 6.x.",
"P2": "Design and development of a full-stack web application for collision analysis (Angular + Spring Boot + Docker)."
}
}
}
}
}

View File

@@ -1,13 +1,62 @@
{
"APP": { "TITLE": "Playground" },
"WELCOME": {
"TITLE": "Welcome 👋",
"SUB": "Angular 20 + Material",
"TEXT": "This is a simple start component.",
"COUNTER": "Counter",
"INC": "Increment",
"RESET": "Reset"
"APP": {
"TITLE": "Playground",
"COPYRIGHT": "Images protected by copyright, no use without permission!"
},
"TOPBAR": {
"ABOUT": "About me",
"CONTACT": "Contact",
"PROJECTS": "Projects",
"HOBBY": "Hobby's",
"SETTINGS": "Settings",
"LANGUAGE": "Language",
"APPEARANCE": "Appearance"
},
"THEME": { "LIGHT": "Light", "DARK": "Dark" },
"LANG": { "LABEL": "Language", "EN": "English", "DE": "Deutsch" }
"LANG": { "LABEL": "Language", "EN": "English", "DE": "Deutsch" },
"ABOUT": {
"ALT": { "PROFILE": "Profile photo of Andreas Dahm" },
"HELLO": "Hello, Im Andreas Dahm.",
"LEAD": "During my training as an application developer, I became fascinated by how complex problems can be solved elegantly through code. After studying Applied Computer Science at FH Bingen, I was able to deepen this passion in a wide variety of projects from 3D simulations to modern web applications. Today, I work as a Senior Software Developer and Architect with a focus on Java, Angular, and DevOps. Thanks for stopping by!",
"ROLE": "Senior Software Developer / Full-Stack Developer / Software Architect",
"LOCATION": "Munich · Remote",
"DOWNLOAD_CV": "Download CV",
"VIEW_PROJECTS": "View projects",
"CONTACT_ME": "Contact me",
"SECTION": {
"SKILLS": "Skills & Stack",
"PRIMARY": "Core",
"TOOLSET": "Toolset",
"EXPERIENCE": "Experience"
},
"SKILLS": {
"JAVA": "Java 8/Java 21+",
"SPRING": "Spring Boot 2/3",
"ANGULAR": "Angular 20+",
"DOCKER": "Docker",
"UNITY": "Unity",
"PYTHON": "Python",
"CSHARP": "C#",
"TYPESCRIPT": "TypeScript"
},
"TOOLS": {
"GIT": "Git",
"GITHUB": "GITHUB",
"K8S": "Kubernetes / k3d",
"POSTGRES": "PostgreSQL",
"MONGO": "MongoDB",
"GRAFANA": "Grafana/Prometheus"
},
"XP": {
"T1": {
"COMPANY": "Teraport GmbH",
"ROLE": "Senior Software Developer / Architect",
"TIME": "Feb. 2024 heute",
"HIGHLIGHTS": {
"P1": "Architecture and implementation of database connectivity using Hibernate 6.x.",
"P2": "Design and development of a full-stack web application for collision analysis (Angular + Spring Boot + Docker)."
}
}
}
}
}

BIN
src/assets/me.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -5,7 +5,7 @@
<title>PlaygroundFrontend</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="icon" type="image/x-icon" href="assets/favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&display=swap" rel="stylesheet">

View File

@@ -1,11 +1,11 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
import packageJson from '../package.json';
import {AppComponent} from './app/layout/app/app.component';
if (packageJson.version) {
console.log(`🌟 Frontend version: ${packageJson.version}`);
}
bootstrapApplication(App, appConfig)
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));