fix: address mobile UI review findings (#16)
- Hide desktop .card-section on mobile, add .m-search-input with same HTMX attrs for mobile course search (fixes horizontal overflow) - Remove dead layoutCount var and .m-layouts-pill block in course-cards - Remove dead 768px breakpoints from players.css (table hidden at 880px) - Move .mobile-section-head inside else-block for empty state in both ratings-cards and course-cards (fixes section head showing on empty) - Add tabindex, role=button, aria-expanded, onkeydown to .m-card and .m-course-card; toggle aria-expanded in JS toggle functions - Fix data-history attribute to use <%= (HTML-escaped) instead of <%- - Convert var to const/let in all new/changed JS blocks
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
.mobile-add-bar { display: none; }
|
.mobile-add-bar { display: none; }
|
||||||
.mobile-section-head { display: none; }
|
.mobile-section-head { display: none; }
|
||||||
.m-tab-pill { display: none; }
|
.m-tab-pill { display: none; }
|
||||||
|
.m-search-input { display: none; }
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════ */
|
/* ═══════════════════════════════════════════════════ */
|
||||||
@media (max-width: 880px) {
|
@media (max-width: 880px) {
|
||||||
@@ -24,6 +25,37 @@
|
|||||||
.add-bar { display: none !important; }
|
.add-bar { display: none !important; }
|
||||||
.footnote { display: none !important; }
|
.footnote { display: none !important; }
|
||||||
|
|
||||||
|
/* Hide desktop search card on mobile (mobile has .m-search-input instead) */
|
||||||
|
.card-section { display: none; }
|
||||||
|
|
||||||
|
/* ── Mobile course search input ─────────────────── */
|
||||||
|
|
||||||
|
.m-search-input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: var(--paper-2);
|
||||||
|
border: 1px solid var(--line-2);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ink);
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-search-input::placeholder {
|
||||||
|
color: var(--ink-3);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-search-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 14%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide desktop table but keep .table-card as wrapper */
|
/* Hide desktop table but keep .table-card as wrapper */
|
||||||
.table-card > #ratings-table table,
|
.table-card > #ratings-table table,
|
||||||
#ratings-table table,
|
#ratings-table table,
|
||||||
|
|||||||
@@ -10,16 +10,6 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.mobile-only {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-name {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Rating values ────────────────────────────── */
|
/* ── Rating values ────────────────────────────── */
|
||||||
|
|
||||||
.rating-value {
|
.rating-value {
|
||||||
@@ -286,19 +276,6 @@
|
|||||||
background: #059669;
|
background: #059669;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Responsive ───────────────────────────────── */
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.chart-container {
|
|
||||||
height: 250px;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-title {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Target Rating Calculator ─────────────────── */
|
/* ── Target Rating Calculator ─────────────────── */
|
||||||
|
|
||||||
.target-rating-icon {
|
.target-rating-icon {
|
||||||
|
|||||||
+11
-6
@@ -31,32 +31,37 @@ function toggleCourseLayouts(courseId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Mobile course card toggle ──────────────────────
|
// ── Mobile course card toggle ──────────────────────
|
||||||
var openMobileCourseId = null;
|
let openMobileCourseId = null;
|
||||||
|
|
||||||
function toggleMobileCourseLayouts(courseId) {
|
function toggleMobileCourseLayouts(courseId) {
|
||||||
var card = document.getElementById('m-course-' + courseId);
|
const card = document.getElementById('m-course-' + courseId);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
|
|
||||||
var isOpen = card.classList.contains('is-open');
|
const isOpen = card.classList.contains('is-open');
|
||||||
|
|
||||||
// Close previously open card
|
// Close previously open card
|
||||||
if (openMobileCourseId !== null && openMobileCourseId !== courseId) {
|
if (openMobileCourseId !== null && openMobileCourseId !== courseId) {
|
||||||
var prevCard = document.getElementById('m-course-' + openMobileCourseId);
|
const prevCard = document.getElementById('m-course-' + openMobileCourseId);
|
||||||
if (prevCard) prevCard.classList.remove('is-open');
|
if (prevCard) {
|
||||||
|
prevCard.classList.remove('is-open');
|
||||||
|
prevCard.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
openMobileCourseId = null;
|
openMobileCourseId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
card.classList.remove('is-open');
|
card.classList.remove('is-open');
|
||||||
|
card.setAttribute('aria-expanded', 'false');
|
||||||
openMobileCourseId = null;
|
openMobileCourseId = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
card.classList.add('is-open');
|
card.classList.add('is-open');
|
||||||
|
card.setAttribute('aria-expanded', 'true');
|
||||||
openMobileCourseId = courseId;
|
openMobileCourseId = courseId;
|
||||||
|
|
||||||
// Lazy-load layouts on first expand
|
// Lazy-load layouts on first expand
|
||||||
var container = document.getElementById('m-layouts-container-' + courseId);
|
const container = document.getElementById('m-layouts-container-' + courseId);
|
||||||
if (container && container.dataset.loaded !== 'true') {
|
if (container && container.dataset.loaded !== 'true') {
|
||||||
htmx.ajax('GET', '/partials/course-layouts/' + courseId, { target: '#m-layouts-container-' + courseId, swap: 'innerHTML' });
|
htmx.ajax('GET', '/partials/course-layouts/' + courseId, { target: '#m-layouts-container-' + courseId, swap: 'innerHTML' });
|
||||||
container.dataset.loaded = 'true';
|
container.dataset.loaded = 'true';
|
||||||
|
|||||||
+18
-13
@@ -31,8 +31,8 @@ function initChartsIn(rootEl) {
|
|||||||
if (container.dataset.charted === 'true') return;
|
if (container.dataset.charted === 'true') return;
|
||||||
if (!container.dataset.history) return;
|
if (!container.dataset.history) return;
|
||||||
try {
|
try {
|
||||||
var history = JSON.parse(container.dataset.history);
|
const history = JSON.parse(container.dataset.history);
|
||||||
var isMobile = container.dataset.variant === 'mobile';
|
const isMobile = container.dataset.variant === 'mobile';
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
createRatingChart(container, history, {
|
createRatingChart(container, history, {
|
||||||
w: 360,
|
w: 360,
|
||||||
@@ -782,32 +782,37 @@ async function refreshHistoryThenCalculate(pdgaNumber) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Mobile player card toggle ──────────────────────
|
// ── Mobile player card toggle ──────────────────────
|
||||||
var openMobilePdgaNumber = null;
|
let openMobilePdgaNumber = null;
|
||||||
|
|
||||||
function toggleMobilePlayerCard(pdgaNumber) {
|
function toggleMobilePlayerCard(pdgaNumber) {
|
||||||
var card = document.getElementById('m-card-' + pdgaNumber);
|
const card = document.getElementById('m-card-' + pdgaNumber);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
|
|
||||||
var isOpen = card.classList.contains('is-open');
|
const isOpen = card.classList.contains('is-open');
|
||||||
|
|
||||||
// Close previously open card
|
// Close previously open card
|
||||||
if (openMobilePdgaNumber !== null && openMobilePdgaNumber !== pdgaNumber) {
|
if (openMobilePdgaNumber !== null && openMobilePdgaNumber !== pdgaNumber) {
|
||||||
var prevCard = document.getElementById('m-card-' + openMobilePdgaNumber);
|
const prevCard = document.getElementById('m-card-' + openMobilePdgaNumber);
|
||||||
if (prevCard) prevCard.classList.remove('is-open');
|
if (prevCard) {
|
||||||
|
prevCard.classList.remove('is-open');
|
||||||
|
prevCard.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
openMobilePdgaNumber = null;
|
openMobilePdgaNumber = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
card.classList.remove('is-open');
|
card.classList.remove('is-open');
|
||||||
|
card.setAttribute('aria-expanded', 'false');
|
||||||
openMobilePdgaNumber = null;
|
openMobilePdgaNumber = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
card.classList.add('is-open');
|
card.classList.add('is-open');
|
||||||
|
card.setAttribute('aria-expanded', 'true');
|
||||||
openMobilePdgaNumber = pdgaNumber;
|
openMobilePdgaNumber = pdgaNumber;
|
||||||
|
|
||||||
// Init charts inside the expand panel
|
// Init charts inside the expand panel
|
||||||
var expand = card.querySelector('.m-card__expand');
|
const expand = card.querySelector('.m-card__expand');
|
||||||
if (expand) {
|
if (expand) {
|
||||||
initChartsIn(expand);
|
initChartsIn(expand);
|
||||||
}
|
}
|
||||||
@@ -816,20 +821,20 @@ function toggleMobilePlayerCard(pdgaNumber) {
|
|||||||
// ── Mobile add player ──────────────────────────────
|
// ── Mobile add player ──────────────────────────────
|
||||||
async function searchAndAddPlayerMobile(event) {
|
async function searchAndAddPlayerMobile(event) {
|
||||||
if (event) event.preventDefault();
|
if (event) event.preventDefault();
|
||||||
var input = document.getElementById('pdga-number-input-mobile');
|
const input = document.getElementById('pdga-number-input-mobile');
|
||||||
var pdgaNumber = input ? input.value.trim() : '';
|
const pdgaNumber = input ? input.value.trim() : '';
|
||||||
|
|
||||||
if (!pdgaNumber) {
|
if (!pdgaNumber) {
|
||||||
alert('Please enter a PDGA number');
|
alert('Please enter a PDGA number');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var button = event && event.target ? event.target.querySelector('button[type="submit"]') : null;
|
const button = event && event.target ? event.target.querySelector('button[type="submit"]') : null;
|
||||||
if (button) { button.disabled = true; button.textContent = 'Searching...'; }
|
if (button) { button.disabled = true; button.textContent = 'Searching...'; }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var response = await fetch('/api/search-player/' + pdgaNumber);
|
const response = await fetch('/api/search-player/' + pdgaNumber);
|
||||||
var data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
showErrorModal(data.error || 'Player not found');
|
showErrorModal(data.error || 'Player not found');
|
||||||
|
|||||||
@@ -25,6 +25,18 @@
|
|||||||
<button class="m-tab-pill__btn m-tab-pill__btn--disabled" type="button" disabled>Import from Tjing</button>
|
<button class="m-tab-pill__btn m-tab-pill__btn--disabled" type="button" disabled>Import from Tjing</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile search input (hidden on desktop, shown on mobile via CSS) -->
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="m-search-input"
|
||||||
|
id="course-search-mobile"
|
||||||
|
name="q"
|
||||||
|
placeholder="Search courses by name or city..."
|
||||||
|
hx-get="/partials/course-table"
|
||||||
|
hx-trigger="input changed delay:300ms, search"
|
||||||
|
hx-target="#courses-table"
|
||||||
|
/>
|
||||||
|
|
||||||
<div id="courses-table" hx-get="/partials/course-table" hx-trigger="load"></div>
|
<div id="courses-table" hx-get="/partials/course-table" hx-trigger="load"></div>
|
||||||
`; %>
|
`; %>
|
||||||
|
|
||||||
|
|||||||
@@ -3,27 +3,25 @@ var _query = (typeof query !== 'undefined') ? query : null;
|
|||||||
var _total = (typeof total !== 'undefined') ? total : courses.length;
|
var _total = (typeof total !== 'undefined') ? total : courses.length;
|
||||||
%>
|
%>
|
||||||
|
|
||||||
|
<% if (courses.length === 0) { %>
|
||||||
|
<p style="text-align: center; color: var(--ink-3); padding: 40px 0;">No courses found.</p>
|
||||||
|
<% } else { %>
|
||||||
<div class="mobile-section-head">
|
<div class="mobile-section-head">
|
||||||
<span class="kicker">Showing <%= courses.length %> of <%= _total %></span>
|
<span class="kicker">Showing <%= courses.length %> of <%= _total %></span>
|
||||||
<a href="/courses">View all →</a>
|
<a href="/courses">View all →</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if (courses.length === 0) { %>
|
|
||||||
<p style="text-align: center; color: var(--ink-3); padding: 40px 0;">No courses found.</p>
|
|
||||||
<% } else { %>
|
|
||||||
<div class="mobile-list">
|
<div class="mobile-list">
|
||||||
<% courses.forEach(function(course) {
|
<% courses.forEach(function(course) {
|
||||||
var lastUpdated = new Date(course.last_updated).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
var lastUpdated = new Date(course.last_updated).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
var layoutCount = course.layoutCount || 0;
|
|
||||||
%>
|
%>
|
||||||
<div class="m-course-card" id="m-course-<%= course.id %>" onclick="toggleMobileCourseLayouts(<%= course.id %>)">
|
<div class="m-course-card" id="m-course-<%= course.id %>"
|
||||||
|
tabindex="0" role="button" aria-expanded="false" aria-label="Expand course details"
|
||||||
|
onclick="toggleMobileCourseLayouts(<%= course.id %>)"
|
||||||
|
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleMobileCourseLayouts(<%= course.id %>);}">
|
||||||
<div class="m-course-card__head">
|
<div class="m-course-card__head">
|
||||||
<div class="m-course-card__name-stack">
|
<div class="m-course-card__name-stack">
|
||||||
<div class="m-course-name-row">
|
<div class="m-course-name-row">
|
||||||
<span class="m-course-name"><%= course.name %></span>
|
<span class="m-course-name"><%= course.name %></span>
|
||||||
<% if (layoutCount > 0) { %>
|
|
||||||
<span class="m-layouts-pill"><%= layoutCount %> layout<%= layoutCount !== 1 ? 's' : '' %></span>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="m-course-card__meta"><%= course.city %> · <%= lastUpdated %></div>
|
<div class="m-course-card__meta"><%= course.city %> · <%= lastUpdated %></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,14 +32,13 @@ function renderSparkline(values, opts) {
|
|||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
|
|
||||||
|
<% if (ratings.length === 0) { %>
|
||||||
|
<p style="text-align: center; color: var(--ink-3); padding: 40px 0;">No players tracked yet.</p>
|
||||||
|
<% } else { %>
|
||||||
<div class="mobile-section-head">
|
<div class="mobile-section-head">
|
||||||
<span class="kicker">TRACKED PLAYERS · <%= ratings.length %></span>
|
<span class="kicker">TRACKED PLAYERS · <%= ratings.length %></span>
|
||||||
<button id="trendchart-toggle-mobile" class="pill-button" type="button" aria-pressed="false">Trend chart</button>
|
<button id="trendchart-toggle-mobile" class="pill-button" type="button" aria-pressed="false">Trend chart</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if (ratings.length === 0) { %>
|
|
||||||
<p style="text-align: center; color: var(--ink-3); padding: 40px 0;">No players tracked yet.</p>
|
|
||||||
<% } else { %>
|
|
||||||
<div class="mobile-list">
|
<div class="mobile-list">
|
||||||
<% ratings.forEach(function(player, index) {
|
<% ratings.forEach(function(player, index) {
|
||||||
var sparkSvg = renderSparkline(player.monthlyHistory || [], { w: 70, h: 26 });
|
var sparkSvg = renderSparkline(player.monthlyHistory || [], { w: 70, h: 26 });
|
||||||
@@ -56,7 +55,10 @@ function renderSparkline(values, opts) {
|
|||||||
var predGlyph = (predIsNull || player.deltaPredicted === 0) ? '–' : (player.deltaPredicted > 0 ? '▲' : '▼');
|
var predGlyph = (predIsNull || player.deltaPredicted === 0) ? '–' : (player.deltaPredicted > 0 ? '▲' : '▼');
|
||||||
var predNum = predIsNull ? '—' : (player.deltaPredicted > 0 ? '+' + player.deltaPredicted : String(player.deltaPredicted));
|
var predNum = predIsNull ? '—' : (player.deltaPredicted > 0 ? '+' + player.deltaPredicted : String(player.deltaPredicted));
|
||||||
%>
|
%>
|
||||||
<div class="m-card" id="m-card-<%= player.pdgaNumber %>" onclick="toggleMobilePlayerCard(<%= player.pdgaNumber %>)">
|
<div class="m-card" id="m-card-<%= player.pdgaNumber %>"
|
||||||
|
tabindex="0" role="button" aria-expanded="false" aria-label="Expand player details"
|
||||||
|
onclick="toggleMobilePlayerCard(<%= player.pdgaNumber %>)"
|
||||||
|
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleMobilePlayerCard(<%= player.pdgaNumber %>);}">
|
||||||
<div class="m-card__head">
|
<div class="m-card__head">
|
||||||
<div class="m-rank-chip<%= isFirst ? ' m-rank-chip--first' : '' %>"><%= rank %></div>
|
<div class="m-rank-chip<%= isFirst ? ' m-rank-chip--first' : '' %>"><%= rank %></div>
|
||||||
<div class="m-card__name-stack">
|
<div class="m-card__name-stack">
|
||||||
@@ -88,7 +90,7 @@ function renderSparkline(values, opts) {
|
|||||||
<% if (player.ratingHistory && player.ratingHistory.length > 0) { %>
|
<% if (player.ratingHistory && player.ratingHistory.length > 0) { %>
|
||||||
<div class="player-chart m-chart"
|
<div class="player-chart m-chart"
|
||||||
data-variant="mobile"
|
data-variant="mobile"
|
||||||
data-history='<%- JSON.stringify(player.ratingHistory) %>'>
|
data-history="<%= JSON.stringify(player.ratingHistory) %>">
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<dl class="m-detail-grid">
|
<dl class="m-detail-grid">
|
||||||
|
|||||||
Reference in New Issue
Block a user