Fonts come from npm, not Google, so no third-party font requests. Pages are prerendered at build time; browser-only code is guarded. nginx serves per-route HTML, a real 404 and gzip.
177 lines
8.1 KiB
HTML
177 lines
8.1 KiB
HTML
<section class="grid gap-fluid-md max-w-app mx-4 mt-auto">
|
|
<mat-card class="card-gradient-bar">
|
|
<div class="flex flex-wrap gap-fluid-lg p-fluid-md items-start">
|
|
<div class="flex-[1_1_min(100%,425px)] max-w-full flex justify-center">
|
|
<img class="block w-full h-auto max-w-[425px] rounded-xl shadow-[0_6px_24px_rgba(0,0,0,0.25)] object-cover"
|
|
[ngSrc]="AssetsConstants.ME" width="421" height="512" alt="{{ 'ABOUT.ALT.PROFILE' | translate }}"
|
|
draggable="false" oncontextmenu="return false;" priority />
|
|
</div>
|
|
|
|
<div class="flex-[999_1_min(100%,400px)]">
|
|
<h1 class="mt-0 mb-2 text-[clamp(1.5rem,5vw,2.5rem)]">{{ 'ABOUT.HELLO' | translate }}</h1>
|
|
<p class="opacity-90 my-2 mb-4 text-[clamp(1rem,2.5vw,1.15rem)]">
|
|
{{ 'ABOUT.LEAD' | translate }}
|
|
</p>
|
|
|
|
<div class="flex flex-col gap-1 mb-4">
|
|
<div class="flex items-center flex-wrap gap-[0.4rem]">
|
|
<mat-icon aria-hidden="true">work</mat-icon>
|
|
<span>{{ 'ABOUT.ROLE' | translate }}</span>
|
|
</div>
|
|
<div class="flex items-center flex-wrap gap-[0.4rem]">
|
|
<mat-icon aria-hidden="true">location_on</mat-icon>
|
|
<span>{{ 'ABOUT.LOCATION' | translate }}</span>
|
|
</div>
|
|
<div class="flex items-center flex-wrap gap-[0.4rem]">
|
|
<mat-icon aria-hidden="true">email</mat-icon>
|
|
<a href="mailto:info@andreas-dahm.eu" data-testid="contact-mail">info@andreas-dahm.eu</a>
|
|
</div>
|
|
<div class="flex items-center flex-wrap gap-[0.4rem]">
|
|
<mat-icon aria-hidden="true">data_object</mat-icon>
|
|
<a href="{{UrlConstants.GITEA}}" target="_blank" rel="noopener">Gitea</a>
|
|
<span>·</span>
|
|
<mat-icon svgIcon="linkedin" aria-hidden="true"></mat-icon>
|
|
<a href="{{UrlConstants.LINKED_IN}}" target="_blank" rel="noopener">LinkedIn</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-fluid-sm">
|
|
<a mat-flat-button [routerLink]="['/projects']">
|
|
<mat-icon aria-hidden="true">grid_view</mat-icon>
|
|
{{ 'ABOUT.VIEW_PROJECTS' | translate }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</mat-card>
|
|
|
|
<mat-card class="p-[clamp(5px,2vw,15px)]">
|
|
<h2 class="mt-1 ml-1 text-[clamp(1.2rem,4vw,1.8rem)]">{{ 'ABOUT.SECTION.SKILLS' | translate }}</h2>
|
|
<div class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,250px),1fr))] gap-fluid-sm ml-1 mb-2">
|
|
@for (group of skillGroups; track group.titleKey) {
|
|
<div>
|
|
<h3 class="my-1 text-[0.95rem] opacity-85">{{ group.titleKey | translate }}</h3>
|
|
<mat-chip-set [attr.aria-label]="group.titleKey | translate">
|
|
@for (skillKey of group.skillKeys; track skillKey) {
|
|
<mat-chip>{{ skillKey | translate }}</mat-chip>
|
|
}
|
|
</mat-chip-set>
|
|
</div>
|
|
}
|
|
</div>
|
|
</mat-card>
|
|
|
|
<mat-card class="p-[clamp(5px,2vw,15px)]">
|
|
<h2 class="mt-1 ml-2 text-[clamp(1.2rem,4vw,1.8rem)]">{{ 'ABOUT.SECTION.EXPERIENCE' | translate }}</h2>
|
|
|
|
<div class="relative ml-2">
|
|
<span class="absolute left-5 top-2 bottom-2 w-px bg-current opacity-15" aria-hidden="true"></span>
|
|
@for (entry of visibleExperience(); track entry.key; let i = $index) {
|
|
<div class="relative pl-14 pb-fluid-sm">
|
|
@if (isFirstRoleAtCompany(i)) {
|
|
<img src="{{entry.logo}}" alt="" aria-hidden="true"
|
|
class="absolute left-0 top-0 w-10 h-10 object-contain opacity-90 rounded-[10%] bg-logo-bg" />
|
|
<div class="font-semibold opacity-90 text-[clamp(0.9rem,2.2vw,1.05rem)] leading-10" data-testid="company">
|
|
{{ (entry.key + '.COMPANY') | translate }}
|
|
</div>
|
|
} @else {
|
|
<span class="absolute left-5 top-[0.6rem] -translate-x-1/2 w-2 h-2 rounded-full bg-current opacity-30"
|
|
aria-hidden="true"></span>
|
|
}
|
|
|
|
<div class="flex flex-wrap items-baseline gap-x-4 gap-y-1">
|
|
<strong class="text-[clamp(0.95rem,2.5vw,1.1rem)]" data-testid="role">{{ (entry.key + '.ROLE') | translate }}</strong>
|
|
<span class="opacity-75 text-[clamp(0.85rem,2vw,0.95rem)]">{{ (entry.key + '.TIME') | translate }}</span>
|
|
</div>
|
|
|
|
<ul class="mt-[0.4rem]">
|
|
@for (highlight of highlightKeys(entry); track highlight) {
|
|
<li class="my-1 text-[clamp(0.9rem,2vw,1rem)]">{{ highlight | translate }}</li>
|
|
}
|
|
</ul>
|
|
|
|
@if (isLastRoleAtCompany(i) && !$last) {
|
|
<mat-divider class="mt-fluid-sm!"></mat-divider>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<button mat-button type="button" class="mt-1 ml-2" data-testid="toggle-experience" (click)="toggleEarlierExperience()"
|
|
[attr.aria-expanded]="showEarlierExperience()">
|
|
<mat-icon aria-hidden="true">{{ showEarlierExperience() ? 'expand_less' : 'expand_more' }}</mat-icon>
|
|
{{ (showEarlierExperience() ? 'ABOUT.SECTION.SHOW_LESS' : 'ABOUT.SECTION.SHOW_EARLIER_XP') | translate }}
|
|
</button>
|
|
</mat-card>
|
|
|
|
<mat-card class="p-[clamp(5px,2vw,15px)]">
|
|
<h2 class="mt-1 ml-1 text-[clamp(1.2rem,4vw,1.8rem)]">{{ 'ABOUT.SECTION.PROJECTS' | translate }}</h2>
|
|
|
|
<div class="ml-1 grid gap-fluid-sm">
|
|
@for (entry of projectKeys; track entry.key) {
|
|
<div>
|
|
<div class="flex flex-wrap items-baseline gap-x-4 gap-y-1">
|
|
<strong class="text-[clamp(0.95rem,2.5vw,1.1rem)]">{{ (entry.key + '.TITLE') | translate }}</strong>
|
|
</div>
|
|
<div class="mt-[0.1rem] opacity-85 text-[clamp(0.85rem,2vw,1rem)]">
|
|
{{ (entry.key + '.DESCRIPTION') | translate }}
|
|
</div>
|
|
@if (entry.externalLink) {
|
|
<div class="mt-[0.1rem] opacity-85">
|
|
<a class="inline-flex items-center gap-[0.35rem] leading-none" href="{{entry.externalLink}}" target="_blank" rel="noopener noreferrer">
|
|
<mat-icon class="text-[18px]! w-[18px]! h-[18px]!" aria-hidden="true">open_in_new</mat-icon>
|
|
{{ (entry.key + '.LINK_EXTERNAL') | translate }}
|
|
</a>
|
|
</div>
|
|
}
|
|
<div class="mt-[0.1rem] opacity-85">
|
|
<a class="inline-flex items-center gap-[0.35rem] leading-none" [routerLink]="['/projects']" [queryParams]="{ project: entry.identifier }"
|
|
rel="noopener noreferrer">
|
|
<mat-icon class="text-[18px]! w-[18px]! h-[18px]!" aria-hidden="true">link</mat-icon>
|
|
{{ (entry.key + '.LINK_INTERNAL') | translate }}
|
|
</a>
|
|
</div>
|
|
<div>
|
|
<ul>
|
|
<li class="my-1 text-[clamp(0.9rem,2vw,1rem)]">{{ entry.key + '.HIGHLIGHTS.P1' | translate }}</li>
|
|
<li class="my-1 text-[clamp(0.9rem,2vw,1rem)]">{{ entry.key + '.HIGHLIGHTS.P2' | translate }}</li>
|
|
<li class="my-1 text-[clamp(0.9rem,2vw,1rem)]">{{ entry.key + '.HIGHLIGHTS.P3' | translate }}</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!$last) {
|
|
<mat-divider></mat-divider>
|
|
}
|
|
}
|
|
</div>
|
|
</mat-card>
|
|
|
|
<mat-card class="p-[clamp(5px,2vw,15px)]">
|
|
<h2 class="mt-1 ml-1 text-[clamp(1.2rem,4vw,1.8rem)]">{{ 'ABOUT.SECTION.EDUCATION' | translate }}</h2>
|
|
|
|
<div class="ml-1 grid gap-fluid-sm">
|
|
<div>
|
|
@for (key of visibleEducationKeys(); track key) {
|
|
<div class="flex flex-wrap items-baseline gap-x-4 gap-y-1">
|
|
<strong class="text-[clamp(0.95rem,2.5vw,1.1rem)]" data-testid="education-where">{{ (key + '.WHERE') | translate }}</strong>
|
|
<span class="opacity-75 text-[clamp(0.85rem,2vw,0.95rem)]">{{ (key + '.WHEN') | translate }}</span>
|
|
</div>
|
|
<div class="mt-[0.1rem] opacity-85 text-[clamp(0.85rem,2vw,1rem)]">
|
|
{{ (key + '.WHAT') | translate }}
|
|
</div>
|
|
@if (!$last) {
|
|
<mat-divider class="my-2!"></mat-divider>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<button mat-button type="button" class="mt-1" data-testid="toggle-education" (click)="toggleEarlierEducation()"
|
|
[attr.aria-expanded]="showEarlierEducation()">
|
|
<mat-icon aria-hidden="true">{{ showEarlierEducation() ? 'expand_less' : 'expand_more' }}</mat-icon>
|
|
{{ (showEarlierEducation() ? 'ABOUT.SECTION.SHOW_LESS' : 'ABOUT.SECTION.SHOW_EARLIER_EDU') | translate }}
|
|
</button>
|
|
</mat-card>
|
|
</section>
|