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();