w-funnel/docs/templates-and-builder.md
2025-09-26 02:44:41 +02:00

17 KiB
Raw Blame History

Шаблоны экранов и конструктор воронки

Этот документ описывает, из каких частей состоит JSON-конфигурация воронки, какие шаблоны экранов доступны в рантайме и как с ними работает конструктор (builder). Используйте его как справочник при ручном редактировании JSON или при настройке воронки через интерфейс администратора.

Архитектура воронки

Воронка описывается объектом FunnelDefinition и состоит из двух частей: метаданных и списка экранов. Навигация осуществляется по идентификаторам экранов, а состояние (выборы пользователя) хранится отдельно в рантайме.

interface FunnelDefinition {
  meta: {
    id: string;
    version?: string;
    title?: string;
    description?: string;
    firstScreenId?: string; // стартовый экран, по умолчанию первый в списке
  };
  defaultTexts?: {
    nextButton?: string;
    continueButton?: string;
  };
  screens: ScreenDefinition[]; // набор экранов разных шаблонов
}

Каждый экран обязан иметь уникальный id и поле template, которое выбирает шаблон визуализации. Дополнительно поддерживаются:

  • header — управляет прогресс-баром, заголовком и кнопкой «Назад». По умолчанию шапка показывается, а прогресс вычисляется автоматически в рантайме.
  • bottomActionButton — универсальное описание основной кнопки («Продолжить», «Далее» и т. п.). Шаблон может переопределить или скрыть её.
  • navigation — правила переходов между экранами.

Навигация

Навигация описывается объектом NavigationDefinition:

interface NavigationDefinition {
  defaultNextScreenId?: string; // переход по умолчанию
  rules?: Array<{
    nextScreenId: string; // куда перейти, если условие выполнено
    conditions: Array<{
      screenId: string; // экран, чьи ответы проверяем
      operator?: "includesAny" | "includesAll" | "includesExactly";
      optionIds: string[]; // выбранные опции, которые проверяются
    }>;
  }>;
}

Рантайм использует первый сработавший rule и только после этого обращается к defaultNextScreenId. Для списков с одиночным выбором и скрытой кнопкой переход совершается автоматически при изменении ответа. Для всех прочих шаблонов пользователь должен нажать действие, сконфигурированное для текущего экрана.

Шаблоны экранов

Ниже приведено краткое описание каждого шаблона и JSON-поле, которое его конфигурирует.

Информационный экран (template: "info")

Используется для показа статических сообщений, промо-блоков или инструкций. Обязательные поля — id, template, title. Дополнительно поддерживаются:

  • description — расширенный текст под заголовком.
  • icon — эмодзи или картинка. type принимает значения emoji или image, value — символ или URL, sizesm | md | lg | xl.
  • bottomActionButton — описание кнопки внизу, если нужно отличное от дефолтного текста.
{
  "id": "welcome",
  "template": "info",
  "title": { "text": "Добро пожаловать" },
  "description": { "text": "Заполните короткую анкету, чтобы получить персональное предложение." },
  "icon": { "type": "emoji", "value": "👋", "size": "lg" },
  "navigation": { "defaultNextScreenId": "question-1" }
}

Рантайм выводит заголовок по центру, кнопку «Next» (или defaultTexts.nextButton) и позволяет вернуться назад, если это разрешено в header. Логика описана в InfoTemplate и buildLayoutQuestionProps — дополнительные параметры (font, color, align) влияют на типографику.【F:src/components/funnel/templates/InfoTemplate.tsx†L1-L99】【F:src/lib/funnel/types.ts†L74-L131】

Экран с вопросом и вариантами (template: "list")

Базовый интерактивный экран. Поле list описывает варианты ответов:

{
  "id": "question-1",
  "template": "list",
  "title": { "text": "Какой формат подходит?" },
  "subtitle": { "text": "Можно выбрать несколько", "color": "muted" },
  "list": {
    "selectionType": "multi", // или "single"
    "options": [
      { "id": "opt-online", "label": "Онлайн" },
      { "id": "opt-offline", "label": "Офлайн", "description": "в вашем городе" }
    ],
    "bottomActionButton": { "text": "Сохранить выбор" }
  },
  "bottomActionButton": { "show": false },
  "navigation": {
    "defaultNextScreenId": "calendar",
    "rules": [
      {
        "nextScreenId": "coupon",
        "conditions": [{
          "screenId": "question-1",
          "operator": "includesAll",
          "optionIds": ["opt-online", "opt-offline"]
        }]
      }
    ]
  }
}

