From 84fb57ab607190b2151be38d3fc69d5411853865 Mon Sep 17 00:00:00 2001 From: "dev.daminik00" Date: Thu, 25 Sep 2025 18:04:52 +0200 Subject: [PATCH 01/27] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=D0=BE=D1=80=D0=BE=D0=BD=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/GuardIcon.svg | 4 + public/funnels/funnel-test.json | 879 ++++++++++++++++++ src/app/[funnelId]/[screenId]/page.tsx | 38 + src/app/[funnelId]/page.tsx | 30 + src/app/admin/funnels/builder/page.tsx | 132 +++ src/app/layout.tsx | 3 +- .../admin/builder/BuilderCanvas.tsx | 215 +++++ .../admin/builder/BuilderLayout.tsx | 85 ++ .../admin/builder/BuilderPreview.tsx | 164 ++++ .../admin/builder/BuilderSidebar.tsx | 658 +++++++++++++ .../admin/builder/BuilderTopBar.tsx | 77 ++ .../builder/templates/CouponScreenConfig.tsx | 164 ++++ .../builder/templates/DateScreenConfig.tsx | 187 ++++ .../builder/templates/FormScreenConfig.tsx | 194 ++++ .../builder/templates/InfoScreenConfig.tsx | 158 ++++ .../builder/templates/TemplateConfig.tsx | 80 ++ .../builder/templates/TextScreenConfig.tsx | 198 ++++ .../admin/builder/templates/index.ts | 6 + src/components/funnel/FunnelRuntime.tsx | 328 +++++++ .../funnel/templates/CouponTemplate.tsx | 187 ++++ .../funnel/templates/DateTemplate.tsx | 315 +++++++ .../funnel/templates/FormTemplate.tsx | 194 ++++ .../funnel/templates/InfoTemplate.tsx | 157 ++++ .../funnel/templates/ListTemplate.tsx | 149 +++ .../funnel/templates/TextTemplate.tsx | 97 ++ src/components/layout/Header/Header.tsx | 29 +- .../layout/LayoutQuestion/LayoutQuestion.tsx | 8 +- src/components/providers/AppProviders.tsx | 13 + .../ui/ActionButton/ActionButton.tsx | 6 +- .../BottomActionButton/BottomActionButton.tsx | 42 +- .../RadioAnswersList/RadioAnswersList.tsx | 4 + .../SelectAnswersList/SelectAnswersList.tsx | 4 + src/lib/admin/builder/context.tsx | 310 ++++++ src/lib/admin/builder/templates.ts | 106 +++ src/lib/admin/builder/types.ts | 15 + src/lib/admin/builder/utils.ts | 68 ++ src/lib/admin/builder/validation.ts | 175 ++++ src/lib/funnel/FunnelProvider.tsx | 211 +++++ src/lib/funnel/loadFunnelDefinition.ts | 20 + src/lib/funnel/mappers.tsx | 305 ++++++ src/lib/funnel/navigation.ts | 78 ++ src/lib/funnel/types.ts | 281 ++++++ 42 files changed, 6339 insertions(+), 35 deletions(-) create mode 100644 public/GuardIcon.svg create mode 100644 public/funnels/funnel-test.json create mode 100644 src/app/[funnelId]/[screenId]/page.tsx create mode 100644 src/app/[funnelId]/page.tsx create mode 100644 src/app/admin/funnels/builder/page.tsx create mode 100644 src/components/admin/builder/BuilderCanvas.tsx create mode 100644 src/components/admin/builder/BuilderLayout.tsx create mode 100644 src/components/admin/builder/BuilderPreview.tsx create mode 100644 src/components/admin/builder/BuilderSidebar.tsx create mode 100644 src/components/admin/builder/BuilderTopBar.tsx create mode 100644 src/components/admin/builder/templates/CouponScreenConfig.tsx create mode 100644 src/components/admin/builder/templates/DateScreenConfig.tsx create mode 100644 src/components/admin/builder/templates/FormScreenConfig.tsx create mode 100644 src/components/admin/builder/templates/InfoScreenConfig.tsx create mode 100644 src/components/admin/builder/templates/TemplateConfig.tsx create mode 100644 src/components/admin/builder/templates/TextScreenConfig.tsx create mode 100644 src/components/admin/builder/templates/index.ts create mode 100644 src/components/funnel/FunnelRuntime.tsx create mode 100644 src/components/funnel/templates/CouponTemplate.tsx create mode 100644 src/components/funnel/templates/DateTemplate.tsx create mode 100644 src/components/funnel/templates/FormTemplate.tsx create mode 100644 src/components/funnel/templates/InfoTemplate.tsx create mode 100644 src/components/funnel/templates/ListTemplate.tsx create mode 100644 src/components/funnel/templates/TextTemplate.tsx create mode 100644 src/components/providers/AppProviders.tsx create mode 100644 src/lib/admin/builder/context.tsx create mode 100644 src/lib/admin/builder/templates.ts create mode 100644 src/lib/admin/builder/types.ts create mode 100644 src/lib/admin/builder/utils.ts create mode 100644 src/lib/admin/builder/validation.ts create mode 100644 src/lib/funnel/FunnelProvider.tsx create mode 100644 src/lib/funnel/loadFunnelDefinition.ts create mode 100644 src/lib/funnel/mappers.tsx create mode 100644 src/lib/funnel/navigation.ts create mode 100644 src/lib/funnel/types.ts diff --git a/public/GuardIcon.svg b/public/GuardIcon.svg new file mode 100644 index 0000000..7a3a219 --- /dev/null +++ b/public/GuardIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/funnels/funnel-test.json b/public/funnels/funnel-test.json new file mode 100644 index 0000000..e2e1284 --- /dev/null +++ b/public/funnels/funnel-test.json @@ -0,0 +1,879 @@ +{ + "meta": { + "id": "funnel-test", + "title": "Relationship Portrait", + "description": "Demo funnel mirroring design screens with branching by analysis target.", + "firstScreenId": "intro-welcome" + }, + "defaultTexts": { + "nextButton": "Next", + "continueButton": "Continue" + }, + "colorPalette": { + "text": { + "primary": "#1E293B", + "secondary": "#475569", + "muted": "#64748B", + "accent": "#3B82F6", + "success": "#10B981", + "error": "#EF4444", + "warning": "#F59E0B" + }, + "background": { + "primary": "#FFFFFF", + "secondary": "#F8FAFC", + "accent": "#EFF6FF", + "success": "#ECFDF5", + "error": "#FEF2F2", + "warning": "#FFFBEB" + }, + "button": { + "primary": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + "primaryText": "#FFFFFF", + "secondary": "#F1F5F9", + "secondaryText": "#334155", + "disabled": "#E2E8F0", + "disabledText": "#94A3B8" + }, + "border": { + "primary": "#E2E8F0", + "accent": "#3B82F6", + "success": "#10B981", + "error": "#EF4444" + }, + "shadow": { + "light": "rgba(0, 0, 0, 0.05)", + "medium": "rgba(0, 0, 0, 0.1)", + "heavy": "rgba(0, 0, 0, 0.15)", + "colored": "rgba(59, 130, 246, 0.3)" + } + }, + "screens": [ + { + "id": "intro-welcome", + "template": "info", + "title": { + "text": "Вы не одиноки в этом страхе", + "font": "manrope", + "weight": "bold", + "align": "center" + }, + "description": { + "text": "Многие боятся повторить прошлый опыт. Мы поможем распознать верные сигналы и выбрать «своего» человека.", + "font": "inter", + "weight": "medium", + "color": "default", + "align": "center" + }, + "icon": { + "type": "emoji", + "value": "❤️", + "size": "xl" + }, + "bottomActionButton": { + "text": "Next" + }, + "navigation": { + "defaultNextScreenId": "intro-statistics" + } + }, + { + "id": "intro-statistics", + "template": "info", + "title": { + "text": "По нашей статистике 51 % женщин Овнов доверяются эмоциям. Но одной чувствительности мало. Мы покажем, какие качества второй половинки дадут тепло и уверенность, и изобразим её портрет.", + "font": "manrope", + "weight": "bold", + "align": "center" + }, + "icon": { + "type": "emoji", + "value": "🔥❤️", + "size": "xl" + }, + "bottomActionButton": { + "text": "Next" + }, + "navigation": { + "defaultNextScreenId": "intro-partner-traits" + } + }, + { + "id": "intro-partner-traits", + "template": "info", + "header": { + "showBackButton": false + }, + "title": { + "text": "Такой партнёр умеет слышать и поддерживать, а вы — человек с глубокой душой, который ценит искренность и силу настоящих чувств.", + "font": "manrope", + "weight": "bold", + "align": "center" + }, + "icon": { + "type": "emoji", + "value": "💖", + "size": "xl" + }, + "bottomActionButton": { + "text": "Next" + }, + "navigation": { + "defaultNextScreenId": "birth-date" + } + }, + { + "id": "birth-date", + "template": "date", + "title": { + "text": "Когда ты родился?", + "font": "manrope", + "weight": "bold" + }, + "subtitle": { + "text": "В момент вашего рождения заложенны глубинные закономерности.", + "font": "inter", + "weight": "medium", + "color": "muted" + }, + "dateInput": { + "monthPlaceholder": "MM", + "dayPlaceholder": "DD", + "yearPlaceholder": "YYYY", + "monthLabel": "Month", + "dayLabel": "Day", + "yearLabel": "Year", + "showSelectedDate": true, + "selectedDateLabel": "Выбранная дата:" + }, + "infoMessage": { + "text": "Мы не передаем личную информацию, она остаётся в безопасности и под вашим контролем.", + "font": "inter", + "weight": "medium", + "color": "muted" + }, + "bottomActionButton": { + "text": "Next" + }, + "navigation": { + "defaultNextScreenId": "address-form" + } + }, + { + "id": "address-form", + "template": "form", + "title": { + "text": "Which best represents your hair loss and goals?", + "font": "manrope", + "weight": "bold" + }, + "subtitle": { + "text": "Let's personalize your hair care journey", + "font": "inter", + "weight": "medium", + "color": "muted" + }, + "fields": [ + { + "id": "address", + "label": "Address", + "placeholder": "Enter your full address", + "type": "text", + "required": true, + "maxLength": 200 + } + ], + "validationMessages": { + "required": "${field} обязательно для заполнения", + "maxLength": "Максимум ${maxLength} символов", + "invalidFormat": "Неверный формат" + }, + "bottomActionButton": { + "text": "Continue" + }, + "navigation": { + "defaultNextScreenId": "statistics-text" + } + }, + { + "id": "statistics-text", + "template": "text", + "title": { + "text": "Which best represents your hair loss and goals?", + "font": "manrope", + "weight": "bold", + "align": "center" + }, + "content": { + "text": "По нашей статистике 51 % женщин Овнов доверяются эмоциям. Но одной чувствительности мало. Мы покажем, какие качества второй половинки дадут тепло и уверенность, и изобразим её портрет.", + "font": "inter", + "weight": "medium", + "color": "default", + "align": "center" + } + }, + { + "id": "gender", + "template": "list", + "title": { + "text": "Какого ты пола?", + "font": "manrope", + "weight": "bold" + }, + "subtitle": { + "text": "Все начинается с тебя! Выбери свой пол.", + "font": "inter", + "weight": "medium", + "color": "muted" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "female", + "label": "FEMALE", + "emoji": "💗" + }, + { + "id": "male", + "label": "MALE", + "emoji": "💙" + } + ] + }, + "navigation": { + "defaultNextScreenId": "relationship-status" + } + }, + { + "id": "relationship-status", + "template": "list", + "title": { + "text": "Вы сейчас?", + "font": "manrope", + "weight": "bold" + }, + "subtitle": { + "text": "Это нужно, чтобы портрет и советы были точнее.", + "color": "muted" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "in-relationship", + "label": "В отношениях" + }, + { + "id": "single", + "label": "Свободны" + }, + { + "id": "after-breakup", + "label": "После расставания" + }, + { + "id": "complicated", + "label": "Все сложно" + } + ] + }, + "navigation": { + "defaultNextScreenId": "analysis-target" + } + }, + { + "id": "analysis-target", + "template": "list", + "header": { + "progress": { + "current": 6, + "total": 15, + "label": "6 of 15" + } + }, + "title": { + "text": "Кого анализируем?", + "font": "manrope", + "weight": "bold" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "current-partner", + "label": "Текущего партнера" + }, + { + "id": "crush", + "label": "Человека, который нравится" + }, + { + "id": "ex-partner", + "label": "Бывшего" + }, + { + "id": "future-partner", + "label": "Будущую встречу" + } + ] + }, + "navigation": { + "rules": [ + { + "conditions": [ + { + "screenId": "analysis-target", + "operator": "includesAny", + "optionIds": ["current-partner"] + } + ], + "nextScreenId": "current-partner-age" + }, + { + "conditions": [ + { + "screenId": "analysis-target", + "operator": "includesAny", + "optionIds": ["crush"] + } + ], + "nextScreenId": "crush-age" + }, + { + "conditions": [ + { + "screenId": "analysis-target", + "operator": "includesAny", + "optionIds": ["ex-partner"] + } + ], + "nextScreenId": "ex-partner-age" + }, + { + "conditions": [ + { + "screenId": "analysis-target", + "operator": "includesAny", + "optionIds": ["future-partner"] + } + ], + "nextScreenId": "future-partner-age" + } + ], + "defaultNextScreenId": "current-partner-age" + } + }, + { + "id": "current-partner-age", + "template": "list", + "header": { + "progress": { + "current": 4, + "total": 9, + "label": "4 of 9" + } + }, + "title": { + "text": "Возраст текущего партнера", + "font": "manrope", + "weight": "bold" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "under-29", + "label": "До 29" + }, + { + "id": "30-39", + "label": "30-39" + }, + { + "id": "40-49", + "label": "40-49" + }, + { + "id": "50-59", + "label": "50-59" + }, + { + "id": "60-plus", + "label": "60+" + } + ] + }, + "navigation": { + "rules": [ + { + "conditions": [ + { + "screenId": "current-partner-age", + "operator": "includesAny", + "optionIds": ["under-29"] + } + ], + "nextScreenId": "age-refine" + } + ], + "defaultNextScreenId": "partner-ethnicity" + } + }, + { + "id": "crush-age", + "template": "list", + "header": { + "progress": { + "current": 4, + "total": 9, + "label": "4 of 9" + } + }, + "title": { + "text": "Возраст человека, который нравится", + "font": "manrope", + "weight": "bold" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "under-29", + "label": "До 29" + }, + { + "id": "30-39", + "label": "30-39" + }, + { + "id": "40-49", + "label": "40-49" + }, + { + "id": "50-59", + "label": "50-59" + }, + { + "id": "60-plus", + "label": "60+" + } + ] + }, + "navigation": { + "rules": [ + { + "conditions": [ + { + "screenId": "crush-age", + "operator": "includesAny", + "optionIds": ["under-29"] + } + ], + "nextScreenId": "age-refine" + } + ], + "defaultNextScreenId": "partner-ethnicity" + } + }, + { + "id": "ex-partner-age", + "template": "list", + "header": { + "progress": { + "current": 4, + "total": 9, + "label": "4 of 9" + } + }, + "title": { + "text": "Возраст бывшего", + "font": "manrope", + "weight": "bold" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "under-29", + "label": "До 29" + }, + { + "id": "30-39", + "label": "30-39" + }, + { + "id": "40-49", + "label": "40-49" + }, + { + "id": "50-59", + "label": "50-59" + }, + { + "id": "60-plus", + "label": "60+" + } + ] + }, + "navigation": { + "rules": [ + { + "conditions": [ + { + "screenId": "ex-partner-age", + "operator": "includesAny", + "optionIds": ["under-29"] + } + ], + "nextScreenId": "age-refine" + } + ], + "defaultNextScreenId": "partner-ethnicity" + } + }, + { + "id": "future-partner-age", + "template": "list", + "header": { + "progress": { + "current": 4, + "total": 9, + "label": "4 of 9" + } + }, + "title": { + "text": "Возраст будущего партнера", + "font": "manrope", + "weight": "bold" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "under-29", + "label": "До 29" + }, + { + "id": "30-39", + "label": "30-39" + }, + { + "id": "40-49", + "label": "40-49" + }, + { + "id": "50-59", + "label": "50-59" + }, + { + "id": "60-plus", + "label": "60+" + } + ] + }, + "navigation": { + "rules": [ + { + "conditions": [ + { + "screenId": "future-partner-age", + "operator": "includesAny", + "optionIds": ["under-29"] + } + ], + "nextScreenId": "age-refine" + } + ], + "defaultNextScreenId": "partner-ethnicity" + } + }, + { + "id": "age-refine", + "template": "list", + "header": { + "progress": { + "current": 5, + "total": 9, + "label": "5 of 9" + } + }, + "title": { + "text": "Уточните чуть точнее", + "font": "manrope", + "weight": "bold" + }, + "subtitle": { + "text": "Чтобы портрет был максимально похож.", + "color": "muted" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "18-21", + "label": "18-21" + }, + { + "id": "22-25", + "label": "22-25" + }, + { + "id": "26-29", + "label": "26-29" + } + ] + }, + "navigation": { + "defaultNextScreenId": "partner-ethnicity" + } + }, + { + "id": "partner-ethnicity", + "template": "list", + "header": { + "progress": { + "current": 6, + "total": 9, + "label": "6 of 9" + } + }, + "title": { + "text": "Этническая принадлежность твоей второй половинки?", + "font": "manrope", + "weight": "bold" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "white", + "label": "White" + }, + { + "id": "hispanic", + "label": "Hispanic / Latino" + }, + { + "id": "african", + "label": "African / African-American" + }, + { + "id": "asian", + "label": "Asian" + }, + { + "id": "south-asian", + "label": "Indian / South Asian" + }, + { + "id": "middle-eastern", + "label": "Middle Eastern / Arab" + }, + { + "id": "indigenous", + "label": "Native American / Indigenous" + }, + { + "id": "no-preference", + "label": "No preference" + } + ] + }, + "navigation": { + "defaultNextScreenId": "partner-eyes" + } + }, + { + "id": "partner-eyes", + "template": "list", + "header": { + "progress": { + "current": 7, + "total": 9, + "label": "7 of 9" + } + }, + "title": { + "text": "Что из этого «про глаза»?", + "font": "manrope", + "weight": "bold" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "warm-glow", + "label": "Тёплые искры на свету" + }, + { + "id": "clear-depth", + "label": "Прозрачная глубина" + }, + { + "id": "green-sheen", + "label": "Зелёный отлив на границе зрачка" + }, + { + "id": "steel-glint", + "label": "Холодный стальной отблеск" + }, + { + "id": "deep-shadow", + "label": "Насыщенная темнота" + }, + { + "id": "dont-know", + "label": "Не знаю" + } + ] + }, + "navigation": { + "defaultNextScreenId": "partner-hair-length" + } + }, + { + "id": "partner-hair-length", + "template": "list", + "header": { + "progress": { + "current": 8, + "total": 9, + "label": "8 of 9" + } + }, + "title": { + "text": "Выберите длину волос", + "font": "manrope", + "weight": "bold" + }, + "subtitle": { + "text": "От неё зависит форма и настроение портрета.", + "color": "muted" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "short", + "label": "Короткие" + }, + { + "id": "medium", + "label": "Средние" + }, + { + "id": "long", + "label": "Длинные" + } + ] + }, + "navigation": { + "defaultNextScreenId": "burnout-support" + } + }, + { + "id": "burnout-support", + "template": "list", + "header": { + "progress": { + "current": 9, + "total": 9, + "label": "9 of 9" + } + }, + "title": { + "text": "Когда ты выгораешь, тебе нужно чтобы партнёр...", + "font": "manrope", + "weight": "bold" + }, + "list": { + "selectionType": "single", + "options": [ + { + "id": "reassure", + "label": "Признал ваше разочарование и успокоил" + }, + { + "id": "emotional-support", + "label": "Дал эмоциональную опору и безопасное пространство" + }, + { + "id": "take-over", + "label": "Перехватил быт/дела, чтобы вы восстановились" + }, + { + "id": "energize", + "label": "Вдохнул энергию через цель и короткий план действий" + }, + { + "id": "switch-positive", + "label": "Переключил на позитив: прогулка, кино, смешные истории" + } + ] + }, + "navigation": { + "defaultNextScreenId": "special-offer" + } + }, + { + "id": "special-offer", + "template": "coupon", + "header": { + "show": false + }, + "title": { + "text": "Тебе повезло!", + "font": "manrope", + "weight": "bold", + "align": "center" + }, + "subtitle": { + "text": "Ты получил специальную эксклюзивную скидку на 94%", + "font": "inter", + "weight": "medium", + "color": "muted", + "align": "center" + }, + "copiedMessage": "Промокод \"{code}\" скопирован!", + "coupon": { + "title": { + "text": "Special Offer", + "font": "manrope", + "weight": "bold", + "color": "primary" + }, + "offer": { + "title": { + "text": "94% OFF", + "font": "manrope", + "weight": "black", + "color": "card", + "size": "4xl" + }, + "description": { + "text": "Одноразовая эксклюзивная скидка", + "font": "inter", + "weight": "semiBold", + "color": "card" + } + }, + "promoCode": { + "text": "HAIR50", + "font": "inter", + "weight": "semiBold" + }, + "footer": { + "text": "Скопируйте или нажмите Continue", + "font": "inter", + "weight": "medium", + "color": "muted", + "size": "sm" + } + }, + "bottomActionButton": { + "text": "Continue" + } + } + ] +} diff --git a/src/app/[funnelId]/[screenId]/page.tsx b/src/app/[funnelId]/[screenId]/page.tsx new file mode 100644 index 0000000..8bf55b4 --- /dev/null +++ b/src/app/[funnelId]/[screenId]/page.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; + +import { loadFunnelDefinition } from "@/lib/funnel/loadFunnelDefinition"; +import { FunnelRuntime } from "@/components/funnel/FunnelRuntime"; + +interface FunnelScreenPageProps { + params: Promise<{ + funnelId: string; + screenId: string; + }>; +} + +export async function generateMetadata({ + params, +}: FunnelScreenPageProps): Promise { + const { funnelId } = await params; + const funnel = await loadFunnelDefinition(funnelId); + + return { + title: funnel.meta.title ?? "Funnel", + description: funnel.meta.description ?? undefined, + } satisfies Metadata; +} + +export default async function FunnelScreenPage({ + params, +}: FunnelScreenPageProps) { + const { funnelId, screenId } = await params; + const funnel = await loadFunnelDefinition(funnelId); + + const screen = funnel.screens.find((item) => item.id === screenId); + if (!screen) { + notFound(); + } + + return ; +} diff --git a/src/app/[funnelId]/page.tsx b/src/app/[funnelId]/page.tsx new file mode 100644 index 0000000..8bdfd1f --- /dev/null +++ b/src/app/[funnelId]/page.tsx @@ -0,0 +1,30 @@ +import { notFound, redirect } from "next/navigation"; + +import { loadFunnelDefinition } from "@/lib/funnel/loadFunnelDefinition"; + +interface FunnelRootPageProps { + params: { + funnelId: string; + }; +} + +export default async function FunnelRootPage({ params }: FunnelRootPageProps) { + const { funnelId } = params; + + let funnel; + try { + funnel = await loadFunnelDefinition(funnelId); + } catch (error) { + console.error(`Failed to load funnel '${funnelId}':`, error); + notFound(); + } + + const firstScreenId = + funnel.meta.firstScreenId ?? funnel.screens.at(0)?.id ?? ""; + + if (!firstScreenId) { + redirect("/"); + } + + redirect(`/${funnel.meta.id}/${firstScreenId}`); +} diff --git a/src/app/admin/funnels/builder/page.tsx b/src/app/admin/funnels/builder/page.tsx new file mode 100644 index 0000000..44b102d --- /dev/null +++ b/src/app/admin/funnels/builder/page.tsx @@ -0,0 +1,132 @@ +"use client"; + +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, + useBuilderDispatch, + useBuilderState, +} from "@/lib/admin/builder/context"; +import { + serializeBuilderState, + deserializeFunnelDefinition, +} from "@/lib/admin/builder/utils"; + +function ExportModal({ json, onClose }: { json: string; onClose: () => void }) { + return ( +
+
+
+

Экспорт JSON

+ +
+

+ Скопируйте JSON и используйте в `public/funnels/*.json`. +

+