fix: address code review for visual layer + topbar (#4)
This commit is contained in:
@@ -72,6 +72,9 @@
|
|||||||
--shadow-overlay: 0 20px 60px rgba(15, 23, 42, 0.15), 0 4px 12px rgba(15, 23, 42, 0.08);
|
--shadow-overlay: 0 20px 60px rgba(15, 23, 42, 0.15), 0 4px 12px rgba(15, 23, 42, 0.08);
|
||||||
|
|
||||||
--transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
--transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
/* Topbar dimensions (used by sticky thead) */
|
||||||
|
--topbar-height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Reset & Base ─────────────────────────────── */
|
/* ── Reset & Base ─────────────────────────────── */
|
||||||
@@ -279,6 +282,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 880px) {
|
@media (max-width: 880px) {
|
||||||
|
:root { --topbar-height: 56px; }
|
||||||
.topbar__inner { padding: 10px 16px; gap: 10px; }
|
.topbar__inner { padding: 10px 16px; gap: 10px; }
|
||||||
.topbar__meta-item, .topbar__divider { display: none; }
|
.topbar__meta-item, .topbar__divider { display: none; }
|
||||||
.topbar__refresh-label { display: none; }
|
.topbar__refresh-label { display: none; }
|
||||||
@@ -322,7 +326,7 @@ table {
|
|||||||
|
|
||||||
thead {
|
thead {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 56px;
|
top: var(--topbar-height);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,7 +551,7 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
top: 56px;
|
top: var(--topbar-height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ app.set('view engine', 'ejs');
|
|||||||
app.set('views', path.join(__dirname, 'views/pages'));
|
app.set('views', path.join(__dirname, 'views/pages'));
|
||||||
app.use(express.static('public'));
|
app.use(express.static('public'));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: false }));
|
||||||
|
|
||||||
app.use(playerRoutes);
|
app.use(playerRoutes);
|
||||||
app.use(courseRoutes);
|
app.use(courseRoutes);
|
||||||
|
|||||||
+11
-9
@@ -185,15 +185,17 @@ function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLastRefresh(callback) {
|
function getLastRefresh() {
|
||||||
db.get(
|
return new Promise((resolve, reject) => {
|
||||||
'SELECT MAX(last_updated) AS lastRefresh FROM players',
|
db.get(
|
||||||
[],
|
'SELECT MAX(last_updated) AS lastRefresh FROM players',
|
||||||
(err, row) => {
|
[],
|
||||||
if (err) return callback(err);
|
(err, row) => {
|
||||||
callback(null, row ? row.lastRefresh : null);
|
if (err) reject(err);
|
||||||
}
|
else resolve(row ? row.lastRefresh : null);
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
+10
-5
@@ -12,21 +12,26 @@ const logger = require('../logger');
|
|||||||
|
|
||||||
let refreshInProgress = false;
|
let refreshInProgress = false;
|
||||||
|
|
||||||
router.post('/api/refresh-all', async (req, res) => {
|
router.post('/api/refresh-all', async (req, res, next) => {
|
||||||
if (refreshInProgress) {
|
if (refreshInProgress) {
|
||||||
logger.info('refresh-all already in progress, rejecting');
|
logger.info('refresh-all already in progress, rejecting');
|
||||||
return res.status(409).json({ error: 'Refresh already in progress' });
|
return res.status(409).json({ error: 'Refresh already in progress' });
|
||||||
}
|
}
|
||||||
refreshInProgress = true;
|
refreshInProgress = true;
|
||||||
try {
|
try {
|
||||||
await refreshAllPlayersInDB();
|
try {
|
||||||
|
await refreshAllPlayersInDB();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, 'refresh-all failed');
|
||||||
|
}
|
||||||
|
const page = req.body?.page === 'courses' ? 'courses' : 'players';
|
||||||
|
const locals = await getTopbarLocals();
|
||||||
|
res.render('../partials/topbar', { activePage: page, ...locals });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, 'refresh-all failed');
|
next(err);
|
||||||
} finally {
|
} finally {
|
||||||
refreshInProgress = false;
|
refreshInProgress = false;
|
||||||
}
|
}
|
||||||
const locals = await getTopbarLocals();
|
|
||||||
res.render('../partials/topbar', { activePage: req.query.page || 'players', ...locals });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/partials/ratings-table', async (req, res) => {
|
router.get('/partials/ratings-table', async (req, res) => {
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ function computeNextUpdate(now = new Date()) {
|
|||||||
|
|
||||||
async function getTopbarLocals() {
|
async function getTopbarLocals() {
|
||||||
try {
|
try {
|
||||||
const lastIso = await new Promise((resolve, reject) => {
|
const lastIso = await getLastRefresh();
|
||||||
getLastRefresh((err, val) => (err ? reject(err) : resolve(val)));
|
|
||||||
});
|
|
||||||
return { lastRefresh: formatRelative(lastIso), nextUpdate: computeNextUpdate() };
|
return { lastRefresh: formatRelative(lastIso), nextUpdate: computeNextUpdate() };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn({ err }, 'topbar locals fallback');
|
logger.warn({ err }, 'topbar locals fallback');
|
||||||
@@ -42,4 +40,4 @@ async function getTopbarLocals() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getTopbarLocals, formatRelative, computeNextUpdate };
|
module.exports = { getTopbarLocals };
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
|
|
||||||
<%- include('../partials/layout', {
|
<%- include('../partials/layout', {
|
||||||
title: 'PDGA Courses - Sweden',
|
title: 'PDGA Courses - Sweden',
|
||||||
heading: 'PDGA Courses - Sweden',
|
|
||||||
activePage: 'courses',
|
activePage: 'courses',
|
||||||
cssFiles: ['courses.css'],
|
cssFiles: ['courses.css'],
|
||||||
jsFiles: ['courses.js'],
|
jsFiles: ['courses.js'],
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
|
|
||||||
<%- include('../partials/layout', {
|
<%- include('../partials/layout', {
|
||||||
title: 'PDGA Ratings',
|
title: 'PDGA Ratings',
|
||||||
heading: 'PDGA Player Ratings',
|
|
||||||
activePage: 'players',
|
activePage: 'players',
|
||||||
cssFiles: ['players.css'],
|
cssFiles: ['players.css'],
|
||||||
jsFiles: ['tooltips.js', 'chart.js', 'progress.js', 'players.js'],
|
jsFiles: ['tooltips.js', 'chart.js', 'progress.js', 'players.js'],
|
||||||
|
|||||||
+41
-40
@@ -1,45 +1,46 @@
|
|||||||
<header class="topbar" id="topbar">
|
<header class="topbar" id="topbar">
|
||||||
<div class="topbar__inner">
|
<div class="topbar__inner">
|
||||||
<a href="/" class="topbar__brand">
|
<a href="/" class="topbar__brand">
|
||||||
<span class="topbar__brand-mark" aria-hidden="true">
|
<span class="topbar__brand-mark" aria-hidden="true">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M3 17 L9 11 L13 15 L21 6" />
|
<path d="M3 17 L9 11 L13 15 L21 6" />
|
||||||
<path d="M14 6 L21 6 L21 13" />
|
<path d="M14 6 L21 6 L21 13" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="topbar__brand-text">
|
<span class="topbar__brand-text">
|
||||||
<span class="topbar__brand-title">Rating Tracker</span>
|
<span class="topbar__brand-title">Rating Tracker</span>
|
||||||
<span class="topbar__brand-sub">Disc golf · unofficial</span>
|
<span class="topbar__brand-sub">Disc golf · unofficial</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav class="topbar__nav" aria-label="Primary">
|
<nav class="topbar__nav" aria-label="Primary">
|
||||||
<a href="/" class="<%= activePage === 'players' ? 'active' : '' %>">Players</a>
|
<a href="/" class="<%= activePage === 'players' ? 'active' : '' %>">Players</a>
|
||||||
<a href="/courses" class="<%= activePage === 'courses' ? 'active' : '' %>">Courses</a>
|
<a href="/courses" class="<%= activePage === 'courses' ? 'active' : '' %>">Courses</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="topbar__meta">
|
<div class="topbar__meta">
|
||||||
<div class="topbar__meta-item">
|
<div class="topbar__meta-item">
|
||||||
<span class="topbar__meta-label">Next update</span>
|
<span class="topbar__meta-label">Next update</span>
|
||||||
<span class="topbar__meta-value"><%= nextUpdate %></span>
|
<span class="topbar__meta-value"><%= nextUpdate %></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar__meta-item">
|
<div class="topbar__meta-item">
|
||||||
<span class="topbar__meta-label">Last refresh</span>
|
<span class="topbar__meta-label">Last refresh</span>
|
||||||
<span class="topbar__meta-value"><%= lastRefresh %></span>
|
<span class="topbar__meta-value"><%= lastRefresh %></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="topbar__divider" aria-hidden="true"></span>
|
<span class="topbar__divider" aria-hidden="true"></span>
|
||||||
<button
|
<button
|
||||||
class="topbar__refresh"
|
class="topbar__refresh"
|
||||||
type="button"
|
type="button"
|
||||||
hx-post="/api/refresh-all"
|
hx-post="/api/refresh-all"
|
||||||
hx-target="#topbar"
|
hx-vals='{"page": "<%= activePage %>"}'
|
||||||
hx-swap="outerHTML"
|
hx-target="#topbar"
|
||||||
hx-disabled-elt="this"
|
hx-swap="outerHTML"
|
||||||
>
|
hx-disabled-elt="this"
|
||||||
<span class="topbar__refresh-icon" aria-hidden="true">↻</span>
|
>
|
||||||
<span class="topbar__refresh-spinner" aria-hidden="true"></span>
|
<span class="topbar__refresh-icon" aria-hidden="true">↻</span>
|
||||||
<span class="topbar__refresh-label">Refresh all</span>
|
<span class="topbar__refresh-spinner" aria-hidden="true"></span>
|
||||||
</button>
|
<span class="topbar__refresh-label">Refresh all</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Reference in New Issue
Block a user