Особенности:

  • selectionType определяет поведение: single строит радиокнопки, multi — чекбоксы. Компоненты RadioAnswersList и SelectAnswersList получают подготовленные данные из mapListOptionsToButtons.
  • Кнопка действия может описываться либо на уровне list.bottomActionButton, либо через общий bottomActionButton. В рантайме она скрывается, если show: false. Для списков с одиночным выбором и скрытой кнопкой включается автопереход на следующий экран при изменении ответа.【F:src/components/funnel/templates/ListTemplate.tsx†L1-L109】【F:src/components/funnel/FunnelRuntime.tsx†L73-L199】
  • Ответы сохраняются в массиве строк (идентификаторы опций) и используются навигацией и аналитикой.

Экран выбора даты (template: "date")

Предлагает три выпадающих списка (месяц, день, год) и опциональный блок с отформатированной датой.

{
  "id": "calendar",
  "template": "date",
  "title": { "text": "Когда планируете начать?" },
  "subtitle": { "text": "Выберите ориентировочную дату", "color": "muted" },
  "dateInput": {
    "monthLabel": "Месяц",
    "dayLabel": "День",
    "yearLabel": "Год",
    "showSelectedDate": true,
    "selectedDateLabel": "Вы выбрали"
  },
  "infoMessage": { "text": "Мы не будем делиться датой с третьими лицами." },
  "navigation": { "defaultNextScreenId": "contact" }
}

Особенности:

  • Значение сохраняется как массив [month, day, year] внутри answers рантайма.
  • Кнопка «Next» активируется только после заполнения всех полей. Настройка текстов и подсказок — через объект dateInput (placeholder, label, формат для превью).
  • При showSelectedDate: true под кнопкой появляется подтверждающий блок с читабельной датой.【F:src/components/funnel/templates/DateTemplate.tsx†L1-L209】【F:src/lib/funnel/types.ts†L133-L189】

Экран формы (template: "form")

Подходит для сбора контактных данных. Поле fields содержит список текстовых инпутов со своими правилами.

{
  "id": "contact",
  "template": "form",
  "title": { "text": "Оставьте контакты" },
  "fields": [
    { "id": "name", "label": "Имя", "required": true, "maxLength": 60 },
    {
      "id": "email",
      "label": "E-mail",
      "type": "email",
      "validation": {
        "pattern": "^\\S+@\\S+\\.\\S+$",
        "message": "Введите корректный e-mail"
      }
    }
  ],
  "validationMessages": {
    "required": "Поле ${field} обязательно",
    "invalidFormat": "Неверный формат"
  },
  "navigation": { "defaultNextScreenId": "coupon" }
}

Особенности рантайма:

  • Локальное состояние синхронизируется с глобальным через onFormDataChange — данные сериализуются в JSON-строку и хранятся в массиве ответов (первый элемент).【F:src/components/funnel/FunnelRuntime.tsx†L46-L118】
  • Кнопка продолжения (defaultTexts.continueButton или «Continue») активна, если все обязательные поля заполнены. Валидаторы проверяют required, maxLength и регулярное выражение из validation.pattern с кастомными сообщениями.【F:src/components/funnel/templates/FormTemplate.tsx†L1-L119】【F:src/lib/funnel/types.ts†L191-L238】

Экран промокода (template: "coupon")

Отображает купон с акцией и позволяет скопировать промокод.

{
  "id": "coupon",
  "template": "coupon",
  "title": { "text": "Поздравляем!" },
  "subtitle": { "text": "Получите скидку" },
  "coupon": {
    "title": { "text": "Скидка 20%" },
    "offer": {
      "title": { "text": "-20% на первый заказ" },
      "description": { "text": "Действует до конца месяца" }
    },
    "promoCode": { "text": "START20" },
    "footer": { "text": "Скопируйте код и введите при оформлении" }
  },
  "copiedMessage": "Код {code} скопирован!",
  "navigation": { "defaultNextScreenId": "final-info" }
}

