541b5f57d2
New GET /history endpoint fetches 365 days of BTC/EUR data from CoinGecko, deduplicates by date, and joins the user's purchases. BTCHistoryChart component renders the price line with orange dot markers on purchase dates and a dashed cyan avg buy price line. Tooltip shows purchase details on marked dates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
122 lines
3.6 KiB
JavaScript
122 lines
3.6 KiB
JavaScript
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' },
|
|
loading: { color: '#666', padding: '1rem 0' },
|
|
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 BTCHistoryChart({ history, stats }) {
|
|
const chartRef = useRef(null);
|
|
|
|
if (!history || history.prices.length === 0) {
|
|
return (
|
|
<div style={styles.card}>
|
|
<div style={styles.title}>1-Year BTC Price (EUR)</div>
|
|
<div style={styles.loading}>Loading price history…</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const { prices, purchases } = history;
|
|
const averagePrice = stats?.average_price ?? 0;
|
|
|
|
const purchaseDateSet = new Set(purchases.map(p => p.date));
|
|
|
|
const pointRadii = prices.map(p => purchaseDateSet.has(p.date) ? 7 : 0);
|
|
const pointColors = prices.map(p => purchaseDateSet.has(p.date) ? '#f7931a' : 'transparent');
|
|
const pointBorderColors = prices.map(p => purchaseDateSet.has(p.date) ? '#fff' : 'transparent');
|
|
|
|
const data = {
|
|
labels: prices.map(p => p.date),
|
|
datasets: [
|
|
{
|
|
label: 'BTC Price (€)',
|
|
data: prices.map(p => p.price),
|
|
borderColor: '#f7931a',
|
|
backgroundColor: 'rgba(247,147,26,0.08)',
|
|
fill: true,
|
|
tension: 0.2,
|
|
pointRadius: pointRadii,
|
|
pointBackgroundColor: pointColors,
|
|
pointBorderColor: pointBorderColors,
|
|
pointBorderWidth: 2,
|
|
pointHoverRadius: 6,
|
|
},
|
|
...(averagePrice > 0 ? [{
|
|
label: `Avg Buy Price (€${averagePrice.toLocaleString()})`,
|
|
data: prices.map(() => averagePrice),
|
|
borderColor: '#4fc3f7',
|
|
backgroundColor: 'transparent',
|
|
borderDash: [8, 4],
|
|
pointRadius: 0,
|
|
pointHoverRadius: 0,
|
|
tension: 0,
|
|
}] : []),
|
|
],
|
|
};
|
|
|
|
const options = {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: { labels: { color: '#aaa' } },
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
callbacks: {
|
|
afterBody: (items) => {
|
|
const date = items[0]?.label;
|
|
const purchase = purchases.find(p => p.date === date);
|
|
if (purchase) {
|
|
return [`Purchased: €${purchase.amount_eur.toLocaleString()} @ €${purchase.price_eur.toLocaleString()}`];
|
|
}
|
|
return [];
|
|
},
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
x: {
|
|
ticks: { color: '#666', maxTicksLimit: 12, maxRotation: 0 },
|
|
grid: { color: '#2a2a2a' },
|
|
},
|
|
y: {
|
|
ticks: { color: '#666' },
|
|
grid: { color: '#2a2a2a' },
|
|
},
|
|
},
|
|
};
|
|
|
|
const handleSave = () => {
|
|
const chart = chartRef.current;
|
|
if (!chart) return;
|
|
const a = document.createElement('a');
|
|
a.href = chart.toBase64Image();
|
|
a.download = 'btc-history.png';
|
|
a.click();
|
|
};
|
|
|
|
return (
|
|
<div style={styles.card}>
|
|
<div style={styles.title}>1-Year BTC Price (EUR)</div>
|
|
<Line ref={chartRef} data={data} options={options} />
|
|
<br />
|
|
<button style={styles.saveBtn} onClick={handleSave}>Save as PNG</button>
|
|
</div>
|
|
);
|
|
}
|