diff --git a/src/hooks/useTreasury.ts b/src/hooks/useTreasury.ts new file mode 100644 index 0000000..b2f3705 --- /dev/null +++ b/src/hooks/useTreasury.ts @@ -0,0 +1,78 @@ +import { useQuery } from "@tanstack/react-query" + +export interface TreasuryStats { + total_deposited_usdc: string + total_disbursed_usdc: string + scholars_funded: number + active_proposals: number + donors_count: number +} + +export interface TreasuryEvent { + type: "deposit" | "disburse" + amount?: string + address?: string + scholar?: string + tx_hash: string + created_at: string +} + +const API_BASE = + (import.meta.env.VITE_API_BASE_URL as string | undefined) || + (import.meta.env.VITE_SERVER_URL as string | undefined) || + "/api" + +async function fetchTreasuryStats(): Promise { + const response = await fetch(`${API_BASE}/treasury/stats`) + if (!response.ok) { + throw new Error("Failed to load treasury stats") + } + const data = (await response.json()) as TreasuryStats + return data +} + +async function fetchTreasuryActivity(): Promise { + const response = await fetch(`${API_BASE}/treasury/activity?limit=20`) + if (!response.ok) { + throw new Error("Failed to load treasury activity") + } + const data = (await response.json()) as { events?: TreasuryEvent[] } + return data.events ?? [] +} + +export function useTreasury() { + const { + data: stats, + isLoading: isStatsLoading, + error: statsError, + refetch: refetchStats, + } = useQuery({ + queryKey: ["treasury", "stats"], + queryFn: fetchTreasuryStats, + staleTime: 30_000, + refetchInterval: 60_000, + }) + + const { + data: activity, + isLoading: isActivityLoading, + error: activityError, + refetch: refetchActivity, + } = useQuery({ + queryKey: ["treasury", "activity"], + queryFn: fetchTreasuryActivity, + staleTime: 30_000, + refetchInterval: 60_000, + }) + + return { + stats, + activity: activity ?? [], + isLoading: isStatsLoading || isActivityLoading, + isError: Boolean(statsError || activityError), + refetch: () => { + refetchStats() + refetchActivity() + }, + } +} diff --git a/src/pages/Treasury.tsx b/src/pages/Treasury.tsx index 3f1888f..857fd2b 100644 --- a/src/pages/Treasury.tsx +++ b/src/pages/Treasury.tsx @@ -1,18 +1,21 @@ +import React, { Suspense } from "react" import React, { useMemo } from "react" import { useQuery } from "@tanstack/react-query" import { Helmet } from "react-helmet" import { - Area, - AreaChart, - CartesianGrid, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, +Area, +AreaChart, +CartesianGrid, +ResponsiveContainer, +Tooltip, +XAxis, +YAxis, } from "recharts" import { EmptyState } from "../components/states/emptyState" import { ErrorState } from "../components/states/errorState" import TxHashLink from "../components/TxHashLink" +import { useTreasury } from "../hooks/useTreasury" +import { DashboardStatsSkeleton, EmptyState } from "../components/SkeletonLoader" import TreasuryHealthChart, { type TreasuryPoint, } from "../components/treasury/TreasuryHealthChart" @@ -24,20 +27,20 @@ const CHART_WINDOW_DAYS = 7 const STROOPS_PER_USDC = 10000000 interface TreasuryStats { - total_deposited_usdc: string - total_disbursed_usdc: string - scholars_funded: number - active_proposals: number - donors_count: number +total_deposited_usdc: string +total_disbursed_usdc: string +scholars_funded: number +active_proposals: number +donors_count: number } interface TreasuryEvent { - type: "deposit" | "disburse" - amount?: string - address?: string - scholar?: string - tx_hash: string - created_at: string +type: "deposit" | "disburse" +amount?: string +address?: string +scholar?: string +tx_hash: string +created_at: string } interface TreasuryActivityResponse { @@ -108,6 +111,28 @@ const buildTreasuryChartData = (events: TreasuryEvent[]): TreasuryPoint[] => { } const Treasury: React.FC = () => { +const { stats, activity, isLoading, isError } = useTreasury() + +const formatUSDC = (stroops: string) => { +const usdc = Number(stroops) / 10000000 +return usdc.toLocaleString("en-US", { +minimumFractionDigits: 0, +maximumFractionDigits: 2, +}) +} + +const formatAmount = (stroops: string) => { +const usdc = Number(stroops) / 10000000 +return usdc.toLocaleString("en-US", { +minimumFractionDigits: 0, +maximumFractionDigits: 2, +}) +} + +const formatAddress = (address: string) => { +if (address.length <= 8) return address +return `${address.slice(0, 4)}...${address.slice(-4)}` +} const { scholarshipTreasury } = useContractIds() const { balance: treasuryUSDC, isLoading: treasuryLoading } = useUSDC(scholarshipTreasury) @@ -188,11 +213,64 @@ const Treasury: React.FC = () => { }) } - const formatAddress = (address: string) => { - if (address.length <= 8) return address - return `${address.slice(0, 4)}...${address.slice(-4)}` - } +const formatTime = (timestamp: string) => { +const date = new Date(timestamp) +const now = new Date() +const diffMs = now.getTime() - date.getTime() +const diffMins = Math.floor(diffMs / 60000) +const diffHours = Math.floor(diffMins / 60) +const diffDays = Math.floor(diffHours / 24) + +if (diffMins < 60) return `${diffMins}m ago` +if (diffHours < 24) return `${diffHours}h ago` +return `${diffDays}d ago` +} +const displayStats = stats +? { +totalTreasury: `${formatUSDC(stats.total_deposited_usdc)} USDC`, +totalDisbursed: `${formatUSDC(stats.total_disbursed_usdc)} USDC`, +scholarsFunded: stats.scholars_funded.toString(), +donorsCount: stats.donors_count.toString(), +} +: { +totalTreasury: isLoading ? "Loading…" : "0 USDC", +totalDisbursed: isLoading ? "Loading…" : "0 USDC", +scholarsFunded: isLoading ? "..." : "0", +donorsCount: isLoading ? "..." : "0", +} + +const deposits = (activity ?? []) +.filter((e) => e.type === "deposit") +.slice(0, 2) +const disbursements = (activity ?? []) +.filter((e) => e.type === "disburse") +.slice(0, 2) + +const siteUrl = "https://learnvault.app" +const title = `Treasury - ${displayStats.totalTreasury} - ${displayStats.scholarsFunded} Scholars Funded - LearnVault` +const description = `LearnVault's decentralized scholarship treasury holds ${displayStats.totalTreasury} and has funded ${displayStats.scholarsFunded} scholars. View real-time inflows and disbursements.` + +const chartData = [ +{ name: "Mon", inflows: 4000, outflows: 2400 }, +{ name: "Tue", inflows: 3000, outflows: 1398 }, +{ name: "Wed", inflows: 2000, outflows: 9800 }, +{ name: "Thu", inflows: 2780, outflows: 3908 }, +{ name: "Fri", inflows: 1890, outflows: 4800 }, +{ name: "Sat", inflows: 2390, outflows: 3800 }, +{ name: "Sun", inflows: 3490, outflows: 4300 }, +] + +return ( +
+ +{title} + + + + + + const formatTime = (timestamp: string) => { const date = new Date(timestamp) if (Number.isNaN(date.getTime())) return "Unknown time" @@ -274,43 +352,51 @@ const Treasury: React.FC = () => { -
-
-

- Treasury Dashboard -

-

- Real-time transparency into the LearnVault decentralized scholarship - fund. -

-
- -
- - - - -
+
+
+

+Treasury Dashboard +

+

+Real-time transparency into the LearnVault decentralized scholarship +fund. +

+
+ +{isLoading ? ( + +) : isError ? ( +
+Failed to load treasury stats. +
+) : ( +
+ + + + +
+)}
@@ -392,43 +478,43 @@ const Treasury: React.FC = () => { )}
-
- -
-
- ) +
+ +
+
+) } const StatCard: React.FC<{ - label: string - value: string - icon: string - color: string +label: string +value: string +icon: string +color: string }> = ({ label, value, icon, color }) => ( -
-
- {icon} -
-

- {label} -

-

{value}

-
+
+
+{icon} +
+

+{label} +

+

{value}

+
) const LegendItem: React.FC<{ color: string; label: string }> = ({ - color, - label, +color, +label, }) => ( -
-
- {label} -
+
+
+{label} +
) const ChartSkeleton = () => (