diff --git a/README.md b/README.md index ef147e0..42f14d8 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,47 @@ npm run build:full # full system development bundle ``` After building, start the chosen bundle with `npm run start` (frontend-only) or `npm run start:full`. + +## Funnel Management + +### Database Synchronization + +To sync published funnels from MongoDB into the codebase: + +```bash +# Sync all published funnels from database +npm run sync:funnels + +# Preview what would be synced (dry-run mode) +npm run sync:funnels -- --dry-run + +# Sync only specific funnels +npm run sync:funnels -- --funnel-ids funnel-test,ru-career-accelerator + +# Keep JSON files for debugging +npm run sync:funnels -- --keep-files +``` + +This script: + +1. Connects to MongoDB and fetches all latest published funnels +2. Saves them as temporary JSON files in `public/funnels/` +3. Bakes them into TypeScript (`src/lib/funnel/bakedFunnels.ts`) +4. Cleans up temporary JSON files + +### Other Funnel Commands + +```bash +# Import JSON files from public/funnels/ to MongoDB +npm run import:funnels + +# Manually bake JSON files to TypeScript +npm run bake:funnels +``` + +**Recommended Workflow:** + +1. Create/edit funnels in the admin panel +2. Publish them in the admin +3. Run `npm run sync:funnels` to update the codebase +4. Build and deploy with the latest funnels diff --git a/package.json b/package.json index fc40d79..e4f2c2b 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "lint": "eslint", "bake:funnels": "node scripts/bake-funnels.mjs", "import:funnels": "node scripts/import-funnels-to-db.mjs", + "sync:funnels": "node scripts/sync-funnels-from-db.mjs", "storybook": "storybook dev -p 6006 --ci", "build-storybook": "storybook build" }, diff --git a/public/funnels/funnel-funnel-1759061433816.json b/public/funnels/funnel-funnel-1759061433816.json deleted file mode 100644 index be13f8a..0000000 --- a/public/funnels/funnel-funnel-1759061433816.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "meta": { - "id": "funnel-1759061433816", - "title": "Новая воронка", - "description": "Описание новой воронки", - "firstScreenId": "screen-1" - }, - "screens": [ - { - "list": { - "options": [] - }, - "id": "screen-1", - "template": "info", - "title": { - "text": "Добро пожаловать!", - "font": "manrope", - "weight": "bold", - "size": "md", - "align": "center", - "color": "default" - }, - "description": { - "text": "Это ваша новая воронка. Начните редактирование.", - "font": "manrope", - "weight": "regular", - "size": "md", - "align": "center", - "color": "muted" - }, - "icon": { - "type": "emoji", - "value": "🎯", - "size": "lg" - }, - "fields": [], - "variants": [], - "position": { - "x": 120, - "y": 120 - } - } - ] -} \ No newline at end of file diff --git a/public/funnels/funnel-test-variants.json b/public/funnels/funnel-test-variants.json deleted file mode 100644 index 9ac5ec9..0000000 --- a/public/funnels/funnel-test-variants.json +++ /dev/null @@ -1,662 +0,0 @@ -{ - "meta": { - "id": "funnel-test-variants", - "title": "Relationship Portrait", - "description": "Demo funnel mirroring design screens with branching by analysis target.", - "firstScreenId": "intro-welcome" - }, - "defaultTexts": { - "nextButton": "Next", - "continueButton": "Continue" - }, - "screens": [ - { - "id": "intro-welcome", - "template": "info", - "title": { - "text": "Вы не одиноки в этом страхе", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Многие боятся повторить прошлый опыт. Мы поможем распознать верные сигналы и выбрать «своего» человека.", - "font": "inter", - "weight": "medium", - "color": "default", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "❤️", - "size": "xl" - }, - "bottomActionButton": { - "text": "Next" - }, - "navigation": { - "defaultNextScreenId": "intro-statistics" - } - }, - { - "id": "intro-statistics", - "template": "info", - "title": { - "text": "По нашей статистике 51 % женщин Овнов доверяются эмоциям. Но одной чувствительности мало. Мы покажем, какие качества второй половинки дадут тепло и уверенность, и изобразим её портрет.", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "🔥❤️", - "size": "xl" - }, - "bottomActionButton": { - "text": "Next" - }, - "navigation": { - "defaultNextScreenId": "intro-partner-traits" - } - }, - { - "id": "intro-partner-traits", - "template": "info", - "header": { - "showBackButton": false - }, - "title": { - "text": "Такой партнёр умеет слышать и поддерживать, а вы — человек с глубокой душой, который ценит искренность и силу настоящих чувств.", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "💖", - "size": "xl" - }, - "bottomActionButton": { - "text": "Next" - }, - "navigation": { - "defaultNextScreenId": "birth-date" - } - }, - { - "id": "birth-date", - "template": "date", - "title": { - "text": "Когда ты родился?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "В момент вашего рождения заложенны глубинные закономерности.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "MM", - "dayPlaceholder": "DD", - "yearPlaceholder": "YYYY", - "monthLabel": "Month", - "dayLabel": "Day", - "yearLabel": "Year", - "showSelectedDate": true, - "selectedDateLabel": "Выбранная дата:" - }, - "infoMessage": { - "text": "Мы не передаем личную информацию, она остаётся в безопасности и под вашим контролем.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "bottomActionButton": { - "text": "Next" - }, - "navigation": { - "defaultNextScreenId": "address-form" - } - }, - { - "id": "address-form", - "template": "form", - "title": { - "text": "Which best represents your hair loss and goals?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Let's personalize your hair care journey", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "fields": [ - { - "id": "address", - "label": "Address", - "placeholder": "Enter your full address", - "type": "text", - "required": true, - "maxLength": 200 - } - ], - "validationMessages": { - "required": "${field} обязательно для заполнения", - "maxLength": "Максимум ${maxLength} символов", - "invalidFormat": "Неверный формат" - }, - "navigation": { - "defaultNextScreenId": "statistics-text" - } - }, - { - "id": "statistics-text", - "template": "info", - "title": { - "text": "Which best represents your hair loss and goals?", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "По нашей статистике 51 % женщин Овнов доверяются эмоциям. Но одной чувствительности мало. Мы покажем, какие качества второй половинки дадут тепло и уверенность, и изобразим её портрет.", - "font": "inter", - "weight": "medium", - "color": "default", - "align": "center" - } - }, - { - "id": "gender", - "template": "list", - "title": { - "text": "Какого ты пола?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Все начинается с тебя! Выбери свой пол.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "female", - "label": "FEMALE", - "emoji": "💗" - }, - { - "id": "male", - "label": "MALE", - "emoji": "💙" - } - ] - }, - "navigation": { - "defaultNextScreenId": "relationship-status" - } - }, - { - "id": "relationship-status", - "template": "list", - "title": { - "text": "Вы сейчас?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Это нужно, чтобы портрет и советы были точнее.", - "color": "muted" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "in-relationship", - "label": "В отношениях" - }, - { - "id": "single", - "label": "Свободны" - }, - { - "id": "after-breakup", - "label": "После расставания" - }, - { - "id": "complicated", - "label": "Все сложно" - } - ] - }, - "navigation": { - "defaultNextScreenId": "analysis-target" - } - }, - { - "id": "analysis-target", - "template": "list", - "title": { - "text": "Кого анализируем?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "current-partner", - "label": "Текущего партнера" - }, - { - "id": "crush", - "label": "Человека, который нравится" - }, - { - "id": "ex-partner", - "label": "Бывшего" - }, - { - "id": "future-partner", - "label": "Будущую встречу" - } - ] - }, - "navigation": { - "defaultNextScreenId": "partner-age" - } - }, - { - "id": "partner-age", - "template": "list", - "title": { - "text": "Возраст партнера", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Чтобы портрет был максимально точным, уточните возраст.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "under-29", - "label": "До 29" - }, - { - "id": "30-39", - "label": "30-39" - }, - { - "id": "40-49", - "label": "40-49" - }, - { - "id": "50-59", - "label": "50-59" - }, - { - "id": "60-plus", - "label": "60+" - } - ] - }, - "variants": [ - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": ["current-partner"] - } - ], - "overrides": { - "title": { - "text": "Возраст текущего партнера", - "font": "manrope", - "weight": "bold" - } - } - }, - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": ["crush"] - } - ], - "overrides": { - "title": { - "text": "Возраст человека, который нравится", - "font": "manrope", - "weight": "bold" - }, - "bottomActionButton": { - "show": false - } - } - }, - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": ["ex-partner"] - } - ], - "overrides": { - "title": { - "text": "Возраст бывшего", - "font": "manrope", - "weight": "bold" - } - } - }, - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": ["future-partner"] - } - ], - "overrides": { - "title": { - "text": "Возраст будущего партнера", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Чтобы мы не упустили важные нюансы будущей встречи.", - "font": "inter", - "weight": "medium", - "color": "muted" - } - } - } - ], - "navigation": { - "rules": [ - { - "conditions": [ - { - "screenId": "partner-age", - "operator": "includesAny", - "optionIds": ["under-29"] - } - ], - "nextScreenId": "age-refine" - } - ], - "defaultNextScreenId": "partner-ethnicity" - } - }, - { - "id": "age-refine", - "template": "list", - "title": { - "text": "Уточните чуть точнее", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Чтобы портрет был максимально похож.", - "color": "muted" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "18-21", - "label": "18-21" - }, - { - "id": "22-25", - "label": "22-25" - }, - { - "id": "26-29", - "label": "26-29" - } - ] - }, - "navigation": { - "defaultNextScreenId": "partner-ethnicity" - } - }, - { - "id": "partner-ethnicity", - "template": "list", - "title": { - "text": "Этническая принадлежность твоей второй половинки?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "white", - "label": "White" - }, - { - "id": "hispanic", - "label": "Hispanic / Latino" - }, - { - "id": "african", - "label": "African / African-American" - }, - { - "id": "asian", - "label": "Asian" - }, - { - "id": "south-asian", - "label": "Indian / South Asian" - }, - { - "id": "middle-eastern", - "label": "Middle Eastern / Arab" - }, - { - "id": "indigenous", - "label": "Native American / Indigenous" - }, - { - "id": "no-preference", - "label": "No preference" - } - ] - }, - "navigation": { - "defaultNextScreenId": "partner-eyes" - } - }, - { - "id": "partner-eyes", - "template": "list", - "title": { - "text": "Что из этого «про глаза»?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "warm-glow", - "label": "Тёплые искры на свету" - }, - { - "id": "clear-depth", - "label": "Прозрачная глубина" - }, - { - "id": "green-sheen", - "label": "Зелёный отлив на границе зрачка" - }, - { - "id": "steel-glint", - "label": "Холодный стальной отблеск" - }, - { - "id": "deep-shadow", - "label": "Насыщенная темнота" - }, - { - "id": "dont-know", - "label": "Не знаю" - } - ] - }, - "navigation": { - "defaultNextScreenId": "partner-hair-length" - } - }, - { - "id": "partner-hair-length", - "template": "list", - "title": { - "text": "Выберите длину волос", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "От неё зависит форма и настроение портрета.", - "color": "muted" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "short", - "label": "Короткие" - }, - { - "id": "medium", - "label": "Средние" - }, - { - "id": "long", - "label": "Длинные" - } - ] - }, - "navigation": { - "defaultNextScreenId": "burnout-support" - } - }, - { - "id": "burnout-support", - "template": "list", - "title": { - "text": "Когда ты выгораешь, тебе нужно чтобы партнёр...", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { - "id": "reassure", - "label": "Признал ваше разочарование и успокоил" - }, - { - "id": "emotional-support", - "label": "Дал эмоциональную опору и безопасное пространство" - }, - { - "id": "take-over", - "label": "Перехватил быт/дела, чтобы вы восстановились" - }, - { - "id": "energize", - "label": "Вдохнул энергию через цель и короткий план действий" - }, - { - "id": "switch-positive", - "label": "Переключил на позитив: прогулка, кино, смешные истории" - } - ] - }, - "bottomActionButton": { - "show": false - }, - "navigation": { - "defaultNextScreenId": "special-offer" - } - }, - { - "id": "special-offer", - "template": "coupon", - "header": { - "show": false - }, - "title": { - "text": "Тебе повезло!", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Ты получил специальную эксклюзивную скидку на 94%", - "font": "inter", - "weight": "medium", - "color": "muted", - "align": "center" - }, - "copiedMessage": "Промокод \"{code}\" скопирован!", - "coupon": { - "title": { - "text": "Special Offer", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "94% OFF", - "font": "manrope", - "weight": "black", - "color": "card", - "size": "4xl" - }, - "description": { - "text": "Одноразовая эксклюзивная скидка", - "font": "inter", - "weight": "semiBold", - "color": "card" - } - }, - "promoCode": { - "text": "HAIR50", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Скопируйте или нажмите Continue", - "font": "inter", - "weight": "medium", - "color": "muted", - "size": "sm" - } - }, - "bottomActionButton": { - "text": "Continue" - } - } - ] -} diff --git a/public/funnels/funnel-test.json b/public/funnels/funnel-test.json deleted file mode 100644 index 09a9f04..0000000 --- a/public/funnels/funnel-test.json +++ /dev/null @@ -1,826 +0,0 @@ -{ - "meta": { - "id": "funnel-test", - "title": "Relationship Portrait", - "description": "Demo funnel mirroring design screens with branching by analysis target.", - "firstScreenId": "intro-welcome" - }, - "defaultTexts": { - "nextButton": "Next", - "continueButton": "Continue", - "privacyBanner": "Мы не передаем личную информацию, она остаётся в безопасности и под вашим контролем." - }, - "screens": [ - { - "id": "intro-welcome", - "template": "info", - "title": { - "text": "Вы не одиноки в этом страхе", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Многие боятся повторить прошлый опыт. Мы поможем распознать верные сигналы и выбрать «своего» человека.", - "font": "inter", - "weight": "medium", - "color": "default", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "❤️", - "size": "xl" - }, - "bottomActionButton": { - "text": "Next" - }, - "navigation": { - "defaultNextScreenId": "intro-statistics" - } - }, - { - "id": "intro-statistics", - "template": "info", - "title": { - "text": "По нашей статистике 51 % женщин Овнов доверяются эмоциям. Но одной чувствительности мало. Мы покажем, какие качества второй половинки дадут тепло и уверенность, и изобразим её портрет.", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "🔥❤️", - "size": "xl" - }, - "bottomActionButton": { - "text": "Next" - }, - "navigation": { - "defaultNextScreenId": "test-loaders" - } - }, - { - "id": "test-loaders", - "template": "loaders", - "title": { - "text": "Анализируем ваши ответы", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Пожалуйста, подождите...", - "font": "inter", - "weight": "medium", - "color": "muted", - "align": "center" - }, - "progressbars": { - "transitionDuration": 3000, - "items": [ - { - "title": "Анализ ответов", - "processingTitle": "Анализируем ваши ответы...", - "processingSubtitle": "Обрабатываем данные", - "completedTitle": "Анализ завершен", - "completedSubtitle": "Готово!" - }, - { - "title": "Поиск совпадений", - "processingTitle": "Ищем идеальные совпадения...", - "processingSubtitle": "Сравниваем профили", - "completedTitle": "Совпадения найдены", - "completedSubtitle": "Отлично!" - }, - { - "title": "Создание портрета", - "processingTitle": "Создаем портрет партнера...", - "processingSubtitle": "Финальный штрих", - "completedTitle": "Портрет готов", - "completedSubtitle": "Все готово!" - } - ] - }, - "bottomActionButton": { - "text": "Продолжить" - }, - "navigation": { - "defaultNextScreenId": "intro-statistics" - } - }, - { - "id": "intro-statistics", - "template": "info", - "header": { - "show": true, - "showBackButton": false - }, - "title": { - "text": "Добро пожаловать в **WitLab**!", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Мы поможем вам найти **идеального партнера** на основе глубокого анализа ваших предпочтений и характера." - }, - "icon": { - "type": "emoji", - "value": "❤️", - "size": "xl" - }, - "bottomActionButton": { - "text": "Начать" - }, - "navigation": { - "defaultNextScreenId": "birth-date" - } - }, - { - "id": "birth-date", - "template": "date", - "title": { - "text": "Когда ты родился?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "В момент вашего рождения заложенны глубинные закономерности.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "MM", - "dayPlaceholder": "DD", - "yearPlaceholder": "YYYY", - "monthLabel": "Month", - "dayLabel": "Day", - "yearLabel": "Year", - "showSelectedDate": true, - "selectedDateLabel": "Выбранная дата:" - }, - "infoMessage": { - "text": "Мы не передаем личную информацию, она остаётся в безопасности и под вашим контролем.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "bottomActionButton": { - "text": "Next" - }, - "navigation": { - "defaultNextScreenId": "address-form" - } - }, - { - "id": "address-form", - "template": "form", - "title": { - "text": "Which best represents your hair loss and goals?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Let's personalize your hair care journey", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "fields": [ - { - "id": "address", - "label": "Address", - "placeholder": "Enter your full address", - "type": "text", - "required": true, - "maxLength": 200 - } - ], - "validationMessages": { - "required": "${field} обязательно для заполнения", - "maxLength": "Максимум ${maxLength} символов", - "invalidFormat": "Неверный формат" - }, - "navigation": { - "defaultNextScreenId": "statistics-text" - } - }, - { - "id": "statistics-text", - "template": "info", - "title": { - "text": "Which best represents your hair loss and goals?", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "По нашей статистике 51 % женщин Овнов доверяются эмоциям. Но одной чувствительности мало. Мы покажем, какие качества второй половинки дадут тепло и уверенность, и изобразим её портрет.", - "font": "inter", - "weight": "medium", - "color": "default", - "align": "center" - } - }, - { - "id": "gender", - "template": "list", - "title": { - "text": "Какого ты пола?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Все начинается с тебя! Выбери свой пол.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "female", - "label": "FEMALE", - "emoji": "💗" - }, - { - "id": "male", - "label": "MALE", - "emoji": "💙" - } - ] - }, - "navigation": { - "defaultNextScreenId": "relationship-status" - } - }, - { - "id": "relationship-status", - "template": "list", - "title": { - "text": "Вы сейчас?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Это нужно, чтобы портрет и советы были точнее.", - "color": "muted" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "in-relationship", - "label": "В отношениях" - }, - { - "id": "single", - "label": "Свободны" - }, - { - "id": "after-breakup", - "label": "После расставания" - }, - { - "id": "complicated", - "label": "Все сложно" - } - ] - }, - "navigation": { - "defaultNextScreenId": "analysis-target" - } - }, - { - "id": "analysis-target", - "template": "list", - "title": { - "text": "Кого анализируем?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "current-partner", - "label": "Текущего партнера" - }, - { - "id": "crush", - "label": "Человека, который нравится" - }, - { - "id": "ex-partner", - "label": "Бывшего" - }, - { - "id": "future-partner", - "label": "Будущую встречу" - } - ] - }, - "navigation": { - "rules": [ - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": ["current-partner"] - } - ], - "nextScreenId": "current-partner-age" - }, - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": ["crush"] - } - ], - "nextScreenId": "crush-age" - }, - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": ["ex-partner"] - } - ], - "nextScreenId": "ex-partner-age" - }, - { - "conditions": [ - { - "screenId": "analysis-target", - "operator": "includesAny", - "optionIds": ["future-partner"] - } - ], - "nextScreenId": "future-partner-age" - } - ], - "defaultNextScreenId": "current-partner-age" - } - }, - { - "id": "current-partner-age", - "template": "list", - "title": { - "text": "Возраст текущего партнера", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "under-29", - "label": "До 29" - }, - { - "id": "30-39", - "label": "30-39" - }, - { - "id": "40-49", - "label": "40-49" - }, - { - "id": "50-59", - "label": "50-59" - }, - { - "id": "60-plus", - "label": "60+" - } - ] - }, - "navigation": { - "rules": [ - { - "conditions": [ - { - "screenId": "current-partner-age", - "operator": "includesAny", - "optionIds": ["under-29"] - } - ], - "nextScreenId": "age-refine" - } - ], - "defaultNextScreenId": "partner-ethnicity" - } - }, - { - "id": "crush-age", - "template": "list", - "title": { - "text": "Возраст человека, который нравится", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "under-29", - "label": "До 29" - }, - { - "id": "30-39", - "label": "30-39" - }, - { - "id": "40-49", - "label": "40-49" - }, - { - "id": "50-59", - "label": "50-59" - }, - { - "id": "60-plus", - "label": "60+" - } - ] - }, - "bottomActionButton": { - "show": false - }, - "navigation": { - "rules": [ - { - "conditions": [ - { - "screenId": "crush-age", - "operator": "includesAny", - "optionIds": ["under-29"] - } - ], - "nextScreenId": "age-refine" - } - ], - "defaultNextScreenId": "partner-ethnicity" - } - }, - { - "id": "ex-partner-age", - "template": "list", - "title": { - "text": "Возраст бывшего", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "under-29", - "label": "До 29" - }, - { - "id": "30-39", - "label": "30-39" - }, - { - "id": "40-49", - "label": "40-49" - }, - { - "id": "50-59", - "label": "50-59" - }, - { - "id": "60-plus", - "label": "60+" - } - ] - }, - "navigation": { - "rules": [ - { - "conditions": [ - { - "screenId": "ex-partner-age", - "operator": "includesAny", - "optionIds": ["under-29"] - } - ], - "nextScreenId": "age-refine" - } - ], - "defaultNextScreenId": "partner-ethnicity" - } - }, - { - "id": "future-partner-age", - "template": "list", - "title": { - "text": "Возраст будущего партнера", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "under-29", - "label": "До 29" - }, - { - "id": "30-39", - "label": "30-39" - }, - { - "id": "40-49", - "label": "40-49" - }, - { - "id": "50-59", - "label": "50-59" - }, - { - "id": "60-plus", - "label": "60+" - } - ] - }, - "navigation": { - "rules": [ - { - "conditions": [ - { - "screenId": "future-partner-age", - "operator": "includesAny", - "optionIds": ["under-29"] - } - ], - "nextScreenId": "age-refine" - } - ], - "defaultNextScreenId": "partner-ethnicity" - } - }, - { - "id": "age-refine", - "template": "list", - "title": { - "text": "Уточните чуть точнее", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Чтобы портрет был максимально похож.", - "color": "muted" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "18-21", - "label": "18-21" - }, - { - "id": "22-25", - "label": "22-25" - }, - { - "id": "26-29", - "label": "26-29" - } - ] - }, - "navigation": { - "defaultNextScreenId": "partner-ethnicity" - } - }, - { - "id": "partner-ethnicity", - "template": "list", - "title": { - "text": "Этническая принадлежность твоей второй половинки?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "white", - "label": "White" - }, - { - "id": "hispanic", - "label": "Hispanic / Latino" - }, - { - "id": "african", - "label": "African / African-American" - }, - { - "id": "asian", - "label": "Asian" - }, - { - "id": "south-asian", - "label": "Indian / South Asian" - }, - { - "id": "middle-eastern", - "label": "Middle Eastern / Arab" - }, - { - "id": "indigenous", - "label": "Native American / Indigenous" - }, - { - "id": "no-preference", - "label": "No preference" - } - ] - }, - "navigation": { - "defaultNextScreenId": "partner-eyes" - } - }, - { - "id": "partner-eyes", - "template": "list", - "title": { - "text": "Что из этого «про глаза»?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "warm-glow", - "label": "Тёплые искры на свету" - }, - { - "id": "clear-depth", - "label": "Прозрачная глубина" - }, - { - "id": "green-sheen", - "label": "Зелёный отлив на границе зрачка" - }, - { - "id": "steel-glint", - "label": "Холодный стальной отблеск" - }, - { - "id": "deep-shadow", - "label": "Насыщенная темнота" - }, - { - "id": "dont-know", - "label": "Не знаю" - } - ] - }, - "navigation": { - "defaultNextScreenId": "partner-hair-length" - } - }, - { - "id": "partner-hair-length", - "template": "list", - "title": { - "text": "Выберите длину волос", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "От неё зависит форма и настроение портрета.", - "color": "muted" - }, - "list": { - "selectionType": "single", - "options": [ - { - "id": "short", - "label": "Короткие" - }, - { - "id": "medium", - "label": "Средние" - }, - { - "id": "long", - "label": "Длинные" - } - ] - }, - "navigation": { - "defaultNextScreenId": "burnout-support" - } - }, - { - "id": "burnout-support", - "template": "list", - "title": { - "text": "Когда ты выгораешь, тебе нужно чтобы партнёр...", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { - "id": "reassure", - "label": "Признал ваше разочарование и успокоил" - }, - { - "id": "emotional-support", - "label": "Дал эмоциональную опору и безопасное пространство" - }, - { - "id": "take-over", - "label": "Перехватил быт/дела, чтобы вы восстановились" - }, - { - "id": "energize", - "label": "Вдохнул энергию через цель и короткий план действий" - }, - { - "id": "switch-positive", - "label": "Переключил на позитив: прогулка, кино, смешные истории" - } - ] - }, - "bottomActionButton": { - "show": false - }, - "navigation": { - "defaultNextScreenId": "special-offer" - } - }, - { - "id": "special-offer", - "template": "coupon", - "header": { - "show": false - }, - "title": { - "text": "Тебе повезло!", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Ты получил специальную эксклюзивную скидку на 94%", - "font": "inter", - "weight": "medium", - "color": "muted", - "align": "center" - }, - "copiedMessage": "Промокод \"{code}\" скопирован!", - "coupon": { - "title": { - "text": "Special Offer", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "94% OFF", - "font": "manrope", - "weight": "black", - "color": "card", - "size": "4xl" - }, - "description": { - "text": "Одноразовая эксклюзивная скидка", - "font": "inter", - "weight": "semiBold", - "color": "card" - } - }, - "promoCode": { - "text": "HAIR50", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Скопируйте или нажмите Continue", - "font": "inter", - "weight": "medium", - "color": "muted", - "size": "sm" - } - }, - "bottomActionButton": { - "text": "Continue" - } - } - ] -} diff --git a/public/funnels/ru-career-accelerator.json b/public/funnels/ru-career-accelerator.json deleted file mode 100644 index 5c538d0..0000000 --- a/public/funnels/ru-career-accelerator.json +++ /dev/null @@ -1,313 +0,0 @@ -{ - "meta": { - "id": "ru-career-accelerator", - "title": "CareerUp: рывок в карьере", - "description": "Воронка карьерного акселератора для специалистов и руководителей.", - "firstScreenId": "welcome" - }, - "defaultTexts": { - "nextButton": "Далее", - "continueButton": "Продолжить" - }, - "screens": [ - { - "id": "welcome", - "template": "info", - "title": { - "text": "Повысь доход и статус за 12 недель", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Коуч, карьерный стратег и HR-директор ведут тебя к новой должности или росту дохода.", - "font": "inter", - "weight": "medium", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "🚀", - "size": "xl" - }, - "bottomActionButton": { - "text": "Пройти диагностику" - }, - "navigation": { - "defaultNextScreenId": "pain" - } - }, - { - "id": "pain", - "template": "info", - "title": { - "text": "Почему карьера застопорилась?", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Нет стратегии, страх переговоров и слабый личный бренд. Мы закрываем каждый пробел.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "goal-date" - } - }, - { - "id": "goal-date", - "template": "date", - "title": { - "text": "Когда хочешь выйти на новую позицию?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Сформируем спринты под конкретный дедлайн.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "ММ", - "dayPlaceholder": "ДД", - "yearPlaceholder": "ГГГГ", - "monthLabel": "Месяц", - "dayLabel": "День", - "yearLabel": "Год", - "showSelectedDate": true, - "selectedDateLabel": "Цель к:" - }, - "navigation": { - "defaultNextScreenId": "current-role" - } - }, - { - "id": "current-role", - "template": "list", - "title": { - "text": "Текущая роль", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "specialist", "label": "Специалист" }, - { "id": "lead", "label": "Тимлид" }, - { "id": "manager", "label": "Руководитель отдела" }, - { "id": "c-level", "label": "C-level" } - ] - }, - "navigation": { - "defaultNextScreenId": "target" - } - }, - { - "id": "target", - "template": "list", - "title": { - "text": "Желаемая цель", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "promotion", "label": "Повышение внутри компании" }, - { "id": "newjob", "label": "Переход в топ-компанию" }, - { "id": "salary", "label": "Рост дохода на 50%" }, - { "id": "relocate", "label": "Релокация" } - ] - }, - "navigation": { - "defaultNextScreenId": "case" - } - }, - { - "id": "case", - "template": "info", - "title": { - "text": "История Марии: +85% к доходу", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "За 9 недель она прошла программу, обновила резюме, договорилась о relocation и заняла позицию руководителя продукта.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "bottlenecks" - } - }, - { - "id": "bottlenecks", - "template": "list", - "title": { - "text": "Где нужна поддержка?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "resume", "label": "Резюме и LinkedIn" }, - { "id": "network", "label": "Нетворкинг" }, - { "id": "interview", "label": "Интервью" }, - { "id": "negotiation", "label": "Переговоры о зарплате" }, - { "id": "leadership", "label": "Лидерские навыки" } - ] - }, - "navigation": { - "defaultNextScreenId": "program-format" - } - }, - { - "id": "program-format", - "template": "list", - "title": { - "text": "Какой формат подходит?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "group", "label": "Групповой акселератор" }, - { "id": "1on1", "label": "Индивидуальное сопровождение" }, - { "id": "vip", "label": "Executive программа" } - ] - }, - "navigation": { - "defaultNextScreenId": "form" - } - }, - { - "id": "form", - "template": "form", - "title": { - "text": "Получить план роста", - "font": "manrope", - "weight": "bold" - }, - "fields": [ - { "id": "name", "label": "Имя", "placeholder": "Как к вам обращаться", "type": "text", "required": true }, - { "id": "phone", "label": "Телефон", "placeholder": "+7 (___) ___-__-__", "type": "tel", "required": true }, - { "id": "email", "label": "Email", "placeholder": "Получить карьерный план", "type": "email", "required": true } - ], - "validationMessages": { - "required": "Поле ${field} обязательно", - "invalidFormat": "Проверьте формат" - }, - "navigation": { - "defaultNextScreenId": "mentor" - } - }, - { - "id": "mentor", - "template": "info", - "title": { - "text": "Твой наставник", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Ex-HR Director из Microsoft поможет построить стратегию и проведёт ролевые интервью.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "packages" - } - }, - { - "id": "packages", - "template": "list", - "title": { - "text": "Выберите пакет", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "start", "label": "Start — 6 недель" }, - { "id": "pro", "label": "Pro — 12 недель" }, - { "id": "elite", "label": "Elite — 16 недель + наставник" } - ] - }, - "navigation": { - "defaultNextScreenId": "bonus" - } - }, - { - "id": "bonus", - "template": "info", - "title": { - "text": "Бонусы при оплате сегодня", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Шаблоны писем рекрутерам, библиотека резюме и доступ к закрытому карьерному клубу.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "coupon" - } - }, - { - "id": "coupon", - "template": "coupon", - "title": { - "text": "Зафиксируй скидку и бонусы", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Скидка 20% и два дополнительных карьерных созвона.", - "font": "inter", - "weight": "medium", - "align": "center", - "color": "muted" - }, - "coupon": { - "title": { - "text": "CareerUp", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "-20%", - "font": "manrope", - "weight": "black", - "size": "4xl" - }, - "description": { - "text": "Программа + 2 коуч-сессии", - "font": "inter", - "weight": "medium" - } - }, - "promoCode": { - "text": "CAREER20", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Нажмите, чтобы активировать предложение", - "font": "inter", - "weight": "medium", - "size": "sm", - "color": "muted" - } - }, - "copiedMessage": "Промокод {code} скопирован!" - } - ] -} diff --git a/public/funnels/ru-finance-freedom.json b/public/funnels/ru-finance-freedom.json deleted file mode 100644 index 9dbf993..0000000 --- a/public/funnels/ru-finance-freedom.json +++ /dev/null @@ -1,314 +0,0 @@ -{ - "meta": { - "id": "ru-finance-freedom", - "title": "Capital Sense: финансовая свобода", - "description": "Воронка для консультаций по инвестициям и личному финансовому планированию.", - "firstScreenId": "intro" - }, - "defaultTexts": { - "nextButton": "Далее", - "continueButton": "Продолжить" - }, - "screens": [ - { - "id": "intro", - "template": "info", - "title": { - "text": "Сформируй капитал, который работает за тебя", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Персональный финансовый план, подбор инструментов и сопровождение на каждом шаге.", - "font": "inter", - "weight": "medium", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "💼", - "size": "xl" - }, - "bottomActionButton": { - "text": "Начать" - }, - "navigation": { - "defaultNextScreenId": "fear" - } - }, - { - "id": "fear", - "template": "info", - "title": { - "text": "Почему деньги не приносят свободу?", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Разные цели, хаотичные инвестиции и страх потерять. Мы создаём стратегию с защитой и ростом.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "goal-date" - } - }, - { - "id": "goal-date", - "template": "date", - "title": { - "text": "Когда хочешь достичь финансовой цели?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Укажи дату, чтобы рассчитать необходимые шаги.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "ММ", - "dayPlaceholder": "ДД", - "yearPlaceholder": "ГГГГ", - "monthLabel": "Месяц", - "dayLabel": "День", - "yearLabel": "Год", - "showSelectedDate": true, - "selectedDateLabel": "Цель к дате:" - }, - "navigation": { - "defaultNextScreenId": "current-income" - } - }, - { - "id": "current-income", - "template": "list", - "title": { - "text": "Какой у тебя ежемесячный доход?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "lt100k", "label": "До 100 000 ₽" }, - { "id": "100-250", "label": "100 000 – 250 000 ₽" }, - { "id": "250-500", "label": "250 000 – 500 000 ₽" }, - { "id": "500plus", "label": "Свыше 500 000 ₽" } - ] - }, - "navigation": { - "defaultNextScreenId": "savings" - } - }, - { - "id": "savings", - "template": "list", - "title": { - "text": "Как распределяются накопления?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "deposit", "label": "Банковские вклады" }, - { "id": "stocks", "label": "Акции и фонды" }, - { "id": "realty", "label": "Недвижимость" }, - { "id": "business", "label": "Собственный бизнес" }, - { "id": "cash", "label": "Храню в наличных" } - ] - }, - "navigation": { - "defaultNextScreenId": "risk" - } - }, - { - "id": "risk", - "template": "list", - "title": { - "text": "Готовность к риску", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "conservative", "label": "Консервативная стратегия" }, - { "id": "balanced", "label": "Сбалансированный портфель" }, - { "id": "aggressive", "label": "Готов к высоким рискам ради роста" } - ] - }, - "navigation": { - "defaultNextScreenId": "case" - } - }, - { - "id": "case", - "template": "info", - "title": { - "text": "История Александра: капитал 12 млн за 5 лет", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Использовали облигации, дивидендные акции и страхование. Доходность 18% при низком риске.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "priorities" - } - }, - { - "id": "priorities", - "template": "list", - "title": { - "text": "Выбери финансовые приоритеты", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "capital", "label": "Долгосрочный капитал" }, - { "id": "passive", "label": "Пассивный доход" }, - { "id": "education", "label": "Образование детей" }, - { "id": "pension", "label": "Пенсия без тревог" }, - { "id": "protection", "label": "Страхование и защита" } - ] - }, - "navigation": { - "defaultNextScreenId": "form" - } - }, - { - "id": "form", - "template": "form", - "title": { - "text": "Получить расчёт стратегии", - "font": "manrope", - "weight": "bold" - }, - "fields": [ - { "id": "name", "label": "Имя", "placeholder": "Как вас зовут", "type": "text", "required": true }, - { "id": "phone", "label": "Телефон", "placeholder": "+7 (___) ___-__-__", "type": "tel", "required": true }, - { "id": "email", "label": "Email", "placeholder": "Получить PDF-план", "type": "email", "required": true } - ], - "validationMessages": { - "required": "Поле ${field} обязательно", - "invalidFormat": "Проверьте формат" - }, - "navigation": { - "defaultNextScreenId": "advisor" - } - }, - { - "id": "advisor", - "template": "info", - "title": { - "text": "Ваш персональный советник", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Сертифицированный финансовый консультант составит портфель и будет сопровождать на ежемесячных созвонах.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "packages" - } - }, - { - "id": "packages", - "template": "list", - "title": { - "text": "Выберите пакет сопровождения", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "start", "label": "Start — до 2 млн ₽" }, - { "id": "growth", "label": "Growth — до 10 млн ₽" }, - { "id": "elite", "label": "Elite — от 10 млн ₽ и Family Office" } - ] - }, - "navigation": { - "defaultNextScreenId": "bonus" - } - }, - { - "id": "bonus", - "template": "info", - "title": { - "text": "Бонусы к записи сегодня", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Инвестиционный чек-лист и бесплатный аудит страховок от партнёра.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "coupon" - } - }, - { - "id": "coupon", - "template": "coupon", - "title": { - "text": "Забронируйте условия", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Скидка 25% на первый месяц сопровождения и аудит портфеля.", - "font": "inter", - "weight": "medium", - "align": "center", - "color": "muted" - }, - "coupon": { - "title": { - "text": "Capital Sense", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "-25%", - "font": "manrope", - "weight": "black", - "size": "4xl" - }, - "description": { - "text": "Первый месяц и аудит портфеля", - "font": "inter", - "weight": "medium" - } - }, - "promoCode": { - "text": "FIN25", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Нажмите, чтобы активировать промокод", - "font": "inter", - "weight": "medium", - "size": "sm", - "color": "muted" - } - }, - "copiedMessage": "Промокод {code} скопирован!" - } - ] -} diff --git a/public/funnels/ru-fitness-transform.json b/public/funnels/ru-fitness-transform.json deleted file mode 100644 index c992552..0000000 --- a/public/funnels/ru-fitness-transform.json +++ /dev/null @@ -1,356 +0,0 @@ -{ - "meta": { - "id": "ru-fitness-transform", - "title": "Фитнес-вызов: Тело мечты за 12 недель", - "description": "Воронка для продажи онлайн-программы персональных тренировок и питания.", - "firstScreenId": "intro-hero" - }, - "defaultTexts": { - "nextButton": "Далее", - "continueButton": "Продолжить" - }, - "screens": [ - { - "id": "intro-hero", - "template": "info", - "title": { - "text": "Создай тело, которое будет восхищать", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Личный куратор, готовые тренировки и поддержка нутрициолога для стремительного результата.", - "font": "inter", - "weight": "medium", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "💪", - "size": "xl" - }, - "bottomActionButton": { - "text": "Начать диагностику" - }, - "navigation": { - "defaultNextScreenId": "pain-check" - } - }, - { - "id": "pain-check", - "template": "info", - "title": { - "text": "Почему результат не держится?", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "92% наших клиентов приходят после десятков попыток похудеть. Мы устраняем коренные причины: гормональный фон, сон, питание.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "target-date" - } - }, - { - "id": "target-date", - "template": "date", - "title": { - "text": "Когда планируешь увидеть первые изменения?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Укажи желаемую дату — мы построим обратный план.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "ММ", - "dayPlaceholder": "ДД", - "yearPlaceholder": "ГГГГ", - "monthLabel": "Месяц", - "dayLabel": "День", - "yearLabel": "Год", - "showSelectedDate": true, - "selectedDateLabel": "Целевая дата:" - }, - "navigation": { - "defaultNextScreenId": "current-state" - } - }, - { - "id": "current-state", - "template": "list", - "title": { - "text": "Что больше всего мешает сейчас?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "time", "label": "Нет времени на зал" }, - { "id": "food", "label": "Срывы в питании" }, - { "id": "motivation", "label": "Не хватает мотивации" }, - { "id": "health", "label": "Боли в спине/суставах" }, - { "id": "plateau", "label": "Вес стоит на месте" } - ] - }, - "navigation": { - "defaultNextScreenId": "goal-selection" - } - }, - { - "id": "goal-selection", - "template": "list", - "title": { - "text": "Какая цель приоритетна?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Выбери один вариант — мы адаптируем программу.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "fat-loss", "label": "Снижение веса" }, - { "id": "tone", "label": "Упругость и рельеф" }, - { "id": "health", "label": "Самочувствие и энергия" }, - { "id": "postpartum", "label": "Восстановление после родов" } - ] - }, - "navigation": { - "defaultNextScreenId": "success-story" - } - }, - { - "id": "success-story", - "template": "info", - "title": { - "text": "Света минус 14 кг за 12 недель", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Она работала по 12 часов в офисе. Мы составили план из 30-минутных тренировок и настроили питание без голода. Теперь она ведёт блог и вдохновляет подруг.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "lifestyle" - } - }, - { - "id": "lifestyle", - "template": "list", - "title": { - "text": "Сколько времени готов(а) уделять?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "15min", "label": "15–20 минут в день" }, - { "id": "30min", "label": "30–40 минут" }, - { "id": "60min", "label": "60 минут и более" } - ] - }, - "navigation": { - "defaultNextScreenId": "nutrition" - } - }, - { - "id": "nutrition", - "template": "info", - "title": { - "text": "Питание без жёстких запретов", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Балансируем рацион под твои привычки: любимые блюда остаются, меняются только пропорции.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "support-format" - } - }, - { - "id": "support-format", - "template": "list", - "title": { - "text": "Какой формат поддержки комфортен?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "chat", "label": "Чат с куратором ежедневно" }, - { "id": "calls", "label": "Созвоны раз в неделю" }, - { "id": "video", "label": "Видеоразбор техники" }, - { "id": "community", "label": "Группа единомышленников" } - ] - }, - "navigation": { - "defaultNextScreenId": "contact-form" - } - }, - { - "id": "contact-form", - "template": "form", - "title": { - "text": "Почти готово! Оставь контакты для персональной стратегии", - "font": "manrope", - "weight": "bold" - }, - "fields": [ - { - "id": "name", - "label": "Имя", - "placeholder": "Как к тебе обращаться", - "type": "text", - "required": true, - "maxLength": 60 - }, - { - "id": "phone", - "label": "Телефон", - "placeholder": "+7 (___) ___-__-__", - "type": "tel", - "required": true - }, - { - "id": "email", - "label": "Email", - "placeholder": "Для отправки материалов", - "type": "email", - "required": true - } - ], - "validationMessages": { - "required": "Поле ${field} обязательно", - "maxLength": "Максимум ${maxLength} символов", - "invalidFormat": "Проверь формат" - }, - "navigation": { - "defaultNextScreenId": "coach-match" - } - }, - { - "id": "coach-match", - "template": "info", - "title": { - "text": "Подбираем наставника", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Мы нашли тренера, который специализируется на твоём запросе и будет на связи 24/7.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "bonus-overview" - } - }, - { - "id": "bonus-overview", - "template": "info", - "title": { - "text": "Что входит в программу", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Получишь 36 адаптивных тренировок, 3 чек-листа питания, психологическую поддержку и доступ к закрытым эфиром.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "package-choice" - } - }, - { - "id": "package-choice", - "template": "list", - "title": { - "text": "Выбери формат участия", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "online", "label": "Онлайн-куратор и видеоуроки" }, - { "id": "vip", "label": "VIP: личные созвоны и чат 24/7" }, - { "id": "studio", "label": "Комбо: онлайн + студийные тренировки" } - ] - }, - "navigation": { - "defaultNextScreenId": "final-offer" - } - }, - { - "id": "final-offer", - "template": "coupon", - "title": { - "text": "Зафиксируй место и подарок", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Скидка действует 24 часа после прохождения диагностики.", - "font": "inter", - "weight": "medium", - "align": "center", - "color": "muted" - }, - "coupon": { - "title": { - "text": "Фитнес-вызов", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "-35%", - "font": "manrope", - "weight": "black", - "size": "4xl" - }, - "description": { - "text": "Персональная программа и чат с тренером", - "font": "inter", - "weight": "medium" - } - }, - "promoCode": { - "text": "BODY35", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Нажми \"Продолжить\" чтобы закрепить скидку", - "font": "inter", - "weight": "medium", - "size": "sm", - "color": "muted" - } - }, - "copiedMessage": "Промокод {code} скопирован!" - } - ] -} diff --git a/public/funnels/ru-interior-signature.json b/public/funnels/ru-interior-signature.json deleted file mode 100644 index 9790026..0000000 --- a/public/funnels/ru-interior-signature.json +++ /dev/null @@ -1,330 +0,0 @@ -{ - "meta": { - "id": "ru-interior-signature", - "title": "Design Bureau: интерьер под ключ", - "description": "Воронка студии дизайна интерьера с авторским сопровождением ремонта.", - "firstScreenId": "intro" - }, - "defaultTexts": { - "nextButton": "Далее", - "continueButton": "Продолжить" - }, - "screens": [ - { - "id": "intro", - "template": "info", - "title": { - "text": "Интерьер, который отражает ваш характер", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Создаём дизайн-проекты премиум-класса с полным контролем ремонта и экономией бюджета до 18%.", - "font": "inter", - "weight": "medium", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "🏡", - "size": "xl" - }, - "bottomActionButton": { - "text": "Начать проект" - }, - "navigation": { - "defaultNextScreenId": "problem" - } - }, - { - "id": "problem", - "template": "info", - "title": { - "text": "Типовая планировка крадёт эмоции", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Мы превращаем квадратные метры в пространство, где хочется жить, а не просто находиться.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "finish-date" - } - }, - { - "id": "finish-date", - "template": "date", - "title": { - "text": "Когда планируете переезд?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Укажи сроки, чтобы мы составили реалистичный план работ.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "ММ", - "dayPlaceholder": "ДД", - "yearPlaceholder": "ГГГГ", - "monthLabel": "Месяц", - "dayLabel": "День", - "yearLabel": "Год", - "showSelectedDate": true, - "selectedDateLabel": "Переезд:" - }, - "navigation": { - "defaultNextScreenId": "property-type" - } - }, - { - "id": "property-type", - "template": "list", - "title": { - "text": "Какой объект оформляете?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "apartment", "label": "Квартира" }, - { "id": "house", "label": "Дом" }, - { "id": "office", "label": "Коммерческое пространство" } - ] - }, - "navigation": { - "defaultNextScreenId": "style" - } - }, - { - "id": "style", - "template": "list", - "title": { - "text": "Стиль, который вдохновляет", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "minimal", "label": "Минимализм" }, - { "id": "loft", "label": "Лофт" }, - { "id": "classic", "label": "Современная классика" }, - { "id": "eco", "label": "Эко" }, - { "id": "mix", "label": "Эклектика" } - ] - }, - "navigation": { - "defaultNextScreenId": "pain-points" - } - }, - { - "id": "pain-points", - "template": "list", - "title": { - "text": "Что вызывает наибольшие сложности?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "planning", "label": "Планировка" }, - { "id": "contractors", "label": "Поиск подрядчиков" }, - { "id": "budget", "label": "Контроль бюджета" }, - { "id": "decor", "label": "Подбор мебели и декора" } - ] - }, - "navigation": { - "defaultNextScreenId": "case" - } - }, - { - "id": "case", - "template": "info", - "title": { - "text": "Квартира в ЖК CITY PARK", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Мы оптимизировали планировку, сэкономили 2,4 млн ₽ на поставщиках и завершили ремонт на 3 недели раньше срока.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "services" - } - }, - { - "id": "services", - "template": "info", - "title": { - "text": "Что входит в нашу работу", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "3D-визуализации, рабочие чертежи, авторский надзор, логистика материалов и финансовый контроль.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "budget" - } - }, - { - "id": "budget", - "template": "list", - "title": { - "text": "Планируемый бюджет проекта", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "2m", "label": "до 2 млн ₽" }, - { "id": "5m", "label": "2 – 5 млн ₽" }, - { "id": "10m", "label": "5 – 10 млн ₽" }, - { "id": "10mplus", "label": "Более 10 млн ₽" } - ] - }, - "navigation": { - "defaultNextScreenId": "form" - } - }, - { - "id": "form", - "template": "form", - "title": { - "text": "Получите концепцию и смету", - "font": "manrope", - "weight": "bold" - }, - "fields": [ - { "id": "name", "label": "Имя", "placeholder": "Как к вам обращаться", "type": "text", "required": true }, - { "id": "phone", "label": "Телефон", "placeholder": "+7 (___) ___-__-__", "type": "tel", "required": true }, - { "id": "email", "label": "Email", "placeholder": "Получить презентацию", "type": "email", "required": true } - ], - "validationMessages": { - "required": "Поле ${field} обязательно", - "invalidFormat": "Проверьте формат" - }, - "navigation": { - "defaultNextScreenId": "designer" - } - }, - { - "id": "designer", - "template": "info", - "title": { - "text": "Персональный дизайнер", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Автор проектов для бизнес-элиты. Ведёт максимум 5 объектов, чтобы уделять максимум внимания.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "packages" - } - }, - { - "id": "packages", - "template": "list", - "title": { - "text": "Выберите формат работы", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "concept", "label": "Concept — планировка и визуализации" }, - { "id": "supervision", "label": "Control — авторский надзор" }, - { "id": "turnkey", "label": "Turnkey — ремонт под ключ" } - ] - }, - "navigation": { - "defaultNextScreenId": "bonus" - } - }, - { - "id": "bonus", - "template": "info", - "title": { - "text": "Бонусы при бронировании сегодня", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Авторский колорит и подбор мебели от итальянских брендов со скидкой до 30%.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "coupon" - } - }, - { - "id": "coupon", - "template": "coupon", - "title": { - "text": "Зафиксируйте привилегии", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Скидка 20% на дизайн-проект и доступ к базе подрядчиков.", - "font": "inter", - "weight": "medium", - "align": "center", - "color": "muted" - }, - "coupon": { - "title": { - "text": "Design Bureau", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "-20%", - "font": "manrope", - "weight": "black", - "size": "4xl" - }, - "description": { - "text": "Дизайн-проект + база подрядчиков", - "font": "inter", - "weight": "medium" - } - }, - "promoCode": { - "text": "DESIGN20", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Нажмите, чтобы получить предложение", - "font": "inter", - "weight": "medium", - "size": "sm", - "color": "muted" - } - }, - "copiedMessage": "Промокод {code} скопирован!" - } - ] -} diff --git a/public/funnels/ru-kids-robotics.json b/public/funnels/ru-kids-robotics.json deleted file mode 100644 index e4508f6..0000000 --- a/public/funnels/ru-kids-robotics.json +++ /dev/null @@ -1,311 +0,0 @@ -{ - "meta": { - "id": "ru-kids-robotics", - "title": "RoboKids: будущее ребёнка", - "description": "Воронка для школы робототехники и программирования для детей 6-14 лет.", - "firstScreenId": "welcome" - }, - "defaultTexts": { - "nextButton": "Далее", - "continueButton": "Продолжить" - }, - "screens": [ - { - "id": "welcome", - "template": "info", - "title": { - "text": "Подарите ребёнку навыки будущего", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Проектные занятия по робототехнике, программированию и soft skills в игровой форме.", - "font": "inter", - "weight": "medium", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "🤖", - "size": "xl" - }, - "bottomActionButton": { - "text": "Узнать программу" - }, - "navigation": { - "defaultNextScreenId": "pain" - } - }, - { - "id": "pain", - "template": "info", - "title": { - "text": "Почему важно развивать навыки сейчас", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "90% современных профессий требуют технического мышления. Мы даём ребёнку уверенность и любовь к обучению.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "birth-date" - } - }, - { - "id": "birth-date", - "template": "date", - "title": { - "text": "Когда родился ваш ребёнок?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Возраст помогает подобрать подходящую программу.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "ММ", - "dayPlaceholder": "ДД", - "yearPlaceholder": "ГГГГ", - "monthLabel": "Месяц", - "dayLabel": "День", - "yearLabel": "Год", - "showSelectedDate": true, - "selectedDateLabel": "Возраст:" - }, - "navigation": { - "defaultNextScreenId": "interest" - } - }, - { - "id": "interest", - "template": "list", - "title": { - "text": "Что нравится ребёнку?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "lego", "label": "Конструировать" }, - { "id": "games", "label": "Компьютерные игры" }, - { "id": "science", "label": "Экспериментировать" }, - { "id": "art", "label": "Рисовать и создавать истории" } - ] - }, - "navigation": { - "defaultNextScreenId": "skills" - } - }, - { - "id": "skills", - "template": "list", - "title": { - "text": "Какие навыки хотите усилить?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "logic", "label": "Логика и математика" }, - { "id": "team", "label": "Командная работа" }, - { "id": "presentation", "label": "Презентация проектов" }, - { "id": "creativity", "label": "Креативность" } - ] - }, - "navigation": { - "defaultNextScreenId": "case" - } - }, - { - "id": "case", - "template": "info", - "title": { - "text": "Кейс семьи Еремовых", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Сын собрал робота-доставщика и выиграл региональный конкурс. Теперь учится в технопарке.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "format" - } - }, - { - "id": "format", - "template": "list", - "title": { - "text": "Какой формат занятий удобен?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "offline", "label": "Очно в технопарке" }, - { "id": "online", "label": "Онлайн-лаборатория" }, - { "id": "hybrid", "label": "Комбо: онлайн + офлайн" } - ] - }, - "navigation": { - "defaultNextScreenId": "schedule" - } - }, - { - "id": "schedule", - "template": "list", - "title": { - "text": "Выберите расписание", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "weekend", "label": "Выходные" }, - { "id": "weekday", "label": "Будни после школы" }, - { "id": "intensive", "label": "Интенсивные каникулы" } - ] - }, - "navigation": { - "defaultNextScreenId": "form" - } - }, - { - "id": "form", - "template": "form", - "title": { - "text": "Получите бесплатный пробный урок", - "font": "manrope", - "weight": "bold" - }, - "fields": [ - { "id": "parentName", "label": "Имя родителя", "placeholder": "Как к вам обращаться", "type": "text", "required": true }, - { "id": "childName", "label": "Имя ребёнка", "placeholder": "Имя ребёнка", "type": "text", "required": true }, - { "id": "phone", "label": "Телефон", "placeholder": "+7 (___) ___-__-__", "type": "tel", "required": true } - ], - "validationMessages": { - "required": "Поле ${field} обязательно", - "invalidFormat": "Проверьте корректность" - }, - "navigation": { - "defaultNextScreenId": "mentor" - } - }, - { - "id": "mentor", - "template": "info", - "title": { - "text": "Ваш наставник", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Педагог MIT и финалист World Robot Olympiad проведёт вводную встречу и вовлечёт ребёнка в проект.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "packages" - } - }, - { - "id": "packages", - "template": "list", - "title": { - "text": "Выберите программу", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "start", "label": "Start — 2 месяца" }, - { "id": "pro", "label": "Pro — 6 месяцев" }, - { "id": "elite", "label": "Elite — 12 месяцев + наставник" } - ] - }, - "navigation": { - "defaultNextScreenId": "bonus" - } - }, - { - "id": "bonus", - "template": "info", - "title": { - "text": "Бонусы для новых семей", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Сертификат на 3D-печать проекта и доступ к киберспортивной студии.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "coupon" - } - }, - { - "id": "coupon", - "template": "coupon", - "title": { - "text": "Забронируйте место", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Скидка 15% и подарок на первый месяц обучения.", - "font": "inter", - "weight": "medium", - "align": "center", - "color": "muted" - }, - "coupon": { - "title": { - "text": "RoboKids", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "-15%", - "font": "manrope", - "weight": "black", - "size": "4xl" - }, - "description": { - "text": "Первый месяц + подарок", - "font": "inter", - "weight": "medium" - } - }, - "promoCode": { - "text": "ROBO15", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Нажмите, чтобы активировать скидку", - "font": "inter", - "weight": "medium", - "size": "sm", - "color": "muted" - } - }, - "copiedMessage": "Промокод {code} скопирован!" - } - ] -} diff --git a/public/funnels/ru-language-immersion.json b/public/funnels/ru-language-immersion.json deleted file mode 100644 index 438c440..0000000 --- a/public/funnels/ru-language-immersion.json +++ /dev/null @@ -1,330 +0,0 @@ -{ - "meta": { - "id": "ru-language-immersion", - "title": "LinguaPro: английский за 3 месяца", - "description": "Воронка онлайн-школы английского языка для взрослых.", - "firstScreenId": "start" - }, - "defaultTexts": { - "nextButton": "Далее", - "continueButton": "Продолжить" - }, - "screens": [ - { - "id": "start", - "template": "info", - "title": { - "text": "Говори уверенно через 12 недель", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Живые уроки с преподавателем, ежедневная практика и контроль прогресса.", - "font": "inter", - "weight": "medium", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "🌍", - "size": "xl" - }, - "bottomActionButton": { - "text": "Диагностика уровня" - }, - "navigation": { - "defaultNextScreenId": "pain" - } - }, - { - "id": "pain", - "template": "info", - "title": { - "text": "Почему 4 из 5 студентов не доходят до результата?", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Нерегулярность, отсутствие практики и скучные уроки. Мы исправили каждую точку.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "goal-date" - } - }, - { - "id": "goal-date", - "template": "date", - "title": { - "text": "Когда предстоит важное событие на английском?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Сформируем план подготовки под конкретную дату.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "ММ", - "dayPlaceholder": "ДД", - "yearPlaceholder": "ГГГГ", - "monthLabel": "Месяц", - "dayLabel": "День", - "yearLabel": "Год", - "showSelectedDate": true, - "selectedDateLabel": "Событие:" - }, - "navigation": { - "defaultNextScreenId": "current-level" - } - }, - { - "id": "current-level", - "template": "list", - "title": { - "text": "Оцени свой текущий уровень", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "starter", "label": "Начинаю с нуля" }, - { "id": "elementary", "label": "Могу поддержать простую беседу" }, - { "id": "intermediate", "label": "Хочу говорить свободно" }, - { "id": "advanced", "label": "Нужен профессиональный английский" } - ] - }, - "navigation": { - "defaultNextScreenId": "difficulties" - } - }, - { - "id": "difficulties", - "template": "list", - "title": { - "text": "Что даётся сложнее всего?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "speaking", "label": "Разговорная речь" }, - { "id": "listening", "label": "Понимание на слух" }, - { "id": "grammar", "label": "Грамматика" }, - { "id": "vocabulary", "label": "Словарный запас" }, - { "id": "confidence", "label": "Стеснение" } - ] - }, - "navigation": { - "defaultNextScreenId": "success-story" - } - }, - { - "id": "success-story", - "template": "info", - "title": { - "text": "Кейс Максима: оффер в международной компании", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "За 10 недель он прокачал разговорный до Upper-Intermediate, прошёл интервью и удвоил доход.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "study-format" - } - }, - { - "id": "study-format", - "template": "list", - "title": { - "text": "Как удобнее заниматься?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "individual", "label": "Индивидуально с преподавателем" }, - { "id": "mini-group", "label": "Мини-группа до 4 человек" }, - { "id": "intensive", "label": "Интенсив по выходным" } - ] - }, - "navigation": { - "defaultNextScreenId": "practice" - } - }, - { - "id": "practice", - "template": "info", - "title": { - "text": "Практика каждый день", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Марафоны спикинга, разговорные клубы с носителями и тренажёр произношения в приложении.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "support" - } - }, - { - "id": "support", - "template": "list", - "title": { - "text": "Что важно в поддержке?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "mentor", "label": "Личный куратор" }, - { "id": "feedback", "label": "Еженедельный фидбек" }, - { "id": "chat", "label": "Чат 24/7" }, - { "id": "reports", "label": "Отчёт о прогрессе" } - ] - }, - "navigation": { - "defaultNextScreenId": "contact-form" - } - }, - { - "id": "contact-form", - "template": "form", - "title": { - "text": "Получите индивидуальный учебный план", - "font": "manrope", - "weight": "bold" - }, - "fields": [ - { "id": "name", "label": "Имя", "placeholder": "Как к вам обращаться", "type": "text", "required": true }, - { "id": "phone", "label": "Телефон", "placeholder": "+7 (___) ___-__-__", "type": "tel", "required": true }, - { "id": "email", "label": "Email", "placeholder": "Получите PDF-план", "type": "email", "required": true } - ], - "validationMessages": { - "required": "Поле ${field} обязательно", - "invalidFormat": "Проверьте ввод" - }, - "navigation": { - "defaultNextScreenId": "mentor-match" - } - }, - { - "id": "mentor-match", - "template": "info", - "title": { - "text": "Мы подобрали вам преподавателя", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Сертифицированный CELTA преподаватель с опытом подготовки к собеседованиям.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "programs" - } - }, - { - "id": "programs", - "template": "list", - "title": { - "text": "Выберите программу", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "starter", "label": "Start Now — 8 недель" }, - { "id": "pro", "label": "Career Boost — 12 недель" }, - { "id": "vip", "label": "Executive — 16 недель + коуч" } - ] - }, - "navigation": { - "defaultNextScreenId": "bonus" - } - }, - { - "id": "bonus", - "template": "info", - "title": { - "text": "Бонусы для тех, кто оплачивает сегодня", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Доступ к библиотеке TED-тренажёров и разговорный клуб в подарок.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "coupon" - } - }, - { - "id": "coupon", - "template": "coupon", - "title": { - "text": "Закрепите скидку", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Скидка 30% на первый модуль и бонусный урок с носителем.", - "font": "inter", - "weight": "medium", - "align": "center", - "color": "muted" - }, - "coupon": { - "title": { - "text": "LinguaPro", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "-30%", - "font": "manrope", - "weight": "black", - "size": "4xl" - }, - "description": { - "text": "Курс и разговорный клуб", - "font": "inter", - "weight": "medium" - } - }, - "promoCode": { - "text": "LINGUA30", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Нажмите, чтобы использовать промокод", - "font": "inter", - "weight": "medium", - "size": "sm", - "color": "muted" - } - }, - "copiedMessage": "Промокод {code} скопирован!" - } - ] -} diff --git a/public/funnels/ru-mind-balance.json b/public/funnels/ru-mind-balance.json deleted file mode 100644 index af21137..0000000 --- a/public/funnels/ru-mind-balance.json +++ /dev/null @@ -1,313 +0,0 @@ -{ - "meta": { - "id": "ru-mind-balance", - "title": "MindBalance: психотерапия для результата", - "description": "Воронка сервиса подбора психолога с поддержкой и пакетами сопровождения.", - "firstScreenId": "welcome" - }, - "defaultTexts": { - "nextButton": "Далее", - "continueButton": "Продолжить" - }, - "screens": [ - { - "id": "welcome", - "template": "info", - "title": { - "text": "Верни устойчивость за 8 недель", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Персональный подбор терапевта, структурные сессии и поддержка между встречами.", - "font": "inter", - "weight": "medium", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "🧠", - "size": "xl" - }, - "bottomActionButton": { - "text": "Пройти тест" - }, - "navigation": { - "defaultNextScreenId": "pain" - } - }, - { - "id": "pain", - "template": "info", - "title": { - "text": "Ты не обязан справляться в одиночку", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Выгорание, тревога, сложности в отношениях — наши клиенты чувствовали то же. Сейчас живут без этого тяжёлого груза.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "stress-date" - } - }, - { - "id": "stress-date", - "template": "date", - "title": { - "text": "Когда ты последний раз отдыхал(а) без тревог?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Это помогает оценить уровень стресса и подобрать ритм терапии.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "ММ", - "dayPlaceholder": "ДД", - "yearPlaceholder": "ГГГГ", - "monthLabel": "Месяц", - "dayLabel": "День", - "yearLabel": "Год", - "showSelectedDate": true, - "selectedDateLabel": "Дата отдыха:" - }, - "navigation": { - "defaultNextScreenId": "state" - } - }, - { - "id": "state", - "template": "list", - "title": { - "text": "Что чувствуешь чаще всего?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "anxiety", "label": "Тревога" }, - { "id": "apathy", "label": "Апатия" }, - { "id": "anger", "label": "Раздражительность" }, - { "id": "insomnia", "label": "Проблемы со сном" }, - { "id": "relationships", "label": "Конфликты" } - ] - }, - "navigation": { - "defaultNextScreenId": "goals" - } - }, - { - "id": "goals", - "template": "list", - "title": { - "text": "К чему хочешь прийти?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "energy", "label": "Больше энергии" }, - { "id": "confidence", "label": "Уверенность в решениях" }, - { "id": "relations", "label": "Гармония в отношениях" }, - { "id": "selfcare", "label": "Ценность себя" }, - { "id": "career", "label": "Сфокусированность в работе" } - ] - }, - "navigation": { - "defaultNextScreenId": "success" - } - }, - { - "id": "success", - "template": "info", - "title": { - "text": "История Ани: спокойствие вместо паники", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Через 7 сессий она перестала просыпаться ночью, получила повышение и наладила отношения с мужем.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "format" - } - }, - { - "id": "format", - "template": "list", - "title": { - "text": "Какой формат терапии удобен?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "online", "label": "Онлайн-видеосессии" }, - { "id": "audio", "label": "Аудио и чат-поддержка" }, - { "id": "offline", "label": "Офлайн в кабинете" } - ] - }, - "navigation": { - "defaultNextScreenId": "frequency" - } - }, - { - "id": "frequency", - "template": "list", - "title": { - "text": "С какой частотой готовы встречаться?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "weekly", "label": "Раз в неделю" }, - { "id": "twice", "label": "Дважды в неделю" }, - { "id": "flex", "label": "Гибкий график" } - ] - }, - "navigation": { - "defaultNextScreenId": "form" - } - }, - { - "id": "form", - "template": "form", - "title": { - "text": "Получить подбор психолога", - "font": "manrope", - "weight": "bold" - }, - "fields": [ - { "id": "name", "label": "Имя", "placeholder": "Ваше имя", "type": "text", "required": true }, - { "id": "phone", "label": "Телефон", "placeholder": "+7 (___) ___-__-__", "type": "tel", "required": true }, - { "id": "email", "label": "Email", "placeholder": "Для плана терапии", "type": "email", "required": true } - ], - "validationMessages": { - "required": "Поле ${field} обязательно", - "invalidFormat": "Проверьте ввод" - }, - "navigation": { - "defaultNextScreenId": "therapist" - } - }, - { - "id": "therapist", - "template": "info", - "title": { - "text": "Мы нашли специалиста", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Психолог с 9-летним опытом CBT, работает с тревогой и выгоранием. Первичная консультация — завтра.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "packages" - } - }, - { - "id": "packages", - "template": "list", - "title": { - "text": "Выберите пакет", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "start", "label": "Start — 4 сессии" }, - { "id": "focus", "label": "Focus — 8 сессий + чат" }, - { "id": "deep", "label": "Deep — 12 сессий + коуч" } - ] - }, - "navigation": { - "defaultNextScreenId": "bonus" - } - }, - { - "id": "bonus", - "template": "info", - "title": { - "text": "Подарок к старту", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Медитации MindBalance и ежедневный трекер настроения бесплатно.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "coupon" - } - }, - { - "id": "coupon", - "template": "coupon", - "title": { - "text": "Закрепите скидку", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Скидка 20% на первый пакет и бонусный аудио-курс.", - "font": "inter", - "weight": "medium", - "align": "center", - "color": "muted" - }, - "coupon": { - "title": { - "text": "MindBalance", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "-20%", - "font": "manrope", - "weight": "black", - "size": "4xl" - }, - "description": { - "text": "Первый пакет + аудио-курс", - "font": "inter", - "weight": "medium" - } - }, - "promoCode": { - "text": "MIND20", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Нажмите, чтобы применить промокод", - "font": "inter", - "weight": "medium", - "size": "sm", - "color": "muted" - } - }, - "copiedMessage": "Промокод {code} скопирован!" - } - ] -} diff --git a/public/funnels/ru-skin-renewal.json b/public/funnels/ru-skin-renewal.json deleted file mode 100644 index f700858..0000000 --- a/public/funnels/ru-skin-renewal.json +++ /dev/null @@ -1,314 +0,0 @@ -{ - "meta": { - "id": "ru-skin-renewal", - "title": "Glow Clinic: омоложение без боли", - "description": "Воронка для клиники косметологии с диагностикой кожи и продажей курса процедур.", - "firstScreenId": "welcome" - }, - "defaultTexts": { - "nextButton": "Далее", - "continueButton": "Продолжить" - }, - "screens": [ - { - "id": "welcome", - "template": "info", - "title": { - "text": "Верни коже сияние за 28 дней", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Лицо свежее, овал подтянутый, поры незаметны — результат подтверждён 418 клиентками.", - "font": "inter", - "weight": "medium", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "✨", - "size": "xl" - }, - "bottomActionButton": { - "text": "Пройти диагностику" - }, - "navigation": { - "defaultNextScreenId": "problem" - } - }, - { - "id": "problem", - "template": "info", - "title": { - "text": "85% женщин старят три фактора", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Обезвоженность, пигментация и потеря тонуса. Находим источник и устраняем его комплексно.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "skin-date" - } - }, - { - "id": "skin-date", - "template": "date", - "title": { - "text": "Когда была последняя профессиональная чистка?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Дата поможет подобрать интенсивность и глубину процедур.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "ММ", - "dayPlaceholder": "ДД", - "yearPlaceholder": "ГГГГ", - "monthLabel": "Месяц", - "dayLabel": "День", - "yearLabel": "Год", - "showSelectedDate": true, - "selectedDateLabel": "Последний визит:" - }, - "navigation": { - "defaultNextScreenId": "skin-type" - } - }, - { - "id": "skin-type", - "template": "list", - "title": { - "text": "Какой у тебя тип кожи?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "dry", "label": "Сухая" }, - { "id": "combination", "label": "Комбинированная" }, - { "id": "oily", "label": "Жирная" }, - { "id": "sensitive", "label": "Чувствительная" } - ] - }, - "navigation": { - "defaultNextScreenId": "primary-concern" - } - }, - { - "id": "primary-concern", - "template": "list", - "title": { - "text": "Что беспокоит больше всего?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "wrinkles", "label": "Морщины" }, - { "id": "pigmentation", "label": "Пигментация" }, - { "id": "pores", "label": "Расширенные поры" }, - { "id": "acne", "label": "Воспаления" }, - { "id": "dryness", "label": "Сухость и шелушение" } - ] - }, - "navigation": { - "defaultNextScreenId": "success" - } - }, - { - "id": "success", - "template": "info", - "title": { - "text": "История Нади: минус 7 лет визуально", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Через 3 сеанса HydraGlow кожа стала плотной, контур подтянулся, ушла желтизна. Её фото попало в наш кейсбук.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "home-care" - } - }, - { - "id": "home-care", - "template": "list", - "title": { - "text": "Как ухаживаешь дома?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "basic", "label": "Только базовый уход" }, - { "id": "active", "label": "Активные сыворотки" }, - { "id": "spapro", "label": "Домашние аппараты" }, - { "id": "none", "label": "Практически не ухаживаю" } - ] - }, - "navigation": { - "defaultNextScreenId": "allergy" - } - }, - { - "id": "allergy", - "template": "list", - "title": { - "text": "Есть ли ограничения?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "pregnancy", "label": "Беременность/ГВ" }, - { "id": "allergy", "label": "Аллергия на кислоты" }, - { "id": "derm", "label": "Дерматологические заболевания" }, - { "id": "no", "label": "Нет ограничений" } - ] - }, - "navigation": { - "defaultNextScreenId": "diagnostic-form" - } - }, - { - "id": "diagnostic-form", - "template": "form", - "title": { - "text": "Получить персональный план ухода", - "font": "manrope", - "weight": "bold" - }, - "fields": [ - { "id": "name", "label": "Имя", "placeholder": "Как к вам обращаться", "type": "text", "required": true }, - { "id": "phone", "label": "Телефон", "placeholder": "+7 (___) ___-__-__", "type": "tel", "required": true }, - { "id": "email", "label": "Email", "placeholder": "Получите чек-лист ухода", "type": "email", "required": true } - ], - "validationMessages": { - "required": "Поле ${field} обязательно", - "invalidFormat": "Проверьте формат" - }, - "navigation": { - "defaultNextScreenId": "expert" - } - }, - { - "id": "expert", - "template": "info", - "title": { - "text": "Ваш персональный эксперт", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Врач-косметолог с 12-летним опытом проведёт диагностику, составит план процедур и будет на связи между визитами.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "plan-options" - } - }, - { - "id": "plan-options", - "template": "list", - "title": { - "text": "Выберите программу", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "express", "label": "Express Glow — 2 визита" }, - { "id": "course", "label": "Total Lift — 4 визита" }, - { "id": "vip", "label": "VIP Anti-Age — 6 визитов" } - ] - }, - "navigation": { - "defaultNextScreenId": "bonus" - } - }, - { - "id": "bonus", - "template": "info", - "title": { - "text": "Подарок к записи сегодня", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Профессиональная сыворотка Medik8 и массаж шеи в подарок на первом приёме.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "coupon" - } - }, - { - "id": "coupon", - "template": "coupon", - "title": { - "text": "Забронируй курс со скидкой", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Только сегодня — до 40% на программу и подарок.", - "font": "inter", - "weight": "medium", - "align": "center", - "color": "muted" - }, - "coupon": { - "title": { - "text": "Glow Clinic", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "-40%", - "font": "manrope", - "weight": "black", - "size": "4xl" - }, - "description": { - "text": "Курс омоложения + сыворотка", - "font": "inter", - "weight": "medium" - } - }, - "promoCode": { - "text": "GLOW40", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Нажмите, чтобы закрепить предложение", - "font": "inter", - "weight": "medium", - "size": "sm", - "color": "muted" - } - }, - "copiedMessage": "Промокод {code} скопирован!" - } - ] -} diff --git a/public/funnels/ru-travel-signature.json b/public/funnels/ru-travel-signature.json deleted file mode 100644 index 3c419a6..0000000 --- a/public/funnels/ru-travel-signature.json +++ /dev/null @@ -1,315 +0,0 @@ -{ - "meta": { - "id": "ru-travel-signature", - "title": "Signature Trips: путешествие мечты", - "description": "Воронка для премиального турагентства по созданию индивидуальных путешествий.", - "firstScreenId": "hero" - }, - "defaultTexts": { - "nextButton": "Далее", - "continueButton": "Продолжить" - }, - "screens": [ - { - "id": "hero", - "template": "info", - "title": { - "text": "Создадим путешествие, о котором будут говорить", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Личный тревел-архитектор, закрытые локации и полный сервис 24/7.", - "font": "inter", - "weight": "medium", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "✈️", - "size": "xl" - }, - "bottomActionButton": { - "text": "Начать проект" - }, - "navigation": { - "defaultNextScreenId": "inspiration" - } - }, - { - "id": "inspiration", - "template": "info", - "title": { - "text": "Премиальный отдых начинается с мечты", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Мы создаём маршруты для Forbes, топ-менеджеров и семей, которые ценят приватность.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "travel-date" - } - }, - { - "id": "travel-date", - "template": "date", - "title": { - "text": "Когда планируете отправиться?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Дата позволяет нам зарезервировать лучшие отели и гидов заранее.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "ММ", - "dayPlaceholder": "ДД", - "yearPlaceholder": "ГГГГ", - "monthLabel": "Месяц", - "dayLabel": "День", - "yearLabel": "Год", - "showSelectedDate": true, - "selectedDateLabel": "Старт путешествия:" - }, - "navigation": { - "defaultNextScreenId": "companions" - } - }, - { - "id": "companions", - "template": "list", - "title": { - "text": "С кем летите?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "solo", "label": "Соло" }, - { "id": "couple", "label": "Пара" }, - { "id": "family", "label": "Семья" }, - { "id": "friends", "label": "Компания друзей" } - ] - }, - "navigation": { - "defaultNextScreenId": "style" - } - }, - { - "id": "style", - "template": "list", - "title": { - "text": "Какой стиль отдыха хотите?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "beach", "label": "Пляжный релакс" }, - { "id": "city", "label": "Городской lifestyle" }, - { "id": "adventure", "label": "Приключения" }, - { "id": "culture", "label": "Культура и гастрономия" }, - { "id": "wellness", "label": "Wellness & spa" } - ] - }, - "navigation": { - "defaultNextScreenId": "case" - } - }, - { - "id": "case", - "template": "info", - "title": { - "text": "Кейс семьи Морозовых", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "10 дней на Бали: вилла на скале, частный шеф, экскурсии на вертолёте. Экономия времени — 60 часов.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "wishlist" - } - }, - { - "id": "wishlist", - "template": "list", - "title": { - "text": "Что должно быть обязательно?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "private", "label": "Приватные перелёты" }, - { "id": "events", "label": "Закрытые мероприятия" }, - { "id": "photographer", "label": "Личный фотограф" }, - { "id": "kids", "label": "Детский клуб" }, - { "id": "chef", "label": "Шеф-повар" } - ] - }, - "navigation": { - "defaultNextScreenId": "budget" - } - }, - { - "id": "budget", - "template": "list", - "title": { - "text": "Какой бюджет готовы инвестировать?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "5k", "label": "до $5 000" }, - { "id": "10k", "label": "$5 000 – $10 000" }, - { "id": "20k", "label": "$10 000 – $20 000" }, - { "id": "20kplus", "label": "Более $20 000" } - ] - }, - "navigation": { - "defaultNextScreenId": "form" - } - }, - { - "id": "form", - "template": "form", - "title": { - "text": "Получите концепт путешествия", - "font": "manrope", - "weight": "bold" - }, - "fields": [ - { "id": "name", "label": "Имя", "placeholder": "Как к вам обращаться", "type": "text", "required": true }, - { "id": "phone", "label": "Телефон", "placeholder": "+7 (___) ___-__-__", "type": "tel", "required": true }, - { "id": "email", "label": "Email", "placeholder": "Получить концепт", "type": "email", "required": true } - ], - "validationMessages": { - "required": "Поле ${field} обязательно", - "invalidFormat": "Проверьте формат" - }, - "navigation": { - "defaultNextScreenId": "concierge" - } - }, - { - "id": "concierge", - "template": "info", - "title": { - "text": "Ваш персональный консьерж", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Будет на связи 24/7, бронирует рестораны, решает любые вопросы во время поездки.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "packages" - } - }, - { - "id": "packages", - "template": "list", - "title": { - "text": "Выберите формат сервиса", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "concept", "label": "Concept — разработка маршрута" }, - { "id": "full", "label": "Full Care — сопровождение 24/7" }, - { "id": "ultra", "label": "Ultra Lux — частный самолёт и охрана" } - ] - }, - "navigation": { - "defaultNextScreenId": "bonus" - } - }, - { - "id": "bonus", - "template": "info", - "title": { - "text": "Специальный бонус", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "При бронировании сегодня — апгрейд номера и приватная фотосессия.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "coupon" - } - }, - { - "id": "coupon", - "template": "coupon", - "title": { - "text": "Забронируйте бонус", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Подарочный апгрейд и персональный гид входят в промо", - "font": "inter", - "weight": "medium", - "align": "center", - "color": "muted" - }, - "coupon": { - "title": { - "text": "Signature Trips", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "Premium Bonus", - "font": "manrope", - "weight": "black", - "size": "3xl" - }, - "description": { - "text": "Апгрейд номера + личный гид", - "font": "inter", - "weight": "medium" - } - }, - "promoCode": { - "text": "SIGNATURE", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Нажмите, чтобы зафиксировать бонус", - "font": "inter", - "weight": "medium", - "size": "sm", - "color": "muted" - } - }, - "copiedMessage": "Промокод {code} скопирован!" - } - ] -} diff --git a/public/funnels/ru-wedding-dream.json b/public/funnels/ru-wedding-dream.json deleted file mode 100644 index f3c5bd5..0000000 --- a/public/funnels/ru-wedding-dream.json +++ /dev/null @@ -1,315 +0,0 @@ -{ - "meta": { - "id": "ru-wedding-dream", - "title": "DreamDay: свадьба без стресса", - "description": "Воронка агентства свадебного продюсирования.", - "firstScreenId": "welcome" - }, - "defaultTexts": { - "nextButton": "Далее", - "continueButton": "Продолжить" - }, - "screens": [ - { - "id": "welcome", - "template": "info", - "title": { - "text": "Создадим свадьбу, о которой мечтаете", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "description": { - "text": "Команда продюсеров возьмёт на себя всё: от концепции до финального танца.", - "font": "inter", - "weight": "medium", - "align": "center" - }, - "icon": { - "type": "emoji", - "value": "💍", - "size": "xl" - }, - "bottomActionButton": { - "text": "Начать план" - }, - "navigation": { - "defaultNextScreenId": "vision" - } - }, - { - "id": "vision", - "template": "info", - "title": { - "text": "Каждая история любви уникальна", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Мы создаём сценарии, которые отражают вашу пару, а не Pinterest-копию.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "date" - } - }, - { - "id": "date", - "template": "date", - "title": { - "text": "На какую дату планируется свадьба?", - "font": "manrope", - "weight": "bold" - }, - "subtitle": { - "text": "Мы проверим занятость площадок и команд.", - "font": "inter", - "weight": "medium", - "color": "muted" - }, - "dateInput": { - "monthPlaceholder": "ММ", - "dayPlaceholder": "ДД", - "yearPlaceholder": "ГГГГ", - "monthLabel": "Месяц", - "dayLabel": "День", - "yearLabel": "Год", - "showSelectedDate": true, - "selectedDateLabel": "Дата свадьбы:" - }, - "navigation": { - "defaultNextScreenId": "guests" - } - }, - { - "id": "guests", - "template": "list", - "title": { - "text": "Сколько гостей ожидаете?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "small", "label": "До 30 гостей" }, - { "id": "medium", "label": "30-70 гостей" }, - { "id": "large", "label": "70-120 гостей" }, - { "id": "xl", "label": "Более 120 гостей" } - ] - }, - "navigation": { - "defaultNextScreenId": "style" - } - }, - { - "id": "style", - "template": "list", - "title": { - "text": "Опишите стиль праздника", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "classic", "label": "Классика" }, - { "id": "modern", "label": "Современный шик" }, - { "id": "boho", "label": "Бохо" }, - { "id": "destination", "label": "Destination wedding" }, - { "id": "party", "label": "Ночной клуб" } - ] - }, - "navigation": { - "defaultNextScreenId": "case" - } - }, - { - "id": "case", - "template": "info", - "title": { - "text": "Свадьба Кати и Максима", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Горная Швейцария, закрытая вилла и живой оркестр. Сэкономили 18 часов подготовки еженедельно.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "priorities" - } - }, - { - "id": "priorities", - "template": "list", - "title": { - "text": "Что важнее всего?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "multi", - "options": [ - { "id": "venue", "label": "Локация мечты" }, - { "id": "show", "label": "Вау-программа" }, - { "id": "decor", "label": "Дизайн и флористика" }, - { "id": "photo", "label": "Фото и видео" }, - { "id": "care", "label": "Отсутствие стресса" } - ] - }, - "navigation": { - "defaultNextScreenId": "budget" - } - }, - { - "id": "budget", - "template": "list", - "title": { - "text": "Какой бюджет планируете?", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "3m", "label": "До 3 млн ₽" }, - { "id": "5m", "label": "3-5 млн ₽" }, - { "id": "8m", "label": "5-8 млн ₽" }, - { "id": "8mplus", "label": "Более 8 млн ₽" } - ] - }, - "navigation": { - "defaultNextScreenId": "form" - } - }, - { - "id": "form", - "template": "form", - "title": { - "text": "Получите концепцию свадьбы", - "font": "manrope", - "weight": "bold" - }, - "fields": [ - { "id": "names", "label": "Имена пары", "placeholder": "Имена", "type": "text", "required": true }, - { "id": "phone", "label": "Телефон", "placeholder": "+7 (___) ___-__-__", "type": "tel", "required": true }, - { "id": "email", "label": "Email", "placeholder": "Получить презентацию", "type": "email", "required": true } - ], - "validationMessages": { - "required": "Поле ${field} обязательно", - "invalidFormat": "Проверьте формат" - }, - "navigation": { - "defaultNextScreenId": "team" - } - }, - { - "id": "team", - "template": "info", - "title": { - "text": "Команда под вашу историю", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Продюсер, стилист, режиссёр и координатор. Каждую неделю — отчёт и контроль бюджета.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "packages" - } - }, - { - "id": "packages", - "template": "list", - "title": { - "text": "Выберите формат сопровождения", - "font": "manrope", - "weight": "bold" - }, - "list": { - "selectionType": "single", - "options": [ - { "id": "concept", "label": "Concept — идея и сценарий" }, - { "id": "production", "label": "Production — организация под ключ" }, - { "id": "lux", "label": "Luxury — destination + премиум команда" } - ] - }, - "navigation": { - "defaultNextScreenId": "bonus" - } - }, - { - "id": "bonus", - "template": "info", - "title": { - "text": "Бонусы при бронировании сегодня", - "font": "manrope", - "weight": "bold" - }, - "description": { - "text": "Пробная встреча с ведущим и авторские клятвы, подготовленные нашим спичрайтером.", - "font": "inter", - "weight": "medium" - }, - "navigation": { - "defaultNextScreenId": "coupon" - } - }, - { - "id": "coupon", - "template": "coupon", - "title": { - "text": "Зафиксируйте дату и бонус", - "font": "manrope", - "weight": "bold", - "align": "center" - }, - "subtitle": { - "text": "Скидка 15% на продюсирование и бесплатная love-story съёмка.", - "font": "inter", - "weight": "medium", - "align": "center", - "color": "muted" - }, - "coupon": { - "title": { - "text": "DreamDay", - "font": "manrope", - "weight": "bold", - "color": "primary" - }, - "offer": { - "title": { - "text": "-15%", - "font": "manrope", - "weight": "black", - "size": "4xl" - }, - "description": { - "text": "Продюсирование + love-story", - "font": "inter", - "weight": "medium" - } - }, - "promoCode": { - "text": "DREAM15", - "font": "inter", - "weight": "semiBold" - }, - "footer": { - "text": "Нажмите, чтобы закрепить предложение", - "font": "inter", - "weight": "medium", - "size": "sm", - "color": "muted" - } - }, - "copiedMessage": "Промокод {code} скопирован!" - } - ] -} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..8b93b6d --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,77 @@ +# Scripts Documentation + +## Funnel Management Scripts + +### 📥 `import-funnels-to-db.mjs` + +Импортирует воронки из JSON файлов в `public/funnels/` в MongoDB. + +```bash +npm run import:funnels +``` + +### 📤 `sync-funnels-from-db.mjs` + +Синхронизирует опубликованные воронки из MongoDB обратно в проект: + +1. Извлекает все последние версии опубликованных воронок из БД +2. Сохраняет их во временные JSON файлы в `public/funnels/` +3. Запекает их в TypeScript (`src/lib/funnel/bakedFunnels.ts`) +4. Удаляет временные JSON файлы + +#### Основное использование: + +```bash +# Синхронизация всех воронок +npm run sync:funnels + +# Просмотр справки +npm run sync:funnels -- --help +``` + +#### Опции: + +**`--dry-run`** - Показать что будет синхронизировано без реальных изменений: +```bash +npm run sync:funnels -- --dry-run +``` + +**`--keep-files`** - Сохранить JSON файлы после запекания (полезно для отладки): +```bash +npm run sync:funnels -- --keep-files +``` + +**`--funnel-ids `** - Синхронизировать только определенные воронки: +```bash +npm run sync:funnels -- --funnel-ids funnel-test,ru-career-accelerator +``` + +**Комбинирование опций:** +```bash +npm run sync:funnels -- --dry-run --funnel-ids funnel-test +npm run sync:funnels -- --keep-files --dry-run +``` + +### 🔥 `bake-funnels.mjs` + +Конвертирует JSON файлы воронок в TypeScript константы. + +```bash +npm run bake:funnels +``` + +## Workflow + +### Разработка локально: +1. Создать/редактировать воронки в админке +2. Опубликовать их +3. Запустить `npm run sync:funnels` для обновления кода + +### Деплой: +1. Запустить `npm run sync:funnels` перед билдом +2. Собрать проект с актуальными воронками + +### Отладка: +1. `npm run sync:funnels -- --dry-run` - посмотреть что будет синхронизировано +2. `npm run sync:funnels -- --keep-files` - оставить JSON файлы для проверки +3. `npm run sync:funnels -- --funnel-ids specific-id` - синхронизировать только одну воронку diff --git a/scripts/sync-funnels-from-db.mjs b/scripts/sync-funnels-from-db.mjs new file mode 100755 index 0000000..63904de --- /dev/null +++ b/scripts/sync-funnels-from-db.mjs @@ -0,0 +1,290 @@ +#!/usr/bin/env node + +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { execSync } from 'child_process'; +import mongoose from 'mongoose'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config({ path: '.env.local' }); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const projectRoot = path.dirname(__dirname); +const funnelsDir = path.join(projectRoot, 'public', 'funnels'); + +// MongoDB connection URI +const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/witlab-funnel'; + +// Mongoose schemas (same as in import script) +const FunnelDataSchema = new mongoose.Schema({ + meta: { + id: { type: String, required: true }, + version: String, + title: String, + description: String, + firstScreenId: String + }, + defaultTexts: { + nextButton: { type: String, default: 'Next' }, + continueButton: { type: String, default: 'Continue' } + }, + screens: [mongoose.Schema.Types.Mixed] +}, { _id: false }); + +const FunnelSchema = new mongoose.Schema({ + funnelData: { + type: FunnelDataSchema, + required: true + }, + name: { + type: String, + required: true, + trim: true, + maxlength: 200 + }, + description: { + type: String, + trim: true, + maxlength: 1000 + }, + status: { + type: String, + enum: ['draft', 'published', 'archived'], + default: 'draft', + required: true + }, + version: { + type: Number, + default: 1, + min: 1 + }, + createdBy: { type: String, default: 'system' }, + lastModifiedBy: { type: String, default: 'system' }, + usage: { + totalViews: { type: Number, default: 0, min: 0 }, + totalCompletions: { type: Number, default: 0, min: 0 }, + lastUsed: Date + }, + publishedAt: { type: Date, default: Date.now } +}, { + timestamps: true, + collection: 'funnels' +}); + +const Funnel = mongoose.model('Funnel', FunnelSchema); + +async function connectDB() { + try { + await mongoose.connect(MONGODB_URI); + console.log('✅ Connected to MongoDB'); + } catch (error) { + console.error('❌ MongoDB connection failed:', error.message); + process.exit(1); + } +} + +async function ensureFunnelsDir() { + try { + await fs.access(funnelsDir); + } catch { + await fs.mkdir(funnelsDir, { recursive: true }); + console.log('📁 Created funnels directory'); + } +} + +async function clearFunnelsDir() { + try { + const files = await fs.readdir(funnelsDir); + for (const file of files) { + if (file.endsWith('.json')) { + await fs.unlink(path.join(funnelsDir, file)); + } + } + console.log('🧹 Cleared existing JSON files'); + } catch (error) { + console.error('⚠️ Error clearing funnels directory:', error.message); + } +} + +async function getLatestPublishedFunnels() { + try { + // Группируем по funnelData.meta.id и берем последнюю версию каждой опубликованной воронки + const latestFunnels = await Funnel.aggregate([ + // Фильтруем только опубликованные воронки + { $match: { status: 'published' } }, + + // Сортируем по версии в убывающем порядке + { $sort: { 'funnelData.meta.id': 1, version: -1 } }, + + // Группируем по ID воронки и берем первый документ (с наибольшей версией) + { + $group: { + _id: '$funnelData.meta.id', + latestFunnel: { $first: '$$ROOT' } + } + }, + + // Заменяем корневой документ на latestFunnel + { $replaceRoot: { newRoot: '$latestFunnel' } } + ]); + + console.log(`📊 Found ${latestFunnels.length} latest published funnels`); + return latestFunnels; + } catch (error) { + console.error('❌ Error fetching funnels:', error.message); + throw error; + } +} + +async function saveFunnelToFile(funnel) { + const funnelId = funnel.funnelData.meta.id; + const fileName = `${funnelId}.json`; + const filePath = path.join(funnelsDir, fileName); + + try { + // Сохраняем только funnelData (структуру воронки) + const funnelContent = JSON.stringify(funnel.funnelData, null, 2); + await fs.writeFile(filePath, funnelContent, 'utf8'); + console.log(`💾 Saved ${fileName} (v${funnel.version})`); + } catch (error) { + console.error(`❌ Error saving ${fileName}:`, error.message); + throw error; + } +} + +async function bakeFunnels() { + try { + console.log('🔥 Baking funnels...'); + execSync('npm run bake:funnels', { + cwd: projectRoot, + stdio: 'inherit' + }); + console.log('✅ Funnels baked successfully'); + } catch (error) { + console.error('❌ Error baking funnels:', error.message); + throw error; + } +} + + +// Парсим аргументы командной строки +const args = process.argv.slice(2); +const options = { + funnelIds: [], + dryRun: false, + keepFiles: false, +}; + +// Парсим опции +for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === '--dry-run') { + options.dryRun = true; + } else if (arg === '--keep-files') { + options.keepFiles = true; + } else if (arg === '--funnel-ids') { + // Следующий аргумент должен содержать ID воронок через запятую + const idsArg = args[++i]; + if (idsArg) { + options.funnelIds = idsArg.split(',').map(id => id.trim()); + } + } else if (arg === '--help' || arg === '-h') { + console.log(` +🔄 Sync Funnels from Database + +Usage: npm run sync:funnels [options] + +Options: + --dry-run Show what would be synced without actually doing it + --keep-files Keep JSON files after baking (useful for debugging) + --funnel-ids Sync only specific funnel IDs (comma-separated) + --help, -h Show this help message + +Examples: + npm run sync:funnels + npm run sync:funnels -- --dry-run + npm run sync:funnels -- --funnel-ids funnel-test,ru-career-accelerator + npm run sync:funnels -- --keep-files --dry-run +`); + process.exit(0); + } +} + +// Обновляем функцию syncFunnels для поддержки опций +async function syncFunnelsWithOptions() { + if (options.dryRun) { + console.log('🔍 DRY RUN MODE - No actual changes will be made\n'); + } + + console.log('🚀 Starting funnel sync from database...\n'); + + try { + // 1. Подключаемся к базе данных + await connectDB(); + + // 2. Создаем/очищаем папку для воронок (только если не dry-run) + await ensureFunnelsDir(); + if (!options.dryRun) { + await clearFunnelsDir(); + } + + // 3. Получаем последние версии всех опубликованных воронок + const allFunnels = await getLatestPublishedFunnels(); + + // Фильтруем по указанным ID если они заданы + let funnels = allFunnels; + if (options.funnelIds.length > 0) { + funnels = allFunnels.filter(funnel => + options.funnelIds.includes(funnel.funnelData.meta.id) + ); + console.log(`🎯 Filtering to ${funnels.length} specific funnels: ${options.funnelIds.join(', ')}`); + } + + if (funnels.length === 0) { + console.log('ℹ️ No published funnels found matching criteria'); + return; + } + + // 4. Сохраняем каждую воронку в JSON файл + for (const funnel of funnels) { + if (options.dryRun) { + console.log(`🔍 Would save ${funnel.funnelData.meta.id}.json (v${funnel.version})`); + } else { + await saveFunnelToFile(funnel); + } + } + + // 5. Запекаем воронки в TypeScript + if (!options.dryRun) { + await bakeFunnels(); + } else { + console.log('🔍 Would bake funnels to TypeScript'); + } + + // 6. Удаляем JSON файлы после запекания (если не указано сохранить) + if (!options.dryRun && !options.keepFiles) { + await clearFunnelsDir(); + } else if (options.keepFiles) { + console.log('📁 Keeping JSON files as requested'); + } else if (options.dryRun) { + console.log('🔍 Would clean up JSON files'); + } + + console.log('\n🎉 Funnel sync completed successfully!'); + console.log(`📈 ${options.dryRun ? 'Would sync' : 'Synced'} ${funnels.length} funnels from database`); + + } catch (error) { + console.error('\n💥 Sync failed:', error.message); + process.exit(1); + } finally { + await mongoose.disconnect(); + console.log('🔌 Disconnected from MongoDB'); + } +} + +// Запускаем скрипт с опциями +syncFunnelsWithOptions(); diff --git a/src/app/admin/AdminCatalogPageClient.tsx b/src/app/admin/AdminCatalogPageClient.tsx index c165f1f..099ebb2 100644 --- a/src/app/admin/AdminCatalogPageClient.tsx +++ b/src/app/admin/AdminCatalogPageClient.tsx @@ -124,8 +124,7 @@ export default function AdminCatalogPage() { firstScreenId: 'screen-1' }, defaultTexts: { - nextButton: 'Далее', - continueButton: 'Продолжить' + nextButton: 'Continue' }, screens: [ { diff --git a/src/app/admin/builder/[id]/FunnelBuilderPageClient.tsx b/src/app/admin/builder/[id]/FunnelBuilderPageClient.tsx index fe82244..99d8d79 100644 --- a/src/app/admin/builder/[id]/FunnelBuilderPageClient.tsx +++ b/src/app/admin/builder/[id]/FunnelBuilderPageClient.tsx @@ -11,7 +11,8 @@ import { BuilderPreview } from "@/components/admin/builder"; import type { BuilderState } from '@/lib/admin/builder/context'; -import type { FunnelDefinition } from '@/lib/funnel/types'; +import type { FunnelDefinition, ScreenDefinition } from '@/lib/funnel/types'; +import type { BuilderScreen } from '@/lib/admin/builder/types'; import { deserializeFunnelDefinition } from '@/lib/admin/builder/utils'; interface FunnelData { @@ -71,6 +72,29 @@ export default function FunnelBuilderPage() { } }; + // Функция очистки экрана от служебных полей админки + const cleanScreen = (screen: BuilderScreen): ScreenDefinition => { + // Создаем копию экрана без служебного поля position + const result = { ...screen } as BuilderScreen & Record; + + // Убираем служебное поле position (используется только в canvas админки) + delete result.position; + + // Для НЕ-list экранов убираем поле list если оно есть + if (result.template !== "list" && 'list' in result) { + delete result.list; + } + + // Для НЕ-form экранов убираем поле fields если оно есть + if (result.template !== "form" && 'fields' in result) { + delete result.fields; + } + + // variants оставляем для всех экранов - это валидное поле + + return result as ScreenDefinition; + }; + // Сохранение воронки const saveFunnel = async (builderState: BuilderState, publish: boolean = false) => { if (!funnelData || saving) return; @@ -82,10 +106,9 @@ export default function FunnelBuilderPage() { const updatedFunnelData: FunnelDefinition = { meta: builderState.meta, defaultTexts: { - nextButton: 'Далее', - continueButton: 'Продолжить' + nextButton: 'Counitue' }, - screens: builderState.screens + screens: builderState.screens.map(cleanScreen) }; const response = await fetch(`/api/funnels/${funnelId}`, { @@ -133,8 +156,7 @@ export default function FunnelBuilderPage() { const funnelSnapshot: FunnelDefinition = { meta: builderState.meta, defaultTexts: { - nextButton: 'Далее', - continueButton: 'Продолжить' + nextButton: 'Continue' }, screens: builderState.screens }; @@ -246,7 +268,7 @@ export default function FunnelBuilderPage() {
{/* Sidebar */} -
-
-
+
@@ -158,7 +156,6 @@ export function BuilderCanvas() { const isDropAfter = dropIndex === screens.length && index === screens.length - 1; const rules = screen.navigation?.rules ?? []; const defaultNext = screen.navigation?.defaultNextScreenId; - const isLast = index === screens.length - 1; const defaultTargetIndex = defaultNext ? screens.findIndex((candidate) => candidate.id === defaultNext) : null; @@ -166,16 +163,7 @@ export function BuilderCanvas() { return (
{isDropBefore && } -
-
- - {!isLast && ( -
-
- -
- )} -
+
, value: string) => { + dispatch({ type: "set-default-texts", payload: { [field]: value } }); + }; + const handleScreenIdChange = (currentId: string, newId: string) => { if (newId.trim() === "" || newId === currentId) { return; @@ -297,9 +302,8 @@ export function BuilderSidebar() {
{activeTab === "funnel" ? (
-
- -
+ {/* Валидация всегда вверху, без заголовка */} +
+
+ handleDefaultTextsChange("nextButton", event.target.value)} + /> + handleDefaultTextsChange("privacyBanner", event.target.value)} + /> +
+
@@ -352,6 +371,9 @@ export function BuilderSidebar() {
) : selectedScreen ? (
+ {/* Валидация всегда вверху, без заголовка */} + +
@@ -457,9 +479,10 @@ export function BuilderSidebar() { Правило {ruleIndex + 1}
@@ -533,10 +556,6 @@ export function BuilderSidebar() {
)} -
- -
-

@@ -548,6 +567,7 @@ export function BuilderSidebar() { disabled={state.screens.length <= 1} onClick={() => handleDeleteScreen(selectedScreen.id)} > + {state.screens.length <= 1 ? "Нельзя удалить последний экран" : "Удалить экран"}

diff --git a/src/components/admin/builder/layout/BuilderPreview.tsx b/src/components/admin/builder/layout/BuilderPreview.tsx index 7494a33..107a791 100644 --- a/src/components/admin/builder/layout/BuilderPreview.tsx +++ b/src/components/admin/builder/layout/BuilderPreview.tsx @@ -2,12 +2,13 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { useBuilderSelectedScreen } from "@/lib/admin/builder/context"; +import { useBuilderSelectedScreen, useBuilderState } from "@/lib/admin/builder/context"; import { renderScreen } from "@/lib/funnel/screenRenderer"; import { mergeScreenWithOverrides } from "@/lib/admin/builder/variants"; export function BuilderPreview() { const selectedScreen = useBuilderSelectedScreen(); + const builderState = useBuilderState(); const [selectedIds, setSelectedIds] = useState([]); const [previewVariantIndex, setPreviewVariantIndex] = useState(null); @@ -72,7 +73,7 @@ export function BuilderPreview() { canGoBack: true, // Show back button in preview onBack: () => {}, // Mock back handler for preview screenProgress: { current: 1, total: 10 }, // Mock progress for preview - defaultTexts: { nextButton: "Next", continueButton: "Continue" }, // Mock texts + defaultTexts: builderState.defaultTexts, // Use real defaultTexts from builder }); } catch (error) { console.error('Error rendering preview:', error); @@ -82,7 +83,7 @@ export function BuilderPreview() {
); } - }, [previewScreen, selectedIds, handleSelectionChange]); + }, [previewScreen, selectedIds, handleSelectionChange, builderState.defaultTexts]); const preview = useMemo(() => { if (!previewScreen) { diff --git a/src/components/admin/builder/templates/CouponScreenConfig.tsx b/src/components/admin/builder/templates/CouponScreenConfig.tsx index 105640d..6bdc55f 100644 --- a/src/components/admin/builder/templates/CouponScreenConfig.tsx +++ b/src/components/admin/builder/templates/CouponScreenConfig.tsx @@ -1,6 +1,7 @@ "use client"; import { TextInput } from "@/components/ui/TextInput/TextInput"; +import { TextAreaInput } from "@/components/ui/TextAreaInput/TextAreaInput"; import type { CouponScreenDefinition } from "@/lib/funnel/types"; import type { BuilderScreen } from "@/lib/admin/builder/types"; @@ -32,7 +33,7 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
diff --git a/src/components/admin/builder/templates/DateScreenConfig.tsx b/src/components/admin/builder/templates/DateScreenConfig.tsx index 06fc26c..9698b00 100644 --- a/src/components/admin/builder/templates/DateScreenConfig.tsx +++ b/src/components/admin/builder/templates/DateScreenConfig.tsx @@ -48,17 +48,6 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) { }); }; - const handleInfoMessageChange = (field: "text" | "icon", value: string) => { - const baseInfo = dateScreen.infoMessage ?? { text: "", icon: "ℹ️" }; - const nextInfo = { ...baseInfo, [field]: value }; - - if (!nextInfo.text) { - onUpdate({ infoMessage: undefined }); - return; - } - - onUpdate({ infoMessage: nextInfo }); - }; return (
@@ -185,25 +174,6 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
-
-

Информационный блок

- - {dateScreen.infoMessage && ( - - )} -
); } diff --git a/src/components/admin/builder/templates/FormScreenConfig.tsx b/src/components/admin/builder/templates/FormScreenConfig.tsx index 81e26a3..17bbe30 100644 --- a/src/components/admin/builder/templates/FormScreenConfig.tsx +++ b/src/components/admin/builder/templates/FormScreenConfig.tsx @@ -2,7 +2,8 @@ import { Button } from "@/components/ui/button"; import { TextInput } from "@/components/ui/TextInput/TextInput"; -import { Plus } from "lucide-react"; +import { TextAreaInput } from "@/components/ui/TextAreaInput/TextAreaInput"; +import { Plus, Trash2 } from "lucide-react"; import type { FormScreenDefinition, FormFieldDefinition, FormValidationMessages } from "@/lib/funnel/types"; import type { BuilderScreen } from "@/lib/admin/builder/types"; @@ -59,57 +60,61 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
{formScreen.fields?.map((field, index) => ( -
+
Поле {index + 1}
-
-
- updateValidationMessages({ required: event.target.value || undefined })} + rows={2} + className="resize-y" />
-
- -
- -
- -
diff --git a/src/components/admin/builder/templates/InfoScreenConfig.tsx b/src/components/admin/builder/templates/InfoScreenConfig.tsx index 53f7783..ada74bd 100644 --- a/src/components/admin/builder/templates/InfoScreenConfig.tsx +++ b/src/components/admin/builder/templates/InfoScreenConfig.tsx @@ -1,6 +1,7 @@ "use client"; import { TextInput } from "@/components/ui/TextInput/TextInput"; +import { TextAreaInput } from "@/components/ui/TextAreaInput/TextAreaInput"; import { MarkupPreview } from "@/components/ui/MarkupText/MarkupText"; import type { InfoScreenDefinition } from "@/lib/funnel/types"; import type { BuilderScreen } from "@/lib/admin/builder/types"; @@ -51,10 +52,12 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
diff --git a/src/components/admin/builder/templates/ListScreenConfig.tsx b/src/components/admin/builder/templates/ListScreenConfig.tsx index 08e38e0..db8b275 100644 --- a/src/components/admin/builder/templates/ListScreenConfig.tsx +++ b/src/components/admin/builder/templates/ListScreenConfig.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { TextInput } from "@/components/ui/TextInput/TextInput"; +import { TextAreaInput } from "@/components/ui/TextAreaInput/TextAreaInput"; import { Button } from "@/components/ui/button"; import { ArrowDown, ArrowUp, Plus, Trash2, ChevronDown, ChevronRight } from "lucide-react"; import type { @@ -161,7 +162,7 @@ export function ListScreenConfig({ screen, onUpdate }: ListScreenConfigProps) {
{listScreen.list.options.map((option, index) => (
@@ -222,9 +223,11 @@ export function ListScreenConfig({ screen, onUpdate }: ListScreenConfigProps) { @@ -240,12 +243,14 @@ export function ListScreenConfig({ screen, onUpdate }: ListScreenConfigProps) { diff --git a/src/components/admin/builder/templates/TemplateConfig.tsx b/src/components/admin/builder/templates/TemplateConfig.tsx index f7aec55..3a7fbcd 100644 --- a/src/components/admin/builder/templates/TemplateConfig.tsx +++ b/src/components/admin/builder/templates/TemplateConfig.tsx @@ -13,6 +13,7 @@ import { LoadersScreenConfig } from "./LoadersScreenConfig"; import { SoulmatePortraitScreenConfig } from "./SoulmatePortraitScreenConfig"; 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, @@ -84,12 +85,13 @@ function CollapsibleSection({ interface TypographyControlsProps { label: string; - value: TypographyVariant | undefined; - onChange: (value: TypographyVariant | undefined) => void; + value: (TypographyVariant & { show?: boolean }) | undefined; + onChange: (value: (TypographyVariant & { show?: boolean }) | undefined) => void; allowRemove?: boolean; + showToggle?: boolean; // Показывать ли чекбокс "Показывать" } -function TypographyControls({ label, value, onChange, allowRemove = false }: TypographyControlsProps) { +function TypographyControls({ label, value, onChange, allowRemove = false, showToggle = false }: TypographyControlsProps) { const storageKey = `typography-advanced-${label.toLowerCase().replace(/\s+/g, '-')}`; const [showAdvanced, setShowAdvanced] = useState(false); @@ -124,11 +126,35 @@ function TypographyControls({ label, value, onChange, allowRemove = false }: Typ }); }; + const handleShowToggle = (show: boolean) => { + onChange({ + ...value, + text: value?.text || "", + show, + }); + }; + return (
+ {showToggle && ( + + )} +
- handleTextChange(event.target.value)} /> + handleTextChange(event.target.value)} + rows={2} + className="resize-y" + />
{value?.text && ( @@ -412,8 +438,20 @@ export function TemplateConfig({ screen, onUpdate }: TemplateConfigProps) { return (
- - + + diff --git a/src/components/funnel/templates/CouponTemplate/CouponTemplate.stories.tsx b/src/components/funnel/templates/CouponTemplate/CouponTemplate.stories.tsx index 6355cb8..c4521ab 100644 --- a/src/components/funnel/templates/CouponTemplate/CouponTemplate.stories.tsx +++ b/src/components/funnel/templates/CouponTemplate/CouponTemplate.stories.tsx @@ -22,8 +22,7 @@ const meta: Meta = { onBack: fn(), screenProgress: { current: 8, total: 10 }, defaultTexts: { - nextButton: "Next", - continueButton: "Continue" + nextButton: "Next" }, }, argTypes: { diff --git a/src/components/funnel/templates/CouponTemplate/CouponTemplate.tsx b/src/components/funnel/templates/CouponTemplate/CouponTemplate.tsx index c3e768c..b6f76a0 100644 --- a/src/components/funnel/templates/CouponTemplate/CouponTemplate.tsx +++ b/src/components/funnel/templates/CouponTemplate/CouponTemplate.tsx @@ -6,7 +6,7 @@ import { Coupon } from "@/components/widgets/Coupon/Coupon"; import Typography from "@/components/ui/Typography/Typography"; import { buildTypographyProps } from "@/lib/funnel/mappers"; -import type { CouponScreenDefinition } from "@/lib/funnel/types"; +import type { CouponScreenDefinition, DefaultTexts } from "@/lib/funnel/types"; import { TemplateLayout } from "../layouts/TemplateLayout"; interface CouponTemplateProps { @@ -15,7 +15,7 @@ interface CouponTemplateProps { canGoBack: boolean; onBack: () => void; screenProgress?: { current: number; total: number }; - defaultTexts?: { nextButton?: string; continueButton?: string }; + defaultTexts?: DefaultTexts; } export function CouponTemplate({ @@ -109,7 +109,7 @@ export function CouponTemplate({ titleDefaults={{ font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" }} subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }} actionButtonOptions={{ - defaultText: defaultTexts?.continueButton || "Continue", + defaultText: defaultTexts?.nextButton || "Continue", disabled: false, onClick: onContinue, }} diff --git a/src/components/funnel/templates/DateTemplate/DateTemplate.stories.tsx b/src/components/funnel/templates/DateTemplate/DateTemplate.stories.tsx index 817830c..cc91212 100644 --- a/src/components/funnel/templates/DateTemplate/DateTemplate.stories.tsx +++ b/src/components/funnel/templates/DateTemplate/DateTemplate.stories.tsx @@ -24,8 +24,7 @@ const meta: Meta = { onBack: fn(), screenProgress: { current: 4, total: 10 }, defaultTexts: { - nextButton: "Next", - continueButton: "Continue" + nextButton: "Next" }, }, argTypes: { @@ -99,7 +98,6 @@ export const WithoutInfoMessage: Story = { args: { screen: { ...defaultScreen, - infoMessage: undefined, }, }, }; diff --git a/src/components/funnel/templates/DateTemplate/DateTemplate.tsx b/src/components/funnel/templates/DateTemplate/DateTemplate.tsx index 995ad34..3e9b31c 100644 --- a/src/components/funnel/templates/DateTemplate/DateTemplate.tsx +++ b/src/components/funnel/templates/DateTemplate/DateTemplate.tsx @@ -1,12 +1,10 @@ "use client"; import { useMemo } from "react"; -import Image from "next/image"; import DateInput from "@/components/widgets/DateInput/DateInput"; import Typography from "@/components/ui/Typography/Typography"; -import { buildTypographyProps } from "@/lib/funnel/mappers"; -import type { DateScreenDefinition } from "@/lib/funnel/types"; -import { cn } from "@/lib/utils"; +import PrivacySecurityBanner from "@/components/widgets/PrivacySecurityBanner/PrivacySecurityBanner"; +import type { DateScreenDefinition, DefaultTexts } from "@/lib/funnel/types"; import { TemplateLayout } from "../layouts/TemplateLayout"; // Утилита для форматирования даты на основе паттерна @@ -32,7 +30,7 @@ interface DateTemplateProps { canGoBack: boolean; onBack: () => void; screenProgress?: { current: number; total: number }; - defaultTexts?: { nextButton?: string; continueButton?: string }; + defaultTexts?: DefaultTexts; } export function DateTemplate({ @@ -138,36 +136,18 @@ export function DateTemplate({ locale="en" /> - {screen.infoMessage && ( + {defaultTexts?.privacyBanner && (
-
-
- Security icon -
- - {screen.infoMessage.text} - -
+
)}
diff --git a/src/components/funnel/templates/EmailTemplate/EmailTemplate.stories.tsx b/src/components/funnel/templates/EmailTemplate/EmailTemplate.stories.tsx index 5abbd75..7647a69 100644 --- a/src/components/funnel/templates/EmailTemplate/EmailTemplate.stories.tsx +++ b/src/components/funnel/templates/EmailTemplate/EmailTemplate.stories.tsx @@ -23,7 +23,7 @@ const meta: Meta = { screenProgress: { current: 9, total: 10 }, defaultTexts: { nextButton: "Next", - continueButton: "Continue" + }, }, argTypes: { diff --git a/src/components/funnel/templates/FormTemplate/FormTemplate.stories.tsx b/src/components/funnel/templates/FormTemplate/FormTemplate.stories.tsx index 7929f13..f4bdb47 100644 --- a/src/components/funnel/templates/FormTemplate/FormTemplate.stories.tsx +++ b/src/components/funnel/templates/FormTemplate/FormTemplate.stories.tsx @@ -25,7 +25,6 @@ const richFormScreen: FormScreenDefinition = { placeholder: "Введите ваше имя", type: "text", required: true, - maxLength: 50, }, { id: "email", @@ -41,13 +40,6 @@ const richFormScreen: FormScreenDefinition = { type: "tel", required: false, }, - { - id: "website", - label: "Веб-сайт", - placeholder: "https://example.com", - type: "url", - required: false, - }, ], }; @@ -69,7 +61,7 @@ const meta: Meta = { screenProgress: { current: 6, total: 10 }, defaultTexts: { nextButton: "Next", - continueButton: "Continue" + }, }, argTypes: { @@ -117,9 +109,7 @@ export const CustomValidation: Story = { screen: { ...richFormScreen, validationMessages: { - required: "Пожалуйста, заполните это поле", - maxLength: "Слишком длинное значение", - invalidFormat: "Неправильный формат данных", + required: "Пожалуйста, заполните это поле", }, }, }, diff --git a/src/components/funnel/templates/FormTemplate/FormTemplate.tsx b/src/components/funnel/templates/FormTemplate/FormTemplate.tsx index 69b903b..ab84495 100644 --- a/src/components/funnel/templates/FormTemplate/FormTemplate.tsx +++ b/src/components/funnel/templates/FormTemplate/FormTemplate.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from "react"; import { TextInput } from "@/components/ui/TextInput/TextInput"; -import type { FormScreenDefinition } from "@/lib/funnel/types"; +import type { FormScreenDefinition, DefaultTexts } from "@/lib/funnel/types"; import { TemplateLayout } from "../layouts/TemplateLayout"; interface FormTemplateProps { @@ -15,7 +15,7 @@ interface FormTemplateProps { canGoBack: boolean; onBack: () => void; screenProgress?: { current: number; total: number }; - defaultTexts?: { nextButton?: string; continueButton?: string }; + defaultTexts?: DefaultTexts; } export function FormTemplate({ @@ -47,16 +47,7 @@ export function FormTemplate({ return screen.validationMessages?.required?.replace('${field}', field.label || field.id) || `${field.label || field.id} is required`; } - if (field.maxLength && value.length > field.maxLength) { - return screen.validationMessages?.maxLength?.replace('${maxLength}', String(field.maxLength)) || `Maximum ${field.maxLength} characters allowed`; - } - if (field.validation?.pattern) { - const regex = new RegExp(field.validation.pattern); - if (!regex.test(value)) { - return field.validation.message || screen.validationMessages?.invalidFormat || "Invalid format"; - } - } return null; }; @@ -111,7 +102,11 @@ export function FormTemplate({ titleDefaults={{ font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" }} subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }} actionButtonOptions={{ - defaultText: defaultTexts?.continueButton || "Continue", + // Правильная логика приоритетов для текста кнопки: + // 1. screen.bottomActionButton.text (настройка экрана) + // 2. defaultTexts.nextButton (глобальная настройка воронки) + // 3. "Next" (хардкод fallback) + defaultText: screen.bottomActionButton?.text || defaultTexts?.nextButton || "Next", disabled: !isFormComplete, onClick: handleContinue, }} @@ -125,7 +120,6 @@ export function FormTemplate({ type={field.type || "text"} value={localFormData[field.id] || ""} onChange={(e) => handleFieldChange(field.id, e.target.value)} - maxLength={field.maxLength} aria-invalid={!!errors[field.id]} aria-errormessage={errors[field.id]} /> diff --git a/src/components/funnel/templates/InfoTemplate/InfoTemplate.stories.tsx b/src/components/funnel/templates/InfoTemplate/InfoTemplate.stories.tsx index 6f1d653..411b780 100644 --- a/src/components/funnel/templates/InfoTemplate/InfoTemplate.stories.tsx +++ b/src/components/funnel/templates/InfoTemplate/InfoTemplate.stories.tsx @@ -22,8 +22,7 @@ const meta: Meta = { onBack: fn(), screenProgress: { current: 3, total: 10 }, defaultTexts: { - nextButton: "Next", - continueButton: "Continue" + nextButton: "Next" }, }, argTypes: { diff --git a/src/components/funnel/templates/InfoTemplate/InfoTemplate.tsx b/src/components/funnel/templates/InfoTemplate/InfoTemplate.tsx index 17ecb61..2d02665 100644 --- a/src/components/funnel/templates/InfoTemplate/InfoTemplate.tsx +++ b/src/components/funnel/templates/InfoTemplate/InfoTemplate.tsx @@ -4,7 +4,7 @@ import { useMemo } from "react"; import Image from "next/image"; import Typography from "@/components/ui/Typography/Typography"; import { buildTypographyProps } from "@/lib/funnel/mappers"; -import type { InfoScreenDefinition } from "@/lib/funnel/types"; +import type { InfoScreenDefinition, DefaultTexts } from "@/lib/funnel/types"; import { TemplateLayout } from "../layouts/TemplateLayout"; import { cn } from "@/lib/utils"; @@ -14,7 +14,7 @@ interface InfoTemplateProps { canGoBack: boolean; onBack: () => void; screenProgress?: { current: number; total: number }; - defaultTexts?: { nextButton?: string; continueButton?: string }; + defaultTexts?: DefaultTexts; } export function InfoTemplate({ diff --git a/src/components/funnel/templates/LoadersTemplate/LoadersTemplate.stories.tsx b/src/components/funnel/templates/LoadersTemplate/LoadersTemplate.stories.tsx index 850ab71..332720d 100644 --- a/src/components/funnel/templates/LoadersTemplate/LoadersTemplate.stories.tsx +++ b/src/components/funnel/templates/LoadersTemplate/LoadersTemplate.stories.tsx @@ -23,7 +23,7 @@ const meta: Meta = { screenProgress: undefined, // У лоадеров обычно нет прогресса defaultTexts: { nextButton: "Next", - continueButton: "Continue" + }, }, argTypes: { diff --git a/src/components/funnel/templates/LoadersTemplate/LoadersTemplate.tsx b/src/components/funnel/templates/LoadersTemplate/LoadersTemplate.tsx index 7e0a02b..ac0995c 100644 --- a/src/components/funnel/templates/LoadersTemplate/LoadersTemplate.tsx +++ b/src/components/funnel/templates/LoadersTemplate/LoadersTemplate.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { TemplateLayout } from "../layouts/TemplateLayout"; import CircularProgressbarsList from "@/components/widgets/CircularProgressbarsList/CircularProgressbarsList"; -import type { LoadersScreenDefinition } from "@/lib/funnel/types"; +import type { LoadersScreenDefinition, DefaultTexts } from "@/lib/funnel/types"; interface LoadersTemplateProps { screen: LoadersScreenDefinition; @@ -11,7 +11,7 @@ interface LoadersTemplateProps { canGoBack: boolean; onBack: () => void; screenProgress?: { current: number; total: number }; - defaultTexts?: { nextButton?: string; continueButton?: string }; + defaultTexts?: DefaultTexts; } export function LoadersTemplate({ diff --git a/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.stories.tsx b/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.stories.tsx index afabd51..087b57b 100644 --- a/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.stories.tsx +++ b/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.stories.tsx @@ -23,7 +23,7 @@ const meta: Meta = { screenProgress: { current: 10, total: 10 }, // Обычно финальный экран defaultTexts: { nextButton: "Next", - continueButton: "Continue" + }, }, argTypes: { diff --git a/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.tsx b/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.tsx index aea5490..7b40969 100644 --- a/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.tsx +++ b/src/components/funnel/templates/SoulmatePortraitTemplate/SoulmatePortraitTemplate.tsx @@ -1,6 +1,6 @@ "use client"; -import type { SoulmatePortraitScreenDefinition } from "@/lib/funnel/types"; +import type { SoulmatePortraitScreenDefinition, DefaultTexts } from "@/lib/funnel/types"; import { TemplateLayout } from "../layouts/TemplateLayout"; interface SoulmatePortraitTemplateProps { @@ -9,7 +9,7 @@ interface SoulmatePortraitTemplateProps { canGoBack: boolean; onBack: () => void; screenProgress?: { current: number; total: number }; - defaultTexts?: { nextButton?: string; continueButton?: string }; + defaultTexts?: DefaultTexts; } export function SoulmatePortraitTemplate({ diff --git a/src/components/funnel/templates/layouts/TemplateLayout.tsx b/src/components/funnel/templates/layouts/TemplateLayout.tsx index 7b7d29f..74a56f7 100644 --- a/src/components/funnel/templates/layouts/TemplateLayout.tsx +++ b/src/components/funnel/templates/layouts/TemplateLayout.tsx @@ -39,6 +39,7 @@ interface TemplateLayoutProps { }; // Дополнительные props для BottomActionButton + childrenAboveButton?: React.ReactNode; childrenUnderButton?: React.ReactNode; // Контент template @@ -57,6 +58,7 @@ export function TemplateLayout({ titleDefaults = { font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" }, subtitleDefaults = { font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }, actionButtonOptions, + childrenAboveButton, childrenUnderButton, children, }: TemplateLayoutProps) { @@ -123,6 +125,7 @@ export function TemplateLayout({ )} diff --git a/src/components/ui/TextAreaInput/TextAreaInput.tsx b/src/components/ui/TextAreaInput/TextAreaInput.tsx new file mode 100644 index 0000000..7b4f2b1 --- /dev/null +++ b/src/components/ui/TextAreaInput/TextAreaInput.tsx @@ -0,0 +1,54 @@ +import { cn } from "@/lib/utils"; +import { Textarea } from "../textarea"; +import { Label } from "../label"; +import { useId } from "react"; + +interface TextAreaInputProps extends React.ComponentProps { + label?: string; + containerProps?: React.ComponentProps<"div">; +} + +function TextAreaInput({ + className, + label, + containerProps, + ...props +}: TextAreaInputProps) { + const id = useId(); + const textareaId = props.id || id; + + return ( +
+ {label && ( + + )} +