feat(bootcamp): waiting-list form for another timezone or language

Three bootcamp pages (EN/DE/ES) get a small waiting-list block below the Buy
button, in the Price and booking section next to the live seat count.

- two required fields: email + comment; framed as "another timezone or
  language?" and worded around the January cohort's own schedule
- posts to the same n8n CRM webhook as the site's other forms, with the
  footer-message payload shape copied exactly: area:'message',
  requirements carries the comment, no name field, wrapped in UTM.inject()
- success hides the form and shows a confirmation; a failure restores the
  button and shows a retryable error (never stuck on "sending")
- Plausible event 'Bootcamp Waitlist' on success only (cookieless); no
  Google Ads tag and no conversion, as on the rest of these pages
- new page-local /static/bootcamp-waitlist.js carries the verbatim UTM
  helpers from script.js (this page does not load script.js)
- buying is untouched: no markup change to the three product-17 forms

Verified: exact posted body per language captured on the wire, success and
error states in a real browser, 375 px no overflow, verify-html 0, JS syntax
0, competitor QC 0 new, impeccable detect byte-identical (zero new findings).
This commit is contained in:
webmaster committed 2026-11-02 15:26:31 +00:00
1 parent 812571b177
commit 8f72652ada
4 files changed
+393

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
/* bootcamp-waitlist.js — page-local waiting-list form for the three bootcamp pages.
*
* The user's rule: "You want another timezone / language? Join the waiting list."
* The form posts to the same n8n CRM webhook the site's other forms use, with the
* site's own footer-message payload shape — area:'message', no `name` (that belongs to
* the `area:'quote'` shape of the Custom Quote modal).
*
* This page does not load /script.js, so the UTM helpers are copied verbatim from it
* (same as /static/black-friday.js does). If script.js's helpers change, change these.
*
* All copy — labels, placeholders, the "sending" label, success and error text — lives in
* the page markup, in the page's own language. This file carries no strings.
*
* A 200 from the webhook means the workflow accepted the request, not that a CRM record
* exists (the workflow answers before it runs) — same as the site's other forms.
*/
(function () {
'use strict';
/* ---- UTM tracking helpers (verbatim from /script.js) ---- */
function getCookie(name) {
var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? decodeURIComponent(match[2]) : null;
}
function setCookie(name, value, days) {
var expires = '';
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = '; expires=' + date.toUTCString();
}
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/; SameSite=Lax';
}
function parseUTM() {
var urlParams = new URLSearchParams(window.location.search);
var utm = {};
var keys = ['source', 'medium', 'campaign'];
for (var i = 0; i < keys.length; i++) {
var val = urlParams.get('utm_' + keys[i]);
if (val) {
utm[keys[i]] = val;
setCookie('utm_' + keys[i], val, 90);
}
}
return utm;
}
function getStoredUTM() {
var utm = {};
var keys = ['source', 'medium', 'campaign'];
for (var i = 0; i < keys.length; i++) {
var val = getCookie('utm_' + keys[i]);
if (val) utm[keys[i]] = val;
}
return utm;
}
function injectUTM(data) {
var stored = getStoredUTM();
if (Object.keys(stored).length > 0) {
data.utm_source = stored.source || null;
data.utm_medium = stored.medium || null;
data.utm_campaign = stored.campaign || null;
}
return data;
}
window.UTM = { parse: parseUTM, getStored: getStoredUTM, inject: injectUTM };
window.UTM.parse();
/* ---- Waiting list ---- */
var ENDPOINT = 'https://n8n.odoo4projects.com/webhook/ODOO4projects/crm';
function run() {
var form = document.getElementById('bcWaitlistForm');
if (!form) return;
var emailInput = document.getElementById('bcWaitlistEmail');
var commentInput = document.getElementById('bcWaitlistComment');
var submitBtn = form.querySelector('button[type="submit"]');
var success = document.getElementById('bcWaitlistSuccess');
var error = document.getElementById('bcWaitlistError');
var label = submitBtn ? submitBtn.textContent : '';
/* Both fields are required, exactly as the footer form requires both. */
form.addEventListener('submit', function (e) {
e.preventDefault();
var email = emailInput.value.trim();
var comment = commentInput.value.trim();
if (!email || !comment) return;
error.style.display = 'none';
submitBtn.textContent = submitBtn.getAttribute('data-sending') || label;
submitBtn.disabled = true;
fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(UTM.inject({
email: email,
requirements: comment,
area: 'message'
}))
}).then(function (r) {
if (!r.ok) throw new Error('bad status ' + r.status);
if (window.plausible) window.plausible('Bootcamp Waitlist');
form.style.display = 'none';
success.style.display = 'block';
}).catch(function () {
submitBtn.textContent = label;
submitBtn.disabled = false;
error.style.display = 'block';
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run);
} else {
run();
}
})();