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
+47
View File
@@ -0,0 +1,47 @@
<script setup lang="ts">
import { ref } from 'vue'
import Navbar from '@/components/Navbar.vue'
import Hero from '@/components/Hero.vue'
import ImpactStats from '@/components/ImpactStats.vue'
import CapabilityDossiers from '@/components/CapabilityDossiers.vue'
import SkillMatrix from '@/components/SkillMatrix.vue'
import EngineeringPhilosophy from '@/components/EngineeringPhilosophy.vue'
import ContactSection from '@/components/ContactSection.vue'
import ImpressumModal from '@/components/ImpressumModal.vue'
const isImpressumOpen = ref(false)
</script>
<template>
<div class="min-h-screen flex flex-col bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 selection:bg-blue-500/20 selection:text-blue-700 dark:selection:text-blue-300">
<!-- Sticky Navigation -->
<Navbar />
<!-- Main Content -->
<main class="flex-grow">
<!-- Hero Section -->
<Hero />
<!-- Impact Stats Banner -->
<ImpactStats />
<!-- Deep Capability Dossiers (6 Pillars) -->
<CapabilityDossiers />
<!-- Interactive Skill Matrix & Filter -->
<SkillMatrix />
<!-- Architectural & Leadership Principles -->
<EngineeringPhilosophy />
<!-- Contact & Connect -->
<ContactSection @open-impressum="isImpressumOpen = true" />
</main>
<!-- Impressum Modal -->
<ImpressumModal
:is-open="isImpressumOpen"
@close="isImpressumOpen = false"
/>
</div>
</template>
@@ -0,0 +1,115 @@
<script setup lang="ts">
import { capabilityDossiers } from '@/data/portfolioData'
import {
Cloud,
Server,
Database,
Code2,
Users,
Cpu,
CheckCircle2,
Sparkles
} from 'lucide-vue-next'
const getDossierIcon = (iconName: string) => {
switch (iconName) {
case 'Cloud': return Cloud
case 'Server': return Server
case 'Database': return Database
case 'Code2': return Code2
case 'Users': return Users
case 'Cpu': return Cpu
default: return Cloud
}
}
</script>
<template>
<section id="capabilities" class="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Section Header -->
<div class="text-left max-w-3xl mb-12 sm:mb-16">
<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 mb-3">
<Sparkles class="w-3.5 h-3.5" />
<span>Core Competencies & Scope</span>
</div>
<h2 class="text-3xl sm:text-4xl font-extrabold text-slate-900 dark:text-white tracking-tight">
Capability Dossiers
</h2>
<p class="mt-3 text-base sm:text-lg text-slate-600 dark:text-slate-300">
A deep architectural breakdown of what I build, operate, and lead — spanning enterprise multi-cloud platforms, Kubernetes orchestration, corporate carve-outs, and low-level systems engineering.
</p>
</div>
<!-- Dossiers Grid: 6 Pillars -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div
v-for="dossier in capabilityDossiers"
:key="dossier.id"
class="flex flex-col justify-between p-6 sm:p-7 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/90 dark:border-slate-800/90 shadow-xs hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-500/50 transition-all duration-300 group text-left"
>
<div>
<!-- Card Header & Icon -->
<div class="flex items-center justify-between mb-5">
<span class="text-xs font-semibold uppercase tracking-wider text-blue-600 dark:text-blue-400">
{{ dossier.category }}
</span>
<div class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 group-hover:bg-blue-600 group-hover:text-white transition-colors">
<component :is="getDossierIcon(dossier.icon)" class="w-5 h-5" />
</div>
</div>
<!-- Title & Tagline -->
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{{ dossier.title }}
</h3>
<p class="text-xs font-medium text-slate-500 dark:text-slate-400 italic mb-4">
"{{ dossier.tagline }}"
</p>
<!-- Overview -->
<p class="text-xs sm:text-sm text-slate-600 dark:text-slate-300 leading-relaxed mb-6">
{{ dossier.overview }}
</p>
<!-- Key Scope Areas -->
<div class="space-y-2.5 mb-6">
<div class="text-[11px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500">
Key Focus Areas
</div>
<ul class="space-y-2">
<li
v-for="(area, aIdx) in dossier.keyAreas"
:key="aIdx"
class="flex items-start gap-2 text-xs text-slate-700 dark:text-slate-300"
>
<CheckCircle2 class="w-3.5 h-3.5 text-blue-500 mt-0.5 shrink-0" />
<span class="leading-snug">{{ area }}</span>
</li>
</ul>
</div>
</div>
<div>
<!-- Impact Box -->
<div class="p-3 rounded-xl bg-slate-50 dark:bg-slate-800/60 border border-slate-100 dark:border-slate-800 text-[11px] text-slate-600 dark:text-slate-300 mb-5">
<span class="font-semibold text-slate-800 dark:text-slate-200">Outcome:</span> {{ dossier.impactNote }}
</div>
<!-- Technologies Badges -->
<div class="pt-3 border-t border-slate-100 dark:border-slate-800">
<div class="flex flex-wrap gap-1.5">
<span
v-for="tech in dossier.technologies"
:key="tech"
class="px-2 py-0.5 rounded-md text-[11px] font-mono font-medium bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 border border-slate-200/60 dark:border-slate-700/60"
>
{{ tech }}
</span>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
@@ -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>
@@ -0,0 +1,60 @@
<script setup lang="ts">
import { engineeringPrinciples } from '@/data/portfolioData'
import { FileCode, Zap, Layers, HeartHandshake, Compass } from 'lucide-vue-next'
const getPrincipleIcon = (name: string) => {
switch (name) {
case 'FileCode': return FileCode
case 'Zap': return Zap
case 'Layers': return Layers
case 'HeartHandshake': return HeartHandshake
default: return Compass
}
}
</script>
<template>
<section id="principles" class="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Section Header -->
<div class="text-left max-w-3xl mb-12 sm:mb-16">
<div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold uppercase tracking-wider bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 border border-slate-200 dark:border-slate-700 mb-3">
<Compass class="w-3.5 h-3.5" />
<span>Philosophy & Stance</span>
</div>
<h2 class="text-3xl sm:text-4xl font-extrabold text-slate-900 dark:text-white tracking-tight">
Engineering Principles
</h2>
<p class="mt-3 text-base sm:text-lg text-slate-600 dark:text-slate-300">
How I approach architecture, platform governance, and team leadership.
</p>
</div>
<!-- Principles Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div
v-for="p in engineeringPrinciples"
:key="p.title"
class="p-7 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/90 dark:border-slate-800/90 shadow-xs hover:border-slate-300 dark:hover:border-slate-700 transition-all text-left flex flex-col justify-between"
>
<div>
<div class="flex items-center gap-3 mb-4">
<div class="p-2.5 rounded-xl bg-blue-50 dark:bg-blue-950/60 text-blue-600 dark:text-blue-400">
<component :is="getPrincipleIcon(p.icon)" class="w-5 h-5" />
</div>
<h3 class="text-lg font-bold text-slate-900 dark:text-white">
{{ p.title }}
</h3>
</div>
<blockquote class="text-sm font-semibold text-slate-800 dark:text-slate-200 italic border-l-2 border-blue-500 pl-3 my-3">
"{{ p.statement }}"
</blockquote>
<p class="text-xs sm:text-sm text-slate-600 dark:text-slate-400 leading-relaxed mt-4">
{{ p.explanation }}
</p>
</div>
</div>
</div>
</section>
</template>
@@ -0,0 +1,164 @@
<script setup lang="ts">
import { personalInfo, credentials } from '@/data/portfolioData'
import {
ShieldCheck,
Award,
GraduationCap,
CheckCircle2,
Globe,
ArrowRight,
Terminal,
MapPin
} from 'lucide-vue-next'
const getIcon = (name: string) => {
switch (name) {
case 'ShieldCheck': return ShieldCheck
case 'Award': return Award
case 'GraduationCap': return GraduationCap
case 'CheckCircle2': return CheckCircle2
case 'Globe': return Globe
default: return Award
}
}
const scrollTo = (selector: string) => {
const el = document.querySelector(selector)
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
}
}
</script>
<template>
<section id="overview" class="relative pt-12 pb-16 md:pt-20 md:pb-24 overflow-hidden">
<!-- Subtle Background Ambient Accents -->
<div class="absolute -top-24 left-1/2 -translate-x-1/2 w-full max-w-7xl h-96 bg-gradient-to-b from-blue-50/70 via-indigo-50/30 to-transparent dark:from-blue-950/20 dark:via-indigo-950/10 dark:to-transparent pointer-events-none -z-10 blur-3xl"></div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8 items-center">
<!-- Left Column: Copy, Headlines & Badges (7 cols) -->
<div class="lg:col-span-7 space-y-6 text-left">
<!-- Status Pill -->
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 dark:bg-emerald-950/50 dark:text-emerald-400 border border-emerald-200/80 dark:border-emerald-800/80 shadow-xs">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</span>
<span>{{ personalInfo.availabilityBadge }}</span>
<span class="text-slate-300 dark:text-slate-700">|</span>
<span class="inline-flex items-center gap-1 text-slate-500 dark:text-slate-400">
<MapPin class="w-3 h-3" />
{{ personalInfo.location }}
</span>
</div>
<!-- Main Title -->
<div class="space-y-2">
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-slate-900 dark:text-white tracking-tight leading-tight">
Moritz Graf
</h1>
<p class="text-xl sm:text-2xl font-semibold bg-gradient-to-r from-blue-600 via-indigo-600 to-cyan-600 dark:from-blue-400 dark:via-indigo-400 dark:to-cyan-400 bg-clip-text text-transparent">
{{ personalInfo.title }}
</p>
</div>
<!-- Value Proposition / Bio Summary -->
<p class="text-base sm:text-lg text-slate-600 dark:text-slate-300 leading-relaxed font-normal max-w-2xl">
{{ personalInfo.bioSummary }}
</p>
<!-- Verified Credential Badges -->
<div class="pt-2">
<div class="text-xs uppercase tracking-wider font-semibold text-slate-400 dark:text-slate-500 mb-3">
Verified Credentials & Degrees
</div>
<div class="flex flex-wrap gap-2.5">
<div
v-for="cred in credentials"
:key="cred.id"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all"
:class="[
cred.highlight
? 'bg-blue-50 text-blue-800 border border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-800'
: 'bg-slate-100 text-slate-700 border border-slate-200 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-800'
]"
:title="`${cred.title} — ${cred.issuer}`"
>
<component :is="getIcon(cred.icon)" class="w-3.5 h-3.5" :class="cred.highlight ? 'text-blue-600 dark:text-blue-400' : 'text-slate-500 dark:text-slate-400'" />
<span class="font-semibold">{{ cred.badgeText }}</span>
<span class="text-slate-400 dark:text-slate-500 text-[10px] hidden sm:inline">({{ cred.issuer.split(' ')[0] }})</span>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex flex-wrap items-center gap-4 pt-4">
<button
@click="scrollTo('#capabilities')"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl font-semibold text-sm bg-blue-600 text-white hover:bg-blue-700 shadow-md hover:shadow-lg transition-all cursor-pointer"
>
<span>Explore Capabilities</span>
<ArrowRight class="w-4 h-4" />
</button>
<button
@click="scrollTo('#skills')"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl font-semibold text-sm bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700 shadow-xs hover:bg-slate-50 dark:hover:bg-slate-800/80 transition-all cursor-pointer"
>
<Terminal class="w-4 h-4 text-slate-500" />
<span>Skill Matrix & Tech</span>
</button>
</div>
</div>
<!-- Right Column: Executive Portrait & Cluster Info (5 cols) -->
<div class="lg:col-span-5 flex justify-center lg:justify-end">
<div class="relative w-full max-w-sm sm:max-w-md">
<!-- Glow background -->
<div class="absolute inset-0 bg-gradient-to-tr from-blue-500/20 to-indigo-500/20 rounded-3xl blur-2xl -z-10 transform scale-95"></div>
<!-- Portrait Frame -->
<div class="relative rounded-3xl p-2 bg-gradient-to-b from-slate-200 to-slate-100 dark:from-slate-800 dark:to-slate-900 shadow-xl border border-slate-200/80 dark:border-slate-800/80">
<div class="relative rounded-2xl overflow-hidden bg-slate-900 aspect-square">
<img
src="/headshot.jpg"
alt="Moritz Graf"
class="w-full h-full object-cover object-center filter contrast-[1.02] hover:scale-102 transition-transform duration-500"
/>
<!-- Floating Info Badge on bottom of image -->
<div class="absolute bottom-3 inset-x-3 p-3 rounded-xl bg-slate-950/85 backdrop-blur-md border border-white/10 text-white shadow-lg text-left">
<div class="flex items-center justify-between">
<div>
<div class="text-xs font-semibold text-white tracking-wide">Moritz Graf</div>
<div class="text-[11px] text-slate-300">Central Platform Engineer & Lead</div>
</div>
<div class="text-right">
<span class="inline-block px-2 py-0.5 rounded text-[10px] font-mono font-medium bg-blue-500/20 text-blue-300 border border-blue-400/30">
GCP · AWS · K8s
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Decorative floating badge -->
<div class="absolute -top-4 -right-4 px-3.5 py-1.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-lg text-xs font-mono text-slate-700 dark:text-slate-300 hidden sm:flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
<span>Central Platform</span>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { impactStats } from '@/data/portfolioData'
import { Cloud, GitFork, Server, Activity } from 'lucide-vue-next'
const getStatIcon = (index: number) => {
switch (index) {
case 0: return Cloud
case 1: return GitFork
case 2: return Server
case 3: return Activity
default: return Server
}
}
</script>
<template>
<section class="py-10 border-y border-slate-200/80 dark:border-slate-800/80 bg-slate-100/50 dark:bg-slate-900/30">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div
v-for="(stat, idx) in impactStats"
:key="stat.label"
class="relative p-6 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800/80 shadow-xs hover:shadow-md transition-all group text-left"
>
<div class="flex items-center justify-between mb-4">
<span class="text-2xl lg:text-3xl font-extrabold text-slate-900 dark:text-white tracking-tight">
{{ stat.value }}
</span>
<div class="p-2.5 rounded-xl bg-blue-50 dark:bg-blue-950/60 text-blue-600 dark:text-blue-400 group-hover:scale-110 transition-transform">
<component :is="getStatIcon(idx)" class="w-5 h-5" />
</div>
</div>
<h3 class="text-sm font-bold text-slate-900 dark:text-slate-100 mb-1">
{{ stat.label }}
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 leading-relaxed font-normal">
{{ stat.description }}
</p>
</div>
</div>
</div>
</section>
</template>
@@ -0,0 +1,79 @@
<script setup lang="ts">
import { X, Shield } from 'lucide-vue-next'
defineProps<{
isOpen: boolean
}>()
const emit = defineEmits<{
(e: 'close'): void
}>()
</script>
<template>
<div
v-if="isOpen"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/70 backdrop-blur-xs"
@click.self="emit('close')"
>
<div
class="relative w-full max-w-lg p-6 sm:p-8 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-2xl text-left max-h-[90vh] overflow-y-auto"
>
<!-- Close Button -->
<button
@click="emit('close')"
class="absolute top-4 right-4 p-2 rounded-lg text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors cursor-pointer"
aria-label="Close Modal"
>
<X class="w-5 h-5" />
</button>
<div class="flex items-center gap-2.5 mb-6">
<div class="p-2 rounded-lg bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400">
<Shield class="w-5 h-5" />
</div>
<h3 class="text-xl font-bold text-slate-900 dark:text-white">
Legal Notice (Impressum)
</h3>
</div>
<div class="space-y-4 text-xs sm:text-sm text-slate-600 dark:text-slate-300 leading-relaxed">
<div>
<h4 class="font-bold text-slate-900 dark:text-white mb-1">Information according to § 5 TMG</h4>
<p>Moritz Graf</p>
<p>Prinz-Ludwig-Str. 4a</p>
<p>93055 Regensburg, Germany</p>
</div>
<div>
<h4 class="font-bold text-slate-900 dark:text-white mb-1">Contact</h4>
<p>Email: <a href="mailto:contact-moritz@moritzgraf.de" class="text-blue-600 dark:text-blue-400 underline">contact-moritz@moritzgraf.de</a></p>
<p>Website: <a href="https://moritzgraf.de" class="text-blue-600 dark:text-blue-400 underline">https://moritzgraf.de</a></p>
</div>
<div>
<h4 class="font-bold text-slate-900 dark:text-white mb-1">Responsible for Content (§ 55 Abs. 2 RStV)</h4>
<p>Moritz Graf</p>
<p>Prinz-Ludwig-Str. 4a, 93055 Regensburg</p>
</div>
<div>
<h4 class="font-bold text-slate-900 dark:text-white mb-1">Disclaimer</h4>
<p class="text-slate-500 dark:text-slate-400 text-xs">
This personal website presents professional capabilities, skills, and engineering philosophies. All trademarks, service marks, and company names cited are the property of their respective owners. Self-hosted on private Kubernetes infrastructure.
</p>
</div>
</div>
<div class="mt-8 pt-4 border-t border-slate-200 dark:border-slate-800 text-right">
<button
@click="emit('close')"
class="px-4 py-2 rounded-lg text-xs font-semibold bg-slate-900 text-white dark:bg-white dark:text-slate-900 hover:bg-slate-800 dark:hover:bg-slate-100 transition-colors cursor-pointer"
>
Close
</button>
</div>
</div>
</div>
</template>
@@ -0,0 +1,149 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Sun, Moon, Menu, X, ArrowUpRight } from 'lucide-vue-next'
const isDark = ref(false)
const isMobileMenuOpen = ref(false)
const toggleDarkMode = () => {
isDark.value = !isDark.value
if (isDark.value) {
document.documentElement.classList.add('dark')
localStorage.setItem('theme', 'dark')
} else {
document.documentElement.classList.remove('dark')
localStorage.setItem('theme', 'light')
}
}
onMounted(() => {
const savedTheme = localStorage.getItem('theme')
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
isDark.value = true
document.documentElement.classList.add('dark')
} else {
isDark.value = false
document.documentElement.classList.remove('dark')
}
})
const navLinks = [
{ name: 'Overview', href: '#overview' },
{ name: 'Capabilities', href: '#capabilities' },
{ name: 'Skills & Tech', href: '#skills' },
{ name: 'Principles', href: '#principles' },
{ name: 'Contact', href: '#contact' },
]
const scrollTo = (href: string) => {
isMobileMenuOpen.value = false
const element = document.querySelector(href)
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}
}
</script>
<template>
<header class="sticky top-0 z-50 w-full border-b border-slate-200/80 dark:border-slate-800/80 bg-white/80 dark:bg-slate-950/80 backdrop-blur-md">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<!-- Brand / Identity -->
<a href="#overview" class="flex items-center space-x-3 group text-left">
<div class="w-9 h-9 rounded-lg bg-gradient-to-tr from-blue-600 to-indigo-600 flex items-center justify-center text-white font-bold text-base shadow-sm group-hover:shadow transition-all">
MG
</div>
<div>
<div class="font-bold text-slate-900 dark:text-white tracking-tight leading-none group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
Moritz Graf
</div>
<div class="text-xs text-slate-500 dark:text-slate-400 font-medium">
Cloud Platform & Infrastructure
</div>
</div>
</a>
<!-- Desktop Navigation -->
<nav class="hidden md:flex items-center space-x-1 lg:space-x-2">
<button
v-for="link in navLinks"
:key="link.name"
@click="scrollTo(link.href)"
class="px-3 py-2 rounded-md text-sm font-medium text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-900 transition-colors cursor-pointer"
>
{{ link.name }}
</button>
</nav>
<!-- Right Side: Dark Mode Toggle & Direct Connect -->
<div class="hidden md:flex items-center space-x-3">
<!-- Theme switch button -->
<button
@click="toggleDarkMode"
type="button"
class="p-2 rounded-lg text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-900 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer"
:title="isDark ? 'Switch to Light Mode' : 'Switch to Dark Mode'"
aria-label="Toggle Theme"
>
<Sun v-if="isDark" class="w-5 h-5 text-amber-400 transition-transform rotate-0 scale-100" />
<Moon v-else class="w-5 h-5 text-slate-600 transition-transform -rotate-90 scale-100" />
</button>
<a
href="mailto:contact-moritz@moritzgraf.de"
class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-semibold bg-slate-900 text-white dark:bg-white dark:text-slate-900 hover:bg-slate-800 dark:hover:bg-slate-100 shadow-sm transition-all"
>
<span>Get in touch</span>
<ArrowUpRight class="w-3.5 h-3.5" />
</a>
</div>
<!-- Mobile Menu Trigger -->
<div class="flex md:hidden items-center space-x-2">
<button
@click="toggleDarkMode"
type="button"
class="p-2 rounded-lg text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-900"
aria-label="Toggle Theme"
>
<Sun v-if="isDark" class="w-5 h-5 text-amber-400" />
<Moon v-else class="w-5 h-5 text-slate-600" />
</button>
<button
@click="isMobileMenuOpen = !isMobileMenuOpen"
type="button"
class="p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-900"
aria-label="Toggle Navigation Menu"
>
<X v-if="isMobileMenuOpen" class="w-6 h-6" />
<Menu v-else class="w-6 h-6" />
</button>
</div>
</div>
</div>
<!-- Mobile Dropdown Menu -->
<div
v-if="isMobileMenuOpen"
class="md:hidden border-b border-slate-200 dark:border-slate-800 bg-white/95 dark:bg-slate-950/95 backdrop-blur-md px-4 pt-2 pb-4 space-y-1"
>
<button
v-for="link in navLinks"
:key="link.name"
@click="scrollTo(link.href)"
class="block w-full text-left px-3 py-2 rounded-md text-base font-medium text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-900"
>
{{ link.name }}
</button>
<div class="pt-2 border-t border-slate-100 dark:border-slate-800 mt-2">
<a
href="mailto:contact-moritz@moritzgraf.de"
class="block text-center w-full px-4 py-2 rounded-lg text-sm font-semibold bg-blue-600 text-white hover:bg-blue-700 shadow-sm"
>
Contact Moritz
</a>
</div>
</div>
</header>
</template>
@@ -0,0 +1,202 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { skills } from '@/data/portfolioData'
import type { SkillCategory } from '@/types'
import { Search, X, Layers, Star } from 'lucide-vue-next'
const searchQuery = ref('')
const selectedCategory = ref<SkillCategory>('all')
const categories: { id: SkillCategory; label: string }[] = [
{ id: 'all', label: 'All Technologies' },
{ id: 'cloud', label: 'Cloud & Multi-Cloud' },
{ id: 'k8s', label: 'Kubernetes & CNCF' },
{ id: 'iac', label: 'IaC & Automation' },
{ id: 'systems', label: 'Systems & Networks' },
{ id: 'data', label: 'Data & Observability' },
{ id: 'leadership', label: 'Leadership & Agile' },
{ id: 'iot', label: 'Hardware & IoT' },
]
const filteredSkills = computed(() => {
const query = searchQuery.value.toLowerCase().trim()
return skills.filter((skill) => {
const matchesCategory =
selectedCategory.value === 'all' || skill.category === selectedCategory.value
const matchesSearch =
!query ||
skill.name.toLowerCase().includes(query) ||
skill.description.toLowerCase().includes(query) ||
skill.tags.some((tag) => tag.toLowerCase().includes(query))
return matchesCategory && matchesSearch
})
})
const clearSearch = () => {
searchQuery.value = ''
selectedCategory.value = 'all'
}
const getCategoryBadgeClass = (category: string) => {
switch (category) {
case 'cloud':
return 'bg-sky-50 text-sky-700 border-sky-200 dark:bg-sky-950/50 dark:text-sky-300 dark:border-sky-800'
case 'k8s':
return 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/50 dark:text-blue-300 dark:border-blue-800'
case 'iac':
return 'bg-indigo-50 text-indigo-700 border-indigo-200 dark:bg-indigo-950/50 dark:text-indigo-300 dark:border-indigo-800'
case 'systems':
return 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/50 dark:text-amber-300 dark:border-amber-800'
case 'data':
return 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300 dark:border-emerald-800'
case 'leadership':
return 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/50 dark:text-purple-300 dark:border-purple-800'
case 'iot':
return 'bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-950/50 dark:text-rose-300 dark:border-rose-800'
default:
return 'bg-slate-50 text-slate-700 border-slate-200 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-800'
}
}
</script>
<template>
<section id="skills" class="py-16 md:py-24 border-t border-slate-200/80 dark:border-slate-800/80 bg-slate-50/50 dark:bg-slate-900/20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Section Header -->
<div class="text-left max-w-3xl mb-10">
<div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold uppercase tracking-wider bg-indigo-50 text-indigo-700 dark:bg-indigo-950/60 dark:text-indigo-300 border border-indigo-200/80 dark:border-indigo-800/80 mb-3">
<Layers class="w-3.5 h-3.5" />
<span>Interactive Skill Matrix</span>
</div>
<h2 class="text-3xl sm:text-4xl font-extrabold text-slate-900 dark:text-white tracking-tight">
Tools, Technologies & Applied Craft
</h2>
<p class="mt-3 text-base sm:text-lg text-slate-600 dark:text-slate-300">
Search or filter across hyperscalers, container runtimes, telecom protocols, and hardware nodes.
</p>
</div>
<!-- Controls: Search & Category Pills -->
<div class="space-y-4 mb-8">
<!-- Live Search Bar -->
<div class="relative max-w-xl">
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
<Search class="w-4 h-4" />
</div>
<input
v-model="searchQuery"
type="text"
placeholder="Search technologies, tools or concepts (e.g. GCP, Istio, BGP, Ansible)..."
class="block w-full pl-10 pr-10 py-2.5 rounded-xl border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all shadow-xs"
/>
<button
v-if="searchQuery"
@click="searchQuery = ''"
class="absolute inset-y-0 right-0 pr-3.5 flex items-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 cursor-pointer"
>
<X class="w-4 h-4" />
</button>
</div>
<!-- Category Pills -->
<div class="flex flex-wrap gap-2 pt-2">
<button
v-for="cat in categories"
:key="cat.id"
@click="selectedCategory = cat.id"
class="px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer"
:class="[
selectedCategory === cat.id
? 'bg-blue-600 text-white shadow-xs'
: 'bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-750'
]"
>
{{ cat.label }}
</button>
</div>
</div>
<!-- Result Counter -->
<div class="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400 mb-6">
<div>
Showing <span class="font-semibold text-slate-800 dark:text-slate-200">{{ filteredSkills.length }}</span> technologies
</div>
<button
v-if="searchQuery || selectedCategory !== 'all'"
@click="clearSearch"
class="text-blue-600 dark:text-blue-400 hover:underline cursor-pointer"
>
Reset filters
</button>
</div>
<!-- Skills Cards Grid -->
<div v-if="filteredSkills.length > 0" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div
v-for="skill in filteredSkills"
:key="skill.name"
class="p-4 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800/80 shadow-xs hover:border-slate-300 dark:hover:border-slate-700 transition-all text-left flex flex-col justify-between"
>
<div>
<div class="flex items-start justify-between gap-2 mb-2">
<h4 class="font-bold text-sm text-slate-900 dark:text-white">
{{ skill.name }}
</h4>
<span
v-if="skill.featured"
class="inline-flex items-center gap-1 text-[10px] font-semibold text-amber-600 dark:text-amber-400"
title="Core Competency"
>
<Star class="w-3 h-3 fill-amber-500 text-amber-500" />
</span>
</div>
<p class="text-xs text-slate-600 dark:text-slate-300 leading-relaxed mb-3">
{{ skill.description }}
</p>
</div>
<div>
<!-- Category and tags -->
<div class="pt-2 border-t border-slate-100 dark:border-slate-800/80 flex flex-wrap gap-1">
<span
class="px-1.5 py-0.5 rounded text-[10px] font-mono border"
:class="getCategoryBadgeClass(skill.category)"
>
{{ skill.category }}
</span>
<span
v-for="tag in skill.tags"
:key="tag"
class="px-1.5 py-0.5 rounded text-[10px] font-mono bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400"
>
#{{ tag }}
</span>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div
v-else
class="text-center py-12 px-4 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800"
>
<p class="text-slate-600 dark:text-slate-300 font-medium text-sm">
No technologies match "{{ searchQuery }}" in this category.
</p>
<button
@click="clearSearch"
class="mt-3 px-4 py-2 rounded-lg text-xs font-semibold bg-blue-600 text-white hover:bg-blue-700 cursor-pointer"
>
Clear filters & search
</button>
</div>
</div>
</section>
</template>
@@ -0,0 +1,290 @@
import type {
Credential,
StatItem,
CapabilityDossier,
SkillItem,
Principle,
SocialLink
} from '@/types'
export const personalInfo = {
name: 'Moritz Graf',
title: 'Cloud Platform & Infrastructure Leader',
tagline: 'Architecting resilient multi-cloud foundations, scaling Kubernetes platforms, and steering enterprise carve-out migrations.',
bioSummary: 'Central Platform Engineer and Engineering Leader with over a decade of production operations experience. I specialize in building autonomous developer platforms on Google Cloud and AWS, operating Kubernetes at scale, and orchestrating complex enterprise cloud spin-offs. Grounded in low-level systems engineering, network routing (BGP, SIP), and maker craft.',
location: 'Regensburg, Germany',
email: 'contact-moritz@moritzgraf.de',
headshotUrl: '/headshot.jpg',
availabilityBadge: 'Active in Central Platform Engineering',
credentialsSummary: 'CKA · PSM II · M.Sc. Computer Science'
}
export const credentials: Credential[] = [
{
id: 'cka',
title: 'Certified Kubernetes Administrator (CKA)',
issuer: 'Cloud Native Computing Foundation (CNCF)',
badgeText: 'CKA Certified',
icon: 'ShieldCheck',
highlight: true
},
{
id: 'psm2',
title: 'Professional Scrum Master II (PSM II)',
issuer: 'Scrum.org',
badgeText: 'PSM II Advanced',
icon: 'Award',
highlight: true
},
{
id: 'msc',
title: 'M.Sc. Computer Science (Technical Systems)',
issuer: 'OTH Regensburg',
badgeText: 'M.Sc. Degree',
icon: 'GraduationCap'
},
{
id: 'psm1',
title: 'Professional Scrum Master I (PSM I)',
issuer: 'Scrum.org',
badgeText: 'PSM I',
icon: 'Award'
},
{
id: 'itil',
title: 'ITIL Foundation (v3)',
issuer: 'AXELOS',
badgeText: 'ITIL Certified',
icon: 'CheckCircle2'
},
{
id: 'lang',
title: 'UNIcert III Technical English (C1)',
issuer: 'Language Center Regensburg',
badgeText: 'C1 Fluent',
icon: 'Globe'
}
]
export const impactStats: StatItem[] = [
{
value: 'Multi-Cloud',
label: 'Hyperscaler Mastery',
description: 'Central Platform on GCP & enterprise analytics platforms on AWS'
},
{
value: 'Spin-Off',
label: 'Carve-Out Leadership',
description: 'Technical cloud separation & migration from Continental to AUMOVIO'
},
{
value: 'K8s at Scale',
label: 'CKA-Certified GitOps',
description: 'Bare-metal & cloud clusters managed via Kubespray, Helm, & Istio'
},
{
value: '12+ Years',
label: 'Infrastructure Depth',
description: 'Continuous production operations, high-availability & on-call leadership'
}
]
export const capabilityDossiers: CapabilityDossier[] = [
{
id: 'cloud-platforms',
title: 'Enterprise Multi-Cloud Platforms',
category: 'Cloud Architecture & Governance',
tagline: 'Resilient, secure, and self-service landing zones across Google Cloud and AWS.',
overview: 'Designing and maintaining centralized enterprise cloud foundations that balance developer velocity with enterprise-grade guardrails. Leading central platform strategy for Google Cloud (GCP) and enterprise analytics platforms on AWS.',
icon: 'Cloud',
keyAreas: [
'GCP Organization & Landing Zones architecture with fine-grained IAM & VPC Service Controls',
'AWS multi-account governance (Control Tower, SCPs, Transit Gateway, Direct Connect)',
'Automated tenant onboarding with self-service project/account vending machines',
'Cost governance, FinOps visibility, resource tagging, and budget alerting automation'
],
technologies: ['Google Cloud (GCP)', 'AWS', 'IAM', 'VPC Service Controls', 'Transit Gateway', 'Organizations', 'FinOps'],
impactNote: 'Powers centralized platform capabilities across hundreds of projects and thousands of internal users.'
},
{
id: 'kubernetes-cncf',
title: 'Kubernetes & Cloud-Native Ecosystem',
category: 'Container Orchestration & GitOps',
tagline: 'CKA-certified cluster architecture, service meshes, and GitOps delivery pipelines.',
overview: 'Deep hands-on expertise operating production Kubernetes clusters on bare-metal and in cloud environments. Specializing in immutable cluster provisioning, zero-trust service mesh networking, and declarative GitOps pipelines.',
icon: 'Server',
keyAreas: [
'Production cluster lifecycle provisioning via Kubespray, Ansible, and managed cloud engines',
'Service mesh routing, mutual TLS (mTLS), and traffic shaping with Istio and Traefik Ingress',
'Declarative GitOps deployment workflows using ArgoCD, Flux, and Kustomize overlays',
'Full-stack cluster observability with Prometheus, Thanos, Loki, and custom Grafana dashboards'
],
technologies: ['Kubernetes (CKA)', 'Kubespray', 'Istio', 'Traefik', 'Helm', 'Kustomize', 'ArgoCD', 'Prometheus', 'Grafana'],
impactNote: 'Maintains zero-downtime microservice environments with automated certificate lifecycle management.'
},
{
id: 'data-platforms',
title: 'Enterprise Data Lake & Analytics Enablement',
category: 'Data Engineering & Service Ownership',
tagline: 'Product & Service Ownership of global enterprise data lake platforms.',
overview: 'Led the Continental.datalake platform as Global Team Lead, Service Owner, and Product Owner. Bridging business data requirements with rock-solid infrastructure to deliver self-service analytics for data scientists and engineers.',
icon: 'Database',
keyAreas: [
'Global Service & Product Ownership of an enterprise data lake serving hundreds of data workloads',
'Scalable analytical storage pipelines leveraging Amazon S3, Redshift, Glue, and Matillion ETL',
'Data governance, cataloging, compliance, and granular data access control policies',
'High-throughput telemetry and metrics processing with real-time alerting'
],
technologies: ['Amazon Redshift', 'S3 Data Lake', 'AWS Glue', 'Matillion', 'Kafka', 'Python Data Tooling', 'Athena'],
impactNote: 'Delivered mission-critical data lake services supporting global enterprise analytics and machine learning initiatives.'
},
{
id: 'iac-automation',
title: 'Declarative Infrastructure as Code (IaC)',
category: 'Automation & Continuous Delivery',
tagline: '100% Everything as Code: repeatable, testable, and drift-resistant infrastructure.',
overview: 'Pioneer of GitOps and automated infrastructure workflows. Managing multi-region infrastructure exclusively through declarative manifests, pull-request driven automation, and automated validation tests.',
icon: 'Code2',
keyAreas: [
'Modular Terraform and Terragrunt architectures with DRY module reuse and remote state locking',
'Automated PR workflows with Atlantis, Renovatebot, and pre-commit security validation',
'Configuration management and server templating using Ansible and Packer',
'Robust CI/CD pipelines in Jenkins and GitHub Actions with automated linting and security scanning'
],
technologies: ['Terraform', 'Terragrunt', 'Atlantis', 'Ansible', 'GitHub Actions', 'Jenkins', 'Packer', 'Renovatebot'],
impactNote: 'Eliminated manual infrastructure changes, enforcing peer review and automated compliance across all environments.'
},
{
id: 'leadership-governance',
title: 'Engineering Leadership & Enterprise Spin-Offs',
category: 'People, Culture & Organizational Transformation',
tagline: 'Steering high-stakes cloud carve-outs and cultivating high-trust engineering teams.',
overview: 'Experienced team lead and PSM II Scrum Master with a proven track record of guiding engineering teams through structural corporate carve-outs. Steered the cloud infrastructure separation during the spin-off from Continental to AUMOVIO.',
icon: 'Users',
keyAreas: [
'Technical leadership of the cloud platform carve-out and migration from Continental to AUMOVIO',
'Serving as Global Team Lead and Agile Coach (PSM II) fostering psychological safety and autonomy',
'Founding and hosting technical exchange forums (DevOps Office Hours, Architecture Syncs)',
'Mentoring junior and mid-level platform engineers in infrastructure best practices'
],
technologies: ['Scrum (PSM II)', 'Carve-Out Strategy', 'Service Management', 'Team Mentorship', 'DevOps Advocacy', 'Stakeholder Management'],
impactNote: 'Executed seamless platform transition during enterprise spin-off without operational downtime.'
},
{
id: 'systems-networking-iot',
title: 'Systems Depth, Telecom Roots & Maker Craft',
category: 'Low-Level Engineering & Physical Hardware',
tagline: 'From BGP and VoIP SIP protocols to physical IoT sensors and microcontroller labs.',
overview: 'Unlike purely conceptual architects, my foundation is built on deep telecommunications engineering, Linux kernel performance tuning, and hands-on physical hardware hacking. Understanding the metal makes cloud abstractions trivial.',
icon: 'Cpu',
keyAreas: [
'Telecom infrastructure: SIP redirect servers, dynamic least-cost routing, ISDN, and Cisco ASR platforms',
'Enterprise networking: BGP routing and troubleshooting, Fortinet firewalls, DMZ segmentation',
'Deep Linux performance engineering and kernel diagnostics using atop, eBPF, and system tools',
'Physical IoT and hardware tinkering: ESPHome firmware, BLE sensor mesh, and home automation nodes'
],
technologies: ['BGP', 'Cisco ASR', 'VoIP (SIP/ISDN)', 'Linux Internals (atop)', 'ESPHome', 'ESP32 / BLE', 'Fortinet', 'Python / Bash'],
impactNote: 'Applies deep understanding of physical networking and hardware constraints to diagnose complex distributed systems.'
}
]
export const skills: SkillItem[] = [
// Cloud & Multi-Cloud
{ name: 'Google Cloud (GCP)', category: 'cloud', tags: ['Central Platform', 'Landing Zones', 'IAM', 'VPC Service Controls'], description: 'Leading central enterprise platform architecture and organizational governance.', featured: true },
{ name: 'Amazon Web Services (AWS)', category: 'cloud', tags: ['Enterprise Scale', 'Control Tower', 'EKS', 'S3', 'Redshift'], description: 'Multi-account AWS landing zones, analytics architectures, and automation.', featured: true },
{ name: 'Multi-Cloud Architecture', category: 'cloud', tags: ['Enterprise Strategy', 'Hybrid Cloud', 'Zero-Trust'], description: 'Designing unified identity and network boundaries across disparate providers.' },
{ name: 'FinOps & Cost Governance', category: 'cloud', tags: ['Budgets', 'Tagging Enforcement', 'Cost Optimization'], description: 'Algorithmic cost allocation, rightsizing, and spend visibility.' },
// Kubernetes & CNCF
{ name: 'Kubernetes (CKA)', category: 'k8s', tags: ['Certified', 'Cluster Architecture', 'Production Operations'], description: 'Certified Kubernetes Administrator with bare-metal and cloud cluster expertise.', featured: true },
{ name: 'Kubespray & Kubeadm', category: 'k8s', tags: ['Ansible', 'Bare Metal', 'Cluster Lifecycle'], description: 'Automated cluster bootstrapping, OS hardening, and zero-downtime upgrades.', featured: true },
{ name: 'Istio Service Mesh', category: 'k8s', tags: ['mTLS', 'Traffic Management', 'Security'], description: 'Zero-trust networking, traffic splitting, and service-to-service encryption.' },
{ name: 'Traefik & Ingress-Nginx', category: 'k8s', tags: ['Ingress Controllers', 'Let\'s Encrypt', 'Cert-Manager'], description: 'Edge routing, automatic SSL/TLS certificate lifecycle, and header mutation.' },
{ name: 'Helm & Kustomize', category: 'k8s', tags: ['Package Management', 'Overlays', 'GitOps'], description: 'Declarative workload templating across staging and production clusters.' },
{ name: 'ArgoCD & Flux', category: 'k8s', tags: ['GitOps', 'Continuous Deployment', 'Reconciliation'], description: 'Pull-based deployment engines enforcing cluster state synchronization.' },
// IaC & Automation
{ name: 'Terraform', category: 'iac', tags: ['HCL', 'State Management', 'Provider Authoring'], description: 'Declarative cloud provisioning with reusable, modular design patterns.', featured: true },
{ name: 'Terragrunt', category: 'iac', tags: ['DRY Modules', 'Multi-Account', 'State Locking'], description: 'Orchestrating complex multi-account and multi-region infrastructure hierarchies.', featured: true },
{ name: 'Atlantis', category: 'iac', tags: ['GitOps for IaC', 'PR Workflows', 'Automated Plans'], description: 'PR-based Terraform execution ensuring full transparency and compliance.' },
{ name: 'Ansible', category: 'iac', tags: ['Playbooks', 'Roles', 'Configuration Management'], description: 'Server provisioning, fleet configuration, and Kubespray automation.' },
{ name: 'GitHub Actions & Jenkins', category: 'iac', tags: ['CI/CD Pipelines', 'Automated Testing', 'Docker Builds'], description: 'Building resilient automated pipelines for container builds and lint checks.' },
{ name: 'Python (Scripting & Boto3)', category: 'iac', tags: ['Automation', 'CLI Tooling', 'AWS SDK'], description: 'Custom cloud automation scripts, CLI utilities, and API wrappers.' },
// Systems & Networking
{ name: 'BGP & IP Routing', category: 'systems', tags: ['Border Gateway Protocol', 'Troubleshooting', 'Autonomous Systems'], description: 'Dynamic routing protocols, peering troubleshooting, and route reflection.', featured: true },
{ name: 'VoIP, SIP & ISDN', category: 'systems', tags: ['Telecom', 'Least Cost Routing', 'Cirpack', 'Oracle SBC'], description: 'Architected SIP redirect servers and enterprise telephony carrier routing.', featured: true },
{ name: 'Cisco ASR & Switching', category: 'systems', tags: ['Hardware Routers', 'VLANs', 'Core Infrastructure'], description: 'Carrier-grade hardware router operation, interface diagnostics, and QoS.' },
{ name: 'Linux Performance & Internals', category: 'systems', tags: ['atop', 'eBPF', 'Kernel Diagnostics', 'Troubleshooting'], description: 'Deep performance analysis, memory leak isolation, and system optimization.' },
{ name: 'Network Security & Firewalls', category: 'systems', tags: ['Fortinet', 'DMZ Design', 'Security Zones'], description: 'Designing perimeter security zones, DMZ isolation, and packet filtering.' },
// Data & Observability
{ name: 'Enterprise Data Lake', category: 'data', tags: ['Product Owner', 'Service Owner', 'Architecture'], description: 'End-to-end service management of enterprise analytics and data pipelines.', featured: true },
{ name: 'Amazon Redshift & S3', category: 'data', tags: ['Columnar Storage', 'Data Warehousing', 'Query Optimization'], description: 'Analytical storage layers and performance tuning for big data analytics.' },
{ name: 'Prometheus & Grafana', category: 'data', tags: ['PromQL', 'Metrics', 'Dashboards', 'Alertmanager'], description: 'Comprehensive observability stacks and custom operational dashboards.' },
{ name: 'Loki & ELK Stack', category: 'data', tags: ['Log Aggregation', 'LogQL', 'Elasticsearch'], description: 'Centralized log aggregation, fast querying, and audit retention.' },
{ name: 'Matillion & ETL', category: 'data', tags: ['Pipelines', 'Transformation', 'Data Loading'], description: 'Automating high-volume data transforms and warehouse ingestion.' },
// Leadership & Agile
{ name: 'Global Team Leadership', category: 'leadership', tags: ['People Management', 'Platform Strategy', 'Hiring'], description: 'Leading cross-functional distributed platform engineering teams.', featured: true },
{ name: 'Enterprise Spin-Off Management', category: 'leadership', tags: ['Carve-Out', 'Separation', 'Migration Strategy'], description: 'Successfully governed IT infrastructure carve-out from Continental to AUMOVIO.', featured: true },
{ name: 'Professional Scrum Master (PSM II)', category: 'leadership', tags: ['Agile Coaching', 'Scrum.org', 'Continuous Improvement'], description: 'Advanced Scrum Master fostering psychological safety and iterative delivery.', featured: true },
{ name: 'Service & Product Ownership', category: 'leadership', tags: ['ITIL', 'SLAs / SLOs', 'Stakeholder Alignment'], description: 'Full lifecycle responsibility for enterprise platforms and user satisfaction.' },
{ name: 'Community & Office Hours', category: 'leadership', tags: ['DevOps Advocacy', 'Knowledge Sharing', 'Mentorship'], description: 'Founded internal technical office hours to bridge platform teams and users.' },
// Hardware & IoT
{ name: 'ESPHome & Microcontrollers', category: 'iot', tags: ['ESP32', 'Firmware', 'Sensor Networks'], description: 'Building custom firmware for environmental sensors and smart controllers.', featured: true },
{ name: 'BLE & Sensor Telemetry', category: 'iot', tags: ['Bluetooth LE', 'Soil/Plant Monitors', 'Telemetry'], description: 'Low-power wireless telemetry pipelines bridging edge devices to MQTT.' },
{ name: 'Self-Hosted Home Lab', category: 'iot', tags: ['Bare-Metal K8s', 'Storage', 'Home Automation'], description: 'Private infrastructure testing ground running production-grade workloads.' }
]
export const engineeringPrinciples: Principle[] = [
{
title: 'Declarative Everything',
statement: 'If it is not written in code and version-controlled, it does not exist.',
explanation: 'From cloud landing zones and Kubernetes manifests to firewall policies and firmware: every single component is defined declaratively, reviewed via pull requests, and deployed through automated GitOps pipelines.',
icon: 'FileCode'
},
{
title: 'Self-Service over Gatekeeping',
statement: 'A platform team is a product team whose customers are developers.',
explanation: 'Great platform engineering does not create bureaucratic approval tickets. It provides secure, automated guardrails, golden paths, and self-service APIs so product developers can move fast with complete confidence.',
icon: 'Zap'
},
{
title: 'Ground-Up Systems Thinking',
statement: 'Higher-level cloud abstractions always rest on physical networks and kernels.',
explanation: 'Troubleshooting distributed microservices becomes effortless when you truly understand BGP routing, TCP handshake latency, Linux memory paging, and hardware constraints. Real-world reliability requires full-stack depth.',
icon: 'Layers'
},
{
title: 'Pragmatic & Sustainable Agility',
statement: 'Scrum exists to empower engineers, not to worship dogma.',
explanation: 'As a PSM II Scrum Master, I treat agility as a tool to protect team focus, foster psychological safety, align on clear goals, and eliminate friction so high-performing engineers can do their best work sustainably.',
icon: 'HeartHandshake'
}
]
export const socialLinks: SocialLink[] = [
{
platform: 'LinkedIn',
label: 'Connect on LinkedIn',
url: 'https://www.linkedin.com/in/moritz-graf-92abaa91/',
icon: 'Linkedin',
handle: 'moritz-graf-92abaa91'
},
{
platform: 'GitHub',
label: 'Follow on GitHub',
url: 'https://github.com/iptizer',
icon: 'Github',
handle: 'iptizer'
},
{
platform: 'Email',
label: 'Email Moritz',
url: 'mailto:contact-moritz@moritzgraf.de',
icon: 'Mail',
handle: 'contact-moritz@moritzgraf.de'
}
]
+5
View File
@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
+38
View File
@@ -0,0 +1,38 @@
@import "tailwindcss";
@layer base {
html {
scroll-behavior: smooth;
font-family: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
code, pre, .font-mono {
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
}
/* Smooth transitions for theme switching */
body, nav, header, section, footer, div, button, input {
transition-property: background-color, border-color, color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 7px;
height: 7px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.3);
border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(100, 116, 139, 0.5);
}
+59
View File
@@ -0,0 +1,59 @@
export interface Credential {
id: string
title: string
issuer: string
badgeText: string
icon: string
highlight?: boolean
}
export interface StatItem {
value: string
label: string
description: string
}
export interface CapabilityDossier {
id: string
title: string
category: string
tagline: string
overview: string
icon: string
keyAreas: string[]
technologies: string[]
impactNote: string
}
export type SkillCategory =
| 'all'
| 'cloud'
| 'k8s'
| 'iac'
| 'systems'
| 'data'
| 'leadership'
| 'iot'
export interface SkillItem {
name: string
category: Exclude<SkillCategory, 'all'>
tags: string[]
description: string
featured?: boolean
}
export interface Principle {
title: string
statement: string
explanation: string
icon: string
}
export interface SocialLink {
platform: string
label: string
url: string
icon: string
handle: string
}