Skip to content

Commit 4c551b7

Browse files
authored
Merge pull request #452 from Adeyemi-cmd/treasury_page
feat(treasury): hookify treasury page with /api/treasury/stats + /api…
2 parents 94cdf0a + bfa6778 commit 4c551b7

2 files changed

Lines changed: 252 additions & 88 deletions

File tree

src/hooks/useTreasury.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useQuery } from "@tanstack/react-query"
2+
3+
export interface TreasuryStats {
4+
total_deposited_usdc: string
5+
total_disbursed_usdc: string
6+
scholars_funded: number
7+
active_proposals: number
8+
donors_count: number
9+
}
10+
11+
export interface TreasuryEvent {
12+
type: "deposit" | "disburse"
13+
amount?: string
14+
address?: string
15+
scholar?: string
16+
tx_hash: string
17+
created_at: string
18+
}
19+
20+
const API_BASE =
21+
(import.meta.env.VITE_API_BASE_URL as string | undefined) ||
22+
(import.meta.env.VITE_SERVER_URL as string | undefined) ||
23+
"/api"
24+
25+
async function fetchTreasuryStats(): Promise<TreasuryStats> {
26+
const response = await fetch(`${API_BASE}/treasury/stats`)
27+
if (!response.ok) {
28+
throw new Error("Failed to load treasury stats")
29+
}
30+
const data = (await response.json()) as TreasuryStats
31+
return data
32+
}
33+
34+
async function fetchTreasuryActivity(): Promise<TreasuryEvent[]> {
35+
const response = await fetch(`${API_BASE}/treasury/activity?limit=20`)
36+
if (!response.ok) {
37+
throw new Error("Failed to load treasury activity")
38+
}
39+
const data = (await response.json()) as { events?: TreasuryEvent[] }
40+
return data.events ?? []
41+
}
42+
43+
export function useTreasury() {
44+
const {
45+
data: stats,
46+
isLoading: isStatsLoading,
47+
error: statsError,
48+
refetch: refetchStats,
49+
} = useQuery({
50+
queryKey: ["treasury", "stats"],
51+
queryFn: fetchTreasuryStats,
52+
staleTime: 30_000,
53+
refetchInterval: 60_000,
54+
})
55+
56+
const {
57+
data: activity,
58+
isLoading: isActivityLoading,
59+
error: activityError,
60+
refetch: refetchActivity,
61+
} = useQuery({
62+
queryKey: ["treasury", "activity"],
63+
queryFn: fetchTreasuryActivity,
64+
staleTime: 30_000,
65+
refetchInterval: 60_000,
66+
})
67+
68+
return {
69+
stats,
70+
activity: activity ?? [],
71+
isLoading: isStatsLoading || isActivityLoading,
72+
isError: Boolean(statsError || activityError),
73+
refetch: () => {
74+
refetchStats()
75+
refetchActivity()
76+
},
77+
}
78+
}

src/pages/Treasury.tsx

Lines changed: 174 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1+
import React, { Suspense } from "react"
12
import React, { useMemo } from "react"
23
import { useQuery } from "@tanstack/react-query"
34
import { Helmet } from "react-helmet"
45
import {
5-
Area,
6-
AreaChart,
7-
CartesianGrid,
8-
ResponsiveContainer,
9-
Tooltip,
10-
XAxis,
11-
YAxis,
6+
Area,
7+
AreaChart,
8+
CartesianGrid,
9+
ResponsiveContainer,
10+
Tooltip,
11+
XAxis,
12+
YAxis,
1213
} from "recharts"
1314
import { EmptyState } from "../components/states/emptyState"
1415
import { ErrorState } from "../components/states/errorState"
1516
import TxHashLink from "../components/TxHashLink"
17+
import { useTreasury } from "../hooks/useTreasury"
18+
import { DashboardStatsSkeleton, EmptyState } from "../components/SkeletonLoader"
1619
import TreasuryHealthChart, {
1720
type TreasuryPoint,
1821
} from "../components/treasury/TreasuryHealthChart"
@@ -24,20 +27,20 @@ const CHART_WINDOW_DAYS = 7
2427
const STROOPS_PER_USDC = 10000000
2528

