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:
@@ -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'
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user