Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5198a1c0f4 | |||
| 7297c0a16b | |||
| ada2dcb4ae | |||
| 5ece854340 | |||
| 2ef7de4e58 | |||
| 16c045e7cc | |||
| 8ee5cc3861 | |||
| 2561ee12ef |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pdga-ratings",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pdga-ratings",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.2",
|
||||
"dependencies": {
|
||||
"ejs": "^4.0.1",
|
||||
"express": "^4.18.2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pdga-ratings",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.2",
|
||||
"description": "PDGA rating scraper and display",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -342,6 +342,38 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Refresh button: hidden by default, revealed only when the card is open.
|
||||
Larger than the desktop icon to give a comfortable touch target (≥44px). */
|
||||
.m-card__head .m-refresh-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m-card.is-open .m-card__head .m-refresh-icon {
|
||||
display: grid;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-left: 0;
|
||||
font-size: 15px;
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.m-card.is-open .m-card__head .m-refresh-icon:active {
|
||||
opacity: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Spin only the icon glyph, not the 44px button box — otherwise the button's
|
||||
lingering touch-hover frame (background + border) rotates too, which looks odd. */
|
||||
.m-card.is-open .m-card__head .m-refresh-icon.spinning {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.m-card.is-open .m-card__head .m-refresh-icon.spinning i {
|
||||
display: inline-block;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.m-card__body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
|
||||
+10
-4
@@ -131,10 +131,16 @@ async function clearCache() {
|
||||
// Refreshes both the current rating and the prediction in one click, then
|
||||
// re-swaps the table so every derived value (deltas, pills, sparkline) reflects
|
||||
// the new state. Cheaper than fine-grained DOM updates and guaranteed consistent
|
||||
// because the server renders the truth.
|
||||
// because the server renders the truth. The mobile cards partial is included
|
||||
// inside ratings-table, so swapping #ratings-table re-renders both views at once.
|
||||
async function refreshPlayerData(pdgaNumber) {
|
||||
const icon = document.querySelector(`#row-${pdgaNumber} .cell-actions .refresh-icon`);
|
||||
if (icon) icon.classList.add('spinning');
|
||||
// The desktop row exists in the DOM even on mobile (hidden via CSS), so spin
|
||||
// both possible icons; only the one visible in the active viewport is seen.
|
||||
const icons = [
|
||||
document.querySelector(`#row-${pdgaNumber} .cell-actions .refresh-icon`),
|
||||
document.querySelector(`#m-card-${pdgaNumber} .m-refresh-icon`)
|
||||
].filter(Boolean);
|
||||
icons.forEach(icon => icon.classList.add('spinning'));
|
||||
try {
|
||||
await Promise.allSettled([
|
||||
fetch(`/api/refresh-player/${pdgaNumber}`, { method: 'POST' }),
|
||||
@@ -144,7 +150,7 @@ async function refreshPlayerData(pdgaNumber) {
|
||||
} catch (error) {
|
||||
console.error('Error refreshing player data:', error);
|
||||
} finally {
|
||||
if (icon) icon.classList.remove('spinning');
|
||||
icons.forEach(icon => icon.classList.remove('spinning'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-4
@@ -17,12 +17,27 @@ function getPlayerFromDB(pdgaNumber) {
|
||||
function savePlayerToDB(playerData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO players (pdga_number, name, current_rating, rating_change, last_updated)
|
||||
VALUES (?, ?, ?, ?, datetime('now'))`,
|
||||
// UPSERT (not INSERT OR REPLACE): updating in place preserves columns not
|
||||
// listed here — predicted_rating, std_dev, last_round_update,
|
||||
// excluded_rounds_count, cutoff_rating. INSERT OR REPLACE would delete the
|
||||
// existing row and reset those to their DEFAULT (NULL).
|
||||
`INSERT INTO players (pdga_number, name, current_rating, rating_change, last_updated)
|
||||
VALUES (?, ?, ?, ?, datetime('now'))
|
||||
ON CONFLICT(pdga_number) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
current_rating = excluded.current_rating,
|
||||
rating_change = excluded.rating_change,
|
||||
last_updated = excluded.last_updated`,
|
||||
[playerData.pdgaNumber, playerData.name, playerData.rating, playerData.ratingChange],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve(this.lastID);
|
||||
if (err) return reject(err);
|
||||
// node-sqlite3 leaves lastID = 0 when ON CONFLICT triggers an UPDATE.
|
||||
// Fall back to a SELECT to get the real id in that case.
|
||||
if (this.lastID !== 0) return resolve(this.lastID);
|
||||
db.get('SELECT id FROM players WHERE pdga_number = ?', [playerData.pdgaNumber], (err2, row) => {
|
||||
if (err2) reject(err2);
|
||||
else resolve(row ? row.id : 0);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -65,6 +65,11 @@ function renderSparkline(values, opts) {
|
||||
<span class="m-player-name"><%= player.name %></span>
|
||||
<span class="m-pdga-num">#<%= player.pdgaNumber %></span>
|
||||
</div>
|
||||
<button class="icon-btn refresh-icon m-refresh-icon" type="button"
|
||||
onclick="event.stopPropagation(); refreshPlayerData(<%= player.pdgaNumber %>)"
|
||||
title="Refresh rating + prediction" aria-label="Refresh rating and prediction">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<span class="m-chevron">▼</span>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user