Redesign: 2/3 + 1/3 layout for exercises with native helper panel

ExerciseLayout.tsx: Reusable layout component for all exercises.
- Left 2/3: Standard exercise area (EN + DE)
- Right 1/3: Native language helper (explanation + word list)
- Only shows right panel for non-DE/EN speakers
- Explanation card describes what the child should do
- Column headers are trilingual (TR · English · Deutsch)

Match page rebuilt using ExerciseLayout:
- EN+DE cards in 2/3 left area with equal height + audio
- Native words in 1/3 right panel with audio buttons
- Highlights native word when EN word is selected
- Progress bar with count, score counter

ExerciseLayout can be reused for flashcards, quiz, type, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-27 22:30:07 +02:00
parent afe7a983d1
commit 82f5b4fbba
2 changed files with 244 additions and 210 deletions

View File

@@ -6,6 +6,7 @@ import { useTheme } from '@/lib/ThemeContext'
import { useNativeLanguage } from '@/lib/useNativeLanguage'
import { StarRating, accuracyToStars } from '@/components/gamification/StarRating'
import { AudioButton } from '@/components/learn/AudioButton'
import { ExerciseLayout } from '@/components/learn/ExerciseLayout'
interface QAItem {
id: string; question: string; answer: string
@@ -30,7 +31,6 @@ export default function MatchPage() {
const [retryCorrect, setRetryCorrect] = useState(0)
const [errors, setErrors] = useState(0)
const [failedIds, setFailedIds] = useState<Set<string>>(new Set())
const [flashNative, setFlashNative] = useState<string | null>(null)
const [isComplete, setIsComplete] = useState(false)
const glassCard = isDark
@@ -46,78 +46,38 @@ export default function MatchPage() {
})()
}, [unitId])
const roundItems = useMemo(() => {
const start = round * 6
return allItems.slice(start, start + 6)
}, [allItems, round])
const roundItems = useMemo(() => allItems.slice(round * 6, round * 6 + 6), [allItems, round])
const shuffledRight = useMemo(() => [...roundItems].sort(() => Math.random() - 0.5), [roundItems])
const shuffledRight = useMemo(() => {
return [...roundItems].sort(() => Math.random() - 0.5)
}, [roundItems])
const handleLeftTap = useCallback((item: QAItem) => {
if (matched.has(item.id)) return
setSelectedLeft(item.id === selectedLeft ? null : item.id)
const handleLeftTap = useCallback((id: string) => {
if (matched.has(id)) return
setSelectedLeft(id === selectedLeft ? null : id)
setWrongPair(null)
// 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])
}, [selectedLeft, matched])
const handleRightTap = useCallback((id: string) => {
if (!selectedLeft || matched.has(id)) return
if (selectedLeft === id) {
// Correct
setMatched(prev => new Set([...prev, id]))
if (failedIds.has(id)) {
setRetryCorrect(c => c + 1)
} else {
setFirstTryCorrect(c => c + 1)
}
if (failedIds.has(id)) setRetryCorrect(c => c + 1)
else setFirstTryCorrect(c => c + 1)
setSelectedLeft(null)
setFlashNative(null)
if (matched.size + 1 >= roundItems.length) {
const nextStart = (round + 1) * 6
if (nextStart >= allItems.length) {
setTimeout(() => setIsComplete(true), 500)
} else {
setTimeout(() => {
setRound(r => r + 1)
setMatched(new Set())
setSelectedLeft(null)
}, 800)
}
if (nextStart >= allItems.length) setTimeout(() => setIsComplete(true), 500)
else setTimeout(() => { setRound(r => r + 1); setMatched(new Set()); setSelectedLeft(null) }, 800)
}
} else {
// Wrong
setWrongPair(id)
setErrors(e => e + 1)
setFailedIds(prev => new Set([...prev, selectedLeft]))
setTimeout(() => {
setWrongPair(null)
setSelectedLeft(null)
setFlashNative(null)
}, 600)
setTimeout(() => { setWrongPair(null); setSelectedLeft(null) }, 600)
}
}, [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)
setRound(0); setMatched(new Set()); setFirstTryCorrect(0); setRetryCorrect(0)
setErrors(0); setFailedIds(new Set()); setIsComplete(false); setSelectedLeft(null)
}
if (isLoading) {
@@ -130,169 +90,107 @@ export default function MatchPage() {
const matchedTotal = round * 6 + matched.size
const isPerfect = isComplete && errors === 0
// Native helper panel: list of words in native language
const nativePanel = (
<div className={`${glassCard} rounded-2xl p-4`}>
<p className={`text-xs font-medium mb-3 ${isDark ? 'text-cyan-300/70' : 'text-cyan-600'}`}>
{nativeLang.toUpperCase()} · {t('english')} · {t('german')}
</p>
<div className="space-y-1.5">
{roundItems.map(item => {
const native = wordInNative(item.translations)
const isSelected = selectedLeft === item.id
const isMatched = matched.has(item.id)
return (
<div key={`n-${item.id}`}
className={`flex items-center gap-2 px-3 py-2.5 rounded-lg border text-sm transition-all ${
isMatched
? 'opacity-30 border-green-400/30 bg-green-500/5'
: isSelected
? (isDark ? 'border-cyan-400/50 bg-cyan-500/10 text-cyan-200' : 'border-cyan-400 bg-cyan-50 text-cyan-800')
: (isDark ? 'border-white/10 text-white/50' : 'border-slate-200 text-slate-500')
}`}>
<span className="flex-1 truncate">{native || '—'}</span>
{native && !isMatched && (
<AudioButton text={native} lang={nativeLang as 'en' | 'de'} isDark={isDark} size="sm" />
)}
</div>
)
})}
</div>
</div>
)
return (
<>
{/* Header */}
<div className={`${glassCard} border-0 border-b`}>
<div className="max-w-3xl mx-auto px-6 py-4 flex items-center justify-between">
<button onClick={() => router.push('/learn')} className={`flex items-center gap-2 text-sm ${isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-900'}`}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
{t('back')}
</button>
<h1 className={`text-lg font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>{t('match')}</h1>
<div className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
<span className="text-green-400">{firstTryCorrect}</span>{' '}
<span className="text-yellow-400">{retryCorrect}</span>{' '}
<span className="text-red-400">{errors}</span>
<ExerciseLayout
title={t('match')}
onBack={() => router.push('/learn')}
progress={{ current: matchedTotal, total: totalPairs }}
exerciseExplanation={undefined}
nativeHelper={nativePanel}
score={
<div className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
<span className="text-green-400">{firstTryCorrect}</span>{' '}
<span className="text-yellow-400">{retryCorrect}</span>{' '}
<span className="text-red-400">{errors}</span>
</div>
}
>
{isComplete ? (
<div className={`${glassCard} rounded-3xl p-10 text-center max-w-md mx-auto`}>
<StarRating stars={isPerfect ? 3 : accuracyToStars(firstTryCorrect, totalPairs)} size="lg" animated />
<h2 className={`text-2xl font-bold mt-4 mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
{isPerfect ? t('well_done') : t('all_matched')}
</h2>
<div className={`flex justify-center gap-6 mb-4 text-sm ${isDark ? 'text-white/70' : 'text-slate-600'}`}>
<div><span className="text-green-400 font-bold text-lg">{firstTryCorrect}</span><br/>{t('correct')}</div>
<div><span className="text-yellow-400 font-bold text-lg">{retryCorrect}</span><br/>2.</div>
<div><span className="text-red-400 font-bold text-lg">{errors}</span><br/>{t('errors')}</div>
</div>
<div className="flex gap-3">
<button onClick={restart} className="flex-1 py-3 rounded-xl bg-gradient-to-r from-indigo-500 to-violet-500 text-white font-medium">{t('again')}</button>
<button onClick={() => router.push('/learn')} className={`flex-1 py-3 rounded-xl border font-medium ${isDark ? 'border-white/20 text-white/80' : 'border-slate-300 text-slate-700'}`}>{t('back')}</button>
</div>
</div>
</div>
{/* Progress bar with count */}
<div className="max-w-3xl mx-auto w-full px-6 pt-3">
<div className="flex items-center gap-3">
<div className={`flex-1 h-2 rounded-full ${isDark ? 'bg-white/10' : 'bg-slate-200'}`}>
<div className="h-full rounded-full bg-gradient-to-r from-indigo-500 to-violet-500 transition-all" style={{ width: `${(matchedTotal / Math.max(totalPairs, 1)) * 100}%` }} />
) : (
<div className="grid grid-cols-2 gap-4">
{/* Left: English */}
<div className="space-y-2">
<p className={`text-xs font-medium text-center mb-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{t('english')} / English
</p>
{roundItems.map(item => (
<div key={`l-${item.id}`}
className={`flex items-center gap-2 p-3 min-h-[48px] rounded-xl border-2 text-sm font-medium transition-all ${
matched.has(item.id) ? 'opacity-30 border-green-400 bg-green-500/10'
: selectedLeft === item.id ? (isDark ? 'border-blue-400 bg-blue-500/20 text-white' : 'border-blue-500 bg-blue-50 text-blue-900')
: (isDark ? 'border-white/20 bg-white/5 text-white' : 'border-slate-200 bg-white text-slate-900')
}`}>
<button onClick={() => handleLeftTap(item.id)} disabled={matched.has(item.id)} className="flex-1 text-left">{item.question}</button>
{!matched.has(item.id) && <AudioButton text={item.question} lang="en" isDark={isDark} size="sm" />}
</div>
))}
</div>
<span className={`text-xs font-medium tabular-nums ${isDark ? 'text-white/50' : 'text-slate-400'}`}>
{matchedTotal}/{totalPairs}
</span>
</div>
</div>
{/* Native flash overlay */}
{flashNative && (
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 pointer-events-none">
<div className={`px-8 py-4 rounded-2xl text-2xl font-bold animate-pulse ${isDark ? 'bg-cyan-500/30 text-cyan-200 backdrop-blur-xl' : 'bg-cyan-100 text-cyan-800'}`}>
{flashNative}
{/* Right: German */}
<div className="space-y-2">
<p className={`text-xs font-medium text-center mb-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{t('german')} / Deutsch
</p>
{shuffledRight.map(item => (
<div key={`r-${item.id}`}
className={`flex items-center gap-2 p-3 min-h-[48px] rounded-xl border-2 text-sm font-medium transition-all ${
matched.has(item.id) ? 'opacity-30 border-green-400 bg-green-500/10'
: wrongPair === item.id ? (isDark ? 'border-red-400 bg-red-500/20 text-red-200 animate-pulse' : 'border-red-500 bg-red-50 text-red-800 animate-pulse')
: (isDark ? 'border-white/20 bg-white/5 text-white' : 'border-slate-200 bg-white text-slate-900')
}`}>
<button onClick={() => handleRightTap(item.id)} disabled={matched.has(item.id) || !selectedLeft} className="flex-1 text-left">{item.answer}</button>
{!matched.has(item.id) && <AudioButton text={item.answer} lang="de" isDark={isDark} size="sm" />}
</div>
))}
</div>
</div>
)}
<div className="flex-1 flex items-center justify-center px-6 py-8">
{isComplete ? (
<div className={`${glassCard} rounded-3xl p-10 text-center max-w-md w-full`}>
<StarRating stars={isPerfect ? 3 : accuracyToStars(firstTryCorrect, totalPairs)} size="lg" animated />
<h2 className={`text-2xl font-bold mt-4 mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
{isPerfect ? t('well_done') : t('all_matched')}
</h2>
<div className={`flex justify-center gap-6 mb-4 text-sm ${isDark ? 'text-white/70' : 'text-slate-600'}`}>
<div><span className="text-green-400 font-bold text-lg">{firstTryCorrect}</span><br/>{t('correct')}</div>
<div><span className="text-yellow-400 font-bold text-lg">{retryCorrect}</span><br/>2. Versuch</div>
<div><span className="text-red-400 font-bold text-lg">{errors}</span><br/>{t('errors')}</div>
</div>
{!isPerfect && (
<p className={`text-xs mb-4 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{isThirdLanguage
? (nativeLang === 'tr' ? 'Tam puan icin hatasiz tamamlayin' : 'Fuer volle Punkte fehlerfrei abschliessen')
: 'Fuer volle Punkte fehlerfrei abschliessen'}
</p>
)}
<div className="flex gap-3">
<button onClick={restart} className="flex-1 py-3 rounded-xl bg-gradient-to-r from-indigo-500 to-violet-500 text-white font-medium">
{t('again')}
</button>
<button onClick={() => router.push('/learn')} className={`flex-1 py-3 rounded-xl border font-medium ${isDark ? 'border-white/20 text-white/80' : 'border-slate-300 text-slate-700'}`}>
{t('back')}
</button>
</div>
</div>
) : (
<div className="w-full max-w-3xl">
{/* Column headers */}
<div className={`grid ${isThirdLanguage ? 'grid-cols-3' : 'grid-cols-2'} gap-4 mb-3`}>
<p className={`text-xs font-medium text-center ${isDark ? 'text-white/40' : 'text-slate-400'}`}>{t('english')}</p>
<p className={`text-xs font-medium text-center ${isDark ? 'text-white/40' : 'text-slate-400'}`}>{t('german')}</p>
{isThirdLanguage && (
<p className={`text-xs font-medium text-center ${isDark ? 'text-cyan-300/50' : 'text-cyan-600'}`}>{nativeLang.toUpperCase()}</p>
)}
</div>
{/* Grid */}
<div className={`grid ${isThirdLanguage ? 'grid-cols-3' : 'grid-cols-2'} gap-4`}>
{/* Left: English (click to select, no sound) */}
<div className="space-y-2">
{roundItems.map(item => (
<div
key={`l-${item.id}`}
className={`flex items-center gap-2 p-3 min-h-[48px] rounded-xl border-2 text-sm font-medium transition-all ${
matched.has(item.id)
? 'opacity-30 border-green-400 bg-green-500/10'
: selectedLeft === item.id
? (isDark ? 'border-blue-400 bg-blue-500/20 text-white' : 'border-blue-500 bg-blue-50 text-blue-900')
: (isDark ? 'border-white/20 bg-white/5 text-white' : 'border-slate-200 bg-white text-slate-900')
}`}
>
<button
onClick={() => handleLeftTap(item)}
disabled={matched.has(item.id)}
className="flex-1 text-left hover:opacity-80"
>
{item.question}
</button>
{!matched.has(item.id) && (
<AudioButton text={item.question} lang="en" isDark={isDark} size="sm" />
)}
</div>
))}
</div>
{/* Middle: German (click to match + sound) */}
<div className="space-y-2">
{shuffledRight.map(item => (
<div
key={`r-${item.id}`}
className={`flex items-center gap-2 p-3 min-h-[48px] rounded-xl border-2 text-sm font-medium transition-all ${
matched.has(item.id)
? 'opacity-30 border-green-400 bg-green-500/10'
: wrongPair === item.id
? (isDark ? 'border-red-400 bg-red-500/20 text-red-200 animate-pulse' : 'border-red-500 bg-red-50 text-red-800 animate-pulse')
: (isDark ? 'border-white/20 bg-white/5 text-white' : 'border-slate-200 bg-white text-slate-900')
}`}
>
<button
onClick={() => handleRightTap(item.id)}
disabled={matched.has(item.id) || !selectedLeft}
className="flex-1 text-left hover:opacity-80"
>
{item.answer}
</button>
{!matched.has(item.id) && (
<AudioButton text={item.answer} lang="de" isDark={isDark} size="sm" />
)}
</div>
))}
</div>
{/* Right: Native language (display only + sound) */}
{isThirdLanguage && (
<div className="space-y-2">
{roundItems.map(item => {
const native = wordInNative(item.translations)
return (
<div
key={`n-${item.id}`}
className={`flex items-center gap-2 p-3 min-h-[48px] rounded-xl border text-sm transition-all ${
matched.has(item.id)
? 'opacity-30 border-green-400/30 bg-green-500/5'
: selectedLeft === item.id
? (isDark ? 'border-cyan-400/50 bg-cyan-500/10 text-cyan-200' : 'border-cyan-500/50 bg-cyan-50 text-cyan-800')
: (isDark ? 'border-white/10 bg-white/3 text-white/50' : 'border-slate-100 bg-slate-50/50 text-slate-400')
}`}
>
<span className="flex-1">{native || '—'}</span>
{native && !matched.has(item.id) && (
<AudioButton text={native} lang={nativeLang as 'en' | 'de'} isDark={isDark} size="sm" />
)}
</div>
)
})}
</div>
)}
</div>
</div>
)}
</div>
</>
</ExerciseLayout>
)
}

View File

@@ -0,0 +1,136 @@
'use client'
import React from 'react'
import { useTheme } from '@/lib/ThemeContext'
import { useNativeLanguage } from '@/lib/useNativeLanguage'
interface ExerciseLayoutProps {
/** Main exercise content (2/3 left) */
children: React.ReactNode
/** Native language helper content for the right panel */
nativeHelper?: React.ReactNode
/** Explanation text for parents (shown above native words) */
exerciseExplanation?: string
/** Title for the exercise in the header */
title: string
/** Progress: current / total */
progress?: { current: number; total: number }
/** Back button handler */
onBack?: () => void
/** Score display */
score?: React.ReactNode
}
const explanations: Record<string, Record<string, string>> = {
match: {
de: 'Ihr Kind soll die richtige Uebersetzung finden. Es klickt zuerst ein Wort in der linken Spalte und dann die passende Uebersetzung in der rechten Spalte. In diesem Bereich sehen Sie die Woerter in Ihrer Sprache.',
en: 'Your child should find the correct translation. They click a word on the left, then the matching translation on the right. Here you see the words in your language.',
tr: 'Cocugunuz dogru ceviriyi bulmali. Soldaki bir kelimeye, sonra sagdaki dogru ceviriye tiklar. Bu alanda kelimeleri kendi dilinizde gorursunuz.',
ar: '\u064a\u062c\u0628 \u0639\u0644\u0649 \u0637\u0641\u0644\u0643 \u0625\u064a\u062c\u0627\u062f \u0627\u0644\u062a\u0631\u062c\u0645\u0629 \u0627\u0644\u0635\u062d\u064a\u062d\u0629. \u064a\u0646\u0642\u0631 \u0639\u0644\u0649 \u0643\u0644\u0645\u0629 \u0639\u0644\u0649 \u0627\u0644\u064a\u0633\u0627\u0631 \u062b\u0645 \u0627\u0644\u062a\u0631\u062c\u0645\u0629 \u0627\u0644\u0645\u0646\u0627\u0633\u0628\u0629 \u0639\u0644\u0649 \u0627\u0644\u064a\u0645\u064a\u0646. \u0647\u0646\u0627 \u062a\u0631\u0649 \u0627\u0644\u0643\u0644\u0645\u0627\u062a \u0628\u0644\u063a\u062a\u0643.',
uk: '\u0412\u0430\u0448\u0430 \u0434\u0438\u0442\u0438\u043d\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0437\u043d\u0430\u0439\u0442\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043f\u0435\u0440\u0435\u043a\u043b\u0430\u0434. \u0412\u043e\u043d\u0430 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u0454 \u043d\u0430 \u0441\u043b\u043e\u0432\u043e \u043b\u0456\u0432\u043e\u0440\u0443\u0447, \u043f\u043e\u0442\u0456\u043c \u043d\u0430 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u0438\u0439 \u043f\u0435\u0440\u0435\u043a\u043b\u0430\u0434 \u043f\u0440\u0430\u0432\u043e\u0440\u0443\u0447. \u0422\u0443\u0442 \u0432\u0438 \u0431\u0430\u0447\u0438\u0442\u0435 \u0441\u043b\u043e\u0432\u0430 \u0432\u0430\u0448\u043e\u044e \u043c\u043e\u0432\u043e\u044e.',
ru: '\u0412\u0430\u0448 \u0440\u0435\u0431\u0435\u043d\u043e\u043a \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0439\u0442\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043f\u0435\u0440\u0435\u0432\u043e\u0434. \u041e\u043d \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u0442 \u043d\u0430 \u0441\u043b\u043e\u0432\u043e \u0441\u043b\u0435\u0432\u0430, \u0437\u0430\u0442\u0435\u043c \u043d\u0430 \u043f\u0435\u0440\u0435\u0432\u043e\u0434 \u0441\u043f\u0440\u0430\u0432\u0430. \u0417\u0434\u0435\u0441\u044c \u0432\u044b \u0432\u0438\u0434\u0438\u0442\u0435 \u0441\u043b\u043e\u0432\u0430 \u043d\u0430 \u0432\u0430\u0448\u0435\u043c \u044f\u0437\u044b\u043a\u0435.',
pl: 'Twoje dziecko powinno znalezc poprawne tlumaczenie. Klika slowo po lewej, potem odpowiednie tlumaczenie po prawej. Tutaj widzisz slowa w swoim jezyku.',
},
flashcards: {
de: 'Ihr Kind sieht ein englisches Wort und soll die deutsche Uebersetzung wissen. Klicken Sie auf die Karte um sie umzudrehen. Hier sehen Sie die Woerter in Ihrer Sprache.',
tr: 'Cocugunuz bir Ingilizce kelime gorur ve Almanca cevirisini bilmeli. Karti cevirmek icin tiklayin. Burada kelimeleri kendi dilinizde gorursunuz.',
ar: '\u064a\u0631\u0649 \u0637\u0641\u0644\u0643 \u0643\u0644\u0645\u0629 \u0625\u0646\u062c\u0644\u064a\u0632\u064a\u0629 \u0648\u064a\u062c\u0628 \u0623\u0646 \u064a\u0639\u0631\u0641 \u0627\u0644\u062a\u0631\u062c\u0645\u0629 \u0627\u0644\u0623\u0644\u0645\u0627\u0646\u064a\u0629. \u0627\u0646\u0642\u0631 \u0644\u0642\u0644\u0628 \u0627\u0644\u0628\u0637\u0627\u0642\u0629. \u0647\u0646\u0627 \u062a\u0631\u0649 \u0627\u0644\u0643\u0644\u0645\u0627\u062a \u0628\u0644\u063a\u062a\u0643.',
uk: '\u0414\u0438\u0442\u0438\u043d\u0430 \u0431\u0430\u0447\u0438\u0442\u044c \u0430\u043d\u0433\u043b\u0456\u0439\u0441\u044c\u043a\u0435 \u0441\u043b\u043e\u0432\u043e \u0456 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0437\u043d\u0430\u0442\u0438 \u043d\u0456\u043c\u0435\u0446\u044c\u043a\u0438\u0439 \u043f\u0435\u0440\u0435\u043a\u043b\u0430\u0434. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u0442\u0430\u043d\u043d\u044f. \u0422\u0443\u0442 \u0441\u043b\u043e\u0432\u0430 \u0432\u0430\u0448\u043e\u044e \u043c\u043e\u0432\u043e\u044e.',
ru: '\u0420\u0435\u0431\u0435\u043d\u043e\u043a \u0432\u0438\u0434\u0438\u0442 \u0430\u043d\u0433\u043b\u0438\u0439\u0441\u043a\u043e\u0435 \u0441\u043b\u043e\u0432\u043e \u0438 \u0434\u043e\u043b\u0436\u0435\u043d \u0437\u043d\u0430\u0442\u044c \u043d\u0435\u043c\u0435\u0446\u043a\u0438\u0439 \u043f\u0435\u0440\u0435\u0432\u043e\u0434. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0447\u0442\u043e\u0431\u044b \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u0442\u044c. \u0417\u0434\u0435\u0441\u044c \u0441\u043b\u043e\u0432\u0430 \u043d\u0430 \u0432\u0430\u0448\u0435\u043c \u044f\u0437\u044b\u043a\u0435.',
pl: 'Dziecko widzi angielskie slowo i powinno znac niemieckie tlumaczenie. Kliknij aby odwrocic kartke. Tutaj slowa w Twoim jezyku.',
en: 'Your child sees an English word and should know the German translation. Click to flip the card. Here you see the words in your language.',
},
}
/**
* Standard exercise layout: 2/3 work area (left) + 1/3 native helper (right).
* The right panel only appears for non-DE/EN speakers.
*/
export function ExerciseLayout({
children,
nativeHelper,
exerciseExplanation,
title,
progress,
onBack,
score,
}: ExerciseLayoutProps) {
const { isDark } = useTheme()
const { nativeLang, isThirdLanguage, t } = useNativeLanguage()
const glassCard = isDark
? 'bg-white/10 backdrop-blur-xl border border-white/10'
: 'bg-white/80 backdrop-blur-xl border border-black/5'
const explanation = exerciseExplanation
|| explanations[title.toLowerCase()]?.[nativeLang]
|| explanations[title.toLowerCase()]?.['de']
|| ''
return (
<>
{/* Header */}
<div className={`${glassCard} border-0 border-b`}>
<div className="max-w-5xl mx-auto px-6 py-3 flex items-center justify-between">
{onBack ? (
<button onClick={onBack} className={`flex items-center gap-2 text-sm ${isDark ? 'text-white/60 hover:text-white' : 'text-slate-500 hover:text-slate-900'}`}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>
{t('back')}
</button>
) : <span />}
<h1 className={`text-lg font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>{title}</h1>
{score || <span />}
</div>
</div>
{/* Progress bar */}
{progress && (
<div className="max-w-5xl mx-auto w-full px-6 pt-3">
<div className="flex items-center gap-3">
<div className={`flex-1 h-2 rounded-full ${isDark ? 'bg-white/10' : 'bg-slate-200'}`}>
<div className="h-full rounded-full bg-gradient-to-r from-indigo-500 to-violet-500 transition-all"
style={{ width: `${(progress.current / Math.max(progress.total, 1)) * 100}%` }} />
</div>
<span className={`text-xs font-medium tabular-nums ${isDark ? 'text-white/50' : 'text-slate-400'}`}>
{progress.current}/{progress.total}
</span>
</div>
</div>
)}
{/* Main content: 2/3 left + 1/3 right */}
<div className="max-w-5xl mx-auto w-full px-6 py-6">
<div className={`flex gap-6 ${isThirdLanguage ? '' : ''}`}>
{/* Left: Exercise area (2/3 or full) */}
<div className={isThirdLanguage ? 'w-2/3' : 'w-full'}>
{children}
</div>
{/* Right: Native language helper (1/3) — only for migrants */}
{isThirdLanguage && (
<div className="w-1/3">
<div className={`sticky top-20 space-y-4`}>
{/* Explanation card */}
{explanation && (
<div className={`${glassCard} rounded-2xl p-4`}>
<h3 className={`text-xs font-semibold mb-2 ${isDark ? 'text-cyan-300' : 'text-cyan-700'}`}>
{nativeLang.toUpperCase()} · {t('english')} · {t('german')}
</h3>
<p className={`text-xs leading-relaxed ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
{explanation}
</p>
</div>
)}
{/* Native words */}
{nativeHelper}
</div>
</div>
)}
</div>
</div>
</>
)
}
export { explanations as exerciseExplanations }