Files
webmaster 2aa9b19c3c perf(bootcamp): fix CLS/LCP + a11y on EN/DE/ES; add the missing footer-form handler
PageSpeed pass on /odoo-erp-automation-bootcamp/ (EN + DE + ES):

- CLS: /style.css was deferred via media="print" onload=... → unstyled first paint
  shifted BODY 0.31 (live CLS 0.326). Now a normal <link rel="stylesheet">; the two
  hero webp files are preloaded (fetchpriority=high) by viewport.
- a11y: footer <h4> → <h3> (heading-order), a lighter brass + underline for links
  inside on-dark notes (color-contrast, link-in-text-block), .fcopy 3.2:1 → 5.17:1.
- best-practices: the footer contact forms and the waiting-list form now post to the
  CRM webhook instead of action="mailto:", which Lighthouse flags as an insecure
  request (is-on-https 0 → 1). No Google Ads tag on these pages (no consent gate).
- regression fix: these pages do NOT load /script.js, so after the action change the
  footer form had no JS handler and a submit navigated the visitor to the webhook's raw
  JSON reply. static/bootcamp-waitlist.js now carries a footer-form handler mirroring
  /script.js one-for-one (same webhook, payload { email, requirements, area:'message' }
  in UTM.inject, success/failure behaviour); its two strings are read from the form's
  own data-ok / data-err attributes so each language keeps its own text. ENDPOINT
  hoisted to module scope so both handlers share it.
- no-JS path: a <noscript> footer line with support@ODOO4projects.com as a plain
  mailto link. Verified it does not re-trigger is-on-https (unlike a mailto FORM action).

Guards held: Buy forms still post product_id 17; exactly one [data-bc-seats];
waitlist still posts area:'message'; plausible snippet present; competitor QC exit 0.
2026-11-02 16:07:20 +00:00

187 lines
7.2 KiB
JavaScript

/* 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.
*/
var ENDPOINT = 'https://n8n.odoo4projects.com/webhook/ODOO4projects/crm';
(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 ---- */
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();
}
})();
/* ---- Footer contact form ------------------------------------------------------------
* These pages do NOT load /script.js, so its footer-form handler does not run here. This
* mirrors /script.js's handler one-for-one — same webhook, same payload shape
* ({ email, requirements, area:'message' } wrapped in UTM.inject), same success/failure
* behaviour — but reads its two strings from the form's own data-ok / data-err attributes
* so the message is in the page's language. Without it a submit would navigate the visitor
* to the webhook's raw JSON reply.
*/
(function () {
'use strict';
function runFooter() {
var form = document.getElementById('footerContactForm');
if (!form) return;
var emailInput = document.getElementById('footerContactEmail');
var textInput = document.getElementById('footerContactText');
var submitBtn = form.querySelector('.footer-send-btn');
var status = document.getElementById('footerFormStatus');
var okText = form.getAttribute('data-ok') || "Thanks! We'll get back to you shortly.";
var errText = form.getAttribute('data-err') || 'Something went wrong. Please try again.';
function showStatus(message, isError) {
status.textContent = message;
status.classList.toggle('error', !!isError);
status.classList.add('visible');
}
form.addEventListener('submit', function (e) {
e.preventDefault();
var email = emailInput.value.trim();
var text = textInput.value.trim();
if (!email || !text) return;
submitBtn.disabled = true;
status.classList.remove('visible');
fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(window.UTM.inject({
email: email,
requirements: text,
area: 'message'
}))
}).then(function (r) {
if (!r.ok) throw new Error('bad status ' + r.status);
form.reset();
submitBtn.disabled = false;
showStatus(okText, false);
}).catch(function () {
submitBtn.disabled = false;
showStatus(errText, true);
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', runFooter);
} else {
runFooter();
}
})();