216 lines
8.3 KiB
TypeScript
216 lines
8.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { motion } from 'framer-motion';
|
|
import { Database, Server, Code2, Bot, Workflow, FileCode2 } from 'lucide-react';
|
|
|
|
interface Skill {
|
|
name: string;
|
|
level: number;
|
|
}
|
|
|
|
interface SkeletonProps {
|
|
className?: string;
|
|
}
|
|
|
|
const Skeleton: React.FC<SkeletonProps> = ({ className }) => (
|
|
<div className={`animate-pulse bg-zinc-700/50 rounded ${className}`}></div>
|
|
);
|
|
|
|
const SkillsSkeleton = () => (
|
|
<section id="skills" className="py-24 relative" aria-label="Loading skills" aria-busy="true">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<Skeleton className="h-8 w-48 mb-12" />
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Skills Progress Bars Skeleton */}
|
|
<div className="space-y-6">
|
|
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
|
<div key={i} className="space-y-2">
|
|
<div className="flex justify-between items-center">
|
|
<Skeleton className="h-4 w-24" />
|
|
<Skeleton className="h-4 w-12" />
|
|
</div>
|
|
<Skeleton className="h-2 w-full rounded-full" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Skills Icons Grid Skeleton */}
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
|
<div key={i} className="p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm">
|
|
<Skeleton className="w-8 h-8 mx-auto mb-3" />
|
|
<Skeleton className="h-4 w-16 mx-auto" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
|
|
const iconMap: Record<string, React.ReactNode> = {
|
|
'AI & LLMs': <Bot className="w-8 h-8" />,
|
|
'Automation': <Workflow className="w-8 h-8" />,
|
|
'Automatizacija': <Workflow className="w-8 h-8" />,
|
|
'Python': <Code2 className="w-8 h-8" />,
|
|
'TypeScript': <FileCode2 className="w-8 h-8" />,
|
|
'Datenbank': <Database className="w-8 h-8" />,
|
|
'Database': <Database className="w-8 h-8" />,
|
|
'Baze podataka': <Database className="w-8 h-8" />,
|
|
'DevOps': <Server className="w-8 h-8" />
|
|
};
|
|
|
|
const Skills = () => {
|
|
const t = useTranslations('pages.home.skills');
|
|
const [skills, setSkills] = useState<Skill[]>([]);
|
|
|
|
const handleHoverStart = useCallback((skillName: string) => {
|
|
setSelectedSkill(skillName);
|
|
}, []);
|
|
|
|
const handleHoverEnd = useCallback(() => {
|
|
setSelectedSkill(null);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
// Temporary delay to see skeleton loading state
|
|
const timer = setTimeout(() => {
|
|
try {
|
|
const translatedSkills = t.raw('skills') as Skill[];
|
|
if (Array.isArray(translatedSkills)) {
|
|
setSkills(translatedSkills);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading skills:', error);
|
|
setSkills([]);
|
|
}
|
|
}, 2000);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [t]);
|
|
|
|
if (skills.length === 0) {
|
|
return <SkillsSkeleton />;
|
|
}
|
|
|
|
return (
|
|
<section id="skills" className="py-24 relative">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<h2 className="text-3xl font-bold text-white mb-12">
|
|
{t('title')}
|
|
</h2>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Skills Progress Bars */}
|
|
<div className="space-y-6">
|
|
{skills.map((skill, idx) => (
|
|
<motion.div
|
|
key={skill.name}
|
|
className="space-y-2"
|
|
initial={{ opacity: 0, x: -20 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, delay: idx * 0.1 }}
|
|
>
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-white text-sm font-medium">{skill.name}</span>
|
|
<span className="text-white text-sm">{skill.level}%</span>
|
|
</div>
|
|
<div
|
|
className="h-2 bg-zinc-800/50 rounded-full overflow-hidden"
|
|
role="progressbar"
|
|
aria-valuenow={skill.level}
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
>
|
|
<motion.div
|
|
className="h-full rounded-full bg-gradient-to-r from-zinc-600 to-zinc-500"
|
|
initial={{ width: 0 }}
|
|
whileInView={{ width: `${skill.level}%` }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 1, delay: idx * 0.1 }}
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Skills Icons Grid */}
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
|
{skills.map((skill, idx) => (
|
|
<motion.div
|
|
key={skill.name}
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
whileInView={{ opacity: 1, scale: 1 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, delay: idx * 0.1 }}
|
|
className="p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3"
|
|
>
|
|
<<<<<<< HEAD
|
|
<motion.div
|
|
className={`relative p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3 cursor-pointer transition-colors duration-300 hover:bg-zinc-700/50 ${
|
|
selectedSkill === skill.name ? 'ring-2 ring-zinc-500 z-20' : ''
|
|
}`}
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
onHoverStart={() => handleHoverStart(skill.name)}
|
|
onHoverEnd={handleHoverEnd}
|
|
role="button"
|
|
>
|
|
<motion.div
|
|
className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}
|
|
animate={{
|
|
rotate: selectedSkill === skill.name ? [0, -10, 10, 0] : 0,
|
|
scale: selectedSkill === skill.name ? 1.1 : 1
|
|
}}
|
|
transition={{ duration: 0.4 }}
|
|
>
|
|
{iconMap[skill.name] || <Code2 className="w-8 h-8" />}
|
|
</motion.div>
|
|
<span className="text-white text-sm text-center">{skill.name}</span>
|
|
|
|
<AnimatePresence>
|
|
{selectedSkill === skill.name && (
|
|
<motion.div
|
|
className="absolute left-1/2 top-0 -translate-x-1/2 bg-zinc-800/80 backdrop-blur-sm rounded-lg px-4 py-2 text-zinc-300 text-xs text-center shadow-xl pointer-events-none"
|
|
initial={{ opacity: 0, y: 5 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: 5 }}
|
|
transition={{ duration: 0.15 }}
|
|
style={{
|
|
whiteSpace: 'nowrap',
|
|
transform: 'translate(-50%, calc(-100% - 12px))',
|
|
zIndex: 999
|
|
}}
|
|
>
|
|
{skill.description}
|
|
<div className="absolute left-1/2 bottom-0 translate-y-1/2 -translate-x-1/2 w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[6px] border-t-zinc-800" />
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
=======
|
|
<div className="text-white">
|
|
{iconMap[skill.name] || <Code2 className="w-8 h-8" />}
|
|
</div>
|
|
<span className="text-white text-sm text-center">{skill.name}</span>
|
|
>>>>>>> origin/master
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default Skills;
|