import { faChevronLeft, faCirclePlay, faFile, faLayerGroup, faPen, faPlus, faSpinner, faTrash, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Link, useParams } from "wouter"; import { ApiClientError, apiClient } from "../api"; import { CreateNoteModal } from "../components/CreateNoteModal"; import { DeleteCardModal } from "../components/DeleteCardModal"; import { DeleteNoteModal } from "../components/DeleteNoteModal"; import { EditCardModal } from "../components/EditCardModal"; import { EditNoteModal } from "../components/EditNoteModal"; interface Card { id: string; deckId: string; noteId: string; isReversed: boolean; front: string; back: string; state: number; due: string; reps: number; lapses: number; createdAt: string; updatedAt: string; } /** Combined type for display: note group */ type CardDisplayItem = { type: "note"; noteId: string; cards: Card[] }; interface Deck { id: string; name: string; description: string | null; } const CardStateLabels: Record = { 0: "New", 1: "Learning", 2: "Review", 3: "Relearning", }; const CardStateColors: Record = { 0: "bg-info/10 text-info", 1: "bg-warning/10 text-warning", 2: "bg-success/10 text-success", 3: "bg-error/10 text-error", }; /** Component for displaying a group of cards from the same note */ function NoteGroupCard({ noteId, cards, index, onEditNote, onDeleteNote, }: { noteId: string; cards: Card[]; index: number; onEditNote: () => void; onDeleteNote: () => void; }) { // Use the first card's front/back as preview (normal card takes precedence) const previewCard = cards.find((c) => !c.isReversed) ?? cards[0]; if (!previewCard) return null; return (
{/* Note Header */}
{/* Note Content Preview */}
Front

{previewCard.front}

Back

{previewCard.back}

{/* Cards within this note */}
{cards.map((card) => (
{CardStateLabels[card.state] || "Unknown"} {card.isReversed ? ( Reversed ) : ( Normal )} {card.reps} reviews {card.lapses > 0 && ( {card.lapses} lapses )}
))}
); } export function DeckDetailPage() { const { deckId } = useParams<{ deckId: string }>(); const [deck, setDeck] = useState(null); const [cards, setCards] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [editingCard, setEditingCard] = useState(null); const [editingNoteId, setEditingNoteId] = useState(null); const [deletingCard, setDeletingCard] = useState(null); const [deletingNoteId, setDeletingNoteId] = useState(null); // Group cards by note for display const displayItems = useMemo((): CardDisplayItem[] => { const noteGroups = new Map(); for (const card of cards) { const existing = noteGroups.get(card.noteId); if (existing) { existing.push(card); } else { noteGroups.set(card.noteId, [card]); } } // Sort note groups by earliest card creation (newest first) const sortedNoteGroups = Array.from(noteGroups.entries()).sort( ([, cardsA], [, cardsB]) => { const minA = Math.min( ...cardsA.map((c) => new Date(c.createdAt).getTime()), ); const minB = Math.min( ...cardsB.map((c) => new Date(c.createdAt).getTime()), ); return minB - minA; // Newest first }, ); const items: CardDisplayItem[] = []; for (const [noteId, noteCards] of sortedNoteGroups) { // Sort cards within group: normal first, then reversed noteCards.sort((a, b) => { if (a.isReversed === b.isReversed) return 0; return a.isReversed ? 1 : -1; }); items.push({ type: "note", noteId, cards: noteCards }); } return items; }, [cards]); const fetchDeck = useCallback(async () => { if (!deckId) return; const authHeader = apiClient.getAuthHeader(); if (!authHeader) { throw new ApiClientError("Not authenticated", 401); } const res = await fetch(`/api/decks/${deckId}`, { headers: authHeader, }); if (!res.ok) { const errorBody = await res.json().catch(() => ({})); throw new ApiClientError( (errorBody as { error?: string }).error || `Request failed with status ${res.status}`, res.status, ); } const data = await res.json(); setDeck(data.deck); }, [deckId]); const fetchCards = useCallback(async () => { if (!deckId) return; const authHeader = apiClient.getAuthHeader(); if (!authHeader) { throw new ApiClientError("Not authenticated", 401); } const res = await fetch(`/api/decks/${deckId}/cards`, { headers: authHeader, }); if (!res.ok) { const errorBody = await res.json().catch(() => ({})); throw new ApiClientError( (errorBody as { error?: string }).error || `Request failed with status ${res.status}`, res.status, ); } const data = await res.json(); setCards(data.cards); }, [deckId]); const fetchData = useCallback(async () => { setIsLoading(true); setError(null); try { await Promise.all([fetchDeck(), fetchCards()]); } catch (err) { if (err instanceof ApiClientError) { setError(err.message); } else { setError("Failed to load data. Please try again."); } } finally { setIsLoading(false); } }, [fetchDeck, fetchCards]); useEffect(() => { fetchData(); }, [fetchData]); if (!deckId) { return (

Invalid deck ID

Back to decks
); } return (
{/* Header */}
{/* Main Content */}
{/* Loading State */} {isLoading && (
)} {/* Error State */} {error && (
{error}
)} {/* Deck Content */} {!isLoading && !error && deck && (
{/* Deck Header */}

{deck.name}

{deck.description && (

{deck.description}

)}
{/* Study Button */}
{/* Cards Section */}

Cards{" "} ({cards.length})

{/* Empty State */} {cards.length === 0 && (

No cards yet

Add notes to start studying

)} {/* Card List - Grouped by Note */} {cards.length > 0 && (
{displayItems.map((item, index) => ( setEditingNoteId(item.noteId)} onDeleteNote={() => setDeletingNoteId(item.noteId)} /> ))}
)}
)}
{/* Modals */} {deckId && ( setIsCreateModalOpen(false)} onNoteCreated={fetchCards} /> )} {deckId && ( setEditingCard(null)} onCardUpdated={fetchCards} /> )} {deckId && ( setEditingNoteId(null)} onNoteUpdated={fetchCards} /> )} {deckId && ( setDeletingCard(null)} onCardDeleted={fetchCards} /> )} {deckId && ( setDeletingNoteId(null)} onNoteDeleted={fetchCards} /> )}
); }