177 lines
7.0 KiB
TypeScript
177 lines
7.0 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||
import { useTranslations } from 'next-intl';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
|
||
interface ExperiencePosition {
|
||
role: string;
|
||
company: string;
|
||
period: string;
|
||
highlights: string[];
|
||
}
|
||
|
||
const Experience = () => {
|
||
const t = useTranslations('pages.home.experience');
|
||
const [currentPage, setCurrentPage] = useState(0);
|
||
const [touchStart, setTouchStart] = useState<number | null>(null);
|
||
const [touchEnd, setTouchEnd] = useState<number | null>(null);
|
||
const [itemsPerPage, setItemsPerPage] = useState(2);
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
|
||
const experiences = t.raw('positions') as ExperiencePosition[];
|
||
|
||
useEffect(() => {
|
||
const updateItemsPerPage = () => {
|
||
setItemsPerPage(window.innerWidth >= 768 ? 2 : 1);
|
||
};
|
||
updateItemsPerPage();
|
||
window.addEventListener('resize', updateItemsPerPage);
|
||
return () => window.removeEventListener('resize', updateItemsPerPage);
|
||
}, []);
|
||
|
||
const totalPages = Math.ceil((Array.isArray(experiences) ? experiences.length : 0) / itemsPerPage);
|
||
|
||
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||
setTouchStart(e.touches[0].clientX);
|
||
}, []);
|
||
|
||
const handleTouchMove = useCallback((e: React.TouchEvent) => {
|
||
setTouchEnd(e.touches[0].clientX);
|
||
}, []);
|
||
|
||
const handleTouchEnd = useCallback(() => {
|
||
if (!touchStart || !touchEnd) return;
|
||
|
||
const distance = touchStart - touchEnd;
|
||
const isLeftSwipe = distance > 50;
|
||
const isRightSwipe = distance < -50;
|
||
|
||
if (isLeftSwipe && currentPage < totalPages - 1) {
|
||
setCurrentPage(prev => prev + 1);
|
||
}
|
||
if (isRightSwipe && currentPage > 0) {
|
||
setCurrentPage(prev => prev - 1);
|
||
}
|
||
|
||
setTouchStart(null);
|
||
setTouchEnd(null);
|
||
}, [touchStart, touchEnd, currentPage, totalPages]);
|
||
|
||
const getCurrentPageItems = useCallback(() => {
|
||
if (!Array.isArray(experiences)) return [];
|
||
const startIndex = currentPage * itemsPerPage;
|
||
return experiences.slice(startIndex, startIndex + itemsPerPage);
|
||
}, [experiences, currentPage, itemsPerPage]);
|
||
|
||
return (
|
||
<section id="experience" className="py-12 md:py-24 overflow-hidden">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 40 }}
|
||
whileInView={{ opacity: 1, y: 0 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||
>
|
||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-foreground text-center mb-8 sm:mb-12">
|
||
{t('title')}
|
||
</h2>
|
||
<div
|
||
ref={containerRef}
|
||
onTouchStart={handleTouchStart}
|
||
onTouchMove={handleTouchMove}
|
||
onTouchEnd={handleTouchEnd}
|
||
className="relative"
|
||
>
|
||
<AnimatePresence mode="wait">
|
||
<motion.div
|
||
key={currentPage}
|
||
initial={{ opacity: 0, x: 100, scale: 0.95 }}
|
||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||
exit={{ opacity: 0, x: -100, scale: 0.95 }}
|
||
transition={{ duration: 0.5, ease: [0.43, 0.13, 0.23, 0.96] }}
|
||
className="grid grid-cols-1 md:grid-cols-2 gap-6"
|
||
>
|
||
{getCurrentPageItems().map((exp, index) => (
|
||
<motion.div
|
||
key={index}
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.6, delay: index * 0.15, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||
whileHover={{ scale: 1.02, y: -5, transition: { duration: 0.3 } }}
|
||
className="p-6 md:p-8 rounded-3xl bg-card/40 hover:bg-card/60 transition-all duration-300 hover:shadow-xl hover:shadow-background/50"
|
||
>
|
||
<h3 className="text-lg md:text-xl font-semibold text-foreground mb-1">
|
||
{exp.role}
|
||
</h3>
|
||
<h4 className="text-base md:text-lg text-foreground/80 mb-2">
|
||
{exp.company}
|
||
</h4>
|
||
<p className="text-sm text-muted-foreground mb-6">
|
||
{exp.period}
|
||
</p>
|
||
<ul className="space-y-3 md:space-y-4">
|
||
{exp.highlights?.map((highlight: string, hIndex: number) => (
|
||
<motion.li
|
||
key={hIndex}
|
||
initial={{ opacity: 0, x: -20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
transition={{ duration: 0.4, delay: 0.3 + (hIndex * 0.1), ease: 'easeOut' }}
|
||
className="text-muted-foreground text-sm leading-relaxed flex items-start"
|
||
>
|
||
<span className="text-foreground/40 mr-2 mt-0.5">→</span>
|
||
<span>{highlight}</span>
|
||
</motion.li>
|
||
))}
|
||
</ul>
|
||
</motion.div>
|
||
))}
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</div>
|
||
|
||
{/* Navigation Dots */}
|
||
{totalPages > 1 && (
|
||
<div className="flex justify-center items-center gap-4 mt-8">
|
||
<button
|
||
onClick={() => currentPage > 0 && setCurrentPage(prev => prev - 1)}
|
||
className="text-muted-foreground hover:text-foreground transition-all duration-300 p-3 sm:p-2 text-2xl font-light disabled:opacity-30"
|
||
disabled={currentPage === 0}
|
||
aria-label="Previous page"
|
||
>
|
||
‹
|
||
</button>
|
||
<div className="flex items-center gap-1">
|
||
{[...Array(totalPages)].map((_, index) => (
|
||
<button
|
||
key={index}
|
||
onClick={() => setCurrentPage(index)}
|
||
className="relative flex items-center justify-center w-10 h-10 sm:w-6 sm:h-6 transition-all duration-300"
|
||
aria-label={`Page ${index + 1}`}
|
||
>
|
||
<span
|
||
className={`rounded-full transition-all duration-300 ${
|
||
index === currentPage ? 'bg-foreground w-2.5 h-2.5' : 'bg-muted hover:bg-muted-foreground w-2 h-2'
|
||
}`}
|
||
/>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<button
|
||
onClick={() => currentPage < totalPages - 1 && setCurrentPage(prev => prev + 1)}
|
||
className="text-muted-foreground hover:text-foreground transition-all duration-300 p-3 sm:p-2 text-2xl font-light disabled:opacity-30"
|
||
disabled={currentPage === totalPages - 1}
|
||
aria-label="Next page"
|
||
>
|
||
›
|
||
</button>
|
||
</div>
|
||
)}
|
||
</motion.div>
|
||
</div>
|
||
</section>
|
||
);
|
||
};
|
||
|
||
export default Experience;
|