274 lines
7.0 KiB
TypeScript
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>
|
|
);
|
|
}
|