"use client"; import React, { useState, useEffect } from "react"; import { ChevronDown, ChevronRight } from "lucide-react"; import { InfoScreenConfig } from "./InfoScreenConfig"; import { DateScreenConfig } from "./DateScreenConfig"; import { CouponScreenConfig } from "./CouponScreenConfig"; import { FormScreenConfig } from "./FormScreenConfig"; import { ListScreenConfig } from "./ListScreenConfig"; import { EmailScreenConfig } from "./EmailScreenConfig"; import { LoadersScreenConfig } from "./LoadersScreenConfig"; import { SoulmatePortraitScreenConfig } from "./SoulmatePortraitScreenConfig"; import { TrialPaymentScreenConfig } from "./TrialPaymentScreenConfig"; import { TrialChoiceScreenConfig } from "./TrialChoiceScreenConfig"; import { TextInput } from "@/components/ui/TextInput/TextInput"; import { TextAreaInput } from "@/components/ui/TextAreaInput/TextAreaInput"; import type { BuilderScreen } from "@/lib/admin/builder/types"; import type { ScreenDefinition, InfoScreenDefinition, DateScreenDefinition, CouponScreenDefinition, FormScreenDefinition, ListScreenDefinition, EmailScreenDefinition, LoadersScreenDefinition, SoulmatePortraitScreenDefinition, TypographyVariant, BottomActionButtonDefinition, HeaderDefinition, TrialPaymentScreenDefinition, SpecialOfferScreenDefinition, TrialChoiceScreenDefinition, } from "@/lib/funnel/types"; import { SpecialOfferScreenConfig } from "./SpecialOfferScreenConfig"; const RADIUS_OPTIONS: ("3xl" | "full")[] = ["3xl", "full"]; interface TemplateConfigProps { screen: BuilderScreen; onUpdate: (updates: Partial) => void; } function CollapsibleSection({ title, children, defaultExpanded = false, }: { title: string; children: React.ReactNode; defaultExpanded?: boolean; }) { const storageKey = `template-section-${title .toLowerCase() .replace(/\s+/g, "-")}`; const [isExpanded, setIsExpanded] = useState(() => { if (typeof window === "undefined") return defaultExpanded; const stored = sessionStorage.getItem(storageKey); return stored !== null ? JSON.parse(stored) : defaultExpanded; }); const handleToggle = () => { const newExpanded = !isExpanded; setIsExpanded(newExpanded); if (typeof window !== "undefined") { sessionStorage.setItem(storageKey, JSON.stringify(newExpanded)); } }; return (
{isExpanded &&
{children}
}
); } interface TypographyControlsProps { label: string; value: (TypographyVariant & { show?: boolean }) | undefined; onChange: ( value: (TypographyVariant & { show?: boolean }) | undefined ) => void; allowRemove?: boolean; showToggle?: boolean; // Показывать ли чекбокс "Показывать" } function TypographyControls({ label, value, onChange, allowRemove = false, showToggle = false, }: TypographyControlsProps) { const storageKey = `typography-advanced-${label .toLowerCase() .replace(/\s+/g, "-")}`; const [showAdvanced, setShowAdvanced] = useState(false); const [isHydrated, setIsHydrated] = useState(false); useEffect(() => { const stored = sessionStorage.getItem(storageKey); if (stored !== null) { setShowAdvanced(JSON.parse(stored)); } setIsHydrated(true); }, [storageKey]); const handleTextChange = (text: string) => { // Всегда обновляем текст, даже если пустой // Это позволяет controlled input работать корректно onChange({ ...value, text, show: value?.show ?? true, // Если show не задан, по умолчанию true }); }; const handleTextBlur = () => { // При потере фокуса удаляем объект если текст пустой if (allowRemove && (!value?.text || value.text.trim() === "")) { onChange(undefined); } }; const handleAdvancedChange = ( field: keyof TypographyVariant, fieldValue: string ) => { onChange({ ...value, text: value?.text || "", [field]: fieldValue || undefined, }); }; const handleShowToggle = (show: boolean) => { if (!show) { // Скрываем элемент if (allowRemove) { // Для опциональных полей - удаляем объект полностью onChange(undefined); } else { // Для обязательных полей - сохраняем с show: false onChange({ ...value, text: value?.text || "", show: false, }); } } else { // Показываем элемент onChange({ ...value, text: value?.text || "", show: true, }); } }; return (
{showToggle && ( )}
handleTextChange(event.target.value)} onBlur={handleTextBlur} rows={2} className="resize-y" aria-invalid={ !allowRemove && (!value?.text || value.text.trim() === "") } /> {!allowRemove && (!value?.text || value.text.trim() === "") && (

Это поле обязательно для заполнения

)}
{value?.text && (
{(isHydrated ? showAdvanced : false) && (
)}
)}
); } interface HeaderControlsProps { header: HeaderDefinition | undefined; onChange: (value: HeaderDefinition | undefined) => void; } function HeaderControls({ header, onChange }: HeaderControlsProps) { const activeHeader = header ?? { show: true, showBackButton: true, showProgress: true }; const handleToggle = ( field: "show" | "showBackButton" | "showProgress", checked: boolean ) => { onChange({ ...activeHeader, [field]: checked, }); }; return (
{activeHeader.show !== false && (
)}
); } interface ActionButtonControlsProps { label: string; value: BottomActionButtonDefinition | undefined; onChange: (value: BottomActionButtonDefinition | undefined) => void; } function ActionButtonControls({ label, value, onChange, }: ActionButtonControlsProps) { // По умолчанию кнопка включена (show !== false) const isEnabled = value?.show !== false; const buttonText = value?.text || ""; const cornerRadius = value?.cornerRadius; const showPrivacyTermsConsent = value?.showPrivacyTermsConsent ?? false; const showGradientBlur = value?.showGradientBlur ?? true; const handleToggle = (enabled: boolean) => { if (enabled) { // Включаем кнопку - убираем show: false или создаем объект const newValue = value ? { ...value, show: true } : { show: true }; // Если show: true по умолчанию, можем убрать это поле if (newValue.show === true && !newValue.text && !newValue.cornerRadius) { onChange(undefined); // Дефолтное состояние } else { const { show, ...rest } = newValue; onChange(Object.keys(rest).length > 0 ? { show, ...rest } : undefined); } } else { // Отключаем кнопку onChange({ show: false }); } }; const handleTextChange = (text: string) => { if (!isEnabled) return; const trimmedText = text.trim(); const newValue = { ...value, text: trimmedText || undefined, }; // Убираем undefined поля для чистоты if (!newValue.text && !newValue.cornerRadius && newValue.show !== false) { onChange(undefined); } else { onChange(newValue); } }; const handleRadiusChange = (radius: string) => { if (!isEnabled) return; const newRadius = (radius as "3xl" | "full") || undefined; const newValue = { ...value, cornerRadius: newRadius, }; // Убираем undefined поля для чистоты if ( !newValue.text && !newValue.cornerRadius && newValue.show !== false && !newValue.showPrivacyTermsConsent ) { onChange(undefined); } else { onChange(newValue); } }; const handlePrivacyTermsToggle = (checked: boolean) => { if (!isEnabled) return; const newValue = { ...value, showPrivacyTermsConsent: checked || undefined, }; // Убираем undefined поля для чистоты if ( !newValue.text && !newValue.cornerRadius && newValue.show !== false && !newValue.showPrivacyTermsConsent ) { onChange(undefined); } else { onChange(newValue); } }; const handleGradientBlurToggle = (checked: boolean) => { // Работает даже когда кнопка отключена (для LIST экранов) const newValue = { ...value, // Если checked = true (дефолт), не сохраняем поле // Если checked = false, сохраняем явно showGradientBlur: checked ? undefined : false, }; // Убираем undefined поля для чистоты if ( !newValue.text && !newValue.cornerRadius && newValue.show !== false && !newValue.showPrivacyTermsConsent && newValue.showGradientBlur !== false ) { onChange(undefined); } else { onChange(newValue); } }; return (
{isEnabled && (
)} {/* Gradient Blur - доступен даже когда кнопка отключена */}
); } export function TemplateConfig({ screen, onUpdate }: TemplateConfigProps) { const { template } = screen; const handleTitleChange = (value: TypographyVariant | undefined) => { // Заголовок обязательный, но разрешаем временно пустой текст // (для корректной работы controlled input) // Валидация при сохранении покажет ошибку если текст пустой if (!value) { // Создаем минимальный объект вместо undefined onUpdate({ title: { text: "" } }); return; } onUpdate({ title: value }); }; const handleSubtitleChange = (newValue: TypographyVariant | undefined) => { onUpdate({ subtitle: newValue }); }; const handleHeaderChange = (value: HeaderDefinition | undefined) => { onUpdate({ header: value }); }; const handleButtonChange = ( value: BottomActionButtonDefinition | undefined ) => { onUpdate({ bottomActionButton: value }); }; return (
{template === "info" && ( ) => void } /> )} {template === "date" && ( ) => void } /> )} {template === "coupon" && ( ) => void } /> )} {template === "form" && ( ) => void } /> )} {template === "list" && ( ) => void } /> )} {template === "email" && ( ) => void } /> )} {template === "loaders" && ( ) => void } /> )} {template === "soulmate" && ( ) => void } /> )} {template === "trialPayment" && ( ) => void } /> )} {template === "specialOffer" && ( ) => void } /> )} {template === "trialChoice" && ( ) => void } /> )}
); }