// 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.eyebrow}

{t.hero.h1_a} {t.hero.h1_b} {t.hero.h1_c}

{t.hero.lead}

{e.preventDefault();document.getElementById('contact')?.scrollIntoView({behavior:'smooth'});}}> {t.hero.cta_primary} {e.preventDefault();document.getElementById('process')?.scrollIntoView({behavior:'smooth'});}}> {t.hero.cta_secondary}
{t.hero.hud_top}
{t.hero.hud_bot}
{t.hero.hud_left}
{t.hero.hud_right}
TecnoWolf
{[1,2,3,4].map(i => (
{t.hero[`stat_${i}_l`]}
))}
); } /* ============================================================ About ============================================================ */ function About({ t }) { const vIcons = [Icon.Shield, Icon.Heart, Icon.Bolt]; return (
TecnoWolf
{t.about.img_corner}
{t.about.eyebrow}

{t.about.h2_a} {t.about.h2_b}

{t.about.p1}

{t.about.p2}

    {[1,2,3].map(i => { const Vi = vIcons[i-1]; return (
  • {t.about[`val_${i}_t`]} {t.about[`val_${i}_d`]}
  • ); })}
); } /* ============================================================ Tech ============================================================ */ function TechCard({ card, Ci, i }) { const ref = useRef(null); const onMove = (e) => { const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); const px = (e.clientX - r.left) / r.width - 0.5; const py = (e.clientY - r.top) / r.height - 0.5; el.style.transform = `translateY(-4px) rotateX(${-py * 8}deg) rotateY(${px * 8}deg)`; }; const reset = () => { if (ref.current) ref.current.style.transform = ''; }; return (

{card.t}

{card.tags.map((tag, ti) => )}
); } function Tech({ t }) { const cardIcons = [Icon.Browser, Icon.Server, Icon.Mobile, Icon.Cloud, Icon.Brain, Icon.Cart, Icon.Doc, Icon.Shield]; const marqueeWords = ['React','Next.js','Node.js','TypeScript','Python','AWS','Docker','Flutter','PostgreSQL','GraphQL','Kubernetes','OpenAI','Stripe','Figma']; const double = [...marqueeWords, ...marqueeWords]; return (
{t.tech.eyebrow}

{t.tech.h2_a} {t.tech.h2_b}

{t.tech.lead}

{t.tech.cards.map((c, i) => { const Ci = cardIcons[i] || Icon.Spark; return ; })}
{double.map((w, i) => ( {w} ))}
); } /* ============================================================ Process ============================================================ */ function Process({ t }) { return (
{t.process.eyebrow}

{t.process.h2_a} {t.process.h2_b}

{t.process.lead}

{t.process.steps.map((s, i) => (
{String(i+1).padStart(2,'0')}

{s.t}

{s.d}

{s.time}
))}
); } /* ============================================================ Contact + CTA Final ============================================================ */ function Contact({ t }) { const [sent, setSent] = useState(false); const cIcons = [Icon.Mail, Icon.Phone, Icon.Pin, Icon.Clock]; const submit = (e) => { e.preventDefault(); setSent(true); setTimeout(()=>setSent(false), 4000); }; return (
{t.contact.eyebrow}

{t.contact.h2_a} {t.contact.h2_b}

{t.contact.lead}

    {t.contact.items.map((it, i) => { const Ci = cIcons[i]; return (
  • {it.l}
    {it.v}
  • ); })}
); } function CTAFinal({ t, waUrl }) { return (
{t.cta.eyebrow}

{t.cta.h2_a} {t.cta.h2_b}

{t.cta.lead}

{e.preventDefault();document.getElementById('contact')?.scrollIntoView({behavior:'smooth'});}}> {t.cta.btn_primary} {t.cta.btn_secondary}
); } /* ============================================================ Footer ============================================================ */ function Footer({ t, updated }) { return ( ); } /* ============================================================ Floating WhatsApp ============================================================ */ function FloatingWA({ t, url }) { return ( {t.wa_tooltip} ); } /* ============================================================ Portfolio — GIF showcase grid ============================================================ */ const PORTFOLIO_PROJECTS = [ { img: 'assets/img/empresas/Infinity.png', label: 'Infinity FQP', desc: 'Plataforma corporativa', url: 'https://www.infinityfqp.com.co/', }, { img: 'assets/img/empresas/Polla.png', label: 'Polla Mundialista', desc: 'E-commerce & apuestas deportivas', url: 'https://pollamundialista.com.co/', }, { img: 'assets/img/empresas/pollab2b.png', label: 'Polla B2B', desc: 'Landing page B2B', url: 'https://www.pollamundialistab2b.com.co/', }, { img: 'assets/img/empresas/innovacion.png', label: 'Innovación Emtelco', desc: 'Landing de innovación', url: 'https://www.innovacionemtelco.digital/', }, ]; function Portfolio({ t }) { return (
{t.portfolio.eyebrow}

{t.portfolio.h2_a} {t.portfolio.h2_b}

{t.portfolio.lead}

{PORTFOLIO_PROJECTS.map((item, i) => ( {item.label}
{item.label} {item.desc}
))}
); } /* ============================================================ Scroll reveal hook ============================================================ */ function useScrollReveal() { useEffect(() => { const obs = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); obs.unobserve(e.target); } }); }, { threshold: 0.12 }); document.querySelectorAll('.reveal').forEach(el => obs.observe(el)); return () => obs.disconnect(); }); } /* ============================================================ Background layer ============================================================ */ function BgLayer() { return ( <>
); } // Export to window so other Babel scripts can find them Object.assign(window, { Nav, Hero, About, Tech, Process, Portfolio, Contact, CTAFinal, Footer, FloatingWA, BgLayer, useScrollReveal });