Frontend : intégration du design Claude Design (6 écrans)

Portage du projet « Bénévoles Festival.dc.html » en implémentation statique
pilotée par un jeu de démonstration déterministe (portage du générateur du
design : 15 postes, 64 bénévoles, ~209 affectations).

- Système visuel néo-brutaliste : palette + primitives Tailwind, polices
  Archivo / DM Mono, primitives dans components/ui.tsx.
- Domaine (aligné CLAUDE.md) : types, jours/plages, moteur de validation
  (mêmes codes BLOCKING/WARNING que le back-end), sélecteurs, différentiel
  de publication.
- État applicatif : PlanningProvider (reducer) — affecter / désaffecter /
  publier / confirmer / éditer besoins et bénévoles / import simulé / pointage.
- Écrans : 01 Grille des besoins + panneau d'affectation (candidats triés,
  alertes), 02 Vue par bénévole, 03 Publication, 04 Mobile bénévole,
  05 Mobile référent, 06 Configuration (4 sous-onglets).
- i18n : aucun texte d'interface en dur, locales/fr.json.
- Vérifs : build tsc + vite, eslint (0 erreur), rendu headless des 6 écrans
  (npm run smoke).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwswqSChc3Rwtu61pRXiY2
This commit is contained in:
AdamThomas
2026-09-09 23:16:38 +02:00
co-authored by Claude Sonnet 5
parent 5154a3c61b
commit ac469f0cd1
35 changed files with 4267 additions and 125 deletions
+141
View File
@@ -0,0 +1,141 @@
import { useMemo } from 'react';
import { usePlanning } from '@/app/planning-store';
import { slotKey } from '@/domain/festival';
import { indexPlanning } from '@/domain/selectors';
import { DesignNote, PhoneFrame } from '@/pages/mobile/PhoneFrame';
import { t } from '@/i18n';
import { cx } from '@/lib/cx';
const LEAD_SLOT = slotKey('BAR', 'ven', 'S');
export function MobileLeadPage() {
const { state, dispatch } = usePlanning();
const index = useMemo(
() => indexPlanning(state.assignments, state.volunteers),
[state.assignments, state.volunteers],
);
const position = state.positions.find((p) => p.id === 'BAR')!;
const leadDemo = state.volunteers[1];
const team = (index.bySlot.get(LEAD_SLOT) ?? []).map((assignment) => {
const volunteer = state.volunteers.find((v) => v.id === assignment.volunteerId)!;
const presence = state.presence[assignment.id];
return {
assignmentId: assignment.id,
name: volunteer.fullName,
initials: volunteer.initials,
phone: volunteer.phone,
status:
assignment.status === 'CONFIRMED'
? t('mobileLead.statusConfirmed')
: t('mobileLead.statusProposed'),
present: presence === 'present',
absent: presence === 'absent',
};
});
const presentCount = team.filter((m) => m.present).length;
const absentCount = team.filter((m) => m.absent).length;
return (
<div className="flex flex-wrap items-start gap-[26px] p-6">
<PhoneFrame time={t('mobileLead.statusTime')}>
<div className="bg-ink px-4 pb-[15px] pt-3 text-cream">
<div className="font-mono text-[10px] text-yellow">
{t('mobileLead.kicker', { name: leadDemo.fullName })}
</div>
<div className="mt-[5px] font-display text-[27px] font-black leading-tight tracking-tight">
{`${position.name} — ${position.zone}`.toUpperCase()}
</div>
<div className="font-display text-[13px] font-semibold opacity-80">
{position.zone} · {position.location}
</div>
<div className="mt-3 flex items-stretch gap-2">
<div className="bg-yellow px-[9px] py-[7px] font-display text-[12px] font-extrabold text-ink">
{t('mobileLead.inProgress', { hours: t('mobileLead.shiftHours') })}
</div>
<div className="border-2 border-cream px-[9px] py-[6px] font-display text-[12px] font-extrabold">
{t('mobileLead.present', { present: presentCount, total: team.length })}
</div>
</div>
</div>
{absentCount > 0 ? (
<div className="border-b-[3px] border-ink bg-hatch-orange px-[14px] py-[11px]">
<div className="font-display text-[15px] font-black leading-tight">
{t('mobileLead.absenceAlert', { count: absentCount })}
</div>
<button
type="button"
className="mt-[9px] min-h-[52px] w-full cursor-pointer border-[3px] border-ink bg-ink font-display text-[14px] font-black text-cream hover:bg-yellow hover:text-ink"
>
{t('mobileLead.requestBackup')}
</button>
</div>
) : null}
<div className="flex flex-col gap-[11px] px-[14px] pb-[26px] pt-[13px]">
<div className="font-display text-[12px] font-black uppercase tracking-widest">
{t('mobileLead.teamTitle')}
</div>
{team.map((member) => (
<div key={member.assignmentId} className="border-[3px] border-ink bg-paper shadow-brutal-md">
<div className="flex items-center gap-[10px] px-[11px] py-[10px]">
<span className="flex h-[38px] w-[38px] flex-none items-center justify-center border-[3px] border-ink bg-yellow font-display text-[13px] font-extrabold">
{member.initials}
</span>
<span className="min-w-0 flex-1">
<span className="block font-display text-[17px] font-extrabold leading-tight">
{member.name}
</span>
<span className="block font-mono text-[10.5px] opacity-65">
{member.status} · {member.phone}
</span>
</span>
{member.present ? (
<span className="flex-none border-2 border-ink bg-green px-[7px] py-[5px] font-display text-[11px] font-extrabold text-white">
{t('mobileLead.badgePresent')}
</span>
) : null}
{member.absent ? (
<span className="flex-none border-2 border-ink bg-orange px-[7px] py-[5px] font-display text-[11px] font-extrabold">
{t('mobileLead.badgeAbsent')}
</span>
) : null}
</div>
<div className="grid grid-cols-2 border-t-[3px] border-ink">
<button
type="button"
onClick={() =>
dispatch({ type: 'setPresence', assignmentId: member.assignmentId, value: 'present' })
}
className={cx(
'min-h-[56px] cursor-pointer border-r-[3px] border-ink bg-cream font-display text-[15px] font-black hover:bg-green hover:text-white',
)}
>
{t('mobileLead.markPresent')}
</button>
<button
type="button"
onClick={() =>
dispatch({ type: 'setPresence', assignmentId: member.assignmentId, value: 'absent' })
}
className="min-h-[56px] cursor-pointer bg-cream font-display text-[15px] font-black hover:bg-orange"
>
{t('mobileLead.markAbsent')}
</button>
</div>
</div>
))}
<div className="border-[3px] border-dashed border-ink p-[11px] font-mono text-[11px] leading-relaxed opacity-70">
{t('mobileLead.nextSlot')}
</div>
</div>
</PhoneFrame>
<DesignNote>{t('mobileLead.designNote')}</DesignNote>
</div>
);
}