diff --git a/src/app/admin/funnels/builder/page.tsx b/src/app/admin/funnels/builder/page.tsx index 0b69e40..beb1f5e 100644 --- a/src/app/admin/funnels/builder/page.tsx +++ b/src/app/admin/funnels/builder/page.tsx @@ -5,6 +5,7 @@ import { useCallback, useState } from "react"; import { BuilderLayout } from "@/components/admin/builder/BuilderLayout"; import { BuilderSidebar } from "@/components/admin/builder/BuilderSidebar"; import { BuilderCanvas } from "@/components/admin/builder/BuilderCanvas"; +import { BuilderPreview } from "@/components/admin/builder/BuilderPreview"; import { BuilderTopBar } from "@/components/admin/builder/BuilderTopBar"; import { BuilderProvider, @@ -41,6 +42,7 @@ function BuilderView() { } sidebar={} canvas={} + preview={} showPreview={showPreview} onTogglePreview={handleTogglePreview} /> diff --git a/src/components/admin/builder/BuilderCanvas.tsx b/src/components/admin/builder/BuilderCanvas.tsx index 5d39263..97b2ea3 100644 --- a/src/components/admin/builder/BuilderCanvas.tsx +++ b/src/components/admin/builder/BuilderCanvas.tsx @@ -1,10 +1,14 @@ "use client"; import React, { useCallback, useMemo, useRef, useState } from "react"; - +import { ArrowDown, ArrowRight, CircleSlash2, GitBranch } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context"; -import type { ListOptionDefinition, ScreenDefinition } from "@/lib/funnel/types"; +import type { + ListOptionDefinition, + NavigationConditionDefinition, + ScreenDefinition, +} from "@/lib/funnel/types"; import { cn } from "@/lib/utils"; function DropIndicator({ isActive }: { isActive: boolean }) { @@ -18,6 +22,106 @@ function DropIndicator({ isActive }: { isActive: boolean }) { ); } +const TEMPLATE_TITLES: Record = { + list: "Список", + form: "Форма", + info: "Инфо", + date: "Дата", + coupon: "Купон", +}; + +const OPERATOR_LABELS: Record, string> = { + includesAny: "любой из", + includesAll: "все из", + includesExactly: "точное совпадение", +}; + +interface TransitionRowProps { + type: "default" | "branch" | "end"; + label: string; + targetLabel?: string; + targetIndex?: number | null; + optionSummaries?: { id: string; label: string }[]; + operator?: string; +} + +function TransitionRow({ + type, + label, + targetLabel, + targetIndex, + optionSummaries = [], + operator, +}: TransitionRowProps) { + const Icon = type === "branch" ? GitBranch : type === "end" ? CircleSlash2 : ArrowDown; + + return ( +
+
+ +
+
+
+ + {label} + + {operator && ( + + {operator} + + )} +
+ {optionSummaries.length > 0 && ( +
+ {optionSummaries.map((option) => ( + + {option.label} + + ))} +
+ )} +
+ {type === "end" ? ( + Завершение воронки + ) : ( + <> + + {typeof targetIndex === "number" && ( + + #{targetIndex + 1} + + )} + + {targetLabel ?? "Не выбрано"} + + + )} +
+
+
+ ); +} + function TemplateSummary({ screen }: { screen: ScreenDefinition }) { switch (screen.template) { case "list": { @@ -253,16 +357,27 @@ export function BuilderCanvas() { const isDropAfter = dropIndex === screens.length && index === screens.length - 1; const rules = screen.navigation?.rules ?? []; const defaultNext = screen.navigation?.defaultNextScreenId; + const isLast = index === screens.length - 1; + const defaultTargetIndex = defaultNext + ? screens.findIndex((candidate) => candidate.id === defaultNext) + : null; return (
-
{isDropBefore && }
-
+
+ + {!isLast && ( +
+
+ +
+ )} +
handleSelectScreen(screen.id)} > -
-
-
+ + {TEMPLATE_TITLES[screen.template] ?? screen.template} + +
+
+
{index + 1}
-
- #{screen.id} - +
+ + #{screen.id} + + {screen.title.text || "Без названия"}
- - {screen.template} -
- {"subtitle" in screen && screen.subtitle?.text && ( -

{screen.subtitle.text}

+ {("subtitle" in screen && screen.subtitle?.text) && ( +

+ {screen.subtitle.text} +

)} -
+
-
+
Переходы
-
-
- - По умолчанию - - - {defaultNext ? screenTitleMap[defaultNext] ?? defaultNext : "Воронка завершится"} - -
+
+ {rules.map((rule, ruleIndex) => { const condition = rule.conditions[0]; @@ -322,37 +439,28 @@ export function BuilderCanvas() { })) : []; + const operatorKey = condition?.operator as + | Exclude + | undefined; + const operatorLabel = operatorKey + ? OPERATOR_LABELS[operatorKey] ?? operatorKey + : undefined; + + const ruleTargetIndex = screens.findIndex( + (candidate) => candidate.id === rule.nextScreenId + ); + const ruleTargetLabel = screenTitleMap[rule.nextScreenId] ?? rule.nextScreenId; + return ( -
-
- - Вариативность - - {condition?.operator && ( - - {condition.operator} - - )} -
- {optionSummaries.length > 0 && ( -
- {optionSummaries.map((option) => ( - - {option.label} - - ))} -
- )} -
- → {screenTitleMap[rule.nextScreenId] ?? rule.nextScreenId} -
-
+ type="branch" + label="Вариативность" + targetLabel={ruleTargetLabel} + targetIndex={ruleTargetIndex !== -1 ? ruleTargetIndex : null} + optionSummaries={optionSummaries} + operator={operatorLabel} + /> ); })}
diff --git a/src/components/admin/builder/BuilderSidebar.tsx b/src/components/admin/builder/BuilderSidebar.tsx index 30a48a8..342138d 100644 --- a/src/components/admin/builder/BuilderSidebar.tsx +++ b/src/components/admin/builder/BuilderSidebar.tsx @@ -478,30 +478,28 @@ export function BuilderSidebar() { - {selectedScreen.template === "list" && ( -
- Варианты ответа -
- {selectedScreen.list.options.map((option) => { - const condition = rule.conditions[0]; - const isChecked = condition.optionIds?.includes(option.id) ?? false; - return ( - - ); - })} -
+
+ Варианты ответа +
+ {selectedScreen.list.options.map((option) => { + const condition = rule.conditions[0]; + const isChecked = condition.optionIds?.includes(option.id) ?? false; + return ( + + ); + })}
- )} +