Add new animations for retail, corporate, education, outdoor, live data, healthcare, transit, and fitness sectors

- Extend block attributes to include new animation options in index.php
- Implement animation rendering logic for each sector in oribi_render_platform_row function
- Enqueue new JavaScript file for solutions page animations in enqueue.php
- Create solutions-animator.js to handle live data KPI and transit board animations
This commit is contained in:
Matt Batchelder
2026-03-16 20:29:15 -04:00
parent 9f415320de
commit fa6dce039b
5 changed files with 2144 additions and 8 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,291 @@
/**
* Solutions Page Animators
* Handles the two JS-driven animations on the Solutions page:
* 1. Live Data board — ticking KPI values + animated sparkline
* 2. Transit board — live clock, split-flap flip characters, row cycling
*
* Both respect prefers-reduced-motion and pause via IntersectionObserver.
* Mirrors the patterns and conventions of dashboard-animator.js.
*/
/* ── 1. Live Data KPI Animator ─────────────────────────────────────────── */
(function () {
'use strict';
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
/* KPI definitions: label, base value, unit, variance range, display format */
var KPIS = [
{ id: 'ld-orders', base: 1847, range: 120, fmt: function (v) { return v.toLocaleString(); } },
{ id: 'ld-uptime', base: 9997, range: 2, fmt: function (v) { return (v / 100).toFixed(2) + '%'; } },
{ id: 'ld-alerts', base: 3, range: 2, fmt: function (v) { return Math.max(0, v).toString(); } },
{ id: 'ld-latency', base: 42, range: 18, fmt: function (v) { return Math.max(8, v) + 'ms'; } },
];
/* Sparkline path parameters */
var LINE_PTS = 16;
var LINE_W = 260;
var LINE_H = 60;
var SPEED = 0.0008;
function wave(t, off) {
return Math.max(0, Math.min(1,
0.5 +
Math.sin(t + off) * 0.28 +
Math.sin(t * 2.1 + off * 1.7) * 0.12
));
}
function makeState(stage) {
var kpiEls = [];
for (var i = 0; i < KPIS.length; i++) {
kpiEls.push(stage.querySelector('#' + KPIS[i].id));
}
return {
stage: stage,
kpiEls: kpiEls,
linePath: stage.querySelector('#ld-line-path'),
fillPath: stage.querySelector('#ld-fill-path'),
phase: Math.random() * Math.PI * 2,
ticker: 0, /* frame counter — update KPI text every N frames */
paused: false,
};
}
function updateKpis(st) {
for (var i = 0; i < KPIS.length; i++) {
var el = st.kpiEls[i];
if (!el) continue;
var k = KPIS[i];
var raw = Math.round(k.base + wave(st.phase, i * 1.5) * k.range - k.range / 2);
el.textContent = k.fmt(raw);
}
}
function updateSparkline(st) {
if (!st.linePath) return;
var pts = [];
for (var i = 0; i < LINE_PTS; i++) {
var x = (i / (LINE_PTS - 1)) * LINE_W;
var y = 8 + (1 - wave(st.phase * 0.7, i * 0.8)) * (LINE_H - 16);
pts.push(x.toFixed(1) + ',' + y.toFixed(1));
}
var d = 'M' + pts.join(' L');
st.linePath.setAttribute('d', d);
if (st.fillPath) {
st.fillPath.setAttribute('d', d + ' L' + LINE_W + ',' + LINE_H + ' L0,' + LINE_H + ' Z');
}
}
function tick(st) {
if (!st.paused) {
st.phase += SPEED * 16;
st.ticker++;
/* Update KPI text every 12 frames (~5/sec at 60fps) for legibility */
if (st.ticker >= 12) {
st.ticker = 0;
updateKpis(st);
}
updateSparkline(st);
}
requestAnimationFrame(function () { tick(st); });
}
function observe(st) {
if (!('IntersectionObserver' in window)) return;
new IntersectionObserver(function (entries) {
for (var i = 0; i < entries.length; i++) {
st.paused = !entries[i].isIntersecting;
}
}, { rootMargin: '200px', threshold: 0.05 }).observe(st.stage);
}
function boot() {
var stages = document.querySelectorAll('.ld-stage');
if (!stages.length) return;
for (var i = 0; i < stages.length; i++) {
if (stages[i]._ldAnim) continue;
var st = makeState(stages[i]);
stages[i]._ldAnim = st;
observe(st);
tick(st);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();
/* ── 2. Transit Departure Board Animator ───────────────────────────────── */
(function () {
'use strict';
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
/* Still run the clock in reduced-motion mode */
startClocks();
return;
}
/* Departure data sets — cycle between these every CYCLE_MS */
var CYCLE_MS = 8000;
var DATA_SETS = [
[
{ time: '10:14', dest: 'London Victoria', plat: '2', status: 'On Time', cls: 'on-time' },
{ time: '10:22', dest: 'Brighton', plat: '4', status: 'On Time', cls: 'on-time' },
{ time: '10:31', dest: 'Gatwick Airport', plat: '1', status: 'Delayed', cls: 'delayed' },
{ time: '10:45', dest: 'London Bridge', plat: '3', status: 'On Time', cls: 'on-time' },
{ time: '11:02', dest: 'East Croydon', plat: '2', status: 'On Time', cls: 'on-time' },
],
[
{ time: '10:22', dest: 'Brighton', plat: '4', status: 'On Time', cls: 'on-time' },
{ time: '10:31', dest: 'Gatwick Airport', plat: '1', status: 'Delayed', cls: 'delayed' },
{ time: '10:45', dest: 'London Bridge', plat: '3', status: 'On Time', cls: 'on-time' },
{ time: '11:02', dest: 'East Croydon', plat: '2', status: 'On Time', cls: 'on-time' },
{ time: '11:14', dest: 'London Victoria', plat: '2', status: 'On Time', cls: 'on-time' },
],
[
{ time: '10:31', dest: 'Gatwick Airport', plat: '1', status: 'Delayed', cls: 'delayed' },
{ time: '10:45', dest: 'London Bridge', plat: '3', status: 'On Time', cls: 'on-time' },
{ time: '11:02', dest: 'East Croydon', plat: '2', status: 'On Time', cls: 'on-time' },
{ time: '11:14', dest: 'London Victoria', plat: '2', status: 'On Time', cls: 'on-time' },
{ time: '11:28', dest: 'Three Bridges', plat: '4', status: 'Cancelled', cls: 'cancelled'},
],
];
/* ── Clock ── */
function startClocks() {
var clocks = document.querySelectorAll('#transit-clock');
if (!clocks.length) return;
function updateClock() {
var now = new Date();
var hh = String(now.getHours()).padStart(2, '0');
var mm = String(now.getMinutes()).padStart(2, '0');
var ss = String(now.getSeconds()).padStart(2, '0');
var str = hh + ':' + mm + ':' + ss;
for (var i = 0; i < clocks.length; i++) clocks[i].textContent = str;
}
updateClock();
setInterval(updateClock, 1000);
}
/* ── Flip helpers ── */
function flipCells(rowEl, newDest) {
var flapEls = rowEl.querySelectorAll('.transit-flap');
var chars = newDest.split('');
/* Extend or shrink the flap container to match new length */
var destCell = rowEl.querySelector('.transit-cell--dest');
if (!destCell) return;
/* Animate existing flaps, create/remove extras */
var i;
for (i = 0; i < chars.length; i++) {
var ch = chars[i] === ' ' ? '\u00a0' : chars[i];
if (i < flapEls.length) {
/* Animate existing */
(function (el, character) {
el.classList.add('is-flipping');
setTimeout(function () {
el.textContent = character;
el.classList.remove('is-flipping');
}, 125);
})(flapEls[i], ch);
} else {
/* Append new flap */
var newFlap = document.createElement('span');
newFlap.className = 'transit-flap is-flipping';
newFlap.textContent = ch;
destCell.appendChild(newFlap);
setTimeout(function (el) {
el.classList.remove('is-flipping');
}, 125, newFlap);
}
}
/* Remove surplus flaps */
for (i = chars.length; i < flapEls.length; i++) {
(function (el) {
el.classList.add('is-flipping');
setTimeout(function () { el.parentNode && el.parentNode.removeChild(el); }, 250);
})(flapEls[i]);
}
}
function applyRow(rowEl, departure) {
var timeEl = rowEl.querySelector('.transit-cell--time');
var platEl = rowEl.querySelector('.transit-cell--plat');
var statusEl = rowEl.querySelector('.transit-cell--status');
if (timeEl) timeEl.textContent = departure.time;
if (platEl) platEl.textContent = departure.platform || departure.plat;
if (statusEl) {
statusEl.textContent = departure.status;
statusEl.className = 'transit-cell transit-cell--status transit-status--' + departure.cls;
}
flipCells(rowEl, departure.dest);
}
function cycleBoard(stage, dataIdx) {
var rows = stage.querySelectorAll('.transit-row');
var set = DATA_SETS[dataIdx % DATA_SETS.length];
for (var i = 0; i < Math.min(rows.length, set.length); i++) {
/* Stagger each row by 180ms */
(function (row, dep) {
setTimeout(function () { applyRow(row, dep); }, i * 180);
})(rows[i], set[i]);
}
}
function initBoard(stage) {
var state = { idx: 0, timer: null, paused: false };
function advance() {
if (state.paused) return;
state.idx++;
cycleBoard(stage, state.idx);
}
function startTimer() {
if (state.timer) return;
state.timer = setInterval(advance, CYCLE_MS);
}
function stopTimer() {
clearInterval(state.timer);
state.timer = null;
}
if ('IntersectionObserver' in window) {
new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
state.paused = !e.isIntersecting;
e.isIntersecting ? startTimer() : stopTimer();
});
}, { rootMargin: '200px', threshold: 0.05 }).observe(stage);
}
startTimer();
}
function boot() {
startClocks();
var stages = document.querySelectorAll('.transit-stage');
if (!stages.length) return;
for (var i = 0; i < stages.length; i++) {
if (stages[i]._transitAnim) continue;
stages[i]._transitAnim = true;
initBoard(stages[i]);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();

View File

@@ -576,6 +576,15 @@ add_action('init', function () {
'cameraAnim' => ['type' => 'boolean', 'default' => false],
'neverGoesDark' => ['type' => 'boolean', 'default' => false],
'brandedAnim' => ['type' => 'boolean', 'default' => false],
'hospitalityAnim' => ['type' => 'boolean', 'default' => false],
'retailAnim' => ['type' => 'boolean', 'default' => false],
'corporateAnim' => ['type' => 'boolean', 'default' => false],
'educationAnim' => ['type' => 'boolean', 'default' => false],
'outdoorAnim' => ['type' => 'boolean', 'default' => false],
'liveDataAnim' => ['type' => 'boolean', 'default' => false],
'healthcareAnim' => ['type' => 'boolean', 'default' => false],
'transitAnim' => ['type' => 'boolean', 'default' => false],
'fitnessAnim' => ['type' => 'boolean', 'default' => false],
'galleryIds' => ['type' => 'array', 'default' => [], 'items' => ['type' => 'number']],
],
'supports' => $block_supports,
@@ -2062,6 +2071,355 @@ function oribi_render_platform_row($a)
$visual_html = $bd;
$visual_cls = 'platform-visual has-branded';
}
elseif (!empty($a['retailAnim'])) {
/* ── Retail Sign: TV cycling 3 promo slides ── */
$ra = '<div class="retail-stage" aria-hidden="true">';
$ra .= '<div class="retail-tv">';
$ra .= '<div class="retail-tv__body">';
$ra .= '<div class="retail-tv__screen">';
$ra .= '<div class="retail-slides">';
// Slide 1: Flash Sale
$ra .= '<div class="retail-slide retail-slide--sale">';
$ra .= '<div class="retail-promo">';
$ra .= '<div class="retail-promo__eyebrow">Today Only</div>';
$ra .= '<div class="retail-promo__headline">Flash Sale</div>';
$ra .= '<div class="retail-promo__badge">Up to 40% Off</div>';
$ra .= '<div class="retail-promo__items">';
$ra .= '<div class="retail-promo__item"><span class="retail-promo__dot"></span><span class="retail-promo__lbl">Select Apparel</span></div>';
$ra .= '<div class="retail-promo__item"><span class="retail-promo__dot"></span><span class="retail-promo__lbl">Footwear Range</span></div>';
$ra .= '<div class="retail-promo__item"><span class="retail-promo__dot"></span><span class="retail-promo__lbl">Accessories</span></div>';
$ra .= '</div>';
$ra .= '<div class="retail-promo__cta">In-Store Only</div>';
$ra .= '</div>';
$ra .= '</div>';
// Slide 2: New Arrivals
$ra .= '<div class="retail-slide retail-slide--new">';
$ra .= '<div class="retail-promo">';
$ra .= '<div class="retail-promo__eyebrow">Just Landed</div>';
$ra .= '<div class="retail-promo__headline">New In Store</div>';
$ra .= '<div class="retail-promo__grid">';
$ra .= '<div class="retail-promo__swatch retail-promo__swatch--a"></div>';
$ra .= '<div class="retail-promo__swatch retail-promo__swatch--b"></div>';
$ra .= '<div class="retail-promo__swatch retail-promo__swatch--c"></div>';
$ra .= '<div class="retail-promo__swatch retail-promo__swatch--d"></div>';
$ra .= '</div>';
$ra .= '<div class="retail-promo__sub">Spring/Summer Collection</div>';
$ra .= '</div>';
$ra .= '</div>';
// Slide 3: Loyalty Rewards
$ra .= '<div class="retail-slide retail-slide--loyalty">';
$ra .= '<div class="retail-promo">';
$ra .= '<div class="retail-promo__eyebrow">Member Exclusive</div>';
$ra .= '<div class="retail-promo__headline">Earn Points</div>';
$ra .= '<div class="retail-promo__points">';
$ra .= '<div class="retail-promo__pts-val">2x</div>';
$ra .= '<div class="retail-promo__pts-lbl">Points This Weekend</div>';
$ra .= '</div>';
$ra .= '<div class="retail-promo__bar"><div class="retail-promo__bar-fill"></div></div>';
$ra .= '<div class="retail-promo__sub">Scan your card at checkout</div>';
$ra .= '</div>';
$ra .= '</div>';
$ra .= '</div>'; // retail-slides
$ra .= '</div>'; // retail-tv__screen
$ra .= '</div>'; // retail-tv__body
$ra .= '<div class="retail-tv__feet"><div class="retail-tv__foot"></div><div class="retail-tv__foot"></div></div>';
$ra .= '</div>'; // retail-tv
$ra .= '</div>'; // retail-stage
$visual_html = $ra;
$visual_cls = 'platform-visual has-retail';
}
elseif (!empty($a['corporateAnim'])) {
/* ── Corporate: Meeting room door panel ── */
$ca = '<div class="corp-stage" aria-hidden="true">';
$ca .= '<div class="corp-panel">';
$ca .= '<div class="corp-panel__header">';
$ca .= '<div class="corp-panel__room">Boardroom A</div>';
$ca .= '<div class="corp-panel__status corp-panel__status--busy"><span class="corp-panel__dot"></span>In Use</div>';
$ca .= '</div>';
$ca .= '<div class="corp-panel__meeting">';
$ca .= '<div class="corp-panel__meeting-name">Q2 Strategy Review</div>';
$ca .= '<div class="corp-panel__meeting-time">10:00 11:30</div>';
$ca .= '<div class="corp-panel__organiser">Sarah Mitchell</div>';
$ca .= '</div>';
$ca .= '<div class="corp-panel__timeline">';
$ca .= '<div class="corp-panel__tl-track"><div class="corp-panel__tl-fill"></div><div class="corp-panel__tl-now"></div></div>';
$ca .= '<div class="corp-panel__tl-labels"><span>09:00</span><span>12:00</span><span>17:00</span></div>';
$ca .= '</div>';
$ca .= '<div class="corp-panel__next">';
$ca .= '<div class="corp-panel__next-lbl">Next</div>';
$ca .= '<div class="corp-panel__next-name">Design Sprint Planning</div>';
$ca .= '<div class="corp-panel__next-time">13:00 14:00</div>';
$ca .= '</div>';
$ca .= '<div class="corp-panel__teams"><div class="corp-panel__teams-icon"></div><span>Teams Meeting Active</span></div>';
$ca .= '</div>'; // corp-panel
$ca .= '</div>'; // corp-stage
$visual_html = $ca;
$visual_cls = 'platform-visual has-corporate';
}
elseif (!empty($a['educationAnim'])) {
/* ── Education: Campus schedule board ── */
$ea = '<div class="edu-stage" aria-hidden="true">';
$ea .= '<div class="edu-board">';
$ea .= '<div class="edu-board__header">';
$ea .= '<div class="edu-board__title">Today\'s Timetable</div>';
$ea .= '<div class="edu-board__date">Monday, 16 Mar</div>';
$ea .= '</div>';
$ea .= '<div class="edu-board__rows">';
$rows = [
['time' => '09:00', 'subject' => 'Advanced Mathematics', 'room' => 'B204', 'state' => 'done'],
['time' => '10:30', 'subject' => 'Physics Lab', 'room' => 'Lab 3', 'state' => 'now'],
['time' => '13:00', 'subject' => 'English Literature', 'room' => 'A101', 'state' => ''],
['time' => '14:30', 'subject' => 'Computer Science', 'room' => 'IT Suite', 'state' => ''],
['time' => '16:00', 'subject' => 'Art & Design', 'room' => 'Studio 1', 'state' => ''],
];
foreach ($rows as $row) {
$state_cls = $row['state'] ? ' edu-row--' . $row['state'] : '';
$ea .= '<div class="edu-board__row' . $state_cls . '">';
$ea .= '<span class="edu-row__time">' . esc_html($row['time']) . '</span>';
$ea .= '<span class="edu-row__subject">' . esc_html($row['subject']) . '</span>';
$ea .= '<span class="edu-row__room">' . esc_html($row['room']) . '</span>';
if ($row['state'] === 'now') {
$ea .= '<span class="edu-row__badge">Now</span>';
}
$ea .= '</div>';
}
$ea .= '</div>'; // edu-board__rows
$ea .= '<div class="edu-board__alert">';
$ea .= '<span class="edu-alert__icon">!</span>';
$ea .= '<span class="edu-alert__txt">Fire drill scheduled — 15:45 today</span>';
$ea .= '</div>';
$ea .= '</div>'; // edu-board
$ea .= '</div>'; // edu-stage
$visual_html = $ea;
$visual_cls = 'platform-visual has-education';
}
elseif (!empty($a['outdoorAnim'])) {
/* ── Outdoor Marketplace: wide board cycling 2 slides ── */
$oa = '<div class="outdoor-stage" aria-hidden="true">';
$oa .= '<div class="outdoor-board">';
$oa .= '<div class="outdoor-board__screen">';
$oa .= '<div class="outdoor-slides">';
// Slide 1: Market Info
$oa .= '<div class="outdoor-slide outdoor-slide--info">';
$oa .= '<div class="outdoor-info">';
$oa .= '<div class="outdoor-info__header">';
$oa .= '<div class="outdoor-info__name">Riverside Market</div>';
$oa .= '<div class="outdoor-info__weather"><span class="outdoor-info__temp">18°</span><span class="outdoor-info__cond">Mostly Sunny</span></div>';
$oa .= '</div>';
$oa .= '<div class="outdoor-info__details">';
$oa .= '<div class="outdoor-info__row"><span class="outdoor-info__icon outdoor-info__icon--clock"></span><span>Open 8am 3pm</span></div>';
$oa .= '<div class="outdoor-info__row"><span class="outdoor-info__icon outdoor-info__icon--pin"></span><span>Victoria Embankment</span></div>';
$oa .= '<div class="outdoor-info__row"><span class="outdoor-info__icon outdoor-info__icon--stall"></span><span>42 Stallholders Today</span></div>';
$oa .= '</div>';
$oa .= '</div>';
$oa .= '</div>';
// Slide 2: Stall Directory
$oa .= '<div class="outdoor-slide outdoor-slide--directory">';
$oa .= '<div class="outdoor-dir">';
$oa .= '<div class="outdoor-dir__title">Stall Directory</div>';
$oa .= '<div class="outdoor-dir__grid">';
$stalls = [['A1A5', 'Produce & Veg'], ['B1B4', 'Artisan Bakery'], ['C1C6', 'Street Food'], ['D1D3', 'Crafts & Gifts']];
foreach ($stalls as $s) {
$oa .= '<div class="outdoor-dir__cell"><span class="outdoor-dir__zone">' . esc_html($s[0]) . '</span><span class="outdoor-dir__cat">' . esc_html($s[1]) . '</span></div>';
}
$oa .= '</div>';
$oa .= '</div>';
$oa .= '</div>';
$oa .= '</div>'; // outdoor-slides
$oa .= '</div>'; // outdoor-board__screen
$oa .= '<div class="outdoor-board__bezel"></div>';
$oa .= '</div>'; // outdoor-board
$oa .= '</div>'; // outdoor-stage
$visual_html = $oa;
$visual_cls = 'platform-visual has-outdoor';
}
elseif (!empty($a['liveDataAnim'])) {
/* ── Live Data: Operations centre KPI board ── */
$la = '<div class="ld-stage" aria-hidden="true">';
$la .= '<div class="ld-board">';
$la .= '<div class="ld-board__header">';
$la .= '<div class="ld-board__title">Operations Dashboard</div>';
$la .= '<div class="ld-board__live"><span class="ld-board__live-dot"></span>LIVE</div>';
$la .= '</div>';
$la .= '<div class="ld-kpis">';
$kpis = [
['id' => 'ld-orders', 'label' => 'Orders / hr', 'value' => '1,847', 'trend' => 'up'],
['id' => 'ld-uptime', 'label' => 'Uptime', 'value' => '99.97%', 'trend' => 'up'],
['id' => 'ld-alerts', 'label' => 'Active Alerts', 'value' => '3', 'trend' => 'down'],
['id' => 'ld-latency', 'label' => 'Avg Latency', 'value' => '42ms', 'trend' => 'neutral'],
];
foreach ($kpis as $k) {
$la .= '<div class="ld-kpi ld-kpi--' . esc_attr($k['trend']) . '">';
$la .= '<div class="ld-kpi__label">' . esc_html($k['label']) . '</div>';
$la .= '<div class="ld-kpi__value" id="' . esc_attr($k['id']) . '">' . esc_html($k['value']) . '</div>';
$la .= '<div class="ld-kpi__trend"></div>';
$la .= '</div>';
}
$la .= '</div>'; // ld-kpis
$la .= '<div class="ld-chart">';
$la .= '<svg class="ld-sparkline" viewBox="0 0 260 60" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">';
$la .= '<defs><linearGradient id="ld-grad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#4CAF50" stop-opacity=".35"/><stop offset="100%" stop-color="#4CAF50" stop-opacity="0"/></linearGradient></defs>';
$la .= '<path id="ld-fill-path" d="M0,40 L0,40 L260,40 Z" fill="url(#ld-grad)"/>';
$la .= '<path id="ld-line-path" d="M0,40 L260,40" stroke="#4CAF50" stroke-width="2" fill="none" stroke-linejoin="round"/>';
$la .= '</svg>';
$la .= '<div class="ld-chart__label">Orders / hr — last 60 min</div>';
$la .= '</div>'; // ld-chart
$la .= '<div class="ld-status">';
$services = [
['API Gateway', 'ok'],
['Payment Proc.', 'ok'],
['Warehouse Feed', 'warn'],
['CDN', 'ok'],
];
foreach ($services as $svc) {
$la .= '<div class="ld-svc ld-svc--' . esc_attr($svc[1]) . '"><span class="ld-svc__dot"></span><span class="ld-svc__name">' . esc_html($svc[0]) . '</span></div>';
}
$la .= '</div>'; // ld-status
$la .= '</div>'; // ld-board
$la .= '</div>'; // ld-stage
$visual_html = $la;
$visual_cls = 'platform-visual has-live-data';
}
elseif (!empty($a['healthcareAnim'])) {
/* ── Healthcare: Queue management display ── */
$hca = '<div class="hc-stage" aria-hidden="true">';
$hca .= '<div class="hc-board">';
$hca .= '<div class="hc-board__header">';
$hca .= '<div class="hc-board__logo"></div>';
$hca .= '<div class="hc-board__title">Patient Queue</div>';
$hca .= '</div>';
$hca .= '<div class="hc-now">';
$hca .= '<div class="hc-now__lbl">Now Serving</div>';
$hca .= '<div class="hc-now__number hc-ticker">A042</div>';
$hca .= '<div class="hc-now__counter">Counter 3 — Dr. Patel</div>';
$hca .= '</div>';
$hca .= '<div class="hc-counters">';
$counters = [
['id' => 'C1', 'doctor' => 'Dr. Patel', 'ticket' => 'A042', 'wait' => '0 min', 'state' => 'active'],
['id' => 'C2', 'doctor' => 'Dr. Okonkwo', 'ticket' => 'B018', 'wait' => '4 min', 'state' => 'active'],
['id' => 'C3', 'doctor' => 'Dr. Williams', 'ticket' => 'A041', 'wait' => '8 min', 'state' => 'active'],
['id' => 'C4', 'doctor' => 'Dr. Nguyen', 'ticket' => '—', 'wait' => 'Closed', 'state' => 'closed'],
];
foreach ($counters as $c) {
$hca .= '<div class="hc-counter hc-counter--' . esc_attr($c['state']) . '">';
$hca .= '<span class="hc-counter__id">' . esc_html($c['id']) . '</span>';
$hca .= '<span class="hc-counter__doctor">' . esc_html($c['doctor']) . '</span>';
$hca .= '<span class="hc-counter__ticket">' . esc_html($c['ticket']) . '</span>';
$hca .= '<span class="hc-counter__wait">' . esc_html($c['wait']) . '</span>';
$hca .= '</div>';
}
$hca .= '</div>'; // hc-counters
$hca .= '<div class="hc-board__footer">Take a seat — your number will be called</div>';
$hca .= '</div>'; // hc-board
$hca .= '</div>'; // hc-stage
$visual_html = $hca;
$visual_cls = 'platform-visual has-healthcare';
}
elseif (!empty($a['transitAnim'])) {
/* ── Transit: Split-flap departure board ── */
$ta = '<div class="transit-stage" aria-hidden="true">';
$ta .= '<div class="transit-board">';
$ta .= '<div class="transit-board__header">';
$ta .= '<div class="transit-board__title">Departures</div>';
$ta .= '<div class="transit-board__clock" id="transit-clock">--:--</div>';
$ta .= '</div>';
$ta .= '<div class="transit-board__cols">';
$ta .= '<span class="transit-col-hd">Time</span><span class="transit-col-hd">Destination</span><span class="transit-col-hd">Platform</span><span class="transit-col-hd">Status</span>';
$ta .= '</div>';
$ta .= '<div class="transit-rows" id="transit-rows">';
$departures = [
['time' => '10:14', 'destination' => 'London Victoria', 'platform' => '2', 'status' => 'On Time', 'status_cls' => 'on-time'],
['time' => '10:22', 'destination' => 'Brighton', 'platform' => '4', 'status' => 'On Time', 'status_cls' => 'on-time'],
['time' => '10:31', 'destination' => 'Gatwick Airport', 'platform' => '1', 'status' => 'Delayed', 'status_cls' => 'delayed'],
['time' => '10:45', 'destination' => 'London Bridge', 'platform' => '3', 'status' => 'On Time', 'status_cls' => 'on-time'],
['time' => '11:02', 'destination' => 'East Croydon', 'platform' => '2', 'status' => 'On Time', 'status_cls' => 'on-time'],
];
foreach ($departures as $d) {
$ta .= '<div class="transit-row">';
$ta .= '<span class="transit-cell transit-cell--time">' . esc_html($d['time']) . '</span>';
$ta .= '<span class="transit-cell transit-cell--dest">';
// Each character gets a flap span for the CSS flip animation
$dest = esc_html($d['destination']);
foreach (str_split($dest) as $ch) {
$ta .= '<span class="transit-flap">' . ($ch === ' ' ? '&nbsp;' : htmlspecialchars($ch)) . '</span>';
}
$ta .= '</span>';
$ta .= '<span class="transit-cell transit-cell--plat">' . esc_html($d['platform']) . '</span>';
$ta .= '<span class="transit-cell transit-cell--status transit-status--' . esc_attr($d['status_cls']) . '">' . esc_html($d['status']) . '</span>';
$ta .= '</div>';
}
$ta .= '</div>'; // transit-rows
$ta .= '</div>'; // transit-board
$ta .= '</div>'; // transit-stage
$visual_html = $ta;
$visual_cls = 'platform-visual has-transit';
}
elseif (!empty($a['fitnessAnim'])) {
/* ── Fitness: Class schedule with live capacity bar ── */
$fa = '<div class="fit-stage" aria-hidden="true">';
$fa .= '<div class="fit-board">';
$fa .= '<div class="fit-board__header">';
$fa .= '<div class="fit-board__logo"></div>';
$fa .= '<div class="fit-board__title">Today\'s Classes</div>';
$fa .= '</div>';
$fa .= '<div class="fit-now">';
$fa .= '<div class="fit-now__badge">LIVE NOW</div>';
$fa .= '<div class="fit-now__name">HIIT Circuit</div>';
$fa .= '<div class="fit-now__detail">Studio 1 &nbsp;·&nbsp; Coach: Emma T &nbsp;·&nbsp; Ends 10:45</div>';
$fa .= '<div class="fit-now__capacity">';
$fa .= '<span class="fit-cap__lbl">Capacity</span>';
$fa .= '<div class="fit-cap__track"><div class="fit-cap__fill"></div></div>';
$fa .= '<span class="fit-cap__val">24/30</span>';
$fa .= '</div>';
$fa .= '</div>';
$fa .= '<div class="fit-upcoming">';
$classes = [
['time' => '11:00', 'name' => 'Yoga Flow', 'coach' => 'Sarah K', 'spaces' => 6],
['time' => '12:15', 'name' => 'Spin & Burn', 'coach' => 'James R', 'spaces' => 2],
['time' => '13:30', 'name' => 'Pilates Core', 'coach' => 'Lisa M', 'spaces' => 12],
];
foreach ($classes as $cls) {
$full_cls = $cls['spaces'] <= 3 ? ' fit-class--filling' : '';
$fa .= '<div class="fit-class' . $full_cls . '">';
$fa .= '<span class="fit-class__time">' . esc_html($cls['time']) . '</span>';
$fa .= '<span class="fit-class__name">' . esc_html($cls['name']) . '</span>';
$fa .= '<span class="fit-class__coach">' . esc_html($cls['coach']) . '</span>';
$fa .= '<span class="fit-class__spaces">' . esc_html($cls['spaces']) . ' spaces</span>';
$fa .= '</div>';
}
$fa .= '</div>'; // fit-upcoming
$fa .= '</div>'; // fit-board
$fa .= '</div>'; // fit-stage
$visual_html = $fa;
$visual_cls = 'platform-visual has-fitness';
}
elseif (!empty($a['hospitalityAnim'])) {
/* ── Hospitality Sign: Rotating Menu (Breakfast, Lunch, Dinner) ── */
$ha = '<div class="hosp-stage" aria-hidden="true">';

View File

@@ -56,6 +56,15 @@ add_action( 'wp_enqueue_scripts', function () {
true
);
// Solutions page animators - live data KPI ticker and transit departure board
wp_enqueue_script(
'oribi-solutions-animator',
ORIBI_URI . '/assets/js/solutions-animator.js',
[],
ORIBI_VERSION . '.' . filemtime( ORIBI_DIR . '/assets/js/solutions-animator.js' ),
true
);
// Localize AJAX endpoint for the contact form
wp_localize_script( 'oribi-main', 'oribiAjax', [
'url' => admin_url( 'admin-ajax.php' ),