fix
This commit is contained in:
parent
b3eaa19fcd
commit
e98b1bfc05
@ -3,11 +3,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { BuilderProvider } from "@/lib/admin/builder/context";
|
||||
import { BuilderUndoRedoProvider } from "@/components/admin/builder/BuilderUndoRedoProvider";
|
||||
import { BuilderTopBar } from "@/components/admin/builder/BuilderTopBar";
|
||||
import { BuilderSidebar } from "@/components/admin/builder/BuilderSidebar";
|
||||
import { BuilderCanvas } from "@/components/admin/builder/BuilderCanvas";
|
||||
import { BuilderPreview } from "@/components/admin/builder/BuilderPreview";
|
||||
import {
|
||||
BuilderUndoRedoProvider,
|
||||
BuilderTopBar,
|
||||
BuilderSidebar,
|
||||
BuilderCanvas,
|
||||
BuilderPreview
|
||||
} from "@/components/admin/builder";
|
||||
import type { BuilderState } from '@/lib/admin/builder/context';
|
||||
import type { FunnelDefinition } from '@/lib/funnel/types';
|
||||
import { deserializeFunnelDefinition } from '@/lib/admin/builder/utils';
|
||||
@ -83,12 +85,7 @@ export default function FunnelBuilderPage() {
|
||||
nextButton: 'Далее',
|
||||
continueButton: 'Продолжить'
|
||||
},
|
||||
screens: builderState.screens.map(screen => {
|
||||
// Убираем position из экрана при сохранении
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { position, ...screenWithoutPosition } = screen;
|
||||
return screenWithoutPosition;
|
||||
})
|
||||
screens: builderState.screens
|
||||
};
|
||||
|
||||
const response = await fetch(`/api/funnels/${funnelId}`, {
|
||||
@ -139,11 +136,7 @@ export default function FunnelBuilderPage() {
|
||||
nextButton: 'Далее',
|
||||
continueButton: 'Продолжить'
|
||||
},
|
||||
screens: builderState.screens.map(screen => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { position, ...screenWithoutPosition } = screen;
|
||||
return screenWithoutPosition;
|
||||
})
|
||||
screens: builderState.screens
|
||||
};
|
||||
|
||||
await fetch(`/api/funnels/${funnelId}/history`, {
|
||||
|
||||
@ -10,6 +10,8 @@ interface RouteParams {
|
||||
}>;
|
||||
}
|
||||
|
||||
// No normalization needed: we require `progressbars` for loaders
|
||||
|
||||
// GET /api/funnels/[id] - получить конкретную воронку
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
@ -86,6 +88,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
||||
|
||||
if (status !== undefined) funnel.status = status;
|
||||
if (funnelData !== undefined) {
|
||||
// Save as-is; schema expects `progressbars` for loaders
|
||||
funnel.funnelData = funnelData as FunnelDefinition;
|
||||
|
||||
// Увеличиваем версию только при публикации
|
||||
@ -111,7 +114,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
||||
await FunnelHistoryModel.create({
|
||||
funnelId: id,
|
||||
sessionId,
|
||||
funnelSnapshot: funnelData,
|
||||
funnelSnapshot: funnelData as FunnelDefinition,
|
||||
actionType: status === 'published' ? 'publish' : 'update',
|
||||
sequenceNumber: nextSequenceNumber,
|
||||
description: actionDescription || 'Воронка обновлена',
|
||||
@ -119,7 +122,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
||||
changeDetails: {
|
||||
action: 'update-funnel',
|
||||
previousValue: previousData,
|
||||
newValue: funnelData
|
||||
newValue: funnelData as FunnelDefinition
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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 { Button } from "@/components/ui/button";
|
||||
import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context";
|
||||
import { AddScreenDialog } from "@/components/admin/builder/AddScreenDialog";
|
||||
import { AddScreenDialog } from "../dialogs/AddScreenDialog";
|
||||
import type {
|
||||
ListOptionDefinition,
|
||||
NavigationConditionDefinition,
|
||||
|
||||
@ -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 { Button } from "@/components/ui/button";
|
||||
import { TemplateConfig } from "@/components/admin/builder/templates";
|
||||
import { ScreenVariantsConfig } from "@/components/admin/builder/ScreenVariantsConfig";
|
||||
import { ScreenVariantsConfig } from "../forms/ScreenVariantsConfig";
|
||||
import { useBuilderDispatch, useBuilderSelectedScreen, useBuilderState } from "@/lib/admin/builder/context";
|
||||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||||
import type {
|
||||
|
||||
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 { serializeBuilderState, deserializeFunnelDefinition } from "@/lib/admin/builder/utils";
|
||||
import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context";
|
||||
import { useBuilderUndoRedo } from "@/components/admin/builder/BuilderUndoRedoProvider";
|
||||
import { useBuilderUndoRedo } from "../providers/BuilderUndoRedoProvider";
|
||||
import type { BuilderState } from "@/lib/admin/builder/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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) {
|
||||
const couponScreen = screen as CouponScreenDefinition & { position: { x: number; y: number } };
|
||||
const couponScreen = screen as CouponScreenDefinition;
|
||||
|
||||
const handleCouponUpdate = <T extends keyof CouponScreenDefinition["coupon"]>(
|
||||
field: T,
|
||||
|
||||
@ -10,7 +10,7 @@ interface DateScreenConfigProps {
|
||||
}
|
||||
|
||||
export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
const dateScreen = screen as DateScreenDefinition & { position: { x: number; y: number } };
|
||||
const dateScreen = screen as DateScreenDefinition;
|
||||
|
||||
const handleDateInputChange = <T extends keyof DateScreenDefinition["dateInput"]>(
|
||||
field: T,
|
||||
|
||||
@ -12,7 +12,7 @@ interface FormScreenConfigProps {
|
||||
}
|
||||
|
||||
export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
const formScreen = screen as FormScreenDefinition & { position: { x: number; y: number } };
|
||||
const formScreen = screen as FormScreenDefinition;
|
||||
|
||||
const updateField = (index: number, updates: Partial<FormFieldDefinition>) => {
|
||||
const newFields = [...(formScreen.fields || [])];
|
||||
|
||||
@ -11,7 +11,7 @@ interface InfoScreenConfigProps {
|
||||
}
|
||||
|
||||
export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
||||
const infoScreen = screen as InfoScreenDefinition & { position: { x: number; y: number } };
|
||||
const infoScreen = screen as InfoScreenDefinition;
|
||||
|
||||
const handleDescriptionChange = (text: string) => {
|
||||
onUpdate({
|
||||
|
||||
@ -25,7 +25,7 @@ function mutateOptions(
|
||||
}
|
||||
|
||||
export function ListScreenConfig({ screen, onUpdate }: ListScreenConfigProps) {
|
||||
const listScreen = screen as ListScreenDefinition & { position: { x: number; y: number } };
|
||||
const listScreen = screen as ListScreenDefinition;
|
||||
const [expandedOptions, setExpandedOptions] = useState<Set<number>>(new Set());
|
||||
|
||||
const toggleOptionExpanded = (index: number) => {
|
||||
|
||||
@ -5,7 +5,7 @@ import Image from "next/image";
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
import { buildTypographyProps } from "@/lib/funnel/mappers";
|
||||
import type { InfoScreenDefinition } from "@/lib/funnel/types";
|
||||
import { TemplateLayout } from "./TemplateLayout";
|
||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface InfoTemplateProps {
|
||||
@ -43,11 +43,9 @@ export function InfoTemplate({
|
||||
return (
|
||||
<TemplateLayout
|
||||
screen={screen}
|
||||
onContinue={onContinue}
|
||||
canGoBack={canGoBack}
|
||||
onBack={onBack}
|
||||
screenProgress={screenProgress}
|
||||
defaultTexts={defaultTexts}
|
||||
titleDefaults={{ font: "manrope", weight: "bold", align: "center" }}
|
||||
subtitleDefaults={{ font: "inter", weight: "medium", color: "muted", align: "center" }}
|
||||
actionButtonOptions={{
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { TemplateLayout } from "./TemplateLayout";
|
||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||
import CircularProgressbarsList from "@/components/widgets/CircularProgressbarsList/CircularProgressbarsList";
|
||||
import type { LoadersScreenDefinition } from "@/lib/funnel/types";
|
||||
|
||||
@ -44,11 +44,11 @@ export function LoadersTemplate({
|
||||
},
|
||||
processing: typedItem.processingTitle ? {
|
||||
title: { children: typedItem.processingTitle },
|
||||
subtitle: typedItem.processingSubtitle ? { children: typedItem.processingSubtitle } : undefined,
|
||||
text: typedItem.processingSubtitle ? { children: typedItem.processingSubtitle } : undefined,
|
||||
} : undefined,
|
||||
completed: typedItem.completedTitle ? {
|
||||
title: { children: typedItem.completedTitle },
|
||||
subtitle: typedItem.completedSubtitle ? { children: typedItem.completedSubtitle } : undefined,
|
||||
text: typedItem.completedSubtitle ? { children: typedItem.completedSubtitle } : undefined,
|
||||
} : undefined,
|
||||
};
|
||||
}) || [],
|
||||
@ -59,11 +59,9 @@ export function LoadersTemplate({
|
||||
return (
|
||||
<TemplateLayout
|
||||
screen={screen}
|
||||
onContinue={onContinue}
|
||||
canGoBack={canGoBack}
|
||||
onBack={onBack}
|
||||
screenProgress={screenProgress}
|
||||
defaultTexts={defaultTexts}
|
||||
titleDefaults={{ font: "manrope", weight: "bold", align: "center", size: "2xl", color: "default" }}
|
||||
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "center", size: "lg" }}
|
||||
actionButtonOptions={{
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { SoulmatePortraitScreenDefinition } from "@/lib/funnel/types";
|
||||
import { TemplateLayout } from "./TemplateLayout";
|
||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||
|
||||
interface SoulmatePortraitTemplateProps {
|
||||
screen: SoulmatePortraitScreenDefinition;
|
||||
@ -23,11 +23,9 @@ export function SoulmatePortraitTemplate({
|
||||
return (
|
||||
<TemplateLayout
|
||||
screen={screen}
|
||||
onContinue={onContinue}
|
||||
canGoBack={canGoBack}
|
||||
onBack={onBack}
|
||||
screenProgress={screenProgress}
|
||||
defaultTexts={defaultTexts}
|
||||
titleDefaults={{ font: "manrope", weight: "bold", align: "center", size: "xl", color: "primary" }}
|
||||
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }}
|
||||
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 type { DateScreenDefinition } from "@/lib/funnel/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TemplateLayout } from "./TemplateLayout";
|
||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||
|
||||
interface DateTemplateProps {
|
||||
screen: DateScreenDefinition;
|
||||
@ -70,11 +70,9 @@ export function DateTemplate({
|
||||
return (
|
||||
<TemplateLayout
|
||||
screen={screen}
|
||||
onContinue={onContinue}
|
||||
canGoBack={canGoBack}
|
||||
onBack={onBack}
|
||||
screenProgress={screenProgress}
|
||||
defaultTexts={defaultTexts}
|
||||
titleDefaults={{ font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" }}
|
||||
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }}
|
||||
actionButtonOptions={{
|
||||
@ -5,7 +5,7 @@ import Image from "next/image";
|
||||
import PrivacySecurityBanner from "@/components/widgets/PrivacySecurityBanner/PrivacySecurityBanner";
|
||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||||
import type { EmailScreenDefinition, DefaultTexts } from "@/lib/funnel/types";
|
||||
import { TemplateLayout } from "./TemplateLayout";
|
||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
@ -34,6 +34,7 @@ export function EmailTemplate({
|
||||
onContinue,
|
||||
canGoBack,
|
||||
onBack,
|
||||
screenProgress,
|
||||
defaultTexts,
|
||||
}: EmailTemplateProps) {
|
||||
const [isTouched, setIsTouched] = useState(false);
|
||||
@ -61,11 +62,9 @@ export function EmailTemplate({
|
||||
return (
|
||||
<TemplateLayout
|
||||
screen={screen}
|
||||
onContinue={onContinue}
|
||||
canGoBack={canGoBack}
|
||||
onBack={onBack}
|
||||
screenProgress={undefined}
|
||||
defaultTexts={defaultTexts}
|
||||
screenProgress={screenProgress}
|
||||
titleDefaults={{ font: "manrope", weight: "bold", align: "center", size: "2xl", color: "default" }}
|
||||
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "center", size: "lg" }}
|
||||
actionButtonOptions={{
|
||||
@ -2,13 +2,10 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { LayoutQuestion } from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||||
|
||||
import {
|
||||
buildLayoutQuestionProps,
|
||||
} from "@/lib/funnel/mappers";
|
||||
import type { FormScreenDefinition } from "@/lib/funnel/types";
|
||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||
|
||||
interface FormTemplateProps {
|
||||
screen: FormScreenDefinition;
|
||||
@ -105,22 +102,20 @@ export function FormTemplate({
|
||||
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 (
|
||||
<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">
|
||||
{screen.fields.map((field) => (
|
||||
<div key={field.id}>
|
||||
@ -142,6 +137,6 @@ export function FormTemplate({
|
||||
</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 { LayoutQuestion } from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
||||
import { Coupon } from "@/components/widgets/Coupon/Coupon";
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
|
||||
import {
|
||||
buildLayoutQuestionProps,
|
||||
buildTypographyProps,
|
||||
} from "@/lib/funnel/mappers";
|
||||
import { buildTypographyProps } from "@/lib/funnel/mappers";
|
||||
import type { CouponScreenDefinition } from "@/lib/funnel/types";
|
||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||
|
||||
interface CouponTemplateProps {
|
||||
screen: CouponScreenDefinition;
|
||||
@ -41,20 +38,6 @@ export function CouponTemplate({
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const layoutQuestionProps = buildLayoutQuestionProps({
|
||||
screen,
|
||||
titleDefaults: { font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" },
|
||||
subtitleDefaults: { font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" },
|
||||
canGoBack,
|
||||
onBack,
|
||||
actionButtonOptions: {
|
||||
defaultText: defaultTexts?.continueButton || "Continue",
|
||||
disabled: false,
|
||||
onClick: onContinue,
|
||||
},
|
||||
screenProgress,
|
||||
});
|
||||
|
||||
const couponProps = {
|
||||
title: buildTypographyProps(screen.coupon.title, {
|
||||
as: "h3" as const,
|
||||
@ -118,7 +101,19 @@ export function CouponTemplate({
|
||||
};
|
||||
|
||||
return (
|
||||
<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="mb-8">
|
||||
<Coupon {...couponProps} />
|
||||
@ -141,6 +136,6 @@ export function CouponTemplate({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LayoutQuestion>
|
||||
</TemplateLayout>
|
||||
);
|
||||
}
|
||||
@ -9,7 +9,7 @@ import type { RadioAnswersListProps } from "@/components/widgets/RadioAnswersLis
|
||||
import type { SelectAnswersListProps } from "@/components/widgets/SelectAnswersList/SelectAnswersList";
|
||||
import { mapListOptionsToButtons } from "@/lib/funnel/mappers";
|
||||
import type { ListScreenDefinition } from "@/lib/funnel/types";
|
||||
import { TemplateLayout } from "./TemplateLayout";
|
||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||
|
||||
interface ListTemplateProps {
|
||||
screen: ListScreenDefinition;
|
||||
@ -104,7 +104,6 @@ export function ListTemplate({
|
||||
return (
|
||||
<TemplateLayout
|
||||
screen={screen}
|
||||
onContinue={() => {}}
|
||||
canGoBack={canGoBack}
|
||||
onBack={onBack}
|
||||
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 {
|
||||
screen: ScreenDefinition;
|
||||
onContinue: () => void;
|
||||
canGoBack: boolean;
|
||||
onBack: () => void;
|
||||
screenProgress?: { current: number; total: number };
|
||||
defaultTexts?: { nextButton?: string; continueButton?: string };
|
||||
|
||||
// Настройки template
|
||||
titleDefaults?: {
|
||||
@ -53,11 +51,9 @@ interface TemplateLayoutProps {
|
||||
*/
|
||||
export function TemplateLayout({
|
||||
screen,
|
||||
// onContinue, // Unused in this component
|
||||
canGoBack,
|
||||
onBack,
|
||||
screenProgress,
|
||||
// defaultTexts, // Unused in this component
|
||||
titleDefaults = { font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" },
|
||||
subtitleDefaults = { font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" },
|
||||
actionButtonOptions,
|
||||
@ -86,12 +82,7 @@ export function TemplateLayout({
|
||||
const bottomActionButtonProps = actionButtonOptions
|
||||
? buildTemplateBottomActionButtonProps({
|
||||
screen,
|
||||
titleDefaults,
|
||||
subtitleDefaults,
|
||||
canGoBack,
|
||||
onBack,
|
||||
actionButtonOptions,
|
||||
screenProgress,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
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"
|
||||
size="xl"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
{...title}
|
||||
className={cn(title.className, "leading-[140%]")}
|
||||
className={cn(title.className, "leading-[140%] text-white")}
|
||||
/>
|
||||
)}
|
||||
{offer && (
|
||||
@ -79,18 +78,17 @@ function Coupon({
|
||||
as="h3"
|
||||
size="4xl"
|
||||
weight="black"
|
||||
color="card"
|
||||
{...offer.title}
|
||||
className={cn(offer.title?.className, "text-[#1F2937]")}
|
||||
/>
|
||||
)}
|
||||
{offer.description && (
|
||||
<Typography
|
||||
as="p"
|
||||
weight="semiBold"
|
||||
color="card"
|
||||
{...offer.description}
|
||||
className={cn(
|
||||
"text-[17px] leading-[100%]",
|
||||
"text-[17px] leading-[100%] text-[#1F2937]",
|
||||
"mt-2",
|
||||
offer.description.className
|
||||
)}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { BuilderFunnelState, BuilderScreen } from "@/lib/admin/builder/types";
|
||||
import type { BuilderState } from "./types";
|
||||
import { buildListDefaults } from "./defaults/list";
|
||||
|
||||
export const INITIAL_META: BuilderFunnelState["meta"] = {
|
||||
id: "funnel-builder-draft",
|
||||
@ -9,37 +10,13 @@ export const INITIAL_META: BuilderFunnelState["meta"] = {
|
||||
};
|
||||
|
||||
export const INITIAL_SCREEN: BuilderScreen = {
|
||||
id: "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,
|
||||
},
|
||||
...buildListDefaults("screen-1"),
|
||||
// Переопределяем опции для начального экрана
|
||||
list: {
|
||||
selectionType: "single",
|
||||
options: [
|
||||
{
|
||||
id: "option-1",
|
||||
id: "option-1",
|
||||
label: "Вариант 1",
|
||||
},
|
||||
{
|
||||
@ -48,15 +25,7 @@ export const INITIAL_SCREEN: BuilderScreen = {
|
||||
},
|
||||
],
|
||||
},
|
||||
navigation: {
|
||||
defaultNextScreenId: undefined,
|
||||
rules: [],
|
||||
},
|
||||
position: {
|
||||
x: 80,
|
||||
y: 120,
|
||||
},
|
||||
};
|
||||
} as BuilderScreen;
|
||||
|
||||
export const INITIAL_STATE: BuilderState = {
|
||||
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 { BuilderScreen, BuilderScreenPosition } from "@/lib/admin/builder/types";
|
||||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||||
import type { BuilderState, BuilderAction } from "./types";
|
||||
import { INITIAL_STATE } from "./constants";
|
||||
import { withDirty, generateScreenId, createScreenByTemplate } from "./utils";
|
||||
@ -18,12 +18,8 @@ export function builderReducer(state: BuilderState, action: BuilderAction): Buil
|
||||
case "add-screen": {
|
||||
const nextId = generateScreenId(state.screens.map((s) => s.id));
|
||||
const template = action.payload?.template || "list";
|
||||
const position = {
|
||||
x: (action.payload?.position?.x ?? 120) + state.screens.length * 40,
|
||||
y: (action.payload?.position?.y ?? 120) + state.screens.length * 20,
|
||||
};
|
||||
|
||||
const newScreen = createScreenByTemplate(template, nextId, position);
|
||||
const newScreen = createScreenByTemplate(template, nextId);
|
||||
|
||||
// 🎯 АВТОМАТИЧЕСКОЕ СВЯЗЫВАНИЕ С ПРЕДЫДУЩИМ ЭКРАНОМ
|
||||
let updatedScreens = [...state.screens, newScreen];
|
||||
@ -96,11 +92,11 @@ export function builderReducer(state: BuilderState, action: BuilderAction): Buil
|
||||
...(current.template === "list" && "list" in screen && screen.list
|
||||
? {
|
||||
list: {
|
||||
...(current as ListScreenDefinition & { position: BuilderScreenPosition }).list,
|
||||
...(current as ListScreenDefinition).list,
|
||||
...screen.list,
|
||||
options:
|
||||
screen.list.options ??
|
||||
(current as ListScreenDefinition & { position: BuilderScreenPosition }).list.options,
|
||||
(current as ListScreenDefinition).list.options,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
@ -129,16 +125,6 @@ export function builderReducer(state: BuilderState, action: BuilderAction): Buil
|
||||
selectedScreenId: nextSelectedScreenId,
|
||||
});
|
||||
}
|
||||
case "reposition-screen": {
|
||||
return withDirty(state, {
|
||||
...state,
|
||||
screens: state.screens.map((screen) =>
|
||||
screen.id === action.payload.screenId
|
||||
? { ...screen, position: action.payload.position }
|
||||
: screen
|
||||
),
|
||||
});
|
||||
}
|
||||
case "reorder-screens": {
|
||||
const { fromIndex, toIndex } = action.payload;
|
||||
const previousScreens = state.screens;
|
||||
|
||||
@ -14,7 +14,6 @@ export type BuilderAction =
|
||||
| { type: "add-screen"; payload?: { template?: ScreenDefinition["template"] } & Partial<BuilderScreen> }
|
||||
| { type: "remove-screen"; payload: { screenId: string } }
|
||||
| { 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: "set-selected-screen"; payload: { screenId: string | null } }
|
||||
| { 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 { BuilderState } from "./types";
|
||||
import { buildInfoDefaults } from "./defaults/info";
|
||||
import { buildListDefaults } from "./defaults/list";
|
||||
import { buildFormDefaults } from "./defaults/form";
|
||||
import { buildDateDefaults } from "./defaults/date";
|
||||
import { buildCouponDefaults } from "./defaults/coupon";
|
||||
import { buildEmailDefaults } from "./defaults/email";
|
||||
import { buildLoadersDefaults } from "./defaults/loaders";
|
||||
import { buildSoulmateDefaults } from "./defaults/soulmate";
|
||||
|
||||
/**
|
||||
* Marks the state as dirty if it has changed
|
||||
@ -30,213 +38,25 @@ export function generateScreenId(existing: string[]): string {
|
||||
*/
|
||||
export function createScreenByTemplate(
|
||||
template: ScreenDefinition["template"],
|
||||
id: string,
|
||||
position: BuilderScreenPosition
|
||||
id: string
|
||||
): BuilderScreen {
|
||||
// ✅ Единые базовые настройки для ВСЕХ типов экранов
|
||||
const baseScreen = {
|
||||
id,
|
||||
position,
|
||||
// ✅ Современные настройки header (без устаревшего progress)
|
||||
header: {
|
||||
show: true,
|
||||
showBackButton: true,
|
||||
},
|
||||
// ✅ Базовые тексты согласно Figma
|
||||
title: {
|
||||
text: "Новый экран",
|
||||
font: "manrope" as const,
|
||||
weight: "bold" as const,
|
||||
align: "left" as const,
|
||||
size: "2xl" as const,
|
||||
color: "default" as const,
|
||||
},
|
||||
subtitle: {
|
||||
text: "Добавьте детали справа",
|
||||
font: "manrope" as const,
|
||||
weight: "medium" as const,
|
||||
color: "default" as const,
|
||||
align: "left" as const,
|
||||
size: "lg" as const,
|
||||
},
|
||||
// ✅ Единые настройки нижней кнопки
|
||||
bottomActionButton: {
|
||||
text: "Продолжить",
|
||||
show: true,
|
||||
},
|
||||
// ✅ Навигация
|
||||
navigation: {
|
||||
defaultNextScreenId: undefined,
|
||||
rules: [],
|
||||
},
|
||||
};
|
||||
|
||||
switch (template) {
|
||||
case "info":
|
||||
// Деструктурируем baseScreen исключая subtitle для InfoScreenDefinition
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { subtitle, ...baseScreenWithoutSubtitle } = baseScreen;
|
||||
return {
|
||||
...baseScreenWithoutSubtitle,
|
||||
template: "info",
|
||||
title: {
|
||||
text: "Заголовок информации",
|
||||
font: "manrope" as const,
|
||||
weight: "bold" as const,
|
||||
align: "center" as const, // 🎯 Центрированный заголовок по умолчанию
|
||||
size: "2xl" as const,
|
||||
color: "default" as const,
|
||||
},
|
||||
// 🚫 Подзаголовок не включается (InfoScreenDefinition не поддерживает subtitle)
|
||||
description: {
|
||||
text: "Добавьте описание для информационного экрана. Используйте **жирный текст** для выделения важного.",
|
||||
align: "center" as const, // 🎯 Центрированный текст
|
||||
},
|
||||
// 🚫 Иконка не добавляется по умолчанию - пользователь может добавить при необходимости
|
||||
};
|
||||
|
||||
return buildInfoDefaults(id);
|
||||
case "list":
|
||||
return {
|
||||
...baseScreen,
|
||||
template: "list",
|
||||
list: {
|
||||
selectionType: "single" as const,
|
||||
options: [
|
||||
{ id: "option-1", label: "Вариант 1" },
|
||||
{ id: "option-2", label: "Вариант 2" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return buildListDefaults(id);
|
||||
case "form":
|
||||
return {
|
||||
...baseScreen,
|
||||
template: "form",
|
||||
fields: [
|
||||
{
|
||||
id: "field-1",
|
||||
type: "text",
|
||||
label: "Поле 1",
|
||||
placeholder: "Введите значение",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return buildFormDefaults(id);
|
||||
case "date":
|
||||
return {
|
||||
...baseScreen,
|
||||
template: "date",
|
||||
dateInput: {
|
||||
monthLabel: "Месяц",
|
||||
dayLabel: "День",
|
||||
yearLabel: "Год",
|
||||
monthPlaceholder: "ММ",
|
||||
dayPlaceholder: "ДД",
|
||||
yearPlaceholder: "ГГГГ",
|
||||
showSelectedDate: true,
|
||||
selectedDateFormat: "dd MMMM yyyy",
|
||||
selectedDateLabel: "Выбранная дата:",
|
||||
},
|
||||
infoMessage: {
|
||||
text: "Мы используем эту информацию только для анализа",
|
||||
icon: "🔒",
|
||||
},
|
||||
};
|
||||
|
||||
return buildDateDefaults(id);
|
||||
case "coupon":
|
||||
return {
|
||||
...baseScreen,
|
||||
template: "coupon",
|
||||
coupon: {
|
||||
title: {
|
||||
text: "Промокод на скидку",
|
||||
font: "manrope" as const,
|
||||
weight: "bold" as const,
|
||||
},
|
||||
offer: {
|
||||
title: {
|
||||
text: "Скидка 20%",
|
||||
font: "manrope" as const,
|
||||
weight: "bold" as const,
|
||||
},
|
||||
description: {
|
||||
text: "На первую покупку",
|
||||
font: "inter" as const,
|
||||
weight: "medium" as const,
|
||||
color: "muted" as const,
|
||||
},
|
||||
},
|
||||
promoCode: {
|
||||
text: "WELCOME20",
|
||||
font: "geistMono" as const,
|
||||
weight: "bold" as const,
|
||||
},
|
||||
footer: {
|
||||
text: "Сохраните код или скопируйте",
|
||||
font: "inter" as const,
|
||||
weight: "medium" as const,
|
||||
color: "muted" as const,
|
||||
},
|
||||
},
|
||||
copiedMessage: "Промокод {code} скопирован!",
|
||||
};
|
||||
|
||||
return buildCouponDefaults(id);
|
||||
case "email":
|
||||
return {
|
||||
...baseScreen,
|
||||
template: "email",
|
||||
emailInput: {
|
||||
label: "Email адрес",
|
||||
placeholder: "example@email.com",
|
||||
},
|
||||
};
|
||||
|
||||
return buildEmailDefaults(id);
|
||||
case "loaders":
|
||||
return {
|
||||
...baseScreen,
|
||||
template: "loaders",
|
||||
header: {
|
||||
show: false,
|
||||
showBackButton: false,
|
||||
},
|
||||
progressbars: {
|
||||
items: [
|
||||
{
|
||||
title: "Анализ ответов",
|
||||
subtitle: "Обработка данных...",
|
||||
processingTitle: "Анализируем ваши ответы...",
|
||||
processingSubtitle: "Это займет несколько секунд",
|
||||
completedTitle: "Готово!",
|
||||
completedSubtitle: "Данные проанализированы",
|
||||
},
|
||||
{
|
||||
title: "Создание портрета",
|
||||
subtitle: "Построение результата...",
|
||||
processingTitle: "Строим персональный портрет...",
|
||||
processingSubtitle: "Почти готово",
|
||||
completedTitle: "Готово!",
|
||||
completedSubtitle: "Портрет создан",
|
||||
},
|
||||
],
|
||||
transitionDuration: 3000,
|
||||
},
|
||||
};
|
||||
|
||||
return buildLoadersDefaults(id);
|
||||
case "soulmate":
|
||||
return {
|
||||
...baseScreen,
|
||||
template: "soulmate",
|
||||
header: {
|
||||
show: false,
|
||||
showBackButton: false,
|
||||
},
|
||||
bottomActionButton: {
|
||||
text: "Получить полный анализ",
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
|
||||
return buildSoulmateDefaults(id);
|
||||
default:
|
||||
throw new Error(`Unknown template: ${template}`);
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
export type BuilderScreenPosition = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type BuilderScreen = ScreenDefinition & {
|
||||
position: BuilderScreenPosition;
|
||||
};
|
||||
export type BuilderScreen = ScreenDefinition;
|
||||
|
||||
export interface BuilderFunnelState {
|
||||
meta: FunnelDefinition["meta"];
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { BuilderState } from "@/lib/admin/builder/context";
|
||||
import type { BuilderScreen, BuilderFunnelState, BuilderScreenPosition } from "@/lib/admin/builder/types";
|
||||
import type { BuilderScreen, BuilderFunnelState } from "@/lib/admin/builder/types";
|
||||
import type {
|
||||
FunnelDefinition,
|
||||
ScreenDefinition,
|
||||
@ -45,8 +45,7 @@ export function deserializeFunnelDefinition(funnel: FunnelDefinition): BuilderSt
|
||||
}
|
||||
|
||||
export function serializeBuilderState(state: BuilderFunnelState): FunnelDefinition {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const screens = state.screens.map(({ position: _position, ...rest }) => rest);
|
||||
const screens = state.screens;
|
||||
const meta: FunnelDefinition["meta"] = {
|
||||
...state.meta,
|
||||
firstScreenId: state.meta.firstScreenId ?? state.screens[0]?.id,
|
||||
@ -61,11 +60,10 @@ export function serializeBuilderState(state: BuilderFunnelState): FunnelDefiniti
|
||||
export function cloneScreen(screen: BuilderScreen, overrides?: Partial<BuilderScreen>): BuilderScreen {
|
||||
const copy = {
|
||||
...screen,
|
||||
position: { ...screen.position },
|
||||
...(screen.template === "list" && 'list' in screen ? {
|
||||
list: {
|
||||
...(screen as ListScreenDefinition & { position: BuilderScreenPosition }).list,
|
||||
options: (screen as ListScreenDefinition & { position: BuilderScreenPosition }).list.options.map((option) => ({ ...option })),
|
||||
...(screen as ListScreenDefinition).list,
|
||||
options: (screen as ListScreenDefinition).list.options.map((option) => ({ ...option })),
|
||||
}
|
||||
} : {}),
|
||||
...(Array.isArray(screen.variants)
|
||||
|
||||
@ -158,7 +158,6 @@ interface BuildLayoutQuestionOptions {
|
||||
subtitleDefaults?: TypographyDefaults;
|
||||
canGoBack: boolean;
|
||||
onBack: () => void;
|
||||
actionButtonOptions?: BuildActionButtonOptions;
|
||||
screenProgress?: { current: number; total: number };
|
||||
}
|
||||
|
||||
@ -202,21 +201,21 @@ export function buildLayoutQuestionProps(
|
||||
}
|
||||
|
||||
// Отдельная функция для получения bottomActionButtonProps
|
||||
export function buildTemplateBottomActionButtonProps(
|
||||
options: BuildLayoutQuestionOptions
|
||||
) {
|
||||
const {
|
||||
screen,
|
||||
actionButtonOptions
|
||||
} = options;
|
||||
export function buildTemplateBottomActionButtonProps(options: {
|
||||
screen: ScreenDefinition;
|
||||
actionButtonOptions: BuildActionButtonOptions;
|
||||
}) {
|
||||
const { screen, actionButtonOptions } = options;
|
||||
|
||||
return actionButtonOptions ? buildBottomActionButtonProps(
|
||||
// Наличие actionButtonOptions — явный сигнал показать кнопку.
|
||||
// Принудительно включаем кнопку независимо от screen.bottomActionButton.show
|
||||
return buildBottomActionButtonProps(
|
||||
actionButtonOptions,
|
||||
// Если передаются actionButtonOptions, это означает что кнопка должна показываться
|
||||
// Принудительно включаем её независимо от настроек экрана
|
||||
'bottomActionButton' in screen ?
|
||||
(screen.bottomActionButton?.show === false ? { ...screen.bottomActionButton, show: true } : screen.bottomActionButton)
|
||||
'bottomActionButton' in screen
|
||||
? (screen.bottomActionButton?.show === false
|
||||
? { ...screen.bottomActionButton, show: true }
|
||||
: screen.bottomActionButton)
|
||||
: undefined
|
||||
) : undefined;
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,14 +2,16 @@
|
||||
|
||||
import type { JSX } from "react";
|
||||
|
||||
import { ListTemplate } from "@/components/funnel/templates/ListTemplate";
|
||||
import { InfoTemplate } from "@/components/funnel/templates/InfoTemplate";
|
||||
import { DateTemplate } from "@/components/funnel/templates/DateTemplate";
|
||||
import { CouponTemplate } from "@/components/funnel/templates/CouponTemplate";
|
||||
import { FormTemplate } from "@/components/funnel/templates/FormTemplate";
|
||||
import { EmailTemplate } from "@/components/funnel/templates/EmailTemplate";
|
||||
import { LoadersTemplate } from "@/components/funnel/templates/LoadersTemplate";
|
||||
import { SoulmatePortraitTemplate } from "@/components/funnel/templates/SoulmatePortraitTemplate";
|
||||
import {
|
||||
ListTemplate,
|
||||
InfoTemplate,
|
||||
DateTemplate,
|
||||
CouponTemplate,
|
||||
FormTemplate,
|
||||
EmailTemplate,
|
||||
LoadersTemplate,
|
||||
SoulmatePortraitTemplate,
|
||||
} from "@/components/funnel/templates";
|
||||
import type {
|
||||
ListScreenDefinition,
|
||||
DateScreenDefinition,
|
||||
|
||||
@ -16,6 +16,15 @@ export type TypographyVariant = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// Extended typography for titles/subtitles with show property
|
||||
export interface TitleDefinition extends TypographyVariant {
|
||||
show?: boolean; // Controls whether title should be displayed. Defaults to true.
|
||||
}
|
||||
|
||||
export interface SubtitleDefinition extends TypographyVariant {
|
||||
show?: boolean; // Controls whether subtitle should be displayed. Defaults to true.
|
||||
}
|
||||
|
||||
export interface HeaderProgressDefinition {
|
||||
/** When both current and total provided, value is computed automatically (current / total * 100). */
|
||||
current?: number;
|
||||
@ -53,6 +62,8 @@ export interface BottomActionButtonDefinition {
|
||||
cornerRadius?: "3xl" | "full";
|
||||
/** Controls whether PrivacyTermsConsent should be shown under the button. Defaults to false. */
|
||||
showPrivacyTermsConsent?: boolean;
|
||||
/** Controls whether gradient blur should be shown. Defaults to true. */
|
||||
showGradientBlur?: boolean;
|
||||
}
|
||||
|
||||
export interface DefaultTexts {
|
||||
@ -62,6 +73,7 @@ export interface DefaultTexts {
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface NavigationConditionDefinition {
|
||||
screenId: string;
|
||||
/**
|
||||
@ -103,18 +115,20 @@ export interface ScreenVariantDefinition<T extends { id: string; template: strin
|
||||
overrides: ScreenVariantOverrides<T>;
|
||||
}
|
||||
|
||||
export interface IconDefinition {
|
||||
type: "emoji" | "image";
|
||||
value: string;
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface InfoScreenDefinition {
|
||||
id: string;
|
||||
template: "info";
|
||||
header?: HeaderDefinition;
|
||||
title: TypographyVariant;
|
||||
title: TitleDefinition;
|
||||
description?: TypographyVariant;
|
||||
icon?: {
|
||||
type: "emoji" | "image";
|
||||
value: string; // emoji character or image URL/path
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
className?: string;
|
||||
};
|
||||
icon?: IconDefinition;
|
||||
bottomActionButton?: BottomActionButtonDefinition;
|
||||
navigation?: NavigationDefinition;
|
||||
variants?: ScreenVariantDefinition<InfoScreenDefinition>[];
|
||||
@ -137,16 +151,18 @@ export interface DateInputDefinition {
|
||||
};
|
||||
}
|
||||
|
||||
export interface InfoMessageDefinition extends TypographyVariant {
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface DateScreenDefinition {
|
||||
id: string;
|
||||
template: "date";
|
||||
header?: HeaderDefinition;
|
||||
title: TypographyVariant;
|
||||
subtitle?: TypographyVariant;
|
||||
title: TitleDefinition;
|
||||
subtitle?: SubtitleDefinition;
|
||||
dateInput: DateInputDefinition;
|
||||
infoMessage?: TypographyVariant & {
|
||||
icon?: string; // emoji or icon
|
||||
};
|
||||
infoMessage?: InfoMessageDefinition;
|
||||
bottomActionButton?: BottomActionButtonDefinition;
|
||||
navigation?: NavigationDefinition;
|
||||
variants?: ScreenVariantDefinition<DateScreenDefinition>[];
|
||||
@ -166,10 +182,10 @@ export interface CouponScreenDefinition {
|
||||
id: string;
|
||||
template: "coupon";
|
||||
header?: HeaderDefinition;
|
||||
title: TypographyVariant;
|
||||
subtitle?: TypographyVariant;
|
||||
title: TitleDefinition;
|
||||
subtitle?: SubtitleDefinition;
|
||||
coupon: CouponDefinition;
|
||||
copiedMessage?: string; // "Промокод скопирован!" text
|
||||
copiedMessage?: string;
|
||||
bottomActionButton?: BottomActionButtonDefinition;
|
||||
navigation?: NavigationDefinition;
|
||||
variants?: ScreenVariantDefinition<CouponScreenDefinition>[];
|
||||
@ -198,8 +214,8 @@ export interface FormScreenDefinition {
|
||||
id: string;
|
||||
template: "form";
|
||||
header?: HeaderDefinition;
|
||||
title: TypographyVariant;
|
||||
subtitle?: TypographyVariant;
|
||||
title: TitleDefinition;
|
||||
subtitle?: SubtitleDefinition;
|
||||
fields: FormFieldDefinition[];
|
||||
validationMessages?: FormValidationMessages;
|
||||
bottomActionButton?: BottomActionButtonDefinition;
|
||||
@ -212,8 +228,8 @@ export interface ListScreenDefinition {
|
||||
id: string;
|
||||
template: "list";
|
||||
header?: HeaderDefinition;
|
||||
title: TypographyVariant;
|
||||
subtitle?: TypographyVariant;
|
||||
title: TitleDefinition;
|
||||
subtitle?: SubtitleDefinition;
|
||||
list: {
|
||||
selectionType: SelectionType;
|
||||
options: ListOptionDefinition[];
|
||||
@ -223,43 +239,48 @@ export interface ListScreenDefinition {
|
||||
variants?: ScreenVariantDefinition<ListScreenDefinition>[];
|
||||
}
|
||||
|
||||
export interface ImageDefinition {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export interface EmailInputDefinition {
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
// Email Screen Definition
|
||||
export interface EmailScreenDefinition {
|
||||
id: string;
|
||||
template: "email";
|
||||
header?: HeaderDefinition;
|
||||
title: TypographyVariant;
|
||||
subtitle?: TypographyVariant;
|
||||
emailInput: {
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
};
|
||||
image?: {
|
||||
src: string; // Единственное настраиваемое поле - остальное зашито в коде
|
||||
};
|
||||
title: TitleDefinition;
|
||||
subtitle?: SubtitleDefinition;
|
||||
emailInput: EmailInputDefinition;
|
||||
image?: ImageDefinition;
|
||||
bottomActionButton?: BottomActionButtonDefinition;
|
||||
navigation?: NavigationDefinition;
|
||||
variants?: ScreenVariantDefinition<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 {
|
||||
id: string;
|
||||
template: "loaders";
|
||||
header?: HeaderDefinition;
|
||||
title: TypographyVariant;
|
||||
subtitle?: TypographyVariant;
|
||||
progressbars: {
|
||||
items: Array<{
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
processingTitle?: string;
|
||||
processingSubtitle?: string;
|
||||
completedTitle?: string;
|
||||
completedSubtitle?: string;
|
||||
}>;
|
||||
transitionDuration?: number; // в миллисекундах, по умолчанию 5000
|
||||
};
|
||||
title: TitleDefinition;
|
||||
subtitle?: SubtitleDefinition;
|
||||
progressbars: ProgressbarDefinition;
|
||||
bottomActionButton?: BottomActionButtonDefinition;
|
||||
navigation?: NavigationDefinition;
|
||||
variants?: ScreenVariantDefinition<LoadersScreenDefinition>[];
|
||||
@ -270,8 +291,8 @@ export interface SoulmatePortraitScreenDefinition {
|
||||
id: string;
|
||||
template: "soulmate";
|
||||
header?: HeaderDefinition;
|
||||
title: TypographyVariant;
|
||||
subtitle?: TypographyVariant;
|
||||
title: TitleDefinition;
|
||||
subtitle?: SubtitleDefinition;
|
||||
description?: TypographyVariant; // 🎯 Настраиваемый текст описания
|
||||
bottomActionButton?: BottomActionButtonDefinition;
|
||||
navigation?: NavigationDefinition;
|
||||
|
||||
@ -86,12 +86,14 @@ const ListOptionDefinitionSchema = new Schema({
|
||||
|
||||
const NavigationConditionSchema = new Schema({
|
||||
screenId: { type: String, required: true },
|
||||
conditionType: { type: String, enum: ['options', 'values'], default: 'options' },
|
||||
operator: {
|
||||
type: String,
|
||||
enum: ['includesAny', 'includesAll', 'includesExactly'],
|
||||
enum: ['includesAny', 'includesAll', 'includesExactly', 'equals'],
|
||||
default: 'includesAny'
|
||||
},
|
||||
optionIds: [{ type: String, required: true }]
|
||||
optionIds: [{ type: String }],
|
||||
values: [{ type: String }],
|
||||
}, { _id: false });
|
||||
|
||||
const NavigationRuleSchema = new Schema({
|
||||
@ -101,7 +103,8 @@ const NavigationRuleSchema = new Schema({
|
||||
|
||||
const NavigationDefinitionSchema = new Schema({
|
||||
rules: [NavigationRuleSchema],
|
||||
defaultNextScreenId: String
|
||||
defaultNextScreenId: String,
|
||||
isEndScreen: { type: Boolean, default: false },
|
||||
}, { _id: false });
|
||||
|
||||
const BottomActionButtonSchema = new Schema({
|
||||
@ -111,7 +114,8 @@ const BottomActionButtonSchema = new Schema({
|
||||
type: String,
|
||||
enum: ['3xl', 'full'],
|
||||
default: '3xl'
|
||||
}
|
||||
},
|
||||
showPrivacyTermsConsent: { type: Boolean, default: false },
|
||||
}, { _id: false });
|
||||
|
||||
// Схемы для различных типов экранов (используем Mixed для гибкости)
|
||||
@ -146,7 +150,8 @@ const ScreenDefinitionSchema = new Schema({
|
||||
},
|
||||
emailInput: Schema.Types.Mixed, // email
|
||||
image: Schema.Types.Mixed, // email, soulmate
|
||||
loadersConfig: Schema.Types.Mixed, // loaders
|
||||
// loaders
|
||||
progressbars: Schema.Types.Mixed, // preferred key used by runtime/templates
|
||||
variants: [Schema.Types.Mixed] // variants для всех типов
|
||||
}, { _id: false });
|
||||
|
||||
@ -160,7 +165,8 @@ const FunnelMetaSchema = new Schema({
|
||||
|
||||
const DefaultTextsSchema = new Schema({
|
||||
nextButton: { type: String, default: 'Next' },
|
||||
continueButton: { type: String, default: 'Continue' }
|
||||
continueButton: { type: String, default: 'Continue' },
|
||||
privacyBanner: { type: String },
|
||||
}, { _id: false });
|
||||
|
||||
const FunnelDataSchema = new Schema({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user