openvibers/openvibe.shoes Commits
Page links: the ends are muted text, not faded links (#35)
Changes
@@@ -90,7 +90,8 @@.sh-list { padding-left: 20px; line-height: 1.65; color: var(--text-secondary, #96a7c2); }.sh-code { overflow-x: auto; padding: 12px 14px; border-radius: 12px; background: var(--bg-secondary, #101828); border: 1px solid var(--border, rgba(255,255,255,.1)); }.sh-page-nav { display: flex; gap: 14px; align-items: center; justify-content: center; margin: 24px 0; }--.sh-page-nav .is-off { opacity: .4; pointer-events: none; }++/* The end of the list is plain text in the muted colour (AA on every theme), not a faded link to no page. */++.sh-page-nav .is-off { color: var(--text-muted, #7386a3); }.crumbs { font-size: 13px; color: var(--text-muted, #7386a3); margin: 10px 0 0; }.crumbs a { color: inherit; }@media (max-width: 900px) { .sh-search { grid-template-columns: 1fr 1fr; } }@@@ -307,7 +307,7 @@ function shoesIndex({ models = [], facets = {}, query = {}, page = 1, total = 0,<header class="sh-page-h"><p class="sh-kicker">Every shoe</p><h1>Search the catalogue</h1><p class="sh-lede">${total} model${total === 1 ? '' : 's'}${query.q ? ` for “${esc(query.q)}”` : ''}, each with every store's all-in price, the time it was checked and a stale flag.${facets.checked_at ? ` The catalogue was last read ${esc(checked(facets.checked_at, now))}.` : ''}</p></header>${searchForm({ q: query.q || '', facets, values: query })}${models.length ? `<ul class="sh-grid">${models.map((m) => `<li>${modelCard(m)}</li>`).join('')}</ul>` : `<div class="sh-empty"><h2>No shoe matches yet</h2><p>Try a brand, a model or a shorter word. The catalogue is filled by the Deals job; a model appears the first time a source names it.</p><p><a class="sh-btn" href="/shoes">Clear the filters</a></p></div>`}--${pages > 1 ? `<nav class="sh-page-nav" aria-label="Pages"><a${page <= 1 ? ' class="is-off"' : ''} href="/shoes?${esc(qs(page - 1))}">← Previous</a><span>Page ${page} of ${pages}</span><a${page >= pages ? ' class="is-off"' : ''} href="/shoes?${esc(qs(page + 1))}">Next →</a></nav>` : ''}++${pages > 1 ? `<nav class="sh-page-nav" aria-label="Pages">${page <= 1 ? '<span class="is-off" aria-disabled="true">← Previous</span>' : `<a href="/shoes?${esc(qs(page - 1))}" rel="prev">← Previous</a>`}<span>Page ${page} of ${pages}</span>${page >= pages ? '<span class="is-off" aria-disabled="true">Next →</span>' : `<a href="/shoes?${esc(qs(page + 1))}" rel="next">Next →</a>`}</nav>` : ''}<p class="sh-fine">A model with no price says so; a price that has not been checked in ${esc(facets.stale_hours || 48)} hours is marked stale. No affiliate links, ever.</p></div>`;return body;}@@@ -44,5 +44,14 @@ const views = require('../server/views');assert(bars.includes('−5.0%'), 'the comparison');});++ await check('the list\'s page links: the ends are plain text, not links to no page', () => {++ const first = views.shoesIndex({ page: 1, total: 60 });++ assert(first.includes('<span class="is-off" aria-disabled="true">← Previous</span>'), 'page 1 has no previous link');++ assert(/<a href="\/shoes\?[^"]*page=2[^"]*" rel="next">Next →<\/a>/.test(first), 'page 1 links to page 2');++ const last = views.shoesIndex({ page: 3, total: 60 });++ assert(last.includes('<span class="is-off" aria-disabled="true">Next →</span>'), 'the last page has no next link');++ assert(last.includes('rel="prev">← Previous</a>'), 'the last page links back');++ });++done();})();
@@ -90,7 +90,8 @@
@@ -90,7 +90,8 @@
.sh-list { padding-left: 20px; line-height: 1.65; color: var(--text-secondary, #96a7c2); }
.sh-list { padding-left: 20px; line-height: 1.65; color: var(--text-secondary, #96a7c2); }
.sh-code { overflow-x: auto; padding: 12px 14px; border-radius: 12px; background: var(--bg-secondary, #101828); border: 1px solid var(--border, rgba(255,255,255,.1)); }
.sh-code { overflow-x: auto; padding: 12px 14px; border-radius: 12px; background: var(--bg-secondary, #101828); border: 1px solid var(--border, rgba(255,255,255,.1)); }
.sh-page-nav { display: flex; gap: 14px; align-items: center; justify-content: center; margin: 24px 0; }
.sh-page-nav { display: flex; gap: 14px; align-items: center; justify-content: center; margin: 24px 0; }
-.sh-page-nav .is-off { opacity: .4; pointer-events: none; }
+/* The end of the list is plain text in the muted colour (AA on every theme), not a faded link to no page. */
+.sh-page-nav .is-off { color: var(--text-muted, #7386a3); }
.crumbs { font-size: 13px; color: var(--text-muted, #7386a3); margin: 10px 0 0; }
.crumbs { font-size: 13px; color: var(--text-muted, #7386a3); margin: 10px 0 0; }
.crumbs a { color: inherit; }
.crumbs a { color: inherit; }
@media (max-width: 900px) { .sh-search { grid-template-columns: 1fr 1fr; } }
@media (max-width: 900px) { .sh-search { grid-template-columns: 1fr 1fr; } }
@@ -307,7 +307,7 @@ function shoesIndex({ models = [], facets = {}, query = {}, page = 1, total = 0,
@@ -307,7 +307,7 @@ function shoesIndex({ models = [], facets = {}, query = {}, page = 1, total = 0,
<header class="sh-page-h"><p class="sh-kicker">Every shoe</p><h1>Search the catalogue</h1><p class="sh-lede">${total} model${total === 1 ? '' : 's'}${query.q ? ` for “${esc(query.q)}”` : ''}, each with every store's all-in price, the time it was checked and a stale flag.${facets.checked_at ? ` The catalogue was last read ${esc(checked(facets.checked_at, now))}.` : ''}</p></header>
<header class="sh-page-h"><p class="sh-kicker">Every shoe</p><h1>Search the catalogue</h1><p class="sh-lede">${total} model${total === 1 ? '' : 's'}${query.q ? ` for “${esc(query.q)}”` : ''}, each with every store's all-in price, the time it was checked and a stale flag.${facets.checked_at ? ` The catalogue was last read ${esc(checked(facets.checked_at, now))}.` : ''}</p></header>
${searchForm({ q: query.q || '', facets, values: query })}
${searchForm({ q: query.q || '', facets, values: query })}
${models.length ? `<ul class="sh-grid">${models.map((m) => `<li>${modelCard(m)}</li>`).join('')}</ul>` : `<div class="sh-empty"><h2>No shoe matches yet</h2><p>Try a brand, a model or a shorter word. The catalogue is filled by the Deals job; a model appears the first time a source names it.</p><p><a class="sh-btn" href="/shoes">Clear the filters</a></p></div>`}
${models.length ? `<ul class="sh-grid">${models.map((m) => `<li>${modelCard(m)}</li>`).join('')}</ul>` : `<div class="sh-empty"><h2>No shoe matches yet</h2><p>Try a brand, a model or a shorter word. The catalogue is filled by the Deals job; a model appears the first time a source names it.</p><p><a class="sh-btn" href="/shoes">Clear the filters</a></p></div>`}
-${pages > 1 ? `<nav class="sh-page-nav" aria-label="Pages"><a${page <= 1 ? ' class="is-off"' : ''} href="/shoes?${esc(qs(page - 1))}">← Previous</a><span>Page ${page} of ${pages}</span><a${page >= pages ? ' class="is-off"' : ''} href="/shoes?${esc(qs(page + 1))}">Next →</a></nav>` : ''}
+${pages > 1 ? `<nav class="sh-page-nav" aria-label="Pages">${page <= 1 ? '<span class="is-off" aria-disabled="true">← Previous</span>' : `<a href="/shoes?${esc(qs(page - 1))}" rel="prev">← Previous</a>`}<span>Page ${page} of ${pages}</span>${page >= pages ? '<span class="is-off" aria-disabled="true">Next →</span>' : `<a href="/shoes?${esc(qs(page + 1))}" rel="next">Next →</a>`}</nav>` : ''}
<p class="sh-fine">A model with no price says so; a price that has not been checked in ${esc(facets.stale_hours || 48)} hours is marked stale. No affiliate links, ever.</p></div>`;
<p class="sh-fine">A model with no price says so; a price that has not been checked in ${esc(facets.stale_hours || 48)} hours is marked stale. No affiliate links, ever.</p></div>`;
return body;
return body;
}
}
@@ -44,5 +44,14 @@ const views = require('../server/views');
@@ -44,5 +44,14 @@ const views = require('../server/views');
assert(bars.includes('−5.0%'), 'the comparison');
assert(bars.includes('−5.0%'), 'the comparison');
});
});
+ await check('the list\'s page links: the ends are plain text, not links to no page', () => {
+ const first = views.shoesIndex({ page: 1, total: 60 });
+ assert(first.includes('<span class="is-off" aria-disabled="true">← Previous</span>'), 'page 1 has no previous link');
+ assert(/<a href="\/shoes\?[^"]*page=2[^"]*" rel="next">Next →<\/a>/.test(first), 'page 1 links to page 2');
+ const last = views.shoesIndex({ page: 3, total: 60 });
+ assert(last.includes('<span class="is-off" aria-disabled="true">Next →</span>'), 'the last page has no next link');
+ assert(last.includes('rel="prev">← Previous</a>'), 'the last page links back');
+ });
+
done();
done();
})();
})();