diff --git a/public/funnels/soulmate.json b/public/funnels/soulmate.json index d86f1bb..8e628e2 100644 --- a/public/funnels/soulmate.json +++ b/public/funnels/soulmate.json @@ -48,17 +48,12 @@ }, "description": { "text": "Ваш персональный портрет почти готов.", - "show": true, "font": "manrope", "weight": "regular", "size": "md", "align": "center", "color": "default" }, - "fields": [], - "list": { - "options": [] - }, "variants": [] }, { @@ -91,7 +86,6 @@ "defaultNextScreenId": "relationship-status", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -146,7 +140,6 @@ "defaultNextScreenId": "analysis-target", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -200,7 +193,6 @@ "defaultNextScreenId": "partner-age", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -269,7 +261,6 @@ "defaultNextScreenId": "partner-ethnicity", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -310,7 +301,8 @@ "current_partner" ] } - ] + ], + "overrides": {} }, { "conditions": [ @@ -397,7 +389,6 @@ "defaultNextScreenId": "partner-ethnicity", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -446,7 +437,6 @@ "defaultNextScreenId": "partner-eye-color", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -520,7 +510,6 @@ "defaultNextScreenId": "partner-hair-length", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -598,7 +587,6 @@ "defaultNextScreenId": "burnout-support", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -647,7 +635,6 @@ "defaultNextScreenId": "burnout-result", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -720,10 +707,6 @@ "value": "/images/ac321d94-62e3-45c6-85f4-51faf6769bab.svg", "size": "md" }, - "fields": [], - "list": { - "options": [] - }, "variants": [ { "conditions": [ @@ -734,7 +717,8 @@ "acknowledged_and_calmed" ] } - ] + ], + "overrides": {} }, { "conditions": [ @@ -852,10 +836,6 @@ "storageKey": "userZodiac" } }, - "fields": [], - "list": { - "options": [] - }, "variants": [] }, { @@ -884,7 +864,6 @@ "defaultNextScreenId": "love-priority", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -942,7 +921,6 @@ "defaultNextScreenId": "core-need", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1000,7 +978,6 @@ "defaultNextScreenId": "partner-similarity", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1059,7 +1036,6 @@ "defaultNextScreenId": "partner-role", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1103,7 +1079,6 @@ "defaultNextScreenId": "relationship-strength", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1167,7 +1142,6 @@ "defaultNextScreenId": "love-expression", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1226,7 +1200,6 @@ "defaultNextScreenId": "relationship-future", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1290,7 +1263,6 @@ "defaultNextScreenId": "relationship-energy", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1349,7 +1321,6 @@ "defaultNextScreenId": "relationship-metaphor", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1417,7 +1388,6 @@ "defaultNextScreenId": "portrait-generation", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "multi", "options": [ @@ -1492,10 +1462,6 @@ "defaultNextScreenId": "email", "isEndScreen": false }, - "fields": [], - "list": { - "options": [] - }, "progressbars": { "items": [ { @@ -1545,12 +1511,9 @@ }, "navigation": { "rules": [], + "defaultNextScreenId": "screen-25", "isEndScreen": true }, - "fields": [], - "list": { - "options": [] - }, "emailInput": { "label": "Email", "placeholder": "example@email.com" diff --git a/scripts/sync-funnels-from-db.mjs b/scripts/sync-funnels-from-db.mjs index 48360c0..a178d93 100755 --- a/scripts/sync-funnels-from-db.mjs +++ b/scripts/sync-funnels-from-db.mjs @@ -270,14 +270,76 @@ async function getLatestPublishedFunnels() { } } +/** + * Нормализует данные воронки перед сохранением + * Удаляет лишние поля которые не соответствуют типам TypeScript + */ +function normalizeFunnelData(funnelData) { + return { + ...funnelData, + screens: funnelData.screens.map((screen) => { + const normalizedScreen = { ...screen }; + + // Удаляем поле 'show' из description (TypographyVariant не содержит его) + // Поле 'show' есть только у TitleDefinition и SubtitleDefinition + if (normalizedScreen.description && typeof normalizedScreen.description === 'object') { + if ('show' in normalizedScreen.description) { + delete normalizedScreen.description.show; + } + } + + // Удаляем специфичные для других шаблонов поля + // Каждый шаблон должен содержать только свои поля + switch (normalizedScreen.template) { + case 'form': + // fields нужно только для form экранов + break; + case 'email': + // email имеет emailInput, а не fields + if ('fields' in normalizedScreen) delete normalizedScreen.fields; + if ('list' in normalizedScreen) delete normalizedScreen.list; + break; + case 'list': + // list нужно только для list экранов + if ('fields' in normalizedScreen) delete normalizedScreen.fields; + break; + case 'loaders': + // progressbars нужно только для loaders + if ('fields' in normalizedScreen) delete normalizedScreen.fields; + if ('list' in normalizedScreen) delete normalizedScreen.list; + break; + default: + // Для остальных шаблонов (info, date, coupon, soulmate) удаляем специфичные поля + if ('fields' in normalizedScreen) delete normalizedScreen.fields; + if ('list' in normalizedScreen) delete normalizedScreen.list; + if ('progressbars' in normalizedScreen) delete normalizedScreen.progressbars; + break; + } + + // Нормализуем variants - добавляем пустой overrides если его нет + if ('variants' in normalizedScreen && Array.isArray(normalizedScreen.variants)) { + normalizedScreen.variants = normalizedScreen.variants.map((variant) => ({ + conditions: variant.conditions || [], + overrides: variant.overrides || {}, + })); + } + + return normalizedScreen; + }), + }; +} + async function saveFunnelToFile(funnel) { const funnelId = funnel.funnelData.meta.id; const fileName = `${funnelId}.json`; const filePath = path.join(funnelsDir, fileName); try { + // Нормализуем данные перед сохранением (удаляем лишние поля) + const normalizedData = normalizeFunnelData(funnel.funnelData); + // Сохраняем только funnelData (структуру воронки) - const funnelContent = JSON.stringify(funnel.funnelData, null, 2); + const funnelContent = JSON.stringify(normalizedData, null, 2); await fs.writeFile(filePath, funnelContent, 'utf8'); console.log(`💾 Saved ${fileName} (v${funnel.version})`); } catch (error) { diff --git a/src/app/api/funnels/[id]/route.ts b/src/app/api/funnels/[id]/route.ts index 6e1123d..9810014 100644 --- a/src/app/api/funnels/[id]/route.ts +++ b/src/app/api/funnels/[id]/route.ts @@ -19,7 +19,7 @@ function normalizeTypography(typography: TypographyVariant | undefined): Typogra /** * Нормализует данные воронки перед сохранением в MongoDB - * Удаляет пустые текстовые поля которые не пройдут валидацию + * Удаляет пустые текстовые поля и лишние поля которые не соответствуют типам */ function normalizeFunnelData(funnelData: FunnelDefinition): FunnelDefinition { return { @@ -39,15 +39,56 @@ function normalizeFunnelData(funnelData: FunnelDefinition): FunnelDefinition { } // Нормализуем description (для info и soulmate экранов) + // ⚠️ TypographyVariant НЕ содержит поле 'show', удаляем его если есть if ('description' in normalizedScreen) { const normalized = normalizeTypography(normalizedScreen.description); if (normalized === undefined) { delete normalizedScreen.description; } else { normalizedScreen.description = normalized; + // Удаляем поле 'show' если оно есть (TypographyVariant не содержит его) + if ('show' in normalizedScreen.description) { + delete normalizedScreen.description.show; + } } } + // Удаляем специфичные для других шаблонов поля + // Каждый шаблон должен содержать только свои поля + switch (normalizedScreen.template) { + case 'form': + // fields нужно только для form экранов + break; + case 'email': + // email имеет emailInput, а не fields + if ('fields' in normalizedScreen) delete normalizedScreen.fields; + if ('list' in normalizedScreen) delete normalizedScreen.list; + break; + case 'list': + // list нужно только для list экранов + if ('fields' in normalizedScreen) delete normalizedScreen.fields; + break; + case 'loaders': + // progressbars нужно только для loaders + if ('fields' in normalizedScreen) delete normalizedScreen.fields; + if ('list' in normalizedScreen) delete normalizedScreen.list; + break; + default: + // Для остальных шаблонов (info, date, coupon, soulmate) удаляем специфичные поля + if ('fields' in normalizedScreen) delete normalizedScreen.fields; + if ('list' in normalizedScreen) delete normalizedScreen.list; + if ('progressbars' in normalizedScreen) delete normalizedScreen.progressbars; + break; + } + + // Нормализуем variants - добавляем пустой overrides если его нет + if ('variants' in normalizedScreen && Array.isArray(normalizedScreen.variants)) { + normalizedScreen.variants = normalizedScreen.variants.map((variant: { conditions?: unknown; overrides?: unknown }) => ({ + conditions: variant.conditions || [], + overrides: variant.overrides || {}, + })); + } + return normalizedScreen as ScreenDefinition; }), }; diff --git a/src/components/admin/builder/layout/BuilderPreview.tsx b/src/components/admin/builder/layout/BuilderPreview.tsx index 9bf1f65..141b782 100644 --- a/src/components/admin/builder/layout/BuilderPreview.tsx +++ b/src/components/admin/builder/layout/BuilderPreview.tsx @@ -74,8 +74,15 @@ export function BuilderPreview() { if (!previewScreen) return null; try { - // ✅ Используем мемоизированные моки + // ✅ Собираем объект funnel из builderState для передачи в renderScreen + const mockFunnel = { + meta: builderState.meta, + defaultTexts: builderState.defaultTexts, + screens: builderState.screens, + }; + return renderScreen({ + funnel: mockFunnel, screen: previewScreen, selectedOptionIds: selectedIds, onSelectionChange: handleSelectionChange, @@ -93,7 +100,7 @@ export function BuilderPreview() { ); } - }, [previewScreen, selectedIds, handleSelectionChange, builderState.defaultTexts]); + }, [previewScreen, selectedIds, handleSelectionChange, builderState.meta, builderState.defaultTexts, builderState.screens]); const preview = useMemo(() => { if (!previewScreen) { diff --git a/src/components/funnel/templates/DateTemplate/DateTemplate.tsx b/src/components/funnel/templates/DateTemplate/DateTemplate.tsx index b8101a5..a567150 100644 --- a/src/components/funnel/templates/DateTemplate/DateTemplate.tsx +++ b/src/components/funnel/templates/DateTemplate/DateTemplate.tsx @@ -97,6 +97,7 @@ export function DateTemplate({ as="p" size="sm" color="muted" + align="center" className="font-medium" > {screen.dateInput?.selectedDateLabel || "Выбранная дата:"} @@ -106,6 +107,7 @@ export function DateTemplate({ size="xl" weight="bold" color="default" + align="center" className="font-semibold" > {formattedDate} @@ -124,7 +126,7 @@ export function DateTemplate({ disabled: !isFormValid, onClick: onContinue, }, - childrenUnderButton: selectedDateDisplay, + childrenAboveButton: selectedDateDisplay, } ); diff --git a/src/components/ui/Avatar/Avatar.tsx b/src/components/ui/Avatar/Avatar.tsx index c49fc95..818a176 100644 --- a/src/components/ui/Avatar/Avatar.tsx +++ b/src/components/ui/Avatar/Avatar.tsx @@ -4,7 +4,8 @@ import { AvatarFallback, } from "../avatar"; -interface AvatarProps extends React.ComponentProps { +export interface AvatarProps extends Omit, never> { + className?: string; imageProps?: React.ComponentProps; fallbackProps?: React.ComponentProps; } diff --git a/src/components/widgets/BottomActionButton/BottomActionButton.tsx b/src/components/widgets/BottomActionButton/BottomActionButton.tsx index 514f448..4c46854 100644 --- a/src/components/widgets/BottomActionButton/BottomActionButton.tsx +++ b/src/components/widgets/BottomActionButton/BottomActionButton.tsx @@ -82,7 +82,11 @@ const BottomActionButton = forwardRef( {...props} > - {childrenAboveButton} + {childrenAboveButton && ( +
+ {childrenAboveButton} +
+ )} {hasButton ? : null} {childrenUnderButton}
diff --git a/src/lib/funnel/bakedFunnels.ts b/src/lib/funnel/bakedFunnels.ts index cfd154f..1f9c0c3 100644 --- a/src/lib/funnel/bakedFunnels.ts +++ b/src/lib/funnel/bakedFunnels.ts @@ -56,17 +56,12 @@ export const BAKED_FUNNELS: Record = { }, "description": { "text": "Ваш персональный портрет почти готов.", - "show": true, "font": "manrope", "weight": "regular", "size": "md", "align": "center", "color": "default" }, - "fields": [], - "list": { - "options": [] - }, "variants": [] }, { @@ -99,7 +94,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "relationship-status", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -154,7 +148,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "analysis-target", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -208,7 +201,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "partner-age", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -277,7 +269,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "partner-ethnicity", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -318,7 +309,8 @@ export const BAKED_FUNNELS: Record = { "current_partner" ] } - ] + ], + "overrides": {} }, { "conditions": [ @@ -405,7 +397,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "partner-ethnicity", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -454,7 +445,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "partner-eye-color", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -528,7 +518,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "partner-hair-length", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -606,7 +595,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "burnout-support", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -655,7 +643,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "burnout-result", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -728,10 +715,6 @@ export const BAKED_FUNNELS: Record = { "value": "/images/ac321d94-62e3-45c6-85f4-51faf6769bab.svg", "size": "md" }, - "fields": [], - "list": { - "options": [] - }, "variants": [ { "conditions": [ @@ -742,7 +725,8 @@ export const BAKED_FUNNELS: Record = { "acknowledged_and_calmed" ] } - ] + ], + "overrides": {} }, { "conditions": [ @@ -860,10 +844,6 @@ export const BAKED_FUNNELS: Record = { "storageKey": "userZodiac" } }, - "fields": [], - "list": { - "options": [] - }, "variants": [] }, { @@ -892,7 +872,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "love-priority", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -950,7 +929,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "core-need", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1008,7 +986,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "partner-similarity", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1067,7 +1044,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "partner-role", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1111,7 +1087,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "relationship-strength", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1175,7 +1150,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "love-expression", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1234,7 +1208,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "relationship-future", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1298,7 +1271,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "relationship-energy", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1357,7 +1329,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "relationship-metaphor", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "single", "options": [ @@ -1425,7 +1396,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "portrait-generation", "isEndScreen": false }, - "fields": [], "list": { "selectionType": "multi", "options": [ @@ -1500,10 +1470,6 @@ export const BAKED_FUNNELS: Record = { "defaultNextScreenId": "email", "isEndScreen": false }, - "fields": [], - "list": { - "options": [] - }, "progressbars": { "items": [ { @@ -1553,12 +1519,9 @@ export const BAKED_FUNNELS: Record = { }, "navigation": { "rules": [], + "defaultNextScreenId": "screen-25", "isEndScreen": true }, - "fields": [], - "list": { - "options": [] - }, "emailInput": { "label": "Email", "placeholder": "example@email.com"