'use client' import { useEffect, useRef, useState } from 'react' import { useBack } from '@/hooks/useBack' import Image from 'next/image' import { useVisitor } from '@/context/VisitorContext' import { t, tPlain } from '@/lib/i18n' import { trackEvent } from '@/lib/stats' import type { SectionDTO, TranslationAndResourceDTO, ResourceDTO } from '@/lib/api/types' import AppBar from '@/components/ui/AppBar' import ResourceViewer from '@/components/ui/ResourceViewer' interface Props { section: SectionDTO slug: string configId: string languages: string[] } type Phase = 'quiz' | 'result' | 'review' function isAudio(r: ResourceDTO) { const t = r.type as string | number | undefined return t === 'Audio' || t === 4 } function isVideo(r: ResourceDTO) { const t = r.type as string | number | undefined return t === 'Video' || t === 'VideoUrl' || t === 1 || t === 3 } function isImage(r: ResourceDTO) { const t = r.type as string | number | undefined return t === 'Image' || t === 'ImageUrl' || t === 0 || t === 2 } export default function QuizSection({ section, configId, languages }: Props) { const { language, setAvailableLanguages, instanceId } = useVisitor() const back = useBack() useEffect(() => { setAvailableLanguages([]) }, [languages]) const quiz = section.quiz const questions = [...(quiz?.questions ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) const [phase, setPhase] = useState('quiz') const [currentIndex, setCurrentIndex] = useState(0) const [answers, setAnswers] = useState>({}) const [modalResource, setModalResource] = useState(null) const trackedRef = useRef(false) if (questions.length === 0) { return (
Aucune question disponible.
) } const totalQuestions = questions.length const correctCount = questions.filter((q, i) => { const chosen = answers[i] if (chosen === undefined) return false const sorted = [...(q.responses ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) return sorted[chosen]?.isCorrect === true }).length const scorePercent = totalQuestions > 0 ? (correctCount / totalQuestions) * 100 : 0 function getLevel() { if (scorePercent < 25) return quiz?.badLevel if (scorePercent < 50) return quiz?.mediumLevel if (scorePercent < 75) return quiz?.goodLevel return quiz?.greatLevel } function selectAnswer(questionIndex: number, answerIndex: number) { const newAnswers = { ...answers, [questionIndex]: answerIndex } setAnswers(newAnswers) if (questionIndex === totalQuestions - 1) { setTimeout(() => setPhase('result'), 400) } else { setTimeout(() => setCurrentIndex(questionIndex + 1), 500) } } function restart() { setAnswers({}) setCurrentIndex(0) setPhase('quiz') trackedRef.current = false } // ── RESULT SCREEN ──────────────────────────────────────────────────────── if (phase === 'result') { if (!trackedRef.current && instanceId) { trackedRef.current = true trackEvent({ instanceId, configurationId: configId, sectionId: section.id, eventType: 'QuizComplete', language, metadata: JSON.stringify({ score: correctCount, totalQuestions }), }) } const levelText = t(getLevel(), language) return (
{correctCount}/{totalQuestions} {Math.round(scorePercent)}%
{levelText && (
)}
) } // ── QUIZ + REVIEW SCREEN ───────────────────────────────────────────────── const isReview = phase === 'review' const question = questions[currentIndex] const sortedResponses = [...(question.responses ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) const chosen = answers[currentIndex] const questionEntry = getLabelEntry(question.label, language) const questionText = questionEntry?.value ?? '' const questionResource = questionEntry?.resource return ( <>
{ setCurrentIndex(0); setPhase('result') } : back} />
{/* Background image */} {question.imageBackgroundResourceUrl && (
)}
{/* Progress header — segments + counter pill */}
{questions.map((q, i) => { const navigable = isReview || i <= currentIndex const reached = i === currentIndex || i < currentIndex || answers[i] !== undefined return (
{currentIndex + 1} / {totalQuestions}
{/* Question media */} {questionResource?.url && ( isAudio(questionResource) ? (
) : isVideo(questionResource) ? ( ) : ( ) )} {/* Question text */} {questionText && (
)} {/* Answer buttons */}
{sortedResponses.map((response, i) => { const isSelected = chosen === i const st = getAnswerStyle(isReview, isSelected, response.isCorrect) const letter = String.fromCharCode(65 + i) const responseEntry = getLabelEntry(response.label, language) const responseText = responseEntry?.value ?? '' const responseResource = responseEntry?.resource return ( ) })}
{/* Restart button (review only) */} {isReview && ( )}
{/* Media modal */} {modalResource && (
setModalResource(null)} >
e.stopPropagation()} >
)} ) } function youtubeThumbnail(url: string): string | null { const match = url.match(/(?:v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/) return match?.[1] ? `https://img.youtube.com/vi/${match[1]}/mqdefault.jpg` : null } function VideoPreview({ resource, height, playSize }: { resource: ResourceDTO; height: number; playSize: number }) { const thumbnail = resource.url ? youtubeThumbnail(resource.url) : null return (
{thumbnail && ( // eslint-disable-next-line @next/next/no-img-element )}
) } function PlayIcon({ size = 48 }: { size?: number }) { const r = size / 2 return (
) } function ZoomIcon() { return ( ) } function getAnswerStyle(isReview: boolean, isSelected: boolean, isCorrect?: boolean): { bg: string; text: string; border: string; filled: boolean; badgeBg: string; badgeText: string } { const neutral = { bg: '#fff', text: '#1E2A33', border: '#E2DCD2', filled: false, badgeBg: '#E8E3DA', badgeText: '#54636E' } const filledBadge = { badgeBg: 'rgba(255,255,255,0.22)', badgeText: '#fff' } if (!isReview) { if (isSelected) return { bg: 'var(--color-primary)', text: '#fff', border: 'var(--color-primary)', filled: true, ...filledBadge } return neutral } if (isCorrect) return { bg: '#2E9E6B', text: '#fff', border: '#2E9E6B', filled: true, ...filledBadge } if (isSelected) return { bg: '#D14343', text: '#fff', border: '#D14343', filled: true, ...filledBadge } return neutral } function getLabelEntry(labels: TranslationAndResourceDTO[] | undefined, lang: string) { if (!labels || labels.length === 0) return null return ( labels.find((l) => l.language === lang) ?? labels.find((l) => l.language === 'FR') ?? labels[0] ) }