Skip to content

Dev tazi 033#38

Merged
swoosh1337 merged 27 commits into
mainfrom
dev-tazi-033
May 26, 2026
Merged

Dev tazi 033#38
swoosh1337 merged 27 commits into
mainfrom
dev-tazi-033

Conversation

@swoosh1337

@swoosh1337 swoosh1337 commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Google Identity sign-in, new landing page, circular match countdown, animated score flights, and expanded post-match XP/progression summary.
  • Improvements
    • UI restyles (privacy/terms/results), richer timers playground, added localization strings, improved back/navigation behavior, and broader analytics instrumentation across flows.
  • Refactor
    • Streamlined play/home surfaces and game flow wiring.
  • Chores
    • Added environment template and brand-color audit/migration tooling; new match/result tests.

Review Change Stack

swoosh1337 and others added 20 commits May 24, 2026 19:09
…etry

Adds explicit events for matchmaking (started/ai_fallback/human_found/cancelled),
match lifecycle (started/completed/forfeit/disconnected/reconnected), per-question
answer telemetry from possession matches, auth/onboarding/logout, store flow
(viewed/modal_opened/cancelled/avatar_equipped), profile/settings (nickname/club/
language), daily challenge start, and in-app browser blocked. Sets PostHog person
properties on identify so cohorts work.

Co-Authored-By: swoosh1337 <noreply@anthropic.com>
- AppShell: replace Date.now() in render with 1s-tick state
- LocaleContext / useFriendLobbyLogic: defer setState in mount effects via microtask
- PossessionMatchViewport: rename hook's `ref` field to `containerRef` and
  destructure all three values to avoid Compiler's ref-during-render heuristic
- ChallengeInvitePrompt / MatchmakingMapScreen: drop useMemo wrappers that
  React Compiler couldn't preserve (deps array referenced unstable t())

Co-Authored-By: swoosh1337 <noreply@anthropic.com>
- friend_lobby_invite_sent — fires when host copies the lobby code
- friend_lobby_invite_accepted — fires when a user joins via invite code
- challenge_invite_sent — fires from the social screen challenge button
- challenge_invite_accepted / declined — fires from both the notifications
  dropdown and the floating challenge invite prompt

Co-Authored-By: swoosh1337 <noreply@anthropic.com>
Adds a popup/JWT-based Google sign-in path that bypasses the classic OAuth
redirect — the redirect endpoint returns `disallowed_useragent` 403 inside
in-app browsers (Messenger, Instagram), but the GIS endpoint isn't UA-gated.

Flow:
  1. Welcome screen loads https://accounts.google.com/gsi/client lazily
  2. User taps "Continue with Google" → GIS popup opens with a nonce-bound
     request to accounts.google.com/gsi/select
  3. GIS returns a signed JWT credential + raw nonce
  4. We POST { provider, id_token, nonce } to /auth/v1/social-login-token
  5. Backend exchanges with Supabase server-side, returns access/refresh

Falls back to the old redirect flow if GIS fails to load (CDN blocked) or
refuses to render (some webview versions detect the UA and bail). The
in-app browser overlay remains the last-resort safety net.

Requires NEXT_PUBLIC_GOOGLE_CLIENT_ID env var (Web OAuth client ID from
Google Cloud Console with the deployed origin in Authorized JavaScript
origins).

Co-Authored-By: swoosh1337 <noreply@anthropic.com>
…layout fixes

