Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
409d9455f6 | ||
|
|
d7f584059e | ||
|
|
9dfced04f1 | ||
|
|
cba8d4055b | ||
|
|
75ead4f915 | ||
|
|
2e6ee9c887 | ||
|
|
0524c26d8a | ||
|
|
185939fe9e | ||
|
|
68bfcf2f8d | ||
|
|
f53e567aeb |
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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
npm-debug.log*
|
||||
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
.vscode
|
||||
.idea
|
||||
.DS_Store
|
||||
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
|
||||
README.md
|
||||
@@ -11,6 +11,10 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.next
|
||||
.claude
|
||||
out
|
||||
.env*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# ---------- Dependencias ----------
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# ---------- Build (SSR) ----------
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Las variables NEXT_PUBLIC_* se incrustan en el bundle durante el build,
|
||||
# por lo que deben estar disponibles ANTES de "npm run build".
|
||||
ARG NEXT_PUBLIC_API_URL=https://control-api.rynext.com.mx
|
||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# ---------- Runtime ----------
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 nextjs
|
||||
|
||||
# output: 'standalone' genera un servidor Node autocontenido en .next/standalone
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-https://control-api.rynext.com.mx}
|
||||
image: rynext-landing:latest
|
||||
container_name: rynext-landing
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
@@ -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: 'standalone',
|
||||
trailingSlash: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -2,36 +2,29 @@
|
||||
"name": "sitio-rynext",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@tanstack/react-query": "^5.102.4",
|
||||
"framer-motion": "^12.43.0",
|
||||
"lucide-react": "^1.28.0",
|
||||
"next": "^16.3.3",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-router": "^8.3.0",
|
||||
"react-icons": "^5.7.0",
|
||||
"tailwindcss": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"@types/node": "^24.13.3",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.4",
|
||||
"eslint": "^10.8.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"globals": "^17.7.0",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"vite": "^8.2.0",
|
||||
"vite-plugin-pwa": "^1.3.0",
|
||||
"workbox-window": "^7.4.1"
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "^15.3.0",
|
||||
"typescript": "~6.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1 +0,0 @@
|
||||
/* /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,42 +0,0 @@
|
||||
import { Routes, Route } from 'react-router';
|
||||
import { Navbar } from './components/Navbar';
|
||||
import { Footer } from './components/Footer';
|
||||
import { FloatingButtons } from './components/FloatingButtons';
|
||||
import { CookieBanner } from './components/CookieBanner';
|
||||
import { Home } from './pages/Home';
|
||||
import { SobreNosotros } from './pages/SobreNosotros';
|
||||
import { Productos } from './pages/Productos';
|
||||
import { Blog } from './pages/Blog';
|
||||
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' }}
|
||||
>
|
||||
<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,60 @@
|
||||
"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 },
|
||||
);
|
||||
|
||||
const Partners = dynamic(
|
||||
() => import("../sections/Partners").then((m) => ({ default: m.Partners })),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const Portfolio = dynamic(
|
||||
() => import("../sections/Portfolio").then((m) => ({ default: m.Portfolio })),
|
||||
{ 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} />
|
||||
<Suspense fallback={null}>
|
||||
<Portfolio isNavyBg={false} />
|
||||
</Suspense>
|
||||
<PlatformAdvantage isNavyBg={false} />
|
||||
<Services isNavyBg={false} />
|
||||
<Testimonial isNavyBg={false} />
|
||||
<Anatomy isNavyBg={false} />
|
||||
<Process isNavyBg={false} />
|
||||
<Principles isNavyBg={false} />
|
||||
<FAQ isNavyBg={false} />
|
||||
<Suspense fallback={null}>
|
||||
<Partners isNavyBg={false} />
|
||||
</Suspense>
|
||||
<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,111 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
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, cursor: art.slug ? 'pointer' : 'default' }}
|
||||
>
|
||||
<Link href={art.slug ? `/blog/${art.slug}` : '#'}
|
||||
style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, textDecoration: 'none', color: 'inherit', pointerEvents: art.slug ? 'auto' : 'none' }}>
|
||||
<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>
|
||||
</Link>
|
||||
</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,141 @@
|
||||
'use client';
|
||||
import { useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useBlogPost } from '../../../services/blog';
|
||||
import { assetUrl } from '../../../services/api';
|
||||
import Loader from '../../../components/Loader';
|
||||
|
||||
function renderContent(content: string) {
|
||||
return content.split('\n').map((line, i) => {
|
||||
if (line.startsWith('# '))
|
||||
return <h2 key={i} style={{ fontSize: '1.5rem', fontWeight: 700, color: '#0a0a0a', marginTop: '2rem', marginBottom: '0.5rem', lineHeight: 1.3 }}>{line.slice(2)}</h2>;
|
||||
if (line.startsWith('## '))
|
||||
return <h3 key={i} style={{ fontSize: '1.15rem', fontWeight: 600, color: '#1a1a1a', marginTop: '1.5rem', marginBottom: '0.4rem' }}>{line.slice(3)}</h3>;
|
||||
if (line.startsWith('- '))
|
||||
return <li key={i} style={{ color: '#4a4744', lineHeight: 1.8, marginLeft: '1.2rem', listStyleType: 'disc' }}>{line.slice(2)}</li>;
|
||||
if (line.trim() === '')
|
||||
return <br key={i} />;
|
||||
return <p key={i} style={{ color: '#4a4744', lineHeight: 1.85, marginBottom: '0.25rem' }}>{line}</p>;
|
||||
});
|
||||
}
|
||||
|
||||
export default function ArticlePage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const slug = typeof params.slug === 'string' ? params.slug : params.slug?.[0] ?? '';
|
||||
|
||||
const { data: post, isLoading, isError } = useBlogPost(slug);
|
||||
|
||||
useEffect(() => {
|
||||
if (post) document.title = `${post.title} | Blog`;
|
||||
}, [post]);
|
||||
|
||||
if (isLoading) return <Loader className="py-40" />;
|
||||
if (isError || !post) {
|
||||
return (
|
||||
<main style={{ minHeight: '80vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 16 }}>
|
||||
<p style={{ color: '#aaa8a4', fontSize: 14, fontFamily: 'monospace', letterSpacing: '0.1em' }}>ARTÍCULO NO ENCONTRADO</p>
|
||||
<button onClick={() => router.push('/blog')}
|
||||
style={{ color: '#3b82f6', fontSize: 14, textDecoration: 'underline', background: 'none', border: 'none', cursor: 'pointer' }}>
|
||||
Volver al blog
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const imageUrl = assetUrl(post.imageUrl);
|
||||
const publishDate = post.publishedAt
|
||||
? new Date(post.publishedAt).toLocaleDateString('es-MX', { day: '2-digit', month: 'long', year: 'numeric' })
|
||||
: null;
|
||||
|
||||
return (
|
||||
<main id="main-content">
|
||||
{/* Hero */}
|
||||
<section style={{ background: '#ffffff', borderBottom: '1px solid #dddbd5', paddingTop: '140px', paddingBottom: '60px' }}
|
||||
className="px-8 md:px-14">
|
||||
<div className="mx-auto" style={{ maxWidth: '780px' }}>
|
||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}>
|
||||
{/* Breadcrumb */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 28, fontFamily: 'monospace', fontSize: 11, letterSpacing: '0.14em', color: '#aaa8a4' }}>
|
||||
<Link href="/blog" style={{ color: '#3b82f6', textDecoration: 'none' }}>BLOG</Link>
|
||||
<span>/</span>
|
||||
<span style={{ color: '#aaa8a4', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '40ch' }}>
|
||||
{post.category.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Categoría */}
|
||||
<span style={{ display: 'inline-block', background: '#3b82f6', color: '#fff', fontFamily: 'monospace', fontSize: 10, letterSpacing: '0.14em', padding: '3px 10px', borderRadius: 2, marginBottom: 20 }}>
|
||||
{post.category.toUpperCase()}
|
||||
</span>
|
||||
|
||||
{/* Título */}
|
||||
<h1 style={{ fontSize: 'clamp(1.8rem, 4vw, 2.8rem)', fontWeight: 700, color: '#0a0a0a', lineHeight: 1.2, marginBottom: 20 }}>
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
{/* Resumen */}
|
||||
{post.summary && (
|
||||
<p style={{ fontSize: '1.1rem', color: '#6a6866', lineHeight: 1.7, marginBottom: 28, borderLeft: '3px solid #dddbd5', paddingLeft: 16 }}>
|
||||
{post.summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Meta */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, paddingTop: 20, borderTop: '1px solid #dddbd5' }}>
|
||||
{post.authorName && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: '50%', background: 'linear-gradient(135deg,#3b82f6,#6366f1)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 13, fontWeight: 700 }}>
|
||||
{post.authorName.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span style={{ fontSize: 14, fontWeight: 500, color: '#2a2a2a' }}>{post.authorName}</span>
|
||||
</div>
|
||||
)}
|
||||
{publishDate && (
|
||||
<>
|
||||
{post.authorName && <span style={{ color: '#dddbd5' }}>·</span>}
|
||||
<span style={{ fontFamily: 'monospace', fontSize: 11, color: '#aaa8a4', letterSpacing: '0.1em' }}>
|
||||
{publishDate.toUpperCase()}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Imagen de portada */}
|
||||
{imageUrl && (
|
||||
<div style={{ background: '#f0efec', borderBottom: '1px solid #dddbd5' }}>
|
||||
<div className="mx-auto" style={{ maxWidth: '1100px', padding: '0 2rem' }}>
|
||||
<motion.div initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.6, delay: 0.15 }}
|
||||
style={{ overflow: 'hidden', borderRadius: 4, aspectRatio: '16/7', marginTop: -1 }}>
|
||||
<img src={imageUrl} alt={post.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contenido */}
|
||||
<section style={{ background: '#f5f5f3', borderBottom: '1px solid #dddbd5', padding: '80px 0' }}
|
||||
className="px-8 md:px-14">
|
||||
<motion.div className="mx-auto" style={{ maxWidth: '780px' }}
|
||||
initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, delay: 0.2 }}>
|
||||
<div style={{ background: '#ffffff', border: '1px solid #dddbd5', borderRadius: 4, padding: '48px 52px', fontSize: '1rem', lineHeight: 1.85 }}>
|
||||
{renderContent(post.content)}
|
||||
</div>
|
||||
|
||||
{/* Volver */}
|
||||
<div style={{ marginTop: 48, paddingTop: 32, borderTop: '1px solid #dddbd5' }}>
|
||||
<Link href="/blog"
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontFamily: 'monospace', fontSize: 11, letterSpacing: '0.14em', color: '#3b82f6', textDecoration: 'none' }}>
|
||||
← VOLVER AL BLOG
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import ArticlePage from './ArticlePage';
|
||||
import { API_URL, assetUrl } from '../../../services/api';
|
||||
import type { BlogPost } from '../../../services/blog';
|
||||
|
||||
const BASE_URL = 'https://rynext.com.mx';
|
||||
|
||||
async function getPost(slug: string): Promise<BlogPost | null> {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/blog/${slug}`, { next: { revalidate: 3600 } });
|
||||
if (!res.ok) return null;
|
||||
return (await res.json()) as BlogPost;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
type Props = { params: Promise<{ slug: string }> };
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const post = await getPost(slug);
|
||||
|
||||
if (!post || !post.isPublished) {
|
||||
return { title: 'Artículo no encontrado | Blog', robots: { index: false } };
|
||||
}
|
||||
|
||||
const url = `${BASE_URL}/blog/${post.slug}`;
|
||||
const image = assetUrl(post.imageUrl);
|
||||
|
||||
return {
|
||||
title: `${post.title} | Blog | Rynext Technologies`,
|
||||
description: post.summary,
|
||||
alternates: { canonical: url },
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.summary,
|
||||
type: 'article',
|
||||
url,
|
||||
publishedTime: post.publishedAt,
|
||||
images: image ? [{ url: image, width: 1200, height: 630, alt: post.title }] : undefined,
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: post.title,
|
||||
description: post.summary,
|
||||
images: image ? [image] : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const post = await getPost(slug);
|
||||
|
||||
if (!post || !post.isPublished) notFound();
|
||||
|
||||
return <ArticlePage />;
|
||||
}
|
||||
@@ -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,148 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import Loader from '../../components/Loader';
|
||||
import { usePortfolio } from '../../services/portfolio';
|
||||
import { assetUrl } from '../../services/api';
|
||||
|
||||
const bg = '#f9f9f7';
|
||||
const text = '#0a0a0a';
|
||||
const sub = '#6a6866';
|
||||
const border = 'rgba(0,0,0,0.08)';
|
||||
const card = '#ffffff';
|
||||
const tagBg = 'rgba(0,0,0,0.05)';
|
||||
|
||||
function PortafolioContent() {
|
||||
const { data, isLoading } = usePortfolio();
|
||||
const projects = data ?? [];
|
||||
|
||||
if (isLoading) return <Loader className="py-32" />;
|
||||
|
||||
return (
|
||||
<main id="main-content">
|
||||
{/* Hero */}
|
||||
<section
|
||||
style={{ background: '#ffffff', borderBottom: `1px solid ${border}`, paddingTop: 140, paddingBottom: 80 }}
|
||||
className="px-8 md:px-14"
|
||||
>
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
<span className="mb-3 block text-xs font-semibold uppercase tracking-[0.14em]" style={{ color: '#3b82f6' }}>
|
||||
Casos de éxito
|
||||
</span>
|
||||
<h1 className="text-display mb-6" style={{ color: text, maxWidth: '22ch' }}>
|
||||
Construimos el sistema.{' '}
|
||||
<span style={{ color: '#3b82f6' }}>Tú operas el negocio.</span>
|
||||
</h1>
|
||||
<p className="max-w-2xl text-lg leading-relaxed" style={{ color: sub }}>
|
||||
Proyectos reales, resultados medibles. Cada implementación parte de entender la operación antes de escribir una sola línea de código.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Grid */}
|
||||
<section style={{ background: bg }} className="px-8 py-20 md:px-14">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
{projects.length === 0 ? (
|
||||
<p className="text-center py-20 text-sm" style={{ color: sub }}>Próximamente casos de éxito.</p>
|
||||
) : (
|
||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{projects.map((p, i) => {
|
||||
const cover = p.images.find((img) => img.isCover) ?? p.images.sort((a, b) => a.order - b.order)[0];
|
||||
return (
|
||||
<motion.article
|
||||
key={p.id}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-40px' }}
|
||||
transition={{ duration: 0.55, delay: i * 0.08, ease: [0.22, 1, 0.36, 1] }}
|
||||
style={{
|
||||
borderRadius: 14,
|
||||
border: `1px solid ${border}`,
|
||||
background: card,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* Cover */}
|
||||
<div style={{ aspectRatio: '16/9', overflow: 'hidden', background: 'rgba(0,0,0,0.03)', borderBottom: `1px solid ${border}` }}>
|
||||
{cover ? (
|
||||
<img
|
||||
src={assetUrl(cover.url)}
|
||||
alt={p.title}
|
||||
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>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ padding: '24px 24px 28px', display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
{p.clientCompanyName && (
|
||||
<span className="text-xs font-semibold uppercase tracking-wider mb-2 block" style={{ color: '#3b82f6' }}>
|
||||
{p.clientCompanyName}
|
||||
</span>
|
||||
)}
|
||||
<h2 className="font-display font-bold mb-3" style={{ fontSize: '1.1rem', color: text, letterSpacing: '-0.01em' }}>
|
||||
{p.title}
|
||||
</h2>
|
||||
{p.publicSummary && (
|
||||
<p className="text-sm leading-relaxed mb-5" style={{ color: sub }}>
|
||||
{p.publicSummary}
|
||||
</p>
|
||||
)}
|
||||
{p.technologies.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-auto">
|
||||
{p.technologies.map((t) => (
|
||||
<span
|
||||
key={t.id}
|
||||
style={{
|
||||
fontSize: 10.5,
|
||||
fontWeight: 600,
|
||||
padding: '3px 9px',
|
||||
borderRadius: 99,
|
||||
background: tagBg,
|
||||
color: t.color ?? sub,
|
||||
letterSpacing: '0.01em',
|
||||
}}
|
||||
>
|
||||
{t.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export function PortafolioPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
if (!mounted) return (
|
||||
<div className="flex min-h-screen items-center justify-center" style={{ background: bg }}>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
return <PortafolioContent />;
|
||||
}
|
||||
@@ -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 { PortafolioPage } from './PortafolioPage';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Portafolio | Rynext Technologies',
|
||||
description: 'Casos de éxito reales. Proyectos que transformaron operaciones empresariales con software diseñado a medida.',
|
||||
alternates: { canonical: 'https://rynext.com.mx/portafolio' },
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return <PortafolioPage />;
|
||||
}
|
||||
@@ -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,40 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { API_URL } from '../services/api';
|
||||
import type { BlogPost } from '../services/blog';
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
const BASE_URL = 'https://rynext.com.mx';
|
||||
|
||||
async function getPublishedPosts(): Promise<BlogPost[]> {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/blog`, { next: { revalidate } });
|
||||
if (!res.ok) return [];
|
||||
const posts = (await res.json()) as BlogPost[];
|
||||
return posts.filter(p => p.isPublished && p.slug);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const posts = await getPublishedPosts();
|
||||
|
||||
const staticRoutes: MetadataRoute.Sitemap = [
|
||||
{ url: BASE_URL, changeFrequency: 'monthly', priority: 1 },
|
||||
{ url: `${BASE_URL}/sobre-nosotros`, changeFrequency: 'monthly', priority: 0.8 },
|
||||
{ url: `${BASE_URL}/productos`, changeFrequency: 'monthly', priority: 0.8 },
|
||||
{ url: `${BASE_URL}/portafolio`, changeFrequency: 'monthly', priority: 0.8 },
|
||||
{ url: `${BASE_URL}/blog`, changeFrequency: 'weekly', priority: 0.6 },
|
||||
{ url: `${BASE_URL}/privacidad`, changeFrequency: 'yearly', priority: 0.3 },
|
||||
];
|
||||
|
||||
const blogRoutes: MetadataRoute.Sitemap = posts.map(p => ({
|
||||
url: `${BASE_URL}/blog/${p.slug}`,
|
||||
lastModified: p.publishedAt ? new Date(p.publishedAt) : undefined,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.5,
|
||||
}));
|
||||
|
||||
return [...staticRoutes, ...blogRoutes];
|
||||
}
|
||||
@@ -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 {
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
variant?: 'primary' | 'secondary';
|
||||
variant?: "primary" | "secondary";
|
||||
className?: string;
|
||||
isNavyBg?: boolean;
|
||||
}
|
||||
|
||||
export const Button = ({ children, onClick, href, variant = 'primary', className = '', isNavyBg = false }: ButtonProps) => {
|
||||
const base = 'inline-flex items-center gap-3 px-8 py-4 text-base font-bold transition hover:scale-[1.02] cursor-pointer';
|
||||
export const Button = ({
|
||||
children,
|
||||
onClick,
|
||||
href,
|
||||
variant = "primary",
|
||||
className = "",
|
||||
isNavyBg = false,
|
||||
}: ButtonProps) => {
|
||||
const base =
|
||||
"inline-flex items-center gap-3 px-8 py-4 text-base font-bold transition hover:scale-[1.02] cursor-pointer";
|
||||
|
||||
const styles = {
|
||||
primary: 'bg-brand-red text-white shadow-lg shadow-red-500/20 hover:bg-[#b60014]',
|
||||
primary:
|
||||
"bg-brand-red text-white shadow-lg shadow-blue-500/20 hover:bg-[#2563eb]",
|
||||
secondary: isNavyBg
|
||||
? 'border border-white/15 bg-white/5 text-white hover:bg-white/10'
|
||||
: 'border border-brand-navy/15 text-brand-navy hover:border-brand-blue hover:text-brand-blue',
|
||||
? "border border-white/15 bg-white/5 text-white hover:bg-white/10"
|
||||
: "border border-brand-navy/15 text-brand-navy hover:border-brand-blue hover:text-brand-blue",
|
||||
};
|
||||
|
||||
const cls = `${base} ${styles[variant]} ${className}`;
|
||||
|
||||
if (href) {
|
||||
return <a href={href} className={cls}>{children}</a>;
|
||||
return (
|
||||
<a href={href} className={cls}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return <button onClick={onClick} className={cls}>{children}</button>;
|
||||
return (
|
||||
<button onClick={onClick} className={cls}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
const supportEmail = '[email protected]';
|
||||
const mailToHref = `mailto:${supportEmail}?subject=${encodeURIComponent('Consulta Rynext')}&body=${encodeURIComponent('Hola Rynext,\n\nQuiero agendar una conversación sobre un proyecto.\n\nGracias.')}`;
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'https://localhost:7001';
|
||||
|
||||
type Status = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
const initialForm = { nombre: '', empresa: '', email: '', telefono: '', mensaje: '' };
|
||||
|
||||
export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
|
||||
const [form, setForm] = useState(initialForm);
|
||||
const [status, setStatus] = useState<Status>('idle');
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const set = (k: keyof typeof form) =>
|
||||
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
|
||||
setForm(f => ({ ...f, [k]: e.target.value }));
|
||||
|
||||
const handleClose = () => {
|
||||
if (status !== 'loading') {
|
||||
onClose();
|
||||
setTimeout(() => { setForm(initialForm); setStatus('idle'); }, 300);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!form.nombre.trim() || !form.email.trim()) return;
|
||||
|
||||
setStatus('loading');
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/contact`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: form.nombre,
|
||||
email: form.email,
|
||||
phone: form.telefono || undefined,
|
||||
company: form.empresa || undefined,
|
||||
message: form.mensaje || 'Sin mensaje adicional.',
|
||||
source: 'landing',
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error('Error del servidor');
|
||||
setStatus('success');
|
||||
} catch {
|
||||
setStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
const card = isNavyBg ? '#0d1b35' : '#ffffff';
|
||||
const bdr = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.1)';
|
||||
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
|
||||
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866';
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%', borderRadius: 8, padding: '11px 14px',
|
||||
fontSize: 14, fontFamily: 'inherit', outline: 'none',
|
||||
transition: 'border-color 150ms',
|
||||
...(isNavyBg
|
||||
? { background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff' }
|
||||
: { background: '#f8fafc', border: '1px solid rgba(0,0,0,0.1)', color: '#0a0a0a' }),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fadeIn">
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
style={{ background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(6px)' }}
|
||||
onClick={e => { if (e.target === e.currentTarget) handleClose(); }}
|
||||
>
|
||||
<div
|
||||
className={`relative w-full max-w-md rounded-2xl border p-8 shadow-2xl transition-all ${
|
||||
isNavyBg
|
||||
? 'border-white/20 bg-brand-navy text-white'
|
||||
: 'border-gray-200 bg-white text-brand-black'
|
||||
}`}
|
||||
className="relative w-full max-w-xl rounded-2xl shadow-2xl"
|
||||
style={{ background: card, border: `1px solid ${bdr}` }}
|
||||
>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 cursor-pointer text-gray-400 transition-colors hover:text-brand-blue"
|
||||
aria-label="Cerrar modal"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-brand-blue/20 text-brand-blue">
|
||||
<Mail size={24} />
|
||||
</div>
|
||||
<h3 className="mb-2 text-2xl font-bold">Hablemos de tu proyecto</h3>
|
||||
<p className={`mb-6 text-sm ${isNavyBg ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||
Puedes escribirnos directamente a nuestro correo oficial de atención y tu cliente de correo se abrirá automáticamente.
|
||||
</p>
|
||||
|
||||
<div className={`mb-6 rounded-xl border p-4 ${isNavyBg ? 'border-brand-blue/30 bg-brand-blue/10' : 'border-brand-blue/30 bg-brand-blue/5'}`}>
|
||||
<p className="mb-1 text-xs font-semibold tracking-[0.08em] text-brand-blue">
|
||||
Correo electrónico
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-8 pt-8 pb-6" style={{ borderBottom: `1px solid ${bdr}` }}>
|
||||
<div>
|
||||
<h3 className="font-display font-bold text-xl" style={{ color: text, letterSpacing: '-0.02em' }}>
|
||||
Hablemos de tu proyecto
|
||||
</h3>
|
||||
<p className="text-sm mt-0.5" style={{ color: sub }}>
|
||||
Te respondemos en menos de 24 h
|
||||
</p>
|
||||
<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>
|
||||
<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">
|
||||
<a
|
||||
href={mailToHref}
|
||||
className="inline-flex cursor-pointer items-center justify-center rounded-xl bg-brand-blue px-4 py-3 text-sm font-bold text-white transition hover:bg-brand-blue/90"
|
||||
>
|
||||
Enviar correo
|
||||
</a>
|
||||
{/* Success state */}
|
||||
{status === 'success' ? (
|
||||
<div className="flex flex-col items-center justify-center gap-4 px-8 py-14 text-center">
|
||||
<CheckCircle size={48} style={{ color: '#22c55e' }} />
|
||||
<h4 className="font-display font-bold text-lg" style={{ color: text }}>
|
||||
¡Mensaje enviado!
|
||||
</h4>
|
||||
<p className="text-sm leading-relaxed" style={{ color: sub, maxWidth: '32ch' }}>
|
||||
Recibimos tu solicitud. Un miembro del equipo te contactará pronto.
|
||||
</p>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={`cursor-pointer rounded-xl border px-4 py-3 text-sm font-bold transition hover:border-brand-blue hover:text-brand-blue ${
|
||||
isNavyBg
|
||||
? 'border-white/20 bg-white/5 text-white'
|
||||
: 'border-brand-navy/15 bg-white text-brand-navy'
|
||||
}`}
|
||||
onClick={handleClose}
|
||||
className="mt-2 px-6 py-2.5 text-sm font-bold text-white rounded-xl transition hover:opacity-90"
|
||||
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer' }}
|
||||
>
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { ThemeProps } from '../types';
|
||||
|
||||
@@ -45,7 +46,7 @@ export const CookieBanner = ({ isNavyBg }: ThemeProps) => {
|
||||
<button
|
||||
onClick={accept}
|
||||
className="shrink-0 px-5 py-2 text-sm font-bold text-white transition hover:opacity-90"
|
||||
style={{ background: '#e50914', borderRadius: 3, border: 'none', cursor: 'pointer' }}
|
||||
style={{ background: '#3b82f6', borderRadius: 3, border: 'none', cursor: 'pointer' }}
|
||||
>
|
||||
Aceptar
|
||||
</button>
|
||||
|
||||
@@ -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 { motion, AnimatePresence } from 'framer-motion';
|
||||
import type { ThemeProps } from '../types';
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import { Mail, MapPin, MessageCircle } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
import type { ThemeProps } from '../types';
|
||||
|
||||
const navLinks = [
|
||||
{ label: 'Sistemas', href: '#machinery' },
|
||||
{ label: 'Anatomía', href: '#anatomy' },
|
||||
{ label: 'Proceso', href: '#proceso' },
|
||||
{ label: 'Principios', href: '#principios' },
|
||||
];
|
||||
"use client";
|
||||
import { Mail, MapPin, MessageCircle } from "lucide-react";
|
||||
import { FaFacebookF, FaLinkedinIn, FaMapMarkerAlt } from "react-icons/fa";
|
||||
import Link from "next/link";
|
||||
import type { ThemeProps } from "../types";
|
||||
import { pageLinks } from "../data/navigationData";
|
||||
import { FaRegMessage } from "react-icons/fa6";
|
||||
import { MdEmail } from "react-icons/md";
|
||||
|
||||
export const Footer = ({ isNavyBg }: ThemeProps) => {
|
||||
const bg = isNavyBg ? '#001040' : '#0a0a0a';
|
||||
const sub = 'rgba(255,255,255,0.45)';
|
||||
const border = 'rgba(255,255,255,0.08)';
|
||||
|
||||
const handleNav = (href: string) => {
|
||||
const el = document.querySelector(href);
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
const bg = isNavyBg ? "#001040" : "#0a0a0a";
|
||||
const sub = "rgba(255,255,255,0.45)";
|
||||
const border = "rgba(255,255,255,0.08)";
|
||||
|
||||
return (
|
||||
<footer
|
||||
style={{ background: bg, borderTop: `1px solid ${border}` }}
|
||||
className="px-8 pt-20 pb-10 md:px-14 transition-colors duration-500"
|
||||
className="px-14 pt-10 transition-colors duration-500"
|
||||
role="contentinfo"
|
||||
>
|
||||
<div className="mx-auto max-w-7xl">
|
||||
@@ -30,23 +23,36 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
||||
<div className="grid gap-14 md:grid-cols-[1.8fr_1fr_1.2fr]">
|
||||
{/* Brand */}
|
||||
<div>
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div className="mb-6 flex items-center gap-2.5">
|
||||
<img
|
||||
src="/icons/Logo_blanco.svg"
|
||||
alt="Rynext logo"
|
||||
className="h-9 w-auto object-contain"
|
||||
/>
|
||||
<img
|
||||
src="/icons/Text_Logo_Blanco.svg"
|
||||
alt="Rynext"
|
||||
style={{
|
||||
width: "130px",
|
||||
height: "18px",
|
||||
objectFit: "cover",
|
||||
objectPosition: "center 52%",
|
||||
display: "block",
|
||||
}}
|
||||
alt="Logo"
|
||||
className="h-11 w-11 shrink-0 object-contain"
|
||||
/>
|
||||
<div className="flex flex-col leading-none">
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "'Quicksand', sans-serif",
|
||||
fontWeight: 700,
|
||||
fontSize: 19,
|
||||
letterSpacing: "-0.01em",
|
||||
color: "#ffffff",
|
||||
}}
|
||||
>
|
||||
Rynext
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "'Quicksand', sans-serif",
|
||||
fontWeight: 500,
|
||||
fontSize: 12,
|
||||
letterSpacing: "0.04em",
|
||||
color: "rgba(255,255,255,0.45)",
|
||||
}}
|
||||
>
|
||||
Technologies
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className="text-sm leading-relaxed mb-8"
|
||||
@@ -65,18 +71,17 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
||||
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
|
||||
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLAnchorElement).style.background = "rgba(255,255,255,0.14)";
|
||||
(e.currentTarget as HTMLAnchorElement).style.color = "#fff";
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.14)";
|
||||
e.currentTarget.style.color = "#fff";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLAnchorElement).style.background = "rgba(255,255,255,0.07)";
|
||||
(e.currentTarget as HTMLAnchorElement).style.color = sub;
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.07)";
|
||||
e.currentTarget.style.color = sub;
|
||||
}}
|
||||
>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/>
|
||||
</svg>
|
||||
<FaFacebookF />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://linkedin.com/company/rynext"
|
||||
target="_blank"
|
||||
@@ -85,52 +90,38 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
||||
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
|
||||
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLAnchorElement).style.background =
|
||||
"rgba(255,255,255,0.14)";
|
||||
(e.currentTarget as HTMLAnchorElement).style.color = "#fff";
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.14)";
|
||||
e.currentTarget.style.color = "#fff";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLAnchorElement).style.background =
|
||||
"rgba(255,255,255,0.07)";
|
||||
(e.currentTarget as HTMLAnchorElement).style.color = sub;
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.07)";
|
||||
e.currentTarget.style.color = sub;
|
||||
}}
|
||||
>
|
||||
{/* LinkedIn SVG */}
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
|
||||
<rect x="2" y="9" width="4" height="12" />
|
||||
<circle cx="4" cy="4" r="2" />
|
||||
</svg>
|
||||
<FaLinkedinIn />
|
||||
</a>
|
||||
<a
|
||||
href={`https://wa.me/529984814889?text=${encodeURIComponent('¿En qué podemos ayudarte?')}`}
|
||||
href={`https://wa.me/529984814889?text=${encodeURIComponent("¿En qué podemos ayudarte?")}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Rynext en WhatsApp"
|
||||
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
|
||||
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLAnchorElement).style.background =
|
||||
"rgba(255,255,255,0.14)";
|
||||
(e.currentTarget as HTMLAnchorElement).style.color = "#fff";
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.14)";
|
||||
e.currentTarget.style.color = "#fff";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLAnchorElement).style.background =
|
||||
"rgba(255,255,255,0.07)";
|
||||
(e.currentTarget as HTMLAnchorElement).style.color = sub;
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.07)";
|
||||
e.currentTarget.style.color = sub;
|
||||
}}
|
||||
>
|
||||
<MessageCircle size={15} />
|
||||
<FaRegMessage />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navegación */}
|
||||
{/* Páginas */}
|
||||
<div>
|
||||
<p
|
||||
className="text-mono-label mb-6"
|
||||
@@ -139,26 +130,20 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
||||
letterSpacing: "0.12em",
|
||||
}}
|
||||
>
|
||||
Navegación
|
||||
Páginas
|
||||
</p>
|
||||
<ul className="flex flex-col gap-3.5">
|
||||
{navLinks.map((link) => (
|
||||
{pageLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<button
|
||||
onClick={() => handleNav(link.href)}
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm font-medium transition-colors"
|
||||
style={{
|
||||
color: sub,
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: 0,
|
||||
}}
|
||||
style={{ color: sub, textDecoration: "none" }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.color = "#fff")}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
|
||||
>
|
||||
{link.label}
|
||||
</button>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -176,19 +161,14 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
||||
Contacto
|
||||
</p>
|
||||
<ul className="flex flex-col gap-4">
|
||||
<li className="flex items-start gap-3">
|
||||
<MapPin
|
||||
size={14}
|
||||
style={{ color: "#e50914", marginTop: 2, flexShrink: 0 }}
|
||||
/>
|
||||
<li className="flex items-center gap-3">
|
||||
<FaMapMarkerAlt className="text-white text-center" />
|
||||
<span className="text-sm leading-snug" style={{ color: sub }}>
|
||||
Cancún, Quintana Roo
|
||||
<br />
|
||||
México
|
||||
Cancún, Quintana Roo. México
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-3">
|
||||
<Mail size={14} style={{ color: "#e50914", flexShrink: 0 }} />
|
||||
<MdEmail className="text-white text-center" />
|
||||
<a
|
||||
href="mailto:[email protected]"
|
||||
className="text-sm font-medium transition-colors"
|
||||
@@ -203,7 +183,7 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
||||
<a
|
||||
href="mailto:[email protected]"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-bold text-white transition hover:opacity-90"
|
||||
style={{ background: "#e50914", borderRadius: 3 }}
|
||||
style={{ background: "#3b82f6", borderRadius: 3 }}
|
||||
>
|
||||
Hablemos de tu proyecto
|
||||
</a>
|
||||
@@ -211,26 +191,31 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<div
|
||||
className="mt-16 border-t pt-8 text-center"
|
||||
style={{ borderColor: border }}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2 sm:flex-row sm:justify-center sm:gap-4">
|
||||
<p className="text-xs" style={{ color: "rgba(255,255,255,0.25)" }}>
|
||||
© {new Date().getFullYear()} Rynext Technologies S.A.S. | Todos los derechos reservados.
|
||||
</p>
|
||||
<Link
|
||||
to="/privacidad"
|
||||
className="text-xs transition-colors"
|
||||
style={{ color: "rgba(255,255,255,0.25)", textDecoration: "none" }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.color = "rgba(255,255,255,0.6)")}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.color = "rgba(255,255,255,0.25)")}
|
||||
>
|
||||
Aviso de Privacidad
|
||||
</Link>
|
||||
</div>
|
||||
{/* Bottom bar — full width */}
|
||||
<div
|
||||
className="mt-10 -mx-8 px-8 md:-mx-14 md:px-14"
|
||||
style={{ borderTop: `1px solid ${border}` }}
|
||||
>
|
||||
<div className="mx-auto flex max-w-7xl flex-col items-center gap-2 py-5 sm:flex-row sm:justify-between">
|
||||
<p className="text-xs" style={{ color: "rgba(255,255,255,0.25)" }}>
|
||||
© {new Date().getFullYear()} Rynext Technologies — Todos los
|
||||
derechos reservados.
|
||||
</p>
|
||||
<Link
|
||||
href="/privacidad"
|
||||
className="text-xs transition-colors"
|
||||
style={{ color: "rgba(255,255,255,0.25)", textDecoration: "none" }}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.color = "rgba(255,255,255,0.6)")
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.color = "rgba(255,255,255,0.25)")
|
||||
}
|
||||
>
|
||||
Aviso de Privacidad
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</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,34 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import { Link, useLocation } from "react-router";
|
||||
import type { NavbarProps } from "../types";
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Button } from './Button';
|
||||
import { pageLinks } from '../data/navigationData';
|
||||
|
||||
const pageLinks = [
|
||||
{ label: "Sobre Nosotros", href: "/sobre-nosotros" },
|
||||
];
|
||||
|
||||
export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
||||
export const Navbar = () => {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
const rawPathname = usePathname();
|
||||
const pathname = rawPathname?.replace(/\/$/, '') || '/';
|
||||
|
||||
const scrollToContact = () => {
|
||||
setMenuOpen(false);
|
||||
const el = document.getElementById('contacto');
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
||||
else window.location.href = '/#contacto';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 80);
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setMenuOpen(false);
|
||||
}, [location.pathname]);
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -34,36 +41,42 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: scrolled ? "rgba(255,255,255,0.97)" : "transparent",
|
||||
borderBottom: scrolled ? "1px solid rgba(0,0,0,0.08)" : "1px solid transparent",
|
||||
backdropFilter: scrolled ? "blur(16px)" : "none",
|
||||
WebkitBackdropFilter: scrolled ? "blur(16px)" : "none",
|
||||
transition: "background 200ms, border-color 200ms",
|
||||
background: scrolled ? 'rgba(255,255,255,0.97)' : 'transparent',
|
||||
borderBottom: scrolled ? '1px solid rgba(0,0,0,0.08)' : '1px solid transparent',
|
||||
backdropFilter: scrolled ? 'blur(16px)' : 'none',
|
||||
WebkitBackdropFilter: scrolled ? 'blur(16px)' : 'none',
|
||||
transition: 'background 200ms, border-color 200ms',
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-8 py-4 md:px-14">
|
||||
{/* Logo — siempre modo claro */}
|
||||
<Link to="/" aria-label="Rynext Technologies — inicio" className="flex items-center gap-3">
|
||||
<img src="/icons/Logo_negro.svg" alt="Rynext logo" className="h-9 w-auto object-contain" />
|
||||
<Link href="/" aria-label="Rynext Technologies — inicio" className="flex items-center gap-2.5">
|
||||
<img
|
||||
src="/icons/Text_Logo_Negro.svg"
|
||||
alt="Rynext"
|
||||
style={{ width: "130px", height: "18px", objectFit: "cover", objectPosition: "center 52%", display: "block" }}
|
||||
src="/icons/Logo_negro.svg"
|
||||
alt="Logo"
|
||||
className="h-11 w-11 shrink-0 object-contain"
|
||||
/>
|
||||
<div className="flex flex-col leading-none">
|
||||
<span style={{ fontFamily: "'Quicksand', sans-serif", fontWeight: 700, fontSize: 19, letterSpacing: '-0.01em', color: '#111827' }}>
|
||||
Rynext
|
||||
</span>
|
||||
<span style={{ fontFamily: "'Quicksand', sans-serif", fontWeight: 500, fontSize: 12, letterSpacing: '0.04em', color: '#6b7280' }}>
|
||||
Technologies
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<nav className="hidden items-center gap-8 md:flex" aria-label="Navegación principal">
|
||||
{pageLinks.map((link) => {
|
||||
const active = location.pathname === link.href;
|
||||
{pageLinks.filter(l => l.href !== '/privacidad').map((link) => {
|
||||
const active = pathname === link.href;
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
to={link.href}
|
||||
href={link.href}
|
||||
className="text-sm font-semibold transition-colors"
|
||||
style={{ color: active ? "#0a0a0a" : "#6a6866", textDecoration: "none" }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.color = "#0a0a0a")}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.color = active ? "#0a0a0a" : "#6a6866")}
|
||||
style={{ color: active ? '#0a0a0a' : '#6a6866', textDecoration: 'none' }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.color = '#0a0a0a')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.color = active ? '#0a0a0a' : '#6a6866')}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
@@ -71,9 +84,9 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
||||
})}
|
||||
|
||||
<button
|
||||
onClick={onOpenContact}
|
||||
onClick={scrollToContact}
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-bold text-white transition hover:scale-[1.02] hover:opacity-90"
|
||||
style={{ background: "#e50914", border: "none", cursor: "pointer", borderRadius: 3 }}
|
||||
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer', borderRadius: 3 }}
|
||||
>
|
||||
Hablemos
|
||||
</button>
|
||||
@@ -83,8 +96,8 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
||||
<button
|
||||
onClick={() => setMenuOpen((v) => !v)}
|
||||
className="flex items-center justify-center rounded p-2 md:hidden"
|
||||
style={{ color: "#0a0a0a", background: "none", border: "none", cursor: "pointer" }}
|
||||
aria-label={menuOpen ? "Cerrar menú" : "Abrir menú"}
|
||||
style={{ color: '#0a0a0a', background: 'none', border: 'none', cursor: 'pointer' }}
|
||||
aria-label={menuOpen ? 'Cerrar menú' : 'Abrir menú'}
|
||||
aria-expanded={menuOpen}
|
||||
>
|
||||
{menuOpen ? <X size={22} /> : <Menu size={22} />}
|
||||
@@ -101,8 +114,8 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-x-0 top-[65px] z-40 px-8 py-6 md:hidden"
|
||||
style={{ background: "#ffffff", borderBottom: "1px solid #dddbd5" }}
|
||||
className="fixed inset-x-0 top-[73px] z-40 px-8 py-6 md:hidden"
|
||||
style={{ background: '#ffffff', borderBottom: '1px solid #dddbd5' }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Menú de navegación"
|
||||
@@ -111,21 +124,17 @@ export const Navbar = ({ onOpenContact }: NavbarProps) => {
|
||||
{pageLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
to={link.href}
|
||||
href={link.href}
|
||||
className="text-left text-base font-semibold"
|
||||
style={{ color: location.pathname === link.href ? "#0a0a0a" : "#6a6866", textDecoration: "none" }}
|
||||
style={{ color: pathname === link.href ? '#0a0a0a' : '#6a6866', textDecoration: 'none' }}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
<div className="pt-2">
|
||||
<button
|
||||
onClick={onOpenContact}
|
||||
className="w-full py-3 text-center text-sm font-bold text-white"
|
||||
style={{ background: "#e50914", border: "none", cursor: "pointer", borderRadius: 3 }}
|
||||
>
|
||||
<Button onClick={scrollToContact} className="w-full py-3 text-center text-sm font-bold text-white">
|
||||
Hablemos
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</motion.div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
import { useEffect } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export const ScrollToTop = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -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 = [
|
||||
{
|
||||
id: 1,
|
||||
@@ -5,6 +7,7 @@ export const machineryData = [
|
||||
title: 'OPERACIONES',
|
||||
description: 'Plataformas internas, automatización de flujos y control de procesos críticos. Sistemas que eliminan la fricción operativa y dan visibilidad en tiempo real.',
|
||||
tags: ['Portales de gestión', 'Automatización de flujos', 'Reportes operacionales', 'Control de acceso por rol'],
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -12,6 +15,7 @@ export const machineryData = [
|
||||
title: 'VENTAS',
|
||||
description: 'Sistemas de venta, cobro y facturación integrados al núcleo del negocio. Desde el punto de contacto con el cliente hasta el comprobante fiscal.',
|
||||
tags: ['E-commerce a medida', 'Integración de pagos', 'Facturación y CFDI', 'Recuperación de carritos'],
|
||||
icon: ShoppingCart,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -19,6 +23,7 @@ export const machineryData = [
|
||||
title: 'INTEGRACIONES',
|
||||
description: 'Conectamos sistemas heterogéneos en un ecosistema coherente: ERPs, CRMs, proveedores y APIs de terceros. Sin silos, sin duplicidad de datos.',
|
||||
tags: ['ERP / SAP / Odoo', 'APIs de terceros', 'Sincronización bidireccional', 'Webhooks y eventos'],
|
||||
icon: Workflow,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@@ -26,6 +31,7 @@ export const machineryData = [
|
||||
title: 'AUTOMATIZACIÓN',
|
||||
description: 'Eliminamos intervención humana donde no aporta valor: aprobaciones, notificaciones, sincronizaciones y pipelines de datos que corren solos.',
|
||||
tags: ['Bots y pipelines', 'Procesamiento de datos', 'Notificaciones inteligentes', 'Triggers y colas'],
|
||||
icon: Zap,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
@@ -33,5 +39,6 @@ export const machineryData = [
|
||||
title: 'DATOS',
|
||||
description: 'Instrumentación del negocio para tomar decisiones con evidencia. Dashboards ejecutivos, modelos analíticos y alertas que activan antes de que el problema escale.',
|
||||
tags: ['Dashboards ejecutivos', 'Pipelines de datos', 'KPIs e instrumentación', 'Alertas y umbrales'],
|
||||
icon: BarChart3,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
export const pageLinks = [
|
||||
{ label: 'Sobre Nosotros', href: '/sobre-nosotros' },
|
||||
{ label: 'Productos', href: '/productos' },
|
||||
{ label: 'Portafolio', href: '/portafolio' },
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
{ label: 'Aviso de Privacidad', href: '/privacidad' },
|
||||
];
|
||||
|
||||
export const navLinks = [
|
||||
{ label: 'Soluciones', href: '#showcase' },
|
||||
{ label: 'Portafolio', href: '#portafolio' },
|
||||
{ label: 'Sistemas', href: '#machinery' },
|
||||
{ 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";
|
||||
|
||||
@theme {
|
||||
--color-brand-blue: #0077c2;
|
||||
--color-brand-navy: #001f5b;
|
||||
--color-brand-black: #0a0a0a;
|
||||
--color-brand-red: #e50914;
|
||||
--color-brand-red: #3b82f6;
|
||||
--color-bg-light: #f9f9f7;
|
||||
--color-bg-alt: #f0efec;
|
||||
--color-border-light: #dddbd5;
|
||||
@@ -14,16 +14,11 @@
|
||||
--color-dark-surface: #0f1520;
|
||||
--color-dark-border: #1c2535;
|
||||
|
||||
--font-sans: 'Inter', system-ui, sans-serif;
|
||||
--font-display: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
--font-sans: 'Quicksand', system-ui, sans-serif;
|
||||
--font-display: 'Quicksand', system-ui, sans-serif;
|
||||
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(229, 9, 20, 0.18);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
body {
|
||||
@@ -37,14 +32,9 @@
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: 2px solid #e50914;
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #001f5b;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* Typography scale */
|
||||
@@ -176,7 +166,7 @@
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.prose-rynext a {
|
||||
color: #e50914;
|
||||
color: #3b82f6;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
@@ -1,13 +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'
|
||||
|
||||
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. de C.V.</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 type { ThemeProps } from '../types';
|
||||
|
||||
const layers = [
|
||||
{
|
||||
id: 'presentation',
|
||||
num: '01',
|
||||
title: 'PRESENTACIÓN',
|
||||
subtitle: 'La interfaz donde opera el usuario',
|
||||
description:
|
||||
'Portales de administración, apps web, paneles de monitoreo y puntos de venta. Construidos para adopción real, no para demos.',
|
||||
tech: ['React', 'Next.js', 'TypeScript', 'Tailwind', 'Mobile Web'],
|
||||
},
|
||||
{
|
||||
id: 'logic',
|
||||
num: '02',
|
||||
title: 'LÓGICA DE NEGOCIO',
|
||||
subtitle: 'Las reglas que gobiernan la operación',
|
||||
description:
|
||||
'Validaciones, cálculos, flujos de aprobación y orquestación de procesos. El código que representa cómo funciona tu negocio.',
|
||||
tech: ['Node.js', 'Python', 'REST APIs', 'GraphQL', 'Microservicios'],
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
num: '03',
|
||||
title: 'DATOS',
|
||||
subtitle: 'Persistencia y acceso a la información',
|
||||
description:
|
||||
'Modelo de datos diseñado para tu operación. Bases relacionales, NoSQL, caché y búsqueda de texto. Una sola fuente de verdad.',
|
||||
tech: ['PostgreSQL', 'MongoDB', 'Redis', 'Elasticsearch', 'Prisma'],
|
||||
},
|
||||
{
|
||||
id: 'infra',
|
||||
num: '04',
|
||||
title: 'INFRAESTRUCTURA',
|
||||
subtitle: 'La base que soporta el sistema',
|
||||
description:
|
||||
'Cloud, contenedores, pipelines de CI/CD y observabilidad. Diseñada para que el sistema esté siempre disponible y sea fácil de operar.',
|
||||
tech: ['AWS', 'Docker', 'GitHub Actions', 'Terraform', 'Monitoring'],
|
||||
},
|
||||
{
|
||||
id: 'integrations',
|
||||
num: '05',
|
||||
title: 'INTEGRACIONES',
|
||||
subtitle: 'Conexión con el ecosistema externo',
|
||||
description:
|
||||
'ERPs, sistemas de pago, servicios de mensajería, proveedores y APIs externas. El sistema habla con todo lo que el negocio necesita.',
|
||||
tech: ['Stripe', 'SAP / Odoo', 'OAuth 2.0', 'Webhooks', 'SMS / Email'],
|
||||
},
|
||||
];
|
||||
import { anatomyLayers as layers } from '../data/anatomyData';
|
||||
|
||||
export const Anatomy = ({ isNavyBg }: ThemeProps) => {
|
||||
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
|
||||
@@ -101,12 +55,12 @@ export const Anatomy = ({ isNavyBg }: ThemeProps) => {
|
||||
{/* Number */}
|
||||
<span
|
||||
className="font-display font-black"
|
||||
style={{ fontSize: 64, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
|
||||
style={{ fontSize: 64, lineHeight: 1, color: '#3b82f6', letterSpacing: '-0.04em' }}
|
||||
>
|
||||
{layer.num}
|
||||
</span>
|
||||
|
||||
{/* Title + description */}
|
||||
{/* Title + description + tech */}
|
||||
<div style={{ paddingTop: 8 }}>
|
||||
<p
|
||||
className="font-display font-black mb-1"
|
||||
@@ -117,9 +71,25 @@ export const Anatomy = ({ isNavyBg }: ThemeProps) => {
|
||||
<p className="text-xs mb-3" style={{ color: sub }}>
|
||||
{layer.subtitle}
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed" style={{ color: sub, maxWidth: '60ch' }}>
|
||||
<p className="text-sm leading-relaxed mb-4" style={{ color: sub, maxWidth: '60ch' }}>
|
||||
{layer.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{layer.tech.map(t => (
|
||||
<span key={t} style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
padding: '3px 10px',
|
||||
borderRadius: 99,
|
||||
border: `1px solid ${border}`,
|
||||
color: isNavyBg ? 'rgba(255,255,255,0.55)' : 'rgba(0,0,0,0.5)',
|
||||
background: isNavyBg ? 'rgba(255,255,255,0.04)' : 'rgba(59,130,246,0.05)',
|
||||
letterSpacing: '0.01em',
|
||||
}}>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -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 { ArrowRight } from 'lucide-react';
|
||||
import type { HeroProps } from '../types';
|
||||
import { Button } from './Button';
|
||||
import type { ThemeProps } from '../types';
|
||||
import { Button } from '../components/Button';
|
||||
import { SystemMap } from './SystemMap';
|
||||
import { useContact } from '../contexts/ContactContext';
|
||||
|
||||
const ease = [0.22, 1, 0.36, 1] as const;
|
||||
|
||||
@@ -19,7 +21,8 @@ function RevealLine({ children, delay = 0 }: { children: React.ReactNode; delay?
|
||||
);
|
||||
}
|
||||
|
||||
export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
|
||||
export const Hero = ({ isNavyBg }: ThemeProps) => {
|
||||
const { open: openContact } = useContact();
|
||||
const bg = isNavyBg ? '#001f5b' : '#ffffff';
|
||||
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
|
||||
const sub = isNavyBg ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.45)';
|
||||
@@ -31,20 +34,14 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
|
||||
style={{ background: bg, color: text, borderBottom: isNavyBg ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.1)' }}
|
||||
aria-label="Rynext Technologies — Hero"
|
||||
>
|
||||
{/* Subtle grid bg */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 bg-grid-lines"
|
||||
style={{ color: gridLine }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
|
||||
{/* Main content */}
|
||||
<div className="relative mx-auto grid w-full max-w-7xl flex-1 items-center gap-12 px-8 pb-24 pt-36 md:px-14 lg:grid-cols-[1fr_1fr] lg:gap-20">
|
||||
|
||||
{/* Left column */}
|
||||
<div>
|
||||
{/* Headline */}
|
||||
<h1 className="text-display mb-8" style={{ color: text }}>
|
||||
<RevealLine delay={0.08}>Construimos</RevealLine>
|
||||
<RevealLine delay={0.16}>el sistema</RevealLine>
|
||||
@@ -54,7 +51,6 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
|
||||
<RevealLine delay={0.32}>negocio.</RevealLine>
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 14 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -67,33 +63,26 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
|
||||
la operación real de tu negocio.
|
||||
</motion.p>
|
||||
|
||||
{/* CTAs */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.65, ease }}
|
||||
className="flex flex-wrap items-center gap-6"
|
||||
>
|
||||
<Button variant="primary" onClick={onOpenContact}>
|
||||
<Button className="rounded-md" variant="primary" onClick={openContact}>
|
||||
Hablemos de tu proyecto <ArrowRight size={16} />
|
||||
</Button>
|
||||
<button
|
||||
onClick={() => {
|
||||
document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' });
|
||||
}}
|
||||
onClick={() => document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
className="group flex items-center gap-2 text-sm font-semibold transition"
|
||||
style={{ color: sub }}
|
||||
>
|
||||
Explorar la maquinaria
|
||||
<ArrowRight
|
||||
size={14}
|
||||
className="transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
<ArrowRight size={14} className="transition-transform group-hover:translate-x-0.5" />
|
||||
</button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Right column — System Map */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -103,7 +92,6 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
|
||||
<SystemMap isNavyBg={isNavyBg} />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ThemeProps } from '../types';
|
||||
@@ -57,9 +58,9 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
|
||||
aria-label={item.title}
|
||||
style={{
|
||||
background: isHov
|
||||
? isNavyBg ? 'rgba(229,9,20,0.06)' : 'rgba(229,9,20,0.03)'
|
||||
? isNavyBg ? 'rgba(59,130,246,0.06)' : 'rgba(59,130,246,0.03)'
|
||||
: cardBg,
|
||||
border: `1px solid ${isHov ? 'rgba(229,9,20,0.25)' : cardBdr}`,
|
||||
border: `1px solid ${isHov ? 'rgba(59,130,246,0.25)' : cardBdr}`,
|
||||
borderRadius: 6,
|
||||
padding: '28px 28px 32px',
|
||||
position: 'relative',
|
||||
@@ -83,7 +84,7 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
|
||||
fontSize: 96,
|
||||
fontWeight: 900,
|
||||
lineHeight: 1,
|
||||
color: isHov ? 'rgba(229,9,20,0.07)' : numColor,
|
||||
color: isHov ? 'rgba(59,130,246,0.07)' : numColor,
|
||||
transition: 'color 220ms',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none',
|
||||
@@ -101,13 +102,34 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 2,
|
||||
background: '#e50914',
|
||||
background: '#3b82f6',
|
||||
opacity: isHov ? 1 : 0,
|
||||
transition: 'opacity 220ms',
|
||||
borderRadius: '6px 6px 0 0',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Icon */}
|
||||
<div style={{ position: 'relative', marginBottom: 20 }}>
|
||||
{(() => { const Icon = item.icon; return Icon ? (
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 10,
|
||||
background: isHov
|
||||
? 'rgba(59,130,246,0.15)'
|
||||
: isNavyBg ? 'rgba(255,255,255,0.07)' : 'rgba(59,130,246,0.07)',
|
||||
color: isHov ? '#3b82f6' : isNavyBg ? 'rgba(255,255,255,0.5)' : '#3b82f6',
|
||||
transition: 'background 220ms, color 220ms',
|
||||
}}>
|
||||
<Icon size={20} />
|
||||
</span>
|
||||
) : null; })()}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<h3
|
||||
@@ -115,7 +137,7 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
|
||||
style={{
|
||||
fontSize: 'clamp(1.1rem, 2vw, 1.3rem)',
|
||||
letterSpacing: '-0.02em',
|
||||
color: isHov ? '#e50914' : text,
|
||||
color: isHov ? '#3b82f6' : text,
|
||||
transition: 'color 220ms',
|
||||
}}
|
||||
>
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import Loader from "../components/Loader";
|
||||
import { useTechPartners } from "../services/techPartners";
|
||||
import { assetUrl } from "../services/api";
|
||||
import type { ThemeProps } from "../types";
|
||||
|
||||
function PartnersInner({ isNavyBg }: ThemeProps) {
|
||||
const { data, isLoading } = useTechPartners();
|
||||
|
||||
const bg = isNavyBg ? "#071233" : "#f7f6f3";
|
||||
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 partners = (data ?? []).filter((p) => p.isActive && p.logoUrl).sort((a, b) => a.displayOrder - b.displayOrder);
|
||||
|
||||
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 (partners.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Partners tecnológicos de 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-5xl"
|
||||
initial={{ opacity: 0, y: 14 }}
|
||||
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 }}
|
||||
>
|
||||
Partners tecnológicos
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-8 md:gap-12">
|
||||
{partners.map((p, i) => {
|
||||
const inner = (
|
||||
<img
|
||||
src={assetUrl(p.logoUrl!)}
|
||||
alt={p.name}
|
||||
style={{
|
||||
height: "auto",
|
||||
width: "auto",
|
||||
maxHeight: 150,
|
||||
maxWidth: 200,
|
||||
objectFit: "contain",
|
||||
filter: "grayscale(1)",
|
||||
opacity: 0.55,
|
||||
transition: "filter 300ms, opacity 300ms",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.filter = "grayscale(0)";
|
||||
(e.currentTarget as HTMLImageElement).style.opacity = "1";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.filter = "grayscale(1)";
|
||||
(e.currentTarget as HTMLImageElement).style.opacity = "0.55";
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<motion.div
|
||||
key={p.id}
|
||||
title={p.name}
|
||||
initial={{ opacity: 0, scale: 0.92 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: i * 0.07, ease: [0.22, 1, 0.36, 1] }}
|
||||
whileHover={{ scale: 1.06 }}
|
||||
style={{ lineHeight: 1 }}
|
||||
>
|
||||
{p.websiteUrl
|
||||
? <a href={p.websiteUrl} target="_blank" rel="noopener noreferrer">{inner}</a>
|
||||
: inner}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const Partners = (props: ThemeProps) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
if (!mounted) return null;
|
||||
return <PartnersInner {...props} />;
|
||||
};
|
||||
@@ -1,47 +1,8 @@
|
||||
'use client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Building2, Layers3, ShieldCheck, X, Check } from 'lucide-react';
|
||||
import { X, Check } from 'lucide-react';
|
||||
import type { ThemeProps } from '../types';
|
||||
|
||||
const comparisonRows = [
|
||||
{
|
||||
criterion: 'Adaptación al negocio',
|
||||
saas: 'Configuración limitada y procesos estándar.',
|
||||
ry: 'Plataforma diseñada para tus reglas y operaciones.',
|
||||
},
|
||||
{
|
||||
criterion: 'Propiedad y evolución',
|
||||
saas: 'Depende del proveedor y su roadmap.',
|
||||
ry: 'Roadmap compartido con autonomía técnica real.',
|
||||
},
|
||||
{
|
||||
criterion: 'Integraciones',
|
||||
saas: 'APIs restringidas y costos extra por conectividad.',
|
||||
ry: 'Arquitectura abierta para sistemas, ERPs y herramientas.',
|
||||
},
|
||||
{
|
||||
criterion: 'Soporte y continuidad',
|
||||
saas: 'Respuesta general, soporte tipo ticket.',
|
||||
ry: 'Equipo senior con acompañamiento directo.',
|
||||
},
|
||||
];
|
||||
|
||||
const proofCards = [
|
||||
{
|
||||
title: 'Diseño de experiencia',
|
||||
description: 'UX orientada a adopción, claridad y flujo de trabajo real.',
|
||||
icon: Layers3,
|
||||
},
|
||||
{
|
||||
title: 'Arquitectura robusta',
|
||||
description: 'Infraestructura, seguridad y escalabilidad definida desde el inicio.',
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
title: 'Equipo senior',
|
||||
description: 'Product owner, ingeniería y operación trabajando con criterio empresarial.',
|
||||
icon: Building2,
|
||||
},
|
||||
];
|
||||
import { comparisonRows, proofCards } from '../data/platformAdvantageData';
|
||||
|
||||
export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
|
||||
return (
|
||||
@@ -0,0 +1,162 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import Loader from "../components/Loader";
|
||||
import { usePortfolio } from "../services/portfolio";
|
||||
import { assetUrl } from "../services/api";
|
||||
import type { ThemeProps } from "../types";
|
||||
|
||||
function PortfolioInner({ isNavyBg }: ThemeProps) {
|
||||
const { data, isLoading } = usePortfolio();
|
||||
|
||||
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 card = isNavyBg ? "rgba(255,255,255,0.03)" : "#ffffff";
|
||||
const tagBg = isNavyBg ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.05)";
|
||||
|
||||
const projects = data ?? [];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<section style={{ background: bg, borderBottom: `1px solid ${border}` }} className="px-8 py-28 md:px-14">
|
||||
<Loader className="py-20" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (projects.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
id="portafolio"
|
||||
aria-labelledby="portfolio-heading"
|
||||
style={{ background: bg, 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" }}
|
||||
>
|
||||
Casos de éxito
|
||||
</span>
|
||||
<h2
|
||||
id="portfolio-heading"
|
||||
className="text-headline"
|
||||
style={{ maxWidth: "28ch", color: text }}
|
||||
>
|
||||
Construimos el sistema. <span style={{ color: "#3b82f6" }}>Tú operas el negocio.</span>
|
||||
</h2>
|
||||
<p className="mt-4 max-w-xl text-base leading-relaxed" style={{ color: sub }}>
|
||||
Proyectos reales, resultados medibles. Cada implementación parte de entender la operación antes de escribir una sola línea de código.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{projects.map((p, i) => {
|
||||
const cover = p.images.find((img) => img.isCover) ?? p.images.sort((a, b) => a.order - b.order)[0];
|
||||
return (
|
||||
<motion.article
|
||||
key={p.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: card,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Cover image */}
|
||||
<div
|
||||
style={{
|
||||
aspectRatio: "16/9",
|
||||
overflow: "hidden",
|
||||
background: isNavyBg ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.03)",
|
||||
borderBottom: `1px solid ${border}`,
|
||||
}}
|
||||
>
|
||||
{cover ? (
|
||||
<img
|
||||
src={assetUrl(cover.url)}
|
||||
alt={p.title}
|
||||
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>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ padding: "24px 24px 28px", display: "flex", flexDirection: "column", flex: 1 }}>
|
||||
{p.clientCompanyName && (
|
||||
<span className="text-xs font-semibold uppercase tracking-wider mb-2 block" style={{ color: "#3b82f6" }}>
|
||||
{p.clientCompanyName}
|
||||
</span>
|
||||
)}
|
||||
<h3
|
||||
className="font-display font-bold mb-3"
|
||||
style={{ fontSize: "1.05rem", color: text, letterSpacing: "-0.01em" }}
|
||||
>
|
||||
{p.title}
|
||||
</h3>
|
||||
{p.publicSummary && (
|
||||
<p className="text-sm leading-relaxed mb-5" style={{ color: sub }}>
|
||||
{p.publicSummary}
|
||||
</p>
|
||||
)}
|
||||
{p.technologies.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-auto">
|
||||
{p.technologies.map((t) => (
|
||||
<span
|
||||
key={t.id}
|
||||
style={{
|
||||
fontSize: 10.5,
|
||||
fontWeight: 600,
|
||||
padding: "3px 9px",
|
||||
borderRadius: 99,
|
||||
background: tagBg,
|
||||
color: t.color ?? sub,
|
||||
letterSpacing: "0.01em",
|
||||
}}
|
||||
>
|
||||
{t.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const Portfolio = (props: ThemeProps) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
if (!mounted) return null;
|
||||
return <PortfolioInner {...props} />;
|
||||
};
|
||||
@@ -1,28 +1,7 @@
|
||||
'use client';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ThemeProps } from '../types';
|
||||
|
||||
const principles = [
|
||||
{
|
||||
title: 'Simple por diseño',
|
||||
description: 'La complejidad existe solo cuando aporta valor real. Todo lo demás se elimina.',
|
||||
},
|
||||
{
|
||||
title: 'Diseñado para fallar',
|
||||
description: 'Los sistemas asumen que algo fallará. La resiliencia es diseñada, no improvisada.',
|
||||
},
|
||||
{
|
||||
title: 'Una sola fuente de verdad',
|
||||
description: 'La información crítica tiene una única fuente confiable. Sin duplicidad de datos.',
|
||||
},
|
||||
{
|
||||
title: 'Automatiza lo repetitivo',
|
||||
description: 'Si una persona lo hace manualmente diez veces, el sistema debe hacerlo.',
|
||||
},
|
||||
{
|
||||
title: 'Construido para cambiar',
|
||||
description: 'El software debe poder evolucionar. La arquitectura anticipa el cambio inevitable.',
|
||||
},
|
||||
];
|
||||
import { principles } from '../data/principlesData';
|
||||
|
||||
export const Principles = ({ isNavyBg }: ThemeProps) => {
|
||||
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
|
||||
@@ -56,11 +35,10 @@ export const Principles = ({ isNavyBg }: ThemeProps) => {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Principles — 5 columns */}
|
||||
{/* Principles — responsive grid */}
|
||||
<div
|
||||
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(5, 1fr)',
|
||||
border: `1px solid ${gridGap}`,
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
@@ -75,13 +53,13 @@ export const Principles = ({ isNavyBg }: ThemeProps) => {
|
||||
transition={{ duration: 0.4, delay: i * 0.06 }}
|
||||
style={{
|
||||
padding: '32px 28px 36px',
|
||||
borderRight: i < principles.length - 1 ? `1px solid ${gridGap}` : 'none',
|
||||
borderTop: '3px solid #e50914',
|
||||
borderTop: '3px solid #3b82f6',
|
||||
borderBottom: `1px solid ${gridGap}`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-display font-black block mb-5"
|
||||
style={{ fontSize: 56, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
|
||||
style={{ fontSize: 56, lineHeight: 1, color: '#3b82f6', letterSpacing: '-0.04em' }}
|
||||
>
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</span>
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ThemeProps } from '../types';
|
||||
|
||||
@@ -20,10 +21,7 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
|
||||
viewport={{ once: false, amount: 0.15 }}
|
||||
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
|
||||
<div>
|
||||
|
||||
{/* Headline */}
|
||||
<h2
|
||||
id="problem-heading"
|
||||
className="text-display"
|
||||
@@ -52,7 +50,6 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
|
||||
</motion.span>
|
||||
</h2>
|
||||
|
||||
{/* Descripción */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
@@ -62,8 +59,8 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
|
||||
style={{ maxWidth: '56ch' }}
|
||||
>
|
||||
<span
|
||||
className="mt-1 shrink-0 block h-full w-px"
|
||||
style={{ background: '#e50914', minHeight: '100%', width: 2, borderRadius: 2, alignSelf: 'stretch', opacity: 0.7 }}
|
||||
className="mt-1 shrink-0 block"
|
||||
style={{ background: '#3b82f6', minHeight: '100%', width: 2, borderRadius: 2, alignSelf: 'stretch', opacity: 0.7 }}
|
||||
/>
|
||||
<p className="text-base leading-relaxed" style={{ color: sub }}>
|
||||
Software que impone sus propias reglas en lugar de adaptarse
|
||||
@@ -72,7 +69,6 @@ export const Problem = ({ isNavyBg }: ThemeProps) => {
|
||||
de arquitectura.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
@@ -1,33 +1,7 @@
|
||||
'use client';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ThemeProps } from '../types';
|
||||
|
||||
const steps = [
|
||||
{
|
||||
num: '01',
|
||||
title: 'ENTENDER',
|
||||
desc: 'Auditamos la operación, el stack y la fricción real antes de proponer nada. Sin suposiciones.',
|
||||
},
|
||||
{
|
||||
num: '02',
|
||||
title: 'MODELAR',
|
||||
desc: 'Diseñamos la arquitectura del sistema: flujos, interfaces, datos y dependencias.',
|
||||
},
|
||||
{
|
||||
num: '03',
|
||||
title: 'CONSTRUIR',
|
||||
desc: 'Desarrollamos en sprints con entregables medibles y revisión técnica constante.',
|
||||
},
|
||||
{
|
||||
num: '04',
|
||||
title: 'CONECTAR',
|
||||
desc: 'Integramos el sistema con el ecosistema del negocio: ERPs, pagos, APIs externas.',
|
||||
},
|
||||
{
|
||||
num: '05',
|
||||
title: 'OPERAR',
|
||||
desc: 'Despliegue a producción, monitoreo, soporte y evolución continua del sistema.',
|
||||
},
|
||||
];
|
||||
import { processSteps as steps } from '../data/processData';
|
||||
|
||||
export const Process = ({ isNavyBg }: ThemeProps) => {
|
||||
const bg = isNavyBg ? '#001f5b' : '#ffffff';
|
||||
@@ -77,7 +51,7 @@ export const Process = ({ isNavyBg }: ThemeProps) => {
|
||||
>
|
||||
<span
|
||||
className="font-display font-black block mb-6"
|
||||
style={{ fontSize: 52, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
|
||||
style={{ fontSize: 52, lineHeight: 1, color: '#3b82f6', letterSpacing: '-0.04em' }}
|
||||
>
|
||||
{step.num}
|
||||
</span>
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Check } from 'lucide-react';
|
||||
import type { ThemeProps } from '../types';
|
||||
@@ -36,7 +37,7 @@ export const Services = ({ isNavyBg }: ThemeProps) => {
|
||||
const accentClass = service.accent === 'blue' ? 'bg-brand-blue' : 'bg-brand-red';
|
||||
const iconBgClass = service.accent === 'blue'
|
||||
? (isNavyBg ? 'bg-brand-blue/15 text-sky-300' : 'bg-brand-blue/10 text-brand-blue')
|
||||
: (isNavyBg ? 'bg-brand-red/15 text-red-300' : 'bg-brand-red/10 text-brand-red');
|
||||
: (isNavyBg ? 'bg-brand-red/15 text-blue-300' : 'bg-brand-red/10 text-brand-red');
|
||||
|
||||
return (
|
||||
<motion.article
|
||||
@@ -77,7 +78,7 @@ export const Services = ({ isNavyBg }: ThemeProps) => {
|
||||
size={13}
|
||||
className={`shrink-0 ${service.accent === 'blue'
|
||||
? (isNavyBg ? 'text-sky-400' : 'text-brand-blue')
|
||||
: (isNavyBg ? 'text-red-400' : 'text-brand-red')
|
||||
: (isNavyBg ? 'text-blue-400' : 'text-brand-red')
|
||||
}`}
|
||||
/>
|
||||
<span className={`text-xs font-medium ${isNavyBg ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||
@@ -0,0 +1,176 @@
|
||||
"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" }}
|
||||
>
|
||||
Nuestras soluciones
|
||||
</span>
|
||||
<h2
|
||||
id="showcase-heading"
|
||||
className="text-headline"
|
||||
style={{ maxWidth: "28ch" }}
|
||||
>
|
||||
Software diseñado para tu operación.
|
||||
</h2>
|
||||
<p
|
||||
className="mt-4 max-w-xl text-base leading-relaxed"
|
||||
style={{ color: sub }}
|
||||
>
|
||||
Cada módulo resuelve un problema real. Sin funciones de relleno, sin licencias por usuario, sin límites artificiales.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{isLoading ? (
|
||||
<Loader className="py-20" />
|
||||
) : products.length > 0 ? (
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{products.map(({ id, imageUrl, name, description, badges }, i) => (
|
||||
<motion.article
|
||||
key={id}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-40px" }}
|
||||
transition={{
|
||||
duration: 0.55,
|
||||
delay: i * 0.1,
|
||||
ease: [0.22, 1, 0.36, 1],
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 14,
|
||||
border: `1px solid ${border}`,
|
||||
background: isNavyBg ? "rgba(255,255,255,0.03)" : "#ffffff",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: isNavyBg
|
||||
? "rgba(255,255,255,0.04)"
|
||||
: "rgba(0,0,0,0.03)",
|
||||
borderBottom: `1px solid ${border}`,
|
||||
aspectRatio: "16/9",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{imageUrl ? (
|
||||
<img
|
||||
src={assetUrl(imageUrl)}
|
||||
alt={name}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={sub}
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||
<path d="m21 15-5-5L5 21" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: "24px 24px 28px" }}>
|
||||
<h3
|
||||
className="font-display font-bold mb-2"
|
||||
style={{
|
||||
fontSize: "1.05rem",
|
||||
color: text,
|
||||
letterSpacing: "-0.01em",
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</h3>
|
||||
<p
|
||||
className="text-sm leading-relaxed mb-5"
|
||||
style={{ color: sub }}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
{badges && badges.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{badges.map((badge) => (
|
||||
<span
|
||||
key={badge}
|
||||
style={{
|
||||
fontSize: 10.5,
|
||||
fontWeight: 600,
|
||||
padding: "3px 9px",
|
||||
borderRadius: 99,
|
||||
background: tagBg,
|
||||
color: "#3b82f6",
|
||||
letterSpacing: "0.01em",
|
||||
}}
|
||||
>
|
||||
{badge}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm" style={{ color: sub }}>
|
||||
Sin productos registrados.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +1,7 @@
|
||||
'use client';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ThemeProps } from '../types';
|
||||
|
||||
const stackGroups = [
|
||||
{
|
||||
label: 'FRONTEND',
|
||||
items: ['React', 'TypeScript', 'Next.js', 'Tailwind CSS', 'Vite'],
|
||||
},
|
||||
{
|
||||
label: 'BACKEND',
|
||||
items: ['Node.js', 'Python', 'Express', 'FastAPI', 'NestJS'],
|
||||
},
|
||||
{
|
||||
label: 'DATOS',
|
||||
items: ['PostgreSQL', 'MongoDB', 'Redis', 'Prisma', 'Elasticsearch'],
|
||||
},
|
||||
{
|
||||
label: 'CLOUD & OPS',
|
||||
items: ['AWS', 'Docker', 'GitHub Actions', 'Terraform', 'Linux'],
|
||||
},
|
||||
{
|
||||
label: 'INTEGRACIONES',
|
||||
items: ['REST', 'GraphQL', 'Stripe', 'OAuth 2.0', 'Webhooks'],
|
||||
},
|
||||
];
|
||||
import { stackGroups } from '../data/stackData';
|
||||
|
||||
export const Stack = ({ isNavyBg }: ThemeProps) => {
|
||||
const bg = isNavyBg ? '#0f1520' : '#f9f9f7';
|
||||
@@ -45,7 +24,7 @@ export const Stack = ({ isNavyBg }: ThemeProps) => {
|
||||
<div>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<span className="h-px w-8 bg-brand-red" />
|
||||
<span className="text-mono-label" style={{ color: '#e50914' }}>Las herramientas son el medio</span>
|
||||
<span className="text-mono-label" style={{ color: '#3b82f6' }}>Las herramientas son el medio</span>
|
||||
</div>
|
||||
<h2
|
||||
id="stack-heading"
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Node {
|
||||
@@ -84,10 +85,10 @@ function tooltipHeight(desc: string): number {
|
||||
export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
|
||||
const [active, setActive] = useState<string | null>(null);
|
||||
|
||||
const RED = '#e50914';
|
||||
const RED = '#3b82f6';
|
||||
const dimStroke = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)';
|
||||
const defStroke = isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,31,91,0.3)';
|
||||
const connStroke = 'rgba(229,9,20,0.45)';
|
||||
const connStroke = 'rgba(59,130,246,0.45)';
|
||||
const descText = isNavyBg ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.65)';
|
||||
const tooltipBg = isNavyBg ? '#0a1628' : '#ffffff';
|
||||
const tooltipBorder = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,31,91,0.15)';
|
||||
@@ -130,7 +131,7 @@ export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
|
||||
const isActive = active === node.id;
|
||||
const isConn = isConnected(node.id, active);
|
||||
const isDim = active !== null && !isActive && !isConn;
|
||||
const stroke = isActive ? RED : isConn ? 'rgba(229,9,20,0.5)' : dimStroke;
|
||||
const stroke = isActive ? RED : isConn ? 'rgba(59,130,246,0.5)' : dimStroke;
|
||||
const fillOpacity = isActive ? 0.12 : 0;
|
||||
const textColor = isActive ? RED : isDim
|
||||
? (isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.2)')
|
||||
@@ -151,7 +152,7 @@ export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
|
||||
<rect
|
||||
x={node.x} y={node.y}
|
||||
width={node.w} height={node.h}
|
||||
fill={`rgba(229,9,20,${fillOpacity})`}
|
||||
fill={`rgba(59,130,246,${fillOpacity})`}
|
||||
stroke={stroke}
|
||||
strokeWidth={isActive ? 1.5 : 1}
|
||||
rx={2}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||