// casos-v2.jsx — Portfolio real de productos (Tasty destacado + grilla de 5) // Aislado: sólo lo carga "Eurekant Website v2.html". No toca la página actual. const CASOS_CONTENT = { es: { eyebrow: 'CASOS — SOFTWARE EN PRODUCCIÓN', title: 'Lo que construimos', sub: 'Productos reales, en producción, en sectores muy distintos. Del punto de venta al aula, del campo al municipio: mismo estándar de calidad.', sectorsLabel: 'Sectores donde ya operamos', sectors: ['Gastronomía', 'Agro', 'Salud', 'Gobierno', 'Educación', 'RR.HH.'], featuredTag: 'CASO DESTACADO', items: [ { key: 'crm-agro', img: 'assets/product-crm-agro.png', kind: 'laptop', sector: 'Agro', name: 'CRM Agro', desc: 'CRM para el agro: clientes, segmentación por hectáreas y cultivos, y seguimiento de actividad comercial en un solo lugar.' }, { key: 'synergys', img: 'assets/product-synergys.png', kind: 'laptop', sector: 'Salud ocupacional', name: 'Synergys', desc: 'Plataforma de medicina laboral: exámenes, campañas de vacunación, controles y seguimiento de eventos por espacio de trabajo.' }, { key: 'comunidad-cima', img: 'assets/product-comunidad-cima.png', kind: 'phone', sector: 'Odontología', name: 'Comunidad Cima', desc: 'App para centros odontológicos con gamificación: sistema de créditos, turnos por sucursal y gestión del grupo familiar.' }, { key: 'portal-municipal', img: 'assets/product-portal-municipal.png', kind: 'phone', sector: 'Gobierno', name: 'Portal Municipal', desc: 'Portal ciudadano para municipios: noticias, ejes de inversión y participación de la comunidad desde el celular.' }, { key: 'schoolfy', img: 'assets/product-schoolfy.png', kind: 'phone', sector: 'Educación', name: 'Schoolfy 365', desc: 'Comunicación escuela–familia: comunicados con estadísticas de lectura, likes y participación en tiempo real.' } ], cta: { label: 'Tu producto, el próximo caso', text: 'Contanos qué querés construir y armamos el camino.', btn: 'Agendar diagnóstico' }, cardCta: 'Ver detalle' }, en: { eyebrow: 'CASES — SOFTWARE IN PRODUCTION', title: 'What we build', sub: 'Real products, in production, across very different industries. From point of sale to the classroom, from the field to the city hall: same quality standard.', sectorsLabel: 'Industries we already serve', sectors: ['Hospitality', 'Agro', 'Health', 'Government', 'Education', 'HR'], featuredTag: 'FEATURED CASE', items: [ { key: 'crm-agro', img: 'assets/product-crm-agro.png', kind: 'laptop', sector: 'Agro', name: 'CRM Agro', desc: 'CRM for agribusiness: clients, segmentation by hectares and crops, and commercial activity tracking in one place.' }, { key: 'synergys', img: 'assets/product-synergys.png', kind: 'laptop', sector: 'Occupational health', name: 'Synergys', desc: 'Occupational medicine platform: exams, vaccination campaigns, controls and event tracking per workspace.' }, { key: 'comunidad-cima', img: 'assets/product-comunidad-cima.png', kind: 'phone', sector: 'Dental', name: 'Comunidad Cima', desc: 'App for dental centers with gamification: a credits system, branch booking and family-group management.' }, { key: 'portal-municipal', img: 'assets/product-portal-municipal.png', kind: 'phone', sector: 'Government', name: 'Portal Municipal', desc: 'Citizen portal for municipalities: news, investment areas and community participation from the phone.' }, { key: 'schoolfy', img: 'assets/product-schoolfy.png', kind: 'phone', sector: 'Education', name: 'Schoolfy 365', desc: 'School–family communication: announcements with read stats, likes and participation in real time.' } ], cta: { label: 'Your product, the next case', text: 'Tell us what you want to build and we map the path.', btn: 'Book diagnosis' }, cardCta: 'View detail' } }; window.CASOS_CONTENT = CASOS_CONTENT; function injectPortfolioStyles() { if (document.getElementById('portfolio-v2-styles')) return; const s = document.createElement('style'); s.id = 'portfolio-v2-styles'; s.textContent = ` @property --pfAngle { syntax: ''; inherits: false; initial-value: 0deg; } @keyframes pfBorderRotate { to { --pfAngle: 360deg; } } @keyframes pfFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } @keyframes pfCardIn { from { opacity: 0; filter: blur(6px); transform: perspective(1400px) rotateX(-46deg) rotateY(10deg) translateY(120px) scale(.78); } 60% { filter: blur(0px); } to { opacity: 1; filter: blur(0px); transform: perspective(1400px) rotateX(0deg) rotateY(0deg) translateY(0) scale(1); } } @keyframes pfDeviceIn { from { opacity: 0; transform: translateY(40px) scale(.96); } to { opacity: 1; transform: none; } } @keyframes pfCtaGlowPulse { 0%,100% { opacity: .5; transform: scale(1); } 50% { opacity: .95; transform: scale(1.1); } } @keyframes pfCtaArrow { 0%,100% { transform: translateX(0); } 50% { transform: translateX(6px); } } @keyframes pfCtaDash { to { stroke-dashoffset: -300; } } .pf-featured-wrap { position: relative; border-radius: 34px; padding: 1px; background: conic-gradient(from var(--pfAngle), transparent 0%, #2199ea 12%, #63b7f0 22%, #137dc5 32%, transparent 46%, transparent 100%), rgba(120,140,210,0.16); animation: pfBorderRotate 7s linear infinite; } @media (prefers-reduced-motion: reduce) { .pf-featured-wrap { animation: none; } .pf-device { animation: none !important; } } `; document.head.appendChild(s); } // ── Tarjeta de producto con tilt 3D + spotlight + float del device ── function PortfolioCard({ item, index, cardCta }) { const F = window.F; const ref = React.useRef(null); const inView = useInView(ref, { threshold: 0.15 }); const [tilt, setTilt] = React.useState({ x: 0, y: 0 }); const [hover, setHover] = React.useState(false); const [spot, setSpot] = React.useState({ x: 50, y: 30 }); const onMove = (e) => { const r = ref.current.getBoundingClientRect(); const nx = (e.clientX - r.left) / r.width; const ny = (e.clientY - r.top) / r.height; setTilt({ x: (nx - 0.5) * 14, y: (ny - 0.5) * -14 }); setSpot({ x: nx * 100, y: ny * 100 }); }; const isPhone = item.kind === 'phone'; return (
setHover(true)} onMouseLeave={() => { setHover(false); setTilt({ x: 0, y: 0 }); }} onMouseMove={onMove} style={{ position: 'relative', borderRadius: 24, overflow: 'hidden', cursor: 'pointer', background: hover ? F.surfaceHi : F.surface, border: `1px solid ${hover ? F.accent : F.line}`, padding: 28, height: '100%', minHeight: 440, display: 'flex', flexDirection: 'column', transform: `perspective(1100px) rotateY(${tilt.x}deg) rotateX(${tilt.y}deg) translateY(${hover ? -10 : 0}px) scale(${hover ? 1.015 : 1})`, transformStyle: 'preserve-3d', transition: inView ? 'transform .45s cubic-bezier(.2,.7,.2,1), border-color .3s, background .3s, box-shadow .4s' : 'none', opacity: inView ? 1 : 0, animation: inView ? `pfCardIn .9s cubic-bezier(.16,.84,.3,1) ${index * 0.1}s backwards` : 'none', boxShadow: hover ? `0 34px 70px -20px rgba(0,0,0,0.65), 0 0 46px ${F.accentGlow}` : '0 8px 24px -12px rgba(0,0,0,0.5)', willChange: 'transform' }}> {/* top hairline glow */}
{/* spotlight follow */}
{/* media */}
{/* glow behind device */}
{item.name}
{/* meta */}
{item.sector}

