Skip to content

Commit 2fcd108

Browse files
authored
Merge pull request #157 from D240021/main
feat: enhance Vault dashboard with new layout and performance chart.
2 parents d699242 + a5bb25c commit 2fcd108

8 files changed

Lines changed: 697 additions & 78 deletions

File tree

frontend/package-lock.json

Lines changed: 401 additions & 69 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"lucide-react": "^0.577.0",
2323
"react": "^19.2.0",
2424
"react-dom": "^19.2.0",
25-
"react-router-dom": "^7.13.2"
25+
"react-router-dom": "^7.13.2",
26+
"recharts": "^3.8.1"
2627
},
2728
"devDependencies": {
2829
"@eslint/js": "^9.39.1",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

frontend/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Navigate,
77
} from "react-router-dom";
88
import { ThemeProvider } from "./context/ThemeContext";
9+
import { ToastProvider } from "./context/ToastContext";
910
import { VaultProvider } from "./context/VaultContext";
1011
import { ToastProvider } from "./context/ToastContext";
1112
import Navbar from "./components/Navbar";
@@ -62,6 +63,8 @@ function App() {
6263
showDialog
6364
>
6465
<ThemeProvider>
66+
<ToastProvider>
67+
<VaultProvider>
6568
<VaultProvider>
6669
<ToastProvider>
6770
<Router>
@@ -93,6 +96,8 @@ function App() {
9396
</main>
9497
</div>
9598
</Router>
99+
</VaultProvider>
100+
</ToastProvider>
96101
</ToastProvider>
97102
</VaultProvider>
98103
</ThemeProvider>

frontend/src/components/VaultDashboard.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Activity, ShieldCheck, TrendingUp, Wallet as WalletIcon } from "./icons
33
import { hasCustomRpcConfig, networkConfig } from "../config/network";
44
import { useVault } from "../context/VaultContext";
55
import ApiStatusBanner from "./ApiStatusBanner";
6+
import VaultPerformanceChart from "./VaultPerformanceChart";
67
import { useToast } from "../context/ToastContext";
78
import { Tabs, TabsList, TabsTrigger, TabsContent } from "./Tabs";
89
import { FormField, SubmitButton, useForm, type ValidationSchema } from "../forms";
@@ -88,14 +89,13 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
8889
};
8990

