diff --git a/src/app/admin/builder/[id]/page.tsx b/src/app/admin/builder/[id]/page.tsx index add1a35..fe82244 100644 --- a/src/app/admin/builder/[id]/page.tsx +++ b/src/app/admin/builder/[id]/page.tsx @@ -3,11 +3,13 @@ import { useEffect, useState } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { BuilderProvider } from "@/lib/admin/builder/context"; -import { BuilderUndoRedoProvider } from "@/components/admin/builder/BuilderUndoRedoProvider"; -import { BuilderTopBar } from "@/components/admin/builder/BuilderTopBar"; -import { BuilderSidebar } from "@/components/admin/builder/BuilderSidebar"; -import { BuilderCanvas } from "@/components/admin/builder/BuilderCanvas"; -import { BuilderPreview } from "@/components/admin/builder/BuilderPreview"; +import { + BuilderUndoRedoProvider, + BuilderTopBar, + BuilderSidebar, + BuilderCanvas, + BuilderPreview +} from "@/components/admin/builder"; import type { BuilderState } from '@/lib/admin/builder/context'; import type { FunnelDefinition } from '@/lib/funnel/types'; import { deserializeFunnelDefinition } from '@/lib/admin/builder/utils'; @@ -83,12 +85,7 @@ export default function FunnelBuilderPage() { nextButton: 'Далее', continueButton: 'Продолжить' }, - screens: builderState.screens.map(screen => { - // Убираем position из экрана при сохранении - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { position, ...screenWithoutPosition } = screen; - return screenWithoutPosition; - }) + screens: builderState.screens }; const response = await fetch(`/api/funnels/${funnelId}`, { @@ -139,11 +136,7 @@ export default function FunnelBuilderPage() { nextButton: 'Далее', continueButton: 'Продолжить' }, - screens: builderState.screens.map(screen => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { position, ...screenWithoutPosition } = screen; - return screenWithoutPosition; - }) + screens: builderState.screens }; await fetch(`/api/funnels/${funnelId}/history`, { diff --git a/src/app/api/funnels/[id]/route.ts b/src/app/api/funnels/[id]/route.ts index bb60df1..e993ffd 100644 --- a/src/app/api/funnels/[id]/route.ts +++ b/src/app/api/funnels/[id]/route.ts @@ -10,6 +10,8 @@ interface RouteParams { }>; } +// No normalization needed: we require `progressbars` for loaders + // GET /api/funnels/[id] - получить конкретную воронку export async function GET(request: NextRequest, { params }: RouteParams) { try { @@ -86,6 +88,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { if (status !== undefined) funnel.status = status; if (funnelData !== undefined) { + // Save as-is; schema expects `progressbars` for loaders funnel.funnelData = funnelData as FunnelDefinition; // Увеличиваем версию только при публикации @@ -111,7 +114,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { await FunnelHistoryModel.create({ funnelId: id, sessionId, - funnelSnapshot: funnelData, + funnelSnapshot: funnelData as FunnelDefinition, actionType: status === 'published' ? 'publish' : 'update', sequenceNumber: nextSequenceNumber, description: actionDescription || 'Воронка обновлена', @@ -119,7 +122,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { changeDetails: { action: 'update-funnel', previousValue: previousData, - newValue: funnelData + newValue: funnelData as FunnelDefinition } }); diff --git a/src/components/admin/builder/AgeDemo.tsx b/src/components/admin/builder/AgeDemo.tsx deleted file mode 100644 index 9dc6db1..0000000 --- a/src/components/admin/builder/AgeDemo.tsx +++ /dev/null @@ -1,166 +0,0 @@ -"use client"; - -import { AgeSelector } from "./AgeSelector"; -import { AGE_EXAMPLES, calculateAgeFromArray, getAgeGroup, getGenerationFromArray, createAgeValue, createGenerationValue } from "@/lib/age-utils"; -import { useState } from "react"; - -/** - * Демо компонент для показа возможностей системы возраста - */ -export function AgeDemo() { - const [selectedValues, setSelectedValues] = useState([]); - - const toggleValue = (value: string) => { - setSelectedValues(prev => - prev.includes(value) - ? prev.filter(v => v !== value) - : [...prev, value] - ); - }; - - const addCustomValue = (value: string) => { - if (!selectedValues.includes(value)) { - setSelectedValues(prev => [...prev, value]); - } - }; - - return ( -
-
-

🎂 Система работы с возрастом WitLab

-

- Автоматический расчет возраста и поколений из даты рождения для системы вариативности -

-
- - {/* 📖 ПРИМЕРЫ РАСЧЕТОВ */} -
-

📖 Примеры автоматических расчетов:

- - {AGE_EXAMPLES.map((example, index) => ( -
-
- {example.description} -
- - {/* Исходная дата */} -
- Дата: [{example.input.join(', ')}] ({example.input[1]}.{example.input[0]}.{example.input[2]}) -
- - {/* Рассчитанные значения */} -
-
- Возраст: {example.age} лет -
- Группа: {example.ageGroup || 'Не определена'} -
-
- Поколение: {example.generation || 'Не определено'} -
- Значения: {[ - createAgeValue(example.age), - `age-${example.age}`, - createGenerationValue(example.input[2]) - ].join(', ')} -
-
-
- ))} -
- - {/* 🎯 ИНТЕРАКТИВНЫЙ СЕЛЕКТОР */} -
-

🎯 Интерактивный селектор возраста:

- - -
- - {/* 💡 ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ */} -
-

💡 Как использовать в условиях навигации:

- -
-
- Пример 1 - Возрастные группы: -
-{`{
-  "conditions": [{
-    "screenId": "birth-date",
-    "conditionType": "values", 
-    "operator": "includesAny",
-    "values": ["22-25", "26-30"] // Молодые профессионалы
-  }]
-}`}
-            
-
- -
- Пример 2 - Поколения: -
-{`{
-  "conditions": [{
-    "screenId": "birth-date",
-    "conditionType": "values",
-    "operator": "equals", 
-    "values": ["millennials"] // Только миллениалы
-  }]
-}`}
-            
-
- -
- Пример 3 - Комбинированные условия: -
-{`{
-  "conditions": [
-    {
-      "screenId": "birth-date",
-      "conditionType": "values",
-      "operator": "includesAny",
-      "values": ["aries", "leo", "sagittarius"] // Огненные знаки
-    },
-    {
-      "screenId": "birth-date", 
-      "conditionType": "values",
-      "operator": "includesAny",
-      "values": ["22-25", "26-30"] // Молодые взрослые
-    }
-  ]
-}`}
-            
-
-
-
- - {/* 🚀 ВОЗМОЖНОСТИ СИСТЕМЫ */} -
-

🚀 Автоматические значения из даты рождения:

-
    -
  • Точный возраст: age-25, age-30, age-45
  • -
  • Возрастные группы: 18-21, 22-25, 26-30, 31-35, 36-40, 41-45, 46-50, 51-60, 60+
  • -
  • Поколения: gen-z, millennials, gen-x, boomers, silent
  • -
  • Знаки зодиака: aries, taurus, gemini, cancer, leo, virgo, libra, scorpio, sagittarius, capricorn, aquarius, pisces
  • -
  • Кастомные диапазоны: 25-35, 40+, любые пользовательские значения
  • -
-
- - {/* 📋 ТЕХНИЧЕСКИЕ ДЕТАЛИ */} -
-

📋 Как это работает технически:

-
    -
  • 1. Пользователь вводит дату: [4, 8, 1987] в date экране
  • -
  • 2. Система рассчитывает: возраст = {calculateAgeFromArray([4, 8, 1987])} лет
  • -
  • 3. Определяет группу: {getAgeGroup(calculateAgeFromArray([4, 8, 1987]))?.name}
  • -
  • 4. Определяет поколение: {getGenerationFromArray([4, 8, 1987])?.name}
  • -
  • 5. Добавляет в ответы: все вычисленные значения автоматически
  • -
  • 6. Система навигации: использует эти значения для условий
  • -
-
-
- ); -} diff --git a/src/components/admin/builder/BuilderCanvas.tsx b/src/components/admin/builder/BuilderCanvas.tsx deleted file mode 100644 index 5a835e5..0000000 --- a/src/components/admin/builder/BuilderCanvas.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @deprecated This file has been refactored into modular structure. - * Use imports from "./Canvas" instead: - * - BuilderCanvas main component - * - DropIndicator, TransitionRow, TemplateSummary, VariantSummary sub-components - * - TEMPLATE_TITLES, OPERATOR_LABELS constants - * - getOptionLabel utility - */ - -// Re-export everything from the new modular structure for backward compatibility -export { - BuilderCanvas, - DropIndicator, - TransitionRow, - TemplateSummary, - VariantSummary, - getOptionLabel, - TEMPLATE_TITLES, - OPERATOR_LABELS, -} from "./Canvas"; diff --git a/src/components/admin/builder/BuilderSidebar.tsx b/src/components/admin/builder/BuilderSidebar.tsx deleted file mode 100644 index 631f931..0000000 --- a/src/components/admin/builder/BuilderSidebar.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @deprecated This file has been refactored into modular structure. - * Use imports from "./Sidebar" instead: - * - BuilderSidebar main component - * - Section, ValidationSummary sub-components - * - isListScreen utility, ValidationIssues type - */ - -// Re-export everything from the new modular structure for backward compatibility -export { - BuilderSidebar, - Section, - ValidationSummary, - isListScreen, -} from "./Sidebar"; - -// Re-export types for backward compatibility -export type { ValidationIssues, SectionProps } from "./Sidebar"; diff --git a/src/components/admin/builder/Canvas/BuilderCanvas.tsx b/src/components/admin/builder/Canvas/BuilderCanvas.tsx index 4af268d..d8e461b 100644 --- a/src/components/admin/builder/Canvas/BuilderCanvas.tsx +++ b/src/components/admin/builder/Canvas/BuilderCanvas.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useMemo, useRef, useState } from "react"; import { ArrowDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context"; -import { AddScreenDialog } from "@/components/admin/builder/AddScreenDialog"; +import { AddScreenDialog } from "../dialogs/AddScreenDialog"; import type { ListOptionDefinition, NavigationConditionDefinition, diff --git a/src/components/admin/builder/MarkupDemo.tsx b/src/components/admin/builder/MarkupDemo.tsx deleted file mode 100644 index 1f6fe5b..0000000 --- a/src/components/admin/builder/MarkupDemo.tsx +++ /dev/null @@ -1,79 +0,0 @@ -"use client"; - -import { MarkupText, MarkupPreview } from "@/components/ui/MarkupText/MarkupText"; -import { MARKUP_EXAMPLES } from "@/lib/text-markup"; - -/** - * Демо компонент для показа возможностей системы разметки - */ -export function MarkupDemo() { - return ( -
-
-

