'use client' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { motion, AnimatePresence } from 'motion/react' import { GraduationCap, Layers, ArrowLeft, ChevronLeft, ChevronRight, Calendar, BarChart3, Loader2, BookOpen, Check, ChevronDown, Trash2, Pencil, X, BookMarked, Clock, Timer, Flame, TrendingUp, } from 'lucide-react' import { useSearchParams } from 'next/navigation' import { useLanguage } from '@/lib/i18n' import { cn } from '@/lib/utils' import { NoteChart } from '@/components/note-chart' import { RevisionHeatmap } from './revision-heatmap' import { RetentionCurve } from './retention-curve' import { toast } from 'sonner' interface DeckSummary { id: string name: string notebookId: string | null notebookName: string | null totalCards: number dueCount: number masteredCount: number lastReviewedAt: string | null nextReviewAt: string | null } interface FlashcardItem { id: string front: string back: string type: string due: boolean mastered: boolean } interface StatsResponse { heatmap: { date: string; count: number }[] retentionRate: number retentionByWeek: { week: string; rate: number; total: number }[] difficultCards: { id: string; front: string; easinessFactor: number; deckId: string; deckName: string }[] totalReviews: number streak: number totalCards: number masteredCount: number } type PageTab = 'decks' | 'progress' type SessionGrade = 1 | 2 | 3 | 4 type ReviewMode = 'due' | 'all' function shuffleCards(items: T[]): T[] { const arr = [...items] for (let i = arr.length - 1; i > 0; i -= 1) { const j = Math.floor(Math.random() * (i + 1)) ;[arr[i], arr[j]] = [arr[j], arr[i]] } return arr } function buildSessionQueue(cards: FlashcardItem[], dueOnly: boolean): FlashcardItem[] { const toReview = dueOnly ? cards.filter((c) => c.due) : cards return shuffleCards(toReview) } function formatDuration(seconds: number): string { const m = Math.floor(seconds / 60) const s = seconds % 60 if (m === 0) return `${s}s` return `${m}m ${s}s` } const GRADE_OPTIONS: { grade: SessionGrade idle: string selected: string ring: string }[] = [ { grade: 1, idle: 'bg-red-500/10 text-red-600 border-red-500/20 hover:bg-red-500/15', selected: 'bg-red-500 text-white border-red-500 shadow-lg shadow-red-500/25', ring: 'ring-red-400/50', }, { grade: 2, idle: 'bg-amber-500/10 text-amber-700 border-amber-500/20 hover:bg-amber-500/15', selected: 'bg-amber-500 text-white border-amber-500 shadow-lg shadow-amber-500/25', ring: 'ring-amber-400/50', }, { grade: 3, idle: 'bg-emerald-500/10 text-emerald-700 border-emerald-500/20 hover:bg-emerald-500/15', selected: 'bg-emerald-500 text-white border-emerald-500 shadow-lg shadow-emerald-500/25', ring: 'ring-emerald-400/50', }, { grade: 4, idle: 'bg-brand-accent/10 text-brand-accent border-brand-accent/30 hover:bg-brand-accent/15', selected: 'bg-brand-accent text-white border-brand-accent shadow-lg shadow-brand-accent/25', ring: 'ring-brand-accent/50', }, ] const TYPE_LABEL: Record = { qa: 'Q&A', cloze: 'Cloze', concept: 'Concept', } /** Inline card editor inside the expanded deck list */ function CardEditRow({ card, onSaved, onDeleted, }: { card: FlashcardItem onSaved: (id: string, front: string, back: string) => void onDeleted: (id: string) => void }) { const { t } = useLanguage() const [editing, setEditing] = useState(false) const [front, setFront] = useState(card.front) const [back, setBack] = useState(card.back) const [saving, setSaving] = useState(false) const [deleting, setDeleting] = useState(false) const [confirmDelete, setConfirmDelete] = useState(false) const handleSave = async () => { setSaving(true) try { const res = await fetch(`/api/flashcards/${card.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ front: front.trim(), back: back.trim() }), }) if (!res.ok) throw new Error() onSaved(card.id, front.trim(), back.trim()) setEditing(false) toast.success(t('flashcards.cardSaved')) } catch { toast.error(t('flashcards.deleteFailed')) } finally { setSaving(false) } } const handleDelete = async () => { setDeleting(true) try { const res = await fetch(`/api/flashcards/${card.id}`, { method: 'DELETE' }) if (!res.ok) throw new Error() onDeleted(card.id) toast.success(t('flashcards.cardDeleted')) } catch { toast.error(t('flashcards.deleteFailed')) } finally { setDeleting(false) setConfirmDelete(false) } } if (editing) { return (
  • setFront(e.target.value)} className="w-full text-sm font-medium bg-transparent border-b border-border/50 pb-1 outline-none" placeholder={t('flashcards.frontPlaceholder')} autoFocus />