feat: Add professional portfolio images and Hero redesign

- Add 15 optimized WebP images for portfolio (21-53KB each)
- Redesign Hero section with full-width background image
- Add SSR support for Hero component
- Update About section with new portrait image
- Update Contact page with professional headshot
- Add images to Services/Leistungen page
- Add image optimization script (sharp)
- Update translations for gallery section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 23:33:28 +01:00
co-authored by Claude Opus 4.5
parent b1ec7b4d61
commit 87c7ebc5e3
75 changed files with 10271 additions and 1314 deletions
+131
View File
@@ -0,0 +1,131 @@
'use client';
import { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { motion } from 'framer-motion';
import { Database, Server, Store, Code2, Box } from 'lucide-react';
interface SkillItem {
name: string;
level: number;
}
interface SkillGroup {
category: string;
items: SkillItem[];
}
const iconMap: Record<string, React.ReactNode> = {
'Python': <Code2 className="w-8 h-8" />,
'Server': <Server className="w-8 h-8" />,
'Google Ads': <Store className="w-8 h-8" />,
'Meta Ads': <Store className="w-8 h-8" />,
'Shopware': <Store className="w-8 h-8" />,
'Shopify': <Store className="w-8 h-8" />,
'WooCommerce': <Store className="w-8 h-8" />,
'JTL WAWI': <Box className="w-8 h-8" />,
'AirFlow': <Database className="w-8 h-8" />
};
const Skills = () => {
const t = useTranslations('pages.about.skills');
const [selectedSkill, setSelectedSkill] = useState<string | null>(null);
const [allSkills, setAllSkills] = useState<SkillItem[]>([]);
useEffect(() => {
try {
const skillGroups = t.raw('skillGroups') as SkillGroup[];
if (Array.isArray(skillGroups)) {
const skills = skillGroups.flatMap(group => group.items);
setAllSkills(skills);
}
} catch {
setAllSkills([]);
}
}, [t]);
if (allSkills.length === 0) {
return null;
}
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Skills Progress Bars */}
<div className="space-y-6">
{allSkills.map((skill) => (
<motion.div
key={skill.name}
className="space-y-2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
onHoverStart={() => setSelectedSkill(skill.name)}
onHoverEnd={() => setSelectedSkill(null)}
>
<div className="flex justify-between items-center">
<div className="flex flex-col">
<span className="text-white text-sm font-medium">
{skill.name}
</span>
{selectedSkill === skill.name && (
<span className="text-zinc-400 text-xs">
{skill.level}% Proficiency
</span>
)}
</div>
<span className="text-white text-sm">
{skill.level}%
</span>
</div>
<div
className="h-2 bg-zinc-800 rounded-full overflow-hidden"
role="progressbar"
aria-valuenow={skill.level}
aria-valuemin={0}
aria-valuemax={100}
>
<motion.div
className={`h-full rounded-full ${
selectedSkill === skill.name ? 'bg-zinc-500' : 'bg-zinc-600'
}`}
initial={{ width: 0 }}
whileInView={{ width: `${skill.level}%` }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
/>
</div>
</motion.div>
))}
</div>
{/* Skills Icons Grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
{allSkills.map((skill) => (
<motion.div
key={skill.name}
className={`p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3 cursor-pointer transition-all duration-300 hover:bg-zinc-700/50 ${
selectedSkill === skill.name ? 'ring-2 ring-zinc-500' : ''
}`}
whileHover={{ scale: 1.05 }}
onClick={() => setSelectedSkill(skill.name === selectedSkill ? null : skill.name)}
role="button"
aria-pressed={selectedSkill === skill.name}
>
<div className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}>
{iconMap[skill.name] || <Box className="w-8 h-8" />}
</div>
<span className="text-white text-sm text-center">
{skill.name}
</span>
{selectedSkill === skill.name && (
<span className="text-zinc-400 text-xs text-center mt-1">
{skill.level}%
</span>
)}
</motion.div>
))}
</div>
</div>
);
};
export default Skills;