// PeopleFlow 2.0 — landing app const { useState, useEffect, useRef, useMemo, useCallback } = React; // ---------------------------------------------------------------------------- // RibbonMark — logo symbol (V1: 3 teste stroke gradient, archi circolari G1) // Generato programmaticamente: teste + valli, tangenti orizzontali. // ---------------------------------------------------------------------------- function ribbonV1Path({ n = 3, r = 25, cx0 = 45, cy = 50 } = {}) { let x = cx0 - r; let d = `M ${x} ${cy} `; for (let i = 0; i < n; i++) { const xh = x + 2 * r; d += `A ${r} ${r} 0 0 1 ${xh} ${cy} `; x = xh; if (i < n - 1) { const xv = x + 2 * r; d += `A ${r} ${r} 0 0 0 ${xv} ${cy} `; x = xv; } } return d; } function RibbonMark({ width = 32, height = 14, strokeWidth = 14, ariaLabel }) { return ( ); } // ---------------------------------------------------------------------------- // Language hook (persisted) // ---------------------------------------------------------------------------- function useLang() { const [lang, setLang] = useState(() => { try { return localStorage.getItem("pf_lang") || "it"; } catch { return "it"; } }); useEffect(() => { try { localStorage.setItem("pf_lang", lang); } catch {} document.documentElement.lang = lang; }, [lang]); const t = window.PF_CONTENT[lang]; return { lang, setLang, t }; } // ---------------------------------------------------------------------------- // Reveal on scroll // ---------------------------------------------------------------------------- function useReveal() { useEffect(() => { let io = null; let safety = null; // Delay a tick so React has finished rendering children. const raf = requestAnimationFrame(() => { document.documentElement.classList.add("js-reveal-ready"); const els = Array.from(document.querySelectorAll(".reveal")); const revealAll = () => els.forEach((el) => el.classList.add("in")); if (!("IntersectionObserver" in window)) { revealAll(); return; } io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } }); }, { threshold: 0.01, rootMargin: "0px 0px -2% 0px" }); els.forEach((el) => { // Elements already in viewport at load — reveal immediately. const rect = el.getBoundingClientRect(); if (rect.top < window.innerHeight && rect.bottom > 0) { el.classList.add("in"); } else { io.observe(el); } }); // Safety net: after 1500ms, force-reveal anything still hidden. // Protects against IntersectionObserver batching quirks in embedded/iframed contexts. safety = setTimeout(() => { document.querySelectorAll(".reveal:not(.in)").forEach((el) => { el.classList.add("in"); }); }, 1500); }); return () => { cancelAnimationFrame(raf); if (io) io.disconnect(); if (safety) clearTimeout(safety); }; }, []); } // ---------------------------------------------------------------------------- // Demo booking page URL (single funnel — all CTAs point here) // ---------------------------------------------------------------------------- const DEMO_URL = "demo.html"; // ---------------------------------------------------------------------------- // Nav // ---------------------------------------------------------------------------- function Nav({ lang, setLang, t }) { return ( ); } // ---------------------------------------------------------------------------- // Hero — headline + live chart // ---------------------------------------------------------------------------- function HeroChart({ title, note }) { const ref = useRef(null); const chartRef = useRef(null); const dataRef = useRef([]); useEffect(() => { if (!window.echarts || !ref.current) return; const chart = window.echarts.init(ref.current, null, { renderer: "svg" }); chartRef.current = chart; const N = 40; const now = Date.now(); const seed = []; for (let i = N - 1; i >= 0; i--) { // Simulate retail traffic curve (hours pattern-like) const t = (N - i) / N; const base = 12 + 20 * Math.sin(t * Math.PI * 1.6) + Math.random() * 6; seed.push([now - i * 3000, Math.max(2, Math.round(base))]); } dataRef.current = seed; const s = getComputedStyle(document.documentElement); const accent = s.getPropertyValue("--accent").trim(); const accent2 = s.getPropertyValue("--accent-2").trim(); const ink2 = s.getPropertyValue("--ink-2").trim(); const line = s.getPropertyValue("--line").trim(); const bg = s.getPropertyValue("--bg-2").trim(); chart.setOption({ animation: true, grid: { left: 40, right: 12, top: 20, bottom: 30 }, xAxis: { type: "time", axisLine: { lineStyle: { color: line } }, axisLabel: { color: ink2, fontFamily: "JetBrains Mono, monospace", fontSize: 10, formatter: (v) => { const d = new Date(v); return String(d.getMinutes()).padStart(2, "0") + ":" + String(d.getSeconds()).padStart(2, "0"); }, }, splitLine: { show: false }, }, yAxis: { type: "value", min: 0, axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: ink2, fontFamily: "JetBrains Mono, monospace", fontSize: 10 }, splitLine: { lineStyle: { color: line, type: "dashed" } }, }, series: [ { type: "line", smooth: 0.35, showSymbol: false, data: dataRef.current, lineStyle: { color: accent, width: 2 }, areaStyle: { color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: `color-mix(in oklch, ${accent} 35%, transparent)` }, { offset: 1, color: `color-mix(in oklch, ${accent} 0%, transparent)` }, ], }, }, }, ], }); const tick = setInterval(() => { const arr = dataRef.current; const last = arr[arr.length - 1]; const prevV = last[1]; const drift = (Math.random() - 0.45) * 6; const v = Math.max(2, Math.round(prevV + drift)); arr.push([Date.now(), v]); while (arr.length > 40) arr.shift(); chart.setOption({ series: [{ data: arr }] }); }, 1500); const onResize = () => chart.resize(); window.addEventListener("resize", onResize); return () => { clearInterval(tick); window.removeEventListener("resize", onResize); chart.dispose(); }; }, []); // Redraw colors when theme changes (tweaks) useEffect(() => { const obs = new MutationObserver(() => { if (!chartRef.current) return; const s = getComputedStyle(document.documentElement); const accent = s.getPropertyValue("--accent").trim(); const line = s.getPropertyValue("--line").trim(); const ink2 = s.getPropertyValue("--ink-2").trim(); chartRef.current.setOption({ xAxis: { axisLine: { lineStyle: { color: line } }, axisLabel: { color: ink2 } }, yAxis: { axisLabel: { color: ink2 }, splitLine: { lineStyle: { color: line, type: "dashed" } } }, series: [{ lineStyle: { color: accent, width: 2 }, areaStyle: { color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: `color-mix(in oklch, ${accent} 35%, transparent)` }, { offset: 1, color: `color-mix(in oklch, ${accent} 0%, transparent)` }, ], }, }, }], }); }); obs.observe(document.documentElement, { attributes: true, attributeFilter: ["style", "data-theme"] }); return () => obs.disconnect(); }, []); return (
{title}
{note}
LIVE
); } function Hero({ t }) { return (
{t.hero.eyebrow}

{t.hero.title_a} {t.hero.title_b}

{t.hero.sub}

{t.hero.cta_primary} {t.hero.cta_secondary}
{t.hero.trust.map((it, i) => {it})}
); } // ---------------------------------------------------------------------------- // Social proof — numeric counters // ---------------------------------------------------------------------------- function ProofCounter({ target, suffix }) { const ref = useRef(null); const done = useRef(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting && !done.current) { done.current = true; const dur = 1400; const t0 = performance.now(); const step = (t) => { const p = Math.min(1, (t - t0) / dur); const eased = 1 - Math.pow(1 - p, 3); const v = Math.round(target * eased); el.textContent = v.toLocaleString("it-IT") + suffix; if (p < 1) requestAnimationFrame(step); }; requestAnimationFrame(step); } }); }, { threshold: 0.4 }); io.observe(el); return () => io.disconnect(); }, [target, suffix]); return {target.toLocaleString("it-IT")}{suffix}; } function SocialProof({ t }) { return (
{t.proof.items.map((it, i) => (
{it.l}
))}
); } // ---------------------------------------------------------------------------- // Problem // ---------------------------------------------------------------------------- function Problem({ t }) { return (
{t.problem.eyebrow}

