Skip to content

Add support data#48

Open
adetoye-dev wants to merge 4 commits intomainfrom
add-support-data
Open

Add support data#48
adetoye-dev wants to merge 4 commits intomainfrom
add-support-data

Conversation

@adetoye-dev
Copy link
Copy Markdown
Collaborator

@adetoye-dev adetoye-dev commented Aug 18, 2025

Summary by CodeRabbit

  • New Features

    • Added a “Vote Curves” section to the Governance sidebar with a multi-series line chart, details panel, loading state, and live threshold reporting.
  • API

    • New backend endpoint to fetch vote curve data for proposals.
  • Chores

    • Added charting libraries, updated a network subsquid URL, introduced supporting types and curve-threshold calculation utilities.

@vercel
Copy link
Copy Markdown

vercel bot commented Aug 18, 2025

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

Project Deployment Preview Comments Updated (UTC)
fellowship-test Ready Ready Preview Comment Sep 1, 2025 9:44am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 18, 2025

Walkthrough

Adds charting dependencies and implements vote curves: a new POST API to fetch curve data from Subsquid, a GraphQL query constant, types and curve utilities, UI components to render multi-series charts in the governance sidebar, and an updated COLLECTIVES Subsquid URL.

Changes

Cohort / File(s) Summary of changes
Dependencies
package.json
Added chart.js@^4.5.0 and react-chartjs-2@^5.3.0.
API: Curves endpoint
src/app/api/v1/curves/route.ts
New POST handler: validates blockGte and postId, derives network from headers, queries Subsquid with GET_CURVE_DATA_BY_INDEX, returns JSON, wrapped with error handling and API error codes.
GraphQL queries
src/app/api/v1/subsquidQueries.ts
Added GET_CURVE_DATA_BY_INDEX GraphQL query (variables: index_eq, block_gte, limit) returning curve fields ordered by block_ASC.
Network config
src/global/networkConstants.ts
Updated COLLECTIVES subsquidUrl to https://polkassembly.squids.live/collectives-polkassembly@v3/api/graphql.
Types
src/global/types.ts
Added IVoteCurve, IStatusHistoryItem, EPostOrigin, EProposalStatus.
Curve utilities
src/utils/trackCurvesUtils.ts
New helpers: makeReciprocalCurve, makeLinearCurve, getTrackFunctions producing approval/support threshold functions using BigNumber.
Governance sidebar: Vote curves UI
src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurves.tsx, .../VoteCurvesDetails.tsx, .../VoteCurvesData.tsx, .../VoteCurvesDataWrapper.tsx, src/components/Post/GovernanceSidebar/index.tsx
Added VoteCurves chart (multi-series: approval/support and thresholds), VoteCurvesDetails, VoteCurvesData container, VoteCurvesDataWrapper (fetches API, manages state), and integrated wrapper into GovernanceSidebar.

Sequence Diagram(s)

sequenceDiagram
  participant UI as GovernanceSidebar
  participant VCW as VoteCurvesDataWrapper
  participant API as /api/v1/curves (POST)
  participant SQ as Subsquid GraphQL

  UI->>VCW: mount (postId, timeline)
  VCW->>API: POST { blockGte, postId }
  API->>SQ: Query GET_CURVE_DATA_BY_INDEX
  SQ-->>API: curveData[]
  API-->>VCW: JSON curveData[]
  VCW->>VCW: set state (voteCurveData, latestApproval/Support)
  VCW-->>UI: render VoteCurvesData
Loading
sequenceDiagram
  participant VCD as VoteCurvesData
  participant VC as VoteCurves
  participant VCDt as VoteCurvesDetails
  participant Utils as trackCurvesUtils

  VCD->>VC: provide props (voteCurveData, trackName, timeline, createdAt)
  VC->>Utils: getTrackFunctions({network, trackName})
  VC->>VC: build datasets (Approval, Support, ApprovalThreshold, SupportThreshold)
  VC-->>VCD: call setThresholdValues({approvalThreshold, supportThreshold})
  VCD->>VCDt: pass latestApproval/latestSupport and thresholds
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • alphainfinitus

Poem

I hopped through data, whiskers keen,
Lines of votes in green and sheen,
Thresholds wiggle, curves align,
Subsquid sings, the chart looks fine.
🐰📈✨


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4772fdf and 656e3b0.

📒 Files selected for processing (1)
  • src/global/networkConstants.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/global/networkConstants.ts
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add-support-data

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (21)
src/app/api/v1/subsquidQueries.ts (1)

476-488: Make variables non-nullable and plan for pagination to avoid unintended wide scans

