Public Access
directory: never show the stale build-time cards before the webhook answers
With JS the build-time card painted first and was then replaced by the webhook's list — a visitor could see a person who is no longer listed. - `js` class set on <html> by an inline script as the first thing in <head>, so the static cards are hidden from the first frame only when JS will replace them. - JS on: a text loading line (role=status, aria-live=polite) stands in their place; on success the static cards are removed from the DOM and the live list rendered; on failure an honest error with a Try again button replaces them — never the stale cards. - Count line + filter row held out (invisible, out of the tab order) until the data is there; pager absent while loading. - Space reserved (dir-grid min-height, dir-count min-height, filter row kept in flow) so the list does not jump when the cards arrive — measured 0px movement loading -> ready. - JS off: unchanged — the static cards show.
This commit is contained in:
1 parent
cdf166632a
commit
1f8179d562
1 file changed
+154
-3
@@ -1,6 +1,11 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en-US">
|
<html lang="en-US">
|
||||||
<head>
|
<head>
|
||||||
|
<!-- Set `js` on <html> as the first thing in <head>, so the build-time directory cards
|
||||||
|
can be hidden from the very first frame when (and only when) JavaScript will replace
|
||||||
|
them with the webhook's list. Tiny and synchronous on purpose: it must land before
|
||||||
|
any stylesheet is applied, or the stale card paints for a moment. -->
|
||||||
|
<script>document.documentElement.className += " js";</script>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Hire an Odoo Developer for Your Project</title>
|
<title>Hire an Odoo Developer for Your Project</title>
|
||||||
@@ -300,6 +305,9 @@
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 0.6);
|
||||||
margin: 0 0 1.25rem;
|
margin: 0 0 1.25rem;
|
||||||
|
/* Hold one line of height even while empty (the JS-on loading state keeps this line
|
||||||
|
in the layout but hidden), so its text arriving does not move the list. */
|
||||||
|
min-height: 1.6em;
|
||||||
}
|
}
|
||||||
.dir-empty {
|
.dir-empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -389,6 +397,63 @@
|
|||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
/* Reserve the space a row of cards occupies, so the page does not jump when the
|
||||||
|
webhook list replaces the loading line. Matched to the tallest single-row card
|
||||||
|
measured (~701 px at 1280, ~728 px at 375). */
|
||||||
|
min-height: 730px;
|
||||||
|
}
|
||||||
|
/* ---- With JavaScript: the build-time cards are hidden until the webhook answers ----
|
||||||
|
`js` is set on <html> in <head> before the first paint, so the stale card never
|
||||||
|
shows for a moment; a no-JS visitor never gets the class and sees every static card
|
||||||
|
exactly as before. The build-time cards are NEVER a fallback: on success the list is
|
||||||
|
re-rendered from the webhook (and the static cards removed from the DOM), and on a
|
||||||
|
failed fetch the honest error below stands in their place. */
|
||||||
|
.js #dirGrid > .dev-card {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.js #directory[data-dir-state="ready"] #dirGrid > .dev-card {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
/* The count line and the filter row say something ABOUT the data — so with JavaScript
|
||||||
|
they stay hidden until the list has arrived (a count of zero during load is a lie,
|
||||||
|
and a filter over an empty list is a bug). `visibility: hidden` rather than
|
||||||
|
`display: none`: the space is held, so the row appearing does not shove the list
|
||||||
|
down. It is also taken out of the tab order and the accessibility tree, so it cannot
|
||||||
|
be operated even though it is present. */
|
||||||
|
.js #directory:not([data-dir-state="ready"]) #dirFilters,
|
||||||
|
.js #directory:not([data-dir-state="ready"]) #dirCount {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
/* The pager and the empty state sit below the list and appear only in specific
|
||||||
|
conditions, so they are simply removed while loading. */
|
||||||
|
.js #directory:not([data-dir-state="ready"]) #dirPager,
|
||||||
|
.js #directory:not([data-dir-state="ready"]) #dirEmpty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
/* The loading line and the failure state: full-width items in the list's own grid,
|
||||||
|
both plain text and both announced (status / alert). The failure is a real message
|
||||||
|
with a real retry button — never the stale card, never colour alone. */
|
||||||
|
.dir-loading,
|
||||||
|
.dir-fail {
|
||||||
|
display: none;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.js #directory:not([data-dir-state="ready"]):not([data-dir-state="error"]) .dir-loading {
|
||||||
|
display: block;
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
.js #directory[data-dir-state="error"] .dir-fail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.9rem;
|
||||||
|
color: rgba(255, 255, 255, 0.78);
|
||||||
|
}
|
||||||
|
.dir-fail p {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 560px;
|
||||||
}
|
}
|
||||||
.dev-card {
|
.dev-card {
|
||||||
background: var(--ivory);
|
background: var(--ivory);
|
||||||
@@ -1274,6 +1339,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="dir-count" id="dirCount" hidden></p>
|
<p class="dir-count" id="dirCount" hidden></p>
|
||||||
<div class="dir-grid" id="dirGrid">
|
<div class="dir-grid" id="dirGrid">
|
||||||
|
<!-- Two states that exist only while JavaScript is running: the loading line shown
|
||||||
|
until the webhook answers, and the honest failure (with a retry) that replaces
|
||||||
|
the cards if it does not. A no-JS visitor sees neither — only the build-time
|
||||||
|
cards below. -->
|
||||||
|
<p class="dir-loading" id="dirLoading" role="status" aria-live="polite">Loading freelancers…</p>
|
||||||
|
<div class="dir-fail" id="dirError" role="alert">
|
||||||
|
<p id="dirErrorText"></p>
|
||||||
|
<button type="button" class="btn btn-outline" id="dirRetry">Try again</button>
|
||||||
|
</div>
|
||||||
<!-- DIRECTORY-LIST-START -->
|
<!-- DIRECTORY-LIST-START -->
|
||||||
<article class="dev-card" data-search="oliver - the oc guy i love to see how far i can push the odoo community version for my clients. besides the module implementation i am quite happy with adding to odoo using hermes agent, n8n and progressive web apps agent automation, manufacturing, e-commerce english german spanish" data-id="2" data-name="Oliver - The OC Guy" data-country="py" data-languages="english|german|spanish" data-oca="1" data-relay="1">
|
<article class="dev-card" data-search="oliver - the oc guy i love to see how far i can push the odoo community version for my clients. besides the module implementation i am quite happy with adding to odoo using hermes agent, n8n and progressive web apps agent automation, manufacturing, e-commerce english german spanish" data-id="2" data-name="Oliver - The OC Guy" data-country="py" data-languages="english|german|spanish" data-oca="1" data-relay="1">
|
||||||
<header class="dev-head">
|
<header class="dev-head">
|
||||||
@@ -2748,9 +2822,50 @@
|
|||||||
else card.appendChild(row);
|
else card.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The list, from the webhook. With JavaScript the build-time cards are hidden until
|
||||||
|
this answers (see the `.js` rules in the page style): on success the static cards are
|
||||||
|
REMOVED from the DOM and the live list is rendered; on failure they are removed too
|
||||||
|
and an honest error with a retry stands in their place. The static cards are never a
|
||||||
|
fallback — a stale listing is the very thing this replaces. */
|
||||||
|
function setDirState(state) {
|
||||||
|
var section = document.getElementById("directory");
|
||||||
|
if (section) section.setAttribute("data-dir-state", state);
|
||||||
|
}
|
||||||
|
// Take the build-time cards out of the DOM entirely — hidden is not enough, they must
|
||||||
|
// not be reachable or readable as the list.
|
||||||
|
function dropStaticCards(grid) {
|
||||||
|
Array.prototype.forEach.call(grid.querySelectorAll(".dev-card"), function (card) {
|
||||||
|
if (card.parentNode) card.parentNode.removeChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// The failure state: no cards (never the stale ones), the message written into the
|
||||||
|
// role="alert" region so it is announced, the retry button left to re-run the fetch.
|
||||||
|
// `dirReady` stays false, so the count line and the filters stay away.
|
||||||
|
function failDir(message) {
|
||||||
|
var grid = document.getElementById("dirGrid");
|
||||||
|
dirReady = false;
|
||||||
|
if (grid) dropStaticCards(grid);
|
||||||
|
var text = document.getElementById("dirErrorText");
|
||||||
|
if (text) text.textContent = message;
|
||||||
|
setDirState("error");
|
||||||
|
}
|
||||||
|
|
||||||
function initLiveList() {
|
function initLiveList() {
|
||||||
var grid = document.getElementById("dirGrid");
|
var grid = document.getElementById("dirGrid");
|
||||||
if (!grid || !window.fetch || !ODOO_EXPERTS_WEBHOOK) return;
|
if (!grid) return;
|
||||||
|
if (!window.fetch || !ODOO_EXPERTS_WEBHOOK) {
|
||||||
|
// No way to load the list at all: say so plainly rather than leave the loading line
|
||||||
|
// standing. The static cards stay hidden either way — they are never the answer.
|
||||||
|
failDir("We could not load the freelancers just now. Please try again in a moment.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Every attempt starts from the loading state: static cards out, loading line in,
|
||||||
|
// count / filters / pager away. This is the first load and every retry alike.
|
||||||
|
dropStaticCards(grid);
|
||||||
|
setDirState("loading");
|
||||||
|
var errorText = document.getElementById("dirErrorText");
|
||||||
|
if (errorText) errorText.textContent = "";
|
||||||
|
|
||||||
fetch(ODOO_EXPERTS_WEBHOOK, { headers: { Accept: "application/json" } })
|
fetch(ODOO_EXPERTS_WEBHOOK, { headers: { Accept: "application/json" } })
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
@@ -2760,7 +2875,12 @@
|
|||||||
var all = Array.isArray(payload) ? payload : (payload && payload.data) || [];
|
var all = Array.isArray(payload) ? payload : (payload && payload.data) || [];
|
||||||
// A record with no Name is not a freelancer (build-directory.py skips it too).
|
// A record with no Name is not a freelancer (build-directory.py skips it too).
|
||||||
var records = all.filter(function (r) { return r && String(r.Name || "").trim(); });
|
var records = all.filter(function (r) { return r && String(r.Name || "").trim(); });
|
||||||
if (!records.length) return; // never trade a stale list for an empty one
|
if (!records.length) {
|
||||||
|
// The endpoint answered but lists nobody: not a load failure, but not the stale
|
||||||
|
// card either. Say so, with the same way to retry.
|
||||||
|
failDir("No freelancers are listed right now.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
// NO sort: the endpoint's order is rendered exactly as received.
|
// NO sort: the endpoint's order is rendered exactly as received.
|
||||||
var active = liveFilters(records);
|
var active = liveFilters(records);
|
||||||
while (grid.firstChild) grid.removeChild(grid.firstChild);
|
while (grid.firstChild) grid.removeChild(grid.firstChild);
|
||||||
@@ -2768,9 +2888,20 @@
|
|||||||
// 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;
|
dirPage = 1;
|
||||||
|
dirReady = true;
|
||||||
|
setDirState("ready");
|
||||||
applyFilters();
|
applyFilters();
|
||||||
})
|
})
|
||||||
.catch(function () { /* the build-time list already on the page stands */ });
|
.catch(function () {
|
||||||
|
failDir("We could not load the freelancers just now. Please try again in a moment.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry — wired once; the button lives in the failure region, which the loading and
|
||||||
|
// success states remove from the DOM.
|
||||||
|
function initDirRetry() {
|
||||||
|
var retry = document.getElementById("dirRetry");
|
||||||
|
if (retry) retry.addEventListener("click", function () { initLiveList(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Directory filtering (an enhancement over the build-time HTML) ---- */
|
/* ---- Directory filtering (an enhancement over the build-time HTML) ---- */
|
||||||
@@ -2803,6 +2934,10 @@
|
|||||||
the facets follow. With a short list it stays hidden, so no pager appears. */
|
the facets follow. With a short list it stays hidden, so no pager appears. */
|
||||||
var PER_PAGE = 9;
|
var PER_PAGE = 9;
|
||||||
var dirPage = 1;
|
var dirPage = 1;
|
||||||
|
// False until the webhook's list is on screen. The count line, the filters, the pager
|
||||||
|
// and the empty state all read this: nothing about the list is shown while the data is
|
||||||
|
// not there yet (a zero count and an operable empty filter are both lies during load).
|
||||||
|
var dirReady = false;
|
||||||
|
|
||||||
function renderPager(total) {
|
function renderPager(total) {
|
||||||
var pager = document.getElementById("dirPager");
|
var pager = document.getElementById("dirPager");
|
||||||
@@ -2867,6 +3002,21 @@
|
|||||||
var empty = document.getElementById("dirEmpty");
|
var empty = document.getElementById("dirEmpty");
|
||||||
if (!bar || !grid) return;
|
if (!bar || !grid) return;
|
||||||
|
|
||||||
|
// No list on screen yet (the webhook has not answered): there is nothing to count,
|
||||||
|
// filter or page, so keep the count line, the filter row, the empty state and the
|
||||||
|
// pager out of the way. The CSS already hides them; this keeps the logic honest too.
|
||||||
|
if (!dirReady) {
|
||||||
|
// Hold the count line and the filter row in the layout (the CSS keeps them
|
||||||
|
// invisible), so their arrival does not jump the list; the pager and the empty
|
||||||
|
// state stay fully out.
|
||||||
|
bar.hidden = false;
|
||||||
|
if (count) count.hidden = false;
|
||||||
|
if (empty) empty.hidden = true;
|
||||||
|
var pager0 = document.getElementById("dirPager");
|
||||||
|
if (pager0) pager0.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var search = bar.querySelector("[data-search-box]");
|
var search = bar.querySelector("[data-search-box]");
|
||||||
var query = search ? fold(search.value.trim()) : "";
|
var query = search ? fold(search.value.trim()) : "";
|
||||||
var selects = Array.prototype.slice.call(bar.querySelectorAll("select"));
|
var selects = Array.prototype.slice.call(bar.querySelectorAll("select"));
|
||||||
@@ -3055,6 +3205,7 @@
|
|||||||
Array.prototype.forEach.call(document.querySelectorAll(".dev-card"), enhanceAsk);
|
Array.prototype.forEach.call(document.querySelectorAll(".dev-card"), enhanceAsk);
|
||||||
initPager();
|
initPager();
|
||||||
initFilters();
|
initFilters();
|
||||||
|
initDirRetry();
|
||||||
initLiveList();
|
initLiveList();
|
||||||
initApplyForm();
|
initApplyForm();
|
||||||
initApplyModal();
|
initApplyModal();
|
||||||
|
|||||||
Reference in new issue
Block a user