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

@@ -452,6 +452,77 @@
.btn-brass:focus-visible { .btn-brass:focus-visible {
outline-color: #fff; 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);
}
</style> </style>
<script type="application/ld+json"> <script type="application/ld+json">
{ {
@@ -783,6 +854,24 @@
<button type="submit" class="btn btn-brass btn-lg">Für 595.00 USD kaufen</button> <button type="submit" class="btn btn-brass btn-lg">Für 595.00 USD kaufen</button>
</form> </form>
</div> </div>
<!-- Waiting list: for a visitor who cannot attend the January cohort.
Secondary to the Buy button above. Same CRM webhook and same payload shape as the
site's footer contact form (area:'message'); the copy lives here, in the page's
language, and /static/bootcamp-waitlist.js carries no strings. -->
<div class="bc-waitlist">
<h3 class="bc-wl-title">Andere Zeitzone oder andere Sprache?</h3>
<p class="bc-wl-lead">Der Januar-Kurs läuft donnerstags 18:00&ndash;19:00 US-Zentralzeit, auf Englisch. Wenn Sie eine andere Uhrzeit oder eine andere Sprache brauchen, tragen Sie sich in die Warteliste ein. Wir schreiben Ihnen, sobald ein passender Kurs startet.</p>
<form id="bcWaitlistForm" class="bc-wl-form" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
<label class="bc-sr-only" for="bcWaitlistEmail">Ihre E-Mail-Adresse</label>
<input type="email" id="bcWaitlistEmail" name="email" placeholder="Ihre E-Mail-Adresse" required />
<label class="bc-sr-only" for="bcWaitlistComment">Welche Zeitzone oder Sprache Sie brauchen</label>
<textarea id="bcWaitlistComment" name="comment" rows="2" placeholder="Welche Zeitzone oder Sprache brauchen Sie?" required></textarea>
<button type="submit" class="btn bc-wl-btn" data-sending="Wird eingetragen&hellip;">In die Warteliste eintragen</button>
</form>
<p class="bc-wl-success" id="bcWaitlistSuccess" role="status" style="display:none">Sie stehen auf der Warteliste. Wir schreiben Ihnen, sobald ein Kurs in Ihrer Zeitzone oder Sprache angekündigt wird.</p>
<p class="bc-wl-error" id="bcWaitlistError" role="alert" style="display:none">Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.</p>
</div>
<ul class="bc-related"> <ul class="bc-related">
<li><a href="/de/support.html#contact">Fragen vor der Buchung? Sprechen Sie zuerst mit uns</a></li> <li><a href="/de/support.html#contact">Fragen vor der Buchung? Sprechen Sie zuerst mit uns</a></li>
<li><a href="/de/odoo-hosting-2027.html">Wie das Hosting nach Ihrem Jahr bei uns aussieht</a></li> <li><a href="/de/odoo-hosting-2027.html">Wie das Hosting nach Ihrem Jahr bei uns aussieht</a></li>
@@ -881,6 +970,7 @@
</footer> </footer>
<script defer src="/static/buy.js"></script> <script defer src="/static/buy.js"></script>
<script defer src="/static/bootcamp-availability.js"></script> <script defer src="/static/bootcamp-availability.js"></script>
<script defer src="/static/bootcamp-waitlist.js"></script>
<script defer src="/static/lang.js"></script> <script defer src="/static/lang.js"></script>
</body> </body>
</html> </html>
@@ -452,6 +452,77 @@
.btn-brass:focus-visible { .btn-brass:focus-visible {
outline-color: #fff; 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);
}
</style> </style>
<script type="application/ld+json"> <script type="application/ld+json">
{ {
@@ -783,6 +854,24 @@
<button type="submit" class="btn btn-brass btn-lg">Comprar por 595.00 USD</button> <button type="submit" class="btn btn-brass btn-lg">Comprar por 595.00 USD</button>
</form> </form>
</div> </div>
<!-- Waiting list: for a visitor who cannot attend the January cohort.
Secondary to the Buy button above. Same CRM webhook and same payload shape as the
site's footer contact form (area:'message'); the copy lives here, in the page's
language, and /static/bootcamp-waitlist.js carries no strings. -->
<div class="bc-waitlist">
<h3 class="bc-wl-title">&iquest;Otra zona horaria u otro idioma?</h3>
<p class="bc-wl-lead">El grupo de enero se reúne los jueves, 18:00&ndash;19:00, hora del centro de EE. UU., en inglés. Si necesita otro horario u otro idioma, apúntese a la lista de espera y le escribiremos cuando se abra un grupo que le encaje.</p>
<form id="bcWaitlistForm" class="bc-wl-form" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
<label class="bc-sr-only" for="bcWaitlistEmail">Su dirección de correo electrónico</label>
<input type="email" id="bcWaitlistEmail" name="email" placeholder="Su dirección de correo electrónico" required />
<label class="bc-sr-only" for="bcWaitlistComment">Qu&eacute; zona horaria o idioma necesita</label>
<textarea id="bcWaitlistComment" name="comment" rows="2" placeholder="¿Qué zona horaria o idioma necesita?" required></textarea>
<button type="submit" class="btn bc-wl-btn" data-sending="Apuntando&hellip;">Apuntarme a la lista de espera</button>
</form>
<p class="bc-wl-success" id="bcWaitlistSuccess" role="status" style="display:none">Está en la lista de espera. Le escribiremos en cuanto se anuncie un grupo en su zona horaria o idioma.</p>
<p class="bc-wl-error" id="bcWaitlistError" role="alert" style="display:none">Algo ha fallado. Inténtelo de nuevo.</p>
</div>
<ul class="bc-related"> <ul class="bc-related">
<li><a href="/es/support.html#contact">¿Preguntas antes de reservar? Hable antes con nosotros</a></li> <li><a href="/es/support.html#contact">¿Preguntas antes de reservar? Hable antes con nosotros</a></li>
<li><a href="/es/odoo-hosting-2027.html">Cómo es el hosting después de su año con nosotros</a></li> <li><a href="/es/odoo-hosting-2027.html">Cómo es el hosting después de su año con nosotros</a></li>
@@ -881,6 +970,7 @@
</footer> </footer>
<script defer src="/static/buy.js"></script> <script defer src="/static/buy.js"></script>
<script defer src="/static/bootcamp-availability.js"></script> <script defer src="/static/bootcamp-availability.js"></script>
<script defer src="/static/bootcamp-waitlist.js"></script>
<script defer src="/static/lang.js"></script> <script defer src="/static/lang.js"></script>
</body> </body>
</html> </html>
+90
View File
@@ -452,6 +452,77 @@
.btn-brass:focus-visible { .btn-brass:focus-visible {
outline-color: #fff; 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);
}
</style> </style>
<script type="application/ld+json"> <script type="application/ld+json">
{ {
@@ -801,6 +872,24 @@
<button type="submit" class="btn btn-brass btn-lg">Buy for 595.00 USD</button> <button type="submit" class="btn btn-brass btn-lg">Buy for 595.00 USD</button>
</form> </form>
</div> </div>
<!-- Waiting list: for a visitor who cannot attend the January cohort.
Secondary to the Buy button above. Same CRM webhook and same payload shape as the
site's footer contact form (area:'message'); the copy lives here, in the page's
language, and /static/bootcamp-waitlist.js carries no strings. -->
<div class="bc-waitlist">
<h3 class="bc-wl-title">Another timezone or language?</h3>
<p class="bc-wl-lead">The January cohort runs Thursdays 18:00&ndash;19:00 Central Time, in English. If you need a different time or another language, join the waiting list and we will write to you when a cohort that fits opens.</p>
<form id="bcWaitlistForm" class="bc-wl-form" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
<label class="bc-sr-only" for="bcWaitlistEmail">Your email address</label>
<input type="email" id="bcWaitlistEmail" name="email" placeholder="Your email address" required />
<label class="bc-sr-only" for="bcWaitlistComment">Which timezone or language you need</label>
<textarea id="bcWaitlistComment" name="comment" rows="2" placeholder="Which timezone or language do you need?" required></textarea>
<button type="submit" class="btn bc-wl-btn" data-sending="Adding you&hellip;">Join the waiting list</button>
</form>
<p class="bc-wl-success" id="bcWaitlistSuccess" role="status" style="display:none">You are on the waiting list. We will write to you as soon as a cohort in your timezone or language is announced.</p>
<p class="bc-wl-error" id="bcWaitlistError" role="alert" style="display:none">Something went wrong. Please try again.</p>
</div>
<ul class="bc-related"> <ul class="bc-related">
<li><a href="/support.html#contact">Questions before you book? Talk to us first</a></li> <li><a href="/support.html#contact">Questions before you book? Talk to us first</a></li>
<li><a href="/workshops/">All our live courses, side by side</a></li> <li><a href="/workshops/">All our live courses, side by side</a></li>
@@ -901,6 +990,7 @@
</footer> </footer>
<script defer src="/static/buy.js"></script> <script defer src="/static/buy.js"></script>
<script defer src="/static/bootcamp-availability.js"></script> <script defer src="/static/bootcamp-availability.js"></script>
<script defer src="/static/bootcamp-waitlist.js"></script>
<script defer src="/static/lang.js"></script> <script defer src="/static/lang.js"></script>
</body> </body>
</html> </html>
+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();
}
})();