- Countdown matches now show the opponent's REAL live found-count via a new
  match:opponent_countdown_progress server event. Previously the UI rendered
  a seeded-RNG simulator that could show a count higher than the opponent's
  actual answers (you'd see 6 when they only got 4).
- For AI opponents, the AI commits answers atomically but the server now
  drip-feeds the progress events with staggered delays so the human side
  still sees a realistic counter tick up.
- Removed the InAppBrowserOverlay from auth layout so users in Messenger
  can attempt GIS sign-in instead of being blocked by a fullscreen overlay.
- Moved JSON-LD <script> from <head> to <body> + suppressHydrationWarning
  on <body> to silence the Facebook pcm.js hydration mismatch warning in
  Messenger's in-app browser.

Co-Authored-By: swoosh1337 <noreply@anthropic.com>
`audit-brand-colors.mjs` scans the codebase for raw `bg-[#hex]` /
`text-[#hex]` / `border-[#hex]` Tailwind classes and reports them
against `brand-colors-allowlist.json`. The existing
`no-hex-class-colors.test.ts` CI guard depends on these.

`migrate-brand-colors.mjs` rewrites known hex values to brand tokens;
`rename-tokens.mjs` handles bulk token renames. Both are dev-only
utilities, not part of the runtime bundle.
Nine daily-game screens were using a hardcoded hex equivalent of our
`surface-deep` token. Swaps them to the design-system token so the
`no-hex-class-colors` regression guard stays green without needing
new allowlist entries.
Anonymous visitors used to land on / and immediately get redirected to
/auth/welcome, which flashed the URL and split SEO between two paths.
Now / is a public WelcomeScreen wrapped in PublicOnlyGate (logged-in
users get bounced to /play), and /auth/welcome 301-redirects to / so
existing links and the sitemap entry don't break.

Updates every internal reference to /auth/welcome:
- AppAuthGate redirects anonymous users to / instead of /auth/welcome
- AppShell logout, OAuthCallback fallback, forgot-password back button
- sitemap drops the /auth/welcome entry (was 0.9 priority duplicate)
- removes the dead sticky Play button in AppShell that briefly flashed
  during navigation because it was gated on currentPath === "/"
Methodical sweep — every entry had zero non-test consumers in src/:

- entire src/features/home/ tree (HomeScreen, HomeDashboard, all
  HomeRightRail/HomeQuickStatsRow/HomeFeaturedChallenge/etc.)
- training-offer modal + the commented-out training-gate logic that
  referenced it from play/page.tsx
- orphan components: Timer, HelpButtons, LanguageSelector, both bottom
  sheets, InlineAvatar, AnswerCard, QuestionArena, CategoryDraftPanel,
  ResultsShared, WalletSection, RewardQuestCard, ShotOverlay,
  DevToolbar, IntroScreen, PregameOverlay, PenaltyTransition
- unused hooks/utils: features/play/hooks/useDraftLogic.ts,
  utils/gameHelpers.ts, utils/rankSystem.ts
- unused barrels: src/features/daily/index.ts (consumers all import
  per-file), src/lib/sounds/index.ts (consumers go file-by-file)
`MatchCountdownPuck` consolidates the cyan-border + brand-blue-fill
countdown puck used by the kickoff overlay, party-quiz start, and
reconnect resume. Three sizes (sm/md/lg), optional yellow progress
bar via `durationMs`. Replaces three near-identical implementations.

`KickoffCountdownOverlay` now delegates the center column to the
shared puck so future tweaks land in one file.

`/dev/timers` is rebuilt as a labeled gallery grouped by mode (Ranked,
Party Quiz, Daily, Shared) covering all 8 timer surfaces: kickoff,
MCQ pill, party MCQ pill, penalty HUD, shot HUD, daily countdown,
reconnect grace, forfeit, and a "match complete / calculating" preview.
Plus a section that renders the shared puck at all 3 sizes for
side-by-side visual comparison.
Results screen
- adds personal stats row (Rank / Correct / Points) + XP footer with
  level-up + xp-to-next, mirroring the ranked results layout
- bumps podium rank pill from h-6 to h-10 and name/score sizes so they
  read at a glance in Georgian (the visible bug — small 1/2 chips)
- pending "Returning to lobby…" full-screen LoadingScreen when Play
  Again is tapped, so the user gets feedback while the server emits
  fresh lobby state

In-match
- first-question intro: round transitions only fire after a round
  resolves, so question 1 used to skip the overlay. New
  `firstQuestionIntroVisible` one-shot triggers on qIndex===0
- finalizing-results modal: royal-blue Figma style (was border-card
  with dim font-fun)
- forfeit-pending banner + pause overlay: royal-blue Figma style
- match-start countdown puck now uses the shared MatchCountdownPuck
- RoundTransitionOverlay category label: text-[11px] text-white/55 →
  text-[16-20px] text-brand-yellow font-bold for legibility

i18n
- new partyResults.statRank / statCorrect / statPoints + returningToLobby
- new results.levelUp for the "Level up! Now Lvl N" line
- ka.partyQuizFinished: "Party quiz" → "ფარტი ქვიზი" for consistency
Both in-match overlays moved from `border-surface-deep` rounded-2xl
cards with `font-fun text-white/60` text to solid `bg-brand-blue
rounded-[20px]` Poppins SemiBold cards, matching the QuitMatchModal /
Play With Friend / Match Paused / Calculating Final Scores family.

Reads as one consistent modal language across the app.
…a buttons

Ready button used to wait for the server `lobby:state` echo before
flipping; on flaky networks it felt unresponsive. Adds
`optimisticReady` in useFriendLobbyLogic so the click immediately
reflects the intended value and clears once the server confirms.

Non-host members previously saw no UI between "host tapped Start" and
the match-start countdown. Now the "preparing match" banner reads
from `matchIsStarting = isStartingMatch || lobby.status === "active"`
so every member lights up (backend now broadcasts the status flip).

Visual:
- start match: now `bg-brand-green` (solid Figma green) instead of
  `bg-brand-blue`, hover → `brand-green-deep`
- leave lobby: now fully filled `bg-brand-red` (Figma node 1155-3037
  reference) instead of outlined
- ready checkmark: size-5 → size-7 with shrink-0 and `min-h-14`
  wrapper so long Georgian copy ("მზადაა (დააჭირე გასაუქმებლად)")
  wraps cleanly without stranding the icon
- all buttons get `active:scale-[0.98]` press feedback
FriendPlayModal "Room Code | Join" used a rigid 3-col grid (2/3 input,
1/3 button). Georgian "შემოერთება" overflowed the 1/3 column. Switches
to flex so the button auto-sizes to text content with shrink-0 +
whitespace-nowrap, while the input takes the remaining space via flex-1.

ModeSelectionScreen mobile ranked card title had `whitespace-nowrap`
which forced "რეიტინგული მატჩი" to overflow into the RP score on the
right. Drops nowrap, adds `leading-[1.05] break-words [hyphens:auto]`
so it wraps cleanly on two lines.
…on phase

Backend now sends `correctIndex` on MatchQuestionPayload (was already
in the cache, just unsurfaced). Store merges it into the per-question
slot, useRealtimeGameLogic prefers it over the older sources, and
PossessionQuestionPanel only switches a non-selected option to the
"reveal-correct" style when `phase === 'reveal'`.

Without the phase gate, if `correctIndex` arrived before the user
answered (which now happens reliably for the picker), the correct
option would visually highlight as soon as the user picked a wrong
one — leaking the answer mid-round.
Two CR findings on realtimeMatch.store.ts:

setFinalResults — `trackMatchCompleted` could fire for a stale matchId
when a final-results payload arrived after the store had moved on to a
new match (e.g. on rejoin). Now gates the analytics call on
`state.match?.matchId === payload.matchId`.

clearMatchPaused — the previous downtime calculation was algebraically
`Date.now() - (state.pauseUntil - remainingMs)` which simplifies to
0 in every case, so reconnect telemetry was always reporting 0s pauses.
Adds `pausedAt: number | null` to the state, stamps it in
setMatchPaused, and computes downtime as `now - pausedAt`. Also clears
pausedAt in all reset paths.
…strings

CR flagged hardcoded English in RealtimePartyQuizScreen for the
in-match standings label, the "Live scores" caption, and the new
"Match Complete / Calculating final scores…" finalizing overlay.

Adds partyResults.standings / liveScores / matchComplete /
calculatingFinalScores keys (en + ka) and wires them through
useLocale().t(...) so Georgian players don't see English fragments
inside an otherwise localized screen.
@vercel

vercel Bot commented May 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
quizball-web Ready Ready Preview, Comment May 26, 2026 1:27pm

Request Review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds GIS social-login and server token endpoint, broad analytics and PostHog hooks, realtime opponent countdown events and store fields, party-quiz score-flight animations and results XP UI, possession/countdown UI refactors, color-audit/migration CLIs, landing/routing/layout edits, translations, and tests.

Changes

Unified application changes

Layer / File(s) Summary
Auth, GIS and analytics surface
src/lib/auth/*, src/lib/posthog.ts, src/lib/analytics/game-events.ts, src/components/auth/WelcomeScreen.tsx
Adds Google Identity Services helpers and GIS sign-in flow, new socialLoginWithIdToken endpoint usage, PostHog setPersonProperties, and a large expansion/reorganization of analytics tracker functions and signatures.
Realtime types, socket handlers, and store
src/lib/realtime/socket.types.ts, src/lib/realtime/socket-handlers.ts, src/stores/realtimeMatch.store.ts, src/stores/__tests__/*
Introduces MatchOpponentCountdownProgressPayload, registers match:opponent_countdown_progress handler, adds store fields/setter for opponent countdown and pausedAt, enforces monotonic updates and countdown defaults, and adds tests for party-quiz replay hydration.
Party quiz score flights and results XP
src/features/party/RealtimePartyQuizScreen.tsx, src/features/party/PartyQuizResultsScreen.tsx, src/app/(fullscreen)/dev/party-quiz/page.tsx
Implements DOM-anchored animated score flights, displayed/held totals model, MatchCountdownPuck integration, results XP/progression computation and footer UI, and dev simulation controls/timers.
Possession overlays, flights, and countdowns
src/features/possession/components/*, src/features/possession/hooks/*
Adds DOM-anchor retry/deduping and synthetic rect fallbacks, scroll-pinning transform for flight overlays, replaces local opponent-count simulation with store-driven progress, and swaps kickoff/countdown UIs to MatchCountdownPuck.
Color audit and migration tooling
scripts/audit-brand-colors.mjs, scripts/migrate-brand-colors.mjs, scripts/brand-colors-allowlist.json, scripts/rename-tokens.mjs
Adds CLI to audit inline hex Tailwind classes with an allowlist, a migration script to map known hexes to design tokens (dry-run / --write), and a token-rename utility.
Landing, routing, UI restyles and component removals
src/app/page.tsx, src/app/(auth)/*, src/app/layout.tsx, many src/components/* and src/features/*
Adds landing page metadata, moves JSON-LD into body, adjusts redirects/sitemap and back-button targets, restyles privacy/terms/timers, adds MatchCountdownPuck, and removes or refactors many home/dashboard/daily/features/components.
Dev pages, timers and misc edits
src/app/(fullscreen)/dev/timers/page.tsx, src/components/shared/MatchCountdownPuck.tsx, src/messages/*, tests
Expands dev timers playground, adds MatchCountdownPuck component and translations, and includes targeted tests for realtime match hydration.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant NextApp
  participant RealtimeStore
  participant Socket
  participant Analytics
  Client->>NextApp: GIS sign-in (signInWithGoogleIdentity)
  NextApp->>NextApp: POST /api/v1/auth/social-login-token (socialLoginWithIdToken)
  NextApp->>Analytics: setPersonProperties / trackLoginCompleted
  Socket-->>RealtimeStore: 'match:opponent_countdown_progress' events
  RealtimeStore->>Client: opponentCountdownFoundCount -> MatchCountdownPuck / score flights
  Client->>Analytics: trackMatchStarted / trackMatchResultsViewed
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Quizball-trivia/web#12 — Potential conflict: both PRs change/remove InlineAvatar usage/implementation.
  • Quizball-trivia/web#31 — Related: overlaps on realtime match lifecycle and store behavior changes.
  • Quizball-trivia/web#34 — Related: dev Party Quiz simulation and match-driving logic touch the same dev page and behavior.

Poem

"I'm a rabbit on the keys, I hop and gently cheer,
I wired Google sign-in and made the analytics clear.
Colors found new tokens, flights of points take wing,
Sockets hum the countdown, and the countdown pucks now sing. 🐇"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev-tazi-033

The hand-rolled useEffect-based intro had two bugs that produced the
weird sequence you saw:

1. It started with `firstQuestionIntroVisible = false` and only flipped
   true via a useEffect when qIndex === 0 arrived. That left a brief
   render frame where the question + options were already in DOM before
   the overlay mounted — hence the split-second flash.

2. It only gated the overlay opacity; the underlying question-phase
   timer in useRealtimeGameLogic still advanced normally, so even with
   the overlay on top the panel was animating into its "playing" phase
   underneath.

Switches to `usePossessionFirstQuestionIntro` (the hook the ranked /
possession match already uses successfully) and passes its value as
`blockReveal` to useRealtimeGameLogic. The hook defaults to true on
mount, waits for the kickoff countdown to finish, then runs for
FIRST_QUESTION_INTRO_MS (2s) — and blockReveal stops the question
timer from advancing during that window, so there's nothing to flash.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/app/(fullscreen)/dev/party-quiz/page.tsx (1)

280-307: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove explicit any from the dev socket + mock auth user

src/app/(fullscreen)/dev/party-quiz/page.tsx has explicit any despite strict: true in tsconfig.json:

  • Stub socket: lines 281–282 (const stub: any = { ... } + eslint disable)
  • Mock auth user: lines 382–383 (} as any + eslint disable)

Type the stub/socket and user shape explicitly (or use unknown + guards) and drop the no-explicit-any disables so the file stays strict-TS clean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(fullscreen)/dev/party-quiz/page.tsx around lines 280 - 307, The
stub socket currently uses an explicit any and disables the rule; update
createStubSocket to return a properly typed Socket-like interface (or a named
DevStubSocket type) that declares id, connected, active, auth, emit, on, once,
off, removeListener, removeAllListeners, connect, disconnect, listenersAny, and
listeners with proper parameter and return types (use the onMatchAnswer callback
type for the emit payload) and remove the eslint-disable comment; do the same
for the mock auth user by declaring an explicit AuthUser (or DevAuthUser) shape
and casting the mock object to that type (or validate via unknown + type guards)
instead of using "as any" so the file remains strict-TS clean.
src/features/game/components/MatchmakingMapScreen.tsx (1)

1267-1293: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Memoize opponentPin to prevent repeated zoom/logging churn

opponentPin is recreated on every render via the IIFE, and it’s used in the “Zoom to opponent when found” effect and the “Matchmaking opponent location resolved” effect (both depend on opponentPin), plus mapPlayersuseMemo. That can retrigger animations/work when unrelated state changes.

Suggested fix
-  const opponentPin: FakePlayer | null = (() => {
+  const opponentPin = useMemo<FakePlayer | null>(() => {
     if (!rankedFoundOpponent) return null;
     const resolved = resolveOpponentLocation(rankedFoundOpponent, t("matchmaking.unknownCity"), t("matchmaking.unknownCountry"));
     const [oppX, oppY] = projectPoint(resolved.lon, resolved.lat);
     return {
       id:
         10000 +
         (hashString(
           rankedFoundOpponent.id ?? rankedFoundOpponent.username ?? "opponent",
         ) %
           1000),
       lon: resolved.lon,
       lat: resolved.lat,
       x: oppX,
       y: oppY,
       color: "`#FF4B4B`",
       avatarUrl:
         rankedFoundOpponent.avatarUrl ?? getAvatarAsset("blue"),
       avatarCustomization: rankedFoundOpponent.avatarCustomization ?? null,
       name: rankedFoundOpponent.username || t("matchmaking.opponentFallback"),
       flag: resolved.flag,
       city: resolved.city,
       country: resolved.country,
       delay: 0,
       source: resolved.source ?? "resolved",
     };
-  })();
+  }, [rankedFoundOpponent, t]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/game/components/MatchmakingMapScreen.tsx` around lines 1267 -
1293, opponentPin is recreated every render via the IIFE which causes the "Zoom
to opponent when found" and "Matchmaking opponent location resolved" effects
(and mapPlayers useMemo) to retrigger; wrap that creation in a useMemo to
stabilize the object. Specifically, replace the IIFE that defines opponentPin
with a React.useMemo that returns null if rankedFoundOpponent is falsy,
otherwise runs resolveOpponentLocation and projectPoint and builds the same
object; include all inputs used to compute the pin in the dependency array
(rankedFoundOpponent, t,
resolveOpponentLocation/projectPoint/getAvatarAsset/hashString references if
they can change) so the pin only updates when relevant data changes.
src/features/party/RealtimePartyQuizScreen.tsx (1)

833-858: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Align game UI classes with tokenized game-style guidelines.

These changed overlays use arbitrary pixel radii (rounded-[20px], rounded-[28px]) and font-poppins in core game UI. The repo guidelines require rounded-2xl/rounded-3xl, chunky 3D border treatment, and font-fun for game typography.

As per coding guidelines, “Use font-fun class for game typography”, “Use chunky 3D borders pattern with border-b-4 border-surface-card-deep on cards, buttons, and badges”, and “Use rounded corners of rounded-2xl or rounded-3xl for game elements”.

Also applies to: 1028-1037

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/party/RealtimePartyQuizScreen.tsx` around lines 833 - 858, The
overlay components (the forfeit/finalizing overlay and the pause overlay
rendered in the motion.div blocks around forfeitPending/forfeitPendingTitle and
the one keyed "party-pause" using state.matchPaused and pauseSeconds) use
arbitrary radii and font classes; update them to follow game-style tokens:
replace rounded-[20px]/rounded-[28px] with rounded-2xl or rounded-3xl as
appropriate, swap font-poppins for font-fun on the text container, and add the
chunky 3D border classes (border-b-4 border-surface-card-deep) to the overlay
container divs so they match the repo guidelines.
🧹 Nitpick comments (10)
src/app/(fullscreen)/dev/timers/page.tsx (1)

186-222: 🏗️ Heavy lift

Align newly added timer UI with the game design-system classes.

New blocks/buttons are using font-poppins, rounded-[...]/rounded-xl, and non-chunky border patterns. Please switch these to font-fun, rounded-2xl|rounded-3xl, and border-b-4 border-surface-card-deep where applicable to match the required game UI primitives.

As per coding guidelines, "**/*.{ts,tsx}: Use font-fun class for game typography (Nunito font family)", "Use chunky 3D borders pattern with border-b-4 border-surface-card-deep on cards, buttons, and badges", and "Use rounded corners of rounded-2xlorrounded-3xl for game elements."

Also applies to: 257-317, 511-515, 574-575

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(fullscreen)/dev/timers/page.tsx around lines 186 - 222, Replace the
game UI utility classes in the new timer components to match the design-system:
in the Pause preview block and FinalizingResultsPreview component (look for
PreviewFrame usage, the motion.div with key={`finalizing-${runId}`}, TimerMeta
and any elements using useRemainingMs), swap font-poppins -> font-fun, change
any rounded-[...] or rounded-xl to rounded-2xl or rounded-3xl as appropriate,
and add the chunky 3D border classes (border-b-4 border-surface-card-deep) to
card/button/badge containers (e.g., the root motion.divs and status badges).
Apply the same substitutions in the other affected ranges mentioned (around
lines 257-317, 511-515, 574-575) so all timer UI uses font-fun, rounded-2xl/3xl,
and border-b-4 border-surface-card-deep consistently.
src/components/game/RoundTransitionOverlay.tsx (1)

116-116: ⚡ Quick win

Use font-fun for this game overlay label.

The updated category label switched to font-poppins, which conflicts with the typography rule for game UI text.

♻️ Suggested class update
- className="font-poppins text-[16px] sm:text-[18px] md:text-[20px] font-bold uppercase tracking-[0.22em] text-brand-yellow mb-4"
+ className="font-fun text-[16px] sm:text-[18px] md:text-[20px] font-bold uppercase tracking-[0.22em] text-brand-yellow mb-4"

As per coding guidelines, "Use font-fun class for game typography (Nunito font family)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/game/RoundTransitionOverlay.tsx` at line 116, The category
label in the RoundTransitionOverlay component is using "font-poppins" instead of
the required game typography; update the className on the label inside the
RoundTransitionOverlay component (the element with className="font-poppins
text-[16px] sm:text-[18px] md:text-[20px] font-bold uppercase tracking-[0.22em]
text-brand-yellow mb-4") to use "font-fun" in place of "font-poppins" so the
overlay follows the Nunito-based game font rule.
src/components/auth/PrivacyPolicyScreen.tsx (1)

84-84: ⚡ Quick win

Apply the required chunky 3D border pattern to the updated button/card.

The updated button and primary content card omit the required border treatment for interactive/game UI surfaces.

♻️ Suggested class updates
- className="gap-2 rounded-full bg-white/10 px-4 font-poppins text-sm font-semibold text-white hover:bg-white/20 hover:text-white"
+ className="gap-2 rounded-full border border-surface-card-deep border-b-4 bg-white/10 px-4 font-poppins text-sm font-semibold text-white hover:bg-white/20 hover:text-white"

- <div className="rounded-[24px] bg-surface-card/40 backdrop-blur-sm p-6 md:p-10">
+ <div className="rounded-[24px] border border-surface-card-deep border-b-4 bg-surface-card/40 backdrop-blur-sm p-6 md:p-10">

As per coding guidelines, "Use chunky 3D borders pattern with border-b-4 border-surface-card-deep on cards, buttons, and badges".

Also applies to: 101-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/auth/PrivacyPolicyScreen.tsx` at line 84, The button and
primary content card in the PrivacyPolicyScreen component are missing the
required chunky 3D border; update the relevant JSX elements in
PrivacyPolicyScreen (the element using className="gap-2 rounded-full bg-white/10
px-4 font-poppins text-sm font-semibold text-white hover:bg-white/20
hover:text-white" and the main content card element) to include the classes
border-b-4 and border-surface-card-deep so they follow the "chunky 3D borders"
pattern for cards, buttons, and badges.
src/components/auth/WelcomeScreen.tsx (1)

933-936: ⚡ Quick win

Align the “Browse all categories” CTA with the shared game button pattern.

This updated CTA should use the required chunky border treatment and guideline corner token.

♻️ Suggested class update
- className="h-14 min-w-[280px] rounded-[20px] bg-brand-green px-10 font-poppins text-base font-semibold uppercase tracking-wide text-white shadow-none transition-colors hover:bg-brand-green/90 hover:shadow-none"
+ className="h-14 min-w-[280px] rounded-2xl border border-surface-card-deep border-b-4 bg-brand-green px-10 font-poppins text-base font-semibold uppercase tracking-wide text-white shadow-none transition-colors hover:bg-brand-green/90 hover:shadow-none"

As per coding guidelines, "Use chunky 3D borders pattern with border-b-4 border-surface-card-deep on cards, buttons, and badges" and "Use rounded corners of rounded-2xl or rounded-3xl for game elements".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/auth/WelcomeScreen.tsx` around lines 933 - 936, Update the
Browse all categories CTA Button in WelcomeScreen.tsx (the Button that calls
setCategoriesOpen(true)) to use the chunky 3D border and guideline corner token:
add the classes border-b-4 and border-surface-card-deep and replace the current
rounded-[20px] with the specified token (rounded-2xl or rounded-3xl) while
keeping existing sizing and color classes; ensure the className on that Button
includes border-b-4 border-surface-card-deep and rounded-2xl (or rounded-3xl) so
it matches the shared game button pattern.
src/components/auth/TermsOfServiceScreen.tsx (1)

68-68: ⚡ Quick win

Use the required 3D border treatment on the updated terms button/card.

Both newly styled surfaces currently miss the project’s card/button border pattern.

♻️ Suggested class updates
- className="gap-2 rounded-full bg-white/10 px-4 font-poppins text-sm font-semibold text-white hover:bg-white/20 hover:text-white"
+ className="gap-2 rounded-full border border-surface-card-deep border-b-4 bg-white/10 px-4 font-poppins text-sm font-semibold text-white hover:bg-white/20 hover:text-white"

- <div className="rounded-[24px] bg-surface-card/40 backdrop-blur-sm p-6 md:p-10">
+ <div className="rounded-[24px] border border-surface-card-deep border-b-4 bg-surface-card/40 backdrop-blur-sm p-6 md:p-10">

As per coding guidelines, "Use chunky 3D borders pattern with border-b-4 border-surface-card-deep on cards, buttons, and badges".

Also applies to: 85-85

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/auth/TermsOfServiceScreen.tsx` at line 68, The button/card in
TermsOfServiceScreen.tsx using className "gap-2 rounded-full bg-white/10 px-4
font-poppins text-sm font-semibold text-white hover:bg-white/20
hover:text-white" is missing the project's chunky 3D border; update the
element(s) in TermsOfServiceScreen (both the instance at the shown diff and the
other occurrence around line 85) to include the 3D border utility classes by
adding "border-b-4 border-surface-card-deep" to their className strings so the
button/card follows the card/button/badge border pattern.
src/components/shared/FriendPlayModal.tsx (1)

155-156: ⚡ Quick win

Add the required chunky 3D border tokens to the Join button.

The updated button classes keep sizing behavior, but the required button border pattern is missing.

Suggested patch
-            "shrink-0 h-14 rounded-2xl px-5 text-base font-black uppercase tracking-wide text-white",
+            "shrink-0 h-14 rounded-2xl border-b-4 border-surface-card-deep px-5 text-base font-black uppercase tracking-wide text-white",

As per coding guidelines, "Use chunky 3D borders pattern with border-b-4 border-surface-card-deep on cards, buttons, and badges".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/shared/FriendPlayModal.tsx` around lines 155 - 156, The Join
button is missing the chunky 3D border tokens; update the class list used for
the Join button in FriendPlayModal (the class array containing "shrink-0 h-14
rounded-2xl px-5 text-base font-black uppercase tracking-wide text-white" and
the responsive "transition-all md:h-16 md:text-lg md:px-7 whitespace-nowrap") to
include the required border tokens (e.g., add "border-b-4
border-surface-card-deep") so the button keeps its sizing and responsive classes
but also applies the chunky 3D border pattern.
src/features/friend/components/FriendLobbyScreen.tsx (1)

120-120: ⚡ Quick win

Use standard rounded tokens on the updated action controls.

The edited controls still use arbitrary rounded values (rounded-[20px], rounded-[14px]). Please switch these to rounded-2xl/rounded-3xl to match the shared game UI convention.

As per coding guidelines, "Use rounded corners of rounded-2xl or rounded-3xl for game elements".

Also applies to: 149-149, 174-174, 188-188

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/friend/components/FriendLobbyScreen.tsx` at line 120, In
FriendLobbyScreen component replace any arbitrary Tailwind rounded classes
(e.g., rounded-[20px], rounded-[14px]) used on action control className strings
with the standard tokens: use rounded-3xl for the larger controls and
rounded-2xl for smaller ones so they match the shared game UI convention; update
every occurrence (the one shown and the other instances) in the component’s JSX
className values accordingly (search for rounded-[20px] and rounded-[14px] in
FriendLobbyScreen and swap to rounded-3xl/rounded-2xl).
src/features/party/PartyQuizResultsScreen.tsx (1)

182-203: ⚡ Quick win

Align new game UI elements with required design tokens/patterns.

The new rank pill/badge/buttons use custom corner radii (rounded-[12px], rounded-[14px], rounded-[16px]) and omit the required chunky 3D border treatment for buttons/badges. Please switch these changed elements to approved rounded tokens (rounded-2xl/rounded-3xl) and apply border-b-4 border-surface-card-deep where applicable.

As per coding guidelines, "Use chunky 3D borders pattern with border-b-4 border-surface-card-deep on cards, buttons, and badges" and "Use rounded corners of rounded-2xl or rounded-3xl for game elements".

Also applies to: 477-486

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/party/PartyQuizResultsScreen.tsx` around lines 182 - 203,
Replace the custom arbitrary radii and missing 3D border on the rank pill and
related badges with the approved tokens: change classes using rounded-[12px],
rounded-[14px], rounded-[16px] to rounded-2xl or rounded-3xl (use rounded-2xl
for smaller pills, rounded-3xl for larger/button-like elements) and add the
chunky 3D border classes border-b-4 border-surface-card-deep where applicable;
specifically update the span that renders standing.rank (the rank pill) and the
self indicator span (the rounded-full bg-brand-orange badge) to use the new
rounded-* tokens and include border-b-4 border-surface-card-deep so they follow
the design token/pattern.
src/lib/auth/google-identity.ts (1)

76-83: 💤 Low value

Consider adding a guard for crypto.subtle availability.

crypto.subtle requires a secure context (HTTPS or localhost). While modern browsers support it, if called in an insecure context (e.g., HTTP during development), this will throw. The error would bubble up to the caller, which may be acceptable, but documenting or guarding against this edge case would improve robustness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/auth/google-identity.ts` around lines 76 - 83, In generateNoncePair
add a runtime guard for crypto.subtle (e.g., check globalThis.crypto &&
globalThis.crypto.subtle) and if missing either throw a clear, descriptive error
or use an explicit fallback (such as Node's webcrypto) before calling
crypto.subtle.digest; update the function to detect the environment and fail
fast with a helpful message like "crypto.subtle unavailable: require secure
context or provide webcrypto fallback" so callers get actionable feedback
instead of an unexpected exception.
src/lib/posthog.ts (1)

71-98: ⚡ Quick win

Prefer posthog.setPersonProperties() over posthog.capture('$set', ...)

In src/lib/posthog.ts (setPersonProperties), use posthog.setPersonProperties(set, setOnce) when either argument is provided—the SDK has a dedicated API for $set / $set_once person property updates, making the intent more explicit than emitting a $set capture event.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/posthog.ts` around lines 71 - 98, The setPersonProperties function
currently builds a payload and calls posthog.capture('$set', payload); replace
that capture call with the SDK's dedicated method by invoking
posthog.setPersonProperties(set, setOnce) (when either set or setOnce is
provided) so person properties use the explicit API; keep the existing
environment checks and try/catch/error logging around the call and remove the
posthog.capture('$set', ...) usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/migrate-brand-colors.mjs`:
- Around line 63-65: The results array is only receiving entries when a file had
replacements (changed or mapped > 0), which omits files that only had unmapped
hex usages (skipped) and undercounts the "Hex usages left as-is" total; update
the conditional around results.push in the migration loop (where variables
changed, mapped, skipped and results are used) to include files with skipped > 0
(e.g., change if (changed || mapped > 0) to if (changed || mapped > 0 || skipped
> 0)) so every file with unmapped hits is recorded; also review the aggregation
logic that computes the totals (the block referencing results later) to ensure
it sums skipped values from results rather than relying on presence of mapped
entries.

In `@src/app/`(app)/profile/page.tsx:
- Around line 69-70: Tracking calls (trackNicknameChanged()) inside the same try
that awaits updateMe(...) can throw and trigger the outer catch, turning a
successful update into an error UX; wrap the analytics call in its own try/catch
(or move it after the mutation and guard with try { await
trackNicknameChanged(); } catch { /* swallow/log locally */ }) so that
toast.success(t("profile.nicknameUpdated")) and the successful update are not
rolled back by tracking errors; apply the same pattern to the other occurrence
around line 114-115.

In `@src/app/`(fullscreen)/dev/timers/page.tsx:
- Around line 53-55: The description text contradicts the preset's behavior: the
object with durationMs: 8_000 (and source 'RealtimePossessionMatchScreen /
RealtimePartyQuizScreen forfeitPending block') currently says "No timer" but
renders countdown metadata; update the description field to accurately reflect
an 8s countdown (e.g., change "No timer — shown while server confirms forfeit
result." to something like "Static finalizing-results modal — 8s countdown shown
while server confirms forfeit result.") so QA/devs are not misled.

In `@src/features/friend/components/CreateJoinPanel.tsx`:
- Around line 71-78: Currently trackFriendInviteAccepted(code) is called before
emitting getSocket().emit("lobby:join_by_code"), which can record “accepted”
even if join fails; move the trackFriendInviteAccepted call into the
confirmed-join handler instead (the socket response/listener that establishes
lobby membership, e.g., where you handle the successful join event or update
membership state). Specifically, remove the try/catch around
trackFriendInviteAccepted from CreateJoinPanel and invoke
trackFriendInviteAccepted(code) inside the success callback/handler for the
"lobby:join_by_code" flow (the code path that runs when the server confirms
join/membership), keeping existing error handling and the
toast.info(t("friend.joiningCode", { code })) placement as appropriate.
- Line 71: The code uses inviteCode.toUpperCase() which permits leading/trailing
whitespace; change it to trim before uppercasing (e.g. use
inviteCode?.trim().toUpperCase() or (inviteCode || '').trim().toUpperCase()) so
the CreateJoinPanel logic and the const code variable produce a normalized code
without surrounding spaces.

In `@src/features/friend/hooks/useFriendLobbyLogic.ts`:
- Around line 350-355: Remove the state mutation inside the reconciliation
useEffect in useFriendLobbyLogic: delete the useEffect body that calls
setOptimisticReady(null) when me?.isReady === optimisticReady and instead
compute a derived value for the effective optimistic-ready flag (e.g.,
derivedOptimisticReady = optimisticReady !== null ? optimisticReady :
me?.isReady) and return/use that variable; this keeps optimisticReady as the
optimistic override state without setting state in an effect and avoids the
extra render and the react-hooks/set-state-in-effect warning.

In `@src/features/party/RealtimePartyQuizScreen.tsx`:
- Around line 394-397: When matchId changes the effect currently resets
displayedTotalsByUserId and pendingDisplayedTotalsRef but leaves other tracking
refs that must also be cleared; update the useEffect that runs on
partyState?.matchId to also reset/clear spawnedFlightKeysRef,
liveDeltaShownKeysRef, liveFlightShownKeysRef and the previous totals/answered
refs (previousTotalsRef, previousAnsweredRef) so animations and delta
calculations restart correctly when qIndex resets; locate the useEffect with
setDisplayedTotalsByUserId and pendingDisplayedTotalsRef and add appropriate
clear/reset calls for spawnedFlightKeysRef, liveDeltaShownKeysRef,
liveFlightShownKeysRef, previousTotalsRef and previousAnsweredRef.

In `@src/features/possession/components/BarBattleFlightOverlay.tsx`:
- Around line 78-99: The hook useScrollPinTransform creates a window scroll
listener per sprite which causes many duplicate listeners; instead move the
single scroll listener to the overlay/root component (BarBattleFlightOverlay) so
it publishes the shared CSS vars (--scroll-pin-x, --scroll-pin-y) once for all
sprites, remove per-sprite useScrollPinTransform usage (including the other
occurrences around the mentioned ranges), and have sprite elements read those
CSS vars; ensure the listener is added with { passive: true }, stores
initialX/initialY at mount, updates the two CSS custom properties on the root
node, and is cleaned up on unmount.

In `@src/features/possession/RealtimePossessionMatchScreen.tsx`:
- Around line 227-236: Replace ad-hoc typography and radius on the pause/forfeit
overlay card with the game tokens: change instances of "font-poppins" to
"font-fun", replace "rounded-[20px]" with the standardized rounded class (use
"rounded-2xl" or "rounded-3xl" depending on surrounding components), and add the
chunky 3D border pattern "border-b-4 border-surface-card-deep" to the card
container; apply the same changes to the other overlay elements referenced in
this component (the nodes rendering forfeitPendingTitle and
forfeitPending.message and the other overlay block around lines 258-274) so all
game UI overlays use font-fun, the standard rounded-xl token, and the border-b-4
border-surface-card-deep pattern.

In `@src/stores/realtimeMatch.store.ts`:
- Around line 676-694: The code reads id/questionId, difficulty and category
from the wrong object (the currentQuestion root) before calling
trackAnswerSubmitted; instead, extract these metadata fields from
state.match.currentQuestion.question (use currentQuestion.question.questionId or
.id, .difficulty, and .categoryName or .category) and pass those values to
trackAnswerSubmitted (fall back to the payload matchId:qIndex only if question
id is truly missing). Update the references around the q variable and the
trackAnswerSubmitted call to use q.question for id/questionId, difficulty, and
categoryName/category to ensure accurate analytics metadata.

---

Outside diff comments:
In `@src/app/`(fullscreen)/dev/party-quiz/page.tsx:
- Around line 280-307: The stub socket currently uses an explicit any and
disables the rule; update createStubSocket to return a properly typed
Socket-like interface (or a named DevStubSocket type) that declares id,
connected, active, auth, emit, on, once, off, removeListener,
removeAllListeners, connect, disconnect, listenersAny, and listeners with proper
parameter and return types (use the onMatchAnswer callback type for the emit
payload) and remove the eslint-disable comment; do the same for the mock auth
user by declaring an explicit AuthUser (or DevAuthUser) shape and casting the
mock object to that type (or validate via unknown + type guards) instead of
using "as any" so the file remains strict-TS clean.

In `@src/features/game/components/MatchmakingMapScreen.tsx`:
- Around line 1267-1293: opponentPin is recreated every render via the IIFE
which causes the "Zoom to opponent when found" and "Matchmaking opponent
location resolved" effects (and mapPlayers useMemo) to retrigger; wrap that
creation in a useMemo to stabilize the object. Specifically, replace the IIFE
that defines opponentPin with a React.useMemo that returns null if
rankedFoundOpponent is falsy, otherwise runs resolveOpponentLocation and
projectPoint and builds the same object; include all inputs used to compute the
pin in the dependency array (rankedFoundOpponent, t,
resolveOpponentLocation/projectPoint/getAvatarAsset/hashString references if
they can change) so the pin only updates when relevant data changes.

In `@src/features/party/RealtimePartyQuizScreen.tsx`:
- Around line 833-858: The overlay components (the forfeit/finalizing overlay
and the pause overlay rendered in the motion.div blocks around
forfeitPending/forfeitPendingTitle and the one keyed "party-pause" using
state.matchPaused and pauseSeconds) use arbitrary radii and font classes; update
them to follow game-style tokens: replace rounded-[20px]/rounded-[28px] with
rounded-2xl or rounded-3xl as appropriate, swap font-poppins for font-fun on the
text container, and add the chunky 3D border classes (border-b-4
border-surface-card-deep) to the overlay container divs so they match the repo
guidelines.

---

Nitpick comments:
In `@src/app/`(fullscreen)/dev/timers/page.tsx:
- Around line 186-222: Replace the game UI utility classes in the new timer
components to match the design-system: in the Pause preview block and
FinalizingResultsPreview component (look for PreviewFrame usage, the motion.div
with key={`finalizing-${runId}`}, TimerMeta and any elements using
useRemainingMs), swap font-poppins -> font-fun, change any rounded-[...] or
rounded-xl to rounded-2xl or rounded-3xl as appropriate, and add the chunky 3D
border classes (border-b-4 border-surface-card-deep) to card/button/badge
containers (e.g., the root motion.divs and status badges). Apply the same
substitutions in the other affected ranges mentioned (around lines 257-317,
511-515, 574-575) so all timer UI uses font-fun, rounded-2xl/3xl, and border-b-4
border-surface-card-deep consistently.

In `@src/components/auth/PrivacyPolicyScreen.tsx`:
- Line 84: The button and primary content card in the PrivacyPolicyScreen
component are missing the required chunky 3D border; update the relevant JSX
elements in PrivacyPolicyScreen (the element using className="gap-2 rounded-full
bg-white/10 px-4 font-poppins text-sm font-semibold text-white hover:bg-white/20
hover:text-white" and the main content card element) to include the classes
border-b-4 and border-surface-card-deep so they follow the "chunky 3D borders"
pattern for cards, buttons, and badges.

In `@src/components/auth/TermsOfServiceScreen.tsx`:
- Line 68: The button/card in TermsOfServiceScreen.tsx using className "gap-2
rounded-full bg-white/10 px-4 font-poppins text-sm font-semibold text-white
hover:bg-white/20 hover:text-white" is missing the project's chunky 3D border;
update the element(s) in TermsOfServiceScreen (both the instance at the shown
diff and the other occurrence around line 85) to include the 3D border utility
classes by adding "border-b-4 border-surface-card-deep" to their className
strings so the button/card follows the card/button/badge border pattern.

In `@src/components/auth/WelcomeScreen.tsx`:
- Around line 933-936: Update the Browse all categories CTA Button in
WelcomeScreen.tsx (the Button that calls setCategoriesOpen(true)) to use the
chunky 3D border and guideline corner token: add the classes border-b-4 and
border-surface-card-deep and replace the current rounded-[20px] with the
specified token (rounded-2xl or rounded-3xl) while keeping existing sizing and
color classes; ensure the className on that Button includes border-b-4
border-surface-card-deep and rounded-2xl (or rounded-3xl) so it matches the
shared game button pattern.

In `@src/components/game/RoundTransitionOverlay.tsx`:
- Line 116: The category label in the RoundTransitionOverlay component is using
"font-poppins" instead of the required game typography; update the className on
the label inside the RoundTransitionOverlay component (the element with
className="font-poppins text-[16px] sm:text-[18px] md:text-[20px] font-bold
uppercase tracking-[0.22em] text-brand-yellow mb-4") to use "font-fun" in place
of "font-poppins" so the overlay follows the Nunito-based game font rule.

In `@src/components/shared/FriendPlayModal.tsx`:
- Around line 155-156: The Join button is missing the chunky 3D border tokens;
update the class list used for the Join button in FriendPlayModal (the class
array containing "shrink-0 h-14 rounded-2xl px-5 text-base font-black uppercase
tracking-wide text-white" and the responsive "transition-all md:h-16 md:text-lg
md:px-7 whitespace-nowrap") to include the required border tokens (e.g., add
"border-b-4 border-surface-card-deep") so the button keeps its sizing and
responsive classes but also applies the chunky 3D border pattern.

In `@src/features/friend/components/FriendLobbyScreen.tsx`:
- Line 120: In FriendLobbyScreen component replace any arbitrary Tailwind
rounded classes (e.g., rounded-[20px], rounded-[14px]) used on action control
className strings with the standard tokens: use rounded-3xl for the larger
controls and rounded-2xl for smaller ones so they match the shared game UI
convention; update every occurrence (the one shown and the other instances) in
the component’s JSX className values accordingly (search for rounded-[20px] and
rounded-[14px] in FriendLobbyScreen and swap to rounded-3xl/rounded-2xl).

In `@src/features/party/PartyQuizResultsScreen.tsx`:
- Around line 182-203: Replace the custom arbitrary radii and missing 3D border
on the rank pill and related badges with the approved tokens: change classes
using rounded-[12px], rounded-[14px], rounded-[16px] to rounded-2xl or
rounded-3xl (use rounded-2xl for smaller pills, rounded-3xl for
larger/button-like elements) and add the chunky 3D border classes border-b-4
border-surface-card-deep where applicable; specifically update the span that
renders standing.rank (the rank pill) and the self indicator span (the
rounded-full bg-brand-orange badge) to use the new rounded-* tokens and include
border-b-4 border-surface-card-deep so they follow the design token/pattern.

In `@src/lib/auth/google-identity.ts`:
- Around line 76-83: In generateNoncePair add a runtime guard for crypto.subtle
(e.g., check globalThis.crypto && globalThis.crypto.subtle) and if missing
either throw a clear, descriptive error or use an explicit fallback (such as
Node's webcrypto) before calling crypto.subtle.digest; update the function to
detect the environment and fail fast with a helpful message like "crypto.subtle
unavailable: require secure context or provide webcrypto fallback" so callers
get actionable feedback instead of an unexpected exception.

In `@src/lib/posthog.ts`:
- Around line 71-98: The setPersonProperties function currently builds a payload
and calls posthog.capture('$set', payload); replace that capture call with the
SDK's dedicated method by invoking posthog.setPersonProperties(set, setOnce)
(when either set or setOnce is provided) so person properties use the explicit
API; keep the existing environment checks and try/catch/error logging around the
call and remove the posthog.capture('$set', ...) usage.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 55060fad-4ca2-4cc6-b26d-0ecc0409e698

📥 Commits

Reviewing files that changed from the base of the PR and between df942ca and 44f731c.

📒 Files selected for processing (114)
  • .env.example
  • scripts/audit-brand-colors.mjs
  • scripts/brand-colors-allowlist.json
  • scripts/migrate-brand-colors.mjs
  • scripts/rename-tokens.mjs
  • src/app/(app)/daily/challenges/[challengeId]/page.tsx
  • src/app/(app)/page.tsx
  • src/app/(app)/play/page.tsx
  • src/app/(app)/profile/page.tsx
  • src/app/(auth)/auth/forgot-password/page.tsx
  • src/app/(auth)/auth/welcome/page.tsx
  • src/app/(fullscreen)/dev/party-quiz/page.tsx
  • src/app/(fullscreen)/dev/timers/page.tsx
  • src/app/(fullscreen)/onboarding/page.tsx
  • src/app/(public)/privacy/PrivacyClient.tsx
  • src/app/(public)/terms/TermsClient.tsx
  • src/app/layout.tsx
  • src/app/page.tsx
  • src/app/sitemap.ts
  • src/components/FriendModeBottomSheet.tsx
  • src/components/HelpButtons.tsx
  • src/components/InlineAvatar.tsx
  • src/components/LanguageSelector.tsx
  • src/components/RankedModeBottomSheet.tsx
  • src/components/Timer.tsx
  • src/components/auth/AppAuthGate.tsx
  • src/components/auth/OAuthCallbackScreen.tsx
  • src/components/auth/PrivacyPolicyScreen.tsx
  • src/components/auth/TermsOfServiceScreen.tsx
  • src/components/auth/WelcomeScreen.tsx
  • src/components/game/AnswerCard.tsx
  • src/components/game/PossessionQuestionPanel.tsx
  • src/components/game/QuestionArena.tsx
  • src/components/game/RoundTransitionOverlay.tsx
  • src/components/layout/AppShell.tsx
  • src/components/layout/ChallengeInvitePrompt.tsx
  • src/components/layout/NotificationsDropdown.tsx
  • src/components/shared/FriendPlayModal.tsx
  • src/components/shared/MatchCountdownPuck.tsx
  • src/contexts/LocaleContext.tsx
  • src/features/daily/CareerPathGame.tsx
  • src/features/daily/ClueGame.tsx
  • src/features/daily/CountdownGame.tsx
  • src/features/daily/EmojiGuessGame.tsx
  • src/features/daily/FootballLogicGame.tsx
  • src/features/daily/HighLowGame.tsx
  • src/features/daily/ImposterGame.tsx
  • src/features/daily/PutInOrderGame.tsx
  • src/features/daily/TrueFalseGame.tsx
  • src/features/daily/index.ts
  • src/features/friend/components/CategoryDraftPanel.tsx
  • src/features/friend/components/CreateJoinPanel.tsx
  • src/features/friend/components/FriendLobbyScreen.tsx
  • src/features/friend/components/LobbyHeader.tsx
  • src/features/friend/hooks/useFriendLobbyLogic.ts
  • src/features/game/GameStageRouter.tsx
  • src/features/game/RealtimeResultsScreen.tsx
  • src/features/game/__tests__/GameStageRouter.test.tsx
  • src/features/game/components/MatchmakingMapScreen.tsx
  • src/features/game/components/ResultsShared.tsx
  • src/features/game/hooks/useRealtimeGameLogic.ts
  • src/features/home/HomeDashboard.tsx
  • src/features/home/HomeScreen.tsx
  • src/features/home/challenges.ts
  • src/features/home/components/DailyChallengeCard.tsx
  • src/features/home/components/DailyChallengesDrawer.tsx
  • src/features/home/components/DailyChallengesSection.tsx
  • src/features/home/components/GameHistorySection.tsx
  • src/features/home/components/RewardsObjectivesSection.tsx
  • src/features/home/components/dashboard/HomeDailyObjectives.tsx
  • src/features/home/components/dashboard/HomeEventTeaser.tsx
  • src/features/home/components/dashboard/HomeFeaturedChallenge.tsx
  • src/features/home/components/dashboard/HomePlayHero.tsx
  • src/features/home/components/dashboard/HomeQuickStatsRow.tsx
  • src/features/home/components/dashboard/HomeRightRail.tsx
  • src/features/party/PartyQuizResultsScreen.tsx
  • src/features/party/RealtimePartyQuizScreen.tsx
  • src/features/play/ModeSelectionScreen.tsx
  • src/features/play/hooks/useDraftLogic.ts
  • src/features/possession/RealtimePossessionMatchScreen.tsx
  • src/features/possession/components/BarBattleFlightOverlay.tsx
  • src/features/possession/components/DevToolbar.tsx
  • src/features/possession/components/IntroScreen.tsx
  • src/features/possession/components/KickoffCountdownOverlay.tsx
  • src/features/possession/components/LiveSpecialQuestionPanel.tsx
  • src/features/possession/components/PenaltyTransition.tsx
  • src/features/possession/components/PitchVisualization.tsx
  • src/features/possession/components/PossessionMatchViewport.tsx
  • src/features/possession/components/PregameOverlay.tsx
  • src/features/possession/components/ShotOverlay.tsx
  • src/features/possession/hooks/__tests__/usePossessionFieldState.test.ts
  • src/features/possession/hooks/usePossessionBarBattleFlights.ts
  • src/features/settings/SettingsScreen.tsx
  • src/features/social/SocialScreen.tsx
  • src/features/store/StoreScreen.tsx
  • src/features/store/components/RewardQuestCard.tsx
  • src/features/store/components/WalletSection.tsx
  • src/features/training/components/TrainingOfferModal.tsx
  • src/lib/analytics/game-events.ts
  • src/lib/auth/auth.service.ts
  • src/lib/auth/google-identity.ts
  • src/lib/posthog.ts
  • src/lib/realtime/socket-handlers.ts
  • src/lib/realtime/socket.types.ts
  • src/lib/sounds/index.ts
  • src/messages/en.json
  • src/messages/ka.json
  • src/stores/__tests__/realtimeMatch.store.test.ts
  • src/stores/auth.store.ts
  • src/stores/rankedMatchmaking.store.ts
  • src/stores/realtimeMatch.store.ts
  • src/types/api.generated.ts
  • src/utils/gameHelpers.ts
  • src/utils/rankSystem.ts
💤 Files with no reviewable changes (41)
  • src/app/sitemap.ts
  • src/features/home/components/dashboard/HomeEventTeaser.tsx
  • src/features/store/components/RewardQuestCard.tsx
  • src/features/possession/components/ShotOverlay.tsx
  • src/features/possession/components/IntroScreen.tsx
  • src/app/(app)/page.tsx
  • src/features/possession/components/PregameOverlay.tsx
  • src/components/InlineAvatar.tsx
  • src/features/possession/components/DevToolbar.tsx
  • src/features/home/components/GameHistorySection.tsx
  • src/lib/sounds/index.ts
  • src/features/daily/index.ts
  • src/components/LanguageSelector.tsx
  • src/components/HelpButtons.tsx
  • src/features/home/HomeDashboard.tsx
  • src/components/game/AnswerCard.tsx
  • src/features/game/components/ResultsShared.tsx
  • src/features/training/components/TrainingOfferModal.tsx
  • src/features/home/components/dashboard/HomePlayHero.tsx
  • src/components/Timer.tsx
  • src/utils/gameHelpers.ts
  • src/utils/rankSystem.ts
  • src/features/store/components/WalletSection.tsx
  • src/features/home/components/dashboard/HomeFeaturedChallenge.tsx
  • src/features/home/components/DailyChallengesSection.tsx
  • src/components/game/QuestionArena.tsx
  • src/features/home/HomeScreen.tsx
  • src/components/FriendModeBottomSheet.tsx
  • src/features/friend/components/CategoryDraftPanel.tsx
  • src/features/home/components/DailyChallengeCard.tsx
  • src/features/home/components/dashboard/HomeQuickStatsRow.tsx
  • src/features/home/components/RewardsObjectivesSection.tsx
  • src/components/RankedModeBottomSheet.tsx
  • src/features/play/hooks/useDraftLogic.ts
  • src/features/home/challenges.ts
  • src/features/home/components/dashboard/HomeRightRail.tsx
  • src/features/home/components/dashboard/HomeDailyObjectives.tsx
  • src/app/(app)/play/page.tsx
  • src/features/possession/components/PenaltyTransition.tsx
  • src/features/home/components/DailyChallengesDrawer.tsx
  • src/features/possession/components/PitchVisualization.tsx

Comment thread scripts/migrate-brand-colors.mjs Outdated
Comment thread src/app/(app)/profile/page.tsx Outdated
Comment thread src/app/(fullscreen)/dev/timers/page.tsx Outdated
Comment thread src/features/friend/components/CreateJoinPanel.tsx Outdated
Comment thread src/features/friend/components/CreateJoinPanel.tsx Outdated
Comment thread src/features/friend/hooks/useFriendLobbyLogic.ts Outdated
Comment thread src/features/party/RealtimePartyQuizScreen.tsx
Comment thread src/features/possession/components/BarBattleFlightOverlay.tsx Outdated
Comment thread src/features/possession/RealtimePossessionMatchScreen.tsx
Comment thread src/stores/realtimeMatch.store.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/features/party/RealtimePartyQuizScreen.tsx (1)

23-23: ⚡ Quick win

Cross-feature import violates module isolation guideline.

This imports usePossessionFirstQuestionIntro from @/features/possession/ into @/features/party/. Consider moving this shared hook to @/lib/hooks/ or a common location to maintain feature module isolation.

As per coding guidelines: "Feature modules in src/features/ should be self-contained with their own components, hooks, and logic — do NOT import from other features directly; use shared stores or libs instead."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/party/RealtimePartyQuizScreen.tsx` at line 23, The file imports
usePossessionFirstQuestionIntro from another feature which breaks module
isolation; move the hook (usePossessionFirstQuestionIntro) out of
src/features/possession and into a shared location such as src/lib/hooks (or a
new common hooks module), export it from there, update its original export to
re-export from the new location if needed, and change the import in
RealtimePartyQuizScreen.tsx to import from the new shared path; ensure any
internal dependencies of the hook are either moved or adapted to avoid pulling
more feature-private modules into a shared area.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/features/party/RealtimePartyQuizScreen.tsx`:
- Line 23: The file imports usePossessionFirstQuestionIntro from another feature
which breaks module isolation; move the hook (usePossessionFirstQuestionIntro)
out of src/features/possession and into a shared location such as src/lib/hooks
(or a new common hooks module), export it from there, update its original export
to re-export from the new location if needed, and change the import in
RealtimePartyQuizScreen.tsx to import from the new shared path; ensure any
internal dependencies of the hook are either moved or adapted to avoid pulling
more feature-private modules into a shared area.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e4cd208-397e-42a3-ac86-0563cb5e7500

📥 Commits

Reviewing files that changed from the base of the PR and between 44f731c and cb3b039.

📒 Files selected for processing (1)
  • src/features/party/RealtimePartyQuizScreen.tsx

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 9 file(s) based on 10 unresolved review comments.

Files modified:

  • scripts/migrate-brand-colors.mjs
  • src/app/(app)/profile/page.tsx
  • src/app/(fullscreen)/dev/timers/page.tsx
  • src/features/friend/components/CreateJoinPanel.tsx
  • src/features/friend/hooks/useFriendLobbyLogic.ts
  • src/features/party/RealtimePartyQuizScreen.tsx
  • src/features/possession/RealtimePossessionMatchScreen.tsx
  • src/features/possession/components/BarBattleFlightOverlay.tsx
  • src/stores/realtimeMatch.store.ts

Commit: 6106de56ea1aab6ea68f53083f79bdf9f1d6258f

The changes have been pushed to the dev-tazi-033 branch.

Time taken: 7m 8s

Fixed 9 file(s) based on 10 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/friend/components/CreateJoinPanel.tsx (1)

59-71: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate the normalized code before emitting join.

Line 59 validates raw input length, but Line 71 normalizes with trim(). Inputs like " AB " can pass the guard, then emit an invalid 2-char code.

💡 Suggested fix
   const handleJoin = () => {
     if (isJoining) return;
-    if (!inviteCode || inviteCode.length < 3) {
+    const code = inviteCode.trim().toUpperCase();
+    if (!code || code.length < 3) {
       toast.error(t("friend.enterValidCode"));
       return;
     }
@@
-    const code = inviteCode.trim().toUpperCase();
     try {
       trackFriendInviteAccepted(code);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/friend/components/CreateJoinPanel.tsx` around lines 59 - 71, The
validation currently checks the raw inviteCode length but the code is later
normalized (trim().toUpperCase()), allowing inputs like " AB " to bypass the
guard; change the logic to normalize first (use const code =
inviteCode?.trim().toUpperCase()), then validate code length (e.g., if (!code ||
code.length < 3) { toast.error(...); return; }) and use that normalized code for
the join emit and timeout handling (references: inviteCode, code, setIsJoining,
joinTimeoutRef).
♻️ Duplicate comments (1)
src/features/party/RealtimePartyQuizScreen.tsx (1)

405-413: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Also clear live delta timers/state on match reset.

This effect still leaves liveDeltaTimeoutsRef and liveScoreDeltas carrying over from the previous match. If matchId flips before an old 1.5s timeout fires, a stale badge can flash in the new match or the old timeout can remove a fresh delta for the same userId.

Suggested patch
   useEffect(() => {
     setDisplayedTotalsByUserId({});
+    setLiveScoreDeltas({});
     pendingDisplayedTotalsRef.current.clear();
     spawnedFlightKeysRef.current.clear();
     liveDeltaShownKeysRef.current.clear();
     liveFlightShownKeysRef.current.clear();
     previousPartyTotalsRef.current.clear();
     previousPartyAnsweredRef.current.clear();
+    for (const timeoutId of liveDeltaTimeoutsRef.current.values()) {
+      window.clearTimeout(timeoutId);
+    }
+    liveDeltaTimeoutsRef.current.clear();
   }, [partyState?.matchId]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/party/RealtimePartyQuizScreen.tsx` around lines 405 - 413, The
effect that resets state on match change must also cancel and clear any
outstanding live-delta timers and the live score delta state: inside the
useEffect tied to partyState?.matchId (the same block that calls
setDisplayedTotalsByUserId and clears pendingDisplayedTotalsRef,
spawnedFlightKeysRef, liveDeltaShownKeysRef, liveFlightShownKeysRef,
previousPartyTotalsRef, previousPartyAnsweredRef), iterate
liveDeltaTimeoutsRef.current to call clearTimeout on each stored timeout handle
and then clear the container, and also clear liveScoreDeltas (e.g., empty the
map/set holding per-user deltas) so stale timeouts or delta entries cannot
affect the new match.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/features/friend/components/CreateJoinPanel.tsx`:
- Around line 59-71: The validation currently checks the raw inviteCode length
but the code is later normalized (trim().toUpperCase()), allowing inputs like "
AB " to bypass the guard; change the logic to normalize first (use const code =
inviteCode?.trim().toUpperCase()), then validate code length (e.g., if (!code ||
code.length < 3) { toast.error(...); return; }) and use that normalized code for
the join emit and timeout handling (references: inviteCode, code, setIsJoining,
joinTimeoutRef).

---

Duplicate comments:
In `@src/features/party/RealtimePartyQuizScreen.tsx`:
- Around line 405-413: The effect that resets state on match change must also
cancel and clear any outstanding live-delta timers and the live score delta
state: inside the useEffect tied to partyState?.matchId (the same block that
calls setDisplayedTotalsByUserId and clears pendingDisplayedTotalsRef,
spawnedFlightKeysRef, liveDeltaShownKeysRef, liveFlightShownKeysRef,
previousPartyTotalsRef, previousPartyAnsweredRef), iterate
liveDeltaTimeoutsRef.current to call clearTimeout on each stored timeout handle
and then clear the container, and also clear liveScoreDeltas (e.g., empty the
map/set holding per-user deltas) so stale timeouts or delta entries cannot
affect the new match.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3930f93f-e3d7-4f55-836c-af0b53bf0f7e

📥 Commits

Reviewing files that changed from the base of the PR and between cb3b039 and 6106de5.

📒 Files selected for processing (9)
  • scripts/migrate-brand-colors.mjs
  • src/app/(app)/profile/page.tsx
  • src/app/(fullscreen)/dev/timers/page.tsx
  • src/features/friend/components/CreateJoinPanel.tsx
  • src/features/friend/hooks/useFriendLobbyLogic.ts
  • src/features/party/RealtimePartyQuizScreen.tsx
  • src/features/possession/RealtimePossessionMatchScreen.tsx
  • src/features/possession/components/BarBattleFlightOverlay.tsx
  • src/stores/realtimeMatch.store.ts

…style

CodeRabbit's autofix swapped the forfeit and pause overlay cards from
`rounded-[20px] font-poppins` (the Figma flat style) to `rounded-2xl
border-b-4 border-surface-card-deep font-fun` (the retired Duolingo
chunky 3D pattern).

frontend CLAUDE.md explicitly says:
> Do NOT use border-b-4 ... on new work.
> font-fun (Nunito) is retired except in legacy screens.

These overlays were among the screens being migrated AWAY from
chunky 3D, so applying that pattern is the opposite of what we want.
Restores the Figma-pinned royal-blue Poppins style.
… analytics

Mirrors the backend change (server no longer ships correctIndex on
match:question). Removes the field from the frontend socket type and
the three store updaters that previously merged it into per-question
state; the merge becomes a passthrough since the payload no longer
carries it. useRealtimeGameLogic's correctIndex memo drops the
payload fallback and reads from the canonical sources (questions[].
correctIndex set on round resolve, answerAck.correctIndex, and
roundResult.reveal.correctIndex).

trackAnswerSubmitted used to fire synchronously inside submitAnswer
and was gated on `correctIndex !== undefined` — which would have
broken now that the payload no longer carries it. Moved the
analytics call into a dedicated effect that fires when match:
answer_ack arrives, using answerAck.isCorrect as the source of truth
(server-authoritative rather than a client-side compare). Adds a
trackedAckQIndexRef so the event fires exactly once per question.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/features/game/components/MatchmakingMapScreen.tsx (1)

1273-1299: 💤 Low value

Consider restoring useMemo for opponentPin computation.

The refactor from useMemo to an IIFE means resolveOpponentLocation and projectPoint run on every render, even when rankedFoundOpponent hasn't changed. While this is unlikely to cause visible performance issues given the component's usage, wrapping this in useMemo with [rankedFoundOpponent, t] dependencies would avoid unnecessary recomputation during animations and timer ticks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/game/components/MatchmakingMapScreen.tsx` around lines 1273 -
1299, The opponentPin IIFE forces resolveOpponentLocation and projectPoint to
run on every render; wrap the computation in useMemo so it only recomputes when
inputs change: replace the IIFE that creates opponentPin with useMemo(() => {
... }, [rankedFoundOpponent, t]) so that opponentPin, which depends on
rankedFoundOpponent and the translation function t (and indirectly
resolveOpponentLocation/projectPoint), is memoized and avoids unnecessary
recomputation during animations/ticks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/game/hooks/useRealtimeGameLogic.ts`:
- Around line 396-410: Duplicate analytics are emitted because
trackAnswerSubmitted is called both inside the useEffect in useRealtimeGameLogic
(the effect that listens to answerAck/currentQuestion and uses
trackedAckQIndexRef and answerSubmitElapsedRef) and inside setAnswerAck in
realtimeMatch.store.ts; remove the duplication by deleting this useEffect (the
block starting with useEffect(() => { if (!answerAck || !currentQuestion)
return; ... }, [answerAck, currentQuestion]) ) so the store’s setAnswerAck
remains the single emitter, and then remove or repurpose trackedAckQIndexRef and
answerSubmitElapsedRef in this hook if they are no longer used elsewhere.

---

Nitpick comments:
In `@src/features/game/components/MatchmakingMapScreen.tsx`:
- Around line 1273-1299: The opponentPin IIFE forces resolveOpponentLocation and
projectPoint to run on every render; wrap the computation in useMemo so it only
recomputes when inputs change: replace the IIFE that creates opponentPin with
useMemo(() => { ... }, [rankedFoundOpponent, t]) so that opponentPin, which
depends on rankedFoundOpponent and the translation function t (and indirectly
resolveOpponentLocation/projectPoint), is memoized and avoids unnecessary
recomputation during animations/ticks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3514d20d-276a-4a08-884e-cb70efb31e60

📥 Commits

Reviewing files that changed from the base of the PR and between 09aa48d and ef72d8f.

📒 Files selected for processing (4)
  • src/features/game/components/MatchmakingMapScreen.tsx
  • src/features/game/hooks/useRealtimeGameLogic.ts
  • src/lib/realtime/socket.types.ts
  • src/stores/realtimeMatch.store.ts
💤 Files with no reviewable changes (1)
  • src/lib/realtime/socket.types.ts

Comment thread src/features/game/hooks/useRealtimeGameLogic.ts
Mirrors the backend revert. Server now ships MCQ correctIndex on
match:question again (intentional UX leak, server still validates
independently). Restores `payload.correctIndex` as a source in the
question slot + the useRealtimeGameLogic correctIndex memo so the
client-side compare lights up green/red the moment the user taps,
matching Trivia Crack / QuizUp behavior.

Keeps the analytics-on-ack effect from the previous change — that's a
genuine improvement independent of the leak decision. trackAnswerSubmitted
still fires once per question from match:answer_ack using the server-
authoritative isCorrect, not the optimistic client compare.
… modal

Two polish fixes for the friendly party-quiz flow:

1. Category preload — when the host picks "random categories", the
   server resolves the category inside startFriendlyMatch and then
   immediately emits match:start. The client used to fall back to a
   "Football" placeholder on the round-1 intro and flicker once
   match:question arrived. Backend now ships the resolved category
   name on match:start; this commit mirrors the type, stores it on
   MatchStatus, and prefers it in the round-1 transition snapshot
   (locale-resolved via useLocale, falling back to en, then to the
   existing question?.categoryName, then to the legacy placeholder).

2. Forfeit overlay — was a small top-banner without any backdrop or
   blur, while the matching possession (ranked) overlay was a centered
   modal with bg dim + backdrop-blur. Both modes now use the same
   centered-modal layout, with the same Poppins typography, the
   same brand-yellow eyebrow ("Finalizing Match"), and a shared
   translation key. Reads as one consistent overlay across modes.
@swoosh1337 swoosh1337 merged commit 2f03dcc into main May 26, 2026
3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant