Public Access
fix(freelancer-directory): apply the ICP's five ship-blockers
- hide the inert client request form; announce it as coming soon with an email fallback (no reachable error state, no submit that refuses a completed brief) - soften the contact claim to 'where a freelancer published their details' and make a card with no contact say so instead of showing an empty space - move the admin add/edit panel off the public page to its own draft page (/odoo-community-developers/admin/), unlinked, reachable only by URL - admin edit: load-and-confirm the record before writing, and read-modify-write the booleans so an untouched OCA_member/is_individual keeps its current value - application form: add Country + Odoo versions (the filters' inputs), GitHub and OCA profile URLs, and an explicit email-publication consent line - tighten 'How this works' from ~120 words to two short options + one caveat - build-directory.py: skip records with no Name; render GitHub/OCA links and the no-contact line; read Country via alias
This commit is contained in:
1 parent
e8c93d89ad
commit
c74315e950
2 files changed
+604
-242
No files matched your search
@@ -0,0 +1,548 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Freelancer records — admin</title>
|
||||||
|
<!-- DRAFT / INTERNAL: not indexed, not linked, not in sitemap.xml / llms.txt.
|
||||||
|
Reachable only by this URL. All five draft conditions hold
|
||||||
|
(noindex · not in sitemap · not in llms.txt · no inbound links · no announcement
|
||||||
|
surface). Do NOT link to this page from anywhere — not from the directory, not
|
||||||
|
from the footer. Remove this robots meta only if it ever becomes public. -->
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<link rel="canonical" href="https://odoo4projects.com/odoo-community-developers/admin/" />
|
||||||
|
<link rel="icon" href="/images/favicon.ico" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap"
|
||||||
|
media="print"
|
||||||
|
onload="this.media='all'"
|
||||||
|
/>
|
||||||
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||||
|
<link rel="stylesheet" href="/style.css" />
|
||||||
|
<style>
|
||||||
|
/* ---- Freelancer records — admin (page-scoped). Internal page, never linked. ---- */
|
||||||
|
.adm-bar {
|
||||||
|
background: var(--midnight-2);
|
||||||
|
border-bottom: 1px solid var(--line-brass);
|
||||||
|
padding: 0.85rem 0;
|
||||||
|
}
|
||||||
|
.adm-bar .container {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.adm-brand {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.adm-brand span {
|
||||||
|
color: var(--brass);
|
||||||
|
}
|
||||||
|
.adm-tag {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.adm-wrap {
|
||||||
|
padding: 2.5rem 0 4rem;
|
||||||
|
}
|
||||||
|
.adm-wrap h1 {
|
||||||
|
font-family: Marcellus, Georgia, serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.9rem;
|
||||||
|
color: var(--midnight);
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
|
}
|
||||||
|
.adm-note {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 0 1.75rem;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.adm-note code {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--midnight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
max-width: 860px;
|
||||||
|
background: var(--ivory);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow-rest);
|
||||||
|
}
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.form-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
.form-field.full,
|
||||||
|
.form-grid > .full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.form-field label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--midnight);
|
||||||
|
}
|
||||||
|
.form-field .hint {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
.form-field input,
|
||||||
|
.form-field textarea,
|
||||||
|
.form-field select {
|
||||||
|
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%;
|
||||||
|
}
|
||||||
|
.form-check {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.6rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
.form-check input {
|
||||||
|
width: auto;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.form-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
.form-status {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
.form-status[data-state="ok"] {
|
||||||
|
color: var(--success, #3f7d4f);
|
||||||
|
}
|
||||||
|
.form-status[data-state="err"] {
|
||||||
|
color: #b3452f;
|
||||||
|
}
|
||||||
|
.adm-loaded {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
.adm-loaded[data-state="ok"] {
|
||||||
|
color: var(--success, #3f7d4f);
|
||||||
|
}
|
||||||
|
.adm-loaded[data-state="err"] {
|
||||||
|
color: #b3452f;
|
||||||
|
}
|
||||||
|
.adm-sep {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
margin: 0.35rem 0;
|
||||||
|
}
|
||||||
|
/* Page-local quiet button: the shared .btn-outline is cream-on-transparent (built
|
||||||
|
for the dark sections) and would be near-invisible on this page's ivory card. */
|
||||||
|
.btn-quiet {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink);
|
||||||
|
border-color: rgba(23, 28, 38, 0.22);
|
||||||
|
}
|
||||||
|
.btn-quiet:hover {
|
||||||
|
color: var(--brass-deep);
|
||||||
|
border-color: var(--brass-deep);
|
||||||
|
background: rgba(201, 162, 39, 0.08);
|
||||||
|
}
|
||||||
|
.btn:focus-visible {
|
||||||
|
outline: 3px solid var(--brass-deep);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="adm-bar">
|
||||||
|
<div class="container">
|
||||||
|
<span class="adm-brand">ODOO4<span>projects</span></span>
|
||||||
|
<span class="adm-tag">Freelancer records — internal</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="adm-wrap">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Add or correct a freelancer record</h1>
|
||||||
|
<p class="adm-note">
|
||||||
|
Internal page — not linked from the directory. Add a new record, or load an existing one by
|
||||||
|
its id, <strong>confirm it is the right person</strong>, then correct what is wrong. A save
|
||||||
|
posts to the one <code>odoo_experts</code> webhook and is applied after email confirmation.
|
||||||
|
Fields you leave alone keep the record's current value.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form class="form-card" id="adminForm" novalidate>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adId">Record id <span class="hint">(leave blank to create a new record)</span></label>
|
||||||
|
<input id="adId" name="id" type="number" min="1" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adLoadBtn">Confirm before you edit</label>
|
||||||
|
<button type="button" class="btn btn-quiet" id="adLoadBtn">Load this record</button>
|
||||||
|
</div>
|
||||||
|
<p class="adm-loaded" id="adLoadInfo" role="status">Enter a record id and load it — you will see who it is before anything is written.</p>
|
||||||
|
<hr class="adm-sep" />
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adName">Name</label>
|
||||||
|
<input id="adName" name="Name" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adEmail">Email</label>
|
||||||
|
<input id="adEmail" name="email" type="email" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adClients">Projects on ODOO4projects</label>
|
||||||
|
<input id="adClients" name="odoo4projects_clients" type="number" min="0" />
|
||||||
|
<span class="hint">Our own count, not the freelancer's.</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adFocus">Focus</label>
|
||||||
|
<input id="adFocus" name="Focus" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field full">
|
||||||
|
<label for="adDescription">Description</label>
|
||||||
|
<textarea id="adDescription" name="description" rows="4"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adCountry">Country</label>
|
||||||
|
<input id="adCountry" name="Country" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adVersions">Odoo versions</label>
|
||||||
|
<input id="adVersions" name="odoo_versions" type="text" />
|
||||||
|
<span class="hint">Separate with commas.</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adLanguages">Languages</label>
|
||||||
|
<input id="adLanguages" name="languages" type="text" />
|
||||||
|
<span class="hint">Separate with commas.</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adSpecialities">Specialities</label>
|
||||||
|
<input id="adSpecialities" name="specialities" type="text" />
|
||||||
|
<span class="hint">Separate with commas.</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adGithub">GitHub URL</label>
|
||||||
|
<input id="adGithub" name="github" type="url" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adOca">OCA profile URL</label>
|
||||||
|
<input id="adOca" name="oca_profile" type="url" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adLinkedin">LinkedIn URL</label>
|
||||||
|
<input id="adLinkedin" name="Linkedin" type="url" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adUpwork">Upwork URL</label>
|
||||||
|
<input id="adUpwork" name="upwork" type="url" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adFiverr">Fiverr URL</label>
|
||||||
|
<input id="adFiverr" name="fiverr" type="url" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="adImage">Image URL</label>
|
||||||
|
<input id="adImage" name="Image" type="url" />
|
||||||
|
</div>
|
||||||
|
<label class="form-check">
|
||||||
|
<input type="checkbox" name="OCA_member" value="1" />
|
||||||
|
<span>OCA member.</span>
|
||||||
|
</label>
|
||||||
|
<label class="form-check">
|
||||||
|
<input type="checkbox" name="is_individual" value="1" />
|
||||||
|
<span>Works as an individual.</span>
|
||||||
|
</label>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-brass">Save this record</button>
|
||||||
|
<span class="form-status" id="adminStatus" role="status"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ============================================================================
|
||||||
|
Freelancer records — admin page script.
|
||||||
|
|
||||||
|
ONE webhook, for everything. The freelancer platform is a SEPARATE system from
|
||||||
|
the CRM: there is no /crm, no /crm_odoo and nothing from the CRM skill here.
|
||||||
|
|
||||||
|
Write contract (same URL, id decides):
|
||||||
|
POST with an id → UPDATE that record
|
||||||
|
POST without an id → CREATE a new record
|
||||||
|
Email confirmation applies to both paths.
|
||||||
|
|
||||||
|
READ-MODIFY-WRITE (the reason this page loads the record first): a boolean the
|
||||||
|
owner did not touch must keep the record's CURRENT value. Loading the record
|
||||||
|
syncs the checkboxes to it, so an untouched box sends what the record already
|
||||||
|
holds instead of flipping it to false. An edit is refused until the record has
|
||||||
|
been loaded and confirmed — a bare numeric id can no longer edit the wrong person.
|
||||||
|
============================================================================ */
|
||||||
|
var ODOO_EXPERTS_WEBHOOK =
|
||||||
|
"https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts";
|
||||||
|
|
||||||
|
var MULTI_FIELDS = ["languages", "specialities", "odoo_versions"]; // comma-separated → array
|
||||||
|
var BOOL_FIELDS = ["OCA_member", "is_individual"];
|
||||||
|
var NUM_FIELDS = ["odoo4projects_clients"];
|
||||||
|
var TEXT_FIELDS = [
|
||||||
|
"Name", "email", "Focus", "description", "Country",
|
||||||
|
"Linkedin", "upwork", "fiverr", "github", "oca_profile", "Image"
|
||||||
|
];
|
||||||
|
|
||||||
|
var loadedRecord = null;
|
||||||
|
|
||||||
|
function fieldElements(form, name) {
|
||||||
|
return Array.prototype.slice.call(
|
||||||
|
form.querySelectorAll('[name="' + name.replace(/"/g, '\\"') + '"]')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(el, message, state) {
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = message;
|
||||||
|
if (state) el.setAttribute("data-state", state);
|
||||||
|
else el.removeAttribute("data-state");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The webhook casing is inconsistent; read a field's value by its known names. */
|
||||||
|
function recordField(rec, name) {
|
||||||
|
if (rec[name] !== undefined) return rec[name];
|
||||||
|
if (name === "Country" && rec.country !== undefined) return rec.country;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinMulti(value) {
|
||||||
|
if (value == null) return "";
|
||||||
|
if (Array.isArray(value)) return value.join(", ");
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy a loaded record into the form, so an untouched field carries the record's
|
||||||
|
current value through to the write. */
|
||||||
|
function populate(rec) {
|
||||||
|
var form = document.getElementById("adminForm");
|
||||||
|
TEXT_FIELDS.forEach(function (name) {
|
||||||
|
var el = form.querySelector('[name="' + name + '"]');
|
||||||
|
if (!el) return;
|
||||||
|
var v = recordField(rec, name);
|
||||||
|
el.value = v == null ? "" : String(v);
|
||||||
|
});
|
||||||
|
MULTI_FIELDS.forEach(function (name) {
|
||||||
|
var el = form.querySelector('[name="' + name + '"]');
|
||||||
|
if (el) el.value = joinMulti(recordField(rec, name));
|
||||||
|
});
|
||||||
|
var clients = form.querySelector('[name="odoo4projects_clients"]');
|
||||||
|
if (clients) {
|
||||||
|
var n = rec.odoo4projects_clients;
|
||||||
|
clients.value = n == null ? "" : String(n);
|
||||||
|
}
|
||||||
|
BOOL_FIELDS.forEach(function (name) {
|
||||||
|
var box = form.querySelector('[name="' + name + '"]');
|
||||||
|
if (!box) return;
|
||||||
|
var val = rec[name] === true;
|
||||||
|
box.checked = val;
|
||||||
|
box.dataset.loaded = val ? "true" : "false";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadRecord() {
|
||||||
|
var form = document.getElementById("adminForm");
|
||||||
|
var info = document.getElementById("adLoadInfo");
|
||||||
|
var idRaw = form.querySelector('[name="id"]').value.trim();
|
||||||
|
loadedRecord = null;
|
||||||
|
if (!idRaw) {
|
||||||
|
setStatus(info, "Enter a record id first — or leave it blank to create a new record.", "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus(info, "Loading record " + idRaw + "…", null);
|
||||||
|
fetch(ODOO_EXPERTS_WEBHOOK, { method: "GET" })
|
||||||
|
.then(function (r) {
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(function (payload) {
|
||||||
|
var list = Array.isArray(payload) ? payload : (payload && payload.data) || [];
|
||||||
|
var rec = null;
|
||||||
|
list.forEach(function (r) {
|
||||||
|
if (String(r.id) === String(idRaw)) rec = r;
|
||||||
|
});
|
||||||
|
if (!rec) {
|
||||||
|
setStatus(info, "No record with id " + idRaw + ". Nothing was loaded.", "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadedRecord = rec;
|
||||||
|
populate(rec);
|
||||||
|
var who = rec.Name ? rec.Name : "(no name)";
|
||||||
|
setStatus(
|
||||||
|
info,
|
||||||
|
"Loaded: " + who + " — id " + rec.id + ". Check this is the right person, then edit and save. Fields you leave alone keep their current value.",
|
||||||
|
"ok"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
setStatus(info, "Could not read the record list. Try again.", "err");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPayload(form, withId) {
|
||||||
|
var payload = {};
|
||||||
|
var names = [];
|
||||||
|
Array.prototype.forEach.call(form.querySelectorAll("[name]"), function (el) {
|
||||||
|
if (names.indexOf(el.name) === -1) names.push(el.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
names.forEach(function (name) {
|
||||||
|
var els = fieldElements(form, name);
|
||||||
|
if (!els.length) return;
|
||||||
|
|
||||||
|
if (name === "id") {
|
||||||
|
if (!withId) return;
|
||||||
|
var raw = els[0].value.trim();
|
||||||
|
if (raw) payload.id = Number(raw);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MULTI_FIELDS.indexOf(name) !== -1) {
|
||||||
|
var text = els[0].value || "";
|
||||||
|
var list = text
|
||||||
|
.split(",")
|
||||||
|
.map(function (s) {
|
||||||
|
return s.trim();
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
if (list.length) payload[name] = list; // an empty box must not wipe the field
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BOOL_FIELDS.indexOf(name) !== -1) {
|
||||||
|
var box = els[0];
|
||||||
|
if (withId && box.dataset.loaded !== undefined) {
|
||||||
|
// READ-MODIFY-WRITE: an untouched box keeps the record's current value.
|
||||||
|
var original = box.dataset.loaded === "true";
|
||||||
|
var now = !!box.checked;
|
||||||
|
payload[name] = now === original ? original : now;
|
||||||
|
} else {
|
||||||
|
payload[name] = !!box.checked;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NUM_FIELDS.indexOf(name) !== -1) {
|
||||||
|
var n = els[0].value.trim();
|
||||||
|
if (n !== "") payload[name] = Number(n);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = els[0].value.trim();
|
||||||
|
if (value !== "") payload[name] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function postToWebhook(payload) {
|
||||||
|
return fetch(ODOO_EXPERTS_WEBHOOK, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(function init() {
|
||||||
|
var form = document.getElementById("adminForm");
|
||||||
|
if (!form) return;
|
||||||
|
var info = document.getElementById("adLoadInfo");
|
||||||
|
var status = document.getElementById("adminStatus");
|
||||||
|
|
||||||
|
document.getElementById("adLoadBtn").addEventListener("click", loadRecord);
|
||||||
|
|
||||||
|
// Changing the id after a load invalidates the confirmation.
|
||||||
|
form.querySelector('[name="id"]').addEventListener("input", function () {
|
||||||
|
if (loadedRecord && String(loadedRecord.id) !== this.value.trim()) {
|
||||||
|
loadedRecord = null;
|
||||||
|
setStatus(info, "The id changed — load the record again to confirm who you are editing.", "err");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener("submit", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!form.reportValidity()) return;
|
||||||
|
var idRaw = form.querySelector('[name="id"]').value.trim();
|
||||||
|
var hasId = !!idRaw;
|
||||||
|
|
||||||
|
if (hasId && (!loadedRecord || String(loadedRecord.id) !== idRaw)) {
|
||||||
|
setStatus(
|
||||||
|
status,
|
||||||
|
"Load the record first (Load this record) and confirm it is the right person. Nothing was saved.",
|
||||||
|
"err"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var submit = form.querySelector('button[type="submit"]');
|
||||||
|
submit.disabled = true;
|
||||||
|
setStatus(status, "Saving…", null);
|
||||||
|
|
||||||
|
postToWebhook(buildPayload(form, true))
|
||||||
|
.then(function () {
|
||||||
|
setStatus(
|
||||||
|
status,
|
||||||
|
hasId
|
||||||
|
? "Saved. Check your inbox to confirm the change before it is applied."
|
||||||
|
: "New record sent. Check your inbox to confirm it before it goes live.",
|
||||||
|
"ok"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
setStatus(status, "That did not save. Please try again.", "err");
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
submit.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -4,14 +4,14 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Find an Odoo Freelancer for Your Project</title>
|
<title>Find an Odoo Freelancer for Your Project</title>
|
||||||
<meta name="description" content="Browse independent Odoo freelancers for your project. Contact a freelancer directly, or post your brief and we forward it to them anonymously. Odoo Community specialists." />
|
<meta name="description" content="Browse independent Odoo freelancers for your project and write to one directly, using the contact details they have published. Odoo Community specialists." />
|
||||||
<!-- DRAFT: not indexed, not linked, not in sitemap.xml / llms.txt. All five draft
|
<!-- DRAFT: not indexed, not linked, not in sitemap.xml / llms.txt. All five draft
|
||||||
conditions hold (noindex · not in sitemap · not in llms.txt · no inbound links ·
|
conditions hold (noindex · not in sitemap · not in llms.txt · no inbound links ·
|
||||||
no announcement surface). Remove this robots meta on go-live. -->
|
no announcement surface). Remove this robots meta on go-live. -->
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<link rel="canonical" href="https://odoo4projects.com/odoo-community-developers/" />
|
<link rel="canonical" href="https://odoo4projects.com/odoo-community-developers/" />
|
||||||
<meta property="og:title" content="Find an Odoo freelancer for your project" />
|
<meta property="og:title" content="Find an Odoo freelancer for your project" />
|
||||||
<meta property="og:description" content="Independent Odoo Community freelancers you can contact directly — or post your brief and we forward it for you, anonymously." />
|
<meta property="og:description" content="Independent Odoo Community freelancers you can write to directly, using the contact details they publish on their card." />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://odoo4projects.com/odoo-community-developers/" />
|
<meta property="og:url" content="https://odoo4projects.com/odoo-community-developers/" />
|
||||||
<meta name="twitter:card" content="summary" />
|
<meta name="twitter:card" content="summary" />
|
||||||
@@ -338,6 +338,13 @@
|
|||||||
.dev-links a {
|
.dev-links a {
|
||||||
color: var(--brass-deep);
|
color: var(--brass-deep);
|
||||||
}
|
}
|
||||||
|
/* A card with no published contact says so — it never shows an empty space. */
|
||||||
|
.dev-contact-none {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
/* The three forms */
|
/* The three forms */
|
||||||
.dir-forms {
|
.dir-forms {
|
||||||
@@ -453,47 +460,6 @@
|
|||||||
color: #b3452f;
|
color: #b3452f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Collapsed admin block */
|
|
||||||
.dir-admin {
|
|
||||||
max-width: 860px;
|
|
||||||
margin: 1.75rem auto 0;
|
|
||||||
}
|
|
||||||
.dir-admin summary {
|
|
||||||
cursor: pointer;
|
|
||||||
list-style: none;
|
|
||||||
padding: 0.95rem 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
font-family: Marcellus, Georgia, serif;
|
|
||||||
font-size: 1.02rem;
|
|
||||||
color: var(--midnight);
|
|
||||||
background: var(--ivory);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
}
|
|
||||||
.dir-admin summary::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.dir-admin summary::after {
|
|
||||||
content: "+";
|
|
||||||
font-family: Inter, sans-serif;
|
|
||||||
color: var(--brass);
|
|
||||||
font-size: 1.15rem;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.dir-admin[open] summary::after {
|
|
||||||
content: "\2212";
|
|
||||||
}
|
|
||||||
.dir-admin[open] summary {
|
|
||||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
||||||
}
|
|
||||||
.dir-admin .form-card {
|
|
||||||
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Final CTA */
|
/* Final CTA */
|
||||||
.cmp-cta {
|
.cmp-cta {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -560,7 +526,7 @@
|
|||||||
"@id": "https://odoo4projects.com/odoo-community-developers/#webpage",
|
"@id": "https://odoo4projects.com/odoo-community-developers/#webpage",
|
||||||
"url": "https://odoo4projects.com/odoo-community-developers/",
|
"url": "https://odoo4projects.com/odoo-community-developers/",
|
||||||
"name": "Find an Odoo Freelancer for Your Project",
|
"name": "Find an Odoo Freelancer for Your Project",
|
||||||
"description": "Browse independent Odoo freelancers for your project. Contact a freelancer directly, or post your brief and we forward it to them anonymously.",
|
"description": "Browse independent Odoo freelancers for your project and write to one directly, using the contact details they have published.",
|
||||||
"inLanguage": "en-US",
|
"inLanguage": "en-US",
|
||||||
"isPartOf": {
|
"isPartOf": {
|
||||||
"@id": "https://odoo4projects.com/#organization"
|
"@id": "https://odoo4projects.com/#organization"
|
||||||
@@ -621,10 +587,10 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<p class="hero-kicker">ODOO COMMUNITY FREELANCERS</p>
|
<p class="hero-kicker">ODOO COMMUNITY FREELANCERS</p>
|
||||||
<h1>Find an Odoo freelancer for your next project</h1>
|
<h1>Find an Odoo freelancer for your next project</h1>
|
||||||
<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. Contact a freelancer directly, or post your brief and we forward it to them without your name attached.</p>
|
<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">
|
<div class="hero-actions">
|
||||||
<a href="#directory" class="btn btn-brass btn-lg">Browse the freelancers</a>
|
<a href="#directory" class="btn btn-brass btn-lg">Browse the freelancers</a>
|
||||||
<a href="#request" class="btn btn-outline btn-lg">Post a request anonymously</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -634,9 +600,9 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">How this works</h2>
|
<h2 class="section-title">How this works</h2>
|
||||||
<div class="oc-body">
|
<div class="oc-body">
|
||||||
<p>You have an Odoo Community system and a job that needs a specialist. Below is a list of independent Odoo freelancers. Each one wrote their own profile — their focus, their languages, the work they take on — and we publish it as theirs. We do not rate them and we do not recommend one over another.</p>
|
<p><strong>Write to a freelancer directly.</strong> Each card carries what that freelancer published — their focus, their languages, the work they take on, and their contact details where they chose to publish them. We do not rate them and we do not recommend one over another.</p>
|
||||||
<p>You have two ways to reach them. <strong>Contact them directly</strong> — every freelancer's own contact details are published for exactly that purpose, so you can write to whoever looks right. Or <strong>post your project and stay anonymous</strong>: we forward your brief to the freelancer or freelancers you pick, they answer, and we send their answer back to you. Your name and your contact details are not shown to them until you decide to engage.</p>
|
<p><strong>Or let us ask them for you.</strong> We forward your brief to the freelancers you choose, without your name attached. That service is not switched on yet — see <a href="#request">Post a request</a> below.</p>
|
||||||
<p>Either way, the work is agreed between you and the freelancer. We list people and pass messages; the project and the contract are yours.</p>
|
<p>Either way the work is agreed between you and the freelancer. We publish the listings; the project and the contract are yours.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -672,6 +638,7 @@
|
|||||||
<li class="dev-tag">OCA member (as declared)</li>
|
<li class="dev-tag">OCA member (as declared)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<dl class="dev-facts"><div><dt>Projects on ODOO4projects</dt><dd>4 projects — a count from our own records</dd></div></dl>
|
<dl class="dev-facts"><div><dt>Projects on ODOO4projects</dt><dd>4 projects — a count from our own records</dd></div></dl>
|
||||||
|
<p class="dev-contact-none">No contact details published for this freelancer.</p>
|
||||||
</article>
|
</article>
|
||||||
<!-- DIRECTORY-LIST-END -->
|
<!-- DIRECTORY-LIST-END -->
|
||||||
</div>
|
</div>
|
||||||
@@ -683,41 +650,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Client request / anonymous get-quote (form 3) -->
|
<!-- Client request / anonymous get-quote — NOT BUILT. The feature is announced,
|
||||||
|
not offered: there is no form and no submit button, so a client can never
|
||||||
|
write a brief into a submit that refuses it. When the workflow exists, the
|
||||||
|
freelancer selection must be an explicit pick-list over the directory — never
|
||||||
|
an implicit "all" — and the endpoint must be the odoo_experts webhook or a new
|
||||||
|
one the user names, nothing else. -->
|
||||||
<section class="section-light" id="request">
|
<section class="section-light" id="request">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">Post a request — anonymously</h2>
|
<h2 class="section-title">Post a request — coming soon</h2>
|
||||||
<div class="oc-body">
|
<div class="oc-body">
|
||||||
<p>Not sure who to ask? Write your brief once and we forward it to the freelancers you choose, with no name and no contact details attached. A freelancer who is interested answers us, and we send their answer straight back to you. You decide whether to take it further — your details are only shared once you choose to engage.</p>
|
<p>We are building the anonymous request: you write your brief once, we forward it to the freelancers you choose with no name attached, and we send their answers back to you. It is not switched on yet. Until it is, email <a href="mailto:hello@odoo4projects.com">hello@odoo4projects.com</a> with your brief and we will pass it on by hand.</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- FORM 3 — endpoint NOT BUILT YET. The form is complete; the destination is
|
|
||||||
a single clearly-named constant in the page script, left empty on purpose.
|
|
||||||
We do not guess a URL. Wire CLIENT_REQUEST_ENDPOINT when the workflow exists
|
|
||||||
(it must be the odoo_experts webhook or a new one the user names — nothing else). -->
|
|
||||||
<div class="dir-forms">
|
|
||||||
<form class="form-card" id="clientRequestForm" novalidate>
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="crEmail">Your email</label>
|
|
||||||
<input id="crEmail" name="email" type="email" autocomplete="email" required />
|
|
||||||
<span class="hint">So we can send the freelancer's answer back to you. It is not shared with anyone until you decide to engage.</span>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="crTargets">Freelancers to ask <span class="hint">(optional)</span></label>
|
|
||||||
<input id="crTargets" name="targets" type="text" placeholder="Names, or leave blank for all" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field full">
|
|
||||||
<label for="crProject">What do you need done?</label>
|
|
||||||
<textarea id="crProject" name="project" rows="6" required placeholder="The job, your Odoo version, and anything the freelancer should know to give you a price."></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="btn btn-brass">Send my request</button>
|
|
||||||
<span class="form-status" id="clientRequestStatus" role="status"></span>
|
|
||||||
</div>
|
|
||||||
</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 pass your brief along by hand.</p></noscript>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -738,8 +682,12 @@
|
|||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="apEmail">Email</label>
|
<label for="apEmail">Email</label>
|
||||||
<input id="apEmail" name="email" type="email" autocomplete="email" required />
|
<input id="apEmail" name="email" type="email" autocomplete="email" required />
|
||||||
<span class="hint">Used to confirm your listing before it goes live.</span>
|
<span class="hint">We confirm it before your listing goes live — and it is published on your card (see the consent below).</span>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="form-check">
|
||||||
|
<input type="checkbox" name="email_public_consent" value="1" required />
|
||||||
|
<span>My email address is published on my public listing. I have read that and I agree. <span class="hint">(Your declaration — required.)</span></span>
|
||||||
|
</label>
|
||||||
<div class="form-field full">
|
<div class="form-field full">
|
||||||
<label for="apFocus">Your focus</label>
|
<label for="apFocus">Your focus</label>
|
||||||
<input id="apFocus" name="Focus" type="text" placeholder="e.g. accounting and stock for small manufacturers" />
|
<input id="apFocus" name="Focus" type="text" placeholder="e.g. accounting and stock for small manufacturers" />
|
||||||
@@ -748,6 +696,16 @@
|
|||||||
<label for="apDescription">About you, in your own words</label>
|
<label for="apDescription">About you, in your own words</label>
|
||||||
<textarea id="apDescription" name="description" rows="5" placeholder="What you do, who you do it for, how you work."></textarea>
|
<textarea id="apDescription" name="description" rows="5" placeholder="What you do, who you do it for, how you work."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="apCountry">Country</label>
|
||||||
|
<input id="apCountry" name="Country" type="text" placeholder="NL" />
|
||||||
|
<span class="hint">Where you work from. A two-letter code is enough (NL, DE, US).</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="apVersions">Odoo versions</label>
|
||||||
|
<input id="apVersions" name="odoo_versions" type="text" placeholder="16, 17, 18" />
|
||||||
|
<span class="hint">Separate with commas. The versions you actually work in.</span>
|
||||||
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="apLanguages">Languages</label>
|
<label for="apLanguages">Languages</label>
|
||||||
<input id="apLanguages" name="languages" type="text" placeholder="English, German" />
|
<input id="apLanguages" name="languages" type="text" placeholder="English, German" />
|
||||||
@@ -758,6 +716,14 @@
|
|||||||
<input id="apSpecialities" name="specialities" type="text" placeholder="accounting, stock, migration" />
|
<input id="apSpecialities" name="specialities" type="text" placeholder="accounting, stock, migration" />
|
||||||
<span class="hint">Separate with commas.</span>
|
<span class="hint">Separate with commas.</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="apGithub">GitHub URL <span class="hint">(optional)</span></label>
|
||||||
|
<input id="apGithub" name="github" type="url" placeholder="https://github.com/…" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="apOca">OCA profile URL <span class="hint">(optional)</span></label>
|
||||||
|
<input id="apOca" name="oca_profile" type="url" placeholder="https://github.com/OCA" />
|
||||||
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="apLinkedin">LinkedIn URL <span class="hint">(optional)</span></label>
|
<label for="apLinkedin">LinkedIn URL <span class="hint">(optional)</span></label>
|
||||||
<input id="apLinkedin" name="Linkedin" type="url" placeholder="https://www.linkedin.com/in/…" />
|
<input id="apLinkedin" name="Linkedin" type="url" placeholder="https://www.linkedin.com/in/…" />
|
||||||
@@ -794,93 +760,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Admin upload / edit (form 2) — the user's own form -->
|
|
||||||
<section class="section-light">
|
|
||||||
<div class="container">
|
|
||||||
<details class="dir-admin">
|
|
||||||
<summary>Admin: add or correct a freelancer record</summary>
|
|
||||||
<form class="form-card" id="adminForm" novalidate>
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="adId">Record id <span class="hint">(leave blank to create a new record)</span></label>
|
|
||||||
<input id="adId" name="id" type="number" min="1" />
|
|
||||||
<span class="hint">With an id, the record is updated. Without one, a new record is created.</span>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="adClients">Projects on ODOO4projects</label>
|
|
||||||
<input id="adClients" name="odoo4projects_clients" type="number" min="0" />
|
|
||||||
<span class="hint">Our own count, not the freelancer's.</span>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="adName">Name</label>
|
|
||||||
<input id="adName" name="Name" type="text" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="adEmail">Email</label>
|
|
||||||
<input id="adEmail" name="email" type="email" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="adFocus">Focus</label>
|
|
||||||
<input id="adFocus" name="Focus" type="text" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field full">
|
|
||||||
<label for="adDescription">Description</label>
|
|
||||||
<textarea id="adDescription" name="description" rows="4"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="adLanguages">Languages</label>
|
|
||||||
<input id="adLanguages" name="languages" type="text" />
|
|
||||||
<span class="hint">Separate with commas.</span>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="adSpecialities">Specialities</label>
|
|
||||||
<input id="adSpecialities" name="specialities" type="text" />
|
|
||||||
<span class="hint">Separate with commas.</span>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="adLinkedin">LinkedIn URL</label>
|
|
||||||
<input id="adLinkedin" name="Linkedin" type="url" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="adUpwork">Upwork URL</label>
|
|
||||||
<input id="adUpwork" name="upwork" type="url" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="adFiverr">Fiverr URL</label>
|
|
||||||
<input id="adFiverr" name="fiverr" type="url" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="adImage">Image URL</label>
|
|
||||||
<input id="adImage" name="Image" type="url" />
|
|
||||||
</div>
|
|
||||||
<label class="form-check">
|
|
||||||
<input type="checkbox" name="OCA_member" value="1" />
|
|
||||||
<span>OCA member.</span>
|
|
||||||
</label>
|
|
||||||
<label class="form-check">
|
|
||||||
<input type="checkbox" name="is_individual" value="1" />
|
|
||||||
<span>Works as an individual.</span>
|
|
||||||
</label>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="btn btn-brass">Save this record</button>
|
|
||||||
<span class="form-status" id="adminStatus" role="status"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Final CTA -->
|
<!-- Final CTA -->
|
||||||
<section class="section-dark cmp-cta" id="start">
|
<section class="section-dark cmp-cta" id="start">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="section-title">Start</h2>
|
<h2 class="section-title">Start</h2>
|
||||||
<div class="cmp-lead">
|
<div class="cmp-lead">
|
||||||
<p>Send your brief and we will forward it to the freelancers who fit it — anonymously, if you prefer. Or write to a freelancer directly from their card.</p>
|
<p>Browse the freelancers and write to one directly from their card — or email us your brief and we will pass it on by hand until the anonymous request service is switched on.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cmp-cta-actions">
|
<div class="cmp-cta-actions">
|
||||||
<a href="#request" class="btn btn-brass btn-lg">Post a request</a>
|
<a href="#directory" class="btn btn-brass btn-lg">Browse the freelancers</a>
|
||||||
<a href="#directory" class="btn btn-outline 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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -939,6 +828,10 @@
|
|||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
Odoo freelancer directory — page script.
|
Odoo freelancer directory — page script.
|
||||||
|
|
||||||
|
This page FILTERS the build-time HTML and posts the freelancer application.
|
||||||
|
The admin add/edit form is NOT here — it lives on its own page at
|
||||||
|
/odoo-community-developers/admin/ (a draft, not linked from this page).
|
||||||
|
|
||||||
ONE webhook, for everything. The freelancer platform is a SEPARATE system
|
ONE webhook, for everything. The freelancer platform is a SEPARATE system
|
||||||
from the CRM: there is no /crm, no /crm_odoo and nothing from the CRM skill
|
from the CRM: there is no /crm, no /crm_odoo and nothing from the CRM skill
|
||||||
here. If you find yourself reaching for a CRM endpoint, stop — wrong system.
|
here. If you find yourself reaching for a CRM endpoint, stop — wrong system.
|
||||||
@@ -951,19 +844,8 @@
|
|||||||
var ODOO_EXPERTS_WEBHOOK =
|
var ODOO_EXPERTS_WEBHOOK =
|
||||||
"https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts";
|
"https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts";
|
||||||
|
|
||||||
/* FORM 3 — client request / anonymous get-quote.
|
var MULTI_FIELDS = ["languages", "specialities", "odoo_versions"]; // comma-separated → array
|
||||||
NOT BUILT YET. Deliberately inert: we do not guess a URL. When the workflow
|
var BOOL_FIELDS = ["OCA_member", "is_individual", "email_public_consent"];
|
||||||
exists, set this to the odoo_experts webhook or a new endpoint the user names
|
|
||||||
(nothing else), and the form below starts working unchanged. */
|
|
||||||
var CLIENT_REQUEST_ENDPOINT = "";
|
|
||||||
|
|
||||||
/* OPTIONAL-LATER (do not build now): a mail-based "send me their details" gate
|
|
||||||
against bots — the client asks for a freelancer's contact and we mail it, so a
|
|
||||||
scraper reading the page gets nothing usable. Left out because the mail webhook
|
|
||||||
is currently 404 (spec §5). */
|
|
||||||
|
|
||||||
var MULTI_FIELDS = ["languages", "specialities"]; // comma-separated → array
|
|
||||||
var BOOL_FIELDS = ["OCA_member", "is_individual"];
|
|
||||||
var NUM_FIELDS = ["odoo4projects_clients"];
|
var NUM_FIELDS = ["odoo4projects_clients"];
|
||||||
|
|
||||||
function fieldElements(form, name) {
|
function fieldElements(form, name) {
|
||||||
@@ -1127,76 +1009,8 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Form 2: admin upload / edit (update when an id is present) ---- */
|
|
||||||
function initAdminForm() {
|
|
||||||
var form = document.getElementById("adminForm");
|
|
||||||
if (!form) return;
|
|
||||||
var status = document.getElementById("adminStatus");
|
|
||||||
form.addEventListener("submit", function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!form.reportValidity()) return;
|
|
||||||
var submit = form.querySelector('button[type="submit"]');
|
|
||||||
var hasId = !!form.querySelector('[name="id"]').value.trim();
|
|
||||||
submit.disabled = true;
|
|
||||||
setStatus(status, "Saving…", null);
|
|
||||||
|
|
||||||
var payload = buildPayload(form, true);
|
|
||||||
postToWebhook(payload)
|
|
||||||
.then(function () {
|
|
||||||
setStatus(
|
|
||||||
status,
|
|
||||||
hasId
|
|
||||||
? "Saved. Check your inbox to confirm the change before it is applied."
|
|
||||||
: "New record sent. Check your inbox to confirm it before it goes live.",
|
|
||||||
"ok"
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(function () {
|
|
||||||
setStatus(status, "That did not save. Please try again.", "err");
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
submit.disabled = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Form 3: client request (inert until CLIENT_REQUEST_ENDPOINT exists) ---- */
|
|
||||||
function initClientRequestForm() {
|
|
||||||
var form = document.getElementById("clientRequestForm");
|
|
||||||
if (!form) return;
|
|
||||||
var status = document.getElementById("clientRequestStatus");
|
|
||||||
form.addEventListener("submit", function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!form.reportValidity()) return;
|
|
||||||
if (!CLIENT_REQUEST_ENDPOINT) {
|
|
||||||
// Not wired yet — say so plainly rather than pretend.
|
|
||||||
setStatus(
|
|
||||||
status,
|
|
||||||
"This request form is not switched on yet. For now, email us at hello@odoo4projects.com and we will pass your brief along.",
|
|
||||||
"err"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var payload = buildPayload(form, false);
|
|
||||||
fetch(CLIENT_REQUEST_ENDPOINT, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
setStatus(status, "Sent. We will forward it and come back to you with any answers.", "ok");
|
|
||||||
form.reset();
|
|
||||||
})
|
|
||||||
.catch(function () {
|
|
||||||
setStatus(status, "That did not send. Please try again.", "err");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initFilters();
|
initFilters();
|
||||||
initApplyForm();
|
initApplyForm();
|
||||||
initAdminForm();
|
|
||||||
initClientRequestForm();
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user