diff --git a/public/css/players.css b/public/css/players.css index 7a512f7..5a68d0d 100644 --- a/public/css/players.css +++ b/public/css/players.css @@ -2,15 +2,7 @@ Players Page ═══════════════════════════════════════════════════ */ -/* ── Add Player Button ─────────────────────────── */ - -.btn-add { - background: var(--green); -} - -.btn-add:hover { - background: #059669; -} +/* ── (Add player now uses .btn-primary from shared.css) ── */ /* ── Mobile helpers ───────────────────────────── */ @@ -30,12 +22,6 @@ /* ── Rating values ────────────────────────────── */ -.rating { - font-weight: 700; - color: var(--accent); - font-variant-numeric: tabular-nums; -} - .rating-value { font-variant-numeric: tabular-nums; } diff --git a/public/css/shared.css b/public/css/shared.css index e8a7d67..204cc57 100644 --- a/public/css/shared.css +++ b/public/css/shared.css @@ -293,9 +293,12 @@ body { /* ── Container ────────────────────────────────── */ .container { - max-width: 960px; + max-width: 1180px; margin: 0 auto; - padding: 28px 24px 60px; + padding: 28px 32px 60px; + display: flex; + flex-direction: column; + gap: 22px; } .page-title { @@ -303,7 +306,7 @@ body { font-weight: 700; color: var(--text-primary); letter-spacing: -0.03em; - margin: 0 0 24px 0; + margin: 0; } /* ── Tables ───────────────────────────────────── */ @@ -322,28 +325,23 @@ thead { } th { - padding: 10px 16px; + padding: 0 16px; + height: 48px; text-align: left; font-weight: 600; font-size: 11px; text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--text-secondary); - background: var(--surface-2); - border-bottom: 1px solid var(--border); -} - -th:first-child { - border-radius: var(--radius-md) 0 0 0; -} - -th:last-child { - border-radius: 0 var(--radius-md) 0 0; + letter-spacing: 0.08em; + color: var(--ink-3); + background: var(--paper-2); + border-bottom: 1px solid var(--line); + vertical-align: middle; } td { - padding: 12px 16px; - border-bottom: 1px solid var(--border-light); + padding: 0 16px; + height: 64px; + border-bottom: 1px solid var(--line); vertical-align: middle; } @@ -351,13 +349,116 @@ tr:last-child td { border-bottom: none; } +/* Column widths */ +.col-rank { width: 56px; text-align: center; } +.col-player { min-width: 200px; } +.col-rating { min-width: 220px; } +.col-predicted { min-width: 180px; } +.col-actions { width: 72px; } + +/* Rank number */ +.rank-num { + font-size: 13px; + color: var(--ink-3); + font-weight: 500; +} + +/* Player name cell */ +.cell-name { + display: flex; + flex-direction: column; + gap: 2px; +} + +.player-name a { + font-weight: 600; + font-size: 14px; + letter-spacing: -0.005em; + color: var(--ink); + text-decoration: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; +} + +.player-name a:hover { + color: var(--accent); +} + +.pdga-num { + font-size: 11px; + color: var(--ink-3); +} + +/* Rating cell stack */ +.cell-rating { + vertical-align: middle; +} + +.rating-stack { + display: flex; + flex-direction: column; + gap: 10px; + align-items: flex-start; +} + +.rating-big { + font-size: 20px; + font-weight: 600; + color: var(--ink); + letter-spacing: -0.02em; + line-height: 1; +} + +.rating-pending { + font-size: 12px; + font-style: italic; + color: var(--ink-3); +} + +/* Predicted cell stack */ +.pred-stack { + display: flex; + flex-direction: column; + gap: 10px; + align-items: flex-start; +} + +.pred-num { + font-size: 20px; + font-weight: 600; + color: var(--ink-2); + letter-spacing: -0.02em; + line-height: 1; +} + +/* Actions cell */ +.cell-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; +} + +/* Chevron rotation when row open */ +tr.row-open .icon-chev i { + transform: rotate(180deg); +} + +.icon-chev i { + transition: transform 180ms ease; +} + .expandable-row { cursor: pointer; - transition: background var(--transition); + transition: background 120ms ease; + outline: none; + position: relative; } .expandable-row:hover { - background: var(--accent-subtle); + background: var(--hover); } .expanded-content { @@ -376,7 +477,7 @@ tr:last-child td { .expanded-content td { padding: 0; background: color-mix(in oklab, var(--accent) 4%, var(--paper-2)); - border-top: 2px solid var(--accent); + border-bottom: 1px solid var(--line); } .expanded-cell { @@ -506,31 +607,129 @@ a:hover { box-shadow: none; } -/* ── Card Section ─────────────────────────────── */ +/* ── Add Player Bar ───────────────────────────── */ -.card-section { - background: var(--surface-1); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 24px; - margin-bottom: 28px; - box-shadow: var(--shadow-sm); -} - -.card-section h3 { - margin: 0 0 14px 0; - color: var(--text-primary); - font-size: 15px; - font-weight: 600; - text-align: center; -} - -.card-section-form { +.add-bar { + background: var(--paper); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 20px 24px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + box-shadow: var(--shadow-card); +} + +.add-bar-label { + display: flex; + flex-direction: column; + gap: 2px; +} + +.add-bar-kicker { + font-weight: 700; + font-size: 16px; + letter-spacing: -0.01em; + color: var(--ink); +} + +.add-bar-hint { + font-size: 12px; + color: var(--ink-3); +} + +.add-bar-controls { display: flex; - justify-content: center; align-items: center; gap: 10px; - flex-wrap: wrap; + flex-shrink: 0; +} + +.input-wrap { + position: relative; + display: flex; + align-items: center; + background: var(--paper-2); + border: 1px solid var(--line-2); + border-radius: 10px; + height: 40px; + width: 220px; + transition: border-color 150ms ease, box-shadow 150ms ease; +} + +.input-wrap:focus-within { + border-color: var(--accent); + box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 14%, transparent); +} + +.input-prefix { + padding: 0 0 0 14px; + color: var(--ink-3); + font-family: var(--font-mono); + font-size: 14px; + font-weight: 500; + flex-shrink: 0; +} + +.input-wrap input { + flex: 1; + background: transparent; + border: none; + outline: none; + padding: 0 14px 0 6px; + height: 100%; + font-family: var(--font-mono); + font-size: 14px; + color: var(--ink); + font-feature-settings: "tnum"; +} + +.input-wrap input::placeholder { + color: var(--ink-3); + opacity: 0.7; +} + +.btn-primary { + display: inline-flex; + align-items: center; + gap: 8px; + background: var(--accent); + color: white; + border: none; + height: 40px; + padding: 0 18px; + border-radius: 10px; + font-weight: 600; + font-size: 13px; + font-family: var(--font-sans); + cursor: pointer; + box-shadow: 0 1px 0 color-mix(in oklab, var(--accent) 50%, black), 0 1px 3px color-mix(in oklab, var(--accent) 40%, transparent); + transition: transform 80ms ease, box-shadow 150ms ease, background 150ms ease; +} + +.btn-primary:hover { + background: color-mix(in oklab, var(--accent) 92%, black); +} + +.btn-primary:active { + transform: translateY(1px); +} + +@media (max-width: 768px) { + .add-bar { + flex-direction: column; + align-items: stretch; + } + + .add-bar-controls { + flex-wrap: wrap; + } + + .input-wrap { + flex: 1; + width: auto; + } } /* ── Inputs ───────────────────────────────────── */ @@ -556,20 +755,6 @@ a:hover { color: var(--text-muted); } -@media (max-width: 768px) { - .card-section { - padding: 16px; - } - - .card-section-form { - flex-direction: column; - } - - .card-section-form .input { - width: 100%; - } -} - /* ── Refresh Icons ────────────────────────────── */ .refresh-icon { @@ -601,13 +786,10 @@ a:hover { /* ── Responsive ───────────────────────────────── */ -@media (max-width: 768px) { +@media (max-width: 880px) { .container { - padding: 16px 12px 40px; - } - - .page-title { - font-size: 22px; + padding: 18px 16px 40px; + gap: 16px; } table { @@ -615,25 +797,17 @@ a:hover { } th, td { - padding: 10px 8px; + padding: 0 10px; } - th { - font-size: 10px; - } - - .mobile-hide { - display: none; - } - - thead { - top: var(--topbar-height); - } + .col-rank { width: 40px; } + .col-actions { width: 40px; } + .col-predicted { display: none; } } -@media (max-width: 480px) { - th, td { - padding: 8px 6px; +@media (max-width: 768px) { + .mobile-hide { + display: none; } } @@ -642,13 +816,27 @@ a:hover { .delta-pill { display: inline-flex; align-items: center; - padding: 2px 8px; + gap: 4px; + padding: 3px 8px 3px 6px; border-radius: 999px; + font-size: 12px; + font-weight: 600; + line-height: 1; + white-space: nowrap; +} + +.delta-glyph { + font-size: 9px; + display: inline-block; + line-height: 1; + position: relative; + top: -0.5px; +} + +.delta-num { font-family: var(--font-mono); font-feature-settings: "tnum", "zero"; - font-size: 11px; - font-weight: 500; - margin-top: 4px; + letter-spacing: 0; } .delta-pill.up { @@ -664,6 +852,7 @@ a:hover { .delta-pill.flat { background: var(--paper-2); color: var(--ink-3); + border: 1px solid var(--line); } /* ── Table Header Hints ───────────────────────── */ @@ -684,9 +873,9 @@ a:hover { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; - margin-bottom: 16px; list-style: none; padding: 0; + margin: 0; } .kpi-tile { @@ -749,14 +938,62 @@ a:hover { } } -/* ── Table Toolbar + Pill Toggle ─────────────── */ +/* ── Table Card + Toolbar ─────────────────────── */ + +.table-card { + background: var(--paper); + border: 1px solid var(--line); + border-radius: var(--radius); + box-shadow: var(--shadow-card); + overflow: hidden; +} + +.kicker { + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.10em; + color: var(--ink-3); +} .table-toolbar { display: flex; - justify-content: flex-end; - margin-bottom: 12px; + align-items: center; + justify-content: space-between; + padding: 12px 20px; + border-bottom: 1px solid var(--line); } +.toolbar-actions { + display: flex; + gap: 6px; + align-items: center; +} + +/* ── Icon Buttons ─────────────────────────────── */ + +.icon-btn { + background: transparent; + border: 1px solid transparent; + width: 28px; + height: 28px; + border-radius: 8px; + display: grid; + place-items: center; + color: var(--ink-3); + cursor: pointer; + font-size: 12px; + transition: background 150ms ease, color 150ms ease, border-color 150ms ease; +} + +.icon-btn:hover { + background: var(--hover); + color: var(--ink); + border-color: var(--line); +} + +/* ── Table Pill Toggle ────────────────────────── */ + .pill-toggle { display: inline-flex; align-items: center; @@ -808,12 +1045,29 @@ a:hover { /* ── Sparklines ───────────────────────────────── */ .sparkline { - display: inline-block; - vertical-align: middle; + display: block; line-height: 0; - margin-top: 4px; + flex-shrink: 0; } -body[data-sparklines="off"] .sparkline { +.spark { + display: block; + overflow: visible; +} + +body[data-sparklines="off"] .sparkline, +body[data-sparklines="off"] .spark { display: none; } + +/* ── Footnote ─────────────────────────────────── */ + +.footnote { + font-size: 11.5px; + color: var(--ink-3); + text-align: center; + max-width: 640px; + margin: 0 auto; + line-height: 1.6; + padding: 0 16px; +} diff --git a/public/js/players.js b/public/js/players.js index 9701eb1..c581361 100644 --- a/public/js/players.js +++ b/public/js/players.js @@ -5,9 +5,25 @@ let openPdgaNumber = null; // ── Delta-pill helper ───────────────────────────── function renderDeltaPill(value, extraClass) { if (value == null) return null; - const cls = value > 0 ? 'up' : value < 0 ? 'down' : 'flat'; - const text = value > 0 ? '+' + value : String(value); - return { text, cls }; + const cls = value > 0 ? 'up' : value < 0 ? 'down' : 'flat'; + const glyph = value > 0 ? '▲' : value < 0 ? '▼' : '–'; + const num = value > 0 ? '+' + value : String(value); + return { glyph, num, cls }; +} + +function applyDeltaPill(pillEl, value) { + if (!pillEl || value == null) return; + const pill = renderDeltaPill(value); + pillEl.className = 'delta-pill ' + pill.cls; + while (pillEl.firstChild) pillEl.removeChild(pillEl.firstChild); + const glyphSpan = document.createElement('span'); + glyphSpan.className = 'delta-glyph'; + glyphSpan.textContent = pill.glyph; + const numSpan = document.createElement('span'); + numSpan.className = 'delta-num'; + numSpan.textContent = pill.num; + pillEl.appendChild(glyphSpan); + pillEl.appendChild(numSpan); } function setupTooltipsAfterSwap() { @@ -120,8 +136,8 @@ async function clearCache() { } async function refreshPlayer(pdgaNumber) { - const icon = document.querySelector(`#row-${pdgaNumber} .rating .refresh-icon`); - icon.classList.add('spinning'); + const icon = document.querySelector(`#row-${pdgaNumber} .cell-actions .refresh-icon`); + if (icon) icon.classList.add('spinning'); try { const response = await fetch(`/api/refresh-player/${pdgaNumber}`, { method: 'POST' }); @@ -129,12 +145,12 @@ async function refreshPlayer(pdgaNumber) { if (data.success) { const row = document.getElementById(`row-${pdgaNumber}`); - const ratingCell = row.querySelector('.rating'); + const ratingCell = row.querySelector('.cell-rating'); const nameLink = row.querySelector('.player-name a'); - nameLink.textContent = data.player.name; + if (nameLink) nameLink.textContent = data.player.name; - const ratingValue = ratingCell.querySelector('.rating-value'); + const ratingValue = ratingCell ? ratingCell.querySelector('.rating-value') : null; if (ratingValue) { ratingValue.textContent = data.player.rating || 'N/A'; ratingValue.dataset.rating = data.player.rating || ''; @@ -150,24 +166,20 @@ async function refreshPlayer(pdgaNumber) { } } - const deltaMonthPill = ratingCell.querySelector('.delta-pill'); - if (deltaMonthPill && data.player.ratingChange != null) { - const pill = renderDeltaPill(data.player.ratingChange); - deltaMonthPill.textContent = pill.text; - deltaMonthPill.className = `delta-pill ${pill.cls}`; - } + const deltaMonthPill = ratingCell ? ratingCell.querySelector('.delta-pill') : null; + applyDeltaPill(deltaMonthPill, data.player.ratingChange); } } catch (error) { console.error('Error refreshing player:', error); alert('Failed to refresh player data'); } finally { - icon.classList.remove('spinning'); + if (icon) icon.classList.remove('spinning'); } } async function refreshRoundHistory(pdgaNumber) { - const icon = document.querySelector(`#predicted-${pdgaNumber} .refresh-icon`); - icon.classList.add('spinning'); + const icon = document.querySelector(`#row-${pdgaNumber} .cell-actions .refresh-icon`); + if (icon) icon.classList.add('spinning'); try { const response = await fetch(`/api/refresh-round-history/${pdgaNumber}`, { method: 'POST' }); @@ -197,8 +209,8 @@ async function refreshRoundHistory(pdgaNumber) { } const row = document.getElementById(`row-${pdgaNumber}`); - const ratingCell = row.querySelector('.rating'); - const ratingValue = ratingCell.querySelector('.rating-value'); + const ratingCell = row.querySelector('.cell-rating'); + const ratingValue = ratingCell ? ratingCell.querySelector('.rating-value') : null; if (ratingValue && data.stdDev) { ratingValue.dataset.stddev = data.stdDev; @@ -244,7 +256,7 @@ async function refreshRoundHistory(pdgaNumber) { const fullMessage = errorDetails ? errorMessage + '\n\n' + errorDetails : errorMessage; alert(fullMessage); } finally { - icon.classList.remove('spinning'); + if (icon) icon.classList.remove('spinning'); } } @@ -307,7 +319,8 @@ function closeDebugModal(event) { document.getElementById('debug-modal').style.display = 'none'; } -async function searchAndAddPlayer() { +async function searchAndAddPlayer(event) { + if (event) event.preventDefault(); const input = document.getElementById('pdga-number-input'); const pdgaNumber = input.value.trim(); diff --git a/views/pages/index.ejs b/views/pages/index.ejs index 1c4898b..c600e18 100644 --- a/views/pages/index.ejs +++ b/views/pages/index.ejs @@ -1,21 +1,27 @@ <% var body = ` - -
Unofficial PDGA rating tracker. Ratings scraped from pdga.com on each refresh.
`; %> <% var modals = ` @@ -100,4 +112,4 @@ initScript: 'setupTooltipsAfterSwap();', body: body, modals: modals -}) %> \ No newline at end of file +}) %> diff --git a/views/partials/delta-pill.ejs b/views/partials/delta-pill.ejs index 819e94e..4fb7cf9 100644 --- a/views/partials/delta-pill.ejs +++ b/views/partials/delta-pill.ejs @@ -4,7 +4,8 @@ extraClass {string} — optional additional CSS class (e.g. 'delta-predicted-pill') */%> <% if (typeof value !== 'undefined' && value != null) { - const _cls = value > 0 ? 'up' : value < 0 ? 'down' : 'flat'; - const _text = value > 0 ? '+' + value : value.toString(); - const _xtra = (typeof extraClass !== 'undefined' && extraClass) ? ' ' + extraClass : ''; -%><%= _text %><% } %> + const _cls = value > 0 ? 'up' : value < 0 ? 'down' : 'flat'; + const _glyph = value > 0 ? '▲' : value < 0 ? '▼' : '–'; + const _num = value > 0 ? '+' + value : value.toString(); + const _xtra = (typeof extraClass !== 'undefined' && extraClass) ? ' ' + extraClass : ''; +%><%= _glyph %><%= _num %><% } %> diff --git a/views/partials/layout.ejs b/views/partials/layout.ejs index 78edd66..c759746 100644 --- a/views/partials/layout.ejs +++ b/views/partials/layout.ejs @@ -7,7 +7,7 @@ - + <% if (typeof cssFiles !== 'undefined') { %> diff --git a/views/partials/ratings-table.ejs b/views/partials/ratings-table.ejs index e8df398..0ebbacd 100644 --- a/views/partials/ratings-table.ejs +++ b/views/partials/ratings-table.ejs @@ -21,7 +21,7 @@ function renderSparkline(values) { var last = pts[pts.length - 1]; var areaPath = linePath + ' L ' + last.x + ' ' + h + ' L 0 ' + h + ' Z'; - return '