feat: creation d'un FormRenderer.js pour creer un formulaire dynamique [NEWTS-17]

This commit is contained in:
Luc SORIGNET
2025-08-31 12:26:04 +02:00
parent 482e8c1357
commit 9481a0132d
47 changed files with 324 additions and 130 deletions

View 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;

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
</>
);
}

View 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>
</>
);
}

View 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;

View 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;

View 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>
);
}

View 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;

View 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>
</>
);
}

View 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;

View 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>
);
}