feat(freelancer-directory): anonymous-contact modal live, description cap, copy removals, Get-listed modals, Best Practices accordion

Publishes the accumulated freelancer-directory work (six held passes) plus the
contact-modal success copy the user supplied today.

1. Anonymous-question button + modal — LIVE. ODOO_EXPERTS_QUOTE_WEBHOOK is now
   set, so the per-card "Ask question anonymously" button and its modal render.
   The button is added by script only on cards marked data-relay (a record we can
   relay to); the modal posts {freelancers:[{id,name}], brief, email} to the
   odoo_experts_quote route. With the constant empty the surface stays dark and
   no dead button is rendered.
2. Description cap 230 — the "About you" textarea is capped at 230 characters
   with a live counter (DESC_MAX_CHARS); the same cap truncates a longer legacy
   value on the render path. The matching change to the owner's edit form lives
   in odoo-community-developers/admin/index.html (included here).
3. Removal of "Ask us to find one" — the dead call to action is gone.
4. Copy removals — the invitation swap, four lede sentences, the whole invite
   section, and the Start line are removed.
5. Two "Get listed" buttons + the application form as a modal — the ONE
   #applyForm stays inline in the served HTML (works with JS off) and is moved
   into #applyModal once when the script runs; the data-apply-open anchors then
   open the dialog instead of jumping, and are working links with JS off.
6. Best Practices accordion — 11 <details name="best-practices">, all CLOSED on
   load; at most one open via the shared name attribute plus a JS fallback.

Contact-modal success copy (user, 2026-11-03): the receipt clause is kept and the
user's two promises are used in their words —

  "Thank you — we have your question. We will contact the freelancer, and as soon
   as we have their answer we will come back to you."

The sending and error states are unchanged. NOTE: both promises (the relay and
the reply) depend on the n8n workflow that does not exist yet — the endpoint is a
stub. The owner took that decision knowingly; the copy ships as asked.

QC: verify-html.py 0 · check-inline-js OK · competitor QC exit 3, 74 site-wide
pre-existing, 0 in our files · impeccable detect --no-advisory dir 14 (0 new).
No form submitted.
This commit is contained in:
webmaster committed 2026-11-03 15:19:15 +00:00
1 parent a0ef97b48d
commit 5535f10e7d
2 files changed
+600 -120

No files matched your search

+21 -2
View File
@@ -350,8 +350,9 @@
<span class="focus-counter" id="adFocusCount" aria-live="polite">0 / 70</span>
</div>
<div class="form-field full">
<label for="adDescription">Description</label>
<textarea id="adDescription" name="description" rows="4"></textarea>
<label for="adDescription">Description <span class="hint">(at most 230 characters)</span></label>
<textarea id="adDescription" name="description" rows="4" maxlength="230" aria-describedby="adDescriptionCount"></textarea>
<span class="focus-counter" id="adDescriptionCount" aria-live="polite">0 / 230</span>
</div>
<div class="form-field">
<label for="adCountry">Country</label>
@@ -727,6 +728,8 @@
var FOCUS_TOTAL = 15;
var FOCUS_MAX = 5;
var FOCUS_MAX_CHARS = 70;
// The description is capped at 230 characters, same as the public application form.
var DESC_MAX_CHARS = 230;
var LANG_FIELDS = [["english", "English"], ["german", "German"], ["spanish", "Spanish"]];
// Country VALUE FORMAT — what the Country select sends to the webhook.
// "code" → the ISO 3166-1 alpha-2 code, e.g. "DE" (default)
@@ -874,6 +877,11 @@
if (focusInput && focusCount) {
focusCount.textContent = focusInput.value.length + " / " + FOCUS_MAX_CHARS;
}
var descInput = document.getElementById("adDescription");
var descCount = document.getElementById("adDescriptionCount");
if (descInput && descCount) {
descCount.textContent = descInput.value.length + " / " + DESC_MAX_CHARS;
}
}
function loadRecord() {
@@ -1001,6 +1009,17 @@
focusInput.addEventListener("input", refreshCount);
refreshCount();
}
var descInput = document.getElementById("adDescription");
var descCount = document.getElementById("adDescriptionCount");
if (descInput && descCount) {
var refreshDescCount = function () {
var n = descInput.value.length;
descCount.textContent = n + " / " + DESC_MAX_CHARS;
descCount.setAttribute("data-over", n > DESC_MAX_CHARS ? "1" : "0");
};
descInput.addEventListener("input", refreshDescCount);
refreshDescCount();
}
refreshFocus(form);
form.addEventListener("submit", function (event) {