Files
igny8/frontend/src/components/common/FormModal.tsx
2025-11-09 10:27:02 +00:00

225 lines
9.6 KiB
TypeScript

import { ReactNode } from 'react';
import { Modal } from '../ui/modal';
import Button from '../ui/button/Button';
import SelectDropdown from '../form/SelectDropdown';
import Label from '../form/Label';
export interface FormField {
key: string;
label: string;
type: 'text' | 'number' | 'email' | 'password' | 'select' | 'textarea';
placeholder?: string;
options?: Array<{ value: string; label: string }>;
value: any;
onChange: (value: any) => void;
required?: boolean;
min?: number;
max?: number;
rows?: number;
className?: string;
}
interface FormModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: () => void;
title: string;
fields?: FormField[];
submitLabel?: string;
cancelLabel?: string;
isLoading?: boolean;
className?: string;
customFooter?: ReactNode;
customBody?: ReactNode; // Custom body content that replaces fields
}
export default function FormModal({
isOpen,
onClose,
onSubmit,
title,
fields = [],
submitLabel = 'Create',
cancelLabel = 'Cancel',
isLoading = false,
className = 'max-w-2xl',
customFooter,
customBody,
}: FormModalProps) {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
className={className}
>
<div className="p-6">
<h3 className="text-lg font-semibold mb-6 text-gray-800 dark:text-white">
{title}
</h3>
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
className="space-y-4"
>
{customBody ? (
customBody
) : (
<>
{fields.find(f => f.key === 'keyword') && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{fields.find(f => f.key === 'keyword')!.label}
{fields.find(f => f.key === 'keyword')!.required && <span className="text-error-500 ml-1">*</span>}
</label>
<input
type="text"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={fields.find(f => f.key === 'keyword')!.value || ''}
onChange={(e) => fields.find(f => f.key === 'keyword')!.onChange(e.target.value)}
placeholder={fields.find(f => f.key === 'keyword')!.placeholder}
required={fields.find(f => f.key === 'keyword')!.required}
/>
</div>
)}
{(fields.find(f => f.key === 'volume') || fields.find(f => f.key === 'difficulty')) && (
<div className="grid grid-cols-2 gap-4">
{fields.find(f => f.key === 'volume') && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{fields.find(f => f.key === 'volume')!.label}
{fields.find(f => f.key === 'volume')!.required && <span className="text-error-500 ml-1">*</span>}
</label>
<input
type="number"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={fields.find(f => f.key === 'volume')!.value || ''}
onChange={(e) => {
const value = e.target.value === '' ? '' : parseInt(e.target.value) || 0;
fields.find(f => f.key === 'volume')!.onChange(value);
}}
placeholder={fields.find(f => f.key === 'volume')!.placeholder}
required={fields.find(f => f.key === 'volume')!.required}
/>
</div>
)}
{fields.find(f => f.key === 'difficulty') && (() => {
const difficultyField = fields.find(f => f.key === 'difficulty')!;
return (
<div>
<Label className="mb-2">
{difficultyField.label}
{difficultyField.required && <span className="text-error-500 ml-1">*</span>}
</Label>
{difficultyField.type === 'select' ? (
<SelectDropdown
options={difficultyField.options || []}
placeholder={difficultyField.placeholder || difficultyField.label}
value={difficultyField.value || ''}
onChange={(value) => difficultyField.onChange(value)}
className="w-full"
/>
) : (
<input
type="number"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={difficultyField.value || ''}
onChange={(e) => {
const value = e.target.value === '' ? '' : parseInt(e.target.value) || 0;
difficultyField.onChange(value);
}}
placeholder={difficultyField.placeholder}
required={difficultyField.required}
min={difficultyField.min}
max={difficultyField.max}
/>
)}
</div>
);
})()}
</div>
)}
{fields.filter(f => f.key !== 'keyword' && f.key !== 'volume' && f.key !== 'difficulty').map((field) => {
if (field.type === 'select') {
return (
<div key={field.key}>
<Label className="mb-2">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}
</Label>
<SelectDropdown
options={field.options || []}
placeholder={field.placeholder || field.label}
value={field.value || ''}
onChange={(value) => field.onChange(value)}
className="w-full"
/>
</div>
);
}
if (field.type === 'textarea') {
return (
<div key={field.key}>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}
</label>
<textarea
rows={field.rows || 4}
className="w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={field.value || ''}
onChange={(e) => field.onChange(e.target.value)}
placeholder={field.placeholder}
required={field.required}
/>
</div>
);
}
return (
<div key={field.key}>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}
</label>
<input
type={field.type}
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={field.value || ''}
onChange={(e) => field.onChange(e.target.value)}
placeholder={field.placeholder}
required={field.required}
min={field.min}
max={field.max}
/>
</div>
);
})}
</>
)}
{customFooter || (
<div className="flex justify-end gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={onClose}
disabled={isLoading}
>
{cancelLabel}
</Button>
<Button
type="submit"
variant="primary"
disabled={isLoading}
>
{isLoading ? 'Processing...' : submitLabel}
</Button>
</div>
)}
</form>
</div>
</Modal>
);
}