Skip to content

Conversation

@Roubean1
Copy link
Contributor

@Roubean1 Roubean1 commented Sep 10, 2025

Summary by CodeRabbit

  • New Features

    • Vote timestamps added and shown as relative times.
    • Voter search with highlighting and auto-expanding matching options.
    • Collapsible per-option voter lists prioritized by matches.
    • Total votes badge and vote distribution chart.
    • Avatars with graceful fallbacks in voter lists.
  • UI/UX

    • Updated section headings and improved no-votes empty state.
  • Chores

    • Added avatar UI dependency.
  • Tests

    • Updated tests to include vote timestamps in payloads.

@Roubean1 Roubean1 linked an issue Sep 10, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Sep 10, 2025

Warning

Rate limit exceeded

@Roubean1 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 4 minutes and 49 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 1f52a0d and c4d4c7e.

📒 Files selected for processing (1)
  • web/src/App.tsx (1 hunks)

Walkthrough

Adds a required timestamp string to Vote types (backend and frontend), attaches timestamp: new Date().toISOString() when creating votes, updates tests/fixtures, converts PollDetail to a named export and refactors its UI/data handling (voter search, lists, relative times, memoized metrics), and adds Radix-based Avatar components and dependency.

Changes

Cohort / File(s) Summary
Backend: vote creation & types
functions/src/handlers/voteHandler.ts, functions/src/types/poll.ts
Adds timestamp: string to created Vote objects in the vote handler and makes timestamp a required string property in the backend Vote type.
Backend tests (fixtures & expectations)
functions/src/tests/*
functions/src/tests/voteHandler.test.ts, functions/src/tests/pollResult.test.ts, functions/src/tests/pollService.test.ts, functions/src/tests/userVotesButtonHandler.test.ts
Updates test fixtures and mock votes to include timestamp (ISO string); adjusts expectations to assert timestamp (uses expect.any(String) where applicable) and normalizes timeStamptimestamp.
Frontend types
web/src/types/poll.tsx
Adds required timestamp: string to the frontend Vote interface to match backend payloads.
PollDetail UI refactor & imports
web/src/components/pollDetail.tsx, web/src/App.tsx
Converts PollDetail from default to named export; subscribes to poll doc via Firestore onSnapshot, normalizes vote.timestamp, adds voter search/highlighting, per-option collapsible voter lists, sorting, periodic relative-time refresh, memoized voteCounts/chartData/totalVotes, avatar usage, and updated UI structure/text; updates imports in App.tsx.
Avatar UI + dependency
web/src/components/ui/avatar.tsx, web/package.json
Adds Radix-based Avatar, AvatarImage, AvatarFallback components and adds @radix-ui/react-avatar dependency to web/package.json.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant C as Client (PollDetail)
  participant BE as Backend (voteHandler)
  participant DB as Firestore

  U->>C: Click "Vote" (optionId)
  C->>BE: handleVoteAction(pollId, optionId, user)
  BE->>BE: Build Vote { userId, optionId, timestamp = ISO }
  BE->>DB: Append vote to polls/{pollId}
  DB-->>C: onSnapshot emits updated poll document
  C->>C: Normalize votes (parse vote.timestamp → Date)
  C->>C: Recompute voteCounts, chartData, totalVotes
  C->>U: Render updated counts, chart, voter lists, relative times
Loading
sequenceDiagram
  autonumber
  participant C as PollDetail Component
  participant DB as Firestore
  actor U as User

  C->>DB: onSnapshot(doc(polls/{pollId}))
  DB-->>C: Poll data (options, votes with timestamp)
  C->>C: useMemo -> voteCounts, chartData, totalVotes
  U->>C: Search / toggle expand / change sort
  C->>C: Filter/highlight voters, expand matching options
  Note over C: Timer every 10s updates relative time labels
  C->>U: Render avatars, voter timestamps, badges, chart
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "40 improve poll detail" is related to the changes (it signals poll detail improvements) so it communicates the primary intent, but it includes an extraneous numeric prefix ("40") likely from the branch/issue and uses informal/lowercase phrasing that reduces clarity. It therefore identifies the main change but could be cleaned up for readability and consistency with conventional PR titles.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

I thump a timestamp on each little hop,
Votes with clocks make data pop.
Avatars peek, lists unfold,
Search and sort and tales retold.
Hooray — small hops make polling stop! 🐇✨

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 40-improve-poll-detail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@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: 5

Caution

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

⚠️ Outside diff range comments (1)
functions/src/handlers/voteHandler.ts (1)

79-88: Prevent voting when option is missing/deleted

After notifying the user, execution continues and may still record a vote. Early-return here.

           if (!option || option.deleted) {
             log.warn(`Option not found or deleted: ${optionId}`);

             await client.chat.postEphemeral({
               channel: body.channel?.id ?? poll.channelId,
               user: userId,
               text: 'This option is no longer available for voting.',
             });
+            return;
           }
🧹 Nitpick comments (6)
web/src/types/poll.tsx (1)

27-31: Casing consistency (nit)

Consider timestamp rather than timeStamp for consistency. Only if feasible repo-wide.

functions/src/types/poll.ts (1)

26-30: LGTM: Optional timestamp on Vote

Optional ISO 8601 string works well for gradual rollout. Consider documenting the format in a JSDoc for clarity.

web/src/components/pollDetail.tsx (4)

9-10: Use AvatarFallback for robustness

Show initials when image isn’t available.

-import { Avatar,  AvatarImage } from '@/components/ui/avatar';
+import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';

401-404: Add avatar fallback content

Improves UX without user images.

-                <Avatar className="h-8 w-8">
-                  <AvatarImage src={profile_placeholder} alt="Default profile" />
-                </Avatar>
+                <Avatar className="h-8 w-8">
+                  <AvatarImage src={profile_placeholder} alt="Default profile" />
+                  <AvatarFallback>{voter.name?.[0] ?? '?'}</AvatarFallback>
+                </Avatar>

249-251: Created-at rendering may break for Firestore Timestamp

new Date(poll.createdAt) fails if createdAt is a Firestore Timestamp. Use a small helper to normalize.

Example helper (place near top):

function toDate(value: unknown): Date | undefined {
  if (!value) return undefined;
  if (value instanceof Date) return value;
  if (typeof value === 'string' || typeof value === 'number') return new Date(value);
  if (typeof (value as any)?.toDate === 'function') return (value as any).toDate();
  return undefined;
}

Then:

-  {poll.createdAt ? new Date(poll.createdAt).toLocaleString() : 'Unknown'}
+  {(() => { const d = toDate(poll.createdAt); return d ? d.toLocaleString() : 'Unknown'; })()}

461-463: Fix Tailwind arbitrary value syntax

fill-(--color-label) isn’t valid. Use fill-[var(--color-label)].

-                      className="fill-(--color-label)"
+                      className="fill-[var(--color-label)]"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14ec229 and 95bb769.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • web/src/assets/Profile_avatar_placeholder_large.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • functions/src/handlers/voteHandler.ts (1 hunks)
  • functions/src/types/poll.ts (1 hunks)
  • web/package.json (1 hunks)
  • web/src/App.tsx (1 hunks)
  • web/src/components/pollDetail.tsx (3 hunks)
  • web/src/components/ui/avatar.tsx (1 hunks)
  • web/src/types/poll.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
web/src/components/ui/avatar.tsx (1)
web/src/lib/utils.ts (1)
  • cn (4-6)
functions/src/handlers/voteHandler.ts (1)
functions/src/tests/voteHandler.test.ts (1)
  • pollId (80-133)
web/src/components/pollDetail.tsx (3)
functions/src/types/poll.ts (1)
  • Poll (3-15)
web/src/types/poll.tsx (1)
  • Poll (3-16)
functions/src/services/pollService.ts (1)
  • vote (10-71)
🔇 Additional comments (3)
web/package.json (1)

13-13: LGTM: Radix Avatar dependency added

Dependency addition is appropriate and scoped.

web/src/App.tsx (1)

2-2: Named export switch for PollDetail confirmed
No default imports of PollDetail remain in the codebase.

web/src/components/ui/avatar.tsx (1)

1-49: LGTM: Accessible, typed avatar wrappers

ForwardRef usage, class merging, and displayName preservation are solid.

Copy link

@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: 1

♻️ Duplicate comments (3)
web/src/components/pollDetail.tsx (3)

19-24: Make voter timestamp optional to avoid misrepresenting legacy votes

Keep timestamps undefined when missing and handle them in UI/sorts.

 interface VoterInfo {
   userId: string;
   name: string;
-  timestamp: Date;
+  timestamp?: Date;
   highlighted?: boolean;
 }

54-57: Don’t default missing vote.timeStamp to “now”; normalize safely (Firestore Timestamp/ISO/epoch)

Defaulting to current time skews chronology. Preserve undefined and coerce robustly.

-        const normalizedVotes = (data.votes ?? []).map((vote: BaseVote) => ({
-          ...vote,
-          timestamp: vote.timeStamp ? new Date(vote.timeStamp) : new Date(),
-        }));
+        const normalizedVotes = (data.votes ?? []).map((vote: BaseVote) => {
+          const ts: any = (vote as any).timeStamp;
+          const timestamp =
+            ts
+              ? typeof ts?.toDate === 'function'
+                ? ts.toDate()
+                : new Date(ts)
+              : undefined;
+          return { ...vote, timestamp };
+        });

194-203: Guard sorting when timestamps are optional

Prevent runtime errors and sort missing timestamps as oldest.

-      if (sortOrder === 'timestamp') {
-        return b.timestamp.getTime() - a.timestamp.getTime(); // nejnovější první
-      } else {
-        return a.name.localeCompare(b.name, 'cs', { sensitivity: 'base' }); // abecedně
-      }
+      if (sortOrder === 'timestamp') {
+        const aTime = a.timestamp ? a.timestamp.getTime() : Number.NEGATIVE_INFINITY;
+        const bTime = b.timestamp ? b.timestamp.getTime() : Number.NEGATIVE_INFINITY;
+        return bTime - aTime; // newest first
+      }
+      return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
🧹 Nitpick comments (5)
web/src/components/pollDetail.tsx (5)

185-192: Type the normalized vote shape instead of casting; allow optional timestamp

Avoid repeated casts and make intent explicit.

-    const voters = (poll.votes as (BaseVote & { timestamp: Date })[])
+    type NormalizedVote = BaseVote & { timestamp?: Date };
+    const voters = (poll.votes as NormalizedVote[])
       .filter((vote) => vote.optionId === optionId)
       .map((vote) => ({
         userId: vote.userId,
         name: users[vote.userId] || 'Unknown User',
-        timestamp: vote.timestamp,
+        timestamp: vote.timestamp,
         highlighted: highlightedVoters.has(vote.userId),
       }));

482-488: Fix invalid Tailwind class for label color

fill-(--color-label) is not a valid Tailwind arbitrary value; it won’t apply.

-                    <LabelList
+                    <LabelList
                       dataKey="optionId"
                       position="insideLeft"
                       offset={8}
-                      className="fill-(--color-label)"
+                      className="fill-[var(--color-label)]"
                       fontSize={12}
                     />

414-417: Prefer stable keys; drop index from key

User can’t vote twice for the same option; userId is sufficient and avoids reorder artifacts.

-                                key={`${voter.userId}-${index}`}
+                                key={voter.userId}

50-86: Optional: handle realtime listener errors

Add an error callback to avoid silent failures and to clear loading on errors.

-    const unsubscribe = onSnapshot(doc(db, 'polls', pollId), (docSnap) => {
+    const unsubscribe = onSnapshot(
+      doc(db, 'polls', pollId),
+      (docSnap) => {
         // existing success handler...
-    });
+      },
+      (err) => {
+        console.error('Realtime poll listener failed', err);
+        setLoading(false);
+      }
+    );

201-202: Use default locale in localeCompare

Hardcoding 'cs' may surprise non-Czech users; default respects user/browser locale.

-        return a.name.localeCompare(b.name, 'cs', { sensitivity: 'base' }); // abecedně
+        return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95bb769 and 08c6bab.

📒 Files selected for processing (1)
  • web/src/components/pollDetail.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/components/pollDetail.tsx (3)
functions/src/types/poll.ts (2)
  • Poll (3-15)
  • PollOption (19-24)
web/src/types/poll.tsx (1)
  • Poll (3-16)
functions/src/services/pollService.ts (1)
  • vote (10-71)

Copy link

@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

🧹 Nitpick comments (4)
functions/src/tests/voteHandler.test.ts (4)

114-114: Make expectation resilient; also validate timestamp parseability

Use objectContaining to avoid brittleness if the vote payload gains fields. Optionally assert the timestamp is parseable/ISO-like.

-expect(mockVote).toHaveBeenCalledWith(pollId, { userId, optionId, timeStamp: expect.any(String) });
+expect(mockVote).toHaveBeenCalledWith(
+  pollId,
+  expect.objectContaining({ userId, optionId, timeStamp: expect.any(String) })
+);

You can additionally check format/parseability:

const payload = mockVote.mock.calls[0][1];
expect(Date.parse(payload.timeStamp)).not.toBeNaN();

183-183: Same here: prefer objectContaining for forward-compatibility

Prevents test breakage if new fields are added to the vote payload.

-expect(mockVote).toHaveBeenCalledWith(pollId, { userId, optionId, timeStamp: expect.any(String) });
+expect(mockVote).toHaveBeenCalledWith(
+  pollId,
+  expect.objectContaining({ userId, optionId, timeStamp: expect.any(String) })
+);

239-239: Apply objectContaining for the rejected-vote case as well

Keeps the assertion robust while still ensuring timeStamp is present.

-expect(mockVote).toHaveBeenCalledWith(pollId, { userId, optionId, timeStamp: expect.any(String) });
+expect(mockVote).toHaveBeenCalledWith(
+  pollId,
+  expect.objectContaining({ userId, optionId, timeStamp: expect.any(String) })
+);

293-293: Use objectContaining in the maxVotes path too

Consistent, less fragile expectation across tests.

-expect(mockVote).toHaveBeenCalledWith(pollId, { userId, optionId, timeStamp: expect.any(String) });
+expect(mockVote).toHaveBeenCalledWith(
+  pollId,
+  expect.objectContaining({ userId, optionId, timeStamp: expect.any(String) })
+);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08c6bab and 5cd177f.

📒 Files selected for processing (1)
  • functions/src/tests/voteHandler.test.ts (4 hunks)

Copy link

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
functions/src/types/poll.ts (1)

1-31: Action required: add Vote.timestamp to tests/mocks or make Vote.timestamp optional

  • Multiple tests/fixtures construct Vote objects without timestamp (will fail type-check now that Vote.timestamp is required).
  • Affected files (examples): functions/src/tests/pollService.test.ts, functions/src/tests/pollResult.test.ts, functions/src/tests/userVotesButtonHandler.test.ts, functions/src/tests/voteHandler.test.ts, functions/src/tests/integration/voteHandling.integration.test.ts.
  • Fix options: (A) Add a timestamp string to Vote literals (e.g. new Date().toISOString() in factories or timestamp: expect.any(String) in expectations), or (B) change the Vote interface to timestamp?: string if timestamps are optional by design.
♻️ Duplicate comments (3)
web/src/types/poll.tsx (1)

27-31: Make timestamp optional to reflect legacy data and avoid unsafe casts

Legacy votes and some tests don’t include a timestamp. Requiring it forces downstream casts/workarounds and breaks type soundness.

 export interface Vote {
   userId: string;
   optionId: string;
-  timestamp: string;
+  timestamp?: string; // ISO 8601
 }
functions/src/types/poll.ts (1)

26-30: Don’t make timestamp required server-side either

Firestore may contain votes without this field; marking it required makes Poll.votes lie about stored shape and can break tests/migrations. Prefer optional here too; new writes should still always set it in handlers.

 export interface Vote {
   userId: string;
   optionId: string;
-  timestamp: string;
+  timestamp?: string; // ISO 8601, set by handlers
 }
web/src/components/pollDetail.tsx (1)

185-203: Fix runtime crash: comparator calls .getTime() on possibly-undefined timestamps
Also remove the unsafe cast to (BaseVote & { timestamp: Date })[] and parse to Date locally.

-    const voters = (poll.votes as (BaseVote & { timestamp: Date })[])
+    const voters = (poll.votes ?? [])
       .filter((vote) => vote.optionId === optionId)
-      .map((vote) => ({
+      .map((vote) => ({
         userId: vote.userId,
         name: users[vote.userId] || 'Unknown User',
-        timestamp: vote.timestamp,
+        timestamp: vote.timestamp ? new Date(vote.timestamp) : undefined,
         highlighted: highlightedVoters.has(vote.userId),
       }));
 
     voters.sort((a, b) => {
       if (a.highlighted && !b.highlighted) return -1;
       if (!a.highlighted && b.highlighted) return 1;
 
       if (sortOrder === 'timestamp') {
-        return b.timestamp.getTime() - a.timestamp.getTime(); // nejnovější první
+        const aTime = a.timestamp ? a.timestamp.getTime() : -Infinity;
+        const bTime = b.timestamp ? b.timestamp.getTime() : -Infinity;
+        return bTime - aTime; // newest first; unknowns last
       } else {
         return a.name.localeCompare(b.name, 'cs', { sensitivity: 'base' }); // abecedně
       }
     });
🧹 Nitpick comments (2)
web/src/components/pollDetail.tsx (2)

50-86: Add onSnapshot error handler to surface failures and end loading state
Currently, permission/network errors will leave the UI in “Loading…”.

-    const unsubscribe = onSnapshot(doc(db, 'polls', pollId), (docSnap) => {
+    const unsubscribe = onSnapshot(
+      doc(db, 'polls', pollId),
+      (docSnap) => {
         if (docSnap.exists()) {
           const data = docSnap.data();
           // ...
         } else {
           setPoll(null);
         }
-      setLoading(false);
-    });
+        setLoading(false);
+      },
+      (err) => {
+        console.error('poll snapshot error', err);
+        setLoading(false);
+      }
+    );

505-511: Tailwind class likely invalid: use CSS var syntax for fill color
fill-(--color-label) won’t be recognized; prefer fill-[var(--color-label)] (or a theme token).

-                      className="fill-(--color-label)"
+                      className="fill-[var(--color-label)]"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5cd177f and 843b50e.

📒 Files selected for processing (5)
  • functions/src/handlers/voteHandler.ts (1 hunks)
  • functions/src/tests/voteHandler.test.ts (4 hunks)
  • functions/src/types/poll.ts (1 hunks)
  • web/src/components/pollDetail.tsx (3 hunks)
  • web/src/types/poll.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • functions/src/tests/voteHandler.test.ts
  • functions/src/handlers/voteHandler.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T08:27:56.594Z
Learnt from: maprokes
PR: profiq/slack-open-poll#44
File: web/src/types/poll.tsx:27-31
Timestamp: 2025-09-12T08:27:56.594Z
Learning: In the slack-open-poll codebase, timestamp fields should use lowercase naming convention ("timestamp") rather than camelCase ("timeStamp").

Applied to files:

  • web/src/types/poll.tsx
🧬 Code graph analysis (1)
web/src/components/pollDetail.tsx (4)
functions/src/types/poll.ts (1)
  • Poll (3-15)
web/src/types/poll.tsx (1)
  • Poll (3-16)
functions/src/services/pollService.ts (1)
  • vote (10-71)
web/src/App.tsx (1)
  • App (8-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Integration Tests functions
🔇 Additional comments (3)
web/src/types/poll.tsx (1)

27-31: Good: lowercase field name matches codebase convention

Naming aligns with the “timestamp” convention from prior feedback.

web/src/components/pollDetail.tsx (2)

159-180: LGTM: robust relative time formatter
Handles Firestore Timestamp and Date safely with sensible fallbacks.


26-26: Resolved — named export verified; no changes needed.
web/src/components/pollDetail.tsx exports PollDetail and imports already use named import (e.g. web/src/App.tsx: import {PollDetail} from "@/components/pollDetail").

Copy link

@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

Caution

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

⚠️ Outside diff range comments (1)
functions/src/tests/pollService.test.ts (1)

220-231: Flaky toggle: transaction.get returns the same state for both calls

Second call still reads an empty votes array, so service can’t infer a prior vote and may not remove it. Return updated state on the second get.

-      const pollWithMultipleVotes = { ...mockPoll, multiple: true, votes: [], maxVotes: 2 };
-      mockTransaction.get.mockResolvedValue({
-        data: () => pollWithMultipleVotes,
-      });
+      const pollWithMultipleVotes = { ...mockPoll, multiple: true, votes: [], maxVotes: 2 };
+      mockTransaction.get
+        .mockResolvedValueOnce({ data: () => pollWithMultipleVotes })
+        .mockResolvedValueOnce({ data: () => ({ ...pollWithMultipleVotes, votes: [mockVote] }) });
🧹 Nitpick comments (4)
functions/src/tests/pollService.test.ts (2)

90-104: Prefer fake timers over spying on Date.prototype; avoid restoreAllMocks here

Spying on Date.prototype is global and vi.restoreAllMocks() can accidentally undo other mocks. Use fake timers to freeze time locally.

-      // Mock the Date.toISOString for consistent testing
-      const mockDate = '2023-01-01T00:00:00.000Z';
-      vi.spyOn(global.Date.prototype, 'toISOString').mockReturnValue(mockDate);
+      // Freeze time for consistent testing
+      const mockDate = '2023-01-01T00:00:00.000Z';
+      vi.useFakeTimers();
+      vi.setSystemTime(new Date(mockDate));
@@
-      vi.restoreAllMocks();
+      vi.useRealTimers();

113-113: Naming consistency: getById

Nit: keep describe block name aligned with the method casing.

-  describe('getbyId', () => {
+  describe('getById', () => {
functions/src/tests/userVotesButtonHandler.test.ts (1)

36-38: Use a stable timestamp constant for fixtures

Minor: replace repeated new Date().toISOString() with a fixed const ts = '2023-01-01T00:00:00.000Z' to reduce noise and improve determinism. No behavior change.

functions/src/tests/pollResult.test.ts (1)

30-32: Deduplicate timestamps in votes

Use a single constant (or fake timers) for all vote timestamps to keep fixtures concise and stable.

Example (add near the top of the file):

const ts = '2023-01-01T00:00:00.000Z';

Then replace new Date().toISOString() with ts in the shown ranges.

Also applies to: 48-51, 72-75, 97-99

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 843b50e and ace99cd.

📒 Files selected for processing (3)
  • functions/src/tests/pollResult.test.ts (4 hunks)
  • functions/src/tests/pollService.test.ts (7 hunks)
  • functions/src/tests/userVotesButtonHandler.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
functions/src/tests/pollService.test.ts (1)
functions/src/types/poll.ts (1)
  • Vote (26-30)
functions/src/tests/userVotesButtonHandler.test.ts (1)
functions/src/tests/voteHandler.test.ts (1)
  • pollId (135-198)

Copy link

@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: 5

♻️ Duplicate comments (1)
web/src/components/pollDetail.tsx (1)

54-57: Fix legacy timestamp normalization (bug: same key used twice).

vote?.timestamp ?? vote?.timestamp never checks legacy timeStamp. Normalize to lowercase timestamp while accepting legacy timeStamp.

-        const normalizedVotes = (data.votes ?? []).map((vote: BaseVote) => {
-          const ts = (vote?.timestamp ?? vote?.timestamp) as string | undefined;
-          return { ...vote, timestamp: ts };
-        });
+        const normalizedVotes = (data.votes ?? []).map((vote: BaseVote | any) => {
+          const ts = (vote?.timestamp ?? vote?.timeStamp) as string | undefined;
+          return { ...vote, timestamp: ts };
+        });
🧹 Nitpick comments (6)
web/src/components/pollDetail.tsx (6)

159-180: Optional: make getRelativeTime accept string too.

This keeps the helper robust if a string slips through in the future.

-  const getRelativeTime = (timestamp: Timestamp | Date | undefined): string => {
+  const getRelativeTime = (timestamp: Timestamp | Date | string | undefined): string => {
     if (!timestamp) return 'Unknown time';
 
     let date: Date;
-    if (timestamp instanceof Date) {
+    if (timestamp instanceof Date) {
       date = timestamp;
-    } else {
+    } else if (typeof timestamp === 'string') {
+      const d = new Date(timestamp);
+      if (isNaN(d.getTime())) return 'Unknown time';
+      date = d;
+    } else {
       date = timestamp.toDate();
     }

201-202: Locale hardcoded to 'cs'.

Likely unintended; prefer default or a configurable locale.

-        return a.name.localeCompare(b.name, 'cs', { sensitivity: 'base' }); // abecedně
+        return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });

439-440: Use stable keys (avoid index).

Prevents unnecessary re-renders on reorder.

-                              <div
-                                key={`${voter.userId}-${index}`}
+                              <div
+                                key={voter.userId}

286-301: Tighten badges: hide Max votes for single-choice.

Avoids “Max votes: 1” noise when multiple=false.

-              <Badge variant="outline">Max votes: {poll.maxVotes ?? '-'}</Badge>
+              {poll.multiple && (
+                <Badge variant="outline">Max votes: {poll.maxVotes ?? '-'}</Badge>
+              )}

136-145: Optional UX: collapse auto-expanded options when clearing search.

Currently, clearing the query keeps prior expansions.

   if (!searchQuery.trim()) {
-      setHighlightedVoters(new Set());
-      return;
+      setHighlightedVoters(new Set());
+      setExpandedOptions(new Set()); // collapse
+      return;
   }

9-9: Minor: extra space in import.

Style nit only.

-import { Avatar,  AvatarImage } from '@/components/ui/avatar';
+import { Avatar, AvatarImage } from '@/components/ui/avatar';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ace99cd and 39c4540.

📒 Files selected for processing (1)
  • web/src/components/pollDetail.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T08:27:56.594Z
Learnt from: maprokes
PR: profiq/slack-open-poll#44
File: web/src/types/poll.tsx:27-31
Timestamp: 2025-09-12T08:27:56.594Z
Learning: In the slack-open-poll codebase, timestamp fields should use lowercase naming convention ("timestamp") rather than camelCase ("timeStamp").

Applied to files:

  • web/src/components/pollDetail.tsx
🧬 Code graph analysis (1)
web/src/components/pollDetail.tsx (3)
functions/src/types/poll.ts (1)
  • Poll (3-15)
web/src/types/poll.tsx (1)
  • Poll (3-16)
functions/src/services/pollService.ts (1)
  • vote (10-71)
🔇 Additional comments (1)
web/src/components/pollDetail.tsx (1)

50-74: Sanity-check createdBy/channelId type narrowing — OK.

functions/src/types/poll.ts declares createdBy and channelId as string and tests mock them as strings; the runtime ref-or-string narrowing in web/src/components/pollDetail.tsx is defensive and safe — either keep the check or normalize writes to store strings consistently.

Copy link

@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 (1)
web/src/components/pollDetail.tsx (1)

510-516: Invalid Tailwind class for SVG fill; use var() form

fill-(--color-label) won’t compile; use fill-[var(--color-label)].

-                      className="fill-(--color-label)"
+                      className="fill-[var(--color-label)]"
🧹 Nitpick comments (4)
web/src/components/pollDetail.tsx (4)

50-86: Add error handling to onSnapshot

Silent failures leave the UI in “Loading…”. Pass an error callback and surface/log it.

-    const unsubscribe = onSnapshot(doc(db, 'polls', pollId), (docSnap) => {
+    const unsubscribe = onSnapshot(
+      doc(db, 'polls', pollId),
+      (docSnap) => {
         // ...existing success path...
-      } else {
+      } else {
         setPoll(null);
-      }
-
-      setLoading(false);
-    });
+      }
+      setLoading(false);
+    },
+      (err) => {
+        console.error('poll onSnapshot error', err);
+        setLoading(false);
+      }
+    );

87-104: Optional: handle errors for users/channels subscriptions

Parity with the poll subscription avoids silent failures and aids debugging.

-    const unsubscribeUsers = onSnapshot(collection(db, 'users_list'), (querySnapshot) => {
+    const unsubscribeUsers = onSnapshot(
+      collection(db, 'users_list'),
+      (querySnapshot) => {
         // ...
-      });
+      },
+      (err) => console.error('users_list onSnapshot error', err)
+    );
 
-    const unsubscribeChannels = onSnapshot(collection(db, 'channels_list'), (querySnapshot) => {
+    const unsubscribeChannels = onSnapshot(
+      collection(db, 'channels_list'),
+      (querySnapshot) => {
         // ...
-      });
+      },
+      (err) => console.error('channels_list onSnapshot error', err)
+    );

Also applies to: 96-104


454-456: Add AvatarFallback for robustness (no image/network failures)

Improves UX when the placeholder fails or when you later switch to real images.

-                                <Avatar className="h-8 w-8">
-                                  <AvatarImage src={profile_placeholder} alt="Default profile" />
-                                </Avatar>
+                                <Avatar className="h-8 w-8">
+                                  <AvatarImage src={profile_placeholder} alt="Default profile" />
+                                  <span className="sr-only">User avatar</span>
+                                </Avatar>

Add import:

// at top
import { Avatar, AvatarImage } from '@/components/ui/avatar';

444-445: Prefer a more stable key

Reduce potential key collisions by incorporating timestamp when available.

-                              <div
-                                key={`${voter.userId}-${index}`}
+                              <div
+                                key={`${voter.userId}-${voter.timestamp ? voter.timestamp.getTime() : index}`}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39c4540 and 1f52a0d.

📒 Files selected for processing (1)
  • web/src/components/pollDetail.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-12T11:53:22.148Z
Learnt from: Roubean1
PR: profiq/slack-open-poll#44
File: web/src/components/pollDetail.tsx:54-57
Timestamp: 2025-09-12T11:53:22.148Z
Learning: The channelTimeStamp field in the Poll interface is separate from vote timestamps and serves a different purpose (tracking when the poll was created in the channel). Vote timestamps use lowercase "timestamp" while channelTimeStamp uses camelCase, and both are valid for their respective contexts.

Applied to files:

  • web/src/components/pollDetail.tsx
📚 Learning: 2025-09-12T11:53:22.148Z
Learnt from: Roubean1
PR: profiq/slack-open-poll#44
File: web/src/components/pollDetail.tsx:54-57
Timestamp: 2025-09-12T11:53:22.148Z
Learning: There are two distinct timestamp fields in the slack-open-poll codebase: 1) Vote timestamps use lowercase "timestamp" field to track when individual votes were cast, and 2) Channel timestamps use camelCase "channelTimeStamp" field to store the Slack message timestamp for poll message management. These serve different purposes and both naming conventions are correct for their respective contexts.

Applied to files:

  • web/src/components/pollDetail.tsx
📚 Learning: 2025-09-12T08:27:56.594Z
Learnt from: maprokes
PR: profiq/slack-open-poll#44
File: web/src/types/poll.tsx:27-31
Timestamp: 2025-09-12T08:27:56.594Z
Learning: In the slack-open-poll codebase, timestamp fields should use lowercase naming convention ("timestamp") rather than camelCase ("timeStamp").

Applied to files:

  • web/src/components/pollDetail.tsx
🧬 Code graph analysis (1)
web/src/components/pollDetail.tsx (3)
functions/src/types/poll.ts (2)
  • Poll (3-15)
  • PollOption (19-24)
web/src/types/poll.tsx (1)
  • Poll (3-16)
functions/src/services/pollService.ts (1)
  • vote (10-71)
🔇 Additional comments (4)
web/src/components/pollDetail.tsx (4)

185-195: Good: safe parsing and sorting with optional timestamps

Converts to Date with invalid-guard and sorts using -Infinity fallback. Prevents crashes and maintains order.

Also applies to: 197-209


226-233: Good: exclude deleted options from chart data

Aligns the visualization with visible options only.


237-255: Good: robust date formatter

Handles Firestore Timestamp, Date, ISO/epoch, and invalid inputs consistently.


54-57: Incorrect — no legacy timeStamp found; simplify normalization

Both Vote definitions (functions/src/types/poll.ts:29 and web/src/types/poll.tsx:30) declare timestamp (no timeStamp), so (vote?.timestamp ?? vote?.timestamp) is a redundant typo — replace with vote?.timestamp (only add a vote?.timeStamp fallback if you intentionally expect legacy external payloads).

Likely an incorrect or invalid review comment.

@Roubean1 Roubean1 force-pushed the 40-improve-poll-detail branch from ae96d0c to c4d4c7e Compare September 12, 2025 12:45
@Roubean1 Roubean1 merged commit 875ee5f into main Sep 12, 2025
8 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Sep 12, 2025
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.

Enhance Poll Detail View

3 participants