194 lines
7.5 KiB
TypeScript
194 lines
7.5 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||
import { TextAreaInput } from "@/components/ui/TextAreaInput/TextAreaInput";
|
||
import type { TrialChoiceScreenDefinition, ArrowHintPosition, AccentedOptionSetting } from "@/lib/funnel/types";
|
||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||
|
||
interface TrialChoiceScreenConfigProps {
|
||
screen: BuilderScreen & { template: "trialChoice" };
|
||
onUpdate: (updates: Partial<TrialChoiceScreenDefinition>) => void;
|
||
}
|
||
|
||
const ARROW_POSITION_OPTIONS: { value: ArrowHintPosition; label: string }[] = [
|
||
{ value: "bottom-right", label: "Снизу справа" },
|
||
{ value: "bottom-left", label: "Снизу слева" },
|
||
{ value: "top-right", label: "Сверху справа" },
|
||
{ value: "top-left", label: "Сверху слева" },
|
||
];
|
||
|
||
const ACCENTED_OPTION_OPTIONS: { value: AccentedOptionSetting; label: string }[] = [
|
||
{ value: "server", label: "С сервера (по умолчанию)" },
|
||
{ value: 1, label: "Вариант 1" },
|
||
{ value: 2, label: "Вариант 2" },
|
||
{ value: 3, label: "Вариант 3" },
|
||
{ value: 4, label: "Вариант 4" },
|
||
];
|
||
|
||
function CollapsibleSection({
|
||
title,
|
||
children,
|
||
defaultExpanded = false,
|
||
}: {
|
||
title: string;
|
||
children: React.ReactNode;
|
||
defaultExpanded?: boolean;
|
||
}) {
|
||
const storageKey = `trial-choice-section-${title.toLowerCase().replace(/\s+/g, "-")}`;
|
||
|
||
const [isExpanded, setIsExpanded] = useState(() => {
|
||
if (typeof window === "undefined") return defaultExpanded;
|
||
|
||
const stored = sessionStorage.getItem(storageKey);
|
||
return stored !== null ? JSON.parse(stored) : defaultExpanded;
|
||
});
|
||
|
||
const handleToggle = () => {
|
||
const newExpanded = !isExpanded;
|
||
setIsExpanded(newExpanded);
|
||
|
||
if (typeof window !== "undefined") {
|
||
sessionStorage.setItem(storageKey, JSON.stringify(newExpanded));
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<button
|
||
type="button"
|
||
onClick={handleToggle}
|
||
className="flex w-full items-center gap-2 text-left text-sm font-medium text-foreground hover:text-primary transition-colors"
|
||
>
|
||
{isExpanded ? (
|
||
<ChevronDown className="h-4 w-4" />
|
||
) : (
|
||
<ChevronRight className="h-4 w-4" />
|
||
)}
|
||
{title}
|
||
</button>
|
||
{isExpanded && <div className="ml-6 space-y-3">{children}</div>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function TrialChoiceScreenConfig({
|
||
screen,
|
||
onUpdate,
|
||
}: TrialChoiceScreenConfigProps) {
|
||
const trialChoiceScreen = screen as TrialChoiceScreenDefinition;
|
||
const arrowHint = trialChoiceScreen.arrowHint;
|
||
|
||
// Для корректной работы с вариантами, передаем полный объект arrowHint
|
||
// чтобы diff/merge функции корректно обрабатывали изменения
|
||
const handleArrowHintTextChange = (text: string) => {
|
||
onUpdate({
|
||
arrowHint: {
|
||
text,
|
||
position: arrowHint?.position ?? "bottom-right",
|
||
},
|
||
});
|
||
};
|
||
|
||
const handleArrowHintPositionChange = (position: ArrowHintPosition) => {
|
||
onUpdate({
|
||
arrowHint: {
|
||
text: arrowHint?.text ?? "",
|
||
position,
|
||
},
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<CollapsibleSection title="Подсказка со стрелкой" defaultExpanded={true}>
|
||
<div className="space-y-4 rounded-2xl border border-border/70 bg-background/80 p-4 shadow-sm">
|
||
{/* Текст подсказки */}
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-medium text-foreground">
|
||
Текст подсказки
|
||
</label>
|
||
<TextAreaInput
|
||
value={arrowHint?.text ?? ""}
|
||
onChange={(event) => handleArrowHintTextChange(event.target.value)}
|
||
rows={3}
|
||
className="resize-y"
|
||
placeholder="Введите текст подсказки..."
|
||
/>
|
||
<div className="rounded-lg bg-muted/50 p-3 space-y-2">
|
||
<p className="text-xs font-medium text-muted-foreground">
|
||
Доступные переменные:
|
||
</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
<code className="bg-background px-2 py-1 rounded text-xs border border-border">
|
||
{"{{maxTrialPrice}}"}
|
||
</code>
|
||
<span className="text-xs text-muted-foreground">
|
||
— максимальная цена триала
|
||
</span>
|
||
</div>
|
||
<p className="text-[10px] text-muted-foreground mt-2">
|
||
Переменная автоматически заменяется на форматированную цену самого дорогого варианта триала.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Позиция стрелки */}
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-medium text-foreground">
|
||
Позиция стрелки
|
||
</label>
|
||
<select
|
||
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||
value={arrowHint?.position ?? "bottom-right"}
|
||
onChange={(event) =>
|
||
handleArrowHintPositionChange(event.target.value as ArrowHintPosition)
|
||
}
|
||
>
|
||
{ARROW_POSITION_OPTIONS.map((option) => (
|
||
<option key={option.value} value={option.value}>
|
||
{option.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<p className="text-xs text-muted-foreground">
|
||
Определяет на какой угол сетки вариантов указывает стрелка.
|
||
При выборе позиции сверху стрелка и текст отображаются над сеткой.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</CollapsibleSection>
|
||
|
||
<CollapsibleSection title="Подсветка варианта" defaultExpanded={true}>
|
||
<div className="space-y-4 rounded-2xl border border-border/70 bg-background/80 p-4 shadow-sm">
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-medium text-foreground">
|
||
Подсвечиваемый вариант
|
||
</label>
|
||
<select
|
||
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||
value={String(trialChoiceScreen.accentedOption ?? "server")}
|
||
onChange={(event) => {
|
||
const value = event.target.value;
|
||
const parsed = value === "server" ? "server" : parseInt(value, 10);
|
||
onUpdate({ accentedOption: parsed as AccentedOptionSetting });
|
||
}}
|
||
>
|
||
{ACCENTED_OPTION_OPTIONS.map((option) => (
|
||
<option key={String(option.value)} value={String(option.value)}>
|
||
{option.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<p className="text-xs text-muted-foreground">
|
||
Выберите какой вариант триала будет подсвечен.
|
||
Если выбранный номер больше количества вариантов с сервера,
|
||
будет использована серверная логика.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</CollapsibleSection>
|
||
</div>
|
||
);
|
||
}
|