misc + remove description (section)
This commit is contained in:
parent
5fd6e60b7d
commit
2af78bbe7a
@ -40,8 +40,8 @@ export default function SectionList({
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col" style={{ background: 'var(--color-background)' }}>
|
||||
|
||||
{/* Header image — 22vh */}
|
||||
<div className="relative shrink-0" style={{ height: '22vh' }}>
|
||||
{/* Header image — covers the image band + the search row below it */}
|
||||
<div className="relative shrink-0 flex flex-col justify-end" style={{ height: 'calc(22vh + 90px)' }}>
|
||||
{configImageSource ? (
|
||||
<Image src={configImageSource} alt="" fill className="object-cover" style={{ opacity: 0.65 }} sizes="100vw" />
|
||||
) : (
|
||||
@ -53,26 +53,22 @@ export default function SectionList({
|
||||
{/* Back button */}
|
||||
<button
|
||||
onClick={back}
|
||||
className="absolute flex items-center justify-center"
|
||||
className="absolute flex items-center justify-center shadow-md"
|
||||
style={{
|
||||
top: 'max(env(safe-area-inset-top), 16px)', left: 16, width: 40, height: 40,
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
background: primaryColor,
|
||||
borderRadius: 16,
|
||||
}}
|
||||
>
|
||||
<ChevronLeftIcon color="white" size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search row */}
|
||||
{/* 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. */}
|
||||
<div className="relative flex gap-2 px-3 py-3" style={{ marginBottom: 30 }}>
|
||||
<div
|
||||
className="flex gap-2 px-3 py-2 shrink-0"
|
||||
style={{ background: 'var(--color-background)' }}
|
||||
>
|
||||
<div
|
||||
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)' }}
|
||||
className="flex items-center gap-2 flex-1 rounded-xl px-3 py-2 shadow-md"
|
||||
style={{ background: 'rgba(255,255,255,0.55)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255,255,255,0.4)' }}
|
||||
>
|
||||
<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"/>
|
||||
@ -92,8 +88,8 @@ export default function SectionList({
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center rounded-xl px-3 py-2"
|
||||
style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)', width: 72 }}
|
||||
className="flex items-center rounded-xl px-3 py-2 shadow-md"
|
||||
style={{ background: 'rgba(255,255,255,0.55)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255,255,255,0.4)', width: 72 }}
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
@ -105,11 +101,13 @@ export default function SectionList({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
{/* List — overlaps the image slightly so its rounded corners reveal the image, not the page background */}
|
||||
<div
|
||||
className="flex-1 overflow-y-auto pt-4"
|
||||
className="relative flex-1 overflow-y-auto pt-4"
|
||||
style={{
|
||||
marginTop: -30,
|
||||
background: 'var(--color-background)',
|
||||
borderTopLeftRadius: 30,
|
||||
borderTopRightRadius: 30,
|
||||
@ -228,7 +226,7 @@ function SectionCard({
|
||||
<div
|
||||
className="absolute flex items-center justify-center"
|
||||
style={{
|
||||
right: 10 - 9,
|
||||
right: -4,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
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 paths: GuidedPathDTO[] = (section.event as { guidedPaths?: GuidedPathDTO[] } | undefined)?.guidedPaths ?? []
|
||||
|
||||
const description = t(section.description, language)
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, background: 'var(--color-background)', overflowY: 'auto' }}>
|
||||
{/* Hero */}
|
||||
@ -280,17 +278,6 @@ export default function EventSection({ section, slug, configId, languages }: Pro
|
||||
</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" />
|
||||
|
||||
{/* Block detail bottom sheet */}
|
||||
|
||||
@ -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) {
|
||||
<h1 style={{ color: '#fff', fontSize: 'clamp(20px, 5vw, 26px)', fontWeight: 600, letterSpacing: '-0.5px', lineHeight: 1.1, margin: 0 }}>
|
||||
{sectionTitle}
|
||||
</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>
|
||||
|
||||
@ -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 (
|
||||
<div
|
||||
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',
|
||||
}}
|
||||
>
|
||||
@ -2001,6 +1993,46 @@ function StepQuiz({ questions, puzzleQuestions, simpleQuestions, answered, puzzl
|
||||
)}
|
||||
|
||||
{kind === 'simple' && (
|
||||
isDigicodeAnswer(expectedSimpleAnswer) ? (
|
||||
<>
|
||||
<DigicodePad
|
||||
value={simpleAnswered[current.id] ?? ''}
|
||||
length={stripHtml(expectedSimpleAnswer).length}
|
||||
disabled={checked}
|
||||
accentColor={accentColor}
|
||||
isGame={isGame}
|
||||
hasAnswer={hasAnswer}
|
||||
isCorrect={simpleIsCorrect}
|
||||
onChange={(v) => onSimpleAnswer(current.id, v)}
|
||||
/>
|
||||
{!checked && (
|
||||
<button
|
||||
onClick={() => onSimpleCheck(current.id)}
|
||||
disabled={(simpleAnswered[current.id] ?? '').length !== stripHtml(expectedSimpleAnswer).length}
|
||||
style={{
|
||||
width: '100%', border: 'none', marginTop: 12, borderRadius: 14, padding: '13px 14px',
|
||||
fontSize: 15, fontWeight: 700,
|
||||
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"
|
||||
@ -2044,6 +2076,7 @@ function StepQuiz({ questions, puzzleQuestions, simpleQuestions, answered, puzzl
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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 }:
|
||||
<img
|
||||
src={current.resource.url}
|
||||
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 */}
|
||||
<div style={{ position: 'absolute', top: 12, left: 14 }}>
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 5,
|
||||
padding: '5px 10px', borderRadius: 9,
|
||||
background: 'rgba(0,0,0,0.45)', backdropFilter: 'blur(6px)',
|
||||
color: '#fff', fontSize: 12.5, fontWeight: 700, letterSpacing: '0.3px',
|
||||
}}>
|
||||
🖼 {imgIndex + 1} / {contents.length}
|
||||
<ImageIcon color="#fff" size={12} /> {imgIndex + 1} / {contents.length}
|
||||
</span>
|
||||
</div>
|
||||
{/* 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 ────────────────────────────────────────────────────────
|
||||
|
||||
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 (
|
||||
<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 {
|
||||
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 (
|
||||
<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 }) {
|
||||
return (
|
||||
<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