/* bootcamp-availability.js — live remaining-seat count for the bootcamp cohort. * * Source of truth: the availability webhook (product 17 = Bootcamp Q1-2027). * Progressive enhancement, same spirit as buy.js — it only ever ADDS, and it can * never block the buy: * * fetch OK, seats > 0 -> fill the [data-bc-seats] line with " of 6 seats available" * in the page's own language; Buy stays enabled. * fetch OK, seats == 0 -> say the cohort is full, point at the next one, and disable * the Buy buttons (we must not sell a full cohort). * anything else -> DO NOTHING. Timeout, network error, CORS, non-200 or an * unexpected payload leave the page exactly as it is: the static * cap text shows and the Buy buttons stay enabled. A * conversion-critical element must not fail closed. * * The count is live and authoritative, so it is never written into the HTML as a fixed * string — the [data-bc-seats] element ships empty and hidden. */ (function () { 'use strict'; var ENDPOINT = 'https://n8n.odoo4projects.com/webhook/ODOO4projects/available'; var PRODUCT_ID = 17; var CAP = 6; /* the page's own cohort cap ("Limited to 6 participants") */ var TIMEOUT_MS = 6000; /* The page's own words and language. "seat" = seat / Platz / plaza, as used elsewhere on the same page ("A seat can be transferred" / "Ein Platz ist übertragbar" / "Una plaza se puede transferir"). */ var TEXT = { en: { available: function (n) { return n + ' of ' + CAP + ' seats available'; }, full: 'This cohort is full.', next: 'Talk to us about the next cohort', href: '/support.html#contact' }, de: { available: function (n) { return n + ' von ' + CAP + ' Plätzen frei'; }, full: 'Dieser Kurs ist ausgebucht.', next: 'Sprechen Sie mit uns über den nächsten Kurs', href: '/de/support.html#contact' }, es: { available: function (n) { return n + ' de ' + CAP + ' plazas disponibles'; }, full: 'Este grupo está completo.', next: 'Hable con nosotros sobre el próximo grupo', href: '/es/support.html#contact' } }; function lang() { var l = (document.documentElement.getAttribute('lang') || 'en').toLowerCase(); if (l.indexOf('de') === 0) return 'de'; if (l.indexOf('es') === 0) return 'es'; return 'en'; } function renderAvailable(el, n, t) { el.textContent = ''; var strong = document.createElement('strong'); strong.textContent = t.available(n); el.appendChild(strong); el.hidden = false; } function renderFull(el, t) { el.textContent = ''; var strong = document.createElement('strong'); strong.textContent = t.full; el.appendChild(strong); el.appendChild(document.createTextNode(' ')); var a = document.createElement('a'); a.href = t.href; a.textContent = t.next; el.appendChild(a); el.hidden = false; disableBuy(); } function disableBuy() { var buttons = document.querySelectorAll('form[action$="/shop/post_basket"] button[type="submit"]'); for (var i = 0; i < buttons.length; i++) { buttons[i].disabled = true; buttons[i].setAttribute('aria-disabled', 'true'); } } /* Unwrap [{data:[...]}] or {data:[...]} and read product 17's qty_available. Returns a finite number >= 0, or null if the shape is not what we expect. */ function seatsFrom(json) { var obj = json; if (Array.isArray(obj)) obj = obj[0]; if (!obj || !Array.isArray(obj.data)) return null; for (var i = 0; i < obj.data.length; i++) { var row = obj.data[i]; if (row && Number(row.id) === PRODUCT_ID) { var q = Number(row.qty_available); return (isFinite(q) && q >= 0) ? q : null; } } return null; } function run() { var el = document.querySelector('[data-bc-seats]'); if (!el) return; if (!window.fetch || typeof AbortController === 'undefined') return; /* keep fallback */ var t = TEXT[lang()]; var ctrl = new AbortController(); var timer = setTimeout(function () { ctrl.abort(); }, TIMEOUT_MS); fetch(ENDPOINT, { signal: ctrl.signal, headers: { 'Accept': 'application/json' }, cache: 'no-store' }) .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(function (json) { var n = seatsFrom(json); if (n === null) return; /* unexpected payload -> keep fallback */ if (n === 0) renderFull(el, t); else renderAvailable(el, n, t); }) .catch(function () { /* keep fallback; Buy stays enabled */ }) .then(function () { clearTimeout(timer); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', run); } else { run(); } })();