add load json
This commit is contained in:
parent
053bb0f106
commit
68e990db12
44
README.md
44
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
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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} скопирован!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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} скопирован!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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} скопирован!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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} скопирован!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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} скопирован!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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} скопирован!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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} скопирован!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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} скопирован!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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} скопирован!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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} скопирован!"
|
||||
}
|
||||
]
|
||||
}
|
||||
77
scripts/README.md
Normal file
77
scripts/README.md
Normal file
@ -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 <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` - синхронизировать только одну воронку
|
||||
290
scripts/sync-funnels-from-db.mjs
Executable file
290
scripts/sync-funnels-from-db.mjs
Executable file
@ -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 <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();
|
||||
@ -124,8 +124,7 @@ export default function AdminCatalogPage() {
|
||||
firstScreenId: 'screen-1'
|
||||
},
|
||||
defaultTexts: {
|
||||
nextButton: 'Далее',
|
||||
continueButton: 'Продолжить'
|
||||
nextButton: 'Continue'
|
||||
},
|
||||
screens: [
|
||||
{
|
||||
|
||||
@ -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<string, unknown>;
|
||||
|
||||
// Убираем служебное поле 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() {
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside className="w-[360px] shrink-0 overflow-y-auto border-r border-border/60 bg-background/95">
|
||||
<aside className="w-[440px] shrink-0 overflow-y-auto border-r border-border/60 bg-background/95">
|
||||
<BuilderSidebar />
|
||||
</aside>
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { ArrowDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context";
|
||||
import { AddScreenDialog } from "../dialogs/AddScreenDialog";
|
||||
@ -145,10 +144,9 @@ export function BuilderCanvas() {
|
||||
</div>
|
||||
|
||||
<div className="relative flex-1 overflow-auto bg-slate-50 p-6 dark:bg-slate-900">
|
||||
<div className="relative mx-auto max-w-4xl">
|
||||
<div className="absolute left-6 top-0 bottom-0 hidden w-px bg-border md:block" aria-hidden />
|
||||
<div className="relative mx-auto w-full max-w-none">
|
||||
<div
|
||||
className="space-y-6 pl-0 md:pl-12"
|
||||
className="space-y-6"
|
||||
onDragOver={handleDragOverList}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
@ -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 (
|
||||
<div key={screen.id} className="relative">
|
||||
{isDropBefore && <DropIndicator isActive={isDropBefore} />}
|
||||
<div className="flex items-start gap-4 md:gap-6">
|
||||
<div className="relative hidden w-8 flex-shrink-0 md:flex md:flex-col md:items-center">
|
||||
<span className="mt-1 h-3 w-3 rounded-full border-2 border-background bg-primary shadow" />
|
||||
{!isLast && (
|
||||
<div className="mt-2 flex h-full flex-col items-center">
|
||||
<div className="flex-1 w-px bg-gradient-to-b from-primary/40 via-border/40 to-transparent" />
|
||||
<ArrowDown className="mt-1 h-4 w-4 text-border/70" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex-1 cursor-grab rounded-2xl border border-border/70 bg-background/95 p-5 shadow-sm transition-all hover:border-primary/40 hover:shadow-md",
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { TemplateConfig } from "@/components/admin/builder/templates";
|
||||
import { ScreenVariantsConfig } from "../forms/ScreenVariantsConfig";
|
||||
import { useBuilderDispatch, useBuilderSelectedScreen, useBuilderState } from "@/lib/admin/builder/context";
|
||||
@ -57,6 +58,10 @@ export function BuilderSidebar() {
|
||||
dispatch({ type: "set-meta", payload: { firstScreenId: value } });
|
||||
};
|
||||
|
||||
const handleDefaultTextsChange = (field: keyof NonNullable<typeof state.defaultTexts>, 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() {
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
{activeTab === "funnel" ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Section title="Валидация">
|
||||
<ValidationSummary issues={validation.issues} />
|
||||
</Section>
|
||||
{/* Валидация всегда вверху, без заголовка */}
|
||||
<ValidationSummary issues={validation.issues} />
|
||||
|
||||
<Section title="Настройки воронки" description="Общие параметры">
|
||||
<TextInput
|
||||
@ -333,6 +337,21 @@ export function BuilderSidebar() {
|
||||
</label>
|
||||
</Section>
|
||||
|
||||
<Section title="Дефолтные тексты" description="Текст кнопок и баннеров">
|
||||
<TextInput
|
||||
label="Текст кнопки Next/Continue"
|
||||
placeholder="Next"
|
||||
value={state.defaultTexts?.nextButton ?? ""}
|
||||
onChange={(event) => handleDefaultTextsChange("nextButton", event.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label="Баннер приватности"
|
||||
placeholder="Мы не передаем личную информацию..."
|
||||
value={state.defaultTexts?.privacyBanner ?? ""}
|
||||
onChange={(event) => handleDefaultTextsChange("privacyBanner", event.target.value)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Экраны">
|
||||
<div className="flex flex-col gap-3 rounded-lg border border-border/60 bg-muted/20 p-3 text-sm text-muted-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -352,6 +371,9 @@ export function BuilderSidebar() {
|
||||
</div>
|
||||
) : selectedScreen ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Валидация всегда вверху, без заголовка */}
|
||||
<ValidationSummary issues={screenValidationIssues} />
|
||||
|
||||
<div className="rounded-lg border border-primary/20 bg-primary/5 p-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
@ -457,9 +479,10 @@ export function BuilderSidebar() {
|
||||
<span className="text-xs font-semibold uppercase text-muted-foreground">Правило {ruleIndex + 1}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-destructive"
|
||||
className="h-8 px-2 text-destructive hover:bg-destructive/10"
|
||||
onClick={() => handleRemoveRule(selectedScreen.id, ruleIndex)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
<span className="text-xs">Удалить</span>
|
||||
</Button>
|
||||
</div>
|
||||
@ -533,10 +556,6 @@ export function BuilderSidebar() {
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section title="Валидация">
|
||||
<ValidationSummary issues={screenValidationIssues} />
|
||||
</Section>
|
||||
|
||||
<Section title="Управление">
|
||||
<div className="rounded-lg border border-destructive/20 bg-destructive/5 p-4">
|
||||
<p className="mb-3 text-sm text-muted-foreground">
|
||||
@ -548,6 +567,7 @@ export function BuilderSidebar() {
|
||||
disabled={state.screens.length <= 1}
|
||||
onClick={() => handleDeleteScreen(selectedScreen.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
{state.screens.length <= 1 ? "Нельзя удалить последний экран" : "Удалить экран"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -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<string[]>([]);
|
||||
const [previewVariantIndex, setPreviewVariantIndex] = useState<number | null>(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() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, [previewScreen, selectedIds, handleSelectionChange]);
|
||||
}, [previewScreen, selectedIds, handleSelectionChange, builderState.defaultTexts]);
|
||||
|
||||
const preview = useMemo(() => {
|
||||
if (!previewScreen) {
|
||||
|
||||
@ -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
|
||||
</h3>
|
||||
<label className="flex flex-col gap-2 text-xs font-medium text-muted-foreground">
|
||||
Заголовок оффера
|
||||
<TextInput
|
||||
<TextAreaInput
|
||||
placeholder="-50% на первый заказ"
|
||||
value={couponScreen.coupon?.offer?.title?.text ?? ""}
|
||||
onChange={(event) =>
|
||||
@ -44,11 +45,13 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
|
||||
},
|
||||
})
|
||||
}
|
||||
rows={2}
|
||||
className="resize-y"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-2 text-xs font-medium text-muted-foreground">
|
||||
Подзаголовок/описание
|
||||
<TextInput
|
||||
<TextAreaInput
|
||||
placeholder="Персональная акция только сегодня"
|
||||
value={couponScreen.coupon?.offer?.description?.text ?? ""}
|
||||
onChange={(event) =>
|
||||
@ -60,6 +63,8 @@ export function CouponScreenConfig({ screen, onUpdate }: CouponScreenConfigProps
|
||||
},
|
||||
})
|
||||
}
|
||||
rows={2}
|
||||
className="resize-y"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@ -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 (
|
||||
<div className="space-y-6 rounded-2xl border border-border/70 bg-background/80 p-5 shadow-sm">
|
||||
@ -185,25 +174,6 @@ export function DateScreenConfig({ screen, onUpdate }: DateScreenConfigProps) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-foreground">Информационный блок</h4>
|
||||
<label className="flex flex-col gap-2 text-sm">
|
||||
<span className="text-xs font-medium text-muted-foreground">Сообщение (оставьте пустым, чтобы скрыть)</span>
|
||||
<TextInput
|
||||
value={dateScreen.infoMessage?.text ?? ""}
|
||||
onChange={(event) => handleInfoMessageChange("text", event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
{dateScreen.infoMessage && (
|
||||
<label className="flex flex-col gap-2 text-sm">
|
||||
<span className="text-xs font-medium text-muted-foreground">Emoji/иконка для сообщения</span>
|
||||
<TextInput
|
||||
value={dateScreen.infoMessage.icon ?? ""}
|
||||
onChange={(event) => handleInfoMessageChange("icon", event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
</div>
|
||||
|
||||
{formScreen.fields?.map((field, index) => (
|
||||
<div key={field.id} className="space-y-2 rounded-lg border border-border/50 bg-muted/5 p-3">
|
||||
<div key={index} className="space-y-2 rounded-lg border border-border/50 bg-muted/5 p-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Поле {index + 1}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 px-3 text-xs text-destructive"
|
||||
className="h-8 px-2 text-destructive hover:bg-destructive/10"
|
||||
onClick={() => removeField(index)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
Удалить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||||
ID поля
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium text-muted-foreground">ID поля</span>
|
||||
<TextInput value={field.id} onChange={(event) => updateField(index, { id: event.target.value })} />
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||||
Тип
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium text-muted-foreground">Тип</span>
|
||||
<select
|
||||
className="rounded-lg border border-border bg-background px-2 py-1"
|
||||
className="rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||||
value={field.type ?? "text"}
|
||||
onChange={(event) => updateField(index, { type: event.target.value as FormFieldDefinition["type"] })}
|
||||
>
|
||||
<option value="text">Текст</option>
|
||||
<option value="email">E-mail</option>
|
||||
<option value="tel">Телефон</option>
|
||||
<option value="url">Ссылка</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||
Метка поля
|
||||
<TextInput
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium text-muted-foreground">Метка поля</span>
|
||||
<TextAreaInput
|
||||
value={field.label ?? ""}
|
||||
onChange={(event) => updateField(index, { label: event.target.value })}
|
||||
rows={2}
|
||||
className="resize-y"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||
Placeholder
|
||||
<TextInput
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium text-muted-foreground">Placeholder</span>
|
||||
<TextAreaInput
|
||||
value={field.placeholder ?? ""}
|
||||
onChange={(event) => updateField(index, { placeholder: event.target.value })}
|
||||
rows={2}
|
||||
className="resize-y"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
<div className="pt-2">
|
||||
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -118,58 +123,8 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
/>
|
||||
Обязательно для заполнения
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||||
Максимальная длина
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
className="rounded-lg border border-border bg-background px-2 py-1"
|
||||
value={field.maxLength ?? ""}
|
||||
onChange={(event) =>
|
||||
updateField(index, {
|
||||
maxLength: event.target.value ? Number(event.target.value) : undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||||
Регулярное выражение (pattern)
|
||||
<TextInput
|
||||
placeholder="Например, ^\\d+$"
|
||||
value={field.validation?.pattern ?? ""}
|
||||
onChange={(event) =>
|
||||
updateField(index, {
|
||||
validation: {
|
||||
...(field.validation ?? {}),
|
||||
pattern: event.target.value || undefined,
|
||||
message: field.validation?.message,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-muted-foreground">
|
||||
Текст ошибки для pattern
|
||||
<TextInput
|
||||
placeholder="Неверный формат"
|
||||
value={field.validation?.message ?? ""}
|
||||
onChange={(event) =>
|
||||
updateField(index, {
|
||||
validation:
|
||||
field.validation || event.target.value
|
||||
? {
|
||||
...(field.validation ?? {}),
|
||||
message: event.target.value || undefined,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -191,45 +146,16 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
||||
Доступна переменная: <code className="bg-muted px-1 rounded">{`{field}`}</code> - название поля
|
||||
</p>
|
||||
</div>
|
||||
<TextInput
|
||||
placeholder="Пример: {field} обязательно для заполнения"
|
||||
<TextAreaInput
|
||||
placeholder="Пример: Поле {field} обязательно"
|
||||
value={formScreen.validationMessages?.required ?? ""}
|
||||
onChange={(event) => updateValidationMessages({ required: event.target.value || undefined })}
|
||||
rows={2}
|
||||
className="resize-y"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border/60 bg-muted/20 p-3">
|
||||
<label className="flex flex-col gap-2 text-muted-foreground">
|
||||
<div>
|
||||
<span className="font-medium">Превышена длина</span>
|
||||
<p className="text-xs mt-1 text-muted-foreground/80">
|
||||
Доступны переменные: <code className="bg-muted px-1 rounded">{`{field}`}</code>, <code className="bg-muted px-1 rounded">{`{maxLength}`}</code>
|
||||
</p>
|
||||
</div>
|
||||
<TextInput
|
||||
placeholder="Пример: {field} не может быть длиннее {maxLength} символов"
|
||||
value={formScreen.validationMessages?.maxLength ?? ""}
|
||||
onChange={(event) => updateValidationMessages({ maxLength: event.target.value || undefined })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border/60 bg-muted/20 p-3">
|
||||
<label className="flex flex-col gap-2 text-muted-foreground">
|
||||
<div>
|
||||
<span className="font-medium">Неверный формат</span>
|
||||
<p className="text-xs mt-1 text-muted-foreground/80">
|
||||
Доступна переменная: <code className="bg-muted px-1 rounded">{`{field}`}</code> - название поля
|
||||
</p>
|
||||
</div>
|
||||
<TextInput
|
||||
placeholder="Пример: Проверьте формат {field}"
|
||||
value={formScreen.validationMessages?.invalidFormat ?? ""}
|
||||
onChange={(event) => updateValidationMessages({ invalidFormat: event.target.value || undefined })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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) {
|
||||
<div className="space-y-3">
|
||||
<label className="flex flex-col gap-2 text-sm">
|
||||
<span className="text-xs font-medium text-muted-foreground">Описание (необязательно)</span>
|
||||
<TextInput
|
||||
<TextAreaInput
|
||||
placeholder="Введите пояснение для пользователя. Используйте **текст** для выделения жирным."
|
||||
value={infoScreen.description?.text ?? ""}
|
||||
onChange={(event) => handleDescriptionChange(event.target.value)}
|
||||
rows={3}
|
||||
className="resize-y"
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
||||
@ -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) {
|
||||
<div className="space-y-3">
|
||||
{listScreen.list.options.map((option, index) => (
|
||||
<div
|
||||
key={option.id}
|
||||
key={index}
|
||||
className="space-y-2 rounded-lg border border-border/50 bg-muted/5 p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
@ -222,9 +223,11 @@ export function ListScreenConfig({ screen, onUpdate }: ListScreenConfigProps) {
|
||||
|
||||
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
||||
Подпись для пользователя
|
||||
<TextInput
|
||||
<TextAreaInput
|
||||
value={option.label}
|
||||
onChange={(event) => handleOptionChange(index, "label", event.target.value)}
|
||||
rows={2}
|
||||
className="resize-y"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@ -240,12 +243,14 @@ export function ListScreenConfig({ screen, onUpdate }: ListScreenConfigProps) {
|
||||
|
||||
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
||||
Описание (необязательно)
|
||||
<TextInput
|
||||
<TextAreaInput
|
||||
placeholder="Дополнительное описание варианта"
|
||||
value={option.description ?? ""}
|
||||
onChange={(event) =>
|
||||
handleOptionChange(index, "description", event.target.value || undefined)
|
||||
}
|
||||
rows={2}
|
||||
className="resize-y"
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
||||
@ -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 (
|
||||
<div className="space-y-3">
|
||||
{showToggle && (
|
||||
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value?.show ?? true}
|
||||
onChange={(event) => handleShowToggle(event.target.checked)}
|
||||
/>
|
||||
Показывать {label.toLowerCase()}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">{label}</label>
|
||||
<TextInput value={value?.text ?? ""} onChange={(event) => handleTextChange(event.target.value)} />
|
||||
<TextAreaInput
|
||||
value={value?.text ?? ""}
|
||||
onChange={(event) => handleTextChange(event.target.value)}
|
||||
rows={2}
|
||||
className="resize-y"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{value?.text && (
|
||||
@ -412,8 +438,20 @@ export function TemplateConfig({ screen, onUpdate }: TemplateConfigProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<CollapsibleSection title="Заголовок и подзаголовок">
|
||||
<TypographyControls label="Заголовок" value={screen.title} onChange={handleTitleChange} />
|
||||
<TypographyControls label="Подзаголовок" value={"subtitle" in screen ? screen.subtitle : undefined} onChange={handleSubtitleChange} allowRemove />
|
||||
<TypographyControls
|
||||
label="Заголовок"
|
||||
value={screen.title}
|
||||
onChange={handleTitleChange}
|
||||
allowRemove
|
||||
showToggle
|
||||
/>
|
||||
<TypographyControls
|
||||
label="Подзаголовок"
|
||||
value={"subtitle" in screen ? screen.subtitle : undefined}
|
||||
onChange={handleSubtitleChange}
|
||||
allowRemove
|
||||
showToggle
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection title="Шапка экрана">
|
||||
|
||||
@ -22,8 +22,7 @@ const meta: Meta<typeof CouponTemplate> = {
|
||||
onBack: fn(),
|
||||
screenProgress: { current: 8, total: 10 },
|
||||
defaultTexts: {
|
||||
nextButton: "Next",
|
||||
continueButton: "Continue"
|
||||
nextButton: "Next"
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
|
||||
@ -24,8 +24,7 @@ const meta: Meta<typeof DateTemplate> = {
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -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 && (
|
||||
<div className="flex justify-center">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex-shrink-0">
|
||||
<Image
|
||||
src="/GuardIcon.svg"
|
||||
alt="Security icon"
|
||||
width={20}
|
||||
height={20}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Typography
|
||||
as="p"
|
||||
size="sm"
|
||||
color="default"
|
||||
{...buildTypographyProps(screen.infoMessage, {
|
||||
as: "p",
|
||||
defaults: {
|
||||
font: "inter",
|
||||
weight: "medium",
|
||||
color: "default",
|
||||
align: "left",
|
||||
},
|
||||
})}
|
||||
className={cn("text-slate-600 leading-relaxed", screen.infoMessage.className)}
|
||||
>
|
||||
{screen.infoMessage.text}
|
||||
</Typography>
|
||||
</div>
|
||||
<PrivacySecurityBanner
|
||||
className="mt-5"
|
||||
text={{
|
||||
children: defaultTexts.privacyBanner,
|
||||
size: "sm",
|
||||
color: "default",
|
||||
font: "inter",
|
||||
weight: "medium"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -23,7 +23,7 @@ const meta: Meta<typeof EmailTemplate> = {
|
||||
screenProgress: { current: 9, total: 10 },
|
||||
defaultTexts: {
|
||||
nextButton: "Next",
|
||||
continueButton: "Continue"
|
||||
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
|
||||
@ -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<typeof FormTemplate> = {
|
||||
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: "Пожалуйста, заполните это поле",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -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]}
|
||||
/>
|
||||
|
||||
@ -22,8 +22,7 @@ const meta: Meta<typeof InfoTemplate> = {
|
||||
onBack: fn(),
|
||||
screenProgress: { current: 3, total: 10 },
|
||||
defaultTexts: {
|
||||
nextButton: "Next",
|
||||
continueButton: "Continue"
|
||||
nextButton: "Next"
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -23,7 +23,7 @@ const meta: Meta<typeof LoadersTemplate> = {
|
||||
screenProgress: undefined, // У лоадеров обычно нет прогресса
|
||||
defaultTexts: {
|
||||
nextButton: "Next",
|
||||
continueButton: "Continue"
|
||||
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -23,7 +23,7 @@ const meta: Meta<typeof SoulmatePortraitTemplate> = {
|
||||
screenProgress: { current: 10, total: 10 }, // Обычно финальный экран
|
||||
defaultTexts: {
|
||||
nextButton: "Next",
|
||||
continueButton: "Continue"
|
||||
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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({
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
ref={bottomActionButtonRef}
|
||||
childrenAboveButton={childrenAboveButton}
|
||||
childrenUnderButton={finalChildrenUnderButton}
|
||||
/>
|
||||
)}
|
||||
|
||||
54
src/components/ui/TextAreaInput/TextAreaInput.tsx
Normal file
54
src/components/ui/TextAreaInput/TextAreaInput.tsx
Normal file
@ -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<typeof Textarea> {
|
||||
label?: string;
|
||||
containerProps?: React.ComponentProps<"div">;
|
||||
}
|
||||
|
||||
function TextAreaInput({
|
||||
className,
|
||||
label,
|
||||
containerProps,
|
||||
...props
|
||||
}: TextAreaInputProps) {
|
||||
const id = useId();
|
||||
const textareaId = props.id || id;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...containerProps}
|
||||
className={cn("w-full flex flex-col gap-2", containerProps?.className)}
|
||||
>
|
||||
{label && (
|
||||
<Label
|
||||
htmlFor={textareaId}
|
||||
className="text-muted-foreground font-inter font-medium text-base"
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
<Textarea
|
||||
data-slot="textarea-input"
|
||||
className={cn(
|
||||
"py-3.5 px-4",
|
||||
"font-inter text-[18px]/[28px] font-semibold text-foreground",
|
||||
"placeholder:text-muted-foreground placeholder:text-[18px]/[28px] font-medium",
|
||||
"border-2 border-primary/30 rounded-2xl",
|
||||
className
|
||||
)}
|
||||
id={textareaId}
|
||||
{...props}
|
||||
/>
|
||||
{props["aria-invalid"] && (
|
||||
<p className="text-destructive font-inter font-medium text-xs">
|
||||
{props["aria-errormessage"]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TextAreaInput };
|
||||
@ -5,10 +5,21 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
// Обеспечиваем корректную работу выделения текста и удаления
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
style={{
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
MozUserSelect: 'text',
|
||||
msUserSelect: 'text'
|
||||
}}
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
// Дополнительные стили для корректной работы с текстом
|
||||
"select-text cursor-text",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
31
src/components/ui/textarea.tsx
Normal file
31
src/components/ui/textarea.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
// Обеспечиваем корректную работу выделения текста и удаления
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
style={{
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
MozUserSelect: 'text',
|
||||
msUserSelect: 'text'
|
||||
}}
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
// Дополнительные стили для корректной работы с текстом
|
||||
"select-text cursor-text",
|
||||
// Стили для textarea
|
||||
"resize-y min-h-[80px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Textarea };
|
||||
@ -51,3 +51,114 @@ export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const WithChildrenAbove: Story = {
|
||||
args: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
},
|
||||
childrenAboveButton: (
|
||||
<div className="w-full text-center space-y-1 mb-4">
|
||||
<Typography
|
||||
as="p"
|
||||
size="sm"
|
||||
color="muted"
|
||||
className="font-medium text-center"
|
||||
>
|
||||
Выбранная дата:
|
||||
</Typography>
|
||||
<Typography
|
||||
as="p"
|
||||
size="xl"
|
||||
weight="bold"
|
||||
color="default"
|
||||
className="font-semibold text-center"
|
||||
>
|
||||
April 8, 1987
|
||||
</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const WithChildrenUnder: Story = {
|
||||
args: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
},
|
||||
childrenUnderButton: (
|
||||
<Typography
|
||||
as="p"
|
||||
size="sm"
|
||||
color="muted"
|
||||
className="text-center mt-3"
|
||||
>
|
||||
By continuing, you agree to our Terms of Service
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const WithBothChildren: Story = {
|
||||
args: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
},
|
||||
childrenAboveButton: (
|
||||
<div className="w-full text-center space-y-1 mb-4">
|
||||
<Typography
|
||||
as="p"
|
||||
size="sm"
|
||||
color="muted"
|
||||
className="font-medium text-center"
|
||||
>
|
||||
Selected Date:
|
||||
</Typography>
|
||||
<Typography
|
||||
as="p"
|
||||
size="xl"
|
||||
weight="bold"
|
||||
color="default"
|
||||
className="font-semibold text-center"
|
||||
>
|
||||
March 15, 1990
|
||||
</Typography>
|
||||
</div>
|
||||
),
|
||||
childrenUnderButton: (
|
||||
<Typography
|
||||
as="p"
|
||||
size="sm"
|
||||
color="muted"
|
||||
className="text-center mt-3"
|
||||
>
|
||||
Your information is secure and private
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const OnlyGradientBlur: Story = {
|
||||
args: {
|
||||
showGradientBlur: true,
|
||||
childrenAboveButton: (
|
||||
<div className="text-center space-y-1">
|
||||
<Typography
|
||||
as="p"
|
||||
size="lg"
|
||||
color="default"
|
||||
className="font-semibold"
|
||||
>
|
||||
Content above the button area
|
||||
</Typography>
|
||||
<Typography
|
||||
as="p"
|
||||
size="sm"
|
||||
color="muted"
|
||||
>
|
||||
This example shows content in the blur area without a button
|
||||
</Typography>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@ -7,7 +7,6 @@ import type {
|
||||
TypographyVariant,
|
||||
IconDefinition,
|
||||
DateInputDefinition,
|
||||
InfoMessageDefinition,
|
||||
CouponDefinition,
|
||||
FormFieldDefinition,
|
||||
FormValidationMessages,
|
||||
@ -67,7 +66,6 @@ export function buildDefaultSubtitle(overrides?: Partial<SubtitleDefinition>): S
|
||||
export function buildDefaultBottomActionButton(overrides?: Partial<BottomActionButtonDefinition>): BottomActionButtonDefinition {
|
||||
return {
|
||||
show: true,
|
||||
text: "Продолжить",
|
||||
showGradientBlur: true,
|
||||
...overrides,
|
||||
};
|
||||
@ -129,21 +127,6 @@ export function buildDefaultDateInput(overrides?: Partial<DateInputDefinition>):
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates default info message configuration
|
||||
*/
|
||||
export function buildDefaultInfoMessage(overrides?: Partial<InfoMessageDefinition>): InfoMessageDefinition {
|
||||
return {
|
||||
text: "Мы используем эту информацию только для анализа",
|
||||
icon: "🔒",
|
||||
font: "manrope",
|
||||
weight: "regular",
|
||||
align: "center",
|
||||
size: "sm",
|
||||
color: "muted",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates default coupon configuration
|
||||
@ -217,8 +200,6 @@ export function buildDefaultFormFields(): FormFieldDefinition[] {
|
||||
export function buildDefaultFormValidation(overrides?: Partial<FormValidationMessages>): FormValidationMessages {
|
||||
return {
|
||||
required: "Это поле обязательно для заполнения",
|
||||
maxLength: "Превышена максимальная длина",
|
||||
invalidFormat: "Неверный формат",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,8 +5,7 @@ import {
|
||||
buildDefaultSubtitle,
|
||||
buildDefaultBottomActionButton,
|
||||
buildDefaultNavigation,
|
||||
buildDefaultDateInput,
|
||||
buildDefaultInfoMessage
|
||||
buildDefaultDateInput
|
||||
} from "./blocks";
|
||||
|
||||
export function buildDateDefaults(id: string): BuilderScreen {
|
||||
@ -17,7 +16,6 @@ export function buildDateDefaults(id: string): BuilderScreen {
|
||||
title: buildDefaultTitle(),
|
||||
subtitle: buildDefaultSubtitle(),
|
||||
dateInput: buildDefaultDateInput(),
|
||||
infoMessage: buildDefaultInfoMessage(),
|
||||
bottomActionButton: buildDefaultBottomActionButton(),
|
||||
navigation: buildDefaultNavigation(),
|
||||
} as BuilderScreen;
|
||||
|
||||
@ -8,7 +8,6 @@ export {
|
||||
buildDefaultDescription,
|
||||
buildDefaultIcon,
|
||||
buildDefaultDateInput,
|
||||
buildDefaultInfoMessage,
|
||||
buildDefaultCoupon,
|
||||
buildDefaultFormFields,
|
||||
buildDefaultFormValidation,
|
||||
|
||||
@ -15,6 +15,15 @@ export function builderReducer(state: BuilderState, action: BuilderAction): Buil
|
||||
},
|
||||
});
|
||||
}
|
||||
case "set-default-texts": {
|
||||
return withDirty(state, {
|
||||
...state,
|
||||
defaultTexts: {
|
||||
...state.defaultTexts,
|
||||
...action.payload,
|
||||
},
|
||||
});
|
||||
}
|
||||
case "add-screen": {
|
||||
const nextId = generateScreenId(state.screens.map((s) => s.id));
|
||||
const template = action.payload?.template || "list";
|
||||
|
||||
@ -11,6 +11,7 @@ export interface BuilderState extends BuilderFunnelState {
|
||||
|
||||
export type BuilderAction =
|
||||
| { type: "set-meta"; payload: Partial<BuilderFunnelState["meta"]> }
|
||||
| { type: "set-default-texts"; payload: Partial<BuilderFunnelState["defaultTexts"]> }
|
||||
| { type: "add-screen"; payload?: { template?: ScreenDefinition["template"] } & Partial<BuilderScreen> }
|
||||
| { type: "remove-screen"; payload: { screenId: string } }
|
||||
| { type: "update-screen"; payload: { screenId: string; screen: Partial<BuilderScreen> } }
|
||||
|
||||
@ -4,5 +4,6 @@ export type BuilderScreen = ScreenDefinition;
|
||||
|
||||
export interface BuilderFunnelState {
|
||||
meta: FunnelDefinition["meta"];
|
||||
defaultTexts?: FunnelDefinition["defaultTexts"];
|
||||
screens: BuilderScreen[];
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ export function deserializeFunnelDefinition(funnel: FunnelDefinition): BuilderSt
|
||||
|
||||
return {
|
||||
meta: funnel.meta,
|
||||
defaultTexts: funnel.defaultTexts,
|
||||
screens: builderScreens,
|
||||
selectedScreenId: builderScreens[0]?.id ?? null,
|
||||
isDirty: false,
|
||||
@ -53,6 +54,7 @@ export function serializeBuilderState(state: BuilderFunnelState): FunnelDefiniti
|
||||
|
||||
return {
|
||||
meta,
|
||||
defaultTexts: state.defaultTexts,
|
||||
screens,
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -193,13 +193,13 @@ export function buildLayoutQuestionProps(
|
||||
onBack: showBackButton ? onBack : undefined,
|
||||
showBackButton,
|
||||
} : undefined,
|
||||
title: buildTypographyProps(screen.title, {
|
||||
title: screen.title ? (buildTypographyProps(screen.title, {
|
||||
as: "h2",
|
||||
defaults: titleDefaults,
|
||||
}) ?? {
|
||||
as: "h2",
|
||||
children: screen.title.text,
|
||||
},
|
||||
}) : undefined,
|
||||
subtitle: 'subtitle' in screen ? buildTypographyProps(screen.subtitle, {
|
||||
as: "p",
|
||||
defaults: subtitleDefaults,
|
||||
|
||||
@ -22,6 +22,7 @@ import type {
|
||||
LoadersScreenDefinition,
|
||||
SoulmatePortraitScreenDefinition,
|
||||
ScreenDefinition,
|
||||
DefaultTexts,
|
||||
} from "@/lib/funnel/types";
|
||||
|
||||
export interface ScreenRenderProps {
|
||||
@ -32,7 +33,7 @@ export interface ScreenRenderProps {
|
||||
canGoBack: boolean;
|
||||
onBack: () => void;
|
||||
screenProgress: { current: number; total: number };
|
||||
defaultTexts?: { nextButton?: string; continueButton?: string };
|
||||
defaultTexts?: DefaultTexts;
|
||||
}
|
||||
|
||||
export type TemplateRenderer = (props: ScreenRenderProps) => JSX.Element;
|
||||
@ -146,9 +147,12 @@ const TEMPLATE_REGISTRY: Record<ScreenDefinition["template"], TemplateRenderer>
|
||||
// Простая логика: кнопка есть если не отключена (show: false)
|
||||
const hasActionButton = !isButtonDisabled;
|
||||
|
||||
const actionConfig = hasActionButton
|
||||
? (bottomActionButton ?? { text: defaultTexts?.nextButton || "Next" })
|
||||
: undefined;
|
||||
// Правильная логика приоритетов для текста кнопки:
|
||||
// 1. bottomActionButton.text (настройка экрана)
|
||||
// 2. defaultTexts.nextButton (глобальная настройка воронки)
|
||||
// 3. "Next" (хардкод fallback)
|
||||
const buttonText = bottomActionButton?.text || defaultTexts?.nextButton || "Next";
|
||||
|
||||
const actionDisabled = hasActionButton && isSelectionEmpty;
|
||||
|
||||
return (
|
||||
@ -158,7 +162,7 @@ const TEMPLATE_REGISTRY: Record<ScreenDefinition["template"], TemplateRenderer>
|
||||
onSelectionChange={onSelectionChange}
|
||||
actionButtonProps={hasActionButton
|
||||
? {
|
||||
children: actionConfig?.text ?? "Next",
|
||||
children: buttonText,
|
||||
disabled: actionDisabled,
|
||||
onClick: actionDisabled ? undefined : onContinue,
|
||||
}
|
||||
|
||||
@ -153,9 +153,6 @@ export interface DateInputDefinition {
|
||||
};
|
||||
}
|
||||
|
||||
export interface InfoMessageDefinition extends TypographyVariant {
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface DateScreenDefinition {
|
||||
id: string;
|
||||
@ -164,7 +161,6 @@ export interface DateScreenDefinition {
|
||||
title: TitleDefinition;
|
||||
subtitle?: SubtitleDefinition;
|
||||
dateInput: DateInputDefinition;
|
||||
infoMessage?: InfoMessageDefinition;
|
||||
bottomActionButton?: BottomActionButtonDefinition;
|
||||
navigation?: NavigationDefinition;
|
||||
variants?: ScreenVariantDefinition<DateScreenDefinition>[];
|
||||
@ -197,19 +193,15 @@ export interface FormFieldDefinition {
|
||||
id: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
type?: "text" | "email" | "tel" | "url";
|
||||
type?: "text" | "email" | "tel";
|
||||
required?: boolean;
|
||||
maxLength?: number;
|
||||
validation?: {
|
||||
pattern?: string;
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FormValidationMessages {
|
||||
required?: string; // "${field} is required"
|
||||
maxLength?: string; // "Maximum ${maxLength} characters allowed"
|
||||
invalidFormat?: string; // "Invalid format"
|
||||
}
|
||||
|
||||
export interface FormScreenDefinition {
|
||||
|
||||
@ -165,7 +165,6 @@ const FunnelMetaSchema = new Schema({
|
||||
|
||||
const DefaultTextsSchema = new Schema({
|
||||
nextButton: { type: String, default: 'Next' },
|
||||
continueButton: { type: String, default: 'Continue' },
|
||||
privacyBanner: { type: String },
|
||||
}, { _id: false });
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user