All React code lives in packages/team-leaderboard/client/ (@zooly/team-leaderboard-client). apps/face-app/ is a thin Vite wrapper that mounts the package's App. Components use inline styles (Lato font) to preserve the original Figma look, and every rendered div carries a data-testid.
packages/team-leaderboard/client/src/lib/env.ts — resolves VITE_APP_URL (backend) and VITE_AUTH_URL.packages/team-leaderboard/client/src/lib/api.ts — fetchBoard, searchLeaderboard, response types.packages/team-leaderboard/client/src/env.d.ts — Vite env typings.:3009 (apps/face-app/vite.config.ts, apps/face-app/package.json)./admin and board slugs via apps/face-app/vercel.json.Production env gotcha:
VITE_APP_URL/VITE_AUTH_URLmust include thehttps://scheme (e.g.https://dev.zooly.ai). Without it, client fetches resolve relative to the current origin and return the SPA HTML fallback instead of JSON.
packages/team-leaderboard/client/src/app/routes.tsx:
/ → redirect to /world-cup (the isDefault board) so there's always a slug./:board → that board's ranked BoardTeams (/world-cup, /nba, /nba-finals, …). The board param is normalized (lowercased, hyphenated). An unknown slug falls back to the default board / a friendly empty state./for-teams, /for-brands → lead-capture pages./admin/* → the admin area (see Admin).packages/team-leaderboard/client/src/app/components/MainApp.tsx:
GET /api/merch/leaderboard/:board (no hardcoded data — the old Figma TEAMS / NBA_TEAMS / mock screens were removed).<0.01% small-value formatting.data-testid (board-loading, team-list-empty, board-error).max-width: 408px; aspect-ratio: 1/1; search box and lists max-width: 480px) and clamp() font sizes, with 16px side padding so nothing is clipped on small screens.The search box suggests boards and BoardTeams from 3+ characters (debounced, via /search). Picking a board navigates to its slug; picking a BoardTeam navigates to that board and enters its store. The "×" clears the search and restores the current board.
MainApp.goToStore navigates to the campaign storefront URL with ?bt=<boardTeamId> (honoring the board's storeLinkTarget). A BoardTeam with no live campaign has a null target and its row is non-actionable.
On same-tab navigation a full-screen loader overlay (data-testid="navigating-overlay") is shown immediately and cleared on pageshow (so it doesn't stick when the user hits Back via bfcache).
The merch store landing (packages/merch/client/src/app/pages/campaign-landing-page.tsx) reads ?bt= and POSTs to /api/merch/leaderboard/link once the session exists. Because the store has one design + only the digital-image product, the experience skips the design (B1) and product (B2) steps and lands on selfie/upload — this is the experience's normal behavior for a single-option store (selection steps reappear automatically if a store later gains more options).
ForTeamsPage.tsx / ForBrandsPage.tsx wire their forms to the leads API. Fields: org/team name, contact person, email, phone, message; on success the submission appears in the admin Leads view.
The consent UI comes from the shared @zooly/consent package and is mounted at the client root (packages/team-leaderboard/client/src/app/App.tsx); the old cosmetic popup in MainApp.tsx was removed.
@zooly/consent — CookieConsent popup (Accept all / Reject all) + useConsent hook.@zooly/util (packages/util/src/utils/consent.ts) — consent state persisted in a cookie scoped to the registrable parent domain (.zooly.ai), so one decision is shared across all subdomains (face., dev., auth., …). Falls back to a host-only cookie on localhost. API: getConsent / hasDecidedConsent / hasAnalyticsConsent / setConsent / onConsentChange.packages/merch/client/src/lib/log-merch-event.ts early-returns when !hasAnalyticsConsent() (no journey session id, no /api/journey-event POST until consent).apps/face-app/public/favicon.svg (apps/face-app/index.html).