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:
parent
d21d794420
commit
81c807cd77
@ -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} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
73
src/components/CastingIntro.tsx
Normal file
73
src/components/CastingIntro.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user