Public Access
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.
187 lines
7.2 KiB
JavaScript
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();
|
|
}
|
|
})();
|