{t.problem.title}

{t.problem.sub}

{t.problem.items.map((it, i) => (
{it.n}

{it.t}

{it.d}

))}
); } // ---------------------------------------------------------------------------- // Showcase — full-bleed editorial image // ---------------------------------------------------------------------------- function Showcase({ t }) { return (
Top-down architectural illustration of a boutique with color-coded customer and staff circulation paths
{t.showcase.meta}
); } // ---------------------------------------------------------------------------- // Platform // ---------------------------------------------------------------------------- function Platform({ t }) { return (
{t.platform.eyebrow}

{t.platform.title}

{t.platform.sub}

{t.platform.features.map((f, i) => (
{f.k}

{f.t}

{f.d}

{f.tag}
))}
); } // ---------------------------------------------------------------------------- // Architecture — flow with pulse // ---------------------------------------------------------------------------- function Architecture({ t }) { const wrapRef = useRef(null); const [active, setActive] = useState(0); useEffect(() => { const el = wrapRef.current; if (!el) return; let timer = null; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { // Start cycling if (!timer) { timer = setInterval(() => { setActive((a) => (a + 1) % t.architecture.nodes.length); }, 1800); } } else { if (timer) { clearInterval(timer); timer = null; } } }); }, { threshold: 0.3 }); io.observe(el); return () => { io.disconnect(); if (timer) clearInterval(timer); }; }, [t.architecture.nodes.length]); return (
{t.architecture.eyebrow}

