w-funnel/src/lib/funnel/screenRenderer.tsx
2025-10-07 00:48:23 +02:00

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;
}