Skip to content

Commit 944df45

Browse files
Merge pull request #281 from Stellar-Insightss/resolve-272-network-new-accounts
feat(network): add New Accounts panel to /network dashboard
2 parents 9a2144d + 25b7cca commit 944df45

3 files changed

Lines changed: 301 additions & 0 deletions

File tree

frontend/src/app/[locale]/network/page.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import dynamic from "next/dynamic";
55
import { Activity, Share2, Info } from "lucide-react";
66
import {
77
fetchNetworkPaymentVolume,
8+
fetchNetworkNewAccounts,
89
fetchNetworkFeeTrends,
910
type NetworkPaymentVolumePoint,
11+
type NetworkNewAccountsPoint,
1012
type NetworkFeeTrendPoint,
1113
} from "@/lib/network-api";
1214
import { logger } from "@/lib/logger";
@@ -36,6 +38,23 @@ const PaymentVolumeChart = dynamic(
3638
},
3739
);
3840

41+
const NewAccountsChart = dynamic(
42+
() =>
43+
import("@/components/charts/NewAccountsChart").then((m) => ({
44+
default: m.NewAccountsChart,
45+
})),
46+
{
47+
ssr: false,
48+
loading: () => (
49+
<div className="glass-card rounded-2xl p-6 border border-border/50 h-[420px] animate-pulse">
50+
<div className="h-4 w-40 bg-white/5 rounded mb-4" />
51+
<div className="h-8 w-64 bg-white/5 rounded mb-8" />
52+
<div className="h-[260px] w-full bg-white/5 rounded-xl" />
53+
</div>
54+
),
55+
},
56+
);
57+
3958
const FeeTrendsChart = dynamic(
4059
() =>
4160
import("@/components/charts/FeeTrendsChart").then((m) => ({
@@ -61,6 +80,10 @@ export default function NetworkPage() {
6180
[],
6281
);
6382
const [volumeLoading, setVolumeLoading] = useState(true);
83+
const [newAccountsPoints, setNewAccountsPoints] = useState<
84+
NetworkNewAccountsPoint[]
85+
>([]);
86+
const [newAccountsLoading, setNewAccountsLoading] = useState(true);
6487
const [feeTrendPoints, setFeeTrendPoints] = useState<NetworkFeeTrendPoint[]>(
6588
[],
6689
);
@@ -98,6 +121,22 @@ export default function NetworkPage() {
98121
void loadPaymentVolume();
99122
}, []);
100123

124+
useEffect(() => {
125+
async function loadNewAccounts() {
126+
setNewAccountsLoading(true);
127+
try {
128+
const result = await fetchNetworkNewAccounts(30);
129+
setNewAccountsPoints(result.points);
130+
} catch (err) {
131+
logger.error("Failed to load new accounts panel:", err);
132+
setNewAccountsPoints([]);
133+
} finally {
134+
setNewAccountsLoading(false);
135+
}
136+
}
137+
void loadNewAccounts();
138+
}, []);
139+
101140
useEffect(() => {
102141
async function loadFeeTrends() {
103142
setFeeTrendsLoading(true);
@@ -158,6 +197,10 @@ export default function NetworkPage() {
158197

159198
{/* Network metrics panels */}
160199
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
200+
<NewAccountsChart
201+
data={newAccountsPoints}
202+
loading={newAccountsLoading}
203+
/>
161204
<FeeTrendsChart data={feeTrendPoints} loading={feeTrendsLoading} />
162205
<PaymentVolumeChart data={volumePoints} loading={volumeLoading} />
163206
</div>
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"use client";
2+
3+
import { useRef } from "react";
4+
import {
5+
Bar,
6+
BarChart,
7+
CartesianGrid,
8+
ResponsiveContainer,
9+
Tooltip,
10+
XAxis,
11+
YAxis,
12+
} from "recharts";
13+
import { ChartExportButton } from "./ChartExportButton";
14+
import { getTooltipContentStyle } from "@/lib/chart-utils";
15+
import type { NetworkNewAccountsPoint } from "@/lib/network-api";
16+
17+
interface NewAccountsChartProps {
18+
data: NetworkNewAccountsPoint[];
19+
loading?: boolean;
20+
}
21+
22+
function formatCompact(value: number): string {
23+
return new Intl.NumberFormat("en-US", {
24+
notation: "compact",
25+
maximumFractionDigits: 1,
26+
}).format(value);
27+
}
28+
29+
export function NewAccountsChart({
30+
data,
31+
loading = false,
32+
}: NewAccountsChartProps) {
33+
const chartRef = useRef<HTMLDivElement>(null);
34+
35+
const chartData = data.map((point) => ({
36+
label: new Date(point.date).toLocaleDateString("en-US", {
37+
month: "short",
38+
day: "numeric",
39+
}),
40+
date: point.date,
41+
count: point.count,
42+
}));
43+
44+
const total = chartData.reduce((sum, d) => sum + d.count, 0);
45+
const latest = chartData[chartData.length - 1]?.count ?? 0;
46+
const peak = chartData.length
47+
? Math.max(...chartData.map((d) => d.count))
48+
: 0;
49+
50+
if (loading) {
51+
return (
52+
<div
53+
className="glass-card rounded-2xl p-6 border border-border/50 h-[420px] animate-pulse"
54+
aria-busy="true"
55+
aria-label="Loading new accounts"
56+
>
57+
<div className="h-4 w-40 bg-white/5 rounded mb-4" />
58+
<div className="h-8 w-64 bg-white/5 rounded mb-8" />
59+
<div className="h-[260px] w-full bg-white/5 rounded-xl" />
60+
</div>
61+
);
62+
}
63+
64+
if (chartData.length === 0) {
65+
return (
66+
<section
67+
aria-labelledby="new-accounts-heading"
68+
className="glass-card rounded-2xl p-6 border border-border/50 flex flex-col items-center justify-center h-[420px]"
69+
>
70+
<div className="text-[10px] font-mono text-accent uppercase tracking-[0.2em] mb-2">
71+
Network // New Accounts
72+
</div>
73+
<h2
74+
id="new-accounts-heading"
75+
className="text-xl font-black tracking-tighter uppercase italic mb-2 opacity-50"
76+
>
77+
New Accounts
78+
</h2>
79+
<p className="text-sm font-mono text-muted-foreground uppercase tracking-widest text-center max-w-md">
80+
No new-account series yet. Data appears once{" "}
81+
<code className="text-accent">/api/v1/network/new-accounts</code>{" "}
82+
returns daily account-creation counts.
83+
</p>
84+
</section>
85+
);
86+
}
87+
88+
return (
89+
<section
90+
ref={chartRef}
91+
aria-labelledby="new-accounts-heading"
92+
className="glass-card rounded-2xl p-6 border border-border/50"
93+
>
94+
<div className="flex flex-col md:flex-row md:items-start justify-between mb-8 gap-4">
95+
<div className="flex-1">
96+
<div className="text-[10px] font-mono text-accent uppercase tracking-[0.2em] mb-2">
97+
Network // New Accounts
98+
</div>
99+
<h2
100+
id="new-accounts-heading"
101+
className="text-xl font-black tracking-tighter uppercase italic mb-2"
102+
>
103+
New Accounts
104+
</h2>
105+
<p className="text-[10px] font-mono text-muted-foreground uppercase tracking-widest">
106+
Accounts created per day
107+
</p>
108+
</div>
109+
<ChartExportButton chartRef={chartRef} chartName="New Accounts" />
110+
</div>
111+
112+
<div className="grid grid-cols-3 gap-4 mb-8">
113+
<div className="p-3 rounded-xl bg-slate-900/30 border border-white/5">
114+
<p className="text-[9px] font-mono text-muted-foreground uppercase tracking-wider mb-1">
115+
Latest day
116+
</p>
117+
<p className="text-xl font-black font-mono tracking-tighter text-emerald-400">
118+
{formatCompact(latest)}
119+
</p>
120+
</div>
121+
<div className="p-3 rounded-xl bg-slate-900/30 border border-white/5">
122+
<p className="text-[9px] font-mono text-muted-foreground uppercase tracking-wider mb-1">
123+
Period total
124+
</p>
125+
<p className="text-xl font-black font-mono tracking-tighter text-foreground/80">
126+
{formatCompact(total)}
127+
</p>
128+
</div>
129+
<div className="p-3 rounded-xl bg-slate-900/30 border border-white/5">
130+
<p className="text-[9px] font-mono text-muted-foreground uppercase tracking-wider mb-1">
131+
Peak day
132+
</p>
133+
<p className="text-xl font-black font-mono tracking-tighter text-accent">
134+
{formatCompact(peak)}
135+
</p>
136+
</div>
137+
</div>
138+
139+
<div className="h-[300px] w-full">
140+
<ResponsiveContainer width="100%" height="100%">
141+
<BarChart
142+
data={chartData}
143+
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
144+
>
145+
<CartesianGrid
146+
strokeDasharray="3 3"
147+
stroke="rgba(255,255,255,0.05)"
148+
vertical={false}
149+
/>
150+
<XAxis
151+
dataKey="label"
152+
stroke="rgba(255,255,255,0.3)"
153+
tick={{ fontSize: 10, fontFamily: "monospace" }}
154+
axisLine={false}
155+
tickLine={false}
156+
dy={10}
157+
/>
158+
<YAxis
159+
stroke="rgba(255,255,255,0.3)"
160+
tickFormatter={formatCompact}
161+
tick={{ fontSize: 10, fontFamily: "monospace" }}
162+
axisLine={false}
163+
tickLine={false}
164+
dx={-10}
165+
width={48}
166+
/>
167+
<Tooltip
168+
contentStyle={getTooltipContentStyle({
169+
backgroundColor: "rgba(15, 23, 42, 0.9)",
170+
border: "1px solid rgba(255, 255, 255, 0.1)",
171+
borderRadius: "12px",
172+
fontSize: "10px",
173+
fontFamily: "monospace",
174+
})}
175+
labelStyle={{ color: "#94a3b8", marginBottom: "4px" }}
176+
formatter={(value) => [
177+
formatCompact(typeof value === "number" ? value : Number(value)),
178+
"New accounts",
179+
]}
180+
/>
181+
<Bar
182+
dataKey="count"
183+
name="New accounts"
184+
fill="#f59e0b"
185+
radius={[4, 4, 0, 0]}
186+
/>
187+
</BarChart>
188+
</ResponsiveContainer>
189+
</div>
190+
</section>
191+
);
192+
}

frontend/src/lib/network-api.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* Network dashboard API client.
33
* Backed by Stellar-Insightss/backend#15–#19:
4+
* GET /api/v1/network/new-accounts → {date, count} (backend#18)
45
* GET /api/v1/network/fee-trends → {date, avg_fee} (backend#19)
56
* GET /api/v1/network/payment-volume → {date, volume} (backend#17)
67
*
@@ -23,6 +24,16 @@ export interface NetworkPaymentVolumeResponse {
2324
unit: "usd";
2425
}
2526

27+
export interface NetworkNewAccountsPoint {
28+
date: string;
29+
/** New accounts created that day. */
30+
count: number;
31+
}
32+
33+
export interface NetworkNewAccountsResponse {
34+
points: NetworkNewAccountsPoint[];
35+
}
36+
2637
export interface NetworkFeeTrendPoint {
2738
date: string;
2839
/** Average transaction fee for the day, in stroops. */
@@ -101,6 +112,37 @@ export async function fetchNetworkPaymentVolume(
101112
}
102113
}
103114

115+
function normalizeNewAccountsPoints(raw: unknown): NetworkNewAccountsPoint[] {
116+
const rows = Array.isArray(raw)
117+
? raw
118+
: Array.isArray((raw as { points?: unknown })?.points)
119+
? (raw as { points: unknown[] }).points
120+
: Array.isArray((raw as { series?: unknown })?.series)
121+
? (raw as { series: unknown[] }).series
122+
: Array.isArray((raw as { data?: unknown })?.data)
123+
? (raw as { data: unknown[] }).data
124+
: [];
125+
126+
return rows
127+
.map((row) => {
128+
const item = row as {
129+
date?: string;
130+
day?: string;
131+
timestamp?: string;
132+
count?: number;
133+
new_accounts?: number;
134+
created_accounts?: number;
135+
};
136+
const date = item.date ?? item.day ?? item.timestamp;
137+
const count = item.count ?? item.new_accounts ?? item.created_accounts;
138+
if (!date || typeof count !== "number" || Number.isNaN(count))
139+
return null;
140+
return { date, count };
141+
})
142+
.filter((point): point is NetworkNewAccountsPoint => point != null)
143+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
144+
}
145+
104146
function normalizeFeeTrendPoints(raw: unknown): NetworkFeeTrendPoint[] {
105147
const rows = Array.isArray(raw)
106148
? raw
@@ -133,6 +175,30 @@ function normalizeFeeTrendPoints(raw: unknown): NetworkFeeTrendPoint[] {
133175
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
134176
}
135177

178+
/**
179+
* New-accounts-per-day time series (backend#18).
180+
* Returns an empty series when the backend is unavailable so the panel
181+
* can show an empty state without failing the whole /network page.
182+
*/
183+
export async function fetchNetworkNewAccounts(
184+
days = 30,
185+
): Promise<NetworkNewAccountsResponse> {
186+
const url = `${API_BASE}/api/v1/network/new-accounts?days=${days}`;
187+
try {
188+
const data = await fetchJson<unknown>(url);
189+
return { points: normalizeNewAccountsPoints(data) };
190+
} catch (error) {
191+
const isNetworkError =
192+
error instanceof TypeError &&
193+
(error.message.includes("Failed to fetch") ||
194+
error.message.includes("Network request failed"));
195+
if (!isNetworkError) {
196+
logger.error("Failed to fetch network new accounts:", error);
197+
}
198+
return { points: [] };
199+
}
200+
}
201+
136202
/**
137203
* Daily average-fee time series (backend#19) — completes the Network
138204
* Dashboard panel set (DAA, tx/day, volume, new accounts, fee trends).

0 commit comments

Comments
 (0)