@@ -719,6 +721,24 @@
+
+
+
+
@@ -751,6 +771,7 @@
- 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
+ +- Projects on ODOO4projects
- 4 projects — a number we recorded
+
@@ -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