From 5535f10e7dd1ec4a8d1eb6e67cf80a50e0e9b837 Mon Sep 17 00:00:00 2001 From: webmaster Date: Tue, 3 Nov 2026 15:19:15 +0000 Subject: [PATCH] feat(freelancer-directory): anonymous-contact modal live, description cap, copy removals, Get-listed modals, Best Practices accordion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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
, 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. --- odoo-community-developers/admin/index.html | 23 +- odoo-community-developers/index.html | 697 +++++++++++++++++---- 2 files changed, 600 insertions(+), 120 deletions(-) diff --git a/odoo-community-developers/admin/index.html b/odoo-community-developers/admin/index.html index af99f25..13144e3 100644 --- a/odoo-community-developers/admin/index.html +++ b/odoo-community-developers/admin/index.html @@ -350,8 +350,9 @@ 0 / 70
- - + + + 0 / 230
@@ -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) { diff --git a/odoo-community-developers/index.html b/odoo-community-developers/index.html index 5c2eac1..8e8b7dc 100644 --- a/odoo-community-developers/index.html +++ b/odoo-community-developers/index.html @@ -4,10 +4,10 @@ Hire an Odoo Developer for Your Project - + - + @@ -429,7 +429,22 @@ .dev-links a { color: var(--brass-deep); } - /* A card with no published contact says so — it never shows an empty space. */ + /* The "Ask question anonymously" trigger — added by script only while its route is + set. Styled as a link so it sits in the links row where the mailto chip was. */ + .dev-links button { + font: inherit; + padding: 0; + border: 0; + background: none; + color: var(--brass-deep); + text-decoration: underline; + cursor: pointer; + } + .dev-links button:focus-visible { + outline: 3px solid var(--brass-deep); + outline-offset: 2px; + } + /* A card with no contact route of any kind says so — it never shows an empty space. */ .dev-contact-none { margin: 0; font-size: 0.82rem; @@ -437,6 +452,99 @@ color: var(--ink-soft); } + /* ---- The ask-a-question modal (ships dark; opened only when wired) ---- */ + .contact-modal[hidden] { + display: none; + } + .contact-modal { + position: fixed; + inset: 0; + z-index: 60; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background: rgba(13, 27, 46, 0.72); + } + .contact-modal-card { + position: relative; + width: 100%; + max-width: 520px; + background: var(--ivory); + border: 1px solid var(--line-brass); + border-radius: var(--radius-lg); + padding: 1.5rem 1.6rem 1.3rem; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35); + max-height: 92vh; + overflow: auto; + } + .contact-modal-card h3 { + font-family: Marcellus, Georgia, serif; + font-weight: 400; + font-size: 1.25rem; + margin: 0 0 0.4rem; + color: var(--midnight); + } + .contact-modal-lede { + font-size: 0.86rem; + line-height: 1.65; + color: var(--ink-soft); + margin: 0 0 0.9rem; + } + .contact-modal-close { + position: absolute; + top: 0.6rem; + right: 0.7rem; + background: transparent; + border: 0; + font-size: 1.4rem; + line-height: 1; + cursor: pointer; + color: var(--ink-soft); + padding: 0.2rem 0.4rem; + } + .contact-modal-close:focus-visible { + outline: 3px solid var(--brass-deep); + outline-offset: 2px; + } + .contact-modal .form-field { + display: flex; + flex-direction: column; + gap: 0.35rem; + margin-bottom: 0.8rem; + } + .contact-modal .form-field label { + font-size: 0.8rem; + font-weight: 600; + color: var(--midnight); + } + .contact-modal .form-field input, + .contact-modal .form-field textarea { + font-family: Inter, sans-serif; + font-size: 0.9rem; + padding: 0.6rem 0.7rem; + border-radius: var(--radius); + border: 1px solid var(--line); + background: #fff; + color: var(--ink); + width: 100%; + } + .contact-modal-to { + font-size: 0.85rem; + color: var(--ink); + margin: 0 0 0.8rem; + } + /* The apply modal's card is wider than the question modal: the apply form is a + two-column grid. Card stays ivory, so the form reads the same moved or inline. */ + .apply-modal-card { + max-width: 760px; + } + /* A "Get listed" trigger: centred under the copy it belongs to. */ + .dir-cta { + text-align: center; + margin: 1.75rem auto 0; + } + /* The three forms */ .dir-forms { max-width: 860px; @@ -850,6 +958,79 @@ gap: 0.4rem; } } + + /* ---- Best Practices accordion (page-scoped, additive). Reuses the house FAQ card + and "+/-" marker; items are full-width, single column. Exclusivity comes from the + native
attribute, with a small JS fallback in the page script. All + items are CLOSED on load — nothing carries `open`. ---- */ + .bp-lede { + max-width: 760px; + margin: 0 0 1.5rem; + font-size: 0.9rem; + line-height: 1.7; + color: var(--ink-soft); + } + .bp-list { + display: grid; + gap: 0.75rem; + max-width: 900px; + } + .bp-item { + background: var(--ivory); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + padding: 0.35rem 1.25rem; + box-shadow: var(--shadow-rest); + transition: box-shadow 0.25s; + } + .bp-item[open], + .bp-item:hover { + box-shadow: var(--shadow-hover); + } + .bp-item summary { + cursor: pointer; + list-style: none; + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.9rem 0; + font-family: Marcellus, Georgia, serif; + font-size: 1rem; + color: var(--midnight); + } + .bp-item summary::-webkit-details-marker { + display: none; + } + .bp-item summary::after { + content: "+"; + font-family: Inter, sans-serif; + font-size: 1.25rem; + line-height: 1; + color: var(--brass); + flex-shrink: 0; + transition: transform 0.25s; + } + .bp-item[open] summary::after { + content: "\2212"; + } + .bp-item summary:focus-visible { + outline: 2px solid var(--brass); + outline-offset: 3px; + border-radius: var(--radius); + } + .bp-q { + flex: 1; + min-width: 0; + } + .bp-a { + font-size: 0.9rem; + line-height: 1.7; + color: var(--ink-soft); + padding: 0 0 1rem; + } + .bp-a p:last-child { + margin-bottom: 0; + } + +