mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 12:41:27 +00:00
fix: Boutons de navigation + mise en page de l'aperçu du formulaire dynamique
This commit is contained in:
@ -36,6 +36,8 @@ export default function FormRenderer({
|
|||||||
}, // Callback de soumission personnalisé (optionnel)
|
}, // Callback de soumission personnalisé (optionnel)
|
||||||
masterFile = null,
|
masterFile = null,
|
||||||
}) {
|
}) {
|
||||||
|
const formFields = formConfig?.fields || [];
|
||||||
|
|
||||||
const resolveMasterFileUrl = (fileValue) => {
|
const resolveMasterFileUrl = (fileValue) => {
|
||||||
if (!fileValue) return null;
|
if (!fileValue) return null;
|
||||||
if (typeof fileValue !== 'string') return null;
|
if (typeof fileValue !== 'string') return null;
|
||||||
@ -50,6 +52,52 @@ export default function FormRenderer({
|
|||||||
|
|
||||||
const masterFileUrl = resolveMasterFileUrl(masterFile);
|
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 {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
@ -149,14 +197,14 @@ export default function FormRenderer({
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit, onError)}
|
onSubmit={handleSubmit(onSubmit, onError)}
|
||||||
className="max-w-md mx-auto"
|
className={formContainerClass}
|
||||||
>
|
>
|
||||||
{csrfToken ? <DjangoCSRFToken csrfToken={csrfToken} /> : null}
|
{csrfToken ? <DjangoCSRFToken csrfToken={csrfToken} /> : null}
|
||||||
<h2 className="text-2xl font-bold text-center mb-4">
|
<h2 className="text-2xl font-bold text-center mb-4">
|
||||||
{formConfig?.title || 'Formulaire'}
|
{formConfig?.title || 'Formulaire'}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{(formConfig?.fields || []).map((field) => (
|
{formFields.map((field) => (
|
||||||
<div
|
<div
|
||||||
key={field.id || `field-${Math.random().toString(36).substr(2, 9)}`}
|
key={field.id || `field-${Math.random().toString(36).substr(2, 9)}`}
|
||||||
className="flex flex-col mt-4"
|
className="flex flex-col mt-4"
|
||||||
@ -355,12 +403,29 @@ export default function FormRenderer({
|
|||||||
{field.label}
|
{field.label}
|
||||||
</p>
|
</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
|
<iframe
|
||||||
src={masterFileUrl}
|
src={masterFileUrl}
|
||||||
title={field.label || 'Document'}
|
title={field.label || 'Document'}
|
||||||
className="w-full rounded border border-gray-200 bg-white"
|
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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<FileUpload
|
<FileUpload
|
||||||
|
|||||||
@ -259,9 +259,9 @@ export default function DynamicFormsList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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">
|
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||||
Formulaires à compléter
|
Formulaires à compléter
|
||||||
</h3>
|
</h3>
|
||||||
@ -404,9 +404,9 @@ export default function DynamicFormsList({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-3/4">
|
<div className="w-full lg:w-3/4 min-w-0">
|
||||||
{currentTemplate && (
|
{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="mb-6">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<h3 className="text-xl font-semibold text-gray-800">
|
<h3 className="text-xl font-semibold text-gray-800">
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import SiblingInputFields from '@/components/Inscription/SiblingInputFields';
|
|||||||
import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector';
|
import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector';
|
||||||
import ProgressStep from '@/components/ProgressStep';
|
import ProgressStep from '@/components/ProgressStep';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { ChevronLeft, ChevronRight, Check, X } from 'lucide-react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composant de formulaire d'inscription partagé
|
* Composant de formulaire d'inscription partagé
|
||||||
@ -761,6 +762,19 @@ export default function InscriptionFormShared({
|
|||||||
'Documents parent',
|
'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) => {
|
const isStepValid = (stepNumber) => {
|
||||||
switch (stepNumber) {
|
switch (stepNumber) {
|
||||||
case 1:
|
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
|
// Rendu du composant
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto p-6">
|
<div className="mx-auto p-6">
|
||||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||||
<ProgressStep
|
<ProgressStep
|
||||||
steps={steps}
|
steps={activeSteps}
|
||||||
stepTitles={stepTitles}
|
stepTitles={activeStepTitles}
|
||||||
currentStep={currentPage}
|
currentStep={currentPage}
|
||||||
setStep={setCurrentPage}
|
setStep={setCurrentPage}
|
||||||
isStepValid={isStepValid}
|
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">
|
<div className="flex-1 h-full mt-6">
|
||||||
{/* Page 1 : Informations sur l'élève */}
|
{/* Page 1 : Informations sur l'élève */}
|
||||||
{currentPage === 1 && (
|
{currentPage === 1 && (
|
||||||
@ -874,7 +975,7 @@ export default function InscriptionFormShared({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Dernière page : Section Fichiers parents */}
|
{/* Dernière page : Section Fichiers parents */}
|
||||||
{currentPage === 6 && (
|
{currentPage === 6 && hasParentFilesStep && (
|
||||||
<FilesToUpload
|
<FilesToUpload
|
||||||
parentFileTemplates={parentFileTemplates}
|
parentFileTemplates={parentFileTemplates}
|
||||||
uploadedFiles={uploadedFiles}
|
uploadedFiles={uploadedFiles}
|
||||||
@ -885,72 +986,6 @@ export default function InscriptionFormShared({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user