From 30ae6e1153afb30f0ea6bdf6a7f5f3ad80520248 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Mon, 15 Sep 2025 11:59:42 -0500 Subject: [PATCH] feat: replace single-column results with persistent card grid layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace detailed results list with ResultsCardGrid component that shows all cards revealed in the same grid arrangement as during guessing. Adds visual success/failure indicators with green/red borders and checkmark/X badges on each card. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- apps/web/src/app/games/memory-quiz/page.tsx | 240 +++++++++++++++++--- 1 file changed, 209 insertions(+), 31 deletions(-) diff --git a/apps/web/src/app/games/memory-quiz/page.tsx b/apps/web/src/app/games/memory-quiz/page.tsx index dcc52177..f3a8a148 100644 --- a/apps/web/src/app/games/memory-quiz/page.tsx +++ b/apps/web/src/app/games/memory-quiz/page.tsx @@ -4,6 +4,7 @@ import Link from 'next/link' import React, { useEffect, useReducer, useRef, useCallback, useMemo, useState } from 'react' import { css } from '../../../../styled-system/css' import { TypstSoroban } from '../../../components/TypstSoroban' +import { isPrefix } from '../../../lib/memory-quiz-utils' interface QuizCard { number: number @@ -705,18 +706,216 @@ function CardGrid({ state }: { state: SorobanQuizState }) { ) } +// Results card grid that reuses CardGrid with all cards revealed and success/failure indicators +function ResultsCardGrid({ state }: { state: SorobanQuizState }) { + if (state.quizCards.length === 0) return null + + // Create a modified state where all cards are revealed for results display + const resultsState = { + ...state, + revealedCards: state.quizCards.map(card => card.number) // Reveal all cards + } + + // Calculate optimal grid layout based on number of cards (same as CardGrid) + const cardCount = state.quizCards.length + + // Define static grid classes that Panda can generate (same as CardGrid) + const getGridClass = (count: number) => { + if (count <= 2) return 'repeat(2, 1fr)' + if (count <= 4) return 'repeat(2, 1fr)' + if (count <= 6) return 'repeat(3, 1fr)' + if (count <= 9) return 'repeat(3, 1fr)' + if (count <= 12) return 'repeat(4, 1fr)' + return 'repeat(5, 1fr)' + } + + const getCardSize = (count: number) => { + if (count <= 2) return { minSize: '180px', cardHeight: '160px' } + if (count <= 4) return { minSize: '160px', cardHeight: '150px' } + if (count <= 6) return { minSize: '140px', cardHeight: '140px' } + if (count <= 9) return { minSize: '120px', cardHeight: '130px' } + if (count <= 12) return { minSize: '110px', cardHeight: '120px' } + return { minSize: '100px', cardHeight: '110px' } + } + + const gridClass = getGridClass(cardCount) + const cardSize = getCardSize(cardCount) + + return ( +
+
+ {state.quizCards.map((card, index) => { + const isRevealed = true // All cards revealed in results + const wasFound = state.foundNumbers.includes(card.number) + + return ( +
+
+ {/* Card back (hidden state) - not visible in results */} +
+
?
+
+ + {/* Card front (revealed state) with success/failure indicators */} +
+
+ +
+ + {/* Right/Wrong indicator overlay */} +
+ {wasFound ? '✓' : '✗'} +
+ + {/* Number label overlay */} +
+ {card.number} +
+
+
+
+ ) + })} +
+ + {/* Summary row for large numbers of cards (same as CardGrid) */} + {cardCount > 8 && ( +
+ {state.foundNumbers.length} of {cardCount} cards found + {state.foundNumbers.length > 0 && ( + + ({Math.round((state.foundNumbers.length / cardCount) * 100)}% complete) + + )} +
+ )} +
+ ) +} + // React component for the input phase function InputPhase({ state, dispatch }: { state: SorobanQuizState; dispatch: React.Dispatch }) { const containerRef = useRef(null) const [displayFeedback, setDisplayFeedback] = useState<'neutral' | 'correct' | 'incorrect'>('neutral') - const isPrefix = useCallback((input: string, numbers: number[], foundNumbers: number[]) => { - return numbers.some(n => - n.toString().startsWith(input) && - n.toString() !== input && - !foundNumbers.includes(n) - ) - }, []) const handleKeyPress = useCallback((e: KeyboardEvent) => { // Only handle if input phase is active and guesses remain @@ -1136,30 +1335,9 @@ function ResultsPhase({ state, dispatch }: { state: SorobanQuizState; dispatch: -
-

Detailed Results:

- {state.correctAnswers.map(number => { - const found = state.foundNumbers.includes(number) - const status = found ? '✅' : '❌' - return ( -
- {number} - {status} -
- ) - })} + {/* Results card grid - reuse CardGrid but with all cards revealed and status indicators */} +
+