all changes
No files matched your search
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.0.1",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Landing",
|
|
||||||
"runtimeExecutable": "npm",
|
|
||||||
"runtimeArgs": ["run", "dev"],
|
|
||||||
"port": 5173,
|
|
||||||
"autoPort": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,9 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
.next
|
||||||
|
.claude
|
||||||
|
out
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
output: 'export',
|
||||||
|
trailingSlash: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -2,36 +2,29 @@
|
|||||||
"name": "sitio-rynext",
|
"name": "sitio-rynext",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "next dev",
|
||||||
"build": "tsc -b && vite build",
|
"build": "next build",
|
||||||
"lint": "eslint .",
|
"start": "next start",
|
||||||
"preview": "vite preview"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tanstack/react-query": "^5.102.4",
|
||||||
"framer-motion": "^12.43.0",
|
"framer-motion": "^12.43.0",
|
||||||
"lucide-react": "^1.28.0",
|
"lucide-react": "^1.28.0",
|
||||||
|
"next": "^16.3.3",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"react-router": "^8.3.0",
|
"react-icons": "^5.7.0",
|
||||||
"tailwindcss": "^4.3.3"
|
"tailwindcss": "^4.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@types/node": "^24.13.3",
|
"@types/node": "^24.13.3",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.4",
|
"eslint": "^9.0.0",
|
||||||
"eslint": "^10.8.0",
|
"eslint-config-next": "^15.3.0",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"typescript": "~6.0.2"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
/sitemap.xml /sitemap.xml 200
|
|
||||||
/robots.txt /robots.txt 200
|
|
||||||
/* /index.html 200
|
|
||||||
|
Before Width: | Height: | Size: 449 KiB After Width: | Height: | Size: 42 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 449 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 449 KiB After Width: | Height: | Size: 42 KiB |
@@ -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,iVBORw0KGgoAAAANSUhEUgAABgAAAAQACAAAAAACGs2eAAAAAmJLR0QA/4ePzL8AACAASURBVHic7d13vCRFgTjw6pn33gYyCAhiOhM5Z06SSFBJggqicugpiooepsMcDrzTO8WfAsKZARMcIiJJkoDg7gILCIpgAMEAKiyyy+5LU78/3u6yoadmpqdn5r2d7/fz4cO+qe6qmp7uquruCiEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALJb1OgO9teG6M7NqllVDqMUQQohhNIQQQ8iyUMkqlUpWCWG5oxRDXPrvLISQhZiFEML4REisTXyaZbWx4UVP/G24nHxuseai0dVnLJhRzbJaCDHGOBayUMmyyuC0WHmi9tNyklnemptMC7EaY602HmMWJhKMq40Oxqz62MNDM37XSmTT/2m4Mn1w0fBAyLJKNcsGK6vPqz6+4KEO5HujjcbWHatkIVQqWahkcTzUxkb/MXpfu/E+Z2StoUVrZ5WQhSzLFp8JoyHELMRQW3yWrHhBZSGEEENYch5VF38wGiZOpGzidFu6/XiIMdRCqIVKyLIsLsrGRv/xt9Yz+9xKZcbqT4a1BmIWQwwxZllWybIwUKlkoRbjbX8ucATq2mxwzawSQozZSAghyyphKAu1GMdqi/44OFKJjzSOYoNnx5GQDVeq1SyrVLIsThyyGGsx1EKIEwdq8dVXDZXFhy3GWqhNfB6zEBYftSwMxYnLeDyrxTgeQ1YJIQtZJYQQs/EYRmMIWayNjD5YK/NAMLV8PZZtrFar1Wq1iT+GH5r73Y/u97T283nQkyumMz4+vjiROBrjqe0nsZIN7qvNHxkbr9XGRkcWLRweHh4eGR0bn0hywaL5i17XUmyfGI212tjo6OjI8MjI6OhYbXw01sYW7daBjN8Ua+Mr/zCXtxvt1o/HRSOxtnLMJavVxsfHxpdJZ9GXC+T2qNGxsWXzWhsfry3798L12z0ey7hg5Mnh8XpHZmzBw3s1Ecc3O3AomzDy3yUehylooNcZ6K3ppcdYXfaPoWc8Y9swPu93cy67cqStWC875IrK8p8s8+dACCePfqyt+HOsdcXzw2ohhBW+0oQsDB773ZaiiwMhWy6mrBKyavWG/a4rnsW6spwb2zXbjXR8zTCtG7fM2QppjE1bu0As353/o+VjXSHj03/x/PkFos119b5hsH5odfiQWU1EMlRWbloz2Oc3AJXGm9Ce6no7nfCDu772z21FctX+MRn+kbe2Ff3KVr9u20To4FiL5X+Ylv9x9ZrWbiSKJ5U+fk2IbcdQzEBYUGS3S16Rzu+GP1+nUHZWMvP2fZPhC7ZppvwPpVVHLRrtUbqTRJ9XAN1qdgy84LhrZu/cTgxXvyp5PWdffE07sa9k5i2p8j+Ez7VY/od6Da3snE+0GFND+T9qoo3anOk9ayvm3II14cd7pV9AbTGrlPvfGbO2SYYvevmDTcVT7Eu2b0aP0p0k+rwC6OITsMGdbpq9XRv7/98rkwXQwLkvayPyFQ1d+qJU8MiZH201xpl1Qz70tZJPwidzP61zC9K8rFdFVN3Ks4EbXrooGf6C+0p4O7Xa7C2T4Yv2bbKDQq9KooU9SneS6PMKoKuNuupO15/WxvG+6ODkPcDAj/YpHvcKBr+bfm13xoktR/mPuiHVY6+qXzsUkV9Qt118j7UbQWFFW+o37F//qIcQwia3blAw5qWmzUmX/2N739xkTL3qj7huj9KdJPq8AujyWbf6u+7evPjelx6ZfJNc+dF+xeNezsCZh6eCa2f9W+txJsrfyj4/K6Ep2kiPnuCXYbzojjfs+Vgy/Fm3blQ06glr3rFZMnxs36ae/4fQ9x3Se6XPK4Culwqbzjq4+M4Xvjz5VHe1i/YvHvcyhj77plRw7ayyXziHbW99XomxTeGiPl/x+9Q7dv99MnyTOc8uHHcIYd25ySeFYf5eN7QTfVfoBUQ3rX5xG8XnVUcmO4TMvGj34nEvVf3Qu1PB8YsnFIk1XSo/687ynl/VMYVbmG3UaPcc+Ggy/BlznlU88qfd+U/J8HkH3FQ88m5RAdBdZxYqQCdc8qb8N5wTshk/LqEl/YGPpELHzk5WD3U1uM5mXv22QtHm6dAdQK+6gbb3he7dIj3kd/1Zhe8Bnjb7Gcnwxw5qpfzvVUnU5yVgn3/9nvjSgcX3/d47ku8B1p79/OJxT3jPKam28sJLCt7ANGpoZWd8oVjEOVHlftr+md6zCqCtm5e/7PhAMvzpt7ygWMQb3fbcZPi8PX/eSnS9ukNb5R4YtqbPK4Ce/PrZhemWU9LXT0oWpevOfmbxuEMI4YTk0PjRucnXw205se3JGhbLL0raLmCm6rOCP23/22T402alh3zU8Zy56TPtr7vcVSRauqvPK4DeNDtmXNHGALTT35O8B1jn5+sVjzuEN5+eDJ67R9GIm6hqD7hlUp+NvXsEVLgX0IRHt7o3Gb7OzemhXLm2mbNhMvyBndOJrmRS//arrj4/7D26prf4cBs7n3Zyskf6xr9J35knHX1mKrR2xy7Fo27CDr9pu196CPV+1LbP9N49K2g35YW7/CoZPn1uy93Hdrs+3XP3D9vf32KMvTq8HgH1s/amaCvu5HT3ubTPfTw5f8naPysc+UHfTA2XivcWelYwoalW7HNv3bV4CkvlH522z/RKz4qKts/SedumH8dkFx3RWoT7XJueW++hHdKdj/Ly0OoOJenzErDPv36vnusOfLadvU95f3I8wEY3bFEs3pdcnJww5670oJ8ybHJdCWMMOlRQD/fqWvnbt9uOYmS7W5LhM85paU6+/a9OT6xx92atr2DQqwqgV23ASaLPK4CeNeoObmtmuNP+Mxm8/hWFOnfvc1Line truncated
|
|
||||||
|
Before Width: | Height: | Size: 90 KiB |
@@ -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,iVBORw0KGgoAAAANSUhEUgAABgAAAAQACAAAAAACGs2eAAAAAmJLR0QA/4ePzL8AACAASURBVHic7N13vBTV/fj/95m9DS5VQFQUEbAiTUGQ3gVBVCwxRo3RxPRPEpN8Psk3n1ST/NL9fFJMTywfe0UB6YgURRFQpKgIIl16v23n/P7Ye+Xey92d2d1zZnZnX8/H52P07tlz3rMzc95TzxEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAvlFhBwBYpEREiaq3nSsRUVpE1/534l8T5ZrYHZrYQfSJL2oR/XFNQN4hAcBTaamjHOU4jhMrKiouLSktKSkpKi4qckQkHtda6Xh1vKampqa6pjpeI1qLKKVEEr2j1ok/iIgoXRyLxVzXdaXYcUTcIseJOcpRStxEP6ocxxGlVMyJi1IxR9yYE4s5MV1Uu6kqpRyn2FEiOi5S5CilHMdRSinlOEqJEi1KRBWpmFYiIo5ylJNoX0nMLVLKUaJdNy5KlGjtKlcrpWOOiomKibgiokRLXDnKVUqU40qir0/8j5MooUS062ql3bgbr6mpqYnHq6sqjx09cujQgaPHK6sq3UDXEZAJEgC8nDv+wrYtWjQrKy0uchJ5QDmOUspJdOqJQ2Ht6sQ/6g6JVeLYWmul41qLUqJEKXESva5WSok68ecT1MdH7br20F0lStQlAEl8Q4mWnNt+tRt3a+I11VUV+z/atXPr5o37D1eEHROQXFHYASDndRl5+Wlhx9CUHOv8RURULFbvv2qOHz204/331q/feYjTAeQkEgC8vLmdrSQjRS1bnnbecDl+aM/aFa+8c/B42PEAjeXgURRyTZ8fTWY7yY4+sGfrupWvbD4SdiBAfezY8Nb73pFhhxAFFbvWLJ6+kRyA3BHzLoKCt2t1z7PCjiECitqcO2LiiHOOHq0MOxIggTMA+DHgL33CDiEy9r47+/HNx8KOAhASAHwa8aeLwg4hQnaue+3JdeQAhI5LQPDlg02D24QdQ3S0OGfI1SNKtx0NOw4UOhIA/NmwdWjLsGOIkhbdRo/tvONQPOw4UNBIAPBp3e5hzcOOIVJKOg275pJdu2vCjgMFjAQAv97cPYQMYFarnlcOqNhaHXYYKFgkAPi2as+wZmHHEDUtLhjbc82esKNAoSIBwL+VnAOY1/zi0W3f4+0whIIEgDSs3D2qNOwYoqf98DF6PdeBEAISANKx6uDI4rBjiB7n9JEXrf8o7ChQgEgASMvr1WPDDiGKSnpMbLua4UIRNBIA0rO0dGjYIURSm0F9d25mdkkEiwSANM07tX/YIURSrPu4dis5CUCgSABI14wuDAxnRYuBF6/fGXYQKCgkAKRt6oUXhx1CNDnnj6xey5vBCA4JAOl75nwygB3thrRfwRBxCAwJAOnTz5zXM+wYIqpsQM/NH4YdBAoGCQCZeLon0wPYoboPqljthh0FCgQJABl5svcFYYcQVe2HlL3Ge8EIBAkAmXm8DxnAkmb9W77KtMEIAgkAGXq8/3lhhxBVJf1PX8qEkQgACQCZenRA97BDiKpY366LGSAU9pEAkLHHBnUNO4TIuqjHsn1hx4DoIwEgY/qx/pwD2NK9xxu7w44BkUcCQObcR/ufG3YMkdW155s7wo4BUUcCQBb0E1wFsubsXm9vCzsGRBwJANlwHx/ZOewYIuusHm9uDzsGRBsJAFmJPzbyrLBjiKyzLlzGfQDYpMIOAPmu+dLeYYcQXfM/tzHsEBBlTtgBIN8dG/522CFE16g/c34FizgDQNbaL2JUCGum37kr7BAQXZwBIGt7RqwNO4Tomnhf+7BDQHRxExjZO/r0lR3CjiGyLjxzQUXYMSCqSAAw4Oj0SaeEHUNk9ez4EmODwg4SAEw4+PLklmHHEFm9Wi9kfgBYQQKAEbsWT24RdgxRpXoXvcwcYbCBBAAztq2YUhp2DFEV6314WdgxIJJIADBk07rJxWHHEFWlF21ZF3YMiCISAExZ/8GkorBjiKo2561kYDiYRwKAMas3XVESdgxRdVr7F3kUCMaRAGDO6s1kAFu6q/lhh4DoIQHAoNVbruQqkB2xrh9wGwCmkQBg0lt7x7FJ2dGq87SjYceAqGFvhVHLD45mm7LjrA4z4mHHgIhhZ4VZy46PYYhZOy74aHnYISBiSAAw7BUZGXYIEVXUZSEThMEoEgBMW9hsSNghRNSpp8yoCTsGRAoJAMbNLR0adggR1XXXG2GHgEghAcC8eWQAO0rOnrMv7BgQJSQAWDCvjKtAVnQ89UVGhoY5JADYMK/DZWGHEE3dP3o97BAQISQAWDGDDGBF0WnPHwk7BkQHCQB2zDi9X9ghRNIZ8blhh4DoIAHAkmmd+4YdQiR1WrQj7BAQGSQA2PL8RT3CDiGK2pZOZ35IGEICgDVPn9cz7BCiqNOKjWGHgKggAcCepzkHsKC8fBqPgsIMEgAseuZCMoB5p7/1TtghICJIALBIPzW4W9gxRE9Z+VSGBIIRTtgBINomzgs7gggaPirsCBARJABYVT1+UdghRE/ru1qEHQKigQQAu2rGMnaBcSNGhB0BooEEAMsqh64IO4TIafWZkrBDQCSQAGBb5eAlYYcQOYMvDTsCRAIJANZVjOI+gGEdbw07AkQCCQD2VY2dH3YIUTO2c9gRIApIAAhA5RVLww4hYrpfH3YEiAISAIJQM/aVsEOImKvKw44AEUACQCCOjeNZIKP6Mtg2skcCQDCOXLk27BAipfWNYUeACCABICC7btwSdgiRMqJj2BEg/5EAEJQ1t+wOO4QouWhs2BEg/zEaKAKz+b0rS8OOITqc48/psGNAvlNhB4BCcuOfSkuUEl3j6kTnFVN1W6AjWotW4py8UWrtiIhb73w1LuIoES3KES3iKkeLFnG163xcJiaSmDpRu8oRVa9WR0TcumkVtYiIanAu7Iq4if/WrkhMO6q2mnhtASXKOfEF5Wrtaq1dVVSW4e+SkQ3DmBwYWSIBIEglopoVFymllRYRpcTRjhKd6G+1OI6KKaW0Uq4W0SJaiWilazdTpUUSmUMrUVoc7ST+JhIXLVqLUjGlHREtSiTxXVdEiRJHxNFS16yIiHaVaHHrmlGiE2lCixaVyCdaXK1UIkuoRFmttYhSosRRIq6jlRJHOU5MqZKWPXp1O61tu4BOc+K3/18wDSG6SACASU5Zj3FD+7UN4ubav+/kGhAA5JjiyU/tcLVtb50a9nICAE5WfuuCGssJ4PjVYS8kAKBJvR+vspsBfh/2EgIAkrj4mQqbCWAxM0MCQM4a+VrcXgLYfVHYi4c8x4tggEUf3F92QXNblTd//1VbVQMAsjf0TWunAE8Vh71wAIAUWj5nKwHwICiywyUgwLKqx1pdZufFsOaLN1ipF4WCBABYN7t6hJUMULp+kY1qUTBIAIB9iyvsDN68+ykr1aJQkACAACzRI21Ue/TxKhvVAgAM+ruNu8Afnhn2YiGvMSMYEIjPzbRQ6Sm9LFSKwkECAIIx4S3zdZb3MV8nCggJAAjIHRau119gvkoUEBIAEJA3/mi+zs7l5usEAJhW9I7xu8BrTw97oZDPOAMAglLzn3HvQulpf7HpGlFISABAYKY+a7rGlt1N14hCQgIAgvPdPYYrLOMSELJAAgCCs+FLine truncated
|
|
||||||
|
Before Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 60 KiB |
@@ -1,4 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Allow: /
|
|
||||||
|
|
||||||
Sitemap: https://rynext.com.mx/sitemap.xml
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 />;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 />;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { NotFoundPage } from './NotFoundPage';
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return <NotFoundPage />;
|
||||||
|
}
|
||||||
@@ -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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 />;
|
||||||
|
}
|
||||||
@@ -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 />;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 />;
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 />;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 />;
|
||||||
|
}
|
||||||
@@ -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 +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 +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 |
@@ -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>
|
||||||
|
);
|
||||||
@@ -1,29 +1,47 @@
|
|||||||
import { type ReactNode } from 'react';
|
'use client';
|
||||||
|
import { type ReactNode } from "react";
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
href?: string;
|
href?: string;
|
||||||
variant?: 'primary' | 'secondary';
|
variant?: "primary" | "secondary";
|
||||||
className?: string;
|
className?: string;
|
||||||
isNavyBg?: boolean;
|
isNavyBg?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button = ({ children, onClick, href, variant = 'primary', className = '', isNavyBg = false }: ButtonProps) => {
|
export const Button = ({
|
||||||
const base = 'inline-flex items-center gap-3 px-8 py-4 text-base font-bold transition hover:scale-[1.02] cursor-pointer';
|
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 = {
|
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
|
secondary: isNavyBg
|
||||||
? 'border border-white/15 bg-white/5 text-white hover:bg-white/10'
|
? "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-brand-navy/15 text-brand-navy hover:border-brand-blue hover:text-brand-blue",
|
||||||
};
|
};
|
||||||
|
|
||||||
const cls = `${base} ${styles[variant]} ${className}`;
|
const cls = `${base} ${styles[variant]} ${className}`;
|
||||||
|
|
||||||
if (href) {
|
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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
@@ -1,71 +1,224 @@
|
|||||||
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';
|
import type { ModalProps } from '../types';
|
||||||
|
|
||||||
const supportEmail = '[email protected]';
|
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'https://localhost:7001';
|
||||||
const mailToHref = `mailto:${supportEmail}?subject=${encodeURIComponent('Consulta Rynext')}&body=${encodeURIComponent('Hola Rynext,\n\nQuiero agendar una conversación sobre un proyecto.\n\nGracias.')}`;
|
|
||||||
|
type Status = 'idle' | 'loading' | 'success' | 'error';
|
||||||
|
|
||||||
|
const initialForm = { nombre: '', empresa: '', email: '', telefono: '', mensaje: '' };
|
||||||
|
|
||||||
export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
|
export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
|
||||||
|
const [form, setForm] = useState(initialForm);
|
||||||
|
const [status, setStatus] = useState<Status>('idle');
|
||||||
|
|
||||||
if (!isOpen) return null;
|
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 (
|
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="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
|
<div
|
||||||
className={`relative w-full max-w-md rounded-2xl border p-8 shadow-2xl transition-all ${
|
className="relative w-full max-w-xl rounded-2xl shadow-2xl"
|
||||||
isNavyBg
|
style={{ background: card, border: `1px solid ${bdr}` }}
|
||||||
? 'border-white/20 bg-brand-navy text-white'
|
|
||||||
: 'border-gray-200 bg-white text-brand-black'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<button
|
{/* Header */}
|
||||||
onClick={onClose}
|
<div className="flex items-center justify-between px-8 pt-8 pb-6" style={{ borderBottom: `1px solid ${bdr}` }}>
|
||||||
className="absolute right-4 top-4 cursor-pointer text-gray-400 transition-colors hover:text-brand-blue"
|
<div>
|
||||||
aria-label="Cerrar modal"
|
<h3 className="font-display font-bold text-xl" style={{ color: text, letterSpacing: '-0.02em' }}>
|
||||||
>
|
Hablemos de tu proyecto
|
||||||
<X size={24} />
|
</h3>
|
||||||
</button>
|
<p className="text-sm mt-0.5" style={{ color: sub }}>
|
||||||
|
Te respondemos en menos de 24 h
|
||||||
<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>
|
</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>
|
||||||
|
<button
|
||||||
|
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={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
{/* Success state */}
|
||||||
<a
|
{status === 'success' ? (
|
||||||
href={mailToHref}
|
<div className="flex flex-col items-center justify-center gap-4 px-8 py-14 text-center">
|
||||||
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"
|
<CheckCircle size={48} style={{ color: '#22c55e' }} />
|
||||||
>
|
<h4 className="font-display font-bold text-lg" style={{ color: text }}>
|
||||||
Enviar correo
|
¡Mensaje enviado!
|
||||||
</a>
|
</h4>
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: sub, maxWidth: '32ch' }}>
|
||||||
|
Recibimos tu solicitud. Un miembro del equipo te contactará pronto.
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={handleClose}
|
||||||
className={`cursor-pointer rounded-xl border px-4 py-3 text-sm font-bold transition hover:border-brand-blue hover:text-brand-blue ${
|
className="mt-2 px-6 py-2.5 text-sm font-bold text-white rounded-xl transition hover:opacity-90"
|
||||||
isNavyBg
|
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer' }}
|
||||||
? 'border-white/20 bg-white/5 text-white'
|
|
||||||
: 'border-brand-navy/15 bg-white text-brand-navy'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Cerrar
|
Cerrar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
'use client';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ export const CookieBanner = ({ isNavyBg }: ThemeProps) => {
|
|||||||
<button
|
<button
|
||||||
onClick={accept}
|
onClick={accept}
|
||||||
className="shrink-0 px-5 py-2 text-sm font-bold text-white transition hover:opacity-90"
|
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
|
Aceptar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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,3 +1,4 @@
|
|||||||
|
'use client';
|
||||||
import { ArrowUp } from 'lucide-react';
|
import { ArrowUp } from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
|
|||||||
@@ -1,28 +1,21 @@
|
|||||||
import { Mail, MapPin, MessageCircle } from 'lucide-react';
|
"use client";
|
||||||
import { Link } from 'react-router';
|
import { Mail, MapPin, MessageCircle } from "lucide-react";
|
||||||
import type { ThemeProps } from '../types';
|
import { FaFacebookF, FaLinkedinIn, FaMapMarkerAlt } from "react-icons/fa";
|
||||||
|
import Link from "next/link";
|
||||||
const navLinks = [
|
import type { ThemeProps } from "../types";
|
||||||
{ label: 'Sistemas', href: '#machinery' },
|
import { pageLinks } from "../data/navigationData";
|
||||||
{ label: 'Anatomía', href: '#anatomy' },
|
import { FaRegMessage } from "react-icons/fa6";
|
||||||
{ label: 'Proceso', href: '#proceso' },
|
import { MdEmail } from "react-icons/md";
|
||||||
{ label: 'Principios', href: '#principios' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Footer = ({ isNavyBg }: ThemeProps) => {
|
export const Footer = ({ isNavyBg }: ThemeProps) => {
|
||||||
const bg = isNavyBg ? '#001040' : '#0a0a0a';
|
const bg = isNavyBg ? "#001040" : "#0a0a0a";
|
||||||
const sub = 'rgba(255,255,255,0.45)';
|
const sub = "rgba(255,255,255,0.45)";
|
||||||
const border = 'rgba(255,255,255,0.08)';
|
const border = "rgba(255,255,255,0.08)";
|
||||||
|
|
||||||
const handleNav = (href: string) => {
|
|
||||||
const el = document.querySelector(href);
|
|
||||||
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
style={{ background: bg, borderTop: `1px solid ${border}` }}
|
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"
|
role="contentinfo"
|
||||||
>
|
>
|
||||||
<div className="mx-auto max-w-7xl">
|
<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]">
|
<div className="grid gap-14 md:grid-cols-[1.8fr_1fr_1.2fr]">
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-6 flex items-center gap-3">
|
<div className="mb-6 flex items-center gap-2.5">
|
||||||
<img
|
<img
|
||||||
src="/icons/Logo_blanco.svg"
|
src="/icons/Logo_blanco.svg"
|
||||||
alt="Rynext logo"
|
alt="Logo"
|
||||||
className="h-9 w-auto object-contain"
|
className="h-11 w-11 shrink-0 object-contain"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src="/icons/Text_Logo_Blanco.svg"
|
|
||||||
alt="Rynext"
|
|
||||||
style={{
|
|
||||||
width: 130,
|
|
||||||
height: 18,
|
|
||||||
objectFit: 'cover',
|
|
||||||
objectPosition: 'center 52%',
|
|
||||||
display: 'block',
|
|
||||||
flexShrink: 0,
|
|
||||||
imageRendering: '-webkit-optimize-contrast',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
<div className="flex flex-col leading-none">
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
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>
|
</div>
|
||||||
<p
|
<p
|
||||||
className="text-sm leading-relaxed mb-8"
|
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"
|
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
|
||||||
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
|
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
(e.currentTarget as HTMLAnchorElement).style.background = "rgba(255,255,255,0.14)";
|
e.currentTarget.style.background = "rgba(255,255,255,0.14)";
|
||||||
(e.currentTarget as HTMLAnchorElement).style.color = "#fff";
|
e.currentTarget.style.color = "#fff";
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
(e.currentTarget as HTMLAnchorElement).style.background = "rgba(255,255,255,0.07)";
|
e.currentTarget.style.background = "rgba(255,255,255,0.07)";
|
||||||
(e.currentTarget as HTMLAnchorElement).style.color = sub;
|
e.currentTarget.style.color = sub;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
|
<FaFacebookF />
|
||||||
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://linkedin.com/company/rynext"
|
href="https://linkedin.com/company/rynext"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -87,52 +90,38 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
|||||||
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
|
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
|
||||||
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
|
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
(e.currentTarget as HTMLAnchorElement).style.background =
|
e.currentTarget.style.background = "rgba(255,255,255,0.14)";
|
||||||
"rgba(255,255,255,0.14)";
|
e.currentTarget.style.color = "#fff";
|
||||||
(e.currentTarget as HTMLAnchorElement).style.color = "#fff";
|
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
(e.currentTarget as HTMLAnchorElement).style.background =
|
e.currentTarget.style.background = "rgba(255,255,255,0.07)";
|
||||||
"rgba(255,255,255,0.07)";
|
e.currentTarget.style.color = sub;
|
||||||
(e.currentTarget as HTMLAnchorElement).style.color = sub;
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* LinkedIn SVG */}
|
<FaLinkedinIn />
|
||||||
<svg
|
|
||||||
width="15"
|
|
||||||
height="15"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
|
|
||||||
<rect x="2" y="9" width="4" height="12" />
|
|
||||||
<circle cx="4" cy="4" r="2" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
<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"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label="Rynext en WhatsApp"
|
aria-label="Rynext en WhatsApp"
|
||||||
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
|
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
|
||||||
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
|
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
(e.currentTarget as HTMLAnchorElement).style.background =
|
e.currentTarget.style.background = "rgba(255,255,255,0.14)";
|
||||||
"rgba(255,255,255,0.14)";
|
e.currentTarget.style.color = "#fff";
|
||||||
(e.currentTarget as HTMLAnchorElement).style.color = "#fff";
|
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
(e.currentTarget as HTMLAnchorElement).style.background =
|
e.currentTarget.style.background = "rgba(255,255,255,0.07)";
|
||||||
"rgba(255,255,255,0.07)";
|
e.currentTarget.style.color = sub;
|
||||||
(e.currentTarget as HTMLAnchorElement).style.color = sub;
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MessageCircle size={15} />
|
<FaRegMessage />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navegación */}
|
{/* Páginas */}
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
className="text-mono-label mb-6"
|
className="text-mono-label mb-6"
|
||||||
@@ -141,26 +130,20 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
|||||||
letterSpacing: "0.12em",
|
letterSpacing: "0.12em",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Navegación
|
Páginas
|
||||||
</p>
|
</p>
|
||||||
<ul className="flex flex-col gap-3.5">
|
<ul className="flex flex-col gap-3.5">
|
||||||
{navLinks.map((link) => (
|
{pageLinks.map((link) => (
|
||||||
<li key={link.href}>
|
<li key={link.href}>
|
||||||
<button
|
<Link
|
||||||
onClick={() => handleNav(link.href)}
|
href={link.href}
|
||||||
className="text-sm font-medium transition-colors"
|
className="text-sm font-medium transition-colors"
|
||||||
style={{
|
style={{ color: sub, textDecoration: "none" }}
|
||||||
color: sub,
|
|
||||||
background: "none",
|
|
||||||
border: "none",
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: 0,
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.color = "#fff")}
|
onMouseEnter={(e) => (e.currentTarget.style.color = "#fff")}
|
||||||
onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
|
onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
</button>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -178,19 +161,14 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
|||||||
Contacto
|
Contacto
|
||||||
</p>
|
</p>
|
||||||
<ul className="flex flex-col gap-4">
|
<ul className="flex flex-col gap-4">
|
||||||
<li className="flex items-start gap-3">
|
<li className="flex items-center gap-3">
|
||||||
<MapPin
|
<FaMapMarkerAlt className="text-white text-center" />
|
||||||
size={14}
|
|
||||||
style={{ color: "#e50914", marginTop: 2, flexShrink: 0 }}
|
|
||||||
/>
|
|
||||||
<span className="text-sm leading-snug" style={{ color: sub }}>
|
<span className="text-sm leading-snug" style={{ color: sub }}>
|
||||||
Cancún, Quintana Roo
|
Cancún, Quintana Roo. México
|
||||||
<br />
|
|
||||||
México
|
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-3">
|
<li className="flex items-center gap-3">
|
||||||
<Mail size={14} style={{ color: "#e50914", flexShrink: 0 }} />
|
<MdEmail className="text-white text-center" />
|
||||||
<a
|
<a
|
||||||
href="mailto:[email protected]"
|
href="mailto:[email protected]"
|
||||||
className="text-sm font-medium transition-colors"
|
className="text-sm font-medium transition-colors"
|
||||||
@@ -205,7 +183,7 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
|||||||
<a
|
<a
|
||||||
href="mailto:[email protected]"
|
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"
|
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
|
Hablemos de tu proyecto
|
||||||
</a>
|
</a>
|
||||||
@@ -213,26 +191,31 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Bottom bar */}
|
{/* Bottom bar — full width */}
|
||||||
<div
|
<div
|
||||||
className="mt-16 border-t pt-8 text-center"
|
className="mt-10 -mx-8 px-8 md:-mx-14 md:px-14"
|
||||||
style={{ borderColor: border }}
|
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)" }}>
|
<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
|
||||||
</p>
|
derechos reservados.
|
||||||
<Link
|
</p>
|
||||||
to="/privacidad"
|
<Link
|
||||||
className="text-xs transition-colors"
|
href="/privacidad"
|
||||||
style={{ color: "rgba(255,255,255,0.25)", textDecoration: "none" }}
|
className="text-xs transition-colors"
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.color = "rgba(255,255,255,0.6)")}
|
style={{ color: "rgba(255,255,255,0.25)", textDecoration: "none" }}
|
||||||
onMouseLeave={(e) => (e.currentTarget.style.color = "rgba(255,255,255,0.25)")}
|
onMouseEnter={(e) =>
|
||||||
>
|
(e.currentTarget.style.color = "rgba(255,255,255,0.6)")
|
||||||
Aviso de Privacidad
|
}
|
||||||
</Link>
|
onMouseLeave={(e) =>
|
||||||
</div>
|
(e.currentTarget.style.color = "rgba(255,255,255,0.25)")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Aviso de Privacidad
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,27 +1,33 @@
|
|||||||
import { useState, useEffect } from "react";
|
'use client';
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { useState, useEffect } from 'react';
|
||||||
import { Menu, X } from "lucide-react";
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Link, useLocation } from "react-router";
|
import { Menu, X } from 'lucide-react';
|
||||||
import type { NavbarProps } from "../types";
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { pageLinks } from '../data/navigationData';
|
||||||
|
|
||||||
const pageLinks = [
|
export const Navbar = () => {
|
||||||
{ label: "Sobre Nosotros", href: "/sobre-nosotros" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
const [menuOpen, setMenuOpen] = 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(() => {
|
useEffect(() => {
|
||||||
const onScroll = () => setScrolled(window.scrollY > 80);
|
const onScroll = () => setScrolled(window.scrollY > 80);
|
||||||
window.addEventListener("scroll", onScroll, { passive: true });
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
return () => window.removeEventListener("scroll", onScroll);
|
return () => window.removeEventListener('scroll', onScroll);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
}, [location.pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -34,44 +40,42 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: scrolled ? "rgba(255,255,255,0.97)" : "transparent",
|
background: scrolled ? 'rgba(255,255,255,0.97)' : 'transparent',
|
||||||
borderBottom: scrolled ? "1px solid rgba(0,0,0,0.08)" : "1px solid transparent",
|
borderBottom: scrolled ? '1px solid rgba(0,0,0,0.08)' : '1px solid transparent',
|
||||||
backdropFilter: scrolled ? "blur(16px)" : "none",
|
backdropFilter: scrolled ? 'blur(16px)' : 'none',
|
||||||
WebkitBackdropFilter: scrolled ? "blur(16px)" : "none",
|
WebkitBackdropFilter: scrolled ? 'blur(16px)' : 'none',
|
||||||
transition: "background 200ms, border-color 200ms",
|
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">
|
<div className="mx-auto flex max-w-7xl items-center justify-between px-8 py-4 md:px-14">
|
||||||
{/* Logo — siempre modo claro */}
|
<Link href="/" aria-label="Rynext Technologies — inicio" className="flex items-center gap-2.5">
|
||||||
<Link to="/" aria-label="Rynext Technologies — inicio" className="flex items-center gap-3">
|
|
||||||
<img src="/icons/Logo_negro.svg" alt="Rynext logo" className="h-9 w-auto object-contain" />
|
|
||||||
<img
|
<img
|
||||||
src="/icons/Text_Logo_Negro.svg"
|
src="/icons/Logo_negro.svg"
|
||||||
alt="Rynext"
|
alt="Logo"
|
||||||
style={{
|
className="h-11 w-11 shrink-0 object-contain"
|
||||||
width: 130,
|
|
||||||
height: 18,
|
|
||||||
objectFit: 'cover',
|
|
||||||
objectPosition: 'center 52%',
|
|
||||||
display: 'block',
|
|
||||||
flexShrink: 0,
|
|
||||||
imageRendering: '-webkit-optimize-contrast',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
<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>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop nav */}
|
{/* Desktop nav */}
|
||||||
<nav className="hidden items-center gap-8 md:flex" aria-label="Navegación principal">
|
<nav className="hidden items-center gap-8 md:flex" aria-label="Navegación principal">
|
||||||
{pageLinks.map((link) => {
|
{pageLinks.map((link) => {
|
||||||
const active = location.pathname === link.href;
|
const active = pathname === link.href;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={link.href}
|
key={link.href}
|
||||||
to={link.href}
|
href={link.href}
|
||||||
className="text-sm font-semibold transition-colors"
|
className="text-sm font-semibold transition-colors"
|
||||||
style={{ color: active ? "#0a0a0a" : "#6a6866", textDecoration: "none" }}
|
style={{ color: active ? '#0a0a0a' : '#6a6866', textDecoration: 'none' }}
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.color = "#0a0a0a")}
|
onMouseEnter={(e) => (e.currentTarget.style.color = '#0a0a0a')}
|
||||||
onMouseLeave={(e) => (e.currentTarget.style.color = active ? "#0a0a0a" : "#6a6866")}
|
onMouseLeave={(e) => (e.currentTarget.style.color = active ? '#0a0a0a' : '#6a6866')}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -79,9 +83,9 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
<button
|
<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"
|
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
|
Hablemos
|
||||||
</button>
|
</button>
|
||||||
@@ -91,8 +95,8 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setMenuOpen((v) => !v)}
|
onClick={() => setMenuOpen((v) => !v)}
|
||||||
className="flex items-center justify-center rounded p-2 md:hidden"
|
className="flex items-center justify-center rounded p-2 md:hidden"
|
||||||
style={{ color: "#0a0a0a", background: "none", border: "none", cursor: "pointer" }}
|
style={{ color: '#0a0a0a', background: 'none', border: 'none', cursor: 'pointer' }}
|
||||||
aria-label={menuOpen ? "Cerrar menú" : "Abrir menú"}
|
aria-label={menuOpen ? 'Cerrar menú' : 'Abrir menú'}
|
||||||
aria-expanded={menuOpen}
|
aria-expanded={menuOpen}
|
||||||
>
|
>
|
||||||
{menuOpen ? <X size={22} /> : <Menu size={22} />}
|
{menuOpen ? <X size={22} /> : <Menu size={22} />}
|
||||||
@@ -109,8 +113,8 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -8 }}
|
exit={{ opacity: 0, y: -8 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="fixed inset-x-0 top-[65px] z-40 px-8 py-6 md:hidden"
|
className="fixed inset-x-0 top-[73px] z-40 px-8 py-6 md:hidden"
|
||||||
style={{ background: "#ffffff", borderBottom: "1px solid #dddbd5" }}
|
style={{ background: '#ffffff', borderBottom: '1px solid #dddbd5' }}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Menú de navegación"
|
aria-label="Menú de navegación"
|
||||||
@@ -119,21 +123,17 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
|||||||
{pageLinks.map((link) => (
|
{pageLinks.map((link) => (
|
||||||
<Link
|
<Link
|
||||||
key={link.href}
|
key={link.href}
|
||||||
to={link.href}
|
href={link.href}
|
||||||
className="text-left text-base font-semibold"
|
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.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<button
|
<Button onClick={scrollToContact} className="w-full py-3 text-center text-sm font-bold text-white">
|
||||||
onClick={onOpenContact}
|
|
||||||
className="w-full py-3 text-center text-sm font-bold text-white"
|
|
||||||
style={{ background: "#e50914", border: "none", cursor: "pointer", borderRadius: 3 }}
|
|
||||||
>
|
|
||||||
Hablemos
|
Hablemos
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
'use client';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
export const ScrollToTop = () => {
|
export const ScrollToTop = () => {
|
||||||
const { pathname } = useLocation();
|
const pathname = usePathname();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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.',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { LayoutDashboard, ShoppingCart, Workflow, Zap, BarChart3 } from 'lucide-react';
|
||||||
|
|
||||||
export const machineryData = [
|
export const machineryData = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -5,6 +7,7 @@ export const machineryData = [
|
|||||||
title: 'OPERACIONES',
|
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.',
|
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'],
|
tags: ['Portales de gestión', 'Automatización de flujos', 'Reportes operacionales', 'Control de acceso por rol'],
|
||||||
|
icon: LayoutDashboard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -12,6 +15,7 @@ export const machineryData = [
|
|||||||
title: 'VENTAS',
|
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.',
|
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'],
|
tags: ['E-commerce a medida', 'Integración de pagos', 'Facturación y CFDI', 'Recuperación de carritos'],
|
||||||
|
icon: ShoppingCart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -19,6 +23,7 @@ export const machineryData = [
|
|||||||
title: 'INTEGRACIONES',
|
title: 'INTEGRACIONES',
|
||||||
description: 'Conectamos sistemas heterogéneos en un ecosistema coherente: ERPs, CRMs, proveedores y APIs de terceros. Sin silos, sin duplicidad de datos.',
|
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'],
|
tags: ['ERP / SAP / Odoo', 'APIs de terceros', 'Sincronización bidireccional', 'Webhooks y eventos'],
|
||||||
|
icon: Workflow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
@@ -26,6 +31,7 @@ export const machineryData = [
|
|||||||
title: 'AUTOMATIZACIÓN',
|
title: 'AUTOMATIZACIÓN',
|
||||||
description: 'Eliminamos intervención humana donde no aporta valor: aprobaciones, notificaciones, sincronizaciones y pipelines de datos que corren solos.',
|
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'],
|
tags: ['Bots y pipelines', 'Procesamiento de datos', 'Notificaciones inteligentes', 'Triggers y colas'],
|
||||||
|
icon: Zap,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
@@ -33,5 +39,6 @@ export const machineryData = [
|
|||||||
title: 'DATOS',
|
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.',
|
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'],
|
tags: ['Dashboards ejecutivos', 'Pipelines de datos', 'KPIs e instrumentación', 'Alertas y umbrales'],
|
||||||
|
icon: BarChart3,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -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' },
|
||||||
|
];
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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.',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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.',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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.' },
|
||||||
|
];
|
||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Allow importing CSS files as side-effects
|
||||||
|
declare module '*.css' {
|
||||||
|
const content: Record<string, string>;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
@@ -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";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-brand-blue: #0077c2;
|
--color-brand-blue: #0077c2;
|
||||||
--color-brand-navy: #001f5b;
|
--color-brand-navy: #001f5b;
|
||||||
--color-brand-black: #0a0a0a;
|
--color-brand-black: #0a0a0a;
|
||||||
--color-brand-red: #e50914;
|
--color-brand-red: #3b82f6;
|
||||||
--color-bg-light: #f9f9f7;
|
--color-bg-light: #f9f9f7;
|
||||||
--color-bg-alt: #f0efec;
|
--color-bg-alt: #f0efec;
|
||||||
--color-border-light: #dddbd5;
|
--color-border-light: #dddbd5;
|
||||||
@@ -14,16 +14,11 @@
|
|||||||
--color-dark-surface: #0f1520;
|
--color-dark-surface: #0f1520;
|
||||||
--color-dark-border: #1c2535;
|
--color-dark-border: #1c2535;
|
||||||
|
|
||||||
--font-sans: 'Inter', system-ui, sans-serif;
|
--font-sans: 'Quicksand', system-ui, sans-serif;
|
||||||
--font-display: 'Plus Jakarta Sans', system-ui, sans-serif;
|
--font-display: 'Quicksand', system-ui, sans-serif;
|
||||||
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
|
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: rgba(229, 9, 20, 0.18);
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -37,14 +32,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
*:focus-visible {
|
*:focus-visible {
|
||||||
outline: 2px solid #e50914;
|
outline: 2px solid #3b82f6;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: #001f5b;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography scale */
|
/* Typography scale */
|
||||||
@@ -176,7 +166,7 @@
|
|||||||
margin-bottom: 0.35rem;
|
margin-bottom: 0.35rem;
|
||||||
}
|
}
|
||||||
.prose-rynext a {
|
.prose-rynext a {
|
||||||
color: #e50914;
|
color: #3b82f6;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-underline-offset: 3px;
|
text-underline-offset: 3px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
|
||||||
)
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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 { motion } from 'framer-motion';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
|
import { anatomyLayers as layers } from '../data/anatomyData';
|
||||||
const layers = [
|
|
||||||
{
|
|
||||||
id: 'presentation',
|
|
||||||
num: '01',
|
|
||||||
title: 'PRESENTACIÓN',
|
|
||||||
subtitle: 'La interfaz donde opera el usuario',
|
|
||||||
description:
|
|
||||||
'Portales de administración, apps web, paneles de monitoreo y puntos de venta. Construidos para adopción real, no para demos.',
|
|
||||||
tech: ['React', 'Next.js', 'TypeScript', 'Tailwind', 'Mobile Web'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'logic',
|
|
||||||
num: '02',
|
|
||||||
title: 'LÓGICA DE NEGOCIO',
|
|
||||||
subtitle: 'Las reglas que gobiernan la operación',
|
|
||||||
description:
|
|
||||||
'Validaciones, cálculos, flujos de aprobación y orquestación de procesos. El código que representa cómo funciona tu negocio.',
|
|
||||||
tech: ['Node.js', 'Python', 'REST APIs', 'GraphQL', 'Microservicios'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'data',
|
|
||||||
num: '03',
|
|
||||||
title: 'DATOS',
|
|
||||||
subtitle: 'Persistencia y acceso a la información',
|
|
||||||
description:
|
|
||||||
'Modelo de datos diseñado para tu operación. Bases relacionales, NoSQL, caché y búsqueda de texto. Una sola fuente de verdad.',
|
|
||||||
tech: ['PostgreSQL', 'MongoDB', 'Redis', 'Elasticsearch', 'Prisma'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'infra',
|
|
||||||
num: '04',
|
|
||||||
title: 'INFRAESTRUCTURA',
|
|
||||||
subtitle: 'La base que soporta el sistema',
|
|
||||||
description:
|
|
||||||
'Cloud, contenedores, pipelines de CI/CD y observabilidad. Diseñada para que el sistema esté siempre disponible y sea fácil de operar.',
|
|
||||||
tech: ['AWS', 'Docker', 'GitHub Actions', 'Terraform', 'Monitoring'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'integrations',
|
|
||||||
num: '05',
|
|
||||||
title: 'INTEGRACIONES',
|
|
||||||
subtitle: 'Conexión con el ecosistema externo',
|
|
||||||
description:
|
|
||||||
'ERPs, sistemas de pago, servicios de mensajería, proveedores y APIs externas. El sistema habla con todo lo que el negocio necesita.',
|
|
||||||
tech: ['Stripe', 'SAP / Odoo', 'OAuth 2.0', 'Webhooks', 'SMS / Email'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Anatomy = ({ isNavyBg }: ThemeProps) => {
|
export const Anatomy = ({ isNavyBg }: ThemeProps) => {
|
||||||
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
|
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
|
||||||
@@ -101,12 +55,12 @@ export const Anatomy = ({ isNavyBg }: ThemeProps) => {
|
|||||||
{/* Number */}
|
{/* Number */}
|
||||||
<span
|
<span
|
||||||
className="font-display font-black"
|
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}
|
{layer.num}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Title + description */}
|
{/* Title + description + tech */}
|
||||||
<div style={{ paddingTop: 8 }}>
|
<div style={{ paddingTop: 8 }}>
|
||||||
<p
|
<p
|
||||||
className="font-display font-black mb-1"
|
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 }}>
|
<p className="text-xs mb-3" style={{ color: sub }}>
|
||||||
{layer.subtitle}
|
{layer.subtitle}
|
||||||
</p>
|
</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}
|
{layer.description}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
@@ -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} />;
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { motion } from 'framer-motion';
|
||||||
import { ArrowRight } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
import type { HeroProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
import { Button } from './Button';
|
import { Button } from '../components/Button';
|
||||||
import { SystemMap } from './SystemMap';
|
import { SystemMap } from './SystemMap';
|
||||||
|
import { useContact } from '../contexts/ContactContext';
|
||||||
|
|
||||||
const ease = [0.22, 1, 0.36, 1] as const;
|
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 bg = isNavyBg ? '#001f5b' : '#ffffff';
|
||||||
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
|
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
|
||||||
const sub = isNavyBg ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.45)';
|
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)' }}
|
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"
|
aria-label="Rynext Technologies — Hero"
|
||||||
>
|
>
|
||||||
{/* Subtle grid bg */}
|
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute inset-0 bg-grid-lines"
|
className="pointer-events-none absolute inset-0 bg-grid-lines"
|
||||||
style={{ color: gridLine }}
|
style={{ color: gridLine }}
|
||||||
aria-hidden="true"
|
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">
|
<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>
|
<div>
|
||||||
{/* Headline */}
|
|
||||||
<h1 className="text-display mb-8" style={{ color: text }}>
|
<h1 className="text-display mb-8" style={{ color: text }}>
|
||||||
<RevealLine delay={0.08}>Construimos</RevealLine>
|
<RevealLine delay={0.08}>Construimos</RevealLine>
|
||||||
<RevealLine delay={0.16}>el sistema</RevealLine>
|
<RevealLine delay={0.16}>el sistema</RevealLine>
|
||||||
@@ -54,7 +51,6 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
|
|||||||
<RevealLine delay={0.32}>negocio.</RevealLine>
|
<RevealLine delay={0.32}>negocio.</RevealLine>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<motion.p
|
<motion.p
|
||||||
initial={{ opacity: 0, y: 14 }}
|
initial={{ opacity: 0, y: 14 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@@ -67,33 +63,26 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
|
|||||||
la operación real de tu negocio.
|
la operación real de tu negocio.
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
{/* CTAs */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.6, delay: 0.65, ease }}
|
transition={{ duration: 0.6, delay: 0.65, ease }}
|
||||||
className="flex flex-wrap items-center gap-6"
|
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} />
|
Hablemos de tu proyecto <ArrowRight size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' })}
|
||||||
document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}}
|
|
||||||
className="group flex items-center gap-2 text-sm font-semibold transition"
|
className="group flex items-center gap-2 text-sm font-semibold transition"
|
||||||
style={{ color: sub }}
|
style={{ color: sub }}
|
||||||
>
|
>
|
||||||
Explorar la maquinaria
|
Explorar la maquinaria
|
||||||
<ArrowRight
|
<ArrowRight size={14} className="transition-transform group-hover:translate-x-0.5" />
|
||||||
size={14}
|
|
||||||
className="transition-transform group-hover:translate-x-0.5"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right column — System Map */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 24 }}
|
initial={{ opacity: 0, y: 24 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@@ -103,7 +92,6 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
|
|||||||
<SystemMap isNavyBg={isNavyBg} />
|
<SystemMap isNavyBg={isNavyBg} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
'use client';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
@@ -57,9 +58,9 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
|
|||||||
aria-label={item.title}
|
aria-label={item.title}
|
||||||
style={{
|
style={{
|
||||||
background: isHov
|
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,
|
: 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,
|
borderRadius: 6,
|
||||||
padding: '28px 28px 32px',
|
padding: '28px 28px 32px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -83,7 +84,7 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
|
|||||||
fontSize: 96,
|
fontSize: 96,
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
color: isHov ? 'rgba(229,9,20,0.07)' : numColor,
|
color: isHov ? 'rgba(59,130,246,0.07)' : numColor,
|
||||||
transition: 'color 220ms',
|
transition: 'color 220ms',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
@@ -101,13 +102,34 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: 2,
|
height: 2,
|
||||||
background: '#e50914',
|
background: '#3b82f6',
|
||||||
opacity: isHov ? 1 : 0,
|
opacity: isHov ? 1 : 0,
|
||||||
transition: 'opacity 220ms',
|
transition: 'opacity 220ms',
|
||||||
borderRadius: '6px 6px 0 0',
|
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 */}
|
{/* Content */}
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<h3
|
<h3
|
||||||
@@ -115,7 +137,7 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 'clamp(1.1rem, 2vw, 1.3rem)',
|
fontSize: 'clamp(1.1rem, 2vw, 1.3rem)',
|
||||||
letterSpacing: '-0.02em',
|
letterSpacing: '-0.02em',
|
||||||
color: isHov ? '#e50914' : text,
|
color: isHov ? '#3b82f6' : text,
|
||||||
transition: 'color 220ms',
|
transition: 'color 220ms',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1,47 +1,8 @@
|
|||||||
|
'use client';
|
||||||
import { motion } from 'framer-motion';
|
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';
|
import type { ThemeProps } from '../types';
|
||||||
|
import { comparisonRows, proofCards } from '../data/platformAdvantageData';
|
||||||
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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
|
export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
|
||||||
return (
|
return (
|
||||||
@@ -1,28 +1,7 @@
|
|||||||
|
'use client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
|
import { principles } from '../data/principlesData';
|
||||||
const principles = [
|
|
||||||
{
|
|
||||||
title: 'Simple por diseño',
|
|
||||||
description: 'La complejidad existe solo cuando aporta valor real. Todo lo demás se elimina.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Diseñado para fallar',
|
|
||||||
description: 'Los sistemas asumen que algo fallará. La resiliencia es diseñada, no improvisada.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Una sola fuente de verdad',
|
|
||||||
description: 'La información crítica tiene una única fuente confiable. Sin duplicidad de datos.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Automatiza lo repetitivo',
|
|
||||||
description: 'Si una persona lo hace manualmente diez veces, el sistema debe hacerlo.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Construido para cambiar',
|
|
||||||
description: 'El software debe poder evolucionar. La arquitectura anticipa el cambio inevitable.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Principles = ({ isNavyBg }: ThemeProps) => {
|
export const Principles = ({ isNavyBg }: ThemeProps) => {
|
||||||
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
|
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
|
||||||
@@ -74,13 +53,13 @@ export const Principles = ({ isNavyBg }: ThemeProps) => {
|
|||||||
transition={{ duration: 0.4, delay: i * 0.06 }}
|
transition={{ duration: 0.4, delay: i * 0.06 }}
|
||||||
style={{
|
style={{
|
||||||
padding: '32px 28px 36px',
|
padding: '32px 28px 36px',
|
||||||
borderTop: '3px solid #e50914',
|
borderTop: '3px solid #3b82f6',
|
||||||
borderBottom: `1px solid ${gridGap}`,
|
borderBottom: `1px solid ${gridGap}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="font-display font-black block mb-5"
|
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')}
|
{String(i + 1).padStart(2, '0')}
|
||||||
</span>
|
</span>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
'use client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
|
|
||||||
@@ -20,10 +21,7 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
|
|||||||
viewport={{ once: false, amount: 0.15 }}
|
viewport={{ once: false, amount: 0.15 }}
|
||||||
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
||||||
>
|
>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
{/* Headline */}
|
|
||||||
<h2
|
<h2
|
||||||
id="problem-heading"
|
id="problem-heading"
|
||||||
className="text-display"
|
className="text-display"
|
||||||
@@ -52,7 +50,6 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
|
|||||||
</motion.span>
|
</motion.span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Descripción */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
@@ -62,8 +59,8 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
|
|||||||
style={{ maxWidth: '56ch' }}
|
style={{ maxWidth: '56ch' }}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="mt-1 shrink-0 block h-full w-px"
|
className="mt-1 shrink-0 block"
|
||||||
style={{ background: '#e50914', minHeight: '100%', width: 2, borderRadius: 2, alignSelf: 'stretch', opacity: 0.7 }}
|
style={{ background: '#3b82f6', minHeight: '100%', width: 2, borderRadius: 2, alignSelf: 'stretch', opacity: 0.7 }}
|
||||||
/>
|
/>
|
||||||
<p className="text-base leading-relaxed" style={{ color: sub }}>
|
<p className="text-base leading-relaxed" style={{ color: sub }}>
|
||||||
Software que impone sus propias reglas en lugar de adaptarse
|
Software que impone sus propias reglas en lugar de adaptarse
|
||||||
@@ -72,7 +69,6 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
|
|||||||
de arquitectura.
|
de arquitectura.
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1,33 +1,7 @@
|
|||||||
|
'use client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
|
import { processSteps as steps } from '../data/processData';
|
||||||
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.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Process = ({ isNavyBg }: ThemeProps) => {
|
export const Process = ({ isNavyBg }: ThemeProps) => {
|
||||||
const bg = isNavyBg ? '#001f5b' : '#ffffff';
|
const bg = isNavyBg ? '#001f5b' : '#ffffff';
|
||||||
@@ -77,7 +51,7 @@ export const Process = ({ isNavyBg }: ThemeProps) => {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="font-display font-black block mb-6"
|
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}
|
{step.num}
|
||||||
</span>
|
</span>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
'use client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Check } from 'lucide-react';
|
import { Check } from 'lucide-react';
|
||||||
import type { ThemeProps } from '../types';
|
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 accentClass = service.accent === 'blue' ? 'bg-brand-blue' : 'bg-brand-red';
|
||||||
const iconBgClass = service.accent === 'blue'
|
const iconBgClass = service.accent === 'blue'
|
||||||
? (isNavyBg ? 'bg-brand-blue/15 text-sky-300' : 'bg-brand-blue/10 text-brand-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 (
|
return (
|
||||||
<motion.article
|
<motion.article
|
||||||
@@ -77,7 +78,7 @@ export const Services = ({ isNavyBg }: ThemeProps) => {
|
|||||||
size={13}
|
size={13}
|
||||||
className={`shrink-0 ${service.accent === 'blue'
|
className={`shrink-0 ${service.accent === 'blue'
|
||||||
? (isNavyBg ? 'text-sky-400' : 'text-brand-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'}`}>
|
<span className={`text-xs font-medium ${isNavyBg ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||||
@@ -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 { motion } from 'framer-motion';
|
||||||
import type { ThemeProps } from '../types';
|
import type { ThemeProps } from '../types';
|
||||||
|
import { stackGroups } from '../data/stackData';
|
||||||
const stackGroups = [
|
|
||||||
{
|
|
||||||
label: 'FRONTEND',
|
|
||||||
items: ['React', 'TypeScript', 'Next.js', 'Tailwind CSS', 'Vite'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'BACKEND',
|
|
||||||
items: ['Node.js', 'Python', 'Express', 'FastAPI', 'NestJS'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'DATOS',
|
|
||||||
items: ['PostgreSQL', 'MongoDB', 'Redis', 'Prisma', 'Elasticsearch'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'CLOUD & OPS',
|
|
||||||
items: ['AWS', 'Docker', 'GitHub Actions', 'Terraform', 'Linux'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'INTEGRACIONES',
|
|
||||||
items: ['REST', 'GraphQL', 'Stripe', 'OAuth 2.0', 'Webhooks'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Stack = ({ isNavyBg }: ThemeProps) => {
|
export const Stack = ({ isNavyBg }: ThemeProps) => {
|
||||||
const bg = isNavyBg ? '#0f1520' : '#f9f9f7';
|
const bg = isNavyBg ? '#0f1520' : '#f9f9f7';
|
||||||
@@ -45,7 +24,7 @@ export const Stack = ({ isNavyBg }: ThemeProps) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className="mb-4 flex items-center gap-3">
|
||||||
<span className="h-px w-8 bg-brand-red" />
|
<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>
|
</div>
|
||||||
<h2
|
<h2
|
||||||
id="stack-heading"
|
id="stack-heading"
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
'use client';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
interface Node {
|
interface Node {
|
||||||
@@ -84,10 +85,10 @@ function tooltipHeight(desc: string): number {
|
|||||||
export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
|
export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
|
||||||
const [active, setActive] = useState<string | null>(null);
|
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 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 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 descText = isNavyBg ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.65)';
|
||||||
const tooltipBg = isNavyBg ? '#0a1628' : '#ffffff';
|
const tooltipBg = isNavyBg ? '#0a1628' : '#ffffff';
|
||||||
const tooltipBorder = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,31,91,0.15)';
|
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 isActive = active === node.id;
|
||||||
const isConn = isConnected(node.id, active);
|
const isConn = isConnected(node.id, active);
|
||||||
const isDim = active !== null && !isActive && !isConn;
|
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 fillOpacity = isActive ? 0.12 : 0;
|
||||||
const textColor = isActive ? RED : isDim
|
const textColor = isActive ? RED : isDim
|
||||||
? (isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.2)')
|
? (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
|
<rect
|
||||||
x={node.x} y={node.y}
|
x={node.x} y={node.y}
|
||||||
width={node.w} height={node.h}
|
width={node.w} height={node.h}
|
||||||
fill={`rgba(229,9,20,${fillOpacity})`}
|
fill={`rgba(59,130,246,${fillOpacity})`}
|
||||||
stroke={stroke}
|
stroke={stroke}
|
||||||
strokeWidth={isActive ? 1.5 : 1}
|
strokeWidth={isActive ? 1.5 : 1}
|
||||||
rx={2}
|
rx={2}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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}`;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { type Dispatch, type SetStateAction } from 'react';
|
|
||||||
|
|
||||||
export interface ThemeProps {
|
export interface ThemeProps {
|
||||||
isNavyBg: boolean;
|
isNavyBg: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavbarProps {
|
export interface NavbarProps {
|
||||||
onOpenContact: () => void;
|
// no props — uses ContactContext internally
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeroProps extends ThemeProps {
|
export interface HeroProps extends ThemeProps {
|
||||||
onOpenContact: () => void;
|
// onOpenContact removed — uses ContactContext internally
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModalProps extends ThemeProps {
|
export interface ModalProps extends ThemeProps {
|
||||||
@@ -27,5 +26,3 @@ export interface ServiceItem {
|
|||||||
accent: 'blue' | 'red';
|
accent: 'blue' | 'red';
|
||||||
gridCols: string;
|
gridCols: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { Dispatch, SetStateAction };
|
|
||||||
@@ -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"]
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,41 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"compilerOptions": {
|
||||||
"references": [
|
"target": "ES2017",
|
||||||
{ "path": "./tsconfig.app.json" },
|
"lib": [
|
||||||
{ "path": "./tsconfig.node.json" }
|
"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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||