From def2998a2b6dab82211b70193b6e6731856db7a7 Mon Sep 17 00:00:00 2001 From: kelvinchen03 Date: Tue, 19 May 2026 12:58:00 -0400 Subject: [PATCH] fix: resolve usdPerDay mismatch between miner list and detail APIs --- src/components/leaderboard/MinerCard.tsx | 18 +++++++++------- src/components/leaderboard/MinersList.tsx | 3 ++- src/components/miners/MinerIdentityRail.tsx | 17 ++++++++++++--- src/utils/format.ts | 24 +++++++++++++++++++++ 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/components/leaderboard/MinerCard.tsx b/src/components/leaderboard/MinerCard.tsx index 88711cb2..007e4478 100644 --- a/src/components/leaderboard/MinerCard.tsx +++ b/src/components/leaderboard/MinerCard.tsx @@ -5,6 +5,7 @@ import ReactECharts from 'echarts-for-react'; import { useMinerGithubData, useMinerPRs } from '../../api'; import { CHART_COLORS, RANK_COLORS, STATUS_COLORS } from '../../theme'; import { getGithubAvatarSrc } from '../../utils/ExplorerUtils'; +import { formatUsdPerDay } from '../../utils/format'; import { linkResetSx, useLinkBehavior } from '../common/linkBehavior'; import { WatchlistButton } from '../common'; import { type MinerStats, type LeaderboardVariant, FONTS } from './types'; @@ -62,11 +63,6 @@ const formatScore = (score: number): string => }) : '0.00'; -/** Miner-wide USD/day, shown as a secondary stat. '—' when unavailable — - * per-repo views carry no earnings (earnings is a miner-wide figure). */ -const formatUsdPerDay = (usdPerDay: number | undefined): string => - usdPerDay ? `$${Math.round(usdPerDay).toLocaleString()}/d` : '—'; - /* ═══════════════════════════════════════════════════════════════════ */ export const MinerCard: React.FC = ({ @@ -347,7 +343,7 @@ export const MinerCard: React.FC = ({ fontFeatureSettings: TABULAR_NUMS, })} > - ${Math.round(miner.usdPerDay || 0).toLocaleString()} + {formatUsdPerDay(miner.usdPerDay, { includePeriod: false })} ({ @@ -372,7 +368,11 @@ export const MinerCard: React.FC = ({ fontFeatureSettings: TABULAR_NUMS, })} > - ~${Math.round((miner.usdPerDay || 0) * 30).toLocaleString()}/mo + ~$ + {((miner.usdPerDay || 0) * 30) + .toFixed(0) + .replace(/\B(?=(\d{3})+(?!\d))/g, ',')} + /mo ) : ( @@ -432,7 +432,9 @@ export const MinerCard: React.FC = ({ scoreLabel={isWatchlist ? 'OSS' : 'Earnings'} scoreValue={Number(miner.totalScore)} scoreDisplay={ - isWatchlist ? undefined : formatUsdPerDay(miner.usdPerDay) + isWatchlist + ? undefined + : formatUsdPerDay(miner.usdPerDay, { includePeriod: true }) } isEligible={isDiscoveries ? discoveriesEligible : ossEligible} hideScore={isWatchlist} diff --git a/src/components/leaderboard/MinersList.tsx b/src/components/leaderboard/MinersList.tsx index 75e85585..7c1fb5c6 100644 --- a/src/components/leaderboard/MinersList.tsx +++ b/src/components/leaderboard/MinersList.tsx @@ -3,6 +3,7 @@ import { Avatar, Box, Card, Tooltip, Typography } from '@mui/material'; import { useMinerGithubData, useMinerPRs } from '../../api'; import { CHART_COLORS } from '../../theme'; import { getGithubAvatarSrc, type SortOrder } from '../../utils/ExplorerUtils'; +import { formatUsdPerDay } from '../../utils/format'; import { DataTable, type DataTableColumn, WatchlistButton } from '../common'; import { RankIcon } from './RankIcon'; import { @@ -146,7 +147,7 @@ export const MinersList: React.FC = ({ color: earningsHighlighted ? 'status.merged' : 'text.secondary', }} > - ${Math.round(miner.usdPerDay || 0).toLocaleString()} + {formatUsdPerDay(miner.usdPerDay, { includePeriod: false })} ); }, diff --git a/src/components/miners/MinerIdentityRail.tsx b/src/components/miners/MinerIdentityRail.tsx index 1ee10760..19ee23b5 100644 --- a/src/components/miners/MinerIdentityRail.tsx +++ b/src/components/miners/MinerIdentityRail.tsx @@ -27,6 +27,7 @@ import { import { useClipboardCopy } from '../../hooks/useClipboardCopy'; import { STATUS_COLORS } from '../../theme'; import { getRepositoryOwnerAvatarSrc } from '../../utils/avatar'; +import { formatUsdPerDay } from '../../utils/format'; import MinerInsightsCard from './MinerInsightsCard'; type ViewMode = 'prs' | 'issues'; @@ -434,7 +435,10 @@ const MinerIdentityRail: React.FC = ({ color: usdPerDay > 0 ? STATUS_COLORS.success : 'text.primary', }} > - ~${Math.round(usdPerDay).toLocaleString()} + {formatUsdPerDay(usdPerDay, { + includeApprox: true, + includePeriod: false, + })} = ({ mt: 0.6, }} > - ~${Math.round(usdPerDay * 30).toLocaleString()}/mo · ~$ - {Math.round(lifetimeUsd).toLocaleString()} lifetime + ~$ + {((usdPerDay || 0) * 30) + .toFixed(0) + .replace(/\B(?=(\d{3})+(?!\d))/g, ',')} + /mo · ~$ + {(lifetimeUsd || 0) + .toFixed(0) + .replace(/\B(?=(\d{3})+(?!\d))/g, ',')}{' '} + lifetime diff --git a/src/utils/format.ts b/src/utils/format.ts index 4dd8f77c..1fd17d73 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -89,3 +89,27 @@ export const credibilityColor = (cred: number): string => { export const getLowerText = (value: string | null | undefined): string => (value ?? '').toLowerCase(); + +/** + * Format USD per day value with consistent rounding. + * Uses toFixed(0) for predictable rounding instead of Math.round() + * to avoid inconsistencies between API endpoints. + * + * @param value - The USD per day value + * @param options - Formatting options + * @returns Formatted string like "$124/d" or "—" + */ +export const formatUsdPerDay = ( + value: number | undefined, + options?: { includeApprox?: boolean; includePeriod?: boolean }, +): string => { + const { includeApprox = false, includePeriod = true } = options ?? {}; + + if (!value || value <= 0) return '—'; + + const rounded = Number(value.toFixed(0)); + const approx = includeApprox ? '~' : ''; + const period = includePeriod ? '/d' : ''; + + return `${approx}$${rounded.toLocaleString()}${period}`; +};