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:
co-authored by
Claude Sonnet 5
parent
5154a3c61b
commit
ac469f0cd1
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user