// ============================================================ // WhsSolutionsMobile — MOBILE port of the desktop "Solutions for this // sector" section (WhsSolutions) from whs2-page-2.jsx. // // Desktop = interactive distribution-warehouse schematic (left) + 5-row hover // list (right), on a Soft-Green surface with Paper-White text. // Mobile keeps the SAME story, palette, type & iconography but swaps hover for // touch, following the approved Manufacturing mobile pattern: // • Intro (eyebrow + headline + supporting copy), stacked // • Interactive diagram, always visible, the visual focus // • Solution cards as a horizontal swipe carousel below the diagram // • Tapping a card / diagram marker -> updates the active diagram zone // • A dedicated "Explore …" link inside each card handles navigation, // so a first tap activates and the link tap navigates (no touch conflict) // • Default active zone: 01 — Energy Audits (per approved screenshot) // ============================================================ const WHS_AMBER = 'rgba(233,199,134,1)'; // --gi-amber-soft const WHS_AMBER_FILL = 'rgba(233,199,134,0.18)'; const WHS_SOFT_GREEN = '#375844'; // ============================================================ // WhsFacilitySchematicMobile — identical SVG schematic to desktop, with the // numbered markers made tappable (large invisible hit targets). // 5 zones · 01 Audit · 02 High-Bay LED · 03 Rooftop PV · 04 Battery · 05 EV/Forklift // ============================================================ const WhsFacilitySchematicMobile = ({ activeIndex, onSelectZone }) => { const W = 540; const H = 360; const amber = WHS_AMBER; const amberFill= WHS_AMBER_FILL; const ink = 'rgba(255,255,255,0.55)'; const inkDim = 'rgba(255,255,255,0.32)'; const inkFaint = 'rgba(255,255,255,0.18)'; const inkText = 'rgba(255,255,255,0.62)'; // Building footprint (flat-roof warehouse) — x 70..470, y 100..282 const zones = [ // 01 Energy Audits — whole facility ring { id: 0, label: '01', cx: 270, cy: 200, hl: }, // 02 High-Bay LED — interior ceiling row { id: 1, label: '02', cx: 270, cy: 132, hl: }, // 03 Solar PV — flat rooftop { id: 2, label: '03', cx: 270, cy: 92, hl: }, // 04 Battery — BESS pad { id: 3, label: '04', cx: 502, cy: 244, hl: }, // 05 EV / Forklift Charging — front apron { id: 4, label: '05', cx: 250, cy: 308, hl: }, ]; return (
{/* Ground line + horizon */} {/* Flat-roof warehouse outline (big-box) */} {/* Roof parapet line */} {/* Loading-dock band along base (right end) */} {[340, 360, 380, 400, 420, 440].map(x => ( ))} DOCK DOORS {/* Racking grid — palette racks visible interior detail */} {[155, 195, 235, 275, 315].map(x => ( {[180, 200, 220, 240].map(y => ( ))} ))} {/* High-Bay LED — interior ceiling row of fixtures (zone 02) */} {[110, 150, 190, 230, 270, 310, 350, 390, 430].map(x => ( {/* fixture body */} {/* light cone */} ))} HIGH-BAY LED {/* Rooftop solar — flat-roof array (zone 03) Panels lie low at a shallow tilt; many rows across full roof. */} {(() => { const positions = []; const tilt = -6; // shallow tilt for ballasted flat-roof const startX = 86; const endX = 460; const step = 28; for (let x = startX; x <= endX; x += step) { positions.push({ cx: x, key: `flat${x}` }); } const panelStroke = activeIndex === 2 ? amber : ink; const panelFill = activeIndex === 2 ? amber : ink; const legStroke = activeIndex === 2 ? amber : inkDim; const panelLen = 22; const panelThick = 2; const yPanel = 87; const yLeg = 96; return positions.map(({ cx, key }) => ( )); })()} ROOFTOP PV {/* EV / FORKLIFT CHARGING — front apron stalls (zone 05) */} {[180, 220, 260, 300, 340].map(x => ( ))} {/* EVSE pedestals — pole + head */} {[200, 240, 280, 320].map(x => ( ))} EV / FORKLIFT CHARGING {/* Outdoor BESS pad — Battery zone (04) */} {[0,1,2,3,4].map(i => ( ))} {[0,1,2,3,4].map(i => ( ))} BESS {/* Whole-facility audit ring — Energy Audits (01) */} {zones[activeIndex].hl} {/* Numbered markers — tappable */} {zones.map((z, i) => { const isActive = i === activeIndex; return ( onSelectZone && onSelectZone(i)} role="button" tabIndex={0} aria-label={`Show zone ${z.label}`} style={{ cursor: 'pointer' }}> {/* large invisible touch target */} {z.label} ); })}
); }; // ============================================================ // WhsSolutionsMobile — full mobile section // ============================================================ const WhsSolutionsMobile = () => { const rows = [ { idx: '01', icon: 'clipboard-list', name: 'Energy Audits', linkLabel: 'Energy Audits', zone: 'Whole facility', tag: 'Establish the documented baseline.', why: 'A commercial energy audit produces the facility-specific baseline that sizes every subsequent measure — load profile, demand-charge exposure, refrigeration efficiency, and the strongest first-dollar opportunities.', href: 'Solutions - Energy Audits.html' }, { idx: '02', icon: 'lightbulb', name: 'High-Bay LED Lighting', linkLabel: 'LED Lighting', zone: 'Interior · 24/7', tag: 'The fastest payback in the building.', why: 'High-bay LED retrofits in 24/7 warehouses typically pay back in two to four years through reduced electricity, longer maintenance intervals, and reduced HVAC heat-rejection load on cooled spaces.', href: 'Solutions - LED Lighting.html' }, { idx: '03', icon: 'sun', name: 'Commercial Solar', linkLabel: 'Commercial Solar', zone: 'Rooftop', tag: 'Large roofs, large arrays.', why: 'Modern distribution facilities have the largest unobstructed flat roofs in any commercial sector — well-suited to ballasted PV arrays that offset daytime base load and reduce exposure to rising utility rates.', href: 'Solutions - Commercial Solar.html' }, { idx: '04', icon: 'battery-charging', name: 'Battery Storage', linkLabel: 'Battery Storage', zone: 'Mechanical pad', tag: 'Cut peak demand charges.', why: 'Battery storage discharges during short peak events to hold metered demand below the threshold that sets the monthly bill — operating cost reductions independent of energy savings, plus resilience for cold-chain and automation systems.', href: 'Solutions - Battery Storage.html' }, { idx: '05', icon: 'plug-zap', name: 'EV / Forklift Charging', linkLabel: 'EV Charging', zone: 'Yard · dock apron', tag: 'Fleet electrification on a managed load.', why: 'Truck-fleet, last-mile, and forklift charging — planned around available service capacity, smart load management, and fleet replacement schedules rather than emergency service upgrades under deadline pressure.', href: 'Solutions - EV Charging.html' }, ]; // Default active zone: 01 — Energy Audits (index 0), per approved screenshot. const DEFAULT_INDEX = 0; const [active, setActive] = React.useState(DEFAULT_INDEX); const scrollerRef = React.useRef(null); const cardRefs = React.useRef([]); const programmatic = React.useRef(false); const rafId = React.useRef(0); const settleTimer = React.useRef(0); const didInit = React.useRef(false); // (Re)draw lucide icons whenever the active card changes. React.useEffect(() => { const t = setTimeout(() => window.lucide && window.lucide.createIcons({ attrs: { 'stroke-width': 1.4 } }), 20); return () => clearTimeout(t); }, [active]); const scrollToCard = (i, instant) => { const sc = scrollerRef.current; const card = cardRefs.current[i]; if (!sc || !card) return; programmatic.current = true; window.clearTimeout(settleTimer.current); const target = card.offsetLeft - (sc.clientWidth - card.clientWidth) / 2; const max = sc.scrollWidth - sc.clientWidth; const end = Math.max(0, Math.min(max, target)); const start = sc.scrollLeft; const dist = end - start; window.cancelAnimationFrame(scrollToCard._raf); if (instant) { sc.scrollLeft = end; window.requestAnimationFrame(() => { sc.scrollLeft = end; programmatic.current = false; }); return; } if (Math.abs(dist) < 1) { programmatic.current = false; return; } const dur = 420; const t0 = (window.performance || Date).now(); const ease = (t) => 1 - Math.pow(1 - t, 3); // easeOutCubic const step = (now) => { const p = Math.min(1, (now - t0) / dur); sc.scrollLeft = start + dist * ease(p); if (p < 1) { scrollToCard._raf = window.requestAnimationFrame(step); } else { programmatic.current = false; } }; scrollToCard._raf = window.requestAnimationFrame(step); }; // Center the default card on first paint. The flex children may not have // their final layout on the first frame (offsetLeft still 0), so retry // across a few frames until a card reports a non-zero offset, then jump. React.useEffect(() => { if (didInit.current) return; let tries = 0; const tryCenter = () => { const sc = scrollerRef.current; const card = cardRefs.current[DEFAULT_INDEX]; if (sc && card && (DEFAULT_INDEX === 0 || card.offsetLeft > 0)) { didInit.current = true; scrollToCard(DEFAULT_INDEX, true); return; } if (tries++ < 40) requestAnimationFrame(tryCenter); }; requestAnimationFrame(tryCenter); const onLoad = () => { didInit.current = false; tries = 0; requestAnimationFrame(tryCenter); }; window.addEventListener('load', onLoad); return () => window.removeEventListener('load', onLoad); }, []); // Tap a card OR a diagram marker -> select that zone + center its card. const selectZone = (i) => { setActive(i); scrollToCard(i); }; const nearestCard = () => { const sc = scrollerRef.current; if (!sc) return 0; const center = sc.scrollLeft + sc.clientWidth / 2; let best = 0, bestDist = Infinity; cardRefs.current.forEach((c, i) => { if (!c) return; const cc = c.offsetLeft + c.clientWidth / 2; const d = Math.abs(cc - center); if (d < bestDist) { bestDist = d; best = i; } }); return best; }; // Swipe -> live-update the active zone to the nearest card, then JS-snap to // center it once the user stops scrolling. const handleScroll = () => { if (programmatic.current) return; window.clearTimeout(settleTimer.current); settleTimer.current = window.setTimeout(() => { const i = nearestCard(); scrollToCard(i); }, 110); if (rafId.current) return; rafId.current = requestAnimationFrame(() => { rafId.current = 0; const best = nearestCard(); setActive((prev) => (prev === best ? prev : best)); }); }; return (
{/* Top amber tick — mirrors desktop */}
); }; Object.assign(window, { WhsSolutionsMobile, WhsFacilitySchematicMobile });