From 5c2da5fc5d8217b26b2b42ac403cbd2c4f08cdd7 Mon Sep 17 00:00:00 2001 From: MananTank Date: Thu, 20 Feb 2025 20:00:55 +0000 Subject: [PATCH] [TOOL-3485] Dashboard: Improve USD currency formatting (#6304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on improving the formatting of monetary values across various components by replacing manual formatting with a utility function `toUSD`. This enhances consistency and readability in displaying currency values. ### Detailed summary - Added `toUSD` utility function for consistent currency formatting. - Replaced manual currency formatting in `PaymentsSuccessRate.tsx`, `Payouts.tsx`, `PayCustomersTable.tsx`, `TotalVolumePieChart.tsx`, and `TotalSponsoredChartCard.tsx`. - Modified random number generation in `storyUtils.ts` to use non-integer values. - Updated `format-utils.ts` to use a new compact number formatter. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .../components/PayCustomersTable.tsx | 3 ++- .../components/PaymentsSuccessRate.tsx | 3 ++- .../pay/PayAnalytics/components/Payouts.tsx | 3 ++- .../components/TotalVolumePieChart.tsx | 5 +++-- .../TotalSponsoredChartCard.tsx | 15 ++++++++++++--- .../AccountAbstractionAnalytics/storyUtils.ts | 8 +++++--- apps/dashboard/src/lib/format-utils.ts | 16 +++++----------- apps/dashboard/src/utils/number.ts | 14 ++++++++++---- 8 files changed, 41 insertions(+), 26 deletions(-) diff --git a/apps/dashboard/src/components/pay/PayAnalytics/components/PayCustomersTable.tsx b/apps/dashboard/src/components/pay/PayAnalytics/components/PayCustomersTable.tsx index bf8a3dd763f..7f80d81daff 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/components/PayCustomersTable.tsx +++ b/apps/dashboard/src/components/pay/PayAnalytics/components/PayCustomersTable.tsx @@ -11,6 +11,7 @@ import { } from "@/components/ui/select"; import { SkeletonContainer } from "@/components/ui/skeleton"; import { useState } from "react"; +import { toUSD } from "../../../../utils/number"; import { type PayTopCustomersData, usePayCustomers, @@ -249,7 +250,7 @@ function getCSVData(data: PayTopCustomersData["customers"]) { const header = ["Wallet Address", "Total spend"]; const rows = data.map((customer) => [ customer.walletAddress, - `$${(customer.totalSpendUSDCents / 100).toLocaleString()}`, + toUSD(customer.totalSpendUSDCents / 100), ]); return { header, rows }; diff --git a/apps/dashboard/src/components/pay/PayAnalytics/components/PaymentsSuccessRate.tsx b/apps/dashboard/src/components/pay/PayAnalytics/components/PaymentsSuccessRate.tsx index 14c439a0a2e..56a3f3bb53d 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/components/PaymentsSuccessRate.tsx +++ b/apps/dashboard/src/components/pay/PayAnalytics/components/PaymentsSuccessRate.tsx @@ -9,6 +9,7 @@ import { SkeletonContainer } from "@/components/ui/skeleton"; import { ToolTipLabel } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { useState } from "react"; +import { toUSD } from "../../../../utils/number"; import { usePayVolume } from "../hooks/usePayVolume"; import { CardHeading, FailedToLoad } from "./common"; @@ -229,7 +230,7 @@ function InfoRow(props: { props.isEmpty ? "$-" : props.amount !== undefined - ? `$${props.amount.toLocaleString()}` + ? toUSD(props.amount) : undefined } skeletonData="$50" diff --git a/apps/dashboard/src/components/pay/PayAnalytics/components/Payouts.tsx b/apps/dashboard/src/components/pay/PayAnalytics/components/Payouts.tsx index cb52860af8f..0f5b4437a19 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/components/Payouts.tsx +++ b/apps/dashboard/src/components/pay/PayAnalytics/components/Payouts.tsx @@ -2,6 +2,7 @@ import { SkeletonContainer } from "@/components/ui/skeleton"; import { format } from "date-fns"; import { useEffect, useState } from "react"; import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis } from "recharts"; +import { toUSD } from "../../../../utils/number"; import { AreaChartLoadingState } from "../../../analytics/area-chart"; import { usePayVolume } from "../hooks/usePayVolume"; import { @@ -144,7 +145,7 @@ function RenderData(props: { props.query.isEmpty ? "$-" : props.query.data - ? `$${props.query.data?.totalPayoutsUSD}` + ? toUSD(props.query.data.totalPayoutsUSD) : undefined } skeletonData="$20" diff --git a/apps/dashboard/src/components/pay/PayAnalytics/components/TotalVolumePieChart.tsx b/apps/dashboard/src/components/pay/PayAnalytics/components/TotalVolumePieChart.tsx index e24666209da..0933c321a92 100644 --- a/apps/dashboard/src/components/pay/PayAnalytics/components/TotalVolumePieChart.tsx +++ b/apps/dashboard/src/components/pay/PayAnalytics/components/TotalVolumePieChart.tsx @@ -1,6 +1,7 @@ import { SkeletonContainer } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { Cell, Pie, PieChart } from "recharts"; +import { toUSD } from "../../../../utils/number"; import { usePayVolume } from "../hooks/usePayVolume"; import { FailedToLoad, chartHeight } from "./common"; @@ -155,7 +156,7 @@ function RenderData(props: { query: ProcessedQuery }) { & { time: string; // human readable date @@ -210,13 +210,22 @@ export function TotalSponsoredChartCard(props: { }} tickLine={false} axisLine={false} - tickFormatter={(value) => `$${formatTickerNumber(value)}`} + tickFormatter={(value) => toUSD(value)} /> `$${value}`} /> + { + // typeguard + if (typeof value !== "number") { + return ""; + } + + return toUSD(value); + }} + /> } /> } /> diff --git a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/storyUtils.ts b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/storyUtils.ts index 60654cd8e8b..9cedbeec5a5 100644 --- a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/storyUtils.ts +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/storyUtils.ts @@ -5,9 +5,11 @@ export function createUserOpStatsStub(days: number): UserOpStats[] { let d = days; while (d !== 0) { - const successful = Math.floor(Math.random() * 100); - const failed = Math.floor(Math.random() * 100); - const sponsoredUsd = Math.floor(Math.random() * 100); + // don't use Math.floor because real data doesn't not have integer values + const successful = Math.random() * 100; + const failed = Math.random() * 100; + const sponsoredUsd = Math.random() * 100; + stubbedData.push({ date: new Date(2024, 1, d).toLocaleString(), successful, diff --git a/apps/dashboard/src/lib/format-utils.ts b/apps/dashboard/src/lib/format-utils.ts index 8327467cfd9..175caf2ca39 100644 --- a/apps/dashboard/src/lib/format-utils.ts +++ b/apps/dashboard/src/lib/format-utils.ts @@ -1,15 +1,9 @@ +const compactNumberFormatter = new Intl.NumberFormat("en-US", { + notation: "compact", +}); + export const formatTickerNumber = (value: number) => { - if (value >= 1000000) { - const millions = value / 1000000; - // Only show decimal if not a whole number, up to 2 decimals with no trailing zeros - return `${millions % 1 === 0 ? millions.toFixed(0) : Number(millions.toFixed(2)).toString()}M`; - } - if (value >= 1000) { - const thousands = value / 1000; - // Only show decimal if not a whole number - return `${thousands % 1 === 0 ? thousands.toFixed(0) : thousands.toFixed(1)}k`; - } - return value.toString(); + return compactNumberFormatter.format(value); }; export const formatWalletType = (walletType: string) => { diff --git a/apps/dashboard/src/utils/number.ts b/apps/dashboard/src/utils/number.ts index 128ae2da288..357c1193802 100644 --- a/apps/dashboard/src/utils/number.ts +++ b/apps/dashboard/src/utils/number.ts @@ -1,8 +1,14 @@ +const usdCurrencyFormatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", // prefix with $ + minimumFractionDigits: 0, // don't show decimal places if value is a whole number + maximumFractionDigits: 2, // at max 2 decimal places + roundingMode: "halfEven", // round to nearest even number, standard practice for financial calculations + notation: "compact", // shows 1.2M instead of 1,200,000, 1.2B instead of 1,200,000,000 +}); + export const toUSD = (value: number) => { - return new Intl.NumberFormat(undefined, { - style: "currency", - currency: "USD", - }).format(value); + return usdCurrencyFormatter.format(value); }; export const toSize = (value: number | bigint, defaultUnit?: string) => {