-
Coupon Details
-
-
-
+
+ Настройки оффера
+
+
-
-
-
- onUpdate({
- coupon: {
- ...couponScreen.coupon,
- offer: {
- ...couponScreen.coupon?.offer,
- description: {
- ...couponScreen.coupon?.offer?.description,
- text: e.target.value,
- font: couponScreen.coupon?.offer?.description?.font || "inter",
- weight: couponScreen.coupon?.offer?.description?.weight || "medium",
- }
- }
- }
- })}
- />
-
-
-
-
- onUpdate({
- coupon: {
- ...couponScreen.coupon,
- promoCode: {
- ...couponScreen.coupon?.promoCode,
- text: e.target.value,
- font: couponScreen.coupon?.promoCode?.font || "manrope",
- weight: couponScreen.coupon?.promoCode?.weight || "bold",
- }
- }
- })}
- />
-
-
-
-
- onUpdate({
- coupon: {
- ...couponScreen.coupon,
- footer: {
- ...couponScreen.coupon?.footer,
- text: e.target.value,
- font: couponScreen.coupon?.footer?.font || "inter",
- weight: couponScreen.coupon?.footer?.weight || "medium",
- }
- }
- })}
- />
-
-
-
- {/* Bottom Action Button */}
-
-
- onUpdate({
- bottomActionButton: {
- text: e.target.value || "Continue",
+ placeholder="-50% на первый заказ"
+ value={couponScreen.coupon?.offer?.title?.text ?? ""}
+ onChange={(event) =>
+ handleCouponUpdate("offer", {
+ ...couponScreen.coupon.offer,
+ title: {
+ ...(couponScreen.coupon.offer?.title ?? {}),
+ text: event.target.value,
+ },
+ })
}
- })}
- />
+ />
+
+
- {/* Header Configuration */}
-
-
Header Settings
-
-
);
diff --git a/src/components/admin/builder/templates/DateScreenConfig.tsx b/src/components/admin/builder/templates/DateScreenConfig.tsx
index 0e39557..b02a772 100644
--- a/src/components/admin/builder/templates/DateScreenConfig.tsx
+++ b/src/components/admin/builder/templates/DateScreenConfig.tsx
@@ -11,176 +11,140 @@ interface DateScreenConfigProps {
export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
const dateScreen = screen as DateScreenDefinition & { position: { x: number; y: number } };
-
+
+ const handleDateInputChange =
(field: T, value: string | boolean) => {
+ onUpdate({
+ dateInput: {
+ ...dateScreen.dateInput,
+ [field]: value,
+ },
+ });
+ };
+
+ const handleInfoMessageChange = (field: "text" | "icon", value: string) => {
+ const baseInfo = dateScreen.infoMessage ?? { text: "", icon: "ℹ️" };
+ const nextInfo = { ...baseInfo, [field]: value };
+
+ if (!nextInfo.text) {
+ onUpdate({ infoMessage: undefined });
+ return;
+ }
+
+ onUpdate({ infoMessage: nextInfo });
+ };
+
return (
-
- {/* Title Configuration */}
-
- Title
- onUpdate({
- title: {
- ...dateScreen.title,
- text: e.target.value,
- font: dateScreen.title?.font || "manrope",
- weight: dateScreen.title?.weight || "bold",
- }
- })}
- />
-
-
- {/* Subtitle Configuration */}
-
- Subtitle (Optional)
- onUpdate({
- subtitle: e.target.value ? {
- text: e.target.value,
- font: dateScreen.subtitle?.font || "inter",
- weight: dateScreen.subtitle?.weight || "medium",
- color: dateScreen.subtitle?.color || "muted",
- } : undefined
- })}
- />
-
-
- {/* Date Input Labels */}
+
-
Date Input Labels
-
-
-
-
Month Label
+
+ Поля ввода даты
+
+
+
+ Подпись месяца
onUpdate({
- dateInput: {
- ...dateScreen.dateInput,
- monthLabel: e.target.value,
- }
- })}
+ value={dateScreen.dateInput?.monthLabel ?? ""}
+ onChange={(event) => handleDateInputChange("monthLabel", event.target.value)}
/>
-
-
-
- Day Label
+
+
+ Подпись дня
onUpdate({
- dateInput: {
- ...dateScreen.dateInput,
- dayLabel: e.target.value,
- }
- })}
+ value={dateScreen.dateInput?.dayLabel ?? ""}
+ onChange={(event) => handleDateInputChange("dayLabel", event.target.value)}
/>
-
-
-
- Year Label
+
+
+ Подпись года
onUpdate({
- dateInput: {
- ...dateScreen.dateInput,
- yearLabel: e.target.value,
- }
- })}
+ value={dateScreen.dateInput?.yearLabel ?? ""}
+ onChange={(event) => handleDateInputChange("yearLabel", event.target.value)}
/>
-
+
-
-
-
Month Placeholder
+
+
+ Placeholder месяца
onUpdate({
- dateInput: {
- ...dateScreen.dateInput,
- monthPlaceholder: e.target.value,
- }
- })}
+ value={dateScreen.dateInput?.monthPlaceholder ?? ""}
+ onChange={(event) => handleDateInputChange("monthPlaceholder", event.target.value)}
/>
-
-
-
- Day Placeholder
+
+
+ Placeholder дня
onUpdate({
- dateInput: {
- ...dateScreen.dateInput,
- dayPlaceholder: e.target.value,
- }
- })}
+ value={dateScreen.dateInput?.dayPlaceholder ?? ""}
+ onChange={(event) => handleDateInputChange("dayPlaceholder", event.target.value)}
/>
-
-
-
- Year Placeholder
+
+
+ Placeholder года
onUpdate({
- dateInput: {
- ...dateScreen.dateInput,
- yearPlaceholder: e.target.value,
- }
- })}
+ value={dateScreen.dateInput?.yearPlaceholder ?? ""}
+ onChange={(event) => handleDateInputChange("yearPlaceholder", event.target.value)}
/>
-
+
- {/* Info Message */}
-
-
Info Message (Optional)
-
onUpdate({
- infoMessage: e.target.value ? {
- text: e.target.value,
- icon: dateScreen.infoMessage?.icon || "🔒",
- } : undefined
- })}
- />
-
- {dateScreen.infoMessage && (
- onUpdate({
- infoMessage: {
- text: dateScreen.infoMessage?.text || "",
- icon: e.target.value,
- }
- })}
+
+
Поведение поля
+
+ handleDateInputChange("showSelectedDate", event.target.checked)}
/>
- )}
+ Показывать выбранную дату под полем
+
+
+
+
+ Подпись выбранной даты
+ handleDateInputChange("selectedDateLabel", event.target.value)}
+ />
+
+
+ Формат отображения (date-fns)
+ handleDateInputChange("selectedDateFormat", event.target.value)}
+ />
+
+
+
+
+ Текст ошибки валидации
+ handleDateInputChange("validationMessage", event.target.value)}
+ />
+
- {/* Bottom Action Button */}
-
-
Button Text (Optional)
-
onUpdate({
- bottomActionButton: e.target.value ? {
- text: e.target.value,
- } : undefined
- })}
- />
+
+
Информационный блок
+
+ Сообщение (оставьте пустым, чтобы скрыть)
+ handleInfoMessageChange("text", event.target.value)}
+ />
+
+ {dateScreen.infoMessage && (
+
+ Emoji/иконка для сообщения
+ handleInfoMessageChange("icon", event.target.value)}
+ />
+
+ )}
);
diff --git a/src/components/admin/builder/templates/FormScreenConfig.tsx b/src/components/admin/builder/templates/FormScreenConfig.tsx
index b7bafcb..930cdfd 100644
--- a/src/components/admin/builder/templates/FormScreenConfig.tsx
+++ b/src/components/admin/builder/templates/FormScreenConfig.tsx
@@ -2,7 +2,7 @@
import { Button } from "@/components/ui/button";
import { TextInput } from "@/components/ui/TextInput/TextInput";
-import type { FormScreenDefinition, FormFieldDefinition } from "@/lib/funnel/types";
+import type { FormScreenDefinition, FormFieldDefinition, FormValidationMessages } from "@/lib/funnel/types";
import type { BuilderScreen } from "@/lib/admin/builder/types";
interface FormScreenConfigProps {
@@ -12,180 +12,202 @@ interface FormScreenConfigProps {
export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
const formScreen = screen as FormScreenDefinition & { position: { x: number; y: number } };
-
+
const updateField = (index: number, updates: Partial) => {
const newFields = [...(formScreen.fields || [])];
newFields[index] = { ...newFields[index], ...updates };
onUpdate({ fields: newFields });
};
-
+
+ const updateValidationMessages = (updates: Partial) => {
+ onUpdate({
+ validationMessages: {
+ ...(formScreen.validationMessages ?? {}),
+ ...updates,
+ },
+ });
+ };
+
const addField = () => {
const newField: FormFieldDefinition = {
id: `field_${Date.now()}`,
- label: "New Field",
- placeholder: "Enter value",
+ label: "Новое поле",
+ placeholder: "Введите значение",
type: "text",
required: true,
};
-
+
onUpdate({
- fields: [...(formScreen.fields || []), newField]
+ fields: [...(formScreen.fields || []), newField],
});
};
-
+
const removeField = (index: number) => {
const newFields = formScreen.fields?.filter((_, i) => i !== index) || [];
onUpdate({ fields: newFields });
};
-
+
return (
-
- {/* Title Configuration */}
-
- Title
- onUpdate({
- title: {
- ...formScreen.title,
- text: e.target.value,
- font: formScreen.title?.font || "manrope",
- weight: formScreen.title?.weight || "bold",
- }
- })}
- />
-
-
- {/* Subtitle Configuration */}
-
- Subtitle (Optional)
- onUpdate({
- subtitle: e.target.value ? {
- text: e.target.value,
- font: formScreen.subtitle?.font || "inter",
- weight: formScreen.subtitle?.weight || "medium",
- color: formScreen.subtitle?.color || "muted",
- } : undefined
- })}
- />
-
-
- {/* Form Fields Configuration */}
+
-
Form Fields
-
-
+
{formScreen.fields?.map((field, index) => (
-
-
-
Field {index + 1}
+
+
+
+ Поле {index + 1}
+
removeField(index)}
- className="h-6 px-2 text-xs text-red-600 hover:text-red-700"
>
- Remove
+ Удалить
-
-
-
- Field ID
- updateField(index, { id: e.target.value })}
- />
-
-
-
-
Type
+
+
+
+ ID поля
+ updateField(index, { id: event.target.value })} />
+
+
+ Тип
-
+
-
-
- Label
+
+
+ Метка поля
updateField(index, { label: e.target.value })}
+ value={field.label ?? ""}
+ onChange={(event) => updateField(index, { label: event.target.value })}
/>
-
-
-
- Placeholder
+
+
+
+ Placeholder
updateField(index, { placeholder: e.target.value })}
+ value={field.placeholder ?? ""}
+ onChange={(event) => updateField(index, { placeholder: event.target.value })}
/>
-
-
-
-
+
+
+
+
updateField(index, { required: e.target.checked })}
+ checked={field.required ?? false}
+ onChange={(event) => updateField(index, { required: event.target.checked })}
+ />
+ Обязательно для заполнения
+
+
+ Максимальная длина
+
+ updateField(index, {
+ maxLength: event.target.value ? Number(event.target.value) : undefined,
+ })
+ }
+ />
+
+
+
+
+
+ Регулярное выражение (pattern)
+
+ updateField(index, {
+ validation: {
+ ...(field.validation ?? {}),
+ pattern: event.target.value || undefined,
+ message: field.validation?.message,
+ },
+ })
+ }
+ />
+
+
+ Текст ошибки для pattern
+
+ updateField(index, {
+ validation:
+ field.validation || event.target.value
+ ? {
+ ...(field.validation ?? {}),
+ message: event.target.value || undefined,
+ }
+ : undefined,
+ })
+ }
/>
- Required
-
- {field.maxLength && (
-
- Max Length:
- updateField(index, { maxLength: parseInt(e.target.value) || undefined })}
- />
-
- )}
))}
-
+
{(!formScreen.fields || formScreen.fields.length === 0) && (
-
- No fields added yet. Click "Add Field" to get started.
+
+ Пока нет полей. Добавьте хотя бы одно, чтобы форма работала.
)}
- {/* Bottom Action Button */}
-
-
Button Text
-
onUpdate({
- bottomActionButton: {
- text: e.target.value || "Continue",
- }
- })}
- />
+
+
Сообщения валидации
+
+
+ Обязательное поле
+ updateValidationMessages({ required: event.target.value || undefined })}
+ />
+
+
+ Превышена длина
+ updateValidationMessages({ maxLength: event.target.value || undefined })}
+ />
+
+
+ Неверный формат
+ updateValidationMessages({ invalidFormat: event.target.value || undefined })}
+ />
+
+
);
diff --git a/src/components/admin/builder/templates/InfoScreenConfig.tsx b/src/components/admin/builder/templates/InfoScreenConfig.tsx
index ebb91ff..f6c55e1 100644
--- a/src/components/admin/builder/templates/InfoScreenConfig.tsx
+++ b/src/components/admin/builder/templates/InfoScreenConfig.tsx
@@ -1,7 +1,7 @@
"use client";
import { TextInput } from "@/components/ui/TextInput/TextInput";
-import type { InfoScreenDefinition, TypographyVariant } from "@/lib/funnel/types";
+import type { InfoScreenDefinition } from "@/lib/funnel/types";
import type { BuilderScreen } from "@/lib/admin/builder/types";
interface InfoScreenConfigProps {
@@ -11,145 +11,91 @@ interface InfoScreenConfigProps {
export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
const infoScreen = screen as InfoScreenDefinition & { position: { x: number; y: number } };
-
+
+ const handleDescriptionChange = (text: string) => {
+ onUpdate({
+ description: text
+ ? {
+ ...(infoScreen.description ?? {}),
+ text,
+ }
+ : undefined,
+ });
+ };
+
+ const handleIconChange =
>(
+ field: T,
+ value: NonNullable[T] | undefined
+ ) => {
+ const baseIcon = infoScreen.icon ?? { type: "emoji", value: "✨", size: "lg" };
+
+ if (field === "value") {
+ if (!value) {
+ onUpdate({ icon: undefined });
+ } else {
+ onUpdate({ icon: { ...baseIcon, value } });
+ }
+ return;
+ }
+
+ onUpdate({ icon: { ...baseIcon, [field]: value } });
+ };
+
return (
-
- {/* Title Configuration */}
-
-
Title
-
onUpdate({
- title: {
- ...infoScreen.title,
- text: e.target.value,
- font: infoScreen.title?.font || "manrope",
- weight: infoScreen.title?.weight || "bold",
- align: infoScreen.title?.align || "center",
- }
- })}
- />
-
-
-
-
-
+
+
+
+ Информационный контент
+
+
+ Описание (необязательно)
+ handleDescriptionChange(event.target.value)}
+ />
+
+
+
+
+
Иконка
+
+
+ Тип иконки
+
+
+
+ Размер
+
+
-
- {/* Description Configuration */}
-
- Description (Optional)
- onUpdate({
- description: e.target.value ? {
- text: e.target.value,
- font: infoScreen.description?.font || "inter",
- weight: infoScreen.description?.weight || "medium",
- align: infoScreen.description?.align || "center",
- } : undefined
- })}
- />
-
-
- {/* Icon Configuration */}
-
-
Icon (Optional)
-
-
-
-
-
-
-
onUpdate({
- icon: e.target.value ? {
- type: infoScreen.icon?.type || "emoji",
- value: e.target.value,
- size: infoScreen.icon?.size || "lg",
- } : undefined
- })}
- />
-
-
- {/* Bottom Action Button */}
-
- Button Text (Optional)
- onUpdate({
- bottomActionButton: e.target.value ? {
- text: e.target.value,
- } : undefined
- })}
- />
+
+
+ {infoScreen.icon?.type === "image" ? "Ссылка на изображение" : "Emoji символ"}
+
+ handleIconChange("value", event.target.value || undefined)}
+ />
+
);
diff --git a/src/components/admin/builder/templates/ListScreenConfig.tsx b/src/components/admin/builder/templates/ListScreenConfig.tsx
index df33df6..df239d7 100644
--- a/src/components/admin/builder/templates/ListScreenConfig.tsx
+++ b/src/components/admin/builder/templates/ListScreenConfig.tsx
@@ -2,8 +2,8 @@
import { TextInput } from "@/components/ui/TextInput/TextInput";
import { Button } from "@/components/ui/button";
-import { Trash2, Plus } from "lucide-react";
-import type { ListScreenDefinition, ListOptionDefinition, SelectionType } from "@/lib/funnel/types";
+import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react";
+import type { ListScreenDefinition, ListOptionDefinition, SelectionType, BottomActionButtonDefinition } from "@/lib/funnel/types";
import type { BuilderScreen } from "@/lib/admin/builder/types";
interface ListScreenConfigProps {
@@ -11,194 +11,334 @@ interface ListScreenConfigProps {
onUpdate: (updates: Partial
) => void;
}
+function mutateOptions(
+ options: ListOptionDefinition[],
+ index: number,
+ mutation: (option: ListOptionDefinition) => ListOptionDefinition
+): ListOptionDefinition[] {
+ return options.map((option, currentIndex) => (currentIndex === index ? mutation(option) : option));
+}
+
export function ListScreenConfig({ screen, onUpdate }: ListScreenConfigProps) {
const listScreen = screen as ListScreenDefinition & { position: { x: number; y: number } };
-
- const handleTitleChange = (text: string) => {
- onUpdate({
- title: {
- ...listScreen.title,
- text,
- font: listScreen.title?.font || "manrope",
- weight: listScreen.title?.weight || "bold",
- align: listScreen.title?.align || "left",
- }
- });
- };
-
- const handleSubtitleChange = (text: string) => {
- onUpdate({
- subtitle: text ? {
- ...listScreen.subtitle,
- text,
- font: listScreen.subtitle?.font || "inter",
- weight: listScreen.subtitle?.weight || "medium",
- color: listScreen.subtitle?.color || "muted",
- align: listScreen.subtitle?.align || "left",
- } : undefined
- });
- };
const handleSelectionTypeChange = (selectionType: SelectionType) => {
onUpdate({
list: {
...listScreen.list,
selectionType,
- }
+ },
});
};
- const handleOptionChange = (index: number, field: keyof ListOptionDefinition, value: string | boolean) => {
- const newOptions = [...listScreen.list.options];
- newOptions[index] = {
- ...newOptions[index],
- [field]: value,
- };
-
+ const handleAutoAdvanceChange = (checked: boolean) => {
onUpdate({
list: {
...listScreen.list,
- options: newOptions,
- }
+ autoAdvance: checked || undefined,
+ },
+ });
+ };
+
+ const handleOptionChange = (
+ index: number,
+ field: keyof ListOptionDefinition,
+ value: string | boolean | undefined
+ ) => {
+ const nextOptions = mutateOptions(listScreen.list.options, index, (option) => ({
+ ...option,
+ [field]: value,
+ }));
+
+ onUpdate({
+ list: {
+ ...listScreen.list,
+ options: nextOptions,
+ },
+ });
+ };
+
+ const handleMoveOption = (index: number, direction: -1 | 1) => {
+ const nextOptions = [...listScreen.list.options];
+ const targetIndex = index + direction;
+ if (targetIndex < 0 || targetIndex >= nextOptions.length) {
+ return;
+ }
+ const [current] = nextOptions.splice(index, 1);
+ nextOptions.splice(targetIndex, 0, current);
+
+ onUpdate({
+ list: {
+ ...listScreen.list,
+ options: nextOptions,
+ },
});
};
const handleAddOption = () => {
- const newOptions = [...listScreen.list.options];
- newOptions.push({
- id: `option-${Date.now()}`,
- label: "New Option",
- });
-
+ const nextOptions = [
+ ...listScreen.list.options,
+ {
+ id: `option-${Date.now()}`,
+ label: "Новый вариант",
+ },
+ ];
+
onUpdate({
list: {
...listScreen.list,
- options: newOptions,
- }
+ options: nextOptions,
+ },
});
};
const handleRemoveOption = (index: number) => {
- const newOptions = listScreen.list.options.filter((_, i) => i !== index);
-
+ const nextOptions = listScreen.list.options.filter((_, currentIndex) => currentIndex !== index);
+
onUpdate({
list: {
...listScreen.list,
- options: newOptions,
- }
+ options: nextOptions,
+ },
});
};
- const handleBottomActionButtonChange = (text: string) => {
+ const handleListButtonChange = (value: BottomActionButtonDefinition | undefined) => {
onUpdate({
list: {
...listScreen.list,
- bottomActionButton: text ? {
- text,
- show: true,
- } : undefined,
- }
+ bottomActionButton: value,
+ },
});
};
return (
-
- {/* Title Configuration */}
-
- Title
- handleTitleChange(e.target.value)}
- />
-
-
- {/* Subtitle Configuration */}
-
- Subtitle (Optional)
- handleSubtitleChange(e.target.value)}
- />
-
-
- {/* Selection Type */}
-
-
Selection Type
-
- handleSelectionTypeChange("single")}
- className="h-8 px-3 text-sm"
- >
- Single
-
- handleSelectionTypeChange("multi")}
- className="h-8 px-3 text-sm"
- >
- Multi
-
-
-
-
- {/* Options */}
+
-
Options
-
-
- Add Option
+
+ Варианты выбора
+
+
+ handleSelectionTypeChange("single")}
+ >
+ Один ответ
+
+ handleSelectionTypeChange("multi")}
+ >
+ Несколько ответов
+
+
+
+
+
+ handleAutoAdvanceChange(event.target.checked)}
+ />
+ Автоматический переход после выбора (доступно только для одиночного выбора)
+
+
+
+
+
+
Настройка вариантов
+
+ Добавить
-
-
+
+
{listScreen.list.options.map((option, index) => (
-
-
-
handleOptionChange(index, "id", e.target.value)}
- />
+
+
+
+ Вариант {index + 1}
+
+
+
handleMoveOption(index, -1)}
+ disabled={index === 0}
+ title="Переместить выше"
+ >
+
+
+
handleMoveOption(index, 1)}
+ disabled={index === listScreen.list.options.length - 1}
+ title="Переместить ниже"
+ >
+
+
+
handleRemoveOption(index)}
+ >
+
+
+
-
+
+
+
+ ID варианта
+ handleOptionChange(index, "id", event.target.value)}
+ />
+
+
+ Машинное значение (необязательно)
+
+ handleOptionChange(index, "value", event.target.value || undefined)
+ }
+ />
+
+
+
+
+ Подпись для пользователя
handleOptionChange(index, "label", e.target.value)}
+ onChange={(event) => handleOptionChange(index, "label", event.target.value)}
/>
+
+
+
+
+ Описание (необязательно)
+
+ handleOptionChange(index, "description", event.target.value || undefined)
+ }
+ />
+
+
+ Emoji/иконка
+
+ handleOptionChange(index, "emoji", event.target.value || undefined)
+ }
+ />
+
-
handleRemoveOption(index)}
- className="h-8 px-2"
- >
-
-
+
+
+
+ handleOptionChange(index, "disabled", event.target.checked || undefined)
+ }
+ />
+ Сделать вариант неактивным
+
))}
+
+ {listScreen.list.options.length === 0 && (
+
+ Добавьте хотя бы один вариант, чтобы экран работал корректно.
+
+ )}
- {/* Bottom Action Button */}
-
-
Bottom Action Button (Optional)
-
handleBottomActionButtonChange(e.target.value)}
- />
-
- {listScreen.list.selectionType === "multi"
- ? "Multi selection always shows a button"
- : "Single selection: empty = auto-advance, filled = manual button"}
+
+
Кнопка внутри списка
+
+
+
+ handleListButtonChange(
+ event.target.checked
+ ? listScreen.list.bottomActionButton ?? { text: "Продолжить" }
+ : undefined
+ )
+ }
+ />
+ Показать кнопку под списком
+
+
+ {listScreen.list.bottomActionButton && (
+
+ )}
+
+
+ Для одиночного выбора пустая кнопка включает авто-переход. Для множественного выбора кнопка отображается всегда.
+
diff --git a/src/components/admin/builder/templates/TemplateConfig.tsx b/src/components/admin/builder/templates/TemplateConfig.tsx
index 3472c2e..7db1cd3 100644
--- a/src/components/admin/builder/templates/TemplateConfig.tsx
+++ b/src/components/admin/builder/templates/TemplateConfig.tsx
@@ -1,70 +1,432 @@
"use client";
+import { useMemo } from "react";
+
import { InfoScreenConfig } from "./InfoScreenConfig";
import { DateScreenConfig } from "./DateScreenConfig";
import { CouponScreenConfig } from "./CouponScreenConfig";
import { FormScreenConfig } from "./FormScreenConfig";
import { ListScreenConfig } from "./ListScreenConfig";
+import { TextInput } from "@/components/ui/TextInput/TextInput";
import type { BuilderScreen } from "@/lib/admin/builder/types";
-import type { ScreenDefinition, InfoScreenDefinition, DateScreenDefinition, CouponScreenDefinition, FormScreenDefinition, ListScreenDefinition } from "@/lib/funnel/types";
+import type {
+ ScreenDefinition,
+ InfoScreenDefinition,
+ DateScreenDefinition,
+ CouponScreenDefinition,
+ FormScreenDefinition,
+ ListScreenDefinition,
+ TypographyVariant,
+ BottomActionButtonDefinition,
+ HeaderDefinition,
+} from "@/lib/funnel/types";
+
+const FONT_OPTIONS: TypographyVariant["font"][] = ["manrope", "inter", "geistSans", "geistMono"];
+const WEIGHT_OPTIONS: TypographyVariant["weight"][] = [
+ "regular",
+ "medium",
+ "semiBold",
+ "bold",
+ "extraBold",
+ "black",
+];
+const SIZE_OPTIONS: TypographyVariant["size"][] = ["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl"];
+const ALIGN_OPTIONS: TypographyVariant["align"][] = ["left", "center", "right"];
+const COLOR_OPTIONS: Exclude[] = [
+ "default",
+ "primary",
+ "secondary",
+ "destructive",
+ "success",
+ "card",
+ "accent",
+ "muted",
+];
+const RADIUS_OPTIONS: BottomActionButtonDefinition["cornerRadius"][] = ["3xl", "full"];
interface TemplateConfigProps {
screen: BuilderScreen;
onUpdate: (updates: Partial) => void;
}
+interface TypographyControlsProps {
+ label: string;
+ value: TypographyVariant | undefined;
+ onChange: (value: TypographyVariant | undefined) => void;
+ allowRemove?: boolean;
+}
+
+function TypographyControls({ label, value, onChange, allowRemove = false }: TypographyControlsProps) {
+ const merge = (patch: Partial) => {
+ const base: TypographyVariant = {
+ text: value?.text ?? "",
+ font: value?.font ?? "manrope",
+ weight: value?.weight ?? "bold",
+ size: value?.size ?? "lg",
+ align: value?.align ?? "left",
+ color: value?.color ?? "default",
+ ...value,
+ };
+ onChange({ ...base, ...patch });
+ };
+
+ const handleTextChange = (text: string) => {
+ if (text.trim() === "" && allowRemove) {
+ onChange(undefined);
+ return;
+ }
+
+ merge({ text });
+ };
+
+ return (
+
+
+ {label}
+ handleTextChange(event.target.value)} />
+
+
+
+
+ Шрифт
+
+
+
+ Насыщенность
+
+
+
+ Размер
+
+
+
+ Выравнивание
+
+
+
+ Цвет
+
+
+ {allowRemove && (
+
+ Очистить текст
+ onChange(undefined)}
+ >
+ Удалить поле
+
+
+ )}
+
+
+ );
+}
+
+interface HeaderControlsProps {
+ header: HeaderDefinition | undefined;
+ onChange: (value: HeaderDefinition | undefined) => void;
+}
+
+function HeaderControls({ header, onChange }: HeaderControlsProps) {
+ const activeHeader = header ?? { show: true, showBackButton: true };
+
+ const handleProgressChange = (field: "current" | "total" | "value" | "label", rawValue: string) => {
+ const nextProgress = {
+ ...(activeHeader.progress ?? {}),
+ [field]: rawValue === "" ? undefined : field === "label" ? rawValue : Number(rawValue),
+ };
+
+ const normalizedProgress = Object.values(nextProgress).every((v) => v === undefined)
+ ? undefined
+ : nextProgress;
+
+ onChange({
+ ...activeHeader,
+ progress: normalizedProgress,
+ });
+ };
+
+ const handleToggle = (field: "show" | "showBackButton", checked: boolean) => {
+ if (field === "show" && !checked) {
+ onChange({
+ ...activeHeader,
+ show: false,
+ showBackButton: false,
+ progress: undefined,
+ });
+ return;
+ }
+
+ onChange({
+ ...activeHeader,
+ [field]: checked,
+ });
+ };
+
+ return (
+
+
+ handleToggle("show", event.target.checked)}
+ />
+ Показывать шапку с прогрессом
+
+
+ {activeHeader.show !== false && (
+
+ )}
+
+ );
+}
+
+interface ActionButtonControlsProps {
+ label: string;
+ value: BottomActionButtonDefinition | undefined;
+ onChange: (value: BottomActionButtonDefinition | undefined) => void;
+}
+
+function ActionButtonControls({ label, value, onChange }: ActionButtonControlsProps) {
+ const active = useMemo(() => value, [value]);
+ const isEnabled = Boolean(active);
+
+ return (
+
+
+ {
+ if (event.target.checked) {
+ onChange({ text: active?.text || "Продолжить", show: true });
+ } else {
+ onChange(undefined);
+ }
+ }}
+ />
+ {label}
+
+
+ {isEnabled && (
+
+ )}
+
+ );
+}
+
export function TemplateConfig({ screen, onUpdate }: TemplateConfigProps) {
const { template } = screen;
- switch (template) {
- case "info":
- return (
- ) => void}
- />
- );
-
- case "date":
- return (
- ) => void}
- />
- );
-
- case "coupon":
- return (
- ) => void}
- />
- );
-
- case "form":
- return (
- ) => void}
- />
- );
-
- case "list":
- return (
- ) => void}
- />
- );
-
- default:
- return (
-
-
- Unknown template type: {template}
-
-
- );
- }
+ const handleTitleChange = (value: TypographyVariant) => {
+ onUpdate({ title: value });
+ };
+
+ const handleSubtitleChange = (value: TypographyVariant | undefined) => {
+ onUpdate({ subtitle: value });
+ };
+
+ const handleHeaderChange = (value: HeaderDefinition | undefined) => {
+ onUpdate({ header: value });
+ };
+
+ const handleButtonChange = (value: BottomActionButtonDefinition | undefined) => {
+ onUpdate({ bottomActionButton: value });
+ };
+
+ return (
+
+
+
+
+ {template === "info" && (
+ ) => void}
+ />
+ )}
+ {template === "date" && (
+ ) => void}
+ />
+ )}
+ {template === "coupon" && (
+ ) => void}
+ />
+ )}
+ {template === "form" && (
+ ) => void}
+ />
+ )}
+ {template === "list" && (
+ ) => void}
+ />
+ )}
+
+
+ );
}
diff --git a/src/lib/admin/builder/utils.ts b/src/lib/admin/builder/utils.ts
index 0b7625d..e0f6fd4 100644
--- a/src/lib/admin/builder/utils.ts
+++ b/src/lib/admin/builder/utils.ts
@@ -24,6 +24,7 @@ export function deserializeFunnelDefinition(funnel: FunnelDefinition): BuilderSt
}
export function serializeBuilderState(state: BuilderFunnelState): FunnelDefinition {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
const screens = state.screens.map(({ position: _position, ...rest }) => rest);
const meta: FunnelDefinition["meta"] = {
...state.meta,
@@ -65,6 +66,7 @@ export function cloneScreen(screen: BuilderScreen, overrides?: Partial