{item.name}

{item.desc}

{cardCta} →
); } // ── Tarjeta CTA — combina constelación, borde dibujado, scan holográfico, // label cíclico de sectores y stack de capas fantasma ── function PortfolioCtaCard({ cta, index = 5 }) { const F = window.F; const outerRef = React.useRef(null); const wrapRef = React.useRef(null); const canvasRef = React.useRef(null); const inView = useInView(outerRef, { threshold: 0.15 }); const [hover, setHover] = React.useState(false); const [tilt, setTilt] = React.useState({ x: 0, y: 0 }); const [sectorIdx, setSectorIdx] = React.useState(-1); // -1 = estado final "EL TUYO" const sectors = ['AGRO', 'SALUD', 'GOBIERNO', 'EDUCACIÓN', 'GASTRONOMÍA']; const onMove = (e) => { const r = wrapRef.current.getBoundingClientRect(); const nx = (e.clientX - r.left) / r.width; const ny = (e.clientY - r.top) / r.height; setTilt({ x: (nx - 0.5) * 10, y: (ny - 0.5) * -10, mx: nx * 100, my: ny * 100 }); }; // slot-machine del label al hacer hover React.useEffect(() => { if (!hover) { setSectorIdx(-1); return; } let i = 0; const iv = setInterval(() => { i++; if (i >= sectors.length + 3) { clearInterval(iv); setSectorIdx(-1); return; } setSectorIdx(i % sectors.length); }, 110); return () => clearInterval(iv); }, [hover]); // constelación de nodos en canvas React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); let raf; let w = 0, h = 0, dpr = Math.min(window.devicePixelRatio || 1, 2); const N = 22; const pts = Array.from({ length: N }, () => ({ x: Math.random(), y: Math.random(), vx: (Math.random() - 0.5) * 0.0006, vy: (Math.random() - 0.5) * 0.0006 })); const resize = () => { const r = canvas.getBoundingClientRect(); w = r.width; h = r.height; canvas.width = w * dpr; canvas.height = h * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; resize(); const ro = new ResizeObserver(resize); ro.observe(canvas); const mouse = { x: -1, y: -1 }; const moveHandler = (e) => { const r = canvas.getBoundingClientRect(); mouse.x = (e.clientX - r.left) / r.width; mouse.y = (e.clientY - r.top) / r.height; }; const leaveHandler = () => { mouse.x = -1; mouse.y = -1; }; canvas.parentElement.addEventListener('mousemove', moveHandler); canvas.parentElement.addEventListener('mouseleave', leaveHandler); const draw = () => { ctx.clearRect(0, 0, w, h); pts.forEach(p => { p.x += p.vx; p.y += p.vy; if (p.x < 0 || p.x > 1) p.vx *= -1; if (p.y < 0 || p.y > 1) p.vy *= -1; }); const all = mouse.x >= 0 ? pts.concat([{ x: mouse.x, y: mouse.y, mouse: true }]) : pts; for (let i = 0; i < all.length; i++) { for (let j = i + 1; j < all.length; j++) { const a = all[i], b = all[j]; const dx = (a.x - b.x) * w, dy = (a.y - b.y) * h; const d = Math.sqrt(dx * dx + dy * dy); const max = (a.mouse || b.mouse) ? 170 : 90; if (d < max) { const op = (a.mouse || b.mouse) ? (1 - d / max) * 0.55 : (1 - d / max) * 0.16; ctx.strokeStyle = `rgba(125,208,255,${op})`; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(a.x * w, a.y * h); ctx.lineTo(b.x * w, b.y * h); ctx.stroke(); } } } pts.forEach(p => { ctx.fillStyle = 'rgba(125,208,255,0.6)'; ctx.beginPath(); ctx.arc(p.x * w, p.y * h, 1.6, 0, Math.PI * 2); ctx.fill(); }); raf = requestAnimationFrame(draw); }; draw(); return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.parentElement.removeEventListener('mousemove', moveHandler); canvas.parentElement.removeEventListener('mouseleave', leaveHandler); }; }, []); return (
{/* capas fantasma apiladas detrás */}
setHover(true)} onMouseLeave={() => { setHover(false); setTilt({ x: 0, y: 0 }); }} onMouseMove={onMove} style={{ position: 'relative', borderRadius: 25, overflow: 'hidden', minHeight: 436, height: '100%', padding: 40, display: 'flex', flexDirection: 'column', justifyContent: 'center', cursor: 'pointer', background: 'linear-gradient(155deg, rgba(33,153,234,0.2), rgba(19,125,197,0.05) 55%, rgba(19,125,197,0.1))', transform: `perspective(1100px) rotateY(${tilt.x || 0}deg) rotateX(${tilt.y || 0}deg) translateY(${hover ? -10 : 0}px)`, transition: 'transform .4s cubic-bezier(.2,.7,.2,1)', boxShadow: hover ? `0 40px 80px -24px rgba(0,0,0,0.7), 0 0 60px ${F.accentGlow}` : `0 12px 30px -14px rgba(0,0,0,0.5)` }}> {/* borde dibujado (marching ants) */} {/* constelación */} {/* glow orb pulsante */}
{/* spotlight cursor */}
{sectorIdx === -1 ? 'PRÓXIMO CASO: EL TUYO' : sectors[sectorIdx]}

{cta.label}

{cta.text}

); } // ── Sección completa: Tasty destacado + grilla de 5 + CTA ── function FPortfolio({ content }) { const F = window.F; const cc = (window.CASOS_CONTENT && window.CASOS_CONTENT[content.__lang || 'es']) || CASOS_CONTENT.es; const caso = content.caso; const ref = React.useRef(null); const inView = useInView(ref, { threshold: 0.15 }); React.useEffect(() => { injectPortfolioStyles(); }, []); return (
04
{/* header */}
// {cc.eyebrow}

{cc.title}

{cc.sub}

{/* sectors chips */}
{cc.sectorsLabel} · {cc.sectors.map((sec, i) => ( {sec} ))}
{/* FEATURED — Tasty Control con imagen real */}
{/* scan line */}
{cc.featuredTag}

{caso.title}

{caso.desc}

{caso.tags.map((t, i) => ( {t} ))}
{caso.metrics.map((m, i) => (
{m.k}
{m.v}
))}
{/* real image */}
Tasty Control
● Semifinalista Naves 2024
{/* GRID de 5 + CTA */}
{cc.items.map((it, i) => ( ))}
); } Object.assign(window, { FPortfolio, PortfolioCard, PortfolioCtaCard, CASOS_CONTENT });