w-funnel/src/components/admin/builder/templates/ListScreenConfig.tsx
2025-09-26 12:46:40 +02:00

347 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}