fix
This commit is contained in:
parent
b3eaa19fcd
commit
e98b1bfc05
@ -3,11 +3,13 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { BuilderProvider } from "@/lib/admin/builder/context";
|
import { BuilderProvider } from "@/lib/admin/builder/context";
|
||||||
import { BuilderUndoRedoProvider } from "@/components/admin/builder/BuilderUndoRedoProvider";
|
import {
|
||||||
import { BuilderTopBar } from "@/components/admin/builder/BuilderTopBar";
|
BuilderUndoRedoProvider,
|
||||||
import { BuilderSidebar } from "@/components/admin/builder/BuilderSidebar";
|
BuilderTopBar,
|
||||||
import { BuilderCanvas } from "@/components/admin/builder/BuilderCanvas";
|
BuilderSidebar,
|
||||||
import { BuilderPreview } from "@/components/admin/builder/BuilderPreview";
|
BuilderCanvas,
|
||||||
|
BuilderPreview
|
||||||
|
} from "@/components/admin/builder";
|
||||||
import type { BuilderState } from '@/lib/admin/builder/context';
|
import type { BuilderState } from '@/lib/admin/builder/context';
|
||||||
import type { FunnelDefinition } from '@/lib/funnel/types';
|
import type { FunnelDefinition } from '@/lib/funnel/types';
|
||||||
import { deserializeFunnelDefinition } from '@/lib/admin/builder/utils';
|
import { deserializeFunnelDefinition } from '@/lib/admin/builder/utils';
|
||||||
@ -83,12 +85,7 @@ export default function FunnelBuilderPage() {
|
|||||||
nextButton: 'Далее',
|
nextButton: 'Далее',
|
||||||
continueButton: 'Продолжить'
|
continueButton: 'Продолжить'
|
||||||
},
|
},
|
||||||
screens: builderState.screens.map(screen => {
|
screens: builderState.screens
|
||||||
// Убираем position из экрана при сохранении
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { position, ...screenWithoutPosition } = screen;
|
|
||||||
return screenWithoutPosition;
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(`/api/funnels/${funnelId}`, {
|
const response = await fetch(`/api/funnels/${funnelId}`, {
|
||||||
@ -139,11 +136,7 @@ export default function FunnelBuilderPage() {
|
|||||||
nextButton: 'Далее',
|
nextButton: 'Далее',
|
||||||
continueButton: 'Продолжить'
|
continueButton: 'Продолжить'
|
||||||
},
|
},
|
||||||
screens: builderState.screens.map(screen => {
|
screens: builderState.screens
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { position, ...screenWithoutPosition } = screen;
|
|
||||||
return screenWithoutPosition;
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await fetch(`/api/funnels/${funnelId}/history`, {
|
await fetch(`/api/funnels/${funnelId}/history`, {
|
||||||
|
|||||||
@ -10,6 +10,8 @@ interface RouteParams {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No normalization needed: we require `progressbars` for loaders
|
||||||
|
|
||||||
// GET /api/funnels/[id] - получить конкретную воронку
|
// GET /api/funnels/[id] - получить конкретную воронку
|
||||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||||
try {
|
try {
|
||||||
@ -86,6 +88,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
|||||||
|
|
||||||
if (status !== undefined) funnel.status = status;
|
if (status !== undefined) funnel.status = status;
|
||||||
if (funnelData !== undefined) {
|
if (funnelData !== undefined) {
|
||||||
|
// Save as-is; schema expects `progressbars` for loaders
|
||||||
funnel.funnelData = funnelData as FunnelDefinition;
|
funnel.funnelData = funnelData as FunnelDefinition;
|
||||||
|
|
||||||
// Увеличиваем версию только при публикации
|
// Увеличиваем версию только при публикации
|
||||||
@ -111,7 +114,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
|||||||
await FunnelHistoryModel.create({
|
await FunnelHistoryModel.create({
|
||||||
funnelId: id,
|
funnelId: id,
|
||||||
sessionId,
|
sessionId,
|
||||||
funnelSnapshot: funnelData,
|
funnelSnapshot: funnelData as FunnelDefinition,
|
||||||
actionType: status === 'published' ? 'publish' : 'update',
|
actionType: status === 'published' ? 'publish' : 'update',
|
||||||
sequenceNumber: nextSequenceNumber,
|
sequenceNumber: nextSequenceNumber,
|
||||||
description: actionDescription || 'Воронка обновлена',
|
description: actionDescription || 'Воронка обновлена',
|
||||||
@ -119,7 +122,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
|||||||
changeDetails: {
|
changeDetails: {
|
||||||
action: 'update-funnel',
|
action: 'update-funnel',
|
||||||
previousValue: previousData,
|
previousValue: previousData,
|
||||||
newValue: funnelData
|
newValue: funnelData as FunnelDefinition
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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<string[]>([]);
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="space-y-6 p-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h1 className="text-2xl font-bold">🎂 Система работы с возрастом WitLab</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Автоматический расчет возраста и поколений из даты рождения для системы вариативности
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 📖 ПРИМЕРЫ РАСЧЕТОВ */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold">📖 Примеры автоматических расчетов:</h2>
|
|
||||||
|
|
||||||
{AGE_EXAMPLES.map((example, index) => (
|
|
||||||
<div key={index} className="space-y-2 p-4 bg-muted/20 rounded-lg border">
|
|
||||||
<div className="text-sm font-medium text-foreground">
|
|
||||||
{example.description}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Исходная дата */}
|
|
||||||
<div className="text-xs text-muted-foreground bg-slate-100 p-2 rounded font-mono">
|
|
||||||
Дата: [{example.input.join(', ')}] ({example.input[1]}.{example.input[0]}.{example.input[2]})
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Рассчитанные значения */}
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<strong>Возраст:</strong> {example.age} лет
|
|
||||||
<br />
|
|
||||||
<strong>Группа:</strong> {example.ageGroup || 'Не определена'}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Поколение:</strong> {example.generation || 'Не определено'}
|
|
||||||
<br />
|
|
||||||
<strong>Значения:</strong> {[
|
|
||||||
createAgeValue(example.age),
|
|
||||||
`age-${example.age}`,
|
|
||||||
createGenerationValue(example.input[2])
|
|
||||||
].join(', ')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 🎯 ИНТЕРАКТИВНЫЙ СЕЛЕКТОР */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold">🎯 Интерактивный селектор возраста:</h2>
|
|
||||||
|
|
||||||
<AgeSelector
|
|
||||||
selectedValues={selectedValues}
|
|
||||||
onToggleValue={toggleValue}
|
|
||||||
onAddCustomValue={addCustomValue}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 💡 ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ */}
|
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-3">
|
|
||||||
<h3 className="text-sm font-semibold text-blue-800">💡 Как использовать в условиях навигации:</h3>
|
|
||||||
|
|
||||||
<div className="space-y-2 text-xs text-blue-700">
|
|
||||||
<div className="bg-white p-2 rounded border">
|
|
||||||
<strong>Пример 1 - Возрастные группы:</strong>
|
|
||||||
<pre className="mt-1 text-xs">
|
|
||||||
{`{
|
|
||||||
"conditions": [{
|
|
||||||
"screenId": "birth-date",
|
|
||||||
"conditionType": "values",
|
|
||||||
"operator": "includesAny",
|
|
||||||
"values": ["22-25", "26-30"] // Молодые профессионалы
|
|
||||||
}]
|
|
||||||
}`}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-2 rounded border">
|
|
||||||
<strong>Пример 2 - Поколения:</strong>
|
|
||||||
<pre className="mt-1 text-xs">
|
|
||||||
{`{
|
|
||||||
"conditions": [{
|
|
||||||
"screenId": "birth-date",
|
|
||||||
"conditionType": "values",
|
|
||||||
"operator": "equals",
|
|
||||||
"values": ["millennials"] // Только миллениалы
|
|
||||||
}]
|
|
||||||
}`}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-2 rounded border">
|
|
||||||
<strong>Пример 3 - Комбинированные условия:</strong>
|
|
||||||
<pre className="mt-1 text-xs">
|
|
||||||
{`{
|
|
||||||
"conditions": [
|
|
||||||
{
|
|
||||||
"screenId": "birth-date",
|
|
||||||
"conditionType": "values",
|
|
||||||
"operator": "includesAny",
|
|
||||||
"values": ["aries", "leo", "sagittarius"] // Огненные знаки
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"screenId": "birth-date",
|
|
||||||
"conditionType": "values",
|
|
||||||
"operator": "includesAny",
|
|
||||||
"values": ["22-25", "26-30"] // Молодые взрослые
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 🚀 ВОЗМОЖНОСТИ СИСТЕМЫ */}
|
|
||||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4 space-y-2">
|
|
||||||
<h3 className="text-sm font-semibold text-green-800">🚀 Автоматические значения из даты рождения:</h3>
|
|
||||||
<ul className="text-xs text-green-700 space-y-1">
|
|
||||||
<li>✅ <strong>Точный возраст:</strong> age-25, age-30, age-45</li>
|
|
||||||
<li>✅ <strong>Возрастные группы:</strong> 18-21, 22-25, 26-30, 31-35, 36-40, 41-45, 46-50, 51-60, 60+</li>
|
|
||||||
<li>✅ <strong>Поколения:</strong> gen-z, millennials, gen-x, boomers, silent</li>
|
|
||||||
<li>✅ <strong>Знаки зодиака:</strong> aries, taurus, gemini, cancer, leo, virgo, libra, scorpio, sagittarius, capricorn, aquarius, pisces</li>
|
|
||||||
<li>✅ <strong>Кастомные диапазоны:</strong> 25-35, 40+, любые пользовательские значения</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 📋 ТЕХНИЧЕСКИЕ ДЕТАЛИ */}
|
|
||||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 space-y-2">
|
|
||||||
<h3 className="text-sm font-semibold text-gray-800">📋 Как это работает технически:</h3>
|
|
||||||
<ul className="text-xs text-gray-700 space-y-1">
|
|
||||||
<li><strong>1. Пользователь вводит дату:</strong> [4, 8, 1987] в date экране</li>
|
|
||||||
<li><strong>2. Система рассчитывает:</strong> возраст = {calculateAgeFromArray([4, 8, 1987])} лет</li>
|
|
||||||
<li><strong>3. Определяет группу:</strong> {getAgeGroup(calculateAgeFromArray([4, 8, 1987]))?.name}</li>
|
|
||||||
<li><strong>4. Определяет поколение:</strong> {getGenerationFromArray([4, 8, 1987])?.name}</li>
|
|
||||||
<li><strong>5. Добавляет в ответы:</strong> все вычисленные значения автоматически</li>
|
|
||||||
<li><strong>6. Система навигации:</strong> использует эти значения для условий</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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";
|
|
||||||
@ -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";
|
|
||||||
@ -4,7 +4,7 @@ import React, { useCallback, useMemo, useRef, useState } from "react";
|
|||||||
import { ArrowDown } from "lucide-react";
|
import { ArrowDown } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context";
|
import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context";
|
||||||
import { AddScreenDialog } from "@/components/admin/builder/AddScreenDialog";
|
import { AddScreenDialog } from "../dialogs/AddScreenDialog";
|
||||||
import type {
|
import type {
|
||||||
ListOptionDefinition,
|
ListOptionDefinition,
|
||||||
NavigationConditionDefinition,
|
NavigationConditionDefinition,
|
||||||
|
|||||||
@ -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 (
|
|
||||||
<div className="space-y-6 p-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h1 className="text-2xl font-bold">🎨 Система разметки WitLab</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Используйте **двойные звездочки** для выделения текста жирным шрифтом
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 📖 ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold">📖 Примеры использования:</h2>
|
|
||||||
|
|
||||||
{MARKUP_EXAMPLES.map((example, index) => (
|
|
||||||
<div key={index} className="space-y-2 p-4 bg-muted/20 rounded-lg border">
|
|
||||||
<div className="text-sm font-medium text-foreground">
|
|
||||||
{example.description}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Исходный код */}
|
|
||||||
<div className="text-xs text-muted-foreground bg-slate-100 p-2 rounded font-mono">
|
|
||||||
{example.input}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Результат */}
|
|
||||||
<div className="text-sm">
|
|
||||||
<strong>Результат:</strong>{" "}
|
|
||||||
<MarkupText>{example.input}</MarkupText>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 🎯 ИНТЕРАКТИВНОЕ ДЕМО */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold">🎯 Интерактивное превью:</h2>
|
|
||||||
|
|
||||||
<MarkupPreview text="Ваш **идеальный партнер** найден! Скидка **50%** только сегодня." />
|
|
||||||
|
|
||||||
<MarkupPreview text="Добро пожаловать в **WitLab**! Система анализа совместимости нового поколения." />
|
|
||||||
|
|
||||||
<MarkupPreview text="**Анализ завершен** - переходим к результатам вашего **портрета партнера**." />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 💡 ИНСТРУКЦИИ */}
|
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-2">
|
|
||||||
<h3 className="text-sm font-semibold text-blue-800">💡 Как использовать в админке:</h3>
|
|
||||||
<ul className="text-xs text-blue-700 space-y-1">
|
|
||||||
<li>1. Откройте любой экран в админке</li>
|
|
||||||
<li>2. В текстовых полях используйте **двойные звездочки** для выделения</li>
|
|
||||||
<li>3. Система автоматически покажет превью разметки</li>
|
|
||||||
<li>4. В воронке текст будет отображаться с жирным выделением</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 🚀 ПОДДЕРЖИВАЕМЫЕ ЭЛЕМЕНТЫ */}
|
|
||||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4 space-y-2">
|
|
||||||
<h3 className="text-sm font-semibold text-green-800">🚀 Где работает разметка:</h3>
|
|
||||||
<ul className="text-xs text-green-700 space-y-1">
|
|
||||||
<li>✅ Заголовки и подзаголовки всех экранов</li>
|
|
||||||
<li>✅ Описания в Info и Soulmate экранах</li>
|
|
||||||
<li>✅ Информационные сообщения в Date экранах</li>
|
|
||||||
<li>✅ Лейблы и placeholder в Form экранах</li>
|
|
||||||
<li>✅ Все текстовые поля Coupon экранов</li>
|
|
||||||
<li>✅ Любой компонент Typography с enableMarkup</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { TemplateConfig } from "@/components/admin/builder/templates";
|
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 { useBuilderDispatch, useBuilderSelectedScreen, useBuilderState } from "@/lib/admin/builder/context";
|
||||||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
2
src/components/admin/builder/dialogs/index.ts
Normal file
2
src/components/admin/builder/dialogs/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Dialog components for builder interface
|
||||||
|
export { AddScreenDialog } from "./AddScreenDialog";
|
||||||
5
src/components/admin/builder/forms/index.ts
Normal file
5
src/components/admin/builder/forms/index.ts
Normal file
@ -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";
|
||||||
22
src/components/admin/builder/index.ts
Normal file
22
src/components/admin/builder/index.ts
Normal file
@ -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";
|
||||||
@ -6,7 +6,7 @@ import { ArrowLeft, Save, Globe, Download, Upload, Undo, Redo } from "lucide-rea
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { serializeBuilderState, deserializeFunnelDefinition } from "@/lib/admin/builder/utils";
|
import { serializeBuilderState, deserializeFunnelDefinition } from "@/lib/admin/builder/utils";
|
||||||
import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context";
|
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 type { BuilderState } from "@/lib/admin/builder/context";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
3
src/components/admin/builder/layout/index.ts
Normal file
3
src/components/admin/builder/layout/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// Layout components for builder interface
|
||||||
|
export { BuilderTopBar } from "./BuilderTopBar";
|
||||||
|
export { BuilderPreview } from "./BuilderPreview";
|
||||||
2
src/components/admin/builder/providers/index.ts
Normal file
2
src/components/admin/builder/providers/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Provider components for builder state management
|
||||||
|
export { BuilderUndoRedoProvider } from "./BuilderUndoRedoProvider";
|
||||||
@ -10,7 +10,7 @@ interface CouponScreenConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps) {
|
export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps) {
|
||||||
const couponScreen = screen as CouponScreenDefinition & { position: { x: number; y: number } };
|
const couponScreen = screen as CouponScreenDefinition;
|
||||||
|
|
||||||
const handleCouponUpdate = <T extends keyof CouponScreenDefinition["coupon"]>(
|
const handleCouponUpdate = <T extends keyof CouponScreenDefinition["coupon"]>(
|
||||||
field: T,
|
field: T,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ interface DateScreenConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||||
const dateScreen = screen as DateScreenDefinition & { position: { x: number; y: number } };
|
const dateScreen = screen as DateScreenDefinition;
|
||||||
|
|
||||||
const handleDateInputChange = <T extends keyof DateScreenDefinition["dateInput"]>(
|
const handleDateInputChange = <T extends keyof DateScreenDefinition["dateInput"]>(
|
||||||
field: T,
|
field: T,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ interface FormScreenConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FormScreenConfig({ screen, onUpdate }: 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<FormFieldDefinition>) => {
|
const updateField = (index: number, updates: Partial<FormFieldDefinition>) => {
|
||||||
const newFields = [...(formScreen.fields || [])];
|
const newFields = [...(formScreen.fields || [])];
|
||||||
|
|||||||
@ -11,7 +11,7 @@ interface InfoScreenConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function InfoScreenConfig({ screen, onUpdate }: 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) => {
|
const handleDescriptionChange = (text: string) => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
|
|||||||
@ -25,7 +25,7 @@ function mutateOptions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ListScreenConfig({ screen, onUpdate }: ListScreenConfigProps) {
|
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<Set<number>>(new Set());
|
const [expandedOptions, setExpandedOptions] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
const toggleOptionExpanded = (index: number) => {
|
const toggleOptionExpanded = (index: number) => {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import Image from "next/image";
|
|||||||
import Typography from "@/components/ui/Typography/Typography";
|
import Typography from "@/components/ui/Typography/Typography";
|
||||||
import { buildTypographyProps } from "@/lib/funnel/mappers";
|
import { buildTypographyProps } from "@/lib/funnel/mappers";
|
||||||
import type { InfoScreenDefinition } from "@/lib/funnel/types";
|
import type { InfoScreenDefinition } from "@/lib/funnel/types";
|
||||||
import { TemplateLayout } from "./TemplateLayout";
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface InfoTemplateProps {
|
interface InfoTemplateProps {
|
||||||
@ -43,11 +43,9 @@ export function InfoTemplate({
|
|||||||
return (
|
return (
|
||||||
<TemplateLayout
|
<TemplateLayout
|
||||||
screen={screen}
|
screen={screen}
|
||||||
onContinue={onContinue}
|
|
||||||
canGoBack={canGoBack}
|
canGoBack={canGoBack}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
screenProgress={screenProgress}
|
screenProgress={screenProgress}
|
||||||
defaultTexts={defaultTexts}
|
|
||||||
titleDefaults={{ font: "manrope", weight: "bold", align: "center" }}
|
titleDefaults={{ font: "manrope", weight: "bold", align: "center" }}
|
||||||
subtitleDefaults={{ font: "inter", weight: "medium", color: "muted", align: "center" }}
|
subtitleDefaults={{ font: "inter", weight: "medium", color: "muted", align: "center" }}
|
||||||
actionButtonOptions={{
|
actionButtonOptions={{
|
||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TemplateLayout } from "./TemplateLayout";
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||||
import CircularProgressbarsList from "@/components/widgets/CircularProgressbarsList/CircularProgressbarsList";
|
import CircularProgressbarsList from "@/components/widgets/CircularProgressbarsList/CircularProgressbarsList";
|
||||||
import type { LoadersScreenDefinition } from "@/lib/funnel/types";
|
import type { LoadersScreenDefinition } from "@/lib/funnel/types";
|
||||||
|
|
||||||
@ -44,11 +44,11 @@ export function LoadersTemplate({
|
|||||||
},
|
},
|
||||||
processing: typedItem.processingTitle ? {
|
processing: typedItem.processingTitle ? {
|
||||||
title: { children: typedItem.processingTitle },
|
title: { children: typedItem.processingTitle },
|
||||||
subtitle: typedItem.processingSubtitle ? { children: typedItem.processingSubtitle } : undefined,
|
text: typedItem.processingSubtitle ? { children: typedItem.processingSubtitle } : undefined,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
completed: typedItem.completedTitle ? {
|
completed: typedItem.completedTitle ? {
|
||||||
title: { children: typedItem.completedTitle },
|
title: { children: typedItem.completedTitle },
|
||||||
subtitle: typedItem.completedSubtitle ? { children: typedItem.completedSubtitle } : undefined,
|
text: typedItem.completedSubtitle ? { children: typedItem.completedSubtitle } : undefined,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
};
|
};
|
||||||
}) || [],
|
}) || [],
|
||||||
@ -59,11 +59,9 @@ export function LoadersTemplate({
|
|||||||
return (
|
return (
|
||||||
<TemplateLayout
|
<TemplateLayout
|
||||||
screen={screen}
|
screen={screen}
|
||||||
onContinue={onContinue}
|
|
||||||
canGoBack={canGoBack}
|
canGoBack={canGoBack}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
screenProgress={screenProgress}
|
screenProgress={screenProgress}
|
||||||
defaultTexts={defaultTexts}
|
|
||||||
titleDefaults={{ font: "manrope", weight: "bold", align: "center", size: "2xl", color: "default" }}
|
titleDefaults={{ font: "manrope", weight: "bold", align: "center", size: "2xl", color: "default" }}
|
||||||
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "center", size: "lg" }}
|
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "center", size: "lg" }}
|
||||||
actionButtonOptions={{
|
actionButtonOptions={{
|
||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { SoulmatePortraitScreenDefinition } from "@/lib/funnel/types";
|
import type { SoulmatePortraitScreenDefinition } from "@/lib/funnel/types";
|
||||||
import { TemplateLayout } from "./TemplateLayout";
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||||
|
|
||||||
interface SoulmatePortraitTemplateProps {
|
interface SoulmatePortraitTemplateProps {
|
||||||
screen: SoulmatePortraitScreenDefinition;
|
screen: SoulmatePortraitScreenDefinition;
|
||||||
@ -23,11 +23,9 @@ export function SoulmatePortraitTemplate({
|
|||||||
return (
|
return (
|
||||||
<TemplateLayout
|
<TemplateLayout
|
||||||
screen={screen}
|
screen={screen}
|
||||||
onContinue={onContinue}
|
|
||||||
canGoBack={canGoBack}
|
canGoBack={canGoBack}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
screenProgress={screenProgress}
|
screenProgress={screenProgress}
|
||||||
defaultTexts={defaultTexts}
|
|
||||||
titleDefaults={{ font: "manrope", weight: "bold", align: "center", size: "xl", color: "primary" }}
|
titleDefaults={{ font: "manrope", weight: "bold", align: "center", size: "xl", color: "primary" }}
|
||||||
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }}
|
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }}
|
||||||
actionButtonOptions={{
|
actionButtonOptions={{
|
||||||
4
src/components/funnel/templates/content/index.ts
Normal file
4
src/components/funnel/templates/content/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Content templates - informational and display screens
|
||||||
|
export { InfoTemplate } from "./InfoTemplate";
|
||||||
|
export { LoadersTemplate } from "./LoadersTemplate";
|
||||||
|
export { SoulmatePortraitTemplate } from "./SoulmatePortraitTemplate";
|
||||||
@ -7,7 +7,7 @@ import Typography from "@/components/ui/Typography/Typography";
|
|||||||
import { buildTypographyProps } from "@/lib/funnel/mappers";
|
import { buildTypographyProps } from "@/lib/funnel/mappers";
|
||||||
import type { DateScreenDefinition } from "@/lib/funnel/types";
|
import type { DateScreenDefinition } from "@/lib/funnel/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { TemplateLayout } from "./TemplateLayout";
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||||
|
|
||||||
interface DateTemplateProps {
|
interface DateTemplateProps {
|
||||||
screen: DateScreenDefinition;
|
screen: DateScreenDefinition;
|
||||||
@ -70,11 +70,9 @@ export function DateTemplate({
|
|||||||
return (
|
return (
|
||||||
<TemplateLayout
|
<TemplateLayout
|
||||||
screen={screen}
|
screen={screen}
|
||||||
onContinue={onContinue}
|
|
||||||
canGoBack={canGoBack}
|
canGoBack={canGoBack}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
screenProgress={screenProgress}
|
screenProgress={screenProgress}
|
||||||
defaultTexts={defaultTexts}
|
|
||||||
titleDefaults={{ font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" }}
|
titleDefaults={{ font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" }}
|
||||||
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }}
|
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }}
|
||||||
actionButtonOptions={{
|
actionButtonOptions={{
|
||||||
@ -5,7 +5,7 @@ import Image from "next/image";
|
|||||||
import PrivacySecurityBanner from "@/components/widgets/PrivacySecurityBanner/PrivacySecurityBanner";
|
import PrivacySecurityBanner from "@/components/widgets/PrivacySecurityBanner/PrivacySecurityBanner";
|
||||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||||||
import type { EmailScreenDefinition, DefaultTexts } from "@/lib/funnel/types";
|
import type { EmailScreenDefinition, DefaultTexts } from "@/lib/funnel/types";
|
||||||
import { TemplateLayout } from "./TemplateLayout";
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -34,6 +34,7 @@ export function EmailTemplate({
|
|||||||
onContinue,
|
onContinue,
|
||||||
canGoBack,
|
canGoBack,
|
||||||
onBack,
|
onBack,
|
||||||
|
screenProgress,
|
||||||
defaultTexts,
|
defaultTexts,
|
||||||
}: EmailTemplateProps) {
|
}: EmailTemplateProps) {
|
||||||
const [isTouched, setIsTouched] = useState(false);
|
const [isTouched, setIsTouched] = useState(false);
|
||||||
@ -61,11 +62,9 @@ export function EmailTemplate({
|
|||||||
return (
|
return (
|
||||||
<TemplateLayout
|
<TemplateLayout
|
||||||
screen={screen}
|
screen={screen}
|
||||||
onContinue={onContinue}
|
|
||||||
canGoBack={canGoBack}
|
canGoBack={canGoBack}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
screenProgress={undefined}
|
screenProgress={screenProgress}
|
||||||
defaultTexts={defaultTexts}
|
|
||||||
titleDefaults={{ font: "manrope", weight: "bold", align: "center", size: "2xl", color: "default" }}
|
titleDefaults={{ font: "manrope", weight: "bold", align: "center", size: "2xl", color: "default" }}
|
||||||
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "center", size: "lg" }}
|
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "center", size: "lg" }}
|
||||||
actionButtonOptions={{
|
actionButtonOptions={{
|
||||||
@ -2,13 +2,10 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { LayoutQuestion } from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
|
||||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||||||
|
|
||||||
import {
|
|
||||||
buildLayoutQuestionProps,
|
|
||||||
} from "@/lib/funnel/mappers";
|
|
||||||
import type { FormScreenDefinition } from "@/lib/funnel/types";
|
import type { FormScreenDefinition } from "@/lib/funnel/types";
|
||||||
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||||
|
|
||||||
interface FormTemplateProps {
|
interface FormTemplateProps {
|
||||||
screen: FormScreenDefinition;
|
screen: FormScreenDefinition;
|
||||||
@ -105,22 +102,20 @@ export function FormTemplate({
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
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: !isFormComplete,
|
|
||||||
onClick: handleContinue,
|
|
||||||
},
|
|
||||||
screenProgress,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutQuestion {...layoutQuestionProps}>
|
<TemplateLayout
|
||||||
|
screen={screen}
|
||||||
|
canGoBack={canGoBack}
|
||||||
|
onBack={onBack}
|
||||||
|
screenProgress={screenProgress}
|
||||||
|
titleDefaults={{ font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" }}
|
||||||
|
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }}
|
||||||
|
actionButtonOptions={{
|
||||||
|
defaultText: defaultTexts?.continueButton || "Continue",
|
||||||
|
disabled: !isFormComplete,
|
||||||
|
onClick: handleContinue,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="w-full mt-[22px] space-y-4">
|
<div className="w-full mt-[22px] space-y-4">
|
||||||
{screen.fields.map((field) => (
|
{screen.fields.map((field) => (
|
||||||
<div key={field.id}>
|
<div key={field.id}>
|
||||||
@ -142,6 +137,6 @@ export function FormTemplate({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</LayoutQuestion>
|
</TemplateLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
4
src/components/funnel/templates/forms/index.ts
Normal file
4
src/components/funnel/templates/forms/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Form templates - input and data collection screens
|
||||||
|
export { FormTemplate } from "./FormTemplate";
|
||||||
|
export { DateTemplate } from "./DateTemplate";
|
||||||
|
export { EmailTemplate } from "./EmailTemplate";
|
||||||
13
src/components/funnel/templates/index.ts
Normal file
13
src/components/funnel/templates/index.ts
Normal file
@ -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";
|
||||||
@ -2,15 +2,12 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { LayoutQuestion } from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
|
||||||
import { Coupon } from "@/components/widgets/Coupon/Coupon";
|
import { Coupon } from "@/components/widgets/Coupon/Coupon";
|
||||||
import Typography from "@/components/ui/Typography/Typography";
|
import Typography from "@/components/ui/Typography/Typography";
|
||||||
|
|
||||||
import {
|
import { buildTypographyProps } from "@/lib/funnel/mappers";
|
||||||
buildLayoutQuestionProps,
|
|
||||||
buildTypographyProps,
|
|
||||||
} from "@/lib/funnel/mappers";
|
|
||||||
import type { CouponScreenDefinition } from "@/lib/funnel/types";
|
import type { CouponScreenDefinition } from "@/lib/funnel/types";
|
||||||
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||||
|
|
||||||
interface CouponTemplateProps {
|
interface CouponTemplateProps {
|
||||||
screen: CouponScreenDefinition;
|
screen: CouponScreenDefinition;
|
||||||
@ -41,20 +38,6 @@ export function CouponTemplate({
|
|||||||
}, 2000);
|
}, 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 = {
|
const couponProps = {
|
||||||
title: buildTypographyProps(screen.coupon.title, {
|
title: buildTypographyProps(screen.coupon.title, {
|
||||||
as: "h3" as const,
|
as: "h3" as const,
|
||||||
@ -118,7 +101,19 @@ export function CouponTemplate({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutQuestion {...layoutQuestionProps}>
|
<TemplateLayout
|
||||||
|
screen={screen}
|
||||||
|
canGoBack={canGoBack}
|
||||||
|
onBack={onBack}
|
||||||
|
screenProgress={screenProgress}
|
||||||
|
titleDefaults={{ font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" }}
|
||||||
|
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }}
|
||||||
|
actionButtonOptions={{
|
||||||
|
defaultText: defaultTexts?.continueButton || "Continue",
|
||||||
|
disabled: false,
|
||||||
|
onClick: onContinue,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="w-full flex flex-col items-center justify-center mt-[22px]">
|
<div className="w-full flex flex-col items-center justify-center mt-[22px]">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<Coupon {...couponProps} />
|
<Coupon {...couponProps} />
|
||||||
@ -141,6 +136,6 @@ export function CouponTemplate({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</LayoutQuestion>
|
</TemplateLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -9,7 +9,7 @@ import type { RadioAnswersListProps } from "@/components/widgets/RadioAnswersLis
|
|||||||
import type { SelectAnswersListProps } from "@/components/widgets/SelectAnswersList/SelectAnswersList";
|
import type { SelectAnswersListProps } from "@/components/widgets/SelectAnswersList/SelectAnswersList";
|
||||||
import { mapListOptionsToButtons } from "@/lib/funnel/mappers";
|
import { mapListOptionsToButtons } from "@/lib/funnel/mappers";
|
||||||
import type { ListScreenDefinition } from "@/lib/funnel/types";
|
import type { ListScreenDefinition } from "@/lib/funnel/types";
|
||||||
import { TemplateLayout } from "./TemplateLayout";
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||||
|
|
||||||
interface ListTemplateProps {
|
interface ListTemplateProps {
|
||||||
screen: ListScreenDefinition;
|
screen: ListScreenDefinition;
|
||||||
@ -104,7 +104,6 @@ export function ListTemplate({
|
|||||||
return (
|
return (
|
||||||
<TemplateLayout
|
<TemplateLayout
|
||||||
screen={screen}
|
screen={screen}
|
||||||
onContinue={() => {}}
|
|
||||||
canGoBack={canGoBack}
|
canGoBack={canGoBack}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
screenProgress={screenProgress}
|
screenProgress={screenProgress}
|
||||||
3
src/components/funnel/templates/interactive/index.ts
Normal file
3
src/components/funnel/templates/interactive/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// Interactive templates - user choice and engagement screens
|
||||||
|
export { ListTemplate } from "./ListTemplate";
|
||||||
|
export { CouponTemplate } from "./CouponTemplate";
|
||||||
@ -13,11 +13,9 @@ import type { ScreenDefinition } from "@/lib/funnel/types";
|
|||||||
|
|
||||||
interface TemplateLayoutProps {
|
interface TemplateLayoutProps {
|
||||||
screen: ScreenDefinition;
|
screen: ScreenDefinition;
|
||||||
onContinue: () => void;
|
|
||||||
canGoBack: boolean;
|
canGoBack: boolean;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
screenProgress?: { current: number; total: number };
|
screenProgress?: { current: number; total: number };
|
||||||
defaultTexts?: { nextButton?: string; continueButton?: string };
|
|
||||||
|
|
||||||
// Настройки template
|
// Настройки template
|
||||||
titleDefaults?: {
|
titleDefaults?: {
|
||||||
@ -53,11 +51,9 @@ interface TemplateLayoutProps {
|
|||||||
*/
|
*/
|
||||||
export function TemplateLayout({
|
export function TemplateLayout({
|
||||||
screen,
|
screen,
|
||||||
// onContinue, // Unused in this component
|
|
||||||
canGoBack,
|
canGoBack,
|
||||||
onBack,
|
onBack,
|
||||||
screenProgress,
|
screenProgress,
|
||||||
// defaultTexts, // Unused in this component
|
|
||||||
titleDefaults = { font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" },
|
titleDefaults = { font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" },
|
||||||
subtitleDefaults = { font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" },
|
subtitleDefaults = { font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" },
|
||||||
actionButtonOptions,
|
actionButtonOptions,
|
||||||
@ -86,12 +82,7 @@ export function TemplateLayout({
|
|||||||
const bottomActionButtonProps = actionButtonOptions
|
const bottomActionButtonProps = actionButtonOptions
|
||||||
? buildTemplateBottomActionButtonProps({
|
? buildTemplateBottomActionButtonProps({
|
||||||
screen,
|
screen,
|
||||||
titleDefaults,
|
|
||||||
subtitleDefaults,
|
|
||||||
canGoBack,
|
|
||||||
onBack,
|
|
||||||
actionButtonOptions,
|
actionButtonOptions,
|
||||||
screenProgress,
|
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
2
src/components/funnel/templates/layouts/index.ts
Normal file
2
src/components/funnel/templates/layouts/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Layout components - base layouts and structural components
|
||||||
|
export { TemplateLayout } from "./TemplateLayout";
|
||||||
@ -60,9 +60,8 @@ function Coupon({
|
|||||||
as="h3"
|
as="h3"
|
||||||
size="xl"
|
size="xl"
|
||||||
weight="bold"
|
weight="bold"
|
||||||
color="primary"
|
|
||||||
{...title}
|
{...title}
|
||||||
className={cn(title.className, "leading-[140%]")}
|
className={cn(title.className, "leading-[140%] text-white")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{offer && (
|
{offer && (
|
||||||
@ -79,18 +78,17 @@ function Coupon({
|
|||||||
as="h3"
|
as="h3"
|
||||||
size="4xl"
|
size="4xl"
|
||||||
weight="black"
|
weight="black"
|
||||||
color="card"
|
|
||||||
{...offer.title}
|
{...offer.title}
|
||||||
|
className={cn(offer.title?.className, "text-[#1F2937]")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{offer.description && (
|
{offer.description && (
|
||||||
<Typography
|
<Typography
|
||||||
as="p"
|
as="p"
|
||||||
weight="semiBold"
|
weight="semiBold"
|
||||||
color="card"
|
|
||||||
{...offer.description}
|
{...offer.description}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-[17px] leading-[100%]",
|
"text-[17px] leading-[100%] text-[#1F2937]",
|
||||||
"mt-2",
|
"mt-2",
|
||||||
offer.description.className
|
offer.description.className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { BuilderFunnelState, BuilderScreen } from "@/lib/admin/builder/types";
|
import type { BuilderFunnelState, BuilderScreen } from "@/lib/admin/builder/types";
|
||||||
import type { BuilderState } from "./types";
|
import type { BuilderState } from "./types";
|
||||||
|
import { buildListDefaults } from "./defaults/list";
|
||||||
|
|
||||||
export const INITIAL_META: BuilderFunnelState["meta"] = {
|
export const INITIAL_META: BuilderFunnelState["meta"] = {
|
||||||
id: "funnel-builder-draft",
|
id: "funnel-builder-draft",
|
||||||
@ -9,32 +10,8 @@ export const INITIAL_META: BuilderFunnelState["meta"] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const INITIAL_SCREEN: BuilderScreen = {
|
export const INITIAL_SCREEN: BuilderScreen = {
|
||||||
id: "screen-1",
|
...buildListDefaults("screen-1"),
|
||||||
template: "list",
|
// Переопределяем опции для начального экрана
|
||||||
header: {
|
|
||||||
show: true,
|
|
||||||
showBackButton: true,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: "Новый экран",
|
|
||||||
font: "manrope",
|
|
||||||
weight: "bold",
|
|
||||||
align: "left",
|
|
||||||
size: "2xl",
|
|
||||||
color: "default",
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
text: "Добавьте детали справа",
|
|
||||||
font: "manrope",
|
|
||||||
weight: "medium",
|
|
||||||
color: "default",
|
|
||||||
align: "left",
|
|
||||||
size: "lg",
|
|
||||||
},
|
|
||||||
bottomActionButton: {
|
|
||||||
text: "Продолжить",
|
|
||||||
show: true,
|
|
||||||
},
|
|
||||||
list: {
|
list: {
|
||||||
selectionType: "single",
|
selectionType: "single",
|
||||||
options: [
|
options: [
|
||||||
@ -48,15 +25,7 @@ export const INITIAL_SCREEN: BuilderScreen = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
navigation: {
|
} as BuilderScreen;
|
||||||
defaultNextScreenId: undefined,
|
|
||||||
rules: [],
|
|
||||||
},
|
|
||||||
position: {
|
|
||||||
x: 80,
|
|
||||||
y: 120,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const INITIAL_STATE: BuilderState = {
|
export const INITIAL_STATE: BuilderState = {
|
||||||
meta: INITIAL_META,
|
meta: INITIAL_META,
|
||||||
|
|||||||
4
src/lib/admin/builder/state/defaults/baseScreen.ts
Normal file
4
src/lib/admin/builder/state/defaults/baseScreen.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Base screen interface for common properties (used internally by builders)
|
||||||
|
export interface BaseScreenCommon {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
281
src/lib/admin/builder/state/defaults/blocks.ts
Normal file
281
src/lib/admin/builder/state/defaults/blocks.ts
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
import type {
|
||||||
|
HeaderDefinition,
|
||||||
|
TitleDefinition,
|
||||||
|
SubtitleDefinition,
|
||||||
|
BottomActionButtonDefinition,
|
||||||
|
NavigationDefinition,
|
||||||
|
TypographyVariant,
|
||||||
|
IconDefinition,
|
||||||
|
DateInputDefinition,
|
||||||
|
InfoMessageDefinition,
|
||||||
|
CouponDefinition,
|
||||||
|
FormFieldDefinition,
|
||||||
|
FormValidationMessages,
|
||||||
|
ProgressbarDefinition,
|
||||||
|
} from "@/lib/funnel/types";
|
||||||
|
|
||||||
|
// ===== BUILDING BLOCK FUNCTIONS (КИРПИЧИКИ) =====
|
||||||
|
// These functions return default values for different screen components
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates default header configuration
|
||||||
|
*/
|
||||||
|
export function buildDefaultHeader(overrides?: Partial<HeaderDefinition>): HeaderDefinition {
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
showBackButton: true,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates default title configuration
|
||||||
|
*/
|
||||||
|
export function buildDefaultTitle(overrides?: Partial<TitleDefinition>): 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>): 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>): BottomActionButtonDefinition {
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
text: "Продолжить",
|
||||||
|
showGradientBlur: true,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates default navigation configuration
|
||||||
|
*/
|
||||||
|
export function buildDefaultNavigation(overrides?: Partial<NavigationDefinition>): NavigationDefinition {
|
||||||
|
return {
|
||||||
|
defaultNextScreenId: undefined,
|
||||||
|
rules: [],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates default description configuration
|
||||||
|
*/
|
||||||
|
export function buildDefaultDescription(overrides?: Partial<TypographyVariant>): TypographyVariant {
|
||||||
|
return {
|
||||||
|
text: "Добавьте описание для экрана",
|
||||||
|
font: "manrope",
|
||||||
|
weight: "regular",
|
||||||
|
align: "center",
|
||||||
|
size: "md",
|
||||||
|
color: "default",
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates default icon configuration
|
||||||
|
*/
|
||||||
|
export function buildDefaultIcon(overrides?: Partial<IconDefinition>): IconDefinition {
|
||||||
|
return {
|
||||||
|
type: "emoji",
|
||||||
|
value: "ℹ️",
|
||||||
|
size: "md",
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates default date input configuration
|
||||||
|
*/
|
||||||
|
export function buildDefaultDateInput(overrides?: Partial<DateInputDefinition>): 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>): 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>): 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>): FormValidationMessages {
|
||||||
|
return {
|
||||||
|
required: "Это поле обязательно для заполнения",
|
||||||
|
maxLength: "Превышена максимальная длина",
|
||||||
|
invalidFormat: "Неверный формат",
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates default progressbars configuration with sample data from Loaders.stories.tsx
|
||||||
|
*/
|
||||||
|
export function buildDefaultProgressbars(overrides?: Partial<ProgressbarDefinition>): 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
73
src/lib/admin/builder/state/defaults/coupon.ts
Normal file
73
src/lib/admin/builder/state/defaults/coupon.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
24
src/lib/admin/builder/state/defaults/date.ts
Normal file
24
src/lib/admin/builder/state/defaults/date.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
40
src/lib/admin/builder/state/defaults/email.ts
Normal file
40
src/lib/admin/builder/state/defaults/email.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
24
src/lib/admin/builder/state/defaults/form.ts
Normal file
24
src/lib/admin/builder/state/defaults/form.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
32
src/lib/admin/builder/state/defaults/index.ts
Normal file
32
src/lib/admin/builder/state/defaults/index.ts
Normal file
@ -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";
|
||||||
31
src/lib/admin/builder/state/defaults/info.ts
Normal file
31
src/lib/admin/builder/state/defaults/info.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
33
src/lib/admin/builder/state/defaults/list.ts
Normal file
33
src/lib/admin/builder/state/defaults/list.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
31
src/lib/admin/builder/state/defaults/loaders.ts
Normal file
31
src/lib/admin/builder/state/defaults/loaders.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
30
src/lib/admin/builder/state/defaults/soulmate.ts
Normal file
30
src/lib/admin/builder/state/defaults/soulmate.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ListScreenDefinition } from "@/lib/funnel/types";
|
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 type { BuilderState, BuilderAction } from "./types";
|
||||||
import { INITIAL_STATE } from "./constants";
|
import { INITIAL_STATE } from "./constants";
|
||||||
import { withDirty, generateScreenId, createScreenByTemplate } from "./utils";
|
import { withDirty, generateScreenId, createScreenByTemplate } from "./utils";
|
||||||
@ -18,12 +18,8 @@ export function builderReducer(state: BuilderState, action: BuilderAction): Buil
|
|||||||
case "add-screen": {
|
case "add-screen": {
|
||||||
const nextId = generateScreenId(state.screens.map((s) => s.id));
|
const nextId = generateScreenId(state.screens.map((s) => s.id));
|
||||||
const template = action.payload?.template || "list";
|
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];
|
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
|
...(current.template === "list" && "list" in screen && screen.list
|
||||||
? {
|
? {
|
||||||
list: {
|
list: {
|
||||||
...(current as ListScreenDefinition & { position: BuilderScreenPosition }).list,
|
...(current as ListScreenDefinition).list,
|
||||||
...screen.list,
|
...screen.list,
|
||||||
options:
|
options:
|
||||||
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,
|
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": {
|
case "reorder-screens": {
|
||||||
const { fromIndex, toIndex } = action.payload;
|
const { fromIndex, toIndex } = action.payload;
|
||||||
const previousScreens = state.screens;
|
const previousScreens = state.screens;
|
||||||
|
|||||||
@ -14,7 +14,6 @@ export type BuilderAction =
|
|||||||
| { type: "add-screen"; payload?: { template?: ScreenDefinition["template"] } & Partial<BuilderScreen> }
|
| { type: "add-screen"; payload?: { template?: ScreenDefinition["template"] } & Partial<BuilderScreen> }
|
||||||
| { type: "remove-screen"; payload: { screenId: string } }
|
| { type: "remove-screen"; payload: { screenId: string } }
|
||||||
| { type: "update-screen"; payload: { screenId: string; screen: Partial<BuilderScreen> } }
|
| { type: "update-screen"; payload: { screenId: string; screen: Partial<BuilderScreen> } }
|
||||||
| { type: "reposition-screen"; payload: { screenId: string; position: BuilderScreen["position"] } }
|
|
||||||
| { type: "reorder-screens"; payload: { fromIndex: number; toIndex: number } }
|
| { type: "reorder-screens"; payload: { fromIndex: number; toIndex: number } }
|
||||||
| { type: "set-selected-screen"; payload: { screenId: string | null } }
|
| { type: "set-selected-screen"; payload: { screenId: string | null } }
|
||||||
| { type: "set-screens"; payload: BuilderScreen[] }
|
| { type: "set-screens"; payload: BuilderScreen[] }
|
||||||
|
|||||||
@ -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 { ScreenDefinition } from "@/lib/funnel/types";
|
||||||
import type { BuilderState } from "./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
|
* Marks the state as dirty if it has changed
|
||||||
@ -30,213 +38,25 @@ export function generateScreenId(existing: string[]): string {
|
|||||||
*/
|
*/
|
||||||
export function createScreenByTemplate(
|
export function createScreenByTemplate(
|
||||||
template: ScreenDefinition["template"],
|
template: ScreenDefinition["template"],
|
||||||
id: string,
|
id: string
|
||||||
position: BuilderScreenPosition
|
|
||||||
): BuilderScreen {
|
): 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) {
|
switch (template) {
|
||||||
case "info":
|
case "info":
|
||||||
// Деструктурируем baseScreen исключая subtitle для InfoScreenDefinition
|
return buildInfoDefaults(id);
|
||||||
// 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, // 🎯 Центрированный текст
|
|
||||||
},
|
|
||||||
// 🚫 Иконка не добавляется по умолчанию - пользователь может добавить при необходимости
|
|
||||||
};
|
|
||||||
|
|
||||||
case "list":
|
case "list":
|
||||||
return {
|
return buildListDefaults(id);
|
||||||
...baseScreen,
|
|
||||||
template: "list",
|
|
||||||
list: {
|
|
||||||
selectionType: "single" as const,
|
|
||||||
options: [
|
|
||||||
{ id: "option-1", label: "Вариант 1" },
|
|
||||||
{ id: "option-2", label: "Вариант 2" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
case "form":
|
case "form":
|
||||||
return {
|
return buildFormDefaults(id);
|
||||||
...baseScreen,
|
|
||||||
template: "form",
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
id: "field-1",
|
|
||||||
type: "text",
|
|
||||||
label: "Поле 1",
|
|
||||||
placeholder: "Введите значение",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
case "date":
|
case "date":
|
||||||
return {
|
return buildDateDefaults(id);
|
||||||
...baseScreen,
|
|
||||||
template: "date",
|
|
||||||
dateInput: {
|
|
||||||
monthLabel: "Месяц",
|
|
||||||
dayLabel: "День",
|
|
||||||
yearLabel: "Год",
|
|
||||||
monthPlaceholder: "ММ",
|
|
||||||
dayPlaceholder: "ДД",
|
|
||||||
yearPlaceholder: "ГГГГ",
|
|
||||||
showSelectedDate: true,
|
|
||||||
selectedDateFormat: "dd MMMM yyyy",
|
|
||||||
selectedDateLabel: "Выбранная дата:",
|
|
||||||
},
|
|
||||||
infoMessage: {
|
|
||||||
text: "Мы используем эту информацию только для анализа",
|
|
||||||
icon: "🔒",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
case "coupon":
|
case "coupon":
|
||||||
return {
|
return buildCouponDefaults(id);
|
||||||
...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} скопирован!",
|
|
||||||
};
|
|
||||||
|
|
||||||
case "email":
|
case "email":
|
||||||
return {
|
return buildEmailDefaults(id);
|
||||||
...baseScreen,
|
|
||||||
template: "email",
|
|
||||||
emailInput: {
|
|
||||||
label: "Email адрес",
|
|
||||||
placeholder: "example@email.com",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
case "loaders":
|
case "loaders":
|
||||||
return {
|
return buildLoadersDefaults(id);
|
||||||
...baseScreen,
|
|
||||||
template: "loaders",
|
|
||||||
header: {
|
|
||||||
show: false,
|
|
||||||
showBackButton: false,
|
|
||||||
},
|
|
||||||
progressbars: {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Анализ ответов",
|
|
||||||
subtitle: "Обработка данных...",
|
|
||||||
processingTitle: "Анализируем ваши ответы...",
|
|
||||||
processingSubtitle: "Это займет несколько секунд",
|
|
||||||
completedTitle: "Готово!",
|
|
||||||
completedSubtitle: "Данные проанализированы",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Создание портрета",
|
|
||||||
subtitle: "Построение результата...",
|
|
||||||
processingTitle: "Строим персональный портрет...",
|
|
||||||
processingSubtitle: "Почти готово",
|
|
||||||
completedTitle: "Готово!",
|
|
||||||
completedSubtitle: "Портрет создан",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
transitionDuration: 3000,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
case "soulmate":
|
case "soulmate":
|
||||||
return {
|
return buildSoulmateDefaults(id);
|
||||||
...baseScreen,
|
|
||||||
template: "soulmate",
|
|
||||||
header: {
|
|
||||||
show: false,
|
|
||||||
showBackButton: false,
|
|
||||||
},
|
|
||||||
bottomActionButton: {
|
|
||||||
text: "Получить полный анализ",
|
|
||||||
show: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown template: ${template}`);
|
throw new Error(`Unknown template: ${template}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>) => 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>
|
|
||||||
): 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,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
@ -1,13 +1,6 @@
|
|||||||
import type { FunnelDefinition, ScreenDefinition } from "@/lib/funnel/types";
|
import type { FunnelDefinition, ScreenDefinition } from "@/lib/funnel/types";
|
||||||
|
|
||||||
export type BuilderScreenPosition = {
|
export type BuilderScreen = ScreenDefinition;
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BuilderScreen = ScreenDefinition & {
|
|
||||||
position: BuilderScreenPosition;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface BuilderFunnelState {
|
export interface BuilderFunnelState {
|
||||||
meta: FunnelDefinition["meta"];
|
meta: FunnelDefinition["meta"];
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { BuilderState } from "@/lib/admin/builder/context";
|
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 {
|
import type {
|
||||||
FunnelDefinition,
|
FunnelDefinition,
|
||||||
ScreenDefinition,
|
ScreenDefinition,
|
||||||
@ -45,8 +45,7 @@ export function deserializeFunnelDefinition(funnel: FunnelDefinition): BuilderSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function serializeBuilderState(state: BuilderFunnelState): FunnelDefinition {
|
export function serializeBuilderState(state: BuilderFunnelState): FunnelDefinition {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const screens = state.screens;
|
||||||
const screens = state.screens.map(({ position: _position, ...rest }) => rest);
|
|
||||||
const meta: FunnelDefinition["meta"] = {
|
const meta: FunnelDefinition["meta"] = {
|
||||||
...state.meta,
|
...state.meta,
|
||||||
firstScreenId: state.meta.firstScreenId ?? state.screens[0]?.id,
|
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>): BuilderScreen {
|
export function cloneScreen(screen: BuilderScreen, overrides?: Partial<BuilderScreen>): BuilderScreen {
|
||||||
const copy = {
|
const copy = {
|
||||||
...screen,
|
...screen,
|
||||||
position: { ...screen.position },
|
|
||||||
...(screen.template === "list" && 'list' in screen ? {
|
...(screen.template === "list" && 'list' in screen ? {
|
||||||
list: {
|
list: {
|
||||||
...(screen as ListScreenDefinition & { position: BuilderScreenPosition }).list,
|
...(screen as ListScreenDefinition).list,
|
||||||
options: (screen as ListScreenDefinition & { position: BuilderScreenPosition }).list.options.map((option) => ({ ...option })),
|
options: (screen as ListScreenDefinition).list.options.map((option) => ({ ...option })),
|
||||||
}
|
}
|
||||||
} : {}),
|
} : {}),
|
||||||
...(Array.isArray(screen.variants)
|
...(Array.isArray(screen.variants)
|
||||||
|
|||||||
@ -158,7 +158,6 @@ interface BuildLayoutQuestionOptions {
|
|||||||
subtitleDefaults?: TypographyDefaults;
|
subtitleDefaults?: TypographyDefaults;
|
||||||
canGoBack: boolean;
|
canGoBack: boolean;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
actionButtonOptions?: BuildActionButtonOptions;
|
|
||||||
screenProgress?: { current: number; total: number };
|
screenProgress?: { current: number; total: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,21 +201,21 @@ export function buildLayoutQuestionProps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Отдельная функция для получения bottomActionButtonProps
|
// Отдельная функция для получения bottomActionButtonProps
|
||||||
export function buildTemplateBottomActionButtonProps(
|
export function buildTemplateBottomActionButtonProps(options: {
|
||||||
options: BuildLayoutQuestionOptions
|
screen: ScreenDefinition;
|
||||||
) {
|
actionButtonOptions: BuildActionButtonOptions;
|
||||||
const {
|
}) {
|
||||||
screen,
|
const { screen, actionButtonOptions } = options;
|
||||||
actionButtonOptions
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
return actionButtonOptions ? buildBottomActionButtonProps(
|
// Наличие actionButtonOptions — явный сигнал показать кнопку.
|
||||||
|
// Принудительно включаем кнопку независимо от screen.bottomActionButton.show
|
||||||
|
return buildBottomActionButtonProps(
|
||||||
actionButtonOptions,
|
actionButtonOptions,
|
||||||
// Если передаются actionButtonOptions, это означает что кнопка должна показываться
|
'bottomActionButton' in screen
|
||||||
// Принудительно включаем её независимо от настроек экрана
|
? (screen.bottomActionButton?.show === false
|
||||||
'bottomActionButton' in screen ?
|
? { ...screen.bottomActionButton, show: true }
|
||||||
(screen.bottomActionButton?.show === false ? { ...screen.bottomActionButton, show: true } : screen.bottomActionButton)
|
: screen.bottomActionButton)
|
||||||
: undefined
|
: undefined
|
||||||
) : undefined;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
|
|
||||||
import { ListTemplate } from "@/components/funnel/templates/ListTemplate";
|
import {
|
||||||
import { InfoTemplate } from "@/components/funnel/templates/InfoTemplate";
|
ListTemplate,
|
||||||
import { DateTemplate } from "@/components/funnel/templates/DateTemplate";
|
InfoTemplate,
|
||||||
import { CouponTemplate } from "@/components/funnel/templates/CouponTemplate";
|
DateTemplate,
|
||||||
import { FormTemplate } from "@/components/funnel/templates/FormTemplate";
|
CouponTemplate,
|
||||||
import { EmailTemplate } from "@/components/funnel/templates/EmailTemplate";
|
FormTemplate,
|
||||||
import { LoadersTemplate } from "@/components/funnel/templates/LoadersTemplate";
|
EmailTemplate,
|
||||||
import { SoulmatePortraitTemplate } from "@/components/funnel/templates/SoulmatePortraitTemplate";
|
LoadersTemplate,
|
||||||
|
SoulmatePortraitTemplate,
|
||||||
|
} from "@/components/funnel/templates";
|
||||||
import type {
|
import type {
|
||||||
ListScreenDefinition,
|
ListScreenDefinition,
|
||||||
DateScreenDefinition,
|
DateScreenDefinition,
|
||||||
|
|||||||
@ -16,6 +16,15 @@ export type TypographyVariant = {
|
|||||||
className?: string;
|
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 {
|
export interface HeaderProgressDefinition {
|
||||||
/** When both current and total provided, value is computed automatically (current / total * 100). */
|
/** When both current and total provided, value is computed automatically (current / total * 100). */
|
||||||
current?: number;
|
current?: number;
|
||||||
@ -53,6 +62,8 @@ export interface BottomActionButtonDefinition {
|
|||||||
cornerRadius?: "3xl" | "full";
|
cornerRadius?: "3xl" | "full";
|
||||||
/** Controls whether PrivacyTermsConsent should be shown under the button. Defaults to false. */
|
/** Controls whether PrivacyTermsConsent should be shown under the button. Defaults to false. */
|
||||||
showPrivacyTermsConsent?: boolean;
|
showPrivacyTermsConsent?: boolean;
|
||||||
|
/** Controls whether gradient blur should be shown. Defaults to true. */
|
||||||
|
showGradientBlur?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DefaultTexts {
|
export interface DefaultTexts {
|
||||||
@ -62,6 +73,7 @@ export interface DefaultTexts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface NavigationConditionDefinition {
|
export interface NavigationConditionDefinition {
|
||||||
screenId: string;
|
screenId: string;
|
||||||
/**
|
/**
|
||||||
@ -103,18 +115,20 @@ export interface ScreenVariantDefinition<T extends { id: string; template: strin
|
|||||||
overrides: ScreenVariantOverrides<T>;
|
overrides: ScreenVariantOverrides<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IconDefinition {
|
||||||
|
type: "emoji" | "image";
|
||||||
|
value: string;
|
||||||
|
size?: "sm" | "md" | "lg" | "xl";
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InfoScreenDefinition {
|
export interface InfoScreenDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
template: "info";
|
template: "info";
|
||||||
header?: HeaderDefinition;
|
header?: HeaderDefinition;
|
||||||
title: TypographyVariant;
|
title: TitleDefinition;
|
||||||
description?: TypographyVariant;
|
description?: TypographyVariant;
|
||||||
icon?: {
|
icon?: IconDefinition;
|
||||||
type: "emoji" | "image";
|
|
||||||
value: string; // emoji character or image URL/path
|
|
||||||
size?: "sm" | "md" | "lg" | "xl";
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
bottomActionButton?: BottomActionButtonDefinition;
|
bottomActionButton?: BottomActionButtonDefinition;
|
||||||
navigation?: NavigationDefinition;
|
navigation?: NavigationDefinition;
|
||||||
variants?: ScreenVariantDefinition<InfoScreenDefinition>[];
|
variants?: ScreenVariantDefinition<InfoScreenDefinition>[];
|
||||||
@ -137,16 +151,18 @@ export interface DateInputDefinition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InfoMessageDefinition extends TypographyVariant {
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DateScreenDefinition {
|
export interface DateScreenDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
template: "date";
|
template: "date";
|
||||||
header?: HeaderDefinition;
|
header?: HeaderDefinition;
|
||||||
title: TypographyVariant;
|
title: TitleDefinition;
|
||||||
subtitle?: TypographyVariant;
|
subtitle?: SubtitleDefinition;
|
||||||
dateInput: DateInputDefinition;
|
dateInput: DateInputDefinition;
|
||||||
infoMessage?: TypographyVariant & {
|
infoMessage?: InfoMessageDefinition;
|
||||||
icon?: string; // emoji or icon
|
|
||||||
};
|
|
||||||
bottomActionButton?: BottomActionButtonDefinition;
|
bottomActionButton?: BottomActionButtonDefinition;
|
||||||
navigation?: NavigationDefinition;
|
navigation?: NavigationDefinition;
|
||||||
variants?: ScreenVariantDefinition<DateScreenDefinition>[];
|
variants?: ScreenVariantDefinition<DateScreenDefinition>[];
|
||||||
@ -166,10 +182,10 @@ export interface CouponScreenDefinition {
|
|||||||
id: string;
|
id: string;
|
||||||
template: "coupon";
|
template: "coupon";
|
||||||
header?: HeaderDefinition;
|
header?: HeaderDefinition;
|
||||||
title: TypographyVariant;
|
title: TitleDefinition;
|
||||||
subtitle?: TypographyVariant;
|
subtitle?: SubtitleDefinition;
|
||||||
coupon: CouponDefinition;
|
coupon: CouponDefinition;
|
||||||
copiedMessage?: string; // "Промокод скопирован!" text
|
copiedMessage?: string;
|
||||||
bottomActionButton?: BottomActionButtonDefinition;
|
bottomActionButton?: BottomActionButtonDefinition;
|
||||||
navigation?: NavigationDefinition;
|
navigation?: NavigationDefinition;
|
||||||
variants?: ScreenVariantDefinition<CouponScreenDefinition>[];
|
variants?: ScreenVariantDefinition<CouponScreenDefinition>[];
|
||||||
@ -198,8 +214,8 @@ export interface FormScreenDefinition {
|
|||||||
id: string;
|
id: string;
|
||||||
template: "form";
|
template: "form";
|
||||||
header?: HeaderDefinition;
|
header?: HeaderDefinition;
|
||||||
title: TypographyVariant;
|
title: TitleDefinition;
|
||||||
subtitle?: TypographyVariant;
|
subtitle?: SubtitleDefinition;
|
||||||
fields: FormFieldDefinition[];
|
fields: FormFieldDefinition[];
|
||||||
validationMessages?: FormValidationMessages;
|
validationMessages?: FormValidationMessages;
|
||||||
bottomActionButton?: BottomActionButtonDefinition;
|
bottomActionButton?: BottomActionButtonDefinition;
|
||||||
@ -212,8 +228,8 @@ export interface ListScreenDefinition {
|
|||||||
id: string;
|
id: string;
|
||||||
template: "list";
|
template: "list";
|
||||||
header?: HeaderDefinition;
|
header?: HeaderDefinition;
|
||||||
title: TypographyVariant;
|
title: TitleDefinition;
|
||||||
subtitle?: TypographyVariant;
|
subtitle?: SubtitleDefinition;
|
||||||
list: {
|
list: {
|
||||||
selectionType: SelectionType;
|
selectionType: SelectionType;
|
||||||
options: ListOptionDefinition[];
|
options: ListOptionDefinition[];
|
||||||
@ -223,43 +239,48 @@ export interface ListScreenDefinition {
|
|||||||
variants?: ScreenVariantDefinition<ListScreenDefinition>[];
|
variants?: ScreenVariantDefinition<ListScreenDefinition>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageDefinition {
|
||||||
|
src: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailInputDefinition {
|
||||||
|
placeholder?: string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Email Screen Definition
|
// Email Screen Definition
|
||||||
export interface EmailScreenDefinition {
|
export interface EmailScreenDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
template: "email";
|
template: "email";
|
||||||
header?: HeaderDefinition;
|
header?: HeaderDefinition;
|
||||||
title: TypographyVariant;
|
title: TitleDefinition;
|
||||||
subtitle?: TypographyVariant;
|
subtitle?: SubtitleDefinition;
|
||||||
emailInput: {
|
emailInput: EmailInputDefinition;
|
||||||
placeholder?: string;
|
image?: ImageDefinition;
|
||||||
label?: string;
|
|
||||||
};
|
|
||||||
image?: {
|
|
||||||
src: string; // Единственное настраиваемое поле - остальное зашито в коде
|
|
||||||
};
|
|
||||||
bottomActionButton?: BottomActionButtonDefinition;
|
bottomActionButton?: BottomActionButtonDefinition;
|
||||||
navigation?: NavigationDefinition;
|
navigation?: NavigationDefinition;
|
||||||
variants?: ScreenVariantDefinition<EmailScreenDefinition>[];
|
variants?: ScreenVariantDefinition<EmailScreenDefinition>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
export interface LoadersScreenDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
template: "loaders";
|
template: "loaders";
|
||||||
header?: HeaderDefinition;
|
header?: HeaderDefinition;
|
||||||
title: TypographyVariant;
|
title: TitleDefinition;
|
||||||
subtitle?: TypographyVariant;
|
subtitle?: SubtitleDefinition;
|
||||||
progressbars: {
|
progressbars: ProgressbarDefinition;
|
||||||
items: Array<{
|
|
||||||
title?: string;
|
|
||||||
subtitle?: string;
|
|
||||||
processingTitle?: string;
|
|
||||||
processingSubtitle?: string;
|
|
||||||
completedTitle?: string;
|
|
||||||
completedSubtitle?: string;
|
|
||||||
}>;
|
|
||||||
transitionDuration?: number; // в миллисекундах, по умолчанию 5000
|
|
||||||
};
|
|
||||||
bottomActionButton?: BottomActionButtonDefinition;
|
bottomActionButton?: BottomActionButtonDefinition;
|
||||||
navigation?: NavigationDefinition;
|
navigation?: NavigationDefinition;
|
||||||
variants?: ScreenVariantDefinition<LoadersScreenDefinition>[];
|
variants?: ScreenVariantDefinition<LoadersScreenDefinition>[];
|
||||||
@ -270,8 +291,8 @@ export interface SoulmatePortraitScreenDefinition {
|
|||||||
id: string;
|
id: string;
|
||||||
template: "soulmate";
|
template: "soulmate";
|
||||||
header?: HeaderDefinition;
|
header?: HeaderDefinition;
|
||||||
title: TypographyVariant;
|
title: TitleDefinition;
|
||||||
subtitle?: TypographyVariant;
|
subtitle?: SubtitleDefinition;
|
||||||
description?: TypographyVariant; // 🎯 Настраиваемый текст описания
|
description?: TypographyVariant; // 🎯 Настраиваемый текст описания
|
||||||
bottomActionButton?: BottomActionButtonDefinition;
|
bottomActionButton?: BottomActionButtonDefinition;
|
||||||
navigation?: NavigationDefinition;
|
navigation?: NavigationDefinition;
|
||||||
|
|||||||
@ -86,12 +86,14 @@ const ListOptionDefinitionSchema = new Schema({
|
|||||||
|
|
||||||
const NavigationConditionSchema = new Schema({
|
const NavigationConditionSchema = new Schema({
|
||||||
screenId: { type: String, required: true },
|
screenId: { type: String, required: true },
|
||||||
|
conditionType: { type: String, enum: ['options', 'values'], default: 'options' },
|
||||||
operator: {
|
operator: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ['includesAny', 'includesAll', 'includesExactly'],
|
enum: ['includesAny', 'includesAll', 'includesExactly', 'equals'],
|
||||||
default: 'includesAny'
|
default: 'includesAny'
|
||||||
},
|
},
|
||||||
optionIds: [{ type: String, required: true }]
|
optionIds: [{ type: String }],
|
||||||
|
values: [{ type: String }],
|
||||||
}, { _id: false });
|
}, { _id: false });
|
||||||
|
|
||||||
const NavigationRuleSchema = new Schema({
|
const NavigationRuleSchema = new Schema({
|
||||||
@ -101,7 +103,8 @@ const NavigationRuleSchema = new Schema({
|
|||||||
|
|
||||||
const NavigationDefinitionSchema = new Schema({
|
const NavigationDefinitionSchema = new Schema({
|
||||||
rules: [NavigationRuleSchema],
|
rules: [NavigationRuleSchema],
|
||||||
defaultNextScreenId: String
|
defaultNextScreenId: String,
|
||||||
|
isEndScreen: { type: Boolean, default: false },
|
||||||
}, { _id: false });
|
}, { _id: false });
|
||||||
|
|
||||||
const BottomActionButtonSchema = new Schema({
|
const BottomActionButtonSchema = new Schema({
|
||||||
@ -111,7 +114,8 @@ const BottomActionButtonSchema = new Schema({
|
|||||||
type: String,
|
type: String,
|
||||||
enum: ['3xl', 'full'],
|
enum: ['3xl', 'full'],
|
||||||
default: '3xl'
|
default: '3xl'
|
||||||
}
|
},
|
||||||
|
showPrivacyTermsConsent: { type: Boolean, default: false },
|
||||||
}, { _id: false });
|
}, { _id: false });
|
||||||
|
|
||||||
// Схемы для различных типов экранов (используем Mixed для гибкости)
|
// Схемы для различных типов экранов (используем Mixed для гибкости)
|
||||||
@ -146,7 +150,8 @@ const ScreenDefinitionSchema = new Schema({
|
|||||||
},
|
},
|
||||||
emailInput: Schema.Types.Mixed, // email
|
emailInput: Schema.Types.Mixed, // email
|
||||||
image: Schema.Types.Mixed, // email, soulmate
|
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 для всех типов
|
variants: [Schema.Types.Mixed] // variants для всех типов
|
||||||
}, { _id: false });
|
}, { _id: false });
|
||||||
|
|
||||||
@ -160,7 +165,8 @@ const FunnelMetaSchema = new Schema({
|
|||||||
|
|
||||||
const DefaultTextsSchema = new Schema({
|
const DefaultTextsSchema = new Schema({
|
||||||
nextButton: { type: String, default: 'Next' },
|
nextButton: { type: String, default: 'Next' },
|
||||||
continueButton: { type: String, default: 'Continue' }
|
continueButton: { type: String, default: 'Continue' },
|
||||||
|
privacyBanner: { type: String },
|
||||||
}, { _id: false });
|
}, { _id: false });
|
||||||
|
|
||||||
const FunnelDataSchema = new Schema({
|
const FunnelDataSchema = new Schema({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user