Public Access
fix(freelancer-directory): delete the dark anonymous-contact modal; retire hello@ -> support@ site-wide
Two user decisions (2026-11-03). The page is live.
1. The anonymous-contact modal is removed in full, not just its gate: its markup and
comment, the .contact-modal and .dev-anon-btn CSS, the JS (modalEls, openContactModal,
closeContactModal, initContactModal, enhanceContact), the two init-time calls and the
per-card render call. ODOO_EXPERTS_QUOTE_WEBHOOK is deleted — the only readers were the
two gates (initContactModal, enhanceContact) and the postTo inside the modal's own
submit handler; nothing else read it. The modal's lede ("We pass your request on ...")
goes with it, so no relay copy survives. Kept on purpose: the invite relay form and
ODOO_EXPERTS_INVITE_WEBHOOK, the OCA filter + badge, ODOO_EXPERTS_WEBHOOK, and the
card's direct "Email this freelancer" chip.
2. hello@odoo4projects.com is retired site-wide: 24 occurrences (mailto + visible text)
become support@odoo4projects.com, and the 12 support@ODOO4projects.com case variants
are normalised to lower case. oliver@odoo4projects.com is a freelancer's own published
contact address on their card and is left untouched.
Tests run: verify-html.py PROBLEMS 0; check-inline-js.py OK; competitor QC exit 3 with 74
pre-existing hits and 0 added; impeccable detect 1 -> 1 (identical, 0 new).
This commit is contained in:
1 parent
570085cb04
commit
04199bd606
22 files changed
+32
-267
No files matched your search
@@ -619,7 +619,7 @@
|
||||
}
|
||||
|
||||
/* ---- 2026-11-03n — the skill profile (the shared chart), the OCA Member
|
||||
Profile badge, the anonymous-contact modal, and the focus sliders. ---- */
|
||||
Profile badge, and the focus sliders. ---- */
|
||||
|
||||
/* The one number we publish about a person: our own count. Rendered once, with
|
||||
real weight so a visitor can compare two cards — never as a score or a rank. */
|
||||
@@ -717,26 +717,6 @@
|
||||
color: var(--midnight);
|
||||
}
|
||||
|
||||
/* The anonymous-contact trigger (added by script only while its route is set). */
|
||||
.dev-anon-btn {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
align-self: flex-start;
|
||||
padding: 0.45rem 0.85rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--brass-deep);
|
||||
background: transparent;
|
||||
color: var(--brass-deep);
|
||||
}
|
||||
.dev-anon-btn:hover {
|
||||
background: rgba(201, 162, 39, 0.12);
|
||||
}
|
||||
.dev-anon-btn:focus-visible {
|
||||
outline: 3px solid var(--brass-deep);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ---- The focus sliders (application + admin forms) ---- */
|
||||
.focus-field {
|
||||
min-width: 0;
|
||||
@@ -859,89 +839,6 @@
|
||||
color: #f0a58f;
|
||||
}
|
||||
|
||||
/* ---- The anonymous-contact 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;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -1034,7 +931,7 @@
|
||||
<p class="hero-sub">Independent Odoo specialists you can hire for the piece you cannot do yourself — a custom module, a migration, an integration, an app your business needs. Write to a freelancer directly, using the contact details they publish on their card.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="#directory" class="btn btn-brass btn-lg">Browse the freelancers</a>
|
||||
<a href="mailto:hello@odoo4projects.com?subject=Find%20me%20an%20Odoo%20freelancer" class="btn btn-outline btn-lg">Ask us to find one</a>
|
||||
<a href="mailto:support@odoo4projects.com?subject=Find%20me%20an%20Odoo%20freelancer" class="btn btn-outline btn-lg">Ask us to find one</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1525,7 +1422,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<noscript><p class="form-noscript">This form needs JavaScript. Email <a href="mailto:hello@odoo4projects.com">hello@odoo4projects.com</a> and we will add your listing by hand.</p></noscript>
|
||||
<noscript><p class="form-noscript">This form needs JavaScript. Email <a href="mailto:support@odoo4projects.com">support@odoo4projects.com</a> and we will add your listing by hand.</p></noscript>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1539,7 +1436,7 @@
|
||||
<section class="section-light" id="invite">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Know another Odoo freelancer?</h2>
|
||||
<p class="form-notlive" id="inviteNotLive">Not open yet. Email <a href="mailto:hello@odoo4projects.com?subject=Invite%20a%20freelancer">hello@odoo4projects.com</a> a name and an address and we will invite them by hand — the invitation says who suggested them and carries a one-click opt-out.</p>
|
||||
<p class="form-notlive" id="inviteNotLive">Not open yet. Email <a href="mailto:support@odoo4projects.com?subject=Invite%20a%20freelancer">support@odoo4projects.com</a> a name and an address and we will invite them by hand — the invitation says who suggested them and carries a one-click opt-out.</p>
|
||||
<div class="dir-forms" id="inviteFormWrap" hidden>
|
||||
<form class="form-card" id="inviteForm" novalidate>
|
||||
<p class="form-lede">Send us your name and a fellow freelancer's email address. <strong>We</strong> email them one invitation to sign up — from ODOO4projects, not from your address — and we tell them you suggested it. The invitation carries a one-click opt-out, and we use their address for that invitation only.</p>
|
||||
@@ -1560,7 +1457,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<noscript><p class="form-noscript">This form needs JavaScript. Email <a href="mailto:hello@odoo4projects.com">hello@odoo4projects.com</a> a name and an address and we will invite them by hand.</p></noscript>
|
||||
<noscript><p class="form-noscript">This form needs JavaScript. Email <a href="mailto:support@odoo4projects.com">support@odoo4projects.com</a> a name and an address and we will invite them by hand.</p></noscript>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1573,38 +1470,11 @@
|
||||
</div>
|
||||
<div class="cmp-cta-actions">
|
||||
<a href="#directory" class="btn btn-brass btn-lg">Browse the freelancers</a>
|
||||
<a href="mailto:hello@odoo4projects.com?subject=Find%20me%20an%20Odoo%20freelancer" class="btn btn-outline btn-lg">Ask us to find one</a>
|
||||
<a href="mailto:support@odoo4projects.com?subject=Find%20me%20an%20Odoo%20freelancer" class="btn btn-outline btn-lg">Ask us to find one</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Anonymous contact modal — sends the same shape the relay form sends
|
||||
({freelancers:[{id,name}], brief, email}), with ONE freelancer: this card's
|
||||
record. It ships DARK: the button that opens it is rendered by the page script
|
||||
only when ODOO_EXPERTS_QUOTE_WEBHOOK is set (empty today). The client's email
|
||||
comes to US, never to the freelancer — that is the whole point of the flow. -->
|
||||
<div class="contact-modal" id="contactModal" hidden role="dialog" aria-modal="true" aria-labelledby="contactModalTitle">
|
||||
<div class="contact-modal-card" role="document">
|
||||
<button type="button" class="contact-modal-close" id="contactModalClose" aria-label="Close">×</button>
|
||||
<h3 id="contactModalTitle">Contact this freelancer anonymously</h3>
|
||||
<p class="contact-modal-lede">We pass your request on <strong>without your name or address</strong>, so the freelancer never sees who you are. Your email comes to us — ODOO4projects — and we use it to send their answer back to you. You agree the work itself directly with them.</p>
|
||||
<p class="contact-modal-to" id="contactModalTo"></p>
|
||||
<form id="contactModalForm" novalidate>
|
||||
<div class="form-field">
|
||||
<label for="contactModalEmail">Your email address</label>
|
||||
<input type="email" id="contactModalEmail" name="email" autocomplete="email" required />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="contactModalBrief">What you need</label>
|
||||
<textarea id="contactModalBrief" name="brief" rows="4" required></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-brass">Send anonymously</button>
|
||||
<span class="form-status" id="contactModalStatus" role="status"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer (copied from a currently live selling page; no link to this page is added anywhere) -->
|
||||
@@ -1639,7 +1509,7 @@
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="oca-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
@@ -1685,16 +1555,9 @@
|
||||
// submit, so the page can never confirm an action nothing performs. Set the URL
|
||||
// to switch it on; its note then hides itself. Nothing else changes.
|
||||
//
|
||||
// ODOO_EXPERTS_QUOTE_WEBHOOK gates ONLY the per-card "contact this freelancer
|
||||
// anonymously" modal. The client REQUEST form that used to share this constant
|
||||
// was removed (2026-11-03): the relay is gone, clients contact freelancers
|
||||
// directly. The modal is out of scope for that removal and stays DARK here.
|
||||
// Its success copy must already be a RECEIPT ("we have your request"), never a
|
||||
// delivery claim — a visitor sees it the moment the endpoint goes live.
|
||||
var ODOO_EXPERTS_QUOTE_WEBHOOK = "";
|
||||
//
|
||||
// Setting this endpoint switches the INVITE form on. Same rule: receipt, not a
|
||||
// delivery claim.
|
||||
// Setting this endpoint switches the INVITE form on. Its success copy must already
|
||||
// be a RECEIPT ("we have recorded your invitation"), never a delivery claim — a
|
||||
// visitor sees it the moment the endpoint goes live.
|
||||
var ODOO_EXPERTS_INVITE_WEBHOOK = "";
|
||||
|
||||
// ---- The shared constants (the same lists live in build-directory.py) --------
|
||||
@@ -1923,101 +1786,6 @@
|
||||
return payload;
|
||||
}
|
||||
|
||||
/* ---- The modal + the per-card anonymous-contact button ---------------------
|
||||
Both ship DARK: they are only wired when ODOO_EXPERTS_QUOTE_WEBHOOK is set, so a
|
||||
stub route can never be submitted to and no dead button is ever rendered. The
|
||||
client's email goes to US, never to the freelancer. */
|
||||
function modalEls() {
|
||||
return {
|
||||
modal: document.getElementById("contactModal"),
|
||||
to: document.getElementById("contactModalTo"),
|
||||
form: document.getElementById("contactModalForm"),
|
||||
email: document.getElementById("contactModalEmail"),
|
||||
brief: document.getElementById("contactModalBrief"),
|
||||
status: document.getElementById("contactModalStatus"),
|
||||
close: document.getElementById("contactModalClose")
|
||||
};
|
||||
}
|
||||
|
||||
var lastFocus = null;
|
||||
|
||||
function openContactModal(id, name) {
|
||||
var m = modalEls();
|
||||
if (!m.modal || !m.form) return;
|
||||
lastFocus = document.activeElement;
|
||||
m.form.dataset.flId = (id === undefined || id === null) ? "" : String(id);
|
||||
m.form.dataset.flName = name || "";
|
||||
if (m.to) m.to.textContent = name ? ("To: " + name) : "";
|
||||
setStatus(m.status, "", null);
|
||||
m.modal.hidden = false;
|
||||
if (m.email) m.email.focus();
|
||||
}
|
||||
|
||||
function closeContactModal() {
|
||||
var m = modalEls();
|
||||
if (!m.modal) return;
|
||||
m.modal.hidden = true;
|
||||
if (lastFocus && lastFocus.focus) lastFocus.focus();
|
||||
}
|
||||
|
||||
function initContactModal() {
|
||||
if (!ODOO_EXPERTS_QUOTE_WEBHOOK) return; // dark
|
||||
var m = modalEls();
|
||||
if (!m.modal || !m.form) return;
|
||||
if (m.close) m.close.addEventListener("click", closeContactModal);
|
||||
m.modal.addEventListener("click", function (e) {
|
||||
if (e.target === m.modal) closeContactModal();
|
||||
});
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape" && !m.modal.hidden) closeContactModal();
|
||||
});
|
||||
m.form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
if (!m.form.reportValidity()) return;
|
||||
var idRaw = m.form.dataset.flId;
|
||||
var payload = {
|
||||
freelancers: [{
|
||||
id: idRaw === "" ? null : Number(idRaw),
|
||||
name: m.form.dataset.flName || ""
|
||||
}],
|
||||
brief: m.brief ? m.brief.value.trim() : "",
|
||||
email: m.email ? m.email.value.trim() : ""
|
||||
};
|
||||
var submit = m.form.querySelector('button[type="submit"]');
|
||||
if (submit) submit.disabled = true;
|
||||
setStatus(m.status, "Sending…", null);
|
||||
postTo(ODOO_EXPERTS_QUOTE_WEBHOOK, payload)
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
setStatus(m.status,
|
||||
"Thank you — we have your request. We will come back to you by email at the address you gave.",
|
||||
"ok");
|
||||
m.form.reset();
|
||||
})
|
||||
.catch(function () {
|
||||
setStatus(m.status,
|
||||
"That did not send. Please try again, or email us at hello@odoo4projects.com and we will pass it on.",
|
||||
"err");
|
||||
})
|
||||
.then(function () { if (submit) submit.disabled = false; });
|
||||
});
|
||||
}
|
||||
|
||||
/* Add the button to every card (build-time and runtime alike) — ONLY while the
|
||||
route exists. Nothing is added when it is empty. */
|
||||
function enhanceContact(card) {
|
||||
if (!ODOO_EXPERTS_QUOTE_WEBHOOK) return;
|
||||
if (card.querySelector(".dev-anon-btn")) return;
|
||||
var btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "dev-anon-btn";
|
||||
btn.textContent = "Contact this freelancer anonymously";
|
||||
btn.addEventListener("click", function () {
|
||||
openContactModal(card.getAttribute("data-id"), card.getAttribute("data-name") || "");
|
||||
});
|
||||
card.appendChild(btn);
|
||||
}
|
||||
|
||||
/* ---- 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
|
||||
@@ -2258,7 +2026,6 @@
|
||||
card.appendChild(liveEl("p", "dev-contact-none",
|
||||
"No contact details published for this freelancer."));
|
||||
}
|
||||
enhanceContact(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
@@ -2523,8 +2290,6 @@
|
||||
wireRelay("inviteFormWrap", "inviteNotLive", ODOO_EXPERTS_INVITE_WEBHOOK);
|
||||
initFocusUI(document.getElementById("applyForm"), "apFocusWarn", "apFocusTotal");
|
||||
initFocusCounter(document.getElementById("applyForm"), "apFocus", "apFocusCount");
|
||||
initContactModal();
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".dev-card"), enhanceContact);
|
||||
initFilters();
|
||||
initLiveList();
|
||||
initApplyForm();
|
||||
|
||||
Reference in new issue
Block a user