Compare commits

...
25 Commits
Author SHA1 Message Date
Emmanuel Rojas c4673cbae1 hotfic 2026-10-06 08:46:38 -05:00
Emmanuel Rojas 9a8614c871 correccion 2026-10-06 07:14:27 -05:00
Emmanuel Rojas f87cdf5f59 correccion 2026-10-06 06:53:04 -05:00
Emmanuel RojasandClaude Sonnet 5 e60871ec7f fix: quitar el reproductor de música de fondo
El audio (7.4MB, preload="auto") competía por ancho de banda con el resto
de los recursos en móvil y hacía sentir el sitio trabado/lento.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-10-06 06:39:34 -05:00
Emmanuel Rojas c8f89a7907 fix 2026-10-06 02:34:32 -05:00
Emmanuel Rojas 6f661b9365 fix 2026-10-06 02:23:43 -05:00
Emmanuel Rojas 44bf6670dd add 2026-10-06 02:17:10 -05:00
Emmanuel Rojas 26b88d14ac cambios 2026-10-06 02:15:29 -05:00
Emmanuel Rojas 1d78ea599a docker 2026-10-06 02:03:43 -05:00
Emmanuel Rojas 5f99b65611 cambios 2026-10-06 02:00:11 -05:00
Emmanuel RojasandCopilot 794a659369 refactor: reorganizar modulos dentro de app
Coloca las secciones, datos, componentes, servicios y tipos dentro de sus dominios de app y normaliza los imports con el alias @.

Co-authored-by: Copilot <[email protected]>
2026-10-02 15:47:34 -05:00
Emmanuel RojasandClaude Sonnet 5 efba699905 fix: quitar tsconfig.tsbuildinfo del control de versiones
Nixpacks monta ese path como cache dir para el build de TypeScript,
pero al estar commiteado como archivo, BuildKit falla al intentar
montar un directorio sobre un archivo existente (mount ... not a directory).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-18 00:54:24 -05:00
Emmanuel Rojas a663c061e2 fix 2026-09-18 00:48:53 -05:00
Emmanuel Rojas c99086c4be fix-2 2026-09-18 00:33:11 -05:00
Emmanuel Rojas 236b526078 fix 2026-09-18 00:28:29 -05:00
Emmanuel Rojas 409d9455f6 all 2026-09-18 00:15:28 -05:00
Emmanuel d7f584059e cambios 2026-08-31 19:24:16 -05:00
Emmanuel 9dfced04f1 all changes 2026-08-28 21:17:22 -05:00
Emmanuel Rojas cba8d4055b fix 2026-08-10 21:52:57 -05:00
Emmanuel Rojas 75ead4f915 hotfix 2026-08-10 13:51:49 -05:00
Emmanuel Rojas 2e6ee9c887 hotfix 2026-08-10 13:31:13 -05:00
Emmanuel Rojas 0524c26d8a fix(scroll) 2026-08-10 13:17:00 -05:00
Emmanuel Rojas 185939fe9e fix: principles grid responsive on mobile 2026-08-10 00:04:56 -05:00
Emmanuel Rojas 68bfcf2f8d fix: serve sitemap.xml and robots.txt as static files 2026-08-09 23:58:48 -05:00
Emmanuel Rojas f53e567aeb Fix 2026-08-09 23:51:49 -05:00
357 changed files with 38689 additions and 24095 deletions

No files matched your search

