Merge pull request #1 from kjannette/update-charting

Update charting
This commit is contained in:
S Jannette
2026-02-22 17:24:02 -05:00
committed by GitHub
19 changed files with 795 additions and 828 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@
*.key
*.env
*secrets.js
secrets.js
trahn-trade-frontend/src/auth/secrets.js*
# ============================================

View File

@@ -40,7 +40,7 @@ func (r *PriceRepo) GetByDay(ctx context.Context, tradingDay string) ([]models.P
func (r *PriceRepo) GetAvailableDays(ctx context.Context) ([]string, error) {
rows, err := r.pool.Query(ctx,
`SELECT DISTINCT trading_day FROM price_history ORDER BY trading_day DESC LIMIT 30`,
`SELECT DISTINCT trading_day FROM price_history ORDER BY trading_day ASC LIMIT 30`,
)
if err != nil {
return nil, err

View File

@@ -5,8 +5,6 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TRAHN Grid Trader</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,6 @@ import { Login } from './pages/Login';
import { Signup } from './pages/Signup';
import './App.css';
// Create React Query client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -20,7 +19,6 @@ const queryClient = new QueryClient({
},
});
// Redirect authenticated users away from auth pages
function PublicRoute({ children }) {
const { currentUser } = useAuth();

View File

@@ -1,81 +1,54 @@
/**
* Trading API Client
* Communicates with backend REST API
*/
const API_BASE = 'http://localhost:3001/api';
const API_BASE = 'http://localhost:3001/v1';
export const tradingApi = {
/**
* Get current trading day prices
*/
getCurrentPrices: async () => {
const response = await fetch(`${API_BASE}/prices/today`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
/**
* Get current trading day trades
*/
getCurrentTrades: async () => {
const response = await fetch(`${API_BASE}/trades/today`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
getCurrentPrices: async () => {
const response = await fetch(`${API_BASE}/prices/today`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
/**
* Get current grid state
*/
getCurrentGrid: async () => {
const response = await fetch(`${API_BASE}/grid/current`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
getCurrentTrades: async () => {
const response = await fetch(`${API_BASE}/trades/today`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
/**
* Get latest support/resistance levels
*/
getLatestSR: async () => {
const response = await fetch(`${API_BASE}/support-resistance/latest`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
getCurrentGrid: async () => {
const response = await fetch(`${API_BASE}/grid/current`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
/**
* Get available trading days
*/
getAvailableDays: async () => {
const response = await fetch(`${API_BASE}/prices/days`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
/**
* Get historical prices for a specific day
*/
getPricesByDay: async (date) => {
const response = await fetch(`${API_BASE}/prices/day/${date}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
getLatestSR: async () => {
const response = await fetch(`${API_BASE}/support-resistance/latest`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
/**
* Get historical trades for a specific day
*/
getTradesByDay: async (date) => {
const response = await fetch(`${API_BASE}/trades/day/${date}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
getAvailableDays: async () => {
const response = await fetch(`${API_BASE}/prices/days`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
/**
* Get trade statistics
*/
getTradeStats: async () => {
const response = await fetch(`${API_BASE}/trades/stats`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
getPricesByDay: async (date) => {
const response = await fetch(`${API_BASE}/prices/day/${date}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
getTradesByDay: async (date) => {
const response = await fetch(`${API_BASE}/trades/day/${date}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
getTradeStats: async () => {
const response = await fetch(`${API_BASE}/trades/stats`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
};

View File

@@ -7,7 +7,6 @@ const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
// Add email to waitlist collection
export async function addToWaitlist(email) {
try {
await addDoc(collection(db, 'trahn-trade-signups'), {

View File

@@ -1,350 +1,333 @@
import { useRef, useEffect, useState } from 'react';
const COLORS = {
line: '#58a6ff',
green: '#3fb950',
greenFill: 'rgba(63, 185, 80, 0.12)',
red: '#f85149',
redFill: 'rgba(248, 81, 73, 0.12)',
grid: '#21262d',
text: '#8b949e',
textMuted: '#484f58',
baseline: '#30363d',
buy: '#f0c000',
sell: '#db6d28',
bg: '#0d1117',
line: '#58a6ff',
green: '#3fb950',
greenFill: 'rgba(63, 185, 80, 0.12)',
red: '#f85149',
redFill: 'rgba(248, 81, 73, 0.12)',
grid: '#21262d',
text: '#8b949e',
textMuted: '#484f58',
baseline: '#30363d',
buy: '#f0c000',
sell: '#db6d28',
bg: '#0d1117',
};
const PADDING = { top: 20, right: 80, bottom: 40, left: 20 };
export function Chart({ prices, trades }) {
const canvasRef = useRef(null);
const containerRef = useRef(null);
const [tooltip, setTooltip] = useState(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const canvasRef = useRef(null);
const containerRef = useRef(null);
const [tooltip, setTooltip] = useState(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
// Handle resize
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setDimensions({ width: rect.width, height: rect.height });
}
};
updateDimensions();
window.addEventListener('resize', updateDimensions);
return () => window.removeEventListener('resize', updateDimensions);
}, []);
// Draw chart
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 = '14px JetBrains Mono, 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();
}
// Draw baseline
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([]);
// Draw gradient fill
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();
}
// Draw price line
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();
}
// Draw Y axis labels
ctx.fillStyle = COLORS.text;
ctx.font = '11px JetBrains Mono, 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);
}
// Draw X axis labels
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;
}
// Draw trades
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;
// Glow
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();
}
// Draw current price tag
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 11px JetBrains Mono, monospace';
ctx.textAlign = 'left';
ctx.fillText(formatPrice(lastPrice), x + 10, y + 4);
}
}, [prices, trades, dimensions]);
// Mouse handling for tooltip
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,
});
}
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setDimensions({ width: rect.width, height: rect.height });
}
};
return (
<div className="chart-container" ref={containerRef}>
<canvas
ref={canvasRef}
style={{ width: '100%', height: '100%' }}
onMouseMove={handleMouseMove}
onMouseLeave={() => setTooltip(null)}
/>
{tooltip && (
<div
className="tooltip visible"
style={{ left: tooltip.x, top: tooltip.y }}
>
<div className="price">
${tooltip.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
<div className="time">
{new Date(tooltip.timestamp).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
)}
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;
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;
}
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];
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;
};
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();
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
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 (
<div className="chart-container" ref={containerRef}>
<canvas
ref={canvasRef}
style={{ width: '100%', height: '100%' }}
onMouseMove={handleMouseMove}
onMouseLeave={() => setTooltip(null)}
/>
{tooltip && (
<div
className="tooltip visible"
style={{ left: tooltip.x, top: tooltip.y }}
>
<div className="price">
${tooltip.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
<div className="time">
{new Date(tooltip.timestamp).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
);
)}
</div>
);
}
function calculateNiceStep(range, targetSteps) {
const roughStep = range / targetSteps;
const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep)));
const normalized = roughStep / magnitude;
const roughStep = range / targetSteps;
const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep)));
const normalized = roughStep / magnitude;
let niceStep;
if (normalized <= 1.5) niceStep = 1;
else if (normalized <= 3) niceStep = 2;
else if (normalized <= 7) niceStep = 5;
else niceStep = 10;
let niceStep;
if (normalized <= 1.5) niceStep = 1;
else if (normalized <= 3) niceStep = 2;
else if (normalized <= 7) niceStep = 5;
else niceStep = 10;
return niceStep * magnitude;
return niceStep * magnitude;
}
function formatPrice(price) {
if (price >= 10000) {
return '$' + (price / 1000).toFixed(1) + 'k';
}
return '$' + price.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
if (price >= 10000) {
return '$' + (price / 1000).toFixed(1) + 'k';
}
return '$' + price.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
}
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}

View File

@@ -62,7 +62,6 @@ function reducer(state, action) {
export function TradingDataProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
// React Query: Poll current day data every 1 minute
const { data: currentData, error, isLoading } = useQuery({
queryKey: ['currentDay'],
queryFn: async () => {
@@ -87,7 +86,6 @@ export function TradingDataProvider({ children }) {
retryDelay: 2000,
});
// Update context when live data changes
useEffect(() => {
if (currentData) {
dispatch({
@@ -97,7 +95,6 @@ export function TradingDataProvider({ children }) {
}
}, [currentData]);
// Handle connection status
useEffect(() => {
if (error) {
dispatch({ type: 'SET_CONNECTION_STATUS', status: 'error' });

Binary file not shown.

Binary file not shown.

View File

@@ -1,18 +1,3 @@
/**
* DEPRECATED - Replaced by TradingDataContext + React Query
*
* This file is kept for reference but no longer used.
* The new implementation:
* - Uses React Query for polling
* - Uses Context for state management
* - Calls REST API instead of JSON files
*
* See:
* - contexts/TradingDataContext.jsx
* - hooks/useTradingData.js
* - api/tradingApi.js
*/
export function useChartData() {
throw new Error('useChartData is deprecated. Use useTradingData from TradingDataContext instead.');
}

View File

@@ -1,15 +1,7 @@
/**
* Custom hooks for trading data management
* Handles carousel navigation and historical day loading
*/
import { useQuery } from '@tanstack/react-query';
import { useTradingData } from '../contexts/TradingDataContext';
import { tradingApi } from '../api/tradingApi';
/**
* Hook for loading historical day data (lazy load)
*/
export function useHistoricalDay(date, enabled = false) {
return useQuery({
queryKey: ['historicalDay', date],
@@ -26,9 +18,6 @@ export function useHistoricalDay(date, enabled = false) {
});
}
/**
* Hook for carousel navigation
*/
export function useCarousel() {
const { state, dispatch } = useTradingData();
@@ -38,12 +27,9 @@ export function useCarousel() {
}
if (index === state.availableDays.length - 1) {
// Navigate to live (current) day
dispatch({ type: 'SWITCH_TO_LIVE' });
} else {
// Load historical day
const date = state.availableDays[index];
// Historical data will be loaded via useHistoricalDay in Dashboard
dispatch({
type: 'LOAD_HISTORICAL_DAY',
prices: [],

View File

@@ -1 +1,34 @@
/* Reset handled in App.css */
@font-face {
font-family: 'Ac437_Rainbow100_re_1323';
src: local('./fonts/Mx437_Compaq_Port3.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Ac437_Rainbow100_re_1323';
src: local('./fonts/Mx437_Compaq_Port3.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Px437_AMI_EGA_8x8';
src: local('Px437_AMI_EGA_8x8'), url(./fonts/Px437_AMI_EGA_8x8.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Ac437_Rainbow100_re_132';
src: local('Ac437_Rainbow100_re_132'), url(./fonts/Ac437_Rainbow100_re_132.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Ac437_Rainbow100_re_132';
src: local('Ac437_Rainbow100_re_132'), url(./fonts/Ac437_Rainbow100_re_132.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}

View File

@@ -12,11 +12,9 @@ export function Dashboard() {
const { state, dispatch } = useTradingData();
const carousel = useCarousel();
// Load historical day data when viewing past days
const historicalDate = !state.isLive ? state.availableDays[state.currentDayIndex] : null;
const { data: historicalData } = useHistoricalDay(historicalDate, !state.isLive);
// Update context with historical data when loaded
useEffect(() => {
if (historicalData && !state.isLive) {
dispatch({
@@ -52,13 +50,7 @@ export function Dashboard() {
return (
<div className="container">
<header>
<pre className="ascii-logo">{`
████████╗██████╗ █████╗ ██╗ ██╗███╗ ██╗
╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║
██║ ██████╔╝███████║███████║██╔██╗ ██║
██║ ██╔══██╗██╔══██║██╔══██║██║╚██╗██║
██║ ██║ ██║██║ ██║██║ ██║██║ ╚████║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝`}</pre>
<h1 className="logo">TRAHN</h1>
<button className="logout-btn" onClick={logout}>Logout</button>
</header>

View File

@@ -29,13 +29,7 @@ export function Login() {
return (
<div className="auth-container">
<div className="auth-card">
<pre className="ascii-logo">{`
████████╗██████╗ █████╗ ██╗ ██╗███╗ ██╗
╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║
██║ ██████╔╝███████║███████║██╔██╗ ██║
██║ ██╔══██╗██╔══██║██╔══██║██║╚██╗██║
██║ ██║ ██║██║ ██║██║ ██║██║ ╚████║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝`}</pre>
<h1 className="logo">TRAHN</h1>
<h2>Welcome Back</h2>

View File

@@ -1,16 +1,13 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { addToWaitlist } from '../auth/firebase';
//a comment
export function Signup() {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState('');
//
//
//
async function handleSubmit(e) {
e.preventDefault();
@@ -33,18 +30,11 @@ export function Signup() {
}
setLoading(false);
}
// some spacing
//more
return (
<div className="auth-container">
<div className="auth-card">
<pre className="ascii-logo">{`
████████╗██████╗ █████╗ ██╗ ██╗███╗ ██╗
╚══██╔══╝██╔══██╗██╔══██╗██║ ██║████╗ ██║
██║ ██████╔╝███████║███████║██╔██╗ ██║
██║ ██╔══██╗██╔══██║██╔══██║██║╚██╗██║
██║ ██║ ██║██║ ██║██║ ██║██║ ╚████║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝`}</pre>
<h1 className="logo">TRAHN</h1>
{submitted ? (
<>