238 lines
10 KiB
TypeScript
238 lines
10 KiB
TypeScript
"use client";
|
||
|
||
import { Button } from "@/components/ui/button";
|
||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||
import { Plus } from "lucide-react";
|
||
import type { FormScreenDefinition, FormFieldDefinition, FormValidationMessages } from "@/lib/funnel/types";
|
||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||
|
||
interface FormScreenConfigProps {
|
||
screen: BuilderScreen & { template: "form" };
|
||
onUpdate: (updates: Partial<FormScreenDefinition>) => void;
|
||
}
|
||
|
||
export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||
const formScreen = screen as FormScreenDefinition & { position: { x: number; y: number } };
|
||
|
||
const updateField = (index: number, updates: Partial<FormFieldDefinition>) => {
|
||
const newFields = [...(formScreen.fields || [])];
|
||
newFields[index] = { ...newFields[index], ...updates };
|
||
onUpdate({ fields: newFields });
|
||
};
|
||
|
||
const updateValidationMessages = (updates: Partial<FormValidationMessages>) => {
|
||
onUpdate({
|
||
validationMessages: {
|
||
...(formScreen.validationMessages ?? {}),
|
||
...updates,
|
||
},
|
||
});
|
||
};
|
||
|
||
const addField = () => {
|
||
const newField: FormFieldDefinition = {
|
||
id: `field_${Date.now()}`,
|
||
label: "Новое поле",
|
||
placeholder: "Введите значение",
|
||
type: "text",
|
||
required: true,
|
||
};
|
||
|
||
onUpdate({
|
||
fields: [...(formScreen.fields || []), newField],
|
||
});
|
||
};
|
||
|
||
const removeField = (index: number) => {
|
||
const newFields = formScreen.fields?.filter((_, i) => i !== index) || [];
|
||
onUpdate({ fields: newFields });
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">Поля формы</h3>
|
||
<Button onClick={addField} variant="outline" className="h-8 w-8 p-0 flex items-center justify-center">
|
||
<Plus className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
|
||
{formScreen.fields?.map((field, index) => (
|
||
<div key={field.id} className="space-y-2 rounded-lg border border-border/50 bg-muted/5 p-3">
|
||
<div className="flex items-center justify-between gap-2">
|
||
<span className="text-xs font-semibold uppercase text-muted-foreground">
|
||
Поле {index + 1}
|
||
</span>
|
||
<Button
|
||
variant="ghost"
|
||
className="h-8 px-3 text-xs text-destructive"
|
||
onClick={() => removeField(index)}
|
||
>
|
||
Удалить
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||
ID поля
|
||
<TextInput value={field.id} onChange={(event) => updateField(index, { id: event.target.value })} />
|
||
</label>
|
||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||
Тип
|
||
<select
|
||
className="rounded-lg border border-border bg-background px-2 py-1"
|
||
value={field.type ?? "text"}
|
||
onChange={(event) => updateField(index, { type: event.target.value as FormFieldDefinition["type"] })}
|
||
>
|
||
<option value="text">Текст</option>
|
||
<option value="email">E-mail</option>
|
||
<option value="tel">Телефон</option>
|
||
<option value="url">Ссылка</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||
Метка поля
|
||
<TextInput
|
||
value={field.label ?? ""}
|
||
onChange={(event) => updateField(index, { label: event.target.value })}
|
||
/>
|
||
</label>
|
||
|
||
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||
Placeholder
|
||
<TextInput
|
||
value={field.placeholder ?? ""}
|
||
onChange={(event) => updateField(index, { placeholder: event.target.value })}
|
||
/>
|
||
</label>
|
||
|
||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
||
<input
|
||
type="checkbox"
|
||
checked={field.required ?? false}
|
||
onChange={(event) => updateField(index, { required: event.target.checked })}
|
||
/>
|
||
Обязательно для заполнения
|
||
</label>
|
||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||
Максимальная длина
|
||
<input
|
||
type="number"
|
||
min={1}
|
||
className="rounded-lg border border-border bg-background px-2 py-1"
|
||
value={field.maxLength ?? ""}
|
||
onChange={(event) =>
|
||
updateField(index, {
|
||
maxLength: event.target.value ? Number(event.target.value) : undefined,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||
Регулярное выражение (pattern)
|
||
<TextInput
|
||
placeholder="Например, ^\\d+$"
|
||
value={field.validation?.pattern ?? ""}
|
||
onChange={(event) =>
|
||
updateField(index, {
|
||
validation: {
|
||
...(field.validation ?? {}),
|
||
pattern: event.target.value || undefined,
|
||
message: field.validation?.message,
|
||
},
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||
Текст ошибки для pattern
|
||
<TextInput
|
||
placeholder="Неверный формат"
|
||
value={field.validation?.message ?? ""}
|
||
onChange={(event) =>
|
||
updateField(index, {
|
||
validation:
|
||
field.validation || event.target.value
|
||
? {
|
||
...(field.validation ?? {}),
|
||
message: event.target.value || undefined,
|
||
}
|
||
: undefined,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
{(!formScreen.fields || formScreen.fields.length === 0) && (
|
||
<div className="rounded-lg border border-dashed border-border/60 p-4 text-center text-sm text-muted-foreground">
|
||
Пока нет полей. Добавьте хотя бы одно, чтобы форма работала.
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<h4 className="text-sm font-semibold text-foreground">Сообщения валидации</h4>
|
||
<div className="space-y-4 text-xs">
|
||
<div className="rounded-lg border border-border/60 bg-muted/20 p-3">
|
||
<label className="flex flex-col gap-2 text-muted-foreground">
|
||
<div>
|
||
<span className="font-medium">Обязательное поле</span>
|
||
<p className="text-xs mt-1 text-muted-foreground/80">
|
||
Доступна переменная: <code className="bg-muted px-1 rounded">{`{field}`}</code> - название поля
|
||
</p>
|
||
</div>
|
||
<TextInput
|
||
placeholder="Пример: {field} обязательно для заполнения"
|
||
value={formScreen.validationMessages?.required ?? ""}
|
||
onChange={(event) => updateValidationMessages({ required: event.target.value || undefined })}
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-border/60 bg-muted/20 p-3">
|
||
<label className="flex flex-col gap-2 text-muted-foreground">
|
||
<div>
|
||
<span className="font-medium">Превышена длина</span>
|
||
<p className="text-xs mt-1 text-muted-foreground/80">
|
||
Доступны переменные: <code className="bg-muted px-1 rounded">{`{field}`}</code>, <code className="bg-muted px-1 rounded">{`{maxLength}`}</code>
|
||
</p>
|
||
</div>
|
||
<TextInput
|
||
placeholder="Пример: {field} не может быть длиннее {maxLength} символов"
|
||
value={formScreen.validationMessages?.maxLength ?? ""}
|
||
onChange={(event) => updateValidationMessages({ maxLength: event.target.value || undefined })}
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-border/60 bg-muted/20 p-3">
|
||
<label className="flex flex-col gap-2 text-muted-foreground">
|
||
<div>
|
||
<span className="font-medium">Неверный формат</span>
|
||
<p className="text-xs mt-1 text-muted-foreground/80">
|
||
Доступна переменная: <code className="bg-muted px-1 rounded">{`{field}`}</code> - название поля
|
||
</p>
|
||
</div>
|
||
<TextInput
|
||
placeholder="Пример: Проверьте формат {field}"
|
||
value={formScreen.validationMessages?.invalidFormat ?? ""}
|
||
onChange={(event) => updateValidationMessages({ invalidFormat: event.target.value || undefined })}
|
||
/>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|