Pre-Alpha to Alpha Ready

This commit is contained in:
John OReilly
2026-03-16 01:42:17 -04:00
parent 42b7b06dda
commit e6c0baef3b
60 changed files with 12287 additions and 230 deletions

View File

@@ -13,6 +13,47 @@
return document.getElementById(id);
}
/* ── Animated number counter ─────────────────────────────────
Smoothly interpolates a displayed dollar value from old → new.
Targets both the desktop element and its _m mobile clone.
Duration: 350ms ease-out via requestAnimationFrame.
──────────────────────────────────────────────────────────── */
var _animFrames = {};
function animateValue(elId, newVal, formatFn) {
var el = document.getElementById(elId);
if (!el) return;
var elM = document.getElementById(elId + '_m');
var raw = (el.textContent || '').replace(/[^0-9.\-]/g, '');
var from = parseFloat(raw) || 0;
var to = parseFloat(newVal) || 0;
if (from === to) {
el.textContent = formatFn(to);
if (elM) elM.textContent = formatFn(to);
return;
}
if (_animFrames[elId]) cancelAnimationFrame(_animFrames[elId]);
var duration = 350;
var start = null;
function step(ts) {
if (!start) start = ts;
var progress = Math.min((ts - start) / duration, 1);
var eased = 1 - Math.pow(1 - progress, 3);
var current = from + (to - from) * eased;
el.textContent = formatFn(current);
if (elM) elM.textContent = formatFn(current);
if (progress < 1) {
_animFrames[elId] = requestAnimationFrame(step);
} else {
delete _animFrames[elId];
}
}
_animFrames[elId] = requestAnimationFrame(step);
}
function getQuote(q) {
return q || (typeof global.calcQuote === 'function' ? global.calcQuote() : null);
}
@@ -268,7 +309,7 @@
show('sl-users', users > 0);
getEl('sl-users-sub')?.classList.toggle('hidden', users === 0);
if (users > 0) {
getEl('sl-users-val').textContent = fmt(userTotal);
animateValue('sl-users-val', userTotal, fmt);
const sub = getEl('sl-users-sub');
sub.classList.remove('hidden');
const subRows = [{ copy: `${users} × ${fmt(baseUserRate)}/user (${byol ? 'BYOL' : 'M365 Incl.'})` }];
@@ -283,7 +324,7 @@
getEl('sl-endpoints-sub')?.classList.toggle('hidden', endpoints === 0);
if (endpoints > 0) {
const epOnly = endpointTotal - serverBase;
getEl('sl-endpoints-val').textContent = fmt(epOnly);
animateValue('sl-endpoints-val', epOnly, fmt);
const sub = getEl('sl-endpoints-sub');
sub.classList.remove('hidden');
const epRows = [{ copy: `${endpoints} × ${fmt(pricing.RATE_ENDPOINT)}/endpoint` }];
@@ -293,13 +334,13 @@
}
show('sl-servers', servers > 0);
if (servers > 0) getEl('sl-servers-val').textContent = fmt(serverBase);
if (servers > 0) animateValue('sl-servers-val', serverBase, fmt);
show('sl-zt', ztNetTotal > 0);
if (ztNetTotal > 0) getEl('sl-zt-val').textContent = fmt(ztNetTotal);
if (ztNetTotal > 0) animateValue('sl-zt-val', ztNetTotal, fmt);
show('sl-voip', voipTotal > 0);
if (voipTotal > 0) getEl('sl-voip-val').textContent = fmt(voipTotal);
if (voipTotal > 0) animateValue('sl-voip-val', voipTotal, fmt);
const slAdminEl = getEl('sl-admin');
const slAdminValEl = getEl('sl-admin-val');
@@ -322,9 +363,9 @@
slAdminSubEl.innerHTML = renderSubRows(adminRows);
}
getEl('mrrDisplay').textContent = fmt(effectiveMrr);
animateValue('mrrDisplay', effectiveMrr, fmt);
const monthlyTotalEl = getEl('sl-monthly-total-val');
if (monthlyTotalEl) monthlyTotalEl.textContent = fmt(effectiveMrr) + '/mo';
if (monthlyTotalEl) monthlyTotalEl.innerHTML = fmt(effectiveMrr) + '<span class="suffix-mo">/mo</span>';
getEl('annualDisplay').textContent = fmt(effectiveAnnual);
getEl('perUserRow').classList.toggle('hidden', users === 0);
if (users > 0) getEl('perUserDisplay').textContent = fmt(effectiveMrr / users) + '/user';
@@ -620,22 +661,26 @@
if (!q) return;
const collapsed = id => !document.getElementById(id)?.classList.contains('sec-open');
const setSummary = (id, text) => {
const setSummary = (id, text, numericVal) => {
const el = document.getElementById(id);
if (!el) return;
const secId = id.replace('-summary', '').replace('sec0', 'sec-0');
const show = collapsed(secId) && !!text;
el.textContent = text || '';
if (numericVal !== undefined && show) {
animateValue(id, numericVal, function(v) { return fmt(v) + '/mo'; });
} else {
el.textContent = text || '';
}
el.hidden = !show;
el.style.display = show ? 'flex' : 'none';
};
setSummary('sec01-summary', q.adminWaived ? 'WAIVED' : fmt(q.adminFeeNet) + '/mo');
setSummary('sec02-summary', q.users > 0 ? `${fmt(q.userTotal)}/mo` : '');
setSummary('sec03-summary', q.endpoints > 0 ? `${fmt(q.endpointTotal - q.serverBase)}/mo` : '');
setSummary('sec04-summary', q.servers > 0 ? `${fmt(q.serverBase)}/mo` : '');
setSummary('sec05-summary', q.ztNetTotal > 0 ? `${fmt(q.ztNetTotal)}/mo` : '');
setSummary('sec06-summary', q.voipSeats > 0 ? `${fmt(q.voipTotal)}/mo` : '');
setSummary('sec02-summary', q.users > 0 ? `${fmt(q.userTotal)}/mo` : '', q.users > 0 ? q.userTotal : undefined);
setSummary('sec03-summary', q.endpoints > 0 ? `${fmt(q.endpointTotal - q.serverBase)}/mo` : '', q.endpoints > 0 ? q.endpointTotal - q.serverBase : undefined);
setSummary('sec04-summary', q.servers > 0 ? `${fmt(q.serverBase)}/mo` : '', q.servers > 0 ? q.serverBase : undefined);
setSummary('sec05-summary', q.ztNetTotal > 0 ? `${fmt(q.ztNetTotal)}/mo` : '', q.ztNetTotal > 0 ? q.ztNetTotal : undefined);
setSummary('sec06-summary', q.voipSeats > 0 ? `${fmt(q.voipTotal)}/mo` : '', q.voipSeats > 0 ? q.voipTotal : undefined);
}
function updateVsComparison(q) {