Implementation Plan — Tamiyo Scroll Frontend Bootstrap¶
| Comment | ||
|---|---|---|
| Target | barrins-project/tamiyo_scroll |
/ |
| Initial date | 2026-07-15 | / |
| Status | ✅ Implemented | / |
| Source | Suivi Competitif MTG.dc.html handoff from CLAUDE design |
High fidelity, source of truth for FR labels, color/type tokens, calculation thresholds |
| Dependency | barrins_api — /api/v1/auth/* and /api/v1/tamiyo-scroll/* (16 endpoints, see barrins_api/docs/tamiyo_scroll_tracker/00_plan_general.md) |
Both BFFs implemented and tested backend-side — this plan only covers frontend consumption |
Objective¶
By the end of this implementation, the application must:
- Authenticate the user (login, signup, 2-step email verification) against the
real JWT backend — no fake
localStorage-style authentication, unlike the design prototype. - Reproduce the design's 3 tabs (Metagame, BO3 Tracking, My Decklist) pixel-perfectly for the current active personal deck's data.
- Never compute client-side a value the backend already exposes pre-computed (conversion, win rate, decklist coloring) — consume the API response as-is (constitution §4.1/§4.2).
- Allow read-only viewing of another user's shared data, with all editing controls disabled/hidden in that mode.
- Handle session refresh (refresh token) transparently for the user, with a clean logout on failure.
Non-goals (out of scope for this bootstrap)¶
- End-to-end tests (Playwright) — priority goes to unit/component tests (Vitest + Testing Library) for this first phase; E2E can follow separately.
- i18n (constitution §44) — a single hardcoded FR string set for now, structured so as not to block a future extraction (no string concatenation in JSX, labels centralized per screen).
- Deployment (Agent 3, out of scope — reverse proxy, TLS, static hosting).
Options analysis¶
A. Session management (tokens)¶
| Option | Mechanism | Advantages | Disadvantages |
|---|---|---|---|
| A (chosen) | access_token/refresh_token in localStorage, centralized HTTP interceptor |
Simple, consistent with the backend's OAuth2PasswordBearer behavior (no httpOnly cookies on the API side); a single source of truth for auth |
Vulnerable to XSS like any localStorage storage — accepted as consistent with the backend's existing JWT Bearer architecture (no CSRF to handle in exchange) |
| B | httpOnly cookies | More resistant to XSS | The backend doesn't expose this mode (OAuth2PasswordBearer + JSON body) — would change the auth contract shared across the whole Barrin's ecosystem, out of scope for this plan |
Choice: A. A centralized HTTP client (src/api/client.ts) intercepts 401s,
attempts a POST /auth/refresh once, replays the original request, and logs
the user out (purge + redirect to /login) if the refresh also fails.
B. Tab routing¶
| Option | Mechanism | Advantages | Disadvantages |
|---|---|---|---|
| A (chosen) | Dedicated React Router routes (/app/metagame, /app/suivi-bo3, /app/decklist) |
Shareable URL, working browser back button, consistent with react-router being present in the mandated stack |
The prototype handled the active tab as plain React state — a minor, accepted deviation, UX improvement |
| B | Local React state (activeTab) without routing |
Faithful to the prototype | react-router, mandated by the constitution, would then have no real usage in the app |
Choice: A.
C. Email verification screen (no mockup provided)¶
Flagged in docs/signup_email_verification/00_plan_general.md (on the barrins_api
side): the high-fidelity design contains no code-entry field — only
Login/Signup. This plan must fill that gap while respecting the visual tokens of
the existing design rather than improvising a different style.
Decision: reuse the login card as-is (same max-width:400px,
oklch(0.21 0.014 260) background, border, border-radius:16px, 32px padding)
with:
- title "Vérifiez votre email";
- 6-digit code field, pre-filled if
?email=&code=are present in the URL (email link) but never auto-submitted — an explicit click on "Confirmer mon compte" is required (seebarrins_apidocs, Option D: don't let a corporate link scanner consume the code before the user does); - "Renvoyer le code" button with a 60s delay (disabled + countdown, aligned with
verification_resend_cooldown_secondsimplicitly returned by the backend's generic 202 behavior — the frontend keeps its own local countdown, the backend doesn't return the remaining time); - link back to
/login.
D. Test framework¶
| Option | Mechanism | Advantages | Disadvantages |
|---|---|---|---|
| A (chosen) | Vitest + React Testing Library | Native Vite integration (no separate Babel config), API already familiar and close to Jest | — |
| B | Jest | Historical React standard | Extra configuration under Vite (ESM transform), no advantage here |
Choice: A.
Stack and tooling¶
Mandated by the constitution (§14.2) — no alternative considered:
- React 19, TypeScript (strict mode), Vite
- React Router (v7)
- TanStack Query (v5)
- Zod
- TailwindCSS
- shadcn/ui
Additional dependencies not explicitly listed (none is a subjective choice — all required to make the mandated stack work):
| Dependency | Role |
|---|---|
axios (to validate — see Open points) |
HTTP client with interceptors (token refresh) |
vitest, @testing-library/react, @testing-library/user-event |
Tests |
eslint, @typescript-eslint/*, prettier |
Code quality |
Target architecture¶
src/
├── api/
│ ├── client.ts ← fetch/axios wrapper, refresh token interceptor
│ ├── auth.ts ← login, signup, verifyEmail, resendCode, me, logout
│ ├── settings.ts ← getMySettings, updateMySettings, listSharedUsers
│ ├── personalDecks.ts ← personal deck CRUD + versions + decklist-view
│ ├── metaDecks.ts ← roster CRUD
│ ├── matches.ts ← BO3 log CRUD
│ ├── cardTests.ts ← card test feedback CRUD
│ └── stats.ts ← archetype-summary, matchup-summary
│
├── schemas/ ← Zod schemas mirroring backend Pydantic schemas
│ ├── auth.ts
│ └── tamiyoScroll.ts
│
├── hooks/ ← TanStack Query hooks (useQuery/useMutation per
│ │ resource)
│ ├── useAuth.ts (session state, login/logout, refresh)
│ ├── usePersonalDecks.ts
│ ├── useMetaDecks.ts
│ ├── useMatches.ts
│ ├── useCardTests.ts
│ └── useStats.ts
│
├── components/
│ ├── ui/ ← generated shadcn/ui primitives (Button, Input,
│ │ Table, …)
│ ├── layout/
│ │ ├── AppShell.tsx (header, personal deck selector, sharing, tabs)
│ │ └── ProtectedRoute.tsx
│ └── shared/
│ └── EditableTable.tsx (common pattern for roster/metagame/matches/card-tests)
│
├── pages/
│ ├── LoginPage.tsx
│ ├── VerifyEmailPage.tsx
│ ├── MetagameTab.tsx
│ ├── SuiviBo3Tab.tsx
│ └── DecklistTab.tsx
│
├── lib/
│ └── queryClient.ts
│
├── App.tsx ← routes
└── main.tsx
Frontend route map¶
| Route | Page | Protection |
|---|---|---|
/login |
LoginPage (login/signup toggle) |
Public |
/verify-email |
VerifyEmailPage |
Public |
/app/metagame |
MetagameTab |
Authenticated |
/app/suivi-bo3 |
SuiviBo3Tab |
Authenticated |
/app/decklist |
DecklistTab |
Authenticated |
/ |
Redirect → /app/metagame if authenticated, else /login |
— |
ProtectedRoute redirects to /login if there's no valid access_token.
Backend endpoint → frontend hook mapping¶
| Backend endpoint | Hook | Screen |
|---|---|---|
POST /auth/token, /auth/signup, /auth/signup/verify, /auth/signup/resend, /auth/refresh, /auth/logout, GET /auth/me |
useAuth |
Login, VerifyEmail, header |
GET/PATCH /tamiyo-scroll/me/settings, GET /tamiyo-scroll/shared-users |
useSettings |
Header (sharing, "Viewing: …" selector) |
GET/POST/DELETE /tamiyo-scroll/personal-decks, .../versions*, .../decklist-view |
usePersonalDecks |
Header (deck selector), Metagame (import), My Decklist |
GET/POST/PUT/DELETE /tamiyo-scroll/meta-decks |
useMetaDecks |
Metagame (roster + expected-field table) |
GET/POST/PUT/DELETE /tamiyo-scroll/matches |
useMatches |
BO3 Tracking |
GET/POST/PUT/DELETE /tamiyo-scroll/card-tests |
useCardTests |
BO3 Tracking |
GET /tamiyo-scroll/archetype-summary, /matchup-summary |
useStats |
Metagame (archetype breakdown, matchup summary) |
The owner_id parameter (shared read-only mode) is carried by a single
global state ("Viewing: {user}" selection in the header) and automatically
injected by api/client.ts on GET requests only — never on mutations,
an exact mirror of the server-side guarantee (resolve_owner only exists on GET
routes). canEdit = !viewingSharedUser derives from that same state, centralized
in useAuth/a dedicated context — no prop-drilling of a boolean recomputed on
every screen.
Phase breakdown¶
| Phase | Title | Main files | Prerequisites |
|---|---|---|---|
| 1 | Scaffold (Vite, Tailwind, shadcn, ESLint, strict tsconfig) | project root | — |
| 2 | API client + refresh interceptor + Zod schemas | src/api/, src/schemas/ |
Phase 1 |
| 3 | Auth: login/signup/verify-email + useAuth + ProtectedRoute |
src/pages/Login*.tsx, VerifyEmailPage.tsx, useAuth.ts |
Phase 2 |
| 4 | App shell (header, deck selector, sharing, tabs) | AppShell.tsx |
Phase 3 |
| 5 | EditableTable (shared component) |
components/shared/EditableTable.tsx |
Phase 1 |
| 6 | Metagame tab | MetagameTab.tsx, associated hooks |
Phases 4, 5 |
| 7 | BO3 Tracking tab | SuiviBo3Tab.tsx |
Phases 4, 5 |
| 8 | My Decklist tab | DecklistTab.tsx |
Phase 4 |
| 9 | Tests (Vitest + Testing Library) | **/*.test.tsx |
All |
Decisions settled during scoping¶
1. Native fetch, not axios¶
The HTTP client needs an interceptor for transparent token refresh (401 →
refresh → replay the original request). Native fetch is enough for this need (a
roughly thirty-line wrapper) — no extra dependency to justify, consistent
with the dependency-minimization policy (constitution §22.3) and with the
equivalent choice on the backend side (stdlib smtplib rather than an email SDK).
2. Hand-written shadcn/ui primitives, not via the CLI¶
Verified during scoping: ui.shadcn.com (the registry used by npx shadcn add)
is not reachable from this execution environment (403 on the network proxy side),
whereas registry.npmjs.org is. The required primitives (Button, Input, Select,
Table, Tabs, Card, Badge, Checkbox, Textarea, Dialog) will therefore be written
directly in src/components/ui/ following the same foundations the CLI generates
(Radix UI primitives + class-variance-authority + tailwind-merge) — this is
not a deviation from the stack choice: shadcn/ui was never a runtime dependency
but a code pattern to copy into the repo, which is what the CLI does and what
I'll do by hand.
3. Backend address in development¶
VITE_API_BASE_URL=http://localhost:8000 by default in .env.example, to adjust
depending on the environment where barrins_api actually runs.
Implementation notes¶
- Phase 5: the generic
EditableTablecomponent planned originally was abandoned along the way — the design actually contains two incompatible editing patterns (roster/metagame expected-field table: rows always editable inline; matches/tested cards: read-only rows with an explicit "Edit" toggle). Forcing them into a single generic component would have required a per-column renderer/editor config as elaborate as a mini data-grid. Each tab composes its own editing behavior on top of the sharedTableprimitives. - Renamed pages:
MetagameTab.tsx/SuiviBo3Tab.tsx/DecklistTab.tsx(rather than*Page.tsx, an initial deviation corrected to match this plan). - Bug fixed during manual smoke test (Phase 6):
SelectTriggerwasn't truncating long values (e.g. "Mon compte (email@très-long-domaine)"), which made the selector overflow and overlap neighboring header controls instead of ellipsizing —overflow-hidden+truncateadded toSelectTrigger/SelectItem. - Bug fixed during manual smoke test (Phase 7): the "My deck" selector in the
"New match" form wasn't pre-filling with the header's active deck —
the initial
useStatevalue capturedactiveDeckIdbefore the/me/settingsrequest resolved. Fixed with auseEffectthat syncs the draft onceactiveDeckIdis available, without overwriting a selection already made by the user. - End-to-end validation: each phase 6-9 was manually verified against the real
backend (
barrins_api+ PostgreSQL, no mocks) via disposable Playwright scripts in the scratchpad — signup, email verification, creation of a personal deck/opposing decks/matches/card test feedback, and confirmation that the stats (archetype, matchups, decklist coloring) recompute correctly server-side and flow through correctly to the 3 tabs without ever duplicating that computation client-side. - Automated tests (Phase 9): Vitest coverage focused on the pure logic most
at risk of silent regression — draft ↔
MatchWritemapping (in particular the "not played" sentinel ↔null), win rate/rating color thresholds,ActiveDeckContextcontext guard — plus the 8 already-existing API client tests. No Playwright E2E suite committed (out of scope for the plan); real-world usage was manually verified as described above.
Post-delivery fixes and reorganization (2026-07-16)¶
Feedback from the client after real-world app usage, all fixed/adjusted on the frontend
side (except for the two backend bugs, see barrins_api/docs/tamiyo_scroll_tracker/00_plan_general.md,
"Post-delivery fixes" section):
Decklist personnelleandCartes testéesmoved to the My Decklist tab, in that order (import first), at the top of the tab beforeDecklist couranteandHistorique des versions. Removed from the Metagame and BO3 Tracking tabs respectively.- Tested cards scoped to the active deck:
useCardTestsnow takespersonalDeckIdand passespersonal_deck_idas a query filter and in the write payload (mirroring the backend fix above); shows a deck-selection prompt when no deck is active, likePersonalDecklistImportSection. - Match log: "my deck"'s name is no longer bold/white (repeated on
every row, redundant with the header selector) — switched to non-bold
text-muted-foreground; the opponent's deck name stays bold/white (text-foreground), since it's the most useful piece of information to spot at a glance in the log. - Regression check passed: manual Playwright suite against the real backend (disposable account, decks/matches/card-tests created via the API, visual and DOM verification) — archived deck disappears from the archetype breakdown without reappearing after a reload, tested card visible only from the deck it was created in, blocks in the right order on the 2 affected tabs.