cb28979208
Resolves conflicts: combined admin + candles routers in main.py, fixed docker-compose port to 3001:3001, restored admin styles in Dashboard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
153 lines
6.4 KiB
JavaScript
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} />}
|
|
{(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>
|
|
);
|
|
}
|