mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-06-04 21:36:12 +00:00
feat: Ajout FormTemplateBuilder [N3WTS-17]
This commit is contained in:
354
Front-End/src/components/Form/AddFieldModal.js
Normal file
354
Front-End/src/components/Form/AddFieldModal.js
Normal file
@ -0,0 +1,354 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import InputTextIcon from './InputTextIcon';
|
||||
import SelectChoice from './SelectChoice';
|
||||
import Button from './Button';
|
||||
import IconSelector from './IconSelector';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { FIELD_TYPES } from './FormTypes';
|
||||
|
||||
export default function AddFieldModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
editingField = null,
|
||||
editingIndex = -1,
|
||||
}) {
|
||||
const isEditing = editingIndex >= 0;
|
||||
|
||||
const [currentField, setCurrentField] = useState(
|
||||
editingField || {
|
||||
id: '',
|
||||
label: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
icon: '',
|
||||
options: [],
|
||||
text: '',
|
||||
placeholder: '',
|
||||
}
|
||||
);
|
||||
|
||||
const [showIconPicker, setShowIconPicker] = useState(false);
|
||||
const [newOption, setNewOption] = useState('');
|
||||
|
||||
const { control, handleSubmit, reset } = useForm();
|
||||
|
||||
// Ajouter une option au select
|
||||
const addOption = () => {
|
||||
if (newOption.trim()) {
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
options: [...currentField.options, newOption.trim()],
|
||||
});
|
||||
setNewOption('');
|
||||
}
|
||||
};
|
||||
|
||||
// Supprimer une option du select
|
||||
const removeOption = (index) => {
|
||||
const newOptions = currentField.options.filter((_, i) => i !== index);
|
||||
setCurrentField({ ...currentField, options: newOptions });
|
||||
};
|
||||
|
||||
// Sélectionner une icône
|
||||
const selectIcon = (iconName) => {
|
||||
setCurrentField({ ...currentField, icon: iconName });
|
||||
|
||||
// Mettre à jour la valeur dans le formulaire
|
||||
const iconField = control._fields.icon;
|
||||
if (iconField && iconField.onChange) {
|
||||
iconField.onChange(iconName);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFieldSubmit = (data) => {
|
||||
onSubmit(data, currentField, editingIndex);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-xl font-semibold">
|
||||
{isEditing ? 'Modifier le champ' : 'Ajouter un champ'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700 text-xl"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(handleFieldSubmit)} className="space-y-4">
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
defaultValue={currentField.type}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<SelectChoice
|
||||
label="Type de champ"
|
||||
name="type"
|
||||
selected={value}
|
||||
callback={(e) => {
|
||||
onChange(e.target.value);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
type: e.target.value,
|
||||
});
|
||||
}}
|
||||
choices={FIELD_TYPES}
|
||||
placeHolder="Sélectionner un type"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{![
|
||||
'paragraph',
|
||||
'heading1',
|
||||
'heading2',
|
||||
'heading3',
|
||||
'heading4',
|
||||
'heading5',
|
||||
'heading6',
|
||||
].includes(currentField.type) && (
|
||||
<>
|
||||
<Controller
|
||||
name="label"
|
||||
control={control}
|
||||
defaultValue={currentField.label}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<InputTextIcon
|
||||
label="Label du champ"
|
||||
name="label"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
label: e.target.value,
|
||||
});
|
||||
}}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="placeholder"
|
||||
control={control}
|
||||
defaultValue={currentField.placeholder}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<InputTextIcon
|
||||
label="Placeholder (optionnel)"
|
||||
name="placeholder"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
placeholder: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name="required"
|
||||
control={control}
|
||||
defaultValue={currentField.required}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
id="required"
|
||||
checked={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.checked);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
required: e.target.checked,
|
||||
});
|
||||
}}
|
||||
className="mr-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label htmlFor="required">Champ obligatoire</label>
|
||||
</div>
|
||||
|
||||
{(currentField.type === 'text' ||
|
||||
currentField.type === 'email' ||
|
||||
currentField.type === 'date') && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Icône (optionnel)
|
||||
</label>
|
||||
<Controller
|
||||
name="icon"
|
||||
control={control}
|
||||
defaultValue={currentField.icon}
|
||||
render={({ field: { onChange } }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 flex items-center gap-2 p-3 border border-gray-300 rounded-md bg-gray-50">
|
||||
{currentField.icon &&
|
||||
LucideIcons[currentField.icon] ? (
|
||||
<>
|
||||
{React.createElement(
|
||||
LucideIcons[currentField.icon],
|
||||
{
|
||||
size: 20,
|
||||
className: 'text-gray-600',
|
||||
}
|
||||
)}
|
||||
<span className="text-sm text-gray-700">
|
||||
{currentField.icon}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-sm text-gray-500">
|
||||
Aucune icône sélectionnée
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
text="Choisir"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowIconPicker(true);
|
||||
}}
|
||||
className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
||||
/>
|
||||
{currentField.icon && (
|
||||
<Button
|
||||
type="button"
|
||||
text="✕"
|
||||
onClick={() => {
|
||||
onChange('');
|
||||
setCurrentField({ ...currentField, icon: '' });
|
||||
}}
|
||||
className="px-2 py-2 bg-red-500 text-white rounded-md hover:bg-red-600"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{[
|
||||
'paragraph',
|
||||
'heading1',
|
||||
'heading2',
|
||||
'heading3',
|
||||
'heading4',
|
||||
'heading5',
|
||||
'heading6',
|
||||
].includes(currentField.type) && (
|
||||
<Controller
|
||||
name="text"
|
||||
control={control}
|
||||
defaultValue={currentField.text}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{currentField.type === 'paragraph'
|
||||
? 'Texte du paragraphe'
|
||||
: 'Texte du titre'}
|
||||
</label>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
setCurrentField({
|
||||
...currentField,
|
||||
text: e.target.value,
|
||||
});
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={3}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentField.type === 'select' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Options de la liste
|
||||
</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newOption}
|
||||
onChange={(e) => setNewOption(e.target.value)}
|
||||
placeholder="Nouvelle option"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onKeyPress={(e) =>
|
||||
e.key === 'Enter' && (e.preventDefault(), addOption())
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
text="Ajouter"
|
||||
onClick={addOption}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{currentField.options.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between bg-gray-50 p-2 rounded"
|
||||
>
|
||||
<span>{option}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeOption(index)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 mt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
text={isEditing ? 'Modifier' : 'Ajouter'}
|
||||
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
text="Annuler"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Sélecteur d'icônes - déplacé en dehors du formulaire */}
|
||||
<IconSelector
|
||||
isOpen={showIconPicker}
|
||||
onClose={() => setShowIconPicker(false)}
|
||||
onSelect={selectIcon}
|
||||
selectedIcon={currentField.icon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user