Skip to content

[EPIC-003] Story 4: Real-time Voting Status (Supabase)#51

Merged
TheEagleByte merged 4 commits intomainfrom
issue-20-realtime-voting-status
Oct 1, 2025
Merged

[EPIC-003] Story 4: Real-time Voting Status (Supabase)#51
TheEagleByte merged 4 commits intomainfrom
issue-20-realtime-voting-status

Conversation

@TheEagleByte
Copy link
Copy Markdown
Owner

@TheEagleByte TheEagleByte commented Oct 1, 2025

Summary

Implements real-time voting status tracking for Planning Poker sessions, resolving issue #20. Participants can now see who has voted and who is still pending in real-time using Supabase real-time subscriptions and Presence.

Key Features Implemented

  • ✅ Real-time participant list with vote status indicators
  • ✅ Supabase Presence for online/offline tracking
  • ✅ "Waiting for" indicator showing participants who haven't voted
  • ✅ Participant count display (voted/total)
  • ✅ Visual vote status (checkmark for voted, clock for waiting)
  • ✅ Observer distinction (separate display for non-voting participants)
  • ✅ Real-time updates when votes are submitted

Technical Implementation

Database Changes

  • New Migration: 20251001000000_enable_poker_realtime.sql
    • Enabled real-time publication for poker_sessions, poker_stories, poker_participants, and poker_votes tables
    • Allows live updates across all poker-related tables

New Components

  • ParticipantStatus (src/components/poker/ParticipantStatus.tsx)
    • Displays all session participants grouped by voters/observers
    • Shows vote status for each participant (voted ✓ or waiting ⏰)
    • Implements Supabase Presence for online status tracking
    • Real-time updates via postgres_changes subscription
    • "Waiting for X participants" banner when votes are incomplete
    • "All participants have voted" success indicator

New Hooks

  • use-poker-participants (src/hooks/use-poker-participants.ts)
    • useSessionParticipants(sessionId) - Fetches and subscribes to participant changes
    • useJoinPokerSession() - Mutation for joining a session as a participant
    • useParticipantCount(sessionId) - Helper to get participant count
    • Real-time subscription to poker_participants table

Component Updates

  • VotingInterface - Integrated ParticipantStatus component
  • StoryManager - Passes sessionId prop to VotingInterface

Test Plan

  • Build passes without errors
  • Linter passes with no new warnings
  • Type checking passes
  • CodeRabbit analysis completed
  • Manual testing with multiple participants in different browsers
  • Verify real-time updates when votes are submitted
  • Test Presence tracking (online/offline indicators)
  • Verify observer participants display correctly
  • Test "waiting for" indicator updates in real-time
  • Verify participant count accuracy

Acceptance Criteria

  • Show participant list with vote status
  • Implement real-time status updates via Supabase
  • Use Supabase presence for participant tracking
  • Create "waiting for" indicator
  • Show participant count
  • Handle participant removal (kicked/left) - via real-time subscription

Screenshots/Demo

Screenshots will be added after manual testing

Related Issues

Closes #20


🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Participant status panel showing who’s online, who’s voting vs observing, per-player vote status, host badge, and “all voted” / waiting indicators.
    • Live, real-time updates for participants and votes with presence indicators.
    • Faster join-session feedback with optimistic updates and toasts.
  • Refactor
    • Reorganized voting layout to surface participant status above the vote card.
  • Chores
    • Enabled realtime subscriptions for poker sessions, stories, participants, and votes.
  • Tests
    • Added unit tests covering participant hooks, joining flow, and query keys.

Copilot AI review requested due to automatic review settings October 1, 2025 15:12
@vercel
Copy link
Copy Markdown

vercel bot commented Oct 1, 2025

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

Project Deployment Preview Comments Updated (UTC)
scrumkit Ready Ready Preview Comment Oct 1, 2025 3:31pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 1, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds real-time participant presence and voting-status UI for planning poker: new ParticipantStatus component, a React Query + Supabase participants hook (with join mutation), sessionId threaded into VotingInterface via StoryManager, tests for hooks, and a DB migration enabling poker tables in Supabase realtime publication.

Changes

