import { faCheck, faChevronLeft, faCircleCheck, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useAtomValue } from "jotai"; import { Suspense, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Link, useParams } from "wouter"; import { ApiClientError, apiClient } from "../api"; import { studyDataAtomFamily } from "../atoms"; import { ErrorBoundary } from "../components/ErrorBoundary"; import { LoadingSpinner } from "../components/LoadingSpinner"; import { renderCard } from "../utils/templateRenderer"; type Rating = 1 | 2 | 3 | 4; const RatingLabels: Record = { 1: "Again", 2: "Hard", 3: "Good", 4: "Easy", }; const RatingStyles: Record = { 1: "bg-again hover:bg-again/90 focus:ring-again/30", 2: "bg-hard hover:bg-hard/90 focus:ring-hard/30", 3: "bg-good hover:bg-good/90 focus:ring-good/30", 4: "bg-easy hover:bg-easy/90 focus:ring-easy/30", }; function StudySession({ deckId }: { deckId: string }) { const { deck, cards } = useAtomValue(studyDataAtomFamily(deckId)); // Session state (kept as useState - transient UI state) const [currentIndex, setCurrentIndex] = useState(0); const [isFlipped, setIsFlipped] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); const [completedCount, setCompletedCount] = useState(0); const cardStartTimeRef = useRef(Date.now()); // biome-ignore lint/correctness/useExhaustiveDependencies: Reset timer when card changes useEffect(() => { cardStartTimeRef.current = Date.now(); }, [currentIndex]); const handleFlip = useCallback(() => { setIsFlipped(true); }, []); const handleRating = useCallback( async (rating: Rating) => { if (isSubmitting) return; const currentCard = cards[currentIndex]; if (!currentCard) return; setIsSubmitting(true); setSubmitError(null); const durationMs = Date.now() - cardStartTimeRef.current; try { const res = await apiClient.rpc.api.decks[":deckId"].study[ ":cardId" ].$post({ param: { deckId, cardId: currentCard.id }, json: { rating, durationMs }, }); await apiClient.handleResponse(res); setCompletedCount((prev) => prev + 1); setIsFlipped(false); setCurrentIndex((prev) => prev + 1); } catch (err) { if (err instanceof ApiClientError) { setSubmitError(err.message); } else { setSubmitError("Failed to submit review. Please try again."); } } finally { setIsSubmitting(false); } }, [deckId, isSubmitting, cards, currentIndex], ); const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (isSubmitting) return; if (!isFlipped) { if (e.key === " " || e.key === "Enter") { e.preventDefault(); handleFlip(); } } else { const keyRatingMap: Record = { "1": 1, "2": 2, "3": 3, "4": 4, }; const rating = keyRatingMap[e.key]; if (rating) { e.preventDefault(); handleRating(rating); } } }, [isFlipped, isSubmitting, handleFlip, handleRating], ); useEffect(() => { window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [handleKeyDown]); const currentCard = cards[currentIndex]; const isSessionComplete = currentIndex >= cards.length && cards.length > 0; const hasNoCards = cards.length === 0; const remainingCards = cards.length - currentIndex; // Compute rendered card content for both legacy and note-based cards const cardContent = useMemo(() => { if (!currentCard) return null; // Note-based card: use template rendering if (currentCard.noteType && currentCard.fieldValuesMap) { const rendered = renderCard({ frontTemplate: currentCard.noteType.frontTemplate, backTemplate: currentCard.noteType.backTemplate, fieldValues: currentCard.fieldValuesMap, isReversed: currentCard.isReversed ?? false, }); return { front: rendered.front, back: rendered.back }; } // Legacy card: use front/back directly return { front: currentCard.front, back: currentCard.back }; }, [currentCard]); return (
{/* Submit Error */} {submitError && (
{submitError}
)} {/* Study Header */}

{deck.name}

{!isSessionComplete && !hasNoCards && ( {remainingCards} remaining )}
{/* No Cards State */} {hasNoCards && (

All caught up!

No cards due for review right now

Back to Deck
)} {/* Session Complete State */} {isSessionComplete && (

Session Complete!

You reviewed

{completedCount}

card{completedCount !== 1 ? "s" : ""}

Back to Deck All Decks
)} {/* Active Study Card */} {currentCard && cardContent && !isSessionComplete && (
{/* Card */} {/* Rating Buttons */} {isFlipped && (
{([1, 2, 3, 4] as Rating[]).map((rating) => ( ))}
)}
)}
); } export function StudyPage() { const { deckId } = useParams<{ deckId: string }>(); if (!deckId) { return (

Invalid deck ID

Back to decks
); } return (
{/* Header */}
{/* Main Content */}
}>
); }