Badges : photo du bénévole et texte libre

Champ « Photo du bénévole » (PHOTO) : photo du formulaire recadrée au
cadre, orientation EXIF appliquée, réduite à 300 dpi ; cadre vide sans
photo. Champ « Texte libre » (FREE_TEXT) alimenté par le nouveau « Texte
du badge » de la fiche bénévole (Volunteer.badge_text, migration 0024),
repris dans l'export Excel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-27 18:12:09 +01:00
co-authored by Claude Opus 5.5
parent 5b493c9737
commit 54cba467fc
17 changed files with 325 additions and 33 deletions
+66 -25
View File
@@ -45,13 +45,29 @@ function useEditorScale(badgeWidthMm: number): number {
type FieldState = BadgeFieldIn & { enabled: boolean };
const FIELD_TYPES: BadgeFieldType[] = ['FULL_NAME', 'POSITIONS', 'CATEGORY', 'ROLE'];
const FIELD_TYPES: BadgeFieldType[] = [
'FULL_NAME',
'POSITIONS',
'CATEGORY',
'ROLE',
'PHOTO',
'FREE_TEXT',
];
const FIELD_LABEL_KEY: Record<BadgeFieldType, string> = {
FULL_NAME: 'badges.field.fullName',
POSITIONS: 'badges.field.positions',
CATEGORY: 'badges.field.category',
ROLE: 'badges.field.role',
PHOTO: 'badges.field.photo',
FREE_TEXT: 'badges.field.freeText',
};
// Indication affichée à côté du nom du champ, pour ceux dont l'origine du
// contenu n'est pas évidente.
const FIELD_HELP_KEY: Partial<Record<BadgeFieldType, string>> = {
PHOTO: 'badges.field.photo.help',
FREE_TEXT: 'badges.field.freeText.help',
};
const FIELD_PLACEHOLDER_KEY: Record<BadgeFieldType, string> = {
@@ -59,6 +75,8 @@ const FIELD_PLACEHOLDER_KEY: Record<BadgeFieldType, string> = {
POSITIONS: 'badges.field.positions.placeholder',
CATEGORY: 'badges.field.category.placeholder',
ROLE: 'badges.field.role.placeholder',
PHOTO: 'badges.field.photo.placeholder',
FREE_TEXT: 'badges.field.freeText.placeholder',
};
// Position/taille par défaut à l'activation d'un champ — empilés
@@ -83,6 +101,16 @@ const FIELD_DEFAULTS: Record<BadgeFieldType, Omit<BadgeFieldIn, 'field_type'>> =
x_mm: 5, y_mm: 47, width_mm: 70, height_mm: 6,
font_size: 8, align: 'LEFT', bold: true, color: '#a1442b',
},
// Format portrait d'identité (≈ 3,5 × 4,5 cm réduit), en haut à droite.
// Taille de police/couleur sans effet : la photo est une image.
PHOTO: {
x_mm: 58, y_mm: 4, width_mm: 22, height_mm: 28,
font_size: 8, align: 'CENTER', bold: false, color: '#000000',
},
FREE_TEXT: {
x_mm: 5, y_mm: 39, width_mm: 50, height_mm: 7,
font_size: 9, align: 'LEFT', bold: true, color: '#000000',
},
};
function clamp(value: number, min: number, max: number): number {
@@ -445,18 +473,24 @@ function FieldBox({
height: field.height_mm * scale,
}}
>
<span
className="pointer-events-none whitespace-pre-line font-sans leading-tight"
style={{
fontSize: field.font_size * (scale / 2.83), // pt -> px approx. à l'échelle du badge
fontWeight: field.bold ? 800 : 400,
color: field.color,
width: '100%',
textAlign: field.align === 'CENTER' ? 'center' : field.align === 'RIGHT' ? 'right' : 'left',
}}
>
{t(FIELD_PLACEHOLDER_KEY[field.field_type])}
</span>
{field.field_type === 'PHOTO' ? (
<span className="pointer-events-none flex h-full w-full items-center justify-center bg-sand-dark/70 font-mono text-[10px] font-bold uppercase text-ink/60">
{t(FIELD_PLACEHOLDER_KEY.PHOTO)}
</span>
) : (
<span
className="pointer-events-none whitespace-pre-line font-sans leading-tight"
style={{
fontSize: field.font_size * (scale / 2.83), // pt -> px approx. à l'échelle du badge
fontWeight: field.bold ? 800 : 400,
color: field.color,
width: '100%',
textAlign: field.align === 'CENTER' ? 'center' : field.align === 'RIGHT' ? 'right' : 'left',
}}
>
{t(FIELD_PLACEHOLDER_KEY[field.field_type])}
</span>
)}
<div
onPointerDown={startResize}
onPointerMove={onResizeMove}
@@ -482,19 +516,26 @@ function FieldEditorRow({
}) {
return (
<div className={cx('flex flex-col gap-2 p-3', active && 'bg-yellow-soft')} onClick={onFocus}>
<label className="flex items-center gap-[8px]">
<input
type="checkbox"
checked={field.enabled}
onChange={(e) => onChange({ enabled: e.target.checked })}
className="h-[16px] w-[16px] accent-ink"
/>
<span className="font-display text-[12px] font-extrabold uppercase tracking-wide">
{t(FIELD_LABEL_KEY[type])}
</span>
</label>
<div className="flex items-center">
<label className="flex items-center gap-[8px]">
<input
type="checkbox"
checked={field.enabled}
onChange={(e) => onChange({ enabled: e.target.checked })}
className="h-[16px] w-[16px] accent-ink"
/>
<span className="font-display text-[12px] font-extrabold uppercase tracking-wide">
{t(FIELD_LABEL_KEY[type])}
</span>
</label>
{FIELD_HELP_KEY[type] ? (
<HelpTip text={t(FIELD_HELP_KEY[type])} className="ml-[6px]" />
) : null}
</div>
{field.enabled ? (
{/* La photo n'a pas de style de texte : seuls sa place et son cadre
(sur l'aperçu) comptent. */}
{field.enabled && type !== 'PHOTO' ? (
<div className="ml-[24px] flex flex-wrap items-center gap-[10px]">
<label className="flex items-center gap-[5px]">
<FieldLabel>{t('badges.field.size')}</FieldLabel>