Skip to content

feat: enhance polling functionality with voting transparency on the governance page#683

Open
ibsule wants to merge 6 commits into
livepeer:mainfrom
ibsule:governance-vote-transparency
Open

feat: enhance polling functionality with voting transparency on the governance page#683
ibsule wants to merge 6 commits into
livepeer:mainfrom
ibsule:governance-vote-transparency

Conversation

@ibsule

@ibsule ibsule commented May 27, 2026

Copy link
Copy Markdown

Description

This PR introduces a new poll vote transparency experience on the governance voting page through the following:

  • Adds a full PollVote UI flow (desktop/mobile vote tables, vote detail/popover views) so users can inspect who voted and how much stake was used.
  • Expands polling data/query coverage to include richer vote-event fields and wiring needed to render detailed vote data.
  • Updates the voting page/widget UX to make navigating between overview and vote details easier.

Why

Governance users need transparent, in-context access to vote details (who voted, vote stake, and related metadata) to better understand outcomes and build trust in the polling process without leaving the poll flow.

Type of Change

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation update
  • style: Code style/formatting changes (no logic changes)
  • refactor: Code refactoring (no behavior change)
  • perf: Performance improvement
  • test: Adding or updating tests
  • build: Build system or dependency changes
  • ci: CI/CD changes
  • chore: Other changes

Related Issue(s)

Related: #310
Closes: #482

Changes Made

  • Added a new PollVote feature set for deeper vote visibility on governance pages.
  • Introduced new components: PollVoteDetail, DesktopVoteTable, MobileVoteView, MobileVoteCards, PollVotePopover, and components/PollVote/index.tsx.
  • Updated data layer and queries (apollo/subgraph.ts, lib/api/polls.ts, queries/poll.graphql, new queries/voteEvents.graphql) to fetch richer vote details.
  • Enhanced pages/voting/[poll].tsx and components/PollVotingWidget/index.tsx to support improved vote UX/navigation.

Testing

  • Tested locally
  • Added/updated tests
  • All tests passing

Impact / Risk

Risk level: Low

Impacted areas: UI

Rollback plan: PR revert

Screenshots / Recordings (if applicable)

Before

image

After

image image

The ENS names are not displayed here due to my RPC urls setup, but in production (with the production RPC urls) they display just fine.

Summary by CodeRabbit

  • New Features

    • Enhanced vote display now shows full voter details, stake, transaction and timestamps.
    • Added responsive vote UI: desktop table, mobile cards, individual vote view, and vote-detail popover.
    • Added a new "Votes" tab and "View votes" CTA linking to votes section.
    • New voting-history query and client hooks to fetch vote events.
    • Exposed helper to parse poll text and extended vote support mappings.
  • Documentation

    • Added doc comment for an events query hook.

ibsule added 3 commits May 27, 2026 12:01
…overnance page

- Updated GraphQL queries to include additional fields for votes, such as choiceID, voter, voteStake, and nonVoteStake.
- Introduced a new `parsePollText` function to handle proposal parsing from IPFS.
- Enhanced the PollVotingWidget component to display a link to view votes and improved the layout for better user experience.
- Added a new PollVotingTable component for displaying detailed vote information.
- Updated the voting page to support tab navigation between overview and votes, improving accessibility to voting data.

These changes aim to provide a more comprehensive voting experience and better data handling for polls.
…te components

- Removed duplicate imports and organized import statements for clarity in DesktopVoteTable, index, MobileVoteCards, and MobileVoteView components.
- Ensured consistent formatting and spacing in various files to enhance readability.
- Updated GraphQL queries to maintain structure and improve maintainability.
- Minor adjustments to the PollVotingWidget and PollVotePopover components for better integration.
- Updated the PollVote components to include vote stake information, allowing for better transparency in voting data.
- Modified the `onSelect` function to pass `voteStake` along with the voter's address and ENS name.
- Introduced a `formatVoteStake` function to format and display vote stakes consistently across DesktopVoteTable, MobileVoteCards, and MobileVoteView components.
- Adjusted GraphQL queries and state management to accommodate the new vote stake data, improving overall functionality and user experience.
@ibsule ibsule requested a review from ECWireless as a code owner May 27, 2026 20:29
@vercel

vercel Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

@ibsule is attempting to deploy a commit to the Livepeer Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds vote-event querying and responsive vote UI for governance polls: GraphQL updates and generated Apollo hooks, desktop/mobile components to show votes and per-voter history, ENS resolution and vote decoration, and tab-driven page navigation (overview vs. votes).

Changes

Governance Voting Transparency

Layer / File(s) Summary
GraphQL schema and generated Apollo types
queries/poll.graphql, queries/voteEvents.graphql, apollo/subgraph.ts
Poll query now selects vote fields (choiceID, voter, voteStake, nonVoteStake) and a new voteEvents query is added. Apollo types and hooks are generated for the updated poll operation and the new vote-events operation.
Vote support mapping and poll text parsing
lib/api/types/votes.ts, lib/api/polls.ts
VOTING_SUPPORT_MAP extended to map PollChoice.Yes/No to POLL_VOTES entries; parsePollText helper added to fetch/parse IPFS poll proposals.
Vote display components
components/PollVote/DesktopVoteTable.tsx, components/PollVote/MobileVoteCards.tsx, components/PollVote/MobileVoteView.tsx, components/PollVote/PollVoteDetail.tsx, components/PollVote/PollVotePopover.tsx
New responsive components render vote lists: desktop table with columns (voter, support, stake, timestamp, tx), mobile cards and views, detail/timeline layouts, and a modal popover showing a voter's vote-event history.
Vote orchestration and data fetching
components/PollVote/index.tsx
useVotes and the PollVote index component query poll votes and vote events, resolve ENS names with an in-effect cache, decorate votes with latest matching event transaction/timestamp, paginate for mobile, and handle voter selection for the popover.
Page navigation and widget integration
pages/voting/[poll].tsx, components/PollVotingWidget/index.tsx
Voting page adds tab navigation (overview vs. votes) driven by URL query and computes votesTabHref; PollVotingWidget gains an optional votesTabHref prop and renders the "View votes" link accordingly.

Sequence Diagram

sequenceDiagram
  participant User
  participant Page as voting/[poll].tsx
  participant Widget as PollVotingWidget
  participant Table as PollVotingTable
  participant Apollo as Apollo Query
  participant Popover as PollVotePopover
  User->>Page: Load governance poll page
  Page->>Table: Render vote table (view=votes)
  Table->>Apollo: useQuery(poll votes + voteEvents)
  Apollo-->>Table: votes with ENS, tx hash, timestamp
  Table->>Page: Render desktop table or mobile cards
  User->>Table: Click voter row/card
  Table->>Popover: Show voter's vote history
  Popover->>Apollo: useVoteEventsQuery(voter filter)
  Apollo-->>Popover: Sorted vote events
  Popover->>Page: Render PollVoteDetail list
  User->>Page: Click "View votes" link in widget
  Widget->>Page: Navigate to view=votes
Loading

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding voting transparency to the governance page's polling functionality.
Description check ✅ Passed The description fully addresses the template requirements with clear context, checked feature/refactor types, related/closed issues, comprehensive changes list, testing confirmation, risk assessment, and supporting screenshots.
Linked Issues check ✅ Passed The PR successfully implements all primary objectives from issue #482: adds voting transparency to governance polls with desktop/mobile UI components, expands data queries for richer vote details, and reuses patterns from prior transparency work (#457).
Out of Scope Changes check ✅ Passed All changes are directly aligned with the stated objectives. The Apollo types, GraphQL queries, new components (PollVote suite), and page updates are all necessary to implement voting transparency on the governance page.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

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

Inline comments:
In `@components/PollVote/index.tsx`:
- Around line 114-118: The Index component currently ignores the error returned
by useVotes, causing request failures to be rendered as an empty “No votes
found” state; update Index to consume the error value returned from useVotes
(the error/pollError or pollVoteEventsError aggregated into error) and render an
explicit error state (message and optional retry UI) when error is truthy before
the empty-state logic, ensuring you still respect votesLoading/loading to show
spinners while loading and only show the empty-state when there is no error and
no votes.

In `@components/PollVote/MobileVoteView.tsx`:
- Around line 75-77: In MobileVoteView.tsx there are external anchor elements
that set target="_blank" (the one rendering
href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`} and the
other at the later occurrence) but do not include rel protection; update both
anchor elements (in the MobileVoteView component) to add rel="noopener
noreferrer" alongside target="_blank" to prevent reverse-tabnabbing and other
security issues.

In `@components/PollVote/PollVoteDetail.tsx`:
- Around line 80-82: Add rel="noopener noreferrer" to any anchor that opens in a
new tab to prevent reverse-tabnabbing; specifically update the anchor rendering
href={`/voting/${vote.poll.id}`} (inside the PollVoteDetail component) and the
other anchor instance around lines ~185-187 to include rel="noopener noreferrer"
alongside target="_blank" so both links use target="_blank" rel="noopener
noreferrer".

In `@components/PollVote/PollVotePopover.tsx`:
- Around line 41-43: PollVotePopover is currently counting votes by checking
choiceID === "0" and "1" which undercounts when events use enum keys like
PollChoice.Yes/PollChoice.No; update the counting to normalize each
voteEvent.choiceID via the existing POLL_VOTES/VOTING_SUPPORT_MAP semantics (or
a small helper normalizeChoiceID) and then compute totals by testing the
normalized support value (e.g., SUPPORT_FOR vs SUPPORT_AGAINST) rather than raw
"0"/"1" strings so voteEvents.map/ filter logic uses the canonical support
mapping for for/against/abstain.
- Around line 60-62: In PollVotePopover (the JSX anchor that renders
href={`https://explorer.livepeer.org/accounts/${voter}/delegating`} and uses
target="_blank"), add the rel="noopener noreferrer" attribute to the anchor
element to prevent reverse-tabnabbing; update the anchor in the PollVotePopover
component where the external account link is created to include rel="noopener
noreferrer".