Cohort / File(s) Change summary
Participant status UI
src/components/poker/ParticipantStatus.tsx
New client React component rendering real-time participant/observer lists, vote status (voted/waiting), online presence via Supabase Presence, and per-participant UI (avatar, host badge, status icons).
Voting UI integration
src/components/poker/VotingInterface.tsx
Added sessionId prop to VotingInterfaceProps and component; imports and renders ParticipantStatus(story, sessionId) above existing voting UI; reorganized JSX container while preserving existing voting logic.
Prop threading
src/components/poker/StoryManager.tsx
Passes sessionId (from session.id) into VotingInterface; no other logic changes.
Participants hook & mutations
src/hooks/use-poker-participants.ts
New hooks and exports: pokerParticipantKeys, useSessionParticipants(sessionId, ...) (query + Supabase realtime subscription + invalidation), useJoinPokerSession() (optimistic join mutation with rollback/toasts), and useParticipantCount(sessionId).
Hook tests
src/hooks/__tests__/use-poker-participants.test.ts
New unit tests covering participant query behavior, join mutation success/error flows, participant count helper, and query key generation; uses mocked poker actions, Supabase, and QueryClient wrapper.
Realtime publication migration
supabase/migrations/20251001000000_enable_poker_realtime.sql
Adds poker tables (poker_sessions, poker_stories, poker_participants, poker_votes) to supabase_realtime publication and descriptive comments documenting realtime behavior.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant VI as VotingInterface
  participant PS as ParticipantStatus
  participant HP as use-poker-participants
  participant RQ as React Query
  participant SB as Supabase (DB + Realtime)

  U->>VI: Open voting view (story, sessionId)
  VI->>PS: Render ParticipantStatus(story, sessionId)
  PS->>HP: subscribe useSessionParticipants(sessionId)
  HP->>RQ: useQuery(fetch participants)
  RQ->>SB: SELECT participants by sessionId
  SB-->>RQ: participants list
  RQ-->>PS: return participants

  Note over HP,SB: HP opens Supabase realtime & presence channel
  SB-->>HP: broadcast insert/update/delete/presence
  HP->>RQ: invalidate session participants key
  RQ->>SB: refetch updated participants
  RQ-->>PS: updated data -> UI rerender
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant VI as VotingInterface
  participant V as Vote logic
  participant SB as Supabase Realtime
  participant PS as ParticipantStatus

  U->>VI: Cast vote
  VI->>V: submit vote
  V->>SB: insert/update vote
  SB-->>PS: realtime vote/participant updates
  PS->>VI: update counts (voted vs waiting)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

Thump, thump — a rabbit at my post,
I watch the list and count each host.
Who’s voted now, who’s still in queue?
Lights flick on green, then checkmarks too.
Hop, reveal — we’re ready! 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely describes the primary feature added by this pull request—real-time voting status via Supabase for Story 4 of EPIC-003—without extraneous details or unrelated terms.
Linked Issues Check ✅ Passed The changes introduce a new ParticipantStatus component and supporting hooks that subscribe to Supabase realtime channels and Presence for poker participants and votes, compute and display each participant’s vote status, present a “waiting for” list of non-voters, display participant counts and observer separation, and reflect participant removal via subscription invalidations, thereby satisfying all tasks and acceptance criteria of issue #20.
Out of Scope Changes Check ✅ Passed All modifications—including the migration enabling realtime updates, the ParticipantStatus component, updates to VotingInterface and StoryManager for sessionId, the use-poker-participants hooks, and associated tests—are directly aligned with implementing real-time voting status via Supabase and introduce no unrelated functionality.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-20-realtime-voting-status

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Implements real-time voting status tracking for Planning Poker sessions using Supabase real-time subscriptions and Presence. Participants can see who has voted/is pending in real-time, with participant count displays and online status tracking.

  • Real-time participant list with vote status indicators using Supabase subscriptions
  • Supabase Presence integration for online/offline tracking
  • New ParticipantStatus component showing voting progress and participant details

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
supabase/migrations/20251001000000_enable_poker_realtime.sql Enables real-time publication for poker tables to support live updates
src/hooks/use-poker-participants.ts New hook for fetching participants with real-time subscriptions and mutations
src/components/poker/VotingInterface.tsx Integrates ParticipantStatus component and passes sessionId prop
src/components/poker/StoryManager.tsx Updates to pass sessionId to VotingInterface
src/components/poker/ParticipantStatus.tsx New component displaying real-time participant status and voting progress

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
src/hooks/use-poker-participants.ts (5)

29-33: Loosened options type can degrade inference; consider a narrowed options type.

UseQueryOptions here is too generic; prefer an Omit that reserves ownership of queryKey/queryFn/enabled for this hook.

