Public Access
directory card photos: load from the image endpoint, not inline base64
The webhook no longer sends `Image`, so the card photo comes from its own endpoint, addressed by the record id — one <img> per card, one request. - static/expert-image.js: the route AND its query-parameter name in ONE constant (id, confirmed by measurement; unverified in the brief), plus an optional version token used only when the payload carries one. - Directory card: <img src="…odoo_experts_image?id=<id>">, width/height reserve the 72x72 box, decoding=async, eager for the first row and lazy below it; onerror swaps in the SAME monogram a card with no photo shows, so a failing image looks exactly like today. - The static build-time card loses its 185 KB inline base64 photo and shows a monogram: it is the no-JavaScript fallback, where there is no onerror to fall back with. Page 341,345 -> 158,428 bytes. - /freelancer-edit/: the photo block is a look, not a field (no file input), and the whole-record save sends NO `Image` key at all.
This commit is contained in:
1 parent
9d14652a57
commit
2b0e89191a
3 files changed
+134
-71
No files matched your search
@@ -1380,7 +1380,7 @@
|
||||
<!-- 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">
|
||||
<header class="dev-head">
|
||||
<img class="dev-photo" src="data:image/jpeg;base64,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 truncated
|
||||
<span class="dev-avatar" aria-hidden="true">OG</span>
|
||||
<div class="dev-id">
|
||||
<h3 class="dev-name">Oliver - The OC Guy</h3>
|
||||
<a class="dev-oca-badge" href="https://apps.odoo-community.org/community/oliver-arnold" target="_blank" rel="noopener nofollow">OCA Member Profile</a>
|
||||
@@ -2052,6 +2052,7 @@
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
<script src="/static/focus-rule.js?v=22ac72a8f0"></script>
|
||||
<script src="/static/photo-upload.js?v=c17638457b"></script>
|
||||
<script src="/static/expert-image.js?v=e496d4a4e3"></script>
|
||||
<script>
|
||||
/* ============================================================================
|
||||
Odoo freelancer directory — page script.
|
||||
@@ -2093,6 +2094,12 @@
|
||||
var ODOO_EXPERTS_EDIT_WEBHOOK = "https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts_edit";
|
||||
var EDIT_EMAIL_KEY = "email"; // body sent to ODOO_EXPERTS_EDIT_WEBHOOK: { email: "..." }
|
||||
|
||||
// How many cards load their photo EAGERLY: the first visible ROW. The grid is 3
|
||||
// columns at the widest layout (measured), so the first three cards. Every card below
|
||||
// the fold loads lazily — a lazy image in the first row would delay LCP, and a photo
|
||||
// off-screen is not worth its request.
|
||||
var EAGER_CARDS = 3;
|
||||
|
||||
// ---- The shared constants (the same lists live in build-directory.py) --------
|
||||
// The five self-assessed focus axes — each a SEPARATE numeric field (0-5) on the
|
||||
// wire, no CSV, no encoding. They must total exactly FOCUS_TOTAL.
|
||||
@@ -2356,10 +2363,6 @@
|
||||
return v.indexOf("http://") === 0 || v.indexOf("https://") === 0 || v.indexOf("/") === 0;
|
||||
}
|
||||
|
||||
function isImageSrc(v) {
|
||||
return v.indexOf("https://") === 0 || v.indexOf("data:image/") === 0;
|
||||
}
|
||||
|
||||
function liveEl(tag, cls, text) {
|
||||
var node = document.createElement(tag);
|
||||
if (cls) node.className = cls;
|
||||
@@ -2460,9 +2463,25 @@
|
||||
return liveValues(rec, field).map(fold).join("|");
|
||||
}
|
||||
|
||||
/* The monogram — the initials treatment a card shows when there is no photo. It is
|
||||
also the FALLBACK when the photo fails to load, so a missing photo looks exactly
|
||||
like a card that never had one. */
|
||||
function liveMonogram(name) {
|
||||
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);
|
||||
var mono = liveEl("span", "dev-avatar", mark.toUpperCase());
|
||||
mono.setAttribute("aria-hidden", "true");
|
||||
return mono;
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
Everything a freelancer wrote goes in as textContent, never as HTML.
|
||||
|
||||
`eager` is true only for the first visible row: those photos load eagerly because
|
||||
a lazy image in the first row delays LCP. Everything below the fold loads lazily. */
|
||||
function liveCard(rec, active, eager) {
|
||||
var name = String(rec.Name || "").trim();
|
||||
var card = liveEl("article", "dev-card");
|
||||
card.setAttribute("data-name", name);
|
||||
@@ -2474,23 +2493,33 @@
|
||||
card.setAttribute("data-" + pair[2], liveAttrValue(rec, pair));
|
||||
});
|
||||
|
||||
var photo = String(rec.Image || "").trim();
|
||||
var avatar;
|
||||
if (isImageSrc(photo)) {
|
||||
avatar = liveEl("img", "dev-photo");
|
||||
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");
|
||||
// The photo is NOT in the profile payload — the webhook no longer sends `Image`
|
||||
// (it used to ride as inline base64). It comes from its own endpoint, addressed by
|
||||
// the record id: ONE <img>, ONE request (window.O4PExpertImage holds the route and
|
||||
// its query-parameter name in one constant).
|
||||
//
|
||||
// The box is RESERVED (width/height + the .dev-photo CSS), so the card does not
|
||||
// reflow when the photo arrives. A failure — no id, no workflow, a reply that is
|
||||
// not an image — falls back to the SAME monogram a card with no photo already
|
||||
// shows: no broken-image icon, no empty box, no alt text spilling into the layout.
|
||||
var mono = liveMonogram(name);
|
||||
var avatar = mono;
|
||||
var rid = (rec.id === undefined || rec.id === null || rec.id === "") ? "" : String(rec.id);
|
||||
if (rid) {
|
||||
var photo = liveEl("img", "dev-photo");
|
||||
photo.alt = "";
|
||||
photo.width = 72;
|
||||
photo.height = 72;
|
||||
photo.decoding = "async";
|
||||
photo.loading = eager ? "eager" : "lazy";
|
||||
// On failure the <img> is REPLACED by the monogram — a node of the same 72x72
|
||||
// size, so the card does not move. `mono` is captured by name, never the variable
|
||||
// that is reassigned below.
|
||||
photo.addEventListener("error", function () {
|
||||
if (photo.parentNode) photo.parentNode.replaceChild(mono, photo);
|
||||
}, { once: true });
|
||||
photo.src = O4PExpertImage.url(rid, rec);
|
||||
avatar = photo;
|
||||
}
|
||||
var head = liveEl("header", "dev-head");
|
||||
var who = liveEl("div", "dev-id");
|
||||
@@ -2960,7 +2989,9 @@
|
||||
// NO sort: the endpoint's order is rendered exactly as received.
|
||||
var active = liveFilters(records);
|
||||
while (grid.firstChild) grid.removeChild(grid.firstChild);
|
||||
records.forEach(function (rec) { grid.appendChild(liveCard(rec, active)); });
|
||||
records.forEach(function (rec, i) {
|
||||
grid.appendChild(liveCard(rec, active, i < EAGER_CARDS));
|
||||
});
|
||||
// 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;
|
||||
|
||||
Reference in new issue
Block a user