Add complete bewerbung Next.js application

- Add all Next.js application files
- Include components, app structure, and public assets
- Remove empty bewerbung placeholder folder
- Update .gitignore for Next.js project

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 09:11:47 +01:00
co-authored by Claude Sonnet 4.5
parent 571c381718
commit 30a380cae8
48 changed files with 9038 additions and 20 deletions
+82
View File
@@ -0,0 +1,82 @@
'use client';
export default function Contact() {
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return (
<section id="contact" className="relative py-20 px-4 sm:px-6 lg:px-8 bg-bg-dark text-white">
{/* Scroll to Top Button */}
<button
onClick={scrollToTop}
className="absolute -top-6 left-1/2 -translate-x-1/2 w-12 h-12 bg-primary rounded-full flex items-center justify-center hover:bg-primary-dark transition-all shadow-lg group"
aria-label="Nach oben scrollen"
>
<svg
className="w-6 h-6 text-white group-hover:-translate-y-1 transition-transform"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M5 10l7-7m0 0l7 7m-7-7v18"></path>
</svg>
</button>
<div className="max-w-6xl mx-auto">
<h2 className="text-3xl sm:text-4xl font-bold text-center mb-16 relative inline-block w-full after:content-[''] after:block after:w-12 after:h-1 after:bg-primary after:mt-6 after:mx-auto">
Jetzt Kontakt aufnehmen
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Address */}
<div className="bg-white/5 backdrop-blur-sm rounded-lg p-8 text-center hover:bg-white/10 transition-all hover:transform hover:scale-105">
<div className="w-16 h-16 bg-primary rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h3 className="text-xl font-bold mb-2">Adresse</h3>
<p className="text-white/80">
Rosenstraße 4<br />
91564 Neuendettelsau
</p>
</div>
{/* Phone */}
<div className="bg-white/5 backdrop-blur-sm rounded-lg p-8 text-center hover:bg-white/10 transition-all hover:transform hover:scale-105">
<div className="w-16 h-16 bg-primary rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<h3 className="text-xl font-bold mb-2">Telefon</h3>
<a href="tel:+4915732469960" className="text-white/80 hover:text-primary transition-colors">
+49 157 32469960
</a>
</div>
{/* Email */}
<div className="bg-white/5 backdrop-blur-sm rounded-lg p-8 text-center hover:bg-white/10 transition-all hover:transform hover:scale-105">
<div className="w-16 h-16 bg-primary rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-xl font-bold mb-2">Email</h3>
<a
href="mailto:froehlich.sebastian@gmail.com?subject=Antwort - Ihre Bewerbung"
className="text-white/80 hover:text-primary transition-colors break-all"
>
froehlich.sebastian@gmail.com
</a>
</div>
</div>
</div>
</section>
);
}
+42
View File
@@ -0,0 +1,42 @@
interface DownloadSectionProps {
title: string;
subtitle: string;
buttonText: string;
href: string;
backgroundImage: string;
}
export default function DownloadSection({
title,
subtitle,
buttonText,
href,
backgroundImage,
}: DownloadSectionProps) {
return (
<section
className="relative py-24 px-4 sm:px-6 lg:px-8 bg-cover bg-center bg-fixed"
style={{ backgroundImage: `url(${backgroundImage})` }}
>
{/* Overlay */}
<div className="absolute inset-0 bg-black/60" />
<div className="relative z-10 max-w-4xl mx-auto text-center">
<h2 className="text-4xl sm:text-5xl font-bold text-white mb-4">
{title}
</h2>
<p className="text-xl sm:text-2xl text-white/90 mb-8">
{subtitle}
</p>
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="inline-block px-10 py-4 bg-primary text-white font-bold text-lg rounded-md hover:bg-primary-dark transition-all transform hover:scale-105 shadow-lg hover:shadow-xl"
>
{buttonText}
</a>
</div>
</section>
);
}
+256
View File
@@ -0,0 +1,256 @@
'use client';
import { useState } from 'react';
interface Job {
id: string;
title: string;
company: string;
location: string;
period: string;
description: string;
responsibilities: string[];
}
const jobs: Job[] = [
{
id: 'job1',
title: 'Technischer Product Owner',
company: 'IT im Bereich Finanzen und Banken',
location: 'Regulatory & Risk, Nürnberg',
period: 'Juni 2021 - heute',
description: 'Technischer Product Owner in einem Nürnberger Unternehmen, welches die komplette Banken IT steuert.',
responsibilities: [
'Sicherstellung des Betriebs',
'Planung und Priorisierung der Ressourcen & Aufgaben',
'Technische Kommunikation und Architektur',
'Migration der Infrastruktur K8s on prem & Cloud',
'Aufbau neuer Services',
],
},
{
id: 'job2',
title: 'Product Owner',
company: 'Mobile Applikationen Unternehmen',
location: 'Nürnberg',
period: 'Februar 2019 - März 2020',
description: 'Product Owner in einem Nürnberger Unternehmen, welches mobile Applikationen zur Digitalisierung von Prozessen herstellt.',
responsibilities: [
'Abklärung der Anforderungen zwischen Stakeholdern und Entwicklern',
'Planung und Priorisierung der Ressourcen & Aufgaben',
'Technische Kommunikation und Architektur',
'Migration der bestehenden Infrastruktur in die MagentaCloud (Kubernetes)',
'Weiterentwicklung des Produktes',
],
},
{
id: 'job3',
title: 'Leiter IT',
company: 'Unternehmen im Bereich Fahrzeugtechnik',
location: 'Georgensgmünd',
period: 'März 2014 - Januar 2019',
description: 'Leiter einer IT-Abteilung in einem mittelfränkischen Unternehmen aus dem Bereich Fahrzeugtechnik.',
responsibilities: [
'Programmierung & Weiterentwicklung der Online Shops',
'Anbindung der Warenwirtschaft',
'Aufbau und Entwicklung eines Intranets',
'Architektur der Datenbanken',
'API Schnittstellenprogrammierung',
'Migration/Neuaufbau und Entwicklung der Linux Server',
],
},
{
id: 'job4',
title: 'Marketing Manager',
company: 'Trustec Energy GmbH',
location: 'Solarbranche, Mittelfranken',
period: 'Januar 2010 - März 2014',
description: 'Marketing Manager in einem Unternehmen der Solarbranche mit Verantwortung für die komplette digitale Präsenz und Marketing-Kommunikation.',
responsibilities: [
'Aufbau, Programmierung und Pflege der Firmenwebseiten (trustec-energy.eu, mhm-marketing.com) mit Joomla CMS',
'Entwicklung und Programmierung neuer CMS-Inhaltstypen und Module',
'E-Mail Marketing: Erstellung, Versand und Erfolgsmessung von Newslettern und Messe-Respondern',
'Print Design: Kataloge, Preislisten, Flyer, Rollups und Messestände (InDesign, Photoshop, Illustrator)',
'Erstellung von 3D Produkt Mock-ups und Landing Pages',
'Web Analytics mit Google Analytics und Piwik',
'Koordination externer Dienstleister (Grafiker, Übersetzer, Messebauer)',
'IT-Support: Betreuung Windows Server 2008, CRM-Software und Telefonanlage',
],
},
{
id: 'job5',
title: 'Freelancer Web Development & Design',
company: 'Selbstständig',
location: 'Verschiedene Branchen',
period: 'Januar 2000 - Dezember 2010',
description: 'Über 10 Jahre selbstständig für Unternehmenskunden aus unterschiedlichen Branchen tätig, mit Fokus auf webbasierte Lösungen.',
responsibilities: [
'Entwicklung von Präsentations- und Buchungssystemen',
'Programmierung von CMS-Systemen und Content-Management-Lösungen',
'Aufbau und Entwicklung von E-Commerce Shops',
'Webdesign und Frontend-Entwicklung (HTML, CSS, JavaScript, Flash/Actionscript)',
'Kundenberatung und Anforderungsanalyse',
'Print Design und Corporate Design Entwicklung',
],
},
];
export default function Experience() {
const [activeJob, setActiveJob] = useState(0);
return (
<section id="experience" className="py-20 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-6xl mx-auto">
<h2 className="text-3xl sm:text-4xl font-bold text-text text-center mb-16 relative inline-block w-full after:content-[''] after:block after:w-12 after:h-1 after:bg-primary after:mt-6 after:mx-auto">
Berufserfahrung
</h2>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Job Tabs - Jetzt mit besserer Mobile-Darstellung */}
<div className="lg:col-span-1">
<div className="flex flex-col gap-3">
{jobs.map((job, index) => (
<button
key={job.id}
onClick={() => setActiveJob(index)}
className={`text-left p-4 sm:p-5 rounded-lg transition-all duration-300 ${
activeJob === index
? 'bg-primary text-white shadow-lg scale-105'
: 'bg-gray-100 text-text hover:bg-gray-200 hover:shadow-md'
}`}
>
<h3 className="font-bold text-sm sm:text-base lg:text-lg mb-1">
{job.title}
</h3>
<p className={`text-xs sm:text-sm ${activeJob === index ? 'text-white/80' : 'text-text-light'}`}>
{job.company}
</p>
<p className={`text-xs mt-1 font-semibold ${activeJob === index ? 'text-white/90' : 'text-primary'}`}>
{job.period.split(' - ')[0]}
</p>
</button>
))}
</div>
</div>
{/* Job Details */}
<div className="lg:col-span-2">
<div className="bg-bg-light rounded-lg p-6 sm:p-8 shadow-md hover:shadow-xl transition-shadow">
{/* Header */}
<div className="mb-6 pb-6 border-b border-gray-200">
<h3 className="text-xl sm:text-2xl lg:text-3xl font-bold text-text mb-3">
{jobs[activeJob].title}
</h3>
<div className="flex flex-col gap-2 mb-2">
<p className="text-base sm:text-lg font-semibold text-text-light flex items-center">
<svg className="w-5 h-5 mr-2 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
{jobs[activeJob].company}
</p>
<p className="text-sm sm:text-base text-text-light flex items-center">
<svg className="w-5 h-5 mr-2 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{jobs[activeJob].location}
</p>
</div>
<p className="text-primary font-bold text-base sm:text-lg flex items-center">
<svg className="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
{jobs[activeJob].period}
</p>
</div>
{/* Description */}
<p className="text-text-light mb-6 text-sm sm:text-base leading-relaxed">
{jobs[activeJob].description}
</p>
{/* Responsibilities */}
<h4 className="font-bold text-text mb-4 text-base sm:text-lg flex items-center">
<svg className="w-5 h-5 mr-2 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
Verantwortlichkeiten:
</h4>
<ul className="space-y-3">
{jobs[activeJob].responsibilities.map((resp, idx) => (
<li
key={idx}
className="flex items-start group hover:translate-x-1 transition-transform"
>
<svg
className="w-5 h-5 text-primary mt-0.5 mr-3 flex-shrink-0 group-hover:scale-110 transition-transform"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
<span className="text-text-light leading-relaxed text-sm sm:text-base">{resp}</span>
</li>
))}
</ul>
{/* Tags for quick tech overview */}
<div className="mt-8 pt-6 border-t border-gray-200">
<div className="flex flex-wrap gap-2">
{activeJob === 0 && (
<>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Kubernetes</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Cloud Migration</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Systemarchitektur</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Financial IT</span>
</>
)}
{activeJob === 1 && (
<>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Kubernetes</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Mobile Apps</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Stakeholder Management</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Digitalisierung</span>
</>
)}
{activeJob === 2 && (
<>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">E-Commerce</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Laravel</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Vue.js</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">API Development</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Team Leadership</span>
</>
)}
{activeJob === 3 && (
<>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Joomla</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">E-Mail Marketing</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Adobe Suite</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Google Analytics</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">3D Design</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Print Design</span>
</>
)}
{activeJob === 4 && (
<>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">HTML/CSS/JS</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">CMS Development</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">E-Commerce</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Flash/Actionscript</span>
<span className="px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">Webdesign</span>
</>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
+28
View File
@@ -0,0 +1,28 @@
export default function Footer() {
return (
<footer className="bg-bg-dark text-white py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-6xl mx-auto">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div className="text-center md:text-left">
<h3 className="text-xl font-bold mb-1">Sebastian Fröhlich</h3>
<p className="text-white/60 text-sm">Full-Stack Developer, technischer PO, Team Leader</p>
</div>
<div className="text-center md:text-right">
<p className="text-white/80 font-semibold mb-2">Nicht alles hat ein Ende...</p>
<a
href="mailto:froehlich.sebastian@gmail.com?subject=Antwort - Ihre Bewerbung"
className="inline-block px-6 py-2 bg-primary text-white font-semibold rounded-lg hover:bg-primary-dark transition-colors"
>
Jetzt Kontakt aufnehmen
</a>
</div>
</div>
<div className="mt-8 pt-8 border-t border-white/10 text-center text-white/60 text-sm">
<p>&copy; {new Date().getFullYear()} Sebastian Fröhlich. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
);
}
+81
View File
@@ -0,0 +1,81 @@
'use client';
import { useEffect, useState } from 'react';
import Image from 'next/image';
export default function Hero() {
const [scrollY, setScrollY] = useState(0);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const handleScroll = () => setScrollY(window.scrollY);
const handleResize = () => setIsMobile(window.innerWidth < 768);
handleResize(); // Initial check
window.addEventListener('scroll', handleScroll);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<section
id="hero"
className="relative min-h-screen flex items-center justify-center overflow-hidden"
>
{/* Background Image mit Next.js Image */}
<div className="absolute inset-0 w-full h-full">
<Image
src={isMobile ? "/img/bg-ich-mobile.jpg" : "/img/bg-ich-2.png"}
alt="Hero Background"
fill
priority
quality={90}
className="object-cover object-center"
style={{
transform: `translateY(${scrollY * 0.5}px)`,
}}
/>
{/* Overlay für bessere Lesbarkeit */}
<div className="absolute inset-0 bg-gradient-to-br from-slate-900/80 via-slate-800/70 to-slate-900/80" />
</div>
<div className="relative z-10 text-center px-4 sm:px-6 lg:px-8">
<div className="space-y-6 animate-fade-in">
<h1 className="text-5xl sm:text-6xl lg:text-7xl font-bold text-white tracking-wider drop-shadow-2xl">
be<span className="text-primary">Werbung</span>
</h1>
<p className="text-xl sm:text-2xl text-slate-300 max-w-2xl mx-auto drop-shadow-lg">
Die Bewerberwebseite von <br />
<em className="text-primary font-semibold">Sebastian Fröhlich</em>
</p>
</div>
{/* Scroll indicator */}
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 animate-bounce">
<button
onClick={() => document.getElementById('intro')?.scrollIntoView({ behavior: 'smooth' })}
className="w-12 h-12 rounded-full bg-primary/20 backdrop-blur-sm border-2 border-primary flex items-center justify-center hover:bg-primary/30 transition-all group"
aria-label="Scroll nach unten"
>
<svg
className="w-6 h-6 text-primary group-hover:translate-y-1 transition-transform"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</button>
</div>
</div>
</section>
);
}
+82
View File
@@ -0,0 +1,82 @@
'use client';
import { useEffect, useState } from 'react';
export default function Hero() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<section
id="hero"
className="relative min-h-screen flex items-center justify-center overflow-hidden"
>
{/* Background Layer 1 - Wiederholendes Muster - passt sich an Bildschirmhöhe an */}
<div
className="absolute inset-0 w-full h-full"
style={{
backgroundImage: 'url(/img/bg-ich-2025-background.jpg)',
backgroundRepeat: 'repeat',
backgroundSize: 'auto 100%',
backgroundPosition: 'center',
transform: `translateY(${scrollY * 0.3}px)`,
}}
/>
{/* Background Layer 2 - Portrait rechts (PNG mit Transparenz) */}
<div
className="absolute inset-0 w-full h-full"
style={{
backgroundImage: 'url(/img/bg-ich-2025-foreground.png)',
backgroundRepeat: 'no-repeat',
backgroundSize: 'auto 100%',
backgroundPosition: 'right center',
transform: `translateY(${scrollY * 0.5}px)`,
}}
/>
{/* Overlay für bessere Lesbarkeit */}
<div className="absolute inset-0 bg-gradient-to-r from-slate-900/80 via-slate-900/50 to-transparent sm:from-slate-900/70 sm:via-slate-900/40 lg:from-slate-900/60 lg:via-slate-900/20" />
{/* Content - Responsive Positionierung */}
<div className="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-full sm:max-w-2xl lg:max-w-xl space-y-6 animate-fade-in">
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-white tracking-wider drop-shadow-2xl">
be<span className="text-primary">Werbung</span>
</h1>
<p className="text-lg sm:text-xl md:text-2xl text-slate-300 drop-shadow-lg">
Die Bewerberwebseite von <br />
<em className="text-primary font-semibold">Sebastian Fröhlich</em>
</p>
</div>
</div>
{/* Scroll indicator */}
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 animate-bounce z-20">
<button
onClick={() => document.getElementById('intro')?.scrollIntoView({ behavior: 'smooth' })}
className="w-12 h-12 rounded-full bg-primary/20 backdrop-blur-sm border-2 border-primary flex items-center justify-center hover:bg-primary/30 transition-all group"
aria-label="Scroll nach unten"
>
<svg
className="w-6 h-6 text-primary group-hover:translate-y-1 transition-transform"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</button>
</div>
</section>
);
}
+258
View File
@@ -0,0 +1,258 @@
'use client';
import { useEffect, useRef, useState } from 'react';
export default function Intro() {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef<HTMLElement>(null);
useEffect(() => {
const currentRef = ref.current;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 }
);
if (currentRef) {
observer.observe(currentRef);
}
return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, []);
const highlights = [
{ value: '20+', label: 'Jahre Erfahrung' },
{ value: '10+', label: 'Jahre Freelancing' },
{ value: '9+', label: 'Mitarbeiter geführt' },
{ value: '3', label: 'Produkte verantwortet' },
];
const skills = [
'Product Owner',
'Technical Project Manager',
'Laravel',
'Vue.js',
'PHP',
'JavaScript',
'MySQL',
'MSSQL',
'Systemarchitektur',
'User Experience',
'Team Leadership',
'Compliance',
];
const timeline = [
{ year: '2021 - heute', company: 'Sopra Financial Technology GmbH', role: 'Technical Product Owner' },
{ year: '2019 - 2020', company: 'APPsolute Mobility', role: 'Product Owner & Technical Project Manager' },
{ year: '2014 - 2019', company: 'SCC Fahrzeugtechnik GmbH', role: 'Leiter IT Abteilung' },
{ year: '2010 - 2014', company: 'Trustec Energy GmbH', role: 'Marketing Manager' },
{ year: '2000 - 2010', company: 'Freelancer', role: 'Web Development & Design' },
];
return (
<section
id="intro"
ref={ref}
className={`py-20 px-4 sm:px-6 lg:px-8 bg-white transition-opacity duration-1000 ${
isVisible ? 'opacity-100' : 'opacity-0'
}`}
>
<div className="max-w-6xl mx-auto">
<h2 className="text-3xl sm:text-4xl font-bold text-text mb-12 text-center relative inline-block w-full after:content-[''] after:block after:w-12 after:h-1 after:bg-primary after:mt-6 after:mx-auto">
Über mich
</h2>
{/* Highlights Box */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12">
{highlights.map((item, index) => (
<div
key={index}
className={`bg-gradient-to-br from-primary/10 to-primary/5 rounded-lg p-6 text-center border border-primary/20 hover:border-primary/40 transition-all hover:shadow-lg ${
isVisible ? 'animate-fade-in' : 'opacity-0'
}`}
style={{ animationDelay: `${index * 100}ms` }}
>
<div className="text-3xl sm:text-4xl font-bold text-primary mb-2">
{item.value}
</div>
<div className="text-sm text-text-light font-semibold">
{item.label}
</div>
</div>
))}
</div>
{/* Main Text */}
<div className="max-w-4xl mx-auto space-y-6 text-text-light leading-relaxed text-base sm:text-lg mb-12">
<p>
Mein Name ist Sebastian Fröhlich, ich bin 47 Jahre alt und lebe zusammen mit meiner
Familie in Neuendettelsau im Landkreis Ansbach.
</p>
<p>
Von der Diskette zur Cloud durfte ich diesen spannenden und rasanten technischen
Wandel miterleben. Besonders die Themen{' '}
<strong className="text-text font-semibold">
redundante Datenerfassung, Systemarchitektur und User Experience
</strong>{' '}
haben mich schon immer besonders fasziniert.
</p>
<p>
Bereits während meiner Berufsausbildung wurde mir die Verantwortung für den
Firmenserver (Netware) übertragen.
</p>
<p>
Für die A-Socks GmbH, einem Unternehmen aus der Textilbranche entwickelte ich einen{' '}
<strong className="text-text font-semibold">
Shop (HTML/ Javascript/ Actionscript - Flash)
</strong>, gestaltete die Verkaufsverpackungen und programmierte ein automatisiertes
EAN Vergabe System.
</p>
<p>
Als Freelancer war ich über 10 Jahre für Unternehmenskunden aus unterschiedlichen
Branchen tätig. Mein Fokus lag dabei auf webbasierten Lösungen wie z.B.{' '}
<strong className="text-text font-semibold">
Präsentations- und Buchungssystemen, CMS-Systemen und Internetshops
</strong>.
</p>
<p>
Im Jahr 2010 erhielt ich die Gelegenheit meine Kompetenzen als Marketing Manager für
die Trustec Energy GmbH in der Solarbranche einzusetzen. Nach 4 Jahren erhielt ich eine
betriebsbedingte Kündigung und wechselte 2014 zur SCC Fahrzeugtechnik GmbH.
</p>
<p>
Als{' '}
<strong className="text-text font-semibold">Leiter der IT Abteilung</strong> war ich
hier fachlich und disziplinarisch für 6 Mitarbeiter und 3 Auszubildende verantwortlich.
Darüber hinaus entwickelte und programmierte ich ein eigenes Firmen-Intranet{' '}
<strong className="text-text font-semibold">
(PHP Framework: Laravel, Javascript/ Vue.js, MySql/ MSSQL)
</strong>, wartete und migrierte die Server und sorgte für ein funktionierendes Backup-
und Sicherheitssystem{' '}
<strong className="text-text font-semibold">(Master-Master Replication, BashScript)</strong>.
</p>
<p>
Die nächste Karrierestufe erreichte ich mit meinem Wechsel zu einem Startupunternehmen,
wo ich als{' '}
<strong className="text-text font-semibold">
Product Owner & Technical Project Manager
</strong>{' '}
für die Firma &quot;APPsolute Mobility&quot; tätig war. Leider konnte das Startup die gesetzten
Ziele nicht erreichen, weswegen ich 1 Jahr später zu meinem jetzigen Arbeitgeber wechselte.
</p>
<p>
Bei der{' '}
<strong className="text-text font-semibold">Sopra Financial Technology GmbH</strong> bin
ich seit 2021 als{' '}
<strong className="text-text font-semibold">Technical Product Owner</strong> im
Bereich Regulatory & Risk tätig. Ich verantworte{' '}
<strong className="text-text font-semibold">3 Fintech-Produkte</strong> im{' '}
<strong className="text-text font-semibold">Bankenmeldewesen</strong> und stelle
deren reibungslosen Betrieb unter strikten{' '}
<strong className="text-text font-semibold">regulatorischen Vorgaben</strong> sicher.
</p>
<p>
Zu meinen Kernaufgaben gehören die{' '}
<strong className="text-text font-semibold">
technische Projektleitung komplexer Migrationen
</strong>{' '}
(z.B. Kubernetes-Migration ohne Ausfallzeiten), die Steuerung{' '}
<strong className="text-text font-semibold">
internationaler Teams (inkl. Nearshore)
</strong>, Code Reviews sowie die aktive Teilnahme an{' '}
<strong className="text-text font-semibold">Cost-Savings-Programmen</strong>.
Darüber hinaus arbeite ich in den Bereichen{' '}
<strong className="text-text font-semibold">
Managed Application & Cloud Service
</strong>, wo ich neue Produkte architektonisch begleite und bei
Bieterverfahren meine Expertise einbringe.
</p>
<p className="pt-4 border-t border-gray-200">
Mit dieser Präsentation möchte ich die Gelegenheit nutzen, mich als motivierten und
kompetenten Bewerber bei Ihrem Unternehmen vorzustellen.
</p>
<p>
Nachfolgend können Sie sich einen Eindruck über mich und meine abgeschlossenen Projekte{' '}
<a
href="#portfolio"
className="text-primary hover:text-primary-dark font-semibold underline underline-offset-4 transition-colors"
>
hier
</a>{' '}
verschaffen. Gerne überzeuge ich Sie auch in einem persönlichen Gespräch von mir.
</p>
</div>
{/* Timeline */}
<div className="mb-12">
<h3 className="text-2xl font-bold text-text mb-8 text-center">
Karriere-Timeline
</h3>
<div className="max-w-3xl mx-auto">
{timeline.map((item, index) => (
<div
key={index}
className={`relative pl-8 pb-8 border-l-2 border-primary/30 last:border-transparent ${
isVisible ? 'animate-fade-in' : 'opacity-0'
}`}
style={{ animationDelay: `${(index + 4) * 100}ms` }}
>
{/* Timeline dot */}
<div className="absolute -left-[9px] top-0 w-4 h-4 rounded-full bg-primary border-4 border-white shadow-md" />
<div className="bg-bg-light rounded-lg p-6 hover:shadow-lg transition-shadow">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start mb-2">
<h4 className="text-lg font-bold text-text">{item.role}</h4>
<span className="text-sm font-semibold text-primary whitespace-nowrap">
{item.year}
</span>
</div>
<p className="text-text-light">{item.company}</p>
</div>
</div>
))}
</div>
</div>
{/* Skills Tags */}
<div className="max-w-4xl mx-auto">
<h3 className="text-2xl font-bold text-text mb-6 text-center">
Kernkompetenzen
</h3>
<div className="flex flex-wrap justify-center gap-3">
{skills.map((skill, index) => (
<span
key={index}
className={`px-4 py-2 bg-primary/10 text-primary font-semibold rounded-full text-sm border border-primary/20 hover:bg-primary hover:text-white transition-all cursor-default hover:scale-105 ${
isVisible ? 'animate-fade-in' : 'opacity-0'
}`}
style={{ animationDelay: `${(index + timeline.length + 4) * 50}ms` }}
>
{skill}
</span>
))}
</div>
</div>
</div>
</section>
);
}
+255
View File
@@ -0,0 +1,255 @@
'use client';
import { useState } from 'react';
import Image from 'next/image';
interface Project {
id: string;
title: string;
category: 'web' | 'print';
description: string;
image: string;
details: string[];
link?: string;
}
const projects: Project[] = [
{
id: 'estev',
title: 'e-stev Radsport',
category: 'web',
description: '2013 für die STEV Radport GmbH in Gunzenhausen wurde die Webseite e-stev von Grund auf neu geschaffen und für ein CMS umgesetzt.',
image: '/img/Referenz - e-stev -Web.jpg',
details: [
'Modernes Responsive Design',
'CMS-Integration für einfache Pflege',
'Produktkatalog für E-Bikes',
'Kontaktformular mit Kartenfunktion',
],
link: 'https://e-stev.de',
},
{
id: 'planet',
title: 'Planet-Touristik',
category: 'web',
description: 'Für Planet-Touristik, das Reisebüro im Nürnberger Norden, wurde die Startseite 2014 einem Redesign unterworfen.',
image: '/img/Referenz---Planet-Touristik---web.jpg',
details: [
'Redesign der Startseite',
'Optimierung der User Experience',
'Integration in bestehendes System',
'Responsive Layout',
],
},
{
id: 'handstaubsauger',
title: 'Handstaubsauger24.de',
category: 'web',
description: 'Für handstaubsauger24.de wurde ein Magento E-Commerce Template gestaltet.',
image: '/img/Referenz---handstaubsauger---web.jpg',
details: [
'Magento Theme Development',
'E-Commerce Optimierung',
'Produktfilter und Suche',
'Mobile-First Ansatz',
],
},
{
id: 'illy',
title: 'Illy Caffè Ansbach',
category: 'print',
description: '2007 für das Illy Caffe in Ansbach entstandener Trifolder als Menükarte.',
image: '/img/Referenz---Illy---Print.jpg',
details: [
'Tri-Folder Design',
'Hochwertige Druckvorlagen',
'Corporate Design Integration',
'Fotografie und Layout',
],
},
{
id: 'sz',
title: 'SZ Kirchbirlingen',
category: 'print',
description: '2008 für die SZ in Kirchbirlingen entstandener Flyer als Wurfsendung.',
image: '/img/Referenz---SZ---Print.jpg',
details: [
'DIN Lang Format',
'Wurfsendung Konzept',
'Print Design',
'Layout und Satz',
],
},
{
id: 'planet-print',
title: 'Planet-Touristik Flyer',
category: 'print',
description: '2014 wurde ein neuer Flyer für Planet Touristik geschaffen.',
image: '/img/Referenz---Planet-Touristik---print.jpg',
details: [
'DIN A5 Format',
'Verteiler und Auslage',
'Moderne Gestaltung',
'Print-Produktion',
],
},
];
export default function Portfolio() {
const [activeCategory, setActiveCategory] = useState<'all' | 'web' | 'print'>('all');
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [imageErrors, setImageErrors] = useState<Set<string>>(new Set());
const filteredProjects =
activeCategory === 'all'
? projects
: projects.filter((p) => p.category === activeCategory);
const handleImageError = (projectId: string) => {
setImageErrors(prev => new Set(prev).add(projectId));
};
return (
<section id="portfolio" className="py-20 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-7xl mx-auto">
<h2 className="text-3xl sm:text-4xl font-bold text-text text-center mb-6 relative inline-block w-full after:content-[''] after:block after:w-12 after:h-1 after:bg-primary after:mt-6 after:mx-auto">
Portfolio
</h2>
<p className="text-text-light text-center mb-12 max-w-3xl mx-auto">
Zwischen 2000 bis 2010 war ich auch als Freelancer tätig und arbeitete mit Kunden und
Agenturen zusammen. Eine Auswahl meiner Arbeiten aus diesem Zeitraum habe ich hier
zusammengestellt.
</p>
{/* Filter Buttons */}
<div className="flex justify-center gap-4 mb-12">
{(['all', 'web', 'print'] as const).map((cat) => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-6 py-3 rounded-lg font-semibold transition-all capitalize ${
activeCategory === cat
? 'bg-primary text-white shadow-lg'
: 'bg-gray-100 text-text hover:bg-gray-200'
}`}
>
{cat === 'all' ? 'Alle' : cat === 'web' ? 'Web' : 'Print'}
</button>
))}
</div>
{/* Projects Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredProjects.map((project) => (
<div
key={project.id}
className="group relative overflow-hidden rounded-lg shadow-lg hover:shadow-2xl transition-all duration-300 cursor-pointer bg-white"
onClick={() => setSelectedProject(project)}
>
<div className="relative h-64 bg-gray-200 overflow-hidden">
{!imageErrors.has(project.id) ? (
<Image
src={project.image}
alt={project.title}
fill
className="object-cover group-hover:scale-110 transition-transform duration-300"
onError={() => handleImageError(project.id)}
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-primary/20 to-primary/5 flex items-center justify-center">
<div className="text-center p-4">
<svg className="w-16 h-16 mx-auto mb-2 text-primary/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-sm text-gray-500">{project.title}</p>
</div>
</div>
)}
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end p-6">
<h3 className="text-white font-bold text-xl mb-2">{project.title}</h3>
<p className="text-white/90 text-sm line-clamp-2">{project.description}</p>
<button className="mt-4 text-primary font-semibold hover:text-white transition-colors">
Details ansehen
</button>
</div>
<div className="p-4 bg-white">
<h3 className="font-bold text-text mb-2">{project.title}</h3>
<span className="inline-block px-3 py-1 bg-primary/10 text-primary text-xs font-semibold rounded-full">
{project.category === 'web' ? 'Web Design' : 'Print Design'}
</span>
</div>
</div>
))}
</div>
</div>
{/* Modal */}
{selectedProject && (
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={() => setSelectedProject(null)}
>
<div
className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-white border-b p-4 flex justify-between items-center z-10">
<h3 className="text-2xl font-bold text-text">{selectedProject.title}</h3>
<button
onClick={() => setSelectedProject(null)}
className="text-gray-500 hover:text-text transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-6">
<div className="relative h-64 bg-gray-200 rounded-lg mb-6 overflow-hidden">
{!imageErrors.has(selectedProject.id) ? (
<Image
src={selectedProject.image}
alt={selectedProject.title}
fill
className="object-cover"
onError={() => handleImageError(selectedProject.id)}
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-primary/20 to-primary/5 flex items-center justify-center">
<div className="text-center">
<svg className="w-20 h-20 mx-auto mb-2 text-primary/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-sm text-gray-500">Bild wird geladen...</p>
</div>
</div>
)}
</div>
<p className="text-text-light mb-6">{selectedProject.description}</p>
<h4 className="font-bold text-text mb-3">Details:</h4>
<ul className="space-y-2 mb-6">
{selectedProject.details.map((detail, idx) => (
<li key={idx} className="flex items-start">
<svg className="w-5 h-5 text-primary mt-0.5 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span className="text-text-light">{detail}</span>
</li>
))}
</ul>
</div>
</div>
</div>
)}
</section>
);
}
+170
View File
@@ -0,0 +1,170 @@
'use client';
import { useEffect, useRef, useState } from 'react';
interface Skill {
name: string;
level: number; // 1-5
category: string;
}
const skillsData: Skill[] = [
// Programmiersprachen
{ name: 'PHP & FW Laravel', level: 4, category: 'Programmiersprachen' },
{ name: 'Python & Perl', level: 4, category: 'Programmiersprachen' },
{ name: 'JavaScript', level: 4, category: 'Programmiersprachen' },
{ name: 'Node.js', level: 3, category: 'Programmiersprachen' },
{ name: 'Vue.js / Next.js', level: 4, category: 'Programmiersprachen' },
{ name: 'HTML', level: 5, category: 'Programmiersprachen' },
{ name: 'CSS & Frameworks', level: 5, category: 'Programmiersprachen' },
{ name: 'Sass', level: 4, category: 'Programmiersprachen' },
// Datenbanken
{ name: 'SQL (MS/DB2/Oracle)', level: 4, category: 'Datenbanken' },
{ name: 'MySQL, MariaDB, Postgres', level: 4, category: 'Datenbanken' },
{ name: 'Redis', level: 4, category: 'Datenbanken' },
{ name: 'MongoDB', level: 3, category: 'Datenbanken' },
{ name: 'CouchDB', level: 3, category: 'Datenbanken' },
{ name: 'Firebase', level: 1, category: 'Datenbanken' },
// DevOps
{ name: 'Linux Bash Scripting', level: 4, category: 'DevOps' },
{ name: 'Git / SVN', level: 4, category: 'DevOps' },
{ name: 'Docker / Kubernetes', level: 3, category: 'DevOps' },
{ name: 'AWS', level: 3, category: 'DevOps' },
{ name: 'CI/CD Pipelines', level: 4, category: 'DevOps' },
{ name: 'Draw.io Architektur', level: 4, category: 'DevOps' },
// Grafik & Design
{ name: 'Adobe Suite (u.a. PS/XD)', level: 4, category: 'Grafik & Design' },
{ name: 'Affinity', level: 4, category: 'Grafik & Design' },
{ name: 'FinalCut', level: 4, category: 'Grafik & Design' },
];
const categories = [
'Programmiersprachen',
'Datenbanken',
'DevOps',
'Grafik & Design',
];
export default function Skills() {
const [activeCategory, setActiveCategory] = useState<string>('Programmiersprachen');
const [isVisible, setIsVisible] = useState(false);
const ref = useRef<HTMLElement>(null);
useEffect(() => {
const currentRef = ref.current;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 }
);
if (currentRef) {
observer.observe(currentRef);
}
return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, []);
const filteredSkills = skillsData.filter((skill) => skill.category === activeCategory);
return (
<section
id="skills"
ref={ref}
className={`py-20 px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-bg-light to-white transition-opacity duration-1000 ${
isVisible ? 'opacity-100' : 'opacity-0'
}`}
>
<div className="max-w-6xl mx-auto">
<h2 className="text-3xl sm:text-4xl font-bold text-text text-center mb-4 relative inline-block w-full">
Technische Fähigkeiten
</h2>
<p className="text-center text-text-light mb-12 max-w-2xl mx-auto">
Umfassende Expertise in modernen Technologien und Frameworks
</p>
{/* Category Tabs */}
<div className="flex flex-wrap justify-center gap-3 mb-12">
{categories.map((category, index) => (
<button
key={category}
onClick={() => setActiveCategory(category)}
className={`px-5 py-3 rounded-lg font-semibold text-sm transition-all duration-300 ${
activeCategory === category
? 'bg-primary text-white shadow-lg scale-105'
: 'bg-white text-text-light hover:bg-gray-100 hover:text-text shadow-md'
} ${isVisible ? 'animate-fade-in' : 'opacity-0'}`}
style={{ animationDelay: `${index * 50}ms` }}
>
{category}
</button>
))}
</div>
{/* Skills Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredSkills.map((skill, index) => (
<div
key={skill.name}
className={`bg-white rounded-lg p-6 shadow-md hover:shadow-xl transition-all duration-300 hover:-translate-y-1 ${
isVisible ? 'animate-fade-in' : 'opacity-0'
}`}
style={{ animationDelay: `${(index + categories.length) * 50}ms` }}
>
{/* Skill Name */}
<h3 className="font-bold text-text mb-3 text-base">{skill.name}</h3>
{/* Level Bar */}
<div className="flex items-center gap-2">
<div className="flex-1 bg-gray-200 rounded-full h-2.5 overflow-hidden">
<div
className="bg-gradient-to-r from-primary to-primary-dark h-full rounded-full transition-all duration-1000 ease-out"
style={{
width: isVisible ? `${(skill.level / 5) * 100}%` : '0%',
transitionDelay: `${(index + categories.length) * 50 + 200}ms`,
}}
/>
</div>
<span className="text-sm font-bold text-primary min-w-[2rem] text-right">
{skill.level}/5
</span>
</div>
{/* Level Dots */}
<div className="flex gap-1 mt-3">
{[1, 2, 3, 4, 5].map((dot) => (
<div
key={dot}
className={`w-2 h-2 rounded-full transition-all duration-300 ${
dot <= skill.level ? 'bg-primary' : 'bg-gray-300'
}`}
style={{
transitionDelay: `${(index + categories.length) * 50 + dot * 50}ms`,
}}
/>
))}
</div>
</div>
))}
</div>
{/* Info Text */}
<div className="mt-12 text-center">
<p className="text-text-light text-sm">
<strong className="text-primary">Bewertungsskala:</strong> 1 = Grundkenntnisse | 5 = Expertenwissen
</p>
</div>
</div>
</section>
);
}
+133
View File
@@ -0,0 +1,133 @@
'use client';
import { useEffect, useRef, useState } from 'react';
interface SoftSkill {
name: string;
level: number; // 1-5
icon: string;
}
const softSkills: SoftSkill[] = [
{ name: 'Organisation', level: 4, icon: '📋' },
{ name: 'Kommunikation', level: 5, icon: '💬' },
{ name: 'Selbstständigkeit', level: 4, icon: '🎯' },
{ name: 'Teamfähigkeit', level: 5, icon: '🤝' },
{ name: 'Verantwortung', level: 5, icon: '✓' },
{ name: 'Engagement', level: 5, icon: '🚀' },
{ name: 'Kompromissbereitschaft', level: 4, icon: '🤔' },
{ name: 'Aufgeschlossenheit', level: 5, icon: '🌟' },
{ name: 'Lernbereitschaft', level: 5, icon: '📚' },
{ name: 'Zielstrebigkeit', level: 5, icon: '🎯' },
{ name: 'Überzeugungsfähigkeit', level: 4, icon: '💡' },
{ name: 'Lösungsorientiert', level: 5, icon: '🔧' },
];
export default function SoftSkills() {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef<HTMLElement>(null);
useEffect(() => {
const currentRef = ref.current;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 }
);
if (currentRef) {
observer.observe(currentRef);
}
return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, []);
return (
<section
id="soft-skills"
ref={ref}
className={`py-20 px-4 sm:px-6 lg:px-8 bg-white transition-opacity duration-1000 ${
isVisible ? 'opacity-100' : 'opacity-0'
}`}
>
<div className="max-w-6xl mx-auto">
<h2 className="text-3xl sm:text-4xl font-bold text-text text-center mb-4 relative inline-block w-full">
Persönliche Fähigkeiten
</h2>
<p className="text-center text-text-light mb-12 max-w-2xl mx-auto">
Soft Skills, die mich als Teamplayer und Leader auszeichnen
</p>
{/* Skills Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{softSkills.map((skill, index) => (
<div
key={skill.name}
className={`group bg-gradient-to-br from-white to-bg-light rounded-xl p-6 shadow-md hover:shadow-2xl transition-all duration-300 hover:-translate-y-2 border border-gray-100 hover:border-primary/30 ${
isVisible ? 'animate-fade-in' : 'opacity-0'
}`}
style={{ animationDelay: `${index * 50}ms` }}
>
{/* Icon */}
<div className="text-4xl mb-4 group-hover:scale-110 transition-transform duration-300">
{skill.icon}
</div>
{/* Skill Name */}
<h3 className="font-bold text-text mb-3 text-base group-hover:text-primary transition-colors">
{skill.name}
</h3>
{/* Level Dots */}
<div className="flex gap-1.5">
{[1, 2, 3, 4, 5].map((dot) => (
<div
key={dot}
className={`w-2.5 h-2.5 rounded-full transition-all duration-300 ${
dot <= skill.level
? 'bg-primary group-hover:scale-125'
: 'bg-gray-300'
}`}
style={{
transitionDelay: `${index * 50 + dot * 30}ms`,
}}
/>
))}
</div>
{/* Hover Effect - Level Text */}
<div className="mt-3 transition-opacity duration-300">
<span className="text-xs text-primary font-semibold">
{skill.level === 5 ? 'Expertenlevel' : skill.level === 4 ? 'Sehr gut' : 'Gut'}
</span>
</div>
</div>
))}
</div>
{/* Bottom Info */}
<div className="mt-16 bg-gradient-to-r from-primary/5 via-primary/10 to-primary/5 rounded-xl p-8 text-center">
<h3 className="text-xl font-bold text-text mb-4">
Warum diese Fähigkeiten wichtig sind
</h3>
<p className="text-text-light leading-relaxed max-w-3xl mx-auto">
In über 20 Jahren Berufserfahrung habe ich gelernt, dass technisches Know-how nur die
halbe Miete ist. Erfolgreiche Projekte entstehen durch{' '}
<strong className="text-primary">klare Kommunikation</strong>,
<strong className="text-primary"> Teamarbeit</strong> und die Fähigkeit, auch in
stressigen Situationen <strong className="text-primary">lösungsorientiert</strong> zu bleiben.
Diese Soft Skills ermöglichen es mir, komplexe technische Anforderungen verständlich zu
vermitteln und Teams erfolgreich zu führen.
</p>
</div>
</div>
</section>
);
}