// components.jsx — shared primitives for KVB 2.0 const { useState, useEffect, useRef } = React; /* ---------------- Icons (clean line set) ---------------- */ const I = { bolt: "M13 2 4 14h6l-1 8 9-12h-6l1-8z", shield:"M12 3 5 6v5c0 4.4 3 7.6 7 9 4-1.4 7-4.6 7-9V6l-7-3z", eye: "M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z", chart: "M4 20V10M10 20V4M16 20v-7M22 20H2", chat: "M4 5h16v11H9l-5 4V5z", users: "M16 19v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM22 19v-2a4 4 0 0 0-3-3.9M16 3.1A4 4 0 0 1 16 11", check: "M20 6 9 17l-5-5", arrow: "M5 12h14M13 6l6 6-6 6", play: "M7 4v16l13-8z", lock: "M6 10V7a6 6 0 1 1 12 0v3M5 10h14v11H5z", globe: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c2.5 2.5 4 6 4 9s-1.5 6.5-4 9c-2.5-2.5-4-6-4-9s1.5-6.5 4-9z", spark: "M12 3v4M12 17v4M3 12h4M17 12h4M6.3 6.3l2.4 2.4M15.3 15.3l2.4 2.4M17.7 6.3l-2.4 2.4M8.7 15.3l-2.4 2.4", rocket:"M5 15c-1 1-2 5-2 5s4-1 5-2M9 12l3 3M14 4c4 1 6 3 7 7-3 3-7 4-10 3-1.5-2.5-1.5-7 3-10zM9 12c-1.5 0-3 .5-4 2", gauge: "M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM12 12l4-4M5 19a8 8 0 1 1 14 0", card: "M3 6h18v12H3zM3 10h18", grid: "M4 4h7v7H4zM13 4h7v7h-7zM13 13h7v7h-7zM4 13h7v7H4z", cpu: "M6 6h12v12H6zM9 9h6v6H9zM9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3", signal:"M4 18v-3M9 18v-7M14 18v-11M19 18V4", headset:"M4 14v-2a8 8 0 0 1 16 0v2M4 14a2 2 0 0 0 2 2h1v-5H6a2 2 0 0 0-2 2zM20 14a2 2 0 0 1-2 2h-1v-5h1a2 2 0 0 1 2 2zM18 16v1a4 4 0 0 1-4 4h-2", refresh:"M3 12a9 9 0 0 1 15-6.7L21 8M21 3v5h-5M21 12a9 9 0 0 1-15 6.7L3 16M3 21v-5h5", video: "M3 6h12v12H3zM15 10l6-3v10l-6-3", code: "M9 8l-4 4 4 4M15 8l4 4-4 4", phone: "M6 3h12v18H6zM10 18h4", poll: "M5 21V9M12 21V3M19 21v-7", scissors: "M6 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM8.5 8.5 20 18M8.5 15.5 20 6M12 12l2 1.5", zap: "M11 2 4 13h6l-1 9 8-12h-6l1-8z", clock: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 7v5l3 2", star: "M12 3l2.6 5.6 6.1.7-4.5 4.2 1.2 6-5.4-3-5.4 3 1.2-6L3.3 9.3l6.1-.7z", heart: "M12 20s-7-4.5-9.5-9C1 8 2.5 4.5 6 4.5c2 0 3.2 1.2 4 2.3.8-1.1 2-2.3 4-2.3 3.5 0 5 3.5 3.5 6.5C19 15.5 12 20 12 20z", trophy: "M7 4h10v4a5 5 0 0 1-10 0V4zM7 6H4v1a3 3 0 0 0 3 3M17 6h3v1a3 3 0 0 1-3 3M9 16h6M8 21h8M12 18v3", calendar: "M4 6h16v15H4zM4 10h16M8 3v4M16 3v4", flag: "M5 21V4M5 4h11l-2 4 2 4H5", book: "M5 4h11a3 3 0 0 1 3 3v13a2 2 0 0 0-2-2H5zM5 4v14", plus: "M12 5v14M5 12h14", target: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM12 11a1 1 0 1 0 0 2 1 1 0 0 0 0-2z", message: "M21 12a8 8 0 0 1-11.5 7.2L3 21l1.8-6.5A8 8 0 1 1 21 12z", mic: "M12 3a3 3 0 0 1 3 3v6a3 3 0 0 1-6 0V6a3 3 0 0 1 3-3zM5 11a7 7 0 0 0 14 0M12 18v3", }; function Icon({ name, size = 22, stroke = 1.8, fill = false, style }) { const d = I[name] || I.spark; return ( ); } /* ---------------- Logo (KVB.GG wordmark) ---------------- */ function Logo({ size = 30, onClick }) { // `size` = rendered height in px; width follows the 131:37 aspect ratio. const h = Math.round(size * 0.84); return (
); } /* ---------------- Reveal ---------------- Passthrough wrapper: content is always rendered fully visible. We deliberately avoid opacity/transform entrance animations because the preview/runtime can freeze CSS animations at their first keyframe, which would strand content at opacity:0. Layout-only wrapper. */ function Reveal({ children, delay = 0, as: Tag = "div", style, className = "" }) { return {children}; } /* ---------------- Top nav (floating glass) ---------------- */ // ---- ONE nav for the whole public site ------------------------------------- // Targets: "#id" = a section on the landing page, ">route" = a real page. // Both TopNav (landing) and PageHeader (sub-pages) render THIS list, so the nav never // changes identity as you move around. const SITE_LINKS = [ ["Features", "#features"], ["Pricing", "#pricing"], ["AI Chatbot", ">ai-chatbot"], ["KCIP Fix", ">kcip"], ["FAQ", "#faq"], ["Support", ">discord"], ]; // Shared click handler. A section link works from ANY page: if we're not on the landing // page we route there first, then scroll once React has painted it. function siteNavGo(target, route, onNav, closeMenu) { return (e) => { if (e && e.preventDefault) e.preventDefault(); if (closeMenu) closeMenu(); if (target.charAt(0) === ">") { onNav(target.slice(1)); return; } const id = target.slice(1); const scroll = () => document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" }); if (route === "landing") { scroll(); return; } onNav("landing"); // onNav jumps to the top; wait for the landing tree to mount before scrolling. setTimeout(scroll, 140); }; } function siteNavActive(target, route) { return target.charAt(0) === ">" && target.slice(1) === route; } function TopNav({ onNav, onLogin, route }) { const [scrolled, setScrolled] = useState(false); const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { const fn = () => setScrolled(window.scrollY > 24); window.addEventListener("scroll", fn, { passive: true }); fn(); return () => window.removeEventListener("scroll", fn); }, []); const links = SITE_LINKS; const go = (target) => siteNavGo(target, route, onNav, () => setMenuOpen(false)); return (
onNav("landing")} />
{/* Mobile dropdown */}
{links.map(([label, target]) => ( " ? "#" : target} onClick={go(target)} style={{ padding: "12px 14px", borderRadius: 12, fontSize: 15, fontWeight: 500, color: "var(--ink)" }}>{label} ))}

); } /* ---------------- Footer ---------------- */ const FOOTER_ROUTES = { "Features": { id: "features" }, "Performance": { id: "perf" }, "Pricing": { id: "pricing" }, "Dashboard": { route: "dashboard" }, "AI Chatbot": { route: "ai-chatbot" }, "About": { route: "about" }, "Support": { route: "discord" }, "Status": { route: "status" }, "Changelog": { route: "changelog" }, "Privacy Policy": { route: "privacy" }, "Refund Policy": { route: "refund" }, "Terms of Service": { route: "terms" }, }; // The footer badge sits on EVERY page, so a hardcoded "All systems operational" is the site's // most-seen claim. It reads the same feed as the status page (30s server-side cache, so this is // cheap) and says nothing at all until it has an answer — a neutral label beats a confident lie. function FooterStatusChip({ onNav }) { const [s, setS] = React.useState(null); React.useEffect(() => { let alive = true; fetch("/app/api/status.php", { cache: "no-store" }) .then(r => (r.ok ? r.json() : null)) .then(j => { if (alive && j && j.services) setS(j); }) .catch(() => { /* leave the neutral label */ }); return () => { alive = false; }; }, []); const label = !s ? "System status" : s.all_ok ? "All systems operational" : `${s.operational} of ${s.total} systems operational`; const tone = !s || s.all_ok ? null : ((s.services || []).some(x => x.status === "outage") ? "oklch(0.65 0.20 25)" : "oklch(0.82 0.15 75)"); return ( ); } function Footer({ onNav }) { const cols = [ ["Product", ["Features", "AI Chatbot", "Pricing", "Dashboard"]], ["Company", ["About", "Support", "Status", "Changelog"]], ["Legal", ["Privacy Policy", "Refund Policy", "Terms of Service"]], ]; const handle = (x) => (e) => { e.preventDefault(); const t = FOOTER_ROUTES[x]; if (!t) return; if (t.route) onNav(t.route); else if (t.id) { onNav("landing"); setTimeout(() => document.getElementById(t.id)?.scrollIntoView({ behavior: "smooth", block: "start" }), 60); } }; return ( ); } /* ---------------- Stat counter ---------------- */ function Stat({ value, suffix = "", label, decimals = 0 }) { const ref = useRef(null); const [n, setN] = useState(0); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver(([e]) => { if (!e.isIntersecting) return; io.disconnect(); const dur = 1400, t0 = performance.now(); const tick = (t) => { const p = Math.min(1, (t - t0) / dur); const e2 = 1 - Math.pow(1 - p, 3); setN(value * e2); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }, { threshold: 0.4 }); io.observe(el); return () => io.disconnect(); }, [value]); const disp = decimals ? n.toFixed(decimals) : Math.round(n).toLocaleString(); return (
{disp}{suffix}
{label}
); } Object.assign(window, { Icon, Logo, Reveal, TopNav, Footer, Stat, SITE_LINKS, siteNavGo, siteNavActive });