Example:

+type SessionParticipantsKey = ReturnType<typeof pokerParticipantKeys.session>;
+type SessionParticipantsOptions = Omit<
+  UseQueryOptions<PokerParticipant[], Error, PokerParticipant[], SessionParticipantsKey>,
+  "queryKey" | "queryFn" | "enabled"
+>;
-export function useSessionParticipants(
-  sessionId: string,
-  options?: UseQueryOptions<PokerParticipant[]>
-) {
+export function useSessionParticipants(
+  sessionId: string,
+  options?: SessionParticipantsOptions
+) {

46-77: Throttle/limit invalidations and avoid noisy logs.

Every UPDATE (e.g., last_seen_at heartbeats) triggers a refetch. This will thrash under Presence. Filter UPDATEs that only change last_seen_at, and gate logs to dev.

-        (payload) => {
-          console.log("Participant change:", payload);
-
-          // Invalidate queries to refetch data
-          queryClient.invalidateQueries({
-            queryKey: pokerParticipantKeys.session(sessionId),
-          });
-        }
+        (payload: any) => {
+          if (process.env.NODE_ENV !== "production") {
+            // eslint-disable-next-line no-console
+            console.debug("Participant change:", payload?.eventType);
+          }
+          // Skip heartbeat-only updates
+          if (payload?.eventType === "UPDATE") {
+            const oldRow = payload.old ?? {};
+            const newRow = payload.new ?? {};
+            const changed = Object.keys(newRow).filter((k) => oldRow[k] !== newRow[k]);
+            if (changed.length === 1 && changed[0] === "last_seen_at") {
+              return;
+            }
+          }
+          queryClient.invalidateQueries({
+            queryKey: pokerParticipantKeys.session(sessionId),
+            // refetchType defaults to "active"; keep default
+          });
+        }

If heartbeats are still too chatty, consider listening only for INSERT/DELETE and a coarser timer-based refetch for UPDATEs.


106-117: Optimistic participant shape looks fine; consider tagging for reconciliation.

To reduce flicker, store the temp id in mutation context and, onSuccess, replace the optimistic row with the real one before invalidation.


139-147: Minor UX polish: reconcile before invalidating.

Optionally setQueryData to replace the temp participant with the saved one in onSuccess, then invalidate. This avoids a brief duplicate in the list.


152-155: Avoid double realtime subscriptions in the count hook.
useParticipantCount currently calls useSessionParticipants, opening a realtime subscription; consider deriving the count via a head-only query (e.g. select("*", { head: true, count: "exact" })) or adding a subscribe?: boolean flag to useSessionParticipants (defaulting to true) and passing false here to skip the extra subscription. No other calls to useParticipantCount were found in the codebase.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68b53b1 and e6e073a.

📒 Files selected for processing (5)
  • src/components/poker/ParticipantStatus.tsx (1 hunks)
  • src/components/poker/StoryManager.tsx (1 hunks)
  • src/components/poker/VotingInterface.tsx (5 hunks)
  • src/hooks/use-poker-participants.ts (1 hunks)
  • supabase/migrations/20251001000000_enable_poker_realtime.sql (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/{app,components}/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use the "use client" directive only in components that require client-side interactivity

Files:

  • src/components/poker/VotingInterface.tsx
  • src/components/poker/ParticipantStatus.tsx
  • src/components/poker/StoryManager.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use the @/* path alias for imports from ./src/*
Import Supabase clients from src/lib/supabase/{client.ts,server.ts} instead of instantiating ad-hoc clients

Files:

  • src/components/poker/VotingInterface.tsx
  • src/components/poker/ParticipantStatus.tsx
  • src/components/poker/StoryManager.tsx
  • src/hooks/use-poker-participants.ts
🧬 Code graph analysis (3)
src/components/poker/VotingInterface.tsx (1)
src/components/poker/ParticipantStatus.tsx (1)
  • ParticipantStatus (20-281)
src/components/poker/ParticipantStatus.tsx (4)
src/lib/poker/types.ts (1)
  • PokerStory (52-64)
src/hooks/use-poker-participants.ts (1)
  • useSessionParticipants (29-80)
src/hooks/use-poker-votes.ts (1)
  • useStoryVotes (25-38)
src/lib/utils.ts (1)
  • cn (4-6)
src/hooks/use-poker-participants.ts (2)
src/lib/poker/types.ts (2)
  • PokerParticipant (66-77)
  • CreatePokerParticipantInput (135-141)
src/lib/poker/actions.ts (2)
  • getSessionParticipants (379-394)
  • joinPokerSession (311-376)
🔇 Additional comments (1)
src/hooks/use-poker-participants.ts (1)

21-26: Good key factory.

Key hierarchy is stable and specific; nice use of const tuples for type safety.

- Add type safety with narrowed UseQueryOptions
- Optimize real-time subscriptions to skip heartbeat-only updates
- Guard console logs with NODE_ENV check
- Add comprehensive JSDoc documentation for hooks and components
- Improve docstring coverage from 50% toward 80% threshold
- Add comprehensive test suite with 10 test cases
- Test successful participant fetching and error handling
- Test join session mutation with optimistic updates
- Test participant count helper hook
- Test query key factory
- Overall test coverage now at 28.39% (exceeds 28% threshold)
@TheEagleByte TheEagleByte merged commit bb4b518 into main Oct 1, 2025
3 of 4 checks passed
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (6)
src/components/poker/ParticipantStatus.tsx (4)

45-94: Fix presence to track individual participants, not the session.

The current presence setup uses key: sessionId (line 53) and tracks only { online_at } (line 79–81), so all connected clients share one presence entry. This makes it impossible to determine which participant is online. The isOnline() helper (lines 116–118) merely checks if any client is present, causing every participant to show a green dot once anyone connects.

Solution: Track presence per participant by including a unique participant identifier in the presence key and payload:

  1. Pass the current participant's ID (or retrieve it from auth/cookies) into the component or derive it from the session context.
  2. Update the channel key to include the participant ID: poker-session:${sessionId}:${participantId}.
  3. Track { participant_id: participantId, online_at: ... } in the presence payload.
  4. Update isOnline(participantId) to search Object.values(presenceState) for entries whose participant_id matches.

Example diff (assumes currentParticipantId is available):

-      channel = supabase.channel(`poker-session:${sessionId}`, {
-        config: {
-          presence: {
-            key: sessionId,
-          },
-        },
-      });
+      // Obtain currentParticipantId from props, context, or auth
+      const currentParticipantId = /* retrieve participant ID */;
+      channel = supabase.channel(`poker-session:${sessionId}:${currentParticipantId}`, {
+        config: {
+          presence: {
+            key: currentParticipantId,
+          },
+        },
+      });
       
       ...
       
       .subscribe(async (status) => {
         if (status === "SUBSCRIBED") {
-          await channel.track({
-            online_at: new Date().toISOString(),
-          });
+          await channel.track({
+            participant_id: currentParticipantId,
+            online_at: new Date().toISOString(),
+          });
         }
       });

Then update the helper:

-  const isOnline = (): boolean => {
-    return Object.keys(presenceState).length > 0;
+  const isOnline = (participantId: string): boolean => {
+    return Object.values(presenceState).some(presences =>
+      presences.some((p: any) => p.participant_id === participantId)
+    );
   };

And call it with the participant's ID at lines 201 and 264:

-                const online = isOnline();
+                const online = isOnline(participant.id);

Based on past review comments.


116-118: Update isOnline helper to accept participantId parameter.

Currently isOnline() returns true whenever any client is connected. After fixing the presence tracking (lines 45–94), this helper must accept a participantId and check if that specific participant's entry exists in presenceState.

See the previous comment for the fix.

Based on past review comments.


199-201: Pass participant.id to isOnline.

Once isOnline is updated to accept a participant ID, pass participant.id here instead of calling it with no arguments.

-                const online = isOnline();
+                const online = isOnline(participant.id);

Based on past review comments.


263-264: Pass participant.id to isOnline for observers.

Same issue as with voters: the online status must be participant-specific.

-                const online = isOnline();
+                const online = isOnline(participant.id);

Based on past review comments.

src/hooks/use-poker-participants.ts (2)

11-14: Remove server-only imports; use client-safe alternatives.

Lines 11–14 import getSessionParticipants and joinPokerSession from @/lib/poker/actions. That module uses cookies() from next/headers (see joinPokerSession in actions.ts lines 310–375), which is server-only and cannot be imported into client code marked with "use client" (line 1). This will cause a build or runtime failure.

Solution:

  1. For reads (getSessionParticipants): Replace the server action with a direct Supabase client call in the queryFn (lines 57–60).
  2. For writes (joinPokerSession): Call a Next.js API route that wraps the server action instead of importing it directly (lines 130–136).

Read path fix (lines 57–60):

-    queryFn: async () => {
-      const participants = await getSessionParticipants(sessionId);
-      return participants;
-    },
+    queryFn: async () => {
+      const supabase = createClient();
+      const { data, error } = await supabase
+        .from("poker_participants")
+        .select("*")
+        .eq("session_id", sessionId)
+        .order("joined_at", { ascending: true });
+      if (error) {
+        console.error("Error fetching participants:", error);
+        throw error;
+      }
+      return (data as PokerParticipant[]) ?? [];
+    },

Write path fix (lines 130–136):

-    mutationFn: ({
-      sessionId,
-      input,
-    }: {
-      sessionId: string;
-      input: CreatePokerParticipantInput;
-    }) => joinPokerSession(sessionId, input),
+    mutationFn: async ({
+      sessionId,
+      input,
+    }: {
+      sessionId: string;
+      input: CreatePokerParticipantInput;
+    }) => {
+      const res = await fetch(`/api/poker/sessions/${sessionId}/join`, {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify(input),
+      });
+      if (!res.ok) {
+        const text = await res.text();
+        throw new Error(text || "Failed to join session");
+      }
+      return (await res.json()) as PokerParticipant;
+    },

Then remove the imports at lines 11–14:

-import {
-  getSessionParticipants,
-  joinPokerSession,
-} from "@/lib/poker/actions";

You'll also need to create the API route at src/app/api/poker/sessions/[sessionId]/join/route.ts that calls joinPokerSession server-side.

Based on past review comments.


55-63: Reorder options spread to prevent overriding enabled guard.

Spreading ...options after setting enabled: !!sessionId (line 61–62) allows callers to override the guard, potentially causing the query to run with a falsy sessionId and leading to runtime errors or incorrect API calls.

Solution: Spread ...options first, then set enabled last so the guard cannot be overridden.

   const query = useQuery({
     queryKey: pokerParticipantKeys.session(sessionId),
     queryFn: async () => {
       const participants = await getSessionParticipants(sessionId);
       return participants;
     },
-    enabled: !!sessionId,
     ...options,
+    enabled: !!sessionId,
   });

Based on past review comments.

🧹 Nitpick comments (2)
src/hooks/use-poker-participants.ts (2)

77-77: Narrow realtime subscription to INSERT/DELETE instead of wildcard.

Using event: "*" (line 77) causes the handler to fire on every UPDATE, including heartbeat-only last_seen_at changes. Although lines 88–96 filter out single-field last_seen_at updates, it's more efficient to subscribe only to INSERT and DELETE events (the events that actually add/remove participants) to avoid unnecessary payload processing.

       .on(
         "postgres_changes",
         {
-          event: "*",
+          event: "INSERT",
           schema: "public",
           table: "poker_participants",
           filter: `session_id=eq.${sessionId}`,
         },
         (payload) => {
-          if (process.env.NODE_ENV !== "production") {
-            // eslint-disable-next-line no-console
-            console.debug("Participant change:", payload?.eventType);
-          }
-
-          // Skip heartbeat-only updates to reduce unnecessary refetches
-          if (payload?.eventType === "UPDATE") {
-            const oldRow = payload.old ?? {};
-            const newRow = payload.new ?? {};
-            const changed = Object.keys(newRow).filter((k) => oldRow[k] !== newRow[k]);
-            if (changed.length === 1 && changed[0] === "last_seen_at") {
-              return;
-            }
-          }
-
           // Invalidate queries to refetch data
           queryClient.invalidateQueries({
             queryKey: pokerParticipantKeys.session(sessionId),
           });
         }
       )
+      .on(
+        "postgres_changes",
+        {
+          event: "DELETE",
+          schema: "public",
+          table: "poker_participants",
+          filter: `session_id=eq.${sessionId}`,
+        },
+        () => {
+          queryClient.invalidateQueries({
+            queryKey: pokerParticipantKeys.session(sessionId),
+          });
+        }
+      )
       .subscribe();

Alternatively, keep event: "*" if you need to react to non-heartbeat UPDATEs (e.g., name changes), but the current filter already handles that case.


130-136: Consider removing sessionId from CreatePokerParticipantInput to avoid redundancy.

The mutation accepts both sessionId (line 134) and input: CreatePokerParticipantInput (line 135), but CreatePokerParticipantInput (see types.ts lines 134–140) also includes a sessionId field. This duplication is unnecessary and could lead to inconsistencies if the two values differ.

Solution: Update the type definition to omit sessionId from CreatePokerParticipantInput for the mutation, or adjust the input to only contain participant-specific fields:

     mutationFn: ({
       sessionId,
       input,
     }: {
       sessionId: string;
-      input: CreatePokerParticipantInput;
+      input: Omit<CreatePokerParticipantInput, "sessionId">;
     }) => joinPokerSession(sessionId, input),

Then ensure callers don't include sessionId in the input object.

Alternatively, if you prefer to keep CreatePokerParticipantInput unchanged, document that the mutation will use the top-level sessionId parameter and ignore input.sessionId.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6e073a and 4970241.

📒 Files selected for processing (3)
  • src/components/poker/ParticipantStatus.tsx (1 hunks)
  • src/hooks/__tests__/use-poker-participants.test.ts (1 hunks)
  • src/hooks/use-poker-participants.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use the @/* path alias for imports from ./src/*
Import Supabase clients from src/lib/supabase/{client.ts,server.ts} instead of instantiating ad-hoc clients

Files:

  • src/hooks/use-poker-participants.ts
  • src/hooks/__tests__/use-poker-participants.test.ts
  • src/components/poker/ParticipantStatus.tsx
src/{app,components}/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use the "use client" directive only in components that require client-side interactivity

Files:

  • src/components/poker/ParticipantStatus.tsx
🧬 Code graph analysis (3)
src/hooks/use-poker-participants.ts (2)
src/lib/poker/types.ts (2)
  • PokerParticipant (66-77)
  • CreatePokerParticipantInput (135-141)
src/lib/poker/actions.ts (2)
  • getSessionParticipants (379-394)
  • joinPokerSession (311-376)
src/hooks/__tests__/use-poker-participants.test.ts (2)
src/lib/poker/types.ts (1)
  • PokerParticipant (66-77)
src/hooks/use-poker-participants.ts (4)
  • useSessionParticipants (49-113)
  • useJoinPokerSession (126-192)
  • useParticipantCount (208-211)
  • pokerParticipantKeys (22-26)
src/components/poker/ParticipantStatus.tsx (4)
src/lib/poker/types.ts (1)
  • PokerStory (52-64)
src/hooks/use-poker-participants.ts (1)
  • useSessionParticipants (49-113)
src/hooks/use-poker-votes.ts (1)
  • useStoryVotes (25-38)
src/lib/utils.ts (1)
  • cn (4-6)
🔇 Additional comments (6)
src/hooks/__tests__/use-poker-participants.test.ts (1)

1-328: LGTM! Comprehensive test coverage for poker participant hooks.

The test suite thoroughly covers all public APIs from the hooks module:

  • useSessionParticipants: success, empty list, errors, and disabled query behavior
  • useJoinPokerSession: success flow with correct payload validation and error handling
  • useParticipantCount: correct counts across various data states
  • pokerParticipantKeys: query key generation

Mock setup is appropriate, QueryClient configuration disables retries for predictable test execution, and assertions validate both data and API call contracts.

src/components/poker/ParticipantStatus.tsx (2)

106-113: LGTM! Clean helper for extracting initials.

The logic correctly splits names, maps to first characters, uppercases, and limits to two characters.


120-305: LGTM! Well-structured UI with clear participant grouping and status indicators.

The render logic effectively separates loading, waiting-for-votes banner, all-voted indicator, voters grid, observers section, and empty state. Styling differentiates voted vs. waiting participants, and badges/icons provide clear visual cues. Once the presence tracking is fixed, this UI will correctly reflect individual participant online status.

src/hooks/use-poker-participants.ts (3)

208-211: LGTM! Correct derivation of participant count.

useParticipantCount correctly reuses useSessionParticipants and derives the count from the data, avoiding duplicate subscriptions. The note in the JSDoc (lines 194–198) appropriately warns consumers about the realtime subscription overhead if they only need a count.


21-26: LGTM! Clean query key factory.

The key structure is hierarchical and follows React Query best practices for granular invalidation.


137-191: LGTM! Robust optimistic update with proper rollback and toasts.

The mutation correctly cancels in-flight queries, snapshots previous state, applies an optimistic update, rolls back on error, and invalidates on settlement. Toast notifications provide good user feedback.

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.

[EPIC-003] Story 4: Real-time Voting Status (Supabase)

2 participants