Add full-stack BTC portfolio web app

Multi-user FastAPI + React app with JWT auth, SQLite storage, and
CoinGecko price integration. Dockerized with docker-compose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-03-23 22:15:40 +01:00
parent 84679639ef
commit 3907414742
27 changed files with 859 additions and 0 deletions
@@ -0,0 +1,100 @@
import React, { useRef } from 'react';
import {
Chart as ChartJS,
LineElement,
PointElement,
LinearScale,
CategoryScale,
Tooltip,
Legend,
Filler,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Tooltip, Legend, Filler);
const styles = {
card: { background: '#1a1a1a', padding: '1.5rem', borderRadius: '12px', border: '1px solid #333', marginBottom: '1.5rem' },
title: { fontSize: '1.1rem', fontWeight: 700, marginBottom: '1rem', color: '#f7931a' },
saveBtn: { marginTop: '0.75rem', background: 'none', border: '1px solid #555', color: '#aaa', borderRadius: '6px', padding: '0.4rem 1rem', cursor: 'pointer', fontSize: '0.85rem' },
};
export default function PortfolioChart({ purchases, stats }) {
const chartRef = useRef(null);
if (!purchases || purchases.length === 0) return null;
// Build cumulative data points sorted by date
const sorted = [...purchases].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
let cumInvested = 0;
let cumBtc = 0;
const labels = [];
const portfolioValues = [];
const investedValues = [];
sorted.forEach((p, i) => {
cumInvested += p.amount_eur;
cumBtc += p.amount_eur / p.price_eur;
const currentVal = cumBtc * (stats?.current_price || p.price_eur);
labels.push(new Date(p.created_at).toLocaleDateString());
portfolioValues.push(parseFloat(currentVal.toFixed(2)));
investedValues.push(parseFloat(cumInvested.toFixed(2)));
});
const currentPrice = stats?.current_price || 0;
const breakEvenLine = labels.map(() => stats?.average_price || 0);
const data = {
labels,
datasets: [
{
label: 'Portfolio Value (€)',
data: portfolioValues,
borderColor: '#f7931a',
backgroundColor: 'rgba(247,147,26,0.1)',
fill: true,
tension: 0.3,
},
{
label: 'Total Invested (€)',
data: investedValues,
borderColor: '#4fc3f7',
backgroundColor: 'transparent',
borderDash: [6, 3],
tension: 0.3,
},
],
};
const options = {
responsive: true,
plugins: {
legend: { labels: { color: '#aaa' } },
tooltip: { mode: 'index', intersect: false },
},
scales: {
x: { ticks: { color: '#666' }, grid: { color: '#2a2a2a' } },
y: { ticks: { color: '#666' }, grid: { color: '#2a2a2a' } },
},
};
const handleSave = () => {
const chart = chartRef.current;
if (!chart) return;
const url = chart.toBase64Image();
const a = document.createElement('a');
a.href = url;
a.download = 'btc-portfolio.png';
a.click();
};
return (
<div style={styles.card}>
<div style={styles.title}>Portfolio Chart</div>
<Line ref={chartRef} data={data} options={options} />
<br />
<button style={styles.saveBtn} onClick={handleSave}>Save as PNG</button>
</div>
);
}