Started with about page and my cv
This commit is contained in:
1 parent
b80d70d4df
commit
5969e66872
36 files changed
+637
-169
No files matched your search
+1
-1
@@ -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');
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>
|
||||
Whitespace-only changes.
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<p>projects works!</p>
|
||||
Whitespace-only changes.
@@ -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 {
|
||||
|
||||
File renamed without changes.
+59
-9
@@ -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)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
-9
@@ -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)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
+1
-1
@@ -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">
|
||||
|
||||
+2
-2
@@ -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