diff --git a/public/css/shared.css b/public/css/shared.css index 1d38fc3..c619efc 100644 --- a/public/css/shared.css +++ b/public/css/shared.css @@ -364,14 +364,99 @@ tr:last-child td { display: none; } +@keyframes expandIn { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } +} + +.expanded-content.is-open { + animation: expandIn 0.2s ease; +} + .expanded-content td { padding: 0; - background: var(--surface-2); + background: color-mix(in oklab, var(--accent) 4%, var(--paper-2)); border-top: 2px solid var(--accent); } .expanded-cell { - padding: 20px !important; + padding: 22px 28px 28px !important; +} + +tr.row-open { + box-shadow: inset 3px 0 0 var(--accent); +} + +/* ── Expanded Row Detail Grid ─────────────────── */ + +.player-detail { + display: grid; + grid-template-columns: 240px 1fr; + gap: 28px; + align-items: start; +} + +@media (max-width: 880px) { + .player-detail { + grid-template-columns: 1fr; + } +} + +.detail-grid { + display: grid; + grid-template-columns: 1fr; + margin: 0; + padding: 0; + list-style: none; +} + +.detail-grid > div { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; + padding: 8px 0; + border-bottom: 1px dashed var(--line); +} + +.detail-grid > div:last-child { + border-bottom: none; +} + +.detail-grid dt { + color: var(--ink-2); + font-size: 13px; + font-weight: 400; + flex-shrink: 0; +} + +.detail-grid dd { + color: var(--ink); + font-size: 13px; + font-family: var(--font-mono); + font-feature-settings: "tnum", "zero"; + margin: 0; + text-align: right; +} + +.player-chart { + width: 100%; + max-width: 880px; +} + +.link-btn { + background: none; + border: none; + padding: 0; + font-family: var(--font-sans); + font-size: 13px; + cursor: pointer; + color: var(--accent); +} + +.link-btn:disabled { + color: var(--ink-3); + cursor: default; } /* ── Links ────────────────────────────────────── */ diff --git a/public/js/chart.js b/public/js/chart.js index abbf382..09cc5d4 100644 --- a/public/js/chart.js +++ b/public/js/chart.js @@ -1,150 +1,138 @@ function createRatingChart(container, history) { - const width = container.clientWidth; - const height = 280; - const margin = { top: 24, right: 20, bottom: 44, left: 56 }; - const chartWidth = width - margin.left - margin.right; - const chartHeight = height - margin.top - margin.bottom; - - const pdgaNumber = container.id.replace('chart-', ''); - const tooltip = document.getElementById(`tooltip-${pdgaNumber}`); - - const ratings = history.map(h => h.rating); - const minRating = Math.min(...ratings) - 10; - const maxRating = Math.max(...ratings) + 10; - - const xStep = chartWidth / (history.length - 1 || 1); - const yScale = chartHeight / (maxRating - minRating); - - // Build points array - const points = []; - let pathData = ''; - - history.forEach((point, i) => { - const x = margin.left + (i * xStep); - const y = margin.top + ((maxRating - point.rating) * yScale); - points.push({ x, y, rating: point.rating, date: point.displayDate }); - pathData += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`; - }); - - // Area fill path (close to bottom) - const areaPath = pathData + - ` L ${points[points.length - 1].x} ${margin.top + chartHeight}` + - ` L ${points[0].x} ${margin.top + chartHeight} Z`; - - let svg = `'; + var seen = {}; + labelIndices.forEach(function(idx) { + if (seen[idx]) return; + seen[idx] = true; + var p = pts[idx]; + var d = new Date(history[idx].date); + var label = d.toLocaleDateString('en-US', { month: 'short', year: '2-digit' }); + svg.appendChild(txt('text', { + x: p.x.toFixed(1), + y: (pad.top + chartH + 16).toFixed(1), + 'text-anchor': 'middle', 'font-size': '10', + 'font-family': "'JetBrains Mono', monospace", + fill: 'var(--ink-3)' + }, label)); + }); container.textContent = ''; - container.insertAdjacentHTML('beforeend', svg); - - setTimeout(() => { - const svgElement = document.getElementById(`svg-${pdgaNumber}`) || container.querySelector('svg'); - if (!svgElement) return; - - const hoverAreas = svgElement.querySelectorAll('.hover-area'); - const dataPoints = svgElement.querySelectorAll('.data-point'); - - let currentTooltip = null; - let tooltipTimeout = null; - - hoverAreas.forEach((area, i) => { - area.addEventListener('mouseenter', (e) => { - if (tooltipTimeout) { - clearTimeout(tooltipTimeout); - tooltipTimeout = null; - } - - if (currentTooltip !== null && currentTooltip !== i) { - dataPoints[currentTooltip].setAttribute('r', '3.5'); - dataPoints[currentTooltip].setAttribute('fill', '#3b82f6'); - } - - currentTooltip = i; - const point = points[i]; - - tooltip.textContent = ''; - const strong = document.createElement('strong'); - strong.textContent = point.date; - tooltip.appendChild(strong); - tooltip.appendChild(document.createElement('br')); - tooltip.appendChild(document.createTextNode('Rating: ' + point.rating)); - - tooltip.style.display = 'block'; - tooltip.style.left = `${e.clientX + 15}px`; - tooltip.style.top = `${e.clientY - 35}px`; - - dataPoints[i].setAttribute('r', '6'); - dataPoints[i].setAttribute('fill', '#2563eb'); - }); - - area.addEventListener('mousemove', (e) => { - if (currentTooltip === i) { - tooltip.style.left = `${e.clientX + 15}px`; - tooltip.style.top = `${e.clientY - 35}px`; - } - }); - - area.addEventListener('mouseleave', () => { - if (currentTooltip === i) { - tooltipTimeout = setTimeout(() => { - tooltip.style.display = 'none'; - dataPoints[i].setAttribute('r', '3.5'); - dataPoints[i].setAttribute('fill', '#3b82f6'); - currentTooltip = null; - }, 100); - } - }); - }); - }, 100); + container.appendChild(svg); } diff --git a/public/js/players.js b/public/js/players.js index 63a5806..e622de3 100644 --- a/public/js/players.js +++ b/public/js/players.js @@ -1,5 +1,6 @@ -let cachedDebugInfo = {}; -let pendingPlayerData = null; +var cachedDebugInfo = {}; +var pendingPlayerData = null; +var openPdgaNumber = null; function setupTooltipsAfterSwap() { document.body.addEventListener('htmx:afterSwap', function(event) { @@ -9,7 +10,7 @@ function setupTooltipsAfterSwap() { // After player history partial loads, render the chart const target = event.detail.target; if (target.id && target.id.startsWith('history-content-')) { - const container = target.querySelector('.chart-container'); + const container = target.querySelector('.player-chart, .chart-container'); if (container && container.dataset.history) { try { const history = JSON.parse(container.dataset.history); @@ -48,21 +49,46 @@ function initRatingsTooltips() { } function togglePlayerHistory(pdgaNumber) { - const historyRow = document.getElementById(`history-${pdgaNumber}`); - const contentDiv = document.getElementById(`history-content-${pdgaNumber}`); + var historyRow = document.getElementById('history-' + pdgaNumber); + var contentDiv = document.getElementById('history-content-' + pdgaNumber); + var expandableRow = document.getElementById('row-' + pdgaNumber); - if (historyRow.style.display === 'table-row') { + var isOpen = historyRow.style.display === 'table-row'; + + // Close any previously-open row + if (openPdgaNumber !== null && openPdgaNumber !== pdgaNumber) { + var prevHistory = document.getElementById('history-' + openPdgaNumber); + var prevRow = document.getElementById('row-' + openPdgaNumber); + if (prevHistory) { + prevHistory.style.display = 'none'; + prevHistory.classList.remove('is-open'); + } + if (prevRow) prevRow.classList.remove('row-open'); + openPdgaNumber = null; + } + + if (isOpen) { historyRow.style.display = 'none'; + historyRow.classList.remove('is-open'); + expandableRow.classList.remove('row-open'); + openPdgaNumber = null; return; } historyRow.style.display = 'table-row'; + // Force reflow so animation plays each open + historyRow.classList.remove('is-open'); + void historyRow.offsetWidth; + historyRow.classList.add('is-open'); + + expandableRow.classList.add('row-open'); + openPdgaNumber = pdgaNumber; if (contentDiv.dataset.loaded === 'true') { return; } - htmx.ajax('GET', `/partials/player-history/${pdgaNumber}`, {target: `#history-content-${pdgaNumber}`, swap: 'innerHTML'}); + htmx.ajax('GET', '/partials/player-history/' + pdgaNumber, {target: '#history-content-' + pdgaNumber, swap: 'innerHTML'}); contentDiv.dataset.loaded = 'true'; } @@ -497,3 +523,13 @@ document.addEventListener('DOMContentLoaded', function() { localStorage.setItem('ratingtracker.sparklines', next); }); }); + +// ── Expandable row keyboard support ─────────────── +document.addEventListener('keydown', function(e) { + if (e.key !== 'Enter' && e.key !== ' ') return; + var row = e.target; + if (!row.classList || !row.classList.contains('expandable-row')) return; + e.preventDefault(); + var pdgaNumber = row.id.replace('row-', ''); + togglePlayerHistory(parseInt(pdgaNumber, 10)); +}); diff --git a/views/partials/player-history.ejs b/views/partials/player-history.ejs index 9bb0edf..ba627d1 100644 --- a/views/partials/player-history.ejs +++ b/views/partials/player-history.ejs @@ -1,12 +1,55 @@ -<% if (history && history.length > 0) { %> -