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;
|
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 */
|
/* The grid of freelancer cards */
|
||||||
.dir-grid {
|
.dir-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1216,6 +1274,18 @@
|
|||||||
</article>
|
</article>
|
||||||
<!-- DIRECTORY-LIST-END -->
|
<!-- DIRECTORY-LIST-END -->
|
||||||
</div>
|
</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>
|
<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">
|
<div class="dir-note" id="listing-note">
|
||||||
@@ -2545,6 +2615,7 @@
|
|||||||
records.forEach(function (rec) { grid.appendChild(liveCard(rec, active)); });
|
records.forEach(function (rec) { grid.appendChild(liveCard(rec, active)); });
|
||||||
// The button is added after the cards exist — only while the route is set.
|
// The button is added after the cards exist — only while the route is set.
|
||||||
Array.prototype.forEach.call(grid.querySelectorAll(".dev-card"), enhanceAsk);
|
Array.prototype.forEach.call(grid.querySelectorAll(".dev-card"), enhanceAsk);
|
||||||
|
dirPage = 1;
|
||||||
applyFilters();
|
applyFilters();
|
||||||
})
|
})
|
||||||
.catch(function () { /* the build-time list already on the page stands */ });
|
.catch(function () { /* the build-time list already on the page stands */ });
|
||||||
@@ -2554,11 +2625,13 @@
|
|||||||
function initFilters() {
|
function initFilters() {
|
||||||
var bar = document.getElementById("dirFilters");
|
var bar = document.getElementById("dirFilters");
|
||||||
if (!bar) return;
|
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) {
|
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) {
|
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");
|
var emptyReset = document.getElementById("dirEmptyReset");
|
||||||
if (emptyReset) {
|
if (emptyReset) {
|
||||||
@@ -2566,12 +2639,75 @@
|
|||||||
var box = bar.querySelector("[data-search-box]");
|
var box = bar.querySelector("[data-search-box]");
|
||||||
if (box) box.value = "";
|
if (box) box.value = "";
|
||||||
Array.prototype.forEach.call(bar.querySelectorAll("select"), function (s) { s.value = ""; });
|
Array.prototype.forEach.call(bar.querySelectorAll("select"), function (s) { s.value = ""; });
|
||||||
|
dirPage = 1;
|
||||||
applyFilters();
|
applyFilters();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
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() {
|
function applyFilters() {
|
||||||
var bar = document.getElementById("dirFilters");
|
var bar = document.getElementById("dirFilters");
|
||||||
var grid = document.getElementById("dirGrid");
|
var grid = document.getElementById("dirGrid");
|
||||||
@@ -2588,7 +2724,7 @@
|
|||||||
bar.hidden = nothing;
|
bar.hidden = nothing;
|
||||||
if (count) count.hidden = nothing;
|
if (count) count.hidden = nothing;
|
||||||
|
|
||||||
var shown = 0;
|
var matched = [];
|
||||||
cards.forEach(function (card) {
|
cards.forEach(function (card) {
|
||||||
var ok = true;
|
var ok = true;
|
||||||
if (query) ok = (card.getAttribute("data-search") || "").indexOf(query) !== -1;
|
if (query) ok = (card.getAttribute("data-search") || "").indexOf(query) !== -1;
|
||||||
@@ -2603,11 +2739,19 @@
|
|||||||
if (have.split("|").indexOf(want) === -1) ok = false;
|
if (have.split("|").indexOf(want) === -1) ok = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
card.hidden = !ok;
|
if (ok) matched.push(card);
|
||||||
if (ok) shown++;
|
|
||||||
});
|
});
|
||||||
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) ---- */
|
/* ---- Form 1: freelancer application (create — no id) ---- */
|
||||||
@@ -2739,6 +2883,7 @@
|
|||||||
initFocusCounter(document.getElementById("applyForm"), "apDescription", "apDescriptionCount", DESC_MAX_CHARS);
|
initFocusCounter(document.getElementById("applyForm"), "apDescription", "apDescriptionCount", DESC_MAX_CHARS);
|
||||||
initAskModal();
|
initAskModal();
|
||||||
Array.prototype.forEach.call(document.querySelectorAll(".dev-card"), enhanceAsk);
|
Array.prototype.forEach.call(document.querySelectorAll(".dev-card"), enhanceAsk);
|
||||||
|
initPager();
|
||||||
initFilters();
|
initFilters();
|
||||||
initLiveList();
|
initLiveList();
|
||||||
initApplyForm();
|
initApplyForm();
|
||||||
|
|||||||
Reference in new issue
Block a user