Files
BTC-Portfolio/btc-portfolio/frontend/src/pages/Dashboard.js
T
Jonathan 5cf3726f59 Improve portfolio chart with historical price-based data points
Chart now plots weekly data points from first purchase to today using
candle/history price data, giving an accurate view of portfolio value
over time rather than just at purchase dates.

Backend seeds up to 365 days of daily close prices from CoinGecko as
synthetic OHLC candles, refreshing stale entries older than 31 days.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:30:48 +02:00

153 lines
6.4 KiB
JavaScript

import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import AddPurchase from '../components/AddPurchase';
import PurchaseList from '../components/PurchaseList';
import PortfolioChart from '../components/PortfolioChart';
import BTCCandlestickChart from '../components/BTCCandlestickChart';
const API = process.env.REACT_APP_API_URL || 'http://localhost:8000';
const styles = {
app: { maxWidth: '900px', margin: '0 auto', padding: '1.5rem' },
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' },
logo: { fontSize: '1.4rem', fontWeight: 700, color: '#f7931a' },
logoutBtn: { background: 'none', border: '1px solid #555', color: '#aaa', borderRadius: '8px', padding: '0.4rem 1rem', cursor: 'pointer' },
adminBtn: { background: 'none', border: '1px solid #f7931a', color: '#f7931a', borderRadius: '8px', padding: '0.4rem 1rem', cursor: 'pointer', textDecoration: 'none', fontSize: '1rem' },
headerBtns: { display: 'flex', gap: '0.5rem', alignItems: 'center' },
statsGrid: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', marginBottom: '1.5rem' },
statCard: { background: '#1a1a1a', padding: '1rem', borderRadius: '12px', border: '1px solid #333' },
statLabel: { color: '#888', fontSize: '0.8rem', marginBottom: '0.3rem' },
statValue: { fontSize: '1.2rem', fontWeight: 700 },
positive: { color: '#6bff8e' },
negative: { color: '#ff6b6b' },
neutral: { color: '#f7931a' },
tabs: { display: 'flex', gap: '0.5rem', marginBottom: '1rem' },
tab: { padding: '0.45rem 1.1rem', borderRadius: '8px', border: '1px solid #444', background: 'none', color: '#888', cursor: 'pointer', fontSize: '0.9rem' },
tabActive: { padding: '0.45rem 1.1rem', borderRadius: '8px', border: '1px solid #f7931a', background: 'rgba(247,147,26,0.1)', color: '#f7931a', cursor: 'pointer', fontSize: '0.9rem', fontWeight: 700 },
};
function StatCard({ label, value, highlight }) {
return (
<div style={styles.statCard}>
<div style={styles.statLabel}>{label}</div>
<div style={{ ...styles.statValue, ...(highlight ? styles[highlight] : {}) }}>{value}</div>
</div>
);
}
export default function Dashboard() {
const [stats, setStats] = useState(null);
const [purchases, setPurchases] = useState([]);
const [candles, setCandles] = useState(null);
const [candlesAll, setCandlesAll] = useState(null);
const [fullscreenChart, setFullscreenChart] = useState(false);
const [chartView, setChartView] = useState('both'); // 'both' | 'portfolio' | 'history'
const navigate = useNavigate();
const authHeaders = () => ({
Authorization: `Bearer ${localStorage.getItem('token')}`,
});
const fetchData = useCallback(async () => {
try {
const [statsRes, purchasesRes, candlesRes] = await Promise.all([
fetch(`${API}/stats`, { headers: authHeaders() }),
fetch(`${API}/purchases`, { headers: authHeaders() }),
fetch(`${API}/candles?days=365`, { headers: authHeaders() }),
]);
if (statsRes.status === 401) {
localStorage.removeItem('token');
navigate('/login');
return;
}
setStats(await statsRes.json());
setPurchases(await purchasesRes.json());
setCandles(await candlesRes.json());
} catch {
// silently fail — network may be unavailable
}
}, [navigate]);
const fetchAllCandles = useCallback(async () => {
try {
const res = await fetch(`${API}/candles?days=all`, { headers: authHeaders() });
if (res.ok) setCandlesAll(await res.json());
} catch {
// silently fail
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
const isAdmin = localStorage.getItem('is_admin') === 'true';
const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('is_admin');
navigate('/login');
};
const handleToggleFullscreen = useCallback(() => {
if (!fullscreenChart && !candlesAll) fetchAllCandles();
setFullscreenChart(f => !f);
}, [fullscreenChart, candlesAll, fetchAllCandles]);
const plHighlight = stats
? stats.profit_loss >= 0 ? 'positive' : 'negative'
: 'neutral';
// Fullscreen uses all-candles data once loaded, otherwise falls back to 365-day set
const activeCandles = (fullscreenChart && candlesAll) ? candlesAll : candles;
return (
<div style={styles.app}>
<div style={styles.header}>
<div style={styles.logo}> BTC Portfolio</div>
<div style={styles.headerBtns}>
{isAdmin && <Link to="/admin" style={styles.adminBtn}>Manage Users</Link>}
<button style={styles.logoutBtn} onClick={handleLogout}>Logout</button>
</div>
</div>
{stats && (
<div style={styles.statsGrid}>
<StatCard label="Total Invested" value={`${stats.total_invested.toLocaleString()}`} />
<StatCard label="Avg Buy Price" value={`${stats.average_price.toLocaleString()}`} />
<StatCard label="Total BTC" value={`${stats.total_btc}`} highlight="neutral" />
<StatCard label="Current BTC Price" value={`${stats.current_price.toLocaleString()}`} />
<StatCard label="Portfolio Value" value={`${stats.portfolio_value.toLocaleString()}`} />
<StatCard
label="Profit / Loss"
value={`${stats.profit_loss >= 0 ? '+' : ''}${stats.profit_loss.toLocaleString()}`}
highlight={plHighlight}
/>
</div>
)}
<div style={styles.tabs}>
{[['both', 'Both'], ['portfolio', 'Portfolio'], ['history', 'BTC Candles']].map(([key, label]) => (
<button
key={key}
style={chartView === key ? styles.tabActive : styles.tab}
onClick={() => setChartView(key)}
>{label}</button>
))}
</div>
{(chartView === 'both' || chartView === 'portfolio') && <PortfolioChart purchases={purchases} stats={stats} btcHistory={candles?.candles ?? []} />}
{(chartView === 'both' || chartView === 'history') && (
<BTCCandlestickChart
candles={activeCandles?.candles ?? null}
purchases={activeCandles?.purchases ?? purchases}
stats={stats}
fullscreen={fullscreenChart}
onToggleFullscreen={handleToggleFullscreen}
/>
)}
<AddPurchase onAdded={fetchData} />
<PurchaseList purchases={purchases} onChanged={fetchData} />
</div>
);
}