feat: add refresh button to mobile player card (#26) #27

Merged
shcizo merged 2 commits from feat/mobile-card-refresh-button-26 into main 2026-06-08 08:46:15 +02:00
3 changed files with 47 additions and 4 deletions
+32
View File
@@ -342,6 +342,38 @@
transform: rotate(180deg); 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 { .m-card__body {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
+10 -4
View File
@@ -131,10 +131,16 @@ async function clearCache() {
// Refreshes both the current rating and the prediction in one click, then // Refreshes both the current rating and the prediction in one click, then
// re-swaps the table so every derived value (deltas, pills, sparkline) reflects // re-swaps the table so every derived value (deltas, pills, sparkline) reflects
// the new state. Cheaper than fine-grained DOM updates and guaranteed consistent // 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) { async function refreshPlayerData(pdgaNumber) {
const icon = document.querySelector(`#row-${pdgaNumber} .cell-actions .refresh-icon`); // The desktop row exists in the DOM even on mobile (hidden via CSS), so spin
if (icon) icon.classList.add('spinning'); // 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 { try {
await Promise.allSettled([ await Promise.allSettled([
fetch(`/api/refresh-player/${pdgaNumber}`, { method: 'POST' }), fetch(`/api/refresh-player/${pdgaNumber}`, { method: 'POST' }),
@@ -144,7 +150,7 @@ async function refreshPlayerData(pdgaNumber) {
} catch (error) { } catch (error) {
console.error('Error refreshing player data:', error); console.error('Error refreshing player data:', error);
} finally { } finally {
if (icon) icon.classList.remove('spinning'); icons.forEach(icon => icon.classList.remove('spinning'));
} }
} }
+5
View File
@@ -65,6 +65,11 @@ function renderSparkline(values, opts) {
<span class="m-player-name"><%= player.name %></span> <span class="m-player-name"><%= player.name %></span>
<span class="m-pdga-num">#<%= player.pdgaNumber %></span> <span class="m-pdga-num">#<%= player.pdgaNumber %></span>
</div> </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">&#9660;</span> <span class="m-chevron">&#9660;</span>
</div> </div>