Public Access
feat(bf): lead /odoo-black-friday/ with the homepage hero, move the Bootcamp box to the foot
- Hero is the homepage's (same markup/assets): slide-in city image + the server location selector below it that slides a chosen city in; headline adapted to the offer. - Order now: hero > location selector > deal (HOST27, $295 first year / $395 after, one-click product-4 buy with redirect=/coupon/HOST27?r=/shop/checkout) > trust bar > Black Friday FAQ > features + trial card > FAQ > pricing (buy section) > Bootcamp > footer. - Bootcamp box restored from webmaster/out/2026-11-02-bootcamp-box-removed-archive.md: heading, USD 595.00, mechanism line, deadline, CTA, its meta/og contribution, its JSON-LD Product entry and the FAQ answer are all back and consistent. - Excluded (no blog, no OCA modules, no Project Plan); no dangling refs. - New /static/black-friday.js = the homepage behaviours this page needs (root-relative image paths), with the blog/OCA/project-plan blocks removed. - Canonical URL unchanged; competitor QC 0 on the page.
This commit is contained in:
1 parent
e6f0b6a395
commit
0a493d4e10
2 files changed
+767
-99
No files matched your search
@@ -0,0 +1,426 @@
|
||||
/* Page-local script for /odoo-black-friday/.
|
||||
The homepage behaviours this landing page needs, copied from the site's /script.js:
|
||||
the hero slide-in image driven by the server-location selector, the Buy Now buttons,
|
||||
the free-trial card, the Custom Quote modal and the footer contact form.
|
||||
The blog, OCA-modules and project-plan blocks of /script.js are removed here because this
|
||||
page does not carry those sections (and those blocks throw on a page without their markup).
|
||||
Image paths are root-relative: this page lives in a subdirectory (/odoo-black-friday/).
|
||||
ES5, no build step, no new dependency beyond the gsap the homepage already loads. */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
/* ---- UTM tracking helpers (verbatim from /script.js) ---- */
|
||||
(function () {
|
||||
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;
|
||||
}
|
||||
function appendToForm(form) {
|
||||
var stored = getStoredUTM();
|
||||
var keys = ['source', 'medium', 'campaign'];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (stored[keys[i]]) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'utm_' + keys[i];
|
||||
input.value = stored[keys[i]];
|
||||
form.appendChild(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
function toQuery() {
|
||||
var stored = getStoredUTM();
|
||||
var parts = [];
|
||||
var keys = ['source', 'medium', 'campaign'];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (stored[keys[i]]) {
|
||||
parts.push('utm_' + keys[i] + '=' + encodeURIComponent(stored[keys[i]]));
|
||||
}
|
||||
}
|
||||
return parts.join('&');
|
||||
}
|
||||
window.UTM = { parse: parseUTM, getStored: getStoredUTM, inject: injectUTM, appendToForm: appendToForm, toQuery: toQuery };
|
||||
window.UTM.parse();
|
||||
})();
|
||||
|
||||
/* ---- Hero slide-in image + server-location selector (homepage behaviour) ---- */
|
||||
(function () {
|
||||
var SERVERS = [
|
||||
{ host: 'api.meppel.odoo4projects.com', city: 'Amsterdam' },
|
||||
{ host: 'api.boston.odoo4projects.com', city: 'Boston' },
|
||||
{ host: 'api.manchester.odoo4projects.com', city: 'Manchester' },
|
||||
{ host: 'api.mumbai.odoo4projects.com', city: 'Mumbai' },
|
||||
{ host: 'api.saopaulo.odoo4projects.com', city: 'S\u00e3o Paulo' },
|
||||
{ host: 'api.sydney.odoo4projects.com', city: 'Sydney' }
|
||||
];
|
||||
/* Root-relative on purpose: this page is served from /odoo-black-friday/. */
|
||||
var heroImages = {
|
||||
'api.meppel.odoo4projects.com': '/images/hero/amsterdam.webp',
|
||||
'api.boston.odoo4projects.com': '/images/hero/boston.webp',
|
||||
'api.manchester.odoo4projects.com': '/images/hero/london.webp',
|
||||
'api.mumbai.odoo4projects.com': '/images/hero/mumbai.webp',
|
||||
'api.saopaulo.odoo4projects.com': '/images/hero/brazil.webp',
|
||||
'api.sydney.odoo4projects.com': '/images/hero/sydney.webp'
|
||||
};
|
||||
|
||||
function pingServer(host) {
|
||||
return new Promise(function (resolve) {
|
||||
var start = performance.now();
|
||||
var img = new Image();
|
||||
var timedOut = false;
|
||||
var timer = setTimeout(function () {
|
||||
timedOut = true; img.src = '';
|
||||
resolve({ host: host, time: -1 });
|
||||
}, 2000);
|
||||
img.onload = function () {
|
||||
var elapsed = performance.now() - start;
|
||||
if (!timedOut) { clearTimeout(timer); resolve({ host: host, time: elapsed }); }
|
||||
};
|
||||
img.onerror = function () {
|
||||
var elapsed = performance.now() - start;
|
||||
if (!timedOut) { clearTimeout(timer); resolve({ host: host, time: elapsed }); }
|
||||
};
|
||||
img.src = 'https://' + host + '/favicon.ico?_=' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
|
||||
});
|
||||
}
|
||||
|
||||
function animateHero(city, image, showLabel) {
|
||||
var hero = document.querySelector('.hero');
|
||||
if (!hero || !window.gsap) return;
|
||||
var bg = hero.querySelector('.hero-bg');
|
||||
if (!bg) {
|
||||
bg = document.createElement('div');
|
||||
bg.className = 'hero-bg';
|
||||
hero.insertBefore(bg, hero.firstChild);
|
||||
}
|
||||
var hasExisting = bg.children.length > 0;
|
||||
var newEl = document.createElement('div');
|
||||
newEl.className = 'hero-bg-slide';
|
||||
newEl.style.backgroundImage = "url('" + image + "')";
|
||||
bg.appendChild(newEl);
|
||||
|
||||
var tl = gsap.timeline();
|
||||
gsap.set(newEl, { x: '100%' });
|
||||
|
||||
var label = null;
|
||||
if (showLabel !== false && city) {
|
||||
var oldLabel = hero.querySelector('.hero-city-label');
|
||||
if (oldLabel) oldLabel.remove();
|
||||
label = document.createElement('div');
|
||||
label.className = 'hero-city-label';
|
||||
label.innerHTML = 'Our <strong>' + city + '</strong> server fits your location';
|
||||
hero.appendChild(label);
|
||||
gsap.set(label, { x: '150%' });
|
||||
}
|
||||
|
||||
if (hasExisting) {
|
||||
var oldEl = bg.children[0];
|
||||
gsap.set(oldEl, { x: '0%' });
|
||||
tl.to(oldEl, { x: '100%', duration: 3, ease: 'power2.out' }, 0)
|
||||
.to(newEl, { x: '0%', duration: 3, ease: 'power2.out' }, 0);
|
||||
if (label) {
|
||||
tl.to(label, { x: '0%', duration: 2.5, ease: 'power2.out' }, 0.3);
|
||||
}
|
||||
tl.call(function () { oldEl.remove(); });
|
||||
} else {
|
||||
tl.to(newEl, { x: '0%', duration: 3, ease: 'power2.out' }, 0);
|
||||
if (label) {
|
||||
tl.to(label, { x: '0%', duration: 2.5, ease: 'power2.out' }, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Location selector: click a server card to slide its city into the hero. */
|
||||
var serverCards = document.querySelectorAll('.server-card');
|
||||
serverCards.forEach(function (card, idx) {
|
||||
card.addEventListener('click', function () {
|
||||
var s = SERVERS[idx];
|
||||
if (!s || !heroImages[s.host]) return;
|
||||
serverCards.forEach(function (c) { c.classList.remove('server-best'); });
|
||||
card.classList.add('server-best');
|
||||
animateHero(null, heroImages[s.host], false);
|
||||
});
|
||||
});
|
||||
|
||||
/* Populate the trial + pricing location selects. */
|
||||
var trialLoc = document.getElementById('trialLocation');
|
||||
var pricingLoc = document.getElementById('pricingServerLocation');
|
||||
[trialLoc, pricingLoc].forEach(function (el) {
|
||||
if (el) {
|
||||
SERVERS.forEach(function (s) {
|
||||
var opt = document.createElement('option');
|
||||
var cityVal = s.host.replace('api.', '').replace('.odoo4projects.com', '');
|
||||
opt.value = cityVal;
|
||||
opt.textContent = s.city;
|
||||
el.appendChild(opt);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* Ping all servers, highlight the fastest, slide its image in. */
|
||||
var results = [];
|
||||
var pending = SERVERS.length;
|
||||
SERVERS.forEach(function (s) {
|
||||
pingServer(s.host).then(function (r) {
|
||||
results.push(r);
|
||||
pending--;
|
||||
if (pending !== 0) return;
|
||||
var valid = results.filter(function (x) { return x.time >= 0; });
|
||||
if (valid.length === 0) return;
|
||||
valid.sort(function (a, b) { return a.time - b.time; });
|
||||
var best = valid[0];
|
||||
var bestServer = SERVERS.filter(function (x) { return x.host === best.host; })[0];
|
||||
if (bestServer) {
|
||||
[trialLoc, pricingLoc].forEach(function (el) {
|
||||
if (el) {
|
||||
var cityVal = bestServer.host.replace('api.', '').replace('.odoo4projects.com', '');
|
||||
for (var i = 0; i < el.options.length; i++) {
|
||||
if (el.options[i].value === cityVal) { el.selectedIndex = i; break; }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (bestServer && heroImages[best.host]) {
|
||||
var cards = document.querySelectorAll('.server-card');
|
||||
for (var i = 0; i < SERVERS.length && i < cards.length; i++) {
|
||||
if (SERVERS[i].host === best.host) { cards[i].classList.add('server-best'); break; }
|
||||
}
|
||||
var img = new Image();
|
||||
img.onload = function () { animateHero(bestServer.city, heroImages[best.host]); };
|
||||
img.src = heroImages[best.host];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* Free-trial card. */
|
||||
var trialForm = document.getElementById('trialForm');
|
||||
if (trialForm) {
|
||||
trialForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var email = document.getElementById('trialEmail').value.trim();
|
||||
var location = trialLoc ? trialLoc.value : '';
|
||||
if (!email) return;
|
||||
var btn = trialForm.querySelector('.btn-brass');
|
||||
btn.textContent = 'Sending...';
|
||||
btn.disabled = true;
|
||||
fetch('https://n8n.odoo4projects.com/webhook/ODOO4projects/trial', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(UTM.inject({ email: email, location: location }))
|
||||
}).then(function () {
|
||||
if (window.plausible) window.plausible('Trial');
|
||||
trialForm.style.display = 'none';
|
||||
document.getElementById('trialSuccess').classList.add('visible');
|
||||
}).catch(function () {
|
||||
btn.textContent = 'Send';
|
||||
btn.disabled = false;
|
||||
var errorMsg = document.createElement('div');
|
||||
errorMsg.className = 'trial-error visible';
|
||||
errorMsg.textContent = 'Something went wrong. Please try again.';
|
||||
trialForm.appendChild(errorMsg);
|
||||
setTimeout(function () { errorMsg.classList.remove('visible'); }, 3000);
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
/* ---- Buy Now — POST the basket straight to the shop, open checkout in a new tab ---- */
|
||||
window.buyNow = function (productId) {
|
||||
var locSelect = document.getElementById('pricingServerLocation');
|
||||
var location = locSelect ? locSelect.value : '';
|
||||
var verSelect = document.getElementById('pricingOdooVersion');
|
||||
var version = verSelect ? verSelect.value : '';
|
||||
var form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = 'https://20.odoo4projects.com/shop/post_basket';
|
||||
form.target = '_blank';
|
||||
form.style.display = 'none';
|
||||
var basket = document.createElement('input');
|
||||
basket.type = 'hidden';
|
||||
basket.name = 'basket';
|
||||
var item = { product_id: productId, qty: 1 };
|
||||
var info = location ? location.replace('.odoo4projects.com', '') : '';
|
||||
if (version) { info += (info ? '|' : '') + version; }
|
||||
if (info) { item.info = info; }
|
||||
basket.value = JSON.stringify([item]);
|
||||
form.appendChild(basket);
|
||||
var clear = document.createElement('input');
|
||||
clear.type = 'hidden';
|
||||
clear.name = 'clear';
|
||||
clear.value = '1';
|
||||
form.appendChild(clear);
|
||||
UTM.appendToForm(form);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
document.body.removeChild(form);
|
||||
};
|
||||
|
||||
/* ---- Buy buttons + Custom Quote modal ---- */
|
||||
(function () {
|
||||
document.querySelectorAll('.btn-buy').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var id = this.getAttribute('data-id');
|
||||
if (id) buyNow(parseInt(id, 10));
|
||||
});
|
||||
});
|
||||
|
||||
function openCustomQuoteModal() {
|
||||
document.getElementById('customQuotePanel').style.display = 'block';
|
||||
document.getElementById('customQuoteModal').style.display = 'flex';
|
||||
}
|
||||
function closeCustomQuoteModal() {
|
||||
document.getElementById('customQuoteModal').style.display = 'none';
|
||||
document.getElementById('customQuoteForm').reset();
|
||||
document.getElementById('customQuoteSuccess').style.display = 'none';
|
||||
}
|
||||
window.openCustomQuoteModal = openCustomQuoteModal;
|
||||
window.closeCustomQuoteModal = closeCustomQuoteModal;
|
||||
|
||||
document.querySelectorAll('.btn-empire').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () { openCustomQuoteModal(); });
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') {
|
||||
var modal = document.getElementById('customQuoteModal');
|
||||
if (modal && modal.style.display === 'flex') { closeCustomQuoteModal(); }
|
||||
}
|
||||
});
|
||||
|
||||
var customQuoteForm = document.getElementById('customQuoteForm');
|
||||
if (customQuoteForm) {
|
||||
customQuoteForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var name = document.getElementById('cqName').value.trim();
|
||||
var email = document.getElementById('cqEmail').value.trim();
|
||||
var requirements = document.getElementById('cqRequirements').value.trim();
|
||||
if (!name || !email || !requirements) return;
|
||||
var submitBtn = customQuoteForm.querySelector('.btn-lg');
|
||||
submitBtn.textContent = 'Sending...';
|
||||
submitBtn.disabled = true;
|
||||
fetch('https://n8n.odoo4projects.com/webhook/ODOO4projects/crm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(UTM.inject({ name: name, email: email, requirements: requirements, area: 'quote' }))
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error('bad status ' + r.status);
|
||||
if (window.plausible) window.plausible('Custom Quote');
|
||||
customQuoteForm.style.display = 'none';
|
||||
document.getElementById('customQuoteSuccess').style.display = 'block';
|
||||
submitBtn.textContent = 'Submit Request';
|
||||
submitBtn.disabled = false;
|
||||
}).catch(function () {
|
||||
submitBtn.textContent = 'Submit Request';
|
||||
submitBtn.disabled = false;
|
||||
var errorMsg = document.createElement('div');
|
||||
errorMsg.style.marginTop = '1rem';
|
||||
errorMsg.style.color = '#dc3545';
|
||||
errorMsg.textContent = 'Something went wrong. Please try again.';
|
||||
customQuoteForm.appendChild(errorMsg);
|
||||
setTimeout(function () { if (errorMsg.parentNode) errorMsg.parentNode.removeChild(errorMsg); }, 3000);
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
/* ---- Footer contact form ---- */
|
||||
(function () {
|
||||
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');
|
||||
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('https://n8n.odoo4projects.com/webhook/ODOO4projects/crm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(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("Thanks! We'll get back to you shortly.", false);
|
||||
}).catch(function () {
|
||||
submitBtn.disabled = false;
|
||||
showStatus('Something went wrong. Please try again.', true);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---- Scroll-triggered reveal (same grids as the homepage, minus blog / project-plan) ---- */
|
||||
(function () {
|
||||
if (!window.gsap || !('IntersectionObserver' in window)) return;
|
||||
var animated = {};
|
||||
var observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (!entry.isIntersecting) return;
|
||||
var grid = entry.target;
|
||||
if (!grid || !grid.children || grid.children.length === 0) return;
|
||||
var key = grid.className || 'anon';
|
||||
if (animated[key]) return;
|
||||
animated[key] = true;
|
||||
gsap.fromTo(grid.children, { y: 30, opacity: 0 }, {
|
||||
y: 0, opacity: 1, duration: 0.7, ease: 'power2.out', stagger: 0.08, overwrite: 'auto'
|
||||
});
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
var grids = document.querySelectorAll('.features-grid, .faq-grid, .pricing-grid');
|
||||
grids.forEach(function (g) {
|
||||
if (g.children.length > 0) { gsap.set(g.children, { y: 30, opacity: 0 }); }
|
||||
observer.observe(g);
|
||||
});
|
||||
})();
|
||||
})();
|
||||
Reference in new issue
Block a user