-12
View File
@@ -1,12 +0,0 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "Landing",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 5173,
"autoPort": true
}
]
}
+9
View File
@@ -0,0 +1,9 @@
node_modules
.next
.git
.env*.local
.env.local
.DS_Store
npm-debug.log*
.claude
*.md
+5
View File
@@ -11,6 +11,11 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
.next
.claude
out
.env*
tsconfig.tsbuildinfo
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
+9
View File
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+32
View File
@@ -0,0 +1,32 @@
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
USER nextjs
COPY --from=build --chown=nextjs:nodejs /app/public ./public
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
EXPOSE 3000
ENTRYPOINT ["node", "server.js"]
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
+18
View File
@@ -0,0 +1,18 @@
services:
web:
build:
context: .
dockerfile: Dockerfile
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:?Define NEXT_PUBLIC_API_URL (en .env.production) con la URL pública del backend}
env_file:
- .env.production
ports:
- "${FRONTEND_PORT:-3000}:3000"
restart: unless-stopped
networks:
- rynext
networks:
rynext:
external: true
-39
View File
@@ -1,39 +0,0 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- SEO principal -->
<title>Rynext Technologies | Software Empresarial 100% Mexicano</title>
<meta name="description" content="Rynext Technologies diseña y construye software empresarial 100% mexicano. Sistemas a medida, módulos y licencias para empresas en Cancún y toda México." />
<meta name="keywords" content="Rynext, Rynext Technologies, software empresarial México, sistemas a medida, desarrollo de software Cancún, software mexicano, ERP México" />
<meta name="author" content="Rynext Technologies" />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://rynext.com.mx/" />
<!-- Open Graph (redes sociales) -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://rynext.com.mx/" />
<meta property="og:title" content="Rynext Technologies | Software Empresarial 100% Mexicano" />
<meta property="og:description" content="Diseñamos y construimos software empresarial 100% mexicano. Sistemas a medida, módulos y licencias para empresas en México." />
<meta property="og:locale" content="es_MX" />
<meta property="og:site_name" content="Rynext Technologies" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Rynext Technologies | Software Empresarial 100% Mexicano" />
<meta name="twitter:description" content="Diseñamos y construimos software empresarial 100% mexicano. Sistemas a medida, módulos y licencias para empresas en México." />
<!-- Geo -->
<meta name="geo.region" content="MX-ROO" />
<meta name="geo.placename" content="Cancún, Quintana Roo, México" />
<meta name="theme-color" content="#e50914" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/dev/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.
+18
View File
@@ -0,0 +1,18 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
devIndicators: false,
output: 'standalone',
trailingSlash: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
],
},
};
export default nextConfig;
+4551 -4152
View File
File diff suppressed because it is too large. Load diff
+32 -19
View File
@@ -2,36 +2,49 @@
"name": "sitio-rynext", "name": "sitio-rynext",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "next dev",
"build": "tsc -b && vite build", "build": "next build",
"lint": "eslint .", "start": "next start",
"preview": "vite preview" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.3.3", "@codemirror/lang-html": "^6.4.12",
"@codemirror/theme-one-dark": "^6.1.3",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/utilities": "^3.2.2",
"@iconify/react": "^6.0.2",
"@radix-ui/react-dialog": "^1.1.23",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.15",
"@radix-ui/react-navigation-menu": "^1.2.22",
"@radix-ui/react-slot": "^1.3.3",
"@tanstack/react-query": "^5.102.4",
"@tanstack/react-query-devtools": "^5.104.0",
"@uiw/react-codemirror": "^4.25.12",
"axios": "^1.20.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.43.0", "framer-motion": "^12.43.0",
"lucide-react": "^1.28.0", "lucide-react": "^1.28.0",
"next": "^16.3.3",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"react-router": "^8.3.0", "react-icons": "^5.7.0",
"tailwindcss": "^4.3.3" "recharts": "^3.10.1",
"sonner": "^2.0.8",
"tailwind-merge": "^3.7.0",
"tailwindcss": "^4.3.3",
"zod": "^4.6.5",
"zustand": "^5.0.15"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@tailwindcss/postcss": "^4.3.3",
"@types/node": "^24.13.3", "@types/node": "^24.13.3",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.4", "eslint": "^9.0.0",
"eslint": "^10.8.0", "eslint-config-next": "^15.3.0",
"eslint-plugin-react-hooks": "^7.1.1", "typescript": "~6.0.2"
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^17.7.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.65.0",
"vite": "^8.2.0",
"vite-plugin-pwa": "^1.3.0",
"workbox-window": "^7.4.1"
} }
} }
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
-1
View File
@@ -1 +0,0 @@
/* /index.html 200
+568
View File
@@ -0,0 +1,568 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{stroke:#000000;stroke-miterlimit:10;}
.st2{display:inline;fill:#00B5FF;}
.st3{display:inline;fill:#008EFF;}
.st4{display:inline;fill:#0063EE;}
.st5{display:inline;}
.st6{fill:#00B5FF;}
.st7{fill:#008EFF;}
.st8{fill:#0063EE;}
.st9{display:none;fill:#FFFFFF;}
.st10{fill:#FFFFFF;}
</style>
<g id="Capa_2" class="st0">
<polygon points="374.5,793.5 451.5,839.5 451.5,497.5 374.5,448.23 "/>
<polygon class="st1" points="221.5,701.5 294.5,746.5 294.5,366.5 221.5,320.23 "/>
<path d="M294.5,366.5l242-148l102,68l-264,161.73l77,49.27l223-133c0,0,48-35,43-82c0,0-2-48-33-62l-150-90l-313,189.73
L294.5,366.5z"/>
<polygon class="st2" points="464,846.5 464,728.4 535,790.5 553,777.5 631,830.5 539,890.5 "/>
<polyline class="st3" points="848,702 541,506 464,558 778,747 "/>
<polygon class="st4" points="464,558 464,503.53 500,478 541,506 "/>
<polygon class="st3" points="778,278 778,747 848,702 848,321.43 "/>
<path class="st5" d="M374.5,793.5l77,46l0-342l223-133c0,0,87.22-69.98,15.86-140.74L534.5,130.5l-313,189.73l0,381.27l73,45l0-380
l242-148l102,68l-264,161.73L374.5,793.5z"/>
</g>
<g id="Capa_1">
<image style="display:none;overflow:visible;" width="242" height="207" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPIAAADPCAYAAADPjIzXAAAAAXNSR0IArs4c6QAAAARnQU1BAACx
jwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAHPqSURBVHhe3b153BxF8T/+7t3nyB0SQhLCDQEU
BQRRRBQVREAU5PTAWxQBFUWEjyKKIIoKgoCKciigghdyC0K4kRsSAoRwQwKBhITceZ7k2e3fH93V
U11TPTO7z4Hf3/t5zbPT1dXVVdVd08fMzhqMmmrRFowkMBTlFYHKpVTidOPTZWUq6KKyJORRlWo2
EVMMakWerPGDlTG+bgNYzusVMjZ2R8jTkNCDkCoG6JmRuSWygYo8AwTj/wWf8Tbi6UEE949ptT6v
p5U+441tURO5LcCKgyArLALjtZosDknnDcPyWqmeoFZr4oPzRLxCb5Nj8BCKGZsdKj9B2CmD2LDO
EaqoKlNByOJ2saNQtOZ8TiN/FqEKTwuwEEHMZScNEfBG8zbTjhQs/SvgKQNv29Demcz2A3lAfM0M
MyJYAjhPop0pLel5QjFk3aSPRVoW18fIvpJQuKjRVVDDCXKUjpStAMFHRY2Sp0LaFCcduGNUhjcA
ZGQVG+H4qqqfbNdE+7UMJl+YUMuUTCmhICjkjewXfA+i6lV5PjNSsUBfVUZFUIdWxSstSry8zwLu
wiRhihq7KgoVbA+kP3UObos0wyRsy6mj8FTCANsWQTOSI1FvgpxHgcyQVVlYLK+kWDYiG5Rzgzoi
Kecr0vRPwk8Pw1HgV1UfXyDqTL6BLB1EG0iQolKnVD1iulXVv2Wg6lLVtgpSiT5z8lMXa2aL6m/p
rwGwvTK0duIwSj7Xkx2+a5WD+l0CYekIxTcaWKVhfZxWhE2tK2vMHFGkiAZtQybLCvl0LpGqVqbV
CvqLEv/kdPAIxVIMVcHKF6iRoUp9gscivwZnHzl6ADVMCpSnNV4rUHTLwdODz1N8HAoPtZuSlUdR
EFdpLFGYBiPrz1NgXVJZIwcJ+SOMMPSpVZKyiNFVFi5bZYirTbAMHgr0KgzyonKtolU5ZXWndJag
ti+SV1VWm6DZW0xk+rDzSqowJt58silLZZUxlOkkjUoy6vDFRSBLoQIhW1orIeVYx+8/AMVxMp2E
nz4X8gwUeCf5X0FKF+mQUkc6RCxStpdhZJbkK0Or/BIUDCl7pHziF+kICi0lvgwWFfxdIc9KtYrK
cF4jA1krSDTjHVk0jeDgWnkZTFT0SZDpIlRyXjsgvUn3SsYOwCZWK8i1uJImJPzDp28auBtsq372
QmkUN/31D9VdRQZvN7bGjWYUfN1fRSZYX2NHtB+jOJPfJoqWLAxBBhQfKzIT5DiQTfjHYB2N6ypZ
QDSlUpWmgYQrWv7P4v8VPSW0BkRGT5qVKqcgdGKfTsqsCBpEXKIFXVjFVIyL0RDpWlaXkQU8uP00
CBKBHXRatBaO5Es+l84C2XJhojJQnjw4UrubVVDgiAhF9Q8mxI3/cCA7f0NRtX4+iqRQ5lfK14TY
jE67tBpbv1EkVNOf9FKOcIHwvkGRCH5nhPdBpYBCShBZ3JFdXEcNTGcPZbMrURkXGuqIhQEYxE6d
0muw0Z8L1GCjqLElBtMITQ92wZBZ4CNUCrlCChIyqhQFqe2Zk7q2AhmQrYLK5wO1DLXQBqWOZcjV
UaVskXGp8rwMKarRNMg8Xl6T5SF9yK/YST2HEIbrVBWeVzE3hsIQ3NRKfQReRpSXMnO70lp9LdAK
p6oexKKYnYOBMvuic/ZpkNUfjdyaPil6FYViuCe7ULVsovMDmeOsNziAKuAGpQzQ4OuMivKpLdUn
D1a+cJbAykg+rmJg4w3UD/CHYirBj2LR1LiKHz1/shrNZ6Kdkhd54WcVmo5KncSWEgMUZEr5HtKs
gUIU0KICrb5AE35NoihPhxuRAaUhBSRJaQsAyhNXRSjIz4mRFTHYAlFED/6TFwV2pGCRYCgr2F8o
srk9haiiW4FPUZ7toHUCglY/p/Gynh7sMzD+aB8VypayFNhXWpah9MJfll+MbETm4DTeH5J9wxNp
tChVWoNoSDqPRBU4NJGVNQRvkILGkXQLVkE7diVgaMcyJVNxujShCpJ8CUE5fWjWkOBPQspJgIvj
ddsmrLWwrcxYUggiuB0M5OJcf+OgsvwogxSaFK7I47yynGwP+hpjqMtn2gpG8TzV2cnC5UgWTWbk
we1JIpXP/BCflDmmRQyUnARS5uVAjKzDWU6vIkvaUlSA5UXFZBmZTsEW80rVcigoW4ZC2cLOlqqx
Sl+Tdmbn+fvIuQV9GYrWUAMBMkbWwTcUlGy0utusOClXreQhWksV9QPcF7xOrX6pJ0GzgcGGf5nc
qDpZl9SlCoifPn19qloqsQRkY9WypkV+Dl8uuEHKYLa16qYWdFJuP1VAJJ92G3kHq6qxoiSRLNhU
nc5F8BIMC2aDFi9ERc4KlSt0AnWCFtDSJhehaNNKISqkGI6B1qFuLSoqMORb3p6lghk0hXlankte
tNCXCIrMsDRI9U2t3irw5UIAk5zEYGgh6vdlcpu1EmX0dt8QEhRvFbyMKM99nBOtOZ/B8tE3V1hB
qkETMIgbpsXiMaroh4zPwlUmXReJEcpQmVxdcdoYF8j1eo0FtOfju7Jap6zsBI0nZwCDlsd0ydGl
Y/i5r9ugzX2bNsDdEs69XkEFrktqRlkC2rT1qKN7/Ik8vzqEMklI5ydAnSXqH9wjKbQTxNp5CSLR
RtSbgKYOX4aULUlC55N8UjBP8zLUSeKeUqu5wO3oqOOtW22Bjx+wN0aNGoGX5r2KRqMhRmgCyaCj
VZSVSfVmSZPpFIQfJHmwEV04ZP2y/TSUKRrbVUf32ifGDSQM12CR4OG0CsoSu7ziR/K1enyjR1el
CvWpsjSaQEq0LEp90Rb4MZD8iYmIApwueVJKIeOl6buvw9QMaqYGay1GjRyOvffcFd875nAcsO8e
2HGHbTF61EjMeekVLFm6TLntI9MEbqvGQzQtT0MRX1EelHzFf5JlMJCLD3Ye1c99pilmCi5wBCfD
YPTmrEdQQQ6Z9sgpy9OijFFoQKI+Tbash3dQli6FlEMoKy+fOvKQnT2ntycWtgXL4Be0qD7i8Z+0
5jM8zxewXK+s7lqt7j4NsNWbNsfnD9kfH/vo7pgyeSI6OupoNi2WLluOO+9+AOf94a+47c57sWLl
Ki+HLemDXsIgasqILJ2mOUHyIMHXAuT+QzQLQqLOgQJfBik+AvL1h2RCL/JtSOTBApkxVFlzWgih
/FyKTMgoQm5NE1mTkSStEFImoURGtFnCUBrI7AJA5Chb6sP8TuV4A0Zlraif15UJqdXcNoi1wNix
o/CFTx+AQz93MDbacD0M6+4K0+xm09237etrYN6r83H1dTfjgov/jsefeAqNhhPcbDZDVTlfcBS2
uyxXhUfCJMoRmecJvxmkyw4I+hPIuYRDYZ9xMBi9hSsZyvuTIoNTikqkFC+DRblsQtCzoHEDymQq
gQekAxkQDzFA5wGEXwWZj6CcTvoYsILezqJyfhOLAhQA3r3j9vjuMV/Fbu/bCV1dnQBbK4ei/gEM
CujnXpiL8y/6Gy7+y7/w2sLFjtcCzWaD6UMVBjGMXoLCJssJbANKBQZ52oDC6x1V4WnBJNnOakJ3
gYWa4QI5NxWpYGhgyQsFJLmCPAnNEblzz6SqUFSnWiCD9f+oHxjkp2tAhRGZIP0bJ7OKBKyclhvB
Sxm8bDYKN5tNrDdlEo75+pfxhU8fgNFjRgJNd9OReDRYa9FsNmGMQaPRxG133ofTz74At9x+L3p7
elGruXV29tSV36+gc82WFFpizzmuAEo/IVTp3xG8LNtqWTY6a6onRbGMAjM4DEZtkQ1DBsXcEhGr
0DRKtiAzgrjtEqB04Jyj1IIeOeYYNvxjVUl51mUmR2SfX6gjQfARcnWaNC8sjKkFdYYN68YnD/wI
/u/bX8Vmm2wYgi7bkS4HBWutVsOKlatw9XXTcMY5f8BDMx5Do+ECvdloeh+QvdD1S6lt4DIsnByN
J6Ca3hk4P2+HwkpiWDbboGKtlNd0lsVTvoG4M8J5RBmDUXyzq0UlI1aq0X8m5VimBddGs4bxqXVx
iLKBRcrkILla3T5tEO8ZSD1UugaFQVYr0yryTDw4u7u7sNM7t8M3Dv8c9vzgLuju7kJfXx9qtVrh
KJwCBTPVsej1JTj73Ivx69//GYuXLEWj4abubrqdAm8vrjtf5/t2Dvns/qr0eX8QFS9xdmI96vSW
tDKwAkEu62NkbFKl4n2rLJCDsBYRilAt/rNdeWoZzcCUJ2VHYckIPsPSea4CRuNyeDnv/ErQ5CeQ
UkcBBVh3dzc2XH9dfPoTH8PnDzkAU9ZdJ/C0E8ASNN0mWffcPx2nn+Wm20uXLUOzma2v8xAdN5yX
TMlD23DINEEpn6IHERp/ql4OX66IJQJjDFUKvfi9flUtqlPzlxEjsuFrMl6AIXmVStHhZMnshHjd
Cq90zsG5wsI5Ih2t6bSLAxL1e1B1gYXzarpAkScaUEWuohwogDs7O7DOhLWx+wd2xqGfPxjbb/sW
dHV1wlqLer0ui/UbtLtdq9WwclUP/nnF9fjdhZfhsVlPYfnylWj6by3FSNhjfJtw11nporL2JlAh
Xjh1nkAFljz8rEJWwz8Za8YUEeIAtYIn8pPnC3W4WWE+kFOIhGsoyiuDDHTpFUGOmGW93AY5JWZy
1UDOERJI8JV2BD8Vj3RheSpiPppC1+t1jFtrLN7x9m1wyMH7YM/dd8H4cWPDJlXVdXA74Jth1lo8
+/wcXPTnf+Gq66bhuefnYOWqnvxmmCvJpGhpjZUIVeypwmO1Skog7egPuAxWvxrIRPOfUVEKcEdk
U+sSo4Jw/jmABubqJ4dLOpixLA3ovDTLiGj+X0RnzpL8OSgMxmQjflBHE5aiKTC+wUDPQAPG1DBq
5HBsucVm+NhHPoiD9/8wNtlo/TDlHcwAluDT7Z6eXtz34CO45NIrcOMtd2HeK/PR1+fWzd6EePNO
pnOQdsh0ClX5ysB14+eafK3/aXwS3B/eSTmXyEFOwML1j2xELiuQykzRW0Wq/pxlGXI6aQ5l6Yhd
TIdkw+VkEyzjZTwhkH2DtIQEvycbGAAWXV2d2GiD9bHr+96Fg/f/MN65w7YYPqzb8QzyKFwEPvou
eG0Rrrz2JvzjiusxY+YTWLhocbiXbclv5KJcIPP202zRaBJVeKpA9IeAqvKr8PF+yXwDNnOjAU5z
CfECZVNr7QrBISUPAHI6yDSDRWKKDK+bzEhdLCSUQA59TMokGMD6J59CUZPQg5BQxpMpMGumhvHj
xuJd73gbDt7/w9h9150xcZ21Q/AMxGZWf8GDubd3NWbNfgZX//tm/Ps/t+GxWU+Fxz0t+cilmI/I
TeQr7SETpH2WA/mdfxZB46HpK0sDrIFEMV5dThaH0h9y/V4gkk3I6kiPyLIjq6jC0ypksJUYiCLH
Scul7BREo1r/j0+fg2zZkpRXFXleY2gDw2D4sG68bZutsOfuu2CfD++Krd60OWq1bPR9o0bhFHhA
L1+xEvfcNx1/u/xa3HL7PZj78itYs2YNALe2znznbSA/A87XpZDtq5UhHslLeQTR5hFkOdJRkAER
N5JB1sHklgUyOLuUi1Qgy1FOcwIhL7R/kMGWqleg8MLjb3PQBkElsHotso7FA5mPLpU6nkS+jDHu
W0oAsNH6U/DhPd6PA/bdAztsvzVGjRzxPzUKF4Gm081mE3NeegU33XIXrr5uGu6+7yEsen1JcGNw
pw3/fHv50yxT9Vcekqeo70rIsgRZPtWPtLjhkHI4UjIZQvE8ow9kP9pwhxUUKlZI4y+DkKc2YhGk
A1MgZ9msTBV1U9NpTjd8JzzBHyAq9aMvYfTokXjvTjvgwP32wod2fQ8mT5rwPzsCF0Gbbl913U24
4pqbMGv201izpi/4wlralPSNkuwDmv28IbX8/oDL83rI0dP69s81u9Qlx8Cg5FFxMq9gsDIYNdVJ
iHh8IifbaMSKSJVN0IM+Sl6AYhhvUxv+ZZBFOL8GC7gHlJFnjAI5d5JA3Do8MOv1Gt62zVbYf58P
Yc8P7oKt3jQ13BN+Izez+gse0AsXLcbd9z2MK6+5CTfdehdeevlVWMt4wqwpakQG6i/cFzI90H6i
Okm00MmGjAIwGSqK8lAq3wWyysOIhXVwAzkEIZKRaiReTMmLkKtQoKw8nIzApkxtbPjnP2v+09Os
poZxR2g3TQ+XaUw2PV5vyiR8dK9dcdB+e+Ht270Vo0aO+H9yFC4CBXNfXwMvzn0Z99z3MP5xxfX4
z813YtWqnnBhdLvb3Gb5bS+G4Geep/AVgrVRVJQl6FZiIFEZbSQuQ1CagckrBS/r+0hpIMv6clAC
IIeKxhpUqdBDq7RqWQKzMXqqDenptArW0QzfENOQvRvLWmDkiOHY9X074RMHfgQ777Q9pkyeGJ7I
GqoAphF/qECj7+rVa/DMcy/iqutuwiWXXoEnZj8d8WT7Ep4Y2oada1Na69tBAy8bERPy88yinylx
YigtFeeypNJVIcs5mf0IZG+tXC+oKJNFLEUMEprSrZRHXi9ui1z/WusYrU+7jKxw6HicxtV0dBcw
LpC3fsuW+NTB+2Cv3XfB1E03QldXZ5hCD1Vg0ShJGMp66QKyeMlSPPLoE/jTZVfgn1f8G4uXLPV+
c1+XVJs18iuD9bSW7CgajLSM0GFYkrU7O41BstTMCuB9kvUva2AweqoYiojLI1lnkfEaNIdzcrKi
AnCZ7ZQncM8nOg6Q5fGRIhT1CaO1YvZkFgBMmrgO9t17N3ziwI9g263fhNGjRoVbSkMdSNZarFm9
BvWODtTr2dNhQ6UHADQaDVgLzHtlPm67816cde4f8cBDj8L4R0BtU/qT3QqMLqr0WdQvNLuUvkwb
S8bnR32Eg2YErdYJhS8Fz6eKcfX7t2hKDp/m9ZB/jDdaFmkVAyFjUFCkFF/7soY2LM1YARcUdKuo
u7sb73/PjvjuMV/FZz65H960xaYYPqwb9br7iuFQBE/YVPJr1Qcenolzfncx5sydh4032gAjRgwP
+UOhD4KPDEaNGoEtpm6CHbbfBqt6ejH7qWf9955zBeJPsDaJAps6MAUigZ/LYAoN6qEEeQTJz2la
HkdBHo+zQjEuI/E6XFYqEiKNHkQkFR8CmCLP+TxDfMr9XF8028yy2GSj9fG1r3wWx3zjULzrHW/D
mDGjUK/XhyyAwb65ZIzBq/Nfw5//ehVO/eXvcMNNd+Ce+6fjuefnYuI6a2PSxAnhLSBDoRvNAIx/
Re+kietgh+23xsgRIzBj5iz09q52ekTLHW2gDI7P+MAnnFVtYXy8HhW+P0hSJWiCfQBrWSmYMLXW
oElJsA4kqBGs/2e8VUNQdQSqPwnyD+mX8Ro/TbbWYuyY0dh7zw/gsC9+Atu/7a3o7uoMwTsUQQK4
UbjRaKBer6Onpxf/vvE2/Pb8v+C+B2ZgxcqVQfXOzg5suP56+ORBH8EXP3MgNtxgSviW01A+gNK0
Fn2NJhYtXIQLLv47Tj39t+4RT3+bCvD9JApk3lZiiaNOwSUSkcPEaNl5Ikvn9ILgl3meJkVWQGJE
JnArhgA8iA18gwieCIYdAwEur4pMxuOL1PyvNtRqBtttuxV+fMK3cNQRn8dmm26Izs4O1GtDPwrD
Pwk296VXcNLPzsHJp56D2U89h9Wr10Rr5WbT4vXFS3H3fQ/h/gcfwVprjcGGG6yHzs6OIRudAT9C
Axg+fDg222RDrFzVgwcenpm9xdNxsXMJ1m9C4Id/BWVTdJ+lxnqOkCHZjVJ6tBfEKA/kIYScDpHj
k7vi0mKZbgfa017J1mBwo2u9Xoe1FpMnrYOvH/ZZnHrSd/CenXZAV2cnDFz+UAVDs9kMo/Cy5Stw
4cX/wJFH/xDX/edW9K5e7Xno9TzMRuPKvjDnZVx7/S2Y+9I8bD51E6w9fq0QSENhA81YRo0agfXW
nYxnnnsRzz43h9XNL/KJPmKQrTUrtSOhhE/NVokOxs8Icv2KnbainoL/oUD2/8IUlRsurVQspgC0
3nGAzpdEK7wxaIQdPqwbH9x1Z/z2zJPw2U/uh7FjRsP6t2kM1dTUjazZGzDvvu9hfOXrx+M35/0J
CxctDnpY2/S+qsWmW38dNcCavj48NP0x3HbnvVhnwnhsvNH66OzsGJKXFwBZMI8fNxadnR144KGZ
WLp0udt7kFUbttEVdReekIXKIPsck98WhH6RnomLUUX8jwWypBl/c18ayZgpgHlb8al5ZSgPFgCF
DqYAdt8TXg/HfvMrOOWHR2PTjTdAo+FGu6GcRtMUmZ6cOvvci3HsCT/DY7OecnrUatn0lMzyX9CI
QK/rMU7/BQtfx/X/uR2vzn8NG20wBWuNHYOODjf7GArbarUa1ho7Bi+/Mh/TH5mFJr/YkyE8iMOJ
YZuRJXqq2RpRjqxVwQq1Vb4Y7refLBmdOCpXzMu1AV4sJyKRmePjNC0zgeiBdB68eZuM3/xxO6wT
8KkDP4rTf/I97PuRD6KrM3tf1lAFMQ/gBa8twlXXTcN3f3ga/vav67B8xUrUfAA7q4Q+cknDadbA
+odY1qzpwwMPP4obbrodgMEG66+LESOGh5KDZSfJHTNmFJrWYvrMWVi4yL8sH2xNaeifPDQ6Q1Re
IMiUtFZQUGdAerCoijq61q4wIlcJ5lKG1mD6a18r+vhRn3fGMKrHoCCesPY47LzT2/HjE47GkV/5
DCZPmhDt8A5Wx+agHdxGo4klS5fjv/c+hF+ceR7O/u0lePb5OSHfNvk6kYLVCwl68lHO06mMhXtn
dg14ffESTLv1v5gxcxYmrjMeE9Yej2HDuoO9g2l3Z2cnZj/1LB6b9TTb+KJbgTVuFCtVoo+J3ZLL
432jRJQOUSgnQ6u4ddTRNSHxa4x0EIoM0TI0WgVUKlbGRPkKX+S3hBP5LQ4PYwxq9RrGjBqFrd+6
JQ7/8iE4/jtHYNut3xR14sHsyIQQoNZixcpVeHzWU7jk0itw2lnn446778eqnl4Yf/srPFqq6aXR
ku3vbgHSS/Cfe2Eubrn9XqxYsQoT1h6H0aNHoqMje2vnQPvBGIMRw4fhpZdfwYMzHsWqnp7wCiTH
QPZQQHP9pS0Ev65OdINMJiVaRVGZVKXtoeKI7JHUK5WRcmABWmTX0Wq9gtdkGycUnN3DurDJRhvg
gH33wHHf+gr22Ws3jBkzKmxmDWUQW2vRaDQx75X5uOb6m3H62Rfg75dfh1deXRCuQeGVOqEf0glN
NaTN8oT4JLIOuHzFStz7wHRMf2QWuro6MWHt8Rg5YgRqft09UP4gOR0dHVi8eCnuf+gRzJ+/UAQD
2VSlzqp8cHz9sqM/ZaujQiBzRaSzShxCWbQGN7J8P2F9ZzNQdCMwemh3OmH8nIQsgOv1GqZMmYhd
37cTvvaVz+BLnzsIm22yYXg2+o2YRi94bRHuuW86LvrL5fjdhZdhxsxZWNPXx/h4B0/dmwyNo/iM
0blLxaajMQbNpsVLL7+Kex+YjvkLFqG7uwtrjR2DEcOHRXwDAWstenp7cd+DM/DUM8+znEhJRvd9
JCJxPtb2SRVZhinjTYEXiH0Y0LLMGCKQW5EmeaWDfJqzRUVk+RYR/EH1+E9LNKFDzn+ifs/OR9a1
xo7Bjjtsi88fsj8OP/RT2Omd22H48GFDGsBgQbxqVQ9mzJyFSy67Ar89/8/4z813YuGixZ6nqfSy
nNFp5GwRIxF9zVODMVixchVmPj4b02c+gZUrVmHcuDFYa+wY1Ov+G0zof0Abf0vtvgdmYObjs93a
33LdWZtzhGxvk+FEwZODkqGQ3mhUDGTuII0nlUfl/BFGZJnfKmSPKtJPpllx0bEMXCN3d3fhTVts
ioP3/zCO/PIh2HvPD2DiOmsP6TeUaAoN/+WGF+a8jOv+cyt+d+Gl+Nvl1+HFufP8e6MzPgfp3zJQ
24A7xn+w8tLlAY7Hjc5NzF+wEDNmzsKLc+ehq6sLE9YehxH+4jcQfms0G7jvwRmYPmMW+hrZq4Li
T3/OzTEGGD0e2GQboG810LvSsxnH6D8y+ETUZ5n+4VT47A1Chak1v8pLZ3EjK6JF9jyUkSGoKPXk
8IX4VMuXoU5Wq9cwZd2J+PAe78eXP/9xfPLAj2CLzTdBZ2fHkI7CFMTWWixbvgLTbrsbv7/wMlxy
6RV44OGZ6Ol1T2Zlr5Ztsy0IckSjkUtCXjCMYZtLAGBhYNDT24tnnnsRjz7+FBYvXorRo0dh/Lix
6OhwfuwPrLV44OFHcf+DM9G7eg2Tp9nPgswYYMpUYL8jgIkbAiuWuqPh3uqpI+GHKMgR2Z+WNbgo
CGR5lR8gJXMiZFS2AKliJJw7lz7Y7i1No+EaZNTIEdhpx+1w6OcOwhc+c6D7htLoUSHI+9sBq4Kv
hR+b9RQuuewKnHvBX3DzbXfjtYWvh3z3KTuShqI8Ao3IWVIvZ+JD84l3N43Ojz7+JJ5+5gWMGjkC
668/Gd1dXW35kspYa3HfgzNwz/3T0dPTyznYOYdv81oNWGd9YNdPAVtuDzNpY6DeDSxdCPQsUy5e
3idW+CaCnKabNuOE+1WD0Ct3JAM5FVypilKgKxcV9eVbFVME2oDJXR0ZrM83meHG1AALdHTUsdWb
puLznz4AX/7Cx/Gh3d6LKZMnhtF3qEdhYwxeePEl/OVvV+G3F/wFV1xzI559bk74+dJ4Gk3nbBSN
2lx2tCIwHmlv2KyEuL0TGFhbk3/dxa+npxfPvzgXa40Zg3dsvzXGjh3dL3/29TVw1z0P4u57H0ZP
byqQxXnN+2b8FODtewBjxsOsPQVmoy2BcZOB3h5g2UI3OtPjn3Rf2tAmKfO7VD+kyS+tBLPkS6W9
bC00TTKQU5CVtACysR+NCIhptaFO5hORaJbgp2x0HTNmFD72kQ/hW1/7Avb76Iew5eaboLu7CxjC
Ryt5APf2rsa0W+/Gz874HS775zV45NHZWLGCfqFBa0EUt4nh/a+AL8DzyJFJPZfJrCwvPmniBOyz
9244eP8PY8stNg2vM2oVZH/v6tW49Y57cc/907F69RrK9Z8JuXSRG78u8M69gGEjgSZgRo4B1t8U
2HBLYORYYPECYNUy35e8rOgugA9QrRoT/qX1yCHFJ3xuOU0pYwCDUVskeohGVoQUQsqgxpZ5kk+D
zd5kKBGmmUw+78DGBXCg1IB3bLc1Pv2JfbHHbu/FRhush45O982kdjpZO5CBOfOx2fjTZVfihml3
YPZTz2L16tXBHmttNvOIXkGju8OBfOJhyE8S3FnEzzsy8Xk6yeBu8vsO5D9rm36psj0O3v/DeN97
3okN1ls3XCTbAfnr9cVL8KOfno3z/vhX9KzqhQ2vKpbPVNNFn5ZTBthsO+CIM4AJ6wJhaWyBWhNY
+Trw5EPAHVcBs+4GVi33o3Ez/iEC358CLF+utdp3OD9vB2TnNLVX32lNbWGLAnmQEe5JllSfy7Zx
x+OODLzkFApiN7pa28R6UybhM5/4GA7Ydw9sPnVjjBo5wpV4g4L4lVcX4Mprb8Jl/7gGM2bOwtJl
y8U6mCO2K++bMv257zT4Dg/fKXOdx5dl7ieQ6zo7O7Dl5pvg4wfsjb33+AA223TDsGtd68c3wGjm
MmfuPBx/0i9x2T+uDj+unoH3A0GvAdh4G+BrZ7q18mqy0V8kaxZorgEWvQw8chdw17+AF2YBjT4n
iwYLze/G/2PNwzKUAgTpX4LQ3frPHDyfsTAYvbn/kTyNcRARPVxQYGiuUeK3cQDUi2i0yOwwJrt6
dnV1Yo/d3ouvf/WzeNs2b8a4tcYAQziFBuuMxhisWtWDW++4F3/40z9wz/3T8er819BoNNl3hFOQ
ulJa0iUs8yP5SXQY+JE4F8AEtrTxLJnvLCasPQ777/MhHPLxffCWN2+OMaNHD9gtO3q2+v6HZuJ7
J56GW26/B/A+dSeKSSA9jetvG28NHHEmMGlDNyJznxnvH2OBxipg3gvAHVcAd14OrFySBb10mQUb
kRH7LjJZKlaEIJgV0/znM0MgQygw6OBP26QMZPqEwE3xIuI3xsDAfXPHWos3b7kZjvjyIdh37w9i
8qR1UKtlo0N/O1gVWPHD4E88+Sz+dNmVuOKaG/H8i3OjN3XkQY1Faa1xq9ig+JCKWZbwm4AKQ87/
tVoN1jbR2dmBd759W3ztsE9j1/ftFB4EGYgAJpB//nb5dfjBj8/E08++4Fwh37BJyahaFshH/gqY
uEEcyJzXWjeVrlmgsRx4/EHguguA2ff7fmsAXqeRAoS9Iam1bRm0tubI2oX9iJvG2B+oHvXOEGkV
jMmC/WyLBhPxu75jMWrESHzq4x/FYV/8JLbYfJPwW8L9meK1Cj4KL122HFddOw2/u/BSPPLobKxc
1eO+XmiZT4I7pF9S/qS07EyyPDIaZ43YYj/GyBjJf7RU+cKnD8BnP7UfNlhv3XDPfaACGN6HALDo
9cX4xZnn45zfXYyVq3rdL1LIi5/qJj8z23gb4MgzSgKZTvyTch1NYNFrwK1/A266BFj+uruVJaf0
kcuZwEgPrpwtaCdkBTWZAVnZ/q+Rk7ooRE0XjS+ACbf0j6bLtPlFrNk0vKurE+98+zY48iufxod2
ey/G+i83DOWrduCng8bfgrn/oZm48JK/44ab7sCCBYtcgINNDVWwPOPtVTudEc5VmRJpMP6Ub1wZ
F6DOro6OOg7aby98++tfwlvevDm6ujoZT0pOe6CL4YyZT+D7J/8S1994e+Y7a5lNvg8Yf87pxgIb
beOm1pN9IGt6cvdYALYB1AHUG6g9/zSaV50DPHgDY4KX72WFKhXZKhLtEZELZBkA1mrvcq0IA9+5
yJHyYHz8M4JmBIeQY3hHFgI9qzEGnz/kAFx83mk4aL+9MGb0SBhjBuSpoqqgUbZWq+HFOS/jxJ+c
hY9/7hu45NIrMX/BIjSbTcfTbPqpHB2KSww1lve3BgOtB7I0BwnkRxHcbMJ9o8ktEXbYfmv86fzT
8Yff/gzbv+0t4euLg73fMPupZzHriad9APMcYYe6TDR+lC2a2TGQ/FodaBqgtwZs/CbUjvwFsNtn
XB777a6sA1J7Cf9Tk2iHlh8gbdHbtf1AJkVlX6Bzg3gDIacpV0iMNFVgZd2unmHDurDl5ptgwtrj
ACC8qWMoYK1FX18frLVYumw5rrjmRnzh8OPwy3MuwKvzX3M8YadVMTjlS0pYseEi+SGXLWBMOWYl
n5C1EY3CADBh7XH48Q++hZuuvggfP2Dv8HbNjo6OQfOx9fsLc196BdNu/S+ef/ElP0L7i6CGYIq0
169/BUk95zRTA+odsD1NNOvDgLfu7NujmdVhwz8H49NRV9d8nag3QMtkNH9a4v2UlUqHkWlVgQRM
+KfISYD4IrVcIK1YuRJr1tDDAoMPCuBGo4E1a/ow/ZFZOOrYk/H5w47FrXfci6b/lk6z4dfDstEl
oo7IHUJl5IjEkKIH8M4knZ0VdqNwDbWa+0L/3nu8H//88zk47luHYfSokejr6ws8g41m0+L+hx7B
rXfcC3jdqkE6o2hHXkBxuzU1P6gbN1LnGNg5IXd3hg5Ke0RqSb1lOaJlnwWtwBnFecoXRE/lR2Dr
ikDSOpcG1hGDDKcbXcGHCrR+azSaeHX+Qlxy2RX47FeOwUV/vhxLly4DLB+FPcpMVNo5LpQ6l5Ad
QgNvXzf6ugCuYeSIYdhhu7fi9J9+Fxf9/ud4z047hNtJQ7FUcX5t4Mmnn8Nf/3mt26nOfdtLIKgk
eawbWfmFR+viHNw8C/9wCBJhIwREyYKYIeTqzxE8pFIOmkYZA4uX6ChFSgkGkmP4vWK+C6kEOtE5
LNHcYa27ghe19UCAOlmz2cTCRYtx7Q234PBvnoCjjj0Zs2Y/ne3sho7n9Tb+n+EHqe+XIdLPtMlF
/PDn8L4Ltmr+0hxBhXie9eINhg3rxuabbohvHvl5XHzeL/DFzxwUXu07GJtZGuiC/Pripbjqumm4
8ea7YOj1RTndJSjP81V9+EiDZmr03W92BF4RuMbTSBfezjGTgGYnrzeDHsh5PiY0bnz9KECQzfgN
/PrPd1DjdxmtZcGcU0iB8Rfd+HnfgQSNwJbel/XE0zj5Z+fgS0d+F9dcfwt6e92jlW4aDd/ArJGp
HTKJir+9D1XTvbzI1VyA5n+NRnC/qmsM0NlZx7qTJ2C/j34QZ5/2Qxx39GGYuulGIcAHezOLQP7t
6V2NG266Axde/HcsWbpMslWHNF+mUyjqznRBIdfn3M/6N6W1zcqo7aVvWVkVGX8cyAaVChVbWIKo
KLMiMohlh2C2RBBMHqH84I0YtNvc27saTz/7Ai768+U49Mjv4ZzfXYLFi5c6JuM6YtIeScsTPDQ7
Ga9BNipHkGmOuN3cb1TVUO+oYfy4sXjPTjvg+O8cgZ+ffBx2+8BO4b77UN62oyBevXoN7rjrfpzx
6wvxzHMvhry2ENzEyrcpCoAPyFb8wflF7BSGEpUpqssZV4uGesjhvh1onUsg2MSn0pRBaZLjd2pd
AfZJ+sZXOmvdj4ANJKhzNZsWL8+bj6uum4b/+8Ev8N0Tf4H7H34EMC4orLV+U5T7oEQXQ/8q+I1g
kc1gKiNjphHWGIPRo0Zgu23fgq9+6VM47ZT/w5c+exAmT5oQptFDsZlFCCNxTy9uu/Ne/PzM3+Hh
GY+FvBYNZv2H+6pAButWOptvn2idzW8fJgumETW7lQRGl+n4aLGVZAWEFjsigYoUOTm3llDA/JdN
fSVT68hkWSxeshT/vfchnHbW+Tj2hJ/jquumYdUq931Y4nF6cn0r+CSnJy8jMiv3k3SdFMSdnXVs
tvEG+NTBH8UpPzga3/nmoXjbNm9GR4cbfYdqGg3vP5rtLF6yFFdccyN+9NOzwi51NhJX8CeQ4JMB
XRGRz71M7SdrOKI6eFsWFKLBKGIp4A9wtvpATvQQIquK0XmVysogNrkkFNUihCIW1roOMRCw1qK3
dzUeffxJXHjxP/D9k36J8y/6O16c83J46CPrZExPaYpikqPR5pXGgBboXIZRHeYC2O1Irz1+Lez5
wV3wve8cjhOO+xp2e/9OGD1qZBiFhyqA4X1MPnxhzsv4/R8uw4k/+RXuuX96yM8jpZ/0ZYpPgVYN
p8m2Tcrmm15ICE6Ujci8PalNNVkO/An5PDPJCBVIYTJdAunnQPcZOVE2K6CVi1Bwb7UNWGvDz6/8
+Oe/xi9+dT7+e+/DWLWqJ+TnOlnkJ6Z+YCvylzSQ0pJf8hGIL+Z3genOhw3rxjvfvg2OOuJz+NHx
R+FTB30UkydNCHxDOQrTCAzAvZfs1v/ilJ//Bqf96nw8/SxfE3N9uP+knpSmTsbTAw1290DSJBkQ
OqgMGWRzB3CbOFyBFqfWKZCD+dEKEmU4WckOsPCOLOGrCOt3pK/59zT85LTf4qrrpmH+gtf0UZgj
ItvM+aHRRUOU3hZh+SoLyVMzs6A0wEYbTMGXPnsQfvyDb+Frh30G27x1yzdsGk3+W7OmD/c9+AhO
P/sCfO9Hp+OSv/4Li15f4vmQ91eJvWkwOdJO3sckPQUuI9G0MbgwKdj3EcsfVvG0UmR8iUAm6+Qx
lLBOSW3LniPKzgxLBlsJqNzy5Stw30Mz8fisJ90rZYz7WmTOv0n3eEbK48EcyaCCOQEVeon3kQCf
Hq81djQO+the+MmJx+C4b30F73/vjhg9yj1/PpQBDPYMOgC8PG8+/vjny/H9k3+Js357ER6c/ij6
1rhHHttruiI/MlqZcLUtJZjfZbXW1xHJIB97ItWhrtl5e6TaJuvnhEQgvxHgjvGOMiVOVfNUYkuw
/kmt1b1r0PBPZVnrXvWaD8QKCCOjNiUrA29srWxmLw/ger2GHXfYFj/87tfxo+OPwoEf2xPrTZmU
4xsK8Gn00mXLcd1/bsMPTjkTp/7yXNxy+z1YumyFC+Cyp7YGGrIqmU5CC8Ay+L4Tjbr+s/Cx0aK8
DC0EcjWB7UHItv6qlvIWkaNi9ACJO223o1JHstaiyX4nGKo2eYoDr5s9vBFMakG3UAUro1Tr7HUV
bLzhFHztsE/j5z8+Fl/87IHYfLONhuwbShyWvVDBGIOnn30Bp599AX7w4zPxt8uvxYtz/aZhsyle
EOAdxYM6tTwJKMtPIGqXqigoYLyusm/y/CghZXGF6BD9SUFJIPNCUuAAwSIOWj6VTvgKhq5iiRFO
IbULG6ZJ0rlgDSGh0aRSXv8UQjWCj0STu8K9Xou1xo7Bxw/YG2f87Hh856gvY6d3bodRI0eE4B2q
AAabRhtj8MqrC3DpP67GcSf8HL+78FLMmDkLK1f2uGfQw0++yvuwrG3DJ0lPgRgkI0+zC2vboDZh
B1URyU3oEwKdPThEhwre33SIQGYSaf5Oi3CZ7hdYHVK5pIN5GQ8LcRHIsgYWPpgt3fhn9GSlxKQY
FPwo/csOIC/bhsIsgN00+p1v3xY/OfHbOOn7R2GP3d6LSRPXHvDX7VQBH4V7entx48134ZjjT8X3
T/olbph2BxYuXMw2Df1twshF1Ja0C2yCzRkUWmSj4nNInorgoui8aRPfa5byrUIjsIew+MEf0Crb
H2IoGZGRBVBSoRZAevE2aEHZgFAkCAlZxrhv7gwE8us1rjgq+ESW5yjKY+Dt7EGBaa3FupPXwRFf
PgRnn3YCDvn4Pthko/XR1dU5pFNoAr9//8Kcl3DWby/CsSf8DFdccyNemPMSenp6w35DGlV1Fn2y
UCaBXYRlU6Zokm7hXngvQ8cyHTQZEUoZHCqywWnTZqBqo2kp/ChExYyXU9jhlHpUdoWvn0gHgmxZ
TteQ6iFF8L5iSVMzMLUaYAxGjhyGD39oF/zmjBNx/HcOx9u22QojRwwPL1JI6z7wcJuDDRj/XrK/
XX4dDj3ye/jZGb/HY088iVWretw3AHNBXKBjqbs0BkkT8itWrTYth83HsQO1M10wiiqpgurlVXWS
BlRCQUAZ6zsoWxcBBVdTL0c2QEhTXQpfP0FT0iwgvO45aDQwOmvccKSQ8J0BarU6APd7zW/danOc
8sOj8ZszfoS9dn8fxo8bi3rdzUSGMoDB1sKNRhMPTn8U3zz2x/jGd07C7Xfdh6VLl6PhX6aQ/eBc
BSTcAHB6iqEEVZoBJflRH+Rg/bFl9VhZtLh8NdGz1r6g5Uq0ICwHrhincRJ5RKuH9PFJS/9UDwbn
0Rqtv6DRIz8N5LpyfRgfbWYYzsN3YaVMicxPxr+6t9lsYPy4Mfjqlz6Jv170Kxzx5U9jvSmT0NHh
RuCBWk5URbPZDG8JWfT6Epz9u4vde8kuuwILFy1GX5/7rnbsP3mutafsMzyf9Z/oSPG3gFaKVeLl
OkmFJTxNc0cSsf9q2QYLsqtA1WlzKOOPsqtI2JgiPn4uDdeM8vSwSaBjoEalrBOm/MGXBTzf6ycv
AsbnEZm3hWUEv6FGu9GdnXXssvMO+MO5p+KXP/0e3rTFpuHd3EM9Clv/WiNj3IMbN95yFw76zNdx
7Ak/x3PP0/u02AUwckFKT+kI2bbc/3TOjwLRAwGujlSNQ6hUDZzZF07WkfJPamrdDvKyPbQM6qym
ULkIwV5uOCvjN82GZpeWyedqU71l5qTU8+VqtRpq9RrqdYOpm26EU35wNP7xp3PwkT0/gHrdNdlQ
BzDYNNpat5l1zPE/xYGHHIlb77w32JsbhWl2kjOa93qRF/lOliuAWg+HaJ+yrleUl0JL/EW6VoQX
MTCBzJWPjE9YZcGMUBqy1INs+p/7PrI2HW4PxYHi8zhLW9VmtpqaQb1eR71ew7i1RuPg/T+Mv19y
No7++hex9vi1oocrhhLNZhONRgONRhMLXluE3//hMuy53xdx1m8vwvIVK1EztfwrjZJQ8qMAbMuJ
Djb8S6AojzWF1v1yNKMRM3KEHCHvh5xs/lmOioGsKByBgrF6xYVeCyR2wzyigy0DGK8/BiqQk+B+
pqqqmm7hZyPN7F6kMWEtPGbMSLx/l3fivHNOwXlnn4Jt3rplCOChfFMHkO03WGuxbPkK3PHf+/Gl
I7+Lbxx7EmY/+ayf+hv/dBZEP+B6Sprgs3RK944hGrsCAiuvV5YX09YWqwiIyvj6pLk5wTztmbMu
y8AJUlkFnt1/jTHBJCHbIxAJVrOoTfCdbf4QBlNX1adsJK0G6sQqNHdpNAnpHtBmlsHIEcPw5i03
w3FHfwV/ufAMfOwjuw/5bzVzkP0rVq7CzMeexC9+dR6+cPhx+PeNtwHwG3B89mPCv7yRhb6xYrOK
N2oLNgdWrTLheNnlZZojRecIovmFQtNdCuM2avwSsnwGNiKnmBidTpPKFjirFRjkG1dWBSj1DyyS
wRNGjYrtEPnClaUpcldXBzbecAo+dfA+OP/Xp+CYb3wpN41O6jEIoADu62tg3isL8Nd/XouvfvP7
OO2s8zHnpXlh6dL0G3JZwfDPw3fS0j5gRLnSAhUhdemHXGq/qB3phJ768+eFTcXjQ/TvIEQK8HTv
Tt2O8IYQgs7UGoSMsCMek1uDKBypJCPJvzurn9PrEDzSfOlMvmufg6DRzNG4zazJk9fBXru/Dz85
8Rj8/MfH4h3bbx3qfaOm0Y1GE/MXLMRtd96Ln57+W5zw4zNw3wOPYM2aPsdHz0dbMLtTeqboElX5
Bhmyn6a6UKC3qbd6d0ejKZD9Dy7NptaUKZkQN1jUgCnIPH6bph34srxa+ozU7U8dMcKFQHNHAN0v
1piYz7zt1AZjRo/EO9++LY464nM47SffxQH77vmGv2rHWouVq3rw4PRHcfa5F+Nb3z0FF1zyD7w6
fyHgbzW5vayK2yoBRX1F9j0U8JYg1wQFcni1mgpamoNeaw3EfRNKWSDzgUUiiAmUx3kU/1FHYki0
Sk4ThiJFClAkshQpj1N2rFM/B+OAeFSXdktnF1Xq9O7q6sQWUzfGpz+xL0754dE48iufwaYbbxB+
vWEoR2G+B7B69Ro889yL+Nvl1+JHPz0Lv/79n/DY40+7FyoEf/IOxXttuxAbmUko9+M1lMppQ12t
u1Fa1sfpMq/fkEoQMnoikNuF37gwKLEmpVhV+PJ2gPpUAZKBFXUurghvSdcTjDEYM2YUdvvAu3H8
sUfie8ccgV12fgdGDB8GDPFmFh+Bm80mXp3/Gq65/mac8ovf4KRTz8GNN9+JpcuWO94m+1GF8OCQ
Zieli8DyjQ/i4EKSJWTwdXhZ+5blA46Jf+/ZNU8MoqXoUMzOmZ4jMPAKEt+A4pUbuRlIdP/PZylr
ZBKiWVOGovUw3xQYIPDOEHWM/kNdY3OHUn0W7MEW5JRwAWqx6SYb4kufPRgH7bcXJk+a8IZtZJFd
PT29uO/BR3Dmr/+Ik049B5f94xq8OOdl8Ww0tyUYy2gcKTpB5Idkkf0UJUU8HoGN88pyVv/6Yco0
To/ymE5ENxV05SycVRaL8hMZ/Par2+zSKpYWVQUvlzgP1eW80z/Y1M52+4hjOXt0MiA8jGJFqxBT
ZuO4tcZgyrqT0NnZAbwBozDdD240mnjmuRdx4SX/wA9P+RV+d+GlmPnYbDaNFjYm24hsS+VLMFsL
14kekVjeaQtgwz9OYKAgULIIZWYR3fCEPG8TFKeAIo/7j9Md/IisOZa4lVJJUCf2ZbSiGq1laPoO
NkSdwQ5/ASlcTxPcNHsowR+ZXL5iJa66bhpOOPlM/OJX5+GW2+8Jv6nkNrOYDWo7+balqXFLkAIL
gipKU0WMvxBcMR4Z/pNfRKhufkhweshnMsJGJ8nPsjLISqocVE4ko7skmX0Dv0bmSDV2YEsxVISo
biDhpr2colRGU2olS8JNVyV18EDTaGMMmk2LBx5+FCedeg5+eMqv8K+r/4M5c+exZ6d55xGwoCjP
s/A4qYoyfluBJ4kqBRMXD5CtyqHlpWC8vwYShv6l7ct/jTGCZkURZGWpMpKvFbRbrj2UtklZvkdu
xBsk8Gk0AMx+6jmc8ovf4BvHnIQLLvo7Hn/iKT+NptfO+rbg9/uNdrtQjMRVu0SOyZ9L8aC62ScE
X6ib6RpEawIlFJ6cHAW5PLaTLsvmeAugqJME90kOud9+0gKsVU0pX+NjsqTYQCMdpB7IaKGsxjOQ
0HoTc6oB2+R448FH4cVLluKCi/+Orx51As4+92Lc/9BMLFm6zF9Q5PeECd4W8m8YqYmXj2ZF7UTg
dTA5NB2VItRPtg8RdI4aIAtsFawu0kGalYKmvgXzSwFU/wpUYAEYn8rvLq6JqXVRA5mKXpCgqZls
PSGKxKcQyaDzgQd19GxNyx/o5x1a+iKt/GCtj+nbSQDQ19fAzbfdjW8e9xOcdOo5uOueB7Fw0WJY
a7xmCf0i1cgmZlsIhKoQPiH5kYgW5PXXdVSVbC4OxWyVX27oA+IiA2VGo6CUpYwhUyIRyJr2pCBP
Sp4i8OmaJt8bb30duezUVZc70IZZ4UAETW7UsvB2eB2Nsm6MEOuQkzcAIJm1Wg1z583HKaf9Fkce
fSL+eeX1mPvSK2g0AdSHw9qG64HGfWMpCS3L0D8tU4O0k7VrEEGEwYTUg4G6ID8kJC3HJ/Sn9q1q
lpSfQpE8n6cEsiKdBBlk+VGDaCiqnUG2Z5h2+U/uPHXtxuB3JAciiGMU2FxYVcaXbSoNDGgtDACv
LXwdl1x6BT5/2LE46zcX4alnXsDKlSuBdXaA3eEUYOdzgK0OB0ZMcU94hJkFOZf0oukrpxFkOoUU
H937jDpTP5CqJ4UKzzFw0yUfc5E7NzETmWYoUQbuh9SR0IXD5/lA5lrq8tyn4KsEZlQki8kP4oT8
QOejMZOXg2MaiECmXWv3YAS7R62ZnqyOOTDJ0zrogmCtxW133ocvHv5/+MZ3Tsbtd96DJUuXojls
ArDNUbDv+SWw+T7AujsA2xwOvPcsYJN9gfowZ4e6vqcnidhF0y2shfGaIzSagIVSJ0HTh0FmRU9F
VYRtZkWqmMP5ZBlC7ll7mU5BGpSCScs0AOLNLs9YKjshUIUQlitapU6WGTpUAUqy20ZoRGU6LdM5
FNzyaBOvLXwdv/79n3DYUd/Hv2+8DUuWLkWjWYOdsjvwnrOBrQ8DukcBjWWA7QGay4G1Ngbe+UNg
x5OAcZv75y+Nn26LdaxsE0pHdG5Uf42TFUqU+V1makjI4DRKS7M4T07VHMFDVkZI8TNIvXJlYoXE
7yNzKPRIeJH1JYiu7gVl1JG1gJ9gw78BRP/kWf/XH1hrsXLlKlx57U3Y/5Aj8J3vn4qnnn4B1hhg
zMaw233HBer4rYA1K4BmH2BrLlBNB9DoA5q9wPofAN79C2Dzg4Du0Vkwhw2GUKP7kCN3cgTi/YDR
5MU3SjLZUmwErlcKUkCiAGeTqmvgPIQaFPk8nRKm1MkPwNvKfS6ZPCOrTlkjE7ygpD7SiCJIXr7W
NbrRkWESMpOdG4RvEw0MSD+ub96n5eifTrQmfvmV+fj7v/6Nu+55EH0NC4ycArvpJ4F3nQZs8Smg
eyTQXAX4F9nHHaLmjkYvMGoKsP2xwLtOBibuANS7XZ6l1Va+s/iMhM0a0fMGGf4kJ9MjRSdoVUQo
EsB9INquVK7gtcj6heH9VxOUoGnkAF9J7oKZRj6QaUMi3Jujc+oQvGOQVdohIZwcnKHkJUh55OsZ
qC8iuJfJ0fVGk+fr5u4oQAWWlmCtRbN7POwWnwC2OQJYe3PArgLsGhfEObCgNh1+06sGrL8r8O5T
gK2/CIxdH+h0v9oYl2NJq3SDHFg/iPznfUb9SX2wI9+mWf+DqFweElwWKT4A8O8qC0HM7VDhZyX8
4kjP6YfD0yRyfMQb16c8EFKkEEclJoEip0skeMIGxxCiSO3ChxEYqvC0iq6RwMh1gQ4DNFc6mjFi
55kfPp+m2qYG2F5g7CRgh8OA958AbPxeYNgYoO5+2SJnNHWgaAOMG6cZqtEUaGwhgJHXhfhl9UEl
6s/E4O3h5bj6mhwNkh4CMJfByDLPI5A1pRJlKI+5gwUyv/Lynqv1XoHA4iuuUKQSZBtS2kAxMksP
xG2eTIYwRrVNJUawA6hXkFPrAupdbArNgosCLQQbg/EtbwB01IGOJtBdA7Z4L7DXD4B3fRGYuAXQ
OTyemifNLOp4RNPyJKrwCH0ssnL8PIWUOpwu8yRSPg15WoY8HyB4VdiPuOV1S0NjZDQtm9fTCqJi
cm2hOJSoOYe2CU1lE/4J+Dq1LI3WBqx1P8AOeJn1DsDU2Z6D9Y8e+bcC8E5n4Pj8UhldBhheB0Z2
A8M7gTqA8VOAnb8I7H4csNWHgLGTgVrdlQ2i6IR8LPsP3wPhGfxTax8x1Y5GY5/PEXSREPJJl7Bs
1NlUcJ7AKwuRczxC3+PG8H7D7AinnE7nGo2dsyqVqTWrRIU0wl/1DfzIoIkggixbBOll7oyEqsa/
HM5aJXMgQbu70i6fbsXMFsEfBAkIa19yCksbxOmadQHbXQNGdgKjOoERHUB33Y3OANA5DNjiPcBe
xwHvPxzYZEege0xeftLHcqea8/F25R2dDuLVZHM+AtMjFJG6ef4aK8tFSBUoT/JEvETw9eQuOgr4
LMn6Mrm9AnkxY7JlPhBs9YEsDecQ1gRWdm5QUB5CRkl1OURa5yHlyHSboA2z0pE92O4rjjp7YBhk
sDoiffxBAV2zbj09rAMY3eWCeFgH0AkX3HW/kWMM0LDAqAnA2w8C9jgWePuBwIRNfb6ok5AjcT8g
35dy7dqCz6wwM4APJiTfE6oEG4GKhmDiNEX3lE8CivIkBK+B2Oyjz4yP7cW3iOConCdZPgerwyJd
rl0wUaXBVwGmldtFgU12Fun8/iNaI1v6F298AOReNnOoAeisASM6gTHdwIguoKvugjfXFH5DrA9A
swas+2bgA0cAu34D2HJX96CJesGSB4GNQrm+xtM8YlKHh7SXoNJZeSlOq56n+WeAZKSkZJTtL32l
QfLIHXv+mUG7T1EBpHBeoEOKziBtbgeKjIEIYkLlQOZoo0jbsH1A0z9yCKqbTbF5EHf5IB7t18Nd
bK2c0tl4hgbc1HrrDwN7fRvY+QvAlLe4nW2qMwfqhFo7SWKOoQAFvAVZALJprYQkcfWSamo2F4Di
srScVKYaWCCnBGgVazQCywvKJ2CRbUC0isjRse4DGcyloEYnE9SqrXdFG3YqcBcYf5up2YzdR+6k
ow438o7ockd3HehQ2oanpQxTc+3UALD2psB7DwX2OBrY7kBgzGTPIwVAn4JGlcoo4fxaWQGZzUUX
QVZh2VcTUwdtS8g6I2gKSCcXQSpWHeIRzQqFA4uiFF+UcyisESxYr2kFrRvcCipdEEjtiFXprKbN
EV7AhK8hWhdgNZobgyljgJoB6gbo7gBGdrljWN3RSAQ/QgXsPIIf3Rvua5HY9N3Abn66vfGOQEeX
l8V30AUMpKMSbSj7pOTJfNoSwgVQEUlQqonSFvpaW/oxgD8Uw5dA8j48VUafqrAk3M5FpITUXkBt
DPiGVvShE+2pFamsFbQqgYTMRxyVgrAA9DOh7QUfrzsr399nrQHEb/eghzrCFJoFcEfdbWSN7HK7
08Nqjq6ZQ+2v5eVQA5oG6DPA6MnAth9xo/OOnwXW3ihrM8PqqiK7LH+gwfspPzhN5vNyPBHtWZSA
greUt5QhQja1TpbjGXQ1U7bIOauhwJUZshKWtrwxWQ+QRcCL+R5i4w7a3yBGqzKSrNwHAw1/9SJf
14zbUa4b95jlsA53f3hER7YeHqhgMX4zrGncM9pTtgF2+QKwxzHAm3cDOke4Dmv8Fzbiwno6uCnl
M1lO40mB6qhwReFdlR/g028bz7UNL8jr8p/qLSOhSlI1X1AdCDM4L+d4pAW+8dyJqJU1hEHxI5SR
wQp4teSInHFMvlABcAGYu886mCBzVD09yJS2RncNXvbqFcCapW563dEBdNTcenh4h9vQ6q652008
lrgKvBlbhfH/rHEL8eFrA1u+D9jrW8CuXwOmbOUD3qan2jDF/SEgVb5KWQbyAxWL+pug8095Hn7E
LsHLYyKHVL/20OhEo+esFdRyDUufUQEaGWUGL9AKNAuFbFkVoJfL8Ti0NKIqqBx0OT15jyAM5K9K
OBnGAKbnNeD5G4EFD7t7xN2dbh08rCMfxFxPqbNUtwpIBk3rbQ0w3cC4zYAdDwY+8j1gu/2BEeMc
Iy0BQmEuRy7vokxJbB0kgr9rq8hm3oR0zmmyHcWMUJctbalglxq4Gg2owTKZ1hem8ySkUoTCQg5a
MRjhKTqVzL5evv4aJGT3kTWbNFoKfnfZ9v/iQnBfIzdAYw3w0p3A3T8GHrkQaCx0D3oMA9DpHsAN
TcUPCY2moVCG8XunNaA+Clh/O2D3b7jp9npv9beqfEATv6H7utIvNBWVdIaUHhrCup3S7FM7eJ4E
p1kn08W1UCaSV0HRUK/nLSqi6JVNuugrUoDjjAQpJYF8bSm2HKSWomArjQTkyg/c6AdFEd96EpEK
Sr5pYZQvgLU2vDETgPtu8dLngRl/AKb9AHj+VqBjNdAFwNCz1lK/NqHJkG0VgrUDGDUR2H4fYP+T
gXd+xj0pBr+e5zu/hstmHdlAqTREB0uXwcvkRYuKSfH8AOK1Nt+3ivzg4ynQtAqF84JPaF3N/SQ/
Y8RT6wiyYplWkJRF8IqrfLRxw9JRnZQpjB8k5HeYZboMsZ4DdQ85erKLfGQtsGYV8PydwFXfBW44
HXj9ZRfMUO419wcpN0j59DCJ7QImvxn48DeBA08FNt/FvTOsZrJxxBoRERIykgr0UMH8JUd54cpI
rlaHSuNPrim+iCrRwPtKrrCeZ+L+RZ7MGCJIurS0v/DywpWLe1MzyMOC7QoWdYD+IZ4KK85MIWId
eOWcXuzWmLWAbQDNBrByEXD/n4BLjwSmXw/UGm693OjLd+JWQP2myHzOYzzB1IGGAWw3sOV7gI//
DNjlK8Dwcb5MwbPbQJ7O5bdqDg843pX5p8zTIIIoAysUZl9SkEwTUjI9NF2ZrPQ7uxLkQvApRwRP
l05KsQPFChhk6yshYKCm1XEQF+jC7QGYPkKH/qsUkNknWpd+CtVa4JXHgcuPBv56FPDqk8Dougtq
2ycV7h+qiDI1t7PdC2DYeOCDXwc+dQ6w+XuzFxgY+WYSgqggmFw2beWo4HwpgtqVt2+ULpFJ/cdA
EU6gACiTJT7ziejGRCYwVS+gVCrTRGOHlFdB9zy8EOudFGQyun+IY6A2lTIUKBvZQq3slw8DrQbi
C5Vqp7X+NT51dz7rRuCCzwLTLgAaK9w95fAL5oMMrp4x7jZZ0wBrGsDUHYDP/BrY87vA2Cluqh3t
bIeCIu1hZL9SfBHg88IMRjkkXULSjEYsQOgnZE/CrjYh79azUTXqoZxBEmIUOSLX6VuFvDGun6sd
fEgg/MWSA6WTfqHi7eW3qumlAqbupts3nAZcejTw9L0AeoBa003Fc7IGCNTedE6gzbDepnuY5H2f
BT5/PrDN3u5bVRAPkWgPQlj6px0aeENwupIuQ6iGGwh3nrsIeaToyHeZJK0E7vYTkD19MlAIvi1w
Yrsw0IV5Uk19+dwgQlEla3Bv/wDdfmp56UDvroYFnroD+OvRwH/OAuY/DZje7OZq0LcNaOVEH4/A
R+dVTWDyVOCgnwMf+T4weUugozsbnTXZSNRZCKUf8s+UPJ7HeSR/ZGOBQBP+OeTYCmwugNu15sGW
Q1EewRQcgq2/sH4NmJBFHb2lzq7AyagYfMFUz2tTrd9/GP+635ZA020YNzrfczFw+fHAIzcAvQsB
438XinzbKsrU0URa3+9M3Y3OawDssC9w8GnAtvsAoycCNf8seVYgE1ZWJyCYSAlPS+qUryrkcR6J
HC11d4aplcpvA35EzmnBYEWNbdYeFSuqrwgsgCMRPsGDqZ8YmNtFA6NLayiq06+Lmw3gpUeA608F
pv0amPsw0LfUB3QbwUydu6hYUV6t7qfbACZvDuz9f8Ae3wE2fRcwYi3/VhJ6ugVeRyGjtL3EvWsI
vXPylDw616qiJYCBXz4YSnAmdp6Cpkg5CuagmiJtolQEr6uk3iI7LdLlBhsldVca2StAn21I2b7H
hYcSxA6v9feVVy4CHvwHcM1PgPv/CSx6Hmiu9ncERLDIKlLgnZ8OLU+CpttrAHSMBLb7CLDPD4Gd
Pg1MpOk2D5ACBN1lRQm7KC31lofkj+B10spoKDGhVdBNvAqo4MC2UWQxwbr6B0uFdkENxvVK6DhQ
waxD9J6EDoAPGgs3QjXWAC89Btx2LnDjmcCsm4Dl8wE03JNhvG3KOudAwBi3dl5tgPEbAe/9IrD7
t9wrhkatI0ZmDq9c8LEctX0evVFF2iLPpZ2cZmV7a/qAxYySL+X3E/Tgq6QPItq1wCiNQ4j1z49Y
rSP7pYkSWdROkV7ygqMq3Rb0XWsFFViCP40BepYCT9wE3HgGcOeFwLxHgGaP+zKGFtCDCeN3tvss
gBHA1F2A3b8J7PxZYMrW7i2fYXTWoi4RP0WjOYkhUTydO1LLj4RsqR+vY4BQMLXuZy3GW528XURI
Ge8RFS/izaZdlTp6CVqWYZBoIesyWxSXQrIPRfC+L2IMrvRTbTpenwPc91fgprOAR64Blrzs1s7U
nqxYWzZRuVRZohv455WM+yLG+I2BHQ8BPniUe3fY2MlsdPbGWPqnXfBpjSweTOK6yHQReFdM8ae6
q0qXClHspITHSAQyOaeakBxI0UjhlCxBl/YYsN6bkuHhsx17CW8JsocuxGiUQpKlH35MoNQ2ar4q
swkOktu3CnjmbuDm37jj6TuANcvd6Bxeek9lSsyT7VmUJ/PhbTA195hnfSSw2c7ArkcA7zkU2HB7
92sYnDeca8KQPcyoHRJavsZHTxnm2iUhOCIleMDJiXwGfuedHZzWItSriEwr0FgMXEapGuzKoclp
A+5rjFmqFJwl11AIrw4aCITpPm8y46fzJpchDgED1plY29mmG41nXAn851fAnX8AFjzjgrnm15rt
QKkqCa6u8SNq0wBjNnDv2f7gN913nsdOzt63Tb4h2WEAEJVpdRfpJmm5c1mgCEE5QZMblNVlxj8Z
01+oYqorE7PSVY7TW5A1YDCt1xv5QXVK2whLw5xaPlGqqqJPrgt4p1sLNFYD82YB91wC/PvnwGPX
AY2V7qX2cnQug8Yata8C6UvrR+facGDDHYD3HQrs+nVgs/e4b1XBZg6yfhAwvmytno3IYHWndEjl
WfYUV853HrkglA3mz408T0HKi6E/az3UUHVkjpK0HJiAFEuLiL/G2KJQrfEHENEGXKqzBcieJhm1
NNF8MFsLrFoCPH2bWzvfcLoL7g4TT7c1Udp5FTAVIhhk32e2NWDM+sA2HwX2PAZ432HAuI08n3hk
0hj/O1lsal0ELZ/KGS+PaGBpImrltX4U5MkMDVJxSlsZyGrtgwfLNiBSiFRKtS4h5cD2YOniXhkt
MbcFQ1No6gDcYD6NlO2dBO/sqU7l5TUt8Npz7t7z1ScB9/8VWPk60OkVomCmoz/I6SBAo27Tv553
4puBnQ8B9v0h8Lb9gc7u/GzB+FGawHXlOkvfyTTkEs4ry3Xm57RJy2VR2+VGbo8QGxry5RKbXW0i
oZOKUl7GkLJHhUWtpj000Roq3+aB0tgC/dVFhY8dwGQPfFA11v+j0ZQ2Y4qU5FDZbPYY5+pVwJzp
wC2/Bq4+EXjyLrez3QH3vWgpQJVXghKfZvDr5gaA+hhg452ADx0F7P1D94oh4oEP5FTgEChb1h8V
86N9aFaWGbUBg0zDt5mkW3j5rfX7gQ1koLXac1YQUh2BZBfXUTkAC9BSIMN3Ehjv0lhPJ6ZY57YQ
RPL6EvWY8E9AsTHpZt+BrQUaTWDpq8ATtwDXnQLc+htg+avuJQZGPHgB/6lUFcDzivgkeN9o+neG
DZ8IbLs3cODPgHd9Duga4ab/q1e5N6mkQDqm6o/owjk8j0/ry2TlfKzowXlCE8eCByiQec28goQV
EVmWVcqrI5rSaT0p+2nVIYTlu+tU9xDoELlA8VOOJP0m04ycUp9GNtt0T4a99hzw3z8B/zoBePxm
oNnrRmf6mmRKjoRs/hSSXcX4Ll0H0AWMnwrsdiSw/6nAlh8AjIFZs8rbJiqS9fI6cvUwkJ+itpeF
EkiyiBmWyMpszdBmICc1YEjwlK2JNUROr1C+JLsK9GeaS2DDvxgVVO4ffAVKA6f7VAWFVBYpv+lG
395lwNN3AVedDFx/GrDgOf97y2x0HkyQj2lmZOpudO5cC9hqN2C/k4E9j4cds76binM7pG4yzWG8
zcQkecnfkl4FlvcVvywiegnKA5nWWSHNP1MaazQULN4lUnI1KDKrFi1BpUDmdaXY/aZZJXklkAOJ
gxz1REdNrgu5PopuajGNaN3o2+gDlswD7r8U+PsxwAN/B9Ys85thDb+7LcsOAsgU4zeM+gzQvQ6w
4bbuXWENz8S7GZ2n9ON89Gl1twWkZAFZwSKenGxqV7kRltu1VsDn+6AFPm0a5GpK9TSPVB73YooH
sSEayoq3jQKhzDXRZ1GZfoHLzVXK0rQhQ/xV9JH+bdGhdNFv9AGvPAb8+2Tgqh8ALz/qpto16zfD
hhDGuN/Isn5DrOKDeipkOaPQWoL3NXczl2clIY3yQA6QWsvO45EgO6QyOV3jUTqXxgYLa5toNtkP
nbUJerFAXDcDlx/42O5wmUn9QLx5lhCeIFdHQkDkVm9z1BPpuW3rHvV89Frgsq8Dd10E9C7yD5L0
+elpRfAqqjSrxmdM9mw25clPDVoetXGO5utJ+S5A5rM0dymlQ7amjEMLgZwSIpRKrYGD8dLLlObl
6JzSjF8hqfX1E+7FAsY/a61ATpPDpo5iS+UlRVVwP/ojqoKWQ7LNpP9lvoTSlkbS2TmdGmQ6GAMs
fgm4+XTg8u8DT94N2FXZZlkZyJ0DjVbkSjeR2rxduaxwzv3MhUiByBwX4oelI3atDU0rgVwEzSMK
LUcib+YyGBRnqewqsW3oP6uqNYaSH0hZnkFtQNbIhTC8kWXn4TAFeRxeX0P/CtrKQJdp4TL7VgNP
3Axcfixw07nA6y8Apg+Af982XQg1tWU6Ba0sh8xrhZ/rFlwgCzPfJNwUg5gqMAdW6SSXMUCBjFgZ
aR98do7ODeAdRRrm07x8xJIZRrvN/Q2abGpOcmSrSjCalm0wQK8PSsmQG15FKGDk/cSguD2ipOTh
NL/JZQAsXwDc9XvgX8e7wO5bDNSb+cVr1qQOmvgyFJhZiqKyMk/2Nboo5XSWBcGYyM+5Qg40w1Ll
9iuQeYtLr4udbg6uRKRQgj+ADBH1kHFAcETLL6crBAknmT4dzNZ0UsyxUH6Gph0IGZRsSXRF/6gy
U2U1ZnISK0Nr1RceBK78AXDjr4F5jwFYnf1WldZ3FBLA20FmeEi65JX5ZZDljZj6Ruf86S8U+E5C
8gklQzKjp39pQkWZ15DlSV2IZrU8F4DFNNo990KosZW1YKMxUJtdRvQpqVO+7rwdbwCCCsJ/kf4J
PTlbYJcPuzCbIzGKbOvbiIpYvzY2AFYuBu75s3uj50NXAivmuam28RtmRdBcnwLxcn6elnJkmkD0
4Bdvp+pKT0zJKkWoRDaG4HHwI3JZbdILDKEOz0NpjV06LvAUyOfKy3I8ylLF20R6GjzAFbWIsGzg
LitVKWVLik4Q7cL7k9avpEJSL5621FcM8MoTwPU/BW44E3jmdmD1Yt8z2dckSRWhUl6HNqHJ5nnR
OT09BsUZ0jEyn9PbAa8nQ4WX7ymWGfgH9YX1hrFzsYFFGBtGWE+Phj/pDIKiD+Do1r2cvl6v93uN
DLigUZFSYYhgIKdsIcOB9OM+F02VRmVGAaW9NB25XD46r1kJPHI1cO0pwN2XAAufBOwad+9Zrp8J
vDsNNLgbuDssgHon+60qRa/Qp4sU4wJbRV6uH5FlD+DIF3KGcWUFj+aAkMF4ozz+4AlEptTLT62l
av7l7R0dAxPI7aM/jVQBJiE6RSN6qUs0ASjpHxy8L/hPw86hieBrYgssngPc9QfgP2e6lxgsf8lN
tY2yds7JKgD5oawM55HnTZ+gdT5l8qWD5oMkZEWaggUyWBGx2ZUqxOgpFhTcL02Qq0Ea5pHzlTtp
Ni1eePFlzF/wGpr+yxPNNl5L424/0UggnFxoD28cDyPS/UVLogRzpHtVQcRXaLgAKxOKFdQXjc6r
gCdvBW76FXDLucDTtwO9S7LRWS6pCsSqkPwkI3U04b7tBbjnxhc87X5gHojX8jb844Q2II0y4mDZ
PskCWak0GtW8ED4S05ef+U1rqQPRgGodgabs4ZB20KOHjGiyztJsNjHttv/iV7+5CLfcfg9WrHRf
W2vnaS9r+fRa3pgvgOW9IJs9DMwsoewtobwB/GduGVTYSAqK9OaNo9EpSbpkpBwMqeGf3V7ykntn
2LSz3KuGXn0im24X+qACUm4gBBf6W2N1AD2LgRlXANP/ATT7/KhM20xiZpGDRmsVTGHeL030QIji
4Zac5cuzoMqjRF54LliAO0nVKavQWos5c+fhT5ddgRNOPgNnn3sxZj/1HBr+itpOQDuwqXyyOPOB
cEJ686w1WFumf6mSCsp4y/IJ0kbqeHzqqcH7i2Z01v+zANb0utcK3ftn4Oaz3HR71WtA3Y/guRFR
QYqOWMXAZ+H6WbPp4rTZB8x9ELj1V8DtvwZefdwzMnvzTc5QpEC7iJeW/biPLGGZJcKiQiMZVHs5
UVkXgy4cWSXWWqxc1YMHHp6Js357Ef7vBz/H3y6/FsuWrwBaDWbeuPwzCa5gdl65vhLEF/6yAIEy
EktoNImc0z0EPSdKKWeQ14eajncfY7L2ttbdqnrmTuDW37pjzsOAXe16sG1m61dClfbS8qyfPsO6
d4O9Pge474/Af34BzPwXsOxl9ogmFZIGDCQ0JTlc/gAFcsJr0racnYmGlAh57LaLpQyI57sdj21a
2KbF/AUL8e8bb8OPfno2Tjj5DDzy6Ozws6uFAR2m1f5I6TbEsNai0eDfILIFjmO+Cj5LtFV/wEXx
pRZvIw7WVPHyTSDk+aVB32r3+1TTrwRuPB24/zL3yl76NQwanVMmRnqyz3D4Ub5ugL4e4Ikbgf+c
CtzzB+DlGUBfr79g+K9khogmsP6i0ltBK/xVvsbYCkLd2sjpEa1/q2yE8FavACaGnpfu62vg6Wdf
wB//9E989agTcOav/4hX57/mH/hwPDKgwwMhQWCiMSJSygYHWUc74Jt34UJj4D+5r8p0Ram+GSrw
pVhy7vNK5HQpgIHvUzTdXg7MfQj473nAv38KzJ4G2F4XgPzH26Mg9bL4OcHSKAxXxytPALefDdx6
BvDsHe4FgzQVSi3/WgI5gx8yX54X11sQyMUFM3A+ClBGKoT0cgpMYK6zysqYLAsA7uksa4Fly1fi
gYcfxc/O+D0OPfJ7uOb6W7BmTR9QNDorpAhl+R6q7DZgjEGtxh5GMMZ3MO6XAtjwrwUUyEyK0joj
fIFU25XB+l3rpgvY5QvcK3qv/5mbbi+eC3T57z3zgObFOclSADfdq317lwDT/wnccBLw0N+Bhc+7
VxmFuxcQesu+iGy5o/qlyOZ2fQIg/vYT1ZzUogKqKpFq5AqwSJQp19uNZhavLXwdN936X3zruB/j
hJPPwHMvzEWtVotGuzBKa1UlkWYemB1rJ6der7nYXWdddOy8B8wmW/lfWqj7wE64QqO1jRZ28QHm
m3b9YF1ZXrzZdC/QXzwXuP8vwOXfAR6+0t0a6qq5/KYSWNYHHO1GowHMnQHc+DPgjrOBl2YAq5e7
lyCEzbRW9NYc00p5DQXlTe4RTakAeaDo8HUYrbxEYFTOK4KKaYFB1Yf8/EFrzDVr+vD8iy/j3Asu
xecPOxZ/+dvVWL5iZVg/U1DHu81l9qXhRtKCCVBF1Gq1TM7YcTAf+hjM4T8G3rM/MHy0Z/IBTXYb
cbsudb8/gPITfLnvyBYgIaJ1GNHA/pOmW70rXDD++6fAP48Dnp8JdPoHFxs0OvvD+nvRNePW2Hed
B1z9feCxa4Elr/pRmF7rmzIgRYcrl5uCs3gZBPS/ZwFsClNgHN8A0Y5WoZUJ1fsTrg7x+/qajSYa
jQZWrFyFe+6fjm8f/1McdezJeGj6Y2g2LZp+9C4yKQ+pVJweyOk1QAPGMGCTtwBf+C7w2e8Ckzdh
U21vLE33OAr9nsyocBFAQrhlwSjzysDL+PqDCEr74OxZDjx5E/DXw4GbzgRWLXav6LWN7KhZ922r
2bcAVxwH3HkusOgZoNHjp+4pHavaPlhI1G8tlG8/kRGSrsHzRfKNv1IyEVHj+/xwcHoLkMU5vQhi
JG82m+jra2DBa4twyWVX4qDPfB2nn30Bli1bjq6uTs/FjCmTH/mNmPUNtXYRArnZRKPZhO0DTOcI
mN32Bb75S+Ct7wbqdTYyQ7SrpqMC0lcrVgSteZNt3arQorQfbZsWWLkIuPv3wKVfBp68E+ioubd6
dtaBZfOBO84D/n0iMOdBNxXna2ovKm90SldGN2IDN+de6cyUzNZQR9faJ7rTIoFSG8Ebsv2J5URZ
liDoKbYkfIGoPWXjEo0+jHB6dkpYvHQ57r73Idz7wAy8MOclLHhtUVamyEURMsEujgw23mh9fPAD
O2P9KZM9Xam8BNZavL54CW65/V48NuspYNxEYNudgUkbwTQN0LDAOhOBbXcBenuAF2e7F+FVrYvY
LLlSm4bLNIOBMqWUkOVlOgXOJ8qQrpEB/lj2KvDkNKB3JTBqEvDsPe6W0mNXAb3LfZegzSwm14R/
jODTdBqypQ0ynaIRivI4Ur4Nt59SDAR+9SgI4lx2mYJl9RZB0YOqS1brywTejNFaNzoDwKqe1bjv
wUfw2Kyn4lE0arw3Bm5JSHZkj6da49fGPQBGjQcOPQH40onA2AlOacNXUcpU25Md2L3gHHKNzGiS
PlBQnK6QADECWuvsXrMKuOd84KJPA1d/F3hpuvcJlM0samStgv7YlyhrIB6hTaEgz212acO/h6HO
QpUV2FlQjw5ZQKarQOlAxtODnpxHOkucGxckTf84Z7bRxYyVaob1p8LLMFDTamvd+t2Nlt4eb6u1
vr0aAHobwO77wRx1OrDeVEevGcdvoRjikRuBU9D8mU8GpJpARaqTCUTt60dV6Wd35fPT7YXAmp7M
dzYIYTrxunn9Um6cDLC+TmKIxPmA5QdHsEciRcvq8Vu0gidCQWbSz6kMZqCKVLlWoMnnRscOiNmy
BrVN5F/NoxTJUJgJYOCC2aHADmPcryysbMJsvxPMt34JbLpN9vvAGCBX87pJnmGjvaoeC5xcHlpQ
TBGQk8dk0ehs/LmGBDkgxHbB4Afhi1IodkTQ8vK04l3rPH8LCFYr0JQ3Cq0dyHpJJtVZUEdoAEEn
BDqTEW1uBKKqQzvrYgl64sxBjEC82pr7YW+7oglssiVw+CnAm98FdPjfCM4r7cH9VOAr7leDbOSz
PK+oWaWPiOYRynC+1Dml+SdDUJUvKai8P2iZkkKUJfnIdgpwfruP8RSiLD8Bv49RHMhS38pgjaw6
W8CgfUMKwWVKxzJQFlc7BWqoUuSFDdyITHIKbPKwpg67qglsNBX44vHAWyiYC8rlglI7BL9E3nwd
4Tl5po86vVfW7NbTtTxAVM711uRLMP4kO9M7yUNykgx5RKxaOeYzr6L+qh8iUWPyRtEcptGAEiVY
Mi2AQciipBCXgTkv4vEJ6siG21UyZSIk6yTk7RnI208RND0krVYHeiyw7qbAJ74FbLkDUJfBzBwR
3kIqBVUAN7FqcbVfiel36jM61yqUwnMVFSOIFOWizUDWl1QkM3TkVCwvr4/IsjGMV5wS/BsugNIh
yit2UxnpZA0UdNKR7FMVQTowvbWsCKkrexXkhA0K4ouBsrnDQb6xcI9v9hlg4zcBB30N2GJ7v2Ym
vW3Wxqbm7kMb2bYp2OyBDN4wUboCqF8FeH2iUVuktRFdoiDLQfiU0lE51jdyOtKnP5flWobmN83G
zPb8AyGRX3xBKZNgpFEpCJ5KRdioaZEvpNlVhsJ1EO3Oa3KtcEyOIfZXRKzB+Fs/A7VGbgsGLkAb
dWDzbYH9DgOmvo3NTAzQ3Q1M3hh4x+7umLwhG7mL6mVrTN5mYYqeCJSAItlFkHJiZPsJ2j4GB89k
51Jt8AtNmU/aAVvDG5/O2Uj1xnXnR2RezrA+LGGqBjFBOkgqyJDsrMyJmo3tgldHciMVyBEEf677
NEd00+rsPnW/wYMvX3kMnm3gd267gLe8A/jYF4HN3uq+cDFiLLDt+4D9jwAO+TbwuWOB/Q8D3roT
MGykL6vVR2lJ5zStsWRaK1+G4jL5Wkl/WTfl8XO2YWW1fIKQVaxSBXh5kVhNXw5tRIYoF9qJ3FLE
r1mhlNPYAsT0Nsebaoh+gt82gVYF75QSPi+nawY3WJW9b6sasmro3nACoe04zfjrdzew9buBfb8E
7LA78IEDgQOOAN69NzBxY2DCBsB79gE+/g3g/QcAE6b4b1iJWY1FfuoLXmeBfqovSyDrLwLveup0
WELQQ7ctKRtMb8MeCZLTiqzs20+MAm3rnGXzzwgaUSiTEKuiMJgHATb8KwF3QsqgvJw1a/qwevWa
EMxtB7QBm76GnpYGV5Orag3QMRLY/v3AJ78FfPhzwMZbuXc2N+HW07UuYOo2wH6HAgd/DdhmZ6Cz
28utZfdlwXa5Q8fnldF5Sl+Npk+HpeSBB69B1iTTXG/NBo6yfA3Vy9TRNd4/a624SLvyGd8JIgJP
W2JiNI/cVaZIjgZZfoAQxDL5haoUZCZc2Lt6NWq1GtadNBFrj1/Lf6fY1Vd13UvPWt96x3147PEn
gXETgG3fDUzcyL91hsmh0+Sn93etExgzHhg+Kt7sopHeAhgxClh/KsymWwKjxgGLXgWWL/G8KN5w
SzZrTiGW53ySexG/wYC9xFDWlwfXK2UEo2nZEUoZWoCXZcj/IZBlJcRI/3w6TKEEX0CiQQ2UIA4Z
ipwUWuFtAZFYbnsVMBtsvuM5l1isXNWL556fi8eeeAorVqzEpEnrYPSokWGErhLMFMi33HkvHpv1
JLDWBGCbdwOTWCCTGPlJ5xqdmoamrfS8CKWbgKnVgXHrAJu9CVh/c6BnJTB/rvvWUOIupkoDFKWU
c+4PrndaaEX0t3wKfCbLjzYRLo4kQ8qKZ851dI8/0Z17RuOdSOVC/CmCIojRzLDnnVUkM0qgXRAG
CGR3ZdUEI/djIGQCV/X0Ys5L8zBj5hOY/dSz6O7uwnpTJqG7u8u9xKDk52CttVj0+hLcfPs9ePzx
p4Dx64RvP0UjslRBgqsVgrfAduMvUk0AHV3ApA2Bzd8KrDUReH0+sMx/Q8wY0Q9UYSVpRgsffnRW
eYtA/PJzIFGglwn/MliiiXLhlPdvHszGp8kpcXkXyBCNWLobHRhZBTyrSrCxOsKUrqjeKjKHEoqu
kYo8P3Nus2mxbNkKPPPci7jvgel4cc48TFpnbawzYW3UaqYwoK21WLhoMabd+l/Mmv20+xrj23YG
Jm7IOgiDFCHTHEV5oHx233bkWsDULYGpWwOrVgHz5/iXtrNbNDkbpE/ok3VQme/r9bX+D4M0TGga
SEoe0uS8PPKL//R9ro5u+j4yh5RaoCCHgezNaUT345CVy1UhGznH8AYhoYcJ/yTR2eI3uhqNPry+
eClmPfE0/nvfw1i9ejXetMWmGD58mHhLZgZrLRb6EdkF8jp+jbxhfo2sJHNpjqK8CCY76p0wa68L
bLU1MGF9YNF8YPFrns2IttPakPHk6s8R2oBW5yBAjI6eqKQljUFmGUbMzRS5/xz8iMxQOhoXQF0H
p5CoJyK3Iq9dkIe0owgiv7KqMeOaNX2Yv2AB7rl/Bh55dDamrDsRG224Hmq1GhqNRjQ6W+teHDjt
1rvxxOxn3Bp5W7FG5uBJk6s6jzKTI5CPasCwUcCmWwJbbg/Uu4AFc4HVq/yuNgmVnwLUWTOCchEv
gsZHOkraQCPl3BKbJQyzl9/S48XDeSwzC2RbNrWVfmEjqOavIgSbRSEpe1DBlKbqcjZUMcw4AbkR
KIU43+1cG/T09uLpZ1/AtTfcihfnvIyt3jQVa601Blb8CN1rC1/HTbfchdlPPed2rbfeyT2N1UTc
fnTKTeBVp0zTaAQqE8qyhO1wF5attgOmbAksW+x2t5sNpWINWiCneMvgy7VbfMjA+zn1HdGHNBsM
RH+ztN2ocVMB/iVof9BGVktgSheWHaogzswJPrDs0PhVkL6a3qlysc+bzSaaTYu+viZenb8Q515w
KT607+dx0V8ux+rVa8KregG4lwLSjmbRbZ8UCpo7SS+FcaNvsw7TORZ41weBr/8MOPgbbsqt+obg
FaK61WlqqyD/cFl03qrsVvmrgvcb7h+hb851rJxBmAXHLxbIFSqBEXrIzwjMISFfMObKteN4gmxE
Ttfq8khWqRIFJE+qEkLMTy/JbzYtnnrmBXz1qBPw6UO/jQcemhmm181mE300QtN7m5tgG0xMYBFk
/0nROFK+oTLGwBp/u2rcJJgDvwLznV+5h05y37YS8qjuVi5OUbOmFOOdkqc1/hRa4a0CbiPJLqgj
conCZ4BaYcOVgnUe7q8yoZEu3LmI1wYBMu0h2yjwybJeLnX2nHqeEFWh1KfSCEV5JWAqWws0Gk0Y
U8OaNQ1cftUN+Ngnv4oTf3IW5r2yAJZPhmp18XSVZlsCrapL/FXkWwMLwPT1ASuWAz09XjdRKdc3
+KAFxbgurVwAgIqG9BeyDtlA3PiqIBn8iN6iKVC0cWXBKme9MMvM8lI6qnSVmIDUT+pBtAqgohbx
tr7xU5tIjLSb327hbOw+umFyI9DbNahsJttawNomasZg+YqVuO+BGZj5+GwYU8NDMx7H3LnzgLXX
Bd72XvdcdNEaOQWun4YUXcLAGWGt+zG1Wh/w2jzYG/8O/PUc4IXZPtC0jssVrVrh/wvg/VP2VaJV
tbd8KWswanNRS0mhKIg5SEzFQM7Zlup5KYeIetTPFHxZo/jTep0j8X60k2JzNhCPlqHJlnxGKgPA
hvg0xqCrqwsWQG/vavdi+k8fA2z1LqDh16kkhj6lzmXQ7JQgtYmPvotsmsDiecC9twB3XAO8MAvo
6/P8zhbd6f9/g2xXno46FjtPgfFLdtYO4ksTUgEPaoNEtoPoPfJ2QjStkhoVQfJ6RYJOMp945Lli
hOX5BfbxOor8YFCQKUwx8D7y/gpHHvQlC2stentXuyAGgKWvAwtecj8GTj8rKqeYBeqoIBWqlLP0
QoGme0f0o/cD558KXPJz4OkZwJrV/mdI/RsuXSEh5P9V8P4kjwRolhua2fO76RcrmpAlyaGtLPvS
RAg8cSslp5fe2RxYZ+QdNBdsMk00jS4VEDxRILQIQ+UlsQRGUQsoXo4A3hfZaR4aMbYtfFHeWve8
8+LX3Ug8Zqz73jD9LhS/kJK+mvgUpI1R2ne6WtP9SsPcp4Fp/wSu+D3w9PTslhOfToe6uRKtKDRU
kG3IdSQnSp4iMF7eHjl4omHnLcBg1FRXU9CPT61LAjniVypv1WZNhirA0yhLBiONSpxsqGMJGsEi
UT9B6kGN6pEMYkWmJX6fJ4ta0i0YmJ1LnxoAI8YA274XeN++wObbuLStuQsoPUPdLqQK1roAtgCW
zQMe/C8w7XLg2RnZbww3rWOO6uX+kp9vFEgn0ZZRnqRpKJPjwZdokajUBZ4x875rwj9GGrVZtgjj
0i1853dsqpKRcGkAT6ccQJBleV28rD+X4qK6E+tZSJpPBPuqQFYsncuRIyjluQ4edB58r10obcYY
bDfAupsA794L2HFXYN2p7gsOdBeA+DS1ymDhA9gnViwEnp0F3Plv4L4bgZXLfB00PdQMIkgFZHqw
ITtHmX4Ewccv3FGWLE8bmuLClivD+wK1rUjzQqJPGIz2IzJB2pVTjCEEsmU/SVLAXwkkU8gJP+3B
ITaPDFzC0vIgLyYDvwpqM48UuM0+WVRHDkxZKqvWTWXpwsQrkgV8EMEAXcOAN+/g3vSxzbuAtSa5
xwUssq8bamqlQGu3mgX6eoHnHgfu/g/w4M3AKy86HkOjsCZb0TX6HCqk9EjRIToW51NslWI0UPBb
/y+UkY3C67PuMERnCDNPo02tJXiwMAaZHpBA5goIR0eBzEYYUo5f8XggE4uGsHZPMUhw5/JiNiFD
0rj/ZF4KvE5ZRjQY98n4ScD2uwI77uZetjdytHvrB/hFlyHneu/Tmv994fkvADPuAe68BnjKT6O5
/bmpXwrc55pNgwXhq6yzJOgS3D5RRorQkBvBiwKZw/o2k/msvB7I3EBRmG9eBHgew85VFOURpGxm
dJTHd3t9vm3GaZ5vPE0i2Nuqbv48kpuSkaAr6gBQGpyfiIsqMzcDu8jVOoANtwC2fx/wnj2A9bd0
+Q0qqOhmvQ50H3z5IuCx+4H/3gA8di+wlH6d0mQXVxOUikHkqJqS5dqggLeR1l4aTYJ45NpfzuYU
P8D7FKJNgx/IhxxsmWKQ0I1ma8ZPrbnACMxwa/1tBKLTKS+nyUiBeBOGE/j0HXDloiDmupFTKEPo
E6an3KlCVg6iDEgOz0eJDCj5YvPNQPhCqbcQskPRE181oKsbeMsOwE57uhcRjJ/sfoK1yfgssrrq
cN9eenYWcP80YPptwLzn3U+0Wu9vwz6DbbKTCwR/88/BRJHveP2cT9KZbQS+aUX5IVu2oRTJ5HHx
uXqrgPgKA1kIrhTIHCk6h4mUUQ3gnZ0QAtnnhZkC7dAW1K36q4AfyDdiW/B1RHVyuVEmg0bToMkX
S5AJk4G37AjsvCew1TuA7uHAGlamZgDbAF59EXjwNuChW9yaeNVy1gcyV2cJhkJX8osN2V9YoJ9Q
5OcuPhA2pPQRfYBE55ZIns/4uuicZaVBPlH0LgHb7JJXdGEoD2TaHMo5RMDQqCM2pVqCLxQcJ7Il
jXQimqZezrEaE4EJNyJdCaQgq0NtJ3GFjvQvqjMnSNhFh//o7AQ2mOq+yPDuPYH1psLU3Rcd7NIl
wCN3A/fcADz1MLDkNX9PGPoFlRCNUCn4TKv4Y9CQ6xjSsYLG6VpjCJApnNd4X4U8gpSh6dEGfD1Z
IFuieqhK8DURoUCR0GbyItEiVMcokG1jJFGiKI+DVdzSiFwknxRkDWoVuiFaGbS2E/VT0tTc5tdm
W8NsvwvM1LcAy5ag+dBdwMx73YsB+O8Ic4QRjUGmc5Aj8VChzG9apyL9JD0F1n4qXUK7+wKlfGtg
gSwEBT24QkwJQ/8UBSTJAjddfTF2e/9OIqN/sNZi1aoe9K5ejZdefhVPPPksbr7tbvzpsiuxbPlK
FhQc1HiSrtEILKi0jqwixZRqYNkGjM+ItApfNmKTbep1p82pWh0YPR5m7HhgzWrY1xcCPSvox6Fz
xYMMsLyIRyvE9fL15ngUEGtboDraFpCAJlOmNduIx3+qfShHaAlsjeySAYHGG46tk0z45+HPpT7e
pzdddRF2e/+7RebgoKenFzMfexLn/fGvOO+Pf8syaJpPyK3vZaNo0BoBrCxlKrMQ8mER+GYIWN9R
6+QQu6Bg5xb5DTpL9vt1dJjy8jbWKtUu5lDsJ7ImAwnDZFqC11Hixxx42XbKQ7GVnwufRmA8/Ybu
I//rYjyvoLKorbiwvOCAgqzBwrBh3XjH27fG7846Gc/MnIZvf+NLLNcmbNRoAnRLJolUpqeXVWHp
H2PkfSBVPpkn5IQk6eODlvY/tC9dOEYpgAU50ZQObIseD+WGEbhM7SAUtYPkJVpZWtJSqMqnIam0
ghSvTv//AAE/eVjcr77lAAAAAElFTkSuQmCC" transform="matrix(4.1322 0 0 4.1322 0 46)">
</image>
<rect class="st0" width="1000" height="1000"/>
<polygon class="st6" points="464,846.5 464,728.4 535,790.5 553,777.5 631,830.5 539,890.5 "/>
<polyline class="st7" points="847.97,702.06 541.04,505.96 464.02,557.94 777.96,747.04 "/>
<polygon class="st8" points="464,558 464,503.53 500,478 541,506 "/>
<polygon class="st7" points="778,278 778,747 848,702 848,321.43 "/>
<path class="st9" d="M294.5,366.5l242-148l102,68l-264,161.73l77,49.27l223-133c0,0,48-35,43-82c0,0-2-48-33-62l-150-90
l-313,189.73L294.5,366.5z"/>
<polygon class="st9" points="221.5,701.5 294.5,746.5 294.5,366.5 221.5,320.23 "/>
<polygon class="st9" points="374.5,793.5 451.5,839.5 451.5,497.5 374.5,448.23 "/>
<path class="st10" d="M374.5,793.5l77,46l0-342l223-133c0,0,87.22-69.98,15.86-140.74L534.5,130.5l-313,189.73l0,381.27l73,45
l0-380l242-148l102,68l-264,161.73L374.5,793.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 42 KiB

+568
View File
@@ -0,0 +1,568 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:none;stroke:#000000;stroke-miterlimit:10;}
.st2{fill:#00B5FF;}
.st3{fill:#008EFF;}
.st4{fill:#0063EE;}
.st5{display:inline;}
.st6{display:inline;fill:#00B5FF;}
.st7{display:inline;fill:#008EFF;}
.st8{display:inline;fill:#0063EE;}
.st9{fill:#FFFFFF;}
.st10{display:inline;fill:#FFFFFF;}
</style>
<g id="Capa_2">
<polygon class="st0" points="374.5,793.5 451.5,839.5 451.5,497.5 374.5,448.23 "/>
<polygon class="st1" points="221.5,701.5 294.5,746.5 294.5,366.5 221.5,320.23 "/>
<path class="st0" d="M294.5,366.5l242-148l102,68l-264,161.73l77,49.27l223-133c0,0,48-35,43-82c0,0-2-48-33-62l-150-90
l-313,189.73L294.5,366.5z"/>
<polygon class="st2" points="464,846.5 464,728.4 535,790.5 553,777.5 631,830.5 539,890.5 "/>
<polyline class="st3" points="848,702 541,506 464,558 778,747 "/>
<polygon class="st4" points="464,558 464,503.53 500,478 541,506 "/>
<polygon class="st3" points="778,278 778,747 848,702 848,321.43 "/>
<path d="M374.5,793.5l77,46l0-342l223-133c0,0,87.22-69.98,15.86-140.74L534.5,130.5l-313,189.73l0,381.27l73,45l0-380l242-148
l102,68l-264,161.73L374.5,793.5z"/>
</g>
<g id="Capa_1" class="st0">
<image style="overflow:visible;" width="242" height="207" xlink:href="data:image/png;base64,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
ghSvTv//AAE/eVjcr77lAAAAAElFTkSuQmCC" transform="matrix(4.1322 0 0 4.1322 0 46)">
</image>
<rect class="st5" width="1000" height="1000"/>
<polygon class="st6" points="464,846.5 464,728.4 535,790.5 553,777.5 631,830.5 539,890.5 "/>
<polyline class="st7" points="847.97,702.06 541.04,505.96 464.02,557.94 777.96,747.04 "/>
<polygon class="st8" points="464,558 464,503.53 500,478 541,506 "/>
<polygon class="st7" points="778,278 778,747 848,702 848,321.43 "/>
<path class="st9" d="M294.5,366.5l242-148l102,68l-264,161.73l77,49.27l223-133c0,0,48-35,43-82c0,0-2-48-33-62l-150-90
l-313,189.73L294.5,366.5z"/>
<polygon class="st9" points="221.5,701.5 294.5,746.5 294.5,366.5 221.5,320.23 "/>
<polygon class="st9" points="374.5,793.5 451.5,839.5 451.5,497.5 374.5,448.23 "/>
<path class="st10" d="M374.5,793.5l77,46l0-342l223-133c0,0,87.22-69.98,15.86-140.74L534.5,130.5l-313,189.73l0,381.27l73,45
l0-380l242-148l102,68l-264,161.73L374.5,793.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 42 KiB

+568 -5985
View File
File diff suppressed because it is too large. Load diff

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 42 KiB

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

Before

Width:  |  Height:  |  Size: 4.9 KiB

+568 -5985
View File
File diff suppressed because it is too large. Load diff

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 42 KiB

+568 -5985
View File
File diff suppressed because it is too large. Load diff

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 42 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" zoomAndPan="magnify" viewBox="0 0 1500 1499.999933" height="2000" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><filter x="0%" y="0%" width="100%" height="100%" id="8d756b956c"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" color-interpolation-filters="sRGB"/></filter><filter x="0%" y="0%" width="100%" height="100%" id="62b43949b4"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.2126 0.7152 0.0722 0 0" color-interpolation-filters="sRGB"/></filter><clipPath id="3ac8341e25"><path d="M 0 648.902344 L 1500 648.902344 L 1500 851.402344 L 0 851.402344 Z M 0 648.902344 " clip-rule="nonzero"/></clipPath><mask id="60fb1d792f"><g filter="url(#8d756b956c)"><g filter="url(#62b43949b4)" transform="matrix(1.282226, 0, 0, 1.282471, -234.697857, -90.088596)"><image x="0" y="0" width="1536" xlink:href="data:image/png;base64,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
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" zoomAndPan="magnify" viewBox="0 0 1500 1499.999933" height="2000" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><filter x="0%" y="0%" width="100%" height="100%" id="c829cef1fa"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" color-interpolation-filters="sRGB"/></filter><filter x="0%" y="0%" width="100%" height="100%" id="22a7985332"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.2126 0.7152 0.0722 0 0" color-interpolation-filters="sRGB"/></filter><clipPath id="63848e8728"><path d="M 0 694.328125 L 1500 694.328125 L 1500 865.328125 L 0 865.328125 Z M 0 694.328125 " clip-rule="nonzero"/></clipPath><mask id="17db18eb1d"><g filter="url(#c829cef1fa)"><g filter="url(#22a7985332)" transform="matrix(1.20166, 0, 0, 1.201904, -172.876855, -40.289897)"><image x="0" y="0" width="1536" xlink:href="data:image/png;base64,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 truncated

Before

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

-4
View File
@@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://rynext.com.mx/sitemap.xml
-23
View File
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://rynext.com.mx/</loc>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://rynext.com.mx/sobre-nosotros</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://rynext.com.mx/productos</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://rynext.com.mx/blog</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
</urlset>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="BlockRSC" stopProcessing="true">
<match url=".*\.txt\?rsc=.*" />
<action type="AbortRequest" />
</rule>
</rules>
</rewrite>
<staticContent>
<mimeMap fileExtension=".webp" mimeType="image/webp" />
<mimeMap fileExtension=".woff2" mimeType="font/woff2" />
</staticContent>
</system.webServer>
</configuration>
-184
View File
@@ -1,184 +0,0 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}
-42
View File
@@ -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>
);
}
+66
View File
@@ -0,0 +1,66 @@
'use client';
import { useEffect } from 'react';
import Link from 'next/link';
import { Icon } from '@iconify/react';
import { Button } from '@/app/(landing)/components/ui/button';
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from '@/app/(landing)/components/ui/empty';
export function NotFoundPage() {
useEffect(() => {
document.title = '404 — Página no encontrada | Rynext Technologies';
}, []);
return (
<main
id="main-content"
className="relative flex w-full items-center justify-center px-8 pb-24 pt-40"
>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-16 -left-20 h-[24rem] w-[24rem] rounded-full blur-[110px]"
style={{ background: 'rgba(59,130,246,0.16)' }}
/>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-16 -right-20 h-[24rem] w-[24rem] rounded-full blur-[110px]"
style={{ background: 'rgba(139,92,246,0.16)' }}
/>
<Empty className="relative border-none">
<EmptyHeader>
<EmptyTitle
className="font-display font-black"
style={{ fontSize: 'clamp(6rem, 20vw, 10rem)', lineHeight: 1, letterSpacing: '-0.04em', color: 'var(--foreground)' }}
>
404
</EmptyTitle>
<EmptyDescription style={{ color: 'var(--muted-foreground)' }}>
La página que buscas no existe o fue movida.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex gap-3">
<Button asChild>
<Link href="/">
<Icon icon="ph:house" className="mr-2 size-4" />
Ir al inicio
</Link>
</Button>
<Button asChild variant="outline">
<Link href="/portafolio">
<Icon icon="ph:compass" className="mr-2 size-4" />
Ver portafolio
</Link>
</Button>
</div>
</EmptyContent>
</Empty>
</main>
);
}
+192
View File
@@ -0,0 +1,192 @@
"use client";
import Link from "next/link";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import Loader from '@/app/(landing)/components/Loader';
import { useBlogPosts } from '@/app/(landing)/blog/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: "var(--background)",
paddingTop: "120px",
paddingBottom: "16px",
}}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-7xl text-center">
<motion.h1
className="text-2xl md:text-3xl font-bold"
style={{ color: "var(--foreground)" }}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={fadeUp}
>
Artículos
</motion.h1>
</div>
</section>
<section
style={{
background: "var(--background)",
padding: "0 0 100px",
minHeight: "60vh",
}}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-7xl">
{isLoading ? (
<Loader className="py-20" />
) : posts.length > 0 ? (
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{posts.map((art, i) => (
<motion.article
key={art.id}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
whileHover={{ y: -4 }}
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 var(--border)",
background: "var(--card)",
overflow: "hidden",
display: "flex",
flexDirection: "column",
opacity: art.slug ? 1 : 0.82,
}}
>
<Link
href={art.slug ? `/blog/${art.slug}` : "#"}
style={{
display: "flex",
flexDirection: "column",
flex: 1,
textDecoration: "none",
color: "inherit",
pointerEvents: art.slug ? "auto" : "none",
}}
>
<div
style={{
aspectRatio: "16/9",
overflow: "hidden",
background: "rgba(0,0,0,0.03)",
borderBottom: "1px solid var(--border)",
}}
>
{art.imageUrl ? (
<img
src={art.imageUrl}
alt={art.title}
loading="lazy"
decoding="async"
style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
onError={(e) => {
(e.currentTarget as HTMLImageElement).style.display = "none";
}}
/>
) : (
<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="var(--muted-foreground)" 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",
display: "flex",
flexDirection: "column",
flex: 1,
}}
>
{art.category && (
<span
className="text-xs font-semibold uppercase tracking-wider mb-2 block"
style={{ color: "#3b82f6" }}
>
{art.category}
</span>
)}
<h2
className="font-display font-bold mb-3"
style={{ fontSize: "1.1rem", color: "var(--foreground)", letterSpacing: "-0.01em" }}
>
{art.title}
</h2>
<p
className="text-sm leading-relaxed mb-5"
style={{ color: "var(--muted-foreground)", flex: 1 }}
>
{art.summary}
</p>
<p
className="text-xs mt-auto"
style={{ color: "var(--muted-foreground)" }}
>
{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: "var(--muted-foreground)" }}>
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,318 @@
"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 '@/app/(landing)/blog/services/blog';
import { assetUrl } from '@/app/(landing)/services/api';
import Loader from '@/app/(landing)/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: "var(--foreground)",
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: "var(--foreground)",
marginTop: "1.5rem",
marginBottom: "0.4rem",
}}
>
{line.slice(3)}
</h3>
);
if (line.startsWith("- "))
return (
<li
key={i}
style={{
color: "var(--muted-foreground)",
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: "var(--muted-foreground)",
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: "var(--muted-foreground)",
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: "var(--background)",
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 }}
>
{/* Volver */}
<Link
href="/blog"
style={{ display: "inline-flex", alignItems: "center", gap: 8, marginBottom: 20, fontFamily: "monospace", fontSize: 11, letterSpacing: "0.14em", color: "#3b82f6", textDecoration: "none" }}
>
← VOLVER AL BLOG
</Link>
{post.category && (
<div style={{ marginBottom: 20, fontFamily: "monospace", fontSize: 11, letterSpacing: "0.14em", color: "var(--muted-foreground)" }}>
{post.category.toUpperCase()}
</div>
)}
{/* Título */}
<h1
style={{
fontSize: "clamp(1.8rem, 4vw, 2.8rem)",
fontWeight: 700,
color: "var(--foreground)",
lineHeight: 1.2,
marginBottom: 20,
}}
>
{post.title}
</h1>
{/* Resumen */}
{post.summary && (
<p
style={{
fontSize: "1.1rem",
color: "var(--muted-foreground)",
lineHeight: 1.7,
marginBottom: 28,
borderLeft: "3px solid var(--border)",
paddingLeft: 16,
}}
>
{post.summary}
</p>
)}
{/* Meta */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 16,
paddingTop: 20,
borderTop: "1px solid var(--border)",
}}
>
{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: "var(--foreground)",
}}
>
{post.authorName}
</span>
</div>
)}
{publishDate && (
<>
{post.authorName && (
<span style={{ color: "var(--border)" }}>·</span>
)}
<span
style={{
fontFamily: "monospace",
fontSize: 11,
color: "var(--muted-foreground)",
letterSpacing: "0.1em",
}}
>
{publishDate.toUpperCase()}
</span>
</>
)}
</div>
</motion.div>
</div>
</section>
{/* Imagen de portada */}
{imageUrl && (
<div
style={{
background: "var(--muted)",
borderBottom: "1px solid var(--border)",
}}
>
<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}
fetchPriority="high"
decoding="async"
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</motion.div>
</div>
</div>
)}
{/* Contenido */}
<section
style={{ background: "var(--background)", 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: "var(--card)",
border: "1px solid var(--border)",
borderRadius: 4,
padding: "48px 52px",
fontSize: "1rem",
lineHeight: 1.85,
}}
>
{renderContent(post.content)}
</div>
</motion.div>
</section>
</main>
);
}
+72
View File
@@ -0,0 +1,72 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import ArticlePage from "./ArticlePage";
import { API_URL, assetUrl } from '@/app/(landing)/services/api';
import type { BlogPost } from '@/app/(landing)/blog/services/blog';
const BASE_URL = "https://rynext.com.mx";
/**
* null = la API respondió que el artículo no existe o no está publicado (404 real).
* undefined = no se pudo consultar la API (red, TLS local, etc.) — no es un 404,
* se deja que el cliente (ArticlePage) resuelva con su propio fetch.
*/
async function getPost(slug: string): Promise<BlogPost | null | undefined> {
try {
const res = await fetch(`${API_URL}/api/blog/${slug}`, {
next: { revalidate: 3600 },
});
if (!res.ok) return null;
const post = (await res.json()) as BlogPost;
return post.isPublished ? post : null;
} catch {
return undefined;
}
}
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) {
return {};
}
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);
// Solo 404 cuando confirmamos que no existe/no está publicado; si la API
// no respondió, dejamos que ArticlePage lo resuelva del lado del cliente.
if (post === null) notFound();
return <ArticlePage />;
}
+9
View File
@@ -0,0 +1,9 @@
import Loader from '@/app/(landing)/components/Loader';
export default function Loading() {
return (
<div className="flex min-h-dvh items-center justify-center" style={{ background: 'var(--background)' }}>
<Loader />
</div>
);
}
+12
View File
@@ -0,0 +1,12 @@
import type { Metadata } from 'next';
import { BlogPage } from './BlogPage';
export const metadata: Metadata = {
title: 'Blog | Rynext Technologies',
description: 'Artículos sobre arquitectura de software, sistemas empresariales y tecnología. Perspectivas del equipo de Rynext Technologies.',
alternates: { canonical: 'https://rynext.com.mx/blog' },
};
export default function Page() {
return <BlogPage />;
}
+32
View File
@@ -0,0 +1,32 @@
import { useQuery } from "@tanstack/react-query";
import { apiFetch } from '@/app/(landing)/services/api';
export interface BlogPost {
id: string;
title: string;
slug: string;
summary: string;
content: string;
category: string;
imageUrl?: string;
authorName?: string;
isPublished: boolean;
publishedAt?: string;
}
export function useBlogPosts() {
return useQuery<BlogPost[]>({
queryKey: ["blog"],
queryFn: () => apiFetch<BlogPost[]>("/api/blog"),
staleTime: 1000 * 60 * 5,
});
}
export function useBlogPost(slug: string) {
return useQuery<BlogPost>({
queryKey: ["blog", slug],
queryFn: () => apiFetch<BlogPost>(`/api/blog/${slug}`),
staleTime: 1000 * 60 * 5,
enabled: !!slug,
});
}
+12
View File
@@ -0,0 +1,12 @@
export interface BlogPost {
id: string;
title: string;
slug: string;
summary: string;
content: string;
category: string;
imageUrl?: string;
authorName?: string;
isPublished: boolean;
publishedAt?: string;
}
+37
View File
@@ -0,0 +1,37 @@
"use client";
import { usePathname } from "next/navigation";
import { ContactProvider } from "@/app/(landing)/contexts/ContactContext";
import { useIsDark } from "@/app/(landing)/hooks/useIsDark";
import { ScrollToTop } from "./ScrollToTop";
import { Navbar } from "./Navbar";
import { Footer } from "./Footer";
import { FloatingButtons } from "./FloatingButtons";
import { CookieBanner } from "./CookieBanner";
const BARE_ROUTES = ["/demo-hero", "/admin"];
export const AppShell = ({ children }: { children: React.ReactNode }) => {
const pathname = usePathname();
const isBare = BARE_ROUTES.some((route) => pathname?.startsWith(route));
const isDark = useIsDark();
if (isBare) {
return <ContactProvider>{children}</ContactProvider>;
}
return (
<ContactProvider>
<div
className="min-h-dvh font-sans"
style={{ background: "var(--background)", overflowX: "clip", overflowY: "visible" }}
>
<ScrollToTop />
<Navbar />
{children}
<Footer isNavyBg={isDark} />
<FloatingButtons isNavyBg={isDark} setIsNavyBg={() => {}} />
<CookieBanner isNavyBg={isDark} />
</div>
</ContactProvider>
);
};
+47
View File
@@ -0,0 +1,47 @@
'use client';
import { type ReactNode } from "react";
interface ButtonProps {
children: ReactNode;
onClick?: () => void;
href?: string;
variant?: "primary" | "secondary";
className?: string;
isNavyBg?: boolean;
}
export const Button = ({
children,
onClick,
href,
variant = "primary",
className = "",
isNavyBg = false,
}: ButtonProps) => {
const base =
"inline-flex items-center gap-3 px-8 py-4 text-base font-bold transition hover:scale-[1.02] cursor-pointer";
const styles = {
primary:
"bg-brand-red text-white shadow-lg shadow-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",
};
const cls = `${base} ${styles[variant]} ${className}`;
if (href) {
return (
<a href={href} className={cls}>
{children}
</a>
);
}
return (
<button onClick={onClick} className={cls}>
{children}
</button>
);
};
@@ -0,0 +1,224 @@
'use client';
import { useState } from 'react';
import { Icon } from '@iconify/react';
import type { ModalProps } from '@/app/(landing)/types/ui';
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 ? 'var(--card)' : '#ffffff';
const bdr = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.1)';
const text = isNavyBg ? 'var(--foreground)' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866';
const inputStyle: React.CSSProperties = {
width: '100%', borderRadius: 12, 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"
style={{ background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(6px)' }}
onClick={e => { if (e.target === e.currentTarget) handleClose(); }}
>
<div
className="relative w-full max-w-xl rounded-2xl shadow-2xl"
style={{ background: card, border: `1px solid ${bdr}` }}
>
{/* Header */}
<div className="flex items-center justify-between px-8 pt-8 pb-6" style={{ borderBottom: `1px solid ${bdr}` }}>
<div>
<h3 className="font-display font-bold text-xl" style={{ color: text, letterSpacing: '-0.02em' }}>
Hablemos de tu proyecto
</h3>
<p className="text-sm mt-0.5" style={{ color: sub }}>
Te respondemos en menos de 24 h
</p>
</div>
<button
onClick={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"
>
<Icon icon="ph:x" width={16} />
</button>
</div>
{/* Success state */}
{status === 'success' ? (
<div className="flex flex-col items-center justify-center gap-4 px-8 py-14 text-center">
<Icon icon="ph:check-circle" width={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={handleClose}
className="mt-2 px-6 py-2.5 text-sm font-bold text-white rounded-xl transition hover:opacity-90"
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer' }}
>
Cerrar
</button>
</div>
) : (
/* Form */
<form onSubmit={handleSubmit} className="px-8 py-7 flex flex-col gap-5">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>
Nombre *
</label>
<input
type="text"
placeholder="Tu nombre"
value={form.nombre}
onChange={set('nombre')}
required
style={inputStyle}
/>
</div>
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>
Empresa
</label>
<input
type="text"
placeholder="Nombre de tu empresa"
value={form.empresa}
onChange={set('empresa')}
style={inputStyle}
/>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>
Correo electrónico *
</label>
<input
type="email"
placeholder="[email protected]"
value={form.email}
onChange={set('email')}
required
style={inputStyle}
/>
</div>
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>
Teléfono
</label>
<input
type="tel"
placeholder="+52 998 000 0000"
value={form.telefono}
onChange={set('telefono')}
style={inputStyle}
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold mb-1.5" style={{ color: sub, letterSpacing: '0.04em' }}>
Cuéntanos tu proyecto
</label>
<textarea
rows={4}
placeholder="¿Qué sistema necesitas? ¿Cuál es el problema que quieres resolver?"
value={form.mensaje}
onChange={set('mensaje')}
style={{ ...inputStyle, resize: 'none' }}
/>
</div>
{status === 'error' && (
<div className="flex items-center gap-2 px-4 py-3 rounded-lg" style={{ background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.2)' }}>
<Icon icon="ph:warning-circle" width={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' ? (
<>
<Icon icon="ph:spinner-gap" width={16} className="animate-spin" />
Enviando...
</>
) : (
<>
<Icon icon="ph:paper-plane-tilt" width={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,7 +1,8 @@
'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import type { ThemeProps } from '../types'; import type { ThemeProps } from '@/app/(landing)/types/ui';
export const CookieBanner = ({ isNavyBg }: ThemeProps) => { export const CookieBanner = (_props: ThemeProps) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
useEffect(() => { useEffect(() => {
@@ -18,34 +19,31 @@ export const CookieBanner = ({ isNavyBg }: ThemeProps) => {
if (!visible) return null; if (!visible) return null;
const bg = isNavyBg ? 'rgba(0,16,64,0.97)' : 'rgba(10,10,10,0.97)';
const border = isNavyBg ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.08)';
return ( return (
<div <div
role="dialog" role="dialog"
aria-label="Aviso de cookies" aria-label="Aviso de cookies"
className="fixed bottom-6 left-1/2 z-50 w-[calc(100%-3rem)] max-w-xl -translate-x-1/2 px-6 py-4 shadow-2xl" className="fixed-blur fixed bottom-6 left-1/2 z-50 w-[calc(100%-3rem)] max-w-xl -translate-x-1/2 px-6 py-4 shadow-2xl"
style={{ style={{
background: bg, background: 'color-mix(in srgb, var(--card) 97%, transparent)',
border: `1px solid ${border}`, border: '1px solid var(--border)',
borderRadius: 6, borderRadius: 16,
backdropFilter: 'blur(16px)', backdropFilter: 'blur(16px)',
WebkitBackdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
}} }}
> >
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.6)', maxWidth: '42ch' }}> <p className="text-sm leading-relaxed" style={{ color: 'var(--muted-foreground)', maxWidth: '42ch' }}>
Usamos cookies para mejorar tu experiencia en el sitio. Usamos cookies para mejorar tu experiencia en el sitio.
Al continuar, aceptas su uso.{' '} Al continuar, aceptas su uso.{' '}
<a href="/privacidad" className="underline transition hover:text-white" style={{ color: 'rgba(255,255,255,0.45)' }}> <a href="/privacidad" className="underline transition hover:opacity-80" style={{ color: 'var(--foreground)' }}>
Política de privacidad Política de privacidad
</a> </a>
</p> </p>
<button <button
onClick={accept} onClick={accept}
className="shrink-0 px-5 py-2 text-sm font-bold text-white transition hover:opacity-90" className="shrink-0 px-5 py-2 text-sm font-bold text-white transition hover:opacity-90"
style={{ background: '#e50914', borderRadius: 3, border: 'none', cursor: 'pointer' }} style={{ background: '#3b82f6', borderRadius: 999, border: 'none', cursor: 'pointer' }}
> >
Aceptar Aceptar
</button> </button>
@@ -1,6 +1,7 @@
import { ArrowUp } from 'lucide-react'; 'use client';
import { Icon } from '@iconify/react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import type { ThemeProps } from '../types'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { type Dispatch, type SetStateAction, useState, useEffect } from 'react'; import { type Dispatch, type SetStateAction, useState, useEffect } from 'react';
const WHATSAPP_NUMBER = '529984814889'; const WHATSAPP_NUMBER = '529984814889';
@@ -10,12 +11,12 @@ interface FloatingButtonsProps extends ThemeProps {
setIsNavyBg: Dispatch<SetStateAction<boolean>>; setIsNavyBg: Dispatch<SetStateAction<boolean>>;
} }
export const FloatingButtons = ({ isNavyBg, setIsNavyBg: _setIsNavyBg }: FloatingButtonsProps) => { export const FloatingButtons = ({ setIsNavyBg: _setIsNavyBg }: FloatingButtonsProps) => {
const [showTop, setShowTop] = useState(false); const [showTop, setShowTop] = useState(false);
const [showBubble, setShowBubble] = useState(false); const [showBubble, setShowBubble] = useState(false);
const toggleBg = isNavyBg ? 'rgba(0,31,91,0.95)' : 'rgba(255,255,255,0.95)'; const toggleBg = 'color-mix(in srgb, var(--card) 85%, transparent)';
const toggleBorder = isNavyBg ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.12)'; const toggleBorder = 'var(--border)';
const toggleColor = isNavyBg ? '#fbbf24' : '#001f5b'; const toggleColor = '#3b82f6';
useEffect(() => { useEffect(() => {
const onScroll = () => setShowTop(window.scrollY > 400); const onScroll = () => setShowTop(window.scrollY > 400);
@@ -31,7 +32,7 @@ export const FloatingButtons = ({ isNavyBg, setIsNavyBg: _setIsNavyBg }: Floatin
<button <button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
aria-label="Volver arriba" aria-label="Volver arriba"
className="fixed bottom-6 left-6 z-50 flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-transform hover:scale-110 active:scale-95" className="fixed-blur fixed bottom-6 left-6 z-50 flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-transform hover:scale-110 active:scale-95"
style={{ style={{
background: toggleBg, background: toggleBg,
border: `1px solid ${toggleBorder}`, border: `1px solid ${toggleBorder}`,
@@ -40,7 +41,7 @@ export const FloatingButtons = ({ isNavyBg, setIsNavyBg: _setIsNavyBg }: Floatin
color: toggleColor, color: toggleColor,
}} }}
> >
<ArrowUp size={16} /> <Icon icon="ph:arrow-up" width={16} />
</button> </button>
)} )}
+250
View File
@@ -0,0 +1,250 @@
"use client";
import { Icon } from "@iconify/react";
import { FaFacebookF, FaLinkedinIn, FaMapMarkerAlt } from "react-icons/fa";
import Link from "next/link";
import type { ThemeProps } from '@/app/(landing)/types/ui';
import { pageLinks } from "@/app/(landing)/home/data/navigationData";
import { FaRegMessage } from "react-icons/fa6";
import { MdEmail } from "react-icons/md";
export const Footer = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? "var(--background)" : "#f9f9f7";
const text = isNavyBg ? "#ffffff" : "#0a0a0a";
const sub = isNavyBg ? "rgba(255,255,255,0.45)" : "rgba(0,0,0,0.5)";
const faint = isNavyBg ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.35)";
const border = isNavyBg ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)";
const cardBg = isNavyBg ? "color-mix(in srgb, var(--card) 72%, transparent)" : "color-mix(in srgb, #ffffff 72%, transparent)";
const chipBg = isNavyBg ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.05)";
const chipBgHv = isNavyBg ? "rgba(255,255,255,0.14)" : "rgba(0,0,0,0.1)";
return (
<footer
style={{ background: bg, color: text }}
className="relative overflow-hidden px-4 py-12 transition-colors duration-500 sm:px-6 sm:py-16"
role="contentinfo"
>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-dot-grid text-border opacity-40"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute -bottom-24 -left-32 h-[24rem] w-[24rem] rounded-full blur-[110px]"
style={{ background: "rgba(59,130,246,0.12)" }}
/>
<div
className="relative mx-auto max-w-7xl rounded-[32px] px-8 pt-14 pb-2 sm:px-14 sm:pt-16"
style={{
background: cardBg,
border: `1px solid ${border}`,
backdropFilter: "blur(18px)",
WebkitBackdropFilter: "blur(18px)",
boxShadow: isNavyBg ? "0 20px 60px rgba(0,0,0,0.3)" : "0 20px 60px rgba(0,0,0,0.06)",
}}
>
{/* Main grid */}
<div className="grid gap-16 md:grid-cols-[1.8fr_1fr_1.2fr]">
{/* Brand */}
<div>
<div className="mb-6 flex items-center gap-2.5">
<img
src={isNavyBg ? "/icons/Logo_blanco.svg" : "/icons/Logo_negro.svg"}
alt="Logo"
loading="lazy"
decoding="async"
className="h-12 w-12 shrink-0 object-contain"
/>
<div className="flex flex-col leading-none">
<span
style={{
fontFamily: 'var(--font-sans-public), sans-serif',
fontWeight: 700,
fontSize: 21,
letterSpacing: "-0.01em",
color: text,
}}
>
Rynext
</span>
<span
style={{
fontFamily: 'var(--font-sans-public), sans-serif',
fontWeight: 500,
fontSize: 13,
letterSpacing: "0.04em",
color: sub,
}}
>
Technologies
</span>
</div>
</div>
<p
className="text-base leading-relaxed mb-8"
style={{ color: sub, maxWidth: "32ch" }}
>
Diseñamos y construimos sistemas empresariales a medida,
arquitectura real, sin atajos técnicos.
</p>
{/* Social */}
<div className="flex items-center gap-2.5">
<a
href="https://www.facebook.com/RynextTechnologies/"
target="_blank"
rel="noopener noreferrer"
aria-label="Rynext en Facebook"
className="flex h-11 w-11 items-center justify-center rounded-full transition-colors"
style={{ background: chipBg, color: sub }}
onMouseEnter={(e) => {
e.currentTarget.style.background = chipBgHv;
e.currentTarget.style.color = text;
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = chipBg;
e.currentTarget.style.color = sub;
}}
>
<FaFacebookF />
</a>
<a
href="https://linkedin.com/company/rynext"
target="_blank"
rel="noopener noreferrer"
aria-label="Rynext en LinkedIn"
className="flex h-11 w-11 items-center justify-center rounded-full transition-colors"
style={{ background: chipBg, color: sub }}
onMouseEnter={(e) => {
e.currentTarget.style.background = chipBgHv;
e.currentTarget.style.color = text;
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = chipBg;
e.currentTarget.style.color = sub;
}}
>
<FaLinkedinIn />
</a>
<a
href={`https://wa.me/529984814889?text=${encodeURIComponent("¿En qué podemos ayudarte?")}`}
target="_blank"
rel="noopener noreferrer"
aria-label="Rynext en WhatsApp"
className="flex h-11 w-11 items-center justify-center rounded-full transition-colors"
style={{ background: chipBg, color: sub }}
onMouseEnter={(e) => {
e.currentTarget.style.background = chipBgHv;
e.currentTarget.style.color = text;
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = chipBg;
e.currentTarget.style.color = sub;
}}
>
<FaRegMessage />
</a>
</div>
</div>
{/* Páginas */}
<div>
<p
className="text-mono-label mb-6"
style={{
color: faint,
letterSpacing: "0.12em",
}}
>
Páginas
</p>
<ul className="flex flex-col gap-3.5">
{pageLinks.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-sm font-medium transition-colors"
style={{ color: sub, textDecoration: "none" }}
onMouseEnter={(e) => (e.currentTarget.style.color = text)}
onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
{/* Contacto */}
<div>
<p
className="text-mono-label mb-6"
style={{
color: faint,
letterSpacing: "0.12em",
}}
>
Contacto
</p>
<ul className="flex flex-col gap-4">
<li className="flex items-center gap-3">
<FaMapMarkerAlt className="text-center" style={{ color: text }} />
<span className="text-sm leading-snug" style={{ color: sub }}>
Cancún, Quintana Roo. México
</span>
</li>
<li className="flex items-center gap-3">
<MdEmail className="text-center" style={{ color: text }} />
<a
href="mailto:[email protected]"
className="text-sm font-medium transition-colors"
style={{ color: sub }}
onMouseEnter={(e) => (e.currentTarget.style.color = text)}
onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
>
contacto@rynext.com.mx
</a>
</li>
<li className="mt-2">
<a
href="mailto:[email protected]"
className="group inline-flex items-center gap-2 px-6 py-3 text-base font-bold text-white transition hover:scale-[1.03] active:scale-[0.98]"
style={{ background: "#3b82f6", borderRadius: 999 }}
>
Hablemos de tu proyecto
<Icon icon="ph:arrow-up-right" width={14} className="transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
</li>
</ul>
</div>
</div>
{/* Bottom bar */}
<div className="mt-10 border-t" style={{ borderColor: border }}>
<div className="flex flex-col items-center gap-2 py-5 sm:flex-row sm:justify-between">
<p className="text-xs" style={{ color: faint }}>
<Link
href="/admin/login"
aria-label="Acceso interno"
style={{ color: "inherit", textDecoration: "none" }}
>
©
</Link>{" "}
{new Date().getFullYear()} Rynext Technologies — Todos los
derechos reservados.
</p>
<Link
href="/privacidad"
className="text-xs transition-colors"
style={{ color: faint, textDecoration: "none" }}
onMouseEnter={(e) => (e.currentTarget.style.color = sub)}
onMouseLeave={(e) => (e.currentTarget.style.color = faint)}
>
Aviso de Privacidad
</Link>
</div>
</div>
</div>
</footer>
);
};
+1
View File
@@ -0,0 +1 @@
export { default } from "@/app/components/Loader";
+221
View File
@@ -0,0 +1,221 @@
'use client';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Icon } from '@iconify/react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { pageLinks } from '@/app/(landing)/home/data/navigationData';
import { useAuthStore } from '@/app/admin/store/authStore';
import ThemeToggle from './ui/hero-01-utils/theme-toggle';
export const Navbar = () => {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const [hovered, setHovered] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const rawPathname = usePathname();
const pathname = rawPathname?.replace(/\/$/, '') || '/';
const links = pageLinks.filter(l => l.href !== '/privacidad');
useEffect(() => {
setMounted(true);
}, []);
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 > 40);
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => {
setMenuOpen(false);
}, [pathname]);
return (
<>
<motion.header
className="fixed inset-x-0 top-0 z-50 flex justify-center px-4 pt-4 sm:px-6"
initial={{ y: -40, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
role="banner"
>
<div
className="fixed-blur flex w-full max-w-7xl items-center justify-between gap-4 rounded-full px-4 py-2.5 transition-shadow duration-300"
style={{
background: 'color-mix(in srgb, var(--card) 72%, transparent)',
border: '1px solid var(--border)',
backdropFilter: 'blur(18px)',
WebkitBackdropFilter: 'blur(18px)',
boxShadow: scrolled
? '0 8px 30px rgba(0,0,0,0.12)'
: '0 1px 2px rgba(0,0,0,0.03)',
}}
>
{/* Logo */}
<Link href="/" aria-label="Rynext Technologies — inicio" className="flex shrink-0 items-center gap-2">
<img src="/icons/Logo_negro.svg" alt="Logo" className="h-8 w-8 shrink-0 object-contain dark:hidden" />
<img src="/icons/Logo_blanco.svg" alt="Logo" className="hidden h-8 w-8 shrink-0 object-contain dark:block" />
<span className="flex flex-col leading-none">
<span
style={{ fontFamily: 'var(--font-sans-public), sans-serif', fontWeight: 700, fontSize: 16, letterSpacing: '-0.01em', color: 'var(--foreground)' }}
>
Rynext
</span>
<span
style={{ fontFamily: 'var(--font-sans-public), sans-serif', fontWeight: 500, fontSize: 10, letterSpacing: '0.04em', color: 'var(--muted-foreground)' }}
>
Technologies
</span>
</span>
</Link>
{/* Desktop nav — pill with sliding highlight */}
<nav
className="relative hidden items-center gap-0.5 rounded-full p-1 md:flex"
style={{ background: 'var(--muted)' }}
aria-label="Navegación principal"
onMouseLeave={() => setHovered(null)}
>
{links.map((link) => {
const active = pathname === link.href;
const isHovered = hovered === link.href;
// Solo un link muestra la píldora a la vez: el hovered tiene prioridad
// sobre el activo, para que nunca existan dos motion.span con el mismo
// layoutId montados al mismo tiempo (eso causaba el lag al pasar el mouse).
const showPill = hovered ? isHovered : active;
return (
<Link
key={link.href}
href={link.href}
onMouseEnter={() => setHovered(link.href)}
className="relative z-10 rounded-full px-3.5 py-1.5 text-sm font-semibold no-underline transition-colors"
style={{ color: active || isHovered ? 'var(--foreground)' : 'var(--muted-foreground)' }}
>
{showPill && (
<motion.span
layoutId="nav-pill"
className="absolute inset-0 -z-10 rounded-full"
style={{ background: 'var(--background)', boxShadow: '0 1px 2px rgba(0,0,0,0.06)' }}
transition={{ type: 'spring', stiffness: 400, damping: 32 }}
/>
)}
{link.label}
</Link>
);
})}
</nav>
{/* Right cluster */}
<div className="flex shrink-0 items-center gap-2">
<div className="hidden md:block">
<ThemeToggle />
</div>
{mounted && isAuthenticated && (
<Link
href="/admin/dashboard"
className="hidden items-center gap-1.5 rounded-full px-4 py-2 text-sm font-bold no-underline transition hover:scale-[1.03] active:scale-[0.98] sm:inline-flex"
style={{ color: 'var(--foreground)', background: 'var(--muted)', border: '1px solid var(--border)' }}
>
<Icon icon="ph:squares-four" width={14} />
Dashboard
</Link>
)}
<button
onClick={scrollToContact}
className="group hidden items-center gap-1.5 rounded-full px-4 py-2 text-sm font-bold text-white transition hover:scale-[1.03] active:scale-[0.98] sm:inline-flex"
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer' }}
>
Hablemos
<Icon icon="ph:arrow-up-right" width={14} className="transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</button>
{/* Mobile toggle */}
<div className="flex items-center gap-1 md:hidden">
<ThemeToggle />
<button
onClick={() => setMenuOpen((v) => !v)}
className="flex h-9 w-9 items-center justify-center rounded-full"
style={{ color: 'var(--foreground)', background: 'var(--muted)', border: 'none', cursor: 'pointer' }}
aria-label={menuOpen ? 'Cerrar menú' : 'Abrir menú'}
aria-expanded={menuOpen}
>
{menuOpen ? <Icon icon="ph:x" width={18} /> : <Icon icon="ph:list" width={18} />}
</button>
</div>
</div>
</div>
</motion.header>
{/* Mobile menu — floating card */}
<AnimatePresence>
{menuOpen && (
<motion.div
initial={{ opacity: 0, y: -12, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -12, scale: 0.97 }}
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
className="fixed-blur fixed inset-x-4 top-[76px] z-40 rounded-3xl p-5 md:hidden"
style={{
background: 'color-mix(in srgb, var(--card) 96%, transparent)',
border: '1px solid var(--border)',
backdropFilter: 'blur(18px)',
WebkitBackdropFilter: 'blur(18px)',
boxShadow: '0 16px 40px rgba(0,0,0,0.18)',
}}
role="dialog"
aria-modal="true"
aria-label="Menú de navegación"
>
<nav className="flex flex-col gap-1">
{pageLinks.map((link) => {
const active = pathname === link.href;
return (
<Link
key={link.href}
href={link.href}
className="rounded-xl px-3 py-2.5 text-base font-semibold no-underline"
style={{
color: active ? 'var(--foreground)' : 'var(--muted-foreground)',
background: active ? 'var(--muted)' : 'transparent',
}}
>
{link.label}
</Link>
);
})}
</nav>
<div className="mt-4 flex flex-col gap-2">
{mounted && isAuthenticated && (
<Link
href="/admin/dashboard"
className="flex w-full items-center justify-center gap-1.5 rounded-full py-3 text-sm font-bold no-underline"
style={{ color: 'var(--foreground)', background: 'var(--muted)', border: '1px solid var(--border)' }}
>
<Icon icon="ph:squares-four" width={14} />
Dashboard
</Link>
)}
<button
onClick={scrollToContact}
className="flex w-full items-center justify-center gap-1.5 rounded-full py-3 text-sm font-bold text-white"
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer' }}
>
Hablemos
<Icon icon="ph:arrow-up-right" width={14} />
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
};
@@ -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,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/app/(landing)/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-full px-3",
lg: "h-11 rounded-full px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
},
)
Button.displayName = "Button"
export { Button, buttonVariants }
+104
View File
@@ -0,0 +1,104 @@
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/app/(landing)/lib/utils"
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className
)}
{...props}
/>
)
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center",
className
)}
{...props}
/>
)
}
const emptyMediaVariants = cva(
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
},
},
defaultVariants: {
variant: "default",
},
}
)
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
)
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
{...props}
/>
)
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className
)}
{...props}
/>
)
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
}
@@ -0,0 +1,60 @@
"use client"
import { motion } from "framer-motion"
import { useCompanies } from "@/app/(landing)/home/services/companies"
import { assetUrl } from "@/app/(landing)/services/api"
export default function BrandSlider() {
const { data } = useCompanies()
const companies = (data ?? []).filter((c) => c.showInLanding && c.logoUrl)
if (companies.length === 0) return null
// Con pocas marcas reales, un número inflado ("+1000") se nota falso —
// mostramos un mensaje honesto en ese caso y solo presumimos la cifra
// cuando el catálogo ya tiene un volumen que la respalde.
const trustLabel =
companies.length >= 10
? `Con la confianza de +${Math.floor(companies.length / 10) * 10} marcas grandes y pequeñas`
: "Empresas que ya confían en nosotros"
return (
<motion.div
className="relative z-10 w-full pt-16"
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.4 }}
transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1] }}
>
<div className="flex items-center gap-4 sm:gap-6">
<span className="h-px flex-1 bg-gradient-to-r from-transparent to-border" />
<p className="shrink-0 text-xs text-muted-foreground sm:text-sm">
{trustLabel}
</p>
<span className="h-px flex-1 bg-gradient-to-l from-transparent to-border" />
</div>
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-10 gap-y-6">
{companies.map((brand) => (
<div
key={brand.id}
title={brand.name}
className="flex items-center opacity-80 transition hover:opacity-100"
>
<img
src={assetUrl(brand.logoUrl)}
alt={brand.name}
loading="lazy"
decoding="async"
height={150}
className="h-[150px] w-auto max-w-[150px] object-contain"
onError={(e) => {
(e.currentTarget.closest("[title]") as HTMLElement | null)?.style.setProperty("display", "none")
}}
/>
</div>
))}
</div>
</motion.div>
)
}
@@ -0,0 +1,144 @@
"use client"
import {
motion,
MotionConfig,
type Variants,
} from "framer-motion"
import { Icon } from "@iconify/react"
import { Button } from "@/app/(landing)/components/ui/button"
import BrandSlider from "./brand-slider"
export interface AvatarList {
image: string
}
interface HeroSectionProps {
avatarList?: AvatarList[]
}
const container: Variants = {
hidden: {},
show: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.15,
},
},
}
const item: Variants = {
hidden: { opacity: 0, y: 22, filter: "blur(4px)" },
show: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { type: "spring", stiffness: 300, damping: 28, mass: 0.9 },
},
}
const wordContainer: Variants = {
hidden: {},
show: {
transition: { staggerChildren: 0.055, delayChildren: 0.3 },
},
}
const word: Variants = {
hidden: { opacity: 0, y: 16, filter: "blur(6px)" },
show: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { type: "spring", stiffness: 260, damping: 24 },
},
}
const HEADLINE_LINE_1 = ["Construimos", "experiencias", "que"]
const HEADLINE_LINE_2 = ["impulsan", "tu", "negocio"]
export default function HeroSection({}: HeroSectionProps) {
return (
<MotionConfig reducedMotion="user">
<section
className="relative isolate flex min-h-dvh flex-col justify-center px-4 pb-20 pt-32 sm:px-6 sm:pb-28 sm:pt-40 lg:px-8"
>
<div
aria-hidden="true"
className="animate-hero-blob-a pointer-events-none absolute -bottom-24 -left-32 -z-20 h-[24rem] w-[24rem] rounded-full bg-accent/[0.12] blur-[110px]"
/>
<div
aria-hidden="true"
className="animate-hero-blob-b pointer-events-none absolute -top-24 -right-32 -z-20 h-[24rem] w-[24rem] rounded-full bg-[#8b5cf6]/[0.12] blur-[110px]"
/>
<div
className="mx-auto flex max-w-4xl flex-col items-start text-left"
>
<motion.div variants={container} initial="hidden" animate="show">
<motion.span
variants={item}
className="inline-flex items-center gap-1.5 rounded-full border border-accent/20 bg-accent/10 px-3 py-1 text-xs font-semibold text-accent"
>
<Icon icon="ph:sparkle" className="h-3.5 w-3.5" />
Diseño &amp; desarrollo digital
</motion.span>
<motion.h1
variants={wordContainer}
className="text-display mt-6 max-w-2xl text-foreground"
>
<span className="block">
{HEADLINE_LINE_1.map((w, i) => (
<motion.span key={i} variants={word} className="inline-block">
{w}
{i < HEADLINE_LINE_1.length - 1 ? " " : ""}
</motion.span>
))}
</span>
<span className="block text-accent">
{HEADLINE_LINE_2.map((w, i) => (
<motion.span key={i} variants={word} className="inline-block">
{w}
{i < HEADLINE_LINE_2.length - 1 ? " " : ""}
</motion.span>
))}
</span>
</motion.h1>
<motion.p
variants={item}
className="mt-6 max-w-xl text-balance text-base text-muted-foreground sm:text-lg"
>
Diseño, desarrollo y estrategia digital para marcas que quieren
crecer. De la idea al producto en producción.
</motion.p>
<motion.div variants={item} className="mt-8 flex flex-col gap-3 sm:flex-row">
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
<Button
size="lg"
className="group shadow-lg shadow-blue-500/20 transition-colors hover:bg-[#2563eb]"
>
Empezar un proyecto
<Icon icon="ph:arrow-right" className="ml-2 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
</Button>
</motion.div>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
<Button
size="lg"
variant="outline"
className="border-primary/15 text-primary hover:border-primary hover:bg-transparent hover:text-primary"
>
Ver portafolio
</Button>
</motion.div>
</motion.div>
</motion.div>
<BrandSlider />
</div>
</section>
</MotionConfig>
)
}
@@ -0,0 +1,53 @@
"use client"
import * as React from "react"
import { Icon } from "@iconify/react"
import { Button } from "@/app/(landing)/components/ui/button"
export default function ThemeToggle() {
const [isDark, setIsDark] = React.useState(false)
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => {
setMounted(true)
const stored = localStorage.getItem("theme")
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
const dark = stored ? stored === "dark" : prefersDark
setIsDark(dark)
document.documentElement.classList.toggle("dark", dark)
}, [])
const toggle = () => {
const next = !isDark
setIsDark(next)
document.documentElement.classList.toggle("dark", next)
localStorage.setItem("theme", next ? "dark" : "light")
}
if (!mounted) {
return (
<Button
variant="ghost"
size="icon"
aria-label="Cambiar tema"
disabled
className="text-foreground hover:bg-foreground/10"
>
<Icon icon="ph:sun" className="h-5 w-5" />
</Button>
)
}
return (
<Button
variant="ghost"
size="icon"
onClick={toggle}
aria-label="Cambiar tema"
className="text-foreground hover:bg-foreground/10"
>
{isDark ? <Icon icon="ph:sun" className="h-5 w-5" /> : <Icon icon="ph:moon" className="h-5 w-5" />}
</Button>
)
}
+25
View File
@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/app/(landing)/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
+26
View File
@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/app/(landing)/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
@@ -0,0 +1,128 @@
import * as React from "react"
import { ChevronDownIcon } from "@radix-ui/react-icons"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { cn } from "@/app/(landing)/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}
@@ -0,0 +1,23 @@
'use client';
import { createContext, useContext, useState } from 'react';
import { ContactModal } from '@/app/(landing)/components/ContactModal';
import { useIsDark } from '@/app/(landing)/hooks/useIsDark';
interface ContactCtx {
open: () => void;
}
const ContactContext = createContext<ContactCtx>({ open: () => {} });
export const ContactProvider = ({ children }: { children: React.ReactNode }) => {
const [isOpen, setIsOpen] = useState(false);
const isDark = useIsDark();
return (
<ContactContext.Provider value={{ open: () => setIsOpen(true) }}>
{children}
<ContactModal isOpen={isOpen} onClose={() => setIsOpen(false)} isNavyBg={isDark} />
</ContactContext.Provider>
);
};
export const useContact = () => useContext(ContactContext);
+45
View File
@@ -0,0 +1,45 @@
"use client";
import { useIsDark } from "@/app/(landing)/hooks/useIsDark";
import HeroSection from "@/app/(landing)/components/ui/hero-01-utils/hero";
import type { AvatarList } from "@/app/(landing)/components/ui/hero-01-utils/hero";
import { Problem } from "@/app/(landing)/home/sections/Problem";
import { Machinery } from "@/app/(landing)/home/sections/Machinery";
import { Services } from "@/app/(landing)/home/sections/Services";
import { FAQ } from "@/app/(landing)/home/sections/FAQ";
import { Partners } from "@/app/(landing)/home/sections/Partners";
import { ContactSection } from "@/app/(landing)/home/sections/ContactSection";
const avatarList: AvatarList[] = [
{
image:
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=128&h=128&fit=crop&crop=faces",
},
{
image:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=128&h=128&fit=crop&crop=faces",
},
{
image:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=128&h=128&fit=crop&crop=faces",
},
{
image:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=128&h=128&fit=crop&crop=faces",
},
];
export function HomePage() {
const isDark = useIsDark();
return (
<main id="main-content" className="relative">
<HeroSection avatarList={avatarList} />
<Problem isNavyBg={isDark} />
<Machinery isNavyBg={isDark} />
<Services isNavyBg={isDark} />
<FAQ isNavyBg={isDark} />
<Partners isNavyBg={isDark} />
<ContactSection isNavyBg={isDark} />
</main>
);
}
@@ -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'],
},
];
+22
View File
@@ -0,0 +1,22 @@
export const faqs = [
{
q: '¿Cuánto tiempo tarda un proyecto típico?',
a: 'Un sistema funcional suele estar en producción entre 8 y 16 semanas, trabajando en sprints cortos con entregables medibles.',
},
{
q: '¿Trabajan con empresas fuera de Cancún o de México?',
a: 'Sí, trabajamos de forma remota con empresas en todo México y en otros países.',
},
{
q: '¿Qué pasa con el mantenimiento y soporte después de la entrega?',
a: 'Se entrega documentado y en producción, con acompañamiento post-lanzamiento 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 SAP, Odoo, sistemas legacy y APIs internas es parte de nuestro núcleo de trabajo.',
},
{
q: '¿Cómo es el primer paso para arrancar un proyecto?',
a: 'Agendamos una sesión técnica sin costo para analizar tu operación y presentar una ruta concreta de construcción.',
},
];
@@ -3,35 +3,40 @@ export const machineryData = [
id: 1, id: 1,
num: '01', num: '01',
title: 'OPERACIONES', title: 'OPERACIONES',
description: 'Plataformas internas, automatización de flujos y control de procesos críticos. Sistemas que eliminan la fricción operativa y dan visibilidad en tiempo real.', description: 'Plataformas internas y control de procesos críticos, sin fricción.',
tags: ['Portales de gestión', 'Automatización de flujos', 'Reportes operacionales', 'Control de acceso por rol'], tags: ['Portales de gestión', 'Automatización de flujos', 'Reportes operacionales', 'Control de acceso por rol'],
icon: "ph:squares-four",
}, },
{ {
id: 2, id: 2,
num: '02', num: '02',
title: 'VENTAS', title: 'VENTAS',
description: 'Sistemas de venta, cobro y facturación integrados al núcleo del negocio. Desde el punto de contacto con el cliente hasta el comprobante fiscal.', description: 'Del contacto con el cliente al comprobante fiscal, todo integrado.',
tags: ['E-commerce a medida', 'Integración de pagos', 'Facturación y CFDI', 'Recuperación de carritos'], tags: ['E-commerce a medida', 'Integración de pagos', 'Facturación y CFDI', 'Recuperación de carritos'],
icon: "ph:shopping-cart",
}, },
{ {
id: 3, id: 3,
num: '03', num: '03',
title: 'INTEGRACIONES', title: 'INTEGRACIONES',
description: 'Conectamos sistemas heterogéneos en un ecosistema coherente: ERPs, CRMs, proveedores y APIs de terceros. Sin silos, sin duplicidad de datos.', description: 'Conectamos sistemas heterogéneos en un ecosistema coherente.',
tags: ['ERP / SAP / Odoo', 'APIs de terceros', 'Sincronización bidireccional', 'Webhooks y eventos'], tags: ['ERP / SAP / Odoo', 'APIs de terceros', 'Sincronización bidireccional', 'Webhooks y eventos'],
icon: "ph:flow-arrow",
}, },
{ {
id: 4, id: 4,
num: '04', num: '04',
title: 'AUTOMATIZACIÓN', title: 'AUTOMATIZACIÓN',
description: 'Eliminamos intervención humana donde no aporta valor: aprobaciones, notificaciones, sincronizaciones y pipelines de datos que corren solos.', description: 'Tareas repetitivas corriendo solas, para que tu equipo se enfoque en lo que sí requiere criterio humano.',
tags: ['Bots y pipelines', 'Procesamiento de datos', 'Notificaciones inteligentes', 'Triggers y colas'], tags: ['Bots y pipelines', 'Procesamiento de datos', 'Notificaciones inteligentes', 'Triggers y colas'],
icon: "ph:lightning",
}, },
{ {
id: 5, id: 5,
num: '05', num: '05',
title: 'DATOS', title: 'DATOS',
description: 'Instrumentación del negocio para tomar decisiones con evidencia. Dashboards ejecutivos, modelos analíticos y alertas que activan antes de que el problema escale.', description: 'Decisiones con evidencia: dashboards, modelos y alertas a tiempo.',
tags: ['Dashboards ejecutivos', 'Pipelines de datos', 'KPIs e instrumentación', 'Alertas y umbrales'], tags: ['Dashboards ejecutivos', 'Pipelines de datos', 'KPIs e instrumentación', 'Alertas y umbrales'],
icon: "ph:chart-bar",
}, },
]; ];
@@ -0,0 +1,14 @@
export const pageLinks = [
{ label: 'Inicio', href: '/' },
{ label: 'Nosotros', href: '/sobre-nosotros' },
{ label: 'Portafolio', href: '/portafolio' },
{ label: 'Blog', href: '/blog' },
{ label: 'Aviso de Privacidad', href: '/privacidad' },
];
export const navLinks = [
{ label: 'Portafolio', href: '#portafolio' },
{ label: 'Sistemas', href: '#machinery' },
{ label: 'Proceso', href: '#proceso' },
{ label: 'Principios', href: '#principios' },
];
@@ -0,0 +1,40 @@
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: "ph:stack-simple",
},
{
title: 'Arquitectura robusta',
description: 'Infraestructura, seguridad y escalabilidad definida desde el inicio.',
icon: "ph:shield-check",
},
{
title: 'Equipo senior',
description: 'Product owner, ingeniería y operación trabajando con criterio empresarial.',
icon: "ph:buildings",
},
];
@@ -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.',
},
];
@@ -1,5 +1,4 @@
import { Code, Cloud, ShieldCheck, TrendingUp } from 'lucide-react'; import type { ServiceItem } from '@/app/(landing)/types/ui';
import type { ServiceItem } from '../types';
export const servicesData: ServiceItem[] = [ export const servicesData: ServiceItem[] = [
{ {
@@ -7,7 +6,7 @@ export const servicesData: ServiceItem[] = [
title: 'Desarrollo de software', title: 'Desarrollo de software',
description: 'Plataformas web y APIs construidas con arquitectura limpia, código mantenible y enfoque en rendimiento real.', description: 'Plataformas web y APIs construidas con arquitectura limpia, código mantenible y enfoque en rendimiento real.',
features: ['APIs REST & GraphQL', 'Microservicios y monolitos modulares', 'Code review y auditorías técnicas'], features: ['APIs REST & GraphQL', 'Microservicios y monolitos modulares', 'Code review y auditorías técnicas'],
icon: Code, icon: "ph:code",
gridCols: 'md:col-span-7', gridCols: 'md:col-span-7',
accent: 'blue', accent: 'blue',
}, },
@@ -16,7 +15,7 @@ export const servicesData: ServiceItem[] = [
title: 'Infraestructura en la nube', title: 'Infraestructura en la nube',
description: 'Diseño, migración y operación de infraestructura cloud escalable con alta disponibilidad.', description: 'Diseño, migración y operación de infraestructura cloud escalable con alta disponibilidad.',
features: ['AWS, GCP y Azure', 'CI/CD y DevOps', 'Monitoreo y alertas 24/7'], features: ['AWS, GCP y Azure', 'CI/CD y DevOps', 'Monitoreo y alertas 24/7'],
icon: Cloud, icon: "ph:cloud",
gridCols: 'md:col-span-5', gridCols: 'md:col-span-5',
accent: 'blue', accent: 'blue',
}, },
@@ -25,7 +24,7 @@ export const servicesData: ServiceItem[] = [
title: 'Ciberseguridad aplicada', title: 'Ciberseguridad aplicada',
description: 'Protección de datos, auditorías de seguridad y protocolos que cumplen estándares empresariales.', description: 'Protección de datos, auditorías de seguridad y protocolos que cumplen estándares empresariales.',
features: ['Pruebas de penetración', 'Cifrado end-to-end', 'Cumplimiento GDPR / ISO'], features: ['Pruebas de penetración', 'Cifrado end-to-end', 'Cumplimiento GDPR / ISO'],
icon: ShieldCheck, icon: "ph:shield-check",
gridCols: 'md:col-span-5', gridCols: 'md:col-span-5',
accent: 'red', accent: 'red',
}, },
@@ -34,7 +33,7 @@ export const servicesData: ServiceItem[] = [
title: 'Producto y crecimiento', title: 'Producto y crecimiento',
description: 'Estrategia tecnológica alineada al negocio para lanzar, medir y escalar con datos reales.', description: 'Estrategia tecnológica alineada al negocio para lanzar, medir y escalar con datos reales.',
features: ['Roadmap técnico y de producto', 'Analytics e instrumentación', 'Optimización de conversión'], features: ['Roadmap técnico y de producto', 'Analytics e instrumentación', 'Optimización de conversión'],
icon: TrendingUp, icon: "ph:trend-up",
gridCols: 'md:col-span-7', gridCols: 'md:col-span-7',
accent: 'red', accent: 'red',
}, },
@@ -0,0 +1,7 @@
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.' },
];
+22
View File
@@ -0,0 +1,22 @@
export const stackGroups = [
{
label: 'FRONTEND',
items: ['React', 'TypeScript', 'Next.js', 'Tailwind CSS', 'Vite'],
},
{
label: 'BACKEND',
items: ['Node.js', 'Python', 'Express', 'FastAPI', 'NestJS'],
},
{
label: 'DATOS',
items: ['PostgreSQL', 'MongoDB', 'Redis', 'Prisma', 'Elasticsearch'],
},
{
label: 'CLOUD & OPS',
items: ['AWS', 'Docker', 'GitHub Actions', 'Terraform', 'Linux'],
},
{
label: 'INTEGRACIONES',
items: ['REST', 'GraphQL', 'Stripe', 'OAuth 2.0', 'Webhooks'],
},
];
@@ -1,57 +1,11 @@
'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ThemeProps } from '../types'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { anatomyLayers as layers } from '@/app/(landing)/home/data/anatomyData';
const layers = [
{
id: 'presentation',
num: '01',
title: 'PRESENTACIÓN',
subtitle: 'La interfaz donde opera el usuario',
description:
'Portales de administración, apps web, paneles de monitoreo y puntos de venta. Construidos para adopción real, no para demos.',
tech: ['React', 'Next.js', 'TypeScript', 'Tailwind', 'Mobile Web'],
},
{
id: 'logic',
num: '02',
title: 'LÓGICA DE NEGOCIO',
subtitle: 'Las reglas que gobiernan la operación',
description:
'Validaciones, cálculos, flujos de aprobación y orquestación de procesos. El código que representa cómo funciona tu negocio.',
tech: ['Node.js', 'Python', 'REST APIs', 'GraphQL', 'Microservicios'],
},
{
id: 'data',
num: '03',
title: 'DATOS',
subtitle: 'Persistencia y acceso a la información',
description:
'Modelo de datos diseñado para tu operación. Bases relacionales, NoSQL, caché y búsqueda de texto. Una sola fuente de verdad.',
tech: ['PostgreSQL', 'MongoDB', 'Redis', 'Elasticsearch', 'Prisma'],
},
{
id: 'infra',
num: '04',
title: 'INFRAESTRUCTURA',
subtitle: 'La base que soporta el sistema',
description:
'Cloud, contenedores, pipelines de CI/CD y observabilidad. Diseñada para que el sistema esté siempre disponible y sea fácil de operar.',
tech: ['AWS', 'Docker', 'GitHub Actions', 'Terraform', 'Monitoring'],
},
{
id: 'integrations',
num: '05',
title: 'INTEGRACIONES',
subtitle: 'Conexión con el ecosistema externo',
description:
'ERPs, sistemas de pago, servicios de mensajería, proveedores y APIs externas. El sistema habla con todo lo que el negocio necesita.',
tech: ['Stripe', 'SAP / Odoo', 'OAuth 2.0', 'Webhooks', 'SMS / Email'],
},
];
export const Anatomy = ({ isNavyBg }: ThemeProps) => { export const Anatomy = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#0d1117' : '#f5f5f3'; const bg = isNavyBg ? 'var(--background)' : '#f5f5f3';
const text = isNavyBg ? '#eef0f4' : '#0a0a0a'; const text = isNavyBg ? 'var(--foreground)' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866'; const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866';
const border = isNavyBg ? 'rgba(255,255,255,0.08)' : '#dddbd5'; const border = isNavyBg ? 'rgba(255,255,255,0.08)' : '#dddbd5';
@@ -81,7 +35,7 @@ export const Anatomy = ({ isNavyBg }: ThemeProps) => {
</div> </div>
{/* Layers — always visible */} {/* Layers — always visible */}
<div style={{ border: `1px solid ${border}`, borderRadius: 4, overflow: 'hidden' }}> <div style={{ border: `1px solid ${border}`, borderRadius: 20, overflow: 'hidden' }}>
{layers.map((layer, i) => ( {layers.map((layer, i) => (
<motion.div <motion.div
key={layer.id} key={layer.id}
@@ -101,12 +55,12 @@ export const Anatomy = ({ isNavyBg }: ThemeProps) => {
{/* Number */} {/* Number */}
<span <span
className="font-display font-black" className="font-display font-black"
style={{ fontSize: 64, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }} style={{ fontSize: 64, lineHeight: 1, color: '#3b82f6', letterSpacing: '-0.04em' }}
> >
{layer.num} {layer.num}
</span> </span>
{/* Title + description */} {/* Title + description + tech */}
<div style={{ paddingTop: 8 }}> <div style={{ paddingTop: 8 }}>
<p <p
className="font-display font-black mb-1" className="font-display font-black mb-1"
@@ -117,9 +71,25 @@ export const Anatomy = ({ isNavyBg }: ThemeProps) => {
<p className="text-xs mb-3" style={{ color: sub }}> <p className="text-xs mb-3" style={{ color: sub }}>
{layer.subtitle} {layer.subtitle}
</p> </p>
<p className="text-sm leading-relaxed" style={{ color: sub, maxWidth: '60ch' }}> <p className="text-sm leading-relaxed mb-4" style={{ color: sub, maxWidth: '60ch' }}>
{layer.description} {layer.description}
</p> </p>
<div className="flex flex-wrap gap-2">
{layer.tech.map(t => (
<span key={t} style={{
fontSize: 11,
fontWeight: 600,
padding: '3px 10px',
borderRadius: 99,
border: `1px solid ${border}`,
color: isNavyBg ? 'rgba(255,255,255,0.55)' : 'rgba(0,0,0,0.5)',
background: isNavyBg ? 'rgba(255,255,255,0.04)' : 'rgba(59,130,246,0.05)',
letterSpacing: '0.01em',
}}>
{t}
</span>
))}
</div>
</div> </div>
</motion.div> </motion.div>
))} ))}
@@ -0,0 +1,93 @@
"use client";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import Loader from '@/app/(landing)/components/Loader';
import { useCompanies } from '@/app/(landing)/home/services/companies';
import { assetUrl } from '@/app/(landing)/services/api';
import type { ThemeProps } from '@/app/(landing)/types/ui';
function ClientsInner({ isNavyBg }: ThemeProps) {
const { data, isLoading } = useCompanies();
const bg = isNavyBg ? "var(--background)" : "#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}
loading="lazy"
decoding="async"
height={150}
style={{
height: 150,
width: "auto",
maxWidth: 200,
objectFit: "contain",
// grayscale solo: invert+brightness(0) aplanaba logos a color
// (fotos/JPEG) a un bloque negro o blanco ilegible en dark.
filter: "grayscale(1)",
opacity: 0.55,
transition: "opacity 250ms",
}}
onError={(e) => {
(e.currentTarget as HTMLImageElement).style.display = "none";
}}
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,192 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Icon } from '@iconify/react';
import type { ThemeProps } from '@/app/(landing)/types/ui';
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 text = isNavyBg ? 'var(--foreground)' : '#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: 12, 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"
className="relative px-8 py-20 md:px-14 transition-colors duration-500"
>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-dot-grid text-border opacity-40"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute -right-32 -top-24 h-[24rem] w-[24rem] rounded-full blur-[110px]"
style={{ background: 'rgba(139,92,246,0.12)' }}
/>
<div className="relative 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] }}
>
<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: 24, padding: '40px 36px' }}
>
{status === 'success' ? (
<div className="flex flex-col items-center justify-center gap-4 py-10 text-center">
<Icon icon="ph:check-circle" width={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)' }}>
<Icon icon="ph:warning-circle" width={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' ? (
<><Icon icon="ph:spinner-gap" width={16} className="animate-spin" />Enviando...</>
) : (
<><Icon icon="ph:paper-plane-tilt" width={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 { Icon } from "@iconify/react";
import { Button } from '@/app/(landing)/components/Button';
import type { ThemeProps } from "@/app/(landing)/types/ui";
export const CtaBanner = ({ isNavyBg }: ThemeProps) => {
const scrollToContact = () => {
const el = document.getElementById("contacto");
if (el) el.scrollIntoView({ behavior: "smooth" });
};
const bg = isNavyBg ? "var(--background)" : "#f9f9f7";
const text = isNavyBg ? "var(--foreground)" : "#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 <Icon icon="ph:arrow-right" width={16} />
</Button>
</div>
</motion.div>
</div>
</section>
);
};
+111
View File
@@ -0,0 +1,111 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Icon } from '@iconify/react';
import type { ThemeProps } from '@/app/(landing)/types/ui';
import { faqs } from '@/app/(landing)/home/data/faqData';
export const FAQ = ({ isNavyBg }: ThemeProps) => {
const [open, setOpen] = useState<number | null>(null);
const text = isNavyBg ? 'var(--foreground)' : '#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={{ color: text }}
className="relative px-8 py-20 md:px-14 transition-colors duration-500"
>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-dot-grid text-border opacity-40"
/>
<div className="relative 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: 20, 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,
}}
>
<Icon icon="ph:plus" width={14} />
</span>
</button>
<div
className="anatomy-body"
style={{ ...(isOpen ? { gridTemplateRows: '1fr' } : {}) }}
>
<div className="anatomy-body-inner">
<p
className="text-base leading-relaxed"
style={{ padding: '0 28px 24px', color: sub }}
>
{faq.a}
</p>
</div>
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
};
@@ -1,8 +1,10 @@
'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { ArrowRight } from 'lucide-react'; import { Icon } from '@iconify/react';
import type { HeroProps } from '../types'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { Button } from './Button'; import { Button } from '@/app/(landing)/components/Button';
import { SystemMap } from './SystemMap'; import { SystemMap } from './SystemMap';
import { useContact } from '@/app/(landing)/contexts/ContactContext';
const ease = [0.22, 1, 0.36, 1] as const; const ease = [0.22, 1, 0.36, 1] as const;
@@ -19,7 +21,8 @@ function RevealLine({ children, delay = 0 }: { children: React.ReactNode; delay?
); );
} }
export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => { export const Hero = ({ isNavyBg }: ThemeProps) => {
const { open: openContact } = useContact();
const bg = isNavyBg ? '#001f5b' : '#ffffff'; const bg = isNavyBg ? '#001f5b' : '#ffffff';
const text = isNavyBg ? '#ffffff' : '#0a0a0a'; const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.45)'; const sub = isNavyBg ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.45)';
@@ -31,20 +34,14 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
style={{ background: bg, color: text, borderBottom: isNavyBg ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.1)' }} style={{ background: bg, color: text, borderBottom: isNavyBg ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.1)' }}
aria-label="Rynext Technologies — Hero" aria-label="Rynext Technologies — Hero"
> >
{/* Subtle grid bg */}
<div <div
className="pointer-events-none absolute inset-0 bg-grid-lines" className="pointer-events-none absolute inset-0 bg-grid-lines"
style={{ color: gridLine }} style={{ color: gridLine }}
aria-hidden="true" aria-hidden="true"
/> />
{/* Main content */}
<div className="relative mx-auto grid w-full max-w-7xl flex-1 items-center gap-12 px-8 pb-24 pt-36 md:px-14 lg:grid-cols-[1fr_1fr] lg:gap-20"> <div className="relative mx-auto grid w-full max-w-7xl flex-1 items-center gap-12 px-8 pb-24 pt-36 md:px-14 lg:grid-cols-[1fr_1fr] lg:gap-20">
{/* Left column */}
<div> <div>
{/* Headline */}
<h1 className="text-display mb-8" style={{ color: text }}> <h1 className="text-display mb-8" style={{ color: text }}>
<RevealLine delay={0.08}>Construimos</RevealLine> <RevealLine delay={0.08}>Construimos</RevealLine>
<RevealLine delay={0.16}>el sistema</RevealLine> <RevealLine delay={0.16}>el sistema</RevealLine>
@@ -54,7 +51,6 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
<RevealLine delay={0.32}>negocio.</RevealLine> <RevealLine delay={0.32}>negocio.</RevealLine>
</h1> </h1>
{/* Description */}
<motion.p <motion.p
initial={{ opacity: 0, y: 14 }} initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@@ -67,33 +63,26 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
la operación real de tu negocio. la operación real de tu negocio.
</motion.p> </motion.p>
{/* CTAs */}
<motion.div <motion.div
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.65, ease }} transition={{ duration: 0.6, delay: 0.65, ease }}
className="flex flex-wrap items-center gap-6" className="flex flex-wrap items-center gap-6"
> >
<Button variant="primary" onClick={onOpenContact}> <Button className="rounded-md" variant="primary" onClick={openContact}>
Hablemos de tu proyecto <ArrowRight size={16} /> Hablemos de tu proyecto <Icon icon="ph:arrow-right" width={16} />
</Button> </Button>
<button <button
onClick={() => { onClick={() => document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' })}
document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' });
}}
className="group flex items-center gap-2 text-sm font-semibold transition" className="group flex items-center gap-2 text-sm font-semibold transition"
style={{ color: sub }} style={{ color: sub }}
> >
Explorar la maquinaria Explorar la maquinaria
<ArrowRight <Icon icon="ph:arrow-right" width={14} className="transition-transform group-hover:translate-x-0.5" />
size={14}
className="transition-transform group-hover:translate-x-0.5"
/>
</button> </button>
</motion.div> </motion.div>
</div> </div>
{/* Right column — System Map */}
<motion.div <motion.div
initial={{ opacity: 0, y: 24 }} initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@@ -103,7 +92,6 @@ export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
<SystemMap isNavyBg={isNavyBg} /> <SystemMap isNavyBg={isNavyBg} />
</motion.div> </motion.div>
</div> </div>
</section> </section>
); );
}; };
@@ -1,13 +1,14 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ThemeProps } from '../types'; import { Icon } from '@iconify/react';
import { machineryData } from '../data/machineryData'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { machineryData } from '@/app/(landing)/home/data/machineryData';
export const Machinery = ({ isNavyBg }: ThemeProps) => { export const Machinery = ({ isNavyBg }: ThemeProps) => {
const [hovered, setHovered] = useState<number | null>(null); const [hovered, setHovered] = useState<number | null>(null);
const bg = isNavyBg ? '#001f5b' : '#ffffff'; const text = isNavyBg ? 'var(--foreground)' : '#0a0a0a';
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866'; const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866';
const cardBg = isNavyBg ? 'rgba(255,255,255,0.06)' : '#fafaf9'; const cardBg = isNavyBg ? 'rgba(255,255,255,0.06)' : '#fafaf9';
const cardBdr = isNavyBg ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.09)'; const cardBdr = isNavyBg ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.09)';
@@ -17,11 +18,20 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
<section <section
id="machinery" id="machinery"
aria-labelledby="machinery-heading" aria-labelledby="machinery-heading"
style={{ background: bg, color: text, borderBottom: `1px solid ${cardBdr}` }} style={{ color: text }}
className="px-8 py-28 md:px-14 transition-colors duration-500" className="relative px-8 py-20 md:px-14 transition-colors duration-500"
> >
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-dot-grid text-border opacity-40"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute -bottom-24 -left-32 h-[24rem] w-[24rem] rounded-full blur-[110px]"
style={{ background: 'rgba(59,130,246,0.12)' }}
/>
<motion.div <motion.div
className="mx-auto max-w-7xl" className="relative mx-auto max-w-7xl"
initial={{ opacity: 0, y: 28 }} initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false, amount: 0.1 }} viewport={{ once: false, amount: 0.1 }}
@@ -29,7 +39,7 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
> >
{/* Header */} {/* Header */}
<div className="mb-16"> <div className="mb-10">
<h2 <h2
id="machinery-heading" id="machinery-heading"
className="text-headline" className="text-headline"
@@ -57,10 +67,10 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
aria-label={item.title} aria-label={item.title}
style={{ style={{
background: isHov background: isHov
? isNavyBg ? 'rgba(229,9,20,0.06)' : 'rgba(229,9,20,0.03)' ? isNavyBg ? 'rgba(59,130,246,0.06)' : 'rgba(59,130,246,0.03)'
: cardBg, : cardBg,
border: `1px solid ${isHov ? 'rgba(229,9,20,0.25)' : cardBdr}`, border: `1px solid ${isHov ? 'rgba(59,130,246,0.25)' : cardBdr}`,
borderRadius: 6, borderRadius: 20,
padding: '28px 28px 32px', padding: '28px 28px 32px',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
@@ -83,7 +93,7 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
fontSize: 96, fontSize: 96,
fontWeight: 900, fontWeight: 900,
lineHeight: 1, lineHeight: 1,
color: isHov ? 'rgba(229,9,20,0.07)' : numColor, color: isHov ? 'rgba(59,130,246,0.07)' : numColor,
transition: 'color 220ms', transition: 'color 220ms',
userSelect: 'none', userSelect: 'none',
pointerEvents: 'none', pointerEvents: 'none',
@@ -101,13 +111,34 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
left: 0, left: 0,
right: 0, right: 0,
height: 2, height: 2,
background: '#e50914', background: '#3b82f6',
opacity: isHov ? 1 : 0, opacity: isHov ? 1 : 0,
transition: 'opacity 220ms', transition: 'opacity 220ms',
borderRadius: '6px 6px 0 0', borderRadius: '6px 6px 0 0',
}} }}
/> />
{/* Icon */}
<div style={{ position: 'relative', marginBottom: 20 }}>
{item.icon ? (
<span style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 44,
height: 44,
borderRadius: 12,
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 icon={item.icon} width={20} />
</span>
) : null}
</div>
{/* Content */} {/* Content */}
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<h3 <h3
@@ -115,18 +146,36 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
style={{ style={{
fontSize: 'clamp(1.1rem, 2vw, 1.3rem)', fontSize: 'clamp(1.1rem, 2vw, 1.3rem)',
letterSpacing: '-0.02em', letterSpacing: '-0.02em',
color: isHov ? '#e50914' : text, color: isHov ? '#3b82f6' : text,
transition: 'color 220ms', transition: 'color 220ms',
}} }}
> >
{item.title} {item.title}
</h3> </h3>
<p <p
className="text-sm leading-relaxed" className="text-sm leading-relaxed mb-4"
style={{ color: sub, transition: 'color 220ms' }} style={{ color: sub, transition: 'color 220ms' }}
> >
{item.description} {item.description}
</p> </p>
<div className="flex flex-wrap gap-1.5">
{item.tags.map((tag) => (
<span
key={tag}
style={{
fontSize: 11,
fontWeight: 600,
padding: '3px 9px',
borderRadius: 99,
border: `1px solid ${cardBdr}`,
color: isNavyBg ? 'rgba(255,255,255,0.55)' : 'rgba(0,0,0,0.5)',
letterSpacing: '0.01em',
}}
>
{tag}
</span>
))}
</div>
</div> </div>
</motion.div> </motion.div>
); );
@@ -0,0 +1,93 @@
"use client";
import { motion } from "framer-motion";
import { useTechPartners } from "@/app/(landing)/home/services/techPartners";
import { assetUrl } from "@/app/(landing)/services/api";
import type { ThemeProps } from '@/app/(landing)/types/ui';
export const Partners = ({ isNavyBg }: ThemeProps) => {
const { data } = useTechPartners();
const label = isNavyBg ? "rgba(255,255,255,0.28)" : "rgba(0,0,0,0.28)";
const text = isNavyBg ? "var(--foreground)" : "#0a0a0a";
const partners = (data ?? [])
.filter((p) => p.isActive)
.sort((a, b) => a.displayOrder - b.displayOrder);
if (partners.length === 0) return null;
return (
<section
aria-label="Partners tecnológicos de Rynext"
className="relative px-8 py-14 md:px-14 transition-colors duration-500"
>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-dot-grid text-border opacity-40"
/>
<motion.div
className="relative 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-x-10 gap-y-6">
{partners.map((p, i) => {
const content = p.logoUrl ? (
<img
src={assetUrl(p.logoUrl)}
alt={p.name}
loading="lazy"
decoding="async"
height={150}
className="h-[150px] w-auto max-w-[150px] object-contain"
onError={(e) => {
(e.currentTarget as HTMLImageElement).style.display = "none";
}}
/>
) : (
<span
className="text-lg font-bold tracking-tight"
style={{ color: text }}
>
{p.name}
</span>
);
return (
<motion.div
key={p.id}
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.05 }}
className="flex items-center opacity-70 transition hover:opacity-100"
>
{p.websiteUrl ? (
<a
href={p.websiteUrl}
target="_blank"
rel="noopener noreferrer"
title={p.name}
>
{content}
</a>
) : (
<span title={p.name}>{content}</span>
)}
</motion.div>
);
})}
</div>
</motion.div>
</section>
);
};
@@ -1,67 +1,33 @@
'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Building2, Layers3, ShieldCheck, X, Check } from 'lucide-react'; import { Icon } from '@iconify/react';
import type { ThemeProps } from '../types'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { comparisonRows, proofCards } from '@/app/(landing)/home/data/platformAdvantageData';
const comparisonRows = [
{
criterion: 'Adaptación al negocio',
saas: 'Configuración limitada y procesos estándar.',
ry: 'Plataforma diseñada para tus reglas y operaciones.',
},
{
criterion: 'Propiedad y evolución',
saas: 'Depende del proveedor y su roadmap.',
ry: 'Roadmap compartido con autonomía técnica real.',
},
{
criterion: 'Integraciones',
saas: 'APIs restringidas y costos extra por conectividad.',
ry: 'Arquitectura abierta para sistemas, ERPs y herramientas.',
},
{
criterion: 'Soporte y continuidad',
saas: 'Respuesta general, soporte tipo ticket.',
ry: 'Equipo senior con acompañamiento directo.',
},
];
const proofCards = [
{
title: 'Diseño de experiencia',
description: 'UX orientada a adopción, claridad y flujo de trabajo real.',
icon: Layers3,
},
{
title: 'Arquitectura robusta',
description: 'Infraestructura, seguridad y escalabilidad definida desde el inicio.',
icon: ShieldCheck,
},
{
title: 'Equipo senior',
description: 'Product owner, ingeniería y operación trabajando con criterio empresarial.',
icon: Building2,
},
];
export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => { export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
return ( return (
<section <section
id="conveniencia" id="conveniencia"
className={`section-wave px-6 py-28 md:px-12 transition-colors duration-500 ${ className={`section-wave relative overflow-hidden px-6 py-28 md:px-12 transition-colors duration-500 ${
isNavyBg ? 'bg-[#001749] text-white' : 'bg-slate-50 text-brand-black' isNavyBg ? 'bg-[var(--background)] text-[var(--foreground)]' : 'bg-[#f9f9f7] text-brand-black'
}`} }`}
> >
<div className="mx-auto max-w-7xl"> <div
aria-hidden="true"
className="pointer-events-none absolute -right-32 bottom-0 h-[24rem] w-[24rem] rounded-full blur-[110px]"
style={{ background: 'rgba(59,130,246,0.10)' }}
/>
<div className="relative mx-auto max-w-7xl">
{/* Header */} {/* Header */}
<div className="mb-14 flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between"> <div className="mb-14 flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-3xl"> <div className="max-w-3xl">
<span className={`mb-3 block text-xs font-semibold uppercase tracking-[0.14em] ${ <span className={`mb-3 block text-xs font-semibold uppercase tracking-[0.14em] ${
isNavyBg ? 'text-sky-400' : 'text-brand-blue' isNavyBg ? 'text-accent' : 'text-brand-blue'
}`}> }`}>
Por qué Rynext Por qué Rynext
</span> </span>
<h2 className="text-section-title"> <h2 className="text-section-title">
Detrás de cada plataforma hay un equipo senior, una arquitectura clara y una ruta con sentido. Rentar una licencia es fácil. Ser dueño de tu sistema, no.
</h2> </h2>
</div> </div>
<p className={`max-w-sm text-base leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}> <p className={`max-w-sm text-base leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}>
@@ -84,7 +50,7 @@ export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
Licencia SaaS Licencia SaaS
</div> </div>
<div className={`border-t px-6 py-4 font-bold md:border-t-0 md:border-l ${ <div className={`border-t px-6 py-4 font-bold md:border-t-0 md:border-l ${
isNavyBg ? 'border-white/8 text-sky-400' : 'border-gray-100 text-brand-blue' isNavyBg ? 'border-white/8 text-accent' : 'border-gray-100 text-brand-blue'
}`}> }`}>
Rynext software Rynext software
</div> </div>
@@ -106,13 +72,13 @@ export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
<div className={`flex items-start gap-2.5 border-t px-6 py-5 md:border-t-0 md:border-l ${ <div className={`flex items-start gap-2.5 border-t px-6 py-5 md:border-t-0 md:border-l ${
isNavyBg ? 'border-white/5 text-slate-400' : 'border-gray-100 text-gray-500' isNavyBg ? 'border-white/5 text-slate-400' : 'border-gray-100 text-gray-500'
}`}> }`}>
<X size={13} className="mt-0.5 shrink-0 text-gray-400" /> <Icon icon="ph:x" width={13} className="mt-0.5 shrink-0 text-gray-400" />
{row.saas} {row.saas}
</div> </div>
<div className={`flex items-start gap-2.5 border-t px-6 py-5 md:border-t-0 md:border-l ${ <div className={`flex items-start gap-2.5 border-t px-6 py-5 md:border-t-0 md:border-l ${
isNavyBg ? 'border-white/5 text-slate-200' : 'border-gray-100 text-brand-navy' isNavyBg ? 'border-white/5 text-slate-200' : 'border-gray-100 text-brand-navy'
}`}> }`}>
<Check size={13} className={`mt-0.5 shrink-0 ${isNavyBg ? 'text-sky-400' : 'text-brand-blue'}`} /> <Icon icon="ph:check" width={13} className={`mt-0.5 shrink-0 ${isNavyBg ? "text-accent" : "text-brand-blue"}`} />
{row.ry} {row.ry}
</div> </div>
</div> </div>
@@ -122,7 +88,7 @@ export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
{/* Proof cards */} {/* Proof cards */}
<div className="mt-6 grid gap-4 lg:grid-cols-3"> <div className="mt-6 grid gap-4 lg:grid-cols-3">
{proofCards.map((card, index) => { {proofCards.map((card, index) => {
const Icon = card.icon; const cardIcon = card.icon;
return ( return (
<motion.article <motion.article
key={card.title} key={card.title}
@@ -137,9 +103,9 @@ export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
}`} }`}
> >
<div className={`mb-5 inline-flex rounded-xl p-3 ${ <div className={`mb-5 inline-flex rounded-xl p-3 ${
isNavyBg ? 'bg-brand-blue/15 text-sky-300' : 'bg-brand-blue/8 text-brand-blue' isNavyBg ? 'bg-brand-blue/15 text-accent' : 'bg-brand-blue/8 text-brand-blue'
}`}> }`}>
<Icon size={20} /> <Icon icon={cardIcon} width={20} />
</div> </div>
<h3 className="mb-2 font-display text-lg font-bold">{card.title}</h3> <h3 className="mb-2 font-display text-lg font-bold">{card.title}</h3>
<p className={`text-sm leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}> <p className={`text-sm leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}>
@@ -0,0 +1,214 @@
"use client";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import Loader from '@/app/(landing)/components/Loader';
import { usePortfolio } from '@/app/(landing)/portafolio/services/portfolio';
import { assetUrl } from '@/app/(landing)/services/api';
import type { ThemeProps } from '@/app/(landing)/types/ui';
function PortfolioInner({ isNavyBg }: ThemeProps) {
const { data, isLoading } = usePortfolio();
const bg = isNavyBg ? "var(--background)" : "#f9f9f7";
const text = isNavyBg ? "var(--foreground)" : "#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 }} 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 }}
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: 20,
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}
loading="lazy"
decoding="async"
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,32 +1,11 @@
'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ThemeProps } from '../types'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { principles } from '@/app/(landing)/home/data/principlesData';
const principles = [
{
title: 'Simple por diseño',
description: 'La complejidad existe solo cuando aporta valor real. Todo lo demás se elimina.',
},
{
title: 'Diseñado para fallar',
description: 'Los sistemas asumen que algo fallará. La resiliencia es diseñada, no improvisada.',
},
{
title: 'Una sola fuente de verdad',
description: 'La información crítica tiene una única fuente confiable. Sin duplicidad de datos.',
},
{
title: 'Automatiza lo repetitivo',
description: 'Si una persona lo hace manualmente diez veces, el sistema debe hacerlo.',
},
{
title: 'Construido para cambiar',
description: 'El software debe poder evolucionar. La arquitectura anticipa el cambio inevitable.',
},
];
export const Principles = ({ isNavyBg }: ThemeProps) => { export const Principles = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#0d1117' : '#f5f5f3'; const bg = isNavyBg ? 'var(--background)' : '#f5f5f3';
const text = isNavyBg ? '#eef0f4' : '#0a0a0a'; const text = isNavyBg ? 'var(--foreground)' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)'; const sub = isNavyBg ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)';
const gridGap = isNavyBg ? 'rgba(255,255,255,0.07)' : 'rgba(0,0,0,0.07)'; const gridGap = isNavyBg ? 'rgba(255,255,255,0.07)' : 'rgba(0,0,0,0.07)';
@@ -56,13 +35,12 @@ export const Principles = ({ isNavyBg }: ThemeProps) => {
</h2> </h2>
</div> </div>
{/* Principles — 5 columns */} {/* Principles — responsive grid */}
<div <div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5"
style={{ style={{
display: 'grid',
gridTemplateColumns: 'repeat(5, 1fr)',
border: `1px solid ${gridGap}`, border: `1px solid ${gridGap}`,
borderRadius: 4, borderRadius: 20,
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
@@ -75,13 +53,13 @@ export const Principles = ({ isNavyBg }: ThemeProps) => {
transition={{ duration: 0.4, delay: i * 0.06 }} transition={{ duration: 0.4, delay: i * 0.06 }}
style={{ style={{
padding: '32px 28px 36px', padding: '32px 28px 36px',
borderRight: i < principles.length - 1 ? `1px solid ${gridGap}` : 'none', borderTop: '3px solid #3b82f6',
borderTop: '3px solid #e50914', borderBottom: `1px solid ${gridGap}`,
}} }}
> >
<span <span
className="font-display font-black block mb-5" className="font-display font-black block mb-5"
style={{ fontSize: 56, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }} style={{ fontSize: 56, lineHeight: 1, color: '#3b82f6', letterSpacing: '-0.04em' }}
> >
{String(i + 1).padStart(2, '0')} {String(i + 1).padStart(2, '0')}
</span> </span>
+106
View File
@@ -0,0 +1,106 @@
'use client';
import { motion } from 'framer-motion';
import { Icon } from '@iconify/react';
import type { ThemeProps } from '@/app/(landing)/types/ui';
const symptoms = [
{ icon: 'ph:lock', label: 'Software que impone sus reglas', detail: 'en vez de adaptarse a cómo operas' },
{ icon: 'ph:shuffle', label: 'Procesos que siguen siendo manuales', detail: 'porque nadie los diseñó para escalar' },
{ icon: 'ph:eye-slash', label: 'Sin visibilidad real', detail: 'decisiones a ciegas sobre lo que pasa en el negocio' },
];
export const Problem = ({ isNavyBg }: ThemeProps) => {
const text = isNavyBg ? 'var(--foreground)' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.4)';
const border = isNavyBg ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)';
const cardBg = isNavyBg ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.8)';
const rowBdr = isNavyBg ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)';
return (
<section
aria-labelledby="problem-heading"
className="relative px-8 py-16 md:px-14 transition-colors duration-500"
>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-dot-grid text-border opacity-40"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute -right-32 -top-24 h-[24rem] w-[24rem] rounded-full blur-[110px]"
style={{ background: 'rgba(139,92,246,0.12)' }}
/>
<div className="relative mx-auto grid max-w-7xl gap-16 lg:grid-cols-[1.1fr_0.9fr] lg:items-center">
<motion.div
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false, amount: 0.15 }}
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
>
<h2
id="problem-heading"
className="text-display"
style={{ lineHeight: 1.0, letterSpacing: '-0.02em', color: text }}
>
{['La mayoría', 'de los problemas', 'operativos son'].map((line, i) => (
<motion.span
key={line}
className="block"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: i * 0.07, ease: [0.22, 1, 0.36, 1] }}
>
{line}
</motion.span>
))}
<motion.span
className="block text-brand-red"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.24, ease: [0.22, 1, 0.36, 1] }}
>
problemas de sistema.
</motion.span>
</h2>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 24, scale: 0.97 }}
whileInView={{ opacity: 1, y: 0, scale: 1 }}
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
className="relative rounded-[28px] p-2"
style={{ background: cardBg, border: `1px solid ${border}`, backdropFilter: 'blur(12px)' }}
>
<div className="flex flex-col gap-1">
{symptoms.map((s, i) => (
<motion.div
key={s.label}
initial={{ opacity: 0, x: 16 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.45, delay: 0.15 + i * 0.1, ease: [0.22, 1, 0.36, 1] }}
className="flex items-center gap-4 rounded-3xl px-5 py-5"
style={{ borderBottom: i < symptoms.length - 1 ? `1px solid ${rowBdr}` : 'none' }}
>
<span
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl"
style={{ background: 'rgba(59,130,246,0.12)', color: '#3b82f6' }}
>
<Icon icon={s.icon} width={19} />
</span>
<div className="min-w-0">
<p className="text-sm font-bold" style={{ color: text }}>{s.label}</p>
<p className="mt-0.5 text-xs leading-snug" style={{ color: sub }}>{s.detail}</p>
</div>
</motion.div>
))}
</div>
</motion.div>
</div>
</section>
);
};
@@ -1,37 +1,11 @@
'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ThemeProps } from '../types'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { processSteps as steps } from '@/app/(landing)/home/data/processData';
const steps = [
{
num: '01',
title: 'ENTENDER',
desc: 'Auditamos la operación, el stack y la fricción real antes de proponer nada. Sin suposiciones.',
},
{
num: '02',
title: 'MODELAR',
desc: 'Diseñamos la arquitectura del sistema: flujos, interfaces, datos y dependencias.',
},
{
num: '03',
title: 'CONSTRUIR',
desc: 'Desarrollamos en sprints con entregables medibles y revisión técnica constante.',
},
{
num: '04',
title: 'CONECTAR',
desc: 'Integramos el sistema con el ecosistema del negocio: ERPs, pagos, APIs externas.',
},
{
num: '05',
title: 'OPERAR',
desc: 'Despliegue a producción, monitoreo, soporte y evolución continua del sistema.',
},
];
export const Process = ({ isNavyBg }: ThemeProps) => { export const Process = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#001f5b' : '#ffffff'; const bg = isNavyBg ? 'var(--background)' : '#ffffff';
const text = isNavyBg ? '#ffffff' : '#0a0a0a'; const text = isNavyBg ? 'var(--foreground)' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.4)' : '#6a6866'; const sub = isNavyBg ? 'rgba(255,255,255,0.4)' : '#6a6866';
const border = isNavyBg ? 'rgba(255,255,255,0.1)' : '#dddbd5'; const border = isNavyBg ? 'rgba(255,255,255,0.1)' : '#dddbd5';
@@ -62,7 +36,7 @@ export const Process = ({ isNavyBg }: ThemeProps) => {
</div> </div>
{/* Steps */} {/* Steps */}
<div className="grid gap-0 md:grid-cols-3 lg:grid-cols-5" style={{ border: `1px solid ${border}`, borderRadius: 4, overflow: 'hidden' }}> <div className="grid gap-0 md:grid-cols-3 lg:grid-cols-5" style={{ border: `1px solid ${border}`, borderRadius: 20, overflow: 'hidden' }}>
{steps.map((step, i) => ( {steps.map((step, i) => (
<motion.div <motion.div
key={step.num} key={step.num}
@@ -77,7 +51,7 @@ export const Process = ({ isNavyBg }: ThemeProps) => {
> >
<span <span
className="font-display font-black block mb-6" className="font-display font-black block mb-6"
style={{ fontSize: 52, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }} style={{ fontSize: 52, lineHeight: 1, color: '#3b82f6', letterSpacing: '-0.04em' }}
> >
{step.num} {step.num}
</span> </span>
@@ -1,25 +1,25 @@
'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Check } from 'lucide-react'; import { Icon } from '@iconify/react';
import type { ThemeProps } from '../types'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { servicesData } from '../data/servicesData'; import { servicesData } from '@/app/(landing)/home/data/servicesData';
export const Services = ({ isNavyBg }: ThemeProps) => { export const Services = ({ isNavyBg }: ThemeProps) => {
return ( return (
<section <section
id="servicios" id="servicios"
className={`section-wave px-6 py-28 md:px-12 transition-colors duration-500 ${ className={`relative px-6 py-20 md:px-12 transition-colors duration-500 ${
isNavyBg ? 'bg-[#001749] text-white' : 'bg-slate-50 text-brand-black' isNavyBg ? 'text-[var(--foreground)]' : 'text-brand-black'
}`} }`}
> >
<div className="mx-auto max-w-7xl"> <div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-dot-grid text-border opacity-40"
/>
<div className="relative mx-auto max-w-7xl">
{/* Header */} {/* Header */}
<div className="mb-16 flex flex-col justify-between gap-8 md:flex-row md:items-end"> <div className="mb-10 flex flex-col justify-between gap-8 md:flex-row md:items-end">
<div> <div>
<span className={`mb-3 block text-xs font-semibold uppercase tracking-[0.14em] ${
isNavyBg ? 'text-sky-400' : 'text-brand-blue'
}`}>
Servicios
</span>
<h2 className="max-w-2xl text-section-title"> <h2 className="max-w-2xl text-section-title">
Soluciones que conectan producto, infraestructura y crecimiento. Soluciones que conectan producto, infraestructura y crecimiento.
</h2> </h2>
@@ -32,11 +32,11 @@ export const Services = ({ isNavyBg }: ThemeProps) => {
{/* Cards grid */} {/* Cards grid */}
<div className="grid gap-4 md:grid-cols-12"> <div className="grid gap-4 md:grid-cols-12">
{servicesData.map((service, index) => { {servicesData.map((service, index) => {
const Icon = service.icon; const serviceIcon = service.icon;
const accentClass = service.accent === 'blue' ? 'bg-brand-blue' : 'bg-brand-red'; const accentClass = service.accent === 'blue' ? 'bg-brand-blue' : 'bg-brand-red';
const iconBgClass = service.accent === 'blue' const iconBgClass = service.accent === 'blue'
? (isNavyBg ? 'bg-brand-blue/15 text-sky-300' : 'bg-brand-blue/10 text-brand-blue') ? (isNavyBg ? 'bg-brand-blue/15 text-accent' : 'bg-brand-blue/10 text-brand-blue')
: (isNavyBg ? 'bg-brand-red/15 text-red-300' : 'bg-brand-red/10 text-brand-red'); : (isNavyBg ? 'bg-brand-red/15 text-blue-300' : 'bg-brand-red/10 text-brand-red');
return ( return (
<motion.article <motion.article
@@ -57,7 +57,7 @@ export const Services = ({ isNavyBg }: ThemeProps) => {
{/* Header row */} {/* Header row */}
<div className="mb-6 flex items-start justify-between"> <div className="mb-6 flex items-start justify-between">
<div className={`rounded-xl p-3 ${iconBgClass}`}> <div className={`rounded-xl p-3 ${iconBgClass}`}>
<Icon size={22} /> <Icon icon={serviceIcon} width={22} />
</div> </div>
<span className={`font-mono text-xs font-bold ${isNavyBg ? 'text-white/20' : 'text-gray-200'}`}> <span className={`font-mono text-xs font-bold ${isNavyBg ? 'text-white/20' : 'text-gray-200'}`}>
0{service.id} 0{service.id}
@@ -73,11 +73,12 @@ export const Services = ({ isNavyBg }: ThemeProps) => {
<ul className="space-y-2"> <ul className="space-y-2">
{service.features.map((f: string) => ( {service.features.map((f: string) => (
<li key={f} className="flex items-center gap-2.5"> <li key={f} className="flex items-center gap-2.5">
<Check <Icon
size={13} icon="ph:check"
width={13}
className={`shrink-0 ${service.accent === 'blue' className={`shrink-0 ${service.accent === 'blue'
? (isNavyBg ? 'text-sky-400' : 'text-brand-blue') ? (isNavyBg ? 'text-accent' : 'text-brand-blue')
: (isNavyBg ? 'text-red-400' : 'text-brand-red') : (isNavyBg ? 'text-blue-400' : 'text-brand-red')
}`} }`}
/> />
<span className={`text-xs font-medium ${isNavyBg ? 'text-slate-300' : 'text-gray-600'}`}> <span className={`text-xs font-medium ${isNavyBg ? 'text-slate-300' : 'text-gray-600'}`}>
@@ -1,28 +1,7 @@
'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ThemeProps } from '../types'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { stackGroups } from '@/app/(landing)/home/data/stackData';
const stackGroups = [
{
label: 'FRONTEND',
items: ['React', 'TypeScript', 'Next.js', 'Tailwind CSS', 'Vite'],
},
{
label: 'BACKEND',
items: ['Node.js', 'Python', 'Express', 'FastAPI', 'NestJS'],
},
{
label: 'DATOS',
items: ['PostgreSQL', 'MongoDB', 'Redis', 'Prisma', 'Elasticsearch'],
},
{
label: 'CLOUD & OPS',
items: ['AWS', 'Docker', 'GitHub Actions', 'Terraform', 'Linux'],
},
{
label: 'INTEGRACIONES',
items: ['REST', 'GraphQL', 'Stripe', 'OAuth 2.0', 'Webhooks'],
},
];
export const Stack = ({ isNavyBg }: ThemeProps) => { export const Stack = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#0f1520' : '#f9f9f7'; const bg = isNavyBg ? '#0f1520' : '#f9f9f7';
@@ -45,7 +24,7 @@ export const Stack = ({ isNavyBg }: ThemeProps) => {
<div> <div>
<div className="mb-4 flex items-center gap-3"> <div className="mb-4 flex items-center gap-3">
<span className="h-px w-8 bg-brand-red" /> <span className="h-px w-8 bg-brand-red" />
<span className="text-mono-label" style={{ color: '#e50914' }}>Las herramientas son el medio</span> <span className="text-mono-label" style={{ color: '#3b82f6' }}>Las herramientas son el medio</span>
</div> </div>
<h2 <h2
id="stack-heading" id="stack-heading"
@@ -1,3 +1,4 @@
'use client';
import { useState } from 'react'; import { useState } from 'react';
interface Node { interface Node {
@@ -84,10 +85,10 @@ function tooltipHeight(desc: string): number {
export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) { export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
const [active, setActive] = useState<string | null>(null); const [active, setActive] = useState<string | null>(null);
const RED = '#e50914'; const RED = '#3b82f6';
const dimStroke = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)'; const dimStroke = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)';
const defStroke = isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,31,91,0.3)'; const defStroke = isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,31,91,0.3)';
const connStroke = 'rgba(229,9,20,0.45)'; const connStroke = 'rgba(59,130,246,0.45)';
const descText = isNavyBg ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.65)'; const descText = isNavyBg ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.65)';
const tooltipBg = isNavyBg ? '#0a1628' : '#ffffff'; const tooltipBg = isNavyBg ? '#0a1628' : '#ffffff';
const tooltipBorder = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,31,91,0.15)'; const tooltipBorder = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,31,91,0.15)';
@@ -130,7 +131,7 @@ export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
const isActive = active === node.id; const isActive = active === node.id;
const isConn = isConnected(node.id, active); const isConn = isConnected(node.id, active);
const isDim = active !== null && !isActive && !isConn; const isDim = active !== null && !isActive && !isConn;
const stroke = isActive ? RED : isConn ? 'rgba(229,9,20,0.5)' : dimStroke; const stroke = isActive ? RED : isConn ? 'rgba(59,130,246,0.5)' : dimStroke;
const fillOpacity = isActive ? 0.12 : 0; const fillOpacity = isActive ? 0.12 : 0;
const textColor = isActive ? RED : isDim const textColor = isActive ? RED : isDim
? (isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.2)') ? (isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.2)')
@@ -151,7 +152,7 @@ export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
<rect <rect
x={node.x} y={node.y} x={node.x} y={node.y}
width={node.w} height={node.h} width={node.w} height={node.h}
fill={`rgba(229,9,20,${fillOpacity})`} fill={`rgba(59,130,246,${fillOpacity})`}
stroke={stroke} stroke={stroke}
strokeWidth={isActive ? 1.5 : 1} strokeWidth={isActive ? 1.5 : 1}
rx={2} rx={2}
@@ -0,0 +1,121 @@
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Loader from '@/app/(landing)/components/Loader';
import { useTestimonials } from '@/app/(landing)/home/services/testimonials';
import type { ThemeProps } from '@/app/(landing)/types/ui';
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 ? "var(--background)" : "#f9f9f7";
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 ? "var(--foreground)" : "#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 }}
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: 20,
background: i === current ? "#3b82f6" : sub,
border: "none",
cursor: "pointer",
padding: 0,
transition: "width 300ms, background 300ms",
}}
aria-label={`Testimonio ${i + 1}`}
/>
))}
</div>
)}
</>
)}
</div>
</section>
);
};
@@ -0,0 +1,17 @@
import { useQuery } from "@tanstack/react-query";
import { apiFetch } from '@/app/(landing)/services/api';
export interface Company {
id: string;
name: string;
logoUrl?: string;
showInLanding?: boolean;
}
export function useCompanies() {
return useQuery<Company[]>({
queryKey: ["companies"],
queryFn: () => apiFetch<Company[]>("/api/companies"),
staleTime: 1000 * 60 * 5,
});
}
@@ -0,0 +1,20 @@
import { useQuery } from "@tanstack/react-query";
import { apiFetch } from '@/app/(landing)/services/api';
export interface TechPartner {
id: string;
name: string;
logoUrl?: string;
category?: string;
websiteUrl?: string;
displayOrder: number;
isActive: boolean;
}
export function useTechPartners() {
return useQuery<TechPartner[]>({
queryKey: ["tech-partners"],
queryFn: () => apiFetch<TechPartner[]>("/api/tech-partners"),
staleTime: 1000 * 60 * 5,
});
}
@@ -0,0 +1,33 @@
import { useQuery } from "@tanstack/react-query";
import { apiFetch } from '@/app/(landing)/services/api';
interface OnboardingCompleted {
clientCompanyName: string;
testimonialQuote?: string;
testimonialAuthor?: string;
}
export interface Testimonial {
quote: string;
author: string;
company: string;
}
export function useTestimonials() {
return useQuery<Testimonial[]>({
queryKey: ["testimonials"],
queryFn: async () => {
const data = await apiFetch<OnboardingCompleted[]>(
"/api/onboarding/completed",
);
return data
.filter((o) => o.testimonialQuote)
.map((o) => ({
quote: o.testimonialQuote!,
author: o.testimonialAuthor ?? "",
company: o.clientCompanyName,
}));
},
staleTime: 1000 * 60 * 5,
});
}
@@ -0,0 +1,6 @@
export interface Company {
id: string;
name: string;
logoUrl?: string;
showInLanding?: boolean;
}
@@ -0,0 +1,9 @@
export interface TechPartner {
id: string;
name: string;
logoUrl?: string;
category?: string;
websiteUrl?: string;
displayOrder: number;
isActive: boolean;
}
@@ -0,0 +1,5 @@
export interface Testimonial {
quote: string;
author: string;
company: string;
}
+20
View File
@@ -0,0 +1,20 @@
'use client';
import { useEffect, useState } from 'react';
export function useIsDark() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const root = document.documentElement;
setIsDark(root.classList.contains('dark'));
const observer = new MutationObserver(() => {
setIsDark(root.classList.contains('dark'));
});
observer.observe(root, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect();
}, []);
return isDark;
}
File renamed without changes.
+10
View File
@@ -0,0 +1,10 @@
import { Providers } from './providers';
import { AppShell } from './components/AppShell';
export default function LandingLayout({ children }: { children: React.ReactNode }) {
return (
<Providers>
<AppShell>{children}</AppShell>
</Providers>
);
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+9
View File
@@ -0,0 +1,9 @@
import Loader from '@/app/(landing)/components/Loader';
export default function Loading() {
return (
<div className="flex min-h-dvh items-center justify-center" style={{ background: 'var(--background)' }}>
<Loader />
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { NotFoundPage } from './NotFoundPage';
export default function NotFound() {
return <NotFoundPage />;
}
+45
View File
@@ -0,0 +1,45 @@
import type { Metadata } from 'next';
import { HomePage } from '@/app/(landing)/home/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,212 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { motion } from "framer-motion";
import Loader from '@/app/(landing)/components/Loader';
import { usePortfolio } from '@/app/(landing)/portafolio/services/portfolio';
import { assetUrl } from '@/app/(landing)/services/api';
const bg = "var(--background)";
const text = "var(--foreground)";
const sub = "var(--muted-foreground)";
const border = "var(--border)";
const card = "var(--card)";
const tagBg = "rgba(59,130,246,0.08)";
function PortafolioContent() {
const { data, isLoading } = usePortfolio();
const projects = data ?? [];
return (
<main id="main-content">
{/* Hero */}
<section
style={{ background: bg, paddingTop: 120, paddingBottom: 16 }}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-7xl text-center">
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
>
<h1
className="text-2xl md:text-3xl font-bold"
style={{ color: text }}
>
Casos de éxito
</h1>
</motion.div>
</div>
</section>
{/* Grid */}
<section style={{ background: bg }} className="px-8 pt-0 pb-20 md:px-14">
<div className="mx-auto max-w-7xl">
{isLoading ? (
<Loader className="py-20" />
) : 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 }}
whileHover={{ y: -4 }}
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",
}}
>
<Link
href={`/portafolio/${p.id}`}
style={{ display: "flex", flexDirection: "column", flex: 1, textDecoration: "none" }}
>
{/* 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}
loading="lazy"
decoding="async"
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>
</Link>
</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-dvh items-center justify-center"
style={{ background: bg }}
>
<Loader />
</div>
);
return <PortafolioContent />;
}
@@ -0,0 +1,223 @@
"use client";
import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import { motion } from "framer-motion";
import { usePortfolio } from "@/app/(landing)/portafolio/services/portfolio";
import { assetUrl } from "@/app/(landing)/services/api";
import Loader from "@/app/(landing)/components/Loader";
function siteHref(url: string) {
return /^https?:\/\//i.test(url) ? url : `https://${url}`;
}
function siteLabel(url: string) {
const host = url.replace(/^https?:\/\//i, "").replace(/\/$/, "");
return /^www\./i.test(host) ? host : `www.${host}`;
}
function renderRichText(content: string) {
return content.split("\n").map((line, i) => {
if (line.startsWith("# "))
return (
<h2 key={i} style={{ fontSize: "1.5rem", fontWeight: 700, color: "var(--foreground)", 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: "var(--foreground)", marginTop: "1.5rem", marginBottom: "0.4rem" }}>
{line.slice(3)}
</h3>
);
if (line.startsWith("- "))
return (
<li key={i} style={{ color: "var(--muted-foreground)", 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: "var(--muted-foreground)", lineHeight: 1.85, marginBottom: "0.25rem" }}>
{line}
</p>
);
});
}
export default function CaseStudyPage() {
const params = useParams();
const router = useRouter();
const id = typeof params.id === "string" ? params.id : (params.id?.[0] ?? "");
const { data: projects, isLoading, isError } = usePortfolio();
const project = projects?.find((p) => p.id === id);
const [activeImage, setActiveImage] = useState(0);
useEffect(() => {
if (project) document.title = `${project.title} | Portafolio`;
}, [project]);
if (isLoading) return <Loader className="py-40" />;
if (isError || !project) {
return (
<main style={{ minHeight: "80vh", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 16 }}>
<p style={{ color: "var(--muted-foreground)", fontSize: 14, fontFamily: "monospace", letterSpacing: "0.1em" }}>
CASO NO ENCONTRADO
</p>
<button
onClick={() => router.push("/portafolio")}
style={{ color: "#3b82f6", fontSize: 14, textDecoration: "underline", background: "none", border: "none", cursor: "pointer" }}
>
Volver al portafolio
</button>
</main>
);
}
const images = [...project.images].sort((a, b) => a.order - b.order);
const cover = images.find((img) => img.isCover) ?? images[0];
const gallery = images.length > 0 ? images : cover ? [cover] : [];
const activeUrl = assetUrl(gallery[activeImage]?.url ?? cover?.url);
return (
<main id="main-content">
{/* Hero */}
<section style={{ background: "var(--background)", paddingTop: "140px", paddingBottom: "60px" }} className="px-8 md:px-14">
<div className="mx-auto" style={{ maxWidth: "900px" }}>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}>
{/* Volver */}
<Link
href="/portafolio"
style={{ display: "inline-flex", alignItems: "center", gap: 8, marginBottom: 20, fontFamily: "monospace", fontSize: 11, letterSpacing: "0.14em", color: "#3b82f6", textDecoration: "none" }}
>
← VOLVER AL PORTAFOLIO
</Link>
{project.clientCompanyName && (
<div style={{ marginBottom: 20, fontFamily: "monospace", fontSize: 11, letterSpacing: "0.14em", color: "var(--muted-foreground)" }}>
{project.clientCompanyName.toUpperCase()}
</div>
)}
{/* Título */}
<h1 style={{ fontSize: "clamp(1.8rem, 4vw, 2.8rem)", fontWeight: 700, color: "var(--foreground)", lineHeight: 1.2, marginBottom: 20 }}>
{project.title}
</h1>
{/* Resumen */}
{project.publicSummary && (
<p style={{ fontSize: "1.1rem", color: "var(--muted-foreground)", lineHeight: 1.7, marginBottom: 28, borderLeft: "3px solid var(--border)", paddingLeft: 16 }}>
{project.publicSummary}
</p>
)}
{/* Meta: tecnologías + sitio */}
<div style={{ display: "flex", alignItems: "center", gap: 16, flexWrap: "wrap", paddingTop: 20, borderTop: "1px solid var(--border)" }}>
{project.technologies.length > 0 && (
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
{project.technologies.map((t) => (
<span
key={t.id}
style={{
fontSize: 10.5, fontWeight: 600, padding: "3px 9px", borderRadius: 99,
background: "rgba(59,130,246,0.08)", color: t.color ?? "var(--muted-foreground)", letterSpacing: "0.01em",
}}
>
{t.name}
</span>
))}
</div>
)}
{project.siteUrl && (
<a
href={siteHref(project.siteUrl)}
target="_blank"
rel="noopener noreferrer"
style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 13, color: "#3b82f6", textDecoration: "none", marginLeft: "auto" }}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<path d="M2 12h20M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20Z" />
</svg>
{siteLabel(project.siteUrl)}
</a>
)}
</div>
</motion.div>
</div>
</section>
{/* Galería */}
{activeUrl && (
<div style={{ background: "var(--muted)", borderBottom: "1px solid var(--border)" }}>
<div className="mx-auto" style={{ maxWidth: "1100px", padding: "0 2rem" }}>
<motion.div
key={activeUrl}
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4 }}
style={{ overflow: "hidden", borderRadius: 4, aspectRatio: "16/7", marginTop: -1 }}
>
<img src={activeUrl} alt={project.title} fetchPriority="high" decoding="async" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
</motion.div>
{gallery.length > 1 && (
<div style={{ display: "flex", gap: 10, padding: "16px 0 24px" }}>
{gallery.map((img, i) => (
<button
key={img.id}
onClick={() => setActiveImage(i)}
style={{
width: 72, height: 48, borderRadius: 4, overflow: "hidden", padding: 0, cursor: "pointer",
border: i === activeImage ? "2px solid #3b82f6" : "2px solid transparent",
opacity: i === activeImage ? 1 : 0.6,
}}
>
<img src={assetUrl(img.url)} alt="" loading="lazy" decoding="async" style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
</button>
))}
</div>
)}
</div>
</div>
)}
{/* Caso de estudio + solución */}
<section style={{ background: "var(--background)", 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 }}
>
{project.caseStudy && (
<div style={{ background: "var(--card)", border: "1px solid var(--border)", borderRadius: 4, padding: "48px 52px", fontSize: "1rem", lineHeight: 1.85, marginBottom: project.solution ? 32 : 0 }}>
<span style={{ fontFamily: "monospace", fontSize: 10, letterSpacing: "0.14em", color: "var(--muted-foreground)" }}>
CASO DE ESTUDIO
</span>
<div style={{ marginTop: 8 }}>{renderRichText(project.caseStudy)}</div>
</div>
)}
{project.solution && (
<div style={{ background: "var(--card)", border: "1px solid var(--border)", borderRadius: 4, padding: "48px 52px", fontSize: "1rem", lineHeight: 1.85 }}>
<span style={{ fontFamily: "monospace", fontSize: 10, letterSpacing: "0.14em", color: "var(--muted-foreground)" }}>
SOLUCIÓN
</span>
<div style={{ marginTop: 8 }}>{renderRichText(project.solution)}</div>
</div>
)}
{!project.caseStudy && !project.solution && (
<p style={{ color: "var(--muted-foreground)", fontSize: 14 }}>
Aún no hay detalle publicado para este caso.
</p>
)}
</motion.div>
</section>
</main>
);
}
@@ -0,0 +1,74 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import CaseStudyPage from "./CaseStudyPage";
import { API_URL, assetUrl } from "@/app/(landing)/services/api";
import type { PortfolioProject } from "@/app/(landing)/portafolio/types/portfolio";
const BASE_URL = "https://rynext.com.mx";
/**
* null = la API respondió pero el caso no existe/no está publicado (404 real).
* undefined = no se pudo consultar la API (red, TLS local, etc.) — no es un 404,
* se deja que el cliente (CaseStudyPage) resuelva con su propio fetch.
*/
async function getProject(id: string): Promise<PortfolioProject | null | undefined> {
try {
const res = await fetch(`${API_URL}/api/portfolio/public`, {
next: { revalidate: 3600 },
});
if (!res.ok) return undefined;
const projects = (await res.json()) as PortfolioProject[];
return projects.find((p) => p.id === id) ?? null;
} catch {
return undefined;
}
}
type Props = { params: Promise<{ id: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id } = await params;
const project = await getProject(id);
if (!project) {
return {};
}
const url = `${BASE_URL}/portafolio/${project.id}`;
const cover =
project.images.find((img) => img.isCover) ??
[...project.images].sort((a, b) => a.order - b.order)[0];
const image = assetUrl(cover?.url);
return {
title: `${project.title} | Portafolio | Rynext Technologies`,
description: project.publicSummary,
alternates: { canonical: url },
openGraph: {
title: project.title,
description: project.publicSummary,
type: "article",
url,
images: image
? [{ url: image, width: 1200, height: 630, alt: project.title }]
: undefined,
},
twitter: {
card: "summary_large_image",
title: project.title,
description: project.publicSummary,
images: image ? [image] : undefined,
},
};
}
export default async function Page({ params }: Props) {
const { id } = await params;
const project = await getProject(id);
// Solo 404 cuando confirmamos que no existe; si la API no respondió,
// dejamos que CaseStudyPage lo resuelva del lado del cliente.
if (project === null) notFound();
return <CaseStudyPage />;
}
Loaded 100 of 357 files, more files were not shown because too many files have changed in this diff. Show more