Skip to content

Commit 908a919

Browse files
sktbrdclaude
andcommitted
feat(treasury): accented KPI header with synced badge and subnet earnings
Four cards per the design mock — accent-mixed borders, radial corner tints, faded asset marks (noggles/ETH/USDC) — replacing the three plain KPI cards. New Subnet Earnings card (all-time Morpheus USDC + claim count, dash when unknowable). Synced badge binds to the ISR snapshot timestamp, relative time computed client-side against the viewer's clock. TreasuryBalance/TreasuryBalanceClient deleted (superseded). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent bd99f00 commit 908a919

9 files changed

Lines changed: 275 additions & 125 deletions

File tree

messages/en/treasury.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22
"page": {
33
"title": "Treasury",
44
"description": "Overview of the Gnars DAO treasury holdings and financial position",
5+
"synced": "Synced {time}",
56
"kpis": {
67
"totalValue": "Total Treasury Value",
78
"ethBalance": "ETH Balance",
8-
"totalAuctions": "Total Auction Sales"
9+
"totalAuctions": "Total Auction Sales",
10+
"subnetEarnings": "Subnet Earnings",
11+
"acrossAssets": "across {count} assets",
12+
"auctionsSettled": "{count} auctions settled",
13+
"morpheusClaims": "Morpheus · {count} claims"
914
},
1015
"nftSection": {
1116
"title": "NFT Holdings",

messages/pt-br/treasury.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22
"page": {
33
"title": "Tesouro",
44
"description": "Visão geral dos holdings do tesouro da Gnars DAO e da posição financeira",
5+
"synced": "Sincronizado {time}",
56
"kpis": {
67
"totalValue": "Valor Total do Tesouro",
78
"ethBalance": "Saldo de ETH",
8-
"totalAuctions": "Total de Vendas em Leilão"
9+
"totalAuctions": "Total de Vendas em Leilão",
10+
"subnetEarnings": "Ganhos da Subnet",
11+
"acrossAssets": "em {count} ativos",
12+
"auctionsSettled": "{count} leilões concluídos",
13+
"morpheusClaims": "Morpheus · {count} resgates"
914
},
1015
"nftSection": {
1116
"title": "Holdings de NFT",

src/app/[locale]/treasury/page.tsx

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import {
99
} from "@/components/skeletons/treasury-skeletons";
1010
import { NftHoldings } from "@/components/treasury/NftHoldings";
1111
import { SponsorshipYield } from "@/components/treasury/SponsorshipYield";
12+
import { SyncedBadge } from "@/components/treasury/SyncedBadge";
1213
import { TokenHoldings } from "@/components/treasury/TokenHoldings";
1314
import { TreasuryAllocation } from "@/components/treasury/TreasuryAllocation";
14-
import { TreasuryBalance } from "@/components/treasury/TreasuryBalance";
1515
import { TreasuryInflows } from "@/components/treasury/TreasuryInflows";
16-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
16+
import { TreasuryKpiRow } from "@/components/treasury/TreasuryKpiRow";
1717
import { DAO_ADDRESSES } from "@/lib/config";
1818
import { getBrlRateForRequest } from "@/services/exchange-rate";
1919

@@ -79,47 +79,31 @@ export default async function TreasuryPage({ params }: { params: Promise<{ local
7979
return (
8080
<div className="py-8">
8181
<div className="space-y-8">
82-
{/* Page Header */}
83-
<div className="space-y-2">
84-
<h1 className="text-3xl font-bold tracking-tight">{t("page.title")}</h1>
85-
<p className="text-muted-foreground">{t("page.description")}</p>
82+
{/* Page Header — synced badge binds to the ISR snapshot's timestamp */}
83+
<div className="flex flex-wrap items-end justify-between gap-4">
84+
<div className="space-y-2">
85+
<h1 className="text-3xl font-bold tracking-tight">{t("page.title")}</h1>
86+
<p className="text-muted-foreground">{t("page.description")}</p>
87+
</div>
88+
<Suspense fallback={null}>
89+
<SyncedBadge />
90+
</Suspense>
8691
</div>
8792

88-
{/* KPIs */}
89-
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
90-
<Card className="gap-2">
91-
<CardHeader>
92-
<CardTitle className="text-sm font-medium">{t("page.kpis.totalValue")}</CardTitle>
93-
</CardHeader>
94-
<CardContent>
95-
<Suspense fallback={<MetricSkeleton />}>
96-
<TreasuryBalance treasuryAddress={DAO_ADDRESSES.treasury} />
97-
</Suspense>
98-
</CardContent>
99-
</Card>
100-
101-
<Card className="gap-2">
102-
<CardHeader>
103-
<CardTitle className="text-sm font-medium">{t("page.kpis.ethBalance")}</CardTitle>
104-
</CardHeader>
105-
<CardContent>
106-
<Suspense fallback={<MetricSkeleton />}>
107-
<TreasuryBalance treasuryAddress={DAO_ADDRESSES.treasury} metric="eth" />
108-
</Suspense>
109-
</CardContent>
110-
</Card>
111-
112-
<Card className="gap-2">
113-
<CardHeader>
114-
<CardTitle className="text-sm font-medium">{t("page.kpis.totalAuctions")}</CardTitle>
115-
</CardHeader>
116-
<CardContent>
117-
<Suspense fallback={<MetricSkeleton />}>
118-
<TreasuryBalance treasuryAddress={DAO_ADDRESSES.treasury} metric="auctions" />
119-
</Suspense>
120-
</CardContent>
121-
</Card>
122-
</div>
93+
{/* KPIs — one component, one snapshot load, four accented cards */}
94+
<Suspense
95+
fallback={
96+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
97+
{Array.from({ length: 4 }).map((_, i) => (
98+
<div key={i} className="rounded-xl border bg-card p-6">
99+
<MetricSkeleton />
100+
</div>
101+
))}
102+
</div>
103+
}
104+
>
105+
<TreasuryKpiRow />
106+
</Suspense>
123107

124108
{/* What the treasury is MADE OF, before the income breakdown below.
125109
Every figure is already on this page — the point is the share, which
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"use client";
2+
3+
import { useLocale } from "next-intl";
4+
import { CountUp } from "@/components/ui/count-up";
5+
import { localizeFiat } from "@/lib/i18n/fiat";
6+
7+
interface KpiValueProps {
8+
/** `null` = value unavailable — renders an em dash, never 0. */
9+
value: number | null;
10+
decimals: number;
11+
/** Token unit ("ETH", "USDC") rendered after the number. Omit for fiat. */
12+
unit?: string;
13+
/** Convert to BRL on pt-br (Total Treasury Value only). */
14+
fiat?: boolean;
15+
brlRate?: number | null;
16+
className?: string;
17+
}
18+
19+
export function KpiValue({
20+
value,
21+
decimals,
22+
unit,
23+
fiat,
24+
brlRate = null,
25+
className,
26+
}: KpiValueProps) {
27+
const locale = useLocale();
28+
if (value == null) {
29+
return <span className="font-mono text-2xl font-bold text-muted-foreground"></span>;
30+
}
31+
const { value: displayValue, currency } = fiat
32+
? localizeFiat(value, locale, brlRate)
33+
: { value, currency: null };
34+
const prefix =
35+
currency === "BRL" ? "R$ " : currency === "USD" ? (locale === "pt-br" ? "US$ " : "$") : "";
36+
return (
37+
<span className="flex items-baseline gap-1.5 whitespace-nowrap">
38+
<span
39+
className={`font-mono text-2xl font-bold tabular-nums tracking-tight ${className ?? ""}`}
40+
>
41+
{prefix}
42+
<CountUp value={displayValue} decimals={decimals} className="tabular-nums" />
43+
</span>
44+
{unit && <span className="text-xs font-medium text-muted-foreground">{unit}</span>}
45+
</span>
46+
);
47+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { DAO_ADDRESSES } from "@/lib/config";
2+
import { loadTreasurySnapshot } from "@/services/treasury";
3+
import { SyncedBadgeLabel } from "./SyncedBadgeLabel";
4+
5+
/** "Synced Xm ago" pill — bound to when the ISR treasury snapshot was computed. */
6+
export async function SyncedBadge() {
7+
let generatedAt: number;
8+
try {
9+
generatedAt = (await loadTreasurySnapshot(DAO_ADDRESSES.treasury)).generatedAt;
10+
} catch {
11+
return null;
12+
}
13+
return (
14+
<div className="flex items-center gap-2 rounded-lg border bg-card px-3 py-1.5 text-xs font-medium text-muted-foreground">
15+
<span aria-hidden className="size-1.5 rounded-full bg-[var(--chart-2)]" />
16+
<SyncedBadgeLabel generatedAt={generatedAt} />
17+
</div>
18+
);
19+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useFormatter, useTranslations } from "next-intl";
5+
6+
/**
7+
* Relative time must be computed against the viewer's clock — an ISR page's
8+
* server render can itself be minutes old, so the server HTML carries no time
9+
* and the label fills in on mount.
10+
*/
11+
export function SyncedBadgeLabel({ generatedAt }: { generatedAt: number }) {
12+
const t = useTranslations("treasury.page");
13+
const format = useFormatter();
14+
const [time, setTime] = useState<string | null>(null);
15+
useEffect(() => {
16+
setTime(format.relativeTime(generatedAt, Date.now()));
17+
}, [format, generatedAt]);
18+
if (time == null) return null;
19+
return <span>{t("synced", { time })}</span>;
20+
}

src/components/treasury/TreasuryBalance.tsx

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/components/treasury/TreasuryBalanceClient.tsx

Lines changed: 0 additions & 52 deletions
This file was deleted.

0 commit comments

Comments
 (0)