/* global React */ const { useState, useEffect, useRef } = React; // ============================================ // Logo // ============================================ function Logo({ size = "md" }) { const sizeClass = size === "xl" ? "logo logo-xl" : "logo"; return ( FL i NKS ); } // ============================================ // Cursor // ============================================ function Cursor() { const dotRef = useRef(null); const ringRef = useRef(null); useEffect(() => { let dotX = window.innerWidth / 2, dotY = window.innerHeight / 2; let ringX = dotX, ringY = dotY; let mx = dotX, my = dotY; const onMove = (e) => { mx = e.clientX; my = e.clientY; }; window.addEventListener('mousemove', onMove); let raf; const tick = () => { dotX += (mx - dotX) * 0.5; dotY += (my - dotY) * 0.5; ringX += (mx - ringX) * 0.15; ringY += (my - ringY) * 0.15; if (dotRef.current) dotRef.current.style.transform = `translate(${dotX}px, ${dotY}px) translate(-50%, -50%)`; if (ringRef.current) ringRef.current.style.transform = `translate(${ringX}px, ${ringY}px) translate(-50%, -50%)`; raf = requestAnimationFrame(tick); }; tick(); const onOver = (e) => { const isInteractive = e.target.closest('a, button, .pillar, .case, .stack-item, .chip, .faq-q, input, textarea'); if (dotRef.current) dotRef.current.classList.toggle('hover', !!isInteractive); if (ringRef.current) ringRef.current.classList.toggle('hover', !!isInteractive); }; document.addEventListener('mouseover', onOver); return () => { window.removeEventListener('mousemove', onMove); document.removeEventListener('mouseover', onOver); cancelAnimationFrame(raf); }; }, []); return ( <>
); } // ============================================ // Nav // ============================================ function Nav({ page, setPage, lang, setLang, t }) { const links = [ { id: 'home', n: '01', label: t.nav.home }, { id: 'services', n: '02', label: t.nav.services }, { id: 'about', n: '03', label: t.nav.about }, { id: 'contact', n: '04', label: t.nav.contact } ]; return ( ); } // ============================================ // Footer // ============================================ function Footer({ t, setPage }) { const [email, setEmail] = useState(''); const [subscribed, setSubscribed] = useState(false); return ( ); } // ============================================ // Reveal helper // ============================================ function Reveal({ children, delay = 0, className = "", as: Tag = "div" }) { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setTimeout(() => setShown(true), delay); io.disconnect(); } }, { threshold: 0.15 }); io.observe(el); return () => io.disconnect(); }, [delay]); return {children}; } // ============================================ // TypeReveal — char-by-char // ============================================ function TypeReveal({ text, className = "", as: Tag = "span", delay = 0 }) { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setTimeout(() => setShown(true), delay); io.disconnect(); } }, { threshold: 0.2 }); io.observe(el); return () => io.disconnect(); }, [delay]); const chars = String(text).split(''); return ( {chars.map((c, i) => ( {c === ' ' ? '\u00A0' : c} ))} ); } // ============================================ // Hero particles canvas // ============================================ function HeroParticles() { const ref = useRef(null); useEffect(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext('2d'); let w = canvas.width = canvas.offsetWidth * devicePixelRatio; let h = canvas.height = canvas.offsetHeight * devicePixelRatio; const onResize = () => { w = canvas.width = canvas.offsetWidth * devicePixelRatio; h = canvas.height = canvas.offsetHeight * devicePixelRatio; }; window.addEventListener('resize', onResize); const N = 60; const nodes = Array.from({length: N}, () => ({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3, r: Math.random() * 1.6 + 0.4 })); let raf; const draw = () => { ctx.clearRect(0, 0, w, h); // edges for (let i = 0; i < N; i++) { for (let j = i + 1; j < N; j++) { const dx = nodes[i].x - nodes[j].x; const dy = nodes[i].y - nodes[j].y; const d = Math.hypot(dx, dy); const max = 180 * devicePixelRatio; if (d < max) { const a = (1 - d / max) * 0.35; ctx.strokeStyle = `oklch(0.72 0.24 295 / ${a})`; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(nodes[i].x, nodes[i].y); ctx.lineTo(nodes[j].x, nodes[j].y); ctx.stroke(); } } } // dots nodes.forEach(n => { n.x += n.vx; n.y += n.vy; if (n.x < 0 || n.x > w) n.vx *= -1; if (n.y < 0 || n.y > h) n.vy *= -1; ctx.fillStyle = 'oklch(0.82 0.16 220 / 0.7)'; ctx.beginPath(); ctx.arc(n.x, n.y, n.r * devicePixelRatio, 0, Math.PI * 2); ctx.fill(); }); raf = requestAnimationFrame(draw); }; draw(); return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', onResize); }; }, []); return ; } // ============================================ // Marquee // ============================================ function Marquee({ items }) { const doubled = [...items, ...items, ...items]; return (
{doubled.map((item, i) => ( {item} ))}
); } // Export Object.assign(window, { Logo, Cursor, Nav, Footer, Reveal, TypeReveal, HeroParticles, Marquee });