292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||
import { Button } from "@/components/ui/button";
|
||
import { ArrowDown, ArrowUp, Plus, Trash2, ChevronDown, ChevronRight } from "lucide-react";
|
||
import type {
|
||
ListScreenDefinition,
|
||
ListOptionDefinition,
|
||
SelectionType,
|
||
} 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 [expandedOptions, setExpandedOptions] = useState<Set<number>>(new Set());
|
||
|
||
const toggleOptionExpanded = (index: number) => {
|
||
const newExpanded = new Set(expandedOptions);
|
||
if (newExpanded.has(index)) {
|
||
newExpanded.delete(index);
|
||
} else {
|
||
newExpanded.add(index);
|
||
}
|
||
setExpandedOptions(newExpanded);
|
||
};
|
||
|
||
const handleSelectionTypeChange = (selectionType: SelectionType) => {
|
||
onUpdate({
|
||
list: {
|
||
...listScreen.list,
|
||
selectionType,
|
||
},
|
||
});
|
||
};
|
||
|
||
|
||
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,
|
||
},
|
||
});
|
||
};
|
||
|
||
|
||
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>
|
||
<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>
|
||
|
||
<div className="rounded-lg border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground">
|
||
<p><strong>Автопереход:</strong> Для single selection списков без кнопки (bottomActionButton.show: false) включается автоматически.</p>
|
||
</div>
|
||
</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" className="h-8 w-8 p-0 flex items-center justify-center" onClick={handleAddOption}>
|
||
<Plus className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{listScreen.list.options.map((option, index) => (
|
||
<div
|
||
key={option.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">
|
||
<div
|
||
className="flex items-center gap-2 cursor-pointer flex-1"
|
||
onClick={() => toggleOptionExpanded(index)}
|
||
>
|
||
{expandedOptions.has(index) ? (
|
||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||
) : (
|
||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||
)}
|
||
<span className="text-xs font-semibold uppercase text-muted-foreground">
|
||
Вариант {index + 1}
|
||
</span>
|
||
<span className="text-xs text-muted-foreground">
|
||
{option.label || `(Пустой вариант)`}
|
||
</span>
|
||
</div>
|
||
<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"
|
||
className="h-8 px-3 text-xs text-destructive"
|
||
onClick={() => handleRemoveOption(index)}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{expandedOptions.has(index) && (
|
||
<div className="space-y-3 ml-6">
|
||
<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.label}
|
||
onChange={(event) => handleOptionChange(index, "label", event.target.value)}
|
||
/>
|
||
</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>
|
||
|
||
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
||
Описание (необязательно)
|
||
<TextInput
|
||
placeholder="Дополнительное описание варианта"
|
||
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">
|
||
Значение (необязательно)
|
||
<TextInput
|
||
placeholder="Машиночитаемое значение (по умолчанию = ID)"
|
||
value={option.value ?? ""}
|
||
onChange={(event) =>
|
||
handleOptionChange(index, "value", event.target.value || undefined)
|
||
}
|
||
/>
|
||
</label>
|
||
|
||
<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>
|
||
))}
|
||
</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="rounded-lg border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground">
|
||
<p><strong>Автопереход:</strong> Для single selection списков без кнопки (bottomActionButton.show: false) включается автоматически.</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|