w-funnel/src/components/admin/builder/templates/ListScreenConfig.tsx
dev.daminik00 0fc1dc756e admin
2025-09-27 05:48:42 +02:00

292 lines
11 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 { 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>
);
}