Public Access
feat(freelancer-directory): nine cards per page with a pager
Cap the directory at 9 cards per page (3 x 3 at desktop) and add pagination. - The grid rule is untouched (auto-fill minmax(300px,1fr), max-width 1000px): measured 3 columns at 1280, 1 at 375. The cap does not change column behaviour. - A <nav id="dirPager"> ships `hidden` and is un-hidden by script ONLY when the filtered list needs more than one page — so with 7 records no pager appears, the same rule the facets follow. - Previous/Next plus one button per page; the current page carries aria-current, a filled pill and the live "Page N of M" line — never colour alone. Focus is kept on the pager across a rebuild rather than dropped to the document. - A filter or search resets to page 1; a page left out of range falls back to 1 instead of rendering an empty page. - Pagination applies to the filtered set, not the raw list. - No new URLs: the pager holds no href and touches no history, so there is nothing to deep-link into a broken state. - With JavaScript off the pager never appears and every build-time card shows; the #directory anchor is unaffected.
This commit is contained in:
1 parent
53e9ea999d
commit
f0a2c8d745
1 file changed
+152
-7
@@ -324,6 +324,64 @@
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Pagination — nine cards per page. Script-dependent BY DESIGN: with JavaScript
|
||||
off the element keeps its `hidden` attribute and every build-time card shows.
|
||||
`[hidden]` needs its own rule because this class sets `display`. */
|
||||
.dir-pager {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
max-width: 1000px;
|
||||
margin: 1.75rem auto 0;
|
||||
}
|
||||
.dir-pager[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.dir-pager-pages {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.dir-page {
|
||||
font: inherit;
|
||||
font-size: 0.9rem;
|
||||
min-width: 2.4rem;
|
||||
padding: 0.4rem 0.7rem;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
border: 1px solid var(--line-brass);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
.dir-page:hover:not([disabled]) {
|
||||
border-color: var(--brass-bright);
|
||||
}
|
||||
.dir-page:focus-visible {
|
||||
outline: 3px solid var(--brass-bright);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
/* The current page is carried three ways — a filled pill, `aria-current`, and the
|
||||
"Page N of M" line — so it is never signalled by colour alone. */
|
||||
.dir-page[aria-current="page"] {
|
||||
background: var(--brass);
|
||||
border-color: var(--brass-deep);
|
||||
color: var(--midnight);
|
||||
font-weight: 600;
|
||||
}
|
||||
.dir-page[disabled] {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
.dir-pager-status {
|
||||
flex-basis: 100%;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
/* The grid of freelancer cards */
|
||||
.dir-grid {
|
||||
display: grid;
|
||||
@@ -1216,6 +1274,18 @@
|
||||
</article>
|
||||
<!-- DIRECTORY-LIST-END -->
|
||||
</div>
|
||||
|
||||
<!-- Pagination — nine cards per page. Ships `hidden`: it is un-hidden by script
|
||||
only when the filtered list needs more than one page, so with JavaScript off
|
||||
it never appears and every build-time card stays visible. No page → no URL:
|
||||
it holds no href and touches no history, so there is nothing to deep-link. -->
|
||||
<nav class="dir-pager" id="dirPager" hidden aria-label="Listings pages">
|
||||
<button type="button" class="dir-page" id="dirPrev" data-page="prev">Previous</button>
|
||||
<span class="dir-pager-pages" id="dirPages"></span>
|
||||
<button type="button" class="dir-page" id="dirNext" data-page="next">Next</button>
|
||||
<p class="dir-pager-status" id="dirPagerStatus" role="status" aria-live="polite"></p>
|
||||
</nav>
|
||||
|
||||
<p class="dir-empty" id="dirEmpty" hidden>Nothing matches. Widen your search, or <button type="button" class="dir-empty-reset" id="dirEmptyReset">set the filters back to All</button>.</p>
|
||||
|
||||
<div class="dir-note" id="listing-note">
|
||||
@@ -2545,6 +2615,7 @@
|
||||
records.forEach(function (rec) { grid.appendChild(liveCard(rec, active)); });
|
||||
// The button is added after the cards exist — only while the route is set.
|
||||
Array.prototype.forEach.call(grid.querySelectorAll(".dev-card"), enhanceAsk);
|
||||
dirPage = 1;
|
||||
applyFilters();
|
||||
})
|
||||
.catch(function () { /* the build-time list already on the page stands */ });
|
||||
@@ -2554,11 +2625,13 @@
|
||||
function initFilters() {
|
||||
var bar = document.getElementById("dirFilters");
|
||||
if (!bar) return;
|
||||
// A new filter or query starts at page 1 — staying on page 3 after narrowing to
|
||||
// two matches is the classic pagination bug.
|
||||
bar.addEventListener("change", function (event) {
|
||||
if (event.target && event.target.tagName === "SELECT") applyFilters();
|
||||
if (event.target && event.target.tagName === "SELECT") { dirPage = 1; applyFilters(); }
|
||||
});
|
||||
bar.addEventListener("input", function (event) {
|
||||
if (event.target && event.target.hasAttribute("data-search-box")) applyFilters();
|
||||
if (event.target && event.target.hasAttribute("data-search-box")) { dirPage = 1; applyFilters(); }
|
||||
});
|
||||
var emptyReset = document.getElementById("dirEmptyReset");
|
||||
if (emptyReset) {
|
||||
@@ -2566,12 +2639,75 @@
|
||||
var box = bar.querySelector("[data-search-box]");
|
||||
if (box) box.value = "";
|
||||
Array.prototype.forEach.call(bar.querySelectorAll("select"), function (s) { s.value = ""; });
|
||||
dirPage = 1;
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
/* ---- The pager: nine cards per page ---------------------------------------
|
||||
It renders ONLY when the filtered list needs more than one page — the same rule
|
||||
the facets follow. With a short list it stays hidden, so no pager appears. */
|
||||
var PER_PAGE = 9;
|
||||
var dirPage = 1;
|
||||
|
||||
function renderPager(total) {
|
||||
var pager = document.getElementById("dirPager");
|
||||
var pages = document.getElementById("dirPages");
|
||||
var prev = document.getElementById("dirPrev");
|
||||
var next = document.getElementById("dirNext");
|
||||
var status = document.getElementById("dirPagerStatus");
|
||||
if (!pager || !pages) return;
|
||||
var pageCount = Math.ceil(total / PER_PAGE);
|
||||
if (pageCount <= 1) {
|
||||
pager.hidden = true;
|
||||
pages.textContent = "";
|
||||
return;
|
||||
}
|
||||
var active = document.activeElement;
|
||||
var keepFocus = pager.contains(active);
|
||||
pager.hidden = false;
|
||||
pages.textContent = "";
|
||||
for (var p = 1; p <= pageCount; p++) {
|
||||
var b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "dir-page";
|
||||
b.textContent = String(p);
|
||||
b.setAttribute("data-page", String(p));
|
||||
b.setAttribute("aria-label", "Page " + p + " of " + pageCount);
|
||||
if (p === dirPage) b.setAttribute("aria-current", "page");
|
||||
pages.appendChild(b);
|
||||
}
|
||||
if (prev) prev.disabled = dirPage <= 1;
|
||||
if (next) next.disabled = dirPage >= pageCount;
|
||||
if (status) status.textContent = "Page " + dirPage + " of " + pageCount;
|
||||
// Keep the keyboard where it was. Previous/Next survive the rebuild, so focus
|
||||
// stays on them; a number button is replaced, so focus moves to the current page
|
||||
// — never dropped to the top of the document.
|
||||
if (keepFocus) {
|
||||
if (active && document.contains(active) && !active.disabled) active.focus();
|
||||
else {
|
||||
var current = pages.querySelector('[aria-current="page"]');
|
||||
if (current) current.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initPager() {
|
||||
var pager = document.getElementById("dirPager");
|
||||
if (!pager) return;
|
||||
pager.addEventListener("click", function (event) {
|
||||
var btn = event.target.closest ? event.target.closest(".dir-page") : null;
|
||||
if (!btn || btn.disabled) return;
|
||||
var target = btn.getAttribute("data-page");
|
||||
if (target === "prev") dirPage -= 1;
|
||||
else if (target === "next") dirPage += 1;
|
||||
else dirPage = parseInt(target, 10);
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
var bar = document.getElementById("dirFilters");
|
||||
var grid = document.getElementById("dirGrid");
|
||||
@@ -2588,7 +2724,7 @@
|
||||
bar.hidden = nothing;
|
||||
if (count) count.hidden = nothing;
|
||||
|
||||
var shown = 0;
|
||||
var matched = [];
|
||||
cards.forEach(function (card) {
|
||||
var ok = true;
|
||||
if (query) ok = (card.getAttribute("data-search") || "").indexOf(query) !== -1;
|
||||
@@ -2603,11 +2739,19 @@
|
||||
if (have.split("|").indexOf(want) === -1) ok = false;
|
||||
});
|
||||
}
|
||||
card.hidden = !ok;
|
||||
if (ok) shown++;
|
||||
if (ok) matched.push(card);
|
||||
});
|
||||
if (count && !count.hidden) count.textContent = "Showing " + shown + " of " + cards.length;
|
||||
if (empty) empty.hidden = nothing ? true : shown !== 0;
|
||||
|
||||
// Pagination applies to the FILTERED set. A page left out of range by a filter
|
||||
// change falls back to page 1 rather than rendering an empty page.
|
||||
var pageCount = Math.ceil(matched.length / PER_PAGE) || 1;
|
||||
if (dirPage < 1 || dirPage > pageCount) dirPage = 1;
|
||||
var page = matched.slice((dirPage - 1) * PER_PAGE, dirPage * PER_PAGE);
|
||||
cards.forEach(function (card) { card.hidden = page.indexOf(card) === -1; });
|
||||
|
||||
if (count && !count.hidden) count.textContent = "Showing " + matched.length + " of " + cards.length;
|
||||
if (empty) empty.hidden = nothing ? true : matched.length !== 0;
|
||||
renderPager(matched.length);
|
||||
}
|
||||
|
||||
/* ---- Form 1: freelancer application (create — no id) ---- */
|
||||
@@ -2739,6 +2883,7 @@
|
||||
initFocusCounter(document.getElementById("applyForm"), "apDescription", "apDescriptionCount", DESC_MAX_CHARS);
|
||||
initAskModal();
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".dev-card"), enhanceAsk);
|
||||
initPager();
|
||||
initFilters();
|
||||
initLiveList();
|
||||
initApplyForm();
|
||||
|
||||
Reference in new issue
Block a user