This commit is contained in:
dev.daminik00 2025-10-06 00:31:32 +02:00
parent 8333b11d25
commit d050bf7507
8 changed files with 134 additions and 91 deletions

View File

@ -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"

View File

@ -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) {

View File

@ -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;
}),
};

View File

@ -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() {
</div>
);
}
}, [previewScreen, selectedIds, handleSelectionChange, builderState.defaultTexts]);
}, [previewScreen, selectedIds, handleSelectionChange, builderState.meta, builderState.defaultTexts, builderState.screens]);
const preview = useMemo(() => {
if (!previewScreen) {

View File

@ -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,
}
);

View File

@ -4,7 +4,8 @@ import {
AvatarFallback,
} from "../avatar";
interface AvatarProps extends React.ComponentProps<typeof AvatarComponent> {
export interface AvatarProps extends Omit<React.ComponentProps<typeof AvatarComponent>, never> {
className?: string;
imageProps?: React.ComponentProps<typeof AvatarImage>;
fallbackProps?: React.ComponentProps<typeof AvatarFallback>;
}

View File

@ -82,7 +82,11 @@ const BottomActionButton = forwardRef<HTMLDivElement, BottomActionButtonProps>(
{...props}
>
<GradientBlur className="p-6 pt-11" isActiveBlur={showGradientBlur}>
{childrenAboveButton}
{childrenAboveButton && (
<div className="w-full flex justify-center">
{childrenAboveButton}
</div>
)}
{hasButton ? <ActionButton {...actionButtonProps} /> : null}
{childrenUnderButton}
</GradientBlur>

View File

@ -56,17 +56,12 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
},
"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<string, FunnelDefinition> = {
"defaultNextScreenId": "relationship-status",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -154,7 +148,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "analysis-target",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -208,7 +201,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "partner-age",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -277,7 +269,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "partner-ethnicity",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -318,7 +309,8 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"current_partner"
]
}
]
],
"overrides": {}
},
{
"conditions": [
@ -405,7 +397,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "partner-ethnicity",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -454,7 +445,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "partner-eye-color",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -528,7 +518,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "partner-hair-length",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -606,7 +595,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "burnout-support",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -655,7 +643,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "burnout-result",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -728,10 +715,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"value": "/images/ac321d94-62e3-45c6-85f4-51faf6769bab.svg",
"size": "md"
},
"fields": [],
"list": {
"options": []
},
"variants": [
{
"conditions": [
@ -742,7 +725,8 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"acknowledged_and_calmed"
]
}
]
],
"overrides": {}
},
{
"conditions": [
@ -860,10 +844,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"storageKey": "userZodiac"
}
},
"fields": [],
"list": {
"options": []
},
"variants": []
},
{
@ -892,7 +872,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "love-priority",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -950,7 +929,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "core-need",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -1008,7 +986,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "partner-similarity",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -1067,7 +1044,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "partner-role",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -1111,7 +1087,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "relationship-strength",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -1175,7 +1150,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "love-expression",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -1234,7 +1208,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "relationship-future",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -1298,7 +1271,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "relationship-energy",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -1357,7 +1329,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "relationship-metaphor",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "single",
"options": [
@ -1425,7 +1396,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "portrait-generation",
"isEndScreen": false
},
"fields": [],
"list": {
"selectionType": "multi",
"options": [
@ -1500,10 +1470,6 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
"defaultNextScreenId": "email",
"isEndScreen": false
},
"fields": [],
"list": {
"options": []
},
"progressbars": {
"items": [
{
@ -1553,12 +1519,9 @@ export const BAKED_FUNNELS: Record<string, FunnelDefinition> = {
},
"navigation": {
"rules": [],
"defaultNextScreenId": "screen-25",
"isEndScreen": true
},
"fields": [],
"list": {
"options": []
},
"emailInput": {
"label": "Email",
"placeholder": "example@email.com"