168 lines
4.7 KiB
TypeScript
168 lines
4.7 KiB
TypeScript
/**
|
|
* FormFieldRenderer - Dynamic form field renderer
|
|
* Renders form fields based on configuration objects
|
|
*
|
|
* Usage:
|
|
* ```typescript
|
|
* <FormFieldRenderer
|
|
* fields={formFieldsConfig}
|
|
* values={formValues}
|
|
* onChange={handleFieldChange}
|
|
* />
|
|
* ```
|
|
*/
|
|
|
|
import React from 'react';
|
|
import Input from './input/InputField';
|
|
import SelectDropdown from './SelectDropdown';
|
|
import Label from './Label';
|
|
|
|
export interface FormFieldConfig {
|
|
key: string;
|
|
label: string;
|
|
type: 'text' | 'number' | 'email' | 'password' | 'select' | 'textarea';
|
|
placeholder?: string;
|
|
options?: Array<{ value: string; label: string }>;
|
|
required?: boolean;
|
|
min?: number;
|
|
max?: number;
|
|
rows?: number;
|
|
className?: string;
|
|
gridCols?: 1 | 2; // For inline fields (e.g., Volume & Difficulty)
|
|
}
|
|
|
|
interface FormFieldRendererProps {
|
|
fields: FormFieldConfig[];
|
|
values: Record<string, any>;
|
|
onChange: (key: string, value: any) => void;
|
|
errors?: Record<string, string>;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export default function FormFieldRenderer({
|
|
fields,
|
|
values,
|
|
onChange,
|
|
errors = {},
|
|
disabled = false,
|
|
}: FormFieldRendererProps) {
|
|
// Group fields by grid layout
|
|
const fieldGroups: FormFieldConfig[][] = [];
|
|
let currentGroup: FormFieldConfig[] = [];
|
|
|
|
fields.forEach((field) => {
|
|
if (field.gridCols === 2) {
|
|
// Start new group for inline fields
|
|
if (currentGroup.length > 0) {
|
|
fieldGroups.push(currentGroup);
|
|
currentGroup = [];
|
|
}
|
|
currentGroup.push(field);
|
|
if (currentGroup.length === 2) {
|
|
fieldGroups.push(currentGroup);
|
|
currentGroup = [];
|
|
}
|
|
} else {
|
|
// Full-width field
|
|
if (currentGroup.length > 0) {
|
|
fieldGroups.push(currentGroup);
|
|
currentGroup = [];
|
|
}
|
|
fieldGroups.push([field]);
|
|
}
|
|
});
|
|
|
|
if (currentGroup.length > 0) {
|
|
fieldGroups.push(currentGroup);
|
|
}
|
|
|
|
const renderField = (field: FormFieldConfig) => {
|
|
const value = values[field.key] || '';
|
|
const error = errors[field.key];
|
|
const fieldId = `field-${field.key}`;
|
|
|
|
return (
|
|
<div key={field.key} className={field.className}>
|
|
<Label htmlFor={fieldId} className="mb-2">
|
|
{field.label}
|
|
{field.required && <span className="text-error-500 ml-1">*</span>}
|
|
</Label>
|
|
|
|
{field.type === 'select' ? (
|
|
<SelectDropdown
|
|
options={field.options || []}
|
|
placeholder={field.placeholder || field.label}
|
|
value={value}
|
|
onChange={(val) => onChange(field.key, val)}
|
|
className="w-full"
|
|
/>
|
|
) : field.type === 'textarea' ? (
|
|
<textarea
|
|
id={fieldId}
|
|
className={`w-full rounded-lg border ${
|
|
error
|
|
? 'border-error-500'
|
|
: 'border-gray-300 dark:border-gray-700'
|
|
} 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:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800`}
|
|
value={value}
|
|
onChange={(e) => onChange(field.key, e.target.value)}
|
|
placeholder={field.placeholder}
|
|
required={field.required}
|
|
rows={field.rows || 4}
|
|
disabled={disabled}
|
|
/>
|
|
) : (
|
|
<Input
|
|
id={fieldId}
|
|
type={field.type}
|
|
value={value}
|
|
onChange={(e) => {
|
|
const newValue =
|
|
field.type === 'number'
|
|
? e.target.value === ''
|
|
? ''
|
|
: parseInt(e.target.value) || 0
|
|
: e.target.value;
|
|
onChange(field.key, newValue);
|
|
}}
|
|
placeholder={field.placeholder}
|
|
required={field.required}
|
|
min={field.min}
|
|
max={field.max}
|
|
error={!!error}
|
|
disabled={disabled}
|
|
className="w-full"
|
|
/>
|
|
)}
|
|
|
|
{error && (
|
|
<p className="mt-1 text-sm text-error-500">{error}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{fieldGroups.map((group, groupIndex) => {
|
|
if (group.length === 2 && group[0].gridCols === 2) {
|
|
// Render inline fields
|
|
return (
|
|
<div key={`group-${groupIndex}`} className="grid grid-cols-2 gap-4">
|
|
{group.map((field) => renderField(field))}
|
|
</div>
|
|
);
|
|
} else {
|
|
// Render full-width field
|
|
return (
|
|
<div key={`group-${groupIndex}`}>
|
|
{group.map((field) => renderField(field))}
|
|
</div>
|
|
);
|
|
}
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|