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}>
|
<CollapsibleSection title="Иконка" defaultExpanded={true}>
|
||||||
<div className="space-y-3">
|
<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">
|
<label className="flex flex-col gap-1 text-muted-foreground">
|
||||||
Тип иконки
|
Тип иконки
|
||||||
<select
|
<select
|
||||||
|
|||||||
@ -62,13 +62,15 @@ export function LoadersScreenConfig({ screen, onUpdate }: LoadersScreenConfigPro
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium text-slate-700 mb-3">Настройки анимации</h4>
|
<h4 className="text-sm font-medium text-slate-700 mb-3">Настройки анимации</h4>
|
||||||
<TextInput
|
<div className="space-y-3">
|
||||||
label="Длительность анимации (мс)"
|
<TextInput
|
||||||
type="number"
|
label="Длительность анимации (мс)"
|
||||||
placeholder="5000"
|
type="number"
|
||||||
value={screen.progressbars?.transitionDuration?.toString() || "5000"}
|
placeholder="5000"
|
||||||
onChange={(e) => updateProgressbars({ transitionDuration: parseInt(e.target.value) || 5000 })}
|
value={screen.progressbars?.transitionDuration?.toString() || "5000"}
|
||||||
/>
|
onChange={(e) => updateProgressbars({ transitionDuration: parseInt(e.target.value) || 5000 })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -100,7 +102,7 @@ export function LoadersScreenConfig({ screen, onUpdate }: LoadersScreenConfigPro
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="space-y-3">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Заголовок"
|
label="Заголовок"
|
||||||
placeholder="Step 1"
|
placeholder="Step 1"
|
||||||
@ -115,7 +117,7 @@ export function LoadersScreenConfig({ screen, onUpdate }: LoadersScreenConfigPro
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="space-y-3">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Текст во время обработки"
|
label="Текст во время обработки"
|
||||||
placeholder="Processing..."
|
placeholder="Processing..."
|
||||||
@ -130,7 +132,7 @@ export function LoadersScreenConfig({ screen, onUpdate }: LoadersScreenConfigPro
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="space-y-3">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Текст при завершении"
|
label="Текст при завершении"
|
||||||
placeholder="Completed!"
|
placeholder="Completed!"
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
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 { BuilderScreen } from "@/lib/admin/builder/types";
|
||||||
import type { SoulmatePortraitScreenDefinition } from "@/lib/funnel/types";
|
import type { SoulmatePortraitScreenDefinition } from "@/lib/funnel/types";
|
||||||
|
|
||||||
@ -11,6 +14,10 @@ interface SoulmatePortraitScreenConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SoulmatePortraitScreenConfig({ screen, onUpdate }: 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"]>) => {
|
const updateDescription = (updates: Partial<SoulmatePortraitScreenDefinition["description"]>) => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
description: screen.description ? {
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@ -32,6 +97,119 @@ export function SoulmatePortraitScreenConfig({ screen, onUpdate }: SoulmatePortr
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<h4 className="text-sm font-medium text-slate-700 mb-3">Информация</h4>
|
<h4 className="text-sm font-medium text-slate-700 mb-3">Информация</h4>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
|
|||||||
@ -1,110 +1,64 @@
|
|||||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||||
import { SoulmatePortraitTemplate } from "./SoulmatePortraitTemplate";
|
import { SoulmatePortraitTemplate } from "./SoulmatePortraitTemplate";
|
||||||
import { fn } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
import { buildSoulmateDefaults } from "@/lib/admin/builder/state/defaults/soulmate";
|
|
||||||
import type { SoulmatePortraitScreenDefinition } from "@/lib/funnel/types";
|
import type { SoulmatePortraitScreenDefinition } from "@/lib/funnel/types";
|
||||||
|
|
||||||
// Получаем дефолтные значения из builder
|
const defaultScreen: SoulmatePortraitScreenDefinition = {
|
||||||
const defaultScreen = buildSoulmateDefaults("soulmate-screen-story") as 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> = {
|
const meta: Meta<typeof SoulmatePortraitTemplate> = {
|
||||||
title: "Funnel Templates/SoulmatePortraitTemplate",
|
title: "Funnel Templates/SoulmatePortraitTemplate",
|
||||||
component: SoulmatePortraitTemplate,
|
component: SoulmatePortraitTemplate,
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
parameters: {
|
parameters: { layout: "fullscreen" },
|
||||||
layout: "fullscreen",
|
|
||||||
},
|
|
||||||
args: {
|
args: {
|
||||||
screen: defaultScreen,
|
screen: defaultScreen,
|
||||||
onContinue: fn(),
|
onContinue: fn(),
|
||||||
canGoBack: true,
|
canGoBack: true,
|
||||||
onBack: fn(),
|
onBack: fn(),
|
||||||
screenProgress: { current: 10, total: 10 }, // Обычно финальный экран
|
screenProgress: undefined,
|
||||||
defaultTexts: {
|
defaultTexts: { nextButton: "Next" },
|
||||||
nextButton: "Next",
|
|
||||||
|
|
||||||
},
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
screen: {
|
|
||||||
control: { type: "object" },
|
|
||||||
},
|
|
||||||
screenProgress: {
|
|
||||||
control: { type: "object" },
|
|
||||||
},
|
|
||||||
onContinue: { action: "continue" },
|
|
||||||
onBack: { action: "back" },
|
|
||||||
},
|
},
|
||||||
|
argTypes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
/** Дефолтный soulmate portrait экран */
|
|
||||||
export const Default: Story = {};
|
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";
|
"use client";
|
||||||
|
|
||||||
import type { SoulmatePortraitScreenDefinition, DefaultTexts } from "@/lib/funnel/types";
|
import type {
|
||||||
|
SoulmatePortraitScreenDefinition,
|
||||||
|
DefaultTexts,
|
||||||
|
} from "@/lib/funnel/types";
|
||||||
import { TemplateLayout } from "../layouts/TemplateLayout";
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
||||||
import { createTemplateLayoutProps } from "@/lib/funnel/templateHelpers";
|
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 {
|
interface SoulmatePortraitTemplateProps {
|
||||||
screen: SoulmatePortraitScreenDefinition;
|
screen: SoulmatePortraitScreenDefinition;
|
||||||
@ -21,14 +28,26 @@ export function SoulmatePortraitTemplate({
|
|||||||
screenProgress,
|
screenProgress,
|
||||||
defaultTexts,
|
defaultTexts,
|
||||||
}: SoulmatePortraitTemplateProps) {
|
}: SoulmatePortraitTemplateProps) {
|
||||||
|
// Скрываем subtitle как ненужный для этого экрана
|
||||||
|
const screenForLayout: SoulmatePortraitScreenDefinition = {
|
||||||
|
...screen,
|
||||||
|
subtitle: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
const layoutProps = createTemplateLayoutProps(
|
const layoutProps = createTemplateLayoutProps(
|
||||||
screen,
|
screenForLayout,
|
||||||
{ canGoBack, onBack },
|
{ canGoBack, onBack },
|
||||||
screenProgress,
|
screenProgress,
|
||||||
{
|
{
|
||||||
preset: "center",
|
preset: "center",
|
||||||
titleDefaults: { font: "manrope", weight: "bold", align: "center", size: "xl", color: "primary" },
|
titleDefaults: {
|
||||||
subtitleDefaults: { font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" },
|
font: "manrope",
|
||||||
|
weight: "bold",
|
||||||
|
align: "center",
|
||||||
|
size: "xl",
|
||||||
|
className: "leading-[125%] text-primary text-xl",
|
||||||
|
},
|
||||||
|
subtitleDefaults: undefined,
|
||||||
actionButton: {
|
actionButton: {
|
||||||
defaultText: defaultTexts?.nextButton || "Continue",
|
defaultText: defaultTexts?.nextButton || "Continue",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@ -39,7 +58,91 @@ export function SoulmatePortraitTemplate({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TemplateLayout {...layoutProps}>
|
<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>
|
</div>
|
||||||
</TemplateLayout>
|
</TemplateLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -16,39 +16,57 @@ interface TemplateLayoutProps {
|
|||||||
canGoBack: boolean;
|
canGoBack: boolean;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
screenProgress?: { current: number; total: number };
|
screenProgress?: { current: number; total: number };
|
||||||
|
|
||||||
// Настройки template
|
// Настройки template
|
||||||
titleDefaults?: {
|
titleDefaults?: {
|
||||||
font?: "manrope" | "inter" | "geistSans" | "geistMono";
|
font?: "manrope" | "inter" | "geistSans" | "geistMono";
|
||||||
weight?: "regular" | "medium" | "semiBold" | "bold" | "extraBold" | "black";
|
weight?: "regular" | "medium" | "semiBold" | "bold" | "extraBold" | "black";
|
||||||
align?: "left" | "center" | "right";
|
align?: "left" | "center" | "right";
|
||||||
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
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?: {
|
subtitleDefaults?: {
|
||||||
font?: "manrope" | "inter" | "geistSans" | "geistMono";
|
font?: "manrope" | "inter" | "geistSans" | "geistMono";
|
||||||
weight?: "regular" | "medium" | "semiBold" | "bold" | "extraBold" | "black";
|
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";
|
align?: "left" | "center" | "right";
|
||||||
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
actionButtonOptions?: {
|
actionButtonOptions?: {
|
||||||
defaultText: string;
|
defaultText: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Дополнительные props для BottomActionButton
|
// Дополнительные props для BottomActionButton
|
||||||
childrenAboveButton?: React.ReactNode;
|
childrenAboveButton?: React.ReactNode;
|
||||||
childrenUnderButton?: React.ReactNode;
|
childrenUnderButton?: React.ReactNode;
|
||||||
|
|
||||||
// Дополнительные props для Title
|
// Дополнительные props для Title
|
||||||
childrenAboveTitle?: React.ReactNode;
|
childrenAboveTitle?: React.ReactNode;
|
||||||
|
|
||||||
// Переопределения стилей LayoutQuestion (контент и обертка контента)
|
// Переопределения стилей LayoutQuestion (контент и обертка контента)
|
||||||
contentProps?: React.ComponentProps<"div">;
|
contentProps?: React.ComponentProps<"div">;
|
||||||
childrenWrapperProps?: React.ComponentProps<"div">;
|
childrenWrapperProps?: React.ComponentProps<"div">;
|
||||||
|
|
||||||
// Контент template
|
// Контент template
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
@ -62,8 +80,20 @@ export function TemplateLayout({
|
|||||||
canGoBack,
|
canGoBack,
|
||||||
onBack,
|
onBack,
|
||||||
screenProgress,
|
screenProgress,
|
||||||
titleDefaults = { font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" },
|
titleDefaults = {
|
||||||
subtitleDefaults = { font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" },
|
font: "manrope",
|
||||||
|
weight: "bold",
|
||||||
|
align: "left",
|
||||||
|
size: "2xl",
|
||||||
|
color: "default",
|
||||||
|
},
|
||||||
|
subtitleDefaults = {
|
||||||
|
font: "manrope",
|
||||||
|
weight: "medium",
|
||||||
|
color: "default",
|
||||||
|
align: "left",
|
||||||
|
size: "lg",
|
||||||
|
},
|
||||||
actionButtonOptions,
|
actionButtonOptions,
|
||||||
childrenAboveButton,
|
childrenAboveButton,
|
||||||
childrenUnderButton,
|
childrenUnderButton,
|
||||||
@ -73,9 +103,7 @@ export function TemplateLayout({
|
|||||||
children,
|
children,
|
||||||
}: TemplateLayoutProps) {
|
}: TemplateLayoutProps) {
|
||||||
// 🎛️ ЦЕНТРАЛИЗОВАННАЯ ЛОГИКА BOTTOM BUTTON
|
// 🎛️ ЦЕНТРАЛИЗОВАННАЯ ЛОГИКА BOTTOM BUTTON
|
||||||
const {
|
const { elementRef: bottomActionButtonRef } = useDynamicSize<HTMLDivElement>({
|
||||||
elementRef: bottomActionButtonRef,
|
|
||||||
} = useDynamicSize<HTMLDivElement>({
|
|
||||||
defaultHeight: 132,
|
defaultHeight: 132,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -98,20 +126,20 @@ export function TemplateLayout({
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// 🎯 Автоматически создаем PrivacyTermsConsent с фиксированными настройками
|
// 🎯 Автоматически создаем PrivacyTermsConsent с фиксированными настройками
|
||||||
const shouldShowPrivacyTermsConsent =
|
const shouldShowPrivacyTermsConsent =
|
||||||
'bottomActionButton' in screen &&
|
"bottomActionButton" in screen &&
|
||||||
screen.bottomActionButton?.showPrivacyTermsConsent === true;
|
screen.bottomActionButton?.showPrivacyTermsConsent === true;
|
||||||
|
|
||||||
const autoPrivacyTermsConsent = shouldShowPrivacyTermsConsent ? (
|
const autoPrivacyTermsConsent = shouldShowPrivacyTermsConsent ? (
|
||||||
<PrivacyTermsConsent
|
<PrivacyTermsConsent
|
||||||
className="mt-5"
|
className="mt-5"
|
||||||
privacyPolicy={{
|
privacyPolicy={{
|
||||||
href: "/privacy",
|
href: "/privacy",
|
||||||
children: "Privacy Policy"
|
children: "Privacy Policy",
|
||||||
}}
|
}}
|
||||||
termsOfUse={{
|
termsOfUse={{
|
||||||
href: "/terms",
|
href: "/terms",
|
||||||
children: "Terms of use"
|
children: "Terms of use",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
@ -137,8 +165,8 @@ export function TemplateLayout({
|
|||||||
</LayoutQuestion>
|
</LayoutQuestion>
|
||||||
|
|
||||||
{bottomActionButtonProps && (
|
{bottomActionButtonProps && (
|
||||||
<BottomActionButton
|
<BottomActionButton
|
||||||
{...bottomActionButtonProps}
|
{...bottomActionButtonProps}
|
||||||
ref={bottomActionButtonRef}
|
ref={bottomActionButtonRef}
|
||||||
childrenAboveButton={childrenAboveButton}
|
childrenAboveButton={childrenAboveButton}
|
||||||
childrenUnderButton={finalChildrenUnderButton}
|
childrenUnderButton={finalChildrenUnderButton}
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Header } from "@/components/layout/Header/Header";
|
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
|
export interface LayoutQuestionProps
|
||||||
extends Omit<React.ComponentProps<"section">, "title" | "content"> {
|
extends Omit<React.ComponentProps<"section">, "title" | "content"> {
|
||||||
@ -55,7 +57,7 @@ function LayoutQuestion({
|
|||||||
weight="bold"
|
weight="bold"
|
||||||
{...title}
|
{...title}
|
||||||
align={title.align ?? "left"}
|
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}
|
{...subtitle}
|
||||||
align={subtitle.align ?? "left"}
|
align={subtitle.align ?? "left"}
|
||||||
className={cn(
|
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,
|
sign: true,
|
||||||
signDate: new Date().toISOString(),
|
signDate: new Date().toISOString(),
|
||||||
// feature: feature.includes("black") ? "ios" : feature,
|
// feature: feature.includes("black") ? "ios" : feature,
|
||||||
|
feature: "stripe"
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[funnelId]
|
[funnelId]
|
||||||
|
|||||||
@ -54,6 +54,7 @@ export const useSession = ({ funnelId }: IUseSessionProps) => {
|
|||||||
try {
|
try {
|
||||||
const utm = parseQueryParams();
|
const utm = parseQueryParams();
|
||||||
const sessionParams = {
|
const sessionParams = {
|
||||||
|
feature: "stripe",
|
||||||
locale,
|
locale,
|
||||||
timezone,
|
timezone,
|
||||||
// source: funnelId,
|
// source: funnelId,
|
||||||
@ -102,7 +103,10 @@ export const useSession = ({ funnelId }: IUseSessionProps) => {
|
|||||||
}
|
}
|
||||||
const result = await updateSessionApi({
|
const result = await updateSessionApi({
|
||||||
sessionId: _sessionId,
|
sessionId: _sessionId,
|
||||||
data,
|
data: {
|
||||||
|
feature: "stripe",
|
||||||
|
...data,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||||||
import {
|
import {
|
||||||
buildDefaultHeader,
|
buildDefaultHeader,
|
||||||
buildDefaultTitle,
|
buildDefaultTitle,
|
||||||
buildDefaultSubtitle,
|
buildDefaultSubtitle,
|
||||||
buildDefaultBottomActionButton,
|
buildDefaultBottomActionButton,
|
||||||
buildDefaultNavigation,
|
buildDefaultNavigation,
|
||||||
buildDefaultDescription
|
buildDefaultDescription,
|
||||||
} from "./blocks";
|
} from "./blocks";
|
||||||
|
|
||||||
export function buildSoulmateDefaults(id: string): BuilderScreen {
|
export function buildSoulmateDefaults(id: string): BuilderScreen {
|
||||||
@ -19,12 +19,39 @@ export function buildSoulmateDefaults(id: string): BuilderScreen {
|
|||||||
title: buildDefaultTitle(),
|
title: buildDefaultTitle(),
|
||||||
subtitle: buildDefaultSubtitle(),
|
subtitle: buildDefaultSubtitle(),
|
||||||
bottomActionButton: buildDefaultBottomActionButton({
|
bottomActionButton: buildDefaultBottomActionButton({
|
||||||
text: "Получить полный анализ",
|
text: "Continue",
|
||||||
|
showPrivacyTermsConsent: true,
|
||||||
}),
|
}),
|
||||||
description: buildDefaultDescription({
|
description: buildDefaultDescription({
|
||||||
text: "Ваш персональный портрет почти готов.",
|
text: "Готов увидеть, кто твоя настоящая Родственная душа?",
|
||||||
align: "center",
|
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(),
|
navigation: buildDefaultNavigation(),
|
||||||
} as BuilderScreen;
|
} as BuilderScreen;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { TypographyProps } from "@/components/ui/Typography/Typography";
|
import type { TypographyProps } from "@/components/ui/Typography/Typography";
|
||||||
import type { MainButtonProps } from "@/components/ui/MainButton/MainButton";
|
import type { MainButtonProps } from "@/components/ui/MainButton/MainButton";
|
||||||
import { hasTextMarkup } from "@/lib/text-markup";
|
import { hasTextMarkup } from "@/lib/text-markup";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
HeaderDefinition,
|
HeaderDefinition,
|
||||||
@ -34,6 +35,7 @@ interface TypographyDefaults {
|
|||||||
size?: TypographyVariant["size"];
|
size?: TypographyVariant["size"];
|
||||||
align?: TypographyVariant["align"];
|
align?: TypographyVariant["align"];
|
||||||
color?: TypographyVariant["color"];
|
color?: TypographyVariant["color"];
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BuildTypographyOptions<T extends TypographyAs> {
|
interface BuildTypographyOptions<T extends TypographyAs> {
|
||||||
@ -69,7 +71,7 @@ export function buildTypographyProps<T extends TypographyAs>(
|
|||||||
size: variant.size ?? defaults?.size,
|
size: variant.size ?? defaults?.size,
|
||||||
align: variant.align ?? defaults?.align,
|
align: variant.align ?? defaults?.align,
|
||||||
color: variant.color ?? defaults?.color,
|
color: variant.color ?? defaults?.color,
|
||||||
className: variant.className,
|
className: cn(defaults?.className, variant.className),
|
||||||
enableMarkup: hasTextMarkup(variant.text || ""), // 🎨 АВТОМАТИЧЕСКИ включаем разметку если обнаружена
|
enableMarkup: hasTextMarkup(variant.text || ""), // 🎨 АВТОМАТИЧЕСКИ включаем разметку если обнаружена
|
||||||
} as TypographyProps<T>;
|
} as TypographyProps<T>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -326,7 +326,19 @@ export interface SoulmatePortraitScreenDefinition {
|
|||||||
header?: HeaderDefinition;
|
header?: HeaderDefinition;
|
||||||
title: TitleDefinition;
|
title: TitleDefinition;
|
||||||
subtitle?: SubtitleDefinition;
|
subtitle?: SubtitleDefinition;
|
||||||
description?: TypographyVariant; // 🎯 Настраиваемый текст описания
|
description?: TypographyVariant; // Настраиваемый текст описания
|
||||||
|
soulmatePortraitsDelivered?: {
|
||||||
|
image?: string;
|
||||||
|
text?: TypographyVariant;
|
||||||
|
avatars?: Array<{
|
||||||
|
src: string;
|
||||||
|
alt?: string;
|
||||||
|
fallbackText?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
textList?: {
|
||||||
|
items: TypographyVariant[];
|
||||||
|
};
|
||||||
bottomActionButton?: BottomActionButtonDefinition;
|
bottomActionButton?: BottomActionButtonDefinition;
|
||||||
navigation?: NavigationDefinition;
|
navigation?: NavigationDefinition;
|
||||||
variants?: ScreenVariantDefinition<SoulmatePortraitScreenDefinition>[];
|
variants?: ScreenVariantDefinition<SoulmatePortraitScreenDefinition>[];
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user