The query allows both $index_eq and $block_gte to be null. If the route ever omits one by mistake, the where clause could broaden unexpectedly. Also, limit=1000 without pagination may truncate large datasets.

Apply this diff to enforce required variables:

-export const GET_CURVE_DATA_BY_INDEX = gql`
-	query CurveDataByIndex($index_eq: Int, $block_gte: Int, $limit: Int = 1000) {
+export const GET_CURVE_DATA_BY_INDEX = gql`
+	query CurveDataByIndex($index_eq: Int!, $block_gte: Int!, $limit: Int = 1000) {
 		curveData(limit: $limit, where: { index_eq: $index_eq, block_gte: $block_gte }, orderBy: block_ASC) {
 			approvalPercent
 			block
 			id
 			index
 			supportPercent
 			timestamp
 		}
 	}
 `;

Optionally, add offset/cursor support in a future pass to page beyond 1000 results.

src/global/types.ts (2)

807-813: Avoid duplicating SingleStatus; reuse/alias instead of redefining IStatusHistoryItem

SingleStatus (lines 338-344) already models block/id/status/timestamp. Adding IStatusHistoryItem with the same fields invites divergence.

Minimal change: alias to SingleStatus to keep one source of truth.

-export interface IStatusHistoryItem {
-	block: number;
-	id: string;
-	status: ProposalStatus;
-	timestamp?: string;
-}
+export type IStatusHistoryItem = SingleStatus;

If you truly need an optional timestamp in some call sites, consider making SingleStatus.timestamp optional globally, or create a separate narrow view type and convert at the boundary.


814-817: Remove unused EPostOrigin enum

We’ve confirmed via a repository-wide search that EPostOrigin is only declared in src/global/types.ts and never referenced elsewhere. It duplicates ProposalType.FELLOWSHIP_REFERENDUMS, so you can safely delete it to reduce confusion:

--- a/src/global/types.ts
@@ -814,7 +814,3 @@
 export enum EPostOrigin {
-	FELLOWSHIP_REFERENDUMS = 'fellowship_referendums'
-}
-

No imports or other usages of EPostOrigin were found.

src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesDetails.tsx (2)

20-21: Guard display when latestApproval is null; avoid rendering a lone "%"

When latestApproval is null, {latestApproval?.toFixed(2)}% renders just “%”. Use a conditional and show N/A (consistent with thresholds).

Apply this diff:

-					<span className='font-medium text-foreground'>{latestApproval?.toFixed(2)}%</span>
+					<span className='font-medium text-foreground'>
+						{latestApproval !== null && latestApproval !== undefined ? `${latestApproval.toFixed(2)}%` : 'N/A'}
+					</span>

36-37: Apply the same null guard for latestSupport to keep UX consistent

Same issue as Approval: avoid “%” with no number.

Apply this diff:

-					<span className='font-medium text-foreground'>{latestSupport?.toFixed(2)}%</span>
+					<span className='font-medium text-foreground'>
+						{latestSupport !== null && latestSupport !== undefined ? `${latestSupport.toFixed(2)}%` : 'N/A'}
+					</span>
src/components/Post/GovernanceSidebar/index.tsx (1)

9-10: Defer VoteCurvesDataWrapper to client-only to avoid SSR errors

Chart.js registration is already in VoteCurves.tsx, so wrapping the entire curves feature in a client-only bundle will prevent any “window is not defined” errors and speed up initial sidebar render.

Apply this optional refactor in src/components/Post/GovernanceSidebar/index.tsx:

 import React from 'react';
+import dynamic from 'next/dynamic';
 import CastVoteCard from './CastVoteCard';
 import VoteInfoCard from './VoteInfoCard';
 import DecisionStatusCard from './DecisionStatusCard';
-import VoteCurvesDataWrapper from './VoteCurvesData/VoteCurvesDataWrapper';
+const VoteCurvesDataWrapper = dynamic(
+  () => import('./VoteCurvesData/VoteCurvesDataWrapper'),
+  { ssr: false }
+);

@@
   <VoteInfoCard />
-  <VoteCurvesDataWrapper />
+  <VoteCurvesDataWrapper />

— Chart.js elements are registered in VoteCurves.tsx, and dynamic import with { ssr: false } ensures all of it runs client-side.

src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesData.tsx (1)

33-38: Add basic accessibility to the loading overlay.

Expose loading state to assistive tech with role/status and aria-busy for better UX.

Apply:

-       {isFetching && (
-         <div className='absolute inset-0 z-10 flex items-center justify-center bg-white/80 dark:bg-gray-900/80'>
+       {isFetching && (
+         <div
+           className='absolute inset-0 z-10 flex items-center justify-center bg-white/80 dark:bg-gray-900/80'
+           role='status'
+           aria-live='polite'
+           aria-busy='true'
+         >
            <LoadingSpinner size='sm' />
          </div>
        )}
src/app/api/v1/curves/route.ts (2)

26-29: Avoid double Number() coercion downstream.

Use the already-validated numeric values to build variables.

- const variables = {
-   block_gte: Number(blockGte),
-   index_eq: Number(postId)
- };
+ const variables = {
+   block_gte: blockGteNum,
+   index_eq: postIdNum
+ };

33-33: Prefer readable error messages from urql; bubble upstream errors with message and a 502.

Interpolating the Error object in a template string yields “[object Object]”. Use error.message and a gateway status code to indicate upstream failure.

- if (error) throw new APIError(`${error || MESSAGES.SUBSQUID_FETCH_ERROR}`, 500, API_ERROR_CODE.SUBSQUID_FETCH_ERROR);
+ if (error) throw new APIError(error.message || MESSAGES.SUBSQUID_FETCH_ERROR, 502, API_ERROR_CODE.SUBSQUID_FETCH_ERROR);
src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesDataWrapper.tsx (3)

24-31: Deciding block selection: guard for multiple ‘Deciding’ statuses and ensure earliest is used.

If timeline can include multiple ‘Deciding’ entries, using .find() depends on order. Consider selecting the smallest block among matches, or confirm ordering upstream.

- const decidingStatus = timeline.find((status) => status.status === 'Deciding');
- return decidingStatus?.block || 0;
+ const decidingBlocks = timeline.filter((s) => s.status === 'Deciding').map((s) => s.block);
+ return decidingBlocks.length ? Math.min(...decidingBlocks) : 0;

72-75: Refetch on network change; tighten dependencies.

Network affects the API route via headers; include it as a dependency. Optionally, depend on a stable decidingBlock value rather than the entire timeline to avoid unnecessary refetches.

-useEffect(() => {
-  fetchVoteCurves();
-}, [postData.id, timeline]);
+useEffect(() => {
+  fetchVoteCurves();
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+}, [postData.id, network, timeline]);

If you compute a memoized decidingBlock, you can replace timeline in the deps with that value to reduce triggers.


87-89: createdAt prop type may mismatch; normalize to Date.

If postData.created_at is a string (common), pass a Date to match the VoteCurvesData signature.

-      createdAt={postData.created_at}
+      createdAt={postData?.created_at ? new Date(postData.created_at) : undefined}

Also verify the expected type in VoteCurves/VoteCurvesData.

src/utils/trackCurvesUtils.ts (4)

27-28: Decision period comment/value mismatch for Collectives block time.

Collectives in networkConstants uses a 12s block time. For 7 days this should be 50,400 blocks, not 100,800. Either fix the value or update the comment.

-        decisionPeriod: 100800 // 7 days in blocks (assuming 6s block time)
+        decisionPeriod: 50400 // 7 days in blocks (12s block time)

If this field is currently unused, still worth correcting to avoid future confusion.


33-52: Clamp input and centralize the SCALE constant.

Clamping percentage to [0,1] prevents out-of-range inputs from producing invalid thresholds. Also, replace repeated 10 ** 9 literals with a shared SCALE constant for consistency.

+const SCALE = 10 ** 9;
+
 export function makeReciprocalCurve(reciprocal: { factor: number; xOffset: number; yOffset: number }) {
   if (!reciprocal) {
     return null;
   }
   const { factor, xOffset, yOffset } = reciprocal;
   return function fn(percentage: number) {
-    const x = percentage * 10 ** 9;
+    const pct = Math.max(0, Math.min(1, percentage));
+    const x = pct * SCALE;
 
     const v = new BigNumber(factor)
       .div(new BigNumber(x).plus(xOffset))
-      .multipliedBy(10 ** 9)
+      .multipliedBy(SCALE)
       .toFixed(0, BigNumber.ROUND_DOWN);
 
     const calcValue = new BigNumber(v)
       .plus(yOffset)
-      .div(10 ** 9)
+      .div(SCALE)
       .toString();
     return BigNumber.max(calcValue, 0).toNumber();
   };
 }

54-70: Mirror clamping and SCALE usage in linear curve.

Same improvements as above for the linear path.

 export function makeLinearCurve(linearDecreasing: { length: number; floor: number; ceil: number }) {
   if (!linearDecreasing) {
     return null;
   }
   const { length, floor, ceil } = linearDecreasing;
   return function fn(percentage: number) {
-    const x = percentage * 10 ** 9;
+    const pct = Math.max(0, Math.min(1, percentage));
+    const x = pct * SCALE;
 
     const xValue = BigNumber.min(x, length);
     const slope = new BigNumber(ceil).minus(floor).dividedBy(length);
     const deducted = slope.multipliedBy(xValue).toString();
 
     const perbill = new BigNumber(ceil).minus(deducted).toFixed(0, BigNumber.ROUND_DOWN);
-    const calcValue = new BigNumber(perbill).div(10 ** 9).toString();
+    const calcValue = new BigNumber(perbill).div(SCALE).toString();
     return BigNumber.max(calcValue, 0).toNumber();
   };
 }

8-31: Tighten typing for NETWORKS_DETAILS.

Using any hides errors. Consider a typed structure keyed by Network, and restrict trackName keys to ProposalType to improve safety.

If you have the shapes, e.g.:

type Reciprocal = { factor: number; xOffset: number; yOffset: number };
type Linear = { length: number; floor: number; ceil: number };
type Threshold = { reciprocal?: Reciprocal; linearDecreasing?: Linear };
type TrackDetails = Record<ProposalType, { minApproval?: Threshold; minSupport?: Threshold; decisionPeriod?: number }>;
type NetworksDetails = Partial<Record<Network, { trackDetails: TrackDetails }>>;

then:

-const NETWORKS_DETAILS: Record<string, any> = {
+const NETWORKS_DETAILS: NetworksDetails = {

This helps catch typos in track names and mismatched configs at compile time.

src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurves.tsx (5)

93-98: Sort input data by time to ensure monotonic x for rendering and lookups

If voteCurveData isn’t already sorted, the line will zig-zag and tooltips/threshold lookups by x may be off. Sort by timestamp before iterating.

Apply this diff:

-		// Process each data point
-		voteCurveData.forEach((point) => {
+		// Process each data point (sorted by timestamp)
+		const sortedVoteCurveData = [...voteCurveData].sort((a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)));
+		sortedVoteCurveData.forEach((point) => {

200-207: Use Chart.js v3/v4 interaction options instead of tooltip.mode/intersect

In Chart.js v3+, interaction mode and intersect belong under the top-level interaction option, not tooltip. Move them for consistent behavior.

Apply this diff:

 	const chartOptions: ChartOptions<'line'> = {
 		animation: {
 			duration: 0
 		},
 		clip: false,
+		interaction: {
+			mode: 'index',
+			intersect: false
+		},
 		plugins: {
 			legend: {
 				display: false,
 				position: 'bottom'
 			},
 			tooltip: {
 				callbacks: {

And remove these tooltip properties:

-				displayColors: false,
-				intersect: false,
-				mode: 'index'
+				displayColors: false

Also applies to: 243-246


180-181: Keep chart data in sync with its real dependencies

useMemo only depends on voteCurveData, but chartData is also derived from trackName, timeline, createdAt, and the curve functions. Include them to avoid stale charts when these change.

Apply this diff:

-		// eslint-disable-next-line react-hooks/exhaustive-deps
-	}, [voteCurveData]);
+	}, [voteCurveData, trackName, timeline, createdAt, approvalCalc, supportCalc]);

56-61: Optional: Don’t build labels for a linear x-scale

With a linear x-scale and xy objects, labels are unused and may duplicate. You can omit labels entirely and rely solely on x values in data points.

If you want to simplify, return datasets only:

-		const labels: number[] = [];
+		// Labels not required for linear x-scale with xy points
 		...
-		return {
-			datasets: [
+		return {
+			datasets: [
 				...
-			],
-			labels
+			]
 		};

Also applies to: 129-179


156-176: Optional: Visually differentiate thresholds from actuals

Both approval and its threshold share the same color. Consider alpha, lineStyle, or different hue to improve contrast and accessibility.

Example tweak:

-					borderColor: '#2ED47A', // voteAye color
+					borderColor: '#2ED47A',
+					borderDash: undefined,
+					borderWidth: 2
 ...
-					borderColor: '#2ED47A', // voteAye color
+					borderColor: 'rgba(46, 212, 122, 0.6)', // lighter threshold
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 489afe0 and 11a66fa.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (11)
  • package.json (2 hunks)
  • src/app/api/v1/curves/route.ts (1 hunks)
  • src/app/api/v1/subsquidQueries.ts (1 hunks)
  • src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurves.tsx (1 hunks)
  • src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesData.tsx (1 hunks)
  • src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesDataWrapper.tsx (1 hunks)
  • src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesDetails.tsx (1 hunks)
  • src/components/Post/GovernanceSidebar/index.tsx (1 hunks)
  • src/global/networkConstants.ts (1 hunks)
  • src/global/types.ts (1 hunks)
  • src/utils/trackCurvesUtils.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/app/api/v1/curves/route.ts (5)
src/global/exceptions.ts (1)
  • APIError (16-24)
src/global/constants/errorCodes.ts (1)
  • API_ERROR_CODE (5-18)
src/app/api/api-utils/getNetworkFromHeaders.ts (1)
  • getNetworkFromHeaders (10-16)
src/services/urqlClient.ts (1)
  • urqlClient (8-13)
src/app/api/v1/subsquidQueries.ts (1)
  • GET_CURVE_DATA_BY_INDEX (477-488)
src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesDataWrapper.tsx (2)
src/global/types.ts (1)
  • IVoteCurve (798-805)
src/contexts/index.tsx (2)
  • usePostDataContext (28-28)
  • useApiContext (28-28)
src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesData.tsx (1)
src/global/types.ts (2)
  • IStatusHistoryItem (807-812)
  • IVoteCurve (798-805)
src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurves.tsx (2)
src/global/types.ts (2)
  • IVoteCurve (798-805)
  • IStatusHistoryItem (807-812)
src/utils/trackCurvesUtils.ts (1)
  • getTrackFunctions (72-103)
🔇 Additional comments (9)
src/global/types.ts (2)

798-805: IVoteCurve type looks good and aligns with GraphQL shape

Fields and types match the query response and expected usage in charts.


818-821: EProposalStatus is distinct—ProposalStatus doesn’t include these members
A review of ProposalStatus (src/global/types.ts lines 96–116) shows no Deciding or DecisionDepositPlaced entries. The two enums serve different status sets, so EProposalStatus is not a duplicate and should be retained (or, if desired, you may consider adding these values directly to ProposalStatus).

Likely an incorrect or invalid review comment.

src/global/networkConstants.ts (1)

16-16: ✅ Subsquid endpoint verified

The https://polkassembly.squids.live/collectives-polkassembly@v4/api/graphql URL responds with HTTP 200 to a minimal GraphQL query, confirming it’s live and correctly routed.

No further changes needed.

src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesData.tsx (2)

24-54: Component composition looks sound.

Separation of concerns (chart + details), sensible defaults, and guarded rendering with an overlay while fetching are good patterns here.


39-45: VoteCurves Prop Optionality Confirmed
The setThresholdValues prop is already marked as optional in VoteCurves.tsx, so passing it as potentially undefined is safe—no changes required.

src/app/api/v1/curves/route.ts (1)

16-38: Overall route structure is solid.

Good use of withErrorHandling, network determination via headers, and urql client integration. Once the param validation is fixed, this endpoint should be stable.

src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesDataWrapper.tsx (2)

12-70: Data fetching flow is clear and resilient.

Good state handling (isFetching, error fallback), extraction of latest approval/support, and null-render when no data to avoid empty chrome. This aligns well with the API route.


33-49: Verified: x-network header is correctly set in nextApiClientFetch
The network argument is mapped to the 'x-network' header on line 28 of VoteCurvesDataWrapper.tsx’s import (nextApiClientFetch), so no further action is required.

src/utils/trackCurvesUtils.ts (1)

72-103: Helper selection logic is clear and defensive.

Graceful null returns when track info is missing and clean constructor selection for reciprocal/linear are appropriate.

"@urql/core": "^4.1.4",
"@urql/exchange-graphcache": "^6.3.3",
"algoliasearch": "4.17.1",
"chart.js": "^4.5.0",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Chart.js/react-chartjs-2 integration: ensure registration and SSR strategy; consider tree-shaking

  • With react-chartjs-2 v5 + chart.js v4, you must register controllers or import 'chart.js/auto' before rendering charts. Otherwise you'll see “element type is invalid” or blank charts.
  • For Next.js, prefer client-side rendering for Chart.js (or dynamic import with ssr: false) to avoid window undefined/rehydration issues.
  • For bundle size, avoid 'chart.js/auto' in production and register only the used components (LineElement, PointElement, LinearScale, CategoryScale, Tooltip, Legend, etc.).

[suggest_optional_refactor]

Run this to confirm registration and SSR strategy are present:

Also applies to: 62-62


🏁 Script executed:

#!/bin/bash
# Find Chart.js auto import or manual registration
rg -n -C3 -S "chart.js/auto|ChartJS\.register|Chart\.register" --type=ts --type=tsx

# Check if VoteCurves rendering is client-only or dynamically imported with ssr: false
rg -n -C3 -S "use client|next/dynamic|ssr:\s*false" src/components/Post/GovernanceSidebar/VoteCurvesData

Length of output: 220


To ensure we’re catching every import/registration and the SSR setup, let’s broaden the search across all JS/TS extensions and locate the VoteCurvesData component dynamically:

This will confirm:

  • Whether chart.js/auto or manual ChartJS.register calls exist.
  • That VoteCurvesData is either marked "use client" or loaded via next/dynamic with ssr: false.

If nothing shows up, please verify manually that you’re registering Chart.js components before use and that your chart component is client-only.


🏁 Script executed:

#!/bin/bash
# 1. Search for chart.js auto-import and manual registration across all JS/TS files
rg -n -C3 "import ['\"]chart\.js/auto['\"]"
rg -n -C3 "ChartJS\.register|Chart\.register"

# 2. Locate the VoteCurvesData component in any JS/TS file
FILES=$(fd VoteCurvesData -e tsx -e ts -e jsx -e js)
if [ -z "$FILES" ]; then
  echo "⚠️ VoteCurvesData file not found."
else
  for f in $FILES; do
    echo "🔍 Checking SSR setup in $f"
    rg -n -C3 "use client|next/dynamic|ssr:\s*false" "$f" || echo "  – No client-only or dynamic import found in $f"
  done
fi

Length of output: 2432


Ensure client-only rendering for Chart.js in Next.js

We’ve confirmed that you’re manually registering only the needed Chart.js components in VoteCurves.tsx (no chart.js/auto import—good for tree-shaking). However, both VoteCurvesData files are still rendering server-side, which will break under SSR.

Please apply one of the following fixes:

• In each file under src/components/Post/GovernanceSidebar/VoteCurvesData/ (both VoteCurvesData.tsx and VoteCurvesDataWrapper.tsx), add at the very top:

+ "use client";

• Or, wherever you import VoteCurvesData, switch to dynamic loading:

import dynamic from "next/dynamic";

const VoteCurvesData = dynamic(
  () => import("@/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesData"),
  { ssr: false }
);

This ensures Chart.js only runs in the browser and avoids “window is undefined” or blank-chart issues.

🤖 Prompt for AI Agents
In package.json around line 51, the review flags that Chart.js-related
components are being rendered server-side: update both
src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesData.tsx and
src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesDataWrapper.tsx
to be client-only by adding the Next.js client directive as the very first line
of each file ("use client"), or alternatively change every import site to load
VoteCurvesData via next/dynamic with ssr: false; ensure the directive/import
change is applied to both files so Chart.js runs only in the browser.

export const POST = withErrorHandling(async (req: NextRequest) => {
const { blockGte, postId } = await getReqBody(req);

if (!blockGte || !postId || isNaN(postId) || isNaN(blockGte)) throw new APIError(`${MESSAGES.INVALID_PARAMS_ERROR}`, 500, API_ERROR_CODE.INVALID_PARAMS_ERROR);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: valid zero values are incorrectly rejected; use robust numeric validation and return 400 for bad params.

The falsy checks treat 0 as invalid, breaking cases where blockGte can legitimately be 0. Switch to Number coercion with finite checks and respond with 400 for invalid params.

Apply:

- if (!blockGte || !postId || isNaN(postId) || isNaN(blockGte)) throw new APIError(`${MESSAGES.INVALID_PARAMS_ERROR}`, 500, API_ERROR_CODE.INVALID_PARAMS_ERROR);
+ const blockGteNum = Number(blockGte);
+ const postIdNum = Number(postId);
+ if (!Number.isFinite(blockGteNum) || blockGteNum < 0 || !Number.isFinite(postIdNum) || postIdNum < 0) {
+   throw new APIError(MESSAGES.INVALID_PARAMS_ERROR, 400, API_ERROR_CODE.INVALID_PARAMS_ERROR);
+ }
🤖 Prompt for AI Agents
In src/app/api/v1/curves/route.ts around line 19, the current falsy checks
reject valid zero values and incorrectly return a 500; instead coerce postId and
blockGte to numbers (e.g. const postIdNum = Number(postId); const blockGteNum =
Number(blockGte)), validate with Number.isFinite or isFinite(postIdNum) /
isFinite(blockGteNum) to allow 0, and when validation fails throw an APIError
with status 400 and API_ERROR_CODE.INVALID_PARAMS_ERROR; ensure you use the
numeric variables downstream after successful validation.

Comment on lines +30 to +36
function convertGraphPoint(value?: number) {
if (!value) {
return '--';
}

return `${Number(value).toFixed(2)}%`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix percent formatter to correctly handle zero values

Using a falsy check treats 0 as “no value,” rendering 0% as “--”. Return “0.00%” for zero and only use “--” for null/undefined/NaN.

Apply this diff:

 function convertGraphPoint(value?: number) {
-	if (!value) {
-		return '--';
-	}
-
-	return `${Number(value).toFixed(2)}%`;
+	if (value === null || value === undefined || Number.isNaN(value)) {
+		return '--';
+	}
+	return `${Number(value).toFixed(2)}%`;
 }
📝 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
function convertGraphPoint(value?: number) {
if (!value) {
return '--';
}
return `${Number(value).toFixed(2)}%`;
}
function convertGraphPoint(value?: number) {
if (value === null || value === undefined || Number.isNaN(value)) {
return '--';
}
return `${Number(value).toFixed(2)}%`;
}
🤖 Prompt for AI Agents
In src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurves.tsx around
lines 30 to 36, the percent formatter uses a falsy check so 0 is treated as
missing and returns "--"; update the guard to only treat null, undefined, or NaN
as missing (e.g., value === null || value === undefined || Number.isNaN(value))
and otherwise return the formatted string `${Number(value).toFixed(2)}%` so that
0 becomes "0.00%".

Comment on lines +98 to +111
if (decisionPeriodFromTimelineInHrs > decisionPeriodInHrs) {
if (approvalCalc) {
approvalThresholdData.push({
x: hour,
y: approvalCalc(hour / decisionPeriodFromTimelineInHrs) * 100
});
}
if (supportCalc) {
supportThresholdData.push({
x: hour,
y: supportCalc(hour / decisionPeriodFromTimelineInHrs) * 100
});
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Normalize thresholds to the decision period and handle the “equal” case

Two issues:

  • When the timeline equals the decision period, thresholds aren’t generated (strict “>” check skips both branches).
  • When the timeline exceeds the decision period, thresholds are normalized by the timeline length, not the fixed decision period, shifting the curve.

Use “>=” and always normalize by the decision period (clamped to 1).

Apply this diff:

-			if (decisionPeriodFromTimelineInHrs > decisionPeriodInHrs) {
+			if (decisionPeriodFromTimelineInHrs >= decisionPeriodInHrs) {
 				if (approvalCalc) {
 					approvalThresholdData.push({
 						x: hour,
-						y: approvalCalc(hour / decisionPeriodFromTimelineInHrs) * 100
+						y: approvalCalc(Math.min(hour / decisionPeriodInHrs, 1)) * 100
 					});
 				}
 				if (supportCalc) {
 					supportThresholdData.push({
 						x: hour,
-						y: supportCalc(hour / decisionPeriodFromTimelineInHrs) * 100
+						y: supportCalc(Math.min(hour / decisionPeriodInHrs, 1)) * 100
 					});
 				}
 			}
📝 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
if (decisionPeriodFromTimelineInHrs > decisionPeriodInHrs) {
if (approvalCalc) {
approvalThresholdData.push({
x: hour,
y: approvalCalc(hour / decisionPeriodFromTimelineInHrs) * 100
});
}
if (supportCalc) {
supportThresholdData.push({
x: hour,
y: supportCalc(hour / decisionPeriodFromTimelineInHrs) * 100
});
}
}
if (decisionPeriodFromTimelineInHrs >= decisionPeriodInHrs) {
if (approvalCalc) {
approvalThresholdData.push({
x: hour,
y: approvalCalc(Math.min(hour / decisionPeriodInHrs, 1)) * 100
});
}
if (supportCalc) {
supportThresholdData.push({
x: hour,
y: supportCalc(Math.min(hour / decisionPeriodInHrs, 1)) * 100
});
}
}
🤖 Prompt for AI Agents
In src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurves.tsx around
lines 98 to 111, the code skips generating thresholds when
decisionPeriodFromTimelineInHrs equals decisionPeriodInHrs and normalizes by the
timeline length instead of the fixed decision period; change the conditional to
use ">=" so the equal case is handled, and when computing normalized input for
approvalCalc/supportCalc divide by decisionPeriodInHrs (not
decisionPeriodFromTimelineInHrs), clamping the divisor to at least 1 or the
normalized value to a maximum of 1 so inputs never exceed the decision period.

Comment on lines +211 to +227
callbacks: {
label(tooltipItem) {
const { dataIndex, parsed, dataset } = tooltipItem;
if (dataset.label === 'Support Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Support');

const currSupport = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
return `Support: ${convertGraphPoint(currSupport?.y)} / ${threshold}%`;
}
if (dataset.label === 'Approval Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Approval');

const currApproval = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
return `Approval: ${convertGraphPoint(currApproval?.y)} / ${threshold}%`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Tooltip “current” values are computed using dataset index, not x; use parsed.x

The tooltip label compares current vs threshold. It currently uses dataIndex as a proxy for time, which is not the x value and leads to incorrect readings. Use parsed.x to find the nearest point at or before the hovered x.

Apply this diff:

 					label(tooltipItem) {
-						const { dataIndex, parsed, dataset } = tooltipItem;
+						const { parsed, dataset } = tooltipItem;
+						const hoveredX = Number((parsed as any)?.x ?? 0);
 						if (dataset.label === 'Support Threshold') {
 							const threshold = Number(parsed.y).toFixed(2);
-							const data = chartData.datasets.find((d) => d.label === 'Support');
-
-							const currSupport = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
-							return `Support: ${convertGraphPoint(currSupport?.y)} / ${threshold}%`;
+							const data = chartData.datasets.find((d) => d.label === 'Support');
+							const points = ((data?.data as unknown as Point[]) || []) as Point[];
+							const currSupport = points.reduce<Point | undefined>((acc, p) => {
+								const px = (p as any)?.x as number;
+								return px <= hoveredX && (!acc || px > (acc as any).x) ? p : acc;
+							}, undefined);
+							return `Support: ${convertGraphPoint((currSupport as any)?.y)} / ${threshold}%`;
 						}
 						if (dataset.label === 'Approval Threshold') {
 							const threshold = Number(parsed.y).toFixed(2);
-							const data = chartData.datasets.find((d) => d.label === 'Approval');
-
-							const currApproval = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
-							return `Approval: ${convertGraphPoint(currApproval?.y)} / ${threshold}%`;
+							const data = chartData.datasets.find((d) => d.label === 'Approval');
+							const points = ((data?.data as unknown as Point[]) || []) as Point[];
+							const currApproval = points.reduce<Point | undefined>((acc, p) => {
+								const px = (p as any)?.x as number;
+								return px <= hoveredX && (!acc || px > (acc as any).x) ? p : acc;
+							}, undefined);
+							return `Approval: ${convertGraphPoint((currApproval as any)?.y)} / ${threshold}%`;
 						}
 
 						return '';
 					},
📝 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
callbacks: {
label(tooltipItem) {
const { dataIndex, parsed, dataset } = tooltipItem;
if (dataset.label === 'Support Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Support');
const currSupport = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
return `Support: ${convertGraphPoint(currSupport?.y)} / ${threshold}%`;
}
if (dataset.label === 'Approval Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Approval');
const currApproval = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
return `Approval: ${convertGraphPoint(currApproval?.y)} / ${threshold}%`;
}
callbacks: {
label(tooltipItem) {
const { parsed, dataset } = tooltipItem;
const hoveredX = Number((parsed as any)?.x ?? 0);
if (dataset.label === 'Support Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Support');
const points = (data?.data as Point[]) || [];
const currSupport = points.reduce<Point | undefined>((acc, p) => {
const px = (p as any).x as number;
return px <= hoveredX && (!acc || px > (acc as any).x) ? p : acc;
}, undefined);
return `Support: ${convertGraphPoint((currSupport as any)?.y)} / ${threshold}%`;
}
if (dataset.label === 'Approval Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Approval');
const points = (data?.data as Point[]) || [];
const currApproval = points.reduce<Point | undefined>((acc, p) => {
const px = (p as any).x as number;
return px <= hoveredX && (!acc || px > (acc as any).x) ? p : acc;
}, undefined);
return `Approval: ${convertGraphPoint((currApproval as any)?.y)} / ${threshold}%`;
}
return '';
},
},
🤖 Prompt for AI Agents
In src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurves.tsx around
lines 211 to 227, the tooltip label logic uses dataIndex to locate the "current"
Support/Approval point which is incorrect; replace uses of dataIndex with
parsed.x and search the dataset for the point whose x is the greatest value <=
parsed.x (i.e., nearest point at or before the hovered x). Update the find logic
to compare (d as Point).x to parsed.x (cast parsed.x to the correct numeric type
if necessary), handle possible undefined results safely, and keep the existing
formatting using convertGraphPoint and threshold formatting.

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.

1 participant