Redesign Parent Quiz: explanation panel + trilingual buttons

Major improvements for non-DE/EN speaking parents:

1. Right panel: Explains in parent's native language what this
   exercise is about and how it works (TR/AR/UK/RU/PL translations)

2. Trilingual buttons: "Dogru" (primary) with "Richtig / Correct"
   subtitle so parents understand even if language detection is wrong

3. Native word shown prominently: "= elma" next to "apple"

4. Audio buttons labeled EN/DE/TR with language codes

5. Answer card shows all 3 languages: English, Deutsch, native

6. Progress tracker in the explanation panel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-27 08:16:30 +02:00
parent 9de26701dd
commit 7f8743d1e3

View File

@@ -12,7 +12,27 @@ interface QAItem {
translations?: Record<string, { text?: string }>
}
// UI translations now come from useNativeLanguage() hook + exerciseTranslations.ts
// Context explanations in all languages
const parentGuide: Record<string, Record<string, string>> = {
title: {
de: 'Vokabelabfrage fuer Ihr Kind',
en: 'Vocabulary quiz for your child',
tr: 'Cocugunuz icin kelime sorgusu',
ar: '\u0627\u062e\u062a\u0628\u0627\u0631 \u0645\u0641\u0631\u062f\u0627\u062a \u0644\u0637\u0641\u0644\u0643',
uk: '\u041e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u043b\u0456\u0432 \u0434\u043b\u044f \u0432\u0430\u0448\u043e\u0457 \u0434\u0438\u0442\u0438\u043d\u0438',
ru: '\u041e\u043f\u0440\u043e\u0441 \u0441\u043b\u043e\u0432 \u0434\u043b\u044f \u0432\u0430\u0448\u0435\u0433\u043e \u0440\u0435\u0431\u0435\u043d\u043a\u0430',
pl: 'Test slowek dla Twojego dziecka',
},
how_it_works: {
de: 'So funktioniert es: Druecken Sie den Lautsprecher — Ihr Kind hoert das englische Wort und sagt die deutsche Uebersetzung. Sie sehen die richtige Antwort und koennen pruefen ob Ihr Kind richtig liegt.',
en: 'How it works: Press the speaker — your child hears the English word and says the German translation. You see the correct answer and can check.',
tr: 'Nasil calisir: Hoparlore basin — cocugunuz Ingilizce kelimeyi duyar ve Almanca cevirisini soyler. Siz dogru cevabi gorursunuz ve kontrol edebilirsiniz.',
ar: '\u0643\u064a\u0641 \u064a\u0639\u0645\u0644: \u0627\u0636\u063a\u0637 \u0639\u0644\u0649 \u0627\u0644\u0645\u0643\u0628\u0631 \u2014 \u0637\u0641\u0644\u0643 \u064a\u0633\u0645\u0639 \u0627\u0644\u0643\u0644\u0645\u0629 \u0628\u0627\u0644\u0625\u0646\u062c\u0644\u064a\u0632\u064a\u0629 \u0648\u064a\u0642\u0648\u0644 \u0627\u0644\u062a\u0631\u062c\u0645\u0629 \u0628\u0627\u0644\u0623\u0644\u0645\u0627\u0646\u064a\u0629. \u062a\u0631\u0649 \u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0627\u0644\u0635\u062d\u064a\u062d\u0629 \u0648\u064a\u0645\u0643\u0646\u0643 \u0627\u0644\u062a\u062d\u0642\u0642.',
uk: '\u042f\u043a \u0446\u0435 \u043f\u0440\u0430\u0446\u044e\u0454: \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0434\u0438\u043d\u0430\u043c\u0456\u043a \u2014 \u0434\u0438\u0442\u0438\u043d\u0430 \u043f\u043e\u0447\u0443\u0454 \u0430\u043d\u0433\u043b\u0456\u0439\u0441\u044c\u043a\u0435 \u0441\u043b\u043e\u0432\u043e \u0456 \u043a\u0430\u0436\u0435 \u043d\u0456\u043c\u0435\u0446\u044c\u043a\u0438\u0439 \u043f\u0435\u0440\u0435\u043a\u043b\u0430\u0434. \u0412\u0438 \u0431\u0430\u0447\u0438\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0443 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u044c.',
ru: '\u041a\u0430\u043a \u044d\u0442\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442: \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0434\u0438\u043d\u0430\u043c\u0438\u043a \u2014 \u0440\u0435\u0431\u0435\u043d\u043e\u043a \u0443\u0441\u043b\u044b\u0448\u0438\u0442 \u0430\u043d\u0433\u043b\u0438\u0439\u0441\u043a\u043e\u0435 \u0441\u043b\u043e\u0432\u043e \u0438 \u0441\u043a\u0430\u0436\u0435\u0442 \u043d\u0435\u043c\u0435\u0446\u043a\u0438\u0439 \u043f\u0435\u0440\u0435\u0432\u043e\u0434. \u0412\u044b \u0432\u0438\u0434\u0438\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442.',
pl: 'Jak to dziala: Nacisnij glosnik — dziecko slyszy angielskie slowo i mowi niemiecki odpowiednik. Widzisz poprawna odpowiedz i mozesz sprawdzic.',
},
}
export default function ParentQuizPage() {
const { unitId } = useParams<{ unitId: string }>()
@@ -27,11 +47,14 @@ export default function ParentQuizPage() {
const [isLoading, setIsLoading] = useState(true)
const [stats, setStats] = useState({ correct: 0, incorrect: 0 })
const [isComplete, setIsComplete] = useState(false)
const [showGuide, setShowGuide] = useState(true)
const glassCard = isDark
? 'bg-white/10 backdrop-blur-xl border border-white/10'
: 'bg-white/80 backdrop-blur-xl border border-black/5'
const pg = (key: string) => parentGuide[key]?.[nativeLang] || parentGuide[key]?.['de'] || key
useEffect(() => {
fetch(`/api/learning-units/${unitId}/qa`)
.then(r => r.ok ? r.json() : { qa_items: [] })
@@ -46,34 +69,33 @@ export default function ParentQuizPage() {
incorrect: prev.incorrect + (correct ? 0 : 1),
}))
setShowAnswer(false)
if (currentIndex + 1 >= items.length) { setIsComplete(true) }
else { setCurrentIndex(i => i + 1) }
if (currentIndex + 1 >= items.length) setIsComplete(true)
else setCurrentIndex(i => i + 1)
}, [currentIndex, items.length])
const currentItem = items[currentIndex]
const nativeTranslation = wordInNative(currentItem?.translations)
const nativeWord = wordInNative(currentItem?.translations)
if (isLoading) {
return (
<div className="flex items-center justify-center py-20">
return <div className="flex items-center justify-center py-20">
<div className="w-8 h-8 border-4 border-blue-400 border-t-transparent rounded-full animate-spin" />
</div>
)
}
return (
<div dir={language === 'ar' ? 'rtl' : 'ltr'}>
<div dir={nativeLang === 'ar' ? 'rtl' : 'ltr'}>
{/* Header */}
<div className={`${glassCard} border-0 border-b`}>
<div className="max-w-lg mx-auto px-6 py-4 flex items-center justify-between">
<div className="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between">
<button onClick={() => router.push('/parent')} className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
{t('back')}
</button>
<span className={`font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{t('question')} {currentIndex + 1}/{items.length}
</span>
<span />
<button onClick={() => setShowGuide(!showGuide)} className={`text-sm ${isDark ? 'text-blue-300' : 'text-blue-600'}`}>
{showGuide ? '?' : '?'}
</button>
</div>
</div>
@@ -82,9 +104,9 @@ export default function ParentQuizPage() {
<div className="h-full bg-gradient-to-r from-blue-500 to-cyan-500 transition-all" style={{ width: `${(currentIndex / Math.max(items.length, 1)) * 100}%` }} />
</div>
<div className="flex-1 flex items-center justify-center px-6 py-8">
<div className="max-w-4xl mx-auto px-6 py-6">
{isComplete ? (
<div className={`${glassCard} rounded-3xl p-10 text-center max-w-md w-full`}>
<div className={`${glassCard} rounded-3xl p-10 text-center max-w-md mx-auto`}>
<div className="text-5xl mb-4">{stats.correct > stats.incorrect ? '\uD83C\uDF89' : '\uD83D\uDCAA'}</div>
<h2 className={`text-2xl font-bold mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>{t('done')}</h2>
<p className={`text-lg mb-6 ${isDark ? 'text-white/70' : 'text-slate-600'}`}>
@@ -92,32 +114,50 @@ export default function ParentQuizPage() {
</p>
<div className="flex gap-3">
<button onClick={() => { setCurrentIndex(0); setStats({ correct: 0, incorrect: 0 }); setIsComplete(false) }}
className="flex-1 py-3 rounded-xl bg-gradient-to-r from-blue-500 to-cyan-500 text-white font-medium">{t('again')}</button>
className="flex-1 py-3 rounded-xl bg-gradient-to-r from-blue-500 to-cyan-500 text-white font-medium">
{t('again')}
</button>
<button onClick={() => router.push('/parent')}
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>
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>
) : currentItem ? (
<div className="w-full max-w-md space-y-6">
{/* Instruction for parent */}
<p className={`text-center text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
<div className="flex gap-6">
{/* Left: Quiz Card */}
<div className="flex-1 space-y-5">
{/* Instruction */}
<p className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
{t('ask_child')}
</p>
{/* Word to ask */}
{/* Word Card */}
<div className={`${glassCard} rounded-3xl p-8 text-center`}>
<span className={`text-3xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{/* English word */}
<span className={`text-xs font-medium ${isDark ? 'text-white/30' : 'text-slate-400'}`}>ENGLISH</span>
<p className={`text-3xl font-bold mt-1 ${isDark ? 'text-white' : 'text-slate-900'}`}>
{currentItem.question}
</span>
{nativeTranslation && (
<p className={`text-lg mt-3 ${isDark ? 'text-blue-300/70' : 'text-blue-600'}`}>
({nativeTranslation})
</p>
{/* Native translation */}
{nativeWord && (
<p className={`text-lg mt-3 ${isDark ? 'text-cyan-300/80' : 'text-cyan-700'}`}>
= {nativeWord}
</p>
)}
{/* Audio buttons */}
<div className="flex justify-center gap-3 mt-4">
<div className="flex flex-col items-center gap-1">
<AudioButton text={currentItem.question} lang="en" isDark={isDark} size="md" />
{nativeTranslation && (
<AudioButton text={nativeTranslation} lang={nativeLang as 'en' | 'de'} isDark={isDark} size="md" />
<span className={`text-[10px] ${isDark ? 'text-white/30' : 'text-slate-400'}`}>EN</span>
</div>
{nativeWord && (
<div className="flex flex-col items-center gap-1">
<AudioButton text={nativeWord} lang={nativeLang as 'en' | 'de'} isDark={isDark} size="md" />
<span className={`text-[10px] ${isDark ? 'text-white/30' : 'text-slate-400'}`}>{nativeLang.toUpperCase()}</span>
</div>
)}
</div>
</div>
@@ -132,35 +172,72 @@ export default function ParentQuizPage() {
{showAnswer ? t('hide_answer') : t('show_answer')}
</button>
{/* Answer */}
{showAnswer && (
<div className={`${glassCard} rounded-2xl p-6 text-center`}>
<p className={`text-xs mb-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>{t('correct_answer')}</p>
<span className={`text-2xl font-bold ${isDark ? 'text-green-300' : 'text-green-700'}`}>
<p className={`text-xs mb-1 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>{t('correct_answer')}</p>
<span className={`text-xs ${isDark ? 'text-white/30' : 'text-slate-400'}`}>DEUTSCH</span>
<p className={`text-2xl font-bold ${isDark ? 'text-green-300' : 'text-green-700'}`}>
{currentItem.answer}
</span>
{nativeTranslation && (
<p className={`text-sm mt-1 ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
({nativeTranslation})
</p>
{nativeWord && (
<p className={`text-sm mt-1 ${isDark ? 'text-cyan-300/60' : 'text-cyan-600'}`}>
= {nativeWord}
</p>
)}
<div className="flex justify-center mt-3">
<div className="flex justify-center gap-3 mt-3">
<div className="flex flex-col items-center gap-1">
<AudioButton text={currentItem.answer} lang="de" isDark={isDark} size="md" />
<span className={`text-[10px] ${isDark ? 'text-white/30' : 'text-slate-400'}`}>DE</span>
</div>
</div>
</div>
)}
{/* Right/Wrong Buttons */}
{/* Trilingual Buttons */}
<div className="flex gap-4">
<button onClick={() => handleResult(false)}
className="flex-1 py-4 rounded-2xl font-semibold bg-gradient-to-r from-red-500 to-rose-500 text-white hover:shadow-lg transition-all">
{t('wrong')}
<span className="text-lg">{t('wrong')}</span>
{isThirdLanguage && <span className={`block text-xs mt-0.5 text-white/60`}>Falsch / Wrong</span>}
</button>
<button onClick={() => handleResult(true)}
className="flex-1 py-4 rounded-2xl font-semibold bg-gradient-to-r from-green-500 to-emerald-500 text-white hover:shadow-lg transition-all">
{t('correct')}
<span className="text-lg">{t('correct')}</span>
{isThirdLanguage && <span className={`block text-xs mt-0.5 text-white/60`}>Richtig / Correct</span>}
</button>
</div>
</div>
{/* Right: Explanation Card (in parent's native language) */}
{showGuide && (
<div className="w-72 flex-shrink-0">
<div className={`${glassCard} rounded-2xl p-5 sticky top-6`}>
<h3 className={`text-sm font-semibold mb-3 ${isDark ? 'text-cyan-300' : 'text-cyan-700'}`}>
{pg('title')}
</h3>
<p className={`text-xs leading-relaxed ${isDark ? 'text-white/60' : 'text-slate-600'}`}>
{pg('how_it_works')}
</p>
<div className={`mt-4 pt-3 border-t ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
<p className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{items.length} {t('cards').toLowerCase()} · {t('question')} {currentIndex + 1}
</p>
<div className={`mt-2 h-1.5 rounded-full ${isDark ? 'bg-white/10' : 'bg-slate-100'}`}>
<div className="h-full rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 transition-all"
style={{ width: `${((currentIndex + 1) / items.length) * 100}%` }} />
</div>
{stats.correct + stats.incorrect > 0 && (
<p className={`text-xs mt-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{stats.correct} &nbsp; {stats.incorrect}
</p>
)}
</div>
</div>
</div>
)}
</div>
) : null}
</div>
</div>