all changes

This commit is contained in:
Emmanuel committed 2026-08-28 21:17:22 -05:00
1 parent cba8d4055b
commit 9dfced04f1
103 files changed
+7378 -31250

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
'use client';
import { Suspense } from 'react';
import dynamic from 'next/dynamic';
import { Hero } from '../sections/Hero';
import { Problem } from '../sections/Problem';
import { Machinery } from '../sections/Machinery';
import { Showcase } from '../sections/Showcase';
import { PlatformAdvantage } from '../sections/PlatformAdvantage';
import { Services } from '../sections/Services';
import { Testimonial } from '../sections/Testimonial';
import { Anatomy } from '../sections/Anatomy';
import { Process } from '../sections/Process';
import { Principles } from '../sections/Principles';
import { FAQ } from '../sections/FAQ';
import { CtaBanner } from '../sections/CtaBanner';
import { ContactSection } from '../sections/ContactSection';
const Clients = dynamic(
() => import('../sections/Clients').then(m => ({ default: m.Clients })),
{ ssr: false }
);
export function HomePage() {
return (
<main id="main-content">
<Hero isNavyBg={false} />
<Suspense fallback={null}>
<Clients isNavyBg={false} />
</Suspense>
<Problem isNavyBg={false} />
<Machinery isNavyBg={false} />
<Showcase isNavyBg={false} />
<PlatformAdvantage isNavyBg={false} />
<Services isNavyBg={false} />
<Testimonial isNavyBg={false} />
<Anatomy isNavyBg={false} />
<Process isNavyBg={false} />
<Principles isNavyBg={false} />
<FAQ isNavyBg={false} />
<CtaBanner isNavyBg={false} />
<ContactSection isNavyBg={false} />
</main>
);
}
+47
View File
@@ -0,0 +1,47 @@
'use client';
import { useEffect } from 'react';
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';
export function NotFoundPage() {
useEffect(() => {
document.title = '404 — Página no encontrada | Rynext Technologies';
}, []);
return (
<main
id="main-content"
className="flex min-h-screen flex-col items-center justify-center px-8 text-center"
style={{ background: '#f9f9f7' }}
>
<p
className="font-display font-black mb-6"
style={{ fontSize: 'clamp(6rem, 20vw, 12rem)', lineHeight: 1, color: '#3b82f6', letterSpacing: '-0.06em', opacity: 0.15 }}
aria-hidden="true"
>
404
</p>
<div style={{ marginTop: '-2rem' }}>
<h1
className="font-display font-black mb-4"
style={{ fontSize: 'clamp(1.5rem, 4vw, 2.5rem)', color: '#0a0a0a', letterSpacing: '-0.03em' }}
>
Página no encontrada.
</h1>
<p className="text-base mb-10 max-w-sm mx-auto" style={{ color: '#6a6866', lineHeight: 1.7 }}>
La página que buscas no existe o fue movida. Regresa al inicio para continuar.
</p>
<Link
href="/"
className="inline-flex items-center gap-2 px-6 py-3 text-sm font-bold text-white transition hover:opacity-90"
style={{ background: '#3b82f6', borderRadius: 6 }}
>
<ArrowLeft size={15} />
Volver al inicio
</Link>
</div>
</main>
);
}
+107
View File
@@ -0,0 +1,107 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import Loader from '../../components/Loader';
import { useBlogPosts } from '../../services/blog';
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 } },
};
function BlogContent() {
const { data, isLoading } = useBlogPosts();
useEffect(() => { document.title = 'Blog | Rynext Technologies'; }, []);
const posts = (data ?? []).map(p => ({
id: p.id,
category: p.category.toUpperCase(),
title: p.title,
summary: p.summary,
publishedAt: p.publishedAt,
imageUrl: p.imageUrl,
slug: p.slug,
}));
return (
<main id="main-content">
<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: '#3b82f6', 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>
<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: '#3b82f6', letterSpacing: '0.18em' }}
initial="hidden" whileInView="visible" viewport={{ once: true }} variants={fadeUp}>
ARTÍCULOS
</motion.p>
{isLoading ? (
<Loader className="py-20" />
) : posts.length > 0 ? (
<div className="grid gap-6 md:grid-cols-3">
{posts.map((art, i) => (
<motion.article key={art.id}
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: art.slug ? 1 : 0.82 }}
>
<div style={{ aspectRatio: '16/9', background: '#f0efec', overflow: 'hidden', position: 'relative' }}>
{art.imageUrl && (
<img src={art.imageUrl} alt={art.title}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none'; }}
/>
)}
<span style={{ position: 'absolute', top: 14, left: 14, background: '#3b82f6', color: '#fff', fontFamily: 'monospace', fontSize: 10, letterSpacing: '0.12em', padding: '3px 8px', borderRadius: 2 }}>
{art.category}
</span>
</div>
<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.title}</h2>
<p className="text-sm leading-relaxed" style={{ color: '#6a6866', flexGrow: 1 }}>{art.summary}</p>
<p className="text-mono-label" style={{ color: '#aaa8a4', letterSpacing: '0.12em', marginTop: 4 }}>
{art.publishedAt
? new Date(art.publishedAt).toLocaleDateString('es-MX', { day: '2-digit', month: 'long', year: 'numeric' })
: 'Próximamente'}
</p>
</div>
</motion.article>
))}
</div>
) : (
<p className="text-sm" style={{ color: '#aaa8a4' }}>Sin artículos publicados.</p>
)}
</div>
</section>
</main>
);
}
export function BlogPage() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return <BlogContent />;
}
+9
View File
@@ -0,0 +1,9 @@
import Loader from '../../components/Loader';
export default function Loading() {
return (
<div className="flex min-h-screen items-center justify-center" style={{ background: '#f9f9f7' }}>
<Loader />
</div>
);
}
+12
View File
@@ -0,0 +1,12 @@
import type { Metadata } from 'next';
import { BlogPage } from './BlogPage';
export const metadata: Metadata = {
title: 'Blog | Rynext Technologies',
description: 'Artículos sobre arquitectura de software, sistemas empresariales y tecnología. Perspectivas del equipo de Rynext Technologies.',
alternates: { canonical: 'https://rynext.com.mx/blog' },
};
export default function Page() {
return <BlogPage />;
}
+188
View File
@@ -0,0 +1,188 @@
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap');
@import "tailwindcss";
@theme {
--color-brand-blue: #0077c2;
--color-brand-navy: #001f5b;
--color-brand-black: #0a0a0a;
--color-brand-red: #3b82f6;
--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: 'Quicksand', system-ui, sans-serif;
--font-display: 'Quicksand', system-ui, sans-serif;
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
}
@layer base {
body {
@apply font-sans antialiased m-0 p-0 overflow-x-hidden;
background-color: #f9f9f7;
color: #0a0a0a;
}
h1, h2, h3, h4, .font-display {
font-family: var(--font-display);
}
*:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
}
/* 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 {
position: relative;
}
.section-wave::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, currentColor, transparent);
opacity: 0.12;
pointer-events: none;
}
/* Bg patterns */
.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;
}
/* Principles hover */
.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;
}
/* Modal fade */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-fadeIn {
animation: fadeIn 200ms ease;
}
/* Aviso de privacidad — contenido editorial */
.prose-rynext h2 {
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 800;
letter-spacing: -0.02em;
color: #0a0a0a;
margin-top: 2.5rem;
margin-bottom: 0.75rem;
}
.prose-rynext p,
.prose-rynext ul {
font-size: 0.95rem;
line-height: 1.75;
color: #4a4846;
margin-bottom: 1rem;
}
.prose-rynext ul {
padding-left: 1.25rem;
list-style: disc;
}
.prose-rynext li {
margin-bottom: 0.35rem;
}
.prose-rynext a {
color: #3b82f6;
text-decoration: underline;
text-underline-offset: 3px;
}
.prose-rynext strong {
color: #0a0a0a;
font-weight: 600;
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.animate-ticker {
animation: none;
}
}
+48
View File
@@ -0,0 +1,48 @@
import type { Metadata, Viewport } from 'next';
import './globals.css';
import { Providers } from './providers';
import { AppShell } from '../components/AppShell';
export const metadata: Metadata = {
metadataBase: new URL('https://rynext.com.mx'),
title: 'Rynext Technologies | Software Empresarial 100% Mexicano',
description:
'Diseñamos y construimos sistemas empresariales a medida. Arquitectura real, sin atajos técnicos.',
openGraph: {
title: 'Rynext Technologies | Software Empresarial',
description: 'Ingeniería de software empresarial desde la arquitectura. Cancún, México.',
locale: 'es_MX',
type: 'website',
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
alt: 'Rynext Technologies',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'Rynext Technologies | Software Empresarial',
description: 'Ingeniería de software empresarial desde la arquitectura. Cancún, México.',
images: ['/og-image.png'],
},
icons: { icon: '/favicon.svg' },
};
export const viewport: Viewport = {
themeColor: '#3b82f6',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<body>
<Providers>
<AppShell>{children}</AppShell>
</Providers>
</body>
</html>
);
}
+9
View File
@@ -0,0 +1,9 @@
import Loader from '../components/Loader';
export default function Loading() {
return (
<div className="flex min-h-screen items-center justify-center" style={{ background: '#f9f9f7' }}>
<Loader />
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { NotFoundPage } from './NotFoundPage';
export default function NotFound() {
return <NotFoundPage />;
}
+45
View File
@@ -0,0 +1,45 @@
import type { Metadata } from 'next';
import { HomePage } from './HomePage';
export const metadata: Metadata = {
title: 'Rynext Technologies | Software Empresarial 100% Mexicano',
description: 'Diseñamos y construimos sistemas empresariales a medida. Arquitectura real, sin atajos técnicos. Cancún, México.',
alternates: { canonical: 'https://rynext.com.mx' },
};
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'SoftwareCompany',
name: 'Rynext Technologies',
url: 'https://rynext.com.mx',
logo: 'https://rynext.com.mx/icons/Logo_negro.svg',
description: 'Diseñamos y construimos sistemas empresariales a medida. Arquitectura real, sin atajos técnicos.',
address: {
'@type': 'PostalAddress',
addressLocality: 'Cancún',
addressRegion: 'Quintana Roo',
addressCountry: 'MX',
},
contactPoint: {
'@type': 'ContactPoint',
email: '[email protected]',
contactType: 'customer support',
availableLanguage: 'Spanish',
},
sameAs: [
'https://www.facebook.com/RynextTechnologies/',
'https://linkedin.com/company/rynext',
],
};
export default function Page() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<HomePage />
</>
);
}
+202
View File
@@ -0,0 +1,202 @@
"use client";
import { useEffect } from "react";
export function PrivacidadPage() {
useEffect(() => {
document.title = "Aviso de Privacidad | Rynext Technologies";
}, []);
return (
<main
id="main-content"
style={{ background: "#ffffff", minHeight: "100vh" }}
>
<section
style={{
paddingTop: "140px",
paddingBottom: "100px",
borderBottom: "1px solid #dddbd5",
}}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-3xl">
<h1 className="text-headline mb-4" style={{ color: "#0a0a0a" }}>
Aviso de Privacidad Integral
</h1>
<p className="text-sm mb-16" style={{ color: "#aaa8a4" }}>
Última actualización: agosto de 2026
</p>
<div className="prose-rynext">
<h2>1. Responsable del tratamiento de datos personales</h2>
<p>
<strong>Rynext Technologies</strong> (en adelante "Rynext"), con
domicilio en Cancún, Quintana Roo, México, es responsable del
tratamiento de sus datos personales conforme a lo establecido en
la Ley Federal de Protección de Datos Personales en Posesión de
los Particulares (LFPDPPP) y su Reglamento.
</p>
<p>
Para cualquier asunto relacionado con el presente Aviso de
Privacidad puede contactarnos en:{" "}
<a href="mailto:[email protected]">contacto@rynext.com.mx</a>
</p>
<h2>2. Datos personales que recabamos</h2>
<p>
Rynext puede recabar las siguientes categorías de datos
personales:
</p>
<ul>
<li>
<strong>Datos de identificación:</strong> nombre completo,
empresa o razón social.
</li>
<li>
<strong>Datos de contacto:</strong> correo electrónico, número
de teléfono.
</li>
<li>
<strong>Datos de navegación:</strong> dirección IP, tipo de
navegador, páginas visitadas y duración de la sesión (mediante
cookies técnicas).
</li>
</ul>
<p>
Rynext <strong>no recaba datos personales sensibles</strong> tales
como origen racial, estado de salud, creencias religiosas, datos
biométricos ni información financiera a través de este sitio web.
</p>
<h2>3. Finalidades del tratamiento</h2>
<p>
Sus datos personales son utilizados para las siguientes{" "}
<strong>finalidades primarias</strong>:
</p>
<ul>
<li>
Atender solicitudes de contacto, cotizaciones y propuestas
comerciales.
</li>
<li>
Dar seguimiento a proyectos de desarrollo de software y
consultoría.
</li>
<li>Cumplir con obligaciones legales y contractuales.</li>
</ul>
<p>
De manera secundaria, y solo con su consentimiento expreso,
podemos utilizar sus datos para:
</p>
<ul>
<li>
Envío de comunicaciones sobre nuevos servicios, actualizaciones
o contenido de interés.
</li>
<li>
Estadísticas internas y mejora de la experiencia en el sitio.
</li>
</ul>
<p>
Si no desea que sus datos sean tratados para las finalidades
secundarias, puede manifestarlo en cualquier momento
escribiéndonos a{" "}
<a href="mailto:[email protected]">contacto@rynext.com.mx</a>
.
</p>
<h2>4. Transferencia de datos personales</h2>
<p>
Rynext no vende, renta ni comercializa sus datos personales a
terceros. Únicamente podremos transferirlos en los siguientes
casos:
</p>
<ul>
<li>
A proveedores de servicios tecnológicos que actúen como
encargados bajo instrucción de Rynext y con las mismas
obligaciones de confidencialidad.
</li>
<li>
Cuando sea requerido por autoridades competentes en cumplimiento
de disposiciones legales.
</li>
</ul>
<p>
En ningún caso sus datos serán transferidos a terceros con fines
distintos a los aquí establecidos sin su consentimiento previo.
</p>
<h2>5. Derechos ARCO</h2>
<p>
Usted tiene derecho a{" "}
<strong>Acceder, Rectificar, Cancelar u Oponerse</strong>{" "}
(derechos ARCO) al tratamiento de sus datos personales, así como a
revocar el consentimiento que haya otorgado para dicho
tratamiento.
</p>
<p>Para ejercer sus derechos ARCO debe enviar una solicitud a:</p>
<p>
<strong>Correo:</strong>{" "}
<a href="mailto:[email protected]">contacto@rynext.com.mx</a>
<br />
<strong>Asunto:</strong> Ejercicio de Derechos ARCO
</p>
<p>Su solicitud deberá contener:</p>
<ul>
<li>
Nombre completo y correo electrónico con el que se registró o
contactó.
</li>
<li>
Descripción clara del derecho que desea ejercer y los datos a
los que se refiere.
</li>
<li>
Cualquier documento que facilite la localización de sus datos.
</li>
</ul>
<p>
Rynext responderá a su solicitud en un plazo máximo de{" "}
<strong>20 días hábiles{" "}</strong>a partir de su recepción.
</p>
<h2>6. Uso de cookies</h2>
<p>
Este sitio utiliza cookies técnicas estrictamente necesarias para
su funcionamiento. No utilizamos cookies de rastreo ni publicidad
comportamental sin su consentimiento. Puede configurar su
navegador para bloquear o eliminar las cookies; sin embargo,
algunas funcionalidades del sitio podrían verse afectadas.
</p>
<h2>7. Cambios al aviso de privacidad</h2>
<p>
Rynext se reserva el derecho de modificar este Aviso de Privacidad
en cualquier momento para reflejar cambios legislativos,
jurisprudenciales o en nuestras prácticas de privacidad. Las
modificaciones serán publicadas en esta página con la fecha de
actualización correspondiente. Se recomienda revisar
periódicamente este aviso.
</p>
<h2>8. Autoridad competente</h2>
<p>
Si considera que sus derechos han sido vulnerados, tiene derecho a
acudir ante el Instituto Nacional de Transparencia, Acceso a la
Información y Protección de Datos Personales (INAI) en{" "}
<a
href="https://www.inai.org.mx"
target="_blank"
rel="noopener noreferrer"
>
www.inai.org.mx
</a>
.
</p>
</div>
</div>
</section>
</main>
);
}
+9
View File
@@ -0,0 +1,9 @@
import Loader from '../../components/Loader';
export default function Loading() {
return (
<div className="flex min-h-screen items-center justify-center" style={{ background: '#ffffff' }}>
<Loader />
</div>
);
}
+13
View File
@@ -0,0 +1,13 @@
import type { Metadata } from 'next';
import { PrivacidadPage } from './PrivacidadPage';
export const metadata: Metadata = {
title: 'Aviso de Privacidad | Rynext Technologies',
description: 'Aviso de privacidad de Rynext Technologies. Conoce cómo tratamos y protegemos tus datos personales.',
alternates: { canonical: 'https://rynext.com.mx/privacidad' },
robots: { index: false },
};
export default function Page() {
return <PrivacidadPage />;
}
+100
View File
@@ -0,0 +1,100 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import Loader from '../../components/Loader';
import { useProducts } from '../../services/products';
import { assetUrl } from '../../services/api';
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 } },
};
function ProductosContent() {
const { data: products, isLoading } = useProducts();
useEffect(() => { document.title = 'Productos | Rynext Technologies'; }, []);
const active = (products ?? []).filter(p => p.isActive);
return (
<main id="main-content">
<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: '#3b82f6', 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>
<section
style={{ background: '#f5f5f3', borderBottom: '1px solid #dddbd5', padding: '100px 0', minHeight: '40vh' }}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-7xl">
{isLoading ? (
<Loader className="py-20" />
) : active.length === 0 ? (
<p className="text-sm" style={{ color: '#aaa8a4' }}>Sin productos disponibles.</p>
) : (
<div className="flex flex-col gap-8">
{active.map((product, i) => (
<motion.div key={product.id}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: i * 0.08, ease: [0.22, 1, 0.36, 1] as const } } }}
style={{ background: '#ffffff', border: '1px solid #dddbd5', borderRadius: 4, padding: '60px 56px', display: 'flex', flexDirection: 'column', gap: 24, maxWidth: 720 }}>
{product.imageUrl && (
<img src={assetUrl(product.imageUrl)} alt={product.name} style={{ width: '100%', maxWidth: 480, borderRadius: 4, objectFit: 'cover', aspectRatio: '16/9' }} />
)}
<div>
<p className="text-mono-label mb-2" style={{ color: '#aaa8a4', letterSpacing: '0.14em' }}>PRODUCTO</p>
<h2 className="text-headline" style={{ color: '#0a0a0a' }}>{product.name}</h2>
</div>
{product.description && (
<p className="text-base leading-relaxed" style={{ color: '#6a6866', maxWidth: '48ch' }}>{product.description}</p>
)}
{product.badges && product.badges.length > 0 && (
<div className="flex flex-wrap gap-2">
{product.badges.map(badge => (
<span key={badge} style={{ fontSize: 11, fontWeight: 600, padding: '4px 10px', borderRadius: 99, background: 'rgba(59,130,246,0.08)', color: '#3b82f6' }}>
{badge}
</span>
))}
</div>
)}
<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: '#3b82f6', borderRadius: 3 }}>
Solicitar demo
</a>
</div>
</motion.div>
))}
</div>
)}
</div>
</section>
</main>
);
}
export function ProductosPage() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return <ProductosContent />;
}
+9
View File
@@ -0,0 +1,9 @@
import Loader from '../../components/Loader';
export default function Loading() {
return (
<div className="flex min-h-screen items-center justify-center" style={{ background: '#f9f9f7' }}>
<Loader />
</div>
);
}
+12
View File
@@ -0,0 +1,12 @@
import type { Metadata } from 'next';
import { ProductosPage } from './ProductosPage';
export const metadata: Metadata = {
title: 'Productos | Rynext Technologies',
description: 'Explora los productos y soluciones empresariales de Rynext Technologies. Sistemas a medida para la realidad operativa de los negocios mexicanos.',
alternates: { canonical: 'https://rynext.com.mx/productos' },
};
export default function Page() {
return <ProductosPage />;
}
+10
View File
@@ -0,0 +1,10 @@
'use client';
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: { queries: { retry: 1, staleTime: 60_000 } },
}));
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
+9
View File
@@ -0,0 +1,9 @@
export const dynamic = 'force-static';
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/' },
sitemap: 'https://rynext.com.mx/sitemap.xml',
};
}
+12
View File
@@ -0,0 +1,12 @@
export const dynamic = 'force-static';
import type { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
return [
{ url: 'https://rynext.com.mx', changeFrequency: 'monthly', priority: 1 },
{ url: 'https://rynext.com.mx/sobre-nosotros', changeFrequency: 'monthly', priority: 0.8 },
{ url: 'https://rynext.com.mx/productos', changeFrequency: 'monthly', priority: 0.8 },
{ url: 'https://rynext.com.mx/blog', changeFrequency: 'weekly', priority: 0.6 },
{ url: 'https://rynext.com.mx/privacidad', changeFrequency: 'yearly', priority: 0.3 },
];
}
@@ -0,0 +1,179 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import Loader from '../../components/Loader';
import { useTeam, groupByDepartment } from '../../services/team';
import { valores, porQueRynext } from '../../data/sobreNosotrosData';
const TEAM_IMAGE = 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200&q=80';
function SobreNosotrosContent() {
const { data: members, isLoading: loadingTeam } = useTeam();
const departments = groupByDepartment(members ?? []);
useEffect(() => {
document.title = 'Sobre Nosotros | Rynext Technologies';
}, []);
return (
<main id="main-content">
{/* Hero */}
<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">
<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: '#3b82f6' }}>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="#3b82f6" 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>
<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' }}>
<div style={{ position: 'absolute', top: 16, left: -16, width: '100%', height: '100%', border: '3px solid #3b82f6', borderRadius: 2, zIndex: 0 }} />
<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: '#3b82f6' }}>Rynext Technologies?</span>
</motion.h2>
<div className="flex flex-col divide-y" style={{ borderTop: '1px solid #dddbd5', borderBottom: '1px solid #dddbd5' }}>
{porQueRynext.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: '#3b82f6', 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 #3b82f6', paddingTop: 24 }}>
<span className="font-display font-black block mb-4" style={{ fontSize: 48, lineHeight: 1, color: '#3b82f6', 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>
{/* Equipo */}
<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-headline mb-16" style={{ color: '#0a0a0a' }}
initial={{ opacity: 0, y: 24 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}>
El equipo detrás del código.
</motion.h2>
{loadingTeam ? (
<Loader className="py-16" />
) : departments.length === 0 ? (
<p className="text-sm" style={{ color: '#aaa8a4' }}>Sin miembros registrados.</p>
) : (
<div className="flex flex-col gap-14">
{departments.map((dept, di) => (
<motion.div key={dept.name}
initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}
transition={{ duration: 0.5, delay: di * 0.08, ease: [0.22, 1, 0.36, 1] }}>
{/* Dept header */}
<div className="flex items-center gap-3 mb-0">
<span style={{ width: 3, height: 16, background: '#3b82f6', borderRadius: 99, flexShrink: 0, display: 'block' }} />
<p className="text-xs font-semibold uppercase tracking-widest" style={{ color: '#6a6866' }}>
{dept.name}
</p>
</div>
{/* Hierarchy connector */}
<div style={{ position: 'relative', paddingLeft: 20, marginLeft: 1 }}>
{/* Vertical line */}
<span style={{ position: 'absolute', left: 0, top: 0, bottom: 24, width: 1, background: '#dddbd5' }} />
{/* Horizontal branch */}
<span style={{ position: 'absolute', left: 0, top: 24, width: 16, height: 1, background: '#dddbd5' }} />
<div className="grid gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 pt-6">
{dept.members.map((m, mi) => (
<motion.div key={m.id}
initial={{ opacity: 0, y: 12 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}
transition={{ duration: 0.4, delay: mi * 0.05, ease: [0.22, 1, 0.36, 1] }}
style={{ background: '#ffffff', borderRadius: 6, padding: '20px', display: 'flex', alignItems: 'center', gap: 14, boxShadow: '0 1px 3px rgba(0,0,0,0.07)' }}>
<div style={{ width: 44, height: 44, borderRadius: '50%', background: 'linear-gradient(135deg,#3b82f6,#1d4ed8)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ color: '#fff', fontWeight: 700, fontSize: 14, letterSpacing: '0.02em' }}>{m.firstName[0]}{m.lastName[0]}</span>
</div>
<div style={{ minWidth: 0 }}>
<p className="font-display font-bold text-sm" style={{ color: '#0a0a0a', lineHeight: 1.3, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{m.firstName} {m.lastName}</p>
{m.positionName && <p className="text-xs mt-0.5" style={{ color: '#9ca3af' }}>{m.positionName}</p>}
</div>
</motion.div>
))}
</div>
</div>
</motion.div>
))}
</div>
)}
</div>
</section>
</main>
);
}
export function SobreNosotrosPage() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return <SobreNosotrosContent />;
}
+9
View File
@@ -0,0 +1,9 @@
import Loader from '../../components/Loader';
export default function Loading() {
return (
<div className="flex min-h-screen items-center justify-center" style={{ background: '#f9f9f7' }}>
<Loader />
</div>
);
}
+12
View File
@@ -0,0 +1,12 @@
import type { Metadata } from 'next';
import { SobreNosotrosPage } from './SobreNosotrosPage';
export const metadata: Metadata = {
title: 'Sobre Nosotros | Rynext Technologies',
description: 'Conoce al equipo detrás de Rynext Technologies. Software empresarial 100% mexicano de clase mundial, construido con arquitectura real.',
alternates: { canonical: 'https://rynext.com.mx/sobre-nosotros' },
};
export default function Page() {
return <SobreNosotrosPage />;
}
+13
View File
@@ -0,0 +1,13 @@
'use client';
import { motion } from 'framer-motion';
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0, transition: { duration: 0.35, ease: [0.22, 1, 0.36, 1] } }}
>
{children}
</motion.div>
);
}