feedback: remove identity block, heard_about and the publish consent question

- Delete the 'About you' fieldset (name, email, anonymous tick) and its
  validation: the claimed Upwork name now identifies the sender, and the
  feedback payload carries it only when a seat was claimed this visit.
- Remove the heard_about question.
- Remove the publish consent question. That question WAS the consent, so
  no feedback may be published: the cohort-full state now renders the
  closed-cohort line only, and the feedback-rendering branch (entry keys,
  publish constants, isPublishable, the .fb styles) is gone rather than
  left dead. The GET's 'feedback' array is no longer consumed.
- recommend: first label 'Yes' -> 'Yes, good idea' (value yes unchanged).
This commit is contained in:
webmaster committed 2026-11-03 20:09:07 +00:00
1 parent bb963850b1
commit a6ada39784
1 file changed
+27 -136
+27 -136
View File
@@ -190,11 +190,7 @@
border: 0; border: 0;
} }
/* ---- Feedback list (cohort full) ---- */ /* ---- Cohort full (no feedback is ever rendered) ---- */
.fb { border-top: 1px solid var(--line); padding: 1rem 0 0; margin: 1rem 0 0; }
.fb:first-of-type { border-top: 0; padding-top: 0.2rem; margin-top: 0.4rem; }
.fb p { margin: 0 0 0.5rem; }
.fb cite { font-style: normal; font-size: 0.85rem; color: var(--brass-deep); }
.who { .who {
font-size: 0.8rem; font-size: 0.8rem;
letter-spacing: 0.06em; letter-spacing: 0.06em;
@@ -211,26 +207,26 @@
GET statusUrl (no query string) GET statusUrl (no query string)
-> 200 { "seats_left": 7, "seats_total": 10 } <- the intended shape -> 200 { "seats_left": 7, "seats_total": 10 } <- the intended shape
or { "free": 9 } <- what the live route answers today or { "free": 9 } <- what the live route answers today
or { "free": 0, or { "free": 0, "feedback": [ ... ] }
"feedback": [ { "text": "...", "name": "Jane", "publish": "yes" } ] }
A count arrives under the documented names first, then the live route's A count arrives under the documented names first, then the live route's
"Free"/"free"; a count must be a whole number >= 0 or it is not on the wire "Free"/"free"; a count must be a whole number >= 0 or it is not on the wire
(so "free": "" is skipped and the next key is read). "feedback" (aka "entries") (so "free": "" is skipped and the next key is read). A "feedback" array in the
carries the cohort's words: each entry is published only when its "publish" is reply is NOT consumed by this page and nothing from it is ever rendered — the
"yes" (words with the name) or "name" (words without it) — "no", missing or page reads the count only. With no seats, the cohort-full state shows the
unknown is never rendered. With no seats and no "feedback", the cohort-full closed-cohort line alone: no quotes, no excerpts, no paraphrases.
state states plainly that nothing is published yet (never a blank list).
POST submitUrl { "form": "claim", "upwork": "<their Upwork name>" } POST submitUrl { "form": "claim", "upwork": "<their Upwork name>" }
-> 200 { "seats_left": 6, "seats_total": 10, "held_until": "2026-11-03T18:40:00Z" } -> 200 { "seats_left": 6, "seats_total": 10, "held_until": "2026-11-03T18:40:00Z" }
POST submitUrl { "form": "feedback", "upwork": "<their Upwork name>", "name": "...", POST submitUrl { "form": "feedback", "upwork": "<their claimed Upwork name>",
"email": "...", "anonymous": false, "finalized": "yes", ... , "finalized": "yes", ... }
"publish": "yes" | "name" | "no" } "upwork" is present ONLY when a seat was claimed this visit — the claimed Upwork
name, held in memory, sent to this route and nowhere else. With no claim the
submission carries no name and is anonymous.
-> any 2xx = receipt shown; a non-2xx is reported as a failure, never as success. -> any 2xx = receipt shown; a non-2xx is reported as a failure, never as success.
An unexpected shape is named, never guessed: a count that did not come off the wire is An unexpected shape is named, never guessed: a count that did not come off the wire is
not printed, and an entry whose "publish" is missing or unknown is not rendered. --> not printed. -->
<script> <script>
window.O4F = { window.O4F = {
// The one route that answers the count + feedback (read-only). // The one route that answers the count + feedback (read-only).
@@ -242,16 +238,6 @@
// shape still wins if it ever arrives. "free": "" is skipped by pick() below. // shape still wins if it ever arrives. "free": "" is skipped by pick() below.
leftKeys: ["seats_left", "seatsLeft", "Free", "free"], leftKeys: ["seats_left", "seatsLeft", "Free", "free"],
totalKeys: ["seats_total", "seatsTotal"], totalKeys: ["seats_total", "seatsTotal"],
feedbackKeys: ["feedback", "entries"],
// One feedback entry. A missing "" means "not on the wire", never a value.
entryTextKeys: ["text", "feedback", "message"],
entryNameKeys: ["name", "Name"],
entryPublishKeys: ["publish", "consent"],
// The three publish answers, character for character as the form sends them. ONLY "yes"
// and "name" are rendered; anything else — "no", missing, unknown — is never rendered.
PUBLISH_YES: "yes",
PUBLISH_NAME: "name",
PUBLISH_NO: "no",
// The POST reply key that carries the seat deadline (ISO 8601). // The POST reply key that carries the seat deadline (ISO 8601).
heldUntilKeys: ["held_until", "heldUntil", "deadline"] heldUntilKeys: ["held_until", "heldUntil", "deadline"]
}; };
@@ -540,27 +526,13 @@
} }
} }
// 3. Cohort full — consented feedback only. // 3. Cohort full — the closed-cohort line only. No feedback is rendered: the consent
function stateFull(entries) { // question (the old "publish" field) is gone, so there is no record of anyone agreeing
var shown = entries.filter(isPublishable); // 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")]); 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. Thank you — here is what the freelancers who took part told us.")); 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."));
if (shown.length) {
shown.forEach(function (e) {
var fig = el("figure", "fb");
fig.appendChild(el("p", "", String(pick(e, C.entryTextKeys) || "")));
var nm = pick(e, C.entryNameKeys);
var consent = pick(e, C.entryPublishKeys);
if (consent === C.PUBLISH_YES && nm) {
fig.appendChild(el("cite", "", "— " + String(nm)));
} else {
fig.appendChild(el("cite", "", "— published without a name"));
}
box.appendChild(fig);
});
} else {
box.appendChild(el("p", "detail", "No feedback has been published yet."));
}
show([box]); show([box]);
} }
@@ -586,11 +558,6 @@
show([panel(nodes)]); show([panel(nodes)]);
} }
function isPublishable(entry) {
var c = pick(entry, C.entryPublishKeys);
return c === C.PUBLISH_YES || c === C.PUBLISH_NAME;
}
// ---- The feedback form ---------------------------------------------------------------- // ---- 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, plus the fields the owner asked for.
var FIELDS = [ var FIELDS = [
@@ -605,10 +572,8 @@
{ id: "design_feedback", label: "What can we improve about the profile design?", type: "textarea", required: true }, { id: "design_feedback", label: "What can we improve about the profile design?", type: "textarea", required: true },
{ id: "will_stay", label: "Will you stay on the platform?", type: "select", options: [["yes", "Yes"], ["no", "No"]], required: true }, { id: "will_stay", label: "Will you stay on the platform?", type: "select", options: [["yes", "Yes"], ["no", "No"]], required: true },
{ id: "improve_offer", label: "How can we improve what we offer you?", type: "textarea", required: true }, { id: "improve_offer", label: "How can we improve what we offer you?", type: "textarea", required: true },
{ id: "recommend", label: "Would you recommend this platform to a freelance colleague?", type: "select", options: [["yes", "Yes"], ["maybe", "Maybe"], ["no", "Preferably not"]], required: true }, { id: "recommend", label: "Would you recommend this platform to a freelance colleague?", type: "select", options: [["yes", "Yes, good idea"], ["maybe", "Maybe"], ["no", "Preferably not"]], required: true },
{ id: "heard_about", label: "Where did you first hear about this platform?", type: "text", required: true }, { id: "expect_next", label: "What do you expect to happen next — and how soon?", type: "textarea", required: true }
{ id: "expect_next", label: "What do you expect to happen next — and how soon?", type: "textarea", required: true },
{ id: "publish", label: "May we publish your feedback with your name?", type: "radio", options: [["yes", "Yes, with my name"], ["name", "Yes, but not with my name"], ["no", "No"]], required: true }
]; ];
function fieldBlock(spec) { function fieldBlock(spec) {
@@ -663,47 +628,10 @@
function feedbackPanel() { function feedbackPanel() {
var box = el("div", "panel"); var box = el("div", "panel");
box.appendChild(el("h2", "", "How did it go?")); box.appendChild(el("h2", "", "How did it go?"));
box.appendChild(el("p", "detail", "Answer in your own words. Nothing here is published unless you say so, and you can ask us to leave your name off.")); box.appendChild(el("p", "detail", "Answer in your own words. Your answers come to us — nothing here is published."));
var form = el("form"); var form = el("form");
form.setAttribute("novalidate", "novalidate"); form.setAttribute("novalidate", "novalidate");
// Name + email, or an explicit anonymous tick.
var ident = el("fieldset");
ident.setAttribute("data-field", "identity");
ident.appendChild(el("legend", "", "About you"));
var nf = el("div", "field");
var nl = el("label", "", "Your name");
nl.setAttribute("for", "fb-name");
var ni = el("input");
ni.type = "text";
ni.id = "fb-name";
ni.name = "name";
ni.autocomplete = "name";
nf.appendChild(nl); nf.appendChild(ni);
ident.appendChild(nf);
var ef = el("div", "field");
var elab = el("label", "", "Your email");
elab.setAttribute("for", "fb-email");
var ei = el("input");
ei.type = "email";
ei.id = "fb-email";
ei.name = "email";
ei.autocomplete = "email";
ef.appendChild(elab); ef.appendChild(ei);
ident.appendChild(ef);
var anonLab = el("label", "choice");
var anon = el("input");
anon.type = "checkbox";
anon.id = "fb-anonymous";
anon.name = "anonymous";
anonLab.appendChild(anon);
anonLab.appendChild(el("span", "", "Send this anonymously — do not record my name or email."));
ident.appendChild(anonLab);
var idErr = el("p", "err", "");
idErr.hidden = true;
ident.appendChild(idErr);
form.appendChild(ident);
FIELDS.forEach(function (spec) { form.appendChild(fieldBlock(spec)); }); FIELDS.forEach(function (spec) { form.appendChild(fieldBlock(spec)); });
var line = status("", ""); var line = status("", "");
@@ -712,23 +640,6 @@
form.appendChild(btn); form.appendChild(btn);
form.appendChild(line); form.appendChild(line);
// The anonymous tick hides the name/email and removes "with my name" as an option.
function syncAnon() {
var on = anon.checked;
nf.hidden = on;
ef.hidden = on;
var yesName = form.querySelector('input[name="publish"][value="yes"]');
if (yesName) {
yesName.disabled = on;
if (on && yesName.checked) {
var alt = form.querySelector('input[name="publish"][value="name"]');
if (alt) alt.checked = true;
}
}
}
anon.addEventListener("change", syncAnon);
syncAnon();
form.addEventListener("submit", function (ev) { form.addEventListener("submit", function (ev) {
ev.preventDefault(); ev.preventDefault();
var bad = false; var bad = false;
@@ -753,22 +664,6 @@
err.hidden = true; err.hidden = true;
} }
}); });
var anonymous = anon.checked;
if (!anonymous) {
var nm = ni.value.trim(), em = ei.value.trim();
if (!nm || !em) {
ident.setAttribute("data-bad", "1");
idErr.textContent = "Add your name and email — or tick “Send this anonymously”.";
idErr.hidden = false;
bad = true;
} else {
ident.removeAttribute("data-bad");
idErr.hidden = true;
}
} else {
ident.removeAttribute("data-bad");
idErr.hidden = true;
}
if (bad) { if (bad) {
line.className = "status state-warn"; line.className = "status state-warn";
line.textContent = "Some answers are missing — see the notes above."; line.textContent = "Some answers are missing — see the notes above.";
@@ -777,14 +672,11 @@
return; return;
} }
// Build the body. An anonymous sender sends no name and no email. // Build the body. The claimed Upwork name rides along ONLY when a seat was claimed
var body = { // this visit — state the page already holds. With no claim the submission carries no
form: "feedback", // name and is anonymous (nothing is invented).
upwork: claimedUpwork, var body = { form: "feedback" };
name: anonymous ? "" : ni.value.trim(), if (claimedUpwork) body.upwork = claimedUpwork;
email: anonymous ? "" : ei.value.trim(),
anonymous: anonymous
};
FIELDS.forEach(function (spec) { FIELDS.forEach(function (spec) {
if (spec.type === "radio") { if (spec.type === "radio") {
var checked = form.querySelector('input[name="' + spec.id + '"]:checked'); var checked = form.querySelector('input[name="' + spec.id + '"]:checked');
@@ -838,14 +730,13 @@
} }
var seats = count(res.body, C.leftKeys); var seats = count(res.body, C.leftKeys);
var total = count(res.body, C.totalKeys); var total = count(res.body, C.totalKeys);
var entries = pick(res.body, C.feedbackKeys);
if (seats === null) { if (seats === null) {
// No count came off the wire: print no number. // No count came off the wire: print no number.
stateUnavailable(null, "the reply carried no seat count"); stateUnavailable(null, "the reply carried no seat count");
return; return;
} }
if (seats === 0) { if (seats === 0) {
stateFull(Array.isArray(entries) ? entries : []); stateFull();
return; return;
} }
stateAvailable(seats, total); stateAvailable(seats, total);