Formulaire public : logo et texte d'introduction personnalisables
FormTemplate porte désormais un logo (image, PUT/GET .../form/logo — GET public comme .../form/public, affiché sur /inscription) et un texte d'introduction libre (PATCH .../form, description désormais optionnelle comme is_open). Écran de configuration (/inscriptions) : nouveau panneau « Personnalisation » pour importer le logo et rédiger le texte. Le formulaire public affiche le logo en grand au-dessus du panneau et le texte juste sous le titre. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
48d3deec65
commit
957a004623
@@ -615,7 +615,9 @@ export function useSetFormFields() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useSetFormOpen() {
|
||||
/** `PATCH /form` : `is_open` et/ou `description`, chacun optionnel
|
||||
* (absent = inchangé — voir `FormTemplateUpdate`). */
|
||||
export function useUpdateFormTemplate() {
|
||||
const invalidate = useInvalidateFormTemplate();
|
||||
return useMutation({
|
||||
mutationFn: (body: FormTemplateUpdate) => api.forms.setOpen(edition, body),
|
||||
@@ -623,6 +625,40 @@ export function useSetFormOpen() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useUploadFormLogo() {
|
||||
const invalidate = useInvalidateFormTemplate();
|
||||
return useMutation({
|
||||
mutationFn: (file: File) => api.forms.uploadLogo(edition, file),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
/** URL objet (aperçu) du logo du formulaire public, ou `null` si absent.
|
||||
* Utilisable sans compte connecté (`GET .../form/logo` n'est pas protégé —
|
||||
* affiché sur `/inscription`). `refreshKey` (ex. `dataUpdatedAt` de
|
||||
* `useFormTemplate`) force un nouveau fetch après un changement de logo. */
|
||||
export function useFormLogoUrl(hasLogo: boolean, refreshKey: number | string) {
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
let objectUrl: string | null = null;
|
||||
let cancelled = false;
|
||||
if (!hasLogo) {
|
||||
setUrl(null);
|
||||
return;
|
||||
}
|
||||
api.forms.logoBlob(edition).then((blob) => {
|
||||
if (cancelled || !blob) return;
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
setUrl(objectUrl);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
}, [hasLogo, refreshKey]);
|
||||
return url;
|
||||
}
|
||||
|
||||
/** Configuration publique du formulaire — utilisable sans compte connecté
|
||||
* (écran `/inscription`, hors de `RequireAuth`). */
|
||||
export function usePublicFormConfig() {
|
||||
|
||||
@@ -276,7 +276,13 @@ export function createHttpApi(baseUrl: string): PlanningApi {
|
||||
}),
|
||||
setOpen: (editionId, body: FormTemplateUpdate) =>
|
||||
request(`${base(editionId)}/form`, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
uploadLogo: (editionId, file: File) => {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
return request(`${base(editionId)}/form/logo`, { method: 'PUT', body: form });
|
||||
},
|
||||
getPublicConfig: (editionId) => request(`${base(editionId)}/form/public`),
|
||||
logoBlob: (editionId) => requestBlob(`${base(editionId)}/form/logo`),
|
||||
submitPublic: (editionId, body: PublicFormSubmission) => {
|
||||
const form = new FormData();
|
||||
form.append('first_name', body.first_name);
|
||||
|
||||
@@ -53,6 +53,9 @@ export interface MockState {
|
||||
formTemplate: {
|
||||
isOpen: boolean;
|
||||
fields: FormFieldType[];
|
||||
logo: string | null; // data URL
|
||||
logoType: string | null;
|
||||
description: string | null;
|
||||
};
|
||||
/** volunteerId -> photo (formulaire public), data URL. */
|
||||
photos: Map<string, string>;
|
||||
@@ -442,7 +445,7 @@ export function buildMockState(): MockState {
|
||||
backImageType: null,
|
||||
fields: [],
|
||||
},
|
||||
formTemplate: { isOpen: true, fields: [] },
|
||||
formTemplate: { isOpen: true, fields: [], logo: null, logoType: null, description: null },
|
||||
photos: new Map(),
|
||||
seq,
|
||||
};
|
||||
|
||||
@@ -122,6 +122,14 @@ const fileToDataUrl = (file: File): Promise<string> =>
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
const formTemplateRead = (): FormTemplateRead => ({
|
||||
edition_id: state.edition.id,
|
||||
is_open: state.formTemplate.isOpen,
|
||||
fields: state.formTemplate.fields,
|
||||
has_logo: state.formTemplate.logo !== null,
|
||||
description: state.formTemplate.description,
|
||||
});
|
||||
|
||||
// PDF valide minimal (une page blanche) — remplace la vraie génération
|
||||
// (réservée au back-end réel) pour que le téléchargement fonctionne en mode
|
||||
// démonstration.
|
||||
@@ -1467,27 +1475,24 @@ export function createMockApi(): PlanningApi {
|
||||
},
|
||||
|
||||
forms: {
|
||||
getTemplate: (): Promise<FormTemplateRead> =>
|
||||
delay({
|
||||
edition_id: state.edition.id,
|
||||
is_open: state.formTemplate.isOpen,
|
||||
fields: state.formTemplate.fields,
|
||||
}),
|
||||
getTemplate: (): Promise<FormTemplateRead> => delay(formTemplateRead()),
|
||||
setFields: (_edition, body: FormFieldsSet) => {
|
||||
state.formTemplate.fields = [...new Set(body.fields)];
|
||||
return delay<FormTemplateRead>({
|
||||
edition_id: state.edition.id,
|
||||
is_open: state.formTemplate.isOpen,
|
||||
fields: state.formTemplate.fields,
|
||||
});
|
||||
return delay<FormTemplateRead>(formTemplateRead());
|
||||
},
|
||||
setOpen: (_edition, body: FormTemplateUpdate) => {
|
||||
state.formTemplate.isOpen = body.is_open;
|
||||
return delay<FormTemplateRead>({
|
||||
edition_id: state.edition.id,
|
||||
is_open: state.formTemplate.isOpen,
|
||||
fields: state.formTemplate.fields,
|
||||
});
|
||||
if (body.is_open !== undefined) state.formTemplate.isOpen = body.is_open;
|
||||
if (body.description !== undefined) state.formTemplate.description = body.description;
|
||||
return delay<FormTemplateRead>(formTemplateRead());
|
||||
},
|
||||
uploadLogo: async (_edition, file: File) => {
|
||||
state.formTemplate.logo = await fileToDataUrl(file);
|
||||
state.formTemplate.logoType = file.type;
|
||||
return delay<FormTemplateRead>(formTemplateRead());
|
||||
},
|
||||
logoBlob: async () => {
|
||||
if (!state.formTemplate.logo) return null;
|
||||
return (await fetch(state.formTemplate.logo)).blob();
|
||||
},
|
||||
getPublicConfig: (): Promise<PublicFormConfig> => {
|
||||
const fields = state.formTemplate.fields;
|
||||
@@ -1504,6 +1509,8 @@ export function createMockApi(): PlanningApi {
|
||||
performance_date: a.performance_date,
|
||||
}))
|
||||
: [],
|
||||
has_logo: state.formTemplate.logo !== null,
|
||||
description: state.formTemplate.description,
|
||||
});
|
||||
},
|
||||
submitPublic: async (_edition, body: PublicFormSubmission) => {
|
||||
|
||||
@@ -571,10 +571,14 @@ export interface FormTemplateRead {
|
||||
edition_id: string;
|
||||
is_open: boolean;
|
||||
fields: FormFieldType[];
|
||||
has_logo: boolean;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
/** `undefined`/absent = inchangé pour chaque champ. */
|
||||
export interface FormTemplateUpdate {
|
||||
is_open: boolean;
|
||||
is_open?: boolean;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface PublicPositionOption {
|
||||
@@ -594,6 +598,8 @@ export interface PublicFormConfig {
|
||||
fields: FormFieldType[];
|
||||
positions: PublicPositionOption[];
|
||||
artists: PublicArtistOption[];
|
||||
has_logo: boolean;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface FormSubmissionResult {
|
||||
|
||||
@@ -167,9 +167,12 @@ export interface PlanningApi {
|
||||
getTemplate(editionId: string): Promise<FormTemplateRead>;
|
||||
setFields(editionId: string, body: FormFieldsSet): Promise<FormTemplateRead>;
|
||||
setOpen(editionId: string, body: FormTemplateUpdate): Promise<FormTemplateRead>;
|
||||
/** Publics — aucune authentification, utilisables sans compte connecté. */
|
||||
uploadLogo(editionId: string, file: File): Promise<FormTemplateRead>;
|
||||
/** Publics — aucune authentification, utilisables sans compte connecté
|
||||
* (y compris `logoBlob`, affiché sur `/inscription`). */
|
||||
getPublicConfig(editionId: string): Promise<PublicFormConfig>;
|
||||
submitPublic(editionId: string, body: PublicFormSubmission): Promise<FormSubmissionResult>;
|
||||
logoBlob(editionId: string): Promise<Blob | null>;
|
||||
};
|
||||
positionSheets: {
|
||||
/** Feuilles de poste PDF (une page par poste et par jour). */
|
||||
|
||||
Reference in New Issue
Block a user