{t.architecture.title}

{t.architecture.sub}

{t.architecture.nodes.map((n, i) => (
0{i + 1}

{n.t}

{n.d}

{n.tag} {i < t.architecture.nodes.length - 1 && }
))}
); } // ---------------------------------------------------------------------------- // AI Assistant with animated chat // ---------------------------------------------------------------------------- function AI({ t }) { const [visible, setVisible] = useState(0); const wrapRef = useRef(null); const startedRef = useRef(false); useEffect(() => { const el = wrapRef.current; if (!el) return; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting && !startedRef.current) { startedRef.current = true; let i = 0; const total = t.ai.chat.length; const step = () => { i++; setVisible(i); if (i < total) setTimeout(step, 1400); }; setTimeout(step, 400); } }); }, { threshold: 0.35 }); io.observe(el); return () => io.disconnect(); }, [t.ai.chat.length]); // Reset when language changes useEffect(() => { startedRef.current = false; setVisible(0); const el = wrapRef.current; if (!el) return; // Retrigger check const rect = el.getBoundingClientRect(); if (rect.top < window.innerHeight && rect.bottom > 0) { startedRef.current = true; let i = 0; const total = t.ai.chat.length; const step = () => { i++; setVisible(i); if (i < total) setTimeout(step, 1200); }; setTimeout(step, 300); } }, [t]); const renderBubble = (text) => { // Support **bold** const parts = text.split(/(\*\*[^*]+\*\*)/g); return parts.map((p, i) => p.startsWith("**") && p.endsWith("**") ? {p.slice(2, -2)} : {p} ); }; return (
{t.ai.eyebrow}

{t.ai.title}

{t.ai.sub}

    {t.ai.bullets.map((b, i) =>
  • {b}
  • )}
peopleflow · ai assistant · mcp
{t.ai.chat.map((m, i) => (
{renderBubble(m.text)}
))}
{'>'} {t.ai.chat[0].text.slice(0, 24)}...
); } // ---------------------------------------------------------------------------- // Security // ---------------------------------------------------------------------------- function Security({ t }) { return (
{t.security.eyebrow}

{t.security.title}

{t.security.sub}

{t.security.items.map((it, i) => (
{it.t}
{it.d}
))}
); } // ---------------------------------------------------------------------------- // Comparison // ---------------------------------------------------------------------------- function Compare({ t }) { return (
{t.compare.eyebrow}

{t.compare.title}

