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:
203
artifacts/joachim-portfolio/src/components/experience.tsx
Normal file
203
artifacts/joachim-portfolio/src/components/experience.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export function Navbar() {
|
||||
{ name: "Stärken", href: "#strengths" },
|
||||
{ name: "Projekte", href: "#projects" },
|
||||
{ name: "Über mich", href: "#bio" },
|
||||
{ name: "Erfahrung", href: "#experience" },
|
||||
{ name: "Kontakt", href: "#contact" },
|
||||
];
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Competencies } from "@/components/competencies";
|
||||
import { Projects } from "@/components/projects";
|
||||
import { Strengths } from "@/components/strengths";
|
||||
import { Bio } from "@/components/bio";
|
||||
import { Experience } from "@/components/experience";
|
||||
import { Contact } from "@/components/contact";
|
||||
import { Navbar } from "@/components/navbar";
|
||||
|
||||
@@ -32,6 +33,10 @@ export default function Home() {
|
||||
<Bio />
|
||||
</section>
|
||||
|
||||
<section id="experience" className="mt-8">
|
||||
<Experience />
|
||||
</section>
|
||||
|
||||
<section id="contact" className="mt-8">
|
||||
<Contact />
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user