diff --git a/frontend/src/app/[locale]/network/page.tsx b/frontend/src/app/[locale]/network/page.tsx index b0eff2f1..a5fe4ac1 100644 --- a/frontend/src/app/[locale]/network/page.tsx +++ b/frontend/src/app/[locale]/network/page.tsx @@ -5,8 +5,10 @@ import dynamic from "next/dynamic"; import { Activity, Share2, Info } from "lucide-react"; import { fetchNetworkPaymentVolume, + fetchNetworkNewAccounts, fetchNetworkFeeTrends, type NetworkPaymentVolumePoint, + type NetworkNewAccountsPoint, type NetworkFeeTrendPoint, } from "@/lib/network-api"; import { logger } from "@/lib/logger"; @@ -36,6 +38,23 @@ const PaymentVolumeChart = dynamic( }, ); +const NewAccountsChart = dynamic( + () => + import("@/components/charts/NewAccountsChart").then((m) => ({ + default: m.NewAccountsChart, + })), + { + ssr: false, + loading: () => ( +
+
+
+
+
+ ), + }, +); + const FeeTrendsChart = dynamic( () => import("@/components/charts/FeeTrendsChart").then((m) => ({ @@ -61,6 +80,10 @@ export default function NetworkPage() { [], ); const [volumeLoading, setVolumeLoading] = useState(true); + const [newAccountsPoints, setNewAccountsPoints] = useState< + NetworkNewAccountsPoint[] + >([]); + const [newAccountsLoading, setNewAccountsLoading] = useState(true); const [feeTrendPoints, setFeeTrendPoints] = useState( [], ); @@ -98,6 +121,22 @@ export default function NetworkPage() { void loadPaymentVolume(); }, []); + useEffect(() => { + async function loadNewAccounts() { + setNewAccountsLoading(true); + try { + const result = await fetchNetworkNewAccounts(30); + setNewAccountsPoints(result.points); + } catch (err) { + logger.error("Failed to load new accounts panel:", err); + setNewAccountsPoints([]); + } finally { + setNewAccountsLoading(false); + } + } + void loadNewAccounts(); + }, []); + useEffect(() => { async function loadFeeTrends() { setFeeTrendsLoading(true); @@ -158,6 +197,10 @@ export default function NetworkPage() { {/* Network metrics panels */}
+
diff --git a/frontend/src/components/charts/NewAccountsChart.tsx b/frontend/src/components/charts/NewAccountsChart.tsx new file mode 100644 index 00000000..ebfa2e11 --- /dev/null +++ b/frontend/src/components/charts/NewAccountsChart.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { useRef } from "react"; +import { + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { ChartExportButton } from "./ChartExportButton"; +import { getTooltipContentStyle } from "@/lib/chart-utils"; +import type { NetworkNewAccountsPoint } from "@/lib/network-api"; + +interface NewAccountsChartProps { + data: NetworkNewAccountsPoint[]; + loading?: boolean; +} + +function formatCompact(value: number): string { + return new Intl.NumberFormat("en-US", { + notation: "compact", + maximumFractionDigits: 1, + }).format(value); +} + +export function NewAccountsChart({ + data, + loading = false, +}: NewAccountsChartProps) { + const chartRef = useRef(null); + + const chartData = data.map((point) => ({ + label: new Date(point.date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }), + date: point.date, + count: point.count, + })); + + const total = chartData.reduce((sum, d) => sum + d.count, 0); + const latest = chartData[chartData.length - 1]?.count ?? 0; + const peak = chartData.length + ? Math.max(...chartData.map((d) => d.count)) + : 0; + + if (loading) { + return ( +
+
+
+
+
+ ); + } + + if (chartData.length === 0) { + return ( +
+
+ Network // New Accounts +
+

+ New Accounts +

+

+ No new-account series yet. Data appears once{" "} + /api/v1/network/new-accounts{" "} + returns daily account-creation counts. +

+
+ ); + } + + return ( +
+
+
+
+ Network // New Accounts +
+

+ New Accounts +

+

+ Accounts created per day +

+
+ +
+ +
+
+

+ Latest day +

+

+ {formatCompact(latest)} +

+
+
+

+ Period total +

+

+ {formatCompact(total)} +

+
+
+

+ Peak day +

+

+ {formatCompact(peak)} +

+
+
+ +
+ + + + + + [ + formatCompact(typeof value === "number" ? value : Number(value)), + "New accounts", + ]} + /> + + + +
+
+ ); +} diff --git a/frontend/src/lib/network-api.ts b/frontend/src/lib/network-api.ts index ff0ce700..46aae38c 100644 --- a/frontend/src/lib/network-api.ts +++ b/frontend/src/lib/network-api.ts @@ -1,6 +1,7 @@ /** * Network dashboard API client. * Backed by Stellar-Insightss/backend#15–#19: + * GET /api/v1/network/new-accounts → {date, count} (backend#18) * GET /api/v1/network/fee-trends → {date, avg_fee} (backend#19) * GET /api/v1/network/payment-volume → {date, volume} (backend#17) * @@ -23,6 +24,16 @@ export interface NetworkPaymentVolumeResponse { unit: "usd"; } +export interface NetworkNewAccountsPoint { + date: string; + /** New accounts created that day. */ + count: number; +} + +export interface NetworkNewAccountsResponse { + points: NetworkNewAccountsPoint[]; +} + export interface NetworkFeeTrendPoint { date: string; /** Average transaction fee for the day, in stroops. */ @@ -101,6 +112,37 @@ export async function fetchNetworkPaymentVolume( } } +function normalizeNewAccountsPoints(raw: unknown): NetworkNewAccountsPoint[] { + const rows = Array.isArray(raw) + ? raw + : Array.isArray((raw as { points?: unknown })?.points) + ? (raw as { points: unknown[] }).points + : Array.isArray((raw as { series?: unknown })?.series) + ? (raw as { series: unknown[] }).series + : Array.isArray((raw as { data?: unknown })?.data) + ? (raw as { data: unknown[] }).data + : []; + + return rows + .map((row) => { + const item = row as { + date?: string; + day?: string; + timestamp?: string; + count?: number; + new_accounts?: number; + created_accounts?: number; + }; + const date = item.date ?? item.day ?? item.timestamp; + const count = item.count ?? item.new_accounts ?? item.created_accounts; + if (!date || typeof count !== "number" || Number.isNaN(count)) + return null; + return { date, count }; + }) + .filter((point): point is NetworkNewAccountsPoint => point != null) + .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); +} + function normalizeFeeTrendPoints(raw: unknown): NetworkFeeTrendPoint[] { const rows = Array.isArray(raw) ? raw @@ -133,6 +175,30 @@ function normalizeFeeTrendPoints(raw: unknown): NetworkFeeTrendPoint[] { .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); } +/** + * New-accounts-per-day time series (backend#18). + * Returns an empty series when the backend is unavailable so the panel + * can show an empty state without failing the whole /network page. + */ +export async function fetchNetworkNewAccounts( + days = 30, +): Promise { + const url = `${API_BASE}/api/v1/network/new-accounts?days=${days}`; + try { + const data = await fetchJson(url); + return { points: normalizeNewAccountsPoints(data) }; + } catch (error) { + const isNetworkError = + error instanceof TypeError && + (error.message.includes("Failed to fetch") || + error.message.includes("Network request failed")); + if (!isNetworkError) { + logger.error("Failed to fetch network new accounts:", error); + } + return { points: [] }; + } +} + /** * Daily average-fee time series (backend#19) — completes the Network * Dashboard panel set (DAA, tx/day, volume, new accounts, fee trends).