Écrans bénévole et référent redessinés, messages aux bénévoles
É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:
co-authored by
Claude Opus 5.5
parent
4a3f93e137
commit
16d8fb44a7
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { ApiError } from '@/api';
|
||||
import { MESSAGE_MAX_LENGTH } from '@/api/schema';
|
||||
import { Btn } from '@/components/ui';
|
||||
import { cx } from '@/lib/cx';
|
||||
import { messageDateLabel } from '@/lib/slotTimeline';
|
||||
import { useNowMinutes } from '@/lib/useNowMinutes';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
/**
|
||||
* Saisie d'un message montré aux bénévoles — celui de l'organisation
|
||||
* (coordination, `ConfigMessage`) ou celui d'un poste (référent,
|
||||
* `MobileLeadPage`). Enregistré sur demande ; vider le champ retire le
|
||||
* message.
|
||||
*/
|
||||
export function MessageEditor({
|
||||
value,
|
||||
updatedAt,
|
||||
placeholder,
|
||||
saving,
|
||||
error,
|
||||
onSave,
|
||||
dark = false,
|
||||
}: {
|
||||
value: string | null;
|
||||
updatedAt: string | null;
|
||||
placeholder: string;
|
||||
saving: boolean;
|
||||
error: unknown;
|
||||
onSave: (text: string) => Promise<unknown>;
|
||||
/** Sur fond sombre (en-tête de l'écran référent). */
|
||||
dark?: boolean;
|
||||
}) {
|
||||
const [draft, setDraft] = useState(value ?? '');
|
||||
const [saved, setSaved] = useState(false);
|
||||
const now = useNowMinutes();
|
||||
// Le message enregistré change (autre poste choisi, autre appareil) :
|
||||
// le brouillon repart de lui.
|
||||
useEffect(() => setDraft(value ?? ''), [value]);
|
||||
|
||||
const dirty = draft.trim() !== (value ?? '');
|
||||
const tooLong = draft.length > MESSAGE_MAX_LENGTH;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[7px]">
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) => {
|
||||
setDraft(e.target.value);
|
||||
setSaved(false);
|
||||
}}
|
||||
rows={4}
|
||||
placeholder={placeholder}
|
||||
className="w-full resize-y border-2 border-ink bg-cream p-2 font-display text-[14px] font-semibold leading-snug text-ink"
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Btn
|
||||
disabled={!dirty || tooLong || saving}
|
||||
onClick={() => {
|
||||
void onSave(draft.trim()).then(() => setSaved(true), () => undefined);
|
||||
}}
|
||||
>
|
||||
{saving ? t('message.saving') : draft.trim() ? t('message.save') : t('message.remove')}
|
||||
</Btn>
|
||||
<span
|
||||
className={cx(
|
||||
'font-mono text-[10px] leading-snug',
|
||||
tooLong ? 'font-bold text-orange' : 'opacity-65',
|
||||
)}
|
||||
>
|
||||
{saved && !dirty
|
||||
? t('message.saved')
|
||||
: value && updatedAt && !dirty
|
||||
? t('message.updatedAt', { when: messageDateLabel(updatedAt, now) })
|
||||
: t('message.length', { count: draft.length, max: MESSAGE_MAX_LENGTH })}
|
||||
</span>
|
||||
</div>
|
||||
{error ? (
|
||||
<div className={cx('font-mono text-[10.5px] font-bold', dark ? 'text-orange' : 'text-orange-dark')}>
|
||||
{error instanceof ApiError ? t(error.code) : t('error.generic')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { t } from '@/i18n';
|
||||
|
||||
/** Repère horizontal « MAINTENANT », à insérer dans une liste triée
|
||||
* chronologiquement (créneaux d'un bénévole, créneaux d'un poste…) au point
|
||||
* correspondant à l'heure réelle. Purement visuel. */
|
||||
export function NowLine() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-1" aria-hidden="true">
|
||||
<div className="h-[3px] flex-1 bg-orange" />
|
||||
<div className="bg-orange px-[7px] py-[3px] font-mono text-[10px] font-bold text-white">
|
||||
{t('common.now')}
|
||||
</div>
|
||||
<div className="h-[3px] flex-1 bg-orange" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user