diff --git a/studio-v2/app/learn/[unitId]/match/page.tsx b/studio-v2/app/learn/[unitId]/match/page.tsx index f9aa063..3efbaf6 100644 --- a/studio-v2/app/learn/[unitId]/match/page.tsx +++ b/studio-v2/app/learn/[unitId]/match/page.tsx @@ -5,8 +5,12 @@ import { useParams, useRouter } from 'next/navigation' import { useTheme } from '@/lib/ThemeContext' import { useNativeLanguage } from '@/lib/useNativeLanguage' import { StarRating, accuracyToStars } from '@/components/gamification/StarRating' +import { AudioButton } from '@/components/learn/AudioButton' -interface QAItem { id: string; question: string; answer: string } +interface QAItem { + id: string; question: string; answer: string + translations?: Record +} function getApiBase() { return '' } @@ -22,7 +26,11 @@ export default function MatchPage() { const [selectedLeft, setSelectedLeft] = useState(null) const [matched, setMatched] = useState>(new Set()) const [wrongPair, setWrongPair] = useState(null) + const [firstTryCorrect, setFirstTryCorrect] = useState(0) + const [retryCorrect, setRetryCorrect] = useState(0) const [errors, setErrors] = useState(0) + const [failedIds, setFailedIds] = useState>(new Set()) + const [flashNative, setFlashNative] = useState(null) const [isComplete, setIsComplete] = useState(false) const glassCard = isDark @@ -38,32 +46,44 @@ export default function MatchPage() { })() }, [unitId]) - // Take 6 items per round const roundItems = useMemo(() => { const start = round * 6 return allItems.slice(start, start + 6) }, [allItems, round]) - // Shuffled right column const shuffledRight = useMemo(() => { return [...roundItems].sort(() => Math.random() - 0.5) }, [roundItems]) - const handleLeftTap = useCallback((id: string) => { - if (matched.has(id)) return - setSelectedLeft(id === selectedLeft ? null : id) + const handleLeftTap = useCallback((item: QAItem) => { + if (matched.has(item.id)) return + setSelectedLeft(item.id === selectedLeft ? null : item.id) setWrongPair(null) - }, [selectedLeft, matched]) + + // Flash native translation briefly + if (isThirdLanguage && item.id !== selectedLeft) { + const native = wordInNative(item.translations) + if (native) { + setFlashNative(native) + setTimeout(() => setFlashNative(null), 2000) + } + } + }, [selectedLeft, matched, isThirdLanguage, wordInNative]) const handleRightTap = useCallback((id: string) => { if (!selectedLeft || matched.has(id)) return if (selectedLeft === id) { - // Correct match + // Correct setMatched(prev => new Set([...prev, id])) + if (failedIds.has(id)) { + setRetryCorrect(c => c + 1) + } else { + setFirstTryCorrect(c => c + 1) + } setSelectedLeft(null) + setFlashNative(null) - // Check if round complete if (matched.size + 1 >= roundItems.length) { const nextStart = (round + 1) * 6 if (nextStart >= allItems.length) { @@ -77,92 +97,177 @@ export default function MatchPage() { } } } else { - // Wrong match + // Wrong setWrongPair(id) setErrors(e => e + 1) + setFailedIds(prev => new Set([...prev, selectedLeft])) setTimeout(() => { setWrongPair(null) setSelectedLeft(null) + setFlashNative(null) }, 600) } - }, [selectedLeft, matched, roundItems, round, allItems]) + }, [selectedLeft, matched, roundItems, round, allItems, failedIds]) + + const restart = () => { + setRound(0) + setMatched(new Set()) + setFirstTryCorrect(0) + setRetryCorrect(0) + setErrors(0) + setFailedIds(new Set()) + setIsComplete(false) + setSelectedLeft(null) + } if (isLoading) { - return
+ return
} const totalPairs = allItems.length const matchedTotal = round * 6 + matched.size + const isPerfect = isComplete && errors === 0 return ( <> {/* Header */}
-
+
-

Zuordnen

- {matchedTotal}/{totalPairs} +

{t('match')}

+
+ {matchedTotal}/{totalPairs} · {firstTryCorrect}/{retryCorrect}/{errors} +
+ {/* Native flash overlay */} + {flashNative && ( +
+
+ {flashNative} +
+
+ )} +
{isComplete ? (
- -

Alle zugeordnet!

-

{errors} Fehler

+ +

+ {isPerfect ? t('well_done') : t('all_matched')} +

+
+
{firstTryCorrect}
{t('correct')}
+
{retryCorrect}
2. Versuch
+
{errors}
{t('errors')}
+
+ {!isPerfect && ( +

+ {isThirdLanguage + ? (nativeLang === 'tr' ? 'Tam puan icin hatasiz tamamlayin' : 'Fuer volle Punkte fehlerfrei abschliessen') + : 'Fuer volle Punkte fehlerfrei abschliessen'} +

+ )}
- - + +
) : ( -
- {/* Left column: English */} -
-

English

- {roundItems.map(item => ( - - ))} +
+ {/* Column headers */} +
+

{t('english')}

+

{t('german')}

+ {isThirdLanguage && ( +

{nativeLang.toUpperCase()}

+ )}
- {/* Right column: German (shuffled) */} -
-

Deutsch

- {shuffledRight.map(item => ( - - ))} + {/* Grid */} +
+ {/* Left: English (click to select, no sound) */} +
+ {roundItems.map(item => ( + + ))} +
+ + {/* Middle: German (click to match + sound) */} +
+ {shuffledRight.map(item => ( +
+ + {!matched.has(item.id) && ( + + )} +
+ ))} +
+ + {/* Right: Native language (display only + sound) */} + {isThirdLanguage && ( +
+ {roundItems.map(item => { + const native = wordInNative(item.translations) + return ( +
+ {native || '—'} + {native && !matched.has(item.id) && ( + + )} +
+ ) + })} +
+ )}
)}