feat(onboarding): интерактивный онбординг-флоу#611
Conversation
Сама фича поверх подготовительных компонентов (#609) и общих UI-примитивов (#610): - FSM/slice/listeners в features/onboarding - страницы и пошаговые описания в pages/onboarding - регистрация reducer/listener, маршрут /onboarding (+ фикс опечатки OBBOARDING→ONBOARDING), удаление устаревшего listenerModdleware.ts - зависимости @fsmoothy/core, @fsmoothy/graphviz - доки onboarding_spec Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1a52a9c to
0390aad
Compare
Storybook |
| const cycledInfoHooks = { | ||
| guard: (ctx: OnboardingContext) => ctx.data.stack.length === 0 && !ctx.data.isCycledInfoShown, | ||
| onLeave: (ctx: OnboardingContext) => { | ||
| ctx.data.isCycledInfoShown = true |
|
|
||
| export const initialState: OnboardingState = { | ||
| step: OnboardingStep.Opponents, | ||
| game: { |
There was a problem hiding this comment.
Это чем-то отличается от того что в fsm?
https://github.com/MemeBattle/monorepo/pull/611/changes#diff-88b925f11645026078a1ecf0a2377fe8ece8db30273a6d36074b8948237b13dfR71
| } | ||
|
|
||
| interface CardDescriptionProps { | ||
| index: 0 | 1 |
There was a problem hiding this comment.
Какой-то странный дизайн компонента получается.
Может в него просто text пробрасывать, а не брать один из 2х захардкоженных?
|
|
||
| const TEXT_BY_STEP: Partial<Record<OnboardingStep, string>> = { | ||
| [OnboardingStep.GameStartedCycledInfo]: 'Карты в стеке закончились — они перелистаются заново', | ||
| [OnboardingStep.OpponentTurnCycledInfo]: 'Карты в стеке закончились — они перелистаются заново', |
There was a problem hiding this comment.
Тут так же, я бы предложил в компонент только text пробрасывать
| const TEXT_BY_STEP: Partial<Record<OnboardingStep, string>> = { | ||
| [OnboardingStep.Ligretto]: 'Это твоя колода ligretto', | ||
| [OnboardingStep.LigrettoCard]: | ||
| 'Карты из колоды ligretto нужно выкладывать на свободное место в ряду. Раунд закончится, как только первый из игроков выложит все карты из колоды ligretto.', | ||
| [OnboardingStep.LigrettoAvailableCard]: 'Освободилось место в ряду. Выкладывай из колоды ligretto', | ||
| } | ||
|
|
||
| const TOP_ANCHORED_STEPS = new Set<OnboardingStep>([OnboardingStep.LigrettoCard]) |
There was a problem hiding this comment.
И вот это вынести из компонентов
| import { useOnboardingContainerRef } from '../targets' | ||
| import { useTargetRelativePosition } from '#shared/lib/hooks/useTargetRelativePosition' | ||
|
|
||
| const TEXT_BY_STEP: Partial<Record<OnboardingStep, string>> = { |
| const DISABLED_LIGRETTO_STEPS = new Set<OnboardingStep>([ | ||
| OnboardingStep.Opponents, | ||
| OnboardingStep.Playground, | ||
| OnboardingStep.Cards, | ||
| OnboardingStep.Stack, | ||
| OnboardingStep.Row, | ||
| OnboardingStep.Ligretto, | ||
| OnboardingStep.FirstCard, | ||
| OnboardingStep.StackCard, | ||
| OnboardingStep.StackUnavailableCard, | ||
| OnboardingStep.StackAvailableCard, | ||
| OnboardingStep.RowAvailableCard, | ||
| OnboardingStep.GameStarted, | ||
| OnboardingStep.GameStartedCycledInfo, | ||
| ]) | ||
|
|
||
| const STACK_DESCRIPTION_STEPS = new Set<OnboardingStep>([ | ||
| OnboardingStep.Stack, | ||
| OnboardingStep.StackCard, | ||
| OnboardingStep.StackUnavailableCard, | ||
| OnboardingStep.StackAvailableCard, | ||
| ]) | ||
|
|
||
| const PLAYER_ROW_DESCRIPTION_STEPS = new Set<OnboardingStep>([OnboardingStep.Row]) | ||
|
|
||
| const LIGRETTO_DESCRIPTION_STEPS = new Set<OnboardingStep>([ | ||
| OnboardingStep.Ligretto, | ||
| OnboardingStep.LigrettoCard, | ||
| OnboardingStep.LigrettoAvailableCard, | ||
| ]) | ||
|
|
||
| const CENTERED_DESCRIPTION_STEPS = new Set<OnboardingStep>([OnboardingStep.GameStartedCycledInfo, OnboardingStep.OpponentTurnCycledInfo]) |
There was a problem hiding this comment.
Такой подход, когда каждое свойство step'a в отдельном месте хранится, получается сложным.
Я бы предложил собрать все эти переменные в одну типа
const GameStartedStep = {
isDescriptionCentered: false,
hiddenOverlay: true,
describeStack: false,
...
} satisfies StepConfig;
Ревью: - fsm: удалено мёртвое поле stack, guard циклической подсказки проверяет реальную пустоту открытой колоды, колода перелистывается заново - slice: initialState строится из createOnboardingGame() вместо дублирующейся захардкоженной копии - все свойства шага собраны в один StepConfig (stepConfig.ts), тексты вынесены из description-компонентов и передаются через props, общая разметка пузыря — в DescriptionBubble, Layer работает от конфига Сценарий: - продлён финал: соперник отвечает зелёной двойкой на прокрутку колоды, игрок выкладывает зелёную тройку из ряда, раунд заканчивается только когда колода ligretto полностью опустела - карту из колоды ligretto можно положить в свободный слот ряда на шагах opponentTurn и opponentTurnSecondCard (опциональный ход) - подсказка о перелистывании привязана к колоде в руке, а не к центру - экран результатов: текст поздравления внутри панели, без стрелки Тесты и инфраструктура: - юнит-тесты FSM (happy path, циклическая подсказка, зелёная цепочка, опциональные ходы, results только при пустой колоде) - e2e полного прохождения онбординга без бэкенда: webServer в playwright config, стаб /auth/me, data-test-id хуки и page object - listener онбординга завершается при уходе с роута и не дублируется - AGENTS.md с командами тестирования (CLAUDE.md — симлинк) - удалена неиспользуемая зависимость @fsmoothy/graphviz Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Обновил PR по замечаниям ревью:
Сверх ревью: продлён финальный сценарий (зелёная цепочка 1→2→3, раунд кончается при пустой колоде ligretto), опциональные ходы ligretto→ряд, фикс listener-а (завершение при уходе с роута), юнит-тесты FSM и e2e полного прохождения без бэкенда ( |
…rver reuseExistingServer: true — playwright сам проверяет доступность приложения по url/порту и переиспользует уже запущенный сервер (например, поднятый в CI), а команду запускает только если никто не отвечает. Проверка по переменной CI ломала e2e-джобу: CI поднимает фронтенд сам, и порт 5173 уже занят. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| }) | ||
| }, [game.players, results]) | ||
|
|
||
| if (step !== OnboardingStep.Result) { |
There was a problem hiding this comment.
Удалить. За отрисовку или нет должен отвечать родительский компонент
| navigate(routes.HOME) | ||
| }, [navigate]) | ||
|
|
||
| const players = useMemo(() => { |
There was a problem hiding this comment.
Это reselect селектор. Нужно сделать из этого селектор.
| * The onboarding flow itself needs no backend, but the whole app is gated by | ||
| * authorization. Stub the auth endpoint so the test runs without a backend. | ||
| */ | ||
| const stubAuth = async (page: Page) => { |
There was a problem hiding this comment.
Стаб надо будет удалить.
|
|
||
| ## Conventions | ||
|
|
||
| - Code comments and agent docs (this file) — English only. UI copy — Russian. |
There was a problem hiding this comment.
Это поднять в корневой Agents.md
Пункт про UI copy убрать.
| - `pnpm exec playwright test onboarding` — onboarding e2e. **No backend required**: playwright starts the vite dev server itself (`webServer` in `playwright.config.ts`) and reuses one already running on :5173; the auth request (`/auth/me`) is stubbed by the test via `page.route`. | ||
| - `pnpm e2e:start` — all e2e; `e2e/tests/game.spec.ts` requires running backends (core + gameplay) and fails without them — for an isolated frontend check run only `onboarding`. | ||
|
|
||
| Lint/format — from the repo root: `pnpm lint:check`, `pnpm fmt:check`. |
Onboarding
Что внутри
features/onboardingpages/onboardingstepConfig.ts/onboarding(+ фикс опечаткиOBBOARDING→ONBOARDING)listenerModdleware.ts@fsmoothy/coreСценарий
Тесты
src/features/onboarding/model/fsm.spec.ts(pnpm test:ci)pnpm exec playwright test onboarding— vite поднимается сам (webServer),/auth/meстабится, приложение остаётся за авторизациейapps/ligretto-frontend/AGENTS.md🤖 Generated with Claude Code