'use client' import { useEffect, useRef, useState } from 'react' import { useBack } from '@/hooks/useBack' import { useVisitor } from '@/context/VisitorContext' import { t, tPlain } from '@/lib/i18n' import LanguageSelector from '@/components/ui/LanguageSelector' import dynamic from 'next/dynamic' import './map/map.css' import type { SectionDTO, GuidedPathDTO, GuidedStepDTO, QuestionDTO, TranslationAndResourceDTO, ContentDTO } from '@/lib/api/types' const LeafletMap = dynamic(() => import('./map/LeafletMap'), { ssr: false, loading: () => (
Chargement de la carte…
), }) interface Props { section: SectionDTO slug: string configId: string languages: string[] apiKey: string } type View = 'list' | 'start' | 'progress' | 'end' export default function ParcoursSection({ section, languages }: Props) { const { language, setAvailableLanguages } = useVisitor() const back = useBack() useEffect(() => { setAvailableLanguages(languages) }, [languages]) const parcours = section.parcours const paths: GuidedPathDTO[] = [...(parcours?.guidedPaths ?? [])].sort( (a, b) => (a.order ?? 0) - (b.order ?? 0) ) const isGame = parcours?.isGameMode === true const [view, setView] = useState('list') const [activePath, setActivePath] = useState(null) const [stepIndex, setStepIndex] = useState(0) const [completedSteps, setCompletedSteps] = useState>(new Set()) const [primaryColor, setPrimaryColor] = useState('#264863') useEffect(() => { const c = getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim() if (c) setPrimaryColor(c) }, []) const sectionTitle = tPlain(section.title, language) const sectionDesc = t(section.description, language) const steps: GuidedStepDTO[] = activePath ? [...(activePath.steps ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) : [] const currentStep = steps[stepIndex] ?? null function selectPath(path: GuidedPathDTO) { setActivePath(path) setStepIndex(0) setCompletedSteps(new Set()) setView('start') } function startPath() { setView('progress') } function nextStep() { if (currentStep) setCompletedSteps((s) => new Set(s).add(currentStep.id)) if (stepIndex < steps.length - 1) { setStepIndex((i) => i + 1) } else { setView('end') } } function prevStep() { if (stepIndex > 0) setStepIndex((i) => i - 1) } function restart() { setView('list') setActivePath(null) setStepIndex(0) setCompletedSteps(new Set()) } const gameIntro = tRaw(parcours?.gameMessageDebut, language) // ── Render ───────────────────────────────────────────────────────────────── const stepsHaveGeo = steps.some((s) => s.geometry?.coordinates?.length === 2) const useMapView = section.parcours?.showMap === true && stepsHaveGeo if (view === 'progress' && activePath && currentStep) { if (useMapView) { return ( setView('list')} /> ) } return ( setView('list')} /> ) } if (view === 'end' && activePath) { return ( ) } return (
{/* Hero */}
{section.imageSource && ( // eslint-disable-next-line @next/next/no-img-element {sectionTitle} )}
{/* Back */} {/* Language */}
{/* Info overlay */}
{paths.length > 0 && ( {paths.length} parcours )}

{sectionTitle}

{sectionDesc && (

)}

{/* Path list */}
{paths.length === 0 ? (

Aucun parcours disponible

) : ( <>

Choisir un parcours

{paths.map((path) => ( selectPath(path)} /> ))}
)}
{/* Start sheet (bottom drawer) */} {view === 'start' && activePath && ( setView('list')} /> )}
) } // ── Map-based progression ───────────────────────────────────────────────────── function ParcoursMapProgression({ path, steps, stepIndex, completedSteps, primaryColor, isGame, language, sectionLat, sectionLng, onNext, onPrev, onBack }: { path: GuidedPathDTO steps: GuidedStepDTO[] stepIndex: number completedSteps: Set primaryColor: string isGame: boolean language: string sectionLat?: number sectionLng?: number onNext: () => void onPrev: () => void onBack: () => void }) { const currentStep = steps[stepIndex] const [selectedStepId, setSelectedStepId] = useState(currentStep?.id ?? null) // Follow current step when it changes useEffect(() => { setSelectedStepId(currentStep?.id ?? null) }, [currentStep?.id]) // Center: current step coords, or section coords, or Paris fallback const center: [number, number] = (() => { const cs = steps[stepIndex] if (cs?.geometry?.coordinates?.length === 2) { return [cs.geometry.coordinates[1], cs.geometry.coordinates[0]] } const firstWithGeo = steps.find((s) => s.geometry?.coordinates?.length === 2) if (firstWithGeo) return [firstWithGeo.geometry!.coordinates![1], firstWithGeo.geometry!.coordinates![0]] return [sectionLat ?? 48.86, sectionLng ?? 2.35] })() const stepTitle = tPlain(currentStep?.title, language) const stepDesc = t(currentStep?.description, language) const accentColor = isGame ? '#D4AF37' : primaryColor return (
{/* Map fills screen */}
{}} primaryColor={primaryColor} pathSteps={steps} selectedStepId={selectedStepId} onSelectStep={setSelectedStepId} currentStepId={currentStep?.id ?? null} completedStepIds={completedSteps} />
{/* Top header */}
{tPlain(path.title, language)} {stepIndex + 1} / {steps.length}
{/* Bottom sheet — current step */}
{/* Handle */}
{/* Step card */} {/* Progress bar */}
{/* Action buttons */}
{stepIndex > 0 && ( )}
) } // ── Sub-components ──────────────────────────────────────────────────────────── function PathCard({ path, isGame, primaryColor, language, onClick }: { path: GuidedPathDTO isGame: boolean primaryColor: string language: string onClick: () => void }) { const title = tPlain(path.title, language) const desc = tPlain(path.description, language) const stepCount = path.steps?.length ?? 0 return ( ) } function StartSheet({ path, isGame, primaryColor, language, gameIntro, onStart, onClose }: { path: GuidedPathDTO isGame: boolean primaryColor: string language: string gameIntro: string onStart: () => void onClose: () => void }) { const title = tPlain(path.title, language) const desc = tPlain(path.description, language) const stepCount = path.steps?.length ?? 0 return ( <> {/* Backdrop */}
{/* Sheet */}
{/* Handle */}
{isGame ? ( /* ── Game start ── */

ESCAPE GAME

{title}

{gameIntro && (

« {gameIntro} »

)}
{stepCount} énigme{stepCount > 1 ? 's' : ''} {path.estimatedDurationMinutes && <>·~{path.estimatedDurationMinutes} min}

Activez le son pour une immersion totale

) : ( /* ── Normal start ── */
{/* Mini-map sketch */}
DÉCOUVERTE LIBRE

{title}

{/* Stats */}
{path.estimatedDurationMinutes ? `~${path.estimatedDurationMinutes} min` : '—'} {stepCount} étape{stepCount > 1 ? 's' : ''}
{desc && (

{desc}

)}
)}
) } function ProgressView({ path, steps, stepIndex, completedSteps, primaryColor, isGame, language, onNext, onPrev, onBack }: { path: GuidedPathDTO steps: GuidedStepDTO[] stepIndex: number completedSteps: Set primaryColor: string isGame: boolean language: string onNext: () => void onPrev: () => void onBack: () => void }) { const step = steps[stepIndex] const [quizAnswered, setQuizAnswered] = useState>({}) const [showQuizResult, setShowQuizResult] = useState(false) const [quizPassed, setQuizPassed] = useState(false) const [showChallenge, setShowChallenge] = useState(false) const [isAudioPlaying, setIsAudioPlaying] = useState(false) const [audioCurrentTime, setAudioCurrentTime] = useState(0) const [audioDuration, setAudioDuration] = useState(0) const audioRef = useRef(null) const quizQuestions = step?.quizQuestions ?? [] const hasQuiz = quizQuestions.length > 0 const requireSuccess = path.requireSuccessToAdvance === true const canAdvance = !hasQuiz || !requireSuccess || quizPassed const stepTitle = tPlain(step?.title, language) const stepDesc = t(step?.description, language) const audioUrl = step?.audioIds?.find((a) => a.language === language)?.value ?? step?.audioIds?.[0]?.value const stepContents = [...(step?.contents ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) const hasCarousel = stepContents.length > 0 const bgColor = isGame ? '#0B1018' : '#F6F3EE' const mutedColor = isGame ? '#8a8068' : '#6B7B86' const accentColor = isGame ? '#D4AF37' : primaryColor useEffect(() => { setQuizAnswered({}) setShowQuizResult(false) setQuizPassed(false) setShowChallenge(false) setIsAudioPlaying(false) setAudioCurrentTime(0) }, [stepIndex]) function toggleAudio() { if (!audioRef.current) return if (isAudioPlaying) { audioRef.current.pause(); setIsAudioPlaying(false) } else { audioRef.current.play(); setIsAudioPlaying(true) } } function submitQuiz() { const allCorrect = quizQuestions.every((q) => { const chosen = quizAnswered[q.id] return q.responses?.find((r) => r.order === chosen)?.isCorrect === true }) setQuizPassed(allCorrect) setShowQuizResult(true) } return (
{/* Header */}
{steps.map((s, i) => (
))}
{stepIndex + 1} / {steps.length}
{/* Hero — carousel si contents, sinon dégradé */} {hasCarousel ? ( ) : (
{step?.imageUrl && ( // eslint-disable-next-line @next/next/no-img-element {stepTitle} )}
Étape {stepIndex + 1}

{stepTitle}

)} {/* Contenu */}
{/* Titre — uniquement en mode carousel (sinon il est dans le hero) */} {hasCarousel && (

{stepTitle}

)} {/* Label parcours */} {tPlain(path.title, language).toUpperCase() || 'PARCOURS'} {/* Lecteur audio */} {audioUrl && ( <>