feat(moritzgrafde): rewrite personal website with Vue 3, Vite, and Tailwind CSS

- Replace legacy Hugo static site generator and hugo-goa theme with a modern Vue 3 + Vite + TypeScript application
- Implement Clean Enterprise design system using Tailwind CSS with light/dark mode support
- Position website around Cloud Platform & Infrastructure Leadership (GCP & AWS central platforms, Kubernetes at scale, enterprise spin-off governance, and systems depth) without chronological resumes
- Add 6 comprehensive capability dossiers and verified credential badges (CKA, PSM II, M.Sc.)
- Implement interactive skill matrix with real-time text search and category filtering
- Add engineering philosophy section, contact channels with one-click email copying, and Impressum modal
- Configure multi-stage Dockerfile (Node builder to Nginx runtime) with custom gzip, caching, and security headers
- Update build scripts (Makefile, build.sh), .gitignore, and AGENTS.md documentation across the monorepo
This commit is contained in:
Moritz Graf
2026-09-26 20:37:53 +02:00
parent afb80bbba8
commit d2dac3470f
103 changed files with 3872 additions and 1924 deletions
@@ -0,0 +1,133 @@
<script setup lang="ts">
import { ref } from 'vue'
import { personalInfo, socialLinks } from '@/data/portfolioData'
import { Mail, Linkedin, Github, Copy, Check, ExternalLink, ShieldCheck } from 'lucide-vue-next'
defineEmits<{
(e: 'openImpressum'): void
}>()
const copied = ref(false)
const copyEmail = async () => {
try {
await navigator.clipboard.writeText(personalInfo.email)
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
} catch (err) {
console.error('Failed to copy', err)
}
}
const getSocialIcon = (platform: string) => {
switch (platform) {
case 'LinkedIn': return Linkedin
case 'GitHub': return Github
case 'Email': return Mail
default: return ExternalLink
}
}
</script>
<template>
<section id="contact" class="py-16 md:py-24 border-t border-slate-200/80 dark:border-slate-800/80 bg-slate-100/60 dark:bg-slate-900/40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-3xl mx-auto text-center space-y-4 mb-12">
<div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold uppercase tracking-wider bg-blue-50 text-blue-700 dark:bg-blue-950/60 dark:text-blue-300 border border-blue-200/80 dark:border-blue-800/80">
<Mail class="w-3.5 h-3.5" />
<span>Connect & Exchange</span>
</div>
<h2 class="text-3xl sm:text-4xl font-extrabold text-slate-900 dark:text-white tracking-tight">
Let’s Talk Cloud Platforms, Systems & Scale
</h2>
<p class="text-base sm:text-lg text-slate-600 dark:text-slate-300">
Whether you want to discuss cloud platform engineering, Kubernetes at scale, enterprise carve-outs, or maker hardware craft: my inbox is open.
</p>
</div>
<!-- Contact Cards -->
<div class="max-w-2xl mx-auto grid grid-cols-1 sm:grid-cols-2 gap-4 mb-12">
<!-- Direct Email Card with Copy Button -->
<div class="p-6 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/90 dark:border-slate-800/90 shadow-xs flex flex-col justify-between text-left">
<div class="flex items-center justify-between mb-4">
<span class="text-xs font-bold uppercase tracking-wider text-blue-600 dark:text-blue-400">
Direct Mail
</span>
<div class="p-2 rounded-lg bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400">
<Mail class="w-4 h-4" />
</div>
</div>
<div class="mb-4">
<div class="text-xs text-slate-500 dark:text-slate-400 mb-1">Send an inquiry</div>
<a
:href="`mailto:${personalInfo.email}`"
class="text-sm font-semibold text-slate-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors break-all"
>
{{ personalInfo.email }}
</a>
</div>
<button
@click="copyEmail"
class="inline-flex items-center justify-center gap-1.5 w-full py-2 px-3 rounded-lg text-xs font-semibold bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-750 text-slate-700 dark:text-slate-200 transition-colors cursor-pointer"
>
<Check v-if="copied" class="w-3.5 h-3.5 text-emerald-500" />
<Copy v-else class="w-3.5 h-3.5 text-slate-500" />
<span>{{ copied ? 'Copied to Clipboard!' : 'Copy Email Address' }}</span>
</button>
</div>
<!-- Professional Networks -->
<div class="p-6 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/90 dark:border-slate-800/90 shadow-xs flex flex-col justify-between text-left">
<div class="flex items-center justify-between mb-4">
<span class="text-xs font-bold uppercase tracking-wider text-blue-600 dark:text-blue-400">
Public Profiles
</span>
<div class="p-2 rounded-lg bg-indigo-50 dark:bg-indigo-950 text-indigo-600 dark:text-indigo-400">
<ExternalLink class="w-4 h-4" />
</div>
</div>
<div class="space-y-3">
<a
v-for="link in socialLinks.filter(l => l.platform !== 'Email')"
:key="link.platform"
:href="link.url"
target="_blank"
rel="noopener noreferrer"
class="flex items-center justify-between p-2.5 rounded-lg border border-slate-200/80 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/60 transition-all group"
>
<div class="flex items-center gap-2.5">
<component :is="getSocialIcon(link.platform)" class="w-4 h-4 text-slate-500 group-hover:text-blue-600 transition-colors" />
<span class="text-xs font-semibold text-slate-800 dark:text-slate-200">{{ link.platform }}</span>
</div>
<span class="text-[11px] font-mono text-slate-400 group-hover:text-blue-600 dark:group-hover:text-blue-400">
/{{ link.handle }}
</span>
</a>
</div>
</div>
</div>
<!-- Infrastructure Tag & Impressum Trigger -->
<div class="pt-8 border-t border-slate-200/80 dark:border-slate-800/80 flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-slate-500 dark:text-slate-400">
<div class="flex items-center gap-2">
<ShieldCheck class="w-4 h-4 text-emerald-500" />
<span>Self-hosted on private Kubernetes infrastructure (haumdaucher) via GitOps</span>
</div>
<div class="flex items-center gap-4">
<button
@click="$emit('openImpressum')"
class="hover:text-slate-800 dark:hover:text-slate-200 underline cursor-pointer"
>
Legal Notice (Impressum)
</button>
<span>·</span>
<span>© {{ new Date().getFullYear() }} Moritz Graf</span>
</div>
</div>
</div>
</section>
</template>