Skip to content

Commit ca94110

Browse files
authored
Merge pull request #2 from Pi-Defi-world/PIRC
Pirc
2 parents 7f7e5ca + 28dad0a commit ca94110

25 files changed

Lines changed: 4015 additions & 48 deletions

app/dashboard/page.tsx

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
"use client"
2+
3+
import { useMemo } from "react"
4+
import Link from "next/link"
5+
import { Wallet, Activity, Coins, TrendingUp, Droplets, PiggyBank, CreditCard, AlertTriangle } from "lucide-react"
6+
7+
import { TokenCard, type TokenSummary } from "@/components/token-card"
8+
import { TransactionHistory } from "@/components/transaction-history"
9+
import { ActivityChart, type ActivityPoint } from "@/components/activity-chart"
10+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
11+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
12+
import { Button } from "@/components/ui/button"
13+
import { useCurrentUser } from "@/hooks/useCurrentUser"
14+
import { useAccountBalances, useAccountOperations } from "@/hooks/useAccountData"
15+
import { useTokenRegistry } from "@/hooks/useTokenRegistry"
16+
import { useLaunches } from "@/hooks/useLaunchpadData"
17+
import { useSavingsPositions } from "@/hooks/useSavingsData"
18+
import { useLendingPositions } from "@/hooks/useLendingData"
19+
20+
const formatBalanceCard = (asset: {
21+
assetCode: string
22+
assetIssuer: string | null
23+
amount: number
24+
assetType: string
25+
}): TokenSummary => ({
26+
code: asset.assetCode,
27+
issuer: asset.assetIssuer ?? undefined,
28+
totalSupply: asset.amount,
29+
description: asset.assetType === "native" ? "Pi Testnet" : asset.assetType,
30+
})
31+
32+
const formatMintedToken = (token: TokenSummary): TokenSummary => ({
33+
code: token.code,
34+
issuer: token.issuer,
35+
name: token.name,
36+
totalSupply: token.totalSupply,
37+
liquidityPools: token.liquidityPools,
38+
description: token.description,
39+
})
40+
41+
const buildActivitySeries = (operations: ReturnType<typeof useAccountOperations>["operations"]): ActivityPoint[] => {
42+
if (!operations.length) return []
43+
44+
const aggregation = new Map<string, number>()
45+
46+
operations.forEach((op) => {
47+
if (!op.createdAt) return
48+
const date = new Date(op.createdAt)
49+
const label = date.toLocaleDateString(undefined, { month: "short", day: "numeric" })
50+
const amount = Number.parseFloat(op.amount || "0")
51+
const signedAmount = op.action?.includes("sent") || op.action?.includes("merged") ? -Math.abs(amount) : Math.abs(amount)
52+
aggregation.set(label, (aggregation.get(label) ?? 0) + (Number.isFinite(signedAmount) ? signedAmount : 0))
53+
})
54+
55+
return Array.from(aggregation.entries())
56+
.map(([date, volume]) => ({ date, volume: Number(volume.toFixed(3)) }))
57+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
58+
}
59+
60+
export default function DashboardPage() {
61+
const { user } = useCurrentUser()
62+
const publicKey = user?.public_key?.trim() || undefined
63+
const userId = user?.id ?? undefined
64+
65+
const {
66+
balances,
67+
totalBalance,
68+
isLoading: balancesLoading,
69+
error: balancesError,
70+
} = useAccountBalances(publicKey)
71+
const {
72+
operations,
73+
pagination,
74+
isLoading: operationsLoading,
75+
error: operationsError,
76+
} = useAccountOperations(publicKey, { limit: 50 })
77+
const { tokens: mintedTokens, isLoading: tokensLoading, error: tokensError } = useTokenRegistry()
78+
const { launches } = useLaunches({ limit: 10 })
79+
const { positions: savingsPositions } = useSavingsPositions(userId)
80+
const { supplyPositions, borrowPositions } = useLendingPositions(userId)
81+
82+
const balanceCards = useMemo<TokenSummary[]>(
83+
() => balances.map((balance) => formatBalanceCard(balance)),
84+
[balances]
85+
)
86+
87+
const mintedSummaries = useMemo<TokenSummary[]>(
88+
() =>
89+
mintedTokens.map((token) => ({
90+
code: token.assetCode,
91+
issuer: token.issuer,
92+
name: token.name,
93+
totalSupply: token.totalSupply,
94+
description: token.description,
95+
})),
96+
[mintedTokens]
97+
)
98+
99+
const activitySeries = useMemo(() => buildActivitySeries(operations), [operations])
100+
101+
const openLaunchesCount = useMemo(() => launches.filter((l) => l.status === "participation_open" || l.status === "tge_open").length, [launches])
102+
const lockedSavingsCount = useMemo(() => savingsPositions.filter((p) => p.status === "locked").length, [savingsPositions])
103+
const nextUnlock = useMemo(() => {
104+
const locked = savingsPositions.filter((p) => p.status === "locked")
105+
if (!locked.length) return null
106+
const dates = locked.map((p) => new Date(p.unlockedAt).getTime())
107+
return new Date(Math.min(...dates)).toLocaleDateString()
108+
}, [savingsPositions])
109+
const borrowsAtRisk = useMemo(
110+
() => borrowPositions.filter((b) => b.healthFactor != null && parseFloat(b.healthFactor) < 1),
111+
[borrowPositions]
112+
)
113+
114+
const stats = [
115+
{
116+
label: "Tracked Balance",
117+
value: totalBalance.toLocaleString(undefined, { maximumFractionDigits: 2 }),
118+
icon: Wallet,
119+
hint: publicKey ? "Total units across all assets" : "Connect a wallet to track balances",
120+
},
121+
{
122+
label: "Assets",
123+
value: `${balances.length}`,
124+
icon: Coins,
125+
hint: "Assets with balance above threshold",
126+
},
127+
{
128+
label: "Operations",
129+
value: `${operations.length}`,
130+
icon: Activity,
131+
hint: pagination?.hasMore ? "Showing latest 50 operations" : "Latest ledger operations",
132+
},
133+
{
134+
label: "Minted Tokens",
135+
value: `${mintedSummaries.length}`,
136+
icon: TrendingUp,
137+
hint: tokensLoading ? "Loading minted tokens" : "Registered platform tokens",
138+
},
139+
]
140+
141+
const showWalletCta = !publicKey
142+
143+
return (
144+
<div className="min-h-screen premium-gradient pt-16 pb-20">
145+
<div className="container mx-auto px-4 py-6">
146+
<div className="space-y-6">
147+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
148+
{stats.map((stat) => (
149+
<Card key={stat.label}>
150+
<CardHeader className="flex flex-row items-center justify-between pb-2">
151+
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
152+
<stat.icon className="h-4 w-4" />
153+
{stat.label}
154+
</CardTitle>
155+
</CardHeader>
156+
<CardContent>
157+
<div className="text-2xl font-bold text-foreground">{stat.value}</div>
158+
<p className="text-xs text-muted-foreground mt-1">{stat.hint}</p>
159+
</CardContent>
160+
</Card>
161+
))}
162+
</div>
163+
164+
<Card>
165+
<CardHeader>
166+
<CardTitle>Quick Actions</CardTitle>
167+
<CardDescription>Perform common tasks</CardDescription>
168+
</CardHeader>
169+
<CardContent>
170+
<div className="grid gap-4 grid-cols-2 md:grid-cols-3 lg:grid-cols-5">
171+
<Link href="/invest">
172+
<Button className="w-full h-20 text-lg bg-transparent" size="lg" variant="outline">
173+
<TrendingUp className="mr-2 h-5 w-5" />
174+
Invest
175+
</Button>
176+
</Link>
177+
<Link href="/savings">
178+
<Button className="w-full h-20 text-lg bg-transparent" size="lg" variant="outline">
179+
<PiggyBank className="mr-2 h-5 w-5" />
180+
Savings
181+
</Button>
182+
</Link>
183+
<Link href="/lending">
184+
<Button className="w-full h-20 text-lg bg-transparent" size="lg" variant="outline">
185+
<CreditCard className="mr-2 h-5 w-5" />
186+
Borrow
187+
</Button>
188+
</Link>
189+
<Link href="/mint">
190+
<Button className="w-full h-20 text-lg btn-gradient-primary" size="lg">
191+
<Coins className="mr-2 h-5 w-5" />
192+
Mint Token
193+
</Button>
194+
</Link>
195+
<Link href="/liquidity">
196+
<Button className="w-full h-20 text-lg bg-transparent" size="lg" variant="outline">
197+
<Droplets className="mr-2 h-5 w-5" />
198+
Liquidity
199+
</Button>
200+
</Link>
201+
</div>
202+
</CardContent>
203+
</Card>
204+
205+
<Card>
206+
<CardHeader>
207+
<CardTitle>Products & positions</CardTitle>
208+
<CardDescription>Investments, savings, and lending at a glance</CardDescription>
209+
</CardHeader>
210+
<CardContent>
211+
<div className="grid gap-4 md:grid-cols-3">
212+
<Link href="/invest">
213+
<Card className="border-border hover:border-primary/30 transition-colors">
214+
<CardContent className="pt-4">
215+
<div className="flex items-center gap-2">
216+
<TrendingUp className="h-5 w-5 text-muted-foreground" />
217+
<span className="font-medium">Invest</span>
218+
</div>
219+
<p className="text-sm text-muted-foreground mt-1">
220+
{openLaunchesCount} open launch{openLaunchesCount !== 1 ? "es" : ""}
221+
</p>
222+
</CardContent>
223+
</Card>
224+
</Link>
225+
<Link href="/savings">
226+
<Card className="border-border hover:border-primary/30 transition-colors">
227+
<CardContent className="pt-4">
228+
<div className="flex items-center gap-2">
229+
<PiggyBank className="h-5 w-5 text-muted-foreground" />
230+
<span className="font-medium">Savings</span>
231+
</div>
232+
<p className="text-sm text-muted-foreground mt-1">
233+
{lockedSavingsCount} locked position{lockedSavingsCount !== 1 ? "s" : ""}
234+
{nextUnlock && ` · Next unlock ${nextUnlock}`}
235+
</p>
236+
</CardContent>
237+
</Card>
238+
</Link>
239+
<Link href="/lending">
240+
<Card className="border-border hover:border-primary/30 transition-colors">
241+
<CardContent className="pt-4">
242+
<div className="flex items-center gap-2">
243+
<CreditCard className="h-5 w-5 text-muted-foreground" />
244+
<span className="font-medium">Borrow & lend</span>
245+
</div>
246+
<p className="text-sm text-muted-foreground mt-1">
247+
{supplyPositions.length} supply · {borrowPositions.length} borrow
248+
{borrowsAtRisk.length > 0 && (
249+
<span className="text-amber-600 flex items-center gap-1 mt-1">
250+
<AlertTriangle className="h-3 w-3" />
251+
{borrowsAtRisk.length} at risk
252+
</span>
253+
)}
254+
</p>
255+
</CardContent>
256+
</Card>
257+
</Link>
258+
</div>
259+
</CardContent>
260+
</Card>
261+
262+
{showWalletCta ? (
263+
<Card>
264+
<CardHeader>
265+
<CardTitle>Connect a wallet to get started</CardTitle>
266+
<CardDescription>
267+
Connect with Pi and link a wallet from your profile to load balances and activity.
268+
</CardDescription>
269+
</CardHeader>
270+
<CardContent>
271+
<Link href="/profile">
272+
<Button className="btn-gradient-primary">Open Profile</Button>
273+
</Link>
274+
</CardContent>
275+
</Card>
276+
) : (
277+
<Card>
278+
<CardHeader>
279+
<CardTitle>Wallet</CardTitle>
280+
<CardDescription className="font-mono text-xs break-all">
281+
{publicKey}
282+
</CardDescription>
283+
</CardHeader>
284+
<CardContent className="space-y-4">
285+
<div>
286+
<h3 className="text-sm font-medium text-muted-foreground mb-1">Token Balances</h3>
287+
<p className="text-xs text-muted-foreground">Your connected wallet balances</p>
288+
</div>
289+
{balancesError ? (
290+
<p className="text-destructive text-sm">{balancesError.message}</p>
291+
) : (
292+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
293+
{balancesLoading && !balanceCards.length && (
294+
<p className="text-sm text-muted-foreground">Loading balances...</p>
295+
)}
296+
{balanceCards.map((token, idx) => (
297+
<TokenCard key={`${token.code}-${idx}`} token={token} index={idx} />
298+
))}
299+
{!balancesLoading && !balanceCards.length && (
300+
<p className="text-sm text-muted-foreground">No balances above the display threshold.</p>
301+
)}
302+
</div>
303+
)}
304+
</CardContent>
305+
</Card>
306+
)}
307+
308+
<Tabs defaultValue="activity" className="space-y-4">
309+
<TabsList>
310+
<TabsTrigger value="activity">Activity Chart</TabsTrigger>
311+
<TabsTrigger value="history">Transaction History</TabsTrigger>
312+
</TabsList>
313+
<TabsContent value="activity" className="space-y-4">
314+
<Card>
315+
<CardHeader>
316+
<CardTitle>Recent Activity</CardTitle>
317+
<CardDescription>Transaction flow aggregated by day</CardDescription>
318+
</CardHeader>
319+
<CardContent>
320+
<ActivityChart
321+
series={activitySeries}
322+
isLoading={operationsLoading && !activitySeries.length}
323+
/>
324+
</CardContent>
325+
</Card>
326+
</TabsContent>
327+
<TabsContent value="history" className="space-y-4">
328+
<Card>
329+
<CardHeader>
330+
<CardTitle>Transaction History</CardTitle>
331+
<CardDescription>Latest ledger operations for this account</CardDescription>
332+
</CardHeader>
333+
<CardContent>
334+
{operationsError ? (
335+
<p className="text-destructive text-sm">{operationsError.message}</p>
336+
) : (
337+
<TransactionHistory operations={operations} isLoading={operationsLoading} />
338+
)}
339+
</CardContent>
340+
</Card>
341+
</TabsContent>
342+
</Tabs>
343+
344+
<Card>
345+
<CardHeader>
346+
<CardTitle>Platform Tokens</CardTitle>
347+
<CardDescription>Tokens issued through the DEX toolkit</CardDescription>
348+
</CardHeader>
349+
<CardContent>
350+
{tokensError ? (
351+
<p className="text-destructive text-sm">{tokensError.message}</p>
352+
) : (
353+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
354+
{tokensLoading && !mintedSummaries.length && (
355+
<p className="text-sm text-muted-foreground">Loading token registry...</p>
356+
)}
357+
{mintedSummaries.map((token, idx) => (
358+
<TokenCard key={`${token.code}-${idx}`} token={token} index={idx} variant="compact" />
359+
))}
360+
{!tokensLoading && !mintedSummaries.length && (
361+
<p className="text-sm text-muted-foreground">No platform tokens found.</p>
362+
)}
363+
</div>
364+
)}
365+
</CardContent>
366+
</Card>
367+
</div>
368+
</div>
369+
</div>
370+
)
371+
}

0 commit comments

Comments
 (0)