From 1bacf43ed2d895c98bb011361a37b5d01f53a8bb Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Wed, 7 Oct 2026 10:19:44 +0000 Subject: [PATCH] Fix 'Retry missed ones' showing locked, already-answered items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retry reused the same React keys for the same activity ids, so every activity component kept its internal state (selected option, feedback, flip, matched pairs) from the first run — the retried items appeared pre-answered with the old wrong answer and could not be answered again. - StudyFeed now keeps a runId that increments on every restart/retry and includes it in the slide keys, so each run gets fresh component state. - Scroll snaps back to the first slide instantly on a new run (the old requestAnimationFrame + smooth goTo could race the queue change). New retry suite (14 checks): answer wrong → results → retry → items are unanswered and enabled → answer correctly → 2/2 on the retry run, plus the full 'study again' reset. Deck-chrome 15/15, pwa 16/16, final-ux 12/12, security 8/8, contrast 20/20. --- components/StudyFeed.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/components/StudyFeed.tsx b/components/StudyFeed.tsx index d0970c8..55c38b6 100644 --- a/components/StudyFeed.tsx +++ b/components/StudyFeed.tsx @@ -24,6 +24,8 @@ export function StudyFeed({ deck }: { deck: Deck }) { const [streak, setStreak] = useState(0); const [bestStreak, setBestStreak] = useState(0); const [retryMode, setRetryMode] = useState(false); + /** Bumped on every restart so the activity components remount fresh. */ + const [runId, setRunId] = useState(0); const scrollRef = useRef(null); const recordedRef = useRef(false); @@ -134,12 +136,19 @@ export function StudyFeed({ deck }: { deck: Deck }) { setStreak(0); setRetryMode(retry); recordedRef.current = retry; // don't overwrite the best score with a retry run + // A new run: remount every activity (fresh state) and jump to the top. + setRunId((value) => value + 1); setIndex(0); - requestAnimationFrame(() => goTo(0)); }, - [goTo], + [], ); + // Whenever a run starts (including the first render), snap back to slide 1. + useEffect(() => { + const container = scrollRef.current; + if (container) container.scrollTop = 0; + }, [runId]); + const progressValue = Math.min(index + 1, slideCount); const progressMax = slideCount; @@ -205,7 +214,7 @@ export function StudyFeed({ deck }: { deck: Deck }) { > {queue.map((activity, activityIndex) => (