diff --git a/odoo-community-developers/index.html b/odoo-community-developers/index.html index 8088cf9..bd30dc9 100644 --- a/odoo-community-developers/index.html +++ b/odoo-community-developers/index.html @@ -700,13 +700,15 @@ python3 /root/workspace/webmaster/work/build-directory.py -->
-
+
@@ -719,6 +721,24 @@
Projects on ODOO4projects
4 projects — a number we recorded

No contact details published for this freelancer.

+
+
+
+ +
+

Oliver - The OC Guy

+

Agent Automation, Manufacturing, E-commerce

+
+

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

+
    +
  • Works as an individual (as declared)
  • +
  • German
  • +
  • English
  • +
  • Spanish - beginners
  • +
  • Functional Consulting
  • +
+
Projects on ODOO4projects
4 projects — a number we recorded
+
@@ -751,6 +771,7 @@
+
@@ -1396,8 +1417,240 @@ if (note) note.hidden = true; } + /* ---- The live list: render the directory from the webhook at page load ---- + + build-directory.py writes the cards into the page at BUILD time, which is why a + record added afterwards never reached the page. So the build-time cards stay in + the HTML exactly as they are and act as the FALLBACK: this fetch replaces them + only when it succeeds. A visitor with JavaScript off, or arriving while the + webhook is down, still sees the last built list — an empty directory is worse + than a stale one, so an empty or failed response never overwrites a populated one. + + NOT BUILT — the confirmation gate. The user's flow creates a record only after + the email is confirmed, so an unconfirmed record must not show. This endpoint + exposes NO way to tell the two apart: there is no `status`, no `email_verified`, + nothing but the record's own fields. So nothing is guessed here — every record + the endpoint returns with a `Name` is rendered, exactly as build-directory.py + does it. The workflow has to expose a marker before this can gate on one. */ + var LIVE_FILTERS = [ + ["Country", "Country", "country"], + ["odoo_versions", "Odoo version", "odoo-versions"], + ["languages", "Language", "languages"], + ["specialities", "Speciality", "specialities"] + ]; + // The webhook's casing has drifted once already. Accept the known aliases too — + // the same list as FIELD_ALIASES in build-directory.py, so both renderers agree. + var LIVE_ALIASES = { Country: ["country"], odoo_versions: ["Odoo_versions"] }; + + function liveValues(rec, field) { + var names = [field].concat(LIVE_ALIASES[field] || []); + var raw = null; + for (var i = 0; i < names.length; i++) { + var v = rec[names[i]]; + if (v !== null && v !== undefined && v !== "") { raw = v; break; } + } + if (raw === null) return []; + var list = Array.isArray(raw) ? raw : String(raw).split(","); + return list.map(function (s) { return String(s).trim(); }).filter(Boolean); + } + + function isLink(v) { + return v.indexOf("http://") === 0 || v.indexOf("https://") === 0 || v.indexOf("/") === 0; + } + + function liveEl(tag, cls, text) { + var node = document.createElement(tag); + if (cls) node.className = cls; + if (text !== undefined) node.textContent = text; + return node; + } + + /* One card, field for field the same as render_card() in build-directory.py. + Everything a freelancer wrote goes in as textContent, never as HTML. */ + function liveCard(rec, active) { + var name = String(rec.Name || "").trim(); + var card = liveEl("article", "dev-card"); + card.setAttribute("data-name", name.toLowerCase()); + active.forEach(function (pair) { + card.setAttribute("data-" + pair[2], liveValues(rec, pair[0]).map(function (v) { + return v.toLowerCase(); + }).join("|")); + }); + + var photo = String(rec.Image || "").trim(); + var avatar; + if (isLink(photo)) { + avatar = liveEl("img", "dev-photo"); // a real URL only — never a base64 blob + avatar.src = photo; + avatar.alt = ""; + avatar.width = 72; + avatar.height = 72; + avatar.loading = "lazy"; + avatar.decoding = "async"; + } else { + var parts = name.split(/\s+/).filter(Boolean); + var mark = parts.length > 1 + ? parts[0].charAt(0) + parts[parts.length - 1].charAt(0) + : (parts[0] || "?").slice(0, 2); + avatar = liveEl("span", "dev-avatar", mark.toUpperCase()); + avatar.setAttribute("aria-hidden", "true"); + } + var head = liveEl("header", "dev-head"); + var who = liveEl("div", "dev-id"); + who.appendChild(liveEl("h3", "dev-name", name)); + var focus = String(rec.Focus || "").trim(); + if (focus) who.appendChild(liveEl("p", "dev-focus", focus)); + head.appendChild(avatar); + head.appendChild(who); + card.appendChild(head); + + var bio = String(rec.description || "").trim(); + if (bio) card.appendChild(liveEl("p", "dev-bio", bio)); + + // Tags: only what the freelancer themselves declared. + var tags = []; + if (rec.OCA_member === true) tags.push("OCA member (as declared)"); + if (rec.is_individual === true) tags.push("Works as an individual (as declared)"); + liveValues(rec, "languages").forEach(function (v) { tags.push(v); }); + liveValues(rec, "specialities").forEach(function (v) { tags.push(v); }); + if (tags.length) { + var ul = liveEl("ul", "dev-tags"); + tags.forEach(function (t) { ul.appendChild(liveEl("li", "dev-tag", t)); }); + card.appendChild(ul); + } + + // The one number we publish about a person, and only one we recorded ourselves. + var clients = parseInt(rec.odoo4projects_clients, 10); + if (!isNaN(clients)) { + var dl = liveEl("dl", "dev-facts"); + var cell = document.createElement("div"); + cell.appendChild(liveEl("dt", null, "Projects on ODOO4projects")); + cell.appendChild(liveEl("dd", null, clients + (clients === 1 ? " project" : " projects") + + " — a number we recorded")); + dl.appendChild(cell); + card.appendChild(dl); + } + + var email = String(rec.email || "").trim(); + var hasEmail = email.indexOf("@") !== -1; + var links = []; + [["Linkedin", "LinkedIn"], ["github", "GitHub"], ["oca_profile", "OCA profile"], + ["upwork", "Upwork"], ["fiverr", "Fiverr"]].forEach(function (pair) { + var val = String(rec[pair[0]] || "").trim(); + if (isLink(val)) { + var a = liveEl("a", null, pair[1]); + a.href = val; + a.target = "_blank"; + a.rel = "noopener nofollow"; + links.push(a); + } + }); + if (hasEmail) { + var mail = liveEl("a", null, "Email this freelancer"); + mail.href = "mailto:" + email; + links.push(mail); + } + if (links.length) { + var row = liveEl("p", "dev-links"); + links.forEach(function (a, i) { + if (i) row.appendChild(document.createTextNode(" · ")); + row.appendChild(a); + }); + card.appendChild(row); + } + if (!hasEmail) { + card.appendChild(liveEl("p", "dev-contact-none", + "No contact details published for this freelancer.")); + } + return card; + } + + /* ISO code → English name, read off the application form's own Country