Add Berufserfahrung Timeline section to portfolio

Task: #2 — Berufserfahrung Timeline

## What was done
- Created new `experience.tsx` component with a vertical alternating timeline
- Implemented 5 career stations from the PDF profile:
  1. Senior IT-Consultant @ Landesamt für Statistik Bayern (01/2024–heute)
  2. Technischer Redakteur @ Polizei Hessen (04/2024–12/2024)
  3. Projektkoordinator @ Justiz Baden-Württemberg (04/2023–10/2023)
  4. Senior IT-Systems Engineer @ Amt d. öff. Rechts Hamburg (10/2020–04/2023)
  5. Senior IT-Consultant @ Finanzdienstleister München (01/2015–06/2020)
- Each station shows: period, role, client, 4–5 task bullets
- Visual distinction between Behörde (blue, Landmark icon) and Konzern (violet, Building2 icon)
- Legend badges at top of section for type color coding
- Framer Motion scroll-in animations consistent with other sections
- Responsive: single-column on mobile, alternating left/right on desktop
- Added section to `home.tsx` after Bio, with `id="experience"`
- Added "Erfahrung" nav link to `navbar.tsx` (both desktop and mobile menu)

## Deviations
- None. All 5 required stations implemented with 3–5 bullets each as specified.
This commit is contained in:
joachimhummel
2026-05-15 15:47:11 +00:00
parent 9973439b72
commit 8933ab9067
3 changed files with 209 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
import { motion } from "framer-motion";
import { Building2, Landmark } from "lucide-react";
type Station = {
period: string;
role: string;
client: string;
type: "behoerde" | "konzern";
tasks: string[];
};
const stations: Station[] = [
{
period: "01/2024 heute",
role: "Senior IT-Consultant",
client: "Landesamt für Statistik Bayern, München",
type: "behoerde",
tasks: [
"Installation, Konfiguration & 3rd-Level-Support von Apache, Tomcat, JBoss",
"Einführung und Betrieb von Docker, Ansible und Artifactory",
"Konzeption und Aufbau JMX-Monitoring mit Grafana",
"Erstellung ITIL-konformer Betriebshandbücher & Betriebsführungskonzepte",
"Migration von Java-Anwendungen via Artifactory & Bitbucket",
],
},
{
period: "04/2024 12/2024",
role: "Technischer Redakteur",
client: "Polizei Hessen",
type: "behoerde",
tasks: [
"Betriebsführungskonzepte nach ITIL & BSI-Grundschutz für kritische Anwendungen",
"Betriebshandbücher für behördenkritische Systeme",
"Architekturdiagramme & Prozessvisualisierungen mit draw.io",
"Qualitätssicherungsmaßnahmen und Confluence/Jira-Betrieb",
],
},
{
period: "04/2023 10/2023",
role: "Projektkoordinator",
client: "Amt des öffentlichen Rechts, Justiz Baden-Württemberg",
type: "behoerde",
tasks: [
"Betrieb und Support der eAkte-Infrastruktur der Justiz",
"Einführung von Confluence & Jira als Workflow-Tool",
"Dokumentation von Prozessabläufen & Changemanagement",
"Übergreifende Koordination mit IuK, Datenbanken & virtuellen Servern",
],
},
{
period: "10/2020 04/2023",
role: "Senior IT-Systems Engineer",
client: "Amt des öffentlichen Rechts, Hamburg",
type: "behoerde",
tasks: [
"Linux / S390 / Docker-Betrieb von Video- & Messaging-Anwendungen",
"Deployment via Ansible & Gitlab CI/CD",
"Bundesweite Betriebshandbücher & Qualitätssicherung",
"Fachliche Unterstützung für Behörden, Justiz, Bundeswehr & Kanzleramt",
],
},
{
period: "01/2015 06/2020",
role: "Senior IT-Consultant",
client: "Großkonzern Finanzdienstleistung, München (EZB-geprüft)",
type: "konzern",
tasks: [
"Apache, Tomcat, JBoss auf AIX / Solaris / Docker — 3rd-Level-Support",
"Clustering-Betrieb für Webserver & J2EE-Anwendungen",
"Betriebsrichtlinien für Webserver (EZB-Compliance-Prüfung)",
"Migration von Java-Anwendungen via Harvest & Gitlab",
],
},
];
const typeConfig = {
behoerde: {
label: "Behörde",
icon: Landmark,
badge: "bg-blue-50 text-blue-700 border-blue-200",
dot: "bg-blue-500",
line: "border-blue-200",
},
konzern: {
label: "Konzern",
icon: Building2,
badge: "bg-violet-50 text-violet-700 border-violet-200",
dot: "bg-violet-500",
line: "border-violet-200",
},
};
export function Experience() {
return (
<div className="py-24">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.6 }}
>
<div className="text-center max-w-2xl mx-auto mb-16">
<p className="section-number mb-3" data-testid="text-section-label-experience">
Berufserfahrung
</p>
<h2
className="text-3xl md:text-4xl font-bold text-foreground mb-4"
data-testid="text-section-title-experience"
>
Projekte in Behörden & Konzernen
</h2>
<p className="text-muted-foreground text-lg">
Über 25 Jahre in produktionskritischen Umgebungen von Bundesbehörden über Landesämter bis zum EZB-geprüften Finanzkonzern.
</p>
</div>
<div className="flex gap-4 justify-center mb-10">
{(["behoerde", "konzern"] as const).map((type) => {
const cfg = typeConfig[type];
return (
<span
key={type}
className={`inline-flex items-center gap-1.5 px-3 py-1 text-xs font-semibold rounded-full border ${cfg.badge}`}
>
<span className={`w-2 h-2 rounded-full ${cfg.dot}`} />
{cfg.label}
</span>
);
})}
</div>
<div className="relative max-w-3xl mx-auto">
<div className="absolute left-6 top-0 bottom-0 w-px bg-border md:left-1/2" />
{stations.map((station, index) => {
const cfg = typeConfig[station.type];
const Icon = cfg.icon;
const isLeft = index % 2 === 0;
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5, delay: index * 0.08 }}
className={`relative flex gap-6 mb-10 md:mb-12 ${
isLeft ? "md:flex-row" : "md:flex-row-reverse"
}`}
data-testid={`card-experience-${index}`}
>
<div className="absolute left-6 -translate-x-1/2 md:left-1/2 z-10">
<div
className={`w-4 h-4 rounded-full border-2 border-white shadow-sm ${cfg.dot}`}
/>
</div>
<div className={`pl-14 md:pl-0 w-full md:w-[calc(50%-2rem)] ${isLeft ? "md:pr-8 md:text-right" : "md:pl-8"}`}>
<div
className={`p-5 rounded-2xl bg-white border border-border card-hover`}
>
<div className={`flex items-start gap-3 mb-3 ${isLeft ? "md:flex-row-reverse" : ""}`}>
<div className={`w-8 h-8 rounded-lg border flex items-center justify-center shrink-0 ${cfg.badge}`}>
<Icon className="w-4 h-4" />
</div>
<div className={isLeft ? "md:text-right" : ""}>
<p className="text-xs font-semibold text-muted-foreground mb-0.5">
{station.period}
</p>
<p className="text-sm font-bold text-foreground leading-tight">
{station.role}
</p>
</div>
</div>
<p className={`text-xs font-medium text-primary mb-3 ${isLeft ? "md:text-right" : ""}`}>
{station.client}
</p>
<ul className={`space-y-1.5 ${isLeft ? "md:text-right" : ""}`}>
{station.tasks.map((task, tIndex) => (
<li
key={tIndex}
className={`text-xs text-muted-foreground leading-relaxed flex items-start gap-2 ${
isLeft ? "md:flex-row-reverse" : ""
}`}
data-testid={`item-experience-${index}-${tIndex}`}
>
<span className={`w-1 h-1 rounded-full bg-muted-foreground/40 mt-1.5 shrink-0`} />
{task}
</li>
))}
</ul>
</div>
</div>
</motion.div>
);
})}
</div>
</motion.div>
</div>
);
}

