146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } 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[];
|
|
}
|
|
|
|
// Move iconMap outside component for better performance
|
|
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[]>([]);
|
|
|
|
// Memoized event handlers
|
|
const handleHoverStart = useCallback((skillName: string) => {
|
|
setSelectedSkill(skillName);
|
|
}, []);
|
|
|
|
const handleHoverEnd = useCallback(() => {
|
|
setSelectedSkill(null);
|
|
}, []);
|
|
|
|
const handleClick = useCallback((skillName: string) => {
|
|
setSelectedSkill(prev => prev === skillName ? null : skillName);
|
|
}, []);
|
|
|
|
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={() => handleHoverStart(skill.name)}
|
|
onHoverEnd={handleHoverEnd}
|
|
>
|
|
<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={() => handleClick(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;
|