'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' const CONTENT_MAX_WIDTH = 560 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 } 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 canGoNext = isReview || chosen !== undefined const isLast = currentIndex === totalQuestions - 1 const questionEntry = getLabelEntry(question.label, language) const questionText = questionEntry?.value ?? '' const questionResource = questionEntry?.resource // Les réponses illustrées gardent leur hauteur naturelle ; sinon elles se // répartissent l'espace vertical restant pour remplir l'écran. const hasAnswerMedia = sortedResponses.some((r) => getLabelEntry(r.label, language)?.resource?.url) const mediaHeight = 'clamp(120px, 20vh, 240px)' return ( <>
{ setCurrentIndex(0); setPhase('result') } : back} />
{/* Background image */} {question.imageBackgroundResourceUrl && (
)}
{/* Question media */} {questionResource?.url && ( isAudio(questionResource) ? (
) : isVideo(questionResource) ? ( ) : ( ) )} {/* Question text */} {questionText && (
)} {/* Answer buttons */}
{sortedResponses.map((response, i) => { const isSelected = chosen === i const bg = getAnswerBg(isReview, isSelected, response.isCorrect) // Sélection = couleur principale de l'instance → texte lisible calculé (--color-on-primary). // Vert/rouge de correction = couleurs sémantiques fixes → texte blanc. const textColor = isReview ? (response.isCorrect || isSelected ? '#fff' : 'var(--color-text)') : (isSelected ? 'var(--color-on-primary)' : 'var(--color-text)') const responseEntry = getLabelEntry(response.label, language) const responseText = responseEntry?.value ?? '' const responseResource = responseEntry?.resource return ( ) })}
{/* Segmented progress + counter */}
{isReview && ( )}
{questions.map((_, i) => (
))}
{currentIndex + 1} / {totalQuestions} {isReview && ( )}
{/* 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, playSize }: { resource: ResourceDTO; 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 getAnswerBg(isReview: boolean, isSelected: boolean, isCorrect?: boolean): string { if (!isReview) { return isSelected ? 'var(--color-primary)' : '#fff' } if (isCorrect) return '#2E9E6B' if (isSelected) return '#D14343' return '#fff' } 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] ) }