story
This commit is contained in:
parent
e98b1bfc05
commit
b28d22967f
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
1
src/components/funnel/templates/CouponTemplate/index.ts
Normal file
1
src/components/funnel/templates/CouponTemplate/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { CouponTemplate } from "./CouponTemplate";
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -9,6 +9,21 @@ import type { DateScreenDefinition } from "@/lib/funnel/types";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
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 {
|
interface DateTemplateProps {
|
||||||
screen: DateScreenDefinition;
|
screen: DateScreenDefinition;
|
||||||
selectedDate: { month?: string; day?: string; year?: string };
|
selectedDate: { month?: string; day?: string; year?: string };
|
||||||
@ -67,6 +82,38 @@ export function DateTemplate({
|
|||||||
|
|
||||||
const isFormValid = Boolean(isoDate);
|
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 (
|
return (
|
||||||
<TemplateLayout
|
<TemplateLayout
|
||||||
screen={screen}
|
screen={screen}
|
||||||
@ -80,6 +127,7 @@ export function DateTemplate({
|
|||||||
disabled: !isFormValid,
|
disabled: !isFormValid,
|
||||||
onClick: onContinue,
|
onClick: onContinue,
|
||||||
}}
|
}}
|
||||||
|
childrenUnderButton={selectedDateDisplay}
|
||||||
>
|
>
|
||||||
<div className="w-full mt-[22px] space-y-6">
|
<div className="w-full mt-[22px] space-y-6">
|
||||||
<DateInput
|
<DateInput
|
||||||
1
src/components/funnel/templates/DateTemplate/index.ts
Normal file
1
src/components/funnel/templates/DateTemplate/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { DateTemplate } from "./DateTemplate";
|
||||||
@ -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...",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
1
src/components/funnel/templates/EmailTemplate/index.ts
Normal file
1
src/components/funnel/templates/EmailTemplate/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { EmailTemplate } from "./EmailTemplate";
|
||||||
@ -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 || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
1
src/components/funnel/templates/FormTemplate/index.ts
Normal file
1
src/components/funnel/templates/FormTemplate/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { FormTemplate } from "./FormTemplate";
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
1
src/components/funnel/templates/InfoTemplate/index.ts
Normal file
1
src/components/funnel/templates/InfoTemplate/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { InfoTemplate } from "./InfoTemplate";
|
||||||
@ -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, // Используем базовые дефолты без эмодзи
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -93,7 +93,8 @@ export function ListTemplate({
|
|||||||
|
|
||||||
const actionButtonOptions = actionButtonProps ? {
|
const actionButtonOptions = actionButtonProps ? {
|
||||||
defaultText: actionButtonProps.children as string || "Next",
|
defaultText: actionButtonProps.children as string || "Next",
|
||||||
disabled: actionButtonProps.disabled || false,
|
// Кнопка неактивна если: 1) disabled из props ИЛИ 2) ничего не выбрано
|
||||||
|
disabled: actionButtonProps.disabled || selectedOptionIds.length === 0,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (actionButtonProps.onClick) {
|
if (actionButtonProps.onClick) {
|
||||||
actionButtonProps.onClick({} as React.MouseEvent<HTMLButtonElement>);
|
actionButtonProps.onClick({} as React.MouseEvent<HTMLButtonElement>);
|
||||||
1
src/components/funnel/templates/ListTemplate/index.ts
Normal file
1
src/components/funnel/templates/ListTemplate/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { ListTemplate } from "./ListTemplate";
|
||||||
@ -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 },
|
||||||
|
},
|
||||||
|
};
|
||||||
1
src/components/funnel/templates/LoadersTemplate/index.ts
Normal file
1
src/components/funnel/templates/LoadersTemplate/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { LoadersTemplate } from "./LoadersTemplate";
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { SoulmatePortraitTemplate } from "./SoulmatePortraitTemplate";
|
||||||
@ -1,4 +0,0 @@
|
|||||||
// Content templates - informational and display screens
|
|
||||||
export { InfoTemplate } from "./InfoTemplate";
|
|
||||||
export { LoadersTemplate } from "./LoadersTemplate";
|
|
||||||
export { SoulmatePortraitTemplate } from "./SoulmatePortraitTemplate";
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
// Form templates - input and data collection screens
|
|
||||||
export { FormTemplate } from "./FormTemplate";
|
|
||||||
export { DateTemplate } from "./DateTemplate";
|
|
||||||
export { EmailTemplate } from "./EmailTemplate";
|
|
||||||
@ -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)
|
// Layout Templates
|
||||||
export * from "./content";
|
export { TemplateLayout } from "./layouts/TemplateLayout";
|
||||||
|
|
||||||
// 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";
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
// Interactive templates - user choice and engagement screens
|
|
||||||
export { ListTemplate } from "./ListTemplate";
|
|
||||||
export { CouponTemplate } from "./CouponTemplate";
|
|
||||||
@ -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>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -62,7 +62,6 @@ export function TemplateLayout({
|
|||||||
}: TemplateLayoutProps) {
|
}: TemplateLayoutProps) {
|
||||||
// 🎛️ ЦЕНТРАЛИЗОВАННАЯ ЛОГИКА BOTTOM BUTTON
|
// 🎛️ ЦЕНТРАЛИЗОВАННАЯ ЛОГИКА BOTTOM BUTTON
|
||||||
const {
|
const {
|
||||||
height: bottomActionButtonHeight,
|
|
||||||
elementRef: bottomActionButtonRef,
|
elementRef: bottomActionButtonRef,
|
||||||
} = useDynamicSize<HTMLDivElement>({
|
} = useDynamicSize<HTMLDivElement>({
|
||||||
defaultHeight: 132,
|
defaultHeight: 132,
|
||||||
@ -115,10 +114,7 @@ export function TemplateLayout({
|
|||||||
|
|
||||||
// 🎨 ЦЕНТРАЛИЗОВАННЫЙ РЕНДЕРИНГ
|
// 🎨 ЦЕНТРАЛИЗОВАННЫЙ РЕНДЕРИНГ
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="w-full">
|
||||||
className="w-full"
|
|
||||||
style={{ paddingBottom: `${bottomActionButtonHeight}px` }}
|
|
||||||
>
|
|
||||||
<LayoutQuestion {...layoutQuestionProps}>
|
<LayoutQuestion {...layoutQuestionProps}>
|
||||||
{children}
|
{children}
|
||||||
</LayoutQuestion>
|
</LayoutQuestion>
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export function buildDefaultHeader(overrides?: Partial<HeaderDefinition>): Heade
|
|||||||
return {
|
return {
|
||||||
show: true,
|
show: true,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
|
showProgress: true,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,9 @@ export function buildCouponDefaults(id: string): BuilderScreen {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
template: "coupon",
|
template: "coupon",
|
||||||
header: buildDefaultHeader(),
|
header: buildDefaultHeader({
|
||||||
|
showProgress: false
|
||||||
|
}),
|
||||||
title: buildDefaultTitle({
|
title: buildDefaultTitle({
|
||||||
text: "Тебе повезло!",
|
text: "Тебе повезло!",
|
||||||
align: "center",
|
align: "center",
|
||||||
|
|||||||
@ -110,6 +110,10 @@ export function shouldShowBackButton(header?: HeaderDefinition, canGoBack?: bool
|
|||||||
return Boolean(canGoBack);
|
return Boolean(canGoBack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldShowProgress(header?: HeaderDefinition) {
|
||||||
|
return header?.showProgress !== false;
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldShowHeader(header?: HeaderDefinition) {
|
export function shouldShowHeader(header?: HeaderDefinition) {
|
||||||
return header?.show !== false;
|
return header?.show !== false;
|
||||||
}
|
}
|
||||||
@ -175,14 +179,17 @@ export function buildLayoutQuestionProps(
|
|||||||
|
|
||||||
const showBackButton = shouldShowBackButton(screen.header, canGoBack);
|
const showBackButton = shouldShowBackButton(screen.header, canGoBack);
|
||||||
const showHeader = shouldShowHeader(screen.header);
|
const showHeader = shouldShowHeader(screen.header);
|
||||||
|
const showProgress = shouldShowProgress(screen.header);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
headerProps: showHeader ? {
|
headerProps: showHeader ? {
|
||||||
progressProps: screenProgress ? buildHeaderProgress({
|
progressProps: showProgress ? (
|
||||||
current: screenProgress.current,
|
screenProgress ? buildHeaderProgress({
|
||||||
total: screenProgress.total,
|
current: screenProgress.current,
|
||||||
label: `${screenProgress.current} of ${screenProgress.total}`
|
total: screenProgress.total,
|
||||||
}) : buildHeaderProgress(screen.header?.progress),
|
label: `${screenProgress.current} of ${screenProgress.total}`
|
||||||
|
}) : buildHeaderProgress(screen.header?.progress)
|
||||||
|
) : undefined,
|
||||||
onBack: showBackButton ? onBack : undefined,
|
onBack: showBackButton ? onBack : undefined,
|
||||||
showBackButton,
|
showBackButton,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
|||||||
@ -39,6 +39,8 @@ export interface HeaderDefinition {
|
|||||||
progress?: HeaderProgressDefinition;
|
progress?: HeaderProgressDefinition;
|
||||||
/** Controls whether back button should be displayed. Defaults to true. */
|
/** Controls whether back button should be displayed. Defaults to true. */
|
||||||
showBackButton?: boolean;
|
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. */
|
/** Controls whether header should be displayed at all. Defaults to true. */
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user