163 lines
6.3 KiB
TypeScript
163 lines
6.3 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { User, UserCheck } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface Props {
|
|
sessionCode: string
|
|
quizTitle: string
|
|
schoolName: string | null
|
|
className: string | null
|
|
sessionId: string
|
|
}
|
|
|
|
export default function StudentJoinClient({ sessionCode, quizTitle, schoolName, className }: Props) {
|
|
const router = useRouter()
|
|
const [firstName, setFirstName] = useState('')
|
|
const [lastName, setLastName] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const handleJoin = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const res = await fetch('/api/student/join', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
short_code: sessionCode,
|
|
first_name: firstName.trim(),
|
|
last_name: lastName.trim(),
|
|
}),
|
|
})
|
|
|
|
const data = await res.json()
|
|
|
|
if (!res.ok) {
|
|
if (data.code === 'SESSION_INACTIVE') {
|
|
setError('Ce quiz est terminé. Vous ne pouvez plus rejoindre cette session.')
|
|
} else {
|
|
setError(data.error ?? 'Erreur lors de la connexion')
|
|
}
|
|
} else {
|
|
router.push(`/quiz/${sessionCode}/exam?pid=${data.participation.id}`)
|
|
}
|
|
} catch {
|
|
setError('Erreur réseau. Vérifiez votre connexion.')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background flex flex-col">
|
|
{/* Header */}
|
|
<header className="flex items-center justify-between px-10 py-5 border-b border-border">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center shadow-md shadow-primary/30">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z" fill="white"/>
|
|
</svg>
|
|
</div>
|
|
<span className="text-lg font-bold text-text-primary">SolyQuiz</span>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="flex-1 flex items-center justify-center p-4">
|
|
<div className="w-full max-w-md">
|
|
<div className="card overflow-hidden shadow-2xl shadow-black/40">
|
|
{/* Gradient header */}
|
|
<div className="relative h-28 bg-gradient-to-br from-primary/80 to-blue-700/60 flex items-center justify-center overflow-hidden">
|
|
<div className="absolute w-40 h-40 rounded-full bg-white/10 blur-xl -right-5 -top-5" />
|
|
<div className="relative z-10 text-center">
|
|
<div className="w-12 h-12 bg-white/15 backdrop-blur-sm border border-white/20 rounded-xl flex items-center justify-center mx-auto mb-1">
|
|
<UserCheck size={24} className="text-white" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-8">
|
|
<div className="text-center mb-6">
|
|
<h1 className="text-xl font-bold text-text-primary mb-1">{quizTitle}</h1>
|
|
<div className="flex items-center justify-center gap-3 text-xs text-text-muted">
|
|
{schoolName && <span>📍 {schoolName}</span>}
|
|
{className && <span>🎓 {className}</span>}
|
|
<span className="font-mono bg-background-elevated px-2 py-0.5 rounded text-primary font-semibold">
|
|
#{sessionCode}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleJoin} className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-text-secondary uppercase tracking-wider mb-1.5 ml-1">
|
|
Prénom
|
|
</label>
|
|
<div className="relative">
|
|
<User size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
|
|
<input
|
|
type="text"
|
|
value={firstName}
|
|
onChange={(e) => setFirstName(e.target.value)}
|
|
placeholder="Votre prénom"
|
|
className="input-field pl-9"
|
|
required
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-text-secondary uppercase tracking-wider mb-1.5 ml-1">
|
|
Nom
|
|
</label>
|
|
<div className="relative">
|
|
<User size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
|
|
<input
|
|
type="text"
|
|
value={lastName}
|
|
onChange={(e) => setLastName(e.target.value)}
|
|
placeholder="Votre nom de famille"
|
|
className="input-field pl-9"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-500/10 border border-red-500/30 text-red-400 text-sm px-4 py-3 rounded-lg">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className={cn('btn-primary w-full justify-center py-3', loading && 'opacity-70 cursor-not-allowed')}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<svg className="animate-spin w-4 h-4" viewBox="0 0 24 24" fill="none">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
</svg>
|
|
Connexion...
|
|
</>
|
|
) : (
|
|
'Commencer le quiz'
|
|
)}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|