import { useState, useEffect } from 'react'; import { useDrag } from '@use-gesture/react'; import { motion, AnimatePresence, useMotionValue, useTransform, useSpring } from 'framer-motion'; import type { Article } from '../../api/articles'; import { FocusCard } from './FocusCard'; import { FocusArticleView } from './FocusArticleView'; interface FocusCardStackProps { articles: Article[]; onMarkRead: (id: string) => void; onKeep: (id: string) => void; onEmpty: () => void; } export function FocusCardStack({ articles, onMarkRead, onKeep, onEmpty }: FocusCardStackProps) { const [currentIndex, setCurrentIndex] = useState(0); const [readingArticle, setReadingArticle] = useState
(null); const [exitX, setExitX] = useState(0); // Visible stack size const visibleArticles = articles.slice(currentIndex, currentIndex + 3); const topArticle = visibleArticles[0]; // Motion values for drag const x = useMotionValue(0); const rotate = useTransform(x, [-200, 200], [-10, 10]); // Opacity not used for card itself in this design, removed to fix lint // Background color based on drag direction const bgOpacityRead = useTransform(x, [0, 150], [0, 1]); const bgOpacityKeep = useTransform(x, [-150, 0], [1, 0]); // Spring physics for smooth return const xSpring = useSpring(x, { stiffness: 1000, damping: 50 }); const rotateSpring = useSpring(rotate, { stiffness: 1000, damping: 50 }); const bind = useDrag(({ active, movement: [mx], velocity: [vx] }) => { if (readingArticle) return; // Disable drag when reading const trigger = Math.abs(mx) > 200 || (Math.abs(vx) > 0.5 && Math.abs(mx) > 50); if (!active && trigger) { // Swipe Validé const isRight = mx > 0; setExitX(isRight ? 500 : -500); // Trigger exit animation // Logic handled after animation completes via AnimatePresence onExitComplete // But here we need to trigger state change. // Better approach: State change triggers exit animation naturally? // No, libraries like Framer Motion handle exit animations when components are removed from DOM. // So we just need to advance index. if (isRight) { onMarkRead(topArticle.id); } else { onKeep(topArticle.id); } setCurrentIndex(prev => prev + 1); x.set(0); // Reset for next card } else { // Dragging or Snap Back x.set(active ? mx : 0); } }); // Check for empty stack useEffect(() => { if (currentIndex >= articles.length && articles.length > 0) { onEmpty(); } }, [currentIndex, articles.length, onEmpty]); if (!topArticle) return null; return (
{/* Reading View Modal */} {readingArticle && ( setReadingArticle(null)} onNext={(action) => { setReadingArticle(null); if (action === 'read') onMarkRead(readingArticle.id); else onKeep(readingArticle.id); setCurrentIndex(prev => prev + 1); }} /> )} {/* Background Indicators */} ✓ ⭐ {/* Card Stack */}
{visibleArticles.map((article, i) => { const isTop = i === 0; return ( isTop && setReadingArticle(article)} animate={{ scale: 1 - i * 0.05, y: i * 20 }} exit={{ x: exitX, opacity: 0, scale: 0.9, transition: { duration: 0.2 } }} > ); })}
{/* Undo Button */} {currentIndex > 0 && ( )}
{/* Progress Counter */}
{articles.length - currentIndex} Restants
); }