mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 16:03:21 +00:00
feat: creation d'un FormRenderer.js pour creer un formulaire dynamique [NEWTS-17]
This commit is contained in:
37
Front-End/src/components/Form/Button.js
Normal file
37
Front-End/src/components/Form/Button.js
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const Button = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className,
|
||||
primary,
|
||||
icon,
|
||||
disabled,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const baseClass =
|
||||
'px-4 py-2 rounded-md text-white h-8 flex items-center justify-center';
|
||||
const primaryClass = 'bg-emerald-500 hover:bg-emerald-600';
|
||||
const secondaryClass = 'bg-gray-300 hover:bg-gray-400 text-black';
|
||||
const buttonClass = `${baseClass} ${primary && !disabled ? primaryClass : secondaryClass} ${className}`;
|
||||
|
||||
const handleClick = (e) => {
|
||||
if (href) {
|
||||
router.push(href);
|
||||
} else if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button className={buttonClass} onClick={handleClick} disabled={disabled}>
|
||||
{icon && text && <span className="mr-2">{icon}</span>}
|
||||
{icon && !text && icon}
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
60
Front-End/src/components/Form/DraggableFileUpload.js
Normal file
60
Front-End/src/components/Form/DraggableFileUpload.js
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Upload } from 'lucide-react';
|
||||
|
||||
export default function DraggableFileUpload({ fileName, onFileSelect }) {
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
event.preventDefault();
|
||||
setDragActive(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setDragActive(false);
|
||||
};
|
||||
|
||||
const handleFileChosen = (selectedFile) => {
|
||||
onFileSelect && onFileSelect(selectedFile);
|
||||
};
|
||||
|
||||
const handleDrop = (event) => {
|
||||
event.preventDefault();
|
||||
setDragActive(false);
|
||||
const droppedFile = event.dataTransfer.files[0];
|
||||
handleFileChosen(droppedFile);
|
||||
};
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
const selectedFile = event.target.files[0];
|
||||
handleFileChosen(selectedFile);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={`border-2 border-dashed p-8 rounded-md ${dragActive ? 'border-blue-500' : 'border-gray-300'} flex flex-col items-center justify-center`}
|
||||
style={{ height: '200px' }}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
id="fileInput"
|
||||
/>
|
||||
<label
|
||||
htmlFor="fileInput"
|
||||
className="cursor-pointer flex flex-col items-center"
|
||||
>
|
||||
<Upload size={48} className="text-gray-400 mb-2" />
|
||||
<p className="text-center">
|
||||
{fileName ||
|
||||
'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
115
Front-End/src/components/Form/FileUpload.js
Normal file
115
Front-End/src/components/Form/FileUpload.js
Normal file
@ -0,0 +1,115 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { CloudUpload } from 'lucide-react';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export default function FileUpload({
|
||||
selectionMessage,
|
||||
onFileSelect,
|
||||
uploadedFileName,
|
||||
existingFile,
|
||||
required,
|
||||
errorMsg,
|
||||
enable = true, // Nouvelle prop pour activer/désactiver le champ
|
||||
}) {
|
||||
const [localFileName, setLocalFileName] = useState(uploadedFileName || '');
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setLocalFileName(file.name);
|
||||
logger.debug('Fichier sélectionné:', file.name);
|
||||
onFileSelect(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileDrop = (e) => {
|
||||
e.preventDefault();
|
||||
if (!enable) return; // Empêcher le dépôt si désactivé
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) {
|
||||
setLocalFileName(file.name);
|
||||
logger.debug('Fichier déposé:', file.name);
|
||||
onFileSelect(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border p-4 rounded-md shadow-md ${
|
||||
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{`${selectionMessage}`}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</h3>
|
||||
<div
|
||||
className={`border-2 border-dashed p-6 rounded-lg flex flex-col items-center justify-center ${
|
||||
!enable
|
||||
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
|
||||
: 'border-gray-500 hover:border-emerald-500'
|
||||
}`}
|
||||
onClick={() => enable && fileInputRef.current.click()} // Désactiver le clic si `enable` est false
|
||||
onDragOver={(e) => enable && e.preventDefault()}
|
||||
onDrop={handleFileDrop}
|
||||
>
|
||||
<CloudUpload
|
||||
className={`w-12 h-12 mb-4 ${
|
||||
!enable ? 'text-gray-400' : 'text-emerald-500'
|
||||
}`}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf, .png, .jpg, .jpeg, .gif, .bmp"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
ref={fileInputRef}
|
||||
disabled={!enable} // Désactiver l'input si `enable` est false
|
||||
/>
|
||||
<label
|
||||
htmlFor="fileInput"
|
||||
className={`text-center ${
|
||||
!enable ? 'text-gray-400' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<p className="text-lg font-semibold">
|
||||
{enable ? 'Déposez votre fichier ici' : 'Téléversement désactivé'}
|
||||
</p>
|
||||
{enable && (
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
ou cliquez pour sélectionner un fichier
|
||||
</p>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Affichage du fichier existant */}
|
||||
{existingFile && !localFileName && (
|
||||
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
|
||||
<CloudUpload className="w-6 h-6 text-emerald-500" />
|
||||
<p className="text-sm font-medium text-gray-800">
|
||||
<span className="font-semibold">
|
||||
{typeof existingFile === 'string'
|
||||
? existingFile.split('/').pop() // Si c'est une chaîne, utilisez split
|
||||
: existingFile?.name || 'Nom de fichier inconnu'}{' '}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Affichage du fichier sélectionné */}
|
||||
{localFileName && (
|
||||
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
|
||||
<CloudUpload className="w-6 h-6 text-emerald-500" />
|
||||
<p className="text-sm font-medium text-gray-800">
|
||||
<span className="font-semibold">{localFileName}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Message d'erreur */}
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
194
Front-End/src/components/Form/FormRenderer.js
Normal file
194
Front-End/src/components/Form/FormRenderer.js
Normal file
@ -0,0 +1,194 @@
|
||||
import logger from '@/utils/logger';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import SelectChoice from './SelectChoice';
|
||||
import InputTextIcon from './InputTextIcon';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import Button from './Button';
|
||||
import DjangoCSRFToken from '../DjangoCSRFToken';
|
||||
import WisiwigTextArea from './WisiwigTextArea';
|
||||
|
||||
/*
|
||||
* Récupère une icône Lucide par son nom.
|
||||
*/
|
||||
export function getIcon(name) {
|
||||
if (Object.keys(LucideIcons).includes(name)) {
|
||||
const Icon = LucideIcons[name];
|
||||
return Icon ?? null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const formConfigTest = {
|
||||
id: 0,
|
||||
title: 'Mon formulaire dynamique',
|
||||
submitLabel: 'Envoyer',
|
||||
fields: [
|
||||
{ id: 'name', label: 'Nom', type: 'text', required: true },
|
||||
{ id: 'email', label: 'Email', type: 'email' },
|
||||
{
|
||||
id: 'email2',
|
||||
label: 'Email',
|
||||
type: 'text',
|
||||
icon: 'Mail',
|
||||
},
|
||||
{
|
||||
id: 'role',
|
||||
label: 'Rôle',
|
||||
type: 'select',
|
||||
options: ['Admin', 'Utilisateur', 'Invité'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
text: "Bonjour, Bienvenue dans ce formulaire d'inscription haha",
|
||||
},
|
||||
{
|
||||
id: 'birthdate',
|
||||
label: 'Date de naissance',
|
||||
type: 'date',
|
||||
icon: 'Calendar',
|
||||
},
|
||||
{
|
||||
id: 'textarea',
|
||||
label: 'toto',
|
||||
type: 'textarea',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default function FormRenderer({
|
||||
formConfig = formConfigTest,
|
||||
csrfToken,
|
||||
}) {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = (data) => {
|
||||
logger.debug('=== DÉBUT onSubmit ===');
|
||||
logger.debug('Réponses :', data);
|
||||
|
||||
const formattedData = {
|
||||
//TODO: idDossierInscriptions: 123,
|
||||
formId: formConfig.id,
|
||||
responses: { ...data },
|
||||
};
|
||||
|
||||
//TODO: ENVOYER LES DONNÉES AU BACKEND
|
||||
alert('Données reçues : ' + JSON.stringify(formattedData, null, 2));
|
||||
reset(); // Réinitialiser le formulaire après soumission
|
||||
logger.debug('=== FIN onSubmit ===');
|
||||
};
|
||||
|
||||
const onError = (errors) => {
|
||||
logger.error('=== ERREURS DE VALIDATION ===');
|
||||
logger.error('Erreurs :', errors);
|
||||
alert('Erreurs de validation : ' + JSON.stringify(errors, null, 2));
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
className="max-w-md mx-auto"
|
||||
>
|
||||
{csrfToken ? <DjangoCSRFToken csrfToken={csrfToken} /> : null}
|
||||
<h2 className="text-2xl font-bold text-center mb-4">
|
||||
{formConfig.title}
|
||||
</h2>
|
||||
|
||||
{formConfig.fields.map((field) => (
|
||||
<div key={field.id} className="flex flex-col mt-4">
|
||||
{field.type === 'paragraph' && <p>{field.text}</p>}
|
||||
|
||||
{(field.type === 'text' ||
|
||||
field.type === 'email' ||
|
||||
field.type === 'date') && (
|
||||
<Controller
|
||||
name={field.id}
|
||||
control={control}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<InputTextIcon
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
IconItem={field.icon ? getIcon(field.icon) : null}
|
||||
type={field.type}
|
||||
name={name}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
errorMsg={
|
||||
errors[field.id]
|
||||
? field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{field.type === 'select' && (
|
||||
<Controller
|
||||
name={field.id}
|
||||
control={control}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<SelectChoice
|
||||
label={field.label}
|
||||
required={field.required}
|
||||
name={name}
|
||||
selected={value || ''}
|
||||
callback={onChange}
|
||||
choices={field.options.map((e) => ({ label: e, value: e }))}
|
||||
placeHolder={`Sélectionner ${field.label.toLowerCase()}`}
|
||||
errorMsg={
|
||||
errors[field.id]
|
||||
? field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{field.type === 'textarea' && (
|
||||
<Controller
|
||||
name={field.id}
|
||||
control={control}
|
||||
rules={{ required: field.required }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<WisiwigTextArea
|
||||
label={field.label}
|
||||
placeholder={field.placeholder}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
required={field.required}
|
||||
errorMsg={
|
||||
errors[field.id]
|
||||
? field.required
|
||||
? `${field.label} est requis`
|
||||
: 'Champ invalide'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="form-group-submit mt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
primary
|
||||
text={formConfig.submitLabel ? formConfig.submitLabel : 'Envoyer'}
|
||||
className="mb-1 px-4 py-2 rounded-md shadow bg-emerald-500 text-white hover:bg-emerald-600 w-full"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
59
Front-End/src/components/Form/InputPhone.js
Normal file
59
Front-End/src/components/Form/InputPhone.js
Normal file
@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { PhoneInput } from 'react-international-phone';
|
||||
import 'react-international-phone/style.css';
|
||||
|
||||
export default function InputPhone({
|
||||
name,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
errorMsg,
|
||||
className,
|
||||
required,
|
||||
enable = true, // Par défaut, le champ est activé
|
||||
}) {
|
||||
const handlePhoneChange = (phone) => {
|
||||
if (enable && onChange) {
|
||||
if (phone && phone.target) {
|
||||
const { name, value } = phone.target;
|
||||
onChange({ target: { name: name, value: value } });
|
||||
} else if (phone) {
|
||||
onChange({ target: { name: name, value: phone } });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
|
||||
<PhoneInput
|
||||
defaultCountry="fr"
|
||||
value={value}
|
||||
onChange={handlePhoneChange}
|
||||
inputProps={{
|
||||
name: name,
|
||||
required: required,
|
||||
disabled: !enable, // Désactiver l'input si enable est false
|
||||
}}
|
||||
className={`!w-full mt-1 !h-[38px] ${
|
||||
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||
}`}
|
||||
containerClassName={`!w-full !h-[36px] !flex !items-center !rounded-md ${
|
||||
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||
}`}
|
||||
inputClassName={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none !rounded-r-md items-center !border !border-gray-200 rounded-md ${
|
||||
errorMsg ? 'border-red-500' : ''
|
||||
} ${!enable ? 'bg-gray-100 cursor-not-allowed' : 'hover:border-gray-400 focus-within:border-gray-500'}`}
|
||||
buttonClassName={`!h-[38px] !flex !items-center !justify-center !rounded-l-md !border border-gray-200 !border-r-0 ${
|
||||
!enable ? 'cursor-not-allowed' : ''
|
||||
}`}
|
||||
/>
|
||||
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
Front-End/src/components/Form/InputText.js
Normal file
51
Front-End/src/components/Form/InputText.js
Normal file
@ -0,0 +1,51 @@
|
||||
export default function InputText({
|
||||
name,
|
||||
type,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
errorMsg,
|
||||
errorLocalMsg,
|
||||
placeholder,
|
||||
className,
|
||||
required,
|
||||
enable = true, // Nouvelle prop pour activer/désactiver le champ
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className={`${className}`}>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<div
|
||||
className={`mt-1 flex items-center border rounded-md ${
|
||||
errorMsg || errorLocalMsg
|
||||
? 'border-red-500 hover:border-red-700'
|
||||
: 'border-gray-200 hover:border-gray-400'
|
||||
} ${!errorMsg && !errorLocalMsg ? 'focus-within:border-gray-500' : ''} ${
|
||||
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type={type}
|
||||
id={name}
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={enable ? onChange : undefined} // Désactiver onChange si enable est false
|
||||
className={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md ${
|
||||
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||
}`}
|
||||
required={required}
|
||||
readOnly={!enable ? 'readOnly' : ''} // Activer le mode readonly si enable est false
|
||||
/>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
59
Front-End/src/components/Form/InputTextIcon.js
Normal file
59
Front-End/src/components/Form/InputTextIcon.js
Normal file
@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function InputTextIcon({
|
||||
name,
|
||||
type,
|
||||
IconItem,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
errorMsg,
|
||||
errorLocalMsg,
|
||||
placeholder,
|
||||
className,
|
||||
required,
|
||||
enable = true, // Nouvelle prop pour activer/désactiver le champ
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className={`${className}`}>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<div
|
||||
className={`mt-1 flex items-center border rounded-md ${
|
||||
errorMsg || errorLocalMsg
|
||||
? 'border-red-500 hover:border-red-700'
|
||||
: 'border-gray-200 hover:border-gray-400'
|
||||
} ${!errorMsg && !errorLocalMsg ? 'focus-within:border-gray-500' : ''} ${
|
||||
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
{IconItem ? (
|
||||
<span className="inline-flex min-h-9 items-center px-3 rounded-l-md bg-gray-50 text-gray-500 text-sm">
|
||||
<IconItem />
|
||||
</span>
|
||||
) : null}
|
||||
<input
|
||||
type={type}
|
||||
id={name}
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={enable ? onChange : undefined}
|
||||
className={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md ${
|
||||
!enable ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||
}`}
|
||||
required={required}
|
||||
readOnly={!enable ? 'readOnly' : ''}
|
||||
/>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
42
Front-End/src/components/Form/InputTextWithColorIcon.js
Normal file
42
Front-End/src/components/Form/InputTextWithColorIcon.js
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Palette } from 'lucide-react';
|
||||
|
||||
const InputTextWithColorIcon = ({
|
||||
name,
|
||||
textValue,
|
||||
colorValue,
|
||||
onTextChange,
|
||||
onColorChange,
|
||||
placeholder,
|
||||
errorMsg,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
name={name}
|
||||
value={textValue}
|
||||
onChange={onTextChange}
|
||||
placeholder={placeholder}
|
||||
className={`flex-1 px-2 py-1 border ${errorMsg ? 'border-red-500' : 'border-gray-300'} rounded-md`}
|
||||
/>
|
||||
<div className="relative flex items-center space-x-2">
|
||||
<input
|
||||
type="color"
|
||||
name={`${name}_color`}
|
||||
value={colorValue}
|
||||
onChange={onColorChange}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
<Palette className="w-6 h-6 text-gray-500" />
|
||||
<div
|
||||
className="w-6 h-6 rounded-full border border-gray-300"
|
||||
style={{ backgroundColor: colorValue }}
|
||||
></div>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputTextWithColorIcon;
|
||||
101
Front-End/src/components/Form/MultiSelect.js
Normal file
101
Front-End/src/components/Form/MultiSelect.js
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
|
||||
const MultiSelect = ({
|
||||
name,
|
||||
label,
|
||||
options,
|
||||
selectedOptions,
|
||||
onChange,
|
||||
errorMsg,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const handleSelect = (option) => {
|
||||
const isSelected = selectedOptions.some(
|
||||
(selected) => selected.id === option.id
|
||||
);
|
||||
let newSelectedOptions;
|
||||
if (isSelected) {
|
||||
newSelectedOptions = selectedOptions.filter(
|
||||
(selected) => selected.id !== option.id
|
||||
);
|
||||
} else {
|
||||
newSelectedOptions = [...selectedOptions, option];
|
||||
}
|
||||
onChange(newSelectedOptions);
|
||||
};
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<div className="relative mt-1">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-pointer focus:outline-none sm:text-sm hover:border-emerald-500 focus:border-emerald-500"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{selectedOptions.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1 justify-center items-center">
|
||||
{selectedOptions.map((option) => (
|
||||
<span
|
||||
key={option.id}
|
||||
className="bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md text-sm"
|
||||
>
|
||||
{option.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span>{label}</span>
|
||||
)}
|
||||
<ChevronDown className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none h-full w-5" />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<ul className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||
{options.map((option) => (
|
||||
<li
|
||||
key={option.id}
|
||||
className={`cursor-pointer select-none relative py-2 pl-3 pr-9 ${
|
||||
selectedOptions.some((selected) => selected.id === option.id)
|
||||
? 'text-white bg-emerald-600'
|
||||
: 'text-gray-900 hover:bg-emerald-100 hover:text-emerald-900'
|
||||
}`}
|
||||
onClick={() => handleSelect(option)}
|
||||
>
|
||||
<span
|
||||
className={`block truncate ${selectedOptions.some((selected) => selected.id === option.id) ? 'font-semibold' : 'font-normal'}`}
|
||||
>
|
||||
{option.name}
|
||||
</span>
|
||||
{selectedOptions.some(
|
||||
(selected) => selected.id === option.id
|
||||
) && (
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-4 text-white">
|
||||
<Check className="h-5 w-5" />
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiSelect;
|
||||
12
Front-End/src/components/Form/PhoneLabel.js
Normal file
12
Front-End/src/components/Form/PhoneLabel.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { formatPhoneNumber } from '@/utils/Telephone';
|
||||
|
||||
export function PhoneLabel({ phoneNumber }) {
|
||||
return (
|
||||
<a
|
||||
className="text-sm font-semibold text-gray-800"
|
||||
href={'tel:' + phoneNumber}
|
||||
>
|
||||
{formatPhoneNumber(phoneNumber)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
57
Front-End/src/components/Form/RadioList.js
Normal file
57
Front-End/src/components/Form/RadioList.js
Normal file
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
|
||||
const RadioList = ({
|
||||
items,
|
||||
formData,
|
||||
handleChange,
|
||||
fieldName,
|
||||
icon: Icon,
|
||||
className,
|
||||
sectionLabel,
|
||||
required,
|
||||
disabled = false,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`mb-4 ${className}`}>
|
||||
{sectionLabel && (
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">
|
||||
{sectionLabel}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</h3>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`flex items-center ${
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
key={`${item.id}-${Math.random()}`}
|
||||
type="radio"
|
||||
id={`${fieldName}-${item.id}`}
|
||||
name={fieldName}
|
||||
value={item.id}
|
||||
checked={parseInt(formData[fieldName], 10) === item.id}
|
||||
onChange={handleChange}
|
||||
className="form-radio h-4 w-4 text-emerald-600 focus:ring-emerald-500 hover:ring-emerald-400 checked:bg-emerald-600 cursor-pointer"
|
||||
style={{ outline: 'none', boxShadow: 'none' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${fieldName}-${item.id}`}
|
||||
className={`ml-2 block text-sm text-gray-900 flex items-center ${
|
||||
disabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadioList;
|
||||
69
Front-End/src/components/Form/SelectChoice.js
Normal file
69
Front-End/src/components/Form/SelectChoice.js
Normal file
@ -0,0 +1,69 @@
|
||||
export default function SelectChoice({
|
||||
type,
|
||||
name,
|
||||
label,
|
||||
required,
|
||||
placeHolder,
|
||||
choices,
|
||||
callback,
|
||||
selected,
|
||||
errorMsg,
|
||||
errorLocalMsg,
|
||||
IconItem,
|
||||
disabled = false,
|
||||
}) {
|
||||
const isPlaceholderSelected = selected === ''; // Vérifie si le placeholder est sélectionné
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<div
|
||||
className={`mt-1 flex items-center border rounded-md ${
|
||||
errorMsg || errorLocalMsg
|
||||
? 'border-red-500 hover:border-red-700'
|
||||
: 'border-gray-200 hover:border-gray-400'
|
||||
} ${disabled ? '' : 'focus-within:border-gray-500'}`}
|
||||
>
|
||||
{IconItem && (
|
||||
<span className="inline-flex items-center px-3 text-gray-500 text-sm">
|
||||
{<IconItem />}
|
||||
</span>
|
||||
)}
|
||||
<select
|
||||
className={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none rounded-md ${
|
||||
disabled ? 'bg-gray-100' : ''
|
||||
} ${isPlaceholderSelected ? 'italic text-gray-500' : 'not-italic text-gray-800'}`} // Applique le style classique si une option autre que le placeholder est sélectionnée
|
||||
type={type}
|
||||
id={name}
|
||||
name={name}
|
||||
value={selected}
|
||||
onChange={callback}
|
||||
disabled={disabled}
|
||||
>
|
||||
{/* Placeholder en italique */}
|
||||
<option value="" className="italic text-gray-500">
|
||||
{placeHolder?.toLowerCase()}
|
||||
</option>
|
||||
{/* Autres options sans italique */}
|
||||
{choices.map(({ value, label }) => (
|
||||
<option
|
||||
key={value}
|
||||
value={value}
|
||||
className="not-italic text-gray-800"
|
||||
>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
35
Front-End/src/components/Form/ToggleSwitch.js
Normal file
35
Front-End/src/components/Form/ToggleSwitch.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
const ToggleSwitch = ({ name, label, checked, onChange }) => {
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const handleChange = (e) => {
|
||||
onChange(e);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.blur(); // Remove focus
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center mt-4">
|
||||
<label className="mr-2 text-gray-600">{label}</label>
|
||||
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={name}
|
||||
id={name}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
className="hover:text-emerald-500 absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer border-emerald-500 checked:right-0 checked:border-emerald-500 checked:bg-emerald-500 hover:border-emerald-500 hover:bg-emerald-500 focus:outline-none focus:ring-0"
|
||||
ref={inputRef} // Reference to the input element
|
||||
/>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className={`toggle-label block overflow-hidden h-6 rounded-full cursor-pointer transition-colors duration-200 ${checked ? 'bg-emerald-300' : 'bg-gray-300'}`}
|
||||
></label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleSwitch;
|
||||
61
Front-End/src/components/Form/WisiwigTextArea.js
Normal file
61
Front-End/src/components/Form/WisiwigTextArea.js
Normal file
@ -0,0 +1,61 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import 'react-quill/dist/quill.snow.css';
|
||||
|
||||
const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
|
||||
|
||||
export default function WisiwigTextArea({
|
||||
label = 'Zone de Texte',
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Ecrivez votre texte ici...',
|
||||
className = 'h-64',
|
||||
required = false,
|
||||
errorMsg,
|
||||
errorLocalMsg,
|
||||
enable = true,
|
||||
}) {
|
||||
return (
|
||||
<div className={`mb-4 ${className}`}>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<div
|
||||
className={`
|
||||
mt-1 border rounded-md
|
||||
${
|
||||
errorMsg || errorLocalMsg
|
||||
? 'border-red-500 hover:border-red-700'
|
||||
: 'border-gray-200 hover:border-gray-400'
|
||||
}
|
||||
${!errorMsg && !errorLocalMsg ? 'focus-within:border-gray-500' : ''}
|
||||
${!enable ? 'bg-gray-100 cursor-not-allowed' : ''}
|
||||
`}
|
||||
>
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={value}
|
||||
onChange={enable ? onChange : undefined}
|
||||
placeholder={placeholder}
|
||||
readOnly={!enable}
|
||||
className={`bg-white rounded-md border-0 shadow-none !border-0 !outline-none ${!enable ? 'bg-gray-100 cursor-not-allowed' : ''}`}
|
||||
style={{ minHeight: 250, border: 'none', boxShadow: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
|
||||
{errorLocalMsg && (
|
||||
<p className="mt-2 text-sm text-red-600">{errorLocalMsg}</p>
|
||||
)}
|
||||
<style jsx global>{`
|
||||
.ql-toolbar.ql-snow {
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #e5e7eb !important; /* gray-200 */
|
||||
border-radius: 0.375rem 0.375rem 0 0 !important; /* rounded-t-md */
|
||||
}
|
||||
.ql-container.ql-snow {
|
||||
border: none !important;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user