feng-x's picture
Upload folder using huggingface_hub
eecdb88 verified
Raw
History Blame Contribute Delete
4.82 kB
// /feedback — post-shipment KOL fit-feedback form (v8).
//
// A single self-contained screen (no step framework — that's the /m
// capture coach's machinery and irrelevant here). Validates the
// required fields, POSTs multipart to /api/fit-feedback, and swaps the
// form for a thank-you + "measure now" CTA on success.
//
// Email is the cross-table join key (see doc/v8/PRD.md), so it is the
// one field validated for format; the rest are required-but-free.
//
// Validation errors render INLINE, directly under the offending field
// (not in one spot at the bottom): on mobile we focus the bad field, so
// the keyboard would hide a bottom-of-form message entirely.
// Loose RFC-ish check — `something@something.tld`. Mirrors the desktop /
// mobile measurement forms so a KOL who uses both sees the same rule.
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const form = document.getElementById("feedbackForm");
const errorEl = document.getElementById("formError");
const submitBtn = document.getElementById("submitBtn");
const el = (id) => document.getElementById(id);
const val = (id) => (el(id).value || "").trim();
// Required selects/inputs and the human label used in the error message
// when one is left blank.
const REQUIRED = [
["receivedSize", "the ring size you received"],
["receivedModel", "the ring model"],
["bestFitFinger", "which finger fit best"],
["fitQuality", "how it fit"],
["hand", "which hand"],
];
// Every field that can produce a validation error, in DOM order.
const FIELD_IDS = ["kolName", "kolEmail", ...REQUIRED.map(([id]) => id)];
// Inject an inline error slot under each validatable field and clear it
// as soon as the user edits that field.
const errSlots = {};
for (const id of FIELD_IDS) {
const input = el(id);
const slot = document.createElement("small");
slot.className = "field-error";
slot.hidden = true;
input.closest("label").appendChild(slot);
errSlots[id] = slot;
input.addEventListener(input.tagName === "SELECT" ? "change" : "input", () => {
slot.hidden = true;
input.removeAttribute("aria-invalid");
});
}
function clearErrors() {
errorEl.hidden = true;
for (const id of FIELD_IDS) {
errSlots[id].hidden = true;
el(id).removeAttribute("aria-invalid");
}
}
// Inline error on a specific field: show the message under it, focus it,
// and scroll it to mid-screen so the message clears the on-screen keyboard.
function showFieldError(id, msg) {
const input = el(id);
const slot = errSlots[id];
slot.textContent = msg;
slot.hidden = false;
input.setAttribute("aria-invalid", "true");
input.focus({ preventScroll: true });
input.closest("label").scrollIntoView({ block: "center", behavior: "smooth" });
}
// Global error (network / server failures — not tied to one field).
function showFormError(msg) {
errorEl.textContent = msg;
errorEl.hidden = false;
}
// Returns [fieldId, message] for the first invalid field, or null.
function firstInvalid() {
if (!val("kolName")) {
return ["kolName", "Please enter your name."];
}
if (!EMAIL_RE.test(val("kolEmail").toLowerCase())) {
return ["kolEmail", "Please enter a valid email."];
}
for (const [id, label] of REQUIRED) {
if (!val(id)) return [id, `Please tell us ${label}.`];
}
return null;
}
function showSuccess() {
document.getElementById("formView").hidden = true;
document.getElementById("formFoot").hidden = true;
document.getElementById("successView").hidden = false;
window.scrollTo(0, 0);
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
clearErrors();
const invalid = firstInvalid();
if (invalid) {
showFieldError(invalid[0], invalid[1]);
return;
}
const fd = new FormData();
fd.append("kol_name", val("kolName"));
fd.append("kol_email", val("kolEmail").toLowerCase());
fd.append("received_size", val("receivedSize"));
fd.append("received_model", val("receivedModel"));
fd.append("best_fit_finger", val("bestFitFinger"));
fd.append("fit_quality", val("fitQuality"));
fd.append("hand", val("hand"));
fd.append("notes", val("notes"));
const photo = el("photo").files[0];
if (photo) fd.append("photo", photo, photo.name);
submitBtn.disabled = true;
const originalLabel = submitBtn.textContent;
submitBtn.textContent = "Submitting…";
try {
const resp = await fetch("/api/fit-feedback", { method: "POST", body: fd });
const data = await resp.json().catch(() => ({}));
if (resp.ok && data.success) {
showSuccess();
return;
}
showFormError(data.error || "Could not submit. Please try again.");
} catch (err) {
showFormError("Network error. Please check your connection and retry.");
} finally {
submitBtn.disabled = false;
submitBtn.textContent = originalLabel;
}
});