Skip to content

Commit 2b5c185

Browse files
committed
feat(frontend): dashboard UI with chart and NFT card + fix Mesh/Vite compatibility (global, process, Buffer polyfills)
1 parent 19ea768 commit 2b5c185

11 files changed

Lines changed: 1687 additions & 129 deletions

File tree

frontend/package-lock.json

Lines changed: 550 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@
1313
"@meshsdk/core": "^1.9.0-beta.101",
1414
"@meshsdk/react": "^2.0.0-beta.2",
1515
"autoprefixer": "^10.4.27",
16+
"buffer": "^6.0.3",
1617
"framer-motion": "^12.38.0",
1718
"lightweight-charts": "^5.1.0",
1819
"lucide-react": "^0.577.0",
1920
"postcss": "^8.5.8",
21+
"process": "^0.11.10",
2022
"react": "^19.2.4",
2123
"react-dom": "^19.2.4",
22-
"tailwindcss": "^4.2.2"
24+
"stream-browserify": "^3.0.0",
25+
"tailwindcss": "^4.2.2",
26+
"util": "^0.12.5"
2327
},
2428
"devDependencies": {
2529
"@eslint/js": "^9.39.4",

frontend/src/App.tsx

Lines changed: 3 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,7 @@
1-
import { useState } from 'react'
2-
import reactLogo from './assets/react.svg'
3-
import viteLogo from './assets/vite.svg'
4-
import heroImg from './assets/hero.png'
5-
import './App.css'
1+
import Dashboard from "./pages/Dashboard";
62

73
function App() {
8-
const [count, setCount] = useState(0)
9-
10-
return (
11-
<>
12-
<section id="center">
13-
<div className="hero">
14-
<img src={heroImg} className="base" width="170" height="179" alt="" />
15-
<img src={reactLogo} className="framework" alt="React logo" />
16-
<img src={viteLogo} className="vite" alt="Vite logo" />
17-
</div>
18-
<div>
19-
<h1>Get started</h1>
20-
<p>
21-
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
22-
</p>
23-
</div>
24-
<button
25-
className="counter"
26-
onClick={() => setCount((count) => count + 1)}
27-
>
28-
Count is {count}
29-
</button>
30-
</section>
31-
32-
<div className="ticks"></div>
33-
34-
<section id="next-steps">
35-
<div id="docs">
36-
<svg className="icon" role="presentation" aria-hidden="true">
37-
<use href="/icons.svg#documentation-icon"></use>
38-
</svg>
39-
<h2>Documentation</h2>
40-
<p>Your questions, answered</p>
41-
<ul>
42-
<li>
43-
<a href="https://vite.dev/" target="_blank">
44-
<img className="logo" src={viteLogo} alt="" />
45-
Explore Vite
46-
</a>
47-
</li>
48-
<li>
49-
<a href="https://react.dev/" target="_blank">
50-
<img className="button-icon" src={reactLogo} alt="" />
51-
Learn more
52-
</a>
53-
</li>
54-
</ul>
55-
</div>
56-
<div id="social">
57-
<svg className="icon" role="presentation" aria-hidden="true">
58-
<use href="/icons.svg#social-icon"></use>
59-
</svg>
60-
<h2>Connect with us</h2>
61-
<p>Join the Vite community</p>
62-
<ul>
63-
<li>
64-
<a href="https://github.com/vitejs/vite" target="_blank">
65-
<svg
66-
className="button-icon"
67-
role="presentation"
68-
aria-hidden="true"
69-
>
70-
<use href="/icons.svg#github-icon"></use>
71-
</svg>
72-
GitHub
73-
</a>
74-
</li>
75-
<li>
76-
<a href="https://chat.vite.dev/" target="_blank">
77-
<svg
78-
className="button-icon"
79-
role="presentation"
80-
aria-hidden="true"
81-
>
82-
<use href="/icons.svg#discord-icon"></use>
83-
</svg>
84-
Discord
85-
</a>
86-
</li>
87-
<li>
88-
<a href="https://x.com/vite_js" target="_blank">
89-
<svg
90-
className="button-icon"
91-
role="presentation"
92-
aria-hidden="true"
93-
>
94-
<use href="/icons.svg#x-icon"></use>
95-
</svg>
96-
X.com
97-
</a>
98-
</li>
99-
<li>
100-
<a href="https://bsky.app/profile/vite.dev" target="_blank">
101-
<svg
102-
className="button-icon"
103-
role="presentation"
104-
aria-hidden="true"
105-
>
106-
<use href="/icons.svg#bluesky-icon"></use>
107-
</svg>
108-
Bluesky
109-
</a>
110-
</li>
111-
</ul>
112-
</div>
113-
</section>
114-
115-
<div className="ticks"></div>
116-
<section id="spacer"></section>
117-
</>
118-
)
4+
return <Dashboard />;
1195
}
1206

121-
export default App
7+
export default App;

frontend/src/components/Chart.tsx

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { createChart, LineSeries, CandlestickSeries } from "lightweight-charts";
2+
import type { IChartApi, ISeriesApi, Time } from "lightweight-charts";
3+
import { useEffect, useRef, useState } from "react";
4+
import { subscribeWithHistory, getPriceHistory } from "../services/pythMock";
5+
6+
type ChartType = "line" | "candle";
7+
8+
const ff = "-apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif";
9+
10+
// Genera velas OHLC a partir del historial de precios
11+
const buildCandles = (history: { time: number; value: number }[]) => {
12+
const candles: {
13+
time: number;
14+
open: number;
15+
high: number;
16+
low: number;
17+
close: number;
18+
}[] = [];
19+
20+
for (let i = 0; i < history.length; i += 3) {
21+
const chunk = history.slice(i, i + 3);
22+
if (chunk.length === 0) continue;
23+
24+
const open = chunk[0].value;
25+
const close = chunk[chunk.length - 1].value;
26+
const high =
27+
Math.max(...chunk.map((p) => p.value)) * (1 + Math.random() * 0.002);
28+
const low =
29+
Math.min(...chunk.map((p) => p.value)) * (1 - Math.random() * 0.002);
30+
31+
candles.push({
32+
time: chunk[0].time,
33+
open: +open.toFixed(4),
34+
high: +high.toFixed(4),
35+
low: +low.toFixed(4),
36+
close: +close.toFixed(4),
37+
});
38+
}
39+
40+
return candles;
41+
};
42+
43+
export default function Chart() {
44+
const ref = useRef<HTMLDivElement>(null);
45+
const chartRef = useRef<IChartApi | null>(null);
46+
const seriesRef = useRef<
47+
ISeriesApi<"Line"> | ISeriesApi<"Candlestick"> | null
48+
>(null);
49+
const [chartType, setChartType] = useState<ChartType>("line");
50+
const chartTypeRef = useRef<ChartType>("line");
51+
52+
const initChart = (type: ChartType) => {
53+
if (!ref.current) return;
54+
const container = ref.current;
55+
56+
// Destruir chart anterior
57+
if (chartRef.current) {
58+
chartRef.current.remove();
59+
chartRef.current = null;
60+
seriesRef.current = null;
61+
}
62+
63+
const chart = createChart(container, {
64+
width: container.clientWidth || 600,
65+
height: 240,
66+
layout: {
67+
background: { color: "transparent" },
68+
textColor: "rgba(255,255,255,0.4)",
69+
fontSize: 11,
70+
},
71+
grid: {
72+
vertLines: { color: "rgba(255,255,255,0.04)" },
73+
horzLines: { color: "rgba(255,255,255,0.04)" },
74+
},
75+
crosshair: {
76+
vertLine: {
77+
color: "rgba(255,255,255,0.2)",
78+
labelBackgroundColor: "#1c1c1e",
79+
},
80+
horzLine: {
81+
color: "rgba(255,255,255,0.2)",
82+
labelBackgroundColor: "#1c1c1e",
83+
},
84+
},
85+
rightPriceScale: { borderColor: "rgba(255,255,255,0.06)" },
86+
timeScale: {
87+
borderColor: "rgba(255,255,255,0.06)",
88+
timeVisible: true,
89+
secondsVisible: true,
90+
},
91+
});
92+
93+
const history = getPriceHistory();
94+
95+
if (type === "line") {
96+
const series = chart.addSeries(LineSeries, {
97+
color: "#0071e3",
98+
lineWidth: 2,
99+
crosshairMarkerRadius: 5,
100+
crosshairMarkerBackgroundColor: "#0071e3",
101+
crosshairMarkerBorderColor: "#fff",
102+
lastValueVisible: true,
103+
priceLineVisible: false,
104+
});
105+
series.setData(history as unknown as { time: Time; value: number }[]);
106+
seriesRef.current = series;
107+
} else {
108+
const series = chart.addSeries(CandlestickSeries, {
109+
upColor: "#30d158",
110+
downColor: "#ff453a",
111+
borderUpColor: "#30d158",
112+
borderDownColor: "#ff453a",
113+
wickUpColor: "#30d158",
114+
wickDownColor: "#ff453a",
115+
});
116+
const candles = buildCandles(history);
117+
series.setData(candles as unknown as { time: Time }[]);
118+
seriesRef.current = series;
119+
}
120+
121+
chart.timeScale().fitContent();
122+
chartRef.current = chart;
123+
124+
const observer = new ResizeObserver(() => {
125+
chartRef.current?.applyOptions({ width: container.clientWidth });
126+
});
127+
observer.observe(container);
128+
129+
return () => observer.disconnect();
130+
};
131+
132+
// Init inicial
133+
useEffect(() => {
134+
const cleanup = initChart(chartType);
135+
return () => {
136+
cleanup?.();
137+
chartRef.current?.remove();
138+
chartRef.current = null;
139+
seriesRef.current = null;
140+
};
141+
}, []);
142+
143+
// Reinit cuando cambia el tipo
144+
useEffect(() => {
145+
chartTypeRef.current = chartType;
146+
initChart(chartType);
147+
}, [chartType]);
148+
149+
// Suscripción a precios en tiempo real
150+
useEffect(() => {
151+
const unsub = subscribeWithHistory(() => {
152+
const latest = getPriceHistory();
153+
const last = latest[latest.length - 1];
154+
if (!last || !seriesRef.current) return;
155+
156+
try {
157+
if (chartTypeRef.current === "line") {
158+
(seriesRef.current as ISeriesApi<"Line">).update({
159+
time: last.time as unknown as Time,
160+
value: last.value,
161+
});
162+
} else {
163+
const candles = buildCandles(latest);
164+
const lastCandle = candles[candles.length - 1];
165+
if (lastCandle) {
166+
(seriesRef.current as ISeriesApi<"Candlestick">).update(
167+
lastCandle as unknown as { time: Time },
168+
);
169+
}
170+
}
171+
} catch {
172+
// Si hay error de tiempo, recargar serie completa
173+
const history = getPriceHistory();
174+
if (chartTypeRef.current === "line") {
175+
(seriesRef.current as ISeriesApi<"Line">).setData(
176+
history as unknown as { time: Time; value: number }[],
177+
);
178+
} else {
179+
(seriesRef.current as ISeriesApi<"Candlestick">).setData(
180+
buildCandles(history) as unknown as { time: Time }[],
181+
);
182+
}
183+
}
184+
});
185+
186+
return unsub;
187+
}, []);
188+
189+
return (
190+
<div>
191+
{/* Toggle */}
192+
<div style={{ display: "flex", gap: 6, marginBottom: 16 }}>
193+
{(["line", "candle"] as ChartType[]).map((type) => (
194+
<button
195+
key={type}
196+
onClick={() => setChartType(type)}
197+
style={{
198+
display: "flex",
199+
alignItems: "center",
200+
gap: 6,
201+
padding: "5px 14px",
202+
borderRadius: 980,
203+
border:
204+
chartType === type
205+
? "1px solid rgba(0,113,227,0.5)"
206+
: "1px solid rgba(255,255,255,0.08)",
207+
background:
208+
chartType === type
209+
? "rgba(0,113,227,0.15)"
210+
: "rgba(255,255,255,0.04)",
211+
color: chartType === type ? "#0071e3" : "#98989d",
212+
fontSize: 13,
213+
fontWeight: chartType === type ? 600 : 400,
214+
cursor: "pointer",
215+
fontFamily: ff,
216+
transition: "all 0.2s",
217+
}}
218+
>
219+
<span style={{ fontSize: 15 }}>{type === "line" ? "〜" : "▮"}</span>
220+
{type === "line" ? "Line" : "Candles"}
221+
</button>
222+
))}
223+
</div>
224+
225+
{/* Chart container */}
226+
<div
227+
ref={ref}
228+
style={{ width: "100%", borderRadius: 12, overflow: "hidden" }}
229+
/>
230+
</div>
231+
);
232+
}

0 commit comments

Comments
 (0)