-
- ${tooltip.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
-
-
- {new Date(tooltip.timestamp).toLocaleString('en-US', {
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- })}
-
-
- )}
+ updateDimensions();
+ window.addEventListener('resize', updateDimensions);
+ return () => window.removeEventListener('resize', updateDimensions);
+ }, []);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas || dimensions.width === 0) return;
+
+ const ctx = canvas.getContext('2d');
+ const dpr = window.devicePixelRatio || 1;
+
+ canvas.width = dimensions.width * dpr;
+ canvas.height = dimensions.height * dpr;
+ ctx.scale(dpr, dpr);
+
+ const width = dimensions.width;
+ const height = dimensions.height;
+ const chartWidth = width - PADDING.left - PADDING.right;
+ const chartHeight = height - PADDING.top - PADDING.bottom;
+
+ // Clear
+ ctx.clearRect(0, 0, width, height);
+
+ if (!prices || prices.length === 0) {
+ ctx.fillStyle = COLORS.textMuted;
+ ctx.font = '18px Ac437_Rainbow100_re_132, monospace';
+ ctx.textAlign = 'center';
+ ctx.fillText('No data for this day', width / 2, height / 2);
+ return;
+ }
+
+ // Calculate bounds
+ const priceValues = prices.map(d => d.p || d.price);
+ const times = prices.map(d => d.t || d.timestamp);
+
+ let minPrice = Math.min(...priceValues);
+ let maxPrice = Math.max(...priceValues);
+ const minTime = Math.min(...times);
+ const maxTime = Math.max(...times);
+
+ const pricePadding = (maxPrice - minPrice) * 0.05 || 10;
+ minPrice -= pricePadding;
+ maxPrice += pricePadding;
+
+ const baselinePrice = priceValues[0];
+
+ // Coordinate converters
+ const priceToY = (price) => {
+ const range = maxPrice - minPrice;
+ const normalized = (price - minPrice) / range;
+ return PADDING.top + chartHeight * (1 - normalized);
+ };
+
+ const timeToX = (timestamp) => {
+ const range = maxTime - minTime;
+ const normalized = (timestamp - minTime) / range;
+ return PADDING.left + chartWidth * normalized;
+ };
+
+ // Draw grid
+ ctx.strokeStyle = COLORS.grid;
+ ctx.lineWidth = 1;
+
+ const priceStep = calculateNiceStep(maxPrice - minPrice, 5);
+ const startPrice = Math.ceil(minPrice / priceStep) * priceStep;
+
+ for (let price = startPrice; price <= maxPrice; price += priceStep) {
+ const y = priceToY(price);
+ ctx.beginPath();
+ ctx.moveTo(PADDING.left, y);
+ ctx.lineTo(width - PADDING.right, y);
+ ctx.stroke();
+ }
+
+ const baselineY = priceToY(baselinePrice);
+ ctx.strokeStyle = COLORS.baseline;
+ ctx.setLineDash([4, 4]);
+ ctx.beginPath();
+ ctx.moveTo(PADDING.left, baselineY);
+ ctx.lineTo(width - PADDING.right, baselineY);
+ ctx.stroke();
+ ctx.setLineDash([]);
+
+ if (prices.length >= 2) {
+ ctx.beginPath();
+ ctx.moveTo(timeToX(times[0]), baselineY);
+ ctx.lineTo(timeToX(times[0]), priceToY(priceValues[0]));
+
+ for (let i = 1; i < prices.length; i++) {
+ ctx.lineTo(timeToX(times[i]), priceToY(priceValues[i]));
+ }
+
+ ctx.lineTo(timeToX(times[times.length - 1]), baselineY);
+ ctx.closePath();
+
+ const gradient = ctx.createLinearGradient(0, PADDING.top, 0, height - PADDING.bottom);
+ gradient.addColorStop(0, COLORS.greenFill);
+ gradient.addColorStop(0.5, 'rgba(0,0,0,0)');
+ gradient.addColorStop(1, COLORS.redFill);
+ ctx.fillStyle = gradient;
+ ctx.fill();
+ }
+
+ if (prices.length >= 2) {
+ ctx.beginPath();
+ ctx.strokeStyle = COLORS.line;
+ ctx.lineWidth = 2;
+ ctx.lineJoin = 'round';
+ ctx.lineCap = 'round';
+
+ ctx.moveTo(timeToX(times[0]), priceToY(priceValues[0]));
+ for (let i = 1; i < prices.length; i++) {
+ ctx.lineTo(timeToX(times[i]), priceToY(priceValues[i]));
+ }
+ ctx.stroke();
+ }
+
+ ctx.fillStyle = COLORS.text;
+ ctx.font = '14px Ac437_Rainbow100_re_132, monospace';
+ ctx.textAlign = 'left';
+
+ for (let price = startPrice; price <= maxPrice; price += priceStep) {
+ const y = priceToY(price);
+ ctx.fillText(formatPrice(price), width - PADDING.right + 8, y + 4);
+ }
+
+ ctx.textAlign = 'center';
+ const hourMs = 60 * 60 * 1000;
+ const rangeMs = maxTime - minTime;
+ let stepMs = hourMs * 4;
+ if (rangeMs < hourMs * 6) stepMs = hourMs;
+ if (rangeMs < hourMs * 2) stepMs = hourMs / 2;
+
+ let current = Math.ceil(minTime / stepMs) * stepMs;
+ while (current <= maxTime) {
+ const x = timeToX(current);
+ if (x >= PADDING.left && x <= width - PADDING.right) {
+ const date = new Date(current);
+ const label = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
+ ctx.fillText(label, x, height - PADDING.bottom + 20);
+ }
+ current += stepMs;
+ }
+
+ for (const trade of (trades || [])) {
+ const x = timeToX(trade.t || trade.timestamp);
+ const y = priceToY(trade.price);
+
+ if (x < PADDING.left || x > width - PADDING.right) continue;
+
+ const color = trade.side === 'buy' ? COLORS.buy : COLORS.sell;
+ const radius = 7;
+
+ ctx.beginPath();
+ ctx.arc(x, y, radius + 4, 0, Math.PI * 2);
+ ctx.fillStyle = color + '33';
+ ctx.fill();
+
+ // Outer
+ ctx.beginPath();
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
+ ctx.fillStyle = color;
+ ctx.fill();
+
+ // Inner
+ ctx.beginPath();
+ ctx.arc(x, y, radius - 3, 0, Math.PI * 2);
+ ctx.fillStyle = COLORS.bg;
+ ctx.fill();
+ }
+
+ if (prices.length > 0) {
+ const lastPrice = priceValues[priceValues.length - 1];
+ const y = priceToY(lastPrice);
+ const x = width - PADDING.right;
+
+ const isUp = lastPrice >= baselinePrice;
+ const color = isUp ? COLORS.green : COLORS.red;
+
+ ctx.fillStyle = color;
+ roundRect(ctx, x + 4, y - 11, 70, 22, 4);
+ ctx.fill();
+
+ ctx.fillStyle = '#fff';
+ ctx.font = 'bold 14px Ac437_Rainbow100_re_132, monospace';
+ ctx.textAlign = 'left';
+ ctx.fillText(formatPrice(lastPrice), x + 10, y + 4);
+ }
+
+ }, [prices, trades, dimensions]);
+
+ const handleMouseMove = (e) => {
+ if (!prices || prices.length === 0) {
+ setTooltip(null);
+ return;
+ }
+
+ const rect = canvasRef.current.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ const width = dimensions.width;
+ const height = dimensions.height;
+ const chartWidth = width - PADDING.left - PADDING.right;
+
+ if (x < PADDING.left || x > width - PADDING.right ||
+ y < PADDING.top || y > height - PADDING.bottom) {
+ setTooltip(null);
+ return;
+ }
+
+ const times = prices.map(d => d.t || d.timestamp);
+ const minTime = Math.min(...times);
+ const maxTime = Math.max(...times);
+
+ const normalized = (x - PADDING.left) / chartWidth;
+ const timestamp = minTime + normalized * (maxTime - minTime);
+
+ let nearestPoint = null;
+ let nearestDistance = Infinity;
+
+ for (const point of prices) {
+ const t = point.t || point.timestamp;
+ const distance = Math.abs(t - timestamp);
+ if (distance < nearestDistance) {
+ nearestDistance = distance;
+ nearestPoint = point;
+ }
+ }
+
+ if (nearestPoint) {
+ setTooltip({
+ x: e.clientX - rect.left + 15,
+ y: e.clientY - rect.top - 10,
+ price: nearestPoint.p || nearestPoint.price,
+ timestamp: nearestPoint.t || nearestPoint.timestamp,
+ });
+ }
+ };
+
+ return (
+
diff --git a/trahn-trade-frontend/src/pages/Login.jsx b/trahn-trade-frontend/src/pages/Login.jsx
index 9f74cc9..7c951d2 100644
--- a/trahn-trade-frontend/src/pages/Login.jsx
+++ b/trahn-trade-frontend/src/pages/Login.jsx
@@ -29,13 +29,7 @@ export function Login() {
return (
-
{`
- ████████╗██████╗ █████╗ ██╗ ██╗███╗ ██╗
- ╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║
- ██║ ██████╔╝███████║███████║██╔██╗ ██║
- ██║ ██╔══██╗██╔══██║██╔══██║██║╚██╗██║
- ██║ ██║ ██║██║ ██║██║ ██║██║ ╚████║
- ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝`}
+
TRAHN
Welcome Back
diff --git a/trahn-trade-frontend/src/pages/Signup.jsx b/trahn-trade-frontend/src/pages/Signup.jsx
index 64e9988..b795cd6 100644
--- a/trahn-trade-frontend/src/pages/Signup.jsx
+++ b/trahn-trade-frontend/src/pages/Signup.jsx
@@ -38,13 +38,7 @@ export function Signup() {
return (
-
{`
- ████████╗██████╗ █████╗ ██╗ ██╗███╗ ██╗
- ╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║
- ██║ ██████╔╝███████║███████║██╔██╗ ██║
- ██║ ██╔══██╗██╔══██║██╔══██║██║╚██╗██║
- ██║ ██║ ██║██║ ██║██║ ██║██║ ╚████║
- ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝`}
+
TRAHN
{submitted ? (
<>