2629
interface TreasuryStats {
27-
total_deposited_usdc: string
28-
total_disbursed_usdc: string
29-
scholars_funded: number
30-
active_proposals: number
31-
donors_count: number
30+
total_deposited_usdc: string
31+
total_disbursed_usdc: string
32+
scholars_funded: number
33+
active_proposals: number
34+
donors_count: number
3235
}
3336

3437
interface TreasuryEvent {
35-
type: "deposit" | "disburse"
36-
amount?: string
37-
address?: string
38-
scholar?: string
39-
tx_hash: string
40-
created_at: string
38+
type: "deposit" | "disburse"
39+
amount?: string
40+
address?: string
41+
scholar?: string
42+
tx_hash: string
43+
created_at: string
4144
}
4245

4346
interface TreasuryActivityResponse {
@@ -108,6 +111,28 @@ const buildTreasuryChartData = (events: TreasuryEvent[]): TreasuryPoint[] => {
108111
}
109112

110113
const Treasury: React.FC = () => {
114+
const { stats, activity, isLoading, isError } = useTreasury()
115+
116+
const formatUSDC = (stroops: string) => {
117+
const usdc = Number(stroops) / 10000000
118+
return usdc.toLocaleString("en-US", {
119+
minimumFractionDigits: 0,
120+
maximumFractionDigits: 2,
121+
})
122+
}
123+
124+
const formatAmount = (stroops: string) => {
125+
const usdc = Number(stroops) / 10000000
126+
return usdc.toLocaleString("en-US", {
127+
minimumFractionDigits: 0,
128+
maximumFractionDigits: 2,
129+
})
130+
}
131+
132+
const formatAddress = (address: string) => {
133+
if (address.length <= 8) return address
134+
return `${address.slice(0, 4)}...${address.slice(-4)}`
135+
}
111136
const { scholarshipTreasury } = useContractIds()
112137
const { balance: treasuryUSDC, isLoading: treasuryLoading } =
113138
useUSDC(scholarshipTreasury)
@@ -188,11 +213,64 @@ const Treasury: React.FC = () => {
188213
})
189214
}
190215

191-
const formatAddress = (address: string) => {
192-
if (address.length <= 8) return address
193-
return `${address.slice(0, 4)}...${address.slice(-4)}`
194-
}
216+
const formatTime = (timestamp: string) => {
217+
const date = new Date(timestamp)
218+
const now = new Date()
219+
const diffMs = now.getTime() - date.getTime()
220+
const diffMins = Math.floor(diffMs / 60000)
221+
const diffHours = Math.floor(diffMins / 60)
222+
const diffDays = Math.floor(diffHours / 24)
223+
224+
if (diffMins < 60) return `${diffMins}m ago`
225+
if (diffHours < 24) return `${diffHours}h ago`
226+
return `${diffDays}d ago`
227+
}
195228

