payment
add solmate portrait page
This commit is contained in:
parent
21bedbcc53
commit
2f1d71d26d
@ -98,7 +98,7 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
||||
{/* Иконка */}
|
||||
<CollapsibleSection title="Иконка" defaultExpanded={true}>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="space-y-2 text-xs">
|
||||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||||
Тип иконки
|
||||
<select
|
||||
|
||||
@ -62,13 +62,15 @@ export function LoadersScreenConfig({ screen, onUpdate }: LoadersScreenConfigPro
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-3">Настройки анимации</h4>
|
||||
<TextInput
|
||||
label="Длительность анимации (мс)"
|
||||
type="number"
|
||||
placeholder="5000"
|
||||
value={screen.progressbars?.transitionDuration?.toString() || "5000"}
|
||||
onChange={(e) => updateProgressbars({ transitionDuration: parseInt(e.target.value) || 5000 })}
|
||||
/>
|
||||
<div className="space-y-3">
|
||||
<TextInput
|
||||
label="Длительность анимации (мс)"
|
||||
type="number"
|
||||
placeholder="5000"
|
||||
value={screen.progressbars?.transitionDuration?.toString() || "5000"}
|
||||
onChange={(e) => updateProgressbars({ transitionDuration: parseInt(e.target.value) || 5000 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -100,7 +102,7 @@ export function LoadersScreenConfig({ screen, onUpdate }: LoadersScreenConfigPro
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-3">
|
||||
<TextInput
|
||||
label="Заголовок"
|
||||
placeholder="Step 1"
|
||||
@ -115,7 +117,7 @@ export function LoadersScreenConfig({ screen, onUpdate }: LoadersScreenConfigPro
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-3">
|
||||
<TextInput
|
||||
label="Текст во время обработки"
|
||||
placeholder="Processing..."
|
||||
@ -130,7 +132,7 @@ export function LoadersScreenConfig({ screen, onUpdate }: LoadersScreenConfigPro
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-3">
|
||||
<TextInput
|
||||
label="Текст при завершении"
|
||||
placeholder="Completed!"
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
import React from "react";
|
||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ImageUpload } from "@/components/admin/builder/forms/ImageUpload";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||||
import type { SoulmatePortraitScreenDefinition } from "@/lib/funnel/types";
|
||||
|
||||
@ -11,6 +14,10 @@ interface SoulmatePortraitScreenConfigProps {
|
||||
}
|
||||
|
||||
export function SoulmatePortraitScreenConfig({ screen, onUpdate }: SoulmatePortraitScreenConfigProps) {
|
||||
type Delivered = NonNullable<SoulmatePortraitScreenDefinition["soulmatePortraitsDelivered"]>;
|
||||
type Avatar = NonNullable<Delivered["avatars"]>[number];
|
||||
type DeliveredText = NonNullable<Delivered["text"]>;
|
||||
|
||||
const updateDescription = (updates: Partial<SoulmatePortraitScreenDefinition["description"]>) => {
|
||||
onUpdate({
|
||||
description: screen.description ? {
|
||||
@ -20,6 +27,64 @@ export function SoulmatePortraitScreenConfig({ screen, onUpdate }: SoulmatePortr
|
||||
});
|
||||
};
|
||||
|
||||
const updateDelivered = (
|
||||
updates: Partial<NonNullable<SoulmatePortraitScreenDefinition["soulmatePortraitsDelivered"]>>
|
||||
) => {
|
||||
const base = screen.soulmatePortraitsDelivered ?? {};
|
||||
onUpdate({
|
||||
soulmatePortraitsDelivered: {
|
||||
...base,
|
||||
...updates,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateDeliveredText = (
|
||||
updates: Partial<DeliveredText>
|
||||
) => {
|
||||
const currentText = (screen.soulmatePortraitsDelivered?.text ?? { text: "" }) as DeliveredText;
|
||||
const nextText = { ...currentText, ...(updates as object) } as DeliveredText;
|
||||
updateDelivered({ text: nextText });
|
||||
};
|
||||
|
||||
const addAvatar = () => {
|
||||
const avatars = screen.soulmatePortraitsDelivered?.avatars ?? [];
|
||||
updateDelivered({ avatars: [...avatars, { src: "", alt: "", fallbackText: "" }] });
|
||||
};
|
||||
|
||||
const removeAvatar = (index: number) => {
|
||||
const avatars = screen.soulmatePortraitsDelivered?.avatars ?? [];
|
||||
updateDelivered({ avatars: avatars.filter((_, i) => i !== index) });
|
||||
};
|
||||
|
||||
const updateAvatar = (
|
||||
index: number,
|
||||
updates: Partial<Avatar>
|
||||
) => {
|
||||
const avatars = screen.soulmatePortraitsDelivered?.avatars ?? [];
|
||||
const next = avatars.map((a, i) => (i === index ? { ...a, ...updates } : a));
|
||||
updateDelivered({ avatars: next });
|
||||
};
|
||||
|
||||
const addTextListItem = () => {
|
||||
const items = screen.textList?.items ?? [];
|
||||
onUpdate({ textList: { items: [...items, { text: "" }] } });
|
||||
};
|
||||
|
||||
const removeTextListItem = (index: number) => {
|
||||
const items = screen.textList?.items ?? [];
|
||||
onUpdate({ textList: { items: items.filter((_, i) => i !== index) } });
|
||||
};
|
||||
|
||||
const updateTextListItem = (
|
||||
index: number,
|
||||
updates: Partial<NonNullable<SoulmatePortraitScreenDefinition["textList"]>["items"][number]>
|
||||
) => {
|
||||
const items = screen.textList?.items ?? [];
|
||||
const next = items.map((it, i) => (i === index ? { ...it, ...updates } : it));
|
||||
onUpdate({ textList: { items: next } });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@ -32,6 +97,119 @@ export function SoulmatePortraitScreenConfig({ screen, onUpdate }: SoulmatePortr
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-3">Блок доставленных портретов</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<span className="text-xs font-medium text-muted-foreground mb-2 block">Изображение</span>
|
||||
<ImageUpload
|
||||
currentValue={screen.soulmatePortraitsDelivered?.image}
|
||||
onImageSelect={(url) => updateDelivered({ image: url })}
|
||||
onImageRemove={() => updateDelivered({ image: undefined })}
|
||||
funnelId={screen.id}
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
label="Текст под изображением"
|
||||
placeholder="soulmate portraits delivered today"
|
||||
value={screen.soulmatePortraitsDelivered?.text?.text || ""}
|
||||
onChange={(e) => updateDeliveredText({ text: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-sm font-medium text-slate-700">Аватары</h4>
|
||||
<Button type="button" variant="outline" onClick={addAvatar} className="flex items-center gap-2 text-sm px-3 py-1">
|
||||
<Plus className="w-4 h-4" /> Добавить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{(screen.soulmatePortraitsDelivered?.avatars ?? []).map((avatar, index) => (
|
||||
<div key={index} className="border rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h5 className="text-sm font-medium text-slate-600">Аватар {index + 1}</h5>
|
||||
<Button type="button" variant="ghost" onClick={() => removeAvatar(index)} className="text-red-600 hover:text-red-700 text-sm px-2 py-1">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<span className="text-xs font-medium text-muted-foreground mb-2 block">Изображение</span>
|
||||
<ImageUpload
|
||||
currentValue={avatar.src}
|
||||
onImageSelect={(url) => updateAvatar(index, { src: url })}
|
||||
onImageRemove={() => updateAvatar(index, { src: "" })}
|
||||
funnelId={screen.id}
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
label="Alt"
|
||||
placeholder="Описание"
|
||||
value={avatar.alt || ""}
|
||||
onChange={(e) => updateAvatar(index, { alt: e.target.value })}
|
||||
/>
|
||||
<TextInput
|
||||
label="Fallback текст"
|
||||
placeholder="Напр. 900+"
|
||||
value={avatar.fallbackText || ""}
|
||||
onChange={(e) => updateAvatar(index, { fallbackText: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Можно указать изображение или fallback текст (или оба).</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{(screen.soulmatePortraitsDelivered?.avatars ?? []).length === 0 && (
|
||||
<div className="text-center py-6 text-slate-500">
|
||||
<p>Нет аватаров</p>
|
||||
<Button type="button" variant="outline" onClick={addAvatar} className="mt-2 text-sm px-3 py-1">
|
||||
Добавить первый
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-sm font-medium text-slate-700">Список текстов</h4>
|
||||
<Button type="button" variant="outline" onClick={addTextListItem} className="flex items-center gap-2 text-sm px-3 py-1">
|
||||
<Plus className="w-4 h-4" /> Добавить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{(screen.textList?.items ?? []).map((item, index) => (
|
||||
<div key={index} className="space-y-1">
|
||||
<TextInput
|
||||
label={`Элемент ${index + 1}`}
|
||||
placeholder="Текст элемента"
|
||||
value={item.text || ""}
|
||||
onChange={(e) => updateTextListItem(index, { text: e.target.value })}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button type="button" variant="ghost" onClick={() => removeTextListItem(index)} className="text-red-600 hover:text-red-700 text-sm px-2 py-1">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{(screen.textList?.items ?? []).length === 0 && (
|
||||
<div className="text-center py-6 text-slate-500">
|
||||
<p>Пока нет элементов</p>
|
||||
<Button type="button" variant="outline" onClick={addTextListItem} className="mt-2 text-sm px-3 py-1">
|
||||
Добавить первый
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-3">Информация</h4>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
|
||||
@ -1,110 +1,64 @@
|
||||
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;
|
||||
const defaultScreen: SoulmatePortraitScreenDefinition = {
|
||||
id: "soulmate-story",
|
||||
template: "soulmate",
|
||||
header: { show: false, showBackButton: false },
|
||||
title: { text: "Soulmate Portrait" },
|
||||
subtitle: { text: "Готов увидеть, кто твоя настоящая Родственная душа?" },
|
||||
description: {
|
||||
text: "Готов увидеть, кто твоя настоящая Родственная душа?",
|
||||
align: "center",
|
||||
},
|
||||
soulmatePortraitsDelivered: {
|
||||
image: "/soulmate-portrait-delivered-male.jpg",
|
||||
text: {
|
||||
text: "soulmate portraits delivered today",
|
||||
font: "inter",
|
||||
weight: "medium",
|
||||
size: "sm",
|
||||
color: "primary",
|
||||
},
|
||||
avatars: [
|
||||
{ src: "/avatars/male-1.jpg", alt: "Male 1" },
|
||||
{ src: "/avatars/male-2.jpg", alt: "Male 2" },
|
||||
{ src: "/avatars/male-3.jpg", alt: "Male 3" },
|
||||
{ src: "", fallbackText: "900+" },
|
||||
],
|
||||
},
|
||||
textList: {
|
||||
items: [
|
||||
{
|
||||
text: "Всего 2 минуты — и Портрет откроет того, кто связан с тобой судьбой.",
|
||||
},
|
||||
{ text: "Поразительная точность 99%." },
|
||||
{ text: "Тебя ждёт неожиданное открытие." },
|
||||
{ text: "Осталось лишь осмелиться взглянуть." },
|
||||
],
|
||||
},
|
||||
bottomActionButton: { text: "Continue", showPrivacyTermsConsent: true },
|
||||
};
|
||||
|
||||
/** SoulmatePortraitTemplate - результирующие экраны с портретом партнера */
|
||||
const meta: Meta<typeof SoulmatePortraitTemplate> = {
|
||||
title: "Funnel Templates/SoulmatePortraitTemplate",
|
||||
component: SoulmatePortraitTemplate,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
screen: defaultScreen,
|
||||
onContinue: fn(),
|
||||
canGoBack: true,
|
||||
onBack: fn(),
|
||||
screenProgress: { current: 10, total: 10 }, // Обычно финальный экран
|
||||
defaultTexts: {
|
||||
nextButton: "Next",
|
||||
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
screen: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
screenProgress: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
onContinue: { action: "continue" },
|
||||
onBack: { action: "back" },
|
||||
screenProgress: undefined,
|
||||
defaultTexts: { nextButton: "Next" },
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
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: undefined, // Просто удаляем subtitle
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** Финальный экран (без прогресса) */
|
||||
export const FinalScreen: Story = {
|
||||
args: {
|
||||
screen: {
|
||||
...defaultScreen,
|
||||
header: {
|
||||
show: true,
|
||||
showBackButton: false, // На финальном экране обычно нет кнопки назад
|
||||
showProgress: false, // И нет прогресса
|
||||
},
|
||||
},
|
||||
screenProgress: undefined,
|
||||
canGoBack: false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import type { SoulmatePortraitScreenDefinition, DefaultTexts } from "@/lib/funnel/types";
|
||||
import type {
|
||||
SoulmatePortraitScreenDefinition,
|
||||
DefaultTexts,
|
||||
} from "@/lib/funnel/types";
|
||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||
import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers";
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
import { buildTypographyProps } from "@/lib/funnel/mappers";
|
||||
import SoulmatePortraitsDelivered from "@/components/widgets/SoulmatePortraitsDelivered/SoulmatePortraitsDelivered";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SoulmatePortraitTemplateProps {
|
||||
screen: SoulmatePortraitScreenDefinition;
|
||||
@ -21,14 +28,26 @@ export function SoulmatePortraitTemplate({
|
||||
screenProgress,
|
||||
defaultTexts,
|
||||
}: SoulmatePortraitTemplateProps) {
|
||||
// Скрываем subtitle как ненужный для этого экрана
|
||||
const screenForLayout: SoulmatePortraitScreenDefinition = {
|
||||
...screen,
|
||||
subtitle: undefined,
|
||||
};
|
||||
|
||||
const layoutProps = createTemplateLayoutProps(
|
||||
screen,
|
||||
screenForLayout,
|
||||
{ canGoBack, onBack },
|
||||
screenProgress,
|
||||
{
|
||||
preset: "center",
|
||||
titleDefaults: { font: "manrope", weight: "bold", align: "center", size: "xl", color: "primary" },
|
||||
subtitleDefaults: { font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" },
|
||||
titleDefaults: {
|
||||
font: "manrope",
|
||||
weight: "bold",
|
||||
align: "center",
|
||||
size: "xl",
|
||||
className: "leading-[125%] text-primary text-xl",
|
||||
},
|
||||
subtitleDefaults: undefined,
|
||||
actionButton: {
|
||||
defaultText: defaultTexts?.nextButton || "Continue",
|
||||
disabled: false,
|
||||
@ -39,7 +58,91 @@ export function SoulmatePortraitTemplate({
|
||||
|
||||
return (
|
||||
<TemplateLayout {...layoutProps}>
|
||||
<div className="-mt-[20px]">
|
||||
<div className="max-w-[560px] mx-auto flex flex-col items-center gap-[30px]">
|
||||
{screen.soulmatePortraitsDelivered && (
|
||||
<SoulmatePortraitsDelivered
|
||||
image={screen.soulmatePortraitsDelivered.image}
|
||||
textProps={
|
||||
screen.soulmatePortraitsDelivered.text
|
||||
? buildTypographyProps(screen.soulmatePortraitsDelivered.text, {
|
||||
as: "p",
|
||||
defaults: { font: "inter", size: "sm", color: "primary" },
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
avatarsProps={
|
||||
screen.soulmatePortraitsDelivered.avatars
|
||||
? {
|
||||
avatars: screen.soulmatePortraitsDelivered.avatars.map(
|
||||
(a) => ({
|
||||
imageProps: a.src
|
||||
? { src: a.src, alt: a.alt ?? "" }
|
||||
: undefined,
|
||||
fallbackProps: a.fallbackText
|
||||
? {
|
||||
children: (
|
||||
<Typography
|
||||
size="xs"
|
||||
weight="bold"
|
||||
className="text-[#FF6B9D]"
|
||||
>
|
||||
{a.fallbackText}
|
||||
</Typography>
|
||||
),
|
||||
className: "bg-background",
|
||||
}
|
||||
: undefined,
|
||||
className: a.fallbackText ? "w-fit px-1" : undefined,
|
||||
})
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className="w-full flex flex-col items-center gap-2.5">
|
||||
{screen.description &&
|
||||
(() => {
|
||||
const descProps = buildTypographyProps(screen.description, {
|
||||
as: "p",
|
||||
defaults: {
|
||||
align: "center",
|
||||
font: "inter",
|
||||
size: "md",
|
||||
weight: "bold",
|
||||
className: "text-[25px] font-bold",
|
||||
},
|
||||
});
|
||||
if (!descProps) return null;
|
||||
const { children, ...rest } = descProps;
|
||||
return (
|
||||
<Typography {...rest} enableMarkup>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
})()}
|
||||
{screen.textList && (
|
||||
<ul className={cn("list-disc pl-6 w-full")}>
|
||||
{screen.textList.items.map((item, index) => {
|
||||
const itemProps = buildTypographyProps(item, {
|
||||
as: "li",
|
||||
defaults: { font: "inter", weight: "medium", size: "md" },
|
||||
});
|
||||
if (!itemProps) return null;
|
||||
const { children, ...rest } = itemProps;
|
||||
return (
|
||||
<Typography
|
||||
key={index}
|
||||
{...rest}
|
||||
className={cn("list-item text-[17px] leading-[26px]")}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TemplateLayout>
|
||||
);
|
||||
|
||||
@ -16,39 +16,57 @@ interface TemplateLayoutProps {
|
||||
canGoBack: boolean;
|
||||
onBack: () => void;
|
||||
screenProgress?: { current: number; total: number };
|
||||
|
||||
|
||||
// Настройки template
|
||||
titleDefaults?: {
|
||||
font?: "manrope" | "inter" | "geistSans" | "geistMono";
|
||||
weight?: "regular" | "medium" | "semiBold" | "bold" | "extraBold" | "black";
|
||||
align?: "left" | "center" | "right";
|
||||
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
||||
color?: "default" | "primary" | "secondary" | "destructive" | "success" | "card" | "accent" | "muted";
|
||||
color?:
|
||||
| "default"
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "destructive"
|
||||
| "success"
|
||||
| "card"
|
||||
| "accent"
|
||||
| "muted";
|
||||
className?: string;
|
||||
};
|
||||
subtitleDefaults?: {
|
||||
font?: "manrope" | "inter" | "geistSans" | "geistMono";
|
||||
weight?: "regular" | "medium" | "semiBold" | "bold" | "extraBold" | "black";
|
||||
color?: "default" | "primary" | "secondary" | "destructive" | "success" | "card" | "accent" | "muted";
|
||||
color?:
|
||||
| "default"
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "destructive"
|
||||
| "success"
|
||||
| "card"
|
||||
| "accent"
|
||||
| "muted";
|
||||
align?: "left" | "center" | "right";
|
||||
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
||||
className?: string;
|
||||
};
|
||||
actionButtonOptions?: {
|
||||
defaultText: string;
|
||||
disabled: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
|
||||
// Дополнительные props для BottomActionButton
|
||||
childrenAboveButton?: React.ReactNode;
|
||||
childrenUnderButton?: React.ReactNode;
|
||||
|
||||
|
||||
// Дополнительные props для Title
|
||||
childrenAboveTitle?: React.ReactNode;
|
||||
|
||||
|
||||
// Переопределения стилей LayoutQuestion (контент и обертка контента)
|
||||
contentProps?: React.ComponentProps<"div">;
|
||||
childrenWrapperProps?: React.ComponentProps<"div">;
|
||||
|
||||
|
||||
// Контент template
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@ -62,8 +80,20 @@ export function TemplateLayout({
|
||||
canGoBack,
|
||||
onBack,
|
||||
screenProgress,
|
||||
titleDefaults = { font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" },
|
||||
subtitleDefaults = { font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" },
|
||||
titleDefaults = {
|
||||
font: "manrope",
|
||||
weight: "bold",
|
||||
align: "left",
|
||||
size: "2xl",
|
||||
color: "default",
|
||||
},
|
||||
subtitleDefaults = {
|
||||
font: "manrope",
|
||||
weight: "medium",
|
||||
color: "default",
|
||||
align: "left",
|
||||
size: "lg",
|
||||
},
|
||||
actionButtonOptions,
|
||||
childrenAboveButton,
|
||||
childrenUnderButton,
|
||||
@ -73,9 +103,7 @@ export function TemplateLayout({
|
||||
children,
|
||||
}: TemplateLayoutProps) {
|
||||
// 🎛️ ЦЕНТРАЛИЗОВАННАЯ ЛОГИКА BOTTOM BUTTON
|
||||
const {
|
||||
elementRef: bottomActionButtonRef,
|
||||
} = useDynamicSize<HTMLDivElement>({
|
||||
const { elementRef: bottomActionButtonRef } = useDynamicSize<HTMLDivElement>({
|
||||
defaultHeight: 132,
|
||||
});
|
||||
|
||||
@ -98,20 +126,20 @@ export function TemplateLayout({
|
||||
: undefined;
|
||||
|
||||
// 🎯 Автоматически создаем PrivacyTermsConsent с фиксированными настройками
|
||||
const shouldShowPrivacyTermsConsent =
|
||||
'bottomActionButton' in screen &&
|
||||
const shouldShowPrivacyTermsConsent =
|
||||
"bottomActionButton" in screen &&
|
||||
screen.bottomActionButton?.showPrivacyTermsConsent === true;
|
||||
|
||||
const autoPrivacyTermsConsent = shouldShowPrivacyTermsConsent ? (
|
||||
<PrivacyTermsConsent
|
||||
<PrivacyTermsConsent
|
||||
className="mt-5"
|
||||
privacyPolicy={{
|
||||
href: "/privacy",
|
||||
children: "Privacy Policy"
|
||||
children: "Privacy Policy",
|
||||
}}
|
||||
termsOfUse={{
|
||||
href: "/terms",
|
||||
children: "Terms of use"
|
||||
children: "Terms of use",
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
@ -137,8 +165,8 @@ export function TemplateLayout({
|
||||
</LayoutQuestion>
|
||||
|
||||
{bottomActionButtonProps && (
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
ref={bottomActionButtonRef}
|
||||
childrenAboveButton={childrenAboveButton}
|
||||
childrenUnderButton={finalChildrenUnderButton}
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Header } from "@/components/layout/Header/Header";
|
||||
import Typography, { TypographyProps } from "@/components/ui/Typography/Typography";
|
||||
import Typography, {
|
||||
TypographyProps,
|
||||
} from "@/components/ui/Typography/Typography";
|
||||
|
||||
export interface LayoutQuestionProps
|
||||
extends Omit<React.ComponentProps<"section">, "title" | "content"> {
|
||||
@ -55,7 +57,7 @@ function LayoutQuestion({
|
||||
weight="bold"
|
||||
{...title}
|
||||
align={title.align ?? "left"}
|
||||
className={cn(title.className, "w-full text-[25px] leading-[38px]")}
|
||||
className={cn("w-full text-[25px] leading-[38px]", title.className)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -66,8 +68,8 @@ function LayoutQuestion({
|
||||
{...subtitle}
|
||||
align={subtitle.align ?? "left"}
|
||||
className={cn(
|
||||
subtitle.className,
|
||||
"w-full mt-2.5 text-[17px] leading-[26px]"
|
||||
"w-full mt-2.5 text-[17px] leading-[26px]",
|
||||
subtitle.className
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@ -83,4 +85,4 @@ function LayoutQuestion({
|
||||
);
|
||||
}
|
||||
|
||||
export { LayoutQuestion };
|
||||
export { LayoutQuestion };
|
||||
|
||||
@ -60,6 +60,7 @@ export const useAuth = ({ funnelId }: IUseAuthProps) => {
|
||||
sign: true,
|
||||
signDate: new Date().toISOString(),
|
||||
// feature: feature.includes("black") ? "ios" : feature,
|
||||
feature: "stripe"
|
||||
});
|
||||
},
|
||||
[funnelId]
|
||||
|
||||
@ -54,6 +54,7 @@ export const useSession = ({ funnelId }: IUseSessionProps) => {
|
||||
try {
|
||||
const utm = parseQueryParams();
|
||||
const sessionParams = {
|
||||
feature: "stripe",
|
||||
locale,
|
||||
timezone,
|
||||
// source: funnelId,
|
||||
@ -102,7 +103,10 @@ export const useSession = ({ funnelId }: IUseSessionProps) => {
|
||||
}
|
||||
const result = await updateSessionApi({
|
||||
sessionId: _sessionId,
|
||||
data,
|
||||
data: {
|
||||
feature: "stripe",
|
||||
...data,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||||
import {
|
||||
buildDefaultHeader,
|
||||
import {
|
||||
buildDefaultHeader,
|
||||
buildDefaultTitle,
|
||||
buildDefaultSubtitle,
|
||||
buildDefaultBottomActionButton,
|
||||
buildDefaultNavigation,
|
||||
buildDefaultDescription
|
||||
buildDefaultDescription,
|
||||
} from "./blocks";
|
||||
|
||||
export function buildSoulmateDefaults(id: string): BuilderScreen {
|
||||
@ -19,12 +19,39 @@ export function buildSoulmateDefaults(id: string): BuilderScreen {
|
||||
title: buildDefaultTitle(),
|
||||
subtitle: buildDefaultSubtitle(),
|
||||
bottomActionButton: buildDefaultBottomActionButton({
|
||||
text: "Получить полный анализ",
|
||||
text: "Continue",
|
||||
showPrivacyTermsConsent: true,
|
||||
}),
|
||||
description: buildDefaultDescription({
|
||||
text: "Ваш персональный портрет почти готов.",
|
||||
text: "Готов увидеть, кто твоя настоящая Родственная душа?",
|
||||
align: "center",
|
||||
}),
|
||||
soulmatePortraitsDelivered: {
|
||||
image: "/soulmate-portrait-delivered-male.jpg",
|
||||
text: {
|
||||
text: "soulmate portraits delivered today",
|
||||
font: "inter",
|
||||
weight: "medium",
|
||||
size: "sm",
|
||||
color: "primary",
|
||||
},
|
||||
avatars: [
|
||||
{ src: "/avatars/male-1.jpg", alt: "Male 1" },
|
||||
{ src: "/avatars/male-2.jpg", alt: "Male 2" },
|
||||
{ src: "/avatars/male-3.jpg", alt: "Male 3" },
|
||||
{ src: "", fallbackText: "900+" },
|
||||
],
|
||||
},
|
||||
textList: {
|
||||
items: [
|
||||
{
|
||||
text: "Всего 2 минуты — и Портрет откроет того, кто связан с тобой судьбой.",
|
||||
},
|
||||
{ text: "Поразительная точность 99%." },
|
||||
{ text: "Тебя ждёт неожиданное открытие." },
|
||||
{ text: "Осталось лишь осмелиться взглянуть." },
|
||||
],
|
||||
},
|
||||
navigation: buildDefaultNavigation(),
|
||||
} as BuilderScreen;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { TypographyProps } from "@/components/ui/Typography/Typography";
|
||||
import type { MainButtonProps } from "@/components/ui/MainButton/MainButton";
|
||||
import { hasTextMarkup } from "@/lib/text-markup";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import type {
|
||||
HeaderDefinition,
|
||||
@ -34,6 +35,7 @@ interface TypographyDefaults {
|
||||
size?: TypographyVariant["size"];
|
||||
align?: TypographyVariant["align"];
|
||||
color?: TypographyVariant["color"];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface BuildTypographyOptions<T extends TypographyAs> {
|
||||
@ -69,7 +71,7 @@ export function buildTypographyProps<T extends TypographyAs>(
|
||||
size: variant.size ?? defaults?.size,
|
||||
align: variant.align ?? defaults?.align,
|
||||
color: variant.color ?? defaults?.color,
|
||||
className: variant.className,
|
||||
className: cn(defaults?.className, variant.className),
|
||||
enableMarkup: hasTextMarkup(variant.text || ""), // 🎨 АВТОМАТИЧЕСКИ включаем разметку если обнаружена
|
||||
} as TypographyProps<T>;
|
||||
}
|
||||
|
||||
@ -326,7 +326,19 @@ export interface SoulmatePortraitScreenDefinition {
|
||||
header?: HeaderDefinition;
|
||||
title: TitleDefinition;
|
||||
subtitle?: SubtitleDefinition;
|
||||
description?: TypographyVariant; // 🎯 Настраиваемый текст описания
|
||||
description?: TypographyVariant; // Настраиваемый текст описания
|
||||
soulmatePortraitsDelivered?: {
|
||||
image?: string;
|
||||
text?: TypographyVariant;
|
||||
avatars?: Array<{
|
||||
src: string;
|
||||
alt?: string;
|
||||
fallbackText?: string;
|
||||
}>;
|
||||
};
|
||||
textList?: {
|
||||
items: TypographyVariant[];
|
||||
};
|
||||
bottomActionButton?: BottomActionButtonDefinition;
|
||||
navigation?: NavigationDefinition;
|
||||
variants?: ScreenVariantDefinition<SoulmatePortraitScreenDefinition>[];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user