In `@lib/api/polls.ts`:
- Around line 64-69: parsePollText currently calls catIpfsJson(proposal) and
parsePollIpfs without handling IPFS/network errors, so wrap the fetch and parse
in a try/catch inside parsePollText (the function returning Promise<Fm | null)
and return null on any failure; ensure you call catIpfsJson<IpfsPoll>(proposal)
and then parsePollIpfs(ipfsObject) inside the try, and in the catch optionally
log the error (e.g., via console.error or existing logger) before returning null
to avoid unhandled promise rejections.

In `@pages/voting/`[poll].tsx:
- Around line 314-316: The current implementation hides tab panes with CSS
(using view) but keeps both component trees mounted so voteContent() still
executes; change to conditional JSX rendering so only the active pane mounts by
using a state/derived variable (e.g., safeView or view) to choose between
rendering the Overview component and the Votes component (replace the Box
wrappers that use display toggles with conditional expressions like safeView ===
"overview" ? <Overview .../> : <Votes .../>), and ensure voteContent() is only
called/used inside the Votes branch so expensive work runs only when Votes is
active.
- Line 55: The query-derived view value is not validated: normalize it by
creating a safeView (e.g., const safeView = view === "votes" ? "votes" :
"overview") and use safeView everywhere the component reads tab state or
conditionally renders (replace uses of view in tab state initialization and the
conditional rendering blocks around the overview/votes content, e.g., where view
is referenced at the tab state and the render checks currently at lines ~315 and
~469); this ensures any unsupported query value falls back to "overview" and
prevents empty main content.

In `@queries/voteEvents.graphql`:
- Around line 1-6: The voteEvents query call in
components/PollVote/PollVotePopover.tsx is missing a bounded `first` variable
and can return unbounded results; update the useVoteEventsQuery invocation in
PollVotePopover (the hook call) to pass a safe limit (e.g., `first: 200`)
consistent with components/PollVote/index.tsx, or alternatively set a default in
the GraphQL definition by changing `$first: Int` to `$first: Int = 200` in
queries/voteEvents.graphql so the query is always paginated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 346f92d3-4693-46b7-a689-465fd40a8b1e

📥 Commits

Reviewing files that changed from the base of the PR and between 9959381 and 9e80ed9.

📒 Files selected for processing (13)
  • apollo/subgraph.ts
  • components/PollVote/DesktopVoteTable.tsx
  • components/PollVote/MobileVoteCards.tsx
  • components/PollVote/MobileVoteView.tsx
  • components/PollVote/PollVoteDetail.tsx
  • components/PollVote/PollVotePopover.tsx
  • components/PollVote/index.tsx
  • components/PollVotingWidget/index.tsx
  • lib/api/polls.ts
  • lib/api/types/votes.ts
  • pages/voting/[poll].tsx
  • queries/poll.graphql
  • queries/voteEvents.graphql

Comment on lines +114 to +118
return {
votes,
loading: loading || votesLoading,
error: pollError || pollVoteEventsError,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Query errors are dropped and rendered as empty results

  • Problem: useVotes exposes error, but Index ignores it (Line 133) and can render “No votes found” on request failures.
  • Why it matters: Users see incorrect state, and operational issues are hidden.
  • Suggested fix: Consume error in Index and render a dedicated error state/message (or retry UI) before empty-state logic.

Also applies to: 133-177

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

In `@components/PollVote/index.tsx` around lines 114 - 118, The Index component
currently ignores the error returned by useVotes, causing request failures to be
rendered as an empty “No votes found” state; update Index to consume the error
value returned from useVotes (the error/pollError or pollVoteEventsError
aggregated into error) and render an explicit error state (message and optional
retry UI) when error is truthy before the empty-state logic, ensuring you still
respect votesLoading/loading to show spinners while loading and only show the
empty-state when there is no error and no votes.

Comment on lines +75 to +77
href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`}
target="_blank"
css={{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

target="_blank" links missing rel protection

  • Problem: External links opened in a new tab (Line 76, Line 146) omit rel="noopener noreferrer".
  • Why it matters: This allows reverse-tabnabbing and weakens security posture.
  • Suggested fix: Add rel="noopener noreferrer" to both links.

Also applies to: 145-147

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

In `@components/PollVote/MobileVoteView.tsx` around lines 75 - 77, In
MobileVoteView.tsx there are external anchor elements that set target="_blank"
(the one rendering
href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`} and the
other at the later occurrence) but do not include rel protection; update both
anchor elements (in the MobileVoteView component) to add rel="noopener
noreferrer" alongside target="_blank" to prevent reverse-tabnabbing and other
security issues.

