Studio lot 8bis : présenter les personnages au visiteur

Écran d'entrée, une fois par session, 11 langues.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011VxSQeGQYUvPmSEoGdnidA
This commit is contained in:
Thomas Fransolet 2026-09-15 22:41:40 +02:00
parent d21d794420
commit 81c807cd77
4 changed files with 109 additions and 0 deletions

View File

@ -3,6 +3,7 @@ import { getInstanceBySlug, getConfiguration, getSections } from '@/lib/api/clie
import SectionList from '@/components/SectionList' import SectionList from '@/components/SectionList'
import QRScannerButton from '@/components/QRScannerButton' import QRScannerButton from '@/components/QRScannerButton'
import ProximitySuggestion from '@/components/ProximitySuggestion' import ProximitySuggestion from '@/components/ProximitySuggestion'
import CastingIntro from '@/components/CastingIntro'
export default async function ConfigPage({ export default async function ConfigPage({
params, params,
@ -39,6 +40,7 @@ export default async function ConfigPage({
configPrimaryColor={config.primaryColor} configPrimaryColor={config.primaryColor}
languages={config.languages ?? ['FR']} languages={config.languages ?? ['FR']}
/> />
{config.casting && config.casting.length >= 2 && <CastingIntro casting={config.casting} configId={configId} />}
{instance.isQRCodeEnabled !== false && <QRScannerButton slug={slug} configurationId={configId} />} {instance.isQRCodeEnabled !== false && <QRScannerButton slug={slug} configurationId={configId} />}
<ProximitySuggestion sections={activeSections} slug={slug} configId={configId} /> <ProximitySuggestion sections={activeSections} slug={slug} configId={configId} />
</> </>

View File

@ -0,0 +1,73 @@
'use client'
import { useEffect, useState } from 'react'
import Image from 'next/image'
import { useVisitor } from '@/context/VisitorContext'
import { ui } from '@/lib/i18n'
import type { CastingVisitorMemberDTO } from '@/lib/api/types'
/**
* Écran d'entrée « les personnes que vous allez rencontrer » (plan Studio décision 25). Le serveur
* ne l'envoie qu'avec l'interrupteur armé et au moins deux portraits. Montré une fois par visite :
* le visiteur qui revient à la liste des sections ne le revoit pas.
*/
export default function CastingIntro({ casting, configId }: { casting: CastingVisitorMemberDTO[]; configId: string }) {
const { language } = useVisitor()
const [open, setOpen] = useState(false)
const storageKey = `casting-seen-${configId}`
useEffect(() => {
try {
if (sessionStorage.getItem(storageKey)) return
} catch {
// Stockage indisponible (navigation privée) : on montre l'écran, sans mémoriser.
}
setOpen(true)
}, [storageKey])
function close() {
try {
sessionStorage.setItem(storageKey, '1')
} catch {
// Idem : l'écran reviendra à la prochaine ouverture, ce n'est pas grave.
}
setOpen(false)
}
if (!open) return null
return (
<div
role="dialog"
aria-modal="true"
style={{ position: 'fixed', inset: 0, zIndex: 50, background: 'rgba(0,0,0,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
>
<div style={{ width: '100%', maxWidth: 420, background: 'var(--color-paper, #fff)', borderRadius: 22, padding: '24px 20px', boxShadow: '0 12px 32px rgba(0,0,0,0.25)' }}>
<h2 style={{ fontSize: 18, fontWeight: 700, textAlign: 'center', margin: '0 0 20px', color: 'var(--color-ink, #1E2A33)' }}>
{ui('castingTitle', language)}
</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(110px, 1fr))', gap: 16 }}>
{casting.map((member, index) => (
<div key={index} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center', gap: 6 }}>
<Image
src={member.portraitUrl}
alt=""
width={88}
height={88}
style={{ width: 88, height: 88, borderRadius: '50%', objectFit: 'cover' }}
/>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-ink, #1E2A33)' }}>{member.name}</span>
{member.role && <span style={{ fontSize: 12, color: 'var(--color-ink-muted, #6B7B86)' }}>{member.role}</span>}
</div>
))}
</div>
<button
onClick={close}
style={{ marginTop: 24, width: '100%', padding: '12px 16px', borderRadius: 14, border: 'none', cursor: 'pointer', background: 'var(--color-primary)', color: '#fff', fontSize: 15, fontWeight: 600 }}
>
{ui('castingStart', language)}
</button>
</div>
</div>
)
}

View File

@ -96,6 +96,14 @@ export interface ConfigurationDTO {
isOffline?: boolean isOffline?: boolean
sectionIds?: string[] sectionIds?: string[]
sections?: SectionDTO[] sections?: SectionDTO[]
/** Casting montre au visiteur (decision 25). Absent tant que l'ecran d'entree ne doit pas s'afficher. */
casting?: CastingVisitorMemberDTO[]
}
export interface CastingVisitorMemberDTO {
name: string
role?: string
portraitUrl: string
} }
export interface AppConfigurationLinkDTO { export interface AppConfigurationLinkDTO {

View File

@ -24,6 +24,32 @@ const UI_STRINGS: Record<string, Record<string, string>> = {
UK: 'Зображення, створене ШІ', UK: 'Зображення, створене ШІ',
LB: 'Bild generéiert vun der KI', LB: 'Bild generéiert vun der KI',
}, },
castingTitle: {
FR: 'Les personnes que vous allez rencontrer',
NL: 'De personen die u zult ontmoeten',
EN: 'The people you will meet',
DE: 'Die Menschen, denen Sie begegnen werden',
ES: 'Las personas que va a conocer',
IT: 'Le persone che incontrerete',
PL: 'Osoby, które spotkasz',
CN: '您将遇到的人物',
AR: 'الشخصيات التي ستلتقيها',
UK: 'Люди, яких ви зустрінете',
LB: "D'Leit, déi Dir kenne léiert",
},
castingStart: {
FR: 'Commencer la visite',
NL: 'Het bezoek beginnen',
EN: 'Start the visit',
DE: 'Besuch beginnen',
ES: 'Empezar la visita',
IT: 'Inizia la visita',
PL: 'Rozpocznij zwiedzanie',
CN: '开始参观',
AR: 'ابدأ الزيارة',
UK: 'Почати відвідування',
LB: 'Besuch ufänken',
},
noEventsThisMonth: { noEventsThisMonth: {
FR: 'Aucun événement ce mois-ci', FR: 'Aucun événement ce mois-ci',
NL: 'Geen evenementen deze maand', NL: 'Geen evenementen deze maand',