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:
webmaster committed 2026-11-03 22:59:53 +00:00
1 parent 9d14652a57
commit 2b0e89191a
3 files changed
+134 -71

No files matched your search

+30 -46
View File
@@ -149,6 +149,7 @@
<!-- The 15-point rule, in ONE copy for both forms (this one and the application form). -->
<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>
<!-- 1) Read the credential, then take it OUT of the address bar.
The hash arrives as ?has=... or ?hash=... (both spellings accepted, 'has' first — the
@@ -574,53 +575,36 @@
form.appendChild(linkField("edFiverr", "fiverr", "Fiverr URL",
readField(record, "fiverr"), "https://www.fiverr.com/…"));
// The photo is editable here with the SAME rules as the application form
// (static/photo-upload.js). A picked file is validated and downscaled on the spot;
// leaving it untouched keeps the record's own Image, which rides back byte-identical
// because the save sends the whole record (see the submit handler below).
var newImage = null;
// The photo block is a LOOK, not a field. The photo does not live in this record
// any more — the payload carries no `Image` — it comes from its own endpoint,
// addressed by the record id (window.O4PExpertImage). So nothing here edits it,
// and the save below sends no `Image` key at all: not the value, not null, not "".
var photoWrap = el("div", "field");
var photoLabel = el("label", "", "Photo");
photoLabel.setAttribute("for", "edImage");
photoWrap.appendChild(photoLabel);
photoWrap.appendChild(el("label", "", "Photo"));
var photoRow = el("div", "photo-readonly");
var img = readField(record, "Image");
var currentImage = (typeof img === "string" && (img.indexOf("https://") === 0 || img.indexOf("data:image/") === 0)) ? img : null;
var previewImg = el("img");
previewImg.id = "edImagePreview";
previewImg.alt = "";
if (currentImage) {
previewImg.src = currentImage;
photoRow.appendChild(previewImg);
} else {
previewImg.hidden = true;
photoRow.appendChild(previewImg);
photoRow.appendChild(el("span", "mono", initials(who)));
var photoMono = el("span", "mono", initials(who));
photoRow.appendChild(photoMono);
if (rec && rec.id !== undefined && rec.id !== null && rec.id !== "") {
var photoImg = el("img");
photoImg.id = "edImagePreview";
photoImg.alt = "";
photoImg.decoding = "async";
photoImg.hidden = true; // the monogram stands in until the photo is there
photoImg.addEventListener("load", function () {
if (photoMono.parentNode) photoMono.parentNode.removeChild(photoMono);
photoImg.hidden = false;
}, { once: true });
photoImg.addEventListener("error", function () {
// A failing photo is the monogram: no broken-image icon, no empty box.
if (photoImg.parentNode) photoImg.parentNode.removeChild(photoImg);
}, { once: true });
photoImg.src = O4PExpertImage.url(String(rec.id), rec);
photoRow.appendChild(photoImg);
}
photoWrap.appendChild(photoRow);
var photoInput = el("input");
photoInput.type = "file";
photoInput.id = "edImage";
photoInput.name = "Image";
photoInput.accept = "image/jpeg,image/png,image/webp";
photoWrap.appendChild(photoInput);
var photoResult = el("p", "hint", "");
photoResult.id = "edImageResult";
photoResult.setAttribute("role", "status");
photoResult.setAttribute("aria-live", "polite");
photoWrap.appendChild(photoResult);
photoWrap.appendChild(el("p", "hint",
"JPEG, PNG or WebP, up to 8 MB — we resize it to a square of at most 384 px first. " +
"Leave it to keep your current photo."));
O4PPhoto.wire(photoInput, previewImg, photoResult, function (dataUrl) {
newImage = dataUrl;
var fb = photoRow.querySelector(".mono");
if (fb) fb.hidden = !!dataUrl;
if (dataUrl) return; // wire already shows the new image
// reverted to "no new photo": show the record's own photo again
if (currentImage) { previewImg.src = currentImage; previewImg.hidden = false; }
else previewImg.hidden = true;
});
"Your photo comes from your listing and is not editable here — reply to the email " +
"that brought you here if it needs to change."));
form.appendChild(photoWrap);
var save = el("button", "btn", "Save my changes");
@@ -686,10 +670,10 @@
else if (had) payload[pair[1]] = "";
});
// An untouched photo rides back exactly as loaded — it came in with the record
// above and was never touched, so it stays byte-identical. Only a NEWLY picked
// photo (already validated and downscaled) replaces it.
if (newImage !== null) payload.Image = newImage;
// The photo does NOT ride in this record any more: the payload carries no
// `Image` key and this form sends none — not the value, not null, not "". The
// delete is the guarantee, whatever a reply may one day carry.
delete payload.Image;
fetch(C.webhook, {
method: C.saveMethod,