feat: enhance polling functionality with voting transparency on the governance page#683
feat: enhance polling functionality with voting transparency on the governance page#683ibsule wants to merge 17 commits into
Conversation
…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 is attempting to deploy a commit to the Livepeer Foundation Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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). ChangesGovernance Voting Transparency
Sequence DiagramsequenceDiagram
participant User
participant Page as voting/[poll].tsx
participant Widget as PollVotingWidget
participant Table as PollVote
participant Apollo as Apollo Hooks
participant Popover as PollVotePopover
User->>Page: Load governance poll page
Page->>Page: Derive view mode from query, compute votesTabHref
Page->>Widget: Render with votesTabHref prop
Widget->>Widget: Render "View votes" link
User->>Widget: Click "View votes"
Widget->>Page: Navigate to view=votes
Page->>Table: Render vote table (view=votes)
Table->>Apollo: useVotesQuery (poll votes + events)
Apollo-->>Table: Decorated votes with ENS, tx hash, timestamp
Table->>Page: Render desktop table or mobile cards
User->>Table: Click voter row/card
Table->>Popover: Display voter's vote history
Popover->>Apollo: useVoteEventsQuery (voter filter)
Apollo-->>Popover: Sorted vote events
Popover->>Page: Render timeline of PollVoteDetail entries
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (13)
apollo/subgraph.tscomponents/PollVote/DesktopVoteTable.tsxcomponents/PollVote/MobileVoteCards.tsxcomponents/PollVote/MobileVoteView.tsxcomponents/PollVote/PollVoteDetail.tsxcomponents/PollVote/PollVotePopover.tsxcomponents/PollVote/index.tsxcomponents/PollVotingWidget/index.tsxlib/api/polls.tslib/api/types/votes.tspages/voting/[poll].tsxqueries/poll.graphqlqueries/voteEvents.graphql
- Renamed the "Stake Used" header to "Weight" for clarity. - Updated the accessor ID from "stakeUsed" to "weight"
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (9)
lib/api/polls.ts (1)
64-69:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle IPFS/parsing errors in
parsePollText.
- Problem:
catIpfsJson/parsePollIpfserrors 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/catchand returnnullon 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 winAdd a safe default for
$firstto prevent unbounded vote-event fetches.
- Problem:
$firstis optional and this query forwards it directly; an existing consumer (components/PollVote/PollVotePopover.tsx) omitsfirst.- 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 explicitfirstat 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 winProblem:
useVotesreturns anerrorvalue (Line 117), but theIndexcomponent 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
errorfromuseVotesand 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 winProblem: Both "Overview" and "Votes" content sections are rendered and mounted (Line 314-471), with only CSS
displaytoggling visibility. This meansvoteContent()(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 winProblem: The
viewquery 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
viewto 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
vieweverywhere (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 winProblem: External link with
target="_blank"is missingrel="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 winProblem: Vote statistics hardcode
choiceID === "0"and"1"(Line 41-42), but vote events may usePollChoice.Yes/PollChoice.Noenum 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
choiceIDviaVOTING_SUPPORT_MAPorPOLL_VOTESbefore 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 winProblem: External links with
target="_blank"are missingrel="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.openerand 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 winProblem: Links with
target="_blank"are missingrel="noopener noreferrer"(Line 80-81 in mobile layout, Line 185-186 in desktop layout).Why it matters: Even for internal links, omitting
relwithtarget="_blank"creates a security gap and can impact performance by allowing the opened tab to accesswindow.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
📒 Files selected for processing (13)
apollo/subgraph.tscomponents/PollVote/DesktopVoteTable.tsxcomponents/PollVote/MobileVoteCards.tsxcomponents/PollVote/MobileVoteView.tsxcomponents/PollVote/PollVoteDetail.tsxcomponents/PollVote/PollVotePopover.tsxcomponents/PollVote/index.tsxcomponents/PollVotingWidget/index.tsxlib/api/polls.tslib/api/types/votes.tspages/voting/[poll].tsxqueries/poll.graphqlqueries/voteEvents.graphql
…ss PollVote components
…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
…te display - Updated `DesktopVoteTable` to handle unknown voting support by displaying a default badge. - Introduced sorting functionality for vote stakes in `DesktopVoteTable`. - Enhanced `useVotes` hook to manage loading and error states. - Added error message display in the voting UI when fetching votes fails. - Improved mobile and desktop vote views with additional details and consistent link handling.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/PollVote/PollVotePopover.tsx (1)
23-30:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle vote-events query errors explicitly.
- Problem:
useVoteEventsQueryerrors are not handled; failed requests currently fall through to the “No votes found” state.- Why it matters: Users get incorrect transparency data during network/API failures, and incidents are harder to detect.
- Suggested fix: Read
errorfrom the hook and render an explicit error state before the empty-state branch.Proposed patch
- const { data: votesEventsData, loading: isLoading } = useVoteEventsQuery({ + const { + data: votesEventsData, + loading: isLoading, + error, + } = useVoteEventsQuery({ variables: { first: 200, where: { voter: voter, }, }, }); ... - ) : voteEvents.length > 0 ? ( + ) : error ? ( + <Text + css={{ color: "$neutral11", textAlign: "center", marginTop: "$4" }} + > + Unable to load voting history. Please try again. + </Text> + ) : voteEvents.length > 0 ? (Also applies to: 178-223
🤖 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 23 - 30, The useVoteEventsQuery hook call in the PollVotePopover component is not capturing or handling query errors, causing failed requests to fall through and display as "No votes found" instead of showing an actual error state. Extract the error property from the useVoteEventsQuery destructuring (alongside data and loading), and add an explicit error state render that checks for this error condition before rendering the empty-state/no votes found branch to ensure users see proper error messaging during network or API failures.
🤖 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/PollVoteDetail.tsx`:
- Around line 25-44: The PollVoteDetail component is issuing a useVoteQuery
request for each individual vote item rendered, causing N+1 query problems when
there are many votes in the history. Move the data fetching from useVoteQuery
(currently on line 27) up to the parent PollVotePopover component, and batch the
queries by fetching the needed vote records once. Create a lookup map or
precompute the stake weight text for each vote at the parent level, then pass
the precomputed weight text as a prop to PollVoteDetail instead of having each
instance calculate it independently using getStake. This eliminates redundant
queries and repeated payload transfers from the subgraph.
In `@queries/vote.graphql`:
- Around line 6-12: In the vote query, replace the poll.votes field (which
fetches the full array with voteStake and id) with poll.tally and select the yes
and no aggregate fields instead. This avoids fetching the entire votes array and
uses the schema's pre-computed aggregates, which ensures correct vote
calculations regardless of pagination or result truncation.
---
Outside diff comments:
In `@components/PollVote/PollVotePopover.tsx`:
- Around line 23-30: The useVoteEventsQuery hook call in the PollVotePopover
component is not capturing or handling query errors, causing failed requests to
fall through and display as "No votes found" instead of showing an actual error
state. Extract the error property from the useVoteEventsQuery destructuring
(alongside data and loading), and add an explicit error state render that checks
for this error condition before rendering the empty-state/no votes found branch
to ensure users see proper error messaging during network or API failures.
🪄 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: d2de6b77-f66d-4975-9c73-c596ec145a5f
📒 Files selected for processing (12)
apollo/subgraph.tscomponents/PollVote/DesktopVoteTable.tsxcomponents/PollVote/MobileVoteCards.tsxcomponents/PollVote/MobileVoteView.tsxcomponents/PollVote/PollVoteDetail.tsxcomponents/PollVote/PollVotePopover.tsxcomponents/PollVote/index.tsxcomponents/PollVotingWidget/index.tsxlib/api/polls.tslib/api/types/votes.tspages/voting/[poll].tsxqueries/vote.graphql
💤 Files with no reviewable changes (1)
- components/PollVotingWidget/index.tsx
|
@ibsule Everything looks great, just a couple of small nitpicks before we can merge.
|
- Changed the sorting mechanism for paginated votes to prioritize vote stakes instead of timestamps
… optimizations - Removed unnecessary pagination parameters from GraphQL queries in PollVote components for fetching full votes/events
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
queries/voteEvents.graphql (1)
1-2: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
$firstis declared but never applied, sovoteEventsis currently unbounded.Problem: On Line 1,
$firstis part of the operation signature, but on Line 2 it is not passed tovoteEvents(...).
Why it matters: Any caller expecting bounded results cannot enforce limits, which can cause large responses and slower UI paths as vote-event history grows.
Suggested fix: Applyfirst: $firstin the query and set a safe default ($first: Int = 200) or require callers to pass it explicitly.Proposed patch
-query voteEvents($first: Int, $where: VoteEvent_filter) { - voteEvents(orderBy: timestamp, orderDirection: desc, where: $where) { +query voteEvents($first: Int = 200, $where: VoteEvent_filter) { + voteEvents( + first: $first + orderBy: timestamp + orderDirection: desc + where: $where + ) {🤖 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 - 2, The $first variable is declared in the query operation signature but is not being used in the voteEvents field call, causing queries to be unbounded. Add the first parameter to the voteEvents call by including first: $first in the arguments passed to voteEvents alongside orderBy, orderDirection, and where. Additionally, consider setting a default value for the $first variable (such as 200) in the operation signature to provide a safe default limit when callers do not explicitly specify one.
🤖 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 158-160: The sorted array initialization uses parseInt to compare
voteStake values, which truncates decimal portions and causes precision loss
when ranking votes. Replace parseInt with parseFloat or Number in the sort
comparator function for both a.voteStake and b.voteStake to preserve the full
numeric precision of stake values and ensure correct vote ordering regardless of
fractional stake amounts.
---
Duplicate comments:
In `@queries/voteEvents.graphql`:
- Around line 1-2: The $first variable is declared in the query operation
signature but is not being used in the voteEvents field call, causing queries to
be unbounded. Add the first parameter to the voteEvents call by including first:
$first in the arguments passed to voteEvents alongside orderBy, orderDirection,
and where. Additionally, consider setting a default value for the $first
variable (such as 200) in the operation signature to provide a safe default
limit when callers do not explicitly specify one.
🪄 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: c12e9f54-916c-4442-9b6c-23286b3cf9e0
📒 Files selected for processing (5)
apollo/subgraph.tscomponents/PollVote/MobileVoteView.tsxcomponents/PollVote/PollVotePopover.tsxcomponents/PollVote/index.tsxqueries/voteEvents.graphql
- Changed the sorting mechanism for paginated votes to use Number for vote stakes. - Removed unnecessary pagination parameter from GraphQL query for vote events.
|
Looks solid and all the issues have been resolved. We will have to deploy first in the livepeer vercel account to make sure that the issues we saw regarding the incorrect font sizes and older governance pools showing incorrect total non voters and voters is a deployment issue and not logic since you dont seem to have made any changes related to those. |
- Enhanced GraphQL queries for vote events by adding a pagination parameter. - Refactored the PollVote component to utilize a custom hook for fetching vote events.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apollo/subgraph.ts (1)
11170-11187: 🎯 Functional Correctness | 🟠 MajorRegenerate
apollo/subgraph.tsfrom source GraphQL definitions.Problem: The
voteEventsoperation declares an unused$firstvariable. The source GraphQL file (queries/voteEvents.graphql) does not include this parameter, but the generated TypeScript file is out of sync.Why it matters: GraphQL validation can reject the document due to the unused variable, causing the vote-history query to fail at runtime.
Suggested fix: Run
pnpm codegento regenerate the file from the correct source. Do not manually edit auto-generated files.🤖 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 `@apollo/subgraph.ts` around lines 11170 - 11187, The VoteEventsDocument query in apollo/subgraph.ts has an unused `$first: Int` parameter declared that is not referenced anywhere in the query body, causing GraphQL validation to fail. This file is auto-generated and is out of sync with the source GraphQL definitions. Regenerate the apollo/subgraph.ts file by running the pnpm codegen command to pull the correct schema from the source GraphQL file queries/voteEvents.graphql, and do not manually edit this auto-generated file.
♻️ Duplicate comments (1)
queries/voteEvents.graphql (1)
1-2: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winAdd a bounded default page size to
voteEvents.Problem: Line 1/Line 2 define and execute
voteEventswithout afirstbound.
Why it matters: Large voter histories can return very large payloads, causing slow UI responses/timeouts in both vote list and voter-history flows.
Suggested fix: Add a default limit at the query level and wire it into the field call so all consumers are protected.Proposed patch
-query voteEvents($where: VoteEvent_filter) { - voteEvents(orderBy: timestamp, orderDirection: desc, where: $where) { +query voteEvents($where: VoteEvent_filter, $first: Int = 200) { + voteEvents( + first: $first + orderBy: timestamp + orderDirection: desc + where: $where + ) {🤖 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 - 2, The voteEvents query definition lacks a bounded page size limit on the voteEvents field call, which allows unbounded result sets that can cause performance issues. Add a `first` parameter to the voteEvents field invocation in the query with a reasonable default limit (for example, 1000 or similar based on your system requirements). Ensure the `first` parameter is either hardcoded as a default or parameterized as a variable in the query definition so that all consumers of this query are protected from returning excessively large result sets.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@apollo/subgraph.ts`:
- Around line 11170-11187: The VoteEventsDocument query in apollo/subgraph.ts
has an unused `$first: Int` parameter declared that is not referenced anywhere
in the query body, causing GraphQL validation to fail. This file is
auto-generated and is out of sync with the source GraphQL definitions.
Regenerate the apollo/subgraph.ts file by running the pnpm codegen command to
pull the correct schema from the source GraphQL file queries/voteEvents.graphql,
and do not manually edit this auto-generated file.
---
Duplicate comments:
In `@queries/voteEvents.graphql`:
- Around line 1-2: The voteEvents query definition lacks a bounded page size
limit on the voteEvents field call, which allows unbounded result sets that can
cause performance issues. Add a `first` parameter to the voteEvents field
invocation in the query with a reasonable default limit (for example, 1000 or
similar based on your system requirements). Ensure the `first` parameter is
either hardcoded as a default or parameterized as a variable in the query
definition so that all consumers of this query are protected from returning
excessively large result sets.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c0dc9ab7-fd8b-45a5-9a8f-25ac7fa51f3f
📒 Files selected for processing (3)
apollo/subgraph.tscomponents/PollVote/index.tsxqueries/voteEvents.graphql
…ule/explorer into governance-vote-transparency
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/PollVote/index.tsx (1)
55-110: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
useVoteshas an async race and unmount-update hazard.
- Problem:
decorateVotesperforms async work without cancellation/version guards, while the effect can re-run every poll interval.- Why it matters: Older runs can overwrite newer state, and unmounted components can still receive
setVotes/setVotesLoadingcalls.- Suggested fix: Add effect cleanup with a
cancelledflag (or run-id token) and gate all state writes to only the latest active run.🤖 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 55 - 110, The decorateVotes async function lacks protection against race conditions where older runs can overwrite newer state or update unmounted components. Add a cleanup function to the useEffect that sets a cancelled flag to true, then create a cancelled tracking variable (such as a useRef or local variable in the effect scope) initialized to false at the start of decorateVotes. Before each state update (setVotes and setVotesLoading), check if cancelled is true and skip the update if so. Return the cleanup function from the useEffect to set the cancelled flag before the effect re-runs or component unmounts, ensuring only the latest active run can update component state.
🤖 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 `@apollo/subgraph.ts`:
- Around line 11171-11177: The voteEvents query uses only timestamp for
ordering, which is non-deterministic when multiple events share the same
timestamp and can cause rows to be dropped or duplicated at page boundaries
during pagination. Add a secondary tie-breaker field to the orderBy clause in
the voteEvents query (such as a unique identifier like id) to establish a
stable, deterministic sort order that prevents pagination issues and ensures
vote history remains complete and correct.
In `@hooks/useGetAllPollVotesEvents.ts`:
- Around line 24-26: The useEffect hook in useGetAllPollVotesEvents returns
early when voteEventsData is missing, preventing setLoadingAll(false) from being
called when an error occurs without data. Add an error handling path that checks
if an error exists and voteEventsData is not available, then call
setLoadingAll(false) (and optionally reset allVoteEvents) to prevent the loading
state from remaining true indefinitely. This same fix should be applied to the
similar useEffect blocks referenced at lines 59-60 and 84-85 to ensure
consistent error handling across all data fetching effects in this hook.
- Around line 37-43: The pagination logic in the while loop uses timestamp_lt as
the cursor, but without a deterministic tie-breaker or stable ordering by a
unique identifier like id, events with the same timestamp can be skipped between
pages. Modify the fetchMore variables object to include both timestamp_lt and an
id-based ordering constraint as a tie-breaker, or alternatively implement a
deduplication strategy using event id across paginated results to ensure no vote
events are lost when boundary timestamps are shared. Ensure the upstream query
is ordered deterministically by both timestamp and id to prevent gaps in
pagination.
---
Outside diff comments:
In `@components/PollVote/index.tsx`:
- Around line 55-110: The decorateVotes async function lacks protection against
race conditions where older runs can overwrite newer state or update unmounted
components. Add a cleanup function to the useEffect that sets a cancelled flag
to true, then create a cancelled tracking variable (such as a useRef or local
variable in the effect scope) initialized to false at the start of
decorateVotes. Before each state update (setVotes and setVotesLoading), check if
cancelled is true and skip the update if so. Return the cleanup function from
the useEffect to set the cancelled flag before the effect re-runs or component
unmounts, ensuring only the latest active run can update component state.
🪄 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: 89bc37cb-01f7-43d1-a744-f64b5b87259a
📒 Files selected for processing (5)
apollo/subgraph.tscomponents/PollVote/PollVotePopover.tsxcomponents/PollVote/index.tsxhooks/useGetAllPollVotesEvents.tsqueries/voteEvents.graphql
- Added logic to stop loading and clear vote events when an error occurs. - Updated dependencies in the effect hook to include error state.
Description
This PR introduces a new poll vote transparency experience on the governance voting page through the following:
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
Related Issue(s)
Related: #310
Closes: #482
Changes Made
PollVoteDetail,DesktopVoteTable,MobileVoteView,MobileVoteCards,PollVotePopover, andcomponents/PollVote/index.tsx.apollo/subgraph.ts,lib/api/polls.ts,queries/poll.graphql, newqueries/voteEvents.graphql) to fetch richer vote details.pages/voting/[poll].tsxandcomponents/PollVotingWidget/index.tsxto support improved vote UX/navigation.Testing
Impact / Risk
Risk level: Low
Impacted areas: UI
Rollback plan: PR revert
Screenshots / Recordings (if applicable)
Before
After
Summary by CodeRabbit