"use client"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import { ChevronDown, ChevronRight } from "lucide-react"; import { TextInput } from "@/components/ui/TextInput/TextInput"; import { Button } from "@/components/ui/button"; import { TemplateConfig } from "@/components/admin/builder/templates"; import { ScreenVariantsConfig } from "@/components/admin/builder/ScreenVariantsConfig"; import { useBuilderDispatch, useBuilderSelectedScreen, useBuilderState } from "@/lib/admin/builder/context"; import type { BuilderScreen } from "@/lib/admin/builder/types"; import type { NavigationRuleDefinition, ScreenDefinition, ScreenVariantDefinition, } from "@/lib/funnel/types"; import { cn } from "@/lib/utils"; import { validateBuilderState } from "@/lib/admin/builder/validation"; type ValidationIssues = ReturnType["issues"]; function isListScreen( screen: BuilderScreen ): screen is BuilderScreen & { list: { selectionType: "single" | "multi"; options: Array<{ id: string; label: string; description?: string; emoji?: string }>; }; } { return screen.template === "list" && "list" in screen; } function Section({ title, description, children, defaultExpanded = false, alwaysExpanded = false, }: { title: string; description?: string; children: ReactNode; defaultExpanded?: boolean; alwaysExpanded?: boolean; }) { const storageKey = `section-${title.toLowerCase().replace(/\s+/g, '-')}`; const [isExpanded, setIsExpanded] = useState(defaultExpanded); const [isHydrated, setIsHydrated] = useState(false); useEffect(() => { if (alwaysExpanded) { setIsExpanded(true); setIsHydrated(true); return; } const stored = sessionStorage.getItem(storageKey); if (stored !== null) { setIsExpanded(JSON.parse(stored)); } setIsHydrated(true); }, [alwaysExpanded, storageKey]); const handleToggle = () => { if (alwaysExpanded) return; const newExpanded = !isExpanded; setIsExpanded(newExpanded); if (typeof window !== 'undefined') { sessionStorage.setItem(storageKey, JSON.stringify(newExpanded)); } }; const effectiveExpanded = alwaysExpanded || (isHydrated ? isExpanded : defaultExpanded); return (
{!alwaysExpanded && ( effectiveExpanded ? ( ) : ( ) )}

{title}

{description &&

{description}

}
{effectiveExpanded && (
{children}
)}
); } function ValidationSummary({ issues }: { issues: ValidationIssues }) { if (issues.length === 0) { return (
Всё хорошо — воронка валидна.
); } return (
{issues.map((issue, index) => (

{issue.message}

{issue.screenId &&

Экран: {issue.screenId}

}
))}
); } export function BuilderSidebar() { const state = useBuilderState(); const dispatch = useBuilderDispatch(); const selectedScreen = useBuilderSelectedScreen(); const [activeTab, setActiveTab] = useState<"funnel" | "screen">(selectedScreen ? "screen" : "funnel"); const selectedScreenId = selectedScreen?.id ?? null; useEffect(() => { setActiveTab((previous) => { if (selectedScreenId) { return "screen"; } return previous === "screen" ? "funnel" : previous; }); }, [selectedScreenId]); const validation = useMemo(() => validateBuilderState(state), [state]); const screenValidationIssues = useMemo(() => { if (!selectedScreenId) { return [] as ValidationIssues; } return validation.issues.filter((issue) => issue.screenId === selectedScreenId); }, [selectedScreenId, validation]); const screenOptions = useMemo( () => state.screens.map((screen) => ({ id: screen.id, title: screen.title.text })), [state.screens] ); const handleMetaChange = (field: keyof typeof state.meta, value: string) => { dispatch({ type: "set-meta", payload: { [field]: value } }); }; const handleFirstScreenChange = (value: string) => { dispatch({ type: "set-meta", payload: { firstScreenId: value } }); }; const handleScreenIdChange = (currentId: string, newId: string) => { if (newId.trim() === "" || newId === currentId) { return; } // Обновляем ID экрана dispatch({ type: "update-screen", payload: { screenId: currentId, screen: { id: newId } } }); // Если это был первый экран в мета данных, обновляем и там if (state.meta.firstScreenId === currentId) { dispatch({ type: "set-meta", payload: { firstScreenId: newId } }); } }; const getScreenById = (screenId: string): BuilderScreen | undefined => state.screens.find((item) => item.id === screenId); const updateNavigation = ( screen: BuilderScreen, navigationUpdates: Partial = {} ) => { dispatch({ type: "update-navigation", payload: { screenId: screen.id, navigation: { defaultNextScreenId: navigationUpdates.defaultNextScreenId ?? screen.navigation?.defaultNextScreenId, rules: navigationUpdates.rules ?? screen.navigation?.rules ?? [], }, }, }); }; const handleDefaultNextChange = (screenId: string, nextScreenId: string | "") => { const screen = getScreenById(screenId); if (!screen) { return; } updateNavigation(screen, { defaultNextScreenId: nextScreenId || undefined, }); }; const updateRules = (screenId: string, rules: NavigationRuleDefinition[]) => { const screen = getScreenById(screenId); if (!screen) { return; } updateNavigation(screen, { rules }); }; const handleRuleOperatorChange = ( screenId: string, index: number, operator: NavigationRuleDefinition["conditions"][0]["operator"] ) => { const screen = getScreenById(screenId); if (!screen) { return; } const rules = screen.navigation?.rules ?? []; const nextRules = rules.map((rule, ruleIndex) => ruleIndex === index ? { ...rule, conditions: rule.conditions.map((condition, conditionIndex) => conditionIndex === 0 ? { ...condition, operator, } : condition ), } : rule ); updateRules(screenId, nextRules); }; const handleRuleOptionToggle = (screenId: string, ruleIndex: number, optionId: string) => { const screen = getScreenById(screenId); if (!screen) { return; } const rules = screen.navigation?.rules ?? []; const nextRules = rules.map((rule, currentIndex) => { if (currentIndex !== ruleIndex) { return rule; } const [condition] = rule.conditions; const optionIds = new Set(condition.optionIds ?? []); if (optionIds.has(optionId)) { optionIds.delete(optionId); } else { optionIds.add(optionId); } return { ...rule, conditions: [ { ...condition, optionIds: Array.from(optionIds), }, ], }; }); updateRules(screenId, nextRules); }; const handleRuleNextScreenChange = (screenId: string, ruleIndex: number, nextScreenId: string) => { const screen = getScreenById(screenId); if (!screen) { return; } const rules = screen.navigation?.rules ?? []; const nextRules = rules.map((rule, currentIndex) => currentIndex === ruleIndex ? { ...rule, nextScreenId } : rule ); updateRules(screenId, nextRules); }; const handleAddRule = (screen: BuilderScreen) => { if (!isListScreen(screen)) { return; } const defaultCondition: NavigationRuleDefinition["conditions"][number] = { screenId: screen.id, operator: "includesAny", optionIds: screen.list.options.slice(0, 1).map((option) => option.id), }; const nextRules = [ ...(screen.navigation?.rules ?? []), { nextScreenId: state.screens[0]?.id ?? screen.id, conditions: [defaultCondition] }, ]; updateNavigation(screen, { rules: nextRules }); }; const handleRemoveRule = (screenId: string, ruleIndex: number) => { const screen = getScreenById(screenId); if (!screen) { return; } const rules = screen.navigation?.rules ?? []; const nextRules = rules.filter((_, index) => index !== ruleIndex); updateNavigation(screen, { rules: nextRules }); }; const handleDeleteScreen = (screenId: string) => { if (state.screens.length <= 1) { return; } dispatch({ type: "remove-screen", payload: { screenId } }); }; const handleTemplateUpdate = (screenId: string, updates: Partial) => { dispatch({ type: "update-screen", payload: { screenId, screen: updates as Partial, }, }); }; const handleVariantsChange = ( screenId: string, variants: ScreenVariantDefinition[] ) => { dispatch({ type: "update-screen", payload: { screenId, screen: { variants: variants.length > 0 ? variants : undefined, } as Partial, }, }); }; const selectedScreenIsListType = selectedScreen ? isListScreen(selectedScreen) : false; return (

Настройки

{activeTab === "funnel" ? (
handleMetaChange("id", event.target.value)} /> handleMetaChange("title", event.target.value)} /> handleMetaChange("description", event.target.value)} />
Всего экранов {state.screens.length}
{state.screens.map((screen, index) => ( {index + 1}. {screen.title.text} {screen.template} ))}
) : selectedScreen ? (
#{selectedScreen.id} {selectedScreen.template}
{state.screens.findIndex((screen) => screen.id === selectedScreen.id) + 1}/{state.screens.length}
handleScreenIdChange(selectedScreen.id, event.target.value)} />
handleTemplateUpdate(selectedScreen.id, updates)} />
handleVariantsChange(selectedScreen.id, variants)} />
{selectedScreenIsListType && (

Направляйте пользователей на разные экраны в зависимости от выбора.

{(selectedScreen.navigation?.rules ?? []).length === 0 && (
Правил пока нет
)} {(selectedScreen.navigation?.rules ?? []).map((rule, ruleIndex) => (
Правило {ruleIndex + 1}
{selectedScreen.template === "list" ? (
Варианты ответа
{selectedScreen.list.options.map((option) => { const condition = rule.conditions[0]; const isChecked = condition.optionIds?.includes(option.id) ?? false; return ( ); })}
) : (
Навигационные правила с вариантами ответа доступны только для экранов со списком.
)}
))}
)}

Удаление экрана нельзя отменить. Все связи с этим экраном будут потеряны.

) : (
Выберите экран в списке слева, чтобы настроить его параметры.
)}
); }