171 lines
7.6 KiB
TypeScript
171 lines
7.6 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
|
import { useParams, useRouter } from 'next/navigation'
|
|
import { useTheme } from '@/lib/ThemeContext'
|
|
import { StarRating, accuracyToStars } from '@/components/gamification/StarRating'
|
|
|
|
interface QAItem { id: string; question: string; answer: string }
|
|
|
|
function getApiBase() { return '' }
|
|
|
|
export default function MatchPage() {
|
|
const { unitId } = useParams<{ unitId: string }>()
|
|
const router = useRouter()
|
|
const { isDark } = useTheme()
|
|
|
|
const [allItems, setAllItems] = useState<QAItem[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [round, setRound] = useState(0)
|
|
const [selectedLeft, setSelectedLeft] = useState<string | null>(null)
|
|
const [matched, setMatched] = useState<Set<string>>(new Set())
|
|
const [wrongPair, setWrongPair] = useState<string | null>(null)
|
|
const [errors, setErrors] = useState(0)
|
|
const [isComplete, setIsComplete] = useState(false)
|
|
|
|
const glassCard = isDark
|
|
? 'bg-white/10 backdrop-blur-xl border border-white/10'
|
|
: 'bg-white/80 backdrop-blur-xl border border-black/5'
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
try {
|
|
const resp = await fetch(`${getApiBase()}/api/learning-units/${unitId}/qa`)
|
|
if (resp.ok) { const d = await resp.json(); setAllItems(d.qa_items || []) }
|
|
} catch {} finally { setIsLoading(false) }
|
|
})()
|
|
}, [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)
|
|
setWrongPair(null)
|
|
}, [selectedLeft, matched])
|
|
|
|
const handleRightTap = useCallback((id: string) => {
|
|
if (!selectedLeft || matched.has(id)) return
|
|
|
|
if (selectedLeft === id) {
|
|
// Correct match
|
|
setMatched(prev => new Set([...prev, id]))
|
|
setSelectedLeft(null)
|
|
|
|
// Check if round complete
|
|
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)
|
|
}
|
|
}
|
|
} else {
|
|
// Wrong match
|
|
setWrongPair(id)
|
|
setErrors(e => e + 1)
|
|
setTimeout(() => {
|
|
setWrongPair(null)
|
|
setSelectedLeft(null)
|
|
}, 600)
|
|
}
|
|
}, [selectedLeft, matched, roundItems, round, allItems])
|
|
|
|
if (isLoading) {
|
|
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-slate-100 via-indigo-50 to-violet-100'}`}>
|
|
<div className="w-8 h-8 border-4 border-indigo-400 border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
}
|
|
|
|
const totalPairs = allItems.length
|
|
const matchedTotal = round * 6 + matched.size
|
|
|
|
return (
|
|
<div className={`min-h-screen flex flex-col ${isDark ? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800' : 'bg-gradient-to-br from-slate-100 via-indigo-50 to-violet-100'}`}>
|
|
{/* Header */}
|
|
<div className={`${glassCard} border-0 border-b`}>
|
|
<div className="max-w-2xl 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>
|
|
Zurueck
|
|
</button>
|
|
<h1 className={`text-lg font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>Zuordnen</h1>
|
|
<span className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>{matchedTotal}/{totalPairs}</span>
|
|
</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={accuracyToStars(totalPairs, totalPairs + errors)} size="lg" animated />
|
|
<h2 className={`text-2xl font-bold mt-4 mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>Alle zugeordnet!</h2>
|
|
<p className={`mb-6 ${isDark ? 'text-white/60' : 'text-slate-500'}`}>{errors} Fehler</p>
|
|
<div className="flex gap-3">
|
|
<button onClick={() => { setRound(0); setMatched(new Set()); setErrors(0); setIsComplete(false) }} className="flex-1 py-3 rounded-xl bg-gradient-to-r from-indigo-500 to-violet-500 text-white font-medium">Nochmal</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'}`}>Zurueck</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="w-full max-w-2xl grid grid-cols-2 gap-6">
|
|
{/* Left column: English */}
|
|
<div className="space-y-2">
|
|
<p className={`text-xs font-medium text-center mb-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>English</p>
|
|
{roundItems.map(item => (
|
|
<button
|
|
key={`l-${item.id}`}
|
|
onClick={() => handleLeftTap(item.id)}
|
|
disabled={matched.has(item.id)}
|
|
className={`w-full p-3 rounded-xl border-2 text-sm font-medium transition-all ${
|
|
matched.has(item.id)
|
|
? 'opacity-30 border-green-400 bg-green-500/10 cursor-default'
|
|
: 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 hover:bg-white/10' : 'border-slate-200 bg-white text-slate-900 hover:bg-slate-50')
|
|
}`}
|
|
>
|
|
{item.question}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Right column: German (shuffled) */}
|
|
<div className="space-y-2">
|
|
<p className={`text-xs font-medium text-center mb-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>Deutsch</p>
|
|
{shuffledRight.map(item => (
|
|
<button
|
|
key={`r-${item.id}`}
|
|
onClick={() => handleRightTap(item.id)}
|
|
disabled={matched.has(item.id) || !selectedLeft}
|
|
className={`w-full p-3 rounded-xl border-2 text-sm font-medium transition-all ${
|
|
matched.has(item.id)
|
|
? 'opacity-30 border-green-400 bg-green-500/10 cursor-default'
|
|
: 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 hover:bg-white/10' : 'border-slate-200 bg-white text-slate-900 hover:bg-slate-50')
|
|
}`}
|
|
>
|
|
{item.answer}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|