{t.compare.rows.map((r, i) => ( ))}
{t.compare.cols[0]} {t.compare.cols[1]} {t.compare.cols[2]}
{r.l} {r.v[0]} {r.v[1]} {r.v[2]}
); } // ---------------------------------------------------------------------------- // Stats — animated counters // ---------------------------------------------------------------------------- function StatValue({ value }) { // Extract leading number if any, animate it; else just render const numMatch = String(value).match(/^([≥]?)(\d+(?:[.,]\d+)?)(%?.*)$/); const ref = useRef(null); const done = useRef(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting && !done.current) { done.current = true; if (!numMatch) return; const prefix = numMatch[1] || ""; const target = parseFloat(numMatch[2].replace(",", ".")); const suffix = numMatch[3] || ""; const isInt = !numMatch[2].includes(".") && !numMatch[2].includes(","); const dur = 1200; const t0 = performance.now(); const step = (t) => { const p = Math.min(1, (t - t0) / dur); const eased = 1 - Math.pow(1 - p, 3); const v = target * eased; el.textContent = prefix + (isInt ? Math.round(v) : v.toFixed(1)) + suffix; if (p < 1) requestAnimationFrame(step); else el.textContent = value; }; requestAnimationFrame(step); } }); }, { threshold: 0.5 }); io.observe(el); return () => io.disconnect(); }, [value]); // Fallback: render the real value so it's readable even if the observer // never fires (JS off, some sandboxed iframes, etc). The animation just // overwrites textContent when it starts. return
{value}
; } function Stats({ t }) { return (
— 2026

{t.stats.title}

{t.stats.items.map((it, i) => (
{it.l}
{it.sub}
))}
); } // ---------------------------------------------------------------------------- // BI Integration // ---------------------------------------------------------------------------- function BI({ t }) { return (
{t.bi.eyebrow}

{t.bi.title}

{t.bi.sub}

{t.bi.badges.map((b, i) => {b})}
{t.bi.items.map((it, i) => (

{it.t}

{it.d}

))}
); } // ---------------------------------------------------------------------------- // Testimonials // ---------------------------------------------------------------------------- function Testimonials({ t }) { return (
{t.testimonials.eyebrow}

{t.testimonials.title}

{t.testimonials.items.map((it, i) => (
"

{it.quote}

{it.name}
{it.role}
))}
); } // ---------------------------------------------------------------------------- // FAQ // ---------------------------------------------------------------------------- function FAQ({ t }) { const [open, setOpen] = useState(0); return (
{t.faq.eyebrow}

{t.faq.title}

{t.faq.items.map((it, i) => (
setOpen(open === i ? -1 : i)} >
{it.q} +
{it.a}
))}
); } // ---------------------------------------------------------------------------- // Final CTA // ---------------------------------------------------------------------------- function FinalCTA({ t }) { return (
→ 30 min

{t.finalCta.title}

{t.finalCta.sub}

{t.finalCta.cta}
); } // ---------------------------------------------------------------------------- // Sticky CTA (mobile only, appears after scrolling past hero) // ---------------------------------------------------------------------------- function StickyCTA({ t }) { const [show, setShow] = useState(false); useEffect(() => { const onScroll = () => { setShow(window.scrollY > window.innerHeight * 0.8 && window.scrollY < document.documentElement.scrollHeight - window.innerHeight * 1.5); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); return (
{t.stickyCta}
); } // ---------------------------------------------------------------------------- // Cookie banner (technical cookies only — no tracking to accept, but we show // respect for the user's presence) // ---------------------------------------------------------------------------- function CookieBanner({ t }) { const [show, setShow] = useState(false); useEffect(() => { try { if (!localStorage.getItem("pf_cookie_ack")) { setTimeout(() => setShow(true), 1200); } } catch {} }, []); const dismiss = () => { try { localStorage.setItem("pf_cookie_ack", "1"); } catch {} setShow(false); }; return (

{t.cookie.msg}{" "} {t.cookie.link}

); } // ---------------------------------------------------------------------------- // Footer // ---------------------------------------------------------------------------- function Footer({ t }) { return ( ); } // ---------------------------------------------------------------------------- // Tweaks system removed — palette + font definitivi vivono in styles.css :root // ---------------------------------------------------------------------------- // ---------------------------------------------------------------------------- // Root App // ---------------------------------------------------------------------------- function App() { const { lang, setLang, t } = useLang(); useReveal(); return ( <>