feat(all-deployment)
This commit is contained in:
1 parent
e261394133
commit
4f0020bc10
39 files changed
+13151
-482
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Landing",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "dev"],
|
||||||
|
"port": 5173,
|
||||||
|
"autoPort": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+28
-2
@@ -1,10 +1,36 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>sitio-rynext</title>
|
|
||||||
|
<!-- SEO principal -->
|
||||||
|
<title>Rynext Technologies | Software Empresarial 100% Mexicano</title>
|
||||||
|
<meta name="description" content="Rynext Technologies diseña y construye software empresarial 100% mexicano. Sistemas a medida, módulos y licencias para empresas en Cancún y toda México." />
|
||||||
|
<meta name="keywords" content="Rynext, Rynext Technologies, software empresarial México, sistemas a medida, desarrollo de software Cancún, software mexicano, ERP México" />
|
||||||
|
<meta name="author" content="Rynext Technologies" />
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
<link rel="canonical" href="https://rynext.com.mx/" />
|
||||||
|
|
||||||
|
<!-- Open Graph (redes sociales) -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="https://rynext.com.mx/" />
|
||||||
|
<meta property="og:title" content="Rynext Technologies | Software Empresarial 100% Mexicano" />
|
||||||
|
<meta property="og:description" content="Diseñamos y construimos software empresarial 100% mexicano. Sistemas a medida, módulos y licencias para empresas en México." />
|
||||||
|
<meta property="og:locale" content="es_MX" />
|
||||||
|
<meta property="og:site_name" content="Rynext Technologies" />
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="Rynext Technologies | Software Empresarial 100% Mexicano" />
|
||||||
|
<meta name="twitter:description" content="Diseñamos y construimos software empresarial 100% mexicano. Sistemas a medida, módulos y licencias para empresas en México." />
|
||||||
|
|
||||||
|
<!-- Geo -->
|
||||||
|
<meta name="geo.region" content="MX-ROO" />
|
||||||
|
<meta name="geo.placename" content="Cancún, Quintana Roo, México" />
|
||||||
|
|
||||||
|
<meta name="theme-color" content="#e50914" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
Generated
+4663
-6
File diff suppressed because it is too large.
Load diff
+4
-1
@@ -15,6 +15,7 @@
|
|||||||
"lucide-react": "^1.28.0",
|
"lucide-react": "^1.28.0",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
|
"react-router": "^8.3.0",
|
||||||
"tailwindcss": "^4.3.3"
|
"tailwindcss": "^4.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -29,6 +30,8 @@
|
|||||||
"globals": "^17.7.0",
|
"globals": "^17.7.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.65.0",
|
"typescript-eslint": "^8.65.0",
|
||||||
"vite": "^8.2.0"
|
"vite": "^8.2.0",
|
||||||
|
"vite-plugin-pwa": "^1.3.0",
|
||||||
|
"workbox-window": "^7.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5985
-1
File diff suppressed because it is too large.
Load diff
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 449 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" zoomAndPan="magnify" viewBox="0 0 1500 1499.999933" height="2000" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><filter x="0%" y="0%" width="100%" height="100%" id="8d756b956c"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" color-interpolation-filters="sRGB"/></filter><filter x="0%" y="0%" width="100%" height="100%" id="62b43949b4"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.2126 0.7152 0.0722 0 0" color-interpolation-filters="sRGB"/></filter><clipPath id="3ac8341e25"><path d="M 0 648.902344 L 1500 648.902344 L 1500 851.402344 L 0 851.402344 Z M 0 648.902344 " clip-rule="nonzero"/></clipPath><mask id="60fb1d792f"><g filter="url(#8d756b956c)"><g filter="url(#62b43949b4)" transform="matrix(1.282226, 0, 0, 1.282471, -234.697857, -90.088596)"><image x="0" y="0" width="1536" xlink:href="data:image/png;base64,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 truncated
|
||||||
|
After Width: | Height: | Size: 90 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" zoomAndPan="magnify" viewBox="0 0 1500 1499.999933" height="2000" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><filter x="0%" y="0%" width="100%" height="100%" id="c829cef1fa"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" color-interpolation-filters="sRGB"/></filter><filter x="0%" y="0%" width="100%" height="100%" id="22a7985332"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.2126 0.7152 0.0722 0 0" color-interpolation-filters="sRGB"/></filter><clipPath id="63848e8728"><path d="M 0 694.328125 L 1500 694.328125 L 1500 865.328125 L 0 865.328125 Z M 0 694.328125 " clip-rule="nonzero"/></clipPath><mask id="17db18eb1d"><g filter="url(#c829cef1fa)"><g filter="url(#22a7985332)" transform="matrix(1.20166, 0, 0, 1.201904, -172.876855, -40.289897)"><image x="0" y="0" width="1536" xlink:href="data:image/png;base64,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 truncated
|
||||||
|
After Width: | Height: | Size: 245 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="18" y="18" width="124" height="124" rx="24" fill="#EAF6FF"/>
|
|
||||||
<path d="M54 46H106C112.627 46 118 51.3726 118 58V68C118 74.6274 112.627 80 106 80H54C47.3726 80 42 74.6274 42 68V58C42 51.3726 47.3726 46 54 46Z" fill="#0077C2" opacity="0.16"/>
|
|
||||||
<path d="M54 46H106C112.627 46 118 51.3726 118 58V68C118 74.6274 112.627 80 106 80H54C47.3726 80 42 74.6274 42 68V58C42 51.3726 47.3726 46 54 46Z" stroke="#0077C2" stroke-width="6"/>
|
|
||||||
<path d="M42 58C42 64.6274 47.3726 70 54 70H106C112.627 70 118 64.6274 118 58" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
|
|
||||||
<path d="M42 68C42 74.6274 47.3726 80 54 80H106C112.627 80 118 74.6274 118 68" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
|
|
||||||
<path d="M42 78C42 84.6274 47.3726 90 54 90H106C112.627 90 118 84.6274 118 78" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
|
|
||||||
<path d="M42 88C42 94.6274 47.3726 100 54 100H106C112.627 100 118 94.6274 118 88" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
|
|
||||||
<path d="M54 108H106" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
|
|
||||||
<path d="M54 118H92" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
|
|
||||||
<text x="80" y="112" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="20" font-weight="700" fill="#0077C2">SQL</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://rynext.com.mx/sitemap.xml
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://rynext.com.mx/</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://rynext.com.mx/sobre-nosotros</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://rynext.com.mx/productos</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://rynext.com.mx/blog</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
+27
-35
@@ -1,48 +1,40 @@
|
|||||||
import { useState } from 'react';
|
import { Routes, Route } from 'react-router';
|
||||||
import { Navbar } from './components/Navbar';
|
import { Navbar } from './components/Navbar';
|
||||||
import { Hero } from './components/Hero';
|
|
||||||
import { Services } from './components/Services';
|
|
||||||
import { Process } from './components/Process';
|
|
||||||
import { PlatformAdvantage } from './components/PlatformAdvantage';
|
|
||||||
import { CtaBanner } from './components/CtaBanner';
|
|
||||||
import { Footer } from './components/Footer';
|
import { Footer } from './components/Footer';
|
||||||
import { ContactModal } from './components/ContactModal';
|
import { FloatingButtons } from './components/FloatingButtons';
|
||||||
|
import { CookieBanner } from './components/CookieBanner';
|
||||||
|
import { Home } from './pages/Home';
|
||||||
|
import { SobreNosotros } from './pages/SobreNosotros';
|
||||||
|
import { Productos } from './pages/Productos';
|
||||||
|
import { Blog } from './pages/Blog';
|
||||||
|
|
||||||
|
const CONTACT_EMAIL = '[email protected]';
|
||||||
|
const openContact = () => {
|
||||||
|
window.location.href = `mailto:${CONTACT_EMAIL}?subject=Hola%2C%20tengo%20un%20proyecto`;
|
||||||
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [isNavyBg, setIsNavyBg] = useState<boolean>(false);
|
const isNavyBg = false;
|
||||||
const [isContactOpen, setIsContactOpen] = useState<boolean>(false);
|
const setIsNavyBg = (_: boolean | ((prev: boolean) => boolean)) => {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-screen overflow-x-hidden transition-colors duration-500 font-sans ${
|
<div
|
||||||
isNavyBg ? 'bg-[#071b49] text-white' : 'bg-[#f4f9ff] text-brand-black'
|
className="min-h-screen overflow-x-hidden font-sans transition-colors duration-500"
|
||||||
}`}>
|
style={{ background: '#f9f9f7' }}
|
||||||
<Navbar
|
>
|
||||||
isNavyBg={isNavyBg}
|
<Navbar onOpenContact={openContact} />
|
||||||
setIsNavyBg={setIsNavyBg}
|
|
||||||
onOpenContact={() => setIsContactOpen(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<main>
|
<Routes>
|
||||||
<Hero
|
<Route path="/" element={<Home isNavyBg={isNavyBg} onOpenContact={openContact} />} />
|
||||||
isNavyBg={isNavyBg}
|
<Route path="/sobre-nosotros" element={<SobreNosotros />} />
|
||||||
onOpenContact={() => setIsContactOpen(true)}
|
<Route path="/productos" element={<Productos />} />
|
||||||
/>
|
<Route path="/blog" element={<Blog />} />
|
||||||
<Services isNavyBg={isNavyBg} />
|
</Routes>
|
||||||
<Process isNavyBg={isNavyBg} />
|
|
||||||
<PlatformAdvantage isNavyBg={isNavyBg} />
|
|
||||||
<CtaBanner
|
|
||||||
isNavyBg={isNavyBg}
|
|
||||||
onOpenContact={() => setIsContactOpen(true)}
|
|
||||||
/>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<Footer isNavyBg={isNavyBg} />
|
<Footer isNavyBg={isNavyBg} />
|
||||||
|
|
||||||
<ContactModal
|
<FloatingButtons isNavyBg={isNavyBg} setIsNavyBg={setIsNavyBg} />
|
||||||
isOpen={isContactOpen}
|
<CookieBanner isNavyBg={isNavyBg} />
|
||||||
onClose={() => setIsContactOpen(false)}
|
|
||||||
isNavyBg={isNavyBg}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
|
const layers = [
|
||||||
|
{
|
||||||
|
id: 'presentation',
|
||||||
|
num: '01',
|
||||||
|
title: 'PRESENTACIÓN',
|
||||||
|
subtitle: 'La interfaz donde opera el usuario',
|
||||||
|
description:
|
||||||
|
'Portales de administración, apps web, paneles de monitoreo y puntos de venta. Construidos para adopción real, no para demos.',
|
||||||
|
tech: ['React', 'Next.js', 'TypeScript', 'Tailwind', 'Mobile Web'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'logic',
|
||||||
|
num: '02',
|
||||||
|
title: 'LÓGICA DE NEGOCIO',
|
||||||
|
subtitle: 'Las reglas que gobiernan la operación',
|
||||||
|
description:
|
||||||
|
'Validaciones, cálculos, flujos de aprobación y orquestación de procesos. El código que representa cómo funciona tu negocio.',
|
||||||
|
tech: ['Node.js', 'Python', 'REST APIs', 'GraphQL', 'Microservicios'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'data',
|
||||||
|
num: '03',
|
||||||
|
title: 'DATOS',
|
||||||
|
subtitle: 'Persistencia y acceso a la información',
|
||||||
|
description:
|
||||||
|
'Modelo de datos diseñado para tu operación. Bases relacionales, NoSQL, caché y búsqueda de texto. Una sola fuente de verdad.',
|
||||||
|
tech: ['PostgreSQL', 'MongoDB', 'Redis', 'Elasticsearch', 'Prisma'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'infra',
|
||||||
|
num: '04',
|
||||||
|
title: 'INFRAESTRUCTURA',
|
||||||
|
subtitle: 'La base que soporta el sistema',
|
||||||
|
description:
|
||||||
|
'Cloud, contenedores, pipelines de CI/CD y observabilidad. Diseñada para que el sistema esté siempre disponible y sea fácil de operar.',
|
||||||
|
tech: ['AWS', 'Docker', 'GitHub Actions', 'Terraform', 'Monitoring'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'integrations',
|
||||||
|
num: '05',
|
||||||
|
title: 'INTEGRACIONES',
|
||||||
|
subtitle: 'Conexión con el ecosistema externo',
|
||||||
|
description:
|
||||||
|
'ERPs, sistemas de pago, servicios de mensajería, proveedores y APIs externas. El sistema habla con todo lo que el negocio necesita.',
|
||||||
|
tech: ['Stripe', 'SAP / Odoo', 'OAuth 2.0', 'Webhooks', 'SMS / Email'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Anatomy = ({ isNavyBg }: ThemeProps) => {
|
||||||
|
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
|
||||||
|
const text = isNavyBg ? '#eef0f4' : '#0a0a0a';
|
||||||
|
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866';
|
||||||
|
const border = isNavyBg ? 'rgba(255,255,255,0.08)' : '#dddbd5';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="anatomy"
|
||||||
|
aria-labelledby="anatomy-heading"
|
||||||
|
style={{ background: bg, color: text, borderBottom: `1px solid ${border}` }}
|
||||||
|
className="px-8 py-28 md:px-14 transition-colors duration-500"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="mx-auto max-w-7xl"
|
||||||
|
initial={{ opacity: 0, y: 28 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false, amount: 0.1 }}
|
||||||
|
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-16">
|
||||||
|
<h2
|
||||||
|
id="anatomy-heading"
|
||||||
|
className="text-headline"
|
||||||
|
style={{ maxWidth: '24ch' }}
|
||||||
|
>
|
||||||
|
Todo sistema tiene<br /> las mismas capas.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Layers — always visible */}
|
||||||
|
<div style={{ border: `1px solid ${border}`, borderRadius: 4, overflow: 'hidden' }}>
|
||||||
|
{layers.map((layer, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={layer.id}
|
||||||
|
initial={{ opacity: 0, y: 14 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.4, delay: i * 0.07 }}
|
||||||
|
style={{
|
||||||
|
borderBottom: i < layers.length - 1 ? `1px solid ${border}` : 'none',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '120px 1fr',
|
||||||
|
gap: '0 32px',
|
||||||
|
alignItems: 'start',
|
||||||
|
padding: '32px 32px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Number */}
|
||||||
|
<span
|
||||||
|
className="font-display font-black"
|
||||||
|
style={{ fontSize: 64, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
|
||||||
|
>
|
||||||
|
{layer.num}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Title + description */}
|
||||||
|
<div style={{ paddingTop: 8 }}>
|
||||||
|
<p
|
||||||
|
className="font-display font-black mb-1"
|
||||||
|
style={{ fontSize: 13, letterSpacing: '0.04em', color: text }}
|
||||||
|
>
|
||||||
|
{layer.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs mb-3" style={{ color: sub }}>
|
||||||
|
{layer.subtitle}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: sub, maxWidth: '60ch' }}>
|
||||||
|
{layer.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
children: ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
href?: string;
|
||||||
|
variant?: 'primary' | 'secondary';
|
||||||
|
className?: string;
|
||||||
|
isNavyBg?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button = ({ children, onClick, href, variant = 'primary', className = '', isNavyBg = false }: ButtonProps) => {
|
||||||
|
const base = 'inline-flex items-center gap-3 px-8 py-4 text-base font-bold transition hover:scale-[1.02] cursor-pointer';
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
primary: 'bg-brand-red text-white shadow-lg shadow-red-500/20 hover:bg-[#b60014]',
|
||||||
|
secondary: isNavyBg
|
||||||
|
? 'border border-white/15 bg-white/5 text-white hover:bg-white/10'
|
||||||
|
: 'border border-brand-navy/15 text-brand-navy hover:border-brand-blue hover:text-brand-blue',
|
||||||
|
};
|
||||||
|
|
||||||
|
const cls = `${base} ${styles[variant]} ${className}`;
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
return <a href={href} className={cls}>{children}</a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <button onClick={onClick} className={cls}>{children}</button>;
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@ import { X, Mail } from 'lucide-react';
|
|||||||
import type { ModalProps } from '../types';
|
import type { ModalProps } from '../types';
|
||||||
|
|
||||||
const supportEmail = '[email protected]';
|
const supportEmail = '[email protected]';
|
||||||
const mailToHref = `mailto:${supportEmail}?subject=${encodeURIComponent('Consulta RyNext')}&body=${encodeURIComponent('Hola RyNext,\n\nQuiero agendar una conversación sobre un proyecto.\n\nGracias.')}`;
|
const mailToHref = `mailto:${supportEmail}?subject=${encodeURIComponent('Consulta Rynext')}&body=${encodeURIComponent('Hola Rynext,\n\nQuiero agendar una conversación sobre un proyecto.\n\nGracias.')}`;
|
||||||
|
|
||||||
export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
|
export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
@@ -28,7 +28,7 @@ export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
|
|||||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-brand-blue/20 text-brand-blue">
|
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-brand-blue/20 text-brand-blue">
|
||||||
<Mail size={24} />
|
<Mail size={24} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mb-2 text-2xl font-bold">Ponte en Contacto</h3>
|
<h3 className="mb-2 text-2xl font-bold">Hablemos de tu proyecto</h3>
|
||||||
<p className={`mb-6 text-sm ${isNavyBg ? 'text-slate-300' : 'text-gray-600'}`}>
|
<p className={`mb-6 text-sm ${isNavyBg ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||||
Puedes escribirnos directamente a nuestro correo oficial de atención y tu cliente de correo se abrirá automáticamente.
|
Puedes escribirnos directamente a nuestro correo oficial de atención y tu cliente de correo se abrirá automáticamente.
|
||||||
</p>
|
</p>
|
||||||
@@ -40,7 +40,7 @@ export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
|
|||||||
<a
|
<a
|
||||||
href={mailToHref}
|
href={mailToHref}
|
||||||
className="block font-mono text-base font-medium underline decoration-brand-blue/50 underline-offset-4"
|
className="block font-mono text-base font-medium underline decoration-brand-blue/50 underline-offset-4"
|
||||||
aria-label="Enviar correo a soporte de RyNext"
|
aria-label="Enviar correo a soporte de Rynext"
|
||||||
>
|
>
|
||||||
{supportEmail}
|
{supportEmail}
|
||||||
</a>
|
</a>
|
||||||
@@ -55,9 +55,13 @@ export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
|
|||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="cursor-pointer rounded-xl border border-brand-navy/15 bg-white px-4 py-3 text-sm font-bold text-brand-navy transition hover:border-brand-blue hover:text-brand-blue"
|
className={`cursor-pointer rounded-xl border px-4 py-3 text-sm font-bold transition hover:border-brand-blue hover:text-brand-blue ${
|
||||||
|
isNavyBg
|
||||||
|
? 'border-white/20 bg-white/5 text-white'
|
||||||
|
: 'border-brand-navy/15 bg-white text-brand-navy'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
Entendido
|
Cerrar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
|
export const CookieBanner = ({ isNavyBg }: ThemeProps) => {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!localStorage.getItem('rynext_cookies')) {
|
||||||
|
const t = setTimeout(() => setVisible(true), 1200);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const accept = () => {
|
||||||
|
localStorage.setItem('rynext_cookies', 'accepted');
|
||||||
|
setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
const bg = isNavyBg ? 'rgba(0,16,64,0.97)' : 'rgba(10,10,10,0.97)';
|
||||||
|
const border = isNavyBg ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.08)';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Aviso de cookies"
|
||||||
|
className="fixed bottom-6 left-1/2 z-50 w-[calc(100%-3rem)] max-w-xl -translate-x-1/2 px-6 py-4 shadow-2xl"
|
||||||
|
style={{
|
||||||
|
background: bg,
|
||||||
|
border: `1px solid ${border}`,
|
||||||
|
borderRadius: 6,
|
||||||
|
backdropFilter: 'blur(16px)',
|
||||||
|
WebkitBackdropFilter: 'blur(16px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.6)', maxWidth: '42ch' }}>
|
||||||
|
Usamos cookies para mejorar tu experiencia en el sitio.
|
||||||
|
Al continuar, aceptas su uso.{' '}
|
||||||
|
<a href="/privacidad" className="underline transition hover:text-white" style={{ color: 'rgba(255,255,255,0.45)' }}>
|
||||||
|
Política de privacidad
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={accept}
|
||||||
|
className="shrink-0 px-5 py-2 text-sm font-bold text-white transition hover:opacity-90"
|
||||||
|
style={{ background: '#e50914', borderRadius: 3, border: 'none', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Aceptar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,27 +1,63 @@
|
|||||||
|
import { motion } from 'framer-motion';
|
||||||
import { ArrowRight } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { Button } from './Button';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
export const CtaBanner = ({ onOpenContact }: ThemeProps & { onOpenContact: () => void }) => {
|
interface CtaBannerProps extends ThemeProps {
|
||||||
return (
|
onOpenContact: () => void;
|
||||||
<section className="relative overflow-hidden bg-[linear-gradient(135deg,#001f5b,#0d2d78_50%,#005596)] px-6 py-24 text-white md:px-12">
|
}
|
||||||
<div className="absolute -left-10 top-0 h-60 w-60 rounded-full bg-brand-red/20 blur-[120px]" />
|
|
||||||
<div className="absolute bottom-0 right-0 h-72 w-72 rounded-full bg-white/10 blur-[120px]" />
|
|
||||||
<div className="absolute inset-x-0 top-0 h-16 rounded-b-[100%] bg-white/5" />
|
|
||||||
|
|
||||||
<div className="relative z-10 mx-auto max-w-5xl space-y-8 text-center">
|
export const CtaBanner = ({ onOpenContact, isNavyBg }: CtaBannerProps) => {
|
||||||
<h2 className="text-3xl font-black leading-tight tracking-tight font-display sm:text-6xl">
|
const bg = isNavyBg ? '#001f5b' : '#f9f9f7';
|
||||||
¿Listo para dejar de construir con ruido y empezar con dirección?
|
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
|
||||||
</h2>
|
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.45)';
|
||||||
<p className="mx-auto max-w-2xl text-lg font-normal text-sky-100 sm:text-xl">
|
|
||||||
Agenda una conversación técnica con RyNext para revisar tu proyecto, tu stack y la ruta de evolución ideal.
|
return (
|
||||||
</p>
|
<section
|
||||||
<button
|
className="relative overflow-hidden px-8 py-32 md:px-14 transition-colors duration-500"
|
||||||
onClick={onOpenContact}
|
style={{ background: bg, color: text, borderBottom: `1px solid ${isNavyBg ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)'}` }}
|
||||||
className="inline-flex cursor-pointer items-center gap-3 rounded-xl bg-white px-9 py-4 text-base font-bold text-brand-navy shadow-xl transition hover:scale-[1.03] hover:bg-gray-100"
|
aria-labelledby="cta-heading"
|
||||||
>
|
>
|
||||||
<span>Agendar</span>
|
{/* Subtle grid */}
|
||||||
<ArrowRight size={18} />
|
<div
|
||||||
</button>
|
className="pointer-events-none absolute inset-0 bg-grid-lines text-white/[0.03]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="relative mx-auto max-w-4xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.7 }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
id="cta-heading"
|
||||||
|
className="text-display mb-8"
|
||||||
|
style={{ color: text }}
|
||||||
|
>
|
||||||
|
Construimos el sistema.<br /> Tú operas el negocio.
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="mx-auto mb-12 max-w-md text-base leading-relaxed" style={{ color: sub }}>
|
||||||
|
Agenda una sesión técnica con nuestro equipo. Analizamos tu operación, identificamos los cuellos de botella y proponemos una ruta concreta de implementación.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-5">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={onOpenContact}
|
||||||
|
className={isNavyBg ? 'shadow-xl shadow-red-900/30' : ''}
|
||||||
|
>
|
||||||
|
Hablemos de tu proyecto <ArrowRight size={16} />
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" href="mailto:[email protected]" isNavyBg={isNavyBg}>
|
||||||
|
contacto@rynext.com.mx
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { ArrowUp } from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import type { ThemeProps } from '../types';
|
||||||
|
import { type Dispatch, type SetStateAction, useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const WHATSAPP_NUMBER = '521XXXXXXXXXX'; // ← reemplaza con el número real
|
||||||
|
const WHATSAPP_MSG = encodeURIComponent('¿En qué podemos ayudarte?');
|
||||||
|
|
||||||
|
interface FloatingButtonsProps extends ThemeProps {
|
||||||
|
setIsNavyBg: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FloatingButtons = ({ isNavyBg, setIsNavyBg: _setIsNavyBg }: FloatingButtonsProps) => {
|
||||||
|
const [showTop, setShowTop] = useState(false);
|
||||||
|
const [showBubble, setShowBubble] = useState(false);
|
||||||
|
const toggleBg = isNavyBg ? 'rgba(0,31,91,0.95)' : 'rgba(255,255,255,0.95)';
|
||||||
|
const toggleBorder = isNavyBg ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.12)';
|
||||||
|
const toggleColor = isNavyBg ? '#fbbf24' : '#001f5b';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onScroll = () => setShowTop(window.scrollY > 400);
|
||||||
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
const t = setTimeout(() => setShowBubble(true), 3000);
|
||||||
|
return () => { window.removeEventListener('scroll', onScroll); clearTimeout(t); };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Scroll to top — izquierda */}
|
||||||
|
{showTop && (
|
||||||
|
<button
|
||||||
|
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||||
|
aria-label="Volver arriba"
|
||||||
|
className="fixed bottom-6 left-6 z-50 flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-transform hover:scale-110 active:scale-95"
|
||||||
|
style={{
|
||||||
|
background: toggleBg,
|
||||||
|
border: `1px solid ${toggleBorder}`,
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: toggleColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowUp size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="fixed bottom-6 right-6 z-50 flex flex-col items-center gap-3"
|
||||||
|
aria-label="Acciones rápidas"
|
||||||
|
>
|
||||||
|
{/* WhatsApp + bubble wrapper */}
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<AnimatePresence>
|
||||||
|
{showBubble && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10, scale: 0.92 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 6, scale: 0.95 }}
|
||||||
|
transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 'calc(100% + 10px)',
|
||||||
|
right: 0,
|
||||||
|
filter: 'drop-shadow(0 4px 16px rgba(0,0,0,0.12))',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: '12px 36px 12px 16px',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: '#1a1a2e',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
position: 'relative',
|
||||||
|
}}>
|
||||||
|
¡Hola! ¿En qué podemos ayudarte?
|
||||||
|
<button
|
||||||
|
onClick={() => setShowBubble(false)}
|
||||||
|
aria-label="Cerrar"
|
||||||
|
style={{
|
||||||
|
position: 'absolute', top: 6, right: 8,
|
||||||
|
width: 20, height: 20, cursor: 'pointer', fontSize: 10,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
color: '#888',
|
||||||
|
}}
|
||||||
|
>✕</button>
|
||||||
|
</div>
|
||||||
|
{/* tail apuntando al botón */}
|
||||||
|
<div style={{
|
||||||
|
width: 14, height: 10,
|
||||||
|
background: '#fff',
|
||||||
|
clipPath: 'polygon(0 0, 100% 0, 100% 100%)',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -9,
|
||||||
|
right: 16,
|
||||||
|
}} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* WhatsApp */}
|
||||||
|
<a
|
||||||
|
href={`https://wa.me/${WHATSAPP_NUMBER}?text=${WHATSAPP_MSG}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Contactar por WhatsApp"
|
||||||
|
className="flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-transform hover:scale-110 active:scale-95"
|
||||||
|
style={{ background: '#25D366' }}
|
||||||
|
>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="white" aria-hidden="true">
|
||||||
|
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z"/>
|
||||||
|
<path d="M12 0C5.373 0 0 5.373 0 12c0 2.122.554 4.112 1.523 5.837L.057 23.882a.5.5 0 0 0 .611.61l6.109-1.457A11.945 11.945 0 0 0 12 24c6.627 0 12-5.373 12-12S18.627 0 12 0zm0 21.75a9.721 9.721 0 0 1-4.965-1.362l-.356-.211-3.687.88.897-3.583-.231-.369A9.718 9.718 0 0 1 2.25 12C2.25 6.615 6.615 2.25 12 2.25S21.75 6.615 21.75 12 17.385 21.75 12 21.75z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+178
-66
@@ -1,93 +1,205 @@
|
|||||||
|
import { Mail, MapPin, MessageCircle } from 'lucide-react';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
|
const navLinks = [
|
||||||
|
{ label: 'Sistemas', href: '#machinery' },
|
||||||
|
{ label: 'Anatomía', href: '#anatomy' },
|
||||||
|
{ label: 'Proceso', href: '#proceso' },
|
||||||
|
{ label: 'Principios', href: '#principios' },
|
||||||
|
];
|
||||||
|
|
||||||
export const Footer = ({ isNavyBg }: ThemeProps) => {
|
export const Footer = ({ isNavyBg }: ThemeProps) => {
|
||||||
const isDark = isNavyBg;
|
const bg = isNavyBg ? '#001040' : '#0a0a0a';
|
||||||
|
const sub = 'rgba(255,255,255,0.45)';
|
||||||
|
const border = 'rgba(255,255,255,0.08)';
|
||||||
|
|
||||||
|
const handleNav = (href: string) => {
|
||||||
|
const el = document.querySelector(href);
|
||||||
|
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className={`border-t px-6 py-14 md:px-12 transition-colors duration-500 ${
|
<footer
|
||||||
isDark
|
style={{ background: bg, borderTop: `1px solid ${border}` }}
|
||||||
? 'border-white/10 bg-[linear-gradient(135deg,#001f5b,#0a2b78)] text-white'
|
className="px-8 pt-20 pb-10 md:px-14 transition-colors duration-500"
|
||||||
: 'border-brand-navy/10 bg-white text-brand-black'
|
role="contentinfo"
|
||||||
}`}>
|
>
|
||||||
<div className={`mx-auto grid max-w-7xl gap-8 text-sm md:grid-cols-5 ${isDark ? 'text-white' : 'text-brand-navy'}`}>
|
<div className="mx-auto max-w-7xl">
|
||||||
|
{/* Main grid */}
|
||||||
<div className="md:col-span-2">
|
<div className="grid gap-14 md:grid-cols-[1.8fr_1fr_1.2fr]">
|
||||||
<div className="mb-4 flex items-center gap-3">
|
{/* Brand */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-6 flex items-center gap-3">
|
||||||
<img
|
<img
|
||||||
src={isDark ? '/icons/Logo_blanco.svg' : '/icons/Logo_negro.svg'}
|
src="/icons/Logo_blanco.svg"
|
||||||
alt="RyNext logo"
|
alt="Rynext logo"
|
||||||
className="h-9 w-auto object-contain"
|
className="h-9 w-auto object-contain"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center">
|
<img
|
||||||
<span className={`font-display text-xl font-extrabold tracking-tight ${
|
src="/icons/Text_Logo_Blanco.svg"
|
||||||
isDark ? 'text-white' : 'text-brand-black'
|
alt="Rynext"
|
||||||
}`}>
|
style={{
|
||||||
Ry<span className="text-brand-red">Next</span>
|
width: "130px",
|
||||||
</span>
|
height: "18px",
|
||||||
<span className={`ml-2 text-xs font-semibold ${isDark ? 'text-slate-300' : 'text-brand-navy/75'}`}>
|
objectFit: "cover",
|
||||||
— Software Studio
|
objectPosition: "center 52%",
|
||||||
</span>
|
display: "block",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p
|
||||||
|
className="text-sm leading-relaxed mb-8"
|
||||||
<p className={`max-w-md text-[13px] leading-6 ${isDark ? 'text-slate-200' : 'text-brand-navy/75'}`}>
|
style={{ color: sub, maxWidth: "30ch" }}
|
||||||
Diseñamos productos y plataformas con foco en velocidad, estabilidad y crecimiento real de negocio.
|
>
|
||||||
|
Diseñamos y construimos sistemas empresariales a medida,
|
||||||
|
arquitectura real, sin atajos técnicos.
|
||||||
</p>
|
</p>
|
||||||
|
{/* Social */}
|
||||||
<div className="mt-4 grid gap-2 sm:grid-cols-2">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`rounded-xl border px-3 py-2 ${isDark ? 'border-white/10 bg-white/5' : 'border-brand-navy/10 bg-[#001f5b]/5'}`}>
|
<a
|
||||||
<p className="text-[11px] font-semibold tracking-[0.08em] text-sky-500">Operación</p>
|
href="https://linkedin.com/company/rynext"
|
||||||
<p className={`mt-1 text-[13px] ${isDark ? 'text-slate-200' : 'text-brand-navy/80'}`}>MX • LATAM • remoto</p>
|
target="_blank"
|
||||||
</div>
|
rel="noopener noreferrer"
|
||||||
<div className={`rounded-xl border px-3 py-2 ${isDark ? 'border-white/10 bg-white/5' : 'border-brand-navy/10 bg-[#001f5b]/5'}`}>
|
aria-label="Rynext en LinkedIn"
|
||||||
<p className="text-[11px] font-semibold tracking-[0.08em] text-sky-500">Entrega</p>
|
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
|
||||||
<p className={`mt-1 text-[13px] ${isDark ? 'text-slate-200' : 'text-brand-navy/80'}`}>Sprints • roadmap • support</p>
|
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
|
||||||
</div>
|
onMouseEnter={(e) => {
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.background =
|
||||||
|
"rgba(255,255,255,0.14)";
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.color = "#fff";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.background =
|
||||||
|
"rgba(255,255,255,0.07)";
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.color = sub;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* LinkedIn SVG */}
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
|
||||||
|
<rect x="2" y="9" width="4" height="12" />
|
||||||
|
<circle cx="4" cy="4" r="2" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`https://wa.me/529981234567?text=${encodeURIComponent('¿En qué podemos ayudarte?')}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Rynext en WhatsApp"
|
||||||
|
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
|
||||||
|
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.background =
|
||||||
|
"rgba(255,255,255,0.14)";
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.color = "#fff";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.background =
|
||||||
|
"rgba(255,255,255,0.07)";
|
||||||
|
(e.currentTarget as HTMLAnchorElement).style.color = sub;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MessageCircle size={15} />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Columna Servicios */}
|
{/* Navegación */}
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-3 text-[11px] font-semibold tracking-[0.08em] text-sky-500">Servicios</p>
|
<p
|
||||||
<ul className={`space-y-2 text-[13px] ${isDark ? 'text-slate-200' : 'text-brand-navy/80'}`}>
|
className="text-mono-label mb-6"
|
||||||
<li>Software a medida</li>
|
style={{
|
||||||
<li>Plataformas web</li>
|
color: "rgba(255,255,255,0.25)",
|
||||||
<li>Integraciones y APIs</li>
|
letterSpacing: "0.12em",
|
||||||
<li>Arquitectura cloud</li>
|
}}
|
||||||
|
>
|
||||||
|
Navegación
|
||||||
|
</p>
|
||||||
|
<ul className="flex flex-col gap-3.5">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<li key={link.href}>
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav(link.href)}
|
||||||
|
className="text-sm font-medium transition-colors"
|
||||||
|
style={{
|
||||||
|
color: sub,
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.color = "#fff")}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Columna Contacto */}
|
{/* Contacto */}
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-3 text-[11px] font-semibold tracking-[0.08em] text-sky-500">Contacto</p>
|
<p
|
||||||
<ul className={`space-y-2 text-[13px] ${isDark ? 'text-slate-200' : 'text-brand-navy/80'}`}>
|
className="text-mono-label mb-6"
|
||||||
<li>
|
style={{
|
||||||
<a href="mailto:[email protected]?subject=Consulta%20RyNext" className="underline decoration-current/40 underline-offset-4 transition hover:text-brand-blue">
|
color: "rgba(255,255,255,0.25)",
|
||||||
soporte@rynext.com.mx
|
letterSpacing: "0.12em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Contacto
|
||||||
|
</p>
|
||||||
|
<ul className="flex flex-col gap-4">
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<MapPin
|
||||||
|
size={14}
|
||||||
|
style={{ color: "#e50914", marginTop: 2, flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
<span className="text-sm leading-snug" style={{ color: sub }}>
|
||||||
|
Cancún, Quintana Roo
|
||||||
|
<br />
|
||||||
|
México
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-3">
|
||||||
|
<Mail size={14} style={{ color: "#e50914", flexShrink: 0 }} />
|
||||||
|
<a
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
className="text-sm font-medium transition-colors"
|
||||||
|
style={{ color: sub }}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.color = "#fff")}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
|
||||||
|
>
|
||||||
|
contacto@rynext.com.mx
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="mt-2">
|
||||||
|
<a
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
className="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-bold text-white transition hover:opacity-90"
|
||||||
|
style={{ background: "#e50914", borderRadius: 3 }}
|
||||||
|
>
|
||||||
|
Hablemos de tu proyecto
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>Cancún, Quintana Roo, México</li>
|
|
||||||
<li>Reserva de proyecto en 48h</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Columna Horario */}
|
|
||||||
<div>
|
|
||||||
<p className="mb-3 text-[11px] font-semibold tracking-[0.08em] text-sky-500">Horario</p>
|
|
||||||
<ul className={`space-y-2 text-[13px] ${isDark ? 'text-slate-200' : 'text-brand-navy/80'}`}>
|
|
||||||
<li>Lun–Vie 9:00–18:00</li>
|
|
||||||
<li>Reuniones por agenda</li>
|
|
||||||
<li>Soporte de proyecto</li>
|
|
||||||
<li>Consultoría técnica</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
{/* Bottom bar */}
|
||||||
|
<div
|
||||||
{/* Copyright */}
|
className="mt-16 border-t pt-8 text-center"
|
||||||
<div className={`mx-auto mt-8 flex max-w-7xl flex-col items-center justify-between gap-4 border-t pt-6 text-[13px] sm:flex-row ${isDark ? 'border-white/10' : 'border-brand-navy/10'}`}>
|
style={{ borderColor: border }}
|
||||||
<div className={isDark ? 'text-slate-200' : 'text-brand-navy/80'}>
|
>
|
||||||
© {new Date().getFullYear()} RyNext. Todos los derechos reservados.
|
<p className="text-xs" style={{ color: "rgba(255,255,255,0.25)" }}>
|
||||||
|
© {new Date().getFullYear()} Rynext Technologies S.A.S. | Todos los
|
||||||
|
derechos reservados.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
+92
-91
@@ -1,108 +1,109 @@
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { ArrowRight, CheckCircle2, Shield } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
import type { HeroProps } from '../types';
|
import type { HeroProps } from '../types';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { SystemMap } from './SystemMap';
|
||||||
|
|
||||||
export const Hero = ({ isNavyBg, onOpenContact }: HeroProps) => {
|
const ease = [0.22, 1, 0.36, 1] as const;
|
||||||
|
|
||||||
|
function RevealLine({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) {
|
||||||
return (
|
return (
|
||||||
<section className={`section-wave relative flex min-h-screen items-center overflow-hidden px-6 pb-24 pt-36 md:px-12 transition-colors duration-500 ${
|
<motion.span
|
||||||
isNavyBg ? 'bg-[#081b46] text-white' : 'bg-[#f4f9ff] text-brand-black'
|
className="block"
|
||||||
}`}>
|
initial={{ opacity: 0, y: 18 }}
|
||||||
<div className={`absolute inset-x-0 top-0 h-44 ${
|
|
||||||
isNavyBg
|
|
||||||
? 'bg-[radial-gradient(circle_at_top_left,rgba(0,119,194,0.24),transparent_34%),radial-gradient(circle_at_top_right,rgba(229,9,20,0.16),transparent_30%)]'
|
|
||||||
: 'bg-[radial-gradient(circle_at_top_left,rgba(0,119,194,0.20),transparent_34%),radial-gradient(circle_at_top_right,rgba(229,9,20,0.10),transparent_30%)]'
|
|
||||||
}`} />
|
|
||||||
|
|
||||||
<div className="mx-auto grid w-full max-w-7xl grid-cols-1 items-center gap-12 lg:grid-cols-12 lg:gap-10">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 30 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.7 }}
|
transition={{ duration: 0.75, delay, ease }}
|
||||||
className="space-y-8 lg:col-span-12"
|
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
{children}
|
||||||
{/* Título uniforme de un solo color */}
|
</motion.span>
|
||||||
<h1 className={`text-hero-dacodes max-w-5xl font-display transition-colors duration-500 ${
|
);
|
||||||
isNavyBg ? 'text-white' : 'text-[#001f5b]'
|
}
|
||||||
}`}>
|
|
||||||
Diseñamos software
|
export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
|
||||||
<span className="block">
|
const bg = isNavyBg ? '#001f5b' : '#ffffff';
|
||||||
que mantiene el negocio en marcha.
|
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
|
||||||
</span>
|
const sub = isNavyBg ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.45)';
|
||||||
|
const gridLine = isNavyBg ? 'rgba(255,255,255,0.025)' : 'rgba(0,0,0,0.04)';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="relative flex min-h-screen flex-col overflow-hidden transition-colors duration-500"
|
||||||
|
style={{ background: bg, color: text, borderBottom: isNavyBg ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.1)' }}
|
||||||
|
aria-label="Rynext Technologies — Hero"
|
||||||
|
>
|
||||||
|
{/* Subtle grid bg */}
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-0 bg-grid-lines"
|
||||||
|
style={{ color: gridLine }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="relative mx-auto grid w-full max-w-7xl flex-1 items-center gap-12 px-8 pb-24 pt-36 md:px-14 lg:grid-cols-[1fr_1fr] lg:gap-20">
|
||||||
|
|
||||||
|
{/* Left column */}
|
||||||
|
<div>
|
||||||
|
{/* Headline */}
|
||||||
|
<h1 className="text-display mb-8" style={{ color: text }}>
|
||||||
|
<RevealLine delay={0.08}>Construimos</RevealLine>
|
||||||
|
<RevealLine delay={0.16}>el sistema</RevealLine>
|
||||||
|
<RevealLine delay={0.24}>
|
||||||
|
<span className="text-brand-red">detrás</span> del
|
||||||
|
</RevealLine>
|
||||||
|
<RevealLine delay={0.32}>negocio.</RevealLine>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className={`max-w-2xl text-lg leading-relaxed sm:text-xl ${
|
{/* Description */}
|
||||||
isNavyBg ? 'text-slate-200' : 'text-gray-700'
|
<motion.p
|
||||||
}`}>
|
initial={{ opacity: 0, y: 14 }}
|
||||||
RyNext combina arquitectura, producto y operación para lanzar plataformas que se entiendan bien, corran rápido y puedan crecer sin romperse.
|
animate={{ opacity: 1, y: 0 }}
|
||||||
</p>
|
transition={{ duration: 0.7, delay: 0.5, ease }}
|
||||||
</div>
|
className="mb-10 max-w-sm text-base leading-relaxed"
|
||||||
|
style={{ color: sub }}
|
||||||
|
>
|
||||||
|
Ingeniería de software empresarial diseñada desde la arquitectura.
|
||||||
|
No herramientas genéricas, sistemas construidos para
|
||||||
|
la operación real de tu negocio.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-4 pt-2">
|
{/* CTAs */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.65, ease }}
|
||||||
|
className="flex flex-wrap items-center gap-6"
|
||||||
|
>
|
||||||
|
<Button variant="primary" onClick={onOpenContact}>
|
||||||
|
Hablemos de tu proyecto <ArrowRight size={16} />
|
||||||
|
</Button>
|
||||||
<button
|
<button
|
||||||
onClick={onOpenContact}
|
onClick={() => {
|
||||||
className="group inline-flex items-center gap-3 bg-brand-red px-8 py-4 text-base font-bold text-white shadow-lg shadow-red-500/20 transition hover:scale-[1.02] hover:bg-[#b60014]"
|
document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}}
|
||||||
|
className="group flex items-center gap-2 text-sm font-semibold transition"
|
||||||
|
style={{ color: sub }}
|
||||||
>
|
>
|
||||||
<span>Agendar</span>
|
Explorar la maquinaria
|
||||||
<ArrowRight size={18} className="transition group-hover:translate-x-1" />
|
<ArrowRight
|
||||||
|
size={14}
|
||||||
|
className="transition-transform group-hover:translate-x-0.5"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<a
|
|
||||||
href="#servicios"
|
|
||||||
className={`border px-8 py-4 text-base font-semibold transition hover:scale-[1.02] ${
|
|
||||||
isNavyBg
|
|
||||||
? 'border-white/20 bg-white/5 text-white hover:bg-white/10'
|
|
||||||
: 'border-brand-navy/15 bg-white text-brand-navy hover:border-brand-blue hover:text-brand-blue'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Ver servicios
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 pt-4 lg:grid-cols-3">
|
|
||||||
<div className={`rounded-[18px] border-[1.5px] p-4 ${
|
|
||||||
isNavyBg
|
|
||||||
? 'border-white/15 bg-white/5 shadow-[0_8px_30px_rgba(0,0,0,0.18)]'
|
|
||||||
: 'border-brand-navy/15 bg-white shadow-[0_10px_30px_rgba(0,31,91,0.08)]'
|
|
||||||
}`}>
|
|
||||||
<div className="mb-2 flex items-center gap-2">
|
|
||||||
<CheckCircle2 size={16} className="text-brand-blue" />
|
|
||||||
<span className={`text-sm font-semibold ${isNavyBg ? 'text-white' : 'text-brand-navy'}`}>Time to value</span>
|
|
||||||
</div>
|
|
||||||
<p className={`text-sm ${isNavyBg ? 'text-slate-200' : 'text-gray-700'}`}>
|
|
||||||
Entregas visibles desde las primeras semanas con objetivos claros.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`rounded-[18px] border-[1.5px] p-4 ${
|
|
||||||
isNavyBg
|
|
||||||
? 'border-white/15 bg-white/5 shadow-[0_8px_30px_rgba(0,0,0,0.18)]'
|
|
||||||
: 'border-brand-navy/15 bg-white shadow-[0_10px_30px_rgba(0,31,91,0.08)]'
|
|
||||||
}`}>
|
|
||||||
<div className="mb-2 flex items-center gap-2">
|
|
||||||
<CheckCircle2 size={16} className="text-brand-red" />
|
|
||||||
<span className={`text-sm font-semibold ${isNavyBg ? 'text-white' : 'text-brand-navy'}`}>Sistemas seguros</span>
|
|
||||||
</div>
|
|
||||||
<p className={`text-sm ${isNavyBg ? 'text-slate-200' : 'text-gray-700'}`}>
|
|
||||||
Infraestructura preparada para escalar con confianza y control.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`rounded-[18px] border-[1.5px] p-4 ${
|
|
||||||
isNavyBg
|
|
||||||
? 'border-white/15 bg-white/5 shadow-[0_8px_30px_rgba(0,0,0,0.18)]'
|
|
||||||
: 'border-brand-navy/15 bg-white shadow-[0_10px_30px_rgba(0,31,91,0.08)]'
|
|
||||||
}`}>
|
|
||||||
<div className="mb-2 flex items-center gap-2">
|
|
||||||
<Shield size={16} className="text-brand-blue" />
|
|
||||||
<span className={`text-sm font-semibold ${isNavyBg ? 'text-white' : 'text-brand-navy'}`}>Operación estable</span>
|
|
||||||
</div>
|
|
||||||
<p className={`text-sm ${isNavyBg ? 'text-slate-200' : 'text-gray-700'}`}>
|
|
||||||
Alianzas con dirección técnica, roadmap claro y sostenibilidad real.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Right column — System Map */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 1, delay: 0.4, ease }}
|
||||||
|
className="hidden lg:block"
|
||||||
|
>
|
||||||
|
<SystemMap isNavyBg={isNavyBg} />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import type { ThemeProps } from '../types';
|
||||||
|
import { machineryData } from '../data/machineryData';
|
||||||
|
|
||||||
|
export const Machinery = ({ isNavyBg }: ThemeProps) => {
|
||||||
|
const [hovered, setHovered] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const bg = isNavyBg ? '#001f5b' : '#ffffff';
|
||||||
|
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
|
||||||
|
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866';
|
||||||
|
const cardBg = isNavyBg ? 'rgba(255,255,255,0.06)' : '#fafaf9';
|
||||||
|
const cardBdr = isNavyBg ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.09)';
|
||||||
|
const numColor = isNavyBg ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.04)';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="machinery"
|
||||||
|
aria-labelledby="machinery-heading"
|
||||||
|
style={{ background: bg, color: text, borderBottom: `1px solid ${cardBdr}` }}
|
||||||
|
className="px-8 py-28 md:px-14 transition-colors duration-500"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="mx-auto max-w-7xl"
|
||||||
|
initial={{ opacity: 0, y: 28 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false, amount: 0.1 }}
|
||||||
|
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-16">
|
||||||
|
<h2
|
||||||
|
id="machinery-heading"
|
||||||
|
className="text-headline"
|
||||||
|
style={{ maxWidth: '22ch', color: text }}
|
||||||
|
>
|
||||||
|
Los sistemas que<br className="hidden sm:block" /> mueven la operación.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards grid */}
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{machineryData.map((item, i) => {
|
||||||
|
const isHov = hovered === item.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={item.id}
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.45, delay: i * 0.07, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
onMouseEnter={() => setHovered(item.id)}
|
||||||
|
onMouseLeave={() => setHovered(null)}
|
||||||
|
role="article"
|
||||||
|
aria-label={item.title}
|
||||||
|
style={{
|
||||||
|
background: isHov
|
||||||
|
? isNavyBg ? 'rgba(229,9,20,0.06)' : 'rgba(229,9,20,0.03)'
|
||||||
|
: cardBg,
|
||||||
|
border: `1px solid ${isHov ? 'rgba(229,9,20,0.25)' : cardBdr}`,
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: '28px 28px 32px',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
cursor: 'default',
|
||||||
|
transition: 'background 220ms, border-color 220ms, transform 220ms, box-shadow 220ms',
|
||||||
|
transform: isHov ? 'translateY(-2px)' : 'translateY(0)',
|
||||||
|
boxShadow: isHov
|
||||||
|
? '0 8px 28px rgba(0,0,0,0.10)'
|
||||||
|
: '0 1px 3px rgba(0,0,0,0.04)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Faint background number */}
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: -8,
|
||||||
|
right: 12,
|
||||||
|
fontFamily: 'var(--font-display)',
|
||||||
|
fontSize: 96,
|
||||||
|
fontWeight: 900,
|
||||||
|
lineHeight: 1,
|
||||||
|
color: isHov ? 'rgba(229,9,20,0.07)' : numColor,
|
||||||
|
transition: 'color 220ms',
|
||||||
|
userSelect: 'none',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.num}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Red accent top bar on hover */}
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 2,
|
||||||
|
background: '#e50914',
|
||||||
|
opacity: isHov ? 1 : 0,
|
||||||
|
transition: 'opacity 220ms',
|
||||||
|
borderRadius: '6px 6px 0 0',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<h3
|
||||||
|
className="font-display font-black tracking-tight mb-3"
|
||||||
|
style={{
|
||||||
|
fontSize: 'clamp(1.1rem, 2vw, 1.3rem)',
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
color: isHov ? '#e50914' : text,
|
||||||
|
transition: 'color 220ms',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className="text-sm leading-relaxed"
|
||||||
|
style={{ color: sub, transition: 'color 220ms' }}
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
+123
-54
@@ -1,67 +1,136 @@
|
|||||||
import { Sun, Moon, ArrowRight } from 'lucide-react';
|
import { useState, useEffect } from "react";
|
||||||
import type { NavbarProps } from '../types';
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { Menu, X } from "lucide-react";
|
||||||
|
import { Link, useLocation } from "react-router";
|
||||||
|
import type { NavbarProps } from "../types";
|
||||||
|
|
||||||
|
const pageLinks = [
|
||||||
|
{ label: "Sobre Nosotros", href: "/sobre-nosotros" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
||||||
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onScroll = () => setScrolled(window.scrollY > 80);
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener("scroll", onScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMenuOpen(false);
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
export const Navbar = ({ isNavyBg, setIsNavyBg, onOpenContact }: NavbarProps) => {
|
|
||||||
return (
|
return (
|
||||||
<header className="fixed top-0 left-0 right-0 z-50 w-full">
|
<>
|
||||||
<div className={`w-full border-b backdrop-blur-md transition-all duration-500 ${
|
<motion.header
|
||||||
isNavyBg
|
className="fixed top-0 left-0 right-0 z-50"
|
||||||
? 'border-white/10 bg-[#001f5b]/95 text-white shadow-lg shadow-black/10'
|
initial={{ y: -72, opacity: 0 }}
|
||||||
: 'border-brand-navy/10 bg-white/95 text-brand-black shadow-sm'
|
animate={{ y: 0, opacity: 1 }}
|
||||||
}`}>
|
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
||||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4 md:px-12">
|
role="banner"
|
||||||
|
|
||||||
{/* Identidad / Logo */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<img
|
|
||||||
src={isNavyBg ? '/icons/Logo_blanco.svg' : '/icons/Logo_negro.svg'}
|
|
||||||
alt="RyNext logo"
|
|
||||||
className="h-10 w-auto object-contain"
|
|
||||||
/>
|
|
||||||
<span className={`font-display text-2xl font-extrabold tracking-tight ${
|
|
||||||
isNavyBg ? 'text-white' : 'text-brand-black'
|
|
||||||
}`}>
|
|
||||||
Ry<span className="text-brand-red">Next</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Menú y Acciones (Sin 'Stack') */}
|
|
||||||
<div className="flex items-center gap-4 sm:gap-6">
|
|
||||||
<a href="#servicios" className="hidden text-sm font-semibold transition-colors hover:text-brand-red sm:block">
|
|
||||||
Servicios
|
|
||||||
</a>
|
|
||||||
<a href="#metodo" className="hidden text-sm font-semibold transition-colors hover:text-brand-red sm:block">
|
|
||||||
Método
|
|
||||||
</a>
|
|
||||||
<a href="#conveniencia" className="hidden text-sm font-semibold transition-colors hover:text-brand-red sm:block">
|
|
||||||
Conveniencia
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* Switch de Fondo */}
|
|
||||||
<button
|
|
||||||
onClick={() => setIsNavyBg((prev) => !prev)}
|
|
||||||
className={`flex cursor-pointer items-center gap-2 rounded-lg border px-3 py-2 text-xs font-semibold transition-all ${
|
|
||||||
isNavyBg
|
|
||||||
? 'border-white/20 bg-white/5 text-white hover:bg-white/10'
|
|
||||||
: 'border-brand-navy/10 bg-[#001f5b]/5 text-brand-navy hover:bg-[#001f5b]/10'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{isNavyBg ? <Sun size={15} className="text-amber-400" /> : <Moon size={15} className="text-brand-navy" />}
|
<div
|
||||||
<span className="hidden md:inline">{isNavyBg ? 'Fondo blanco' : 'Fondo azul'}</span>
|
style={{
|
||||||
</button>
|
background: scrolled ? "rgba(255,255,255,0.97)" : "transparent",
|
||||||
|
borderBottom: scrolled ? "1px solid rgba(0,0,0,0.08)" : "1px solid transparent",
|
||||||
|
backdropFilter: scrolled ? "blur(16px)" : "none",
|
||||||
|
WebkitBackdropFilter: scrolled ? "blur(16px)" : "none",
|
||||||
|
transition: "background 200ms, border-color 200ms",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="mx-auto flex max-w-7xl items-center justify-between px-8 py-4 md:px-14">
|
||||||
|
{/* Logo — siempre modo claro */}
|
||||||
|
<Link to="/" aria-label="Rynext Technologies — inicio" className="flex items-center gap-3">
|
||||||
|
<img src="/icons/Logo_negro.svg" alt="Rynext logo" className="h-9 w-auto object-contain" />
|
||||||
|
<img
|
||||||
|
src="/icons/Text_Logo_Negro.svg"
|
||||||
|
alt="Rynext"
|
||||||
|
style={{ width: "130px", height: "18px", objectFit: "cover", objectPosition: "center 52%", display: "block" }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Desktop nav */}
|
||||||
|
<nav className="hidden items-center gap-8 md:flex" aria-label="Navegación principal">
|
||||||
|
{pageLinks.map((link) => {
|
||||||
|
const active = location.pathname === link.href;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={link.href}
|
||||||
|
to={link.href}
|
||||||
|
className="text-sm font-semibold transition-colors"
|
||||||
|
style={{ color: active ? "#0a0a0a" : "#6a6866", textDecoration: "none" }}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.color = "#0a0a0a")}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.color = active ? "#0a0a0a" : "#6a6866")}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Botón CTA */}
|
|
||||||
<button
|
<button
|
||||||
onClick={onOpenContact}
|
onClick={onOpenContact}
|
||||||
className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-brand-red px-5 py-2.5 text-sm font-bold text-white shadow-md shadow-red-500/20 transition hover:scale-[1.02] hover:bg-[#b60014]"
|
className="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-bold text-white transition hover:scale-[1.02] hover:opacity-90"
|
||||||
|
style={{ background: "#e50914", border: "none", cursor: "pointer", borderRadius: 3 }}
|
||||||
>
|
>
|
||||||
<span>Agendar</span>
|
Hablemos
|
||||||
<ArrowRight size={16} />
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMenuOpen((v) => !v)}
|
||||||
|
className="flex items-center justify-center rounded p-2 md:hidden"
|
||||||
|
style={{ color: "#0a0a0a", background: "none", border: "none", cursor: "pointer" }}
|
||||||
|
aria-label={menuOpen ? "Cerrar menú" : "Abrir menú"}
|
||||||
|
aria-expanded={menuOpen}
|
||||||
|
>
|
||||||
|
{menuOpen ? <X size={22} /> : <Menu size={22} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.header>
|
||||||
|
|
||||||
|
{/* Mobile menu */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{menuOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -8 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="fixed inset-x-0 top-[65px] z-40 px-8 py-6 md:hidden"
|
||||||
|
style={{ background: "#ffffff", borderBottom: "1px solid #dddbd5" }}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Menú de navegación"
|
||||||
|
>
|
||||||
|
<nav className="flex flex-col gap-5">
|
||||||
|
{pageLinks.map((link) => (
|
||||||
|
<Link
|
||||||
|
key={link.href}
|
||||||
|
to={link.href}
|
||||||
|
className="text-left text-base font-semibold"
|
||||||
|
style={{ color: location.pathname === link.href ? "#0a0a0a" : "#6a6866", textDecoration: "none" }}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<div className="pt-2">
|
||||||
|
<button
|
||||||
|
onClick={onOpenContact}
|
||||||
|
className="w-full py-3 text-center text-sm font-bold text-white"
|
||||||
|
style={{ background: "#e50914", border: "none", cursor: "pointer", borderRadius: 3 }}
|
||||||
|
>
|
||||||
|
Hablemos
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</header>
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { ArrowRight, Building2, Layers3, ShieldCheck } from 'lucide-react';
|
import { Building2, Layers3, ShieldCheck, X, Check } from 'lucide-react';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
const comparisonRows = [
|
const comparisonRows = [
|
||||||
@@ -10,7 +10,7 @@ const comparisonRows = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
criterion: 'Propiedad y evolución',
|
criterion: 'Propiedad y evolución',
|
||||||
saas: 'Depende del proveedor y de su roadmap.',
|
saas: 'Depende del proveedor y su roadmap.',
|
||||||
ry: 'Roadmap compartido con autonomía técnica real.',
|
ry: 'Roadmap compartido con autonomía técnica real.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -21,7 +21,7 @@ const comparisonRows = [
|
|||||||
{
|
{
|
||||||
criterion: 'Soporte y continuidad',
|
criterion: 'Soporte y continuidad',
|
||||||
saas: 'Respuesta general, soporte tipo ticket.',
|
saas: 'Respuesta general, soporte tipo ticket.',
|
||||||
ry: 'Equipo senior con acompañamiento directo y operación.',
|
ry: 'Equipo senior con acompañamiento directo.',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ const proofCards = [
|
|||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Equipo senior detrás',
|
title: 'Equipo senior',
|
||||||
description: 'Product owner, ingeniería y operación trabajando con criterio empresarial.',
|
description: 'Product owner, ingeniería y operación trabajando con criterio empresarial.',
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
},
|
},
|
||||||
@@ -45,110 +45,110 @@ const proofCards = [
|
|||||||
|
|
||||||
export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
|
export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
|
||||||
return (
|
return (
|
||||||
<section id="conveniencia" className={`section-wave border-t border-current/10 px-6 py-28 md:px-12 transition-colors duration-500 ${
|
<section
|
||||||
isNavyBg ? 'bg-[#071b49] text-white' : 'bg-[#f7fbff] text-brand-black'
|
id="conveniencia"
|
||||||
}`}>
|
className={`section-wave px-6 py-28 md:px-12 transition-colors duration-500 ${
|
||||||
|
isNavyBg ? 'bg-[#001749] text-white' : 'bg-slate-50 text-brand-black'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="mx-auto max-w-7xl">
|
<div className="mx-auto max-w-7xl">
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-14 flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
|
<div className="mb-14 flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
|
||||||
<div className="max-w-3xl">
|
<div className="max-w-3xl">
|
||||||
<span className={`mb-3 block text-xs font-semibold tracking-[0.08em] ${
|
<span className={`mb-3 block text-xs font-semibold uppercase tracking-[0.14em] ${
|
||||||
isNavyBg ? 'text-sky-200' : 'text-brand-blue'
|
isNavyBg ? 'text-sky-400' : 'text-brand-blue'
|
||||||
}`}>
|
}`}>
|
||||||
Conveniencia RyNext
|
Por qué Rynext
|
||||||
</span>
|
</span>
|
||||||
<h2 className="text-section-title">
|
<h2 className="text-section-title">
|
||||||
Detrás de cada plataforma hay un equipo senior, una arquitectura clara y una ruta con sentido.
|
Detrás de cada plataforma hay un equipo senior, una arquitectura clara y una ruta con sentido.
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className={`max-w-xl text-base leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>
|
<p className={`max-w-sm text-base leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||||
Un sistema no solo se construye para operar: se diseña para sostener negocio, seguridad, adopción y evolución continua.
|
Un sistema no solo se construye para operar: se diseña para sostener negocio, seguridad, adopción y evolución continua.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`overflow-hidden rounded-[22px] border ${isNavyBg ? 'border-white/10 bg-white/5' : 'border-brand-navy/10 bg-white'}`}>
|
{/* Comparison table */}
|
||||||
<div className={`grid grid-cols-1 border-b text-sm font-semibold md:grid-cols-[1.1fr_0.95fr_0.95fr] ${isNavyBg ? 'border-white/10 text-sky-100' : 'border-brand-navy/10 text-brand-navy'}`}>
|
<div className={`overflow-hidden rounded-2xl border ${
|
||||||
<div className="px-5 py-4">Criterio</div>
|
isNavyBg ? 'border-white/8 bg-white/[0.03]' : 'border-gray-200 bg-white'
|
||||||
<div className="px-5 py-4 border-t md:border-t-0 md:border-l">Licencia SaaS</div>
|
}`}>
|
||||||
<div className="px-5 py-4 border-t md:border-t-0 md:border-l">RyNext software</div>
|
{/* Table header */}
|
||||||
|
<div className={`grid grid-cols-1 text-xs font-semibold uppercase tracking-[0.1em] md:grid-cols-[1.2fr_1fr_1fr] ${
|
||||||
|
isNavyBg ? 'border-b border-white/8 bg-white/[0.04]' : 'border-b border-gray-100 bg-gray-50'
|
||||||
|
}`}>
|
||||||
|
<div className={`px-6 py-4 ${isNavyBg ? 'text-slate-400' : 'text-gray-400'}`}>Criterio</div>
|
||||||
|
<div className={`border-t px-6 py-4 md:border-t-0 md:border-l ${
|
||||||
|
isNavyBg ? 'border-white/8 text-slate-400' : 'border-gray-100 text-gray-400'
|
||||||
|
}`}>
|
||||||
|
Licencia SaaS
|
||||||
|
</div>
|
||||||
|
<div className={`border-t px-6 py-4 font-bold md:border-t-0 md:border-l ${
|
||||||
|
isNavyBg ? 'border-white/8 text-sky-400' : 'border-gray-100 text-brand-blue'
|
||||||
|
}`}>
|
||||||
|
Rynext software
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Rows */}
|
||||||
{comparisonRows.map((row, index) => (
|
{comparisonRows.map((row, index) => (
|
||||||
<div key={row.criterion} className={`grid grid-cols-1 md:grid-cols-[1.1fr_0.95fr_0.95fr] ${isNavyBg ? 'text-slate-200' : 'text-brand-navy/80'}`}>
|
<div
|
||||||
<div className={`px-5 py-4 border-b font-semibold ${isNavyBg ? 'border-white/10 text-white' : 'border-brand-navy/10 text-brand-navy'}`}>
|
key={row.criterion}
|
||||||
|
className={`grid grid-cols-1 text-sm md:grid-cols-[1.2fr_1fr_1fr] ${
|
||||||
|
index < comparisonRows.length - 1
|
||||||
|
? isNavyBg ? 'border-b border-white/5' : 'border-b border-gray-100'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`px-6 py-5 font-semibold ${isNavyBg ? 'text-white' : 'text-brand-navy'}`}>
|
||||||
{row.criterion}
|
{row.criterion}
|
||||||
</div>
|
</div>
|
||||||
<div className={`px-5 py-4 border-b md:border-l ${isNavyBg ? 'border-white/10' : 'border-brand-navy/10'}`}>
|
<div className={`flex items-start gap-2.5 border-t px-6 py-5 md:border-t-0 md:border-l ${
|
||||||
|
isNavyBg ? 'border-white/5 text-slate-400' : 'border-gray-100 text-gray-500'
|
||||||
|
}`}>
|
||||||
|
<X size={13} className="mt-0.5 shrink-0 text-gray-400" />
|
||||||
{row.saas}
|
{row.saas}
|
||||||
</div>
|
</div>
|
||||||
<div className={`px-5 py-4 border-b md:border-l ${isNavyBg ? 'border-white/10' : 'border-brand-navy/10'}`}>
|
<div className={`flex items-start gap-2.5 border-t px-6 py-5 md:border-t-0 md:border-l ${
|
||||||
|
isNavyBg ? 'border-white/5 text-slate-200' : 'border-gray-100 text-brand-navy'
|
||||||
|
}`}>
|
||||||
|
<Check size={13} className={`mt-0.5 shrink-0 ${isNavyBg ? 'text-sky-400' : 'text-brand-blue'}`} />
|
||||||
{row.ry}
|
{row.ry}
|
||||||
</div>
|
</div>
|
||||||
{index === comparisonRows.length - 1 && (
|
|
||||||
<div className={`md:col-span-3 px-5 py-4 text-xs tracking-[0.08em] ${isNavyBg ? 'text-sky-100' : 'text-brand-navy'}`}>
|
|
||||||
Resultado: menos fricción, más control y más valor real.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 grid gap-4 lg:grid-cols-3">
|
{/* Proof cards */}
|
||||||
|
<div className="mt-6 grid gap-4 lg:grid-cols-3">
|
||||||
{proofCards.map((card, index) => {
|
{proofCards.map((card, index) => {
|
||||||
const Icon = card.icon;
|
const Icon = card.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.article
|
<motion.article
|
||||||
key={card.title}
|
key={card.title}
|
||||||
initial={{ opacity: 0, y: 24 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5, delay: index * 0.08 }}
|
transition={{ duration: 0.5, delay: index * 0.08 }}
|
||||||
className={`rounded-[18px] border p-5 ${isNavyBg
|
className={`rounded-2xl border p-6 ${
|
||||||
? 'border-white/10 bg-white/5 shadow-[0_10px_30px_rgba(0,0,0,0.16)]'
|
isNavyBg
|
||||||
: 'border-brand-navy/10 bg-white shadow-[0_10px_30px_rgba(0,31,91,0.08)]'}`}
|
? 'border-white/8 bg-white/[0.04]'
|
||||||
|
: 'border-gray-200 bg-white shadow-sm'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className={`mb-5 inline-flex rounded-xl p-3 ${
|
||||||
<div className={`rounded-xl p-3 ${isNavyBg ? 'bg-white/10 text-white' : 'bg-brand-blue/10 text-brand-blue'}`}>
|
isNavyBg ? 'bg-brand-blue/15 text-sky-300' : 'bg-brand-blue/8 text-brand-blue'
|
||||||
<Icon size={18} />
|
}`}>
|
||||||
|
<Icon size={20} />
|
||||||
</div>
|
</div>
|
||||||
<span className={`text-[10px] font-semibold tracking-[0.08em] ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'}`}>
|
<h3 className="mb-2 font-display text-lg font-bold">{card.title}</h3>
|
||||||
Capacidad real
|
<p className={`text-sm leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||||
</span>
|
{card.description}
|
||||||
</div>
|
</p>
|
||||||
<h3 className="mb-2 text-xl font-bold font-display">{card.title}</h3>
|
|
||||||
<p className={`text-sm leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>{card.description}</p>
|
|
||||||
</motion.article>
|
</motion.article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`mt-8 grid gap-4 rounded-[20px] border p-5 lg:grid-cols-[1.1fr_0.9fr] ${isNavyBg ? 'border-white/10 bg-white/5' : 'border-brand-navy/10 bg-white'}`}>
|
|
||||||
<div>
|
|
||||||
<p className={`mb-3 text-[10px] font-semibold tracking-[0.08em] ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'}`}>
|
|
||||||
Visión operativa
|
|
||||||
</p>
|
|
||||||
<h3 className="mb-3 text-2xl font-bold font-display">Una plataforma respaldada por una operación empresarial madura.</h3>
|
|
||||||
<p className={`max-w-2xl text-sm leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>
|
|
||||||
No entregamos simplemente una solución “bonita”. Dejanos una base técnica diseñada para crecer, sostener usuarios, automatizar procesos y poner a tu negocio en una ruta clara.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className={`rounded-[18px] border p-4 ${isNavyBg ? 'border-white/10 bg-[#081b46]' : 'border-brand-navy/10 bg-[#001f5b]/5'}`}>
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<span className={`text-[10px] font-semibold tracking-[0.08em] ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'}`}>Resumen ejecutivo</span>
|
|
||||||
<ArrowRight size={16} className={isNavyBg ? 'text-sky-200' : 'text-brand-blue'} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3 text-sm">
|
|
||||||
<div className={`rounded-xl border px-3 py-2 ${isNavyBg ? 'border-white/10 bg-white/5 text-slate-200' : 'border-brand-navy/10 bg-white text-brand-navy'}`}>
|
|
||||||
<strong className="font-bold">Sin bloqueo del proveedor</strong>
|
|
||||||
<div className="mt-1">Tu roadmap y evolución no dependen de una licencia estándar.</div>
|
|
||||||
</div>
|
|
||||||
<div className={`rounded-xl border px-3 py-2 ${isNavyBg ? 'border-white/10 bg-white/5 text-slate-200' : 'border-brand-navy/10 bg-white text-brand-navy'}`}>
|
|
||||||
<strong className="font-bold">Equipo con criterio</strong>
|
|
||||||
<div className="mt-1">Diseño, ingeniería, integración y soporte en una sola visión.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
|
const principles = [
|
||||||
|
{
|
||||||
|
title: 'Simple por diseño',
|
||||||
|
description: 'La complejidad existe solo cuando aporta valor real. Todo lo demás se elimina.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Diseñado para fallar',
|
||||||
|
description: 'Los sistemas asumen que algo fallará. La resiliencia es diseñada, no improvisada.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Una sola fuente de verdad',
|
||||||
|
description: 'La información crítica tiene una única fuente confiable. Sin duplicidad de datos.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Automatiza lo repetitivo',
|
||||||
|
description: 'Si una persona lo hace manualmente diez veces, el sistema debe hacerlo.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Construido para cambiar',
|
||||||
|
description: 'El software debe poder evolucionar. La arquitectura anticipa el cambio inevitable.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Principles = ({ isNavyBg }: ThemeProps) => {
|
||||||
|
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
|
||||||
|
const text = isNavyBg ? '#eef0f4' : '#0a0a0a';
|
||||||
|
const sub = isNavyBg ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)';
|
||||||
|
const gridGap = isNavyBg ? 'rgba(255,255,255,0.07)' : 'rgba(0,0,0,0.07)';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="principios"
|
||||||
|
aria-labelledby="principles-heading"
|
||||||
|
style={{ background: bg, color: text, borderBottom: `1px solid ${gridGap}` }}
|
||||||
|
className="px-8 py-28 md:px-14 transition-colors duration-500"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="mx-auto max-w-7xl"
|
||||||
|
initial={{ opacity: 0, y: 28 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false, amount: 0.1 }}
|
||||||
|
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-16">
|
||||||
|
<h2
|
||||||
|
id="principles-heading"
|
||||||
|
className="text-headline"
|
||||||
|
style={{ maxWidth: '24ch', color: text }}
|
||||||
|
>
|
||||||
|
Principios, no valores<br className="hidden sm:block" /> corporativos.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Principles — 5 columns */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(5, 1fr)',
|
||||||
|
border: `1px solid ${gridGap}`,
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{principles.map((p, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={p.title}
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.4, delay: i * 0.06 }}
|
||||||
|
style={{
|
||||||
|
padding: '32px 28px 36px',
|
||||||
|
borderRight: i < principles.length - 1 ? `1px solid ${gridGap}` : 'none',
|
||||||
|
borderTop: '3px solid #e50914',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="font-display font-black block mb-5"
|
||||||
|
style={{ fontSize: 56, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
|
||||||
|
>
|
||||||
|
{String(i + 1).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
<h3
|
||||||
|
className="font-display font-black mb-3"
|
||||||
|
style={{ fontSize: 15, letterSpacing: '-0.01em', color: text, lineHeight: 1.25 }}
|
||||||
|
>
|
||||||
|
{p.title}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className="text-sm leading-relaxed"
|
||||||
|
style={{ color: sub }}
|
||||||
|
>
|
||||||
|
{p.description}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
|
export const Problem = ({ isNavyBg }: ThemeProps) => {
|
||||||
|
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
|
||||||
|
const text = isNavyBg ? '#eef0f4' : '#0a0a0a';
|
||||||
|
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.4)';
|
||||||
|
const border = isNavyBg ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-labelledby="problem-heading"
|
||||||
|
className="px-8 py-24 md:px-14 transition-colors duration-500"
|
||||||
|
style={{ background: bg, borderBottom: `1px solid ${border}` }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="mx-auto max-w-7xl"
|
||||||
|
initial={{ opacity: 0, y: 28 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false, amount: 0.15 }}
|
||||||
|
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
{/* Headline */}
|
||||||
|
<h2
|
||||||
|
id="problem-heading"
|
||||||
|
className="text-display"
|
||||||
|
style={{ lineHeight: 1.0, letterSpacing: '-0.02em', color: text }}
|
||||||
|
>
|
||||||
|
{['La mayoría', 'de los problemas', 'operativos son'].map((line, i) => (
|
||||||
|
<motion.span
|
||||||
|
key={line}
|
||||||
|
className="block"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6, delay: i * 0.07, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
>
|
||||||
|
{line}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
<motion.span
|
||||||
|
className="block text-brand-red"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.24, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
>
|
||||||
|
problemas de sistema.
|
||||||
|
</motion.span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Descripción */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.45 }}
|
||||||
|
className="mt-12 flex gap-4"
|
||||||
|
style={{ maxWidth: '56ch' }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="mt-1 shrink-0 block h-full w-px"
|
||||||
|
style={{ background: '#e50914', minHeight: '100%', width: 2, borderRadius: 2, alignSelf: 'stretch', opacity: 0.7 }}
|
||||||
|
/>
|
||||||
|
<p className="text-base leading-relaxed" style={{ color: sub }}>
|
||||||
|
Software que impone sus propias reglas en lugar de adaptarse
|
||||||
|
a la operación. Procesos que permanecen manuales porque nadie
|
||||||
|
los diseñó para escalar. Ambos tienen la misma raíz: ausencia
|
||||||
|
de arquitectura.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
+77
-29
@@ -2,50 +2,98 @@ import { motion } from 'framer-motion';
|
|||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{ step: '01', title: 'Diagnóstico y mapa de valor', desc: 'Auditamos tu stack actual, objetivos de negocio y fricción operativa para definir una ruta técnica precisa.' },
|
{
|
||||||
{ step: '02', title: 'Diseño de solución', desc: 'Estructuramos arquitectura, alcance, experiencia y entregables con una visión clara, realista y escalable.' },
|
num: '01',
|
||||||
{ step: '03', title: 'Construcción con control', desc: 'Desarrollamos en sprints con entregables medibles, calidad técnica y coordinación constante con tu equipo.' },
|
title: 'ENTENDER',
|
||||||
{ step: '04', title: 'Entrega y evolución', desc: 'Publicamos en la nube, transferimos conocimiento y dejamos una plataforma lista para crecer con seguridad.' },
|
desc: 'Auditamos la operación, el stack y la fricción real antes de proponer nada. Sin suposiciones.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: '02',
|
||||||
|
title: 'MODELAR',
|
||||||
|
desc: 'Diseñamos la arquitectura del sistema: flujos, interfaces, datos y dependencias.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: '03',
|
||||||
|
title: 'CONSTRUIR',
|
||||||
|
desc: 'Desarrollamos en sprints con entregables medibles y revisión técnica constante.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: '04',
|
||||||
|
title: 'CONECTAR',
|
||||||
|
desc: 'Integramos el sistema con el ecosistema del negocio: ERPs, pagos, APIs externas.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
num: '05',
|
||||||
|
title: 'OPERAR',
|
||||||
|
desc: 'Despliegue a producción, monitoreo, soporte y evolución continua del sistema.',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Process = ({ isNavyBg }: ThemeProps) => {
|
export const Process = ({ isNavyBg }: ThemeProps) => {
|
||||||
|
const bg = isNavyBg ? '#001f5b' : '#ffffff';
|
||||||
|
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
|
||||||
|
const sub = isNavyBg ? 'rgba(255,255,255,0.4)' : '#6a6866';
|
||||||
|
const border = isNavyBg ? 'rgba(255,255,255,0.1)' : '#dddbd5';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="metodo" className={`section-wave border-t border-current/10 px-6 py-28 md:px-12 ${
|
<section
|
||||||
isNavyBg ? 'bg-[#0a2b7a] text-white' : 'bg-[#f7fbff] text-brand-black'
|
id="proceso"
|
||||||
}`}>
|
aria-labelledby="process-heading"
|
||||||
<div className="mx-auto max-w-7xl">
|
style={{ background: bg, color: text, borderBottom: `1px solid ${border}` }}
|
||||||
|
className="px-8 py-28 md:px-14 transition-colors duration-500"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="mx-auto max-w-7xl"
|
||||||
|
initial={{ opacity: 0, y: 28 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: false, amount: 0.1 }}
|
||||||
|
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-16">
|
<div className="mb-16">
|
||||||
<span className={`mb-3 block text-xs font-semibold tracking-[0.08em] ${
|
<h2
|
||||||
isNavyBg ? 'text-sky-200' : 'text-brand-blue'
|
id="process-heading"
|
||||||
}`}>
|
className="text-headline"
|
||||||
Método RyNext
|
style={{ maxWidth: '28ch' }}
|
||||||
</span>
|
>
|
||||||
<h2 className="max-w-3xl text-section-title">
|
De la problemática<br className="hidden sm:block" /> al sistema en producción.
|
||||||
Un proceso que mantiene foco, velocidad y resultados concretos.
|
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
{/* Steps */}
|
||||||
{steps.map((item, index) => (
|
<div className="grid gap-0 md:grid-cols-3 lg:grid-cols-5" style={{ border: `1px solid ${border}`, borderRadius: 4, overflow: 'hidden' }}>
|
||||||
|
{steps.map((step, i) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={item.step}
|
key={step.num}
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 16 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
transition={{ duration: 0.4, delay: i * 0.08 }}
|
||||||
className={`rounded-[16px] border p-6 ${
|
style={{
|
||||||
isNavyBg
|
padding: '32px 28px',
|
||||||
? 'border-white/10 bg-white/5'
|
borderRight: i < steps.length - 1 ? `1px solid ${border}` : 'none',
|
||||||
: 'border-gray-200 bg-white shadow-sm'
|
}}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className={`mb-4 text-4xl font-black ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'}`}>{item.step}</div>
|
<span
|
||||||
<h3 className="mb-3 text-xl font-bold font-display">{item.title}</h3>
|
className="font-display font-black block mb-6"
|
||||||
<p className={`text-sm leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>{item.desc}</p>
|
style={{ fontSize: 52, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
|
||||||
|
>
|
||||||
|
{step.num}
|
||||||
|
</span>
|
||||||
|
<h3
|
||||||
|
className="font-display font-black mb-3"
|
||||||
|
style={{ fontSize: 13, letterSpacing: '0.04em', color: text }}
|
||||||
|
>
|
||||||
|
{step.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: sub }}>
|
||||||
|
{step.desc}
|
||||||
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
+55
-23
@@ -1,59 +1,91 @@
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { ArrowUpRight } from 'lucide-react';
|
import { Check } from 'lucide-react';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
import { servicesData } from '../data/servicesData';
|
import { servicesData } from '../data/servicesData';
|
||||||
|
|
||||||
export const Services = ({ isNavyBg }: ThemeProps) => {
|
export const Services = ({ isNavyBg }: ThemeProps) => {
|
||||||
return (
|
return (
|
||||||
<section id="servicios" className={`section-wave border-t border-current/10 px-6 py-28 md:px-12 transition-colors duration-500 ${
|
<section
|
||||||
isNavyBg ? 'bg-[#0a2b7a] text-white' : 'bg-[#f7fbff] text-brand-black'
|
id="servicios"
|
||||||
}`}>
|
className={`section-wave px-6 py-28 md:px-12 transition-colors duration-500 ${
|
||||||
|
isNavyBg ? 'bg-[#001749] text-white' : 'bg-slate-50 text-brand-black'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="mx-auto max-w-7xl">
|
<div className="mx-auto max-w-7xl">
|
||||||
|
{/* Header */}
|
||||||
<div className="mb-16 flex flex-col justify-between gap-8 md:flex-row md:items-end">
|
<div className="mb-16 flex flex-col justify-between gap-8 md:flex-row md:items-end">
|
||||||
<div>
|
<div>
|
||||||
<span className={`mb-3 block text-xs font-semibold tracking-[0.08em] ${
|
<span className={`mb-3 block text-xs font-semibold uppercase tracking-[0.14em] ${
|
||||||
isNavyBg ? 'text-sky-200' : 'text-brand-blue'
|
isNavyBg ? 'text-sky-400' : 'text-brand-blue'
|
||||||
}`}>
|
}`}>
|
||||||
Servicios RyNext
|
Servicios
|
||||||
</span>
|
</span>
|
||||||
<h2 className="max-w-2xl text-section-title">
|
<h2 className="max-w-2xl text-section-title">
|
||||||
Soluciones que conectan producto, infraestructura y crecimiento.
|
Soluciones que conectan producto, infraestructura y crecimiento.
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className={`max-w-xl text-base leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>
|
<p className={`max-w-sm text-base leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||||
No trabajamos con plantillas. Diseñamos ecosistemas de tecnología a medida para que tu operación gane velocidad, claridad y control.
|
No trabajamos con plantillas. Cada ecosistema tecnológico se diseña para la operación específica del cliente.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-5 md:grid-cols-2">
|
{/* Cards grid */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-12">
|
||||||
{servicesData.map((service, index) => {
|
{servicesData.map((service, index) => {
|
||||||
const Icon = service.icon;
|
const Icon = service.icon;
|
||||||
|
const accentClass = service.accent === 'blue' ? 'bg-brand-blue' : 'bg-brand-red';
|
||||||
|
const iconBgClass = service.accent === 'blue'
|
||||||
|
? (isNavyBg ? 'bg-brand-blue/15 text-sky-300' : 'bg-brand-blue/10 text-brand-blue')
|
||||||
|
: (isNavyBg ? 'bg-brand-red/15 text-red-300' : 'bg-brand-red/10 text-brand-red');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.article
|
<motion.article
|
||||||
key={service.id}
|
key={service.id}
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 24 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: '-40px' }}
|
viewport={{ once: true, margin: '-40px' }}
|
||||||
transition={{ duration: 0.5, delay: index * 0.08 }}
|
transition={{ duration: 0.5, delay: index * 0.08 }}
|
||||||
className={`group rounded-[16px] border p-6 transition-all duration-300 hover:-translate-y-1 ${
|
className={`group relative overflow-hidden rounded-2xl border p-7 transition-all duration-300 hover:-translate-y-0.5 ${service.gridCols} ${
|
||||||
isNavyBg
|
isNavyBg
|
||||||
? 'border-white/10 bg-white/5 hover:border-brand-blue/50 hover:bg-white/10'
|
? 'border-white/8 bg-white/[0.04] hover:border-white/15 hover:bg-white/[0.07]'
|
||||||
: 'border-gray-200 bg-white hover:border-brand-blue/40 hover:shadow-xl'
|
: 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-xl hover:shadow-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="mb-5 flex items-center justify-between">
|
{/* Top accent line */}
|
||||||
<div className="flex items-center gap-4">
|
<div className={`absolute inset-x-0 top-0 h-[2px] ${accentClass} opacity-60`} />
|
||||||
<span className={`text-sm font-semibold ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'}`}>0{service.id}</span>
|
|
||||||
<div className={`rounded-xl p-3 ${isNavyBg ? 'bg-white/10 text-white' : 'bg-brand-blue/10 text-brand-blue'}`}>
|
{/* Header row */}
|
||||||
<Icon size={20} />
|
<div className="mb-6 flex items-start justify-between">
|
||||||
|
<div className={`rounded-xl p-3 ${iconBgClass}`}>
|
||||||
|
<Icon size={22} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span className={`font-mono text-xs font-bold ${isNavyBg ? 'text-white/20' : 'text-gray-200'}`}>
|
||||||
<ArrowUpRight size={18} className={`opacity-70 transition ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'} group-hover:translate-x-0.5 group-hover:-translate-y-0.5`} />
|
0{service.id}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="mb-3 text-2xl font-bold font-display">{service.title}</h3>
|
<h3 className="mb-3 font-display text-2xl font-bold">{service.title}</h3>
|
||||||
<p className={`max-w-lg text-sm leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>{service.description}</p>
|
<p className={`mb-6 text-sm leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Feature list */}
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{service.features.map((f: string) => (
|
||||||
|
<li key={f} className="flex items-center gap-2.5">
|
||||||
|
<Check
|
||||||
|
size={13}
|
||||||
|
className={`shrink-0 ${service.accent === 'blue'
|
||||||
|
? (isNavyBg ? 'text-sky-400' : 'text-brand-blue')
|
||||||
|
: (isNavyBg ? 'text-red-400' : 'text-brand-red')
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className={`text-xs font-medium ${isNavyBg ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||||
|
{f}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</motion.article>
|
</motion.article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
|
const stackGroups = [
|
||||||
|
{
|
||||||
|
label: 'FRONTEND',
|
||||||
|
items: ['React', 'TypeScript', 'Next.js', 'Tailwind CSS', 'Vite'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'BACKEND',
|
||||||
|
items: ['Node.js', 'Python', 'Express', 'FastAPI', 'NestJS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'DATOS',
|
||||||
|
items: ['PostgreSQL', 'MongoDB', 'Redis', 'Prisma', 'Elasticsearch'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'CLOUD & OPS',
|
||||||
|
items: ['AWS', 'Docker', 'GitHub Actions', 'Terraform', 'Linux'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'INTEGRACIONES',
|
||||||
|
items: ['REST', 'GraphQL', 'Stripe', 'OAuth 2.0', 'Webhooks'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Stack = ({ isNavyBg }: ThemeProps) => {
|
||||||
|
const bg = isNavyBg ? '#0f1520' : '#f9f9f7';
|
||||||
|
const text = isNavyBg ? '#eef0f4' : '#0a0a0a';
|
||||||
|
const sub = isNavyBg ? 'rgba(255,255,255,0.35)' : '#6a6866';
|
||||||
|
const border = isNavyBg ? 'rgba(255,255,255,0.07)' : '#dddbd5';
|
||||||
|
const tagBg = isNavyBg ? 'rgba(255,255,255,0.04)' : '#f0efec';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="stack"
|
||||||
|
aria-labelledby="stack-heading"
|
||||||
|
style={{ background: bg, color: text }}
|
||||||
|
className="px-8 py-28 md:px-14 transition-colors duration-500"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-16 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 flex items-center gap-3">
|
||||||
|
<span className="h-px w-8 bg-brand-red" />
|
||||||
|
<span className="text-mono-label" style={{ color: '#e50914' }}>Las herramientas son el medio</span>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
id="stack-heading"
|
||||||
|
className="text-headline"
|
||||||
|
style={{ maxWidth: '26ch' }}
|
||||||
|
>
|
||||||
|
Las herramientas son el medio.<br className="hidden sm:block" /> El sistema es el fin.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="max-w-xs text-sm leading-relaxed" style={{ color: sub }}>
|
||||||
|
Elegimos la tecnología correcta para cada problema. No hay un stack universal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stack groups */}
|
||||||
|
<div
|
||||||
|
style={{ borderTop: `1px solid ${border}` }}
|
||||||
|
>
|
||||||
|
{stackGroups.map((group, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={group.label}
|
||||||
|
initial={{ opacity: 0, x: -8 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.4, delay: i * 0.06 }}
|
||||||
|
className="flex flex-col gap-4 py-6 sm:flex-row sm:items-center sm:gap-10"
|
||||||
|
style={{ borderBottom: `1px solid ${border}` }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="text-mono-label shrink-0"
|
||||||
|
style={{ color: sub, width: 120 }}
|
||||||
|
>
|
||||||
|
{group.label}
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{group.items.map(item => (
|
||||||
|
<span
|
||||||
|
key={item}
|
||||||
|
className="text-sm font-medium px-3 py-1 rounded-sm"
|
||||||
|
style={{
|
||||||
|
background: tagBg,
|
||||||
|
border: `1px solid ${border}`,
|
||||||
|
color: text,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footnote */}
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.3 }}
|
||||||
|
className="mt-10 text-xs leading-relaxed"
|
||||||
|
style={{ color: sub, maxWidth: '60ch' }}
|
||||||
|
>
|
||||||
|
Estas son las herramientas con las que trabajamos actualmente. La elección depende siempre del problema, no de la preferencia.
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
type: 'main' | 'side';
|
||||||
|
desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Edge {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
type: 'main' | 'side';
|
||||||
|
}
|
||||||
|
|
||||||
|
const NODES: Node[] = [
|
||||||
|
{ id: 'frontend', label: 'FRONTEND', x: 80, y: 16, w: 160, h: 38, type: 'main', desc: 'Portales, apps y paneles:interfaces donde el usuario opera' },
|
||||||
|
{ id: 'api', label: 'API GATEWAY', x: 80, y: 108, w: 160, h: 38, type: 'main', desc: 'Seguridad, ruteo, rate limiting y versionado unificados' },
|
||||||
|
{ id: 'logic', label: 'LÓGICA DE NEGOCIO', x: 80, y: 200, w: 160, h: 38, type: 'main', desc: 'Reglas de negocio, validaciones, procesamiento y orquestación' },
|
||||||
|
{ id: 'db', label: 'BASE DE DATOS', x: 80, y: 292, w: 160, h: 38, type: 'main', desc: 'Persistencia diseñada para tu modelo:SQL, NoSQL, caché' },
|
||||||
|
{ id: 'infra', label: 'INFRAESTRUCTURA', x: 80, y: 384, w: 160, h: 36, type: 'main', desc: 'Nube, contenedores, CI/CD y observabilidad operacional' },
|
||||||
|
{ id: 'payments', label: 'PAGOS', x: 8, y: 172, w: 62, h: 28, type: 'side', desc: 'Stripe, Conekta:flujo de cobro integrado' },
|
||||||
|
{ id: 'erp', label: 'ERP / CRM', x: 8, y: 222, w: 62, h: 28, type: 'side', desc: 'SAP, Odoo:sincronización bidireccional' },
|
||||||
|
{ id: 'auth', label: 'ACCESO', x: 250, y: 84, w: 62, h: 28, type: 'side', desc: 'SSO, JWT, OAuth 2.0:identidad y permisos por rol' },
|
||||||
|
{ id: 'external', label: 'EXTERNOS', x: 250, y: 134, w: 62, h: 28, type: 'side', desc: 'APIs de terceros:logística, notificaciones, reportes' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const EDGES: Edge[] = [
|
||||||
|
{ from: 'frontend', to: 'api', type: 'main' },
|
||||||
|
{ from: 'api', to: 'logic', type: 'main' },
|
||||||
|
{ from: 'logic', to: 'db', type: 'main' },
|
||||||
|
{ from: 'db', to: 'infra', type: 'main' },
|
||||||
|
{ from: 'payments', to: 'logic', type: 'side' },
|
||||||
|
{ from: 'erp', to: 'logic', type: 'side' },
|
||||||
|
{ from: 'auth', to: 'api', type: 'side' },
|
||||||
|
{ from: 'external', to: 'api', type: 'side' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function nodeCenter(n: Node) {
|
||||||
|
return { cx: n.x + n.w / 2, cy: n.y + n.h / 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function edgeLine(edge: Edge): { x1: number; y1: number; x2: number; y2: number } {
|
||||||
|
const from = NODES.find(n => n.id === edge.from)!;
|
||||||
|
const to = NODES.find(n => n.id === edge.to)!;
|
||||||
|
const fc = nodeCenter(from);
|
||||||
|
const tc = nodeCenter(to);
|
||||||
|
if (edge.type === 'main') {
|
||||||
|
return { x1: fc.cx, y1: from.y + from.h, x2: tc.cx, y2: to.y };
|
||||||
|
}
|
||||||
|
if (from.x < to.x) {
|
||||||
|
return { x1: from.x + from.w, y1: fc.cy, x2: to.x, y2: tc.cy };
|
||||||
|
}
|
||||||
|
return { x1: from.x, y1: fc.cy, x2: to.x + to.w, y2: tc.cy };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isConnected(nodeId: string, activeId: string | null) {
|
||||||
|
if (!activeId || nodeId === activeId) return false;
|
||||||
|
return EDGES.some(e =>
|
||||||
|
(e.from === activeId && e.to === nodeId) ||
|
||||||
|
(e.to === activeId && e.from === nodeId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEdgeActive(edge: Edge, activeId: string | null) {
|
||||||
|
if (!activeId) return false;
|
||||||
|
return edge.from === activeId || edge.to === activeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOLTIP_W = 172;
|
||||||
|
const TAIL_H = 9;
|
||||||
|
const TAIL_W = 14;
|
||||||
|
|
||||||
|
function tooltipHeight(desc: string): number {
|
||||||
|
const charsPerLine = Math.floor((TOOLTIP_W - 26) / 6.3); // ~aprox chars por línea a 10.5px
|
||||||
|
const lines = Math.ceil(desc.length / charsPerLine);
|
||||||
|
return 14 + 16 + lines * 17 + 14; // paddingTop + label + líneas + paddingBottom
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
|
||||||
|
const [active, setActive] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const RED = '#e50914';
|
||||||
|
const dimStroke = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)';
|
||||||
|
const defStroke = isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,31,91,0.3)';
|
||||||
|
const connStroke = 'rgba(229,9,20,0.45)';
|
||||||
|
const descText = isNavyBg ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.65)';
|
||||||
|
const tooltipBg = isNavyBg ? '#0a1628' : '#ffffff';
|
||||||
|
const tooltipBorder = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,31,91,0.15)';
|
||||||
|
void tooltipBorder;
|
||||||
|
const activeDesc = NODES.find(n => n.id === active);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full" style={{ overflow: 'visible' }}>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 320 434"
|
||||||
|
width="100%"
|
||||||
|
aria-label="Mapa de sistema interactivo:hover para explorar capas"
|
||||||
|
role="img"
|
||||||
|
style={{ maxWidth: 340, display: 'block', margin: '0 auto', overflow: 'visible' }}
|
||||||
|
onMouseLeave={() => setActive(null)}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<filter id="bubble-shadow" x="-15%" y="-15%" width="130%" height="130%">
|
||||||
|
<feDropShadow dx="0" dy="3" stdDeviation="4" floodColor="rgba(0,0,0,0.18)" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
{/* Edges */}
|
||||||
|
{EDGES.map((edge, i) => {
|
||||||
|
const { x1, y1, x2, y2 } = edgeLine(edge);
|
||||||
|
const edgeActive = isEdgeActive(edge, active);
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
key={i}
|
||||||
|
x1={x1} y1={y1} x2={x2} y2={y2}
|
||||||
|
stroke={edgeActive ? RED : edge.type === 'side' ? connStroke : defStroke}
|
||||||
|
strokeWidth={edgeActive ? 1.5 : 1}
|
||||||
|
strokeDasharray={edge.type === 'side' ? '3 3' : undefined}
|
||||||
|
style={{ transition: 'stroke 200ms, stroke-width 200ms' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Nodes */}
|
||||||
|
{NODES.map(node => {
|
||||||
|
const isActive = active === node.id;
|
||||||
|
const isConn = isConnected(node.id, active);
|
||||||
|
const isDim = active !== null && !isActive && !isConn;
|
||||||
|
const stroke = isActive ? RED : isConn ? 'rgba(229,9,20,0.5)' : dimStroke;
|
||||||
|
const fillOpacity = isActive ? 0.12 : 0;
|
||||||
|
const textColor = isActive ? RED : isDim
|
||||||
|
? (isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.2)')
|
||||||
|
: (isNavyBg ? 'rgba(255,255,255,0.65)' : 'rgba(0,31,91,0.75)');
|
||||||
|
const { cx } = nodeCenter(node);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
key={node.id}
|
||||||
|
onMouseEnter={() => setActive(node.id)}
|
||||||
|
onFocus={() => setActive(node.id)}
|
||||||
|
onBlur={() => setActive(null)}
|
||||||
|
style={{ cursor: 'default', outline: 'none' }}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
aria-label={`${node.label}: ${node.desc}`}
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x={node.x} y={node.y}
|
||||||
|
width={node.w} height={node.h}
|
||||||
|
fill={`rgba(229,9,20,${fillOpacity})`}
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth={isActive ? 1.5 : 1}
|
||||||
|
rx={2}
|
||||||
|
style={{ transition: 'fill 200ms, stroke 200ms' }}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={cx}
|
||||||
|
y={node.y + node.h / 2 + 1}
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="middle"
|
||||||
|
fill={textColor}
|
||||||
|
fontFamily="'SF Mono','Fira Code',Consolas,monospace"
|
||||||
|
fontSize={node.type === 'main' ? 10 : 8}
|
||||||
|
fontWeight={600}
|
||||||
|
letterSpacing={node.type === 'main' ? 1.2 : 0.8}
|
||||||
|
style={{ transition: 'fill 200ms', pointerEvents: 'none' }}
|
||||||
|
>
|
||||||
|
{node.label}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Tooltip burbuja estilo chat */}
|
||||||
|
{activeDesc && (() => {
|
||||||
|
const node = NODES.find(n => n.id === active)!;
|
||||||
|
const TOOLTIP_H = tooltipHeight(activeDesc.desc);
|
||||||
|
const cy = node.y + node.h / 2;
|
||||||
|
|
||||||
|
// nodos izquierda → burbuja a la derecha
|
||||||
|
// nodos derecha → burbuja a la izquierda
|
||||||
|
// nodos centro → burbuja a la derecha
|
||||||
|
const showLeft = node.x > 160; // solo los nodos de la derecha van a la izquierda
|
||||||
|
|
||||||
|
let tx: number, ty: number, tailPoints: string;
|
||||||
|
|
||||||
|
if (showLeft) {
|
||||||
|
tx = node.x - TOOLTIP_W - TAIL_H - 4;
|
||||||
|
ty = Math.min(Math.max(cy - TOOLTIP_H / 2, 4), 430 - TOOLTIP_H);
|
||||||
|
const tailCy = Math.min(Math.max(cy, ty + 16), ty + TOOLTIP_H - 16);
|
||||||
|
tailPoints = `${tx + TOOLTIP_W},${tailCy - TAIL_W / 2} ${tx + TOOLTIP_W},${tailCy + TAIL_W / 2} ${tx + TOOLTIP_W + TAIL_H},${tailCy}`;
|
||||||
|
} else {
|
||||||
|
tx = node.x + node.w + TAIL_H + 4;
|
||||||
|
ty = Math.min(Math.max(cy - TOOLTIP_H / 2, 4), 430 - TOOLTIP_H);
|
||||||
|
const tailCy = Math.min(Math.max(cy, ty + 16), ty + TOOLTIP_H - 16);
|
||||||
|
tailPoints = `${tx},${tailCy - TAIL_W / 2} ${tx},${tailCy + TAIL_W / 2} ${tx - TAIL_H},${tailCy}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g style={{ pointerEvents: 'none' }}>
|
||||||
|
<g filter="url(#bubble-shadow)">
|
||||||
|
<rect x={tx} y={ty} width={TOOLTIP_W} height={TOOLTIP_H} fill={tooltipBg} rx={12} />
|
||||||
|
<polygon points={tailPoints} fill={tooltipBg} />
|
||||||
|
</g>
|
||||||
|
<foreignObject x={tx} y={ty} width={TOOLTIP_W} height={TOOLTIP_H} style={{ overflow: 'visible' }}>
|
||||||
|
<div style={{ padding: '10px 13px' }}>
|
||||||
|
<p style={{ fontFamily: "'SF Mono',Consolas,monospace", fontSize: 8, letterSpacing: '0.12em', textTransform: 'uppercase', color: RED, marginBottom: 4, fontWeight: 600 }}>
|
||||||
|
{activeDesc.label}
|
||||||
|
</p>
|
||||||
|
<p style={{ fontSize: 10.5, color: descText, lineHeight: 1.5, margin: 0 }}>
|
||||||
|
{activeDesc.desc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export const machineryData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
num: '01',
|
||||||
|
title: 'OPERACIONES',
|
||||||
|
description: 'Plataformas internas, automatización de flujos y control de procesos críticos. Sistemas que eliminan la fricción operativa y dan visibilidad en tiempo real.',
|
||||||
|
tags: ['Portales de gestión', 'Automatización de flujos', 'Reportes operacionales', 'Control de acceso por rol'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
num: '02',
|
||||||
|
title: 'VENTAS',
|
||||||
|
description: 'Sistemas de venta, cobro y facturación integrados al núcleo del negocio. Desde el punto de contacto con el cliente hasta el comprobante fiscal.',
|
||||||
|
tags: ['E-commerce a medida', 'Integración de pagos', 'Facturación y CFDI', 'Recuperación de carritos'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
num: '03',
|
||||||
|
title: 'INTEGRACIONES',
|
||||||
|
description: 'Conectamos sistemas heterogéneos en un ecosistema coherente: ERPs, CRMs, proveedores y APIs de terceros. Sin silos, sin duplicidad de datos.',
|
||||||
|
tags: ['ERP / SAP / Odoo', 'APIs de terceros', 'Sincronización bidireccional', 'Webhooks y eventos'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
num: '04',
|
||||||
|
title: 'AUTOMATIZACIÓN',
|
||||||
|
description: 'Eliminamos intervención humana donde no aporta valor: aprobaciones, notificaciones, sincronizaciones y pipelines de datos que corren solos.',
|
||||||
|
tags: ['Bots y pipelines', 'Procesamiento de datos', 'Notificaciones inteligentes', 'Triggers y colas'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
num: '05',
|
||||||
|
title: 'DATOS',
|
||||||
|
description: 'Instrumentación del negocio para tomar decisiones con evidencia. Dashboards ejecutivos, modelos analíticos y alertas que activan antes de que el problema escale.',
|
||||||
|
tags: ['Dashboards ejecutivos', 'Pipelines de datos', 'KPIs e instrumentación', 'Alertas y umbrales'],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -5,29 +5,37 @@ export const servicesData: ServiceItem[] = [
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: 'Desarrollo de software',
|
title: 'Desarrollo de software',
|
||||||
description: 'Arquitecturas a medida, código mantenible y desarrollo de plataformas de alto rendimiento.',
|
description: 'Plataformas web y APIs construidas con arquitectura limpia, código mantenible y enfoque en rendimiento real.',
|
||||||
|
features: ['APIs REST & GraphQL', 'Microservicios y monolitos modulares', 'Code review y auditorías técnicas'],
|
||||||
icon: Code,
|
icon: Code,
|
||||||
gridCols: 'md:col-span-7',
|
gridCols: 'md:col-span-7',
|
||||||
|
accent: 'blue',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'Soluciones en la nube',
|
title: 'Infraestructura en la nube',
|
||||||
description: 'Infraestructura cloud escalable, migración y alta disponibilidad 24/7 sin interrupciones.',
|
description: 'Diseño, migración y operación de infraestructura cloud escalable con alta disponibilidad.',
|
||||||
|
features: ['AWS, GCP y Azure', 'CI/CD y DevOps', 'Monitoreo y alertas 24/7'],
|
||||||
icon: Cloud,
|
icon: Cloud,
|
||||||
gridCols: 'md:col-span-5',
|
gridCols: 'md:col-span-5',
|
||||||
|
accent: 'blue',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: 'Tecnología segura',
|
title: 'Ciberseguridad aplicada',
|
||||||
description: 'Protección de datos corporativos, cifrado avanzado y protocolos de ciberseguridad activa.',
|
description: 'Protección de datos, auditorías de seguridad y protocolos que cumplen estándares empresariales.',
|
||||||
|
features: ['Pruebas de penetración', 'Cifrado end-to-end', 'Cumplimiento GDPR / ISO'],
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
gridCols: 'md:col-span-5',
|
gridCols: 'md:col-span-5',
|
||||||
|
accent: 'red',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: 'Innovación que impulsa negocios',
|
title: 'Producto y crecimiento',
|
||||||
description: 'Estrategias tecnológicas orientadas a optimizar operaciones y acelerar el crecimiento comercial.',
|
description: 'Estrategia tecnológica alineada al negocio para lanzar, medir y escalar con datos reales.',
|
||||||
|
features: ['Roadmap técnico y de producto', 'Analytics e instrumentación', 'Optimización de conversión'],
|
||||||
icon: TrendingUp,
|
icon: TrendingUp,
|
||||||
gridCols: 'md:col-span-7',
|
gridCols: 'md:col-span-7',
|
||||||
|
accent: 'red',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export function useScrollSnap() {
|
||||||
|
const locked = useRef(false);
|
||||||
|
const current = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getSections = () =>
|
||||||
|
Array.from(document.querySelectorAll('section, footer')) as HTMLElement[];
|
||||||
|
|
||||||
|
const goTo = (index: number) => {
|
||||||
|
const sections = getSections();
|
||||||
|
if (index < 0 || index >= sections.length) return;
|
||||||
|
current.current = index;
|
||||||
|
locked.current = true;
|
||||||
|
sections[index].scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
setTimeout(() => { locked.current = false; }, 900);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCurrent = () => {
|
||||||
|
const sections = getSections();
|
||||||
|
const mid = window.innerHeight / 2;
|
||||||
|
sections.forEach((s, i) => {
|
||||||
|
const rect = s.getBoundingClientRect();
|
||||||
|
if (rect.top <= mid && rect.bottom > mid) current.current = i;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWheel = (e: WheelEvent) => {
|
||||||
|
if (locked.current) { e.preventDefault(); return; }
|
||||||
|
updateCurrent();
|
||||||
|
const dir = e.deltaY > 0 ? 1 : -1;
|
||||||
|
const next = current.current + dir;
|
||||||
|
const sections = getSections();
|
||||||
|
if (next >= 0 && next < sections.length) {
|
||||||
|
e.preventDefault();
|
||||||
|
goTo(next);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (locked.current) return;
|
||||||
|
updateCurrent();
|
||||||
|
if (e.key === 'ArrowDown' || e.key === 'PageDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
goTo(current.current + 1);
|
||||||
|
} else if (e.key === 'ArrowUp' || e.key === 'PageUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
goTo(current.current - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('wheel', onWheel, { passive: false });
|
||||||
|
window.addEventListener('keydown', onKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('wheel', onWheel);
|
||||||
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
+124
-46
@@ -1,34 +1,89 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@700;800;900&family=Inter:wght@300;400;500;600&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,700;0,800;0,900;1,800&family=Inter:wght@300;400;500;600&display=swap');
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-brand-blue: #0077c2;
|
--color-brand-blue: #0077c2;
|
||||||
--color-brand-navy: #001f5b;
|
--color-brand-navy: #001f5b;
|
||||||
--color-brand-black: #000000;
|
--color-brand-black: #0a0a0a;
|
||||||
--color-brand-red: #e50914;
|
--color-brand-red: #e50914;
|
||||||
|
--color-bg-light: #f9f9f7;
|
||||||
|
--color-bg-alt: #f0efec;
|
||||||
|
--color-border-light: #dddbd5;
|
||||||
|
--color-text-secondary: #6a6866;
|
||||||
|
--color-dark-bg: #080c14;
|
||||||
|
--color-dark-surface: #0f1520;
|
||||||
|
--color-dark-border: #1c2535;
|
||||||
|
|
||||||
--font-sans: 'Inter', sans-serif;
|
--font-sans: 'Inter', system-ui, sans-serif;
|
||||||
--font-display: 'Plus Jakarta Sans', sans-serif;
|
--font-display: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||||
|
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: rgba(229, 9, 20, 0.18);
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply font-sans antialiased m-0 p-0 overflow-x-hidden selection:bg-brand-blue selection:text-white;
|
@apply font-sans antialiased m-0 p-0 overflow-x-hidden;
|
||||||
|
background-color: #f9f9f7;
|
||||||
|
color: #0a0a0a;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1, h2, h3, h4, .font-display {
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
.font-display {
|
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 2px solid #e50914;
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: #001f5b;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography scale */
|
||||||
|
.text-display {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(2.6rem, 6vw, 5.5rem);
|
||||||
|
line-height: 1.02;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-headline {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(1.8rem, 4vw, 3.2rem);
|
||||||
|
line-height: 1.1;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-section-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(1.5rem, 3vw, 2.4rem);
|
||||||
|
line-height: 1.15;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-mono-label {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section wave divider */
|
||||||
.section-wave {
|
.section-wave {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-wave::before {
|
.section-wave::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -36,51 +91,74 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: linear-gradient(90deg, transparent, rgba(0, 31, 91, 0.24), transparent);
|
background: linear-gradient(90deg, transparent, currentColor, transparent);
|
||||||
opacity: 0.55;
|
opacity: 0.12;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visual-shell {
|
/* Bg patterns */
|
||||||
position: relative;
|
.bg-dot-grid {
|
||||||
|
background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
|
||||||
|
background-size: 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-grid-lines {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(currentColor 1px, transparent 1px),
|
||||||
|
linear-gradient(to right, currentColor 1px, transparent 1px);
|
||||||
|
background-size: 48px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ticker */
|
||||||
|
@keyframes ticker {
|
||||||
|
from { transform: translateX(0); }
|
||||||
|
to { transform: translateX(-50%); }
|
||||||
|
}
|
||||||
|
.animate-ticker {
|
||||||
|
animation: ticker 32s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anatomy accordion */
|
||||||
|
.anatomy-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
transition: grid-template-rows 320ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
.anatomy-body.open {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
.anatomy-body-inner {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-tile {
|
/* Principles hover */
|
||||||
border-radius: 18px;
|
.principle-desc {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 320ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
.principle-item:hover .principle-desc,
|
||||||
|
.principle-item:focus-within .principle-desc {
|
||||||
|
max-height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visual-shell::before {
|
/* Modal fade */
|
||||||
content: '';
|
@keyframes fadeIn {
|
||||||
position: absolute;
|
from { opacity: 0; }
|
||||||
inset: 0;
|
to { opacity: 1; }
|
||||||
background:
|
}
|
||||||
radial-gradient(circle at top right, rgba(229, 9, 20, 0.18), transparent 24%),
|
.animate-fadeIn {
|
||||||
radial-gradient(circle at bottom left, rgba(0, 119, 194, 0.24), transparent 26%);
|
animation: fadeIn 200ms ease;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.visual-grid {
|
/* Reduced motion */
|
||||||
position: relative;
|
@media (prefers-reduced-motion: reduce) {
|
||||||
z-index: 1;
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
}
|
}
|
||||||
|
.animate-ticker {
|
||||||
.placeholder-tile {
|
animation: none;
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-hero-dacodes {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: clamp(2.5rem, 6vw, 5rem);
|
|
||||||
line-height: 1.05;
|
|
||||||
font-weight: 900;
|
|
||||||
letter-spacing: -0.035em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-section-title {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: clamp(2rem, 4vw, 3.5rem);
|
|
||||||
line-height: 1.1;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { BrowserRouter } from 'react-router'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const fadeUp = {
|
||||||
|
hidden: { opacity: 0, y: 28 },
|
||||||
|
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as const } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const articulos = [
|
||||||
|
{
|
||||||
|
categoria: 'ARQUITECTURA',
|
||||||
|
titulo: 'Por qué el software empresarial fracasa antes de llegar a producción',
|
||||||
|
desc: 'La mayoría de los proyectos de software empresarial no fallan en el código, fallan en el diseño. Exploramos las causas estructurales más comunes y cómo evitarlas desde el inicio.',
|
||||||
|
fecha: 'Próximamente',
|
||||||
|
img: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&q=80',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categoria: 'INTEGRACIONES',
|
||||||
|
titulo: 'ERP vs. sistema a medida: cuándo tiene sentido cada uno',
|
||||||
|
desc: 'La respuesta no es simple. Depende del tamaño de la operación, la madurez del proceso y la capacidad técnica interna. Aquí el marco de decisión que usamos con nuestros clientes.',
|
||||||
|
fecha: 'Próximamente',
|
||||||
|
img: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&q=80',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categoria: 'DATOS',
|
||||||
|
titulo: 'Instrumentación operacional: medir lo que importa',
|
||||||
|
desc: 'Tener datos no es lo mismo que tener visibilidad. La diferencia está en qué se mide, cómo se almacena y cuándo se activa una alerta. Una guía práctica para equipos de operaciones.',
|
||||||
|
fecha: 'Próximamente',
|
||||||
|
img: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&q=80',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Blog = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = 'Blog | Rynext Technologies';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main id="main-content">
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<section
|
||||||
|
style={{
|
||||||
|
background: '#ffffff',
|
||||||
|
borderBottom: '1px solid #dddbd5',
|
||||||
|
paddingTop: '160px',
|
||||||
|
paddingBottom: '100px',
|
||||||
|
}}
|
||||||
|
className="px-8 md:px-14"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<motion.p className="text-mono-label mb-6"
|
||||||
|
style={{ color: '#e50914', letterSpacing: '0.18em' }}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }} variants={fadeUp}>
|
||||||
|
RECURSOS
|
||||||
|
</motion.p>
|
||||||
|
<motion.h1 className="text-display" style={{ color: '#0a0a0a', maxWidth: '16ch' }}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 32 }, visible: { opacity: 1, y: 0, transition: { duration: 0.7, delay: 0.1, ease: [0.22, 1, 0.36, 1] } } }}>
|
||||||
|
Pensamiento de ingeniería.
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p className="mt-8 text-lg leading-relaxed"
|
||||||
|
style={{ color: '#6a6866', maxWidth: '52ch' }}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 0.2, ease: [0.22, 1, 0.36, 1] } } }}>
|
||||||
|
Perspectivas sobre arquitectura de software, sistemas empresariales y decisiones técnicas
|
||||||
|
que impactan la operación real de los negocios.
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Artículos */}
|
||||||
|
<section
|
||||||
|
style={{
|
||||||
|
background: '#f5f5f3',
|
||||||
|
borderBottom: '1px solid #dddbd5',
|
||||||
|
padding: '100px 0',
|
||||||
|
minHeight: '60vh',
|
||||||
|
}}
|
||||||
|
className="px-8 md:px-14"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<motion.p className="text-mono-label mb-12"
|
||||||
|
style={{ color: '#e50914', letterSpacing: '0.18em' }}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }} variants={fadeUp}>
|
||||||
|
PRÓXIMOS ARTÍCULOS
|
||||||
|
</motion.p>
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
{articulos.map((art, i) => (
|
||||||
|
<motion.article key={i}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: i * 0.1, ease: [0.22, 1, 0.36, 1] } } }}
|
||||||
|
style={{
|
||||||
|
background: '#ffffff',
|
||||||
|
border: '1px solid #dddbd5',
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
opacity: 0.82,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Thumbnail */}
|
||||||
|
<div style={{ aspectRatio: '16/9', background: '#f0efec', overflow: 'hidden', position: 'relative' }}>
|
||||||
|
<img
|
||||||
|
src={art.img}
|
||||||
|
alt={art.titulo}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none'; }}
|
||||||
|
/>
|
||||||
|
{/* Categoria badge encima */}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
position: 'absolute', top: 14, left: 14,
|
||||||
|
background: '#e50914', color: '#fff',
|
||||||
|
fontFamily: 'monospace', fontSize: 10, letterSpacing: '0.12em',
|
||||||
|
padding: '3px 8px', borderRadius: 2,
|
||||||
|
}}
|
||||||
|
>{art.categoria}</span>
|
||||||
|
</div>
|
||||||
|
{/* Contenido */}
|
||||||
|
<div style={{ padding: '28px 28px 24px', display: 'flex', flexDirection: 'column', gap: 12, flexGrow: 1 }}>
|
||||||
|
<h2 className="font-display font-bold text-lg leading-snug" style={{ color: '#0a0a0a' }}>
|
||||||
|
{art.titulo}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: '#6a6866', flexGrow: 1 }}>
|
||||||
|
{art.desc}
|
||||||
|
</p>
|
||||||
|
<p className="text-mono-label" style={{ color: '#aaa8a4', letterSpacing: '0.12em', marginTop: 4 }}>
|
||||||
|
{art.fecha}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="mt-20 text-center"
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0 }, visible: { opacity: 1, transition: { duration: 0.6, delay: 0.3 } } }}
|
||||||
|
>
|
||||||
|
<p className="text-sm" style={{ color: '#aaa8a4' }}>
|
||||||
|
El blog está en preparación. Los artículos llegarán pronto.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Hero } from '../components/Hero';
|
||||||
|
import { Problem } from '../components/Problem';
|
||||||
|
import { Machinery } from '../components/Machinery';
|
||||||
|
import { Anatomy } from '../components/Anatomy';
|
||||||
|
import { Process } from '../components/Process';
|
||||||
|
import { Principles } from '../components/Principles';
|
||||||
|
import { CtaBanner } from '../components/CtaBanner';
|
||||||
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
|
interface HomeProps extends ThemeProps {
|
||||||
|
onOpenContact: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Home = ({ isNavyBg, onOpenContact }: HomeProps) => (
|
||||||
|
<main id="main-content">
|
||||||
|
<Hero isNavyBg={isNavyBg} onOpenContact={onOpenContact} />
|
||||||
|
<Problem isNavyBg={isNavyBg} />
|
||||||
|
<Machinery isNavyBg={isNavyBg} />
|
||||||
|
<Anatomy isNavyBg={isNavyBg} />
|
||||||
|
<Process isNavyBg={isNavyBg} />
|
||||||
|
<Principles isNavyBg={isNavyBg} />
|
||||||
|
<CtaBanner isNavyBg={isNavyBg} onOpenContact={onOpenContact} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const fadeUp = {
|
||||||
|
hidden: { opacity: 0, y: 28 },
|
||||||
|
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as const } },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Productos = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = 'Productos | Rynext Technologies';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main id="main-content">
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<section
|
||||||
|
style={{
|
||||||
|
background: '#ffffff',
|
||||||
|
borderBottom: '1px solid #dddbd5',
|
||||||
|
paddingTop: '160px',
|
||||||
|
paddingBottom: '100px',
|
||||||
|
}}
|
||||||
|
className="px-8 md:px-14"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<motion.p className="text-mono-label mb-6"
|
||||||
|
style={{ color: '#e50914', letterSpacing: '0.18em' }}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }} variants={fadeUp}>
|
||||||
|
PRODUCTOS
|
||||||
|
</motion.p>
|
||||||
|
<motion.h1 className="text-display" style={{ color: '#0a0a0a', maxWidth: '18ch' }}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 32 }, visible: { opacity: 1, y: 0, transition: { duration: 0.7, delay: 0.1, ease: [0.22, 1, 0.36, 1] } } }}>
|
||||||
|
Software diseñado para operar.
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p className="mt-8 text-lg leading-relaxed"
|
||||||
|
style={{ color: '#6a6866', maxWidth: '52ch' }}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 0.2, ease: [0.22, 1, 0.36, 1] } } }}>
|
||||||
|
Módulos y licencias listos para implementar, respaldados por la misma ingeniería
|
||||||
|
con la que construimos sistemas a medida.
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Producto — comentado hasta tener contenido real */}
|
||||||
|
{/* <section
|
||||||
|
style={{
|
||||||
|
background: '#f5f5f3',
|
||||||
|
borderBottom: '1px solid #dddbd5',
|
||||||
|
padding: '100px 0',
|
||||||
|
}}
|
||||||
|
className="px-8 md:px-14"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<motion.div
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] } } }}
|
||||||
|
style={{
|
||||||
|
background: '#ffffff',
|
||||||
|
border: '1px solid #dddbd5',
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: '60px 56px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 32,
|
||||||
|
maxWidth: 720,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
width: 72, height: 72,
|
||||||
|
background: '#f0efec',
|
||||||
|
border: '1.5px dashed #c8c5bf',
|
||||||
|
borderRadius: 8,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#c8c5bf" strokeWidth="1.5">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||||
|
<polyline points="21 15 16 10 5 21"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-mono-label mb-2" style={{ color: '#aaa8a4', letterSpacing: '0.14em' }}>NOMBRE DEL PRODUCTO</p>
|
||||||
|
<h2 className="text-headline" style={{ color: '#0a0a0a' }}>Tu producto aquí</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-base leading-relaxed" style={{ color: '#6a6866', maxWidth: '48ch' }}>
|
||||||
|
Descripción del producto. Qué resuelve, para quién está diseñado y qué lo hace diferente.
|
||||||
|
Agrega aquí los detalles que quieres que el cliente vea antes de solicitar una demo.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href="mailto:[email protected]?subject=Solicitud%20de%20demo"
|
||||||
|
className="inline-flex items-center gap-2 px-7 py-3.5 text-sm font-bold text-white transition hover:opacity-90"
|
||||||
|
style={{ background: '#e50914', borderRadius: 3 }}
|
||||||
|
>
|
||||||
|
Solicitar demo
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section> */}
|
||||||
|
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const valores = [
|
||||||
|
{ num: '01', titulo: 'Arquitectura primero', desc: 'Cada sistema empieza con un diseño estructural claro antes de escribir la primera línea de código.' },
|
||||||
|
{ num: '02', titulo: 'Sin atajos técnicos', desc: 'No usamos soluciones genéricas donde la operación exige algo específico. La deuda técnica cuesta más a largo plazo.' },
|
||||||
|
{ num: '03', titulo: 'Código como contrato', desc: 'El software que entregamos es la documentación viva de la operación del cliente. Tiene que ser legible y mantenible.' },
|
||||||
|
{ num: '04', titulo: 'Iteración con propósito', desc: 'Construimos en ciclos cortos y medibles. Cada entrega aporta valor real y permite ajustar la dirección.' },
|
||||||
|
{ num: '05', titulo: 'Responsabilidad completa', desc: 'Desde el análisis hasta la producción, somos dueños del resultado. No transferimos riesgo al cliente.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TEAM_IMAGE = 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200&q=80';
|
||||||
|
|
||||||
|
export const SobreNosotros = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = 'Sobre Nosotros | Rynext Technologies';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main id="main-content">
|
||||||
|
|
||||||
|
{/* Hero — texto + imagen lado a lado */}
|
||||||
|
<section
|
||||||
|
style={{
|
||||||
|
background: '#ffffff',
|
||||||
|
borderBottom: '1px solid #dddbd5',
|
||||||
|
paddingTop: '140px',
|
||||||
|
paddingBottom: '100px',
|
||||||
|
}}
|
||||||
|
className="px-8 md:px-14"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-7xl grid gap-16 lg:grid-cols-2 lg:items-center">
|
||||||
|
{/* Texto */}
|
||||||
|
<div>
|
||||||
|
<motion.h1
|
||||||
|
className="text-display mb-8"
|
||||||
|
style={{ color: '#0a0a0a' }}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 32 }, visible: { opacity: 1, y: 0, transition: { duration: 0.7, delay: 0.1, ease: [0.22, 1, 0.36, 1] } } }}
|
||||||
|
>
|
||||||
|
Software 100%{' '}
|
||||||
|
<span style={{ position: 'relative', display: 'inline-block' }}>
|
||||||
|
<span style={{ color: '#e50914' }}>Mexicano</span>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 200 12"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ position: 'absolute', bottom: -4, left: 0, width: '100%', height: 12 }}
|
||||||
|
>
|
||||||
|
<path d="M2 8 C50 2, 150 2, 198 8" stroke="#e50914" strokeWidth="3" strokeLinecap="round" fill="none" />
|
||||||
|
</svg>
|
||||||
|
</span>{' '}
|
||||||
|
de clase mundial.
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
className="text-lg leading-relaxed"
|
||||||
|
style={{ color: '#6a6866' }}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 0.2, ease: [0.22, 1, 0.36, 1] } } }}
|
||||||
|
>
|
||||||
|
Rynext Technologies nació de una convicción: México tiene el talento para construir
|
||||||
|
software empresarial que compita con cualquier firma en el mundo. No adaptamos
|
||||||
|
herramientas extranjeras diseñamos sistemas propios, con las tecnologías más
|
||||||
|
actuales, para la realidad operativa de los negocios mexicanos.
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Imagen del equipo */}
|
||||||
|
<motion.div
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, x: 24 }, visible: { opacity: 1, x: 0, transition: { duration: 0.7, delay: 0.15, ease: [0.22, 1, 0.36, 1] } } }}
|
||||||
|
style={{ position: 'relative' }}
|
||||||
|
>
|
||||||
|
{/* Acento rojo desplazado atrás */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 16, left: -16,
|
||||||
|
width: '100%', height: '100%',
|
||||||
|
border: '3px solid #e50914',
|
||||||
|
borderRadius: 2,
|
||||||
|
zIndex: 0,
|
||||||
|
}} />
|
||||||
|
{/* Imagen con corte diagonal */}
|
||||||
|
<img
|
||||||
|
src={TEAM_IMAGE}
|
||||||
|
alt="Equipo Rynext Technologies"
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
width: '100%',
|
||||||
|
aspectRatio: '4/3',
|
||||||
|
objectFit: 'cover',
|
||||||
|
display: 'block',
|
||||||
|
clipPath: 'polygon(0 0, 100% 0, 100% 88%, 94% 100%, 0 100%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Por qué Rynext */}
|
||||||
|
<section
|
||||||
|
style={{
|
||||||
|
background: '#f5f5f3',
|
||||||
|
borderBottom: '1px solid #dddbd5',
|
||||||
|
padding: '100px 0',
|
||||||
|
}}
|
||||||
|
className="px-8 md:px-14"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<motion.h2 className="text-display mb-20" style={{ color: '#0a0a0a', maxWidth: '22ch' }}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 28 }, visible: { opacity: 1, y: 0, transition: { duration: 0.7, ease: [0.22, 1, 0.36, 1] } } }}>
|
||||||
|
¿Por qué <span style={{ color: '#e50914' }}>Rynext Technologies?</span>
|
||||||
|
</motion.h2>
|
||||||
|
<div className="flex flex-col divide-y" style={{ borderTop: '1px solid #dddbd5', borderBottom: '1px solid #dddbd5' }}>
|
||||||
|
{[
|
||||||
|
{ num: '01', titulo: 'Diseño desde la raíz', desc: 'Cada sistema empieza con un análisis profundo de la operación. No adaptamos plantillas, diseñamos desde cero para el contexto específico del negocio.' },
|
||||||
|
{ num: '02', titulo: 'Equipo técnico propio', desc: 'Sin intermediarios. El equipo que analiza tu operación es el mismo que diseña la arquitectura y escribe el código.' },
|
||||||
|
{ num: '03', titulo: 'Enfoque en producción', desc: 'No entregamos prototipos. Entregamos sistemas listos para operar con carga real, datos reales y usuarios reales.' },
|
||||||
|
].map((item, i) => (
|
||||||
|
<motion.div key={i}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 16 }, visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: i * 0.08, ease: [0.22, 1, 0.36, 1] } } }}
|
||||||
|
className="grid py-10 md:grid-cols-[80px_1fr_2fr] gap-6 items-start"
|
||||||
|
style={{ borderColor: '#dddbd5' }}
|
||||||
|
>
|
||||||
|
<span className="font-display font-black" style={{ fontSize: 48, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}>{item.num}</span>
|
||||||
|
<h3 className="font-display font-bold text-xl" style={{ color: '#0a0a0a' }}>{item.titulo}</h3>
|
||||||
|
<p className="text-base leading-relaxed" style={{ color: '#6a6866', maxWidth: '52ch' }}>{item.desc}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Valores */}
|
||||||
|
<section
|
||||||
|
style={{
|
||||||
|
background: '#ffffff',
|
||||||
|
borderBottom: '1px solid #dddbd5',
|
||||||
|
padding: '100px 0',
|
||||||
|
}}
|
||||||
|
className="px-8 md:px-14"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<motion.h2 className="text-headline mb-16" style={{ color: '#0a0a0a' }}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 28 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 0.1, ease: [0.22, 1, 0.36, 1] } } }}>
|
||||||
|
Los principios que guían cada proyecto.
|
||||||
|
</motion.h2>
|
||||||
|
<div className="grid gap-8 md:grid-cols-5">
|
||||||
|
{valores.map((v, i) => (
|
||||||
|
<motion.div key={i}
|
||||||
|
initial="hidden" whileInView="visible" viewport={{ once: true }}
|
||||||
|
variants={{ hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: i * 0.08, ease: [0.22, 1, 0.36, 1] } } }}
|
||||||
|
style={{ borderTop: '3px solid #e50914', paddingTop: 24 }}
|
||||||
|
>
|
||||||
|
<span className="font-display font-black block mb-4"
|
||||||
|
style={{ fontSize: 48, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}>
|
||||||
|
{v.num}
|
||||||
|
</span>
|
||||||
|
<h3 className="font-display font-bold text-base mb-3" style={{ color: '#0a0a0a' }}>{v.titulo}</h3>
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: '#6a6866' }}>{v.desc}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
+19
-15
@@ -1,27 +1,31 @@
|
|||||||
import { type ComponentType, type Dispatch, type SetStateAction } from 'react';
|
import type React from 'react';
|
||||||
|
import { type Dispatch, type SetStateAction } from 'react';
|
||||||
export interface ServiceItem {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon: ComponentType<{ size?: number; className?: string }>;
|
|
||||||
gridCols: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ThemeProps {
|
export interface ThemeProps {
|
||||||
isNavyBg: boolean;
|
isNavyBg: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NavbarProps {
|
||||||
|
onOpenContact: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HeroProps extends ThemeProps {
|
||||||
|
onOpenContact: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ModalProps extends ThemeProps {
|
export interface ModalProps extends ThemeProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavbarProps extends ThemeProps {
|
export interface ServiceItem {
|
||||||
setIsNavyBg: Dispatch<SetStateAction<boolean>>;
|
id: number;
|
||||||
onOpenContact: () => void;
|
title: string;
|
||||||
|
description: string;
|
||||||
|
features: string[];
|
||||||
|
icon: React.ElementType;
|
||||||
|
accent: 'blue' | 'red';
|
||||||
|
gridCols: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeroProps extends ThemeProps {
|
export type { Dispatch, SetStateAction };
|
||||||
onOpenContact: () => void;
|
|
||||||
}
|
|
||||||
+29
-2
@@ -1,8 +1,35 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,svg,png,ico,woff2}'],
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: /^https:\/\/fonts\./i,
|
||||||
|
handler: 'CacheFirst',
|
||||||
|
options: { cacheName: 'google-fonts', expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
manifest: {
|
||||||
|
name: 'Rynext Technologies',
|
||||||
|
short_name: 'Rynext',
|
||||||
|
description: 'Ingeniería de software empresarial desde la arquitectura.',
|
||||||
|
theme_color: '#001f5b',
|
||||||
|
background_color: '#ffffff',
|
||||||
|
display: 'standalone',
|
||||||
|
icons: [
|
||||||
|
{ src: '/icons/Logo_blanco.svg', sizes: 'any', type: 'image/svg+xml' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
Reference in new issue
Block a user