/* 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 ' + city + ' 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');
function sendTrial() {
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);
});
}
/* When the consent gate is present it fires the Google Ads trial conversion and runs
sendTrial from the tag's callback, with a hard timeout that runs it anyway — so a
blocked or slow tag can never swallow the trial. Without the gate, send directly. */
if (window.o4pAds && typeof window.o4pAds.trialConversion === 'function') {
window.o4pAds.trialConversion(sendTrial);
} else {
sendTrial();
}
});
}
})();
/* ---- 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);
});
});
}
})();
/* ---- Booking link — fire the Google Ads booking conversion on the click, through the same
consent gate as the buy and trial conversions. The link is a plain , so
the click is never preventDefault-ed: the Google appointment page opens regardless, and a
blocked, slow or absent tag merely skips the event. Fires nothing without consent. ---- */
(function () {
var link = document.getElementById('bookingCTA');
if (!link) return;
link.addEventListener('click', function () {
if (window.o4pAds && typeof window.o4pAds.bookConversion === 'function') {
window.o4pAds.bookConversion();
}
});
})();
/* ---- 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);
});
})();
/* ---- Black Friday time remaining (Fix 10) ----
ONE documented constant: the instant the offer ends — 30 November 2026, end of day, in the
account's US/Eastern context (EST, UTC-5, after the 1 Nov DST change). The day count is
computed client-side from this instant on every page load; it is NEVER hard-coded, so it
stays correct as the date changes.
Fail-safe: the HTML carries the static sentence "Ends Monday 30 November 2026." with an empty
span beside it, so with JavaScript off the deadline still reads correctly on its own. Once the
end instant has passed the text switches to the past tense — it never keeps claiming time
remains. Nothing here animates visibility. */
(function () {
var BF_END = new Date('2026-11-30T23:59:59-05:00');
var DAY = 86400000;
var nodes = document.querySelectorAll('[data-bf-days]');
if (!nodes.length) return;
/* Whole calendar days in US/Eastern, so "days left" is a date difference and not a 24-hour
window (the offer ends at the end of a day, not 28 x 24h from now). */
function easternDayNumber(d) {
var parts = new Intl.DateTimeFormat('en-CA', {
timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit'
}).format(d).split('-');
return Date.UTC(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10)) / DAY;
}
var daysLeft = easternDayNumber(BF_END) - easternDayNumber(new Date());
var text;
if (daysLeft > 1) {
text = daysLeft + ' days left';
} else if (daysLeft === 1) {
text = '1 day left';
} else if (daysLeft === 0) {
text = 'last day today';
} else {
text = 'this offer has ended';
}
nodes.forEach(function (n) { n.textContent = text; });
})();
})();