+ {/* Header image — covers the image band + the search row below it */}
+
{configImageSource ? (
) : (
@@ -53,63 +53,61 @@ export default function SectionList({
{/* Back button */}
-
- {/* Search row */}
-
-
-
-
{ setSearch(e.target.value); setSearchNumber('') }}
- placeholder="Rechercher..."
- className="flex-1 bg-transparent text-sm outline-none"
- style={{ color: 'var(--color-text)' }}
- />
- {search && (
-
- )}
-
-
-
{ setSearchNumber(e.target.value); setSearch('') }}
- placeholder="#"
- className="w-full bg-transparent text-sm outline-none text-center"
- style={{ color: 'var(--color-text)' }}
- />
+ {/* Search row — same spot as before, image visible through translucent fields.
+ marginBottom leaves room for the list below to overlap the image, not this row. */}
+
+
+
+
{ setSearch(e.target.value); setSearchNumber('') }}
+ placeholder="Rechercher..."
+ className="flex-1 bg-transparent text-sm outline-none"
+ style={{ color: 'var(--color-text)' }}
+ />
+ {search && (
+
+ )}
+
+
+ { setSearchNumber(e.target.value); setSearch('') }}
+ placeholder="#"
+ className="w-full bg-transparent text-sm outline-none text-center"
+ style={{ color: 'var(--color-text)' }}
+ />
+
- {/* List */}
+ {/* List — overlaps the image slightly so its rounded corners reveal the image, not the page background */}
{/* Hero */}
@@ -280,17 +278,6 @@ export default function EventSection({ section, slug, configId, languages }: Pro
)}
- {/* À propos */}
- {description && (
-
- )}
-
{/* Block detail bottom sheet */}
diff --git a/src/components/sections/ParcoursSection.tsx b/src/components/sections/ParcoursSection.tsx
index cbe2f71..69bc6e5 100644
--- a/src/components/sections/ParcoursSection.tsx
+++ b/src/components/sections/ParcoursSection.tsx
@@ -55,7 +55,6 @@ export default function ParcoursSection({ section, languages }: Props) {
}, [])
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))
@@ -212,13 +211,6 @@ export default function ParcoursSection({ section, languages }: Props) {
{sectionTitle}
- {sectionDesc && (
-
- )}
@@ -954,12 +946,12 @@ function MiniToast({ message, isGame, onDismiss }: { message: string; isGame: bo
useEffect(() => {
const id = setTimeout(onDismiss, 2500)
return () => clearTimeout(id)
- }, [message, onDismiss])
+ }, [message])
return (
@@ -2001,49 +1993,90 @@ function StepQuiz({ questions, puzzleQuestions, simpleQuestions, answered, puzzl
)}
{kind === 'simple' && (
- <>
-
onSimpleAnswer(current.id, e.target.value)}
- disabled={checked}
- placeholder="Votre réponse…"
- style={{
- width: '100%', padding: '13px 14px', borderRadius: 14, fontSize: 15,
- border: `1.5px solid ${hasAnswer ? (simpleIsCorrect ? '#2E9E6B' : '#D14343') : (isGame ? 'rgba(255,255,255,0.1)' : '#E2DCD2')}`,
- background: hasAnswer
- ? (simpleIsCorrect ? 'rgba(46,158,107,0.14)' : 'rgba(209,67,67,0.12)')
- : (isGame ? 'rgba(255,255,255,0.04)' : '#F9F6F1'),
- color: isGame ? '#F4ECD8' : '#1E2A33',
- }}
- />
- {!checked && (
-
- )}
- {hasAnswer && !simpleIsCorrect && (
- revealImmediately ? (
-
- La bonne réponse était :
-
- ) : (
-
- Ce n'est pas la bonne réponse, réessayez.
-
- )
- )}
- >
+ />
+ {!checked && (
+
onSimpleCheck(current.id)}
+ disabled={(simpleAnswered[current.id] ?? '').trim().length === 0}
+ style={{
+ width: '100%', border: 'none', marginTop: 8, borderRadius: 14, padding: '13px 14px',
+ fontSize: 15, fontWeight: 700,
+ cursor: (simpleAnswered[current.id] ?? '').trim().length === 0 ? 'not-allowed' : 'pointer',
+ opacity: (simpleAnswered[current.id] ?? '').trim().length === 0 ? 0.5 : 1,
+ background: accentColor, color: isGame ? '#1a1305' : '#fff',
+ }}
+ >
+ Valider cette réponse
+
+ )}
+ {hasAnswer && !simpleIsCorrect && (
+ revealImmediately ? (
+
+ La bonne réponse était :
+
+ ) : (
+
+ Ce n'est pas la bonne réponse, réessayez.
+
+ )
+ )}
+ >
+ )
)}
@@ -2189,6 +2222,7 @@ function StepCarousel({ contents, stepIndexKey, isGame, accentColor, language }:
language: string
}) {
const [imgIndex, setImgIndex] = useState(0)
+ const [lightboxOpen, setLightboxOpen] = useState(false)
useEffect(() => { setImgIndex(0) }, [stepIndexKey])
@@ -2201,18 +2235,23 @@ function StepCarousel({ contents, stepIndexKey, isGame, accentColor, language }:

setLightboxOpen(true)}
+ style={{ width: '100%', height: '100%', objectFit: 'cover', cursor: 'zoom-in' }}
/>
)}
-
+ {lightboxOpen && current?.resource?.url && (
+
setLightboxOpen(false)} />
+ )}
+
{/* Counter badge */}
- 🖼 {imgIndex + 1} / {contents.length}
+ {imgIndex + 1} / {contents.length}
{/* Arrows */}
@@ -2265,6 +2304,74 @@ function StepCarousel({ contents, stepIndexKey, isGame, accentColor, language }:
)
}
+function ContentLightbox({ content, isGame, language, onClose }: {
+ content: ContentDTO
+ isGame: boolean
+ language: string
+ onClose: () => void
+}) {
+ const title = tPlain(content.title, language)
+ const description = t(content.description, language)
+
+ return (
+
+
e.stopPropagation()}
+ style={{
+ width: '100%', maxWidth: 480, maxHeight: '90vh', overflowY: 'auto',
+ borderRadius: 20,
+ background: isGame ? '#0B1018' : '#fff',
+ border: isGame ? '1px solid rgba(212,175,55,0.3)' : 'none',
+ boxShadow: '0 20px 50px rgba(0,0,0,0.4)',
+ }}
+ >
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+
+
+ {(title || description) && (
+
+ {title && (
+
+ {title}
+
+ )}
+ {description && (
+
+ )}
+
+ )}
+
+
+ )
+}
+
// ── Waveform decorative ────────────────────────────────────────────────────────
function WaveformDecor({ currentTime, duration, accentColor, onSeek }: {
@@ -2370,6 +2477,77 @@ function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, ' ').replace(/ /g, ' ').replace(/\s+/g, ' ').trim()
}
+// Une réponse "Simple" purement numérique s'affiche comme un digicode (pavé
+// de boutons) plutôt qu'un champ de texte libre.
+function isDigicodeAnswer(expectedHtml: string): boolean {
+ const s = stripHtml(expectedHtml)
+ return s.length > 0 && /^[0-9]+$/.test(s)
+}
+
+function DigicodePad({ value, length, disabled, accentColor, isGame, hasAnswer, isCorrect, onChange }: {
+ value: string
+ length: number
+ disabled: boolean
+ accentColor: string
+ isGame: boolean
+ hasAnswer: boolean
+ isCorrect: boolean
+ onChange: (next: string) => void
+}) {
+ const ink = isGame ? '#F4ECD8' : '#1E2A33'
+ const boxBorder = hasAnswer
+ ? (isCorrect ? '#2E9E6B' : '#D14343')
+ : (isGame ? 'rgba(255,255,255,0.15)' : '#E2DCD2')
+ const boxBg = hasAnswer
+ ? (isCorrect ? 'rgba(46,158,107,0.14)' : 'rgba(209,67,67,0.12)')
+ : (isGame ? 'rgba(255,255,255,0.04)' : '#F9F6F1')
+ const keyBg = isGame ? 'rgba(255,255,255,0.06)' : '#F0EBE3'
+
+ const press = (d: string) => {
+ if (disabled) return
+ if (d === 'back') { onChange(value.slice(0, -1)); return }
+ if (value.length >= length) return
+ onChange(value + d)
+ }
+
+ return (
+
+
+ {Array.from({ length }).map((_, i) => (
+
+ {value[i] ?? ''}
+
+ ))}
+
+
+ {['1', '2', '3', '4', '5', '6', '7', '8', '9'].map((d) => (
+
press(d)} disabled={disabled}
+ style={{
+ padding: '14px 0', borderRadius: 12, border: 'none', fontSize: 18, fontWeight: 700,
+ background: keyBg, color: ink, cursor: disabled ? 'not-allowed' : 'pointer',
+ }}>{d}
+ ))}
+
press('back')} disabled={disabled}
+ style={{ padding: '14px 0', borderRadius: 12, border: 'none', fontSize: 16, fontWeight: 700,
+ background: keyBg, color: ink, cursor: disabled ? 'not-allowed' : 'pointer' }}>
+ ⌫
+
+
press('0')} disabled={disabled}
+ style={{ padding: '14px 0', borderRadius: 12, border: 'none', fontSize: 18, fontWeight: 700,
+ background: keyBg, color: ink, cursor: disabled ? 'not-allowed' : 'pointer' }}>
+ 0
+
+
+
+
+ )
+}
+
function normalizeAnswer(text: string): string {
const withoutAccents = stripHtml(text).toLowerCase().normalize('NFD')
.split('').filter((ch) => {
@@ -2440,6 +2618,16 @@ function ClockIcon({ color = 'currentColor', size = 14 }: { color?: string; size
)
}
+function ImageIcon({ color = 'currentColor', size = 14 }: { color?: string; size?: number }) {
+ return (
+
+ )
+}
+
function LockIcon({ color = 'currentColor', size = 28 }: { color?: string; size?: number }) {
return (