Phase 1.1 — user_language_api.py: Stores native language preference per user (TR/AR/UK/RU/PL/DE/EN). Onboarding page with flag-based language selection for students and parents. Phase 1.2 — translation_service.py: Batch-translates vocabulary words into target languages via Ollama LLM. Stores in translations JSONB. New endpoint POST /vocabulary/translate triggers translation. Phase 2.1 — Parent Portal (/parent): Simplified UI in parent's native language showing child's learning progress. Daily tips translated. Phase 2.2 — Parent Quiz (/parent/quiz/[unitId]): Parents can quiz their child on vocabulary WITHOUT speaking DE or EN. Shows word in child's learning language + parent's native language as hint. Answer hidden by default, revealed on tap. All UI text translated into 7 languages (DE/EN/TR/AR/UK/RU/PL). Arabic gets RTL layout support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
176 lines
7.1 KiB
TypeScript
176 lines
7.1 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useTheme } from '@/lib/ThemeContext'
|
|
import { useLanguage } from '@/lib/LanguageContext'
|
|
|
|
interface LangOption {
|
|
code: string
|
|
name: string
|
|
name_native: string
|
|
flag: string
|
|
rtl: boolean
|
|
}
|
|
|
|
const STORAGE_KEY = 'bp_native_language'
|
|
|
|
export default function OnboardingPage() {
|
|
const router = useRouter()
|
|
const { isDark } = useTheme()
|
|
const { setLanguage } = useLanguage()
|
|
const [languages, setLanguages] = useState<LangOption[]>([])
|
|
const [selected, setSelected] = useState<string | null>(null)
|
|
const [role, setRole] = useState<'student' | 'parent'>('student')
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
// Skip if already onboarded
|
|
useEffect(() => {
|
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
if (stored) router.replace('/learn')
|
|
}, [router])
|
|
|
|
// Load supported languages
|
|
useEffect(() => {
|
|
fetch('/api/user/languages')
|
|
.then(r => r.ok ? r.json() : null)
|
|
.then(d => { if (d?.languages) setLanguages(d.languages) })
|
|
.catch(() => {
|
|
// Fallback if API unavailable
|
|
setLanguages([
|
|
{ code: 'de', name: 'Deutsch', name_native: 'Deutsch', flag: 'de', rtl: false },
|
|
{ code: 'tr', name: 'Tuerkisch', name_native: 'Turkce', flag: 'tr', rtl: false },
|
|
{ code: 'ar', name: 'Arabisch', name_native: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629', flag: 'sy', rtl: true },
|
|
{ code: 'uk', name: 'Ukrainisch', name_native: '\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430', flag: 'ua', rtl: false },
|
|
{ code: 'ru', name: 'Russisch', name_native: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439', flag: 'ru', rtl: false },
|
|
{ code: 'pl', name: 'Polnisch', name_native: 'Polski', flag: 'pl', rtl: false },
|
|
])
|
|
})
|
|
}, [])
|
|
|
|
const handleContinue = async () => {
|
|
if (!selected) return
|
|
setSaving(true)
|
|
|
|
// Save to backend
|
|
try {
|
|
await fetch('/api/user/language-preference?user_id=default', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ native_language: selected, role }),
|
|
})
|
|
} catch {}
|
|
|
|
// Save locally + set UI language
|
|
localStorage.setItem(STORAGE_KEY, selected)
|
|
setLanguage(selected)
|
|
|
|
// Navigate
|
|
if (role === 'parent') {
|
|
router.push('/parent')
|
|
} else {
|
|
router.push('/learn')
|
|
}
|
|
}
|
|
|
|
const greetings: Record<string, string> = {
|
|
de: 'Willkommen!',
|
|
en: 'Welcome!',
|
|
tr: 'Hos geldiniz!',
|
|
ar: '\u0645\u0631\u062d\u0628\u0627!',
|
|
uk: '\u041b\u0430\u0441\u043a\u0430\u0432\u043e \u043f\u0440\u043e\u0441\u0438\u043c\u043e!',
|
|
ru: '\u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c!',
|
|
pl: 'Witamy!',
|
|
}
|
|
|
|
return (
|
|
<div className={`min-h-screen flex items-center justify-center ${
|
|
isDark ? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800' : 'bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50'
|
|
}`}>
|
|
<div className={`w-full max-w-md mx-4 rounded-3xl p-8 ${
|
|
isDark ? 'bg-white/10 backdrop-blur-xl border border-white/20' : 'bg-white shadow-2xl border border-slate-200'
|
|
}`}>
|
|
{/* Greeting in multiple languages */}
|
|
<div className="text-center mb-8">
|
|
<div className="text-4xl mb-3">
|
|
{selected ? greetings[selected] || greetings.de : (
|
|
<span className={`${isDark ? 'text-white/80' : 'text-slate-700'}`}>
|
|
{Object.values(greetings).slice(0, 4).join(' / ')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<h1 className={`text-xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
BreakPilot Sprache
|
|
</h1>
|
|
</div>
|
|
|
|
{/* Role Selection */}
|
|
<div className="flex gap-2 mb-6">
|
|
{(['student', 'parent'] as const).map(r => (
|
|
<button
|
|
key={r}
|
|
onClick={() => setRole(r)}
|
|
className={`flex-1 py-3 rounded-xl text-sm font-medium transition-all ${
|
|
role === r
|
|
? 'bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-md'
|
|
: isDark ? 'bg-white/5 text-white/50 hover:bg-white/10' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
|
}`}
|
|
>
|
|
{r === 'student' ? 'Schueler / Student' : 'Elternteil / Parent'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Question */}
|
|
<p className={`text-center text-sm mb-4 ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
|
{role === 'student' ? 'Was ist deine Muttersprache?' : 'Welche Sprache sprechen Sie?'}
|
|
</p>
|
|
|
|
{/* Language Grid */}
|
|
<div className="grid grid-cols-2 gap-2 mb-6">
|
|
{languages.map(lang => (
|
|
<button
|
|
key={lang.code}
|
|
onClick={() => setSelected(lang.code)}
|
|
className={`p-3 rounded-xl text-left transition-all ${
|
|
selected === lang.code
|
|
? (isDark ? 'bg-blue-500/30 border-2 border-blue-400 ring-2 ring-blue-400/30' : 'bg-blue-50 border-2 border-blue-500 ring-2 ring-blue-500/20')
|
|
: (isDark ? 'bg-white/5 border-2 border-transparent hover:bg-white/10' : 'bg-slate-50 border-2 border-transparent hover:bg-slate-100')
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-lg">{lang.flag === 'de' ? '\uD83C\uDDE9\uD83C\uDDEA' : lang.flag === 'tr' ? '\uD83C\uDDF9\uD83C\uDDF7' : lang.flag === 'sy' ? '\uD83C\uDDF8\uD83C\uDDFE' : lang.flag === 'ua' ? '\uD83C\uDDFA\uD83C\uDDE6' : lang.flag === 'ru' ? '\uD83C\uDDF7\uD83C\uDDFA' : lang.flag === 'pl' ? '\uD83C\uDDF5\uD83C\uDDF1' : lang.flag === 'gb' ? '\uD83C\uDDEC\uD83C\uDDE7' : '\uD83C\uDFF3\uFE0F'}</span>
|
|
<div>
|
|
<div className={`text-sm font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>{lang.name_native}</div>
|
|
<div className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>{lang.name}</div>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Continue */}
|
|
<button
|
|
onClick={handleContinue}
|
|
disabled={!selected || saving}
|
|
className={`w-full py-4 rounded-xl font-semibold text-lg transition-all ${
|
|
selected
|
|
? 'bg-gradient-to-r from-blue-500 to-cyan-500 text-white hover:shadow-lg hover:shadow-blue-500/25'
|
|
: isDark ? 'bg-white/5 text-white/30' : 'bg-slate-100 text-slate-400'
|
|
}`}
|
|
>
|
|
{saving ? 'Wird gespeichert...' : selected ? (
|
|
selected === 'de' ? 'Weiter' :
|
|
selected === 'tr' ? 'Devam' :
|
|
selected === 'ar' ? '\u0627\u0633\u062a\u0645\u0631' :
|
|
selected === 'uk' ? '\u041f\u0440\u043e\u0434\u043e\u0432\u0436\u0438\u0442\u0438' :
|
|
selected === 'ru' ? '\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c' :
|
|
selected === 'pl' ? 'Dalej' :
|
|
'Continue'
|
|
) : 'Sprache waehlen'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|