Add migration learning platform: Onboarding, Translation, Parent Portal
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>
This commit is contained in:
175
studio-v2/app/onboarding/page.tsx
Normal file
175
studio-v2/app/onboarding/page.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user