View File

@@ -16,6 +16,7 @@ export function Navbar() {
{ name: "Stärken", href: "#strengths" }, { name: "Stärken", href: "#strengths" },
{ name: "Projekte", href: "#projects" }, { name: "Projekte", href: "#projects" },
{ name: "Über mich", href: "#bio" }, { name: "Über mich", href: "#bio" },
{ name: "Erfahrung", href: "#experience" },
{ name: "Kontakt", href: "#contact" }, { name: "Kontakt", href: "#contact" },
]; ];

View File

@@ -3,6 +3,7 @@ import { Competencies } from "@/components/competencies";
import { Projects } from "@/components/projects"; import { Projects } from "@/components/projects";
import { Strengths } from "@/components/strengths"; import { Strengths } from "@/components/strengths";
import { Bio } from "@/components/bio"; import { Bio } from "@/components/bio";
import { Experience } from "@/components/experience";
import { Contact } from "@/components/contact"; import { Contact } from "@/components/contact";
import { Navbar } from "@/components/navbar"; import { Navbar } from "@/components/navbar";
@@ -32,6 +33,10 @@ export default function Home() {
<Bio /> <Bio />
</section> </section>
<section id="experience" className="mt-8">
<Experience />
</section>
<section id="contact" className="mt-8"> <section id="contact" className="mt-8">
<Contact /> <Contact />
</section> </section>