From e4985d07e1697d41f7416fcd853f6ae5655639dd Mon Sep 17 00:00:00 2001 From: Elijah Date: Sun, 28 Jun 2026 08:46:05 -0700 Subject: [PATCH] feat: Redesign flashcard study screen with live shuffle toggle --- src/components/flashcards/FlashcardViewer.tsx | 150 +++++++++--------- 1 file changed, 73 insertions(+), 77 deletions(-) diff --git a/src/components/flashcards/FlashcardViewer.tsx b/src/components/flashcards/FlashcardViewer.tsx index a2849bd..0b19e76 100644 --- a/src/components/flashcards/FlashcardViewer.tsx +++ b/src/components/flashcards/FlashcardViewer.tsx @@ -28,7 +28,7 @@ type CardResult = "correct" | "missed"; export function FlashcardViewer({ cards, deckId, isShared = false, initialProgress }: FlashcardViewerProps) { const [order, setOrder] = useState( - initialProgress ? JSON.parse(initialProgress.orderJson) : [] + initialProgress ? JSON.parse(initialProgress.orderJson) : cards.map((c) => c.id) ); const [currentIndex, setCurrentIndex] = useState( initialProgress ? initialProgress.currentIndex : 0 @@ -40,10 +40,10 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre ? JSON.parse(initialProgress.cardResultsJson) : {} ); - const [mode, setMode] = useState<"SEQUENTIAL" | "SHUFFLED">( - initialProgress ? initialProgress.mode : "SEQUENTIAL" + const [isShuffled, setIsShuffled] = useState( + initialProgress ? initialProgress.mode === "SHUFFLED" : false ); - const [started, setStarted] = useState(!!initialProgress); + const [toastMessage, setToastMessage] = useState(null); // If progress is provided and index is already at or past the end, it means completed const [completed, setCompleted] = useState( @@ -62,29 +62,33 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre const missedCount = Object.values(results).filter((r) => r === "missed").length; const totalGraded = correctCount + missedCount; - // Start a study session - function startSession(selectedMode: "SEQUENTIAL" | "SHUFFLED") { - setMode(selectedMode); - let newOrder: string[]; - if (selectedMode === "SHUFFLED") { - newOrder = [...cards.map((c) => c.id)]; - for (let i = newOrder.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [newOrder[i], newOrder[j]] = [newOrder[j], newOrder[i]]; - } - } else { - newOrder = cards.map((c) => c.id); + // Auto-hide toast + useEffect(() => { + if (toastMessage) { + const timer = setTimeout(() => setToastMessage(null), 3000); + return () => clearTimeout(timer); } - setOrder(newOrder); - setCurrentIndex(0); - setResults({}); - setIsFlipped(false); - setHasFlippedOnce(false); - setCompleted(false); - setStarted(true); + }, [toastMessage]); - // Save initial progress - saveProgress(newOrder, 0, {}, selectedMode); + function toggleShuffle() { + const newShuffled = !isShuffled; + setIsShuffled(newShuffled); + + const remainingIds = order.slice(currentIndex); + let newRemaining: string[]; + + if (newShuffled) { + newRemaining = [...remainingIds].sort(() => Math.random() - 0.5); + setToastMessage("Card shuffle on"); + } else { + const originalIndices = new Map(cards.map((c, i) => [c.id, i])); + newRemaining = [...remainingIds].sort((a, b) => (originalIndices.get(a) || 0) - (originalIndices.get(b) || 0)); + setToastMessage("Card shuffle off"); + } + + const newOrder = [...order.slice(0, currentIndex), ...newRemaining]; + setOrder(newOrder); + saveProgress(newOrder, currentIndex, results, newShuffled ? "SHUFFLED" : "SEQUENTIAL"); } // Grade the current card @@ -92,6 +96,7 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre (grade: CardResult) => { if (!currentCard || !hasFlippedOnce) return; + setToastMessage(null); const newResults = { ...results, [currentCard.id]: grade }; setResults(newResults); @@ -105,21 +110,21 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre if (currentIndex + 1 >= order.length) { setCompleted(true); - saveProgress(order, currentIndex, newResults, mode); + saveProgress(order, currentIndex, newResults, isShuffled ? "SHUFFLED" : "SEQUENTIAL"); } else { const nextIndex = currentIndex + 1; setCurrentIndex(nextIndex); - saveProgress(order, nextIndex, newResults, mode); + saveProgress(order, nextIndex, newResults, isShuffled ? "SHUFFLED" : "SEQUENTIAL"); } }, 350); }, - [currentCard, hasFlippedOnce, results, currentIndex, order, mode] + [currentCard, hasFlippedOnce, results, currentIndex, order, isShuffled] ); // Keyboard shortcuts useEffect(() => { function handleKeyDown(e: KeyboardEvent) { - if (!started || completed) return; + if (completed) return; if (e.key === " " || e.key === "Enter" || e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault(); @@ -136,7 +141,7 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [started, completed, isFlipped, gradeCard]); + }, [completed, isFlipped, gradeCard]); // Touch handlers for swipe function handleTouchStart(e: React.TouchEvent) { @@ -196,7 +201,19 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre // Restart handlers function restartFullSet() { - startSession(mode); + let newOrder: string[]; + if (isShuffled) { + newOrder = [...cards.map((c) => c.id)].sort(() => Math.random() - 0.5); + } else { + newOrder = cards.map((c) => c.id); + } + setOrder(newOrder); + setCurrentIndex(0); + setResults({}); + setIsFlipped(false); + setHasFlippedOnce(false); + setCompleted(false); + saveProgress(newOrder, 0, {}, isShuffled ? "SHUFFLED" : "SEQUENTIAL"); } function redoMissed() { @@ -206,15 +223,8 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre if (missedIds.length === 0) return; - const newOrder = mode === "SHUFFLED" - ? (() => { - const arr = [...missedIds]; - for (let i = arr.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [arr[i], arr[j]] = [arr[j], arr[i]]; - } - return arr; - })() + const newOrder = isShuffled + ? [...missedIds].sort(() => Math.random() - 0.5) : missedIds; setOrder(newOrder); @@ -223,42 +233,7 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre setIsFlipped(false); setHasFlippedOnce(false); setCompleted(false); - saveProgress(newOrder, 0, {}, mode); - } - - // Mode selection (not yet started) - if (!started) { - return ( -
-
-
- - - -
-

- {cards.length} cards ready -

-

- Choose how you'd like to study -

-
- - -
-
-
- ); + saveProgress(newOrder, 0, {}, isShuffled ? "SHUFFLED" : "SEQUENTIAL"); } // Completed summary @@ -410,6 +385,15 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre )} + + {/* Toast Notification */} + {toastMessage && ( +
+
+ {toastMessage} +
+
+ )}
@@ -425,7 +409,7 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre const newResults = { ...results }; delete newResults[prevId]; setResults(newResults); - saveProgress(order, prevIndex, newResults, mode); + saveProgress(order, prevIndex, newResults, isShuffled ? "SHUFFLED" : "SEQUENTIAL"); }} className="absolute left-0 top-0 p-2 rounded-full text-text-muted hover:bg-bg-surface-alt hover:text-text-heading transition-all duration-200 cursor-pointer z-10" title="Previous Card" @@ -461,6 +445,18 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre )}
+ +
{hasFlippedOnce ? "← Missed · Got It →"