Dev tazi 007#9
Conversation
Install @vercel/analytics and add <Analytics /> component to root layout for tracking page views and user behavior in production.
…ic, dedicated UI components, and game sound effects.
…and enhance game logic and timer management.
…ndling, prop types, and unicode support in game components.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughAdds a possession-based realtime match mode (types, store, hooks, UI, visuals, shot/penalty flows), a Howler-based sound system and useGameSounds hook, Vercel Analytics in layout, a development quick-match page and dev overlays, realtime socket type/handler extensions, and removes the legacy PossessionMatchDemo. Developer tooling and many new possession UI components/hooks/stores were added. Changes
Sequence Diagram(s)sequenceDiagram
rect rgba(220,240,255,0.5)
participant Player
participant UI as RealtimePossessionMatchScreen
participant Logic as usePossessionGameLogic
participant Store as PossessionMatchStore
participant Socket as ClientSocket
participant Server
end
Player->>UI: start / select answer / select tactic
UI->>Logic: handleAnswer / handleTacticSelected
Logic->>Store: update answer states, phase, scores
Logic->>Socket: emit match events (answer, tactic, dev events)
Socket->>Server: send events
Server->>Socket: broadcast match:state
Socket->>Store: setMatchState(payload)
Store->>UI: state updates (phase, question, opponent)
UI->>Player: render HUD/overlays/feedback
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Fix all issues with AI agents
In `@package.json`:
- Line 53: Replace all imports that reference "framer-motion" with imports from
"motion/react" (e.g., change import { motion, AnimatePresence, useAnimation }
from "framer-motion" to import { motion, AnimatePresence, useAnimation } from
"motion/react"); search codebase for any "from 'framer-motion'" occurrences and
update them, verify any default imports are converted to named imports if
needed, and then remove the "framer-motion" entry from package.json dependencies
to avoid duplicate bundling; run a typecheck/build to catch any leftover
references or typing changes and update imports accordingly.
In `@src/features/dev/DevOverlay.tsx`:
- Around line 46-48: The current Row entries access nested properties directly
(ps.seatMomentum.seat1, ps.goals.seat1, ps.penaltyGoals.seat1) after only
checking ps; update the rendering in DevOverlay so each nested object is safely
accessed (e.g. use optional chaining or explicit null checks for
ps.seatMomentum, ps.goals, ps.penaltyGoals) and provide a sensible fallback
string when any nested value is missing; target the three Row lines that
reference ps.seatMomentum, ps.goals, and ps.penaltyGoals and ensure you
reference seat1 and seat2 via safe access (ps?.seatMomentum?.seat1, etc.) or
equivalent guards.
- Around line 7-12: The component DevOverlay calls hooks (useState and
useRealtimeMatchStore) before conditionally returning based on
process.env.NODE_ENV, violating hook ordering safety; fix by moving the NODE_ENV
check to the top of the DevOverlay function so it returns null immediately when
not in development, and only call useState and useRealtimeMatchStore after that
guard (i.e., place the environment check before any calls to useState and
useRealtimeMatchStore to ensure hooks are only invoked when the component
proceeds).
In `@src/features/game/GameStageRouter.tsx`:
- Around line 47-48: Replace the hardcoded POSSESSION_TOTAL_QUESTIONS and
CLASSIC_TOTAL_QUESTIONS with a dynamic source: when handling incoming
MatchQuestionPayload (e.g., in the GameStageRouter's
onMatchQuestion/handleMatchQuestion code), capture and store payload.total as
the canonical question count for that match and use that stored value when
computing accuracy (instead of the constants); alternatively, if you control
server responses, add a total field to MatchFinalResultsPayload and read that in
GameStageRouter to compute final accuracy. Ensure references to
POSSESSION_TOTAL_QUESTIONS/CLASSIC_TOTAL_QUESTIONS in accuracy calculation are
replaced with the stored dynamic total (or final results total) so the client
always matches server question counts.
In `@src/features/possession/components/HalftimeScreen.tsx`:
- Around line 133-168: showHint is created with useState but never updated, so
its value doesn't reflect localStorage changes on mounts; change the declaration
to const [showHint, setShowHint] = useState(...) and inside the useEffect that
runs on visible change (the existing useEffect referencing visible, timerRef,
hasFiredRef) compute the current hint value (checking typeof window !==
'undefined' && !localStorage.getItem('halftime-hint-seen')) and call
setShowHint(...) when visible becomes true so the component respects the
one-time hint per session; keep the existing
localStorage.setItem('halftime-hint-seen','true') only when you actually show
the hint.
In `@src/features/possession/components/PenaltyCameraView.tsx`:
- Around line 52-56: The overlay motion.div in PenaltyCameraView uses separate
scale and x props (animate={{ scale: 2.5, x: '-35%' }}), which applies scale
before translate and causes misalignment with the pitch's translate+scale;
replace those with a single transform string that applies translate first then
scale (e.g. animate={{ transform: 'translateX(-35%) scale(2.5)' }} and initial
with matching order) so the transform order matches the pitch; make the same
change for the other overlay motion.div instance (the one around lines 87-91) so
both overlays use transform strings with translate then scale.
In `@src/features/possession/components/PenaltyOverlay.tsx`:
- Line 29: PenaltyOverlayProps declares result as 'pending' | 'goal' | 'saved' |
null but PenaltyCameraView only handles 'goal' | 'saved' | null, so filter or
remap 'pending' before passing to PenaltyCameraView: in the PenaltyOverlay
component (where you pass result into <PenaltyCameraView ... />) convert result
=== 'pending' to null (or omit the prop) so PenaltyCameraView only ever receives
'goal' | 'saved' | null; alternatively tighten the type on PenaltyOverlayProps
to match PenaltyCameraView if 'pending' should never be forwarded.
In `@src/features/possession/components/PenaltyTransition.tsx`:
- Around line 59-63: In PenaltyTransition.tsx the status line is hardcoded to
"Match Tied" regardless of the scores; update the JSX around the <div
className="text-gray-400..."> that currently renders "Match Tied" to render that
text only when playerGoals === opponentGoals and otherwise render an appropriate
status (e.g., show a different label or nothing). Use a conditional expression
based on the playerGoals and opponentGoals variables in the PenaltyTransition
component to control the rendered string so the UI reflects the actual score.
In `@src/features/possession/hooks/usePossessionMovement.ts`:
- Around line 70-85: In usePossessionMovement, the "Both wrong" return sets
posDelta: -2 but direction: 'neutral', causing UI mismatch; change the return to
use direction: 'backward' (or compute direction based on posDelta < 0) so the
direction aligns with the negative posDelta; update the object returned in the
both-wrong branch (the literal with posDelta: -2, momDelta: 0, message: 'Both
wrong → -2', direction: 'neutral') to use direction 'backward' (or derive
direction from posDelta) so arrows/animations reflect the backward movement.
In `@src/features/possession/hooks/useShotOnGoal.ts`:
- Around line 34-41: The bug is that when usedQuestionIdsRef.current is null you
create a new Set in usedIds but never persist it back, allowing repeats; inside
initializeShot (in useShotOnGoal.ts) ensure that after const usedIds =
usedQuestionIdsRef.current ?? new Set<string>() you assign
usedQuestionIdsRef.current = usedIds when it was previously null so the Set is
stored for subsequent calls before calling pickQuestion(HARD_QUESTIONS,
usedIds); this guarantees usedQuestionIdsRef retains chosen IDs across shots.
In `@src/features/possession/PossessionMatchScreen.tsx`:
- Around line 85-91: The button currently calls both g.toggleMute() and
usePossessionMatchStore.getState().setMuted(!g.muted), risking stale reads of
g.muted; change this so the mute value is computed once and used for both or,
better, let g.toggleMute() be the single source of truth that also updates the
store internally. Specifically, either (A) compute newMuted = !g.muted and then
call usePossessionMatchStore.getState().setMuted(newMuted) and g.toggleMute()
(or await g.toggleMute() if it returns a promise) so both use the same value, or
(B) refactor g.toggleMute() to call
usePossessionMatchStore.getState().setMuted(...) itself and remove the direct
store call from the button. Ensure references: g.toggleMute,
usePossessionMatchStore.getState().setMuted, and g.muted are updated
accordingly.
🧹 Nitpick comments (14)
src/features/home/components/dashboard/HomeRecentMatches.tsx (1)
66-69: Add ARIA alert semantics to the error banner.This improves screen-reader announcement for failed loads.
✅ Suggested tweak
- {!isLoading && error && ( - <div className="p-3.5 rounded-xl bg-red-500/10 border border-red-500/20 text-sm font-semibold text-red-500"> + {!isLoading && error && ( + <div + role="alert" + aria-live="polite" + className="p-3.5 rounded-xl bg-red-500/10 border border-red-500/20 text-sm font-semibold text-red-500" + > Failed to load recent matches. Please try again later. </div> )}src/features/possession/components/PenaltyOverlay.tsx (1)
71-84: Consider extractingrenderScorePipsoutside the component.This helper creates new array elements on every render. For better performance, consider moving it outside the component or wrapping with
useCallback. This is a minor optimization since the arrays are small.src/features/possession/components/ShotOverlay.tsx (1)
41-47: Use CONFETTI_COLORS.length for modulo.
Avoids drift if the palette changes.♻️ Suggested tweak
- colorIndex: i % 6, + colorIndex: i % CONFETTI_COLORS.length,src/lib/sounds/useGameSounds.ts (1)
3-23: useRef guard won’t prevent Strict Mode remount preloads.
React Strict Mode remounts components (resetting refs), so preloadAll can still run twice in dev. If you’re trying to avoid duplicate preloads, consider a module-level guard or ensure preloadAll is idempotent.♻️ One way to make the guard effective
-import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { playSfx, toggleMute, isMuted, preloadAll, } from './gameSounds'; +let hasPreloaded = false; + export function useGameSounds() { - const initialized = useRef(false); - useEffect(() => { - if (!initialized.current) { - preloadAll(); - initialized.current = true; - } + if (!hasPreloaded) { + preloadAll(); + hasPreloaded = true; + } }, []);src/stores/possessionMatch.store.ts (1)
150-206: resetMatch should rebuild fresh nested state to avoid shared references.
Line 287 uses a shallow copy of initialState, which reuses nested arrays/objects if they were mutated elsewhere. Consider a small factory function so each reset gets fresh nested state.♻️ Suggested refactor
-const initialState: PossessionMatchState = { +const createInitialState = (): PossessionMatchState => ({ phase: 'intro', half: 1, normalQuestionsInHalf: 0, tactic: null, @@ penaltyAnswerStates: [...DEFAULT_ANSWER_STATES], penaltyResult: null, penaltyShowOptions: false, penaltyFieldPhase: 'setup', -}; +}); + +const initialState = createInitialState(); @@ - resetMatch: () => set({ ...initialState }), + resetMatch: () => set(createInitialState()), }));Also applies to: 287-287
src/features/possession/components/PenaltyTransition.tsx (1)
14-140: Inconsistent indentation within AnimatePresence block.The
motion.divon line 15 and its children have inconsistent indentation relative to the{visible && (condition. This affects readability.src/features/game/GameStageRouter.tsx (1)
235-305: Significant duplication in quit/forfeit handlers.The
onQuitandonForfeithandlers forRealtimePossessionMatchScreen(lines 242-267) andRealtimeQuizBallGameScreen(lines 278-304) are nearly identical. Consider extracting these into shared callbacks.Proposed refactor
+ const handleMatchLeave = useCallback(() => { + if (realtimeMatch?.matchId) { + getSocket().emit("match:leave", { matchId: realtimeMatch.matchId }); + logger.info("Socket emit match:leave", { matchId: realtimeMatch.matchId }); + } else { + logger.info("Socket emit match:leave skipped (missing matchId)"); + } + exitToPlay(); + }, [realtimeMatch?.matchId, exitToPlay]); + + const handleMatchForfeit = useCallback(() => { + if (realtimeMatch?.matchId) { + getSocket().emit("match:forfeit", { matchId: realtimeMatch.matchId }); + logger.info("Socket emit match:forfeit", { matchId: realtimeMatch.matchId }); + } else { + logger.info("Socket emit match:forfeit skipped (missing matchId)"); + } + exitToPlay(); + }, [realtimeMatch?.matchId, exitToPlay]);Then use
onQuit={handleMatchLeave}andonForfeit={handleMatchForfeit}for both screens.src/features/possession/components/PenaltyHUD.tsx (1)
54-56: Hardcoded player names "You" and "CPU" limit reusability.The component receives
playerAvatarUrlandopponentAvatarUrlbut hardcodes the display names as "You" and "CPU". For multiplayer scenarios where the opponent is a real player, consider acceptingplayerNameandopponentNameprops similar toPossessionHUD.Proposed interface extension
interface PenaltyHUDProps { penaltyPlayerScore: number; penaltyOpponentScore: number; penaltyRound: number; isPenaltySuddenDeath: boolean; isPlayerShooter: boolean; + playerName: string; + opponentName: string; playerAvatarUrl: string; opponentAvatarUrl: string; timeRemaining: number; phase: Phase; onQuit?: () => void; }Also applies to: 79-81
src/features/possession/components/PenaltyFieldView.tsx (1)
26-27: Fixed overlay with z-50 may conflict with modals or other overlays.The component uses
fixed inset-0 z-50which will overlay the entire viewport. If any modals, toasts, or other overlays use similar z-index values, there could be stacking conflicts.src/features/possession/PossessionMatchScreen.tsx (1)
57-79: Consider using useMemo for computed feed values.The IIFE pattern for
feedSideandfeedPenaltyResultworks but is not idiomatic React. UsinguseMemowould make the dependencies explicit and allow React to optimize re-computation.Proposed refactor
- const feedSide = (() => { - if (g.isPenaltyPhase && g.phase === 'penalty-result') { - if (g.penaltyResult === 'goal') return g.isPlayerShooter ? 'left' as const : 'right' as const; - if (g.penaltyResult === 'saved') return g.isPlayerShooter ? 'right' as const : 'left' as const; - } - // ... rest - return 'left' as const; - })(); + const feedSide = useMemo(() => { + if (g.isPenaltyPhase && g.phase === 'penalty-result') { + if (g.penaltyResult === 'goal') return 'left' as const; + if (g.penaltyResult === 'saved') return g.isPlayerShooter ? 'right' as const : 'left' as const; + } + // ... rest + return 'left' as const; + }, [g.isPenaltyPhase, g.phase, g.penaltyResult, g.isPlayerShooter, g.isShotPhase, g.shotResult]);src/features/possession/components/PitchVisualization.tsx (1)
241-277: Consider scoping SVGdefsIDs per instance.
Fixed IDs (e.g.,pitchGrad,blueGlow,penNet) can collide if multiple PitchVisualization components render simultaneously, breaking filters/clipPaths. A per‑instance prefix (prop or generated) would avoid conflicts.src/features/possession/components/HalftimeScreen.tsx (1)
383-408: Use a button for the info icon to keep it keyboard-accessible.
The clickabledivisn’t focusable, which blocks keyboard users from opening the tooltip.♿️ Suggested fix
- <div - className="relative" - onClick={(e) => { - e.stopPropagation(); - setHoveredTactic(hoveredTactic === tactic.id ? null : tactic.id); - }} - > + <button + type="button" + className="relative" + aria-label={`More info: ${tactic.name}`} + onClick={(e) => { + e.stopPropagation(); + setHoveredTactic(hoveredTactic === tactic.id ? null : tactic.id); + }} + > <Info className="size-3.5 opacity-30 hover:opacity-60 transition-opacity cursor-pointer" style={{ color: isSelected ? tactic.color : '#56707A' }} strokeWidth={2} /> {hoveredTactic === tactic.id && ( <motion.div initial={{ opacity: 0, y: -4 }} animate={{ opacity: 1, y: 0 }} className="absolute top-5 right-0 w-44 p-2 rounded-lg bg-black/90 backdrop-blur-sm border border-white/15 shadow-xl z-50" > <div className="text-[10px] text-white/90 leading-relaxed"> {tactic.tooltip} </div> </motion.div> )} - </div> + </button>src/features/possession/types/possession.types.ts (1)
1-1: Avoid duplicatingTacticalCardacross modules.
This union also exists insrc/lib/realtime/socket.types.ts; keeping a single source prevents drift and accidental mismatches.♻️ Possible consolidation
-export type TacticalCard = 'press-high' | 'play-safe' | 'all-in'; +export type { TacticalCard } from '@/lib/realtime/socket.types';src/features/possession/hooks/usePossessionMovement.ts (1)
29-46: Add a defensive default ingetTacticModifiers.
If an unexpected tactic slips in at runtime, the function currently returnsundefined, which will crash consumers.🛡️ Suggested fix
switch (tactic) { case 'press-high': return { correctVsWrongGain: 12, wrongVsCorrectPenalty: -12, speedBonusMultiplier: 1.25, shotMomentumThreshold: 4 }; case 'play-safe': return { correctVsWrongGain: 9, wrongVsCorrectPenalty: -8, speedBonusMultiplier: 1, shotMomentumThreshold: 4 }; case 'all-in': return { correctVsWrongGain: 14, wrongVsCorrectPenalty: -14, speedBonusMultiplier: 1, shotMomentumThreshold: 3 }; + default: + return defaults; }
…st game question logic, and refine various UI components and animations.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/features/possession/PossessionMatchScreen.tsx`:
- Around line 178-194: Pass the phase-specific opponent answer into the
PossessionQuestionPanel instead of always g.opponentAnswer: update the
opponentAnswer prop on the PossessionQuestionPanel (where
onAnswer={activeHandler} is set) to choose g.penaltyOpponentAnswer when
g.isPenaltyPhase, g.shotOpponentAnswer when g.isShotPhase, otherwise
g.opponentAnswer so the shot-phase reveal uses the shot-specific data; reference
the PossessionQuestionPanel prop opponentAnswer and the flags g.isPenaltyPhase /
g.isShotPhase and fields g.penaltyOpponentAnswer / g.shotOpponentAnswer to
implement the conditional selection.
🧹 Nitpick comments (9)
src/features/possession/hooks/useShotOnGoal.ts (2)
10-16: Type parameter forusedQuestionIdsRefshould includenull.The type
React.RefObject<Set<string>>impliescurrentis never null, but lines 39-40 handle the null case with nullish coalescing and a guard. Update the type to match actual usage.🔧 Suggested fix
export function useShotOnGoal( - usedQuestionIdsRef: React.RefObject<Set<string>>, + usedQuestionIdsRef: React.RefObject<Set<string> | null>, advanceToNextQuestion: (position: number) => void, ) {
53-60: Consider extracting AI behavior constants.The magic numbers for AI defender behavior (delay range, correctness probability) could be extracted to named constants for clarity and easier tuning.
🔧 Suggested constants
// In possession.types.ts or at top of this file const AI_MIN_DELAY_SEC = 2; const AI_MAX_DELAY_SEC = 8; const AI_CORRECT_PROBABILITY = 0.55;- const aiDelay = 2 + Math.random() * 6; - const isCorrect = Math.random() < 0.55; + const aiDelay = AI_MIN_DELAY_SEC + Math.random() * (AI_MAX_DELAY_SEC - AI_MIN_DELAY_SEC); + const isCorrect = Math.random() < AI_CORRECT_PROBABILITY;src/features/game/GameStageRouter.tsx (1)
235-305: Consider extracting shared quit/forfeit handlers to reduce duplication.The
onQuitandonForfeitcallbacks are nearly identical betweenRealtimePossessionMatchScreen(lines 242-267) andRealtimeQuizBallGameScreen(lines 278-303). Extracting these into shared callbacks would reduce duplication and simplify maintenance.♻️ Proposed refactor
+ const handleMatchQuit = useCallback(() => { + if (realtimeMatch?.matchId) { + getSocket().emit("match:leave", { + matchId: realtimeMatch.matchId, + }); + logger.info("Socket emit match:leave", { + matchId: realtimeMatch.matchId, + }); + } else { + logger.info("Socket emit match:leave skipped (missing matchId)"); + } + exitToPlay(); + }, [realtimeMatch?.matchId, exitToPlay]); + + const handleMatchForfeit = useCallback(() => { + if (realtimeMatch?.matchId) { + getSocket().emit("match:forfeit", { + matchId: realtimeMatch.matchId, + }); + logger.info("Socket emit match:forfeit", { + matchId: realtimeMatch.matchId, + }); + } else { + logger.info("Socket emit match:forfeit skipped (missing matchId)"); + } + exitToPlay(); + }, [realtimeMatch?.matchId, exitToPlay]);Then use these in both screen components:
<RealtimePossessionMatchScreen ... onQuit={handleMatchQuit} onForfeit={handleMatchForfeit} />src/features/possession/hooks/usePossessionMovement.ts (1)
11-16: Consider extracting randomness for testability.
getDifficultyForZoneusesMath.random()directly, making unit tests non-deterministic. Consider accepting an optional random function parameter or using a seeded generator for testing scenarios.♻️ Proposed refactor
-export function getDifficultyForZone(position: number): 'easy' | 'medium' | 'hard' { - if (position >= 71) return Math.random() < 0.5 ? 'medium' : 'hard'; +export function getDifficultyForZone(position: number, rand: () => number = Math.random): 'easy' | 'medium' | 'hard' { + if (position >= 71) return rand() < 0.5 ? 'medium' : 'hard'; if (position >= 46) return 'medium'; - if (position >= 21) return Math.random() < 0.5 ? 'easy' : 'medium'; + if (position >= 21) return rand() < 0.5 ? 'easy' : 'medium'; return 'easy'; }src/features/possession/PossessionMatchScreen.tsx (2)
58-79: Consider consolidating feedSide and feedPenaltyResult logic.Both IIFEs share similar phase-checking patterns. A small helper or combined derivation could reduce duplication and make the relationship between side and result clearer.
209-210: Hardcoded ready states may cause UI inconsistency.
myReady={false}andopponentReady={true}are hardcoded. If HalftimeScreen displays ready indicators, this will always show the player as not ready and opponent as ready, which may not reflect actual game state in future multiplayer scenarios.src/features/possession/components/PenaltyOverlay.tsx (2)
221-226: Hardcoded difficulty may not reflect actual question difficulty.
difficulty="Hard"is always passed toQuestionArena, but penalty questions may have varying difficulties. Consider passing the actual difficulty fromcurrentQuestion.difficultyif available.♻️ Proposed fix
<QuestionArena question={currentQuestion.prompt} category={currentQuestion.categoryName ?? 'Penalty'} categoryIcon="⚽" - difficulty="Hard" + difficulty={currentQuestion.difficulty ?? 'Hard'} />
71-84: Consider memoizingrenderScorePipsto avoid recreation on each render.
renderScorePipsis recreated on every render. Since it depends only on stable values (pipColors,fallbackPip), extracting it outside the component or wrapping withuseCallbackwould be a minor optimization.src/features/possession/components/PenaltyCameraView.tsx (1)
15-15: Type mismatch:resultincludes'pending'but is not handled.The
resultprop accepts'pending' | 'goal' | 'saved' | null, but the component only animates for'goal'and'saved'. Whenresult === 'pending', no ball animation plays, which may be intentional, but the type should be tightened or explicitly documented.Note: The caller (
PenaltyOverlay) converts'pending'tonullbefore passing, so this is currently safe, but the interface is misleading.♻️ Proposed type fix
interface PenaltyCameraViewProps { // ... - result: 'pending' | 'goal' | 'saved' | null; + result: 'goal' | 'saved' | null; // ... }
…shot/penalty results, and document ESLint `exhaustive-deps` warnings.
…`PossessionMatchScreen` to display the correct opponent answer based on the game phase.
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@src/features/possession/components/PitchVisualization.tsx`:
- Around line 349-369: The SVG clipPath ids are static and collide when multiple
PitchVisualization instances render; update the PitchVisualization component to
generate a unique id using React's useId() (or a similar unique prefix) and pass
scoped clipIds into the PitchMarker props (e.g., `${id}-oppClipInst` and
`${id}-playerClipInst`) everywhere the static "oppClipInst"/"playerClipInst"
strings are used so each PitchVisualization instance gets unique clipPath ids
and avoids cross-instance collisions.
In `@src/features/possession/components/PossessionQuestionPanel.tsx`:
- Around line 127-149: The mapping for option labels uses ANSWER_LABELS but
doesn't guard against questions with more options than ANSWER_LABELS, so label
can be undefined; in the render loop around question.options.map(...) (and the
AnswerCard prop label) add a safe fallback when ANSWER_LABELS[i] is missing
(e.g., generate a label from index such as a letter or numeric fallback like
`${i+1}` or String.fromCharCode(65 + i)) so AnswerCard always receives a defined
label.
In `@src/features/possession/RealtimePossessionMatchScreen.tsx`:
- Around line 417-422: The penalty feed is incorrectly using the live shooter
flag shooterIsMe which can drift; replace shooterIsMe with the round-result flag
resultShooterIsMe when computing the penalty side and any logic tied to the
penalty outcome (the block handling kind === 'penalty' and the similar block at
the other location around line 441). Specifically, compute side using
resultShooterIsMe (e.g., side = penaltyResult === 'goal' ? (resultShooterIsMe ?
'left' : 'right') : (resultShooterIsMe ? 'right' : 'left')) and preserve
feedResult as before so the feed reflects the shooter seat at the time of the
result rather than the current live state.
- Around line 455-464: The mute toggle button in RealtimePossessionMatchScreen
is icon-only and needs an accessible label for screen readers; update the button
(the element that calls toggleMute and setMuted and uses the muted state) to
include an appropriate ARIA attribute such as aria-label (e.g.,
aria-label={muted ? 'Unmute audio' : 'Mute audio'}) and consider adding
aria-pressed={muted} to convey toggle state, keeping the existing title and
click handlers (toggleMute, setMuted) unchanged.
- Around line 35-41: toAnswerStates currently returns DEFAULT_ANSWER_STATES when
selectedAnswer is null which can mismatch optionsCount; instead, when
selectedAnswer === null construct and return a new AnswerStateArray of length
optionsCount filled with the neutral/default state (rather than using
DEFAULT_ANSWER_STATES), e.g. create an array of size optionsCount and fill each
entry with the same default AnswerState value so downstream consumers always
receive an array sized to optionsCount; update the toAnswerStates function to
use optionsCount for the empty-case construction and keep existing behavior for
non-null selectedAnswer/selfAnsweredCorrectly.
Summary by CodeRabbit
New Features
Documentation