From 1f8179d56246ec74e45cecc0a126129186dcb235 Mon Sep 17 00:00:00 2001 From: webmaster Date: Tue, 3 Nov 2026 21:11:36 +0000 Subject: [PATCH] directory: never show the stale build-time cards before the webhook answers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 by an inline script as the first thing in , 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. --- odoo-community-developers/index.html | 157 ++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 3 deletions(-) diff --git a/odoo-community-developers/index.html b/odoo-community-developers/index.html index c62e563..9021b81 100644 --- a/odoo-community-developers/index.html +++ b/odoo-community-developers/index.html @@ -1,6 +1,11 @@ + + Hire an Odoo Developer for Your Project @@ -300,6 +305,9 @@ font-size: 0.82rem; color: rgba(255, 255, 255, 0.6); 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 { text-align: center; @@ -389,6 +397,63 @@ gap: 1.25rem; max-width: 1000px; 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 in 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 { background: var(--ivory); @@ -1274,6 +1339,15 @@
+ +

Loading freelancers…

+
@@ -2748,9 +2822,50 @@ 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() { 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" } }) .then(function (res) { if (!res.ok) throw new Error("HTTP " + res.status); @@ -2760,7 +2875,12 @@ var all = Array.isArray(payload) ? payload : (payload && payload.data) || []; // 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(); }); - 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. var active = liveFilters(records); 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. Array.prototype.forEach.call(grid.querySelectorAll(".dev-card"), enhanceAsk); dirPage = 1; + dirReady = true; + setDirState("ready"); 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) ---- */ @@ -2803,6 +2934,10 @@ the facets follow. With a short list it stays hidden, so no pager appears. */ var PER_PAGE = 9; 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) { var pager = document.getElementById("dirPager"); @@ -2867,6 +3002,21 @@ var empty = document.getElementById("dirEmpty"); 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 query = search ? fold(search.value.trim()) : ""; var selects = Array.prototype.slice.call(bar.querySelectorAll("select")); @@ -3055,6 +3205,7 @@ Array.prototype.forEach.call(document.querySelectorAll(".dev-card"), enhanceAsk); initPager(); initFilters(); + initDirRetry(); initLiveList(); initApplyForm(); initApplyModal();