Started with about page and my cv
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets/favicon.ico",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
@@ -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; }
|
||||
@@ -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');
|
||||
}
|
||||
7
src/app/constants/AssetsConstants.ts
Normal file
7
src/app/constants/AssetsConstants.ts
Normal 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';
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export class Constants{
|
||||
export class LocalStoreConstants {
|
||||
|
||||
static readonly THEME_KEY = 'theme';
|
||||
static readonly LANGUAGE_KEY = 'lang';
|
||||
4
src/app/constants/UrlConstants.ts
Normal file
4
src/app/constants/UrlConstants.ts
Normal 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';
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,5 +0,0 @@
|
||||
mat-card {
|
||||
margin-top: 2rem;
|
||||
display: block;
|
||||
background-color: var(--app-card-background);
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
9
src/app/layout/app/app.component.html
Normal file
9
src/app/layout/app/app.component.html
Normal 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>
|
||||
10
src/app/layout/app/app.component.scss
Normal file
10
src/app/layout/app/app.component.scss
Normal 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;
|
||||
}
|
||||
16
src/app/layout/app/app.component.ts
Normal file
16
src/app/layout/app/app.component.ts
Normal 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();
|
||||
}
|
||||
58
src/app/layout/topbar/topbar.component.html
Normal file
58
src/app/layout/topbar/topbar.component.html
Normal 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>
|
||||
73
src/app/layout/topbar/topbar.component.scss
Normal file
73
src/app/layout/topbar/topbar.component.scss
Normal 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; }
|
||||
}
|
||||
44
src/app/layout/topbar/topbar.component.ts
Normal file
44
src/app/layout/topbar/topbar.component.ts
Normal 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;
|
||||
}
|
||||
110
src/app/pages/about/about.component.html
Normal file
110
src/app/pages/about/about.component.html
Normal 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>
|
||||
86
src/app/pages/about/about.component.scss
Normal file
86
src/app/pages/about/about.component.scss
Normal 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; }
|
||||
}
|
||||
60
src/app/pages/about/about.component.ts
Normal file
60
src/app/pages/about/about.component.ts
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<p>project-details works!</p>
|
||||
11
src/app/pages/project-details/project-details.component.ts
Normal file
11
src/app/pages/project-details/project-details.component.ts
Normal 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 {
|
||||
|
||||
}
|
||||
1
src/app/pages/projects/projects.component.html
Normal file
1
src/app/pages/projects/projects.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<p>projects works!</p>
|
||||
0
src/app/pages/projects/projects.component.scss
Normal file
0
src/app/pages/projects/projects.component.scss
Normal file
11
src/app/pages/projects/projects.component.ts
Normal file
11
src/app/pages/projects/projects.component.ts
Normal 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 {
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@@ -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)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, I’m 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
BIN
src/assets/me.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
@@ -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">
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user