(() => { const root = document.documentElement; const toggle = document.querySelector('[data-theme-toggle]'); const updateThemeLabel = () => { const label = document.querySelector('[data-theme-label]'); if (label) label.textContent = root.dataset.theme === 'dark' ? 'Light mode' : 'Dark mode'; }; updateThemeLabel(); toggle?.addEventListener('click', () => { root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark'; localStorage.setItem('budget-theme', root.dataset.theme); updateThemeLabel(); document.querySelectorAll('[data-trend-chart]').forEach(drawTrendChart); }); document.querySelector('[data-menu-toggle]')?.addEventListener('click', () => { document.querySelector('#sidebar')?.classList.toggle('open'); }); document.querySelector('[data-month-picker]')?.addEventListener('change', (event) => { const picker = event.currentTarget; const parameters = new URLSearchParams({ route: picker.dataset.route, month: picker.value }); window.location.href = `?${parameters.toString()}`; }); document.querySelectorAll('[data-dialog-open]').forEach((button) => { button.addEventListener('click', () => document.getElementById(button.dataset.dialogOpen)?.showModal()); }); document.querySelectorAll('[data-dialog-close]').forEach((button) => { button.addEventListener('click', () => button.closest('dialog')?.close()); }); document.querySelectorAll('dialog').forEach((dialog) => { dialog.addEventListener('click', (event) => { if (event.target === dialog) dialog.close(); }); }); document.querySelectorAll('[data-confirm]').forEach((form) => { form.addEventListener('submit', (event) => { if (!window.confirm(form.dataset.confirm)) event.preventDefault(); }); }); document.querySelectorAll('.flash button').forEach((button) => { button.addEventListener('click', () => button.closest('.flash')?.remove()); }); document.querySelectorAll('[data-table-search]').forEach((input) => { input.addEventListener('input', () => { const table = document.getElementById(input.dataset.tableSearch); const query = input.value.trim().toLowerCase(); table?.querySelectorAll('tbody tr').forEach((row) => { row.hidden = query !== '' && !row.textContent.toLowerCase().includes(query); }); }); }); const concealed = document.querySelector('[data-concealed-balance]'); if (concealed) { let approved = false; concealed.addEventListener('click', () => { if (!approved) approved = window.confirm('Reveal the dormant account balance while your pointer remains here?'); if (approved) { concealed.querySelector('.revealed').textContent = concealed.dataset.value; concealed.classList.add('reveal'); } }); concealed.addEventListener('mouseleave', () => { concealed.classList.remove('reveal'); approved = false; }); } function drawTrendChart(canvas) { let data; try { data = JSON.parse(canvas.dataset.trendChart || '[]'); } catch { return; } if (!data.length) return; const width = canvas.clientWidth || 600; const height = Number(canvas.getAttribute('height')) || 220; const ratio = window.devicePixelRatio || 1; canvas.width = width * ratio; canvas.height = height * ratio; canvas.style.height = `${height}px`; const context = canvas.getContext('2d'); context.scale(ratio, ratio); const styles = getComputedStyle(document.documentElement); const text = styles.getPropertyValue('--muted').trim(); const border = styles.getPropertyValue('--border-strong').trim(); const purple = styles.getPropertyValue('--purple-bright').trim(); const pink = styles.getPropertyValue('--pink').trim(); const padding = { top: 20, right: 12, bottom: 32, left: 48 }; const plotWidth = width - padding.left - padding.right; const plotHeight = height - padding.top - padding.bottom; const maxValue = Math.max(...data.flatMap((row) => [Number(row.gross_resources) || 0, Number(row.expenses) || 0]), 1) * 1.12; context.clearRect(0, 0, width, height); context.font = '10px system-ui'; context.fillStyle = text; context.strokeStyle = border; context.lineWidth = 1; context.setLineDash([3, 5]); for (let line = 0; line <= 4; line++) { const y = padding.top + plotHeight * (line / 4); context.beginPath(); context.moveTo(padding.left, y); context.lineTo(width - padding.right, y); context.stroke(); const value = maxValue * (1 - line / 4); context.fillText(compactMoney(value, canvas.dataset.symbol), 0, y + 3); } context.setLineDash([]); const pointX = (index) => padding.left + (data.length === 1 ? plotWidth / 2 : plotWidth * index / (data.length - 1)); const pointY = (value) => padding.top + plotHeight - (Number(value) / maxValue * plotHeight); const drawSeries = (key, color, fill) => { context.beginPath(); data.forEach((row, index) => { const x = pointX(index); const y = pointY(row[key]); index === 0 ? context.moveTo(x, y) : context.lineTo(x, y); }); context.strokeStyle = color; context.lineWidth = 2.5; context.lineJoin = 'round'; context.stroke(); if (fill) { context.lineTo(pointX(data.length - 1), padding.top + plotHeight); context.lineTo(pointX(0), padding.top + plotHeight); context.closePath(); const gradient = context.createLinearGradient(0, padding.top, 0, padding.top + plotHeight); gradient.addColorStop(0, `${color}32`); gradient.addColorStop(1, `${color}00`); context.fillStyle = gradient; context.fill(); } data.forEach((row, index) => { context.beginPath(); context.arc(pointX(index), pointY(row[key]), 3.5, 0, Math.PI * 2); context.fillStyle = color; context.fill(); }); }; drawSeries('gross_resources', purple, true); drawSeries('expenses', pink, false); data.forEach((row, index) => { const label = new Date(`${row.month}-02T12:00:00`).toLocaleDateString(undefined, { month: 'short' }); context.fillStyle = text; context.textAlign = 'center'; context.fillText(label, pointX(index), height - 9); }); context.textAlign = 'left'; context.fillStyle = purple; context.fillRect(width - 145, 3, 8, 8); context.fillStyle = text; context.fillText('Resources', width - 132, 11); context.fillStyle = pink; context.fillRect(width - 70, 3, 8, 8); context.fillStyle = text; context.fillText('Spent', width - 57, 11); } function compactMoney(value, symbol = '$') { if (value >= 1000000) return `${symbol}${(value / 1000000).toFixed(1)}m`; if (value >= 1000) return `${symbol}${(value / 1000).toFixed(1)}k`; return `${symbol}${Math.round(value)}`; } document.querySelectorAll('[data-trend-chart]').forEach(drawTrendChart); let resizeTimer; window.addEventListener('resize', () => { window.clearTimeout(resizeTimer); resizeTimer = window.setTimeout(() => document.querySelectorAll('[data-trend-chart]').forEach(drawTrendChart), 120); }); })();