// Reusable icons (lucide-style stroke icons) const Icon = ({name, size=20, stroke=1.7, ...rest}) => { const s = size; const sw = stroke; const props = { width:s, height:s, viewBox:"0 0 24 24", fill:"none", stroke:"currentColor", strokeWidth:sw, strokeLinecap:"round", strokeLinejoin:"round", ...rest }; switch(name){ case "telegram": return ; case "arrow-right": return ; case "arrow-down": return ; case "check": return ; case "x": return ; case "zoom-in": return ; case "plus": return ; case "play": return ; case "shuffle": return ; case "sparkles": return ; case "file-x": return ; case "compass": return ; case "camera": return ; case "shapes": return ; case "users": return ; case "shopping-bag": return ; case "bag": return ; case "diamond": return ; case "book-open": return ; case "book": return ; case "palette": return ; case "instagram": return ( ); default: return null; } }; // data.jsx кладёт данные на window; отдельный Babel-файл не видит const из data.jsx — читаем явно. const { PERSONAS, PROBLEMS, USE_CASES, MODULES, STEPS, FOR_WHO, TESTIMONIALS, FAQ, TG_POSTS, FOUNDER_IMG, FOUNDER_HERO_IMG, FOUNDER_VIDEO_DESKTOP, FOUNDER_VIDEO_MOBILE, FOUNDER_VIDEO_POSTER, CONSISTENCY, LIVE_TG_START, LIVE_TG_STUDENTS, HERO_STATS, STATS_ENDPOINT, } = window; // Cross-browser matchMedia listener (Safari 13- doesn't have addEventListener) const mqlSubscribe = (mql, handler) => { if (mql.addEventListener) { mql.addEventListener("change", handler); return () => mql.removeEventListener("change", handler); } mql.addListener(handler); return () => mql.removeListener(handler); }; // Persona card (procedural visual) const PersonaBg = ({hue, seed=0}) => { // Deterministic offsets from id/hue const a = (hue + seed*40) % 360; const b = (hue + 60 + seed*30) % 360; return (
); }; /** Count-up on first viewport entry (GPU-friendly). */ const CountUp = ({ to, duration = 1400, decimals = 0, className = "", suffix = "" }) => { const ref = React.useRef(null); const [val, setVal] = React.useState(0); React.useEffect(() => { const el = ref.current; if (!el) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const run = () => { const start = performance.now(); const from = 0; const tick = (t) => { const p = Math.min(1, (t - start) / duration); const eased = 1 - Math.pow(1 - p, 3); const cur = from + (to - from) * eased; setVal(decimals ? Number(cur.toFixed(decimals)) : Math.round(cur)); if (p < 1) requestAnimationFrame(tick); }; if (reduce) { setVal(to); return; } requestAnimationFrame(tick); }; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { run(); io.disconnect(); } }); }, { threshold: 0.2 }); io.observe(el); return () => io.disconnect(); }, [to, duration, decimals]); return ( {val}{suffix} ); }; const LiveCounter = ({ start = 0 }) => { const STAGE_1_DELAY = 20000; const STAGE_2_DELAY = 30000; const TOAST_DURATION = 1800; /* было 1200 */ const [display, setDisplay] = React.useState(start); const [showToast, setShowToast] = React.useState(false); const realRef = React.useRef(null); const counterRef = React.useRef(null); const isVisibleRef = React.useRef(false); const stageRef = React.useRef(0); const elapsedRef = React.useRef(0); const lastTickRef = React.useRef(null); const tickIntervalRef = React.useRef(null); const toastTimerRef = React.useRef(null); const animationPlayedForRef = React.useRef(null); const showToastAnimation = () => { if (toastTimerRef.current) clearTimeout(toastTimerRef.current); setShowToast(false); requestAnimationFrame(() => { setShowToast(true); toastTimerRef.current = setTimeout(() => setShowToast(false), TOAST_DURATION); }); }; const tick = React.useCallback(() => { if (!isVisibleRef.current) { lastTickRef.current = null; return; } const now = Date.now(); if (lastTickRef.current === null) { lastTickRef.current = now; return; } const delta = now - lastTickRef.current; lastTickRef.current = now; elapsedRef.current += delta; const real = realRef.current; if (!real) return; const stage = stageRef.current; if (stage === 0 && elapsedRef.current >= STAGE_1_DELAY) { stageRef.current = 1; elapsedRef.current = 0; setDisplay(real - 1); showToastAnimation(); } else if (stage === 1 && elapsedRef.current >= STAGE_2_DELAY) { stageRef.current = 2; elapsedRef.current = 0; setDisplay(real); showToastAnimation(); if (tickIntervalRef.current) { clearInterval(tickIntervalRef.current); tickIntervalRef.current = null; } } }, []); const startAnimation = React.useCallback(() => { const real = realRef.current; if (!real || real < 3) return; if (animationPlayedForRef.current === real) return; animationPlayedForRef.current = real; stageRef.current = 0; elapsedRef.current = 0; lastTickRef.current = isVisibleRef.current ? Date.now() : null; setDisplay(real - 2); if (tickIntervalRef.current) clearInterval(tickIntervalRef.current); tickIntervalRef.current = setInterval(tick, 1000); }, [tick]); const handleRealUpdate = React.useCallback((newReal) => { const oldReal = realRef.current; realRef.current = newReal; if (oldReal === null) { if (isVisibleRef.current) startAnimation(); return; } if (newReal === oldReal) return; if (newReal < oldReal) { setDisplay(newReal); animationPlayedForRef.current = newReal; stageRef.current = 2; if (tickIntervalRef.current) { clearInterval(tickIntervalRef.current); tickIntervalRef.current = null; } return; } animationPlayedForRef.current = null; if (isVisibleRef.current) startAnimation(); }, [startAnimation]); React.useEffect(() => { if (typeof STATS_ENDPOINT === "undefined" || !STATS_ENDPOINT) return; let cancelled = false; let pollTimer = null; const fetchStats = async () => { try { const resp = await fetch(STATS_ENDPOINT, { method: "GET" }); if (!resp.ok) throw new Error(`Stats API ${resp.status}`); const data = await resp.json(); if (cancelled) return; if (typeof data.subscribers === "number" && data.subscribers > 0) { handleRealUpdate(data.subscribers); } } catch (err) { console.warn("[LiveCounter] fetch failed:", err.message); } }; fetchStats(); pollTimer = setInterval(fetchStats, 120000); return () => { cancelled = true; if (pollTimer) clearInterval(pollTimer); if (tickIntervalRef.current) clearInterval(tickIntervalRef.current); if (toastTimerRef.current) clearTimeout(toastTimerRef.current); }; }, [handleRealUpdate]); React.useEffect(() => { const node = counterRef.current; if (!node) return; const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { isVisibleRef.current = true; lastTickRef.current = null; if (realRef.current !== null && animationPlayedForRef.current !== realRef.current) { startAnimation(); } } else { isVisibleRef.current = false; lastTickRef.current = null; } }); }, { threshold: 0.3 } ); observer.observe(node); return () => observer.disconnect(); }, [startAnimation]); return (
{display.toLocaleString("ru-RU")} {showToast && ( )}
); }; const useTilt = () => { const ref = React.useRef(null); const onMove = React.useCallback((e) => { const el = ref.current; if (!el || (window.matchMedia && !window.matchMedia("(hover: hover)").matches)) return; const r = el.getBoundingClientRect(); const px = (e.clientX - r.left) / r.width; const py = (e.clientY - r.top) / r.height; const rx = (py - 0.5) * -10; const ry = (px - 0.5) * 12; el.style.setProperty("--mx", `${px * 100}%`); el.style.setProperty("--my", `${py * 100}%`); const inner = el.querySelector(".persona-inner"); if (inner) inner.style.transform = `perspective(820px) rotateX(${rx}deg) rotateY(${ry}deg) translateZ(0)`; }, []); const onLeave = React.useCallback(() => { const el = ref.current; if (!el) return; const inner = el.querySelector(".persona-inner"); if (inner) inner.style.transform = "perspective(820px) rotateX(0deg) rotateY(0deg)"; }, []); return { ref, onMove, onLeave }; }; /** Hero particle field — inspired by Linear-style ambient depth (CSS/canvas, not Three.js). */ const ParticlesCanvas = ({ enabled }) => { const canvasRef = React.useRef(null); React.useEffect(() => { if (!enabled) return; const canvas = canvasRef.current; if (!canvas) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (reduce) return; const ctx = canvas.getContext("2d"); let raf; let mx = 0, my = 0, tx = 0, ty = 0; const onMm = (e) => { const sec = canvas.closest(".hero-wrap"); if (!sec) return; const b = sec.getBoundingClientRect(); mx = (e.clientX - b.left) / b.width - 0.5; my = (e.clientY - b.top) / b.height - 0.5; }; const pts = []; const resize = () => { const sec = canvas.closest(".hero-wrap"); if (!sec) return; const dpr = Math.min(2, window.devicePixelRatio || 1); const w = sec.clientWidth, h = sec.clientHeight; canvas.width = w * dpr; canvas.height = h * dpr; canvas.style.width = w + "px"; canvas.style.height = h + "px"; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); pts.length = 0; const n = Math.min(55, Math.floor((w * h) / 9000)); for (let i = 0; i < n; i++) { pts.push({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 0.15, vy: (Math.random() - 0.5) * 0.15 }); } }; resize(); window.addEventListener("resize", resize); window.addEventListener("mousemove", onMm, { passive: true }); let t0 = performance.now(); const loop = (t) => { const sec = canvas.closest(".hero-wrap"); if (!sec) return; const w = sec.clientWidth, h = sec.clientHeight; tx += (mx - tx) * 0.06; ty += (my - ty) * 0.06; const driftX = tx * 40, driftY = ty * 36; ctx.clearRect(0, 0, w, h); pts.forEach((p) => { p.x += p.vx + driftX * 0.002; p.y += p.vy + driftY * 0.002; if (p.x < 0 || p.x > w) p.vx *= -1; if (p.y < 0 || p.y > h) p.vy *= -1; }); const dt = Math.min(40, t - t0); t0 = t; ctx.strokeStyle = "rgba(167,139,250,0.12)"; ctx.lineWidth = 1; for (let i = 0; i < pts.length; i++) { for (let j = i + 1; j < pts.length; j++) { const dx = pts[i].x - pts[j].x, dy = pts[i].y - pts[j].y; const d = Math.hypot(dx, dy); if (d < 72) { ctx.globalAlpha = (1 - d / 72) * 0.35; ctx.beginPath(); ctx.moveTo(pts[i].x, pts[i].y); ctx.lineTo(pts[j].x, pts[j].y); ctx.stroke(); } } } ctx.globalAlpha = 0.45; ctx.fillStyle = "rgba(196,181,253,0.35)"; pts.forEach((p) => { ctx.beginPath(); ctx.arc(p.x, p.y, 1.1, 0, Math.PI * 2); ctx.fill(); }); ctx.globalAlpha = 1; raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); window.removeEventListener("mousemove", onMm); }; }, [enabled]); if (!enabled) return null; return