Écrans bénévole et référent redessinés, messages aux bénévoles
ci-cd / build-backend (push) Successful in 29s
ci-cd / build-frontend (push) Successful in 21s
ci-cd / deploy (push) Successful in 12s

Étape 55 : écrans /mobile-benevole et /mobile-referent d'après la maquette
(design/Planning bénévole mobile) — créneau en cours ou prochain mis en
avant, planning par journée de festival avec frise horaire, compteurs et
pointage du référent par créneau.

Étape 56 : message de l'organisation (Edition.volunteer_message, onglet
« Message aux bénévoles ») et message de chaque poste (Position.message,
saisi par le référent), migration 0029, affichés sur l'écran bénévole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-27 23:17:14 +01:00
co-authored by Claude Opus 5.5
parent 4a3f93e137
commit 16d8fb44a7
21 changed files with 1305 additions and 274 deletions
@@ -0,0 +1,96 @@
import { useState } from 'react';
import { useEdition, usePositions, useUpdateEdition, useUpdatePosition } from '@/api/hooks';
import type { PositionRead } from '@/api/schema';
import { MessageEditor } from '@/components/MessageEditor';
import { ErrorNote, Loading } from '@/components/state';
import { Panel, PanelHeader } from '@/components/ui';
import { cx } from '@/lib/cx';
import { t } from '@/i18n';
// Messages montrés sur l'écran bénévole : celui de l'organisation, le même
// pour tous (`Edition.volunteer_message`), et, pour suivi, ceux des postes
// (`Position.message`) — saisis d'ordinaire par chaque référent depuis son
// écran, mais la coordination peut aussi les reprendre ici.
export function ConfigMessage() {
const edition = useEdition();
const positions = usePositions();
const updateEdition = useUpdateEdition();
if (edition.isLoading || positions.isLoading) return <Loading />;
const error = edition.error ?? positions.error;
if (error) return <ErrorNote error={error} />;
return (
<div className="grid items-start gap-4 p-3 sm:p-4 [grid-template-columns:repeat(auto-fit,minmax(min(360px,100%),1fr))]">
<Panel shadow="lg">
<PanelHeader>{t('config.message.orgTitle')}</PanelHeader>
<div className="flex flex-col gap-3 p-[13px]">
<p className="font-mono text-[10.5px] leading-relaxed opacity-70">{t('config.message.orgHelp')}</p>
<MessageEditor
value={edition.data?.volunteer_message ?? null}
updatedAt={edition.data?.volunteer_message_updated_at ?? null}
placeholder={t('config.message.orgPlaceholder')}
saving={updateEdition.isPending}
error={updateEdition.error}
onSave={(text) => updateEdition.mutateAsync({ volunteer_message: text })}
/>
</div>
</Panel>
<Panel shadow="lg">
<PanelHeader>{t('config.message.positionsTitle')}</PanelHeader>
<div className="flex flex-col gap-3 p-[13px]">
<p className="font-mono text-[10.5px] leading-relaxed opacity-70">
{t('config.message.positionsHelp')}
</p>
<div className="flex flex-col border-2 border-ink">
{(positions.data ?? []).map((position) => (
<PositionMessageRow key={position.id} position={position} />
))}
</div>
</div>
</Panel>
</div>
);
}
function PositionMessageRow({ position }: { position: PositionRead }) {
const [open, setOpen] = useState(false);
const update = useUpdatePosition();
return (
<div className="border-b-2 border-ink last:border-b-0">
<button
type="button"
onClick={() => setOpen((o) => !o)}
aria-expanded={open}
className={cx(
'flex w-full cursor-pointer items-baseline gap-2 px-[10px] py-2 text-left hover:bg-yellow-soft',
open && 'bg-sand',
)}
>
<span className="min-w-0 flex-1">
<span className="block font-display text-[12.5px] font-extrabold">{position.name}</span>
<span className="block truncate font-mono text-[10px] opacity-65">
{position.message ?? t('config.message.none')}
</span>
</span>
<span aria-hidden className="font-mono text-[10px]">
{open ? '▲' : '▼'}
</span>
</button>
{open ? (
<div className="border-t-2 border-ink bg-paper p-[10px]">
<MessageEditor
value={position.message}
updatedAt={position.message_updated_at}
placeholder={t('mobileLead.message.placeholder')}
saving={update.isPending}
error={update.error}
onSave={(text) => update.mutateAsync({ id: position.id, body: { message: text } })}
/>
</div>
) : null}
</div>
);
}