347 lines
13 KiB
TypeScript
347 lines
13 KiB
TypeScript
"use client";
|
||
|
||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||
import { Button } from "@/components/ui/button";
|
||
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 {
|
||
screen: BuilderScreen & { template: "list" };
|
||
onUpdate: (updates: Partial<ListScreenDefinition>) => 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 handleSelectionTypeChange = (selectionType: SelectionType) => {
|
||
onUpdate({
|
||
list: {
|
||
...listScreen.list,
|
||
selectionType,
|
||
},
|
||
});
|
||
};
|
||
|
||
const handleAutoAdvanceChange = (checked: boolean) => {
|
||
onUpdate({
|
||
list: {
|
||
...listScreen.list,
|
||
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 nextOptions = [
|
||
...listScreen.list.options,
|
||
{
|
||
id: `option-${Date.now()}`,
|
||
label: "Новый вариант",
|
||
},
|
||
];
|
||
|
||
onUpdate({
|
||
list: {
|
||
...listScreen.list,
|
||
options: nextOptions,
|
||
},
|
||
});
|
||
};
|
||
|
||
const handleRemoveOption = (index: number) => {
|
||
const nextOptions = listScreen.list.options.filter((_, currentIndex) => currentIndex !== index);
|
||
|
||
onUpdate({
|
||
list: {
|
||
...listScreen.list,
|
||
options: nextOptions,
|
||
},
|
||
});
|
||
};
|
||
|
||
const handleListButtonChange = (value: BottomActionButtonDefinition | undefined) => {
|
||
onUpdate({
|
||
list: {
|
||
...listScreen.list,
|
||
bottomActionButton: value,
|
||
},
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6 rounded-2xl border border-border/70 bg-background/80 p-5 shadow-sm">
|
||
<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>
|
||
<div className="flex items-center gap-2 text-xs">
|
||
<button
|
||
type="button"
|
||
className={`rounded-md px-3 py-1 transition ${
|
||
listScreen.list.selectionType === "single"
|
||
? "bg-primary text-primary-foreground shadow"
|
||
: "border border-border/60"
|
||
}`}
|
||
onClick={() => handleSelectionTypeChange("single")}
|
||
>
|
||
Один ответ
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`rounded-md px-3 py-1 transition ${
|
||
listScreen.list.selectionType === "multi"
|
||
? "bg-primary text-primary-foreground shadow"
|
||
: "border border-border/60"
|
||
}`}
|
||
onClick={() => handleSelectionTypeChange("multi")}
|
||
>
|
||
Несколько ответов
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
||
<input
|
||
type="checkbox"
|
||
checked={listScreen.list.autoAdvance === true}
|
||
disabled={listScreen.list.selectionType === "multi"}
|
||
onChange={(event) => handleAutoAdvanceChange(event.target.checked)}
|
||
/>
|
||
Автоматический переход после выбора (доступно только для одиночного выбора)
|
||
</label>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<h4 className="text-sm font-semibold text-foreground">Настройка вариантов</h4>
|
||
<Button variant="outline" size="sm" className="h-8 px-3" onClick={handleAddOption}>
|
||
<Plus className="mr-1 h-4 w-4" /> Добавить
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{listScreen.list.options.map((option, index) => (
|
||
<div
|
||
key={option.id}
|
||
className="space-y-3 rounded-xl border border-border/70 bg-muted/10 p-4"
|
||
>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<span className="text-xs font-semibold uppercase text-muted-foreground">
|
||
Вариант {index + 1}
|
||
</span>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
className="rounded-md border border-border/70 p-1 text-muted-foreground transition hover:bg-muted"
|
||
onClick={() => handleMoveOption(index, -1)}
|
||
disabled={index === 0}
|
||
title="Переместить выше"
|
||
>
|
||
<ArrowUp className="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="rounded-md border border-border/70 p-1 text-muted-foreground transition hover:bg-muted"
|
||
onClick={() => handleMoveOption(index, 1)}
|
||
disabled={index === listScreen.list.options.length - 1}
|
||
title="Переместить ниже"
|
||
>
|
||
<ArrowDown className="h-4 w-4" />
|
||
</button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="text-destructive"
|
||
onClick={() => handleRemoveOption(index)}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
||
ID варианта
|
||
<TextInput
|
||
value={option.id}
|
||
onChange={(event) => handleOptionChange(index, "id", event.target.value)}
|
||
/>
|
||
</label>
|
||
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
||
Машинное значение (необязательно)
|
||
<TextInput
|
||
value={option.value ?? ""}
|
||
onChange={(event) =>
|
||
handleOptionChange(index, "value", event.target.value || undefined)
|
||
}
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
||
Подпись для пользователя
|
||
<TextInput
|
||
value={option.label}
|
||
onChange={(event) => handleOptionChange(index, "label", event.target.value)}
|
||
/>
|
||
</label>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
||
Описание (необязательно)
|
||
<TextInput
|
||
value={option.description ?? ""}
|
||
onChange={(event) =>
|
||
handleOptionChange(index, "description", event.target.value || undefined)
|
||
}
|
||
/>
|
||
</label>
|
||
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
||
Emoji/иконка
|
||
<TextInput
|
||
value={option.emoji ?? ""}
|
||
onChange={(event) =>
|
||
handleOptionChange(index, "emoji", event.target.value || undefined)
|
||
}
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
||
<input
|
||
type="checkbox"
|
||
checked={option.disabled === true}
|
||
onChange={(event) =>
|
||
handleOptionChange(index, "disabled", event.target.checked || undefined)
|
||
}
|
||
/>
|
||
Сделать вариант неактивным
|
||
</label>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{listScreen.list.options.length === 0 && (
|
||
<div className="rounded-lg border border-dashed border-border/60 p-4 text-sm text-muted-foreground">
|
||
Добавьте хотя бы один вариант, чтобы экран работал корректно.
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<h4 className="text-sm font-semibold text-foreground">Кнопка внутри списка</h4>
|
||
<div className="rounded-lg border border-border/70 bg-muted/20 p-4 text-xs">
|
||
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
||
<input
|
||
type="checkbox"
|
||
checked={Boolean(listScreen.list.bottomActionButton)}
|
||
onChange={(event) =>
|
||
handleListButtonChange(
|
||
event.target.checked
|
||
? listScreen.list.bottomActionButton ?? { text: "Продолжить" }
|
||
: undefined
|
||
)
|
||
}
|
||
/>
|
||
Показать кнопку под списком
|
||
</label>
|
||
|
||
{listScreen.list.bottomActionButton && (
|
||
<div className="mt-3 space-y-3">
|
||
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
||
Текст кнопки
|
||
<TextInput
|
||
value={listScreen.list.bottomActionButton.text}
|
||
onChange={(event) =>
|
||
handleListButtonChange({
|
||
...listScreen.list.bottomActionButton!,
|
||
text: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||
<label className="flex items-center gap-2 text-muted-foreground">
|
||
<input
|
||
type="checkbox"
|
||
checked={listScreen.list.bottomActionButton.show !== false}
|
||
onChange={(event) =>
|
||
handleListButtonChange({
|
||
...listScreen.list.bottomActionButton!,
|
||
show: event.target.checked,
|
||
})
|
||
}
|
||
/>
|
||
Показывать кнопку
|
||
</label>
|
||
<label className="flex items-center gap-2 text-muted-foreground">
|
||
<input
|
||
type="checkbox"
|
||
checked={listScreen.list.bottomActionButton.disabled === true}
|
||
onChange={(event) =>
|
||
handleListButtonChange({
|
||
...listScreen.list.bottomActionButton!,
|
||
disabled: event.target.checked || undefined,
|
||
})
|
||
}
|
||
/>
|
||
Выключить по умолчанию
|
||
</label>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<p className="mt-3 text-xs text-muted-foreground">
|
||
Для одиночного выбора пустая кнопка включает авто-переход. Для множественного выбора кнопка отображается всегда.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|