Files
myeasycms-v2/packages/features/module-builder/src/components/field-renderer.tsx
2026-03-29 19:44:57 +02:00

274 lines
7.0 KiB
TypeScript

'use client';
import type { CmsFieldType } from '../schema/module.schema';
import { Input } from '@kit/ui/input';
import { Textarea } from '@kit/ui/textarea';
import { Checkbox } from '@kit/ui/checkbox';
import { Label } from '@kit/ui/label';
interface FieldRendererProps {
name: string;
displayName: string;
fieldType: CmsFieldType;
value: unknown;
onChange: (value: unknown) => void;
placeholder?: string;
helpText?: string;
required?: boolean;
readonly?: boolean;
error?: string;
selectOptions?: Array<{ label: string; value: string }>;
}
/**
* Maps cms_field_type to the appropriate Shadcn UI component.
* Replaces legacy PHP field rendering from my_modulklasse.
*/
export function FieldRenderer({
name,
displayName,
fieldType,
value,
onChange,
placeholder,
helpText,
required,
readonly,
error,
selectOptions,
}: FieldRendererProps) {
const fieldValue = value != null ? String(value) : '';
const renderField = () => {
switch (fieldType) {
case 'text':
case 'phone':
case 'url':
case 'color':
return (
<Input
name={name}
type={fieldType === 'color' ? 'color' : fieldType === 'url' ? 'url' : fieldType === 'phone' ? 'tel' : 'text'}
value={fieldValue}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
readOnly={readonly}
required={required}
/>
);
case 'email':
return (
<Input
name={name}
type="email"
value={fieldValue}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder ?? 'email@example.de'}
readOnly={readonly}
required={required}
/>
);
case 'password':
return (
<Input
name={name}
type="password"
value={fieldValue}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
readOnly={readonly}
required={required}
/>
);
case 'textarea':
return (
<Textarea
name={name}
value={fieldValue}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
readOnly={readonly}
required={required}
rows={4}
/>
);
case 'richtext':
// Phase 3 enhancement: TipTap editor
return (
<Textarea
name={name}
value={fieldValue}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder ?? 'Formatierter Text...'}
readOnly={readonly}
rows={6}
/>
);
case 'integer':
return (
<Input
name={name}
type="number"
step="1"
value={fieldValue}
onChange={(e) => onChange(parseInt(e.target.value, 10) || '')}
placeholder={placeholder}
readOnly={readonly}
required={required}
/>
);
case 'decimal':
case 'currency':
return (
<Input
name={name}
type="number"
step="0.01"
value={fieldValue}
onChange={(e) => onChange(parseFloat(e.target.value) || '')}
placeholder={placeholder ?? (fieldType === 'currency' ? '0,00 €' : undefined)}
readOnly={readonly}
required={required}
/>
);
case 'date':
return (
<Input
name={name}
type="date"
value={fieldValue}
onChange={(e) => onChange(e.target.value)}
readOnly={readonly}
required={required}
/>
);
case 'time':
return (
<Input
name={name}
type="time"
value={fieldValue}
onChange={(e) => onChange(e.target.value)}
readOnly={readonly}
required={required}
/>
);
case 'checkbox':
return (
<div className="flex items-center space-x-2">
<Checkbox
id={name}
checked={Boolean(value)}
onCheckedChange={(checked) => onChange(checked)}
disabled={readonly}
/>
<Label htmlFor={name}>{displayName}</Label>
</div>
);
case 'select':
case 'radio':
return (
<select
name={name}
value={fieldValue}
onChange={(e) => onChange(e.target.value)}
disabled={readonly}
required={required}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background"
>
<option value="">{placeholder ?? 'Bitte wählen...'}</option>
{selectOptions?.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
case 'iban':
return (
<Input
name={name}
type="text"
value={fieldValue}
onChange={(e) => onChange(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''))}
placeholder={placeholder ?? 'DE89 3704 0044 0532 0130 00'}
readOnly={readonly}
required={required}
maxLength={34}
/>
);
case 'file':
return (
<Input
name={name}
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) onChange(file);
}}
disabled={readonly}
required={required}
/>
);
case 'hidden':
return <input type="hidden" name={name} value={fieldValue} />;
case 'computed':
return (
<div className="rounded-md border bg-muted px-3 py-2 text-sm">
{fieldValue || '—'}
</div>
);
default:
return (
<Input
name={name}
type="text"
value={fieldValue}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
readOnly={readonly}
/>
);
}
};
// Checkbox renders its own label
if (fieldType === 'checkbox') {
return (
<div className="space-y-1">
{renderField()}
{helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
{error && <p className="text-xs text-destructive">{error}</p>}
</div>
);
}
return (
<div className="space-y-1.5">
<Label htmlFor={name}>
{displayName}
{required && <span className="text-destructive ml-1">*</span>}
</Label>
{renderField()}
{helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
{error && <p className="text-xs text-destructive">{error}</p>}
</div>
);
}