modified layout and made it so you can switch between card modals and keep the pricing chart

This commit is contained in:
zach
2026-03-16 11:05:10 -04:00
parent 9c81a13c69
commit c4ebbfb060
4 changed files with 323 additions and 346 deletions

View File

@@ -2,7 +2,6 @@ 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)' },
@@ -11,7 +10,7 @@ const CONDITION_COLORS = {
"Damaged": { active: 'rgba(255, 167, 36, 1)', muted: 'rgba(255, 167, 36, 0.67)' },
};
const RANGE_DAYS = { '1m': 30, '3m': 90 };
const RANGE_DAYS = { '1m': 30, '3m': 90, '6m': 180, '1y': 365, 'all': Infinity };
let chartInstance = null;
let allHistory = [];
@@ -24,8 +23,19 @@ function formatDate(dateStr) {
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
function setEmptyState(isEmpty) {
const modal = document.getElementById('cardModal');
const empty = modal?.querySelector('#priceHistoryEmpty');
const canvasWrapper = empty?.nextElementSibling;
if (!empty || !canvasWrapper) return;
empty.classList.toggle('d-none', !isEmpty);
canvasWrapper.classList.toggle('d-none', isEmpty);
}
function buildChartData(history, rangeKey) {
const cutoff = new Date(Date.now() - RANGE_DAYS[rangeKey] * 86_400_000);
const cutoff = RANGE_DAYS[rangeKey] === Infinity
? new Date(0)
: new Date(Date.now() - RANGE_DAYS[rangeKey] * 86_400_000);
const filtered = history.filter(r => new Date(r.calculatedAt) >= cutoff);
@@ -40,17 +50,22 @@ function buildChartData(history, rangeKey) {
lookup[row.condition][row.calculatedAt] = Number(row.marketPrice);
}
// Check specifically whether the active condition has any data points
const activeConditionDates = allDates.filter(
date => lookup[activeCondition]?.[date] != null
);
const activeConditionHasData = activeConditionDates.length > 0;
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,
borderWidth: isActive ? 2 : 1,
pointRadius: isActive ? 2.5 : 0,
pointHoverRadius: isActive ? 5 : 3,
pointBackgroundColor: isActive ? colors.active : colors.muted,
tension: 0.3,
@@ -60,12 +75,20 @@ function buildChartData(history, rangeKey) {
};
});
return { labels, datasets };
return { labels, datasets, hasData: allDates.length > 0, activeConditionHasData };
}
function updateChart() {
if (!chartInstance) return;
const { labels, datasets } = buildChartData(allHistory, activeRange);
const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange);
// Show empty state if no data at all, or if the active condition specifically has no data
if (!hasData || !activeConditionHasData) {
setEmptyState(true);
return;
}
setEmptyState(false);
chartInstance.data.labels = labels;
chartInstance.data.datasets = datasets;
chartInstance.update('none');
@@ -85,11 +108,18 @@ function initPriceChart(canvas) {
}
if (!allHistory.length) {
console.warn('No price history data for this card');
setEmptyState(true);
return;
}
const { labels, datasets } = buildChartData(allHistory, activeRange);
const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange);
if (!hasData || !activeConditionHasData) {
setEmptyState(true);
return;
}
setEmptyState(false);
chartInstance = new Chart(canvas.getContext('2d'), {
type: 'line',
@@ -151,64 +181,54 @@ function initPriceChart(canvas) {
});
}
function setupObserver() {
function initFromCanvas(canvas) {
activeCondition = "Near Mint";
activeRange = '1m';
const modal = document.getElementById('cardModal');
if (!modal) {
console.error('cardModal element not found');
return;
}
modal?.querySelectorAll('.price-range-btn').forEach(b => {
b.classList.toggle('active', b.dataset.range === '1m');
});
initPriceChart(canvas);
}
const observer = new MutationObserver(() => {
const canvas = document.getElementById('priceHistoryChart');
if (!canvas) return;
function setup() {
const modal = document.getElementById('cardModal');
if (!modal) 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);
modal.addEventListener('card-modal:swapped', () => {
const canvas = modal.querySelector('#priceHistoryChart');
if (canvas) initFromCanvas(canvas);
});
observer.observe(modal, { childList: true, subtree: true });
modal.addEventListener('hidden.bs.modal', () => {
if (chartInstance) {
chartInstance.destroy();
chartInstance = null;
}
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) => {
if (!modal.contains(e.target)) return;
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 || !modal.contains(btn)) return;
modal.querySelectorAll('.price-range-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeRange = btn.dataset.range ?? '1m';
updateChart();
});
}
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();
setup();