229+
const displayStats = stats
230+
? {
231+
totalTreasury: `${formatUSDC(stats.total_deposited_usdc)} USDC`,
232+
totalDisbursed: `${formatUSDC(stats.total_disbursed_usdc)} USDC`,
233+
scholarsFunded: stats.scholars_funded.toString(),
234+
donorsCount: stats.donors_count.toString(),
235+
}
236+
: {
237+
totalTreasury: isLoading ? "Loading…" : "0 USDC",
238+
totalDisbursed: isLoading ? "Loading…" : "0 USDC",
239+
scholarsFunded: isLoading ? "..." : "0",
240+
donorsCount: isLoading ? "..." : "0",
241+
}
242+
243+
const deposits = (activity ?? [])
244+
.filter((e) => e.type === "deposit")
245+
.slice(0, 2)
246+
const disbursements = (activity ?? [])
247+
.filter((e) => e.type === "disburse")
248+
.slice(0, 2)
249+
250+
const siteUrl = "https://learnvault.app"
251+
const title = `Treasury - ${displayStats.totalTreasury} - ${displayStats.scholarsFunded} Scholars Funded - LearnVault`
252+
const description = `LearnVault's decentralized scholarship treasury holds ${displayStats.totalTreasury} and has funded ${displayStats.scholarsFunded} scholars. View real-time inflows and disbursements.`
253+
254+
const chartData = [
255+
{ name: "Mon", inflows: 4000, outflows: 2400 },
256+
{ name: "Tue", inflows: 3000, outflows: 1398 },
257+
{ name: "Wed", inflows: 2000, outflows: 9800 },
258+
{ name: "Thu", inflows: 2780, outflows: 3908 },
259+
{ name: "Fri", inflows: 1890, outflows: 4800 },
260+
{ name: "Sat", inflows: 2390, outflows: 3800 },
261+
{ name: "Sun", inflows: 3490, outflows: 4300 },
262+
]
263+
264+
return (
265+
<div className="p-12 max-w-7xl mx-auto min-h-screen text-white animate-in fade-in duration-1000">
266+
<Helmet>
267+
<title>{title}</title>
268+
<meta property="og:title" content={title} />
269+
<meta property="og:description" content={description} />
270+
<meta property="og:image" content={`${siteUrl}/og-image.png`} />
271+
<meta property="og:url" content={`${siteUrl}/treasury`} />
272+
<meta name="twitter:card" content="summary_large_image" />
273+
</Helmet>
196274
const formatTime = (timestamp: string) => {
197275
const date = new Date(timestamp)
198276
if (Number.isNaN(date.getTime())) return "Unknown time"
@@ -274,43 +352,51 @@ const Treasury: React.FC = () => {
274352
<meta name="twitter:card" content="summary_large_image" />
275353
</Helmet>
276354

277-
<header className="text-center mb-20 relative">
278-
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-brand-cyan/20 blur-[100px] rounded-full -z-10" />
279-
<h1 className="text-7xl font-black mb-4 tracking-tighter text-gradient">
280-
Treasury Dashboard
281-
</h1>
282-
<p className="text-white/40 text-lg max-w-2xl mx-auto font-medium">
283-
Real-time transparency into the LearnVault decentralized scholarship
284-
fund.
285-
</p>
286-
</header>
287-
288-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 mb-20">
289-
<StatCard
290-
label="Total in Treasury"
291-
value={displayStats.totalTreasury}
292-
icon={"\u{1F4B0}"}
293-
color="text-brand-cyan"
294-
/>
295-
<StatCard
296-
label="Total Disbursed"
297-
value={displayStats.totalDisbursed}
298-
icon={"\u{1F4B8}"}
299-
color="text-brand-purple"
300-
/>
301-
<StatCard
302-
label="Scholars Funded"
303-
value={displayStats.scholarsFunded}
304-
icon={"\u{1F393}"}
305-
color="text-brand-emerald"
306-
/>
307-
<StatCard
308-
label="Global Donors"
309-
value={displayStats.donorsCount}
310-
icon={"\u{1F30D}"}
311-
color="text-brand-blue"
312-
/>
313-
</div>
355+
<header className="text-center mb-20 relative">
356+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-brand-cyan/20 blur-[100px] rounded-full -z-10" />
357+
<h1 className="text-7xl font-black mb-4 tracking-tighter text-gradient">
358+
Treasury Dashboard
359+
</h1>
360+
<p className="text-white/40 text-lg max-w-2xl mx-auto font-medium">
361+
Real-time transparency into the LearnVault decentralized scholarship
362+
fund.
363+
</p>
364+
</header>
365+
366+
{isLoading ? (
367+
<DashboardStatsSkeleton />
368+
) : isError ? (
369+
<div className="glass-card p-8 rounded-[3rem] border border-white/5 text-center text-red-400">
370+
Failed to load treasury stats.
371+
</div>
372+
) : (
373+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 mb-20">
374+
<StatCard
375+
label="Total in Treasury"
376+
value={displayStats.totalTreasury}
377+
icon={"\u{1F4B0}"}
378+
color="text-brand-cyan"
379+
/>
380+
<StatCard
381+
label="Total Disbursed"
382+
value={displayStats.totalDisbursed}
383+
icon={"\u{1F4B8}"}
384+
color="text-brand-purple"
385+
/>
386+
<StatCard
387+
label="Scholars Funded"
388+
value={displayStats.scholarsFunded}
389+
icon={"\u{1F393}"}
390+
color="text-brand-emerald"
391+
/>
392+
<StatCard
393+
label="Global Donors"
394+
value={displayStats.donorsCount}
395+
icon={"\u{1F30D}"}
396+
color="text-brand-blue"
397+
/>
398+
</div>
399+
)}
314400

315401
<div className="mb-20">
316402
<div className="glass-card p-10 rounded-[3rem] relative overflow-hidden">
@@ -392,43 +478,43 @@ const Treasury: React.FC = () => {
392478
)}
393479
</div>
394480

395-
<div className="mt-20 text-center">
396-
<button className="iridescent-border px-12 py-5 rounded-2xl font-black text-lg uppercase tracking-widest hover:scale-105 active:scale-95 transition-all group overflow-hidden shadow-2xl shadow-brand-cyan/20">
397-
<span className="relative z-10">Donate to Treasury</span>
398-
</button>
399-
</div>
400-
</div>
401-
)
481+
<div className="mt-20 text-center">
482+
<button className="iridescent-border px-12 py-5 rounded-2xl font-black text-lg uppercase tracking-widest hover:scale-105 active:scale-95 transition-all group overflow-hidden shadow-2xl shadow-brand-cyan/20">
483+
<span className="relative z-10">Donate to Treasury</span>
484+
</button>
485+
</div>
486+
</div>
487+
)
402488
}
403489

