This commit is contained in:
dev.daminik00 2025-09-28 16:35:41 +02:00
parent e98b1bfc05
commit b28d22967f
34 changed files with 1281 additions and 35 deletions

View File

@ -0,0 +1,109 @@
import { Meta, StoryObj } from "@storybook/nextjs-vite";
import { CouponTemplate } from "./CouponTemplate";
import { fn } from "storybook/test";
import { buildCouponDefaults } from "@/lib/admin/builder/state/defaults/coupon";
import type { CouponScreenDefinition } from "@/lib/funnel/types";
// Получаем дефолтные значения из builder
const defaultScreen = buildCouponDefaults("coupon-screen-story") as CouponScreenDefinition;
/** CouponTemplate - экраны с купонами и промокодами */
const meta: Meta<typeof CouponTemplate> = {
title: "Funnel Templates/CouponTemplate",
component: CouponTemplate,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
},
args: {
screen: defaultScreen,
onContinue: fn(),
canGoBack: true,
onBack: fn(),
screenProgress: { current: 8, total: 10 },
defaultTexts: {
nextButton: "Next",
continueButton: "Continue"
},
},
argTypes: {
screen: {
control: { type: "object" },
},
screenProgress: {
control: { type: "object" },
},
onContinue: { action: "continue" },
onBack: { action: "back" },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
/** Дефолтный купон экран */
export const Default: Story = {};
/** Купон с показом прогресса */
export const WithProgress: Story = {
args: {
screen: {
...defaultScreen,
header: {
show: true,
showBackButton: true,
showProgress: true, // Показываем прогресс
},
},
},
};
/** Экран без header */
export const WithoutHeader: Story = {
args: {
screen: {
...defaultScreen,
header: {
show: false,
},
},
},
};
/** Купон с другой скидкой */
export const CustomDiscount: Story = {
args: {
screen: {
...defaultScreen,
coupon: {
...defaultScreen.coupon,
offer: {
title: {
text: "50% 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: "FIRST50",
font: "geistMono",
weight: "bold",
align: "center",
size: "lg",
color: "accent",
},
},
},
},
};

View File

@ -0,0 +1 @@
export { CouponTemplate } from "./CouponTemplate";

View File

@ -0,0 +1,133 @@
import { Meta, StoryObj } from "@storybook/nextjs-vite";
import { DateTemplate } from "./DateTemplate";
import { fn } from "storybook/test";
import { buildDateDefaults } from "@/lib/admin/builder/state/defaults/date";
import type { DateScreenDefinition } from "@/lib/funnel/types";
// Получаем дефолтные значения из builder
const defaultScreen = buildDateDefaults("date-screen-story") as DateScreenDefinition;
/** DateTemplate - экраны с выбором даты рождения */
const meta: Meta<typeof DateTemplate> = {
title: "Funnel Templates/DateTemplate",
component: DateTemplate,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
},
args: {
screen: defaultScreen,
selectedDate: {},
onDateChange: fn(),
onContinue: fn(),
canGoBack: true,
onBack: fn(),
screenProgress: { current: 4, total: 10 },
defaultTexts: {
nextButton: "Next",
continueButton: "Continue"
},
},
argTypes: {
screen: {
control: { type: "object" },
},
selectedDate: {
control: { type: "object" },
},
screenProgress: {
control: { type: "object" },
},
onDateChange: { action: "date changed" },
onContinue: { action: "continue" },
onBack: { action: "back" },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
/** Дефолтный экран выбора даты */
export const Default: Story = {};
/** Экран с предзаполненной датой */
export const WithPrefilledDate: Story = {
args: {
selectedDate: {
month: "4",
day: "8",
year: "1987",
},
},
};
/** Экран без показа выбранной даты */
export const WithoutSelectedDate: Story = {
args: {
screen: {
...defaultScreen,
dateInput: {
...defaultScreen.dateInput,
showSelectedDate: false,
},
},
},
};
/** Экран с кастомными лейблами */
export const CustomLabels: Story = {
args: {
screen: {
...defaultScreen,
dateInput: {
...defaultScreen.dateInput,
monthLabel: "Month",
dayLabel: "Day",
yearLabel: "Year",
monthPlaceholder: "MM",
dayPlaceholder: "DD",
yearPlaceholder: "YYYY",
selectedDateLabel: "Selected date:",
selectedDateFormat: "MMMM d, yyyy",
},
},
},
};
/** Экран без информационного сообщения */
export const WithoutInfoMessage: Story = {
args: {
screen: {
...defaultScreen,
infoMessage: undefined,
},
},
};
/** Экран с включенным зодиаком */
export const WithZodiac: Story = {
args: {
screen: {
...defaultScreen,
dateInput: {
...defaultScreen.dateInput,
zodiac: {
enabled: true,
storageKey: "zodiac_sign",
},
},
},
},
};
/** Экран без header */
export const WithoutHeader: Story = {
args: {
screen: {
...defaultScreen,
header: {
show: false,
},
},
},
};

View File

@ -9,6 +9,21 @@ import type { DateScreenDefinition } from "@/lib/funnel/types";
import { cn } from "@/lib/utils";
import { TemplateLayout } from "../layouts/TemplateLayout";
// Утилита для форматирования даты на основе паттерна
function formatDateByPattern(date: Date, pattern: string): string {
const monthNames = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
return pattern
.replace("MMMM", monthNames[date.getMonth()])
.replace("MMM", monthNames[date.getMonth()].substring(0, 3))
.replace("yyyy", date.getFullYear().toString())
.replace("dd", date.getDate().toString().padStart(2, '0'))
.replace("d", date.getDate().toString());
}
interface DateTemplateProps {
screen: DateScreenDefinition;
selectedDate: { month?: string; day?: string; year?: string };
@ -67,6 +82,38 @@ export function DateTemplate({
const isFormValid = Boolean(isoDate);
// Форматированная дата для отображения
const formattedDate = useMemo(() => {
if (!isoDate) return null;
const date = new Date(isoDate);
const pattern = screen.dateInput?.selectedDateFormat || "MMMM d, yyyy";
return formatDateByPattern(date, pattern);
}, [isoDate, screen.dateInput?.selectedDateFormat]);
// Компонент отображения выбранной даты над кнопкой
const selectedDateDisplay = formattedDate && screen.dateInput?.showSelectedDate !== false ? (
<div className="text-center space-y-1 mb-4">
<Typography
as="p"
size="sm"
color="muted"
className="font-medium"
>
{screen.dateInput?.selectedDateLabel || "Выбранная дата:"}
</Typography>
<Typography
as="p"
size="xl"
weight="bold"
color="default"
className="font-semibold"
>
{formattedDate}
</Typography>
</div>
) : null;
return (
<TemplateLayout
screen={screen}
@ -80,6 +127,7 @@ export function DateTemplate({
disabled: !isFormValid,
onClick: onContinue,
}}
childrenUnderButton={selectedDateDisplay}
>
<div className="w-full mt-[22px] space-y-6">
<DateInput

View File

@ -0,0 +1 @@
export { DateTemplate } from "./DateTemplate";

View File

@ -0,0 +1,93 @@
import { Meta, StoryObj } from "@storybook/nextjs-vite";
import { EmailTemplate } from "./EmailTemplate";
import { fn } from "storybook/test";
import { buildEmailDefaults } from "@/lib/admin/builder/state/defaults/email";
import type { EmailScreenDefinition } from "@/lib/funnel/types";
// Получаем дефолтные значения из builder
const defaultScreen = buildEmailDefaults("email-screen-story") as EmailScreenDefinition;
/** EmailTemplate - экраны сбора email адреса */
const meta: Meta<typeof EmailTemplate> = {
title: "Funnel Templates/EmailTemplate",
component: EmailTemplate,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
},
args: {
screen: defaultScreen,
onContinue: fn(),
canGoBack: true,
onBack: fn(),
screenProgress: { current: 9, total: 10 },
defaultTexts: {
nextButton: "Next",
continueButton: "Continue"
},
},
argTypes: {
screen: {
control: { type: "object" },
},
screenProgress: {
control: { type: "object" },
},
onContinue: { action: "continue" },
onBack: { action: "back" },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
/** Дефолтный email экран */
export const Default: Story = {};
/** Экран без изображения */
export const WithoutImage: Story = {
args: {
screen: {
...defaultScreen,
image: undefined,
},
},
};
/** Экран с consent */
export const WithConsent: Story = {
args: {
screen: {
...defaultScreen,
bottomActionButton: {
...defaultScreen.bottomActionButton,
showPrivacyTermsConsent: true,
},
},
},
};
/** Экран без header */
export const WithoutHeader: Story = {
args: {
screen: {
...defaultScreen,
header: {
show: false,
},
},
},
};
/** Экран с кастомными лейблами */
export const CustomLabels: Story = {
args: {
screen: {
...defaultScreen,
emailInput: {
label: "Your Email Address",
placeholder: "Enter your email here...",
},
},
},
};

View File

@ -0,0 +1 @@
export { EmailTemplate } from "./EmailTemplate";

View File

@ -0,0 +1,152 @@
import { Meta, StoryObj } from "@storybook/nextjs-vite";
import { FormTemplate } from "./FormTemplate";
import { fn } from "storybook/test";
import { buildFormDefaults } from "@/lib/admin/builder/state/defaults/form";
import type { FormScreenDefinition } from "@/lib/funnel/types";
// Получаем дефолтные значения из builder
const defaultScreen = buildFormDefaults("form-screen-story") as FormScreenDefinition;
// Создаем более богатую форму для демонстрации
const richFormScreen: FormScreenDefinition = {
...defaultScreen,
title: {
...defaultScreen.title,
text: "Расскажите о себе",
},
subtitle: {
...defaultScreen.subtitle,
text: "Заполните форму для персонализированного анализа",
},
fields: [
{
id: "name",
label: "Полное имя",
placeholder: "Введите ваше имя",
type: "text",
required: true,
maxLength: 50,
},
{
id: "email",
label: "Email адрес",
placeholder: "example@email.com",
type: "email",
required: true,
},
{
id: "phone",
label: "Телефон",
placeholder: "+7 (999) 123-45-67",
type: "tel",
required: false,
},
{
id: "website",
label: "Веб-сайт",
placeholder: "https://example.com",
type: "url",
required: false,
},
],
};
/** FormTemplate - экраны с формами ввода */
const meta: Meta<typeof FormTemplate> = {
title: "Funnel Templates/FormTemplate",
component: FormTemplate,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
},
args: {
screen: richFormScreen,
formData: {},
onFormDataChange: fn(),
onContinue: fn(),
canGoBack: true,
onBack: fn(),
screenProgress: { current: 6, total: 10 },
defaultTexts: {
nextButton: "Next",
continueButton: "Continue"
},
},
argTypes: {
screen: {
control: { type: "object" },
},
formData: {
control: { type: "object" },
},
screenProgress: {
control: { type: "object" },
},
onFormDataChange: { action: "form data changed" },
onContinue: { action: "continue" },
onBack: { action: "back" },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
/** Дефолтная форма */
export const Default: Story = {};
/** Простая форма с одним полем */
export const SimpleForm: Story = {
args: {
screen: defaultScreen, // Используем базовые дефолты
},
};
/** Форма только с обязательными полями */
export const RequiredFieldsOnly: Story = {
args: {
screen: {
...richFormScreen,
fields: richFormScreen.fields.filter(field => field.required),
},
},
};
/** Форма с кастомными сообщениями валидации */
export const CustomValidation: Story = {
args: {
screen: {
...richFormScreen,
validationMessages: {
required: "Пожалуйста, заполните это поле",
maxLength: "Слишком длинное значение",
invalidFormat: "Неправильный формат данных",
},
},
},
};
/** Форма без header */
export const WithoutHeader: Story = {
args: {
screen: {
...richFormScreen,
header: {
show: false,
},
},
},
};
/** Форма без subtitle */
export const WithoutSubtitle: Story = {
args: {
screen: {
...richFormScreen,
subtitle: {
...richFormScreen.subtitle,
show: false,
text: richFormScreen.subtitle?.text || "",
},
},
},
};

View File

@ -0,0 +1 @@
export { FormTemplate } from "./FormTemplate";

View File

@ -0,0 +1,95 @@
import { Meta, StoryObj } from "@storybook/nextjs-vite";
import { InfoTemplate } from "./InfoTemplate";
import { fn } from "storybook/test";
import { buildInfoDefaults } from "@/lib/admin/builder/state/defaults/info";
import type { InfoScreenDefinition } from "@/lib/funnel/types";
// Получаем дефолтные значения из builder
const defaultScreen = buildInfoDefaults("info-screen-story") as InfoScreenDefinition;
/** InfoTemplate - информационные экраны с иконкой и описанием */
const meta: Meta<typeof InfoTemplate> = {
title: "Funnel Templates/InfoTemplate",
component: InfoTemplate,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
},
args: {
screen: defaultScreen,
onContinue: fn(),
canGoBack: true,
onBack: fn(),
screenProgress: { current: 3, total: 10 },
defaultTexts: {
nextButton: "Next",
continueButton: "Continue"
},
},
argTypes: {
screen: {
control: { type: "object" },
},
screenProgress: {
control: { type: "object" },
},
onContinue: { action: "continue" },
onBack: { action: "back" },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
/** Дефолтный информационный экран */
export const Default: Story = {};
/** Экран без иконки */
export const WithoutIcon: Story = {
args: {
screen: {
...defaultScreen,
icon: undefined,
},
},
};
/** Экран с кастомной иконкой */
export const WithCustomIcon: Story = {
args: {
screen: {
...defaultScreen,
icon: {
type: "emoji",
value: "🎯",
size: "lg",
},
},
},
};
/** Экран без header */
export const WithoutHeader: Story = {
args: {
screen: {
...defaultScreen,
header: {
show: false,
},
},
},
};
/** Экран с скрытым прогрессом */
export const WithoutProgress: Story = {
args: {
screen: {
...defaultScreen,
header: {
show: true,
showBackButton: true,
showProgress: false,
},
},
},
};

View File

@ -0,0 +1 @@
export { InfoTemplate } from "./InfoTemplate";

View File

@ -0,0 +1,144 @@
import { Meta, StoryObj } from "@storybook/nextjs-vite";
import { ListTemplate } from "./ListTemplate";
import { fn } from "storybook/test";
import { buildListDefaults } from "@/lib/admin/builder/state/defaults/list";
import type { ListScreenDefinition } from "@/lib/funnel/types";
// Получаем дефолтные значения из builder
const defaultScreen = buildListDefaults("list-screen-story") as ListScreenDefinition;
// Более богатый список опций для демонстрации
const richOptionsScreen: ListScreenDefinition = {
...defaultScreen,
title: {
...defaultScreen.title,
text: "Выберите ваш знак зодиака",
},
list: {
...defaultScreen.list,
options: [
{ id: "aries", label: "Овен", emoji: "♈" },
{ id: "taurus", label: "Телец", emoji: "♉" },
{ id: "gemini", label: "Близнецы", emoji: "♊" },
{ id: "cancer", label: "Рак", emoji: "♋" },
{ id: "leo", label: "Лев", emoji: "♌" },
{ id: "virgo", label: "Дева", emoji: "♍" },
],
},
};
/** ListTemplate - экраны с выбором опций (single/multi selection) */
const meta: Meta<typeof ListTemplate> = {
title: "Funnel Templates/ListTemplate",
component: ListTemplate,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
},
args: {
screen: richOptionsScreen,
selectedOptionIds: [],
onSelectionChange: fn(),
actionButtonProps: {
children: "Continue",
onClick: fn(),
},
canGoBack: true,
onBack: fn(),
screenProgress: { current: 5, total: 10 },
},
argTypes: {
screen: {
control: { type: "object" },
},
selectedOptionIds: {
control: { type: "object" },
},
actionButtonProps: {
control: { type: "object" },
},
screenProgress: {
control: { type: "object" },
},
onSelectionChange: { action: "selection changed" },
onBack: { action: "back" },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
/** Дефолтный single selection список (кнопка disabled пока не выбрано) */
export const SingleSelection: Story = {};
/** Multi selection список (кнопка disabled пока не выбрано) */
export const MultiSelection: Story = {
args: {
screen: {
...richOptionsScreen,
title: {
...richOptionsScreen.title,
text: "Выберите несколько вариантов",
},
list: {
...richOptionsScreen.list,
selectionType: "multi",
},
},
},
};
/** Single selection с предвыбранным значением (кнопка активна) */
export const WithPreselection: Story = {
args: {
selectedOptionIds: ["leo"],
},
};
/** Multi selection с несколькими выбранными (кнопка активна) */
export const MultiWithPreselection: Story = {
args: {
screen: {
...richOptionsScreen,
list: {
...richOptionsScreen.list,
selectionType: "multi",
},
},
selectedOptionIds: ["aries", "leo", "virgo"],
},
};
/** Single selection с автопереходом (без кнопки) */
export const SingleAutoAdvance: Story = {
args: {
screen: {
...richOptionsScreen,
bottomActionButton: {
show: false, // Автопереход при выборе
},
},
actionButtonProps: undefined, // Нет кнопки
},
};
/** Список без header */
export const WithoutHeader: Story = {
args: {
screen: {
...richOptionsScreen,
header: {
show: false,
},
},
},
};
/** Простой список без эмодзи */
export const SimpleList: Story = {
args: {
screen: {
...defaultScreen, // Используем базовые дефолты без эмодзи
},
},
};

View File

@ -93,7 +93,8 @@ export function ListTemplate({
const actionButtonOptions = actionButtonProps ? {
defaultText: actionButtonProps.children as string || "Next",
disabled: actionButtonProps.disabled || false,
// Кнопка неактивна если: 1) disabled из props ИЛИ 2) ничего не выбрано
disabled: actionButtonProps.disabled || selectedOptionIds.length === 0,
onClick: () => {
if (actionButtonProps.onClick) {
actionButtonProps.onClick({} as React.MouseEvent<HTMLButtonElement>);

View File

@ -0,0 +1 @@
export { ListTemplate } from "./ListTemplate";

View File

@ -0,0 +1,112 @@
import { Meta, StoryObj } from "@storybook/nextjs-vite";
import { LoadersTemplate } from "./LoadersTemplate";
import { fn } from "storybook/test";
import { buildLoadersDefaults } from "@/lib/admin/builder/state/defaults/loaders";
import type { LoadersScreenDefinition } from "@/lib/funnel/types";
// Получаем дефолтные значения из builder
const defaultScreen = buildLoadersDefaults("loaders-screen-story") as LoadersScreenDefinition;
/** LoadersTemplate - экраны с анимированными загрузчиками */
const meta: Meta<typeof LoadersTemplate> = {
title: "Funnel Templates/LoadersTemplate",
component: LoadersTemplate,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
},
args: {
screen: defaultScreen,
onContinue: fn(),
canGoBack: false, // Обычно на лоадерах нет кнопки назад
onBack: fn(),
screenProgress: undefined, // У лоадеров обычно нет прогресса
defaultTexts: {
nextButton: "Next",
continueButton: "Continue"
},
},
argTypes: {
screen: {
control: { type: "object" },
},
screenProgress: {
control: { type: "object" },
},
onContinue: { action: "continue" },
onBack: { action: "back" },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
/** Дефолтный loaders экран */
export const Default: Story = {};
/** Лоадеры с быстрой анимацией */
export const FastAnimation: Story = {
args: {
screen: {
...defaultScreen,
progressbars: {
...defaultScreen.progressbars,
transitionDuration: 1000, // Быстрее
},
},
},
};
/** Лоадеры с медленной анимацией */
export const SlowAnimation: Story = {
args: {
screen: {
...defaultScreen,
progressbars: {
...defaultScreen.progressbars,
transitionDuration: 5000, // Медленнее
},
},
},
};
/** Лоадеры с кастомными сообщениями */
export const CustomMessages: Story = {
args: {
screen: {
...defaultScreen,
progressbars: {
...defaultScreen.progressbars,
items: [
{
processingTitle: "Анализируем ваши ответы...",
processingSubtitle: "Обработка данных",
completedTitle: "Анализ завершен",
completedSubtitle: "Готово!",
},
{
processingTitle: "Создаем персональный портрет...",
processingSubtitle: "Генерация",
completedTitle: "Портрет готов",
completedSubtitle: "Завершено",
},
],
},
},
},
};
/** Лоадеры с header (необычно, но возможно) */
export const WithHeader: Story = {
args: {
screen: {
...defaultScreen,
header: {
show: true,
showBackButton: false,
showProgress: true,
},
},
screenProgress: { current: 7, total: 10 },
},
};

View File

@ -0,0 +1 @@
export { LoadersTemplate } from "./LoadersTemplate";

View File

@ -0,0 +1,114 @@
import { Meta, StoryObj } from "@storybook/nextjs-vite";
import { SoulmatePortraitTemplate } from "./SoulmatePortraitTemplate";
import { fn } from "storybook/test";
import { buildSoulmateDefaults } from "@/lib/admin/builder/state/defaults/soulmate";
import type { SoulmatePortraitScreenDefinition } from "@/lib/funnel/types";
// Получаем дефолтные значения из builder
const defaultScreen = buildSoulmateDefaults("soulmate-screen-story") as SoulmatePortraitScreenDefinition;
/** SoulmatePortraitTemplate - результирующие экраны с портретом партнера */
const meta: Meta<typeof SoulmatePortraitTemplate> = {
title: "Funnel Templates/SoulmatePortraitTemplate",
component: SoulmatePortraitTemplate,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
},
args: {
screen: defaultScreen,
onContinue: fn(),
canGoBack: true,
onBack: fn(),
screenProgress: { current: 10, total: 10 }, // Обычно финальный экран
defaultTexts: {
nextButton: "Next",
continueButton: "Continue"
},
},
argTypes: {
screen: {
control: { type: "object" },
},
screenProgress: {
control: { type: "object" },
},
onContinue: { action: "continue" },
onBack: { action: "back" },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
/** Дефолтный soulmate portrait экран */
export const Default: Story = {};
/** Экран без описания */
export const WithoutDescription: Story = {
args: {
screen: {
...defaultScreen,
description: undefined,
},
},
};
/** Экран с кастомным описанием */
export const CustomDescription: Story = {
args: {
screen: {
...defaultScreen,
description: {
text: "На основе ваших ответов мы создали уникальный **портрет вашей второй половинки**. Этот анализ поможет вам лучше понять, кто может стать идеальным партнером.",
font: "inter",
weight: "regular",
align: "center",
size: "md",
color: "default",
},
},
},
};
/** Экран без header */
export const WithoutHeader: Story = {
args: {
screen: {
...defaultScreen,
header: {
show: false,
},
},
},
};
/** Экран без subtitle */
export const WithoutSubtitle: Story = {
args: {
screen: {
...defaultScreen,
subtitle: {
...defaultScreen.subtitle,
show: false,
text: defaultScreen.subtitle?.text || "",
},
},
},
};
/** Финальный экран (без прогресса) */
export const FinalScreen: Story = {
args: {
screen: {
...defaultScreen,
header: {
show: true,
showBackButton: false, // На финальном экране обычно нет кнопки назад
showProgress: false, // И нет прогресса
},
},
screenProgress: undefined,
canGoBack: false,
},
};

View File

@ -0,0 +1 @@
export { SoulmatePortraitTemplate } from "./SoulmatePortraitTemplate";

View File

@ -1,4 +0,0 @@
// Content templates - informational and display screens
export { InfoTemplate } from "./InfoTemplate";
export { LoadersTemplate } from "./LoadersTemplate";
export { SoulmatePortraitTemplate } from "./SoulmatePortraitTemplate";

View File

@ -1,4 +0,0 @@
// Form templates - input and data collection screens
export { FormTemplate } from "./FormTemplate";
export { DateTemplate } from "./DateTemplate";
export { EmailTemplate } from "./EmailTemplate";

View File

@ -1,13 +1,12 @@
// Funnel templates organized by category
// Funnel Templates - каждый в своей папке с stories
export { InfoTemplate } from "./InfoTemplate";
export { ListTemplate } from "./ListTemplate";
export { DateTemplate } from "./DateTemplate";
export { FormTemplate } from "./FormTemplate";
export { EmailTemplate } from "./EmailTemplate";
export { CouponTemplate } from "./CouponTemplate";
export { LoadersTemplate } from "./LoadersTemplate";
export { SoulmatePortraitTemplate } from "./SoulmatePortraitTemplate";
// 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";
// Layout Templates
export { TemplateLayout } from "./layouts/TemplateLayout";

View File

@ -1,3 +0,0 @@
// Interactive templates - user choice and engagement screens
export { ListTemplate } from "./ListTemplate";
export { CouponTemplate } from "./CouponTemplate";

View File

@ -0,0 +1,241 @@
import { Meta, StoryObj } from "@storybook/nextjs-vite";
import { TemplateLayout } from "./TemplateLayout";
import { fn } from "storybook/test";
import type { InfoScreenDefinition } from "@/lib/funnel/types";
// Создаем mock экран для демонстрации TemplateLayout
const mockScreen: InfoScreenDefinition = {
id: "template-layout-demo",
template: "info",
header: {
show: true,
showBackButton: true,
showProgress: true,
},
title: {
text: "TemplateLayout Demo",
font: "manrope",
weight: "bold",
align: "center",
size: "2xl",
color: "default",
},
description: {
text: "Это демонстрация **TemplateLayout** - централизованного layout wrapper для всех funnel templates. Он управляет header, progress bar, кнопкой назад и нижней кнопкой.",
font: "inter",
weight: "regular",
align: "center",
size: "md",
color: "default",
},
bottomActionButton: {
show: true,
text: "Continue",
showGradientBlur: true,
},
navigation: {
defaultNextScreenId: undefined,
rules: [],
},
};
/** TemplateLayout - централизованный wrapper для всех funnel templates */
const meta: Meta<typeof TemplateLayout> = {
title: "Funnel Templates/TemplateLayout",
component: TemplateLayout,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
docs: {
description: {
component: `
TemplateLayout - это централизованный layout wrapper, который используется всеми funnel templates.
**Основные возможности:**
- Управление header (показать/скрыть, progress bar, кнопка назад)
- Bottom action button с gradient blur
- Privacy terms consent
- Динамическая высота для фиксированного нижнего контента
- Единообразные настройки типографики
**Архитектурные принципы:**
- Устраняет дублирование кода между templates
- Централизованная логика UI элементов
- Использует только существующие UI компоненты (LayoutQuestion, BottomActionButton)
`
}
}
},
args: {
screen: mockScreen,
canGoBack: true,
onBack: fn(),
screenProgress: { current: 5, total: 10 },
titleDefaults: { font: "manrope", weight: "bold", align: "center", size: "2xl", color: "default" },
subtitleDefaults: { font: "inter", weight: "medium", color: "default", align: "center", size: "lg" },
actionButtonOptions: {
defaultText: "Continue",
disabled: false,
onClick: fn(),
},
children: (
<div className="w-full flex flex-col items-center gap-4 py-8">
<div className="w-full max-w-sm text-center">
<p className="text-muted-foreground mb-4">
Это контент, который передается в TemplateLayout через children prop.
</p>
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="p-3 bg-muted/20 rounded-lg">
<strong>Header:</strong> управляется TemplateLayout
</div>
<div className="p-3 bg-muted/20 rounded-lg">
<strong>Content:</strong> передается через children
</div>
<div className="p-3 bg-muted/20 rounded-lg">
<strong>Button:</strong> управляется TemplateLayout
</div>
<div className="p-3 bg-muted/20 rounded-lg">
<strong>Blur:</strong> управляется TemplateLayout
</div>
</div>
</div>
</div>
),
},
argTypes: {
screen: {
control: { type: "object" },
description: "Screen definition с настройками header и bottomActionButton"
},
screenProgress: {
control: { type: "object" },
description: "Прогресс для отображения в header"
},
actionButtonOptions: {
control: { type: "object" },
description: "Настройки нижней кнопки (если передается, кнопка показывается)"
},
children: {
control: false,
description: "Контент template, который рендерится внутри layout"
},
onBack: { action: "back" },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
/** Дефолтный TemplateLayout со всеми элементами */
export const Default: Story = {};
/** Layout без header */
export const WithoutHeader: Story = {
args: {
screen: {
...mockScreen,
header: {
show: false,
},
},
},
};
/** Layout без progress bar */
export const WithoutProgressBar: Story = {
args: {
screen: {
...mockScreen,
header: {
show: true,
showBackButton: true,
showProgress: false,
},
},
},
};
/** Layout без кнопки назад */
export const WithoutBackButton: Story = {
args: {
screen: {
...mockScreen,
header: {
show: true,
showBackButton: false,
showProgress: true,
},
},
canGoBack: false,
},
};
/** Layout без нижней кнопки */
export const WithoutActionButton: Story = {
args: {
actionButtonOptions: undefined,
},
};
/** Layout только с blur gradient (без кнопки) */
export const OnlyBlurGradient: Story = {
args: {
screen: {
...mockScreen,
bottomActionButton: {
show: false,
showGradientBlur: true,
},
},
actionButtonOptions: undefined,
},
};
/** Layout с Privacy Terms Consent */
export const WithPrivacyConsent: Story = {
args: {
screen: {
...mockScreen,
bottomActionButton: {
...mockScreen.bottomActionButton,
showPrivacyTermsConsent: true,
},
},
},
};
/** Layout с disabled кнопкой */
export const WithDisabledButton: Story = {
args: {
actionButtonOptions: {
defaultText: "Continue",
disabled: true,
onClick: fn(),
},
},
};
/** Минимальный layout (только контент) */
export const Minimal: Story = {
args: {
screen: {
...mockScreen,
header: {
show: false,
},
bottomActionButton: {
show: false,
showGradientBlur: false,
},
},
actionButtonOptions: undefined,
children: (
<div className="w-full py-16 text-center">
<h1 className="text-2xl font-bold mb-4">Минимальный Layout</h1>
<p className="text-muted-foreground">
Только контент, без header и нижней кнопки
</p>
</div>
),
},
};

View File

@ -62,7 +62,6 @@ export function TemplateLayout({
}: TemplateLayoutProps) {
// 🎛️ ЦЕНТРАЛИЗОВАННАЯ ЛОГИКА BOTTOM BUTTON
const {
height: bottomActionButtonHeight,
elementRef: bottomActionButtonRef,
} = useDynamicSize<HTMLDivElement>({
defaultHeight: 132,
@ -115,10 +114,7 @@ export function TemplateLayout({
// 🎨 ЦЕНТРАЛИЗОВАННЫЙ РЕНДЕРИНГ
return (
<div
className="w-full"
style={{ paddingBottom: `${bottomActionButtonHeight}px` }}
>
<div className="w-full">
<LayoutQuestion {...layoutQuestionProps}>
{children}
</LayoutQuestion>

View File

@ -24,6 +24,7 @@ export function buildDefaultHeader(overrides?: Partial<HeaderDefinition>): Heade
return {
show: true,
showBackButton: true,
showProgress: true,
...overrides,
};
}

View File

@ -13,7 +13,9 @@ export function buildCouponDefaults(id: string): BuilderScreen {
return {
id,
template: "coupon",
header: buildDefaultHeader(),
header: buildDefaultHeader({
showProgress: false
}),
title: buildDefaultTitle({
text: "Тебе повезло!",
align: "center",

View File

@ -110,6 +110,10 @@ export function shouldShowBackButton(header?: HeaderDefinition, canGoBack?: bool
return Boolean(canGoBack);
}
export function shouldShowProgress(header?: HeaderDefinition) {
return header?.showProgress !== false;
}
export function shouldShowHeader(header?: HeaderDefinition) {
return header?.show !== false;
}
@ -175,14 +179,17 @@ export function buildLayoutQuestionProps(
const showBackButton = shouldShowBackButton(screen.header, canGoBack);
const showHeader = shouldShowHeader(screen.header);
const showProgress = shouldShowProgress(screen.header);
return {
headerProps: showHeader ? {
progressProps: screenProgress ? buildHeaderProgress({
current: screenProgress.current,
total: screenProgress.total,
label: `${screenProgress.current} of ${screenProgress.total}`
}) : buildHeaderProgress(screen.header?.progress),
progressProps: showProgress ? (
screenProgress ? buildHeaderProgress({
current: screenProgress.current,
total: screenProgress.total,
label: `${screenProgress.current} of ${screenProgress.total}`
}) : buildHeaderProgress(screen.header?.progress)
) : undefined,
onBack: showBackButton ? onBack : undefined,
showBackButton,
} : undefined,

View File

@ -39,6 +39,8 @@ export interface HeaderDefinition {
progress?: HeaderProgressDefinition;
/** Controls whether back button should be displayed. Defaults to true. */
showBackButton?: boolean;
/** Controls whether progress bar and label should be displayed. Defaults to true. */
showProgress?: boolean;
/** Controls whether header should be displayed at all. Defaults to true. */
show?: boolean;
}