Public Access
Move the homepage's Server Location lockup (label + select) out of section#pricing
and into the HOST27 deal card's CTA row, next to the Buy button. There is still
exactly ONE element with id pricingServerLocation on the page, so buyNow() — which
resolves the selector by id — and every pricing button keep working untouched.
The pricing block keeps only the Odoo Version selector and reads the same global
location selection.
- markup: new .bf-cta-row in the deal form (selector left, brass Buy right);
location lockup removed from .pricing-server-row
- CSS (page-scoped): .bf-cta-row / .bf-cta-location, and the light-on-ivory twin
of .pricing-server-select (style.css carries neither; the homepage styles it
inline for its dark section). Wraps at 375px with no horizontal overflow; the
Buy stays the one dominant action.
- static/black-friday.js: on submit of the native post_basket forms, write the
chosen city + version into the basket line's `info`, exactly as buyNow() does.
Additive only — with JavaScript off the POST is unchanged (no info) and the
selector is empty because its options are built here (pre-existing behaviour).
Untouched: the hidden redirect=/coupon/HOST27, the single plausible-event-name=Buy
mark, the price, the countdown, #trialForm, the booking link and the consent gate.
Verified: verify-html.py PROBLEMS 0; competitor QC exit 0 on the page (74
pre-existing site-wide, 0 added); node --check OK; impeccable detect 35 findings,
identical set to HEAD (0 introduced); live browser: fastest server auto-selected,
wire body basket=[{"product_id":4,"qty":1,"info":"boston|20.0"}] and it tracks the
selectors on repeat submits; 375px no overflow.
522 lines
22 KiB
JavaScript
522 lines
22 KiB
JavaScript
/* 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');
|
|
|
|
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);
|
|
};
|
|
|
|
/* ---- Carry the chosen Server Location + Odoo version into the native buy forms ----
|
|
* The Black Friday buy buttons are plain form POSTs to /shop/post_basket, so they work with
|
|
* JavaScript off. buyNow() builds `info` for the JS buttons; this does exactly the same for
|
|
* the native forms, on submit, from the one global selector pair (#pricingServerLocation +
|
|
* #pricingOdooVersion). Additive only: with JavaScript off the basket posts exactly as before
|
|
* (no info) and the selector is empty anyway because its options are built here. The POST is
|
|
* untouched — only the form's own hidden `basket` input is rewritten. */
|
|
(function () {
|
|
var forms = document.querySelectorAll('form[action*="/shop/post_basket"]');
|
|
forms.forEach(function (form) {
|
|
form.addEventListener('submit', function () {
|
|
var basket = form.querySelector('input[name="basket"]');
|
|
if (!basket) return;
|
|
var locSelect = document.getElementById('pricingServerLocation');
|
|
var verSelect = document.getElementById('pricingOdooVersion');
|
|
var location = locSelect ? locSelect.value : '';
|
|
var version = verSelect ? verSelect.value : '';
|
|
var info = location ? location.replace('.odoo4projects.com', '') : '';
|
|
if (version) { info += (info ? '|' : '') + version; }
|
|
if (!info) return;
|
|
try {
|
|
var items = JSON.parse(basket.value);
|
|
if (!Array.isArray(items)) return;
|
|
items.forEach(function (it) { if (it) { it.info = info; } });
|
|
basket.value = JSON.stringify(items);
|
|
} catch (e) { /* leave the basket untouched rather than break the purchase */ }
|
|
});
|
|
});
|
|
})();
|
|
|
|
/* ---- 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 <a target="_blank">, 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; });
|
|
})();
|
|
})();
|