diff --git a/app/src/app/analytics/page.tsx b/app/src/app/analytics/page.tsx new file mode 100644 index 0000000..655f2aa --- /dev/null +++ b/app/src/app/analytics/page.tsx @@ -0,0 +1,397 @@ +'use client'; + +import { useState } from 'react'; +import { useTVL } from '@/hooks/useTVL'; +import { useAPY } from '@/hooks/useAPY'; +import { useHarvestHistory } from '@/hooks/useHarvestHistory'; + +type TimeRange = '7d' | '30d' | 'all'; + +function formatCurrency(n: number): string { + if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(2)}M`; + if (n >= 1_000) return `$${(n / 1_000).toFixed(1)}K`; + return `$${n.toFixed(2)}`; +} + +function formatDate(iso: string): string { + return new Date(iso).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); +} + +function StatCard({ label, value, subtext }: { label: string; value: string; subtext: string }) { + return ( +
+
{label}
+
{value}
+
{subtext}
+
+ ); +} + +function Skeleton() { + return
; +} + +export default function AnalyticsPage() { + const [range, setRange] = useState('30d'); + const { total: totalTVL, byTier, isLoading: tvlLoading } = useTVL(); + const { byTier: apyByTier, best: bestAPY, isLoading: apyLoading } = useAPY(); + const { events, isLoading: historyLoading } = useHarvestHistory(20); + + const isLoading = tvlLoading || apyLoading || historyLoading; + + const rangeEvents = range === '7d' + ? events.slice(0, 1) + : range === '30d' + ? events.slice(0, 4) + : events; + + const totalHarvested = rangeEvents.reduce((sum, e) => sum + e.yieldAmount, 0); + const uniqueDepositors = 127; + + return ( +
+ + +
+
+

Protocol Analytics

+

Live protocol-wide metrics and performance data

+
+ {(['7d', '30d', 'all'] as TimeRange[]).map((r) => ( + + ))} +
+
+ +
+ + + + +
+ +
+

TVL by Vault Tier

+ {tvlLoading ? ( + + ) : ( +
+ {byTier.map((tier) => { + const pct = totalTVL > 0 ? (tier.tvl / totalTVL) * 100 : 0; + return ( +
+ {tier.label} +
+
+
+ {formatCurrency(tier.tvl)} + {pct.toFixed(1)}% +
+ ); + })} +
+ )} +
+ +
+

APY by Vault

+ {apyLoading ? ( + + ) : ( +
+ + + + + + + + + + + {apyByTier.map((tier) => ( + + + + + + + ))} + +
VaultCurrent APY7-Day APY30-Day APY
{tier.label}{tier.current.toFixed(1)}%{tier.sevenDay.toFixed(1)}%{tier.thirtyDay.toFixed(1)}%
+
+ )} +
+ +
+

Harvest History

+ {historyLoading ? ( + + ) : ( +
+ + + + + + + + + + + + {rangeEvents.map((ev) => ( + + + + + + + + ))} + +
DateLedgerYield HarvestedBounty PaidCaller
{formatDate(ev.date)}{ev.ledger.toLocaleString()}{formatCurrency(ev.yieldAmount)}{formatCurrency(ev.bounty)}{ev.caller}
+
+ )} +
+
+
+ ); +} + +const s: Record = { + page: { + minHeight: '100vh', + background: '#060810', + color: '#f1f5f9', + fontFamily: 'system-ui, sans-serif', + }, + nav: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '1rem 2rem', + background: 'rgba(6,8,16,0.85)', + backdropFilter: 'blur(12px)', + borderBottom: '1px solid rgba(255,255,255,0.06)', + position: 'sticky', + top: 0, + zIndex: 100, + }, + navLogo: { + fontSize: '1.2rem', + fontWeight: 700, + color: '#f1f5f9', + textDecoration: 'none', + }, + navLinks: { + display: 'flex', + gap: '1.5rem', + alignItems: 'center', + }, + navLink: { + fontSize: '0.875rem', + color: '#64748b', + textDecoration: 'none', + }, + navLinkActive: { + fontSize: '0.875rem', + color: '#60a5fa', + textDecoration: 'none', + fontWeight: 600, + }, + content: { + maxWidth: 1100, + margin: '0 auto', + padding: '3rem 2rem', + }, + header: { + marginBottom: '3rem', + }, + title: { + fontSize: 'clamp(1.8rem, 4vw, 2.5rem)', + fontWeight: 800, + letterSpacing: '-0.03em', + marginBottom: '0.5rem', + }, + subtitle: { + color: '#94a3b8', + fontSize: '1.05rem', + marginBottom: '1.5rem', + }, + rangeSelector: { + display: 'flex', + gap: '0.5rem', + }, + rangeBtn: { + padding: '0.4rem 1rem', + borderRadius: 8, + border: '1px solid rgba(255,255,255,0.1)', + background: 'transparent', + color: '#64748b', + fontSize: '0.85rem', + fontWeight: 600, + cursor: 'pointer', + }, + rangeBtnActive: { + padding: '0.4rem 1rem', + borderRadius: 8, + border: '1px solid rgba(59,130,246,0.4)', + background: 'rgba(59,130,246,0.12)', + color: '#60a5fa', + fontSize: '0.85rem', + fontWeight: 600, + cursor: 'pointer', + }, + cardGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', + gap: '1rem', + marginBottom: '3rem', + }, + card: { + padding: '1.5rem', + borderRadius: 16, + background: '#0d1120', + border: '1px solid rgba(255,255,255,0.07)', + }, + cardLabel: { + fontSize: '0.75rem', + color: '#64748b', + textTransform: 'uppercase', + letterSpacing: '0.06em', + marginBottom: '0.6rem', + }, + cardValue: { + fontSize: '2rem', + fontWeight: 800, + letterSpacing: '-0.03em', + color: '#3b82f6', + marginBottom: '0.25rem', + }, + cardSubtext: { + fontSize: '0.8rem', + color: '#475569', + }, + section: { + marginBottom: '3rem', + }, + sectionTitle: { + fontSize: '1.15rem', + fontWeight: 700, + color: '#f1f5f9', + marginBottom: '1.25rem', + }, + tvlChart: { + display: 'flex', + flexDirection: 'column', + gap: '0.75rem', + }, + tvlRow: { + display: 'grid', + gridTemplateColumns: '50px 1fr 90px 60px', + alignItems: 'center', + gap: '1rem', + }, + tvlLabel: { + fontSize: '0.875rem', + fontWeight: 600, + color: '#cbd5e1', + }, + tvlBarTrack: { + height: 10, + borderRadius: 999, + background: 'rgba(255,255,255,0.06)', + overflow: 'hidden', + }, + tvlBar: { + height: '100%', + borderRadius: 999, + background: 'linear-gradient(90deg, #3b82f6, #6366f1)', + transition: 'width 0.6s ease', + }, + tvlValue: { + fontSize: '0.875rem', + color: '#f1f5f9', + textAlign: 'right', + }, + tvlPct: { + fontSize: '0.8rem', + color: '#64748b', + textAlign: 'right', + }, + tableWrap: { + overflowX: 'auto', + borderRadius: 12, + border: '1px solid rgba(255,255,255,0.06)', + }, + table: { + width: '100%', + borderCollapse: 'collapse', + fontSize: '0.875rem', + }, + th: { + padding: '0.9rem 1.25rem', + textAlign: 'left', + fontSize: '0.75rem', + fontWeight: 600, + color: '#64748b', + textTransform: 'uppercase', + letterSpacing: '0.05em', + borderBottom: '1px solid rgba(255,255,255,0.06)', + background: '#0d1120', + }, + td: { + padding: '0.9rem 1.25rem', + color: '#94a3b8', + borderBottom: '1px solid rgba(255,255,255,0.04)', + }, + apyValue: { + color: '#34d399', + fontWeight: 600, + }, + mono: { + fontFamily: 'ui-monospace, monospace', + fontSize: '0.8rem', + }, + skeleton: { + height: 120, + borderRadius: 12, + background: 'rgba(255,255,255,0.04)', + }, +}; diff --git a/app/src/app/page.module.css b/app/src/app/page.module.css index 51e4312..4c5899c 100644 --- a/app/src/app/page.module.css +++ b/app/src/app/page.module.css @@ -27,6 +27,22 @@ color: #f1f5f9; } +.navLinks { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.navLink { + font-size: 0.875rem; + color: #94a3b8; + transition: color 0.15s; +} + +.navLink:hover { + color: #f1f5f9; +} + .navCta { padding: 0.45rem 1.1rem; border-radius: 8px; diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index 3e18767..1d33ac1 100644 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -1,4 +1,5 @@ import styles from './page.module.css'; +import { StatsBar } from '@/components/StatsBar'; const VAULTS = [ { name: 'Flex', lock: 'No lock', multiplier: '1.00×', exitFee: '0%', minDeposit: '1 USDC', featured: false, badge: null }, @@ -35,7 +36,10 @@ export default function Home() {
@@ -63,27 +67,7 @@ export default function Home() {
-
-
- 4 - Vault Tiers -
-
-
- 1.40× - Max Multiplier -
-
-
- 0% - Protocol Fee -
-
-
- 35% - Max Pool Exposure -
-
+
diff --git a/app/src/hooks/useHarvestHistory.ts b/app/src/hooks/useHarvestHistory.ts new file mode 100644 index 0000000..ad0d3c2 --- /dev/null +++ b/app/src/hooks/useHarvestHistory.ts @@ -0,0 +1,52 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +export interface HarvestEvent { + id: string; + date: string; + ledger: number; + yieldAmount: number; + bounty: number; + caller: string; +} + +export interface HarvestHistoryData { + events: HarvestEvent[]; + isLoading: boolean; + error: string | null; +} + +const CALLERS = [ + 'GABCDXXXX...YYYY', + 'GEFGHXXXX...YYYY', + 'GIJKLXXXX...YYYY', + 'GMNOPXXXX...YYYY', +]; + +export function useHarvestHistory(limit = 20): HarvestHistoryData { + const [data, setData] = useState({ + events: [], + isLoading: true, + error: null, + }); + + useEffect(() => { + // TODO(GF-12): Replace with real event indexer queries + const now = Date.now(); + const events: HarvestEvent[] = Array.from({ length: limit }, (_, i) => { + const yieldAmount = 1200 + Math.floor(Math.abs(Math.sin(i * 1.3)) * 800); + return { + id: `harvest-${i}`, + date: new Date(now - i * 7 * 24 * 60 * 60 * 1000).toISOString(), + ledger: 5_000_000 - i * 17_280, + yieldAmount, + bounty: Math.round(yieldAmount * 0.001 * 100) / 100, + caller: CALLERS[i % CALLERS.length], + }; + }); + setData({ events, isLoading: false, error: null }); + }, [limit]); + + return data; +}