🎨 Система разметки WitLab

-

- Используйте **двойные звездочки** для выделения текста жирным шрифтом -

-
- - {/* 📖 ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ */} -
-

📖 Примеры использования:

- - {MARKUP_EXAMPLES.map((example, index) => ( -
-
- {example.description} -
- - {/* Исходный код */} -
- {example.input} -
- - {/* Результат */} -
- Результат:{" "} - {example.input} -
-
- ))} -
- - {/* 🎯 ИНТЕРАКТИВНОЕ ДЕМО */} -
-

🎯 Интерактивное превью:

- - - - - - -
- - {/* 💡 ИНСТРУКЦИИ */} -
-

💡 Как использовать в админке:

-
    -
  • 1. Откройте любой экран в админке
  • -
  • 2. В текстовых полях используйте **двойные звездочки** для выделения
  • -
  • 3. Система автоматически покажет превью разметки
  • -
  • 4. В воронке текст будет отображаться с жирным выделением
  • -
-
- - {/* 🚀 ПОДДЕРЖИВАЕМЫЕ ЭЛЕМЕНТЫ */} -
-

🚀 Где работает разметка:

-
    -
  • ✅ Заголовки и подзаголовки всех экранов
  • -
  • ✅ Описания в Info и Soulmate экранах
  • -
  • ✅ Информационные сообщения в Date экранах
  • -
  • ✅ Лейблы и placeholder в Form экранах
  • -
  • ✅ Все текстовые поля Coupon экранов
  • -
  • ✅ Любой компонент Typography с enableMarkup
  • -