CouponTemplate копирует код в буфер обмена и показывает уведомление copiedMessage (строка с подстановкой {code}). Кнопка продолжения использует defaultTexts.continueButton или значение «Continue».【F:src/components/funnel/templates/CouponTemplate.tsx†L1-L111】【F:src/lib/funnel/types.ts†L191-L230】

Конструктор (Builder)

Конструктор помогает собирать JSON-конфигурацию и состоит из трёх основных областей:

  1. Верхняя панель (BuilderTopBar). Позволяет создать пустой проект, загрузить готовый JSON и экспортировать текущую конфигурацию. Импорт использует deserializeFunnelDefinition, добавляющий служебные координаты для канваса. Экспорт сериализует состояние обратно в формат FunnelDefinition (serializeBuilderState).【F:src/components/admin/builder/BuilderTopBar.tsx†L1-L79】【F:src/lib/admin/builder/utils.ts†L1-L58】
  2. Канвас (BuilderCanvas). Отображает экраны цепочкой, даёт возможность добавлять новые (add-screen), менять порядок drag-and-drop (reorder-screens) и выбирать экран для редактирования. Каждый экран показывает тип шаблона, количество опций и ссылку на следующий экран по умолчанию.【F:src/components/admin/builder/BuilderCanvas.tsx†L1-L132】
  3. Боковая панель (BuilderSidebar). Содержит две вкладки состояния:
    • Когда экран не выбран, показываются настройки воронки (ID, заголовок, описание, стартовый экран) и сводка валидации (validateBuilderState).【F:src/components/admin/builder/BuilderSidebar.tsx†L1-L188】【F:src/lib/admin/builder/validation.ts†L1-L168】
    • Для выбранного экрана доступны поля заголовков, параметры списка (тип выбора, опции), правила навигации, кастомизация кнопок и инструмент удаления. Все изменения отправляются через update-screen, update-navigation и вспомогательные обработчики, формируя корректный JSON.

Предпросмотр

Компонент BuilderPreview визуализирует выбранный экран, используя те же шаблоны, что и боевой рантайм (ListTemplate, InfoTemplate и др.). Для симуляции действий используются заглушки — выбор опций, заполнение формы и навигация обновляют локальное состояние предпросмотра, но не меняют структуру воронки. При переключении экрана состояние сбрасывается, что позволяет увидеть дефолтное поведение каждого шаблона.【F:src/components/admin/builder/BuilderPreview.tsx†L1-L123】

Валидация и сериализация

validateBuilderState проверяет уникальность идентификаторов экранов и опций, корректность ссылок в навигации и наличие переходов. Ошибки и предупреждения отображаются в боковой панели. При экспорте координаты канваса удаляются, чтобы JSON соответствовал ожиданиям рантайма. Ответы пользователей рантайм хранит в структуре Record<string, string[]>, где ключ — id экрана, а значение — массив выбранных значений (опций, компонентов даты или сериализованные данные формы).【F:src/lib/admin/builder/validation.ts†L1-L168】【F:src/lib/admin/builder/utils.ts†L1-L86】【F:src/components/funnel/FunnelRuntime.tsx†L1-L215】

Рабочий процесс

  1. Создайте экраны через верхнюю панель или кнопку на канвасе. Каждый новый экран получает уникальный ID (screen-{n}).
  2. Настройте порядок переходов drag-and-drop и установите firstScreenId, если стартовать нужно не с первого элемента.
  3. Заполните контент для каждого шаблона, настройте условия в navigation.rules и убедитесь, что defaultNextScreenId указан для веток без правил.
  4. Проверьте сводку валидации — при ошибках экспорт JSON будет возможен, но рантайм может не смочь построить маршрут.
  5. Экспортируйте JSON и передайте его рантайму (<FunnelRuntime funnel={definition} initialScreenId={definition.meta.firstScreenId} />).

Такой подход гарантирует, что конструктор и рантайм используют одну и ту же схему данных, а визуальные шаблоны ведут себя предсказуемо при изменении конфигурации.