344 lines
8.2 KiB
TypeScript
344 lines
8.2 KiB
TypeScript
"use client";
|
|
|
|
import type { JSX } from "react";
|
|
|
|
import {
|
|
ListTemplate,
|
|
InfoTemplate,
|
|
DateTemplate,
|
|
CouponTemplate,
|
|
FormTemplate,
|
|
EmailTemplate,
|
|
LoadersTemplate,
|
|
SoulmatePortraitTemplate,
|
|
TrialPaymentTemplate,
|
|
} from "@/components/funnel/templates";
|
|
import type {
|
|
ListScreenDefinition,
|
|
DateScreenDefinition,
|
|
FormScreenDefinition,
|
|
CouponScreenDefinition,
|
|
InfoScreenDefinition,
|
|
EmailScreenDefinition,
|
|
LoadersScreenDefinition,
|
|
SoulmatePortraitScreenDefinition,
|
|
TrialPaymentScreenDefinition,
|
|
ScreenDefinition,
|
|
DefaultTexts,
|
|
FunnelDefinition,
|
|
FunnelAnswers,
|
|
} from "@/lib/funnel/types";
|
|
|
|
export interface ScreenRenderProps {
|
|
funnel: FunnelDefinition;
|
|
screen: ScreenDefinition;
|
|
selectedOptionIds: string[];
|
|
onSelectionChange: (ids: string[]) => void;
|
|
onContinue: () => void;
|
|
canGoBack: boolean;
|
|
onBack: () => void;
|
|
screenProgress: { current: number; total: number };
|
|
defaultTexts?: DefaultTexts;
|
|
answers: FunnelAnswers;
|
|
}
|
|
|
|
export type TemplateRenderer = (props: ScreenRenderProps) => JSX.Element;
|
|
|
|
const TEMPLATE_REGISTRY: Record<
|
|
ScreenDefinition["template"],
|
|
TemplateRenderer
|
|
> = {
|
|
info: ({
|
|
screen,
|
|
onContinue,
|
|
canGoBack,
|
|
onBack,
|
|
screenProgress,
|
|
defaultTexts,
|
|
answers,
|
|
}) => {
|
|
const infoScreen = screen as InfoScreenDefinition;
|
|
|
|
return (
|
|
<InfoTemplate
|
|
screen={infoScreen}
|
|
onContinue={onContinue}
|
|
canGoBack={canGoBack}
|
|
onBack={onBack}
|
|
screenProgress={screenProgress}
|
|
defaultTexts={defaultTexts}
|
|
answers={answers}
|
|
/>
|
|
);
|
|
},
|
|
date: ({
|
|
screen,
|
|
selectedOptionIds,
|
|
onSelectionChange,
|
|
onContinue,
|
|
canGoBack,
|
|
onBack,
|
|
screenProgress,
|
|
defaultTexts,
|
|
}) => {
|
|
const dateScreen = screen as DateScreenDefinition;
|
|
|
|
// For date screens, we store date components as array: [month, day, year]
|
|
const currentDateArray = selectedOptionIds;
|
|
const selectedDate = {
|
|
month: currentDateArray[0] || "",
|
|
day: currentDateArray[1] || "",
|
|
year: currentDateArray[2] || "",
|
|
};
|
|
|
|
const handleDateChange = (date: {
|
|
month?: string;
|
|
day?: string;
|
|
year?: string;
|
|
}) => {
|
|
const dateArray = [date.month || "", date.day || "", date.year || ""];
|
|
onSelectionChange(dateArray);
|
|
};
|
|
|
|
return (
|
|
<DateTemplate
|
|
screen={dateScreen}
|
|
selectedDate={selectedDate}
|
|
onDateChange={handleDateChange}
|
|
onContinue={onContinue}
|
|
canGoBack={canGoBack}
|
|
onBack={onBack}
|
|
screenProgress={screenProgress}
|
|
defaultTexts={defaultTexts}
|
|
/>
|
|
);
|
|
},
|
|
form: ({
|
|
screen,
|
|
selectedOptionIds,
|
|
onSelectionChange,
|
|
onContinue,
|
|
canGoBack,
|
|
onBack,
|
|
screenProgress,
|
|
defaultTexts,
|
|
}) => {
|
|
const formScreen = screen as FormScreenDefinition;
|
|
|
|
// For form screens, we store form data as JSON string in the first element
|
|
const formDataJson = selectedOptionIds[0] || "{}";
|
|
let formData: Record<string, string> = {};
|
|
|
|
try {
|
|
formData = JSON.parse(formDataJson);
|
|
} catch {
|
|
formData = {};
|
|
}
|
|
|
|
const handleFormDataChange = (data: Record<string, string>) => {
|
|
const dataJson = JSON.stringify(data);
|
|
onSelectionChange([dataJson]);
|
|
};
|
|
|
|
return (
|
|
<FormTemplate
|
|
screen={formScreen}
|
|
formData={formData}
|
|
onFormDataChange={handleFormDataChange}
|
|
onContinue={onContinue}
|
|
canGoBack={canGoBack}
|
|
onBack={onBack}
|
|
screenProgress={screenProgress}
|
|
defaultTexts={defaultTexts}
|
|
/>
|
|
);
|
|
},
|
|
coupon: ({
|
|
screen,
|
|
onContinue,
|
|
canGoBack,
|
|
onBack,
|
|
screenProgress,
|
|
defaultTexts,
|
|
}) => {
|
|
const couponScreen = screen as CouponScreenDefinition;
|
|
|
|
return (
|
|
<CouponTemplate
|
|
screen={couponScreen}
|
|
onContinue={onContinue}
|
|
canGoBack={canGoBack}
|
|
onBack={onBack}
|
|
screenProgress={screenProgress}
|
|
defaultTexts={defaultTexts}
|
|
/>
|
|
);
|
|
},
|
|
list: ({
|
|
screen,
|
|
selectedOptionIds,
|
|
onSelectionChange,
|
|
onContinue,
|
|
canGoBack,
|
|
onBack,
|
|
screenProgress,
|
|
defaultTexts,
|
|
}) => {
|
|
const listScreen = screen as ListScreenDefinition;
|
|
const isSelectionEmpty = selectedOptionIds.length === 0;
|
|
|
|
// Используем только общую кнопку экрана
|
|
const bottomActionButton = listScreen.bottomActionButton;
|
|
const isButtonDisabled = bottomActionButton?.show === false;
|
|
|
|
// Простая логика: кнопка есть если не отключена (show: false)
|
|
const hasActionButton = !isButtonDisabled;
|
|
|
|
// Правильная логика приоритетов для текста кнопки:
|
|
// 1. bottomActionButton.text (настройка экрана)
|
|
// 2. defaultTexts.nextButton (глобальная настройка воронки)
|
|
// 3. "Next" (хардкод fallback)
|
|
const buttonText =
|
|
bottomActionButton?.text || defaultTexts?.nextButton || "Next";
|
|
|
|
const actionDisabled = hasActionButton && isSelectionEmpty;
|
|
|
|
return (
|
|
<ListTemplate
|
|
screen={listScreen}
|
|
selectedOptionIds={selectedOptionIds}
|
|
onSelectionChange={onSelectionChange}
|
|
actionButtonProps={
|
|
hasActionButton
|
|
? {
|
|
children: buttonText,
|
|
disabled: actionDisabled,
|
|
onClick: actionDisabled ? undefined : onContinue,
|
|
}
|
|
: undefined
|
|
}
|
|
canGoBack={canGoBack}
|
|
onBack={onBack}
|
|
screenProgress={screenProgress}
|
|
/>
|
|
);
|
|
},
|
|
email: ({
|
|
screen,
|
|
selectedOptionIds,
|
|
onSelectionChange,
|
|
onContinue,
|
|
canGoBack,
|
|
onBack,
|
|
screenProgress,
|
|
defaultTexts,
|
|
funnel,
|
|
}) => {
|
|
const emailScreen = screen as EmailScreenDefinition;
|
|
|
|
// For email screens, we store email as single string in first element
|
|
const selectedEmail = selectedOptionIds[0] || "";
|
|
|
|
const handleEmailChange = (email: string) => {
|
|
onSelectionChange([email]);
|
|
};
|
|
|
|
return (
|
|
<EmailTemplate
|
|
screen={emailScreen}
|
|
selectedEmail={selectedEmail}
|
|
onEmailChange={handleEmailChange}
|
|
onContinue={onContinue}
|
|
canGoBack={canGoBack}
|
|
onBack={onBack}
|
|
screenProgress={screenProgress}
|
|
defaultTexts={defaultTexts}
|
|
funnel={funnel}
|
|
/>
|
|
);
|
|
},
|
|
loaders: ({
|
|
screen,
|
|
onContinue,
|
|
canGoBack,
|
|
onBack,
|
|
screenProgress,
|
|
defaultTexts,
|
|
}) => {
|
|
const loadersScreen = screen as LoadersScreenDefinition;
|
|
|
|
return (
|
|
<LoadersTemplate
|
|
screen={loadersScreen}
|
|
onContinue={onContinue}
|
|
canGoBack={canGoBack}
|
|
onBack={onBack}
|
|
screenProgress={screenProgress}
|
|
defaultTexts={defaultTexts}
|
|
/>
|
|
);
|
|
},
|
|
soulmate: ({
|
|
screen,
|
|
onContinue,
|
|
canGoBack,
|
|
onBack,
|
|
screenProgress,
|
|
defaultTexts,
|
|
}) => {
|
|
const soulmateScreen = screen as SoulmatePortraitScreenDefinition;
|
|
|
|
return (
|
|
<SoulmatePortraitTemplate
|
|
screen={soulmateScreen}
|
|
onContinue={onContinue}
|
|
canGoBack={canGoBack}
|
|
onBack={onBack}
|
|
screenProgress={screenProgress}
|
|
defaultTexts={defaultTexts}
|
|
/>
|
|
);
|
|
},
|
|
trialPayment: ({
|
|
screen,
|
|
onContinue,
|
|
canGoBack,
|
|
onBack,
|
|
screenProgress,
|
|
defaultTexts,
|
|
funnel,
|
|
}) => {
|
|
const trialPaymentScreen = screen as TrialPaymentScreenDefinition;
|
|
|
|
return (
|
|
<TrialPaymentTemplate
|
|
screen={trialPaymentScreen}
|
|
onContinue={onContinue}
|
|
canGoBack={canGoBack}
|
|
onBack={onBack}
|
|
screenProgress={screenProgress}
|
|
defaultTexts={defaultTexts}
|
|
funnel={funnel}
|
|
/>
|
|
);
|
|
},
|
|
};
|
|
|
|
export function renderScreen(props: ScreenRenderProps): JSX.Element {
|
|
const renderer = TEMPLATE_REGISTRY[props.screen.template];
|
|
if (!renderer) {
|
|
throw new Error(`Unsupported template: ${props.screen.template}`);
|
|
}
|
|
return renderer(props);
|
|
}
|
|
|
|
export function getTemplateRenderer(
|
|
screen: ScreenDefinition
|
|
): TemplateRenderer {
|
|
const renderer = TEMPLATE_REGISTRY[screen.template];
|
|
if (!renderer) {
|
|
throw new Error(`Unsupported template: ${screen.template}`);
|
|
}
|
|
return renderer;
|
|
}
|