// ============================================================ // EvIntegratedMobile — MOBILE port of the desktop "Integrated energy // infrastructure" section (EvPlanning) from ev-financial-cta.jsx. // // Desktop = interactive facility schematic (left) + 2×2 hover grid (right). // Mobile keeps the SAME story & visual language but swaps hover for touch: // • Intro (eyebrow + headline + supporting copy), stacked // • Interactive diagram, always visible, the visual focus // • Solution cards as a horizontal swipe carousel below the diagram // • Swiping / tapping a card -> updates the active diagram zone // • Tapping a diagram marker -> scrolls the matching card into view // • Default active zone: 01 — Demand-Charge Reduction // Copy, colours, type, iconography all carried over verbatim. // ============================================================ const EV_AMBER_LOCAL_M = '#D4A24C'; const EV_BLOCKS = [ { label: 'Demand-Charge Reduction', icon: 'activity', body: 'Solar generation offsets daytime charging demand while battery storage dispatches during peak charging periods—helping flatten facility demand spikes and reduce monthly demand charges.' }, { label: 'Reduced Utility Upgrades', icon: 'zap-off', body: 'Battery storage can reduce the peak electrical demand placed on the grid, helping facilities avoid or defer costly utility infrastructure upgrades, transformer replacements, and service capacity expansions.' }, { label: 'Solar-Powered Charging', icon: 'sun', body: 'Daytime EV charging aligns naturally with commercial solar production. Co-locating solar with EV infrastructure reduces net charging costs and improves long-term energy economics.' }, { label: 'Infrastructure Designed for Expansion', icon: 'layers', body: 'Integrated systems allow facilities to scale fleet electrification over time while planning electrical infrastructure, charging loads, solar generation, and battery capacity together from the start.' } ]; const EV_CAPTIONS = [ { zone: 'Solar + Battery', flow: 'Peak demand flattened' }, { zone: 'Battery + Service Panel', flow: 'Grid draw reduced' }, { zone: 'Solar + EV Chargers', flow: 'Daytime self-supply' }, { zone: 'Whole facility', flow: 'Designed for growth' } ]; // ============================================================ // EvSchematicMobile — identical SVG schematic to desktop, with the // numbered markers made tappable (large invisible hit targets). // ============================================================ const EvSchematicMobile = ({ activeIndex, onSelectZone }) => { const W = 540; const H = 420; const amber = EV_AMBER_LOCAL_M; const amberFill = 'rgba(212,162,76,0.18)'; 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)'; const showAll = activeIndex === 3; const isSolar = activeIndex === 2 || showAll; const isBattery = activeIndex === 0 || activeIndex === 1 || showAll; const isService = activeIndex === 1 || showAll; const isEV = activeIndex === 2 || showAll; const isDemandCombo = activeIndex === 0; const flowSolarBatt = activeIndex === 0 || showAll; const flowBattGrid = activeIndex === 1 || showAll; const flowSolarEV = activeIndex === 2 || showAll; const solarLit = isSolar || isDemandCombo; const batteryLit = isBattery; const evLit = isEV || isDemandCombo; const zones = [ { id: 0, label: '01', cx: 270, cy: 200 }, { id: 1, label: '02', cx: 60, cy: 178 }, { id: 2, label: '03', cx: 270, cy: 78 }, { id: 3, label: '04', cx: 502, cy: 388 } ]; return (
{/* Ground line + horizon */} {/* Utility line from off-canvas top-left (the grid) */} {flowBattGrid && ( )} UTILITY GRID {/* Building outline */} {/* Production / floor area divider */} FACILITY {[350, 370, 390, 410, 430].map(x => ( ))} {/* Roof solar panels */} {(() => { const panelLen = 26; const panelThick = 2; const legH = 5; const slope = 40 / 190; const angleDeg = Math.atan(slope) * 180 / Math.PI; const positions = []; for (let i = 0; i < 5; i++) { const cx = 110 + i * 30; const roofY = 118 - (cx - 80) * slope; positions.push({ cx, roofY, angle: -angleDeg, key: `L${i}` }); } for (let i = 0; i < 5; i++) { const cx = 310 + i * 30; const roofY = 78 + (cx - 270) * slope; positions.push({ cx, roofY, angle: angleDeg, key: `R${i}` }); } const panelStroke = solarLit ? amber : ink; const panelFill = solarLit ? amber : ink; const legStroke = solarLit ? amber : inkDim; return positions.map(({ cx, roofY, angle, key }) => { const x0 = cx - panelLen / 2; const x1 = cx + panelLen / 2; const yPanelTop = roofY - legH - panelThick; const yLegTop = roofY - legH; const legInset = 4; return ( ); }); })()} {/* Solar label */} SOLAR PV {/* Outdoor BESS pad — Battery */} {[0,1,2,3,4].map(i => ( ))} {[0,1,2,3,4].map(i => ( ))} BESS {/* Electrical service room / panel — Service */} {[0,1,2,3,4,5].map(i => ( ))} PANEL {/* EV charging — front parking apron */} {[180, 220, 260, 300, 340].map(x => ( ))} {[200, 240, 280, 320, 360].map(x => ( ))} EV CHARGERS {/* ===== FLOW INDICATORS ===== */} {flowSolarBatt && ( )} {flowSolarEV && ( )} {flowBattGrid && ( )} {isDemandCombo && ( )} {showAll && ( )} {/* Numbered markers — tappable */} {zones.map((z, i) => { const isActive = i === activeIndex; return ( onSelectZone && onSelectZone(i)} role="button" tabIndex={0} aria-label={`Show ${EV_BLOCKS[i].label}`} style={{ cursor: 'pointer' }}> {/* large invisible touch target */} {z.label} ); })}
); }; // ============================================================ // EvIntegratedMobile — full mobile section // ============================================================ const EvIntegratedMobile = () => { const [active, setActive] = React.useState(0); const scrollerRef = React.useRef(null); const cardRefs = React.useRef([]); const programmatic = React.useRef(false); const rafId = React.useRef(0); // (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) => { const sc = scrollerRef.current; const card = cardRefs.current[i]; if (!sc || !card) return; programmatic.current = true; 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; // Programmatic smooth scroll via scrollTo() is unreliable next to // scroll-snap, so animate scrollLeft by hand. Snap is suspended during // the tween and restored after we land on a snap point. sc.style.scrollSnapType = 'none'; window.cancelAnimationFrame(scrollToCard._raf); if (Math.abs(dist) < 1) { sc.style.scrollSnapType = 'x mandatory'; 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 { sc.style.scrollSnapType = 'x mandatory'; programmatic.current = false; } }; scrollToCard._raf = window.requestAnimationFrame(step); }; // Tap a card OR a diagram marker -> select that zone + center its card. const selectZone = (i) => { setActive(i); scrollToCard(i); }; // Swipe -> detect the card nearest the scroller centre, set it active. const handleScroll = () => { // Ignore scroll events fired by our own smooth-scroll (tap / marker / dot), // otherwise the in-flight scroll position resets `active` to card 0. if (programmatic.current) return; if (rafId.current) return; rafId.current = requestAnimationFrame(() => { rafId.current = 0; const sc = scrollerRef.current; if (!sc) return; 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; } }); setActive((prev) => (prev === best ? prev : best)); }); }; return (
{/* Decorative grid-rule pattern, top right (mirrors desktop) */}
); }; Object.assign(window, { EvIntegratedMobile });