From 8f72652ada2288c4367b8ebda35d98306aedcb80 Mon Sep 17 00:00:00 2001 From: webmaster Date: Mon, 2 Nov 2026 15:26:31 +0000 Subject: [PATCH] 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). --- de/odoo-erp-automation-bootcamp/index.html | 90 +++++++++++++++ es/odoo-erp-automation-bootcamp/index.html | 90 +++++++++++++++ odoo-erp-automation-bootcamp/index.html | 90 +++++++++++++++ static/bootcamp-waitlist.js | 123 +++++++++++++++++++++ 4 files changed, 393 insertions(+) create mode 100644 static/bootcamp-waitlist.js diff --git a/de/odoo-erp-automation-bootcamp/index.html b/de/odoo-erp-automation-bootcamp/index.html index 4c787d1..465ffc6 100644 --- a/de/odoo-erp-automation-bootcamp/index.html +++ b/de/odoo-erp-automation-bootcamp/index.html @@ -452,6 +452,77 @@ .btn-brass:focus-visible { outline-color: #fff; } + /* Waiting list — for someone who cannot attend the January cohort. + Deliberately quieter than the Buy button: outline button, smaller type, no brass fill. */ + .bc-waitlist { + /* Component-scoped error colour: the site has no shared token for it. */ + --wl-error: #b02a37; + max-width: 720px; + margin: 2.5rem auto 0; + padding: 1.5rem; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--ivory); + text-align: left; + } + .bc-wl-title { + margin: 0 0 0.5rem; + font-size: 1.1rem; + color: var(--ink); + } + .bc-wl-lead { + margin: 0 0 1.1rem; + font-size: 0.88rem; + line-height: 1.6; + color: rgba(23, 28, 38, 0.78); /* the page's own .bc-note value, on the same cream */ + } + .bc-wl-form input, + .bc-wl-form textarea { + display: block; + width: 100%; + box-sizing: border-box; + margin-bottom: 0.6rem; + padding: 0.6rem 0.75rem; + font-family: Inter, sans-serif; + font-size: 0.9rem; + color: var(--ink); + background: #fff; + border: 1px solid var(--line); + border-radius: var(--radius); + } + .bc-wl-form textarea { + min-height: 3.4rem; + resize: vertical; + } + .bc-wl-btn { + padding: 0.6rem 1.5rem; + font-size: 0.8rem; + background: transparent; + color: var(--ink); + border-color: rgba(23, 28, 38, 0.35); + } + .bc-wl-btn:hover { + border-color: var(--brass); + background: rgba(201, 162, 39, 0.12); + transform: none; + box-shadow: none; + } + .bc-wl-btn:disabled { + opacity: 0.6; + cursor: default; + } + .bc-wl-success { + margin: 0; + font-size: 0.9rem; + font-weight: 600; + color: var(--success); + } + .bc-wl-error { + margin: 0; + font-size: 0.9rem; + color: var(--wl-error); + } + + diff --git a/es/odoo-erp-automation-bootcamp/index.html b/es/odoo-erp-automation-bootcamp/index.html index 8da038b..91dc634 100644 --- a/es/odoo-erp-automation-bootcamp/index.html +++ b/es/odoo-erp-automation-bootcamp/index.html @@ -452,6 +452,77 @@ .btn-brass:focus-visible { outline-color: #fff; } + /* Waiting list — for someone who cannot attend the January cohort. + Deliberately quieter than the Buy button: outline button, smaller type, no brass fill. */ + .bc-waitlist { + /* Component-scoped error colour: the site has no shared token for it. */ + --wl-error: #b02a37; + max-width: 720px; + margin: 2.5rem auto 0; + padding: 1.5rem; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--ivory); + text-align: left; + } + .bc-wl-title { + margin: 0 0 0.5rem; + font-size: 1.1rem; + color: var(--ink); + } + .bc-wl-lead { + margin: 0 0 1.1rem; + font-size: 0.88rem; + line-height: 1.6; + color: rgba(23, 28, 38, 0.78); /* the page's own .bc-note value, on the same cream */ + } + .bc-wl-form input, + .bc-wl-form textarea { + display: block; + width: 100%; + box-sizing: border-box; + margin-bottom: 0.6rem; + padding: 0.6rem 0.75rem; + font-family: Inter, sans-serif; + font-size: 0.9rem; + color: var(--ink); + background: #fff; + border: 1px solid var(--line); + border-radius: var(--radius); + } + .bc-wl-form textarea { + min-height: 3.4rem; + resize: vertical; + } + .bc-wl-btn { + padding: 0.6rem 1.5rem; + font-size: 0.8rem; + background: transparent; + color: var(--ink); + border-color: rgba(23, 28, 38, 0.35); + } + .bc-wl-btn:hover { + border-color: var(--brass); + background: rgba(201, 162, 39, 0.12); + transform: none; + box-shadow: none; + } + .bc-wl-btn:disabled { + opacity: 0.6; + cursor: default; + } + .bc-wl-success { + margin: 0; + font-size: 0.9rem; + font-weight: 600; + color: var(--success); + } + .bc-wl-error { + margin: 0; + font-size: 0.9rem; + color: var(--wl-error); + } + + diff --git a/odoo-erp-automation-bootcamp/index.html b/odoo-erp-automation-bootcamp/index.html index dbfb12f..3a9ed0c 100644 --- a/odoo-erp-automation-bootcamp/index.html +++ b/odoo-erp-automation-bootcamp/index.html @@ -452,6 +452,77 @@ .btn-brass:focus-visible { outline-color: #fff; } + /* Waiting list — for someone who cannot attend the January cohort. + Deliberately quieter than the Buy button: outline button, smaller type, no brass fill. */ + .bc-waitlist { + /* Component-scoped error colour: the site has no shared token for it. */ + --wl-error: #b02a37; + max-width: 720px; + margin: 2.5rem auto 0; + padding: 1.5rem; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--ivory); + text-align: left; + } + .bc-wl-title { + margin: 0 0 0.5rem; + font-size: 1.1rem; + color: var(--ink); + } + .bc-wl-lead { + margin: 0 0 1.1rem; + font-size: 0.88rem; + line-height: 1.6; + color: rgba(23, 28, 38, 0.78); /* the page's own .bc-note value, on the same cream */ + } + .bc-wl-form input, + .bc-wl-form textarea { + display: block; + width: 100%; + box-sizing: border-box; + margin-bottom: 0.6rem; + padding: 0.6rem 0.75rem; + font-family: Inter, sans-serif; + font-size: 0.9rem; + color: var(--ink); + background: #fff; + border: 1px solid var(--line); + border-radius: var(--radius); + } + .bc-wl-form textarea { + min-height: 3.4rem; + resize: vertical; + } + .bc-wl-btn { + padding: 0.6rem 1.5rem; + font-size: 0.8rem; + background: transparent; + color: var(--ink); + border-color: rgba(23, 28, 38, 0.35); + } + .bc-wl-btn:hover { + border-color: var(--brass); + background: rgba(201, 162, 39, 0.12); + transform: none; + box-shadow: none; + } + .bc-wl-btn:disabled { + opacity: 0.6; + cursor: default; + } + .bc-wl-success { + margin: 0; + font-size: 0.9rem; + font-weight: 600; + color: var(--success); + } + .bc-wl-error { + margin: 0; + font-size: 0.9rem; + color: var(--wl-error); + } + + diff --git a/static/bootcamp-waitlist.js b/static/bootcamp-waitlist.js new file mode 100644 index 0000000..8c3d301 --- /dev/null +++ b/static/bootcamp-waitlist.js @@ -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(); + } +})();