Étape 21 : formulaire public d'inscription des bénévoles
Retire « constructeur de formulaire » des exclusions du CLAUDE.md (§ 5) — décision explicite prise en conversation. Un formulaire par édition (COORDINATION choisit les champs actifs parmi neuf : email, téléphone, vœux de poste — max 2, vœux d'artiste — un par soir de festival, vœu de compagnon en texte libre, disponibilité/ cotisation déclaratives, photo, commentaire ; nom/prénom toujours collectés) ouvert/fermé à volonté, avec un lien à partager (/inscription, aucun compte requis). Backend : FormTemplate/FormField (migration 0013), quatre nouveaux champs sur Volunteer (photo, companion_wish_note, self_declared_available, willing_to_pay_fee), app/api/forms.py — les deux seuls endpoints publics de toute l'API (GET .../form/public, POST .../form/public/submit), protégés par hCaptcha (app/forms/captcha.py, désactivé sans HCAPTCHA_SECRET_KEY) et un filet de sécurité global par édition (form_submission_limiter). resolve_owned_ids/set_artists/ set_wished_positions déplacés vers app/api/_common.py, partagés avec volunteers.py. Frontend : écran de configuration (/inscriptions) avec lien copiable et bascule ouvert/fermé, formulaire public (/inscription, hors RequireAuth) avec widget hCaptcha chargé dynamiquement, capture photo native (<input capture>), vœux de poste/artiste avec les mêmes règles que le back-end. Réponses du formulaire visibles et éditables depuis la fiche bénévole existante (ConfigVolunteers.tsx). Vérifié : pytest (273 passés), ruff, alembic check, tsc --noEmit, eslint, vite build, smoke (rendu SSR de chaque écran + flux de bout en bout du formulaire en mode démonstration) — tous verts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
55c5d539a8
commit
82088f3b0c
@@ -23,10 +23,13 @@ import type {
|
||||
DayPartCreate,
|
||||
DayPartUpdate,
|
||||
EditionUpdate,
|
||||
FormFieldsSet,
|
||||
FormTemplateUpdate,
|
||||
ImportMapping,
|
||||
PlanningNotificationRequest,
|
||||
PositionCreate,
|
||||
PositionUpdate,
|
||||
PublicFormSubmission,
|
||||
ShiftCreate,
|
||||
ShiftUpdate,
|
||||
VolunteerCreate,
|
||||
@@ -97,6 +100,30 @@ export function useVolunteerPlanning(volunteerId: string | null | undefined) {
|
||||
});
|
||||
}
|
||||
|
||||
/** URL objet (aperçu) de la photo d'un bénévole (formulaire public), ou
|
||||
* `null` si absente. */
|
||||
export function useVolunteerPhotoUrl(volunteerId: string | null | undefined, hasPhoto: boolean) {
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
let objectUrl: string | null = null;
|
||||
let cancelled = false;
|
||||
if (!hasPhoto || !volunteerId) {
|
||||
setUrl(null);
|
||||
return;
|
||||
}
|
||||
api.volunteers.photoBlob(edition, volunteerId).then((blob) => {
|
||||
if (cancelled || !blob) return;
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
setUrl(objectUrl);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
}, [volunteerId, hasPhoto]);
|
||||
return url;
|
||||
}
|
||||
|
||||
export function useShifts(positionId?: string) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.shifts(positionId),
|
||||
@@ -553,3 +580,50 @@ export function useGenerateBadges() {
|
||||
mutationFn: (body: BadgeGenerateRequest) => api.badges.generate(edition, body),
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Formulaire public d'inscription //
|
||||
// --------------------------------------------------------------------------- //
|
||||
/** Configuration du formulaire (COORDINATION). */
|
||||
export function useFormTemplate() {
|
||||
return useQuery({
|
||||
queryKey: ['formTemplate', edition],
|
||||
queryFn: () => api.forms.getTemplate(edition),
|
||||
});
|
||||
}
|
||||
|
||||
function useInvalidateFormTemplate() {
|
||||
const client = useQueryClient();
|
||||
return () => client.invalidateQueries({ queryKey: ['formTemplate', edition] });
|
||||
}
|
||||
|
||||
export function useSetFormFields() {
|
||||
const invalidate = useInvalidateFormTemplate();
|
||||
return useMutation({
|
||||
mutationFn: (body: FormFieldsSet) => api.forms.setFields(edition, body),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSetFormOpen() {
|
||||
const invalidate = useInvalidateFormTemplate();
|
||||
return useMutation({
|
||||
mutationFn: (body: FormTemplateUpdate) => api.forms.setOpen(edition, body),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
/** Configuration publique du formulaire — utilisable sans compte connecté
|
||||
* (écran `/inscription`, hors de `RequireAuth`). */
|
||||
export function usePublicFormConfig() {
|
||||
return useQuery({
|
||||
queryKey: ['publicFormConfig', edition],
|
||||
queryFn: () => api.forms.getPublicConfig(edition),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSubmitPublicForm() {
|
||||
return useMutation({
|
||||
mutationFn: (body: PublicFormSubmission) => api.forms.submitPublic(edition, body),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user