// ============================================================
// HomeTestimonialsMobile — mobile-only ("Built on long-term performance")
// Ported verbatim from the design deliverable testimonials-mobile.html:
// a swipeable client-testimonial carousel with arrows, dots, and a compact
// horizontal client selector. Vanilla-JS logic preserved, run inside a
// useEffect scoped to the component root (no global ids / no document-wide
// side effects beyond the scroll-reveal, which is scoped to this section).
// Rendered (≤900px) by the GI-MOBILE-SWAP-V1 wrapper in app.html, replacing
// the desktop . Brand CSS vars come from the
// site-wide colors_and_type.css. The standalone's full-bleed body background
// (#EEF0ED) becomes the .tm-band wrapper background here.
// ============================================================
const HomeTestimonialsMobile = () => {
const rootRef = React.useRef(null);
React.useEffect(() => {
const root = rootRef.current;
if (!root) return;
// Content sourced verbatim from the live home-page testimonials data.
var QUOTES = [
{ hero: '“Financial modelling translated directly into operating results.”',
body: 'Green Integrations delivered a detailed financial model and system design that aligned with our production demands. The project was executed on time and performed as expected.',
name: 'Robert Engel', org: 'Cober Solutions', sector: 'Print manufacturer · ON' },
{ hero: '“Reliable execution and long-term service support.”',
body: 'Projects were completed efficiently, and ongoing support has remained strong. The team continues to deliver consistent service across multiple engagements.',
name: 'Keith Lawless', org: 'Air Transat', sector: 'Aviation · ON' },
{ hero: '“Consistent delivery across multiple projects.”',
body: 'We selected Green Integrations for their depth of analysis. Over several projects, they have consistently delivered strong results and remained a reliable partner.',
name: 'Luigi De Serio', org: 'Axiom', sector: 'Manufacturing · ON' },
{ hero: '“Responsive and aligned with operational constraints.”',
body: 'Green Integrations worked within our site requirements and timelines, delivering a professional and responsive experience from assessment through execution.',
name: 'John Blanchard', org: 'Markland', sector: 'Commercial Real Estate · ON' },
{ hero: '“Delivered on schedule with minimal operational disruption.”',
body: 'The team worked safely, efficiently, and around our operations. Execution was clean, organized, and aligned with the project timeline.',
name: 'Mike Book', org: 'Richmond Steel', sector: 'Industrial · ON' },
{ hero: '“Clear guidance across multiple system options.”',
body: 'Green Integrations presented multiple system approaches and worked with us to evaluate each against our operational requirements and budget. The process provided clarity on the right technology and path forward.',
name: 'David Sliwin', org: 'Athletic Knit', sector: 'Sports apparel manufacturer · ON' },
];
var track = root.querySelector('.tm-track');
var dotsWrap = root.querySelector('.tm-dots');
var selWrap = root.querySelector('.tm-selector');
var prevBtn = root.querySelector('.tm-prev');
var nextBtn = root.querySelector('.tm-next');
var index = 0;
function esc(s) {
return String(s).replace(/&/g, '&').replace(//g, '>');
}
// Build slides
QUOTES.forEach(function (q, i) {
var slide = document.createElement('div');
slide.className = 'tm-slide';
slide.setAttribute('role', 'group');
slide.setAttribute('aria-roledescription', 'slide');
slide.setAttribute('aria-label', (i + 1) + ' of ' + QUOTES.length + ' — ' + q.org);
slide.innerHTML =
'' +
'Client testimonial
' +
'' + esc(q.hero) + '
' +
'' + esc(q.body) + '
' +
'' +
'
' + esc(q.name) + '
' +
'
' + esc(q.org) + '
' +
'
' + esc(q.sector) + '
' +
'
' +
'';
track.appendChild(slide);
// Dot
var dot = document.createElement('button');
dot.className = 'tm-dot';
dot.type = 'button';
dot.setAttribute('role', 'tab');
dot.setAttribute('aria-label', q.org);
dot.addEventListener('click', function () { go(i); });
dotsWrap.appendChild(dot);
// Selector chip
var chip = document.createElement('button');
chip.className = 'tm-chip';
chip.type = 'button';
chip.setAttribute('role', 'tab');
chip.innerHTML =
'
' + esc(q.org) + '
' +
'' + esc(q.name) + '
';
chip.addEventListener('click', function () { go(i); });
selWrap.appendChild(chip);
});
var dots = Array.prototype.slice.call(dotsWrap.children);
var chips = Array.prototype.slice.call(selWrap.children);
function render(animate) {
if (!animate) track.classList.add('is-dragging');
track.style.transform = 'translateX(' + (-index * 100) + '%)';
if (!animate) { track.offsetHeight; track.classList.remove('is-dragging'); }
dots.forEach(function (d, i) {
d.classList.toggle('is-active', i === index);
d.setAttribute('aria-selected', i === index ? 'true' : 'false');
});
chips.forEach(function (c, i) {
c.classList.toggle('is-active', i === index);
c.setAttribute('aria-selected', i === index ? 'true' : 'false');
});
// keep active chip in view
var active = chips[index];
if (active) {
var l = active.offsetLeft, r = l + active.offsetWidth;
var vl = selWrap.scrollLeft, vr = vl + selWrap.clientWidth;
if (l < vl) selWrap.scrollTo({ left: l - 16, behavior: 'smooth' });
else if (r > vr) selWrap.scrollTo({ left: r - selWrap.clientWidth + 16, behavior: 'smooth' });
}
}
function go(i) {
index = (i + QUOTES.length) % QUOTES.length;
render(true);
}
prevBtn.addEventListener('click', function () { go(index - 1); });
nextBtn.addEventListener('click', function () { go(index + 1); });
function onKey(e) {
if (e.key === 'ArrowLeft') go(index - 1);
else if (e.key === 'ArrowRight') go(index + 1);
}
document.addEventListener('keydown', onKey);
// ---------- Swipe ----------
var startX = 0, startY = 0, dx = 0, dragging = false, decided = false, horiz = false;
var vp = track.parentElement;
function onTouchStart(e) {
if (e.touches.length !== 1) return;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
dx = 0; dragging = true; decided = false; horiz = false;
}
function onTouchMove(e) {
if (!dragging) return;
var x = e.touches[0].clientX, y = e.touches[0].clientY;
dx = x - startX;
var dy = y - startY;
if (!decided) {
if (Math.abs(dx) > 8 || Math.abs(dy) > 8) {
decided = true;
horiz = Math.abs(dx) > Math.abs(dy);
}
}
if (horiz) {
e.preventDefault();
track.classList.add('is-dragging');
var pct = (dx / vp.clientWidth) * 100;
// resistance at the ends
if ((index === 0 && dx > 0) || (index === QUOTES.length - 1 && dx < 0)) pct *= 0.35;
track.style.transform = 'translateX(' + (-index * 100 + pct) + '%)';
}
}
function onTouchEnd() {
if (!dragging) return;
dragging = false;
track.classList.remove('is-dragging');
if (horiz && Math.abs(dx) > vp.clientWidth * 0.18) {
go(dx < 0 ? index + 1 : index - 1);
} else {
render(true);
}
}
vp.addEventListener('touchstart', onTouchStart, { passive: true });
vp.addEventListener('touchmove', onTouchMove, { passive: false });
vp.addEventListener('touchend', onTouchEnd);
// initial
render(false);
function onResize() { render(false); }
window.addEventListener('resize', onResize);
// entrance reveal — opt into the hidden start-state only now that JS runs.
// Scoped to this section's root rather than document.body.
root.classList.add('reveal-ready');
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting) { en.target.classList.add('is-in'); io.unobserve(en.target); }
});
}, { threshold: 0.12 });
root.querySelectorAll('[data-reveal]').forEach(function (el) { io.observe(el); });
return () => {
document.removeEventListener('keydown', onKey);
window.removeEventListener('resize', onResize);
io.disconnect();
};
}, []);
return (
<>
{/* Intro */}
{/* Carousel */}
{/* Compact client selector */}
>
);
};
Object.assign(window, { HomeTestimonialsMobile });