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)
|
||||
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>
|
||||
)}
|
||||
<iframe
|
||||
src={masterFileUrl}
|
||||
title={field.label || 'Document'}
|
||||
className="w-full rounded border border-gray-200 bg-white"
|
||||
style={{ height: '520px', border: 'none' }}
|
||||
/>
|
||||
{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: '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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user