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
+7316 -31188

No files matched your search

-12
View File
@@ -1,12 +0,0 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "Landing",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 5173,
"autoPort": true
}
]
}
+3
View File
@@ -11,6 +11,9 @@ node_modules
dist
dist-ssr
*.local
.next
.claude
out
# Editor directories and files
.vscode/*
-39
View File
@@ -1,39 +0,0 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- 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>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+9
View File
@@ -0,0 +1,9 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
output: 'export',
trailingSlash: true,
};
export default nextConfig;
-7955
View File
File diff suppressed because it is too large. Load diff
+2766 -3970
View File
File diff suppressed because it is too large. Load diff
+11 -18
View File
@@ -2,36 +2,29 @@
"name": "sitio-rynext",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.3",
"@tanstack/react-query": "^5.102.4",
"framer-motion": "^12.43.0",
"lucide-react": "^1.28.0",
"next": "^16.3.3",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router": "^8.3.0",
"react-icons": "^5.7.0",
"tailwindcss": "^4.3.3"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@tailwindcss/postcss": "^4.3.3",
"@types/node": "^24.13.3",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.4",
"eslint": "^10.8.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^17.7.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.65.0",
"vite": "^8.2.0",
"vite-plugin-pwa": "^1.3.0",
"workbox-window": "^7.4.1"
"eslint": "^9.0.0",
"eslint-config-next": "^15.3.0",
"typescript": "~6.0.2"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
-3
View File
@@ -1,3 +0,0 @@
/sitemap.xml /sitemap.xml 200
/robots.txt /robots.txt 200
/* /index.html 200
+554 -5971
View File
File diff suppressed because it is too large. Load diff

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 42 KiB

-24
View File
@@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

+554 -5971
View File
File diff suppressed because it is too large. Load diff

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 42 KiB

+556 -5973
View File
File diff suppressed because it is too large. Load diff

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 42 KiB

-1
View File
@@ -1 +0,0 @@
<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

Before

Width:  |  Height:  |  Size: 90 KiB

-1
View File
@@ -1 +0,0 @@
<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

Before

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

-4
View File
@@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://rynext.com.mx/sitemap.xml
-23
View File
@@ -1,23 +0,0 @@
<?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>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="BlockRSC" stopProcessing="true">
<match url=".*\.txt\?rsc=.*" />
<action type="AbortRequest" />
</rule>
</rules>
</rewrite>
<staticContent>
<mimeMap fileExtension=".webp" mimeType="image/webp" />
<mimeMap fileExtension=".woff2" mimeType="font/woff2" />
</staticContent>
</system.webServer>
</configuration>
-44
View File
@@ -1,44 +0,0 @@
import { Routes, Route } from 'react-router';
import { Navbar } from './components/Navbar';
import { Footer } from './components/Footer';
import { FloatingButtons } from './components/FloatingButtons';
import { ScrollToTop } from './components/ScrollToTop';
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';
import { Privacidad } from './pages/Privacidad';
const CONTACT_EMAIL = '[email protected]';
const openContact = () => {
window.location.href = `mailto:${CONTACT_EMAIL}?subject=Hola%2C%20tengo%20un%20proyecto`;
};
export default function App() {
const isNavyBg = false;
const setIsNavyBg = (_: boolean | ((prev: boolean) => boolean)) => {};
return (
<div
className="min-h-screen overflow-x-hidden font-sans transition-colors duration-500"
style={{ background: '#f9f9f7' }}
>
<ScrollToTop />
<Navbar onOpenContact={openContact} />
<Routes>
<Route path="/" element={<Home isNavyBg={isNavyBg} onOpenContact={openContact} />} />
<Route path="/sobre-nosotros" element={<SobreNosotros />} />
<Route path="/productos" element={<Productos />} />
<Route path="/blog" element={<Blog />} />
<Route path="/privacidad" element={<Privacidad />} />
</Routes>
<Footer isNavyBg={isNavyBg} />
<FloatingButtons isNavyBg={isNavyBg} setIsNavyBg={setIsNavyBg} />
<CookieBanner isNavyBg={isNavyBg} />
</div>
);
}
+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>
);
}
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="usLine truncated

Before

Width:  |  Height:  |  Size: 8.5 KiB

+22
View File
@@ -0,0 +1,22 @@
'use client';
import { ContactProvider } from '../contexts/ContactContext';
import { ScrollProgress } from './ScrollProgress';
import { ScrollToTop } from './ScrollToTop';
import { Navbar } from './Navbar';
import { Footer } from './Footer';
import { FloatingButtons } from './FloatingButtons';
import { CookieBanner } from './CookieBanner';
export const AppShell = ({ children }: { children: React.ReactNode }) => (
<ContactProvider>
<div className="min-h-screen overflow-x-hidden font-sans" style={{ background: '#f9f9f7' }}>
<ScrollProgress />
<ScrollToTop />
<Navbar />
{children}
<Footer isNavyBg={false} />
<FloatingButtons isNavyBg={false} setIsNavyBg={() => {}} />
<CookieBanner isNavyBg={false} />
</div>
</ContactProvider>
);
+27 -9
View File
@@ -1,29 +1,47 @@
import { type ReactNode } from 'react';
'use client';
import { type ReactNode } from "react";
interface ButtonProps {
children: ReactNode;
onClick?: () => void;
href?: string;
variant?: 'primary' | 'secondary';
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';
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]',
primary:
"bg-brand-red text-white shadow-lg shadow-blue-500/20 hover:bg-[#2563eb]",
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',
? "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 (
<a href={href} className={cls}>
{children}
</a>
);
}
return <button onClick={onClick} className={cls}>{children}</button>;
return (
<button onClick={onClick} className={cls}>
{children}
</button>
);
};
+200 -47
View File
@@ -1,70 +1,223 @@
import { X, Mail } from 'lucide-react';
'use client';
import { useState } from 'react';
import { X, Send, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
import type { ModalProps } from '../types';
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 API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'https://localhost:7001';
type Status = 'idle' | 'loading' | 'success' | 'error';
const initialForm = { nombre: '', empresa: '', email: '', telefono: '', mensaje: '' };
export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
const [form, setForm] = useState(initialForm);
const [status, setStatus] = useState<Status>('idle');
if (!isOpen) return null;
const set = (k: keyof typeof form) =>
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
setForm(f => ({ ...f, [k]: e.target.value }));
const handleClose = () => {
if (status !== 'loading') {
onClose();
setTimeout(() => { setForm(initialForm); setStatus('idle'); }, 300);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form.nombre.trim() || !form.email.trim()) return;
setStatus('loading');
try {
const res = await fetch(`${API_URL}/api/contact`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: form.nombre,
email: form.email,
phone: form.telefono || undefined,
company: form.empresa || undefined,
message: form.mensaje || 'Sin mensaje adicional.',
source: 'landing',
}),
});
if (!res.ok) throw new Error('Error del servidor');
setStatus('success');
} catch {
setStatus('error');
}
};
const card = isNavyBg ? '#0d1b35' : '#ffffff';
const bdr = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.1)';
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866';
const inputStyle: React.CSSProperties = {
width: '100%', borderRadius: 8, padding: '11px 14px',
fontSize: 14, fontFamily: 'inherit', outline: 'none',
transition: 'border-color 150ms',
...(isNavyBg
? { background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff' }
: { background: '#f8fafc', border: '1px solid rgba(0,0,0,0.1)', color: '#0a0a0a' }),
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fadeIn">
<div
className={`relative w-full max-w-md rounded-2xl border p-8 shadow-2xl transition-all ${
isNavyBg
? 'border-white/20 bg-brand-navy text-white'
: 'border-gray-200 bg-white text-brand-black'
}`}
className="fixed inset-0 z-50 flex items-center justify-center p-4"
style={{ background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(6px)' }}
onClick={e => { if (e.target === e.currentTarget) handleClose(); }}
>
<div
className="relative w-full max-w-xl rounded-2xl shadow-2xl"
style={{ background: card, border: `1px solid ${bdr}` }}
>
{/* Header */}
<div className="flex items-center justify-between px-8 pt-8 pb-6" style={{ borderBottom: `1px solid ${bdr}` }}>
<div>
<h3 className="font-display font-bold text-xl" style={{ color: text, letterSpacing: '-0.02em' }}>
Hablemos de tu proyecto
</h3>
<p className="text-sm mt-0.5" style={{ color: sub }}>
Te respondemos en menos de 24 h
</p>
</div>
<button
onClick={onClose}
className="absolute right-4 top-4 cursor-pointer text-gray-400 transition-colors hover:text-brand-blue"
aria-label="Cerrar modal"
onClick={handleClose}
disabled={status === 'loading'}
className="flex h-8 w-8 items-center justify-center rounded-full transition-colors"
style={{ background: 'rgba(0,0,0,0.06)', color: sub, border: 'none', cursor: 'pointer' }}
aria-label="Cerrar"
>
<X size={24} />
<X size={16} />
</button>
<div className="text-center">
<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} />
</div>
<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'}`}>
Puedes escribirnos directamente a nuestro correo oficial de atención y tu cliente de correo se abrirá automáticamente.
</p>
<div className={`mb-6 rounded-xl border p-4 ${isNavyBg ? 'border-brand-blue/30 bg-brand-blue/10' : 'border-brand-blue/30 bg-brand-blue/5'}`}>
<p className="mb-1 text-xs font-semibold tracking-[0.08em] text-brand-blue">
Correo electrónico
</p>
<a
href={mailToHref}
className="block font-mono text-base font-medium underline decoration-brand-blue/50 underline-offset-4"
aria-label="Enviar correo a soporte de Rynext"
>
{supportEmail}
</a>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<a
href={mailToHref}
className="inline-flex cursor-pointer items-center justify-center rounded-xl bg-brand-blue px-4 py-3 text-sm font-bold text-white transition hover:bg-brand-blue/90"
>
Enviar correo
</a>
{/* Success state */}
{status === 'success' ? (
<div className="flex flex-col items-center justify-center gap-4 px-8 py-14 text-center">
<CheckCircle size={48} style={{ color: '#22c55e' }} />
<h4 className="font-display font-bold text-lg" style={{ color: text }}>
¡Mensaje enviado!
</h4>
<p className="text-sm leading-relaxed" style={{ color: sub, maxWidth: '32ch' }}>
Recibimos tu solicitud. Un miembro del equipo te contactará pronto.
</p>
<button
onClick={onClose}
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'
}`}
onClick={handleClose}
className="mt-2 px-6 py-2.5 text-sm font-bold text-white rounded-xl transition hover:opacity-90"
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer' }}
>
Cerrar
</button>
</div>
) : (
/* Form */
<form onSubmit={handleSubmit} className="px-8 py-7 flex flex-col gap-5">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>
Nombre *
</label>
<input
type="text"
placeholder="Tu nombre"
value={form.nombre}
onChange={set('nombre')}
required
style={inputStyle}
/>
</div>
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>
Empresa
</label>
<input
type="text"
placeholder="Nombre de tu empresa"
value={form.empresa}
onChange={set('empresa')}
style={inputStyle}
/>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>
Correo electrónico *
</label>
<input
type="email"
placeholder="[email protected]"
value={form.email}
onChange={set('email')}
required
style={inputStyle}
/>
</div>
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>
Teléfono
</label>
<input
type="tel"
placeholder="+52 998 000 0000"
value={form.telefono}
onChange={set('telefono')}
style={inputStyle}
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>
Cuéntanos tu proyecto
</label>
<textarea
rows={4}
placeholder="¿Qué sistema necesitas? ¿Cuál es el problema que quieres resolver?"
value={form.mensaje}
onChange={set('mensaje')}
style={{ ...inputStyle, resize: 'none' }}
/>
</div>
{status === 'error' && (
<div className="flex items-center gap-2 px-4 py-3 rounded-lg" style={{ background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.2)' }}>
<AlertCircle size={15} style={{ color: '#ef4444', flexShrink: 0 }} />
<p className="text-sm" style={{ color: '#ef4444' }}>
Hubo un error al enviar. Intenta de nuevo o escríbenos a contacto@rynext.com.mx
</p>
</div>
)}
<button
type="submit"
disabled={status === 'loading' || !form.nombre.trim() || !form.email.trim()}
className="inline-flex items-center justify-center gap-2 w-full py-3.5 text-sm font-bold text-white rounded-xl transition hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed"
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer' }}
>
{status === 'loading' ? (
<>
<Loader2 size={16} className="animate-spin" />
Enviando...
</>
) : (
<>
<Send size={15} />
Enviar mensaje
</>
)}
</button>
<p className="text-center text-xs" style={{ color: sub, opacity: 0.7 }}>
Sin spam. Solo te contactamos para hablar de tu proyecto.
</p>
</form>
)}
</div>
</div>
);
+2 -1
View File
@@ -1,3 +1,4 @@
'use client';
import { useState, useEffect } from 'react';
import type { ThemeProps } from '../types';
@@ -45,7 +46,7 @@ export const CookieBanner = ({ isNavyBg }: ThemeProps) => {
<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' }}
style={{ background: '#3b82f6', borderRadius: 3, border: 'none', cursor: 'pointer' }}
>
Aceptar
</button>
-61
View File
@@ -1,61 +0,0 @@
import { motion } from 'framer-motion';
import { ArrowRight } from 'lucide-react';
import { Button } from './Button';
import type { ThemeProps } from '../types';
interface CtaBannerProps extends ThemeProps {
onOpenContact: () => void;
}
export const CtaBanner = ({ onOpenContact, isNavyBg }: CtaBannerProps) => {
const bg = isNavyBg ? '#001f5b' : '#f9f9f7';
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.45)';
return (
<section
className="relative overflow-hidden px-8 py-32 md:px-14 transition-colors duration-500"
style={{ background: bg, color: text, borderBottom: `1px solid ${isNavyBg ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)'}` }}
aria-labelledby="cta-heading"
>
{/* Subtle grid */}
<div
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>
</div>
</motion.div>
</div>
</section>
);
};
+1
View File
@@ -1,3 +1,4 @@
'use client';
import { ArrowUp } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import type { ThemeProps } from '../types';
+80 -97
View File
@@ -1,28 +1,21 @@
import { Mail, MapPin, MessageCircle } from 'lucide-react';
import { Link } from 'react-router';
import type { ThemeProps } from '../types';
const navLinks = [
{ label: 'Sistemas', href: '#machinery' },
{ label: 'Anatomía', href: '#anatomy' },
{ label: 'Proceso', href: '#proceso' },
{ label: 'Principios', href: '#principios' },
];
"use client";
import { Mail, MapPin, MessageCircle } from "lucide-react";
import { FaFacebookF, FaLinkedinIn, FaMapMarkerAlt } from "react-icons/fa";
import Link from "next/link";
import type { ThemeProps } from "../types";
import { pageLinks } from "../data/navigationData";
import { FaRegMessage } from "react-icons/fa6";
import { MdEmail } from "react-icons/md";
export const Footer = ({ isNavyBg }: ThemeProps) => {
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' });
};
const bg = isNavyBg ? "#001040" : "#0a0a0a";
const sub = "rgba(255,255,255,0.45)";
const border = "rgba(255,255,255,0.08)";
return (
<footer
style={{ background: bg, borderTop: `1px solid ${border}` }}
className="px-8 pt-20 pb-10 md:px-14 transition-colors duration-500"
className="px-14 pt-10 transition-colors duration-500"
role="contentinfo"
>
<div className="mx-auto max-w-7xl">
@@ -30,25 +23,36 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
<div className="grid gap-14 md:grid-cols-[1.8fr_1fr_1.2fr]">
{/* Brand */}
<div>
<div className="mb-6 flex items-center gap-3">
<div className="mb-6 flex items-center gap-2.5">
<img
src="/icons/Logo_blanco.svg"
alt="Rynext logo"
className="h-9 w-auto object-contain"
alt="Logo"
className="h-11 w-11 shrink-0 object-contain"
/>
<img
src="/icons/Text_Logo_Blanco.svg"
alt="Rynext"
<div className="flex flex-col leading-none">
<span
style={{
width: 130,
height: 18,
objectFit: 'cover',
objectPosition: 'center 52%',
display: 'block',
flexShrink: 0,
imageRendering: '-webkit-optimize-contrast',
fontFamily: "'Quicksand', sans-serif",
fontWeight: 700,
fontSize: 19,
letterSpacing: "-0.01em",
color: "#ffffff",
}}
/>
>
Rynext
</span>
<span
style={{
fontFamily: "'Quicksand', sans-serif",
fontWeight: 500,
fontSize: 12,
letterSpacing: "0.04em",
color: "rgba(255,255,255,0.45)",
}}
>
Technologies
</span>
</div>
</div>
<p
className="text-sm leading-relaxed mb-8"
@@ -67,18 +71,17 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
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";
e.currentTarget.style.background = "rgba(255,255,255,0.14)";
e.currentTarget.style.color = "#fff";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLAnchorElement).style.background = "rgba(255,255,255,0.07)";
(e.currentTarget as HTMLAnchorElement).style.color = sub;
e.currentTarget.style.background = "rgba(255,255,255,0.07)";
e.currentTarget.style.color = sub;
}}
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/>
</svg>
<FaFacebookF />
</a>
<a
href="https://linkedin.com/company/rynext"
target="_blank"
@@ -87,52 +90,38 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
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";
e.currentTarget.style.background = "rgba(255,255,255,0.14)";
e.currentTarget.style.color = "#fff";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLAnchorElement).style.background =
"rgba(255,255,255,0.07)";
(e.currentTarget as HTMLAnchorElement).style.color = sub;
e.currentTarget.style.background = "rgba(255,255,255,0.07)";
e.currentTarget.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>
<FaLinkedinIn />
</a>
<a
href={`https://wa.me/529984814889?text=${encodeURIComponent('¿En qué podemos ayudarte?')}`}
href={`https://wa.me/529984814889?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";
e.currentTarget.style.background = "rgba(255,255,255,0.14)";
e.currentTarget.style.color = "#fff";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLAnchorElement).style.background =
"rgba(255,255,255,0.07)";
(e.currentTarget as HTMLAnchorElement).style.color = sub;
e.currentTarget.style.background = "rgba(255,255,255,0.07)";
e.currentTarget.style.color = sub;
}}
>
<MessageCircle size={15} />
<FaRegMessage />
</a>
</div>
</div>
{/* Navegación */}
{/* Páginas */}
<div>
<p
className="text-mono-label mb-6"
@@ -141,26 +130,20 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
letterSpacing: "0.12em",
}}
>
Navegación
Páginas
</p>
<ul className="flex flex-col gap-3.5">
{navLinks.map((link) => (
{pageLinks.map((link) => (
<li key={link.href}>
<button
onClick={() => handleNav(link.href)}
<Link
href={link.href}
className="text-sm font-medium transition-colors"
style={{
color: sub,
background: "none",
border: "none",
cursor: "pointer",
padding: 0,
}}
style={{ color: sub, textDecoration: "none" }}
onMouseEnter={(e) => (e.currentTarget.style.color = "#fff")}
onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
>
{link.label}
</button>
</Link>
</li>
))}
</ul>
@@ -178,19 +161,14 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
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 }}
/>
<li className="flex items-center gap-3">
<FaMapMarkerAlt className="text-white text-center" />
<span className="text-sm leading-snug" style={{ color: sub }}>
Cancún, Quintana Roo
<br />
México
Cancún, Quintana Roo. México
</span>
</li>
<li className="flex items-center gap-3">
<Mail size={14} style={{ color: "#e50914", flexShrink: 0 }} />
<MdEmail className="text-white text-center" />
<a
href="mailto:[email protected]"
className="text-sm font-medium transition-colors"
@@ -205,7 +183,7 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
<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 }}
style={{ background: "#3b82f6", borderRadius: 3 }}
>
Hablemos de tu proyecto
</a>
@@ -213,28 +191,33 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
</ul>
</div>
</div>
</div>
{/* Bottom bar */}
{/* Bottom bar — full width */}
<div
className="mt-16 border-t pt-8 text-center"
style={{ borderColor: border }}
className="mt-10 -mx-8 px-8 md:-mx-14 md:px-14"
style={{ borderTop: `1px solid ${border}` }}
>
<div className="flex flex-col items-center gap-2 sm:flex-row sm:justify-center sm:gap-4">
<div className="mx-auto flex max-w-7xl flex-col items-center gap-2 py-5 sm:flex-row sm:justify-between">
<p className="text-xs" style={{ color: "rgba(255,255,255,0.25)" }}>
© {new Date().getFullYear()} Rynext Technologies S.A.S. — Todos los derechos reservados.
© {new Date().getFullYear()} Rynext Technologies — Todos los
derechos reservados.
</p>
<Link
to="/privacidad"
href="/privacidad"
className="text-xs transition-colors"
style={{ color: "rgba(255,255,255,0.25)", textDecoration: "none" }}
onMouseEnter={(e) => (e.currentTarget.style.color = "rgba(255,255,255,0.6)")}
onMouseLeave={(e) => (e.currentTarget.style.color = "rgba(255,255,255,0.25)")}
onMouseEnter={(e) =>
(e.currentTarget.style.color = "rgba(255,255,255,0.6)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.color = "rgba(255,255,255,0.25)")
}
>
Aviso de Privacidad
</Link>
</div>
</div>
</div>
</footer>
);
};
+29
View File
@@ -0,0 +1,29 @@
'use client';
const SQUARES = [0, 1, 2, 3];
const DELAYS = [0, 0.15, 0.15, 0.3];
export default function Loader({ className = "" }: { className?: string }) {
return (
<div className={`flex flex-col items-center justify-center gap-3 ${className}`}>
<div className="grid grid-cols-2 gap-1">
{SQUARES.map((i) => (
<div
key={i}
className="h-3 w-3 rounded-sm bg-blue-500"
style={{
animation: `loaderPulse 1.2s ease-in-out ${DELAYS[i]}s infinite`,
opacity: 0.2,
}}
/>
))}
</div>
<span className="text-xs font-medium text-slate-400 tracking-wide">Cargando...</span>
<style>{`
@keyframes loaderPulse {
0%, 100% { opacity: 0.15; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1); }
}
`}</style>
</div>
);
}
+52 -52
View File
@@ -1,27 +1,33 @@
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X } from "lucide-react";
import { Link, useLocation } from "react-router";
import type { NavbarProps } from "../types";
'use client';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Menu, X } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Button } from './Button';
import { pageLinks } from '../data/navigationData';
const pageLinks = [
{ label: "Sobre Nosotros", href: "/sobre-nosotros" },
];
export const Navbar = ({ onOpenContact }: NavbarProps) => {
export const Navbar = () => {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const location = useLocation();
const pathname = usePathname();
const scrollToContact = () => {
setMenuOpen(false);
const el = document.getElementById('contacto');
if (el) el.scrollIntoView({ behavior: 'smooth' });
else window.location.href = '/#contacto';
};
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 80);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => {
setMenuOpen(false);
}, [location.pathname]);
}, [pathname]);
return (
<>
@@ -34,44 +40,42 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
>
<div
style={{
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",
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" />
<Link href="/" aria-label="Rynext Technologies — inicio" className="flex items-center gap-2.5">
<img
src="/icons/Text_Logo_Negro.svg"
alt="Rynext"
style={{
width: 130,
height: 18,
objectFit: 'cover',
objectPosition: 'center 52%',
display: 'block',
flexShrink: 0,
imageRendering: '-webkit-optimize-contrast',
}}
src="/icons/Logo_negro.svg"
alt="Logo"
className="h-11 w-11 shrink-0 object-contain"
/>
<div className="flex flex-col leading-none">
<span style={{ fontFamily: "'Quicksand', sans-serif", fontWeight: 700, fontSize: 19, letterSpacing: '-0.01em', color: '#111827' }}>
Rynext
</span>
<span style={{ fontFamily: "'Quicksand', sans-serif", fontWeight: 500, fontSize: 12, letterSpacing: '0.04em', color: '#6b7280' }}>
Technologies
</span>
</div>
</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;
const active = pathname === link.href;
return (
<Link
key={link.href}
to={link.href}
href={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")}
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>
@@ -79,9 +83,9 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
})}
<button
onClick={onOpenContact}
onClick={scrollToContact}
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 }}
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer', borderRadius: 3 }}
>
Hablemos
</button>
@@ -91,8 +95,8 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
<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ú"}
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} />}
@@ -109,8 +113,8 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
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" }}
className="fixed inset-x-0 top-[73px] 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"
@@ -119,21 +123,17 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
{pageLinks.map((link) => (
<Link
key={link.href}
to={link.href}
href={link.href}
className="text-left text-base font-semibold"
style={{ color: location.pathname === link.href ? "#0a0a0a" : "#6a6866", textDecoration: "none" }}
style={{ color: 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 }}
>
<Button onClick={scrollToContact} className="w-full py-3 text-center text-sm font-bold text-white">
Hablemos
</button>
</Button>
</div>
</nav>
</motion.div>
+43
View File
@@ -0,0 +1,43 @@
'use client';
import { useEffect, useState } from 'react';
export const ScrollProgress = () => {
const [pct, setPct] = useState(0);
useEffect(() => {
const onScroll = () => {
const el = document.documentElement;
const scrolled = el.scrollTop || document.body.scrollTop;
const total = el.scrollHeight - el.clientHeight;
setPct(total > 0 ? (scrolled / total) * 100 : 0);
};
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<div
aria-hidden="true"
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
height: 2,
zIndex: 100,
background: 'transparent',
pointerEvents: 'none',
}}
>
<div
style={{
height: '100%',
width: `${pct}%`,
background: '#3b82f6',
transition: 'width 80ms linear',
boxShadow: '0 0 8px rgba(59,130,246,0.6)',
}}
/>
</div>
);
};
+3 -2
View File
@@ -1,8 +1,9 @@
'use client';
import { useEffect } from 'react';
import { useLocation } from 'react-router';
import { usePathname } from 'next/navigation';
export const ScrollToTop = () => {
const { pathname } = useLocation();
const pathname = usePathname();
useEffect(() => {
window.scrollTo(0, 0);
+21
View File
@@ -0,0 +1,21 @@
'use client';
import { createContext, useContext, useState } from 'react';
import { ContactModal } from '../components/ContactModal';
interface ContactCtx {
open: () => void;
}
const ContactContext = createContext<ContactCtx>({ open: () => {} });
export const ContactProvider = ({ children }: { children: React.ReactNode }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<ContactContext.Provider value={{ open: () => setIsOpen(true) }}>
{children}
<ContactModal isOpen={isOpen} onClose={() => setIsOpen(false)} isNavyBg={false} />
</ContactContext.Provider>
);
};
export const useContact = () => useContext(ContactContext);
+47
View File
@@ -0,0 +1,47 @@
export const anatomyLayers = [
{
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'],
},
];
+26
View File
@@ -0,0 +1,26 @@
export const faqs = [
{
q: '¿Cuánto tiempo tarda un proyecto típico?',
a: 'Depende del alcance, pero un sistema funcional suele estar en producción entre 8 y 16 semanas. Trabajamos en sprints cortos con entregables medibles, por lo que el cliente tiene visibilidad y control desde la primera semana.',
},
{
q: '¿Trabajan con empresas fuera de Cancún o de México?',
a: 'Sí. Trabajamos de forma remota con empresas en toda México y en otros países. Nuestro proceso está diseñado para operar de manera distribuida sin perder calidad ni comunicación.',
},
{
q: '¿Qué pasa con el mantenimiento y soporte después de la entrega?',
a: 'El sistema se entrega documentado y en producción. Ofrecemos acompañamiento post-lanzamiento: corrección de bugs, evolución del sistema y soporte técnico según el acuerdo de cada proyecto.',
},
{
q: '¿Pueden integrarse con el ERP o los sistemas que ya tenemos?',
a: 'Sí. La integración con sistemas existentes —SAP, Odoo, sistemas legacy, APIs internas— forma parte de nuestro núcleo de trabajo. Antes de proponer cualquier solución auditamos tu stack actual.',
},
{
q: '¿Cómo es el primer paso para arrancar un proyecto?',
a: 'Agendamos una sesión técnica sin costo. En esa llamada analizamos tu operación, identificamos los cuellos de botella y presentamos una ruta concreta: qué se construye, en qué orden y por qué.',
},
{
q: '¿Qué diferencia a Rynext de una agencia de desarrollo tradicional?',
a: 'Las agencias entregan código. Nosotros diseñamos sistemas. Eso significa que antes de escribir una línea, entendemos el negocio, modelamos la arquitectura y validamos que lo que vamos a construir resuelve el problema correcto. El resultado es software que opera, no software que se entrega.',
},
];
+7
View File
@@ -1,3 +1,5 @@
import { LayoutDashboard, ShoppingCart, Workflow, Zap, BarChart3 } from 'lucide-react';
export const machineryData = [
{
id: 1,
@@ -5,6 +7,7 @@ export const machineryData = [
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'],
icon: LayoutDashboard,
},
{
id: 2,
@@ -12,6 +15,7 @@ export const machineryData = [
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'],
icon: ShoppingCart,
},
{
id: 3,
@@ -19,6 +23,7 @@ export const machineryData = [
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'],
icon: Workflow,
},
{
id: 4,
@@ -26,6 +31,7 @@ export const machineryData = [
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'],
icon: Zap,
},
{
id: 5,
@@ -33,5 +39,6 @@ export const machineryData = [
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'],
icon: BarChart3,
},
];
+13
View File
@@ -0,0 +1,13 @@
export const pageLinks = [
{ label: 'Sobre Nosotros', href: '/sobre-nosotros' },
{ label: 'Productos', href: '/productos' },
{ label: 'Blog', href: '/blog' },
{ label: 'Aviso de Privacidad', href: '/privacidad' },
];
export const navLinks = [
{ label: 'Sistemas', href: '#machinery' },
{ label: 'Anatomía', href: '#anatomy' },
{ label: 'Proceso', href: '#proceso' },
{ label: 'Principios', href: '#principios' },
];
+42
View File
@@ -0,0 +1,42 @@
import { Building2, Layers3, ShieldCheck } from 'lucide-react';
export const comparisonRows = [
{
criterion: 'Adaptación al negocio',
saas: 'Configuración limitada y procesos estándar.',
ry: 'Plataforma diseñada para tus reglas y operaciones.',
},
{
criterion: 'Propiedad y evolución',
saas: 'Depende del proveedor y su roadmap.',
ry: 'Roadmap compartido con autonomía técnica real.',
},
{
criterion: 'Integraciones',
saas: 'APIs restringidas y costos extra por conectividad.',
ry: 'Arquitectura abierta para sistemas, ERPs y herramientas.',
},
{
criterion: 'Soporte y continuidad',
saas: 'Respuesta general, soporte tipo ticket.',
ry: 'Equipo senior con acompañamiento directo.',
},
];
export const proofCards = [
{
title: 'Diseño de experiencia',
description: 'UX orientada a adopción, claridad y flujo de trabajo real.',
icon: Layers3,
},
{
title: 'Arquitectura robusta',
description: 'Infraestructura, seguridad y escalabilidad definida desde el inicio.',
icon: ShieldCheck,
},
{
title: 'Equipo senior',
description: 'Product owner, ingeniería y operación trabajando con criterio empresarial.',
icon: Building2,
},
];
+22
View File
@@ -0,0 +1,22 @@
export 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.',
},
];
+27
View File
@@ -0,0 +1,27 @@
export const processSteps = [
{
num: '01',
title: 'ENTENDER',
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.',
},
];
+13
View File
@@ -0,0 +1,13 @@
export 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.' },
];
export const porQueRynext = [
{ 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.' },
];
+22
View File
@@ -0,0 +1,22 @@
export 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'],
},
];
+5
View File
@@ -0,0 +1,5 @@
// Allow importing CSS files as side-effects
declare module '*.css' {
const content: Record<string, string>;
export default content;
}
+6 -16
View File
@@ -1,11 +1,11 @@
@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 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: #e50914;
--color-brand-red: #3b82f6;
--color-bg-light: #f9f9f7;
--color-bg-alt: #f0efec;
--color-border-light: #dddbd5;
@@ -14,16 +14,11 @@
--color-dark-surface: #0f1520;
--color-dark-border: #1c2535;
--font-sans: 'Inter', system-ui, sans-serif;
--font-display: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-sans: 'Quicksand', system-ui, sans-serif;
--font-display: 'Quicksand', 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 {
body {
@@ -37,14 +32,9 @@
}
*:focus-visible {
outline: 2px solid #e50914;
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
::selection {
background: #001f5b;
color: #fff;
}
}
/* Typography scale */
@@ -176,7 +166,7 @@
margin-bottom: 0.35rem;
}
.prose-rynext a {
color: #e50914;
color: #3b82f6;
text-decoration: underline;
text-underline-offset: 3px;
}
-19
View File
@@ -1,19 +0,0 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import './index.css'
import App from './App.tsx'
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then((regs) => {
regs.forEach((r) => r.unregister());
});
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)
-151
View File
@@ -1,151 +0,0 @@
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>
);
};
-24
View File
@@ -1,24 +0,0 @@
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>
);
-137
View File
@@ -1,137 +0,0 @@
import { useEffect } from 'react';
export const Privacidad = () => {
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 S.A.S.</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>
);
};
-110
View File
@@ -1,110 +0,0 @@
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>
);
};
-173
View File
@@ -1,173 +0,0 @@
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>
);
};
@@ -1,53 +1,7 @@
'use client';
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'],
},
];
import { anatomyLayers as layers } from '../data/anatomyData';
export const Anatomy = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
@@ -101,12 +55,12 @@ export const Anatomy = ({ isNavyBg }: ThemeProps) => {
{/* Number */}
<span
className="font-display font-black"
style={{ fontSize: 64, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
style={{ fontSize: 64, lineHeight: 1, color: '#3b82f6', letterSpacing: '-0.04em' }}
>
{layer.num}
</span>
{/* Title + description */}
{/* Title + description + tech */}
<div style={{ paddingTop: 8 }}>
<p
className="font-display font-black mb-1"
@@ -117,9 +71,25 @@ export const Anatomy = ({ isNavyBg }: ThemeProps) => {
<p className="text-xs mb-3" style={{ color: sub }}>
{layer.subtitle}
</p>
<p className="text-sm leading-relaxed" style={{ color: sub, maxWidth: '60ch' }}>
<p className="text-sm leading-relaxed mb-4" style={{ color: sub, maxWidth: '60ch' }}>
{layer.description}
</p>
<div className="flex flex-wrap gap-2">
{layer.tech.map(t => (
<span key={t} style={{
fontSize: 11,
fontWeight: 600,
padding: '3px 10px',
borderRadius: 99,
border: `1px solid ${border}`,
color: isNavyBg ? 'rgba(255,255,255,0.55)' : 'rgba(0,0,0,0.5)',
background: isNavyBg ? 'rgba(255,255,255,0.04)' : 'rgba(59,130,246,0.05)',
letterSpacing: '0.01em',
}}>
{t}
</span>
))}
</div>
</div>
</motion.div>
))}
+87
View File
@@ -0,0 +1,87 @@
"use client";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import Loader from "../components/Loader";
import { useCompanies } from "../services/companies";
import { assetUrl } from "../services/api";
import type { ThemeProps } from "../types";
function ClientsInner({ isNavyBg }: ThemeProps) {
const { data, isLoading } = useCompanies();
const bg = isNavyBg ? "#0a0f1e" : "#f0efec";
const border = isNavyBg ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.07)";
const label = isNavyBg ? "rgba(255,255,255,0.28)" : "rgba(0,0,0,0.28)";
const companies = (data ?? []).filter((c) => c.showInLanding && c.logoUrl);
if (isLoading) {
return (
<section
style={{ background: bg, borderBottom: `1px solid ${border}` }}
className="px-8 py-14 md:px-14"
>
<Loader className="py-8" />
</section>
);
}
if (companies.length === 0) return null;
return (
<section
aria-label="Empresas que confían en Rynext"
style={{ background: bg, borderBottom: `1px solid ${border}` }}
className="px-8 py-14 md:px-14 transition-colors duration-500"
>
<motion.div
className="mx-auto max-w-7xl"
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
>
<p
className="mb-10 text-center text-xs font-semibold uppercase tracking-[0.18em]"
style={{ color: label }}
>
Empresas que confían en Rynext
</p>
<div className="flex flex-wrap items-center justify-center gap-10 md:gap-16">
{companies.map((c) => (
<div key={c.id} title={c.name} style={{ lineHeight: 1 }}>
<img
src={assetUrl(c.logoUrl)}
alt={c.name}
style={{
height: 150,
width: "auto",
maxWidth: 200,
objectFit: "contain",
filter: isNavyBg
? "brightness(0) invert(1) grayscale(1)"
: "grayscale(1)",
opacity: 0.55,
transition: "opacity 250ms",
}}
onMouseEnter={(e) =>
((e.currentTarget as HTMLImageElement).style.opacity = "1")
}
onMouseLeave={(e) =>
((e.currentTarget as HTMLImageElement).style.opacity = "0.6")
}
/>
</div>
))}
</div>
</motion.div>
</section>
);
}
export const Clients = (props: ThemeProps) => {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return <ClientsInner {...props} />;
};
+188
View File
@@ -0,0 +1,188 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Send, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
import type { ThemeProps } from '../types';
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'https://localhost:7001';
type Status = 'idle' | 'loading' | 'success' | 'error';
const initialForm = { nombre: '', empresa: '', email: '', telefono: '', mensaje: '' };
export const ContactSection = ({ isNavyBg }: ThemeProps) => {
const [form, setForm] = useState(initialForm);
const [status, setStatus] = useState<Status>('idle');
const bg = isNavyBg ? '#080c14' : '#f5f5f3';
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866';
const border = isNavyBg ? 'rgba(255,255,255,0.08)' : '#dddbd5';
const card = isNavyBg ? 'rgba(255,255,255,0.03)' : '#ffffff';
const inputStyle: React.CSSProperties = {
width: '100%', borderRadius: 6, padding: '12px 14px',
fontSize: 14, fontFamily: 'inherit', outline: 'none',
transition: 'border-color 150ms',
...(isNavyBg
? { background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', color: '#fff' }
: { background: '#f9f9f7', border: `1px solid ${border}`, color: '#0a0a0a' }),
};
const set = (k: keyof typeof form) =>
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
setForm(f => ({ ...f, [k]: e.target.value }));
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form.nombre.trim() || !form.email.trim()) return;
setStatus('loading');
try {
const res = await fetch(`${API_URL}/api/contact`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: form.nombre,
email: form.email,
phone: form.telefono || undefined,
company: form.empresa || undefined,
message: form.mensaje || 'Sin mensaje adicional.',
source: 'landing',
}),
});
if (!res.ok) throw new Error();
setStatus('success');
setForm(initialForm);
} catch {
setStatus('error');
}
};
return (
<section
id="contacto"
aria-labelledby="contact-heading"
style={{ background: bg, borderTop: `1px solid ${border}` }}
className="px-8 py-28 md:px-14 transition-colors duration-500"
>
<div className="mx-auto max-w-7xl grid gap-16 lg:grid-cols-[1fr_1.2fr] lg:items-start">
{/* Left — copy */}
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
>
<span className="block text-xs font-semibold uppercase tracking-[0.14em] mb-4" style={{ color: '#3b82f6' }}>
Contacto
</span>
<h2 id="contact-heading" className="text-headline mb-6" style={{ color: text, maxWidth: '20ch' }}>
Hablemos de tu proyecto.
</h2>
<p className="text-base leading-relaxed mb-10" style={{ color: sub, maxWidth: '38ch' }}>
Agenda una sesión técnica sin costo. Analizamos tu operación, identificamos los cuellos de botella y presentamos una ruta concreta de implementación.
</p>
<ul className="flex flex-col gap-4">
{[
{ label: 'Respuesta en menos de 24 h' },
{ label: 'Sin compromisos ni presupuesto automático' },
{ label: 'Sesión técnica, no comercial' },
].map((item) => (
<li key={item.label} className="flex items-center gap-3 text-sm" style={{ color: sub }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#3b82f6', flexShrink: 0, display: 'inline-block' }} />
{item.label}
</li>
))}
</ul>
</motion.div>
{/* Right — form */}
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1, ease: [0.22, 1, 0.36, 1] }}
style={{ background: card, border: `1px solid ${border}`, borderRadius: 8, padding: '40px 36px' }}
>
{status === 'success' ? (
<div className="flex flex-col items-center justify-center gap-4 py-10 text-center">
<CheckCircle size={48} style={{ color: '#22c55e' }} />
<h3 className="font-display font-bold text-lg" style={{ color: text }}>¡Mensaje enviado!</h3>
<p className="text-sm leading-relaxed" style={{ color: sub, maxWidth: '32ch' }}>
Recibimos tu solicitud. Un miembro del equipo te contactará pronto.
</p>
<button
onClick={() => setStatus('idle')}
className="mt-2 px-6 py-2.5 text-sm font-bold text-white rounded-lg transition hover:opacity-90"
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer' }}
>
Enviar otro mensaje
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>Nombre *</label>
<input type="text" placeholder="Tu nombre" value={form.nombre} onChange={set('nombre')} required style={inputStyle} />
</div>
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>Empresa</label>
<input type="text" placeholder="Nombre de tu empresa" value={form.empresa} onChange={set('empresa')} style={inputStyle} />
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>Correo electrónico *</label>
<input type="email" placeholder="[email protected]" value={form.email} onChange={set('email')} required style={inputStyle} />
</div>
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>Teléfono</label>
<input type="tel" placeholder="+52 998 000 0000" value={form.telefono} onChange={set('telefono')} style={inputStyle} />
</div>
</div>
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>Cuéntanos tu proyecto</label>
<textarea
rows={4}
placeholder="¿Qué sistema necesitas? ¿Cuál es el problema que quieres resolver?"
value={form.mensaje}
onChange={set('mensaje')}
style={{ ...inputStyle, resize: 'none' }}
/>
</div>
{status === 'error' && (
<div className="flex items-center gap-2 px-4 py-3 rounded-lg" style={{ background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.2)' }}>
<AlertCircle size={15} style={{ color: '#ef4444', flexShrink: 0 }} />
<p className="text-sm" style={{ color: '#ef4444' }}>
Hubo un error al enviar. Intenta de nuevo o escríbenos a contacto@rynext.com.mx
</p>
</div>
)}
<button
type="submit"
disabled={status === 'loading' || !form.nombre.trim() || !form.email.trim()}
className="inline-flex items-center justify-center gap-2 w-full py-3.5 text-sm font-bold text-white rounded-lg transition hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed"
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer' }}
>
{status === 'loading' ? (
<><Loader2 size={16} className="animate-spin" />Enviando...</>
) : (
<><Send size={15} />Enviar mensaje</>
)}
</button>
<p className="text-center text-xs" style={{ color: sub, opacity: 0.6 }}>
Sin spam. Solo te contactamos para hablar de tu proyecto.
</p>
</form>
)}
</motion.div>
</div>
</section>
);
};
+72
View File
@@ -0,0 +1,72 @@
"use client";
import { motion } from "framer-motion";
import { ArrowRight } from "lucide-react";
import { Button } from "../components/Button";
import type { ThemeProps } from "../types";
export const CtaBanner = ({ isNavyBg }: ThemeProps) => {
const scrollToContact = () => {
const el = document.getElementById("contacto");
if (el) el.scrollIntoView({ behavior: "smooth" });
};
const bg = isNavyBg ? "#001f5b" : "#f9f9f7";
const text = isNavyBg ? "#ffffff" : "#0a0a0a";
const sub = isNavyBg ? "rgba(255,255,255,0.45)" : "rgba(0,0,0,0.45)";
return (
<section
className="relative overflow-hidden px-8 py-32 md:px-14 transition-colors duration-500"
style={{
background: bg,
color: text,
borderBottom: `1px solid ${isNavyBg ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)"}`,
}}
aria-labelledby="cta-heading"
>
<div
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={scrollToContact}
className={
isNavyBg ? "shadow-xl shadow-red-900/30" : "rounded-md"
}
>
Hablemos de tu proyecto <ArrowRight size={16} />
</Button>
</div>
</motion.div>
</div>
</section>
);
};
+108
View File
@@ -0,0 +1,108 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Plus } from 'lucide-react';
import type { ThemeProps } from '../types';
import { faqs } from '../data/faqData';
export const FAQ = ({ isNavyBg }: ThemeProps) => {
const [open, setOpen] = useState<number | null>(null);
const bg = isNavyBg ? '#080c14' : '#f5f5f3';
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.5)' : '#6a6866';
const border = isNavyBg ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)';
const rowBg = isNavyBg ? 'rgba(255,255,255,0.03)' : '#ffffff';
return (
<section
id="faq"
aria-labelledby="faq-heading"
style={{ background: bg, color: text, borderBottom: `1px solid ${border}` }}
className="px-8 py-28 md:px-14 transition-colors duration-500"
>
<div className="mx-auto max-w-4xl">
<motion.div
className="mb-14"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
>
<span className="mb-3 block text-xs font-semibold uppercase tracking-[0.14em]"
style={{ color: '#3b82f6' }}>
Preguntas frecuentes
</span>
<h2 id="faq-heading" className="text-headline" style={{ maxWidth: '24ch' }}>
Respuestas antes de la primera llamada.
</h2>
</motion.div>
<div style={{ border: `1px solid ${border}`, borderRadius: 8, overflow: 'hidden' }}>
{faqs.map((faq, i) => {
const isOpen = open === i;
return (
<motion.div
key={i}
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: i * 0.05 }}
style={{
borderBottom: i < faqs.length - 1 ? `1px solid ${border}` : 'none',
background: rowBg,
}}
>
<button
onClick={() => setOpen(isOpen ? null : i)}
aria-expanded={isOpen}
className="w-full flex items-start justify-between gap-6 text-left transition-colors"
style={{ padding: '24px 28px', cursor: 'pointer', background: 'none', border: 'none' }}
>
<span
className="font-display font-bold leading-snug"
style={{ fontSize: '1rem', color: text, letterSpacing: '-0.01em' }}
>
{faq.q}
</span>
<span
style={{
flexShrink: 0,
width: 28,
height: 28,
borderRadius: '50%',
border: `1.5px solid ${isOpen ? '#3b82f6' : border}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: isOpen ? '#3b82f6' : sub,
transition: 'transform 250ms, border-color 200ms, color 200ms',
transform: isOpen ? 'rotate(45deg)' : 'rotate(0deg)',
marginTop: 2,
}}
>
<Plus size={14} />
</span>
</button>
<div
className="anatomy-body"
style={{ ...(isOpen ? { gridTemplateRows: '1fr' } : {}) }}
>
<div className="anatomy-body-inner">
<p
className="text-base leading-relaxed"
style={{ padding: '0 28px 24px', color: sub }}
>
{faq.a}
</p>
</div>
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
};
@@ -1,8 +1,10 @@
'use client';
import { motion } from 'framer-motion';
import { ArrowRight } from 'lucide-react';
import type { HeroProps } from '../types';
import { Button } from './Button';
import type { ThemeProps } from '../types';
import { Button } from '../components/Button';
import { SystemMap } from './SystemMap';
import { useContact } from '../contexts/ContactContext';
const ease = [0.22, 1, 0.36, 1] as const;
@@ -19,7 +21,8 @@ function RevealLine({ children, delay = 0 }: { children: React.ReactNode; delay?
);
}
export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
export const Hero = ({ isNavyBg }: ThemeProps) => {
const { open: openContact } = useContact();
const bg = isNavyBg ? '#001f5b' : '#ffffff';
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.45)';
@@ -31,20 +34,14 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
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>
@@ -54,7 +51,6 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
<RevealLine delay={0.32}>negocio.</RevealLine>
</h1>
{/* Description */}
<motion.p
initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
@@ -67,33 +63,26 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
la operación real de tu negocio.
</motion.p>
{/* 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}>
<Button className="rounded-md" variant="primary" onClick={openContact}>
Hablemos de tu proyecto <ArrowRight size={16} />
</Button>
<button
onClick={() => {
document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' });
}}
onClick={() => document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' })}
className="group flex items-center gap-2 text-sm font-semibold transition"
style={{ color: sub }}
>
Explorar la maquinaria
<ArrowRight
size={14}
className="transition-transform group-hover:translate-x-0.5"
/>
<ArrowRight size={14} className="transition-transform group-hover:translate-x-0.5" />
</button>
</motion.div>
</div>
{/* Right column — System Map */}
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
@@ -103,7 +92,6 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
<SystemMap isNavyBg={isNavyBg} />
</motion.div>
</div>
</section>
);
};
@@ -1,3 +1,4 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import type { ThemeProps } from '../types';
@@ -57,9 +58,9 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
aria-label={item.title}
style={{
background: isHov
? isNavyBg ? 'rgba(229,9,20,0.06)' : 'rgba(229,9,20,0.03)'
? isNavyBg ? 'rgba(59,130,246,0.06)' : 'rgba(59,130,246,0.03)'
: cardBg,
border: `1px solid ${isHov ? 'rgba(229,9,20,0.25)' : cardBdr}`,
border: `1px solid ${isHov ? 'rgba(59,130,246,0.25)' : cardBdr}`,
borderRadius: 6,
padding: '28px 28px 32px',
position: 'relative',
@@ -83,7 +84,7 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
fontSize: 96,
fontWeight: 900,
lineHeight: 1,
color: isHov ? 'rgba(229,9,20,0.07)' : numColor,
color: isHov ? 'rgba(59,130,246,0.07)' : numColor,
transition: 'color 220ms',
userSelect: 'none',
pointerEvents: 'none',
@@ -101,13 +102,34 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
left: 0,
right: 0,
height: 2,
background: '#e50914',
background: '#3b82f6',
opacity: isHov ? 1 : 0,
transition: 'opacity 220ms',
borderRadius: '6px 6px 0 0',
}}
/>
{/* Icon */}
<div style={{ position: 'relative', marginBottom: 20 }}>
{(() => { const Icon = item.icon; return Icon ? (
<span style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 44,
height: 44,
borderRadius: 10,
background: isHov
? 'rgba(59,130,246,0.15)'
: isNavyBg ? 'rgba(255,255,255,0.07)' : 'rgba(59,130,246,0.07)',
color: isHov ? '#3b82f6' : isNavyBg ? 'rgba(255,255,255,0.5)' : '#3b82f6',
transition: 'background 220ms, color 220ms',
}}>
<Icon size={20} />
</span>
) : null; })()}
</div>
{/* Content */}
<div style={{ position: 'relative' }}>
<h3
@@ -115,7 +137,7 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
style={{
fontSize: 'clamp(1.1rem, 2vw, 1.3rem)',
letterSpacing: '-0.02em',
color: isHov ? '#e50914' : text,
color: isHov ? '#3b82f6' : text,
transition: 'color 220ms',
}}
>
@@ -1,47 +1,8 @@
'use client';
import { motion } from 'framer-motion';
import { Building2, Layers3, ShieldCheck, X, Check } from 'lucide-react';
import { X, Check } from 'lucide-react';
import type { ThemeProps } from '../types';
const comparisonRows = [
{
criterion: 'Adaptación al negocio',
saas: 'Configuración limitada y procesos estándar.',
ry: 'Plataforma diseñada para tus reglas y operaciones.',
},
{
criterion: 'Propiedad y evolución',
saas: 'Depende del proveedor y su roadmap.',
ry: 'Roadmap compartido con autonomía técnica real.',
},
{
criterion: 'Integraciones',
saas: 'APIs restringidas y costos extra por conectividad.',
ry: 'Arquitectura abierta para sistemas, ERPs y herramientas.',
},
{
criterion: 'Soporte y continuidad',
saas: 'Respuesta general, soporte tipo ticket.',
ry: 'Equipo senior con acompañamiento directo.',
},
];
const proofCards = [
{
title: 'Diseño de experiencia',
description: 'UX orientada a adopción, claridad y flujo de trabajo real.',
icon: Layers3,
},
{
title: 'Arquitectura robusta',
description: 'Infraestructura, seguridad y escalabilidad definida desde el inicio.',
icon: ShieldCheck,
},
{
title: 'Equipo senior',
description: 'Product owner, ingeniería y operación trabajando con criterio empresarial.',
icon: Building2,
},
];
import { comparisonRows, proofCards } from '../data/platformAdvantageData';
export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
return (
@@ -1,28 +1,7 @@
'use client';
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.',
},
];
import { principles } from '../data/principlesData';
export const Principles = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
@@ -74,13 +53,13 @@ export const Principles = ({ isNavyBg }: ThemeProps) => {
transition={{ duration: 0.4, delay: i * 0.06 }}
style={{
padding: '32px 28px 36px',
borderTop: '3px solid #e50914',
borderTop: '3px solid #3b82f6',
borderBottom: `1px solid ${gridGap}`,
}}
>
<span
className="font-display font-black block mb-5"
style={{ fontSize: 56, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
style={{ fontSize: 56, lineHeight: 1, color: '#3b82f6', letterSpacing: '-0.04em' }}
>
{String(i + 1).padStart(2, '0')}
</span>
@@ -1,3 +1,4 @@
'use client';
import { motion } from 'framer-motion';
import type { ThemeProps } from '../types';
@@ -20,10 +21,7 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
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"
@@ -52,7 +50,6 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
</motion.span>
</h2>
{/* Descripción */}
<motion.div
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -62,8 +59,8 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
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 }}
className="mt-1 shrink-0 block"
style={{ background: '#3b82f6', 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
@@ -72,7 +69,6 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
de arquitectura.
</p>
</motion.div>
</div>
</motion.div>
</section>
@@ -1,33 +1,7 @@
'use client';
import { motion } from 'framer-motion';
import type { ThemeProps } from '../types';
const steps = [
{
num: '01',
title: 'ENTENDER',
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.',
},
];
import { processSteps as steps } from '../data/processData';
export const Process = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#001f5b' : '#ffffff';
@@ -77,7 +51,7 @@ export const Process = ({ isNavyBg }: ThemeProps) => {
>
<span
className="font-display font-black block mb-6"
style={{ fontSize: 52, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
style={{ fontSize: 52, lineHeight: 1, color: '#3b82f6', letterSpacing: '-0.04em' }}
>
{step.num}
</span>
@@ -1,3 +1,4 @@
'use client';
import { motion } from 'framer-motion';
import { Check } from 'lucide-react';
import type { ThemeProps } from '../types';
@@ -36,7 +37,7 @@ export const Services = ({ isNavyBg }: ThemeProps) => {
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');
: (isNavyBg ? 'bg-brand-red/15 text-blue-300' : 'bg-brand-red/10 text-brand-red');
return (
<motion.article
@@ -77,7 +78,7 @@ export const Services = ({ isNavyBg }: ThemeProps) => {
size={13}
className={`shrink-0 ${service.accent === 'blue'
? (isNavyBg ? 'text-sky-400' : 'text-brand-blue')
: (isNavyBg ? 'text-red-400' : 'text-brand-red')
: (isNavyBg ? 'text-blue-400' : 'text-brand-red')
}`}
/>
<span className={`text-xs font-medium ${isNavyBg ? 'text-slate-300' : 'text-gray-600'}`}>
+177
View File
@@ -0,0 +1,177 @@
"use client";
import { motion } from "framer-motion";
import Loader from "../components/Loader";
import { useProducts } from "../services/products";
import { assetUrl } from "../services/api";
import type { ThemeProps } from "../types";
export const Showcase = ({ isNavyBg }: ThemeProps) => {
const { data, isLoading } = useProducts();
const products = (data ?? []).filter((p) => p.isActive);
const bg = isNavyBg ? "#080c14" : "#f0efec";
const text = isNavyBg ? "#ffffff" : "#0a0a0a";
const sub = isNavyBg ? "rgba(255,255,255,0.45)" : "#6a6866";
const border = isNavyBg ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)";
const tagBg = isNavyBg ? "rgba(59,130,246,0.1)" : "rgba(59,130,246,0.07)";
return (
<section
id="showcase"
aria-labelledby="showcase-heading"
style={{
background: bg,
color: text,
borderBottom: `1px solid ${border}`,
}}
className="px-8 py-28 md:px-14 transition-colors duration-500"
>
<div className="mx-auto max-w-7xl">
<motion.div
className="mb-16"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
>
<span
className="mb-3 block text-xs font-semibold uppercase tracking-[0.14em]"
style={{ color: "#3b82f6" }}
>
Lo que construimos
</span>
<h2
id="showcase-heading"
className="text-headline"
style={{ maxWidth: "28ch" }}
>
Sistemas reales para operaciones reales.
</h2>
<p
className="mt-4 max-w-xl text-base leading-relaxed"
style={{ color: sub }}
>
No plantillas ni demos genéricos. Cada sistema sale de entender la
operación y construir la arquitectura correcta desde cero.
</p>
</motion.div>
{isLoading ? (
<Loader className="py-20" />
) : products.length > 0 ? (
<div className="grid gap-6 lg:grid-cols-3">
{products.map(({ id, imageUrl, name, description, badges }, i) => (
<motion.article
key={id}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{
duration: 0.55,
delay: i * 0.1,
ease: [0.22, 1, 0.36, 1],
}}
style={{
borderRadius: 14,
border: `1px solid ${border}`,
background: isNavyBg ? "rgba(255,255,255,0.03)" : "#ffffff",
overflow: "hidden",
}}
>
<div
style={{
background: isNavyBg
? "rgba(255,255,255,0.04)"
: "rgba(0,0,0,0.03)",
borderBottom: `1px solid ${border}`,
aspectRatio: "16/9",
overflow: "hidden",
}}
>
{imageUrl ? (
<img
src={assetUrl(imageUrl)}
alt={name}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
display: "block",
}}
/>
) : (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
stroke={sub}
strokeWidth="1.5"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<path d="m21 15-5-5L5 21" />
</svg>
</div>
)}
</div>
<div style={{ padding: "24px 24px 28px" }}>
<h3
className="font-display font-bold mb-2"
style={{
fontSize: "1.05rem",
color: text,
letterSpacing: "-0.01em",
}}
>
{name}
</h3>
<p
className="text-sm leading-relaxed mb-5"
style={{ color: sub }}
>
{description}
</p>
{badges && badges.length > 0 && (
<div className="flex flex-wrap gap-2">
{badges.map((badge) => (
<span
key={badge}
style={{
fontSize: 10.5,
fontWeight: 600,
padding: "3px 9px",
borderRadius: 99,
background: tagBg,
color: "#3b82f6",
letterSpacing: "0.01em",
}}
>
{badge}
</span>
))}
</div>
)}
</div>
</motion.article>
))}
</div>
) : (
<p className="text-sm" style={{ color: sub }}>
Sin productos registrados.
</p>
)}
</div>
</section>
);
};
@@ -1,28 +1,7 @@
'use client';
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'],
},
];
import { stackGroups } from '../data/stackData';
export const Stack = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#0f1520' : '#f9f9f7';
@@ -45,7 +24,7 @@ export const Stack = ({ isNavyBg }: ThemeProps) => {
<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>
<span className="text-mono-label" style={{ color: '#3b82f6' }}>Las herramientas son el medio</span>
</div>
<h2
id="stack-heading"
@@ -1,3 +1,4 @@
'use client';
import { useState } from 'react';
interface Node {
@@ -84,10 +85,10 @@ function tooltipHeight(desc: string): number {
export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
const [active, setActive] = useState<string | null>(null);
const RED = '#e50914';
const RED = '#3b82f6';
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 connStroke = 'rgba(59,130,246,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)';
@@ -130,7 +131,7 @@ export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
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 stroke = isActive ? RED : isConn ? 'rgba(59,130,246,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)')
@@ -151,7 +152,7 @@ export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
<rect
x={node.x} y={node.y}
width={node.w} height={node.h}
fill={`rgba(229,9,20,${fillOpacity})`}
fill={`rgba(59,130,246,${fillOpacity})`}
stroke={stroke}
strokeWidth={isActive ? 1.5 : 1}
rx={2}
+121
View File
@@ -0,0 +1,121 @@
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Loader from "../components/Loader";
import { useTestimonials } from "../services/testimonials";
import type { ThemeProps } from "../types";
export const Testimonial = ({ isNavyBg }: ThemeProps) => {
const { data, isLoading } = useTestimonials();
const [current, setCurrent] = useState(0);
const testimonials = data ?? [];
useEffect(() => {
if (testimonials.length < 2) return;
const t = setInterval(
() => setCurrent((i) => (i + 1) % testimonials.length),
6000,
);
return () => clearInterval(t);
}, [testimonials.length]);
const item = testimonials[current];
const bg = isNavyBg ? "#001f5b" : "#ffffff";
const border = isNavyBg ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)";
const sub = isNavyBg ? "rgba(255,255,255,0.45)" : "#6a6866";
const text = isNavyBg ? "#ffffff" : "#0a0a0a";
if (!isLoading && testimonials.length === 0) return null; // sección se oculta si no hay testimonios
return (
<section
aria-label="Testimonios de clientes"
style={{ background: bg, borderBottom: `1px solid ${border}` }}
className="px-8 py-24 md:px-14 transition-colors duration-500"
>
<div className="mx-auto max-w-4xl">
<div
aria-hidden="true"
className="mx-auto mb-8 flex h-12 w-12 items-center justify-center rounded-full"
style={{ background: "rgba(59,130,246,0.1)" }}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="#3b82f6">
<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z" />
<path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z" />
</svg>
</div>
{isLoading ? (
<Loader className="py-12" />
) : (
<>
<AnimatePresence mode="wait">
<motion.div
key={current}
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -16 }}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
className="text-center"
>
<blockquote>
<p
className="font-display font-bold leading-snug"
style={{
fontSize: "clamp(1.25rem, 2.5vw, 1.75rem)",
color: text,
letterSpacing: "-0.02em",
maxWidth: "36ch",
margin: "0 auto 2.5rem",
}}
>
"{item.quote}"
</p>
</blockquote>
<div className="flex flex-col items-center gap-1">
{item.author && (
<p
className="font-display font-bold"
style={{ color: text, fontSize: "0.95rem" }}
>
{item.author}
</p>
)}
<p
className="text-sm font-semibold"
style={{ color: "#3b82f6" }}
>
{item.company}
</p>
</div>
</motion.div>
</AnimatePresence>
{testimonials.length > 1 && (
<div className="flex justify-center gap-2 mt-10">
{testimonials.map((_, i) => (
<button
key={i}
onClick={() => setCurrent(i)}
style={{
width: i === current ? 24 : 8,
height: 8,
borderRadius: 4,
background: i === current ? "#3b82f6" : sub,
border: "none",
cursor: "pointer",
padding: 0,
transition: "width 300ms, background 300ms",
}}
aria-label={`Testimonio ${i + 1}`}
/>
))}
</div>
)}
</>
)}
</div>
</section>
);
};
+14
View File
@@ -0,0 +1,14 @@
export const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://localhost:7030";
export async function apiFetch<T>(path: string): Promise<T> {
const res = await fetch(`${API_URL}${path}`);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json() as Promise<T>;
}
/** Convierte rutas relativas del API (/uploads/...) a URL absoluta */
export function assetUrl(path?: string | null): string | undefined {
if (!path) return undefined;
if (path.startsWith('http')) return path;
return `${API_URL}${path}`;
}
+23
View File
@@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from './api';
export interface BlogPost {
id: string;
title: string;
slug: string;
summary: string;
content: string;
category: string;
imageUrl?: string;
authorName?: string;
isPublished: boolean;
publishedAt?: string;
}
export function useBlogPosts() {
return useQuery<BlogPost[]>({
queryKey: ['blog'],
queryFn: () => apiFetch<BlogPost[]>('/api/blog'),
staleTime: 1000 * 60 * 5,
});
}
+17
View File
@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from './api';
export interface Company {
id: string;
name: string;
logoUrl?: string;
showInLanding?: boolean;
}
export function useCompanies() {
return useQuery<Company[]>({
queryKey: ['companies'],
queryFn: () => apiFetch<Company[]>('/api/companies'),
staleTime: 1000 * 60 * 5,
});
}
+19
View File
@@ -0,0 +1,19 @@
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from './api';
export interface Product {
id: string;
name: string;
description?: string;
imageUrl?: string;
badges: string[];
isActive: boolean;
}
export function useProducts() {
return useQuery<Product[]>({
queryKey: ['products'],
queryFn: () => apiFetch<Product[]>('/api/products'),
staleTime: 1000 * 60 * 5,
});
}
+40
View File
@@ -0,0 +1,40 @@
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from './api';
export interface TeamMember {
id: string;
firstName: string;
lastName: string;
positionName?: string;
departmentName?: string;
departmentOrder?: number;
positionOrder?: number;
}
export interface Department {
name: string;
order: number;
members: TeamMember[];
}
export function useTeam() {
return useQuery<TeamMember[]>({
queryKey: ['team'],
queryFn: () => apiFetch<TeamMember[]>('/api/employees/public'),
staleTime: 1000 * 60 * 5,
});
}
export function groupByDepartment(members: TeamMember[]): Department[] {
const map = new Map<string, Department>();
for (const m of members) {
const key = m.departmentName ?? 'Sin departamento';
if (!map.has(key)) {
map.set(key, { name: key, order: m.departmentOrder ?? 99, members: [] });
}
map.get(key)!.members.push(m);
}
return Array.from(map.values()).sort((a, b) => a.order - b.order);
}
+31
View File
@@ -0,0 +1,31 @@
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from './api';
interface OnboardingCompleted {
clientCompanyName: string;
testimonialQuote?: string;
testimonialAuthor?: string;
}
export interface Testimonial {
quote: string;
author: string;
company: string;
}
export function useTestimonials() {
return useQuery<Testimonial[]>({
queryKey: ['testimonials'],
queryFn: async () => {
const data = await apiFetch<OnboardingCompleted[]>('/api/onboarding/completed');
return data
.filter(o => o.testimonialQuote)
.map(o => ({
quote: o.testimonialQuote!,
author: o.testimonialAuthor ?? '',
company: o.clientCompanyName,
}));
},
staleTime: 1000 * 60 * 5,
});
}
+2 -5
View File
@@ -1,16 +1,15 @@
import type React from 'react';
import { type Dispatch, type SetStateAction } from 'react';
export interface ThemeProps {
isNavyBg: boolean;
}
export interface NavbarProps {
onOpenContact: () => void;
// no props — uses ContactContext internally
}
export interface HeroProps extends ThemeProps {
onOpenContact: () => void;
// onOpenContact removed — uses ContactContext internally
}
export interface ModalProps extends ThemeProps {
@@ -27,5 +26,3 @@ export interface ServiceItem {
accent: 'blue' | 'red';
gridCols: string;
}
export type { Dispatch, SetStateAction };
-26
View File
@@ -1,26 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"allowArbitraryExtensions": true,
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+38 -4
View File
@@ -1,7 +1,41 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff. Show more