misc + remove description (section)
This commit is contained in:
parent
5fd6e60b7d
commit
2af78bbe7a
@ -40,8 +40,8 @@ export default function SectionList({
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col" style={{ background: 'var(--color-background)' }}>
|
<div className="min-h-screen flex flex-col" style={{ background: 'var(--color-background)' }}>
|
||||||
|
|
||||||
{/* Header image — 22vh */}
|
{/* Header image — covers the image band + the search row below it */}
|
||||||
<div className="relative shrink-0" style={{ height: '22vh' }}>
|
<div className="relative shrink-0 flex flex-col justify-end" style={{ height: 'calc(22vh + 90px)' }}>
|
||||||
{configImageSource ? (
|
{configImageSource ? (
|
||||||
<Image src={configImageSource} alt="" fill className="object-cover" style={{ opacity: 0.65 }} sizes="100vw" />
|
<Image src={configImageSource} alt="" fill className="object-cover" style={{ opacity: 0.65 }} sizes="100vw" />
|
||||||
) : (
|
) : (
|
||||||
@ -53,63 +53,61 @@ export default function SectionList({
|
|||||||
{/* Back button */}
|
{/* Back button */}
|
||||||
<button
|
<button
|
||||||
onClick={back}
|
onClick={back}
|
||||||
className="absolute flex items-center justify-center"
|
className="absolute flex items-center justify-center shadow-md"
|
||||||
style={{
|
style={{
|
||||||
top: 'max(env(safe-area-inset-top), 16px)', left: 16, width: 40, height: 40,
|
top: 'max(env(safe-area-inset-top), 16px)', left: 16, width: 40, height: 40,
|
||||||
background: 'rgba(255,255,255,0.2)',
|
background: primaryColor,
|
||||||
backdropFilter: 'blur(8px)',
|
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon color="white" size={20} />
|
<ChevronLeftIcon color="white" size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search row */}
|
{/* Search row — same spot as before, image visible through translucent fields.
|
||||||
<div
|
marginBottom leaves room for the list below to overlap the image, not this row. */}
|
||||||
className="flex gap-2 px-3 py-2 shrink-0"
|
<div className="relative flex gap-2 px-3 py-3" style={{ marginBottom: 30 }}>
|
||||||
style={{ background: 'var(--color-background)' }}
|
<div
|
||||||
>
|
className="flex items-center gap-2 flex-1 rounded-xl px-3 py-2 shadow-md"
|
||||||
<div
|
style={{ background: 'rgba(255,255,255,0.55)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255,255,255,0.4)' }}
|
||||||
className="flex items-center gap-2 flex-1 rounded-xl px-3 py-2"
|
>
|
||||||
style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style={{ color: 'var(--color-text-muted)', flexShrink: 0 }}>
|
||||||
>
|
<path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style={{ color: 'var(--color-text-muted)', flexShrink: 0 }}>
|
</svg>
|
||||||
<path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
<input
|
||||||
</svg>
|
type="text"
|
||||||
<input
|
value={search}
|
||||||
type="text"
|
onChange={(e) => { setSearch(e.target.value); setSearchNumber('') }}
|
||||||
value={search}
|
placeholder="Rechercher..."
|
||||||
onChange={(e) => { setSearch(e.target.value); setSearchNumber('') }}
|
className="flex-1 bg-transparent text-sm outline-none"
|
||||||
placeholder="Rechercher..."
|
style={{ color: 'var(--color-text)' }}
|
||||||
className="flex-1 bg-transparent text-sm outline-none"
|
/>
|
||||||
style={{ color: 'var(--color-text)' }}
|
{search && (
|
||||||
/>
|
<button onClick={() => setSearch('')} style={{ color: 'var(--color-text-muted)' }}>
|
||||||
{search && (
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||||
<button onClick={() => setSearch('')} style={{ color: 'var(--color-text-muted)' }}>
|
</button>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
)}
|
||||||
</button>
|
</div>
|
||||||
)}
|
<div
|
||||||
</div>
|
className="flex items-center rounded-xl px-3 py-2 shadow-md"
|
||||||
<div
|
style={{ background: 'rgba(255,255,255,0.55)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255,255,255,0.4)', width: 72 }}
|
||||||
className="flex items-center rounded-xl px-3 py-2"
|
>
|
||||||
style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)', width: 72 }}
|
<input
|
||||||
>
|
type="number"
|
||||||
<input
|
value={searchNumber}
|
||||||
type="number"
|
onChange={(e) => { setSearchNumber(e.target.value); setSearch('') }}
|
||||||
value={searchNumber}
|
placeholder="#"
|
||||||
onChange={(e) => { setSearchNumber(e.target.value); setSearch('') }}
|
className="w-full bg-transparent text-sm outline-none text-center"
|
||||||
placeholder="#"
|
style={{ color: 'var(--color-text)' }}
|
||||||
className="w-full bg-transparent text-sm outline-none text-center"
|
/>
|
||||||
style={{ color: 'var(--color-text)' }}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* List */}
|
{/* List — overlaps the image slightly so its rounded corners reveal the image, not the page background */}
|
||||||
<div
|
<div
|
||||||
className="flex-1 overflow-y-auto pt-4"
|
className="relative flex-1 overflow-y-auto pt-4"
|
||||||
style={{
|
style={{
|
||||||
|
marginTop: -30,
|
||||||
background: 'var(--color-background)',
|
background: 'var(--color-background)',
|
||||||
borderTopLeftRadius: 30,
|
borderTopLeftRadius: 30,
|
||||||
borderTopRightRadius: 30,
|
borderTopRightRadius: 30,
|
||||||
@ -228,7 +226,7 @@ function SectionCard({
|
|||||||
<div
|
<div
|
||||||
className="absolute flex items-center justify-center"
|
className="absolute flex items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
right: 10 - 9,
|
right: -4,
|
||||||
top: '50%',
|
top: '50%',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
width: 18,
|
width: 18,
|
||||||
|
|||||||
@ -86,8 +86,6 @@ export default function EventSection({ section, slug, configId, languages }: Pro
|
|||||||
const hasPaths = (section.event as { guidedPaths?: GuidedPathDTO[] } | undefined)?.guidedPaths?.length
|
const hasPaths = (section.event as { guidedPaths?: GuidedPathDTO[] } | undefined)?.guidedPaths?.length
|
||||||
const paths: GuidedPathDTO[] = (section.event as { guidedPaths?: GuidedPathDTO[] } | undefined)?.guidedPaths ?? []
|
const paths: GuidedPathDTO[] = (section.event as { guidedPaths?: GuidedPathDTO[] } | undefined)?.guidedPaths ?? []
|
||||||
|
|
||||||
const description = t(section.description, language)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'fixed', inset: 0, background: 'var(--color-background)', overflowY: 'auto' }}>
|
<div style={{ position: 'fixed', inset: 0, background: 'var(--color-background)', overflowY: 'auto' }}>
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
@ -280,17 +278,6 @@ export default function EventSection({ section, slug, configId, languages }: Pro
|
|||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* À propos */}
|
|
||||||
{description && (
|
|
||||||
<Section title="À propos">
|
|
||||||
<div
|
|
||||||
className="text-sm leading-relaxed [&_p]:m-0 [&_p+p]:mt-2"
|
|
||||||
style={{ color: 'var(--color-text)' }}
|
|
||||||
dangerouslySetInnerHTML={{ __html: description }}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="h-12" />
|
<div className="h-12" />
|
||||||
|
|
||||||
{/* Block detail bottom sheet */}
|
{/* Block detail bottom sheet */}
|
||||||
|
|||||||
@ -55,7 +55,6 @@ export default function ParcoursSection({ section, languages }: Props) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const sectionTitle = tPlain(section.title, language)
|
const sectionTitle = tPlain(section.title, language)
|
||||||
const sectionDesc = t(section.description, language)
|
|
||||||
|
|
||||||
const steps: GuidedStepDTO[] = activePath
|
const steps: GuidedStepDTO[] = activePath
|
||||||
? [...(activePath.steps ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
? [...(activePath.steps ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||||
@ -212,13 +211,6 @@ export default function ParcoursSection({ section, languages }: Props) {
|
|||||||
<h1 style={{ color: '#fff', fontSize: 'clamp(20px, 5vw, 26px)', fontWeight: 600, letterSpacing: '-0.5px', lineHeight: 1.1, margin: 0 }}>
|
<h1 style={{ color: '#fff', fontSize: 'clamp(20px, 5vw, 26px)', fontWeight: 600, letterSpacing: '-0.5px', lineHeight: 1.1, margin: 0 }}>
|
||||||
{sectionTitle}
|
{sectionTitle}
|
||||||
</h1>
|
</h1>
|
||||||
{sectionDesc && (
|
|
||||||
<p
|
|
||||||
className="mt-1"
|
|
||||||
style={{ color: 'rgba(255,255,255,0.85)', fontSize: 13.5, lineHeight: 1.4, margin: '6px 0 0', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}
|
|
||||||
dangerouslySetInnerHTML={{ __html: sectionDesc }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -954,12 +946,12 @@ function MiniToast({ message, isGame, onDismiss }: { message: string; isGame: bo
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = setTimeout(onDismiss, 2500)
|
const id = setTimeout(onDismiss, 2500)
|
||||||
return () => clearTimeout(id)
|
return () => clearTimeout(id)
|
||||||
}, [message, onDismiss])
|
}, [message])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed', left: 16, right: 16, bottom: 'max(24px, env(safe-area-inset-bottom))',
|
position: 'fixed', left: 16, right: 16, top: '50%', transform: 'translateY(-50%)',
|
||||||
zIndex: 5000, display: 'flex', justifyContent: 'center',
|
zIndex: 5000, display: 'flex', justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -2001,49 +1993,90 @@ function StepQuiz({ questions, puzzleQuestions, simpleQuestions, answered, puzzl
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{kind === 'simple' && (
|
{kind === 'simple' && (
|
||||||
<>
|
isDigicodeAnswer(expectedSimpleAnswer) ? (
|
||||||
<input
|
<>
|
||||||
type="text"
|
<DigicodePad
|
||||||
value={simpleAnswered[current.id] ?? ''}
|
value={simpleAnswered[current.id] ?? ''}
|
||||||
onChange={(e) => onSimpleAnswer(current.id, e.target.value)}
|
length={stripHtml(expectedSimpleAnswer).length}
|
||||||
disabled={checked}
|
disabled={checked}
|
||||||
placeholder="Votre réponse…"
|
accentColor={accentColor}
|
||||||
style={{
|
isGame={isGame}
|
||||||
width: '100%', padding: '13px 14px', borderRadius: 14, fontSize: 15,
|
hasAnswer={hasAnswer}
|
||||||
border: `1.5px solid ${hasAnswer ? (simpleIsCorrect ? '#2E9E6B' : '#D14343') : (isGame ? 'rgba(255,255,255,0.1)' : '#E2DCD2')}`,
|
isCorrect={simpleIsCorrect}
|
||||||
background: hasAnswer
|
onChange={(v) => onSimpleAnswer(current.id, v)}
|
||||||
? (simpleIsCorrect ? 'rgba(46,158,107,0.14)' : 'rgba(209,67,67,0.12)')
|
/>
|
||||||
: (isGame ? 'rgba(255,255,255,0.04)' : '#F9F6F1'),
|
{!checked && (
|
||||||
color: isGame ? '#F4ECD8' : '#1E2A33',
|
<button
|
||||||
}}
|
onClick={() => onSimpleCheck(current.id)}
|
||||||
/>
|
disabled={(simpleAnswered[current.id] ?? '').length !== stripHtml(expectedSimpleAnswer).length}
|
||||||
{!checked && (
|
style={{
|
||||||
<button
|
width: '100%', border: 'none', marginTop: 12, borderRadius: 14, padding: '13px 14px',
|
||||||
onClick={() => onSimpleCheck(current.id)}
|
fontSize: 15, fontWeight: 700,
|
||||||
disabled={(simpleAnswered[current.id] ?? '').trim().length === 0}
|
cursor: (simpleAnswered[current.id] ?? '').length !== stripHtml(expectedSimpleAnswer).length ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: (simpleAnswered[current.id] ?? '').length !== stripHtml(expectedSimpleAnswer).length ? 0.5 : 1,
|
||||||
|
background: accentColor, color: isGame ? '#1a1305' : '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Valider ce code
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{hasAnswer && !simpleIsCorrect && (
|
||||||
|
revealImmediately ? (
|
||||||
|
<p style={{ fontSize: 13, color: isGame ? '#8a8068' : '#6B7B86', margin: '8px 0 0' }}>
|
||||||
|
Le bon code était : <strong dangerouslySetInnerHTML={{ __html: expectedSimpleAnswer }} />
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p style={{ fontSize: 13, color: '#D14343', margin: '8px 0 0', fontWeight: 600 }}>
|
||||||
|
Ce n'est pas le bon code, réessayez.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={simpleAnswered[current.id] ?? ''}
|
||||||
|
onChange={(e) => onSimpleAnswer(current.id, e.target.value)}
|
||||||
|
disabled={checked}
|
||||||
|
placeholder="Votre réponse…"
|
||||||
style={{
|
style={{
|
||||||
width: '100%', border: 'none', marginTop: 8, borderRadius: 14, padding: '13px 14px',
|
width: '100%', padding: '13px 14px', borderRadius: 14, fontSize: 15,
|
||||||
fontSize: 15, fontWeight: 700,
|
border: `1.5px solid ${hasAnswer ? (simpleIsCorrect ? '#2E9E6B' : '#D14343') : (isGame ? 'rgba(255,255,255,0.1)' : '#E2DCD2')}`,
|
||||||
cursor: (simpleAnswered[current.id] ?? '').trim().length === 0 ? 'not-allowed' : 'pointer',
|
background: hasAnswer
|
||||||
opacity: (simpleAnswered[current.id] ?? '').trim().length === 0 ? 0.5 : 1,
|
? (simpleIsCorrect ? 'rgba(46,158,107,0.14)' : 'rgba(209,67,67,0.12)')
|
||||||
background: accentColor, color: isGame ? '#1a1305' : '#fff',
|
: (isGame ? 'rgba(255,255,255,0.04)' : '#F9F6F1'),
|
||||||
|
color: isGame ? '#F4ECD8' : '#1E2A33',
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
Valider cette réponse
|
{!checked && (
|
||||||
</button>
|
<button
|
||||||
)}
|
onClick={() => onSimpleCheck(current.id)}
|
||||||
{hasAnswer && !simpleIsCorrect && (
|
disabled={(simpleAnswered[current.id] ?? '').trim().length === 0}
|
||||||
revealImmediately ? (
|
style={{
|
||||||
<p style={{ fontSize: 13, color: isGame ? '#8a8068' : '#6B7B86', margin: '8px 0 0' }}>
|
width: '100%', border: 'none', marginTop: 8, borderRadius: 14, padding: '13px 14px',
|
||||||
La bonne réponse était : <strong dangerouslySetInnerHTML={{ __html: expectedSimpleAnswer }} />
|
fontSize: 15, fontWeight: 700,
|
||||||
</p>
|
cursor: (simpleAnswered[current.id] ?? '').trim().length === 0 ? 'not-allowed' : 'pointer',
|
||||||
) : (
|
opacity: (simpleAnswered[current.id] ?? '').trim().length === 0 ? 0.5 : 1,
|
||||||
<p style={{ fontSize: 13, color: '#D14343', margin: '8px 0 0', fontWeight: 600 }}>
|
background: accentColor, color: isGame ? '#1a1305' : '#fff',
|
||||||
Ce n'est pas la bonne réponse, réessayez.
|
}}
|
||||||
</p>
|
>
|
||||||
)
|
Valider cette réponse
|
||||||
)}
|
</button>
|
||||||
</>
|
)}
|
||||||
|
{hasAnswer && !simpleIsCorrect && (
|
||||||
|
revealImmediately ? (
|
||||||
|
<p style={{ fontSize: 13, color: isGame ? '#8a8068' : '#6B7B86', margin: '8px 0 0' }}>
|
||||||
|
La bonne réponse était : <strong dangerouslySetInnerHTML={{ __html: expectedSimpleAnswer }} />
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p style={{ fontSize: 13, color: '#D14343', margin: '8px 0 0', fontWeight: 600 }}>
|
||||||
|
Ce n'est pas la bonne réponse, réessayez.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -2189,6 +2222,7 @@ function StepCarousel({ contents, stepIndexKey, isGame, accentColor, language }:
|
|||||||
language: string
|
language: string
|
||||||
}) {
|
}) {
|
||||||
const [imgIndex, setImgIndex] = useState(0)
|
const [imgIndex, setImgIndex] = useState(0)
|
||||||
|
const [lightboxOpen, setLightboxOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => { setImgIndex(0) }, [stepIndexKey])
|
useEffect(() => { setImgIndex(0) }, [stepIndexKey])
|
||||||
|
|
||||||
@ -2201,18 +2235,23 @@ function StepCarousel({ contents, stepIndexKey, isGame, accentColor, language }:
|
|||||||
<img
|
<img
|
||||||
src={current.resource.url}
|
src={current.resource.url}
|
||||||
alt={tPlain(current.title, language)}
|
alt={tPlain(current.title, language)}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
onClick={() => setLightboxOpen(true)}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover', cursor: 'zoom-in' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.3), transparent 60%)' }} />
|
{lightboxOpen && current?.resource?.url && (
|
||||||
|
<ContentLightbox content={current} isGame={isGame} language={language} onClose={() => setLightboxOpen(false)} />
|
||||||
|
)}
|
||||||
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.3), transparent 60%)', pointerEvents: 'none' }} />
|
||||||
{/* Counter badge */}
|
{/* Counter badge */}
|
||||||
<div style={{ position: 'absolute', top: 12, left: 14 }}>
|
<div style={{ position: 'absolute', top: 12, left: 14 }}>
|
||||||
<span style={{
|
<span style={{
|
||||||
|
display: 'inline-flex', alignItems: 'center', gap: 5,
|
||||||
padding: '5px 10px', borderRadius: 9,
|
padding: '5px 10px', borderRadius: 9,
|
||||||
background: 'rgba(0,0,0,0.45)', backdropFilter: 'blur(6px)',
|
background: 'rgba(0,0,0,0.45)', backdropFilter: 'blur(6px)',
|
||||||
color: '#fff', fontSize: 12.5, fontWeight: 700, letterSpacing: '0.3px',
|
color: '#fff', fontSize: 12.5, fontWeight: 700, letterSpacing: '0.3px',
|
||||||
}}>
|
}}>
|
||||||
🖼 {imgIndex + 1} / {contents.length}
|
<ImageIcon color="#fff" size={12} /> {imgIndex + 1} / {contents.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Arrows */}
|
{/* 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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed', inset: 0, zIndex: 7000,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
background: 'rgba(0,0,0,0.85)', padding: 24,
|
||||||
|
}}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => 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)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={content.resource?.url}
|
||||||
|
alt={title}
|
||||||
|
style={{ width: '100%', maxHeight: '65vh', objectFit: 'contain', display: 'block', background: '#000' }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
position: 'absolute', top: 12, right: 12,
|
||||||
|
width: 32, height: 32, borderRadius: '50%',
|
||||||
|
background: 'rgba(0,0,0,0.5)', border: 'none', cursor: 'pointer',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XIcon color="#fff" size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{(title || description) && (
|
||||||
|
<div style={{ padding: '18px 20px 22px' }}>
|
||||||
|
{title && (
|
||||||
|
<p style={{ color: isGame ? '#F4ECD8' : '#1E2A33', fontSize: 17, fontWeight: 700, margin: '0 0 8px' }}>
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<div
|
||||||
|
style={{ color: isGame ? '#c8bfa8' : '#46555F', fontSize: 14.5, lineHeight: 1.6 }}
|
||||||
|
className="[&_p]:m-0"
|
||||||
|
dangerouslySetInnerHTML={{ __html: description }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Waveform decorative ────────────────────────────────────────────────────────
|
// ── Waveform decorative ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function WaveformDecor({ currentTime, duration, accentColor, onSeek }: {
|
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()
|
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 (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', marginBottom: 16 }}>
|
||||||
|
{Array.from({ length }).map((_, i) => (
|
||||||
|
<div key={i} style={{
|
||||||
|
width: 40, height: 48, borderRadius: 10,
|
||||||
|
border: `1.5px solid ${boxBorder}`, background: boxBg,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontSize: 22, fontWeight: 800, color: ink,
|
||||||
|
}}>
|
||||||
|
{value[i] ?? ''}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8, maxWidth: 200, margin: '0 auto' }}>
|
||||||
|
{['1', '2', '3', '4', '5', '6', '7', '8', '9'].map((d) => (
|
||||||
|
<button key={d} onClick={() => 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}</button>
|
||||||
|
))}
|
||||||
|
<button onClick={() => 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' }}>
|
||||||
|
⌫
|
||||||
|
</button>
|
||||||
|
<button onClick={() => 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
|
||||||
|
</button>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeAnswer(text: string): string {
|
function normalizeAnswer(text: string): string {
|
||||||
const withoutAccents = stripHtml(text).toLowerCase().normalize('NFD')
|
const withoutAccents = stripHtml(text).toLowerCase().normalize('NFD')
|
||||||
.split('').filter((ch) => {
|
.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 (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||||
|
<circle cx="8.5" cy="9" r="1.5" />
|
||||||
|
<path d="M21 15l-5-5-4 4-3-3-4 4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function LockIcon({ color = 'currentColor', size = 28 }: { color?: string; size?: number }) {
|
function LockIcon({ color = 'currentColor', size = 28 }: { color?: string; size?: number }) {
|
||||||
return (
|
return (
|
||||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.8">
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.8">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user