Files
BTC-Portfolio/btc-portfolio/frontend/src/pages/Dashboard.js
T
Jonathan ff7ba51e84 Add edit purchase feature (date, amount, price)
Backend: PUT /purchases/{id} endpoint accepts updated amount_eur,
price_eur, and created_at. Frontend: inline edit row in PurchaseList
with date picker and number inputs, Save/Cancel actions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 22:24:27 +01:00

100 lines
3.7 KiB
JavaScript

import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import AddPurchase from '../components/AddPurchase';
import PurchaseList from '../components/PurchaseList';
import PortfolioChart from '../components/PortfolioChart';
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' },
statsGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 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' },
};
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 navigate = useNavigate();
const authHeaders = () => ({
Authorization: `Bearer ${localStorage.getItem('token')}`,
});
const fetchData = useCallback(async () => {
try {
const [statsRes, purchasesRes] = await Promise.all([
fetch(`${API}/stats`, { headers: authHeaders() }),
fetch(`${API}/purchases`, { headers: authHeaders() }),
]);
if (statsRes.status === 401) {
localStorage.removeItem('token');
navigate('/login');
return;
}
setStats(await statsRes.json());
setPurchases(await purchasesRes.json());
} catch {
// silently fail — network may be unavailable
}
}, [navigate]);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleLogout = () => {
localStorage.removeItem('token');
navigate('/login');
};
const plHighlight = stats
? stats.profit_loss >= 0 ? 'positive' : 'negative'
: 'neutral';
return (
<div style={styles.app}>
<div style={styles.header}>
<div style={styles.logo}> BTC Portfolio</div>
<button style={styles.logoutBtn} onClick={handleLogout}>Logout</button>
</div>
{stats && (
<div style={styles.statsGrid}>
<StatCard label="Total Invested" value={`${stats.total_invested.toLocaleString()}`} />
<StatCard label="Total BTC" value={`${stats.total_btc}`} highlight="neutral" />
<StatCard label="Avg Buy Price" value={`${stats.average_price.toLocaleString()}`} />
<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>
)}
<PortfolioChart purchases={purchases} stats={stats} />
<AddPurchase onAdded={fetchData} />
<PurchaseList purchases={purchases} onChanged={fetchData} />
</div>
);
}