404490
const StatCard: React.FC<{
405-
label: string
406-
value: string
407-
icon: string
408-
color: string
491+
label: string
492+
value: string
493+
icon: string
494+
color: string
409495
}> = ({ label, value, icon, color }) => (
410-
<div className="glass-card p-8 rounded-4xl hover:border-white/20 transition-all hover:-translate-y-2 group">
411-
<div className="text-3xl mb-4 group-hover:scale-125 transition-transform duration-500">
412-
{icon}
413-
</div>
414-
<p className="text-[10px] uppercase font-black text-white/30 tracking-[2px] mb-1">
415-
{label}
416-
</p>
417-
<p className={`text-2xl font-black ${color} tracking-tight`}>{value}</p>
418-
</div>
496+
<div className="glass-card p-8 rounded-4xl hover:border-white/20 transition-all hover:-translate-y-2 group">
497+
<div className="text-3xl mb-4 group-hover:scale-125 transition-transform duration-500">
498+
{icon}
499+
</div>
500+
<p className="text-[10px] uppercase font-black text-white/30 tracking-[2px] mb-1">
501+
{label}
502+
</p>
503+
<p className={`text-2xl font-black ${color} tracking-tight`}>{value}</p>
504+
</div>
419505
)
420506

421507
const LegendItem: React.FC<{ color: string; label: string }> = ({
422-
color,
423-
label,
508+
color,
509+
label,
424510
}) => (
425-
<div className="flex items-center gap-2">
426-
<div
427-
className="w-3 h-3 rounded-full shadow-[0_0_10px_rgba(0,0,0,0.5)]"
428-
style={{ backgroundColor: color }}
429-
/>
430-
<span className="text-xs font-bold text-white/60">{label}</span>
431-
</div>
511+
<div className="flex items-center gap-2">
512+
<div
513+
className="w-3 h-3 rounded-full shadow-[0_0_10px_rgba(0,0,0,0.5)]"
514+
style={{ backgroundColor: color }}
515+
/>
516+
<span className="text-xs font-bold text-white/60">{label}</span>
517+
</div>
432518
)
433519

434520
const ChartSkeleton = () => (

0 commit comments

Comments
 (0)