diff --git a/CLAUDE.md b/CLAUDE.md index 119fb95..fbe8cc2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -821,6 +821,7 @@ Aucune dépendance locale requise hors Docker. | 29 | Jeu de démonstration : 8 bénévoles avec une cotisation classique payée, 4 avec une cotisation mécénat payée (adresse complète) — `app/seed.py:_add_demo_payments` — pour que l'export Excel et les reçus fiscaux mécénat (étape 28) aient de quoi montrer quelque chose d'emblée | à valider | | 30 | Infobulles (`HelpTip` et détail de cellule de la grille des besoins) repositionnées en `position: fixed` via JS (`lib/useTooltipPosition.ts`), bornées à l'intérieur de l'écran et basculant de côté si la place manque — corrige un centrage `absolute` qui les faisait déborder de l'écran près d'un bord, et une isolation `z-index` qui rendait celle de la grille invisible (coincée sous les en-têtes figés) | à valider | | 31 | Revue de sécurité : logo des emails en pièce jointe inline au lieu d'une URL (fuyait le `access_token` du formulaire public à tout destinataire — régression de l'étape 27, aucun test ne couvrait le rendu HTML du logo), sujet retiré du log de remise des emails (pouvait porter le nom d'un bénévole via un modèle personnalisé), image du formulaire public revalidée par décodage réel (`PIL`, pas seulement le `Content-Type` déclaré) avant stockage — évite qu'une fausse image fasse échouer la génération des badges/feuilles de poste, export Excel des bénévoles protégé contre l'injection de formule (CWE-1236, `app/exporting/xlsx.py:_safe_text`), sondage du statut de paiement en attente limité en fréquence par jeton (`pending_status_limiter`, évite un abus de l'API SumUp), `pyjwt` mis à jour (CVE listées non exploitables ici — HS256 à secret statique uniquement, jamais de JWKS — mais autant ne pas rester sur une version vulnérable) | à valider | +| 32 | Formulaire public : astérisque rouge sur les champs obligatoires (`RequiredMark`, `FieldLabel required`) et légende « * champ obligatoire » | à valider | **Reste hors périmètre** (modèle prêt à les accueillir) : SMS, constructeur de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans + diff --git a/frontend/src/components/ui.tsx b/frontend/src/components/ui.tsx index ca09dbb..f80f7ce 100644 --- a/frontend/src/components/ui.tsx +++ b/frontend/src/components/ui.tsx @@ -225,10 +225,20 @@ export function HelpTip({ ); } -export function FieldLabel({ children }: { children: ReactNode }) { +/** Astérisque rouge des champs obligatoires (purement visuel : la règle vit ailleurs). */ +export function RequiredMark() { + return ( + + ); +} + +export function FieldLabel({ children, required }: { children: ReactNode; required?: boolean }) { return ( {children} + {required ? : null} ); } diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index ff8c041..5cdc316 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -390,7 +390,7 @@ "config.vol.feeStatus.failed": "Cotisation : paiement échoué", "config.vol.feeStatus.expired": "Cotisation : lien de paiement expiré", "config.vol.mecenatAddress": "Adresse (cerfa)", - "form.field.photo": "Une magnifique photo de toi :", + "form.field.photo": "Une magnifique photo de toi pour ton badge :", "form.field.comment": "Commentaire", "form.link.title": "Lien du formulaire", "form.link.hint": "À partager aux bénévoles potentiels — n'importe qui muni du lien peut s'inscrire, mais lui seul : sans ce lien précis, impossible de tomber sur le formulaire.", @@ -436,6 +436,7 @@ "publicForm.sumup.afterSubmitHint": "Un lien de paiement en ligne (SumUp) te sera proposé juste après l'envoi de ton inscription.", "publicForm.captcha.disabled": "(Vérification anti-robot non configurée sur cette instance.)", "publicForm.submitting": "Envoi…", + "publicForm.requiredLegend": "champ obligatoire", "publicForm.submit": "Envoyer mon inscription", "publicForm.success.title": "Inscription reçue", "publicForm.success.body": "Merci ! Ton inscription a bien été enregistrée. La coordination du festival prendra contact avec toi.", diff --git a/frontend/src/pages/PublicSignupPage.tsx b/frontend/src/pages/PublicSignupPage.tsx index 84bdb11..2929563 100644 --- a/frontend/src/pages/PublicSignupPage.tsx +++ b/frontend/src/pages/PublicSignupPage.tsx @@ -12,7 +12,7 @@ import { } from '@/api/hooks'; import type { PublicArtistOption } from '@/api/schema'; import { ErrorNote, Loading } from '@/components/state'; -import { Btn, FieldLabel, Panel, PanelHeader, TextInput } from '@/components/ui'; +import { Btn, FieldLabel, Panel, PanelHeader, RequiredMark, TextInput } from '@/components/ui'; import { t } from '@/i18n'; import { ENV } from '@/lib/env'; import { longDayLabel } from '@/lib/time'; @@ -271,21 +271,21 @@ function MecenatInfoStep({ {t('publicForm.mecenat.hint')}

{error ? ( @@ -516,27 +516,31 @@ function PublicSignupForm({ accessToken }: { accessToken: string }) {

) : null}
+

+ * + {t('publicForm.requiredLegend')} +

{fields.has('EMAIL') ? ( ) : null} {fields.has('PHONE') ? ( ) : null} @@ -607,6 +611,26 @@ function PublicSignupForm({ accessToken }: { accessToken: string }) { ) : null} + {fields.has('PHOTO') ? ( + + ) : null} + {fields.has('COMMENT') ? ( ) : null} @@ -678,30 +705,10 @@ function PublicSignupForm({ accessToken }: { accessToken: string }) { onChange={(e) => setWillingToPayFee(e.target.checked)} className="h-[16px] w-[16px] accent-ink" /> - {t('publicForm.field.willingToPayFee')} - - ) : null} - - {fields.has('PHOTO') ? ( - ) : null}