// components.jsx — TecnoWolf landing components
const { useState, useEffect, useRef } = React;
/* ============================================================
Nav
============================================================ */
function Nav({ t, lang, setLang, theme, setTheme }) {
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const [active, setActive] = useState('top');
const [progress, setProgress] = useState(0);
useEffect(() => {
const on = () => {
const y = window.scrollY;
setScrolled(y > 24);
const max = document.documentElement.scrollHeight - window.innerHeight;
setProgress(max > 0 ? y / max : 0);
};
on();
window.addEventListener('scroll', on, { passive: true });
return () => window.removeEventListener('scroll', on);
}, []);
useEffect(() => {
const ids = ['about','tech','process','contact'];
const obs = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) setActive(e.target.id);
});
}, { rootMargin: '-40% 0px -55% 0px', threshold: 0 });
ids.forEach(id => { const el = document.getElementById(id); if (el) obs.observe(el); });
return () => obs.disconnect();
}, []);
const links = [
{ id: 'about', label: t.nav.about },
{ id: 'tech', label: t.nav.tech },
{ id: 'process', label: t.nav.process },
{ id: 'contact', label: t.nav.contact },
];
const go = (e, id) => {
e.preventDefault();
setOpen(false);
const el = document.getElementById(id);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
return (
);
}
/* ============================================================
CountUp — animates a number into view
============================================================ */
function CountUp({ value }) {
const ref = useRef(null);
const [text, setText] = useState(value);
useEffect(() => {
// Parse a "+50" / "8 yrs" / "24/7" pattern. We animate only if a single
// integer-like number is present; otherwise just render as-is.
const m = String(value).match(/^(\D*)(\d+)(.*)$/);
if (!m) { setText(value); return; }
const prefix = m[1];
const num = parseInt(m[2], 10);
const suffix = m[3];
let started = false;
const obs = new IntersectionObserver((entries) => {
if (started) return;
if (entries[0].isIntersecting) {
started = true;
const dur = 1400;
const t0 = performance.now();
const step = (t) => {
const k = Math.min(1, (t - t0) / dur);
const eased = 1 - Math.pow(1 - k, 3);
const cur = Math.round(num * eased);
setText(prefix + cur + suffix);
if (k < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
}, { threshold: 0.4 });
if (ref.current) obs.observe(ref.current);
return () => obs.disconnect();
}, [value]);
return {text};
}
/* ============================================================
Hero
============================================================ */
function Hero({ t }) {
const visualRef = useRef(null);
const ringsRef = useRef(null);
const frameRef = useRef(null);
const cornersRef = useRef(null);
// Mouse parallax — translates HUD layers based on pointer position
useEffect(() => {
const wrap = visualRef.current;
if (!wrap) return;
let raf = 0;
let tx = 0, ty = 0, cx = 0, cy = 0;
const onMove = (e) => {
const rect = wrap.getBoundingClientRect();
const px = (e.clientX - rect.left - rect.width / 2) / rect.width;
const py = (e.clientY - rect.top - rect.height / 2) / rect.height;
tx = Math.max(-1, Math.min(1, px * 2));
ty = Math.max(-1, Math.min(1, py * 2));
};
const onLeave = () => { tx = 0; ty = 0; };
const loop = () => {
cx += (tx - cx) * 0.08;
cy += (ty - cy) * 0.08;
if (ringsRef.current)
ringsRef.current.style.transform =
`translate3d(${cx * 14}px, ${cy * 14}px, 0) rotateY(${cx * 6}deg) rotateX(${-cy * 6}deg)`;
if (frameRef.current)
frameRef.current.style.transform = `translate3d(${cx * 6}px, ${cy * 6}px, 0)`;
if (cornersRef.current)
cornersRef.current.style.transform = `translate3d(${cx * 8}px, ${cy * 8}px, 0)`;
raf = requestAnimationFrame(loop);
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseleave', onLeave);
raf = requestAnimationFrame(loop);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseleave', onLeave);
};
}, []);
return (
{t.hero.lead} {t.about.p1} {t.about.p2}
{t.hero.h1_a} {t.hero.h1_b} {t.hero.h1_c}
{t.about.h2_a} {t.about.h2_b}
{[1,2,3].map(i => {
const Vi = vIcons[i-1];
return (
{t.tech.lead}
{t.process.lead}
{s.d}
{t.contact.lead}
{t.cta.lead}
{t.portfolio.lead}