Comment on lines +80 to +82
href={`/voting/${vote.poll.id}`}
target="_blank"
css={{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

New-tab links need rel="noopener noreferrer"

  • Problem: Links with target="_blank" (Line 81, Line 186) do not set rel.
  • Why it matters: Creates reverse-tabnabbing exposure.
  • Suggested fix: Add rel="noopener noreferrer" on both link instances.

Also applies to: 185-187

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

In `@components/PollVote/PollVoteDetail.tsx` around lines 80 - 82, Add
rel="noopener noreferrer" to any anchor that opens in a new tab to prevent
reverse-tabnabbing; specifically update the anchor rendering
href={`/voting/${vote.poll.id}`} (inside the PollVoteDetail component) and the
other anchor instance around lines ~185-187 to include rel="noopener noreferrer"
alongside target="_blank" so both links use target="_blank" rel="noopener
noreferrer".

Comment on lines +41 to +43
for: voteEvents.filter((v) => v.choiceID === "0").length,
against: voteEvents.filter((v) => v.choiceID === "1").length,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Vote summary can undercount due to hardcoded choice keys

  • Problem: Stats only count choiceID === "0" and "1" (Line 41-42).
  • Why it matters: If vote events come through PollChoice.Yes/No, totals for “For/Against” show incorrect values.
  • Suggested fix: Normalize with POLL_VOTES/VOTING_SUPPORT_MAP semantics (handle both "0"/"1" and Yes/No) before counting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/PollVotePopover.tsx` around lines 41 - 43,
PollVotePopover is currently counting votes by checking choiceID === "0" and "1"
which undercounts when events use enum keys like PollChoice.Yes/PollChoice.No;
update the counting to normalize each voteEvent.choiceID via the existing
POLL_VOTES/VOTING_SUPPORT_MAP semantics (or a small helper normalizeChoiceID)
and then compute totals by testing the normalized support value (e.g.,
SUPPORT_FOR vs SUPPORT_AGAINST) rather than raw "0"/"1" strings so
voteEvents.map/ filter logic uses the canonical support mapping for
for/against/abstain.

Comment on lines +60 to +62
href={`https://explorer.livepeer.org/accounts/${voter}/delegating`}
target="_blank"
css={{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing rel on external new-tab account link

  • Problem: Link opens with target="_blank" (Line 61) without rel="noopener noreferrer".
  • Why it matters: Reverse-tabnabbing risk.
  • Suggested fix: Add rel="noopener noreferrer".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/PollVotePopover.tsx` around lines 60 - 62, In
PollVotePopover (the JSX anchor that renders
href={`https://explorer.livepeer.org/accounts/${voter}/delegating`} and uses
target="_blank"), add the rel="noopener noreferrer" attribute to the anchor
element to prevent reverse-tabnabbing; update the anchor in the PollVotePopover
component where the external account link is created to include rel="noopener
noreferrer".

Comment thread lib/api/polls.ts
Comment on lines +64 to +69
export const parsePollText = async (proposal: string): Promise<Fm | null> => {
const ipfsObject = await catIpfsJson<IpfsPoll>(proposal);
const attributes = parsePollIpfs(ipfsObject);

return attributes;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unhandled IPFS fetch failures in parsePollText

  • Problem: catIpfsJson errors are not caught in parsePollText (Line 65), so failed IPFS reads reject the promise.
  • Why it matters: Consumers can hit unhandled async failures and lose vote-title rendering, especially on intermittent IPFS/network issues.
  • Suggested fix: Wrap the fetch/parse in try/catch and return null on failure.
Proposed fix
 export const parsePollText = async (proposal: string): Promise<Fm | null> => {
-  const ipfsObject = await catIpfsJson<IpfsPoll>(proposal);
-  const attributes = parsePollIpfs(ipfsObject);
-
-  return attributes;
+  try {
+    const ipfsObject = await catIpfsJson<IpfsPoll>(proposal);
+    return parsePollIpfs(ipfsObject);
+  } catch {
+    return null;
+  }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const parsePollText = async (proposal: string): Promise<Fm | null> => {
const ipfsObject = await catIpfsJson<IpfsPoll>(proposal);
const attributes = parsePollIpfs(ipfsObject);
return attributes;
};
export const parsePollText = async (proposal: string): Promise<Fm | null> => {
try {
const ipfsObject = await catIpfsJson<IpfsPoll>(proposal);
return parsePollIpfs(ipfsObject);
} catch {
return null;
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/api/polls.ts` around lines 64 - 69, parsePollText currently calls
catIpfsJson(proposal) and parsePollIpfs without handling IPFS/network errors, so
wrap the fetch and parse in a try/catch inside parsePollText (the function
returning Promise<Fm | null) and return null on any failure; ensure you call
catIpfsJson<IpfsPoll>(proposal) and then parsePollIpfs(ipfsObject) inside the
try, and in the catch optionally log the error (e.g., via console.error or
existing logger) before returning null to avoid unhandled promise rejections.

Comment thread pages/voting/[poll].tsx

const [pollData, setPollData] = useState<PollExtended | null>(null);
const { query } = router;
const view = query?.view?.toString().toLowerCase() || "overview";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize view to supported values before rendering.

  • Problem: Line 55 accepts any view query value, but Line 315 and Line 469 only render content for "overview" or "votes".
  • Why it matters: Invalid/stale URLs can render an empty main content area.
  • Suggested fix: Derive a safe value, e.g. const safeView = view === "votes" ? "votes" : "overview";, and use safeView for tab state and conditional rendering.

Also applies to: 315-316, 469-470

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

In `@pages/voting/`[poll].tsx at line 55, The query-derived view value is not
validated: normalize it by creating a safeView (e.g., const safeView = view ===
"votes" ? "votes" : "overview") and use safeView everywhere the component reads
tab state or conditionally renders (replace uses of view in tab state
initialization and the conditional rendering blocks around the overview/votes
content, e.g., where view is referenced at the tab state and the render checks
currently at lines ~315 and ~469); this ensures any unsupported query value
falls back to "overview" and prevents empty main content.

Comment thread pages/voting/[poll].tsx
Comment on lines +314 to +316
<Box css={{ marginTop: "$4" }}>
<Box css={{ display: view === "overview" ? "block" : "none" }}>
<Box

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid CSS-only tab hiding for expensive content.

  • Problem: Line 314–316 and Line 469–470 hide panes with display, but still mount both trees; voteContent() at Line 470 runs regardless of active tab.
  • Why it matters: The votes table path can do unnecessary render/data work when users stay on Overview.
  • Suggested fix: Render conditionally in JSX (safeView === "overview" ? <Overview/> : <Votes/>) so only the active pane mounts.

Also applies to: 469-470

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

In `@pages/voting/`[poll].tsx around lines 314 - 316, The current implementation
hides tab panes with CSS (using view) but keeps both component trees mounted so
voteContent() still executes; change to conditional JSX rendering so only the
active pane mounts by using a state/derived variable (e.g., safeView or view) to
choose between rendering the Overview component and the Votes component (replace
the Box wrappers that use display toggles with conditional expressions like
safeView === "overview" ? <Overview .../> : <Votes .../>), and ensure
voteContent() is only called/used inside the Votes branch so expensive work runs
only when Votes is active.

Comment on lines +1 to +6
query voteEvents($first: Int, $where: VoteEvent_filter) {
voteEvents(
orderBy: timestamp
orderDirection: desc
first: $first
where: $where

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that all usages of the voteEvents query provide a 'first' limit

rg -nP --type=ts --type=tsx -A5 'useVoteEventsQuery|voteEvents' -g '!queries/' -g '!apollo/'

Repository: livepeer/explorer

Length of output: 89


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find all JS/TS/TSX call sites that mention voteEvents usage/hooks.
rg -n --glob '*.{ts,tsx,js,jsx}' -S 'voteEvents' .

# 2) Narrow: find GraphQL variable usage patterns for `first` alongside the voteEvents query.
rg -n --glob '*.{ts,tsx,js,jsx}' -S '(first\s*:|first\s*=|variables\s*:\s*\{[^}]*first|first\s*\?)' .

# 3) Specifically look for a hook named like useVoteEventsQuery and inspect nearby code.
rg -n --glob '*.{ts,tsx}' -S 'useVoteEventsQuery' . -A30 -B10

# 4) If the query is imported as voteEvents.gql/graphQL, locate the import and inspect usage.
rg -n --glob '*.{ts,tsx,js,jsx}' -S 'from\s+[\'"].*voteEvents\.graphql[\'"]|voteEvents\.graphql' . -A20 -B5

Repository: livepeer/explorer

Length of output: 20603


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List all call sites of useVoteEventsQuery and inspect variables for `first`
rg -n --glob '*.{ts,tsx}' 'useVoteEventsQuery\s*\(' . -S | cut -d: -f1-2 | sort -u | while read -r file; do
  echo "==== $file ===="
  rg -n --glob "$file" 'useVoteEventsQuery\s*\(' . -S -A25 -B5
done

Repository: livepeer/explorer

Length of output: 98


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show voteEvents usages around useVoteEventsQuery(
rg -n --glob 'components/**/*.{ts,tsx,js,jsx}' -S 'useVoteEventsQuery\s*\(' components/PollVote -A25 -B5 || true

# Inspect the full variables object for PollVotePopover usage
sed -n '1,120p' components/PollVote/PollVotePopover.tsx

# Inspect generated hook defaults in apollo/subgraph.ts around useVoteEventsQuery
rg -n 'export function useVoteEventsQuery' apollo/subgraph.ts
sed -n '11180,11230p' apollo/subgraph.ts

Repository: livepeer/explorer

Length of output: 9372


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n --glob '*.{ts,tsx,js,jsx}' 'useVoteEventsQuery\s*\(\s*\{' . -S

Repository: livepeer/explorer

Length of output: 327


Add pagination (first) to the voteEvents query in PollVotePopover

  • Problem: queries/voteEvents.graphql leaves $first optional, and components/PollVote/PollVotePopover.tsx calls useVoteEventsQuery without providing first (while components/PollVote/index.tsx correctly sets first: 200).
  • Why it matters: voters with large vote histories can trigger large/unbounded responses, increasing latency and risking timeouts/perf issues.
  • Suggested fix: pass a bounded first in components/PollVote/PollVotePopover.tsx (e.g., first: 200, matching the other usage) or add a safe default for $first in queries/voteEvents.graphql (e.g., $first: Int = 200).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@queries/voteEvents.graphql` around lines 1 - 6, The voteEvents query call in
components/PollVote/PollVotePopover.tsx is missing a bounded `first` variable
and can return unbounded results; update the useVoteEventsQuery invocation in
PollVotePopover (the hook call) to pass a safe limit (e.g., `first: 200`)
consistent with components/PollVote/index.tsx, or alternatively set a default in
the GraphQL definition by changing `$first: Int` to `$first: Int = 200` in
queries/voteEvents.graphql so the query is always paginated.

- Renamed the "Stake Used" header to "Weight" for clarity.
- Updated the accessor ID from "stakeUsed" to "weight"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

♻️ Duplicate comments (9)
lib/api/polls.ts (1)

64-69: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle IPFS/parsing errors in parsePollText.

  • Problem: catIpfsJson/parsePollIpfs errors are not caught.
  • Why it matters: A transient IPFS failure can bubble up and break vote-detail rendering.
  • Suggested fix: Wrap the body in try/catch and return null on failure (optionally log once).
Proposed patch
 export const parsePollText = async (proposal: string): Promise<Fm | null> => {
-  const ipfsObject = await catIpfsJson<IpfsPoll>(proposal);
-  const attributes = parsePollIpfs(ipfsObject);
-
-  return attributes;
+  try {
+    const ipfsObject = await catIpfsJson<IpfsPoll>(proposal);
+    return parsePollIpfs(ipfsObject);
+  } catch {
+    return null;
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/api/polls.ts` around lines 64 - 69, parsePollText currently awaits
catIpfsJson and parsePollIpfs without handling failures, so any IPFS or parsing
error can propagate; wrap the body of parsePollText in a try/catch that catches
any thrown error from catIpfsJson or parsePollIpfs and returns null on failure
(optionally call a logger once with context like "parsePollText failed" and the
error), keeping the signature Promise<Fm | null> unchanged and ensuring any
caught error does not rethrow.
queries/voteEvents.graphql (1)

1-6: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a safe default for $first to prevent unbounded vote-event fetches.

  • Problem: $first is optional and this query forwards it directly; an existing consumer (components/PollVote/PollVotePopover.tsx) omits first.
  • Why it matters: Large voter histories can produce oversized responses and hurt latency/UI responsiveness.
  • Suggested fix: Set a default in the query signature ($first: Int = 200) and keep explicit first at callsites where needed.
#!/bin/bash
# Verify all voteEvents hook callsites provide a bounded `first`.
rg -n -C3 --type=ts --type=tsx 'useVoteEventsQuery\s*\('
rg -n -C3 --type=ts --type=tsx 'variables\s*:\s*\{[^}]*first'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@queries/voteEvents.graphql` around lines 1 - 6, The voteEvents GraphQL query
(query voteEvents) accepts an optional $first and can be called without a bound
(e.g., from components/PollVote/PollVotePopover.tsx via useVoteEventsQuery),
which risks huge responses; set a safe default by changing the query signature
to provide a default for $first (e.g., = 200) and leave explicit first values at
callsites that need different limits; after updating, verify consumers
(useVoteEventsQuery callsites) still behave correctly and add explicit first
where necessary.
components/PollVote/index.tsx (1)

133-177: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: useVotes returns an error value (Line 117), but the Index component ignores it (Line 133) and renders "No votes found" (Line 172-177) even when a request fails.

Why it matters: Users see misleading empty-state messages when GraphQL queries fail, masking real operational problems and preventing retry attempts.

Suggested fix: Consume the error from useVotes and render an explicit error state with a message (and optionally retry UI) before the empty-state logic.

🛠️ Proposed fix
-  const { votes, loading } = useVotes(pollId);
+  const { votes, loading, error } = useVotes(pollId);

   // ... existing loading spinner code ...

+  if (error) {
+    return (
+      <Flex
+        css={{
+          flexDirection: "column",
+          alignItems: "center",
+          gap: "$3",
+          marginTop: "$4",
+        }}
+      >
+        <Text css={{ color: "$tomato11", textAlign: "center" }}>
+          Failed to load votes. Please try again.
+        </Text>
+      </Flex>
+    );
+  }
+
   if (!votes.length)
     return (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/index.tsx` around lines 133 - 177, The Index component
currently destructures only { votes, loading } from useVotes but ignores the
returned error; update the destructuring to include error (e.g., const { votes,
loading, error } = useVotes(pollId)) and add an early render branch that checks
error and displays an explicit error state (a user-friendly message and optional
retry control) before the existing empty-state check that renders "No votes
found"; ensure this error branch runs when error is truthy and loading is false
so failed GraphQL requests show the error UI instead of the empty votes message.
pages/voting/[poll].tsx (2)

314-471: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: Both "Overview" and "Votes" content sections are rendered and mounted (Line 314-471), with only CSS display toggling visibility. This means voteContent() (Line 470) executes even when the Overview tab is active.

Why it matters: The votes table performs data fetching and rendering work unnecessarily when users stay on Overview, wasting resources and potentially slowing initial page load.

Suggested fix: Render conditionally in JSX so only the active tab's content mounts.

⚡ Proposed conditional rendering
-            <Box css={{ marginTop: "$4" }}>
-              <Box css={{ display: view === "overview" ? "block" : "none" }}>
+            <Box css={{ marginTop: "$4" }}>
+              {view === "overview" ? (
                 <Box
                   css={{
                     display: "grid",
                     // ... overview content ...
                   }}
                 >
                   <Stat ... />
                   <Stat ... />
                 </Box>
                 <Card ...>
                   <MarkdownRenderer>...</MarkdownRenderer>
                 </Card>
-              </Box>
-            </Box>
-
-            <Box css={{ display: view === "votes" ? "block" : "none" }}>
-              {voteContent()}
-            </Box>
+              ) : (
+                voteContent()
+              )}
+            </Box>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pages/voting/`[poll].tsx around lines 314 - 471, The Overview and Votes
blocks are always mounted and only hidden via CSS (so voteContent() runs even
when view !== "votes"); update the JSX to conditionally render those sections
instead of toggling CSS display—i.e., replace the Box that wraps the overview
content (the block using view === "overview" ? "block" : "none" which contains
the two Stat components and the Card/MarkdownRenderer/pollData.attributes) with
a conditional expression that only renders when view === "overview", and
likewise render voteContent() only when view === "votes" (so voteContent() is
called only when the votes view is active).

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

Problem: The view query parameter is not validated (Line 55). Any unsupported value (e.g., ?view=invalid) will result in both content sections being hidden (Line 315, 469), rendering an empty main area.

Why it matters: Users following stale or malformed URLs see a blank page instead of defaulting to the Overview tab.

Suggested fix: Normalize view to only "overview" or "votes" before using it.

✅ Proposed normalization
   const { query } = router;
-  const view = query?.view?.toString().toLowerCase() || "overview";
+  const rawView = query?.view?.toString().toLowerCase() || "overview";
+  const view = rawView === "votes" ? "votes" : "overview";

Then use view everywhere (tab state, conditional rendering).

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

In `@pages/voting/`[poll].tsx at line 55, The query-derived view value is not
validated and can be any string; change the normalization at the declaration
(the const view = ... line) to only allow "overview" or "votes" and default to
"overview" for any other value (e.g., convert toLowerCase(), then check against
the allowed set and fallback). Use that normalized view variable everywhere the
component reads tab state and in the conditional renders (the places currently
hiding content when view is invalid) so stale/malformed ?view params show the
Overview tab instead of a blank page.
components/PollVote/PollVotePopover.tsx (2)

60-61: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: External link with target="_blank" is missing rel="noopener noreferrer" (Line 60-61).

Why it matters: Creates a reverse-tabnabbing vulnerability where the Livepeer explorer page could manipulate the opener window.

Suggested fix: Add rel="noopener noreferrer" to the <Link> component.

🔒 Proposed fix
         <Link
           href={`https://explorer.livepeer.org/accounts/${voter}/delegating`}
           target="_blank"
+          rel="noopener noreferrer"
           css={{
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/PollVotePopover.tsx` around lines 60 - 61, In
PollVotePopover.tsx update the external Link to the Livepeer explorer (the Link
element whose href is
`https://explorer.livepeer.org/accounts/${voter}/delegating` and has
`target="_blank"`) to include the security attributes `rel="noopener
noreferrer"`; locate the Link in the PollVotePopover component and add the rel
attribute so the anchor prevents reverse-tabnabbing when opening the external
page.

41-42: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: Vote statistics hardcode choiceID === "0" and "1" (Line 41-42), but vote events may use PollChoice.Yes/PollChoice.No enum keys, causing incorrect totals.

Why it matters: The "For" and "Against" counts will undercount or show zero when events arrive with enum keys instead of string literals, breaking the voting history display.

Suggested fix: Normalize choiceID via VOTING_SUPPORT_MAP or POLL_VOTES before counting, so both "0"/"1" and Yes/No are recognized as equivalent.

🔧 Proposed fix
   const stats = React.useMemo(() => {
     if (!voteEvents.length) return null;
+    
+    // Normalize choice to support both "0"/"1" and Yes/No
+    const isFor = (choiceID: string) => choiceID === "0" || choiceID === "Yes";
+    const isAgainst = (choiceID: string) => choiceID === "1" || choiceID === "No";

     return {
       total: voteEvents.length,
-      for: voteEvents.filter((v) => v.choiceID === "0").length,
-      against: voteEvents.filter((v) => v.choiceID === "1").length,
+      for: voteEvents.filter((v) => isFor(v.choiceID)).length,
+      against: voteEvents.filter((v) => isAgainst(v.choiceID)).length,
     };
   }, [voteEvents]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/PollVotePopover.tsx` around lines 41 - 42, The current
tally uses literal checks choiceID === "0" / "1" which misses enum keys; update
the counting in PollVotePopover to normalize each voteEvent.choiceID via the
existing VOTING_SUPPORT_MAP (or POLL_VOTES) mapping before comparing, e.g., map
choiceID to its normalized support value (For/Against) then count by comparing
against the normalized keys; locate the code around voteEvents.filter(...) and
replace the direct string checks with a small normalization step that converts
enums like PollChoice.Yes/PollChoice.No and "0"/"1" into the same canonical
identifiers before computing the for/against totals.
components/PollVote/MobileVoteView.tsx (1)

75-76: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: External links with target="_blank" are missing rel="noopener noreferrer" (Line 75-76 for Livepeer explorer, Line 145-146 for Arbiscan).

Why it matters: This creates a reverse-tabnabbing security vulnerability where the opened page can access window.opener and potentially redirect the original page to a phishing site.

Suggested fix: Add rel="noopener noreferrer" to both <Link> components.

🔒 Proposed security fix
           <Link
             href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`}
             target="_blank"
+            rel="noopener noreferrer"
             css={{
           <Link
             href={`https://arbiscan.io/tx/${vote.transactionHash}`#eventlog``}
             target="_blank"
+            rel="noopener noreferrer"
             css={{

Also applies to: 145-146

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

In `@components/PollVote/MobileVoteView.tsx` around lines 75 - 76, The external
anchor/Link elements in MobileVoteView.tsx that open a new tab (the one with
href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`} and the
Arbiscan link) use target="_blank" but are missing rel="noopener noreferrer";
update both Link/anchor elements to include rel="noopener noreferrer" alongside
target="_blank" to prevent reverse-tabnabbing and ensure window.opener is not
exposed.
components/PollVote/PollVoteDetail.tsx (1)

80-81: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: Links with target="_blank" are missing rel="noopener noreferrer" (Line 80-81 in mobile layout, Line 185-186 in desktop layout).

Why it matters: Even for internal links, omitting rel with target="_blank" creates a security gap and can impact performance by allowing the opened tab to access window.opener.

Suggested fix: Add rel="noopener noreferrer" to both <Link> components.

🔒 Proposed fix
           <Link
             href={`/voting/${vote.poll.id}`}
             target="_blank"
+            rel="noopener noreferrer"
             css={{

(Apply the same change to the second occurrence at Line 185-186)

Also applies to: 185-186

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

In `@components/PollVote/PollVoteDetail.tsx` around lines 80 - 81, In
PollVoteDetail.tsx update both Link elements that open the poll in a new tab
(the mobile Link with href={`/voting/${vote.poll.id}`} and the desktop Link at
the second occurrence) to include rel="noopener noreferrer" alongside
target="_blank" to prevent window.opener access; locate the two Link components
in the PollVoteDetail component and add the rel attribute to each.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/PollVote/DesktopVoteTable.tsx`:
- Around line 51-60: Guard against a missing or null mapping returned by
VOTING_SUPPORT_MAP for row.original.choiceID before accessing support.style:
update the DesktopVoteTable rendering logic to check if support is truthy (from
VOTING_SUPPORT_MAP[row.original.choiceID]) and fall back to a default support
object or render a safe "Unknown" Badge when support is undefined/null, then use
that safe object when reading support.style for the Badge.
- Around line 77-79: The "Weight" column in DesktopVoteTable uses accessor
"voteStake" / id "weight" but lacks an explicit numeric sortType, so
react-table's default alphanumeric sort can misorder stake values; update the
column definition in DesktopVoteTable.tsx to add a sortType that compares
voteStake numerically (e.g., convert rowA.original.voteStake and
rowB.original.voteStake to numbers with parseFloat/Number and return their
difference, with a NaN-safe fallback) so sorting is based on numeric stake
rather than string ordering.

In `@components/PollVote/index.tsx`:
- Around line 121-208: Add a JSDoc comment block immediately above the Index
component declaration describing the component's purpose (renders desktop or
mobile vote views for a poll), list the props with types (PollVotingTableProps
and the pollId prop), and document the return value (React.ReactElement). Ensure
the JSDoc references the component name "Index" and mentions key behaviors such
as pagination, vote formatting via formatVoteStake, and that it opens
PollVotePopover for selected voters.
- Around line 30-119: The useVotes hook lacks JSDoc; add a JSDoc comment
immediately above the useVotes declaration that briefly describes the hook's
purpose (fetches and decorates votes and vote events for a given poll),
documents the parameter (pollId: string) and the return shape (votes:
PollVoteType[], loading: boolean, error: any), and mentions any important
behavior (it resolves ENS names via getEnsForVotes and derives
transactionHash/timestamp from vote events). Reference the hook name useVotes
and core behavior implemented by the decorateVotes async helper so reviewers can
locate the implementation.

In `@components/PollVote/MobileVoteView.tsx`:
- Around line 30-34: Add a JSDoc block above the exported MobileVoteView
function describing its purpose (renders the mobile view for a single poll vote)
and include `@param` entries for the MobileVoteViewProps fields (vote,
formatVoteStake, onSelect) and an `@returns` describing the React element;
reference the MobileVoteView function and the MobileVoteViewProps type so the
doc ties to the component signature. Ensure the description is a one- or
two-sentence summary and each `@param` briefly explains expected types/behavior
(e.g., vote object, formatVoteStake callback, onSelect handler).

In `@components/PollVote/PollVoteDetail.tsx`:
- Around line 21-23: Add a JSDoc block above the exported React component Index
describing the component's purpose and its props (PollVoteDetailProps) at
minimum; mention what the component renders (vote details), any important
behavior (uses POLL_VOTES via choiceID and local title state), and annotate the
function signature so the documentation satisfies the project's contributor
guidelines.

In `@components/PollVote/PollVotePopover.tsx`:
- Around line 19-26: Add JSDoc above the exported React component Index
describing its purpose and props; document that Index is a
React.FC<PollVotePopoverProps> that displays vote events for a given voter and
accepts props voter, ensName, and onClose, and mention side effects like
useVoteEventsQuery usage and loading state (isLoading). Ensure the JSDoc
includes a short description, `@param` entries for voter, ensName, and onClose,
and an `@returns` description for the rendered JSX so the component complies with
contributor documentation standards.

In `@components/PollVotingWidget/index.tsx`:
- Line 2: Remove the no-op empty import "import {} from
\"`@components/PollVote`\";" in components/PollVotingWidget/index.tsx; locate the
import statement (the empty destructuring import) and delete it, and if PollVote
functionality is actually required by this module, replace the empty import with
the correct named or default import (e.g., import PollVote or import {
SomeExport } from "`@components/PollVote`") after verifying usages of PollVote in
this file.

---

Duplicate comments:
In `@components/PollVote/index.tsx`:
- Around line 133-177: The Index component currently destructures only { votes,
loading } from useVotes but ignores the returned error; update the destructuring
to include error (e.g., const { votes, loading, error } = useVotes(pollId)) and
add an early render branch that checks error and displays an explicit error
state (a user-friendly message and optional retry control) before the existing
empty-state check that renders "No votes found"; ensure this error branch runs
when error is truthy and loading is false so failed GraphQL requests show the
error UI instead of the empty votes message.

In `@components/PollVote/MobileVoteView.tsx`:
- Around line 75-76: The external anchor/Link elements in MobileVoteView.tsx
that open a new tab (the one with
href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`} and the
Arbiscan link) use target="_blank" but are missing rel="noopener noreferrer";
update both Link/anchor elements to include rel="noopener noreferrer" alongside
target="_blank" to prevent reverse-tabnabbing and ensure window.opener is not
exposed.

In `@components/PollVote/PollVoteDetail.tsx`:
- Around line 80-81: In PollVoteDetail.tsx update both Link elements that open
the poll in a new tab (the mobile Link with href={`/voting/${vote.poll.id}`} and
the desktop Link at the second occurrence) to include rel="noopener noreferrer"
alongside target="_blank" to prevent window.opener access; locate the two Link
components in the PollVoteDetail component and add the rel attribute to each.

In `@components/PollVote/PollVotePopover.tsx`:
- Around line 60-61: In PollVotePopover.tsx update the external Link to the
Livepeer explorer (the Link element whose href is
`https://explorer.livepeer.org/accounts/${voter}/delegating` and has
`target="_blank"`) to include the security attributes `rel="noopener
noreferrer"`; locate the Link in the PollVotePopover component and add the rel
attribute so the anchor prevents reverse-tabnabbing when opening the external
page.
- Around line 41-42: The current tally uses literal checks choiceID === "0" /
"1" which misses enum keys; update the counting in PollVotePopover to normalize
each voteEvent.choiceID via the existing VOTING_SUPPORT_MAP (or POLL_VOTES)
mapping before comparing, e.g., map choiceID to its normalized support value
(For/Against) then count by comparing against the normalized keys; locate the
code around voteEvents.filter(...) and replace the direct string checks with a
small normalization step that converts enums like PollChoice.Yes/PollChoice.No
and "0"/"1" into the same canonical identifiers before computing the for/against
totals.

In `@lib/api/polls.ts`:
- Around line 64-69: parsePollText currently awaits catIpfsJson and
parsePollIpfs without handling failures, so any IPFS or parsing error can
propagate; wrap the body of parsePollText in a try/catch that catches any thrown
error from catIpfsJson or parsePollIpfs and returns null on failure (optionally
call a logger once with context like "parsePollText failed" and the error),
keeping the signature Promise<Fm | null> unchanged and ensuring any caught error
does not rethrow.

In `@pages/voting/`[poll].tsx:
- Around line 314-471: The Overview and Votes blocks are always mounted and only
hidden via CSS (so voteContent() runs even when view !== "votes"); update the
JSX to conditionally render those sections instead of toggling CSS display—i.e.,
replace the Box that wraps the overview content (the block using view ===
"overview" ? "block" : "none" which contains the two Stat components and the
Card/MarkdownRenderer/pollData.attributes) with a conditional expression that
only renders when view === "overview", and likewise render voteContent() only
when view === "votes" (so voteContent() is called only when the votes view is
active).
- Line 55: The query-derived view value is not validated and can be any string;
change the normalization at the declaration (the const view = ... line) to only
allow "overview" or "votes" and default to "overview" for any other value (e.g.,
convert toLowerCase(), then check against the allowed set and fallback). Use
that normalized view variable everywhere the component reads tab state and in
the conditional renders (the places currently hiding content when view is
invalid) so stale/malformed ?view params show the Overview tab instead of a
blank page.

In `@queries/voteEvents.graphql`:
- Around line 1-6: The voteEvents GraphQL query (query voteEvents) accepts an
optional $first and can be called without a bound (e.g., from
components/PollVote/PollVotePopover.tsx via useVoteEventsQuery), which risks
huge responses; set a safe default by changing the query signature to provide a
default for $first (e.g., = 200) and leave explicit first values at callsites
that need different limits; after updating, verify consumers (useVoteEventsQuery
callsites) still behave correctly and add explicit first where necessary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ab75bc9-eb5f-459e-8095-1b6a104c7660

📥 Commits

Reviewing files that changed from the base of the PR and between 9959381 and 488711c.

📒 Files selected for processing (13)
  • apollo/subgraph.ts
  • components/PollVote/DesktopVoteTable.tsx
  • components/PollVote/MobileVoteCards.tsx
  • components/PollVote/MobileVoteView.tsx
  • components/PollVote/PollVoteDetail.tsx
  • components/PollVote/PollVotePopover.tsx
  • components/PollVote/index.tsx
  • components/PollVotingWidget/index.tsx
  • lib/api/polls.ts
  • lib/api/types/votes.ts
  • pages/voting/[poll].tsx
  • queries/poll.graphql
  • queries/voteEvents.graphql

Comment on lines +51 to +60
const support = VOTING_SUPPORT_MAP[row.original.choiceID];

return (
<Box css={{ minWidth: 100 }}>
<Badge
size="1"
css={{
backgroundColor: support.style.backgroundColor,
color: support.style.color,
fontWeight: support.style.fontWeight,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against unknown/nullable choiceID before using support.style.

  • Problem: VOTING_SUPPORT_MAP[row.original.choiceID] can be undefined (API type allows null/optional choiceID), but the code immediately reads support.style.
  • Why it matters: One unexpected value crashes the votes table render.
  • Suggested fix: Add a safe fallback badge (e.g., Unknown) or default mapping before dereferencing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/DesktopVoteTable.tsx` around lines 51 - 60, Guard against
a missing or null mapping returned by VOTING_SUPPORT_MAP for
row.original.choiceID before accessing support.style: update the
DesktopVoteTable rendering logic to check if support is truthy (from
VOTING_SUPPORT_MAP[row.original.choiceID]) and fall back to a default support
object or render a safe "Unknown" Badge when support is undefined/null, then use
that safe object when reading support.style for the Badge.

Comment on lines +77 to +79
Header: "Weight",
accessor: "voteStake",
id: "weight",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect DataTable wrapper and local column config for sort behavior.
fd -i 'Table' components | xargs -I{} sh -c 'echo "== {} =="; sed -n "1,240p" "{}"'
rg -n -C4 --type=ts --type=tsx 'sortType|initialState|useSortBy|accessor:\s*"voteStake"|id:\s*"weight"'

Repository: livepeer/explorer

Length of output: 14980


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the shared DataTable component implementation
fd -i 'Table\.tsx|DataTable\.tsx|index\.tsx' components | head -n 50

# Inspect how sorting is configured (react-table useSortBy, default sortType, accessor handling)
fd -i 'Table' components/Table components | xargs -I{} sh -c 'echo "== {} =="; rg -n "useSortBy|sortType|defaultColumn|initialState|useTable" "{}" 2>/dev/null || true'

# Specifically look for default sortType behavior for string values
rg -n --type=ts --type=tsx 'sortType\s*:|defaultColumn|column\.sortType|autoResetSortBy|sortTypes' components/Table components -S

Repository: livepeer/explorer

Length of output: 2726


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the DataTable/Table wrapper implementation details around useSortBy config
sed -n '1,220p' components/Table/index.tsx

# Grep for default sortType / sorting comparator implementation inside this file
rg -n 'sortType|sortTypes|defaultColumn|useTable|useSortBy|sorting|alphanum|localeCompare|ascending|descending' components/Table/index.tsx

# If Table is re-exporting another component, inspect sibling files under components/Table
ls -la components/Table || true
fd -i 'Table.*\.tsx|DataTable.*\.tsx|index\.tsx' components/Table | xargs -I{} sh -c 'echo "== {} =="; sed -n "1,220p" "{}"' 2>/dev/null || true

Repository: livepeer/explorer

Length of output: 14750


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check react-table version from package.json / lockfiles
cat package.json | rg -n '"react-table"'
fd -i 'package-lock.json|yarn.lock|pnpm-lock.yaml|package.json' . | head -n 20
rg -n '"react-table"' yarn.lock package-lock.json pnpm-lock.yaml 2>/dev/null || true

# Also sanity-check if PollVoteType defines voteStake type as string/number
rg -n --type=ts --type=tsx 'export type PollVoteType|interface PollVoteType|type PollVoteType' components/PollVote -S

Repository: livepeer/explorer

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the resolved react-table version from pnpm-lock.yaml
rg -n '"react-table":' pnpm-lock.yaml
# Show the resolved package entry
rg -n 'react-table@' pnpm-lock.yaml | head -n 20

# Locate and inspect PollVoteType definition(s)
fd -i 'components/PollVote' components | head -n 50
rg -n --type=ts --type=tsx 'type PollVoteType|interface PollVoteType|export type PollVoteType|export interface PollVoteType' components/PollVote -S
# Read likely source around PollVoteType exports
ls -la components/PollVote
for f in components/PollVote/index.tsx components/PollVote/DesktopVoteTable.tsx components/PollVote/index.* 2>/dev/null; do
  [ -f "$f" ] && echo "== $f ==" && sed -n '1,200p' "$f" ;
done

Repository: livepeer/explorer

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -u
set -o pipefail

echo "== react-table entries in pnpm-lock.yaml =="
rg -n '"react-table":' pnpm-lock.yaml | head -n 20 || true
rg -n 'react-table@' pnpm-lock.yaml | head -n 30 || true

echo
echo "== PollVote files =="
ls -la components/PollVote || true

echo
echo "== PollVoteType definitions (search) =="
rg -n --type=ts --type=tsx 'PollVoteType' components/PollVote -S || true

echo
echo "== DesktopVoteTable imports PollVoteType =="
sed -n '1,120p' components/PollVote/DesktopVoteTable.tsx || true

Repository: livepeer/explorer

Length of output: 4323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== components/PollVote/index.tsx =="
sed -n '1,220p' components/PollVote/index.tsx

echo
echo "== Search for voteStake typing =="
rg -n 'voteStake' components/PollVote -S

Repository: livepeer/explorer

Length of output: 6440


🌐 Web query:

react-table 7.8.0 default sortType alphanumericSort compare numeric strings

💡 Result:

In react-table v7.8.0, the default string sort type used by useSortBy is “alphanumeric” (not a pure numeric string comparator) [1][2]. That means if your column values are numeric strings (e.g., "2", "10", "3"), the default will sort them as mixed alphanumeric/natural-sort style rather than consistently as numbers [3]. How to get “numeric strings” sorted numerically Set the column’s sortType to a custom comparator (or a numeric-focused sortType) so the comparator parses the strings to numbers and compares numerically. The v7 sortType function signature is used to compare two rows; react-table handles direction based on the desc argument, so your function should return 1 / -1 / 0 without manually reversing for desc [2]. Example (custom sortType for numeric strings) Use a custom sort function in your columns: const compareNumericString = (rowA, rowB, columnId) => { const a = Number.parseFloat(rowA.values[columnId]); const b = Number.parseFloat(rowB.values[columnId]); if (a > b) return 1; if (a < b) return -1; return 0; }; And then: { Header: 'Column #1', accessor: 'col1', sortType: compareNumericString, } (React-table v7 also documents that if you pass a function as sortType, it should be memoized) [2]. Related notes - In the v7 docs, the resolved function from a string sortType name (e.g., "alphanumeric") or a passed function is what React Table uses for sorting the column [1][2]. - Built-in “alphanumeric” is described (in TanStack Table docs, mapping from react-table v7) as sorting mixed alphanumeric values without case-sensitivity and being slower but more accurate for strings containing numbers that need natural/numeric-aware ordering [3].

Citations:


Make voteStake sorting explicitly numeric in the desktop votes table

  • Problem: Weight uses accessor: "voteStake" / id: "weight" but doesn’t set a sortType; with react-table v7.8.0 this falls back to the default string "alphanumeric" (natural-sort) comparator, which isn’t guaranteed to always match numeric ordering for stake string formats.
  • Why it matters: Desktop vote ranking by stake can be subtly wrong or inconsistent depending on how voteStake strings are formatted.
  • Suggested fix: Add a sortType to the Weight column that parses and compares voteStake numerically (e.g., parseFloat(rowA.original.voteStake) - parseFloat(rowB.original.voteStake)).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/DesktopVoteTable.tsx` around lines 77 - 79, The "Weight"
column in DesktopVoteTable uses accessor "voteStake" / id "weight" but lacks an
explicit numeric sortType, so react-table's default alphanumeric sort can
misorder stake values; update the column definition in DesktopVoteTable.tsx to
add a sortType that compares voteStake numerically (e.g., convert
rowA.original.voteStake and rowB.original.voteStake to numbers with
parseFloat/Number and return their difference, with a NaN-safe fallback) so
sorting is based on numeric stake rather than string ordering.

Comment on lines +30 to +119
const useVotes = (pollId: string) => {
const pollInterval = 10000;

const {
data: pollVotesData,
loading,
error: pollError,
} = usePollQuery({
variables: {
id: pollId ?? "",
},
pollInterval,
});

const { data: pollVoteEventsData, error: pollVoteEventsError } =
useVoteEventsQuery({
variables: {
first: 200,
where: {
poll: pollId,
},
},
});

const [votes, setVotes] = useState<PollVoteType[]>([]);
const [votesLoading, setVotesLoading] = useState(false);

useEffect(() => {
if (!pollVotesData?.poll?.votes || !pollVoteEventsData?.voteEvents) {
setVotes([]);
}

const decorateVotes = async () => {
setVotesLoading(true);

const uniqueVoters = Array.from(
new Set(pollVotesData?.poll?.votes?.map((v) => v.voter) ?? [])
);

const localEnsCache: { [address: string]: string } = {};

await Promise.all(
uniqueVoters.map(async (address) => {
try {
if (localEnsCache[address]) {
return;
}
const ensAddress = await getEnsForVotes(address);

if (ensAddress && ensAddress.name) {
localEnsCache[address] = ensAddress.name;
} else {
localEnsCache[address] = formatAddress(address);
}
} catch (e) {
console.warn(`Failed to fetch ENS for ${address}`, e);
}
})
);

const votes =
pollVotesData?.poll?.votes?.map((vote) => {
const events = (pollVoteEventsData?.voteEvents ?? [])
.filter((event) => event.voter === vote.voter)
.sort((a, b) => b.timestamp - a.timestamp);

const latestEvent = events[0];
const ensName = localEnsCache[vote.voter] ?? "";

return {
...vote,
ensName,
transactionHash: latestEvent?.transaction.id ?? "",
timestamp: latestEvent?.timestamp,
};
}) ?? [];

setVotes(votes as PollVoteType[]);
setVotesLoading(false);
};

decorateVotes();
}, [pollVotesData?.poll?.votes, pollVoteEventsData?.voteEvents]);

return {
votes,
loading: loading || votesLoading,
error: pollError || pollVoteEventsError,
};
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Problem: The useVotes hook lacks JSDoc documentation.

Why it matters: Contributor guidelines require JSDoc for functions/methods (general description at minimum) to ensure code is self-documenting and maintainable.

Suggested fix: Add JSDoc above the hook declaration.

📝 Proposed JSDoc
+/**
+ * Fetches poll votes and vote events, decorates each vote with ENS names and
+ * the latest transaction details, and returns the decorated votes with loading/error state.
+ */
 const useVotes = (pollId: string) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const useVotes = (pollId: string) => {
const pollInterval = 10000;
const {
data: pollVotesData,
loading,
error: pollError,
} = usePollQuery({
variables: {
id: pollId ?? "",
},
pollInterval,
});
const { data: pollVoteEventsData, error: pollVoteEventsError } =
useVoteEventsQuery({
variables: {
first: 200,
where: {
poll: pollId,
},
},
});
const [votes, setVotes] = useState<PollVoteType[]>([]);
const [votesLoading, setVotesLoading] = useState(false);
useEffect(() => {
if (!pollVotesData?.poll?.votes || !pollVoteEventsData?.voteEvents) {
setVotes([]);
}
const decorateVotes = async () => {
setVotesLoading(true);
const uniqueVoters = Array.from(
new Set(pollVotesData?.poll?.votes?.map((v) => v.voter) ?? [])
);
const localEnsCache: { [address: string]: string } = {};
await Promise.all(
uniqueVoters.map(async (address) => {
try {
if (localEnsCache[address]) {
return;
}
const ensAddress = await getEnsForVotes(address);
if (ensAddress && ensAddress.name) {
localEnsCache[address] = ensAddress.name;
} else {
localEnsCache[address] = formatAddress(address);
}
} catch (e) {
console.warn(`Failed to fetch ENS for ${address}`, e);
}
})
);
const votes =
pollVotesData?.poll?.votes?.map((vote) => {
const events = (pollVoteEventsData?.voteEvents ?? [])
.filter((event) => event.voter === vote.voter)
.sort((a, b) => b.timestamp - a.timestamp);
const latestEvent = events[0];
const ensName = localEnsCache[vote.voter] ?? "";
return {
...vote,
ensName,
transactionHash: latestEvent?.transaction.id ?? "",
timestamp: latestEvent?.timestamp,
};
}) ?? [];
setVotes(votes as PollVoteType[]);
setVotesLoading(false);
};
decorateVotes();
}, [pollVotesData?.poll?.votes, pollVoteEventsData?.voteEvents]);
return {
votes,
loading: loading || votesLoading,
error: pollError || pollVoteEventsError,
};
};
/**
* Fetches poll votes and vote events, decorates each vote with ENS names and
* the latest transaction details, and returns the decorated votes with loading/error state.
*/
const useVotes = (pollId: string) => {
const pollInterval = 10000;
const {
data: pollVotesData,
loading,
error: pollError,
} = usePollQuery({
variables: {
id: pollId ?? "",
},
pollInterval,
});
const { data: pollVoteEventsData, error: pollVoteEventsError } =
useVoteEventsQuery({
variables: {
first: 200,
where: {
poll: pollId,
},
},
});
const [votes, setVotes] = useState<PollVoteType[]>([]);
const [votesLoading, setVotesLoading] = useState(false);
useEffect(() => {
if (!pollVotesData?.poll?.votes || !pollVoteEventsData?.voteEvents) {
setVotes([]);
}
const decorateVotes = async () => {
setVotesLoading(true);
const uniqueVoters = Array.from(
new Set(pollVotesData?.poll?.votes?.map((v) => v.voter) ?? [])
);
const localEnsCache: { [address: string]: string } = {};
await Promise.all(
uniqueVoters.map(async (address) => {
try {
if (localEnsCache[address]) {
return;
}
const ensAddress = await getEnsForVotes(address);
if (ensAddress && ensAddress.name) {
localEnsCache[address] = ensAddress.name;
} else {
localEnsCache[address] = formatAddress(address);
}
} catch (e) {
console.warn(`Failed to fetch ENS for ${address}`, e);
}
})
);
const votes =
pollVotesData?.poll?.votes?.map((vote) => {
const events = (pollVoteEventsData?.voteEvents ?? [])
.filter((event) => event.voter === vote.voter)
.sort((a, b) => b.timestamp - a.timestamp);
const latestEvent = events[0];
const ensName = localEnsCache[vote.voter] ?? "";
return {
...vote,
ensName,
transactionHash: latestEvent?.transaction.id ?? "",
timestamp: latestEvent?.timestamp,
};
}) ?? [];
setVotes(votes as PollVoteType[]);
setVotesLoading(false);
};
decorateVotes();
}, [pollVotesData?.poll?.votes, pollVoteEventsData?.voteEvents]);
return {
votes,
loading: loading || votesLoading,
error: pollError || pollVoteEventsError,
};
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/index.tsx` around lines 30 - 119, The useVotes hook lacks
JSDoc; add a JSDoc comment immediately above the useVotes declaration that
briefly describes the hook's purpose (fetches and decorates votes and vote
events for a given poll), documents the parameter (pollId: string) and the
return shape (votes: PollVoteType[], loading: boolean, error: any), and mentions
any important behavior (it resolves ENS names via getEnsForVotes and derives
transactionHash/timestamp from vote events). Reference the hook name useVotes
and core behavior implemented by the decorateVotes async helper so reviewers can
locate the implementation.

Source: Coding guidelines

Comment on lines +121 to +208
const Index: React.FC<PollVotingTableProps> = ({ pollId }) => {
const { width } = useWindowSize();
const isDesktop = width >= 900;

const [selectedVoter, setSelectedVoter] = useState<{
address: string;
voteStake: string;
ensName?: string;
} | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;

const { votes, loading } = useVotes(pollId);

const totalVoteStake = useMemo(
() => votes.reduce((sum, v) => sum + parseFloat(v.voteStake), 0),
[votes]
);

const formatVoteStake = useMemo(
() => (stake: string) =>
`${formatLPT(parseFloat(stake), { abbreviate: false })} (${
totalVoteStake > 0
? formatPercent(parseFloat(stake) / totalVoteStake)
: formatPercent(0)
})`,
[totalVoteStake]
);

const paginatedVotesForMobile = useMemo(() => {
const sorted = [...votes].sort((a, b) => b.timestamp - a.timestamp);
const startIndex = (currentPage - 1) * pageSize;
return sorted.slice(startIndex, startIndex + pageSize);
}, [votes, currentPage, pageSize]);

const totalPages = Math.ceil(votes.length / pageSize);

if (loading) {
return (
<Flex
css={{
justifyContent: "center",
alignItems: "center",
height: "150px",
}}
>
<Spinner />
</Flex>
);
}

if (!votes.length)
return (
<Text css={{ textAlign: "center", color: "$neutral11", marginTop: "$4" }}>
No votes found for this poll.
</Text>
);

return (
<>
{isDesktop ? (
<DesktopVoteTable
votes={votes}
onSelect={setSelectedVoter}
pageSize={pageSize}
formatVoteStake={formatVoteStake}
/>
) : (
<MobileVoteCards
votes={paginatedVotesForMobile}
onSelect={setSelectedVoter}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
formatVoteStake={formatVoteStake}
/>
)}
{selectedVoter && (
<PollVotePopover
voter={selectedVoter.address}
ensName={selectedVoter.ensName}
formatVoteStake={formatVoteStake}
onClose={() => setSelectedVoter(null)}
/>
)}
</>
);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Problem: The exported Index component lacks JSDoc documentation.

Why it matters: Contributor guidelines mandate JSDoc for newly introduced components to maintain documentation consistency across the PollVote feature.

Suggested fix: Add JSDoc above the component declaration.

📝 Proposed JSDoc
+/**
+ * Renders poll votes in responsive layouts (desktop table or mobile cards with pagination)
+ * and provides a popover to display individual voter history when selected.
+ */
 const Index: React.FC<PollVotingTableProps> = ({ pollId }) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const Index: React.FC<PollVotingTableProps> = ({ pollId }) => {
const { width } = useWindowSize();
const isDesktop = width >= 900;
const [selectedVoter, setSelectedVoter] = useState<{
address: string;
voteStake: string;
ensName?: string;
} | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;
const { votes, loading } = useVotes(pollId);
const totalVoteStake = useMemo(
() => votes.reduce((sum, v) => sum + parseFloat(v.voteStake), 0),
[votes]
);
const formatVoteStake = useMemo(
() => (stake: string) =>
`${formatLPT(parseFloat(stake), { abbreviate: false })} (${
totalVoteStake > 0
? formatPercent(parseFloat(stake) / totalVoteStake)
: formatPercent(0)
})`,
[totalVoteStake]
);
const paginatedVotesForMobile = useMemo(() => {
const sorted = [...votes].sort((a, b) => b.timestamp - a.timestamp);
const startIndex = (currentPage - 1) * pageSize;
return sorted.slice(startIndex, startIndex + pageSize);
}, [votes, currentPage, pageSize]);
const totalPages = Math.ceil(votes.length / pageSize);
if (loading) {
return (
<Flex
css={{
justifyContent: "center",
alignItems: "center",
height: "150px",
}}
>
<Spinner />
</Flex>
);
}
if (!votes.length)
return (
<Text css={{ textAlign: "center", color: "$neutral11", marginTop: "$4" }}>
No votes found for this poll.
</Text>
);
return (
<>
{isDesktop ? (
<DesktopVoteTable
votes={votes}
onSelect={setSelectedVoter}
pageSize={pageSize}
formatVoteStake={formatVoteStake}
/>
) : (
<MobileVoteCards
votes={paginatedVotesForMobile}
onSelect={setSelectedVoter}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
formatVoteStake={formatVoteStake}
/>
)}
{selectedVoter && (
<PollVotePopover
voter={selectedVoter.address}
ensName={selectedVoter.ensName}
formatVoteStake={formatVoteStake}
onClose={() => setSelectedVoter(null)}
/>
)}
</>
);
};
/**
* Renders poll votes in responsive layouts (desktop table or mobile cards with pagination)
* and provides a popover to display individual voter history when selected.
*/
const Index: React.FC<PollVotingTableProps> = ({ pollId }) => {
const { width } = useWindowSize();
const isDesktop = width >= 900;
const [selectedVoter, setSelectedVoter] = useState<{
address: string;
voteStake: string;
ensName?: string;
} | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;
const { votes, loading } = useVotes(pollId);
const totalVoteStake = useMemo(
() => votes.reduce((sum, v) => sum + parseFloat(v.voteStake), 0),
[votes]
);
const formatVoteStake = useMemo(
() => (stake: string) =>
`${formatLPT(parseFloat(stake), { abbreviate: false })} (${
totalVoteStake > 0
? formatPercent(parseFloat(stake) / totalVoteStake)
: formatPercent(0)
})`,
[totalVoteStake]
);
const paginatedVotesForMobile = useMemo(() => {
const sorted = [...votes].sort((a, b) => b.timestamp - a.timestamp);
const startIndex = (currentPage - 1) * pageSize;
return sorted.slice(startIndex, startIndex + pageSize);
}, [votes, currentPage, pageSize]);
const totalPages = Math.ceil(votes.length / pageSize);
if (loading) {
return (
<Flex
css={{
justifyContent: "center",
alignItems: "center",
height: "150px",
}}
>
<Spinner />
</Flex>
);
}
if (!votes.length)
return (
<Text css={{ textAlign: "center", color: "$neutral11", marginTop: "$4" }}>
No votes found for this poll.
</Text>
);
return (
<>
{isDesktop ? (
<DesktopVoteTable
votes={votes}
onSelect={setSelectedVoter}
pageSize={pageSize}
formatVoteStake={formatVoteStake}
/>
) : (
<MobileVoteCards
votes={paginatedVotesForMobile}
onSelect={setSelectedVoter}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
formatVoteStake={formatVoteStake}
/>
)}
{selectedVoter && (
<PollVotePopover
voter={selectedVoter.address}
ensName={selectedVoter.ensName}
formatVoteStake={formatVoteStake}
onClose={() => setSelectedVoter(null)}
/>
)}
</>
);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/index.tsx` around lines 121 - 208, Add a JSDoc comment
block immediately above the Index component declaration describing the
component's purpose (renders desktop or mobile vote views for a poll), list the
props with types (PollVotingTableProps and the pollId prop), and document the
return value (React.ReactElement). Ensure the JSDoc references the component
name "Index" and mentions key behaviors such as pagination, vote formatting via
formatVoteStake, and that it opens PollVotePopover for selected voters.

Source: Coding guidelines

Comment on lines +30 to +34
export function MobileVoteView({
vote,
formatVoteStake,
onSelect,
}: MobileVoteViewProps) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Problem: Exported MobileVoteView function lacks JSDoc documentation.

Why it matters: Project contributor guidelines require JSDoc for functions/methods (at minimum a general description) to maintain code discoverability and self-explanatory changes.

Suggested fix: Add JSDoc above the function declaration.

📝 Proposed JSDoc
+/**
+ * Renders a mobile-optimized vote card showing voter identity, support badge,
+ * stake amount, transaction link, and timestamp with a history action button.
+ */
 export function MobileVoteView({
   vote,
   formatVoteStake,
   onSelect,
 }: MobileVoteViewProps) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function MobileVoteView({
vote,
formatVoteStake,
onSelect,
}: MobileVoteViewProps) {
/**
* Renders a mobile-optimized vote card showing voter identity, support badge,
* stake amount, transaction link, and timestamp with a history action button.
*/
export function MobileVoteView({
vote,
formatVoteStake,
onSelect,
}: MobileVoteViewProps) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/MobileVoteView.tsx` around lines 30 - 34, Add a JSDoc
block above the exported MobileVoteView function describing its purpose (renders
the mobile view for a single poll vote) and include `@param` entries for the
MobileVoteViewProps fields (vote, formatVoteStake, onSelect) and an `@returns`
describing the React element; reference the MobileVoteView function and the
MobileVoteViewProps type so the doc ties to the component signature. Ensure the
description is a one- or two-sentence summary and each `@param` briefly explains
expected types/behavior (e.g., vote object, formatVoteStake callback, onSelect
handler).

Source: Coding guidelines

Comment on lines +21 to +23
const Index: React.FC<PollVoteDetailProps> = ({ vote }) => {
const [title, setTitle] = useState<string>("");
const support = POLL_VOTES[vote.choiceID];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Problem: Exported Index component lacks JSDoc documentation.

Why it matters: Project contributor guidelines mandate JSDoc for newly introduced components (general description at minimum) to ensure reviewable, self-explanatory changes.

Suggested fix: Add JSDoc above the component declaration.

📝 Proposed JSDoc
+/**
+ * Renders vote event details with responsive layouts: mobile card and desktop timeline.
+ * Fetches and displays the poll title from IPFS, vote badge, transaction link, and timestamp.
+ */
 const Index: React.FC<PollVoteDetailProps> = ({ vote }) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const Index: React.FC<PollVoteDetailProps> = ({ vote }) => {
const [title, setTitle] = useState<string>("");
const support = POLL_VOTES[vote.choiceID];
/**
* Renders vote event details with responsive layouts: mobile card and desktop timeline.
* Fetches and displays the poll title from IPFS, vote badge, transaction link, and timestamp.
*/
const Index: React.FC<PollVoteDetailProps> = ({ vote }) => {
const [title, setTitle] = useState<string>("");
const support = POLL_VOTES[vote.choiceID];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/PollVoteDetail.tsx` around lines 21 - 23, Add a JSDoc
block above the exported React component Index describing the component's
purpose and its props (PollVoteDetailProps) at minimum; mention what the
component renders (vote details), any important behavior (uses POLL_VOTES via
choiceID and local title state), and annotate the function signature so the
documentation satisfies the project's contributor guidelines.

Source: Coding guidelines

Comment on lines +19 to +26
const Index: React.FC<PollVotePopoverProps> = ({ voter, ensName, onClose }) => {
const { data: votesEventsData, loading: isLoading } = useVoteEventsQuery({
variables: {
where: {
voter: voter,
},
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Problem: Exported Index component lacks JSDoc documentation.

Why it matters: Contributor guidelines require JSDoc for newly introduced components to maintain code clarity and alignment with project documentation standards.

Suggested fix: Add JSDoc above the component declaration.

📝 Proposed JSDoc
+/**
+ * Displays a voting history modal for a given voter, showing all their poll votes
+ * sorted by timestamp with statistics (total, for, against) and vote event details.
+ */
 const Index: React.FC<PollVotePopoverProps> = ({ voter, ensName, onClose }) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const Index: React.FC<PollVotePopoverProps> = ({ voter, ensName, onClose }) => {
const { data: votesEventsData, loading: isLoading } = useVoteEventsQuery({
variables: {
where: {
voter: voter,
},
},
});
/**
* Displays a voting history modal for a given voter, showing all their poll votes
* sorted by timestamp with statistics (total, for, against) and vote event details.
*/
const Index: React.FC<PollVotePopoverProps> = ({ voter, ensName, onClose }) => {
const { data: votesEventsData, loading: isLoading } = useVoteEventsQuery({
variables: {
where: {
voter: voter,
},
},
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/PollVotePopover.tsx` around lines 19 - 26, Add JSDoc
above the exported React component Index describing its purpose and props;
document that Index is a React.FC<PollVotePopoverProps> that displays vote
events for a given voter and accepts props voter, ensName, and onClose, and
mention side effects like useVoteEventsQuery usage and loading state
(isLoading). Ensure the JSDoc includes a short description, `@param` entries for
voter, ensName, and onClose, and an `@returns` description for the rendered JSX so
the component complies with contributor documentation standards.

Source: Coding guidelines

@@ -1,13 +1,22 @@
import CliVotingInstructionsDialog from "@components/CliVotingInstructionsDialog";
import {} from "@components/PollVote";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Problem: Empty import statement import {} from "@components/PollVote"; (Line 2).

Why it matters: This serves no purpose and appears to be leftover code from development or refactoring, adding unnecessary noise to the import section.

Suggested fix: Remove the unused import.

🧹 Proposed cleanup
 import CliVotingInstructionsDialog from "`@components/CliVotingInstructionsDialog`";
-import {} from "`@components/PollVote`";
 import VoteButton from "`@components/VoteButton`";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {} from "@components/PollVote";
import CliVotingInstructionsDialog from "`@components/CliVotingInstructionsDialog`";
import VoteButton from "`@components/VoteButton`";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVotingWidget/index.tsx` at line 2, Remove the no-op empty
import "import {} from \"`@components/PollVote`\";" in
components/PollVotingWidget/index.tsx; locate the import statement (the empty
destructuring import) and delete it, and if PollVote functionality is actually
required by this module, replace the empty import with the correct named or
default import (e.g., import PollVote or import { SomeExport } from
"`@components/PollVote`") after verifying usages of PollVote in this file.

ibsule added 2 commits June 10, 2026 11:48
…tting

- Updated `PollVoteDetail` to fetch and display total vote stakes
- Modified GraphQL queries to include poll details and associated votes
- Adjusted the rendering of vote stakes in the UI
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.

Extend Voting Transparency to Governance page

1 participant