fix: Boutons de navigation + mise en page de l'aperçu du formulaire dynamique

This commit is contained in:
N3WT DE COMPET
2026-04-05 10:36:15 +02:00
parent ccdbae1c08
commit 762dede0af
3 changed files with 181 additions and 81 deletions

View File

@ -36,6 +36,8 @@ export default function FormRenderer({
}, // Callback de soumission personnalisé (optionnel)
masterFile = null,
}) {
const formFields = formConfig?.fields || [];
const resolveMasterFileUrl = (fileValue) => {
if (!fileValue) return null;
if (typeof fileValue !== 'string') return null;
@ -50,6 +52,52 @@ export default function FormRenderer({
const masterFileUrl = resolveMasterFileUrl(masterFile);
const detectMasterFileType = (fileUrl) => {
if (!fileUrl || typeof fileUrl !== 'string') return 'unknown';
let candidate = fileUrl;
if (fileUrl.startsWith('/api/download?')) {
const queryPart = fileUrl.split('?')[1] || '';
const params = new URLSearchParams(queryPart);
const pathFromQuery = params.get('path') || params.get('file');
if (pathFromQuery) {
candidate = pathFromQuery;
}
}
const cleanUrl = candidate.split('?')[0];
let lowerUrl = cleanUrl.toLowerCase();
try {
lowerUrl = decodeURIComponent(cleanUrl).toLowerCase();
} catch {
lowerUrl = cleanUrl.toLowerCase();
}
if (lowerUrl.endsWith('.pdf')) return 'pdf';
if (
lowerUrl.endsWith('.png') ||
lowerUrl.endsWith('.jpg') ||
lowerUrl.endsWith('.jpeg') ||
lowerUrl.endsWith('.gif') ||
lowerUrl.endsWith('.webp') ||
lowerUrl.endsWith('.bmp') ||
lowerUrl.endsWith('.svg')
) {
return 'image';
}
return 'other';
};
const masterFileType = detectMasterFileType(masterFileUrl);
const hasFileField = formFields.some((field) => field.type === 'file');
const formContainerClass = hasFileField
? 'w-full max-w-4xl mx-auto'
: 'w-full max-w-md mx-auto';
const {
handleSubmit,
control,
@ -149,14 +197,14 @@ export default function FormRenderer({
return (
<form
onSubmit={handleSubmit(onSubmit, onError)}
className="max-w-md mx-auto"
className={formContainerClass}
>
{csrfToken ? <DjangoCSRFToken csrfToken={csrfToken} /> : null}
<h2 className="text-2xl font-bold text-center mb-4">
{formConfig?.title || 'Formulaire'}
</h2>
{(formConfig?.fields || []).map((field) => (
{formFields.map((field) => (
<div
key={field.id || `field-${Math.random().toString(36).substr(2, 9)}`}
className="flex flex-col mt-4"
@ -355,12 +403,29 @@ export default function FormRenderer({
{field.label}
</p>
)}
{masterFileType === 'image' ? (
<img
src={masterFileUrl}
alt={field.label || 'Document'}
className="w-full h-auto rounded border border-gray-200 bg-white"
/>
) : masterFileType === 'pdf' ? (
<iframe
src={masterFileUrl}
title={field.label || 'Document'}
className="w-full rounded border border-gray-200 bg-white"
style={{ height: '520px', border: 'none' }}
style={{ height: '720px', border: 'none' }}
/>
) : (
<a
href={masterFileUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-4 py-2 rounded bg-primary text-white hover:bg-secondary"
>
Ouvrir le document
</a>
)}
</div>
) : (
<FileUpload

View File

@ -259,9 +259,9 @@ export default function DynamicFormsList({
}
return (
<div className="mt-8 mb-4 w-full mx-auto flex gap-8">
<div className="mt-8 mb-4 w-full mx-auto flex flex-col lg:flex-row gap-8 overflow-x-hidden">
{/* Liste des formulaires */}
<div className="w-1/4 bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-200">
<div className="w-full lg:w-1/4 bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-200">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
Formulaires à compléter
</h3>
@ -404,9 +404,9 @@ export default function DynamicFormsList({
})()}
</div>
<div className="w-3/4">
<div className="w-full lg:w-3/4 min-w-0">
{currentTemplate && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 overflow-x-hidden">
<div className="mb-6">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-xl font-semibold text-gray-800">

View File

@ -31,6 +31,7 @@ import SiblingInputFields from '@/components/Inscription/SiblingInputFields';
import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector';
import ProgressStep from '@/components/ProgressStep';
import { useRouter } from 'next/navigation';
import { ChevronLeft, ChevronRight, Check, X } from 'lucide-react';
/**
* Composant de formulaire d'inscription partagé
@ -761,6 +762,19 @@ export default function InscriptionFormShared({
'Documents parent',
];
const hasParentFilesStep = parentFileTemplates.length > 0;
const activeSteps = hasParentFilesStep ? steps : steps.slice(0, 5);
const activeStepTitles = hasParentFilesStep
? stepTitles
: {
1: stepTitles[1],
2: stepTitles[2],
3: stepTitles[3],
4: stepTitles[4],
5: stepTitles[5],
};
const isStepValid = (stepNumber) => {
switch (stepNumber) {
case 1:
@ -780,13 +794,42 @@ export default function InscriptionFormShared({
}
};
useEffect(() => {
if (!hasParentFilesStep && currentPage > 5) {
setCurrentPage(5);
}
}, [hasParentFilesStep, currentPage]);
const nextDisabled =
(currentPage === 1 && !isPage1Valid) ||
(currentPage === 2 && !isPage2Valid) ||
(currentPage === 3 && !isPage3Valid) ||
(currentPage === 4 && !isPage4Valid) ||
(currentPage === 5 && !isPage5Valid);
const submitDisabled = !isStepValid(currentPage);
const navButtonBaseClass =
'min-w-[124px] min-h-[44px] px-5 rounded font-label text-sm font-semibold transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-primary/30';
const navPrimaryClass = nextDisabled
? `${navButtonBaseClass} bg-gray-200 text-gray-500 cursor-not-allowed`
: `${navButtonBaseClass} bg-primary hover:bg-secondary text-white`;
const navSubmitClass = submitDisabled
? `${navButtonBaseClass} bg-gray-200 text-gray-500 cursor-not-allowed`
: `${navButtonBaseClass} bg-primary hover:bg-secondary text-white`;
const navSecondaryClass =
`${navButtonBaseClass} bg-neutral text-secondary border border-gray-300 hover:bg-white`;
// Rendu du composant
return (
<div className="mx-auto p-6">
<DjangoCSRFToken csrfToken={csrfToken} />
<ProgressStep
steps={steps}
stepTitles={stepTitles}
steps={activeSteps}
stepTitles={activeStepTitles}
currentStep={currentPage}
setStep={setCurrentPage}
isStepValid={isStepValid}
@ -802,6 +845,64 @@ export default function InscriptionFormShared({
/>
)}
{/* Navigation toujours au meme endroit (en haut a gauche) */}
<div className="mt-6 mb-4 flex items-center justify-start gap-3">
{enable ? (
<>
{currentPage > 1 && (
<Button
text="Précédent"
icon={<ChevronLeft size={16} strokeWidth={1.8} />}
onClick={(e) => {
e.preventDefault();
handlePreviousPage();
}}
primary
className={navSecondaryClass}
/>
)}
{currentPage < activeSteps.length ? (
<Button
text={
<span className="inline-flex items-center gap-2">
<span>Suivant</span>
<ChevronRight size={16} strokeWidth={1.8} />
</span>
}
onClick={(e) => {
e.preventDefault();
handleNextPage();
}}
className={navPrimaryClass}
disabled={nextDisabled}
primary
name="Next"
/>
) : (
<Button
text="Valider"
icon={<Check size={16} strokeWidth={1.8} />}
onClick={(e) => {
e.preventDefault();
handleSubmit(e);
}}
className={navSubmitClass}
disabled={submitDisabled}
primary
/>
)}
</>
) : (
<Button
onClick={() => router.push(FE_PARENTS_HOME_URL)}
text="Quitter"
icon={<X size={16} strokeWidth={1.8} />}
primary
className={`${navButtonBaseClass} bg-primary text-white hover:bg-secondary shadow-sm`}
/>
)}
</div>
<div className="flex-1 h-full mt-6">
{/* Page 1 : Informations sur l'élève */}
{currentPage === 1 && (
@ -874,7 +975,7 @@ export default function InscriptionFormShared({
)}
{/* Dernière page : Section Fichiers parents */}
{currentPage === 6 && (
{currentPage === 6 && hasParentFilesStep && (
<FilesToUpload
parentFileTemplates={parentFileTemplates}
uploadedFiles={uploadedFiles}
@ -885,72 +986,6 @@ export default function InscriptionFormShared({
)}
</div>
{/* Boutons de contrôle */}
<div className="flex justify-center space-x-4 mt-12">
{enable ? (
<>
{currentPage > 1 && (
<Button
text="Précédent"
onClick={(e) => {
e.preventDefault();
handlePreviousPage();
}}
primary
/>
)}
{currentPage < steps.length ? (
<Button
text="Suivant"
onClick={(e) => {
e.preventDefault();
handleNextPage();
}}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
(currentPage === 1 && !isPage1Valid) ||
(currentPage === 2 && !isPage2Valid) ||
(currentPage === 3 && !isPage3Valid) ||
(currentPage === 4 && !isPage4Valid) ||
(currentPage === 5 && !isPage5Valid)
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
: 'bg-emerald-500 text-white hover:bg-emerald-600'
}`}
disabled={
(currentPage === 1 && !isPage1Valid) ||
(currentPage === 2 && !isPage2Valid) ||
(currentPage === 3 && !isPage3Valid) ||
(currentPage === 4 && !isPage4Valid) ||
(currentPage === 5 && !isPage5Valid)
}
primary
name="Next"
/>
) : (
<Button
text="Valider"
onClick={(e) => {
e.preventDefault();
handleSubmit(e);
}}
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
currentPage === 6 && !isPage6Valid
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
: 'bg-emerald-500 text-white hover:bg-emerald-600'
}`}
disabled={currentPage === 6 && !isPage6Valid}
primary
/>
)}
</>
) : (
<Button
onClick={() => router.push(FE_PARENTS_HOME_URL)}
text="Quitter"
primary
className="bg-emerald-500 text-white hover:bg-emerald-600"
/>
)}
</div>
</div>
);
}