// Projects, Testimonials, FAQ, CTA, Footer const { useState: useS3, useEffect: useE3 } = React; function ProjectVisual({ variant, accent }) { // Abstract placeholder visuals per project const visuals = { dashboard: ( ), system: ( {Array.from({ length: 6 }).map((_, i) => ( Array.from({ length: 8 }).map((_, j) => ( )) ))} ), launch: ( launch ), workflow: ( ), studio: ( studio portfolio · 2024 ) }; return visuals[variant] || visuals.dashboard; } function ProjectModal({ project, variant, onClose, t }) { // Keep onClose in a ref so the scroll-lock effect only reacts to `project` // (otherwise every parent re-render creates a new onClose fn and retriggers // this effect, which can leave body.overflow stuck at "hidden"). const onCloseRef = React.useRef(onClose); React.useEffect(() => { onCloseRef.current = onClose; }, [onClose]); useE3(() => { if (!project) return; function onKey(e) { if (e.key === 'Escape') onCloseRef.current(); } document.body.style.overflow = 'hidden'; document.addEventListener('keydown', onKey); return () => { document.body.style.overflow = ''; document.removeEventListener('keydown', onKey); }; }, [project]); // Extra safety net: if this component ever unmounts mid-open, restore scroll. useE3(() => () => { document.body.style.overflow = ''; }, []); if (!project) return null; const description = project.description || t.projects.soon_desc; const hasImage = Boolean(project.image); const hasTech = Array.isArray(project.tech) && project.tech.length > 0; const hasUrl = Boolean(project.url); return (
e.stopPropagation()}>
{hasImage ? {project.name} : }
{project.tag} {project.year}

{project.name}

{description}

{hasTech && (
{t.projects.tech_label}
{project.tech.map((tag) => ( {tag} ))}
)} {hasUrl && ( {t.projects.live_cta} )}
); } function Projects({ t }) { const variants = ['dashboard', 'system', 'launch', 'workflow', 'studio']; const layouts = ['project-large', 'project-small', 'project-full', 'project-half', 'project-half']; const [activeIdx, setActiveIdx] = useS3(null); // Hover parallax useE3(() => { const cards = document.querySelectorAll('.project'); function onMove(e) { const rect = e.currentTarget.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width - 0.5; const y = (e.clientY - rect.top) / rect.height - 0.5; const vis = e.currentTarget.querySelector('.project-visual'); if (vis) vis.style.transform = `translate(${x * 20}px, ${y * 20}px) scale(1.05)`; } function onLeave(e) { const vis = e.currentTarget.querySelector('.project-visual'); if (vis) vis.style.transform = ''; } cards.forEach(c => { c.addEventListener('mousemove', onMove); c.addEventListener('mouseleave', onLeave); }); return () => cards.forEach(c => { c.removeEventListener('mousemove', onMove); c.removeEventListener('mouseleave', onLeave); }); }, []); const activeProject = activeIdx !== null ? t.projects.items[activeIdx] : null; const activeVariant = activeIdx !== null ? variants[activeIdx] : null; return (
{t.projects.items.map((p, i) => ( { e.preventDefault(); setActiveIdx(i); }} className={`project reveal ${layouts[i]}`} style={{ '--d': `${i * 60}ms` }} >
{p.image ? {p.name} : }
{p.tag} {p.year}

{p.name}
{t.projects.soon}

))}
setActiveIdx(null)} t={t} />
); } function Testimonials({ t }) { return (
{t.testimonials.items.map((item, i) => (
{item.quote}
{item.initials}
{item.name}
{item.role}
))}
); } function FAQ({ t }) { const [open, setOpen] = useS3(0); return (
{t.faq.items.map((item, i) => (
{item.a}
))}
); } function CTA({ t }) { const canvasRef = React.useRef(null); useE3(() => { const c = canvasRef.current; if (!c) return; const ctx = c.getContext('2d'); let W, H; const dpr = Math.min(window.devicePixelRatio || 1, 2); function resize() { const rect = c.getBoundingClientRect(); W = rect.width; H = rect.height; c.width = Math.max(1, Math.round(W * dpr)); c.height = Math.max(1, Math.round(H * dpr)); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } resize(); const ro = new ResizeObserver(resize); ro.observe(c); window.addEventListener('resize', resize); let t0 = performance.now(); let raf; // Orbs pushed FAR off-screen (|x|, |y| ~ 0.4–1.5) so only the smooth // tails of the gradient are visible → no hot spots, no visible edges. const orbs = [ { x: -0.40, y: 0.20, r: 1.6, hue: 262, speed: 0.00022, phase: 0.0 }, { x: 1.40, y: 0.45, r: 1.5, hue: 272, speed: 0.00026, phase: 2.3 }, { x: 0.55, y: 1.55, r: 1.7, hue: 258, speed: 0.00020, phase: 4.1 }, { x: 0.35, y: -0.45, r: 1.4, hue: 268, speed: 0.00024, phase: 1.2 }, { x: 0.90, y: -0.30, r: 1.3, hue: 275, speed: 0.00028, phase: 3.5 }, ]; function draw() { const dark = document.documentElement.getAttribute('data-theme') === 'dark'; ctx.fillStyle = dark ? '#0A0A0C' : '#FAFAFA'; ctx.fillRect(0, 0, W, H); const tt = (performance.now() - t0); for (const o of orbs) { const px = (o.x + Math.sin(tt * o.speed + o.phase) * 0.04) * W; const py = (o.y + Math.cos(tt * o.speed * 1.2 + o.phase) * 0.035) * H; const r = o.r * Math.max(W, H); const g = ctx.createRadialGradient(px, py, 0, px, py, r); const alpha = dark ? 0.32 : 0.16; // Smoother 5-stop falloff → orbs blend into a uniform mesh g.addColorStop(0.00, `hsla(${o.hue}, 78%, 72%, ${alpha})`); g.addColorStop(0.25, `hsla(${o.hue}, 78%, 72%, ${alpha * 0.75})`); g.addColorStop(0.50, `hsla(${o.hue}, 78%, 72%, ${alpha * 0.42})`); g.addColorStop(0.75, `hsla(${o.hue}, 78%, 72%, ${alpha * 0.16})`); g.addColorStop(1.00, `hsla(${o.hue}, 78%, 72%, 0)`); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); } raf = requestAnimationFrame(draw); } draw(); return () => { cancelAnimationFrame(raf); ro.disconnect(); window.removeEventListener('resize', resize); }; }, []); return (
[ {t.cta.num} ] · {t.cta.label}

{t.cta.title1}
{t.cta.title2}
{t.cta.title3}

{t.cta.subtitle}

{t.cta.email} {t.cta.whatsapp_label} · {t.cta.whatsapp_display}
); } function Footer({ t }) { return ( ); } Object.assign(window, { Projects, Testimonials, FAQ, CTA, Footer });