feat(dashboard): implement number formatting for country distribution and notice types
- Integrated formatShortNumber utility to display formatted numbers in the CountryDistribution and NoticeTypeBreakdown components. - Enhanced user experience by providing clearer visual representation of numerical data with optional positive prefixes.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { formatShortNumber } from "@/lib/format-number";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import gsap from "gsap";
|
import gsap from "gsap";
|
||||||
import { useLayoutEffect, useMemo, useRef } from "react";
|
import { useLayoutEffect, useMemo, useRef } from "react";
|
||||||
@@ -155,7 +156,7 @@ export function CountryDistribution({ countries, isLoading }: Props) {
|
|||||||
Total
|
Total
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-dark dark:text-white">
|
<div className="text-2xl font-bold text-dark dark:text-white">
|
||||||
{isLoading ? "—" : totalShown}
|
{isLoading ? "—" : formatShortNumber(totalShown, { plus: true })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { formatShortNumber } from "@/lib/format-number";
|
||||||
import gsap from "gsap";
|
import gsap from "gsap";
|
||||||
import { useLayoutEffect, useMemo, useRef } from "react";
|
import { useLayoutEffect, useMemo, useRef } from "react";
|
||||||
import { TenderIcon } from "./icons";
|
import { TenderIcon } from "./icons";
|
||||||
@@ -94,7 +95,9 @@ export function NoticeTypeBreakdown({ noticeTypes, isLoading }: Props) {
|
|||||||
{isLoading ? "Loading…" : labelize(n.type)}
|
{isLoading ? "Loading…" : labelize(n.type)}
|
||||||
</span>
|
</span>
|
||||||
<span className="tabular-nums text-xs font-semibold text-dark-5 dark:text-dark-6">
|
<span className="tabular-nums text-xs font-semibold text-dark-5 dark:text-dark-6">
|
||||||
{isLoading ? "—" : `${n.count} · ${pct}%`}
|
{isLoading
|
||||||
|
? "—"
|
||||||
|
: `${formatShortNumber(n.count, { plus: true })} · ${pct}%`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-2.5 w-full overflow-hidden rounded-full bg-gray-2 dark:bg-dark-3">
|
<div className="relative h-2.5 w-full overflow-hidden rounded-full bg-gray-2 dark:bg-dark-3">
|
||||||
|
|||||||
@@ -1,10 +1,56 @@
|
|||||||
export function compactFormat(value: number) {
|
export type FormatShortNumberOptions = {
|
||||||
const formatter = new Intl.NumberFormat("en", {
|
/** Prefix positive values with "+" (e.g. +37k) */
|
||||||
notation: "compact",
|
plus?: boolean;
|
||||||
compactDisplay: "short",
|
/** Fraction digits for K / M / B suffixes (default 1) */
|
||||||
});
|
maximumFractionDigits?: number;
|
||||||
|
};
|
||||||
|
|
||||||
return formatter.format(value);
|
function trimTrailingZeros(value: string) {
|
||||||
|
return value.replace(/\.0+$/, "").replace(/(\.[0-9]*?)0+$/, "$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatScaled(value: number, maximumFractionDigits: number) {
|
||||||
|
if (Number.isInteger(value)) return String(value);
|
||||||
|
return trimTrailingZeros(value.toFixed(maximumFractionDigits));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortens large numbers for display: 37030 → "37k", 2500000 → "2.5M".
|
||||||
|
* Use `plus: true` for "+37k" style labels (e.g. chart totals).
|
||||||
|
*/
|
||||||
|
export function formatShortNumber(
|
||||||
|
value: number,
|
||||||
|
options?: FormatShortNumberOptions,
|
||||||
|
): string {
|
||||||
|
const { plus = false, maximumFractionDigits = 1 } = options ?? {};
|
||||||
|
|
||||||
|
if (!Number.isFinite(value)) return "—";
|
||||||
|
|
||||||
|
const sign =
|
||||||
|
value < 0 ? "-" : plus && value > 0 ? "+" : "";
|
||||||
|
const abs = Math.abs(value);
|
||||||
|
|
||||||
|
let body: string;
|
||||||
|
|
||||||
|
if (abs < 1000) {
|
||||||
|
body =
|
||||||
|
abs % 1 === 0
|
||||||
|
? String(Math.round(abs))
|
||||||
|
: trimTrailingZeros(abs.toFixed(maximumFractionDigits));
|
||||||
|
} else if (abs < 1_000_000) {
|
||||||
|
body = `${formatScaled(abs / 1000, maximumFractionDigits)}k`;
|
||||||
|
} else if (abs < 1_000_000_000) {
|
||||||
|
body = `${formatScaled(abs / 1_000_000, maximumFractionDigits)}M`;
|
||||||
|
} else {
|
||||||
|
body = `${formatScaled(abs / 1_000_000_000, maximumFractionDigits)}B`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${sign}${body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Prefer `formatShortNumber` — kept for existing imports */
|
||||||
|
export function compactFormat(value: number) {
|
||||||
|
return formatShortNumber(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function standardFormat(value: number) {
|
export function standardFormat(value: number) {
|
||||||
@@ -12,4 +58,4 @@ export function standardFormat(value: number) {
|
|||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user