/* 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 (
);
}
// 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 (
);
};
// ============================================================
// Site Header
// ============================================================
const SiteHeader = ({ route, go }) => {
const [scrolled, setScrolled] = useState(false);
const [productsOpen, setProductsOpen] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const closeTimer = React.useRef(null);
useEffect(() => {
const root = document.querySelector('.site-scroll') || window;
const onScroll = () => {
const y = root === window ? window.scrollY : root.scrollTop;
setScrolled(y > 80);
};
root.addEventListener('scroll', onScroll);
return () => root.removeEventListener('scroll', onScroll);
}, []);
// Track viewport so the toggle only renders below the mobile breakpoint
const [isMobile, setIsMobile] = useState(
typeof window !== 'undefined' ? window.innerWidth <= 768 : false
);
useEffect(() => {
const onResize = () => {
const m = window.innerWidth <= 768;
setIsMobile(m);
if (!m) setMobileOpen(false);
};
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
// Lock body scroll while drawer is open
useEffect(() => {
document.body.style.overflow = mobileOpen ? 'hidden' : '';
return () => { document.body.style.overflow = ''; };
}, [mobileOpen]);
const linkCls = (r) => "nav-link" + (route === r ? " active" : "");
const openMenu = () => { clearTimeout(closeTimer.current); setProductsOpen(true); };
const closeMenu = () => { closeTimer.current = setTimeout(() => setProductsOpen(false), 120); };
// Close drawer + dropdown on navigation
const navGo = (r) => { setMobileOpen(false); setProductsOpen(false); go(r); };
return (
{ e.preventDefault(); navGo('home'); }}>
Dynawins
{isMobile && (
{ e.preventDefault(); e.stopPropagation(); setMobileOpen((v) => !v); }}
>
{mobileOpen ? (
) : (
)}
)}
{isMobile && mobileOpen && (
);
};
const menuItem = {
display: 'flex', alignItems: 'center', gap: 12, padding: '10px',
borderRadius: 10, textDecoration: 'none', transition: 'background .15s',
};
// ============================================================
// Footer
// ============================================================
const SiteFooter = ({ go }) => (
End-to-end Microsoft Power Platform partner — Dynamics 365, Power Apps, AI & Copilot, automation, code apps, and our own line of productivity products.
© 2026 Dynawins. All rights reserved.
);
// ============================================================
// WhatsApp FAB
// ============================================================
const WhatsAppFAB = () => (
);
// ============================================================
// D365 mock UI (used in hero + product page)
// ============================================================
const D365Mock = () => (
org.crm.dynamics.com / accounts / Contoso Ltd
Sales Hub
Accounts
Contacts
Opportunities
Quotes
Activities
Tasks
Emails
Contoso Ltd
account · Active · owner: Jane Cooper
Annual revenue $48,200,000
Primary contact Marcus Tan
Recent activity
📧 Quote Q-2841 sent · 2d ago
📞 Discovery call w/ Marcus · 5d ago
📝 Renewal note added · 1w ago
You
Summarize this account and tell me what's at risk.
Smart Panel
Contoso is a $48M manufacturer, 312 staff. They're 14 days into the renewal cycle, and the last touchpoint was a quote sent on Apr 26 with no reply. Owner Jane Cooper closed two similar accounts last quarter.
Risk: medium. Suggest a follow-up email — draft ready.
Ask about this record…
);
// ============================================================
// BrowserShot — responsive screenshot in a browser frame.
// `src` is the image URL. `url` is the fake address bar text.
// `alt` describes the image. Falls back to `fallback` if the image fails.
// ============================================================
const BrowserShot = ({ src, url = 'org.crm.dynamics.com', alt = '', fallback = null, dark = false }) => {
const [errored, setErrored] = React.useState(false);
if (errored && fallback) return fallback;
return (
setErrored(true)}
style={{ display: 'block', width: '100%', height: 'auto' }}
/>
);
};
// ============================================================
// Photo — responsive image card with optional caption.
// Uses Unsplash CDN URLs (free for commercial use, Unsplash License).
// ============================================================
const Photo = ({ src, alt = '', radius = 12, ratio = '16 / 10', style = {} }) => (
);
// Stable Unsplash CDN URLs — Unsplash License (free for commercial use, no
// attribution required). Replace any of these with your own assets later.
const STOCK = {
office: 'https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=1600&q=80',
team: 'https://images.unsplash.com/photo-1556761175-5973dc0f32e7?auto=format&fit=crop&w=1600&q=80',
laptopCharts: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1600&q=80',
dashboard: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=1600&q=80',
twoScreens: 'https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&w=1600&q=80',
workingSolo: 'https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?auto=format&fit=crop&w=1600&q=80',
meeting: 'https://images.unsplash.com/photo-1552581234-26160f608093?auto=format&fit=crop&w=1600&q=80',
codeScreen: 'https://images.unsplash.com/photo-1551434678-e076c223a692?auto=format&fit=crop&w=1600&q=80',
reportPaper: 'https://images.unsplash.com/photo-1543286386-713bdd548da4?auto=format&fit=crop&w=1600&q=80',
};
// Local Smart Panel screenshots — drop the user's PNGs at these paths:
// assets/screens/smartpanel-ai-assistant.png (AI Assistant chat panel)
// assets/screens/smartpanel-followup.png (Follow-up Advisor)
// assets/screens/smartpanel-email.png (Email Drafting)
const SHOTS = {
aiAssistant: 'assets/screens/smartpanel-ai-assistant.png',
followUp: 'assets/screens/smartpanel-followup.png',
email: 'assets/screens/smartpanel-email.png',
};
// ============================================================
// Export to global scope for other Babel scripts
// ============================================================
Object.assign(window, { Icon, BrandIcon, SiteHeader, SiteFooter, WhatsAppFAB, D365Mock, BrowserShot, Photo, STOCK, SHOTS, AnimatedCounter, ScrollProgress });