-
This page needs JavaScript to read the seat count and to claim a seat.
+
This page needs JavaScript to show the feedback form.
Turn JavaScript on in your browser and open this page again — or reply to the message
that brought you here.
@@ -288,10 +246,9 @@
(function () {
var C = window.O4F;
var app = document.getElementById("app");
- var claimedUpwork = ""; // the seat claim: an identifier, kept in memory, never in the URL
// The application form on the directory page — the anchor the page's own "Get listed"
// triggers use. There is no separate login or account page, so this is the one target.
- var SIGNUP_URL = "https://odoo4projects.com/odoo-community-developers/#apply";
+ var PORTAL_URL = "https://odoo4projects.com/odoo-community-developers/#apply";
function el(tag, cls, text) {
var n = document.createElement(tag);
@@ -304,35 +261,12 @@
for (var i = 0; i < nodes.length; i++) app.appendChild(nodes[i]);
return nodes;
}
- function panel(children) {
- var box = el("div", "panel");
- for (var i = 0; i < children.length; i++) {
- if (children[i]) box.appendChild(children[i]);
- }
- return box;
- }
function status(text, kind) {
var p = el("p", "status" + (kind ? " state-" + kind : ""), text);
p.setAttribute("role", "status");
p.setAttribute("aria-live", "polite");
return p;
}
- // Read one value by its list of accepted keys. A key not listed is never read.
- function pick(obj, keys) {
- if (!obj || typeof obj !== "object") return undefined;
- for (var i = 0; i < keys.length; i++) {
- var v = obj[keys[i]];
- if (v !== undefined && v !== null && v !== "") return v;
- }
- return undefined;
- }
- // A count must come off the wire as a whole number >= 0. Anything else is "not on the wire".
- function count(obj, keys) {
- var v = pick(obj, keys);
- if (typeof v === "number" && isFinite(v) && v >= 0) return Math.floor(v);
- if (typeof v === "string" && /^\s*\d+\s*$/.test(v)) return parseInt(v, 10);
- return null;
- }
function fetchJson(url, opts) {
return fetch(url, opts).then(function (r) {
return r.text().then(function (t) {
@@ -347,40 +281,41 @@
var m = body.message || body.error || body.detail;
return typeof m === "string" ? m : "";
}
- function retryButton(label, fn) {
- var b = el("button", "btn", label);
- b.type = "button";
- b.addEventListener("click", fn);
- return b;
+ // A link to the freelancer portal. Opens in a new window and says so, so a screen-reader
+ // user is not surprised by a new tab.
+ function portalLink() {
+ var p = el("p", "detail");
+ var a = el("a", "link-signup");
+ a.href = PORTAL_URL;
+ a.target = "_blank";
+ a.rel = "noopener noreferrer";
+ a.textContent = "Sign up on the freelancer portal (opens in a new window)";
+ p.appendChild(a);
+ return p;
}
- // ---- Progress bar (three steps) ---------------------------------------------------------
- // Which step is current follows the page state — it is not decoration. It renders only
- // where there is something to do: seats available (step 1 current) and seat claimed
- // (step 1 done, step 2 current, step 3 visible as what comes next). The cohort-full and
- // count-unavailable states render no bar at all. The bar states no count and no deadline:
- // both come from the wire and are printed elsewhere, from the wire alone.
- var STEP_LABELS = ["Claim seat", "Sign up", "Fill out feedback"];
+ // ---- Progress bar (two steps) ---------------------------------------------------------
+ // Step 1 is the signup on the portal, step 2 is this form. The page has no honest way to
+ // know the visitor has used the portal link, so step 1 stays current and nothing is
+ // guessed: the number, the bolder label and the spoken "(current step)" say which one is
+ // current, so it is never colour alone.
+ var STEP_LABELS = ["Sign up", "Give feedback"];
function progressBar(current) {
var nav = el("nav", "progress");
- nav.setAttribute("aria-label", "Signup progress");
+ nav.setAttribute("aria-label", "Progress");
var ol = el("ol", "steps-bar");
STEP_LABELS.forEach(function (label, i) {
var n = i + 1;
var li = el("li", "step");
var mark = el("span", "step-mark");
mark.setAttribute("aria-hidden", "true");
- // A done step shows the ✓ mark; the current and upcoming steps show their number.
- mark.textContent = n < current ? "\u2713" : String(n);
+ mark.textContent = String(n);
li.appendChild(mark);
li.appendChild(el("span", "step-label", label));
if (n === current) {
li.className = "step step-current";
li.setAttribute("aria-current", "step");
- } else if (n < current) {
- // Done is the ✓ mark AND a word for screen readers — never colour alone.
- li.className = "step step-done";
- li.appendChild(el("span", "vh", " (completed)"));
+ li.appendChild(el("span", "vh", " (current step)"));
} else {
li.className = "step step-next";
}
@@ -390,176 +325,19 @@
return nav;
}
- // ---- The four states -----------------------------------------------------------------
- // 1. Seats available. The number is read from the wire; a cap is not a remaining count.
- function stateAvailable(seats, total) {
- var h = el("p", "seat");
- h.appendChild(el("strong", "", String(seats)));
- h.appendChild(document.createTextNode(
- total != null ? " of " + total + " seats left" : (seats === 1 ? " seat left" : " seats left")
- ));
-
- var form = el("form");
- form.setAttribute("novalidate", "novalidate");
- var field = el("div", "field");
- var label = el("label", "", "Your Upwork name");
- label.setAttribute("for", "fbUpwork");
- var input = el("input");
- input.type = "text";
- input.id = "fbUpwork";
- input.name = "upwork";
- input.autocomplete = "off";
- input.required = true;
- var hint = el("p", "hint", "Exactly as it appears on your Upwork profile — this is how we match your seat to your signup.");
- field.appendChild(label);
- field.appendChild(input);
- field.appendChild(hint);
- var err = el("p", "err", "");
- err.hidden = true;
- field.appendChild(err);
- form.appendChild(field);
-
- var line = status("", "");
- var btn = el("button", "btn", "Claim a seat");
- btn.type = "submit";
- form.appendChild(btn);
- form.appendChild(line);
-
- form.addEventListener("submit", function (ev) {
- ev.preventDefault();
- var name = input.value.trim();
- if (!name) {
- field.setAttribute("data-bad", "1");
- err.textContent = "Enter your Upwork name to claim a seat.";
- err.hidden = false;
- input.focus();
- return;
- }
- field.setAttribute("data-bad", "0");
- err.hidden = true;
- btn.disabled = true;
- line.className = "status";
- line.textContent = "Claiming your seat…";
- // The claim. The Upwork name is an identifier: it goes to our workflow and nowhere
- // else — never into the address and never into an analytics payload.
- fetchJson(C.submitUrl, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ form: "claim", upwork: name })
- }).then(function (res) {
- if (!res.ok) {
- btn.disabled = false;
- line.className = "status state-warn";
- line.textContent = "We could not claim your seat — the service answered " + res.status +
- (msgOf(res.body) ? " — " + msgOf(res.body) : "") + ". Try again.";
- return;
- }
- claimedUpwork = name;
- stateClaimed(count(res.body, C.leftKeys), pick(res.body, C.heldUntilKeys));
- }).catch(function () {
- btn.disabled = false;
- line.className = "status state-warn";
- line.textContent = "We could not reach the service to claim your seat. Check your connection and try again.";
- });
- });
-
- show([
- progressBar(1),
- panel([h, el("p", "detail", "One seat per freelancer, and there is a fixed number for this cohort.")]),
- panel([el("h2", "", "Claim your seat"), form])
- ]);
- }
-
- // 2. Seat claimed — held for one hour. The deadline is shown as a clock time when the
- // workflow tells us when it started; the hour is the workflow's, not this page's.
- function stateClaimed(seats, heldUntil) {
- var dl = el("p", "deadline");
- var t = clockTime(heldUntil);
- if (t) {
- dl.appendChild(document.createTextNode("Your seat is held until "));
- dl.appendChild(el("strong", "", t));
- dl.appendChild(document.createTextNode("."));
- } else {
- dl.textContent = "Your seat is held.";
- }
- var steps = el("ul", "steps");
- steps.appendChild(el("li", "", "Create your account on the ODOO4projects freelancer portal."));
- steps.appendChild(el("li", "", "Add the links to your profiles (Upwork, LinkedIn, Fiverr)."));
- steps.appendChild(el("li", "", "Fill in your focus, about you and the 15-point skill profile."));
- steps.appendChild(el("li", "", "Submit the application. We match your signup to the Upwork name you just gave."));
-
- var claimed = el("div", "panel");
- claimed.appendChild(el("h2", "", "Seat claimed"));
- claimed.appendChild(dl);
- claimed.appendChild(el("p", "detail",
- "The one hour to finish your signup comes from our signup workflow, not from this page."));
- if (seats != null) {
- claimed.appendChild(el("p", "who", (seats === 1 ? "1 seat left" : seats + " seats left") + " after your claim"));
- }
- claimed.appendChild(steps);
-
- // The one thing to do while a seat is held: go and finish the signup. A link, never a
- // second button — the page keeps one primary action at a time, and the target is the
- // application form the page's own "Get listed" triggers use.
- var signup = el("p", "detail");
- var up = el("a", "link-signup");
- up.href = SIGNUP_URL;
- up.target = "_blank";
- up.rel = "noopener noreferrer";
- up.appendChild(document.createTextNode("Sign up here"));
- // Say it opens a new window, so a screen-reader user is not surprised by a new tab.
- up.appendChild(el("span", "", " (opens in a new window)"));
- signup.appendChild(up);
- claimed.appendChild(signup);
-
- show([progressBar(2), claimed, feedbackPanel()]);
- }
-
- function clockTime(iso) {
- if (!iso) return "";
- var d = new Date(iso);
- if (isNaN(d.getTime())) return "";
- try {
- return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
- } catch (e) {
- return "";
- }
- }
-
- // 3. Cohort full — the closed-cohort line only. No feedback is rendered: the consent
- // question (the old "publish" field) is gone, so there is no record of anyone agreeing
- // to have their words shown in public, and anonymous words are still the author's
- // words. This state names no quotes, no excerpts and no paraphrases.
- function stateFull() {
- var box = panel([el("h2", "", "This cohort is full")]);
- box.appendChild(el("p", "detail", "All seats for this cohort have been claimed, so it is closed. We are not accepting new freelancers into this group."));
- show([box]);
- }
-
- // 4. Count unavailable — today's real state. No number, no claim button that cannot work.
- function stateUnavailable(httpStatus, msg) {
- var nodes = [
- status("We cannot read the seat count right now.", "warn"),
- el("p", "detail", "The seat count could not be read from our service, so we are not showing a number and we are not offering a claim that cannot work. Please try again shortly.")
- ];
- if (httpStatus) {
- nodes.push(el("p", "detail", "The service answered " + httpStatus + (msg ? " — " + msg : "") + "."));
- }
- nodes.push(retryButton("Try again", load));
- show([panel(nodes)]);
- }
- function stateLoadFailed(httpStatus, msg) {
- var nodes = [
- status("We could not reach the service.", "warn"),
- el("p", "detail", "The seat count could not be read, so no number is shown and no seat is offered right now.")
- ];
- if (httpStatus) nodes.push(el("p", "detail", "The service answered " + httpStatus + (msg ? " — " + msg : "") + "."));
- nodes.push(retryButton("Try again", load));
- show([panel(nodes)]);
+ // ---- Step 1 — the portal link, again, in the step's own copy -------------------------
+ function signupPanel() {
+ var box = el("div", "panel");
+ box.appendChild(el("h2", "", "Step 1 — sign up on the freelancer portal"));
+ box.appendChild(el("p", "detail",
+ "Create your free freelancer account and add your profile. The directory opens on 1 December."));
+ box.appendChild(portalLink());
+ return box;
}
// ---- The feedback form ----------------------------------------------------------------
- // The n8n form's questions, with the copy corrected, plus the fields the owner asked for.
+ // The n8n form's questions, with the copy corrected. The identity block (name, email,
+ // anonymous tick) and the publish-consent question are gone — do not bring them back.
var FIELDS = [
{ id: "finalized", label: "Did you finalize the signup?", type: "select", options: [["yes", "Yes"], ["no", "No"]], required: true },
{ id: "difficulty", label: "How hard was the signup?", type: "select", options: [["easy", "Easy"], ["ok", "OK"], ["hard", "Hard"]], required: true },
@@ -672,11 +450,8 @@
return;
}
- // Build the body. The claimed Upwork name rides along ONLY when a seat was claimed
- // this visit — state the page already holds. With no claim the submission carries no
- // name and is anonymous (nothing is invented).
+ // Build the body. The answers and nothing else — no name, no email, no identifier.
var body = { form: "feedback" };
- if (claimedUpwork) body.upwork = claimedUpwork;
FIELDS.forEach(function (spec) {
if (spec.type === "radio") {
var checked = form.querySelector('input[name="' + spec.id + '"]:checked');
@@ -719,33 +494,9 @@
return box;
}
- // ---- Load -----------------------------------------------------------------------------
- function load() {
- show([panel([status("Reading the seat count…", "")])]);
- fetchJson(C.statusUrl, { method: "GET", cache: "no-store" }).then(function (res) {
- if (!res.ok) {
- // A workflow that throws is our problem, and it must not look like a closed cohort.
- stateUnavailable(res.status, msgOf(res.body));
- return;
- }
- var seats = count(res.body, C.leftKeys);
- var total = count(res.body, C.totalKeys);
- if (seats === null) {
- // No count came off the wire: print no number.
- stateUnavailable(null, "the reply carried no seat count");
- return;
- }
- if (seats === 0) {
- stateFull();
- return;
- }
- stateAvailable(seats, total);
- }).catch(function () {
- stateLoadFailed(null, "");
- });
- }
-
- load();
+ // ---- Render ---------------------------------------------------------------------------
+ // One state, always: the form. Nothing is read on load, so there is no fetch here.
+ show([progressBar(1), signupPanel(), feedbackPanel()]);
})();