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: "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" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user