-
-
- ); -} diff --git a/src/components/admin/builder/Sidebar/BuilderSidebar.tsx b/src/components/admin/builder/Sidebar/BuilderSidebar.tsx index 4216edf..5a7d943 100644 --- a/src/components/admin/builder/Sidebar/BuilderSidebar.tsx +++ b/src/components/admin/builder/Sidebar/BuilderSidebar.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "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 { ScreenVariantsConfig } from "../forms/ScreenVariantsConfig"; import { useBuilderDispatch, useBuilderSelectedScreen, useBuilderState } from "@/lib/admin/builder/context"; import type { BuilderScreen } from "@/lib/admin/builder/types"; import type { diff --git a/src/components/admin/builder/AddScreenDialog.tsx b/src/components/admin/builder/dialogs/AddScreenDialog.tsx similarity index 100% rename from src/components/admin/builder/AddScreenDialog.tsx rename to src/components/admin/builder/dialogs/AddScreenDialog.tsx diff --git a/src/components/admin/builder/dialogs/index.ts b/src/components/admin/builder/dialogs/index.ts new file mode 100644 index 0000000..5977562 --- /dev/null +++ b/src/components/admin/builder/dialogs/index.ts @@ -0,0 +1,2 @@ +// Dialog components for builder interface +export { AddScreenDialog } from "./AddScreenDialog"; diff --git a/src/components/admin/builder/AgeSelector.tsx b/src/components/admin/builder/forms/AgeSelector.tsx similarity index 100% rename from src/components/admin/builder/AgeSelector.tsx rename to src/components/admin/builder/forms/AgeSelector.tsx diff --git a/src/components/admin/builder/EmailDomainSelector.tsx b/src/components/admin/builder/forms/EmailDomainSelector.tsx similarity index 100% rename from src/components/admin/builder/EmailDomainSelector.tsx rename to src/components/admin/builder/forms/EmailDomainSelector.tsx diff --git a/src/components/admin/builder/ScreenVariantsConfig.tsx b/src/components/admin/builder/forms/ScreenVariantsConfig.tsx similarity index 100% rename from src/components/admin/builder/ScreenVariantsConfig.tsx rename to src/components/admin/builder/forms/ScreenVariantsConfig.tsx diff --git a/src/components/admin/builder/ZodiacSelector.tsx b/src/components/admin/builder/forms/ZodiacSelector.tsx similarity index 100% rename from src/components/admin/builder/ZodiacSelector.tsx rename to src/components/admin/builder/forms/ZodiacSelector.tsx diff --git a/src/components/admin/builder/forms/index.ts b/src/components/admin/builder/forms/index.ts new file mode 100644 index 0000000..415095e --- /dev/null +++ b/src/components/admin/builder/forms/index.ts @@ -0,0 +1,5 @@ +// Form components and selectors for builder interface +export { AgeSelector } from "./AgeSelector"; +export { EmailDomainSelector } from "./EmailDomainSelector"; +export { ZodiacSelector } from "./ZodiacSelector"; +export { ScreenVariantsConfig } from "./ScreenVariantsConfig"; diff --git a/src/components/admin/builder/index.ts b/src/components/admin/builder/index.ts new file mode 100644 index 0000000..96167b9 --- /dev/null +++ b/src/components/admin/builder/index.ts @@ -0,0 +1,22 @@ +// Builder interface components organized by category + +// Layout components (main UI blocks) +export * from "./layout"; + +// Canvas components (screen flow visualization) +export * from "./Canvas"; + +// Sidebar components (screen configuration) +export * from "./Sidebar"; + +// Dialog components (modal windows) +export * from "./dialogs"; + +// Form components (selectors and configuration forms) +export * from "./forms"; + +// Template configuration components +export * from "./templates"; + +// Provider components (state management) +export * from "./providers"; diff --git a/src/components/admin/builder/BuilderPreview.tsx b/src/components/admin/builder/layout/BuilderPreview.tsx similarity index 100% rename from src/components/admin/builder/BuilderPreview.tsx rename to src/components/admin/builder/layout/BuilderPreview.tsx diff --git a/src/components/admin/builder/BuilderTopBar.tsx b/src/components/admin/builder/layout/BuilderTopBar.tsx similarity index 98% rename from src/components/admin/builder/BuilderTopBar.tsx rename to src/components/admin/builder/layout/BuilderTopBar.tsx index a794244..0fac2ae 100644 --- a/src/components/admin/builder/BuilderTopBar.tsx +++ b/src/components/admin/builder/layout/BuilderTopBar.tsx @@ -6,7 +6,7 @@ import { ArrowLeft, Save, Globe, Download, Upload, Undo, Redo } from "lucide-rea import { Button } from "@/components/ui/button"; import { serializeBuilderState, deserializeFunnelDefinition } from "@/lib/admin/builder/utils"; import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context"; -import { useBuilderUndoRedo } from "@/components/admin/builder/BuilderUndoRedoProvider"; +import { useBuilderUndoRedo } from "../providers/BuilderUndoRedoProvider"; import type { BuilderState } from "@/lib/admin/builder/context"; import { cn } from "@/lib/utils"; diff --git a/src/components/admin/builder/layout/index.ts b/src/components/admin/builder/layout/index.ts new file mode 100644 index 0000000..25db624 --- /dev/null +++ b/src/components/admin/builder/layout/index.ts @@ -0,0 +1,3 @@ +// Layout components for builder interface +export { BuilderTopBar } from "./BuilderTopBar"; +export { BuilderPreview } from "./BuilderPreview"; diff --git a/src/components/admin/builder/BuilderUndoRedoProvider.tsx b/src/components/admin/builder/providers/BuilderUndoRedoProvider.tsx similarity index 100% rename from src/components/admin/builder/BuilderUndoRedoProvider.tsx rename to src/components/admin/builder/providers/BuilderUndoRedoProvider.tsx diff --git a/src/components/admin/builder/providers/index.ts b/src/components/admin/builder/providers/index.ts new file mode 100644 index 0000000..8e6e7d4 --- /dev/null +++ b/src/components/admin/builder/providers/index.ts @@ -0,0 +1,2 @@ +// Provider components for builder state management +export { BuilderUndoRedoProvider } from "./BuilderUndoRedoProvider"; diff --git a/src/components/admin/builder/templates/CouponScreenConfig.tsx b/src/components/admin/builder/templates/CouponScreenConfig.tsx index 773cbac..105640d 100644 --- a/src/components/admin/builder/templates/CouponScreenConfig.tsx +++ b/src/components/admin/builder/templates/CouponScreenConfig.tsx @@ -10,7 +10,7 @@ interface CouponScreenConfigProps { } export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps) { - const couponScreen = screen as CouponScreenDefinition & { position: { x: number; y: number } }; + const couponScreen = screen as CouponScreenDefinition; const handleCouponUpdate = ( field: T, diff --git a/src/components/admin/builder/templates/DateScreenConfig.tsx b/src/components/admin/builder/templates/DateScreenConfig.tsx index c733367..06fc26c 100644 --- a/src/components/admin/builder/templates/DateScreenConfig.tsx +++ b/src/components/admin/builder/templates/DateScreenConfig.tsx @@ -10,7 +10,7 @@ interface DateScreenConfigProps { } export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) { - const dateScreen = screen as DateScreenDefinition & { position: { x: number; y: number } }; + const dateScreen = screen as DateScreenDefinition; const handleDateInputChange = ( field: T, diff --git a/src/components/admin/builder/templates/FormScreenConfig.tsx b/src/components/admin/builder/templates/FormScreenConfig.tsx index 124e4cd..81e26a3 100644 --- a/src/components/admin/builder/templates/FormScreenConfig.tsx +++ b/src/components/admin/builder/templates/FormScreenConfig.tsx @@ -12,7 +12,7 @@ interface FormScreenConfigProps { } export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) { - const formScreen = screen as FormScreenDefinition & { position: { x: number; y: number } }; + const formScreen = screen as FormScreenDefinition; const updateField = (index: number, updates: Partial) => { const newFields = [...(formScreen.fields || [])]; diff --git a/src/components/admin/builder/templates/InfoScreenConfig.tsx b/src/components/admin/builder/templates/InfoScreenConfig.tsx index 851d24c..53f7783 100644 --- a/src/components/admin/builder/templates/InfoScreenConfig.tsx +++ b/src/components/admin/builder/templates/InfoScreenConfig.tsx @@ -11,7 +11,7 @@ interface InfoScreenConfigProps { } export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) { - const infoScreen = screen as InfoScreenDefinition & { position: { x: number; y: number } }; + const infoScreen = screen as InfoScreenDefinition; const handleDescriptionChange = (text: string) => { onUpdate({ diff --git a/src/components/admin/builder/templates/ListScreenConfig.tsx b/src/components/admin/builder/templates/ListScreenConfig.tsx index 36f2919..08e38e0 100644 --- a/src/components/admin/builder/templates/ListScreenConfig.tsx +++ b/src/components/admin/builder/templates/ListScreenConfig.tsx @@ -25,7 +25,7 @@ function mutateOptions( } export function ListScreenConfig({ screen, onUpdate }: ListScreenConfigProps) { - const listScreen = screen as ListScreenDefinition & { position: { x: number; y: number } }; + const listScreen = screen as ListScreenDefinition; const [expandedOptions, setExpandedOptions] = useState>(new Set()); const toggleOptionExpanded = (index: number) => { diff --git a/src/components/funnel/templates/InfoTemplate.tsx b/src/components/funnel/templates/content/InfoTemplate.tsx similarity index 96% rename from src/components/funnel/templates/InfoTemplate.tsx rename to src/components/funnel/templates/content/InfoTemplate.tsx index f14d3cf..17ecb61 100644 --- a/src/components/funnel/templates/InfoTemplate.tsx +++ b/src/components/funnel/templates/content/InfoTemplate.tsx @@ -5,7 +5,7 @@ import Image from "next/image"; import Typography from "@/components/ui/Typography/Typography"; import { buildTypographyProps } from "@/lib/funnel/mappers"; import type { InfoScreenDefinition } from "@/lib/funnel/types"; -import { TemplateLayout } from "./TemplateLayout"; +import { TemplateLayout } from "../layouts/TemplateLayout"; import { cn } from "@/lib/utils"; interface InfoTemplateProps { @@ -43,11 +43,9 @@ export function InfoTemplate({ return ( +
{screen.fields.map((field) => (
@@ -142,6 +137,6 @@ export function FormTemplate({
))}
- +
); } diff --git a/src/components/funnel/templates/forms/index.ts b/src/components/funnel/templates/forms/index.ts new file mode 100644 index 0000000..47084e1 --- /dev/null +++ b/src/components/funnel/templates/forms/index.ts @@ -0,0 +1,4 @@ +// Form templates - input and data collection screens +export { FormTemplate } from "./FormTemplate"; +export { DateTemplate } from "./DateTemplate"; +export { EmailTemplate } from "./EmailTemplate"; diff --git a/src/components/funnel/templates/index.ts b/src/components/funnel/templates/index.ts new file mode 100644 index 0000000..9b9899d --- /dev/null +++ b/src/components/funnel/templates/index.ts @@ -0,0 +1,13 @@ +// Funnel templates organized by category + +// Content templates (informational and display) +export * from "./content"; + +// Form templates (input and data collection) +export * from "./forms"; + +// Interactive templates (user choice and engagement) +export * from "./interactive"; + +// Layout components (base layouts and structural) +export * from "./layouts"; diff --git a/src/components/funnel/templates/CouponTemplate.tsx b/src/components/funnel/templates/interactive/CouponTemplate.tsx similarity index 81% rename from src/components/funnel/templates/CouponTemplate.tsx rename to src/components/funnel/templates/interactive/CouponTemplate.tsx index 5c5e44a..c3e768c 100644 --- a/src/components/funnel/templates/CouponTemplate.tsx +++ b/src/components/funnel/templates/interactive/CouponTemplate.tsx @@ -2,15 +2,12 @@ import { useState } from "react"; -import { LayoutQuestion } from "@/components/layout/LayoutQuestion/LayoutQuestion"; import { Coupon } from "@/components/widgets/Coupon/Coupon"; import Typography from "@/components/ui/Typography/Typography"; -import { - buildLayoutQuestionProps, - buildTypographyProps, -} from "@/lib/funnel/mappers"; +import { buildTypographyProps } from "@/lib/funnel/mappers"; import type { CouponScreenDefinition } from "@/lib/funnel/types"; +import { TemplateLayout } from "../layouts/TemplateLayout"; interface CouponTemplateProps { screen: CouponScreenDefinition; @@ -41,20 +38,6 @@ export function CouponTemplate({ }, 2000); }; - const layoutQuestionProps = buildLayoutQuestionProps({ - screen, - titleDefaults: { font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" }, - subtitleDefaults: { font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }, - canGoBack, - onBack, - actionButtonOptions: { - defaultText: defaultTexts?.continueButton || "Continue", - disabled: false, - onClick: onContinue, - }, - screenProgress, - }); - const couponProps = { title: buildTypographyProps(screen.coupon.title, { as: "h3" as const, @@ -118,7 +101,19 @@ export function CouponTemplate({ }; return ( - +
@@ -141,6 +136,6 @@ export function CouponTemplate({
)}
-
+
); } diff --git a/src/components/funnel/templates/ListTemplate.tsx b/src/components/funnel/templates/interactive/ListTemplate.tsx similarity index 98% rename from src/components/funnel/templates/ListTemplate.tsx rename to src/components/funnel/templates/interactive/ListTemplate.tsx index de535a8..afd1cc1 100644 --- a/src/components/funnel/templates/ListTemplate.tsx +++ b/src/components/funnel/templates/interactive/ListTemplate.tsx @@ -9,7 +9,7 @@ import type { RadioAnswersListProps } from "@/components/widgets/RadioAnswersLis import type { SelectAnswersListProps } from "@/components/widgets/SelectAnswersList/SelectAnswersList"; import { mapListOptionsToButtons } from "@/lib/funnel/mappers"; import type { ListScreenDefinition } from "@/lib/funnel/types"; -import { TemplateLayout } from "./TemplateLayout"; +import { TemplateLayout } from "../layouts/TemplateLayout"; interface ListTemplateProps { screen: ListScreenDefinition; @@ -104,7 +104,6 @@ export function ListTemplate({ return ( {}} canGoBack={canGoBack} onBack={onBack} screenProgress={screenProgress} diff --git a/src/components/funnel/templates/interactive/index.ts b/src/components/funnel/templates/interactive/index.ts new file mode 100644 index 0000000..82cbcfa --- /dev/null +++ b/src/components/funnel/templates/interactive/index.ts @@ -0,0 +1,3 @@ +// Interactive templates - user choice and engagement screens +export { ListTemplate } from "./ListTemplate"; +export { CouponTemplate } from "./CouponTemplate"; diff --git a/src/components/funnel/templates/TemplateLayout.tsx b/src/components/funnel/templates/layouts/TemplateLayout.tsx similarity index 93% rename from src/components/funnel/templates/TemplateLayout.tsx rename to src/components/funnel/templates/layouts/TemplateLayout.tsx index 04e2acb..f904b15 100644 --- a/src/components/funnel/templates/TemplateLayout.tsx +++ b/src/components/funnel/templates/layouts/TemplateLayout.tsx @@ -13,11 +13,9 @@ import type { ScreenDefinition } from "@/lib/funnel/types"; interface TemplateLayoutProps { screen: ScreenDefinition; - onContinue: () => void; canGoBack: boolean; onBack: () => void; screenProgress?: { current: number; total: number }; - defaultTexts?: { nextButton?: string; continueButton?: string }; // Настройки template titleDefaults?: { @@ -53,11 +51,9 @@ interface TemplateLayoutProps { */ export function TemplateLayout({ screen, - // onContinue, // Unused in this component canGoBack, onBack, screenProgress, - // defaultTexts, // Unused in this component titleDefaults = { font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" }, subtitleDefaults = { font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }, actionButtonOptions, @@ -86,12 +82,7 @@ export function TemplateLayout({ const bottomActionButtonProps = actionButtonOptions ? buildTemplateBottomActionButtonProps({ screen, - titleDefaults, - subtitleDefaults, - canGoBack, - onBack, actionButtonOptions, - screenProgress, }) : undefined; diff --git a/src/components/funnel/templates/layouts/index.ts b/src/components/funnel/templates/layouts/index.ts new file mode 100644 index 0000000..d2f39e6 --- /dev/null +++ b/src/components/funnel/templates/layouts/index.ts @@ -0,0 +1,2 @@ +// Layout components - base layouts and structural components +export { TemplateLayout } from "./TemplateLayout"; diff --git a/src/components/widgets/Coupon/Coupon.tsx b/src/components/widgets/Coupon/Coupon.tsx index 25e1939..c14a1b4 100644 --- a/src/components/widgets/Coupon/Coupon.tsx +++ b/src/components/widgets/Coupon/Coupon.tsx @@ -60,9 +60,8 @@ function Coupon({ as="h3" size="xl" weight="bold" - color="primary" {...title} - className={cn(title.className, "leading-[140%]")} + className={cn(title.className, "leading-[140%] text-white")} /> )} {offer && ( @@ -79,18 +78,17 @@ function Coupon({ as="h3" size="4xl" weight="black" - color="card" {...offer.title} + className={cn(offer.title?.className, "text-[#1F2937]")} /> )} {offer.description && ( ): HeaderDefinition { + return { + show: true, + showBackButton: true, + ...overrides, + }; +} + +/** + * Creates default title configuration + */ +export function buildDefaultTitle(overrides?: Partial): TitleDefinition { + return { + show: true, + text: "Новый экран", + font: "manrope", + weight: "bold", + align: "left", + size: "2xl", + color: "default", + ...overrides, + }; +} + +/** + * Creates default subtitle configuration + */ +export function buildDefaultSubtitle(overrides?: Partial): SubtitleDefinition { + return { + show: true, + text: "Добавьте детали справа", + font: "manrope", + weight: "medium", + color: "default", + align: "left", + size: "lg", + ...overrides, + }; +} + +/** + * Creates default bottom action button configuration + */ +export function buildDefaultBottomActionButton(overrides?: Partial): BottomActionButtonDefinition { + return { + show: true, + text: "Продолжить", + showGradientBlur: true, + ...overrides, + }; +} + +/** + * Creates default navigation configuration + */ +export function buildDefaultNavigation(overrides?: Partial): NavigationDefinition { + return { + defaultNextScreenId: undefined, + rules: [], + ...overrides, + }; +} + +/** + * Creates default description configuration + */ +export function buildDefaultDescription(overrides?: Partial): TypographyVariant { + return { + text: "Добавьте описание для экрана", + font: "manrope", + weight: "regular", + align: "center", + size: "md", + color: "default", + ...overrides, + }; +} + +/** + * Creates default icon configuration + */ +export function buildDefaultIcon(overrides?: Partial): IconDefinition { + return { + type: "emoji", + value: "ℹ️", + size: "md", + ...overrides, + }; +} + +/** + * Creates default date input configuration + */ +export function buildDefaultDateInput(overrides?: Partial): DateInputDefinition { + return { + monthLabel: "Месяц", + dayLabel: "День", + yearLabel: "Год", + monthPlaceholder: "ММ", + dayPlaceholder: "ДД", + yearPlaceholder: "ГГГГ", + showSelectedDate: true, + selectedDateFormat: "dd MMMM yyyy", + selectedDateLabel: "Выбранная дата:", + ...overrides, + }; +} + +/** + * Creates default info message configuration + */ +export function buildDefaultInfoMessage(overrides?: Partial): InfoMessageDefinition { + return { + text: "Мы используем эту информацию только для анализа", + icon: "🔒", + font: "manrope", + weight: "regular", + align: "center", + size: "sm", + color: "muted", + ...overrides, + }; +} + +/** + * Creates default coupon configuration + */ +export function buildDefaultCoupon(overrides?: Partial): CouponDefinition { + return { + title: { + text: "Специальное предложение", + font: "manrope", + weight: "bold", + align: "center", + size: "lg", + color: "default", + }, + offer: { + title: { + text: "94% OFF", + font: "manrope", + weight: "extraBold", + align: "center", + size: "3xl", + color: "primary", + }, + description: { + text: "Полный анализ личности", + font: "manrope", + weight: "medium", + align: "center", + size: "md", + color: "default", + }, + }, + promoCode: { + text: "PROMO50", + font: "geistMono", + weight: "bold", + align: "center", + size: "lg", + color: "accent", + }, + footer: { + text: "Нажмите чтобы скопировать промокод", + font: "manrope", + weight: "regular", + align: "center", + size: "sm", + color: "muted", + }, + ...overrides, + }; +} + +/** + * Creates default form fields configuration + */ +export function buildDefaultFormFields(): FormFieldDefinition[] { + return [ + { + id: "field1", + label: "Имя", + placeholder: "Введите ваше имя", + type: "text", + required: true, + }, + ]; +} + +/** + * Creates default form validation messages + */ +export function buildDefaultFormValidation(overrides?: Partial): FormValidationMessages { + return { + required: "Это поле обязательно для заполнения", + maxLength: "Превышена максимальная длина", + invalidFormat: "Неверный формат", + ...overrides, + }; +} + +/** + * Creates default progressbars configuration with sample data from Loaders.stories.tsx + */ +export function buildDefaultProgressbars(overrides?: Partial): ProgressbarDefinition { + return { + items: [ + { + processingTitle: "Анализ твоих ответов", + processingSubtitle: "Processing...", + completedTitle: "Анализ твоих ответов", + completedSubtitle: "Complete", + }, + { + processingTitle: "Portrait of the Soulmate", + processingSubtitle: "Processing...", + completedTitle: "Portrait of the Soulmate", + completedSubtitle: "Complete", + }, + { + processingTitle: "Portrait of the Soulmate", + processingSubtitle: "Processing...", + completedTitle: "Connection Insights", + completedSubtitle: "Complete", + }, + ], + transitionDuration: 3000, + ...overrides, + }; +} + +/** + * Creates default coupon copied message + */ +export function buildDefaultCopiedMessage(): string { + return "Промокод скопирован!"; +} + +/** + * Creates default image configuration + */ +export function buildDefaultImage(overrides?: { src?: string }): { src: string } { + return { + src: "/female-portrait.jpg", + ...overrides, + }; +} + +/** + * Creates default email input configuration + */ +export function buildDefaultEmailInput(overrides?: { label?: string; placeholder?: string }): { label: string; placeholder: string } { + return { + label: "Email адрес", + placeholder: "example@email.com", + ...overrides, + }; +} diff --git a/src/lib/admin/builder/state/defaults/coupon.ts b/src/lib/admin/builder/state/defaults/coupon.ts new file mode 100644 index 0000000..dc136e8 --- /dev/null +++ b/src/lib/admin/builder/state/defaults/coupon.ts @@ -0,0 +1,73 @@ +import type { BuilderScreen } from "@/lib/admin/builder/types"; +import { + buildDefaultHeader, + buildDefaultTitle, + buildDefaultSubtitle, + buildDefaultBottomActionButton, + buildDefaultNavigation, + buildDefaultCoupon, + buildDefaultCopiedMessage +} from "./blocks"; + +export function buildCouponDefaults(id: string): BuilderScreen { + return { + id, + template: "coupon", + header: buildDefaultHeader(), + title: buildDefaultTitle({ + text: "Тебе повезло!", + align: "center", + }), + subtitle: buildDefaultSubtitle({ + text: "Ты получил специальную эксклюзивную скидку на 94%", + align: "center", + }), + coupon: buildDefaultCoupon({ + title: { + text: "Special Offer", + font: "manrope", + weight: "bold", + align: "center", + size: "lg", + color: "default", + }, + offer: { + title: { + text: "94% OFF", + font: "manrope", + weight: "bold", + align: "center", + size: "3xl", + color: "primary", + }, + description: { + text: "Одноразовая эксклюзивная скидка", + font: "inter", + weight: "medium", + color: "muted", + align: "center", + size: "md", + }, + }, + promoCode: { + text: "HAIR50", + font: "geistMono", + weight: "bold", + align: "center", + size: "lg", + color: "accent", + }, + footer: { + text: "Скопируйте или нажмите **Continue**", + font: "inter", + weight: "medium", + color: "muted", + align: "center", + size: "sm", + }, + }), + copiedMessage: buildDefaultCopiedMessage(), + bottomActionButton: buildDefaultBottomActionButton(), + navigation: buildDefaultNavigation(), + } as BuilderScreen; +} diff --git a/src/lib/admin/builder/state/defaults/date.ts b/src/lib/admin/builder/state/defaults/date.ts new file mode 100644 index 0000000..f9b054c --- /dev/null +++ b/src/lib/admin/builder/state/defaults/date.ts @@ -0,0 +1,24 @@ +import type { BuilderScreen } from "@/lib/admin/builder/types"; +import { + buildDefaultHeader, + buildDefaultTitle, + buildDefaultSubtitle, + buildDefaultBottomActionButton, + buildDefaultNavigation, + buildDefaultDateInput, + buildDefaultInfoMessage +} from "./blocks"; + +export function buildDateDefaults(id: string): BuilderScreen { + return { + id, + template: "date", + header: buildDefaultHeader(), + title: buildDefaultTitle(), + subtitle: buildDefaultSubtitle(), + dateInput: buildDefaultDateInput(), + infoMessage: buildDefaultInfoMessage(), + bottomActionButton: buildDefaultBottomActionButton(), + navigation: buildDefaultNavigation(), + } as BuilderScreen; +} diff --git a/src/lib/admin/builder/state/defaults/email.ts b/src/lib/admin/builder/state/defaults/email.ts new file mode 100644 index 0000000..aaf9a2e --- /dev/null +++ b/src/lib/admin/builder/state/defaults/email.ts @@ -0,0 +1,40 @@ +import type { BuilderScreen } from "@/lib/admin/builder/types"; +import { + buildDefaultHeader, + buildDefaultTitle, + buildDefaultSubtitle, + buildDefaultBottomActionButton, + buildDefaultNavigation, + buildDefaultImage, + buildDefaultEmailInput +} from "./blocks"; + +export function buildEmailDefaults(id: string): BuilderScreen { + return { + id, + template: "email", + header: buildDefaultHeader(), + title: buildDefaultTitle({ + text: "Портрет твоей второй половинки готов! Куда нам его отправить?", + align: "center", + }), + subtitle: buildDefaultSubtitle({ + show: false, + text: undefined, + }), + image: buildDefaultImage(), + emailInput: buildDefaultEmailInput(), + bottomActionButton: buildDefaultBottomActionButton({ + showPrivacyTermsConsent: true + }), + navigation: buildDefaultNavigation(), + variants: [ + { + conditions: [], + overrides: { + image: buildDefaultImage({ src: "/male-portrait.jpg" }), + }, + }, + ], + } as BuilderScreen; +} diff --git a/src/lib/admin/builder/state/defaults/form.ts b/src/lib/admin/builder/state/defaults/form.ts new file mode 100644 index 0000000..a42f698 --- /dev/null +++ b/src/lib/admin/builder/state/defaults/form.ts @@ -0,0 +1,24 @@ +import type { BuilderScreen } from "@/lib/admin/builder/types"; +import { + buildDefaultHeader, + buildDefaultTitle, + buildDefaultSubtitle, + buildDefaultBottomActionButton, + buildDefaultNavigation, + buildDefaultFormFields, + buildDefaultFormValidation +} from "./blocks"; + +export function buildFormDefaults(id: string): BuilderScreen { + return { + id, + template: "form", + header: buildDefaultHeader(), + title: buildDefaultTitle(), + subtitle: buildDefaultSubtitle(), + fields: buildDefaultFormFields(), + validationMessages: buildDefaultFormValidation(), + bottomActionButton: buildDefaultBottomActionButton(), + navigation: buildDefaultNavigation(), + } as BuilderScreen; +} diff --git a/src/lib/admin/builder/state/defaults/index.ts b/src/lib/admin/builder/state/defaults/index.ts new file mode 100644 index 0000000..996b129 --- /dev/null +++ b/src/lib/admin/builder/state/defaults/index.ts @@ -0,0 +1,32 @@ +// Export all building blocks functions for easy import +export { + buildDefaultHeader, + buildDefaultTitle, + buildDefaultSubtitle, + buildDefaultBottomActionButton, + buildDefaultNavigation, + buildDefaultDescription, + buildDefaultIcon, + buildDefaultDateInput, + buildDefaultInfoMessage, + buildDefaultCoupon, + buildDefaultFormFields, + buildDefaultFormValidation, + buildDefaultProgressbars, + buildDefaultCopiedMessage, + buildDefaultImage, + buildDefaultEmailInput, +} from "./blocks"; + +// Export base screen interface +export { type BaseScreenCommon } from "./baseScreen"; + +// Export specific screen builders +export { buildInfoDefaults } from "./info"; +export { buildDateDefaults } from "./date"; +export { buildFormDefaults } from "./form"; +export { buildListDefaults } from "./list"; +export { buildCouponDefaults } from "./coupon"; +export { buildEmailDefaults } from "./email"; +export { buildLoadersDefaults } from "./loaders"; +export { buildSoulmateDefaults } from "./soulmate"; diff --git a/src/lib/admin/builder/state/defaults/info.ts b/src/lib/admin/builder/state/defaults/info.ts new file mode 100644 index 0000000..cedac3b --- /dev/null +++ b/src/lib/admin/builder/state/defaults/info.ts @@ -0,0 +1,31 @@ +import type { BuilderScreen } from "@/lib/admin/builder/types"; +import { + buildDefaultHeader, + buildDefaultTitle, + buildDefaultBottomActionButton, + buildDefaultNavigation, + buildDefaultDescription, + buildDefaultSubtitle +} from "./blocks"; + +export function buildInfoDefaults(id: string): BuilderScreen { + return { + id, + template: "info", + header: buildDefaultHeader(), + title: buildDefaultTitle({ + text: "Заголовок информации", + align: "center", + }), + subtitle: buildDefaultSubtitle({ + show: false, + text: undefined, + }), + description: buildDefaultDescription({ + text: "Добавьте описание для информационного экрана. Используйте **жирный текст** для выделения важного.", + align: "center", + }), + bottomActionButton: buildDefaultBottomActionButton(), + navigation: buildDefaultNavigation(), + } as BuilderScreen; +} diff --git a/src/lib/admin/builder/state/defaults/list.ts b/src/lib/admin/builder/state/defaults/list.ts new file mode 100644 index 0000000..4beb341 --- /dev/null +++ b/src/lib/admin/builder/state/defaults/list.ts @@ -0,0 +1,33 @@ +import type { BuilderScreen } from "@/lib/admin/builder/types"; +import { + buildDefaultHeader, + buildDefaultTitle, + buildDefaultSubtitle, + buildDefaultBottomActionButton, + buildDefaultNavigation, +} from "./blocks"; + +export function buildListDefaults(id: string): BuilderScreen { + return { + id, + template: "list", + header: buildDefaultHeader(), + title: buildDefaultTitle(), + subtitle: buildDefaultSubtitle(), + list: { + selectionType: "single", + options: [ + { + id: "option-1", + label: "Вариант 1", + }, + { + id: "option-2", + label: "Вариант 2", + }, + ], + }, + bottomActionButton: buildDefaultBottomActionButton(), + navigation: buildDefaultNavigation(), + } as BuilderScreen; +} diff --git a/src/lib/admin/builder/state/defaults/loaders.ts b/src/lib/admin/builder/state/defaults/loaders.ts new file mode 100644 index 0000000..056d085 --- /dev/null +++ b/src/lib/admin/builder/state/defaults/loaders.ts @@ -0,0 +1,31 @@ +import type { BuilderScreen } from "@/lib/admin/builder/types"; +import { + buildDefaultHeader, + buildDefaultTitle, + buildDefaultSubtitle, + buildDefaultBottomActionButton, + buildDefaultNavigation, + buildDefaultProgressbars +} from "./blocks"; + +export function buildLoadersDefaults(id: string): BuilderScreen { + return { + id, + template: "loaders", + header: buildDefaultHeader({ + show: false, + showBackButton: false, + }), + title: buildDefaultTitle({ + text: "Создаем портрет твоей второй половинки.", + align: "center", + }), + subtitle: buildDefaultSubtitle({ + show: false, + text: undefined, + }), + progressbars: buildDefaultProgressbars(), + bottomActionButton: buildDefaultBottomActionButton(), + navigation: buildDefaultNavigation(), + } as BuilderScreen; +} diff --git a/src/lib/admin/builder/state/defaults/soulmate.ts b/src/lib/admin/builder/state/defaults/soulmate.ts new file mode 100644 index 0000000..0e83235 --- /dev/null +++ b/src/lib/admin/builder/state/defaults/soulmate.ts @@ -0,0 +1,30 @@ +import type { BuilderScreen } from "@/lib/admin/builder/types"; +import { + buildDefaultHeader, + buildDefaultTitle, + buildDefaultSubtitle, + buildDefaultBottomActionButton, + buildDefaultNavigation, + buildDefaultDescription +} from "./blocks"; + +export function buildSoulmateDefaults(id: string): BuilderScreen { + return { + id, + template: "soulmate", + header: buildDefaultHeader({ + show: false, + showBackButton: false, + }), + title: buildDefaultTitle(), + subtitle: buildDefaultSubtitle(), + bottomActionButton: buildDefaultBottomActionButton({ + text: "Получить полный анализ", + }), + description: buildDefaultDescription({ + text: "Ваш персональный портрет почти готов.", + align: "center", + }), + navigation: buildDefaultNavigation(), + } as BuilderScreen; +} diff --git a/src/lib/admin/builder/state/reducer.ts b/src/lib/admin/builder/state/reducer.ts index fa34e82..18d839b 100644 --- a/src/lib/admin/builder/state/reducer.ts +++ b/src/lib/admin/builder/state/reducer.ts @@ -1,5 +1,5 @@ import type { ListScreenDefinition } from "@/lib/funnel/types"; -import type { BuilderScreen, BuilderScreenPosition } from "@/lib/admin/builder/types"; +import type { BuilderScreen } from "@/lib/admin/builder/types"; import type { BuilderState, BuilderAction } from "./types"; import { INITIAL_STATE } from "./constants"; import { withDirty, generateScreenId, createScreenByTemplate } from "./utils"; @@ -18,12 +18,8 @@ export function builderReducer(state: BuilderState, action: BuilderAction): Buil case "add-screen": { const nextId = generateScreenId(state.screens.map((s) => s.id)); const template = action.payload?.template || "list"; - const position = { - x: (action.payload?.position?.x ?? 120) + state.screens.length * 40, - y: (action.payload?.position?.y ?? 120) + state.screens.length * 20, - }; - const newScreen = createScreenByTemplate(template, nextId, position); + const newScreen = createScreenByTemplate(template, nextId); // 🎯 АВТОМАТИЧЕСКОЕ СВЯЗЫВАНИЕ С ПРЕДЫДУЩИМ ЭКРАНОМ let updatedScreens = [...state.screens, newScreen]; @@ -96,11 +92,11 @@ export function builderReducer(state: BuilderState, action: BuilderAction): Buil ...(current.template === "list" && "list" in screen && screen.list ? { list: { - ...(current as ListScreenDefinition & { position: BuilderScreenPosition }).list, + ...(current as ListScreenDefinition).list, ...screen.list, options: screen.list.options ?? - (current as ListScreenDefinition & { position: BuilderScreenPosition }).list.options, + (current as ListScreenDefinition).list.options, }, } : {}), @@ -129,16 +125,6 @@ export function builderReducer(state: BuilderState, action: BuilderAction): Buil selectedScreenId: nextSelectedScreenId, }); } - case "reposition-screen": { - return withDirty(state, { - ...state, - screens: state.screens.map((screen) => - screen.id === action.payload.screenId - ? { ...screen, position: action.payload.position } - : screen - ), - }); - } case "reorder-screens": { const { fromIndex, toIndex } = action.payload; const previousScreens = state.screens; diff --git a/src/lib/admin/builder/state/types.ts b/src/lib/admin/builder/state/types.ts index 801f3e0..b1e67b6 100644 --- a/src/lib/admin/builder/state/types.ts +++ b/src/lib/admin/builder/state/types.ts @@ -14,7 +14,6 @@ export type BuilderAction = | { type: "add-screen"; payload?: { template?: ScreenDefinition["template"] } & Partial } | { type: "remove-screen"; payload: { screenId: string } } | { type: "update-screen"; payload: { screenId: string; screen: Partial } } - | { type: "reposition-screen"; payload: { screenId: string; position: BuilderScreen["position"] } } | { type: "reorder-screens"; payload: { fromIndex: number; toIndex: number } } | { type: "set-selected-screen"; payload: { screenId: string | null } } | { type: "set-screens"; payload: BuilderScreen[] } diff --git a/src/lib/admin/builder/state/utils.ts b/src/lib/admin/builder/state/utils.ts index a0e8eee..0312c28 100644 --- a/src/lib/admin/builder/state/utils.ts +++ b/src/lib/admin/builder/state/utils.ts @@ -1,6 +1,14 @@ -import type { BuilderScreen, BuilderScreenPosition } from "@/lib/admin/builder/types"; +import type { BuilderScreen } from "@/lib/admin/builder/types"; import type { ScreenDefinition } from "@/lib/funnel/types"; import type { BuilderState } from "./types"; +import { buildInfoDefaults } from "./defaults/info"; +import { buildListDefaults } from "./defaults/list"; +import { buildFormDefaults } from "./defaults/form"; +import { buildDateDefaults } from "./defaults/date"; +import { buildCouponDefaults } from "./defaults/coupon"; +import { buildEmailDefaults } from "./defaults/email"; +import { buildLoadersDefaults } from "./defaults/loaders"; +import { buildSoulmateDefaults } from "./defaults/soulmate"; /** * Marks the state as dirty if it has changed @@ -30,213 +38,25 @@ export function generateScreenId(existing: string[]): string { */ export function createScreenByTemplate( template: ScreenDefinition["template"], - id: string, - position: BuilderScreenPosition + id: string ): BuilderScreen { - // ✅ Единые базовые настройки для ВСЕХ типов экранов - const baseScreen = { - id, - position, - // ✅ Современные настройки header (без устаревшего progress) - header: { - show: true, - showBackButton: true, - }, - // ✅ Базовые тексты согласно Figma - title: { - text: "Новый экран", - font: "manrope" as const, - weight: "bold" as const, - align: "left" as const, - size: "2xl" as const, - color: "default" as const, - }, - subtitle: { - text: "Добавьте детали справа", - font: "manrope" as const, - weight: "medium" as const, - color: "default" as const, - align: "left" as const, - size: "lg" as const, - }, - // ✅ Единые настройки нижней кнопки - bottomActionButton: { - text: "Продолжить", - show: true, - }, - // ✅ Навигация - navigation: { - defaultNextScreenId: undefined, - rules: [], - }, - }; - switch (template) { case "info": - // Деструктурируем baseScreen исключая subtitle для InfoScreenDefinition - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { subtitle, ...baseScreenWithoutSubtitle } = baseScreen; - return { - ...baseScreenWithoutSubtitle, - template: "info", - title: { - text: "Заголовок информации", - font: "manrope" as const, - weight: "bold" as const, - align: "center" as const, // 🎯 Центрированный заголовок по умолчанию - size: "2xl" as const, - color: "default" as const, - }, - // 🚫 Подзаголовок не включается (InfoScreenDefinition не поддерживает subtitle) - description: { - text: "Добавьте описание для информационного экрана. Используйте **жирный текст** для выделения важного.", - align: "center" as const, // 🎯 Центрированный текст - }, - // 🚫 Иконка не добавляется по умолчанию - пользователь может добавить при необходимости - }; - + return buildInfoDefaults(id); case "list": - return { - ...baseScreen, - template: "list", - list: { - selectionType: "single" as const, - options: [ - { id: "option-1", label: "Вариант 1" }, - { id: "option-2", label: "Вариант 2" }, - ], - }, - }; - + return buildListDefaults(id); case "form": - return { - ...baseScreen, - template: "form", - fields: [ - { - id: "field-1", - type: "text", - label: "Поле 1", - placeholder: "Введите значение", - required: true, - }, - ], - }; - + return buildFormDefaults(id); case "date": - return { - ...baseScreen, - template: "date", - dateInput: { - monthLabel: "Месяц", - dayLabel: "День", - yearLabel: "Год", - monthPlaceholder: "ММ", - dayPlaceholder: "ДД", - yearPlaceholder: "ГГГГ", - showSelectedDate: true, - selectedDateFormat: "dd MMMM yyyy", - selectedDateLabel: "Выбранная дата:", - }, - infoMessage: { - text: "Мы используем эту информацию только для анализа", - icon: "🔒", - }, - }; - + return buildDateDefaults(id); case "coupon": - return { - ...baseScreen, - template: "coupon", - coupon: { - title: { - text: "Промокод на скидку", - font: "manrope" as const, - weight: "bold" as const, - }, - offer: { - title: { - text: "Скидка 20%", - font: "manrope" as const, - weight: "bold" as const, - }, - description: { - text: "На первую покупку", - font: "inter" as const, - weight: "medium" as const, - color: "muted" as const, - }, - }, - promoCode: { - text: "WELCOME20", - font: "geistMono" as const, - weight: "bold" as const, - }, - footer: { - text: "Сохраните код или скопируйте", - font: "inter" as const, - weight: "medium" as const, - color: "muted" as const, - }, - }, - copiedMessage: "Промокод {code} скопирован!", - }; - + return buildCouponDefaults(id); case "email": - return { - ...baseScreen, - template: "email", - emailInput: { - label: "Email адрес", - placeholder: "example@email.com", - }, - }; - + return buildEmailDefaults(id); case "loaders": - return { - ...baseScreen, - template: "loaders", - header: { - show: false, - showBackButton: false, - }, - progressbars: { - items: [ - { - title: "Анализ ответов", - subtitle: "Обработка данных...", - processingTitle: "Анализируем ваши ответы...", - processingSubtitle: "Это займет несколько секунд", - completedTitle: "Готово!", - completedSubtitle: "Данные проанализированы", - }, - { - title: "Создание портрета", - subtitle: "Построение результата...", - processingTitle: "Строим персональный портрет...", - processingSubtitle: "Почти готово", - completedTitle: "Готово!", - completedSubtitle: "Портрет создан", - }, - ], - transitionDuration: 3000, - }, - }; - + return buildLoadersDefaults(id); case "soulmate": - return { - ...baseScreen, - template: "soulmate", - header: { - show: false, - showBackButton: false, - }, - bottomActionButton: { - text: "Получить полный анализ", - show: true, - }, - }; - + return buildSoulmateDefaults(id); default: throw new Error(`Unknown template: ${template}`); } diff --git a/src/lib/admin/builder/templates.ts b/src/lib/admin/builder/templates.ts deleted file mode 100644 index 42c555a..0000000 --- a/src/lib/admin/builder/templates.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { BuilderScreen } from "@/lib/admin/builder/types"; -import type { ListOptionDefinition } from "@/lib/funnel/types"; - -export interface CreateTemplateScreenOptions { - templateId?: string; - screenId: string; - position: { x: number; y: number }; -} - -export interface BuilderTemplateDefinition { - id: string; - label: string; - description?: string; - create: (options: CreateTemplateScreenOptions, overrides?: Partial) => BuilderScreen; -} - -export const DEFAULT_TEMPLATE_ID = "list"; - -function cloneOptions(options: ListOptionDefinition[]): ListOptionDefinition[] { - return options.map((option) => ({ ...option })); -} - -const LIST_TEMPLATE: BuilderTemplateDefinition = { - id: "list", - label: "Вопрос с вариантами", - create: ({ screenId, position }, overrides) => { - const base = { - id: screenId, - template: "list" as const, - header: { - progress: { - current: 1, - total: 1, - label: "1 of 1", - }, - }, - title: { - text: "Новый экран", - font: "manrope" as const, - weight: "bold" as const, - }, - subtitle: { - text: "Опишите вопрос справа", - color: "muted" as const, - font: "inter" as const, - }, - list: { - selectionType: "single" as const, - options: cloneOptions([ - { id: "option-1", label: "Вариант 1" }, - { id: "option-2", label: "Вариант 2" }, - ]), - }, - navigation: { - defaultNextScreenId: undefined, - rules: [], - }, - position, - }; - - if (!overrides) { - return base as BuilderScreen; - } - - return { - ...base, - ...overrides, - list: ('list' in overrides && overrides.list) - ? { - ...base.list, - ...overrides.list, - options: overrides.list.options ?? base.list.options, - } - : base.list, - navigation: overrides.navigation - ? { - defaultNextScreenId: - overrides.navigation.defaultNextScreenId ?? base.navigation?.defaultNextScreenId, - rules: overrides.navigation.rules ?? base.navigation?.rules ?? [], - } - : base.navigation, - } as BuilderScreen; - }, -}; - -const BUILDER_TEMPLATES: BuilderTemplateDefinition[] = [LIST_TEMPLATE]; - -export function getTemplateDefinition(templateId: string): BuilderTemplateDefinition { - return BUILDER_TEMPLATES.find((template) => template.id === templateId) ?? LIST_TEMPLATE; -} - -export function createTemplateScreen( - options: CreateTemplateScreenOptions, - overrides?: Partial -): BuilderScreen { - const definition = getTemplateDefinition(options.templateId ?? DEFAULT_TEMPLATE_ID); - return definition.create(options, overrides); -} - -export function getTemplateOptions(): { id: string; label: string; description?: string }[] { - return BUILDER_TEMPLATES.map((template) => ({ - id: template.id, - label: template.label, - description: template.description, - })); -} diff --git a/src/lib/admin/builder/types.ts b/src/lib/admin/builder/types.ts index 4546e29..7775191 100644 --- a/src/lib/admin/builder/types.ts +++ b/src/lib/admin/builder/types.ts @@ -1,13 +1,6 @@ import type { FunnelDefinition, ScreenDefinition } from "@/lib/funnel/types"; -export type BuilderScreenPosition = { - x: number; - y: number; -}; - -export type BuilderScreen = ScreenDefinition & { - position: BuilderScreenPosition; -}; +export type BuilderScreen = ScreenDefinition; export interface BuilderFunnelState { meta: FunnelDefinition["meta"]; diff --git a/src/lib/admin/builder/utils.ts b/src/lib/admin/builder/utils.ts index 4de8b65..09c80be 100644 --- a/src/lib/admin/builder/utils.ts +++ b/src/lib/admin/builder/utils.ts @@ -1,5 +1,5 @@ import type { BuilderState } from "@/lib/admin/builder/context"; -import type { BuilderScreen, BuilderFunnelState, BuilderScreenPosition } from "@/lib/admin/builder/types"; +import type { BuilderScreen, BuilderFunnelState } from "@/lib/admin/builder/types"; import type { FunnelDefinition, ScreenDefinition, @@ -45,8 +45,7 @@ export function deserializeFunnelDefinition(funnel: FunnelDefinition): BuilderSt } export function serializeBuilderState(state: BuilderFunnelState): FunnelDefinition { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const screens = state.screens.map(({ position: _position, ...rest }) => rest); + const screens = state.screens; const meta: FunnelDefinition["meta"] = { ...state.meta, firstScreenId: state.meta.firstScreenId ?? state.screens[0]?.id, @@ -61,11 +60,10 @@ export function serializeBuilderState(state: BuilderFunnelState): FunnelDefiniti export function cloneScreen(screen: BuilderScreen, overrides?: Partial): BuilderScreen { const copy = { ...screen, - position: { ...screen.position }, ...(screen.template === "list" && 'list' in screen ? { list: { - ...(screen as ListScreenDefinition & { position: BuilderScreenPosition }).list, - options: (screen as ListScreenDefinition & { position: BuilderScreenPosition }).list.options.map((option) => ({ ...option })), + ...(screen as ListScreenDefinition).list, + options: (screen as ListScreenDefinition).list.options.map((option) => ({ ...option })), } } : {}), ...(Array.isArray(screen.variants) diff --git a/src/lib/funnel/mappers.tsx b/src/lib/funnel/mappers.tsx index 77ba39c..8d0c755 100644 --- a/src/lib/funnel/mappers.tsx +++ b/src/lib/funnel/mappers.tsx @@ -158,7 +158,6 @@ interface BuildLayoutQuestionOptions { subtitleDefaults?: TypographyDefaults; canGoBack: boolean; onBack: () => void; - actionButtonOptions?: BuildActionButtonOptions; screenProgress?: { current: number; total: number }; } @@ -202,21 +201,21 @@ export function buildLayoutQuestionProps( } // Отдельная функция для получения bottomActionButtonProps -export function buildTemplateBottomActionButtonProps( - options: BuildLayoutQuestionOptions -) { - const { - screen, - actionButtonOptions - } = options; +export function buildTemplateBottomActionButtonProps(options: { + screen: ScreenDefinition; + actionButtonOptions: BuildActionButtonOptions; +}) { + const { screen, actionButtonOptions } = options; - return actionButtonOptions ? buildBottomActionButtonProps( + // Наличие actionButtonOptions — явный сигнал показать кнопку. + // Принудительно включаем кнопку независимо от screen.bottomActionButton.show + return buildBottomActionButtonProps( actionButtonOptions, - // Если передаются actionButtonOptions, это означает что кнопка должна показываться - // Принудительно включаем её независимо от настроек экрана - 'bottomActionButton' in screen ? - (screen.bottomActionButton?.show === false ? { ...screen.bottomActionButton, show: true } : screen.bottomActionButton) + 'bottomActionButton' in screen + ? (screen.bottomActionButton?.show === false + ? { ...screen.bottomActionButton, show: true } + : screen.bottomActionButton) : undefined - ) : undefined; + ); } diff --git a/src/lib/funnel/screenRenderer.tsx b/src/lib/funnel/screenRenderer.tsx index 818eba2..1943116 100644 --- a/src/lib/funnel/screenRenderer.tsx +++ b/src/lib/funnel/screenRenderer.tsx @@ -2,14 +2,16 @@ import type { JSX } from "react"; -import { ListTemplate } from "@/components/funnel/templates/ListTemplate"; -import { InfoTemplate } from "@/components/funnel/templates/InfoTemplate"; -import { DateTemplate } from "@/components/funnel/templates/DateTemplate"; -import { CouponTemplate } from "@/components/funnel/templates/CouponTemplate"; -import { FormTemplate } from "@/components/funnel/templates/FormTemplate"; -import { EmailTemplate } from "@/components/funnel/templates/EmailTemplate"; -import { LoadersTemplate } from "@/components/funnel/templates/LoadersTemplate"; -import { SoulmatePortraitTemplate } from "@/components/funnel/templates/SoulmatePortraitTemplate"; +import { + ListTemplate, + InfoTemplate, + DateTemplate, + CouponTemplate, + FormTemplate, + EmailTemplate, + LoadersTemplate, + SoulmatePortraitTemplate, +} from "@/components/funnel/templates"; import type { ListScreenDefinition, DateScreenDefinition, diff --git a/src/lib/funnel/types.ts b/src/lib/funnel/types.ts index b125a7b..6e48c02 100644 --- a/src/lib/funnel/types.ts +++ b/src/lib/funnel/types.ts @@ -16,6 +16,15 @@ export type TypographyVariant = { className?: string; }; +// Extended typography for titles/subtitles with show property +export interface TitleDefinition extends TypographyVariant { + show?: boolean; // Controls whether title should be displayed. Defaults to true. +} + +export interface SubtitleDefinition extends TypographyVariant { + show?: boolean; // Controls whether subtitle should be displayed. Defaults to true. +} + export interface HeaderProgressDefinition { /** When both current and total provided, value is computed automatically (current / total * 100). */ current?: number; @@ -53,6 +62,8 @@ export interface BottomActionButtonDefinition { cornerRadius?: "3xl" | "full"; /** Controls whether PrivacyTermsConsent should be shown under the button. Defaults to false. */ showPrivacyTermsConsent?: boolean; + /** Controls whether gradient blur should be shown. Defaults to true. */ + showGradientBlur?: boolean; } export interface DefaultTexts { @@ -62,6 +73,7 @@ export interface DefaultTexts { } + export interface NavigationConditionDefinition { screenId: string; /** @@ -103,18 +115,20 @@ export interface ScreenVariantDefinition; } +export interface IconDefinition { + type: "emoji" | "image"; + value: string; + size?: "sm" | "md" | "lg" | "xl"; + className?: string; +} + export interface InfoScreenDefinition { id: string; template: "info"; header?: HeaderDefinition; - title: TypographyVariant; + title: TitleDefinition; description?: TypographyVariant; - icon?: { - type: "emoji" | "image"; - value: string; // emoji character or image URL/path - size?: "sm" | "md" | "lg" | "xl"; - className?: string; - }; + icon?: IconDefinition; bottomActionButton?: BottomActionButtonDefinition; navigation?: NavigationDefinition; variants?: ScreenVariantDefinition[]; @@ -137,16 +151,18 @@ export interface DateInputDefinition { }; } +export interface InfoMessageDefinition extends TypographyVariant { + icon?: string; +} + export interface DateScreenDefinition { id: string; template: "date"; header?: HeaderDefinition; - title: TypographyVariant; - subtitle?: TypographyVariant; + title: TitleDefinition; + subtitle?: SubtitleDefinition; dateInput: DateInputDefinition; - infoMessage?: TypographyVariant & { - icon?: string; // emoji or icon - }; + infoMessage?: InfoMessageDefinition; bottomActionButton?: BottomActionButtonDefinition; navigation?: NavigationDefinition; variants?: ScreenVariantDefinition[]; @@ -166,10 +182,10 @@ export interface CouponScreenDefinition { id: string; template: "coupon"; header?: HeaderDefinition; - title: TypographyVariant; - subtitle?: TypographyVariant; + title: TitleDefinition; + subtitle?: SubtitleDefinition; coupon: CouponDefinition; - copiedMessage?: string; // "Промокод скопирован!" text + copiedMessage?: string; bottomActionButton?: BottomActionButtonDefinition; navigation?: NavigationDefinition; variants?: ScreenVariantDefinition[]; @@ -198,8 +214,8 @@ export interface FormScreenDefinition { id: string; template: "form"; header?: HeaderDefinition; - title: TypographyVariant; - subtitle?: TypographyVariant; + title: TitleDefinition; + subtitle?: SubtitleDefinition; fields: FormFieldDefinition[]; validationMessages?: FormValidationMessages; bottomActionButton?: BottomActionButtonDefinition; @@ -212,8 +228,8 @@ export interface ListScreenDefinition { id: string; template: "list"; header?: HeaderDefinition; - title: TypographyVariant; - subtitle?: TypographyVariant; + title: TitleDefinition; + subtitle?: SubtitleDefinition; list: { selectionType: SelectionType; options: ListOptionDefinition[]; @@ -223,43 +239,48 @@ export interface ListScreenDefinition { variants?: ScreenVariantDefinition[]; } +export interface ImageDefinition { + src: string; +} + +export interface EmailInputDefinition { + placeholder?: string; + label?: string; +} + // Email Screen Definition export interface EmailScreenDefinition { id: string; template: "email"; header?: HeaderDefinition; - title: TypographyVariant; - subtitle?: TypographyVariant; - emailInput: { - placeholder?: string; - label?: string; - }; - image?: { - src: string; // Единственное настраиваемое поле - остальное зашито в коде - }; + title: TitleDefinition; + subtitle?: SubtitleDefinition; + emailInput: EmailInputDefinition; + image?: ImageDefinition; bottomActionButton?: BottomActionButtonDefinition; navigation?: NavigationDefinition; variants?: ScreenVariantDefinition[]; } -// Loaders Screen Definition +export interface ProgressbarDefinition { + items: Array<{ + title?: string; + subtitle?: string; + processingTitle?: string; + processingSubtitle?: string; + completedTitle?: string; + completedSubtitle?: string; + }>; + transitionDuration?: number; +} + export interface LoadersScreenDefinition { id: string; template: "loaders"; header?: HeaderDefinition; - title: TypographyVariant; - subtitle?: TypographyVariant; - progressbars: { - items: Array<{ - title?: string; - subtitle?: string; - processingTitle?: string; - processingSubtitle?: string; - completedTitle?: string; - completedSubtitle?: string; - }>; - transitionDuration?: number; // в миллисекундах, по умолчанию 5000 - }; + title: TitleDefinition; + subtitle?: SubtitleDefinition; + progressbars: ProgressbarDefinition; bottomActionButton?: BottomActionButtonDefinition; navigation?: NavigationDefinition; variants?: ScreenVariantDefinition[]; @@ -270,8 +291,8 @@ export interface SoulmatePortraitScreenDefinition { id: string; template: "soulmate"; header?: HeaderDefinition; - title: TypographyVariant; - subtitle?: TypographyVariant; + title: TitleDefinition; + subtitle?: SubtitleDefinition; description?: TypographyVariant; // 🎯 Настраиваемый текст описания bottomActionButton?: BottomActionButtonDefinition; navigation?: NavigationDefinition; diff --git a/src/lib/models/Funnel.ts b/src/lib/models/Funnel.ts index b7eb8f3..9dc78d6 100644 --- a/src/lib/models/Funnel.ts +++ b/src/lib/models/Funnel.ts @@ -86,12 +86,14 @@ const ListOptionDefinitionSchema = new Schema({ const NavigationConditionSchema = new Schema({ screenId: { type: String, required: true }, + conditionType: { type: String, enum: ['options', 'values'], default: 'options' }, operator: { type: String, - enum: ['includesAny', 'includesAll', 'includesExactly'], + enum: ['includesAny', 'includesAll', 'includesExactly', 'equals'], default: 'includesAny' }, - optionIds: [{ type: String, required: true }] + optionIds: [{ type: String }], + values: [{ type: String }], }, { _id: false }); const NavigationRuleSchema = new Schema({ @@ -101,7 +103,8 @@ const NavigationRuleSchema = new Schema({ const NavigationDefinitionSchema = new Schema({ rules: [NavigationRuleSchema], - defaultNextScreenId: String + defaultNextScreenId: String, + isEndScreen: { type: Boolean, default: false }, }, { _id: false }); const BottomActionButtonSchema = new Schema({ @@ -111,7 +114,8 @@ const BottomActionButtonSchema = new Schema({ type: String, enum: ['3xl', 'full'], default: '3xl' - } + }, + showPrivacyTermsConsent: { type: Boolean, default: false }, }, { _id: false }); // Схемы для различных типов экранов (используем Mixed для гибкости) @@ -146,7 +150,8 @@ const ScreenDefinitionSchema = new Schema({ }, emailInput: Schema.Types.Mixed, // email image: Schema.Types.Mixed, // email, soulmate - loadersConfig: Schema.Types.Mixed, // loaders + // loaders + progressbars: Schema.Types.Mixed, // preferred key used by runtime/templates variants: [Schema.Types.Mixed] // variants для всех типов }, { _id: false }); @@ -160,7 +165,8 @@ const FunnelMetaSchema = new Schema({ const DefaultTextsSchema = new Schema({ nextButton: { type: String, default: 'Next' }, - continueButton: { type: String, default: 'Continue' } + continueButton: { type: String, default: 'Continue' }, + privacyBanner: { type: String }, }, { _id: false }); const FunnelDataSchema = new Schema({