Веб-клиент
Стек
- Next.js 15 App Router
- TypeScript
- Tailwind CSS + shadcn/ui
- WebSocket native client
- Telegram Login Widget (optional)
Экраны
| Route | Описание | FR |
|---|---|---|
/ |
Landing, CTA create/join | FR-047 |
/join |
Ввод кода приглашения | FR-002 |
/session/[id]/lobby |
Участники, выбор игры | FR-004 |
/session/[id]/game/[gameId] |
Игровое поле | FR-048 |
/profile |
Профиль, история | FR-049 |
/auth/login |
Login / register | FR-007 |
/admin/login |
Admin OTP login | FR-058 |
/admin/games |
Список игр (логи) | FR-057 |
/admin/games/[id] |
Timeline лога игры | FR-057 |
Session flow
sequenceDiagram
participant U as User
participant W as web-app
participant API as API
participant WS as realtime
U->>W: Open /join
W->>API: POST /auth/guest or JWT
W->>API: POST /sessions/join
Note over W: сохраняет participant_id в localStorage
W->>WS: Connect + subscribe session topic
WS-->>W: session events
U->>W: Host starts game
W->>API: POST /sessions/{id}/games
Note over W: 2-й игрок: WS game_started или poll active_game_id → /game/{id}
W->>WS: subscribe game topic
Игровой экран (/session/[id]/game/[gameId])
Общее для всех плагинов:
GameNav— ссылки «В лобби» и «На главную» (всегда видны)- Заголовок и фаза по
plugin_idизGET /games/{id}/state - Кнопки/поля по
view.actions_available(сервер учитываетviewer_idиз auth)
dice_board
BoardGrid6×6 — см. ui-assets- «Бросить кубики» →
roll_dice - Клик клетки →
choose_cell
kontakt
- Фаза Association: поле + «Задать ассоциацию» →
set_association - Фаза Contacting: два игрока отправляют слово (
contact); до второго ответа слова не видны - После двух ответов слова показываются всем; совпадение → конец игры, иначе новый раунд
- Ведущий: «Открыть загаданное слово» →
reveal
shlyapa
- Объясняющий видит секретное слово и список отгадок с кнопками «Верно» / «Неверно»
- Угадывающие отправляют
guess; очки начисляет объясняющий черезjudge_guess
yatzy
- Бросок 5 костей → удержание выбранных → запись в одну из 13 категорий scorecard
roll_dice,keep_dice,score_category
koaliciya
- Кооператив: каждый видит свою руку (числа 1–100), кладёт карту кнопкой
play_card - Стол и жизни общие; порядок на столе должен возрастать
memo
- Сетка 4×4: клик по
?→flip_tile; пары остаются открытыми, промах — ход сопернику
WebSocket и синхронизация
- Подключение WS после
ensureGuest(иначе токена ещё нет) - На игровом экране — poll
GET /games/{id}/stateкаждые 3 с (fallback) - После
game.state_changed— refresh для viewer-specificactions_available - Reconnect WS восстанавливает подписки на топики
Навигация
- «В лобби» ведёт на
/session/{id}/lobby?stay=1— не редиректит обратно в активную игру - В лобби при активной игре: кнопка «Вернуться в игру» для всех участников
- Хост в лобби: «Завершить игру для всех» →
POST /sessions/{id}/games/end - Авто-переход в игру из лобби — только без
stay=1(для 2-го игрока при старте)
WebSocket
// Reconnect with exponential backoff
const ws = new WebSocket(`${WS_URL}?token=${token}`);
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'game.state_changed') setGameState(msg.state);
};
On reconnect: GET /games/{id}/state before resuming UI.
Dual presence
- Tab focus → report
control_channel: webheartbeat - Read-only banner if control on TG
Адаптив
- Mobile-first: поле масштабируется
min(90vw, 400px) - Touch targets ≥ 44px
FR/NFR
FR-047, FR-048, FR-050, FR-025, FR-028, NFR-019, NFR-020