/* global React */ const { useState, useEffect, useRef } = React; // ============================================================ // Inline icons (Lucide subset, 24px, 1.5 stroke) // ============================================================ const Icon = ({ name, size = 20, ...rest }) => { const paths = { "sparkles": , "arrow-right": , "check": , "shield": , "zap": , "panel-right": , "layout-grid": , "video": , "gantt": , "lock": , "clock": , "users": , "menu": , "x": , "twitter": , "github": , "linkedin": , "cube": , "settings": , "rocket": , "trending-up": , "globe": , "mail": , "phone": , "message-square": , "calendar": , "star": , "heart": , "trophy": , "target": , "bar-chart": , "database": , "play": , "briefcase": , "building": , "external-link": , "award": , }; return {paths[name]}; }; // ============================================================ // BrandIcon — real Microsoft / Power Platform product logos from local // Images/ folder. Falls back to a generic gradient initials tile for any // brand we don't have a real PNG/SVG for yet. No external CDNs. // ============================================================ const BRAND_IMG = { "d365": "Images/d365-crm-icon.png", "power-apps": "Images/powerapp-2020-icon-1024x1024.png", "power-automate": "Images/PowerAutomate-2020-icon-1024x1024.png", "power-bi": "Images/powerbi-2020-icon-768x768.png", "power-pages": "Images/PP-Hero_Icon_PowerPages.svg", "azure": "Images/1280px-Microsoft_Azure.svg.png", "copilot": "Images/copilot-icon.png", "ai-agents": "Images/ai-agent-icon.png", "pcf": "Images/pcf-control-icon.png", "ms-certified": "Images/microsoft-certified-professional-certification-microsoft-office-365-microsoft-dynamics-microsoft-3065b9dc32e233e4c2b77ac0b41b3005.png", }; const BrandIcon = ({ name, size = 40, alt }) => { const src = BRAND_IMG[name]; if (src) { return ( {alt ); } // Generic gradient-tile fallback for icons without a local PNG (e.g. pcf). const initials = (name || '').split('-').map((s) => s[0]).join('').toUpperCase().slice(0, 2); return ( {initials} ); }; // ============================================================ // (legacy) inline-SVG BrandIcon kept as _BrandIconSVG for reference. // Not exported — the active BrandIcon above uses local image files instead. // ============================================================ const _BrandIconSVG = ({ name, size = 32 }) => { const w = size, h = size; const marks = { "d365": ( ), "power-apps": ( ), "power-automate": ( ), "power-bi": ( ), "azure": ( ), "azure-openai": ( ), "ai-builder": ( ), "copilot": ( ), "dataverse": ( ), "sharepoint": ( ), "teams": ( T ), "pcf": ( ), "ai-agents": ( ), }; return marks[name] || null; }; // ============================================================ // AnimatedCounter — counts up from 0 to a target when scrolled into view. // `value` accepts a number OR a string with a numeric prefix and a suffix // (e.g. "60s", "3 hrs", "14+", "99.9%"). Non-numeric values render as-is. // ============================================================ const AnimatedCounter = ({ value, duration = 1400 }) => { const ref = React.useRef(null); const [display, setDisplay] = React.useState(value); const ran = React.useRef(false); React.useEffect(() => { const m = String(value).match(/^(\D*)(-?[\d,.]+)(.*)$/); if (!m) { setDisplay(value); return; } const prefix = m[1]; const target = parseFloat(m[2].replace(/,/g, '')); const suffix = m[3]; if (Number.isNaN(target)) { setDisplay(value); return; } const decimals = (m[2].split('.')[1] || '').length; const fmt = (n) => { const fixed = decimals ? n.toFixed(decimals) : Math.round(n).toString(); // re-add thousand separators if original had them const withCommas = m[2].includes(',') ? Number(fixed).toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) : fixed; return prefix + withCommas + suffix; }; setDisplay(fmt(0)); const start = () => { if (ran.current) return; ran.current = true; const t0 = performance.now(); const tick = (now) => { const p = Math.min(1, (now - t0) / duration); // ease-out cubic const eased = 1 - Math.pow(1 - p, 3); setDisplay(fmt(target * eased)); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }; const el = ref.current; if (!el || typeof IntersectionObserver === 'undefined') { start(); return; } const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { start(); io.disconnect(); } }); }, { threshold: 0.4 }); io.observe(el); return () => io.disconnect(); }, [value, duration]); return {display}; }; // ============================================================ // ScrollProgress — thin gradient bar at the top tracking page scroll. // Mounts once, uses requestAnimationFrame to avoid layout thrash. // ============================================================ const ScrollProgress = () => { const [pct, setPct] = React.useState(0); React.useEffect(() => { let raf = null; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { const h = document.documentElement; const scrolled = h.scrollTop || document.body.scrollTop; const max = (h.scrollHeight - h.clientHeight) || 1; setPct(Math.min(100, Math.max(0, (scrolled / max) * 100))); raf = null; }); }; window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); return () => window.removeEventListener('scroll', onScroll); }, []); return (