import Chart from 'chart.js/auto'; const CONDITIONS = ["Near Mint", "Lightly Played", "Moderately Played", "Heavily Played", "Damaged"]; // Match the $tiers colors from your SCSS exactly const CONDITION_COLORS = { "Near Mint": { active: 'rgba(156, 204, 102, 1)', muted: 'rgba(156, 204, 102, 0.67)' }, "Lightly Played": { active: 'rgba(211, 225, 86, 1)', muted: 'rgba(211, 225, 86, 0.67)' }, "Moderately Played": { active: 'rgba(255, 238, 87, 1)', muted: 'rgba(255, 238, 87, 0.67)' }, "Heavily Played": { active: 'rgba(255, 201, 41, 1)', muted: 'rgba(255, 201, 41, 0.67)' }, "Damaged": { active: 'rgba(255, 167, 36, 1)', muted: 'rgba(255, 167, 36, 0.67)' }, }; const RANGE_DAYS = { '1m': 30, '3m': 90 }; let chartInstance = null; let allHistory = []; let activeCondition = "Near Mint"; let activeRange = '1m'; function formatDate(dateStr) { const [year, month, day] = dateStr.split('-'); const d = new Date(Number(year), Number(month) - 1, Number(day)); return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } function buildChartData(history, rangeKey) { const cutoff = new Date(Date.now() - RANGE_DAYS[rangeKey] * 86_400_000); const filtered = history.filter(r => new Date(r.calculatedAt) >= cutoff); const allDates = [...new Set(filtered.map(r => r.calculatedAt))] .sort((a, b) => new Date(a) - new Date(b)); const labels = allDates.map(formatDate); const lookup = {}; for (const row of filtered) { if (!lookup[row.condition]) lookup[row.condition] = {}; lookup[row.condition][row.calculatedAt] = Number(row.marketPrice); } const datasets = CONDITIONS.map(condition => { const isActive = condition === activeCondition; const colors = CONDITION_COLORS[condition]; const data = allDates.map(date => lookup[condition]?.[date] ?? null); return { label: condition, data, borderColor: isActive ? colors.active : colors.muted, borderWidth: isActive ? 2.5 : 1, pointRadius: isActive ? 3 : 0, pointHoverRadius: isActive ? 5 : 3, pointBackgroundColor: isActive ? colors.active : colors.muted, tension: 0.3, fill: false, spanGaps: true, order: isActive ? 0 : 1, }; }); return { labels, datasets }; } function updateChart() { if (!chartInstance) return; const { labels, datasets } = buildChartData(allHistory, activeRange); chartInstance.data.labels = labels; chartInstance.data.datasets = datasets; chartInstance.update('none'); } function initPriceChart(canvas) { if (chartInstance) { chartInstance.destroy(); chartInstance = null; } try { allHistory = JSON.parse(canvas.dataset.history ?? '[]'); } catch (err) { console.error('Failed to parse price history:', err); return; } if (!allHistory.length) { console.warn('No price history data for this card'); return; } const { labels, datasets } = buildChartData(allHistory, activeRange); chartInstance = new Chart(canvas.getContext('2d'), { type: 'line', data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(0, 0, 0, 0.85)', titleColor: 'rgba(255, 255, 255, 0.9)', bodyColor: 'rgba(255, 255, 255, 0.75)', borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 1, padding: 10, callbacks: { labelColor: (ctx) => { const colors = CONDITION_COLORS[ctx.dataset.label]; return { borderColor: colors.active, backgroundColor: colors.active, }; }, label: (ctx) => { const isActive = ctx.dataset.label === activeCondition; const price = ctx.parsed.y != null ? `$${ctx.parsed.y.toFixed(2)}` : '—'; return isActive ? ` ${ctx.dataset.label}: ${price} ◀` : ` ${ctx.dataset.label}: ${price}`; } } } }, scales: { x: { grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: 'rgba(255, 255, 255, 0.4)', maxTicksLimit: 6, maxRotation: 0, }, border: { color: 'rgba(255, 255, 255, 0.1)' }, }, y: { grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: 'rgba(255, 255, 255, 0.4)', callback: (val) => `$${Number(val).toFixed(2)}`, }, border: { color: 'rgba(255, 255, 255, 0.1)' }, } } } }); } function setupObserver() { const modal = document.getElementById('cardModal'); if (!modal) { console.error('cardModal element not found'); return; } const observer = new MutationObserver(() => { const canvas = document.getElementById('priceHistoryChart'); if (!canvas) return; // Disconnect immediately so tab switches don't retrigger initPriceChart observer.disconnect(); activeCondition = "Near Mint"; activeRange = '1m'; document.querySelectorAll('.price-range-btn').forEach(b => { b.classList.toggle('active', b.dataset.range === '1m'); }); initPriceChart(canvas); }); observer.observe(modal, { childList: true, subtree: true }); modal.addEventListener('hidden.bs.modal', () => { if (chartInstance) { chartInstance.destroy(); chartInstance = null; } allHistory = []; // Reconnect so the next card open is detected observer.observe(modal, { childList: true, subtree: true }); }); } document.addEventListener('shown.bs.tab', (e) => { const target = e.target?.getAttribute('data-bs-target'); const conditionMap = { '#nav-nm': 'Near Mint', '#nav-lp': 'Lightly Played', '#nav-mp': 'Moderately Played', '#nav-hp': 'Heavily Played', '#nav-dmg': 'Damaged', }; if (target && conditionMap[target]) { activeCondition = conditionMap[target]; updateChart(); } }); document.addEventListener('click', (e) => { const btn = e.target?.closest('.price-range-btn'); if (!btn) return; document.querySelectorAll('.price-range-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); activeRange = btn.dataset.range ?? '1m'; updateChart(); }); setupObserver();