9091
return (
91-
<div className="flex gap-lg" style={{ flexWrap: 'wrap' }}>
92-
{/* Left Column - Vault Stats */}
93-
<div style={{ flex: '1 1 500px' }} className="flex flex-col gap-lg">
94-
92+
<div className="vault-dashboard gap-lg">
93+
{/* Stats — grid area: stats */}
94+
<div className="vault-dashboard-stats">
9595
<div className="glass-panel" style={{ padding: '32px' }}>
9696
{error && <ApiStatusBanner error={error} />}
9797

98-
<div className="flex justify-between items-center" style={{ marginBottom: '24px' }}>
98+
<div className="vault-stats-header flex justify-between items-center" style={{ marginBottom: '24px' }}>
9999
<div>
100100
<h2 style={{ fontSize: '1.5rem', marginBottom: '4px' }}>Global RWA Yield Fund</h2>
101101
<span className="tag" style={{ background: 'rgba(255, 255, 255, 0.05)', color: 'var(--text-secondary)' }}>
@@ -112,7 +112,7 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
112112

113113
<div style={{ height: '1px', background: 'var(--border-glass)', margin: '24px 0' }} />
114114

115-
<div className="flex gap-xl" style={{ marginBottom: '32px' }}>
115+
<div className="vault-stats-meta flex gap-xl" style={{ marginBottom: '32px' }}>
116116
<div>
117117
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem', marginBottom: '4px', display: 'flex', alignItems: 'center', gap: '6px' }}>
118118
Total Value Locked
@@ -151,8 +151,15 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
151151
</div>
152152
</div>
153153

154-
{/* Right Column - User Interaction */}
155-
<div style={{ flex: '1 1 400px' }}>
154+
{/* Chart — grid area: chart */}
155+
<div className="vault-dashboard-chart">
156+
<div className="glass-panel vault-chart-panel">
157+
<VaultPerformanceChart />
158+
</div>
159+
</div>
160+
161+
{/* Deposit / withdraw — grid area: actions */}
162+
<div className="vault-dashboard-actions">
156163
<div className="glass-panel" style={{ padding: '32px', position: 'relative', overflow: 'hidden' }}>
157164

158165
{/* Decorative Glow */}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { useEffect, useState } from "react";
2+
import {
3+
Area,
4+
AreaChart,
5+
CartesianGrid,
6+
ResponsiveContainer,
7+
Tooltip,
8+
XAxis,
9+
YAxis,
10+
} from "recharts";
11+
import { TrendingUp } from "./icons";
12+
import {
13+
getVaultHistory,
14+
type VaultHistoryPoint,
15+
} from "../lib/vaultApi";
16+
17+
function formatTick(iso: string) {
18+
const d = new Date(iso.includes("T") ? iso : `${iso}T12:00:00`);
19+
return new Intl.DateTimeFormat("en-US", {
20+
month: "short",
21+
day: "numeric",
22+
}).format(d);
23+
}
24+
25+
const chartMargin = { top: 8, right: 12, left: 4, bottom: 8 };
26+
27+
const VaultPerformanceChart: React.FC = () => {
28+
const [data, setData] = useState<VaultHistoryPoint[]>([]);
29+
30+
useEffect(() => {
31+
let cancelled = false;
32+
void getVaultHistory().then((points) => {
33+
if (!cancelled) setData(points);
34+
});
35+
return () => {
36+
cancelled = true;
37+
};
38+
}, []);
39+
40+
return (
41+
<div style={{ width: "100%" }}>
42+
<h3
43+
style={{
44+
fontSize: "1.1rem",
45+
marginBottom: "16px",
46+
display: "flex",
47+
alignItems: "center",
48+
gap: "8px",
49+
}}
50+
>
51+
<TrendingUp size={18} color="var(--accent-cyan)" />
52+
Historical performance
53+
</h3>
54+
<p
55+
style={{
56+
color: "var(--text-secondary)",
57+
fontSize: "0.82rem",
58+
marginBottom: "16px",
59+
}}
60+
>
61+
yvUSDC share price index (100 = baseline)
62+
</p>
63+
<div className="vault-chart-canvas">
64+
<ResponsiveContainer width="100%" height="100%">
65+
<AreaChart data={data} margin={chartMargin}>
66+
<defs>
67+
<linearGradient id="vaultPerfArea" x1="0" y1="0" x2="0" y2="1">
68+
<stop
69+
offset="0%"
70+
stopColor="var(--accent-cyan)"
71+
stopOpacity={0.35}
72+
/>
73+
<stop
74+
offset="100%"
75+
stopColor="var(--accent-cyan)"
76+
stopOpacity={0}
77+
/>
78+
</linearGradient>
79+
</defs>
80+
<CartesianGrid
81+
strokeDasharray="3 3"
82+
stroke="var(--border-glass)"
83+
vertical={false}
84+
/>
85+
<XAxis
86+
dataKey="date"
87+
tickFormatter={formatTick}
88+
tick={{ fill: "var(--text-tertiary)", fontSize: 11 }}
89+
axisLine={{ stroke: "var(--border-glass)" }}
90+
tickLine={false}
91+
minTickGap={24}
92+
/>
93+
<YAxis
94+
domain={["auto", "auto"]}
95+
tick={{ fill: "var(--text-tertiary)", fontSize: 11 }}
96+
axisLine={false}
97+
tickLine={false}
98+
width={44}
99+
tickFormatter={(v) => (typeof v === "number" ? v.toFixed(1) : String(v))}
100+
/>
101+
<Tooltip
102+
contentStyle={{
103+
background: "var(--bg-surface)",
104+
border: "1px solid var(--border-glass)",
105+
borderRadius: "var(--radius-sm)",
106+
color: "var(--text-primary)",
107+
fontSize: "0.85rem",
108+
}}
109+
labelFormatter={(label) =>
110+
typeof label === "string" ? formatTick(label) : String(label)
111+
}
112+
formatter={(value) => [
113+
typeof value === "number" ? value.toFixed(3) : String(value),
114+
"Index",
115+
]}
116+
/>
117+
<Area
118+
type="monotone"
119+
dataKey="value"
120+
stroke="var(--accent-cyan)"
121+
strokeWidth={2}
122+
fill="url(#vaultPerfArea)"
123+
isAnimationActive={data.length > 0}
124+
/>
125+
</AreaChart>
126+
</ResponsiveContainer>
127+
</div>
128+
</div>
129+
);
130+
};
131+
132+
export default VaultPerformanceChart;

frontend/src/index.css

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,84 @@ button {
143143
.gap-lg { gap: 24px; }
144144
.gap-xl { gap: 32px; }
145145

146+
/* Vault dashboard: two columns on desktop; stacked on narrow viewports */
147+
.vault-dashboard {
148+
display: grid;
149+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
150+
grid-template-areas:
151+
"stats actions"
152+
"chart actions";
153+
align-items: start;
154+
}
155+
156+
.vault-dashboard-stats {
157+
grid-area: stats;
158+
min-width: 0;
159+
}
160+
161+
.vault-dashboard-chart {
162+
grid-area: chart;
163+
min-width: 0;
164+
}
165+
166+
.vault-dashboard-actions {
167+
grid-area: actions;
168+
min-width: 0;
169+
align-self: stretch;
170+
display: flex;
171+
flex-direction: column;
172+
}
173+
174+
.vault-dashboard-actions > .glass-panel {
175+
flex: 1;
176+
}
177+
178+
.vault-chart-panel {
179+
padding: 24px 32px;
180+
}
181+
182+
.vault-chart-canvas {
183+
width: 100%;
184+
height: min(40vw, 320px);
185+
min-height: 220px;
186+
}
187+
188+
@media (max-width: 768px) {
189+
.vault-dashboard {
190+
grid-template-columns: minmax(0, 1fr);
191+
grid-template-areas:
192+
"stats"
193+
"actions"
194+
"chart";
195+
}
196+
197+
.vault-stats-header {
198+
flex-direction: column;
199+
align-items: flex-start;
200+
gap: 16px;
201+
}
202+
203+
.vault-stats-header > div:last-child {
204+
align-self: stretch;
205+
text-align: left;
206+
}
207+
208+
.vault-stats-meta {
209+
flex-direction: column;
210+
align-items: flex-start;
211+
gap: 20px;
212+
}
213+
214+
.vault-chart-panel {
215+
padding: 20px 16px;
216+
}
217+
218+
.vault-chart-canvas {
219+
height: clamp(200px, 58vw, 280px);
220+
min-height: 200px;
221+
}
222+
}
223+
146224
/* Gradients & Text Effects */
147225
.text-gradient {
148226
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple));

frontend/src/lib/vaultApi.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,57 @@ export interface VaultSummary {
2323
strategy: StrategyMetadata;
2424
}
2525

26+
export interface VaultHistoryPoint {
27+
date: string;
28+
/** Normalized share price index (100 = baseline). */
29+
value: number;
30+
}
31+
32+
function isValidHistory(data: unknown): data is VaultHistoryPoint[] {
33+
return (
34+
Array.isArray(data) &&
35+
data.length > 0 &&
36+
data.every(
37+
(p) =>
38+
p !== null &&
39+
typeof p === "object" &&
40+
typeof (p as VaultHistoryPoint).date === "string" &&
41+
typeof (p as VaultHistoryPoint).value === "number",
42+
)
43+
);
44+
}
45+
46+
/** Mock series when the API returns no points or is unreachable. */
47+
const MOCK_VAULT_HISTORY: VaultHistoryPoint[] = [
48+
{ date: "2025-09-24", value: 100 },
49+
{ date: "2025-10-01", value: 100.32 },
50+
{ date: "2025-10-08", value: 100.41 },
51+
{ date: "2025-10-15", value: 100.58 },
52+
{ date: "2025-10-22", value: 100.72 },
53+
{ date: "2025-10-29", value: 100.89 },
54+
{ date: "2025-11-05", value: 101.02 },
55+
{ date: "2025-11-12", value: 101.15 },
56+
{ date: "2025-11-19", value: 101.28 },
57+
{ date: "2025-11-26", value: 101.44 },
58+
{ date: "2025-12-03", value: 101.58 },
59+
{ date: "2025-12-10", value: 101.71 },
60+
{ date: "2025-12-17", value: 101.85 },
61+
{ date: "2025-12-24", value: 101.98 },
62+
{ date: "2025-12-31", value: 102.12 },
63+
{ date: "2026-01-07", value: 102.28 },
64+
{ date: "2026-01-14", value: 102.41 },
65+
{ date: "2026-01-21", value: 102.55 },
66+
{ date: "2026-01-28", value: 102.68 },
67+
{ date: "2026-02-04", value: 102.82 },
68+
{ date: "2026-02-11", value: 102.95 },
69+
{ date: "2026-02-18", value: 103.08 },
70+
{ date: "2026-02-25", value: 103.22 },
71+
{ date: "2026-03-04", value: 103.35 },
72+
{ date: "2026-03-11", value: 103.48 },
73+
{ date: "2026-03-18", value: 103.61 },
74+
{ date: "2026-03-25", value: 103.75 },
75+
];
76+
2677
const apiClient = createApiClient({
2778
baseUrl: import.meta.env.VITE_API_BASE_URL,
2879
headers: {
@@ -33,3 +84,15 @@ const apiClient = createApiClient({
3384
export async function getVaultSummary() {
3485
return apiClient.get<VaultSummary>("/mock-api/vault-summary.json");
3586
}
87+
88+
export async function getVaultHistory(): Promise<VaultHistoryPoint[]> {
89+
try {
90+
const data = await apiClient.get<unknown>("/mock-api/vault-history.json");
91+
if (isValidHistory(data)) {
92+
return data;
93+
}
94+
} catch {
95+
// Use mock below
96+
}
97+
return MOCK_VAULT_HISTORY;
98+
}

0 commit comments

Comments
 (0)