Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds 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
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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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 scansThe 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 IStatusHistoryItemSingleStatus (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 enumWe’ve confirmed via a repository-wide search that
EPostOriginis only declared insrc/global/types.tsand never referenced elsewhere. It duplicatesProposalType.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
EPostOriginwere 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 consistentSame 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 errorsChart.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 lookupsIf 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/intersectIn 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: falseAlso applies to: 243-246
180-181: Keep chart data in sync with its real dependenciesuseMemo 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-scaleWith 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 actualsBoth 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.
⛔ Files ignored due to path filters (1)
yarn.lockis 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 shapeFields and types match the query response and expected usage in charts.
818-821: EProposalStatus is distinct—ProposalStatus doesn’t include these members
A review ofProposalStatus(src/global/types.ts lines 96–116) shows noDecidingorDecisionDepositPlacedentries. 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 toProposalStatus).Likely an incorrect or invalid review comment.
src/global/networkConstants.ts (1)
16-16: ✅ Subsquid endpoint verifiedThe
https://polkassembly.squids.live/collectives-polkassembly@v4/api/graphqlURL 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
ThesetThresholdValuesprop is already marked as optional inVoteCurves.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-networkheader is correctly set innextApiClientFetch
Thenetworkargument is mapped to the'x-network'header on line 28 ofVoteCurvesDataWrapper.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", |
There was a problem hiding this comment.
💡 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/VoteCurvesDataLength 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/autoor manualChartJS.registercalls exist. - That
VoteCurvesDatais either marked"use client"or loaded vianext/dynamicwithssr: 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
fiLength 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); |
There was a problem hiding this comment.
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.
| function convertGraphPoint(value?: number) { | ||
| if (!value) { | ||
| return '--'; | ||
| } | ||
|
|
||
| return `${Number(value).toFixed(2)}%`; | ||
| } |
There was a problem hiding this comment.
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.
| 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%".
| 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 | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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}%`; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
Summary by CodeRabbit
New Features
API
Chores