This commit is contained in:
Emmanuel Rojas committed 2026-10-06 02:00:11 -05:00
1 parent 794a659369
commit 5f99b65611
349 files changed
+32239 -2167

No files matched your search

+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
+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"
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
import "./.next/types/root-params.d.ts"; import "./.next/dev/types/root-params.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+9
View File
@@ -2,8 +2,17 @@ import type { NextConfig } from 'next';
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
reactStrictMode: true, reactStrictMode: true,
devIndicators: false,
output: 'standalone', output: 'standalone',
trailingSlash: true, trailingSlash: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
],
},
}; };
export default nextConfig; export default nextConfig;
+2000 -46
View File
File diff suppressed because it is too large. Load diff
+21 -1
View File
@@ -9,14 +9,34 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@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": "^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", "next": "^16.3.3",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"react-icons": "^5.7.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": {
"@tailwindcss/postcss": "^4.3.3", "@tailwindcss/postcss": "^4.3.3",
Binary file not shown.
+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,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
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

-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);
}
}
+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>
);
}
+190
View File
@@ -0,0 +1,190 @@
"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}
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,316 @@
"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}
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>
);
}
File renamed without changes.
+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,
});
}
File renamed without changes.
+39
View File
@@ -0,0 +1,39 @@
"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";
import { MusicPlayer } from "./MusicPlayer";
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={() => {}} />
<MusicPlayer />
<CookieBanner isNavyBg={isDark} />
</div>
</ContactProvider>
);
};
File renamed without changes.
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { X, Send, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'; import { Icon } from '@iconify/react';
import type { ModalProps } from '@/app/types/ui'; import type { ModalProps } from '@/app/(landing)/types/ui';
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'https://localhost:7001'; const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'https://localhost:7001';
@@ -51,12 +51,12 @@ export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
} }
}; };
const card = isNavyBg ? '#0d1b35' : '#ffffff'; const card = isNavyBg ? 'var(--card)' : '#ffffff';
const bdr = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.1)'; const bdr = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.1)';
const text = isNavyBg ? '#ffffff' : '#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 inputStyle: React.CSSProperties = { const inputStyle: React.CSSProperties = {
width: '100%', borderRadius: 8, padding: '11px 14px', width: '100%', borderRadius: 12, padding: '11px 14px',
fontSize: 14, fontFamily: 'inherit', outline: 'none', fontSize: 14, fontFamily: 'inherit', outline: 'none',
transition: 'border-color 150ms', transition: 'border-color 150ms',
...(isNavyBg ...(isNavyBg
@@ -91,14 +91,14 @@ export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
style={{ background: 'rgba(0,0,0,0.06)', color: sub, border: 'none', cursor: 'pointer' }} style={{ background: 'rgba(0,0,0,0.06)', color: sub, border: 'none', cursor: 'pointer' }}
aria-label="Cerrar" aria-label="Cerrar"
> >
<X size={16} /> <Icon icon="ph:x" width={16} />
</button> </button>
</div> </div>
{/* Success state */} {/* Success state */}
{status === 'success' ? ( {status === 'success' ? (
<div className="flex flex-col items-center justify-center gap-4 px-8 py-14 text-center"> <div className="flex flex-col items-center justify-center gap-4 px-8 py-14 text-center">
<CheckCircle size={48} style={{ color: '#22c55e' }} /> <Icon icon="ph:check-circle" width={48} style={{ color: '#22c55e' }} />
<h4 className="font-display font-bold text-lg" style={{ color: text }}> <h4 className="font-display font-bold text-lg" style={{ color: text }}>
¡Mensaje enviado! ¡Mensaje enviado!
</h4> </h4>
@@ -187,7 +187,7 @@ export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
{status === 'error' && ( {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)' }}> <div className="flex items-center gap-2 px-4 py-3 rounded-lg" style={{ background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.2)' }}>
<AlertCircle size={15} style={{ color: '#ef4444', flexShrink: 0 }} /> <Icon icon="ph:warning-circle" width={15} style={{ color: '#ef4444', flexShrink: 0 }} />
<p className="text-sm" style={{ color: '#ef4444' }}> <p className="text-sm" style={{ color: '#ef4444' }}>
Hubo un error al enviar. Intenta de nuevo o escríbenos a contacto@rynext.com.mx Hubo un error al enviar. Intenta de nuevo o escríbenos a contacto@rynext.com.mx
</p> </p>
@@ -202,12 +202,12 @@ export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
> >
{status === 'loading' ? ( {status === 'loading' ? (
<> <>
<Loader2 size={16} className="animate-spin" /> <Icon icon="ph:spinner-gap" width={16} className="animate-spin" />
Enviando... Enviando...
</> </>
) : ( ) : (
<> <>
<Send size={15} /> <Icon icon="ph:paper-plane-tilt" width={15} />
Enviar mensaje Enviar mensaje
</> </>
)} )}
@@ -1,8 +1,8 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import type { ThemeProps } from '@/app/types/ui'; 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(() => {
@@ -19,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 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: '#3b82f6', borderRadius: 3, border: 'none', cursor: 'pointer' }} style={{ background: '#3b82f6', borderRadius: 999, border: 'none', cursor: 'pointer' }}
> >
Aceptar Aceptar
</button> </button>
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { ArrowUp } from 'lucide-react'; import { Icon } from '@iconify/react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import type { ThemeProps } from '@/app/types/ui'; 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';
@@ -11,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);
@@ -32,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 bottom-24 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}`,
@@ -41,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>
)} )}
@@ -1,42 +1,66 @@
"use client"; "use client";
import { Mail, MapPin, MessageCircle } from "lucide-react"; import { Icon } from "@iconify/react";
import { FaFacebookF, FaLinkedinIn, FaMapMarkerAlt } from "react-icons/fa"; import { FaFacebookF, FaLinkedinIn, FaMapMarkerAlt } from "react-icons/fa";
import Link from "next/link"; import Link from "next/link";
import type { ThemeProps } from "@/app/types/ui"; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { pageLinks } from '@/app/home/data/navigationData'; import { pageLinks } from "@/app/(landing)/home/data/navigationData";
import { FaRegMessage } from "react-icons/fa6"; import { FaRegMessage } from "react-icons/fa6";
import { MdEmail } from "react-icons/md"; import { MdEmail } from "react-icons/md";
export const Footer = ({ isNavyBg }: ThemeProps) => { export const Footer = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? "#001040" : "#0a0a0a"; const bg = isNavyBg ? "var(--background)" : "#f9f9f7";
const sub = "rgba(255,255,255,0.45)"; const text = isNavyBg ? "#ffffff" : "#0a0a0a";
const border = "rgba(255,255,255,0.08)"; 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 ( return (
<footer <footer
style={{ background: bg, borderTop: `1px solid ${border}` }} style={{ background: bg, color: text }}
className="px-14 pt-10 transition-colors duration-500" className="relative overflow-hidden px-4 py-12 transition-colors duration-500 sm:px-6 sm:py-16"
role="contentinfo" role="contentinfo"
> >
<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
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 */} {/* Main grid */}
<div className="grid gap-14 md:grid-cols-[1.8fr_1fr_1.2fr]"> <div className="grid gap-16 md:grid-cols-[1.8fr_1fr_1.2fr]">
{/* Brand */} {/* Brand */}
<div> <div>
<div className="mb-6 flex items-center gap-2.5"> <div className="mb-6 flex items-center gap-2.5">
<img <img
src="/icons/Logo_blanco.svg" src={isNavyBg ? "/icons/Logo_blanco.svg" : "/icons/Logo_negro.svg"}
alt="Logo" alt="Logo"
className="h-11 w-11 shrink-0 object-contain" className="h-12 w-12 shrink-0 object-contain"
/> />
<div className="flex flex-col leading-none"> <div className="flex flex-col leading-none">
<span <span
style={{ style={{
fontFamily: "'Quicksand', sans-serif", fontFamily: "'Quicksand', sans-serif",
fontWeight: 700, fontWeight: 700,
fontSize: 19, fontSize: 21,
letterSpacing: "-0.01em", letterSpacing: "-0.01em",
color: "#ffffff", color: text,
}} }}
> >
Rynext Rynext
@@ -45,9 +69,9 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
style={{ style={{
fontFamily: "'Quicksand', sans-serif", fontFamily: "'Quicksand', sans-serif",
fontWeight: 500, fontWeight: 500,
fontSize: 12, fontSize: 13,
letterSpacing: "0.04em", letterSpacing: "0.04em",
color: "rgba(255,255,255,0.45)", color: sub,
}} }}
> >
Technologies Technologies
@@ -55,27 +79,27 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
</div> </div>
</div> </div>
<p <p
className="text-sm leading-relaxed mb-8" className="text-base leading-relaxed mb-8"
style={{ color: sub, maxWidth: "30ch" }} style={{ color: sub, maxWidth: "32ch" }}
> >
Diseñamos y construimos sistemas empresariales a medida, Diseñamos y construimos sistemas empresariales a medida,
arquitectura real, sin atajos técnicos. arquitectura real, sin atajos técnicos.
</p> </p>
{/* Social */} {/* Social */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-2.5">
<a <a
href="https://www.facebook.com/RynextTechnologies/" href="https://www.facebook.com/RynextTechnologies/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="Rynext en Facebook" aria-label="Rynext en Facebook"
className="flex h-9 w-9 items-center justify-center rounded transition-colors" className="flex h-11 w-11 items-center justify-center rounded-full transition-colors"
style={{ background: "rgba(255,255,255,0.07)", color: sub }} style={{ background: chipBg, color: sub }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.14)"; e.currentTarget.style.background = chipBgHv;
e.currentTarget.style.color = "#fff"; e.currentTarget.style.color = text;
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.07)"; e.currentTarget.style.background = chipBg;
e.currentTarget.style.color = sub; e.currentTarget.style.color = sub;
}} }}
> >
@@ -87,14 +111,14 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="Rynext en LinkedIn" aria-label="Rynext en LinkedIn"
className="flex h-9 w-9 items-center justify-center rounded transition-colors" className="flex h-11 w-11 items-center justify-center rounded-full transition-colors"
style={{ background: "rgba(255,255,255,0.07)", color: sub }} style={{ background: chipBg, color: sub }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.14)"; e.currentTarget.style.background = chipBgHv;
e.currentTarget.style.color = "#fff"; e.currentTarget.style.color = text;
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.07)"; e.currentTarget.style.background = chipBg;
e.currentTarget.style.color = sub; e.currentTarget.style.color = sub;
}} }}
> >
@@ -105,14 +129,14 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="Rynext en WhatsApp" aria-label="Rynext en WhatsApp"
className="flex h-9 w-9 items-center justify-center rounded transition-colors" className="flex h-11 w-11 items-center justify-center rounded-full transition-colors"
style={{ background: "rgba(255,255,255,0.07)", color: sub }} style={{ background: chipBg, color: sub }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.14)"; e.currentTarget.style.background = chipBgHv;
e.currentTarget.style.color = "#fff"; e.currentTarget.style.color = text;
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.07)"; e.currentTarget.style.background = chipBg;
e.currentTarget.style.color = sub; e.currentTarget.style.color = sub;
}} }}
> >
@@ -126,7 +150,7 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
<p <p
className="text-mono-label mb-6" className="text-mono-label mb-6"
style={{ style={{
color: "rgba(255,255,255,0.25)", color: faint,
letterSpacing: "0.12em", letterSpacing: "0.12em",
}} }}
> >
@@ -139,7 +163,7 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
href={link.href} href={link.href}
className="text-sm font-medium transition-colors" className="text-sm font-medium transition-colors"
style={{ color: sub, textDecoration: "none" }} style={{ color: sub, textDecoration: "none" }}
onMouseEnter={(e) => (e.currentTarget.style.color = "#fff")} onMouseEnter={(e) => (e.currentTarget.style.color = text)}
onMouseLeave={(e) => (e.currentTarget.style.color = sub)} onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
> >
{link.label} {link.label}
@@ -154,7 +178,7 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
<p <p
className="text-mono-label mb-6" className="text-mono-label mb-6"
style={{ style={{
color: "rgba(255,255,255,0.25)", color: faint,
letterSpacing: "0.12em", letterSpacing: "0.12em",
}} }}
> >
@@ -162,18 +186,18 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
</p> </p>
<ul className="flex flex-col gap-4"> <ul className="flex flex-col gap-4">
<li className="flex items-center gap-3"> <li className="flex items-center gap-3">
<FaMapMarkerAlt className="text-white text-center" /> <FaMapMarkerAlt className="text-center" style={{ color: text }} />
<span className="text-sm leading-snug" style={{ color: sub }}> <span className="text-sm leading-snug" style={{ color: sub }}>
Cancún, Quintana Roo. México Cancún, Quintana Roo. México
</span> </span>
</li> </li>
<li className="flex items-center gap-3"> <li className="flex items-center gap-3">
<MdEmail className="text-white text-center" /> <MdEmail className="text-center" style={{ color: text }} />
<a <a
href="mailto:[email protected]" href="mailto:[email protected]"
className="text-sm font-medium transition-colors" className="text-sm font-medium transition-colors"
style={{ color: sub }} style={{ color: sub }}
onMouseEnter={(e) => (e.currentTarget.style.color = "#fff")} onMouseEnter={(e) => (e.currentTarget.style.color = text)}
onMouseLeave={(e) => (e.currentTarget.style.color = sub)} onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
> >
contacto@rynext.com.mx contacto@rynext.com.mx
@@ -182,42 +206,43 @@ export const Footer = ({ isNavyBg }: ThemeProps) => {
<li className="mt-2"> <li className="mt-2">
<a <a
href="mailto:[email protected]" href="mailto:[email protected]"
className="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-bold text-white transition hover:opacity-90" className="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: 3 }} style={{ background: "#3b82f6", borderRadius: 999 }}
> >
Hablemos de tu proyecto 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> </a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</div>
{/* Bottom bar — full width */} {/* Bottom bar */}
<div <div className="mt-10 border-t" style={{ borderColor: border }}>
className="mt-10 -mx-8 px-8 md:-mx-14 md:px-14" <div className="flex flex-col items-center gap-2 py-5 sm:flex-row sm:justify-between">
style={{ borderTop: `1px solid ${border}` }} <p className="text-xs" style={{ color: faint }}>
<Link
href="/admin/login"
aria-label="Acceso interno"
style={{ color: "inherit", textDecoration: "none" }}
> >
<div className="mx-auto flex max-w-7xl flex-col items-center gap-2 py-5 sm:flex-row sm:justify-between"> ©
<p className="text-xs" style={{ color: "rgba(255,255,255,0.25)" }}> </Link>{" "}
© {new Date().getFullYear()} Rynext Technologies — Todos los {new Date().getFullYear()} Rynext Technologies — Todos los
derechos reservados. derechos reservados.
</p> </p>
<Link <Link
href="/privacidad" href="/privacidad"
className="text-xs transition-colors" className="text-xs transition-colors"
style={{ color: "rgba(255,255,255,0.25)", textDecoration: "none" }} style={{ color: faint, textDecoration: "none" }}
onMouseEnter={(e) => onMouseEnter={(e) => (e.currentTarget.style.color = sub)}
(e.currentTarget.style.color = "rgba(255,255,255,0.6)") onMouseLeave={(e) => (e.currentTarget.style.color = faint)}
}
onMouseLeave={(e) =>
(e.currentTarget.style.color = "rgba(255,255,255,0.25)")
}
> >
Aviso de Privacidad Aviso de Privacidad
</Link> </Link>
</div> </div>
</div> </div>
</div>
</footer> </footer>
); );
}; };
+1
View File
@@ -0,0 +1 @@
export { default } from "@/app/components/Loader";
@@ -0,0 +1,93 @@
"use client";
import { Icon } from "@iconify/react";
import { useEffect, useRef, useState } from "react";
const TRACK_SRC = "/audio/musica.mp3";
const PREF_KEY = "rynext_music_enabled";
export const MusicPlayer = ({
className = "bottom-6 left-6",
inline = false,
}: {
className?: string;
inline?: boolean;
}) => {
const audioRef = useRef<HTMLAudioElement>(null);
const [playing, setPlaying] = useState(false);
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
audio.volume = 0.05;
// Respeta la última preferencia del usuario (guardada en localStorage):
// si la silenció, no la reinicia sola al recargar la página.
let muted = false;
try {
muted = localStorage.getItem(PREF_KEY) === "off";
} catch {}
if (muted) return;
// Si el navegador bloquea el autoplay, no la arrancamos con cualquier
// interacción (scroll, tecla, etc.) — solo el botón de reproducir debe
// encenderla, para que no "se mueva sola" al navegar la página.
audio.play().then(() => setPlaying(true)).catch(() => {});
}, []);
function toggle() {
const audio = audioRef.current;
if (!audio) return;
if (playing) {
audio.pause();
setPlaying(false);
try { localStorage.setItem(PREF_KEY, "off"); } catch {}
} else {
audio
.play()
.then(() => {
setPlaying(true);
try { localStorage.setItem(PREF_KEY, "on"); } catch {}
})
.catch(() => {});
}
}
return (
<>
<audio ref={audioRef} src={TRACK_SRC} loop preload="auto" />
{inline ? (
<button
onClick={toggle}
aria-label={playing ? "Silenciar música" : "Reproducir música"}
aria-pressed={playing}
className="flex items-center justify-center w-8 h-8 rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground transition-colors shrink-0"
>
<Icon
icon={playing ? "ph:speaker-high-fill" : "ph:speaker-slash-fill"}
width={16}
/>
</button>
) : (
<button
onClick={toggle}
aria-label={playing ? "Silenciar música" : "Reproducir música"}
aria-pressed={playing}
className={`fixed ${className} z-50 flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-transform hover:scale-110 active:scale-95`}
style={{
background: "color-mix(in srgb, var(--card) 85%, transparent)",
border: "1px solid var(--border)",
backdropFilter: "blur(8px)",
cursor: "pointer",
color: "#3b82f6",
}}
>
<Icon
icon={playing ? "ph:speaker-high-fill" : "ph:speaker-slash-fill"}
width={18}
/>
</button>
)}
</>
);
};
+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="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: "'Quicksand', sans-serif", fontWeight: 700, fontSize: 16, letterSpacing: '-0.01em', color: 'var(--foreground)' }}
>
Rynext
</span>
<span
style={{ fontFamily: "'Quicksand', 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 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,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,57 @@
"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}
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,
}
@@ -1,6 +1,7 @@
'use client'; 'use client';
import { createContext, useContext, useState } from 'react'; import { createContext, useContext, useState } from 'react';
import { ContactModal } from '@/app/components/ContactModal'; import { ContactModal } from '@/app/(landing)/components/ContactModal';
import { useIsDark } from '@/app/(landing)/hooks/useIsDark';
interface ContactCtx { interface ContactCtx {
open: () => void; open: () => void;
@@ -10,10 +11,11 @@ const ContactContext = createContext<ContactCtx>({ open: () => {} });
export const ContactProvider = ({ children }: { children: React.ReactNode }) => { export const ContactProvider = ({ children }: { children: React.ReactNode }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const isDark = useIsDark();
return ( return (
<ContactContext.Provider value={{ open: () => setIsOpen(true) }}> <ContactContext.Provider value={{ open: () => setIsOpen(true) }}>
{children} {children}
<ContactModal isOpen={isOpen} onClose={() => setIsOpen(false)} isNavyBg={false} /> <ContactModal isOpen={isOpen} onClose={() => setIsOpen(false)} isNavyBg={isDark} />
</ContactContext.Provider> </ContactContext.Provider>
); );
}; };
+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>
);
}
File renamed without changes.
+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.',
},
];
@@ -0,0 +1,42 @@
export const machineryData = [
{
id: 1,
num: '01',
title: 'OPERACIONES',
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'],
icon: "ph:squares-four",
},
{
id: 2,
num: '02',
title: 'VENTAS',
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'],
icon: "ph:shopping-cart",
},
{
id: 3,
num: '03',
title: 'INTEGRACIONES',
description: 'Conectamos sistemas heterogéneos en un ecosistema coherente.',
tags: ['ERP / SAP / Odoo', 'APIs de terceros', 'Sincronización bidireccional', 'Webhooks y eventos'],
icon: "ph:flow-arrow",
},
{
id: 4,
num: '04',
title: 'AUTOMATIZACIÓN',
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'],
icon: "ph:lightning",
},
{
id: 5,
num: '05',
title: 'DATOS',
description: 'Decisiones con evidencia: dashboards, modelos y alertas a tiempo.',
tags: ['Dashboards ejecutivos', 'Pipelines de datos', 'KPIs e instrumentación', 'Alertas y umbrales'],
icon: "ph:chart-bar",
},
];
@@ -1,13 +1,12 @@
export const pageLinks = [ export const pageLinks = [
{ label: 'Sobre Nosotros', href: '/sobre-nosotros' }, { label: 'Inicio', href: '/' },
{ label: 'Productos', href: '/productos' }, { label: 'Nosotros', href: '/sobre-nosotros' },
{ label: 'Portafolio', href: '/portafolio' }, { label: 'Portafolio', href: '/portafolio' },
{ label: 'Blog', href: '/blog' }, { label: 'Blog', href: '/blog' },
{ label: 'Aviso de Privacidad', href: '/privacidad' }, { label: 'Aviso de Privacidad', href: '/privacidad' },
]; ];
export const navLinks = [ export const navLinks = [
{ label: 'Soluciones', href: '#showcase' },
{ label: 'Portafolio', href: '#portafolio' }, { label: 'Portafolio', href: '#portafolio' },
{ label: 'Sistemas', href: '#machinery' }, { label: 'Sistemas', href: '#machinery' },
{ label: 'Proceso', href: '#proceso' }, { label: 'Proceso', href: '#proceso' },
@@ -1,5 +1,3 @@
import { Building2, Layers3, ShieldCheck } from 'lucide-react';
export const comparisonRows = [ export const comparisonRows = [
{ {
criterion: 'Adaptación al negocio', criterion: 'Adaptación al negocio',
@@ -27,16 +25,16 @@ export const proofCards = [
{ {
title: 'Diseño de experiencia', title: 'Diseño de experiencia',
description: 'UX orientada a adopción, claridad y flujo de trabajo real.', description: 'UX orientada a adopción, claridad y flujo de trabajo real.',
icon: Layers3, icon: "ph:stack-simple",
}, },
{ {
title: 'Arquitectura robusta', title: 'Arquitectura robusta',
description: 'Infraestructura, seguridad y escalabilidad definida desde el inicio.', description: 'Infraestructura, seguridad y escalabilidad definida desde el inicio.',
icon: ShieldCheck, icon: "ph:shield-check",
}, },
{ {
title: 'Equipo senior', title: 'Equipo senior',
description: 'Product owner, ingeniería y operación trabajando con criterio empresarial.', description: 'Product owner, ingeniería y operación trabajando con criterio empresarial.',
icon: Building2, icon: "ph:buildings",
}, },
]; ];
File renamed without changes.
@@ -1,5 +1,4 @@
import { Code, Cloud, ShieldCheck, TrendingUp } from 'lucide-react'; import type { ServiceItem } from '@/app/(landing)/types/ui';
import type { ServiceItem } from '@/app/types/ui';
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',
}, },
@@ -5,9 +5,3 @@ export const valores = [
{ 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: '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.' }, { num: '05', titulo: 'Responsabilidad completa', desc: 'Desde el análisis hasta la producción, somos dueños del resultado. No transferimos riesgo al cliente.' },
]; ];
export const porQueRynext = [
{ num: '01', titulo: 'Diseño desde la raíz', desc: 'Cada sistema empieza con un análisis profundo de la operación. No adaptamos plantillas, diseñamos desde cero para el contexto específico del negocio.' },
{ num: '02', titulo: 'Equipo técnico propio', desc: 'Sin intermediarios. El equipo que analiza tu operación es el mismo que diseña la arquitectura y escribe el código.' },
{ num: '03', titulo: 'Enfoque en producción', desc: 'No entregamos prototipos. Entregamos sistemas listos para operar con carga real, datos reales y usuarios reales.' },
];
File renamed without changes.
@@ -1,11 +1,11 @@
'use client'; 'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ThemeProps } from '@/app/types/ui'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { anatomyLayers as layers } from '@/app/home/data/anatomyData'; import { anatomyLayers as layers } from '@/app/(landing)/home/data/anatomyData';
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';
@@ -35,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}
@@ -1,15 +1,15 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import Loader from '@/app/components/Loader'; import Loader from '@/app/(landing)/components/Loader';
import { useCompanies } from "@/app/home/services/companies"; import { useCompanies } from '@/app/(landing)/home/services/companies';
import { assetUrl } from "@/app/services/api"; import { assetUrl } from '@/app/(landing)/services/api';
import type { ThemeProps } from "@/app/types/ui"; import type { ThemeProps } from '@/app/(landing)/types/ui';
function ClientsInner({ isNavyBg }: ThemeProps) { function ClientsInner({ isNavyBg }: ThemeProps) {
const { data, isLoading } = useCompanies(); const { data, isLoading } = useCompanies();
const bg = isNavyBg ? "#0a0f1e" : "#f0efec"; const bg = isNavyBg ? "var(--background)" : "#f0efec";
const border = isNavyBg ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.07)"; 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 label = isNavyBg ? "rgba(255,255,255,0.28)" : "rgba(0,0,0,0.28)";
@@ -58,12 +58,15 @@ function ClientsInner({ isNavyBg }: ThemeProps) {
width: "auto", width: "auto",
maxWidth: 200, maxWidth: 200,
objectFit: "contain", objectFit: "contain",
filter: isNavyBg // grayscale solo: invert+brightness(0) aplanaba logos a color
? "brightness(0) invert(1) grayscale(1)" // (fotos/JPEG) a un bloque negro o blanco ilegible en dark.
: "grayscale(1)", filter: "grayscale(1)",
opacity: 0.55, opacity: 0.55,
transition: "opacity 250ms", transition: "opacity 250ms",
}} }}
onError={(e) => {
(e.currentTarget as HTMLImageElement).style.display = "none";
}}
onMouseEnter={(e) => onMouseEnter={(e) =>
((e.currentTarget as HTMLImageElement).style.opacity = "1") ((e.currentTarget as HTMLImageElement).style.opacity = "1")
} }
@@ -1,8 +1,8 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Send, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'; import { Icon } from '@iconify/react';
import type { ThemeProps } from '@/app/types/ui'; import type { ThemeProps } from '@/app/(landing)/types/ui';
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'https://localhost:7001'; const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'https://localhost:7001';
@@ -13,13 +13,12 @@ export const ContactSection = ({ isNavyBg }: ThemeProps) => {
const [form, setForm] = useState(initialForm); const [form, setForm] = useState(initialForm);
const [status, setStatus] = useState<Status>('idle'); const [status, setStatus] = useState<Status>('idle');
const bg = isNavyBg ? '#080c14' : '#f5f5f3'; 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 border = isNavyBg ? 'rgba(255,255,255,0.08)' : '#dddbd5'; const border = isNavyBg ? 'rgba(255,255,255,0.08)' : '#dddbd5';
const card = isNavyBg ? 'rgba(255,255,255,0.03)' : '#ffffff'; const card = isNavyBg ? 'rgba(255,255,255,0.03)' : '#ffffff';
const inputStyle: React.CSSProperties = { const inputStyle: React.CSSProperties = {
width: '100%', borderRadius: 6, padding: '12px 14px', width: '100%', borderRadius: 12, padding: '12px 14px',
fontSize: 14, fontFamily: 'inherit', outline: 'none', fontSize: 14, fontFamily: 'inherit', outline: 'none',
transition: 'border-color 150ms', transition: 'border-color 150ms',
...(isNavyBg ...(isNavyBg
@@ -60,10 +59,18 @@ export const ContactSection = ({ isNavyBg }: ThemeProps) => {
<section <section
id="contacto" id="contacto"
aria-labelledby="contact-heading" aria-labelledby="contact-heading"
style={{ background: bg, borderTop: `1px solid ${border}` }} className="relative px-8 py-20 md:px-14 transition-colors duration-500"
className="px-8 py-28 md:px-14 transition-colors duration-500"
> >
<div className="mx-auto max-w-7xl grid gap-16 lg:grid-cols-[1fr_1.2fr] lg:items-start"> <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 */} {/* Left — copy */}
<motion.div <motion.div
@@ -72,9 +79,6 @@ export const ContactSection = ({ isNavyBg }: ThemeProps) => {
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }} transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
> >
<span className="block text-xs font-semibold uppercase tracking-[0.14em] mb-4" style={{ color: '#3b82f6' }}>
Contacto
</span>
<h2 id="contact-heading" className="text-headline mb-6" style={{ color: text, maxWidth: '20ch' }}> <h2 id="contact-heading" className="text-headline mb-6" style={{ color: text, maxWidth: '20ch' }}>
Hablemos de tu proyecto. Hablemos de tu proyecto.
</h2> </h2>
@@ -102,11 +106,11 @@ export const ContactSection = ({ isNavyBg }: ThemeProps) => {
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1, ease: [0.22, 1, 0.36, 1] }} transition={{ duration: 0.6, delay: 0.1, ease: [0.22, 1, 0.36, 1] }}
style={{ background: card, border: `1px solid ${border}`, borderRadius: 8, padding: '40px 36px' }} style={{ background: card, border: `1px solid ${border}`, borderRadius: 24, padding: '40px 36px' }}
> >
{status === 'success' ? ( {status === 'success' ? (
<div className="flex flex-col items-center justify-center gap-4 py-10 text-center"> <div className="flex flex-col items-center justify-center gap-4 py-10 text-center">
<CheckCircle size={48} style={{ color: '#22c55e' }} /> <Icon icon="ph:check-circle" width={48} style={{ color: "#22c55e" }} />
<h3 className="font-display font-bold text-lg" style={{ color: text }}>¡Mensaje enviado!</h3> <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' }}> <p className="text-sm leading-relaxed" style={{ color: sub, maxWidth: '32ch' }}>
Recibimos tu solicitud. Un miembro del equipo te contactará pronto. Recibimos tu solicitud. Un miembro del equipo te contactará pronto.
@@ -156,7 +160,7 @@ export const ContactSection = ({ isNavyBg }: ThemeProps) => {
{status === 'error' && ( {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)' }}> <div className="flex items-center gap-2 px-4 py-3 rounded-lg" style={{ background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.2)' }}>
<AlertCircle size={15} style={{ color: '#ef4444', flexShrink: 0 }} /> <Icon icon="ph:warning-circle" width={15} style={{ color: '#ef4444', flexShrink: 0 }} />
<p className="text-sm" style={{ color: '#ef4444' }}> <p className="text-sm" style={{ color: '#ef4444' }}>
Hubo un error al enviar. Intenta de nuevo o escríbenos a contacto@rynext.com.mx Hubo un error al enviar. Intenta de nuevo o escríbenos a contacto@rynext.com.mx
</p> </p>
@@ -170,9 +174,9 @@ export const ContactSection = ({ isNavyBg }: ThemeProps) => {
style={{ background: '#3b82f6', border: 'none', cursor: 'pointer' }} style={{ background: '#3b82f6', border: 'none', cursor: 'pointer' }}
> >
{status === 'loading' ? ( {status === 'loading' ? (
<><Loader2 size={16} className="animate-spin" />Enviando...</> <><Icon icon="ph:spinner-gap" width={16} className="animate-spin" />Enviando...</>
) : ( ) : (
<><Send size={15} />Enviar mensaje</> <><Icon icon="ph:paper-plane-tilt" width={15} />Enviar mensaje</>
)} )}
</button> </button>
@@ -1,16 +1,16 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { ArrowRight } from "lucide-react"; import { Icon } from "@iconify/react";
import { Button } from '@/app/components/Button'; import { Button } from '@/app/(landing)/components/Button';
import type { ThemeProps } from "@/app/types/ui"; import type { ThemeProps } from "@/app/(landing)/types/ui";
export const CtaBanner = ({ isNavyBg }: ThemeProps) => { export const CtaBanner = ({ isNavyBg }: ThemeProps) => {
const scrollToContact = () => { const scrollToContact = () => {
const el = document.getElementById("contacto"); const el = document.getElementById("contacto");
if (el) el.scrollIntoView({ behavior: "smooth" }); if (el) el.scrollIntoView({ behavior: "smooth" });
}; };
const bg = isNavyBg ? "#001f5b" : "#f9f9f7"; const bg = isNavyBg ? "var(--background)" : "#f9f9f7";
const text = isNavyBg ? "#ffffff" : "#0a0a0a"; const text = isNavyBg ? "var(--foreground)" : "#0a0a0a";
const sub = isNavyBg ? "rgba(255,255,255,0.45)" : "rgba(0,0,0,0.45)"; const sub = isNavyBg ? "rgba(255,255,255,0.45)" : "rgba(0,0,0,0.45)";
return ( return (
@@ -62,7 +62,7 @@ export const CtaBanner = ({ isNavyBg }: ThemeProps) => {
isNavyBg ? "shadow-xl shadow-red-900/30" : "rounded-md" isNavyBg ? "shadow-xl shadow-red-900/30" : "rounded-md"
} }
> >
Hablemos de tu proyecto <ArrowRight size={16} /> Hablemos de tu proyecto <Icon icon="ph:arrow-right" width={16} />
</Button> </Button>
</div> </div>
</motion.div> </motion.div>
@@ -1,15 +1,14 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Plus } from 'lucide-react'; import { Icon } from '@iconify/react';
import type { ThemeProps } from '@/app/types/ui'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { faqs } from '@/app/home/data/faqData'; import { faqs } from '@/app/(landing)/home/data/faqData';
export const FAQ = ({ isNavyBg }: ThemeProps) => { export const FAQ = ({ isNavyBg }: ThemeProps) => {
const [open, setOpen] = useState<number | null>(null); const [open, setOpen] = useState<number | null>(null);
const bg = isNavyBg ? '#080c14' : '#f5f5f3'; const text = isNavyBg ? 'var(--foreground)' : '#0a0a0a';
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.5)' : '#6a6866'; 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 border = isNavyBg ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)';
const rowBg = isNavyBg ? 'rgba(255,255,255,0.03)' : '#ffffff'; const rowBg = isNavyBg ? 'rgba(255,255,255,0.03)' : '#ffffff';
@@ -18,10 +17,14 @@ export const FAQ = ({ isNavyBg }: ThemeProps) => {
<section <section
id="faq" id="faq"
aria-labelledby="faq-heading" aria-labelledby="faq-heading"
style={{ background: bg, color: text, borderBottom: `1px solid ${border}` }} 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 className="mx-auto max-w-4xl"> <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 <motion.div
className="mb-14" className="mb-14"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@@ -38,7 +41,7 @@ export const FAQ = ({ isNavyBg }: ThemeProps) => {
</h2> </h2>
</motion.div> </motion.div>
<div style={{ border: `1px solid ${border}`, borderRadius: 8, overflow: 'hidden' }}> <div style={{ border: `1px solid ${border}`, borderRadius: 20, overflow: 'hidden' }}>
{faqs.map((faq, i) => { {faqs.map((faq, i) => {
const isOpen = open === i; const isOpen = open === i;
return ( return (
@@ -81,7 +84,7 @@ export const FAQ = ({ isNavyBg }: ThemeProps) => {
marginTop: 2, marginTop: 2,
}} }}
> >
<Plus size={14} /> <Icon icon="ph:plus" width={14} />
</span> </span>
</button> </button>
@@ -1,10 +1,10 @@
'use client'; 'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { ArrowRight } from 'lucide-react'; import { Icon } from '@iconify/react';
import type { ThemeProps } from '@/app/types/ui'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { Button } from '@/app/components/Button'; import { Button } from '@/app/(landing)/components/Button';
import { SystemMap } from './SystemMap'; import { SystemMap } from './SystemMap';
import { useContact } from '@/app/contexts/ContactContext'; 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;
@@ -70,7 +70,7 @@ export const Hero = ({ isNavyBg }: ThemeProps) => {
className="flex flex-wrap items-center gap-6" className="flex flex-wrap items-center gap-6"
> >
<Button className="rounded-md" variant="primary" onClick={openContact}> <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={() => document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' })} onClick={() => document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' })}
@@ -78,7 +78,7 @@ export const Hero = ({ isNavyBg }: ThemeProps) => {
style={{ color: sub }} style={{ color: sub }}
> >
Explorar la maquinaria Explorar la maquinaria
<ArrowRight size={14} className="transition-transform group-hover:translate-x-0.5" /> <Icon icon="ph:arrow-right" width={14} className="transition-transform group-hover:translate-x-0.5" />
</button> </button>
</motion.div> </motion.div>
</div> </div>
@@ -1,14 +1,14 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ThemeProps } from '@/app/types/ui'; import { Icon } from '@iconify/react';
import { machineryData } from '@/app/home/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)';
@@ -18,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 }}
@@ -30,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"
@@ -61,7 +70,7 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
? isNavyBg ? 'rgba(59,130,246,0.06)' : 'rgba(59,130,246,0.03)' ? isNavyBg ? 'rgba(59,130,246,0.06)' : 'rgba(59,130,246,0.03)'
: cardBg, : cardBg,
border: `1px solid ${isHov ? 'rgba(59,130,246,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',
@@ -111,23 +120,23 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
{/* Icon */} {/* Icon */}
<div style={{ position: 'relative', marginBottom: 20 }}> <div style={{ position: 'relative', marginBottom: 20 }}>
{(() => { const Icon = item.icon; return Icon ? ( {item.icon ? (
<span style={{ <span style={{
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
width: 44, width: 44,
height: 44, height: 44,
borderRadius: 10, borderRadius: 12,
background: isHov background: isHov
? 'rgba(59,130,246,0.15)' ? 'rgba(59,130,246,0.15)'
: isNavyBg ? 'rgba(255,255,255,0.07)' : 'rgba(59,130,246,0.07)', : isNavyBg ? 'rgba(255,255,255,0.07)' : 'rgba(59,130,246,0.07)',
color: isHov ? '#3b82f6' : isNavyBg ? 'rgba(255,255,255,0.5)' : '#3b82f6', color: isHov ? '#3b82f6' : isNavyBg ? 'rgba(255,255,255,0.5)' : '#3b82f6',
transition: 'background 220ms, color 220ms', transition: 'background 220ms, color 220ms',
}}> }}>
<Icon size={20} /> <Icon icon={item.icon} width={20} />
</span> </span>
) : null; })()} ) : null}
</div> </div>
{/* Content */} {/* Content */}
@@ -144,11 +153,29 @@ export const Machinery = ({ isNavyBg }: ThemeProps) => {
{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,90 @@
"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}
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,28 +1,33 @@
'use client'; 'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { X, Check } from 'lucide-react'; import { Icon } from '@iconify/react';
import type { ThemeProps } from '@/app/types/ui'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { comparisonRows, proofCards } from '@/app/home/data/platformAdvantageData'; import { comparisonRows, proofCards } from '@/app/(landing)/home/data/platformAdvantageData';
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'}`}>
@@ -45,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>
@@ -67,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>
@@ -83,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}
@@ -98,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'}`}>
@@ -1,16 +1,16 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import Loader from '@/app/components/Loader'; import Loader from '@/app/(landing)/components/Loader';
import { usePortfolio } from "@/app/portafolio/services/portfolio"; import { usePortfolio } from '@/app/(landing)/portafolio/services/portfolio';
import { assetUrl } from "@/app/services/api"; import { assetUrl } from '@/app/(landing)/services/api';
import type { ThemeProps } from "@/app/types/ui"; import type { ThemeProps } from '@/app/(landing)/types/ui';
function PortfolioInner({ isNavyBg }: ThemeProps) { function PortfolioInner({ isNavyBg }: ThemeProps) {
const { data, isLoading } = usePortfolio(); const { data, isLoading } = usePortfolio();
const bg = isNavyBg ? "#080c14" : "#f0efec"; const bg = isNavyBg ? "var(--background)" : "#f9f9f7";
const text = isNavyBg ? "#ffffff" : "#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)" : "rgba(0,0,0,0.08)"; 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 card = isNavyBg ? "rgba(255,255,255,0.03)" : "#ffffff";
@@ -20,7 +20,7 @@ function PortfolioInner({ isNavyBg }: ThemeProps) {
if (isLoading) { if (isLoading) {
return ( return (
<section style={{ background: bg, borderBottom: `1px solid ${border}` }} className="px-8 py-28 md:px-14"> <section style={{ background: bg }} className="px-8 py-28 md:px-14">
<Loader className="py-20" /> <Loader className="py-20" />
</section> </section>
); );
@@ -32,7 +32,7 @@ function PortfolioInner({ isNavyBg }: ThemeProps) {
<section <section
id="portafolio" id="portafolio"
aria-labelledby="portfolio-heading" aria-labelledby="portfolio-heading"
style={{ background: bg, borderBottom: `1px solid ${border}` }} style={{ background: bg }}
className="px-8 py-28 md:px-14 transition-colors duration-500" className="px-8 py-28 md:px-14 transition-colors duration-500"
> >
<div className="mx-auto max-w-7xl"> <div className="mx-auto max-w-7xl">
@@ -54,25 +54,36 @@ function PortfolioInner({ isNavyBg }: ThemeProps) {
className="text-headline" className="text-headline"
style={{ maxWidth: "28ch", color: text }} style={{ maxWidth: "28ch", color: text }}
> >
Construimos el sistema. <span style={{ color: "#3b82f6" }}>Tú operas el negocio.</span> Construimos el sistema.{" "}
<span style={{ color: "#3b82f6" }}>Tú operas el negocio.</span>
</h2> </h2>
<p className="mt-4 max-w-xl text-base leading-relaxed" style={{ color: sub }}> <p
Proyectos reales, resultados medibles. Cada implementación parte de entender la operación antes de escribir una sola línea de código. 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> </p>
</motion.div> </motion.div>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{projects.map((p, i) => { {projects.map((p, i) => {
const cover = p.images.find((img) => img.isCover) ?? p.images.sort((a, b) => a.order - b.order)[0]; const cover =
p.images.find((img) => img.isCover) ??
p.images.sort((a, b) => a.order - b.order)[0];
return ( return (
<motion.article <motion.article
key={p.id} key={p.id}
initial={{ opacity: 0, y: 24 }} initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }} viewport={{ once: true, margin: "-40px" }}
transition={{ duration: 0.55, delay: i * 0.1, ease: [0.22, 1, 0.36, 1] }} transition={{
duration: 0.55,
delay: i * 0.1,
ease: [0.22, 1, 0.36, 1],
}}
style={{ style={{
borderRadius: 14, borderRadius: 20,
border: `1px solid ${border}`, border: `1px solid ${border}`,
background: card, background: card,
overflow: "hidden", overflow: "hidden",
@@ -85,7 +96,9 @@ function PortfolioInner({ isNavyBg }: ThemeProps) {
style={{ style={{
aspectRatio: "16/9", aspectRatio: "16/9",
overflow: "hidden", overflow: "hidden",
background: isNavyBg ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.03)", background: isNavyBg
? "rgba(255,255,255,0.04)"
: "rgba(0,0,0,0.03)",
borderBottom: `1px solid ${border}`, borderBottom: `1px solid ${border}`,
}} }}
> >
@@ -93,11 +106,31 @@ function PortfolioInner({ isNavyBg }: ThemeProps) {
<img <img
src={assetUrl(cover.url)} src={assetUrl(cover.url)}
alt={p.title} alt={p.title}
style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} style={{
width: "100%",
height: "100%",
objectFit: "cover",
display: "block",
}}
/> />
) : ( ) : (
<div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" }}> <div
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke={sub} strokeWidth="1.5"> 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" /> <rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="8.5" cy="8.5" r="1.5" /> <circle cx="8.5" cy="8.5" r="1.5" />
<path d="m21 15-5-5L5 21" /> <path d="m21 15-5-5L5 21" />
@@ -107,20 +140,37 @@ function PortfolioInner({ isNavyBg }: ThemeProps) {
</div> </div>
{/* Content */} {/* Content */}
<div style={{ padding: "24px 24px 28px", display: "flex", flexDirection: "column", flex: 1 }}> <div
style={{
padding: "24px 24px 28px",
display: "flex",
flexDirection: "column",
flex: 1,
}}
>
{p.clientCompanyName && ( {p.clientCompanyName && (
<span className="text-xs font-semibold uppercase tracking-wider mb-2 block" style={{ color: "#3b82f6" }}> <span
className="text-xs font-semibold uppercase tracking-wider mb-2 block"
style={{ color: "#3b82f6" }}
>
{p.clientCompanyName} {p.clientCompanyName}
</span> </span>
)} )}
<h3 <h3
className="font-display font-bold mb-3" className="font-display font-bold mb-3"
style={{ fontSize: "1.05rem", color: text, letterSpacing: "-0.01em" }} style={{
fontSize: "1.05rem",
color: text,
letterSpacing: "-0.01em",
}}
> >
{p.title} {p.title}
</h3> </h3>
{p.publicSummary && ( {p.publicSummary && (
<p className="text-sm leading-relaxed mb-5" style={{ color: sub }}> <p
className="text-sm leading-relaxed mb-5"
style={{ color: sub }}
>
{p.publicSummary} {p.publicSummary}
</p> </p>
)} )}
@@ -1,11 +1,11 @@
'use client'; 'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ThemeProps } from '@/app/types/ui'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { principles } from '@/app/home/data/principlesData'; import { principles } from '@/app/(landing)/home/data/principlesData';
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)';
@@ -40,7 +40,7 @@ export const Principles = ({ isNavyBg }: ThemeProps) => {
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5" className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5"
style={{ style={{
border: `1px solid ${gridGap}`, border: `1px solid ${gridGap}`,
borderRadius: 4, borderRadius: 20,
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
+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,11 +1,11 @@
'use client'; 'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ThemeProps } from '@/app/types/ui'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { processSteps as steps } from '@/app/home/data/processData'; import { processSteps as steps } from '@/app/(landing)/home/data/processData';
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';
@@ -36,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}
@@ -1,26 +1,25 @@
'use client'; '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 '@/app/types/ui'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { servicesData } from '@/app/home/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>
@@ -33,10 +32,10 @@ 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-blue-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 (
@@ -58,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}
@@ -74,10 +73,11 @@ 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-blue-400' : 'text-brand-red') : (isNavyBg ? 'text-blue-400' : 'text-brand-red')
}`} }`}
/> />
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ThemeProps } from '@/app/types/ui'; import type { ThemeProps } from '@/app/(landing)/types/ui';
import { stackGroups } from '@/app/home/data/stackData'; import { stackGroups } from '@/app/(landing)/home/data/stackData';
export const Stack = ({ isNavyBg }: ThemeProps) => { export const Stack = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#0f1520' : '#f9f9f7'; const bg = isNavyBg ? '#0f1520' : '#f9f9f7';
@@ -1,9 +1,9 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import Loader from '@/app/components/Loader'; import Loader from '@/app/(landing)/components/Loader';
import { useTestimonials } from "@/app/home/services/testimonials"; import { useTestimonials } from '@/app/(landing)/home/services/testimonials';
import type { ThemeProps } from "@/app/types/ui"; import type { ThemeProps } from '@/app/(landing)/types/ui';
export const Testimonial = ({ isNavyBg }: ThemeProps) => { export const Testimonial = ({ isNavyBg }: ThemeProps) => {
const { data, isLoading } = useTestimonials(); const { data, isLoading } = useTestimonials();
@@ -21,17 +21,17 @@ export const Testimonial = ({ isNavyBg }: ThemeProps) => {
}, [testimonials.length]); }, [testimonials.length]);
const item = testimonials[current]; const item = testimonials[current];
const bg = isNavyBg ? "#001f5b" : "#ffffff"; const bg = isNavyBg ? "var(--background)" : "#f9f9f7";
const border = isNavyBg ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)"; 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 sub = isNavyBg ? "rgba(255,255,255,0.45)" : "#6a6866";
const text = isNavyBg ? "#ffffff" : "#0a0a0a"; const text = isNavyBg ? "var(--foreground)" : "#0a0a0a";
if (!isLoading && testimonials.length === 0) return null; // sección se oculta si no hay testimonios if (!isLoading && testimonials.length === 0) return null; // sección se oculta si no hay testimonios
return ( return (
<section <section
aria-label="Testimonios de clientes" aria-label="Testimonios de clientes"
style={{ background: bg, borderBottom: `1px solid ${border}` }} style={{ background: bg }}
className="px-8 py-24 md:px-14 transition-colors duration-500" className="px-8 py-24 md:px-14 transition-colors duration-500"
> >
<div className="mx-auto max-w-4xl"> <div className="mx-auto max-w-4xl">
@@ -101,7 +101,7 @@ export const Testimonial = ({ isNavyBg }: ThemeProps) => {
style={{ style={{
width: i === current ? 24 : 8, width: i === current ? 24 : 8,
height: 8, height: 8,
borderRadius: 4, borderRadius: 20,
background: i === current ? "#3b82f6" : sub, background: i === current ? "#3b82f6" : sub,
border: "none", border: "none",
cursor: "pointer", cursor: "pointer",
@@ -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,
});
}
File renamed without changes.
+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>
);
}
File renamed without changes.
@@ -1,5 +1,5 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { HomePage } from '@/app/home/HomePage'; import { HomePage } from '@/app/(landing)/home/HomePage';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Rynext Technologies | Software Empresarial 100% Mexicano', title: 'Rynext Technologies | Software Empresarial 100% Mexicano',
@@ -0,0 +1,210 @@
"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}
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} 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="" 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 />;
}
+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>
);
}
File renamed without changes.
@@ -0,0 +1,40 @@
import { useQuery } from "@tanstack/react-query";
import { apiFetch } from '@/app/(landing)/services/api';
export interface PortfolioImage {
id: string;
portfolioEntryId: string;
url: string;
order: number;
isCover: boolean;
caption?: string;
}
export interface PortfolioTechnology {
id: string;
name: string;
iconUrl?: string;
color?: string;
}
export interface PortfolioProject {
id: string;
title: string;
clientCompanyName?: string;
publicSummary?: string;
caseStudy?: string;
solution?: string;
siteUrl?: string;
isPublished: boolean;
displayOrder: number;
technologies: PortfolioTechnology[];
images: PortfolioImage[];
}
export function usePortfolio() {
return useQuery<PortfolioProject[]>({
queryKey: ["portfolio"],
queryFn: () => apiFetch<PortfolioProject[]>("/api/portfolio/public"),
staleTime: 1000 * 60 * 5,
});
}
@@ -9,21 +9,20 @@ export function PrivacidadPage() {
return ( return (
<main <main
id="main-content" id="main-content"
style={{ background: "#ffffff", minHeight: "100vh" }} style={{ background: "var(--background)", minHeight: "100vh" }}
> >
<section <section
style={{ style={{
paddingTop: "140px", paddingTop: "140px",
paddingBottom: "100px", paddingBottom: "100px",
borderBottom: "1px solid #dddbd5",
}} }}
className="px-8 md:px-14" className="px-8 md:px-14"
> >
<div className="mx-auto max-w-3xl"> <div className="mx-auto max-w-3xl">
<h1 className="text-headline mb-4" style={{ color: "#0a0a0a" }}> <h1 className="text-headline mb-4" style={{ color: "var(--foreground)" }}>
Aviso de Privacidad Integral Aviso de Privacidad Integral
</h1> </h1>
<p className="text-sm mb-16" style={{ color: "#aaa8a4" }}> <p className="text-sm mb-16" style={{ color: "var(--muted-foreground)" }}>
Última actualización: agosto de 2026 Última actualización: agosto de 2026
</p> </p>
+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>
);
}
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,7 @@
export const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://localhost:7030"; export const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://localhost:7030";
export async function apiFetch<T>(path: string): Promise<T> { export async function apiFetch<T>(path: string): Promise<T> {
const res = await fetch(`${API_URL}${path}`); const res = await fetch(`${API_URL}${path}`, { signal: AbortSignal.timeout(8000) });
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json() as Promise<T>; return res.json() as Promise<T>;
} }
@@ -0,0 +1,434 @@
"use client";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { Icon } from "@iconify/react";
import Loader from '@/app/(landing)/components/Loader';
import { useTeam, groupByDepartment } from '@/app/(landing)/sobre-nosotros/services/team';
import { valores } from "@/app/(landing)/home/data/sobreNosotrosData";
const TEAM_IMAGE =
"https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200&q=80";
const VALOR_ICONS: string[] = [
"ph:stack",
"ph:shield-check",
"ph:file-code",
"ph:arrows-clockwise",
"ph:seal-check",
];
function SobreNosotrosContent() {
const { data: members, isLoading: loadingTeam } = useTeam();
const departments = groupByDepartment(members ?? []);
useEffect(() => {
document.title = "Sobre Nosotros | Rynext Technologies";
}, []);
return (
<main id="main-content">
{/* Hero */}
<section
style={{
background: "var(--background)",
paddingTop: "140px",
paddingBottom: "100px",
}}
className="relative overflow-hidden px-8 md:px-14"
>
<div
aria-hidden="true"
className="pointer-events-none absolute -right-32 -top-24 h-[24rem] w-[24rem] rounded-full blur-[110px]"
style={{ background: "rgba(59,130,246,0.12)" }}
/>
<div className="relative mx-auto max-w-7xl grid gap-16 lg:grid-cols-2 lg:items-center">
<div>
<motion.h1
className="text-display mb-8"
style={{ color: "var(--foreground)" }}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={{
hidden: { opacity: 0, y: 32 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.7,
delay: 0.1,
ease: [0.22, 1, 0.36, 1],
},
},
}}
>
Software 100%{" "}
<span style={{ position: "relative", display: "inline-block" }}>
<span style={{ color: "#3b82f6" }}>Mexicano</span>
<svg
viewBox="0 0 200 12"
preserveAspectRatio="none"
aria-hidden="true"
style={{
position: "absolute",
bottom: -4,
left: 0,
width: "100%",
height: 12,
}}
>
<path
d="M2 8 C50 2, 150 2, 198 8"
stroke="#3b82f6"
strokeWidth="3"
strokeLinecap="round"
fill="none"
/>
</svg>
</span>{" "}
de clase mundial.
</motion.h1>
<motion.p
className="text-lg leading-relaxed"
style={{ color: "var(--muted-foreground)" }}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={{
hidden: { opacity: 0, y: 24 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
delay: 0.2,
ease: [0.22, 1, 0.36, 1],
},
},
}}
>
Rynext Technologies nació de una convicción: México tiene el
talento para construir software empresarial que compita con
cualquier firma en el mundo. No adaptamos herramientas extranjeras
diseñamos sistemas propios, con las tecnologías más actuales, para
la realidad operativa de los negocios mexicanos.
</motion.p>
</div>
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={{
hidden: { opacity: 0, x: 24 },
visible: {
opacity: 1,
x: 0,
transition: {
duration: 0.7,
delay: 0.15,
ease: [0.22, 1, 0.36, 1],
},
},
}}
style={{ position: "relative" }}
>
<div
style={{
position: "relative",
borderRadius: 28,
overflow: "hidden",
border: "1px solid var(--border)",
boxShadow: "0 24px 60px -16px rgba(0,0,0,0.25)",
}}
>
<img
src={TEAM_IMAGE}
alt="Equipo Rynext Technologies"
style={{
width: "100%",
aspectRatio: "4/3",
objectFit: "cover",
display: "block",
}}
/>
</div>
</motion.div>
</div>
</section>
{/* Valores */}
<section
style={{ background: "var(--background)" }}
className="px-8 pb-24 md:px-14 md:pb-28"
>
<div className="mx-auto max-w-7xl">
<motion.h2
className="text-headline mb-16"
style={{ color: "var(--foreground)" }}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={{
hidden: { opacity: 0, y: 28 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
delay: 0.1,
ease: [0.22, 1, 0.36, 1],
},
},
}}
>
Los principios que guían cada proyecto.
</motion.h2>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{valores.map((v, i) => {
const iconName = VALOR_ICONS[i % VALOR_ICONS.length];
return (
<motion.div
key={i}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={{
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
delay: i * 0.08,
ease: [0.22, 1, 0.36, 1],
},
},
}}
whileHover={{ y: -3 }}
className="transition-shadow duration-200 hover:shadow-md"
style={{
background: "var(--card)",
border: "1px solid var(--border)",
borderRadius: 20,
padding: 28,
}}
>
<div className="mb-5 flex items-center justify-between">
<span
className="flex h-11 w-11 items-center justify-center rounded-xl"
style={{
background: "rgba(59,130,246,0.1)",
color: "#3b82f6",
}}
>
<Icon icon={iconName} width={20} />
</span>
<span
className="font-mono text-xs font-semibold"
style={{
color: "var(--muted-foreground)",
letterSpacing: "0.06em",
}}
>
{v.num}
</span>
</div>
<h3
className="font-display font-bold text-base mb-2.5"
style={{ color: "var(--foreground)" }}
>
{v.titulo}
</h3>
<p
className="text-sm leading-relaxed"
style={{ color: "var(--muted-foreground)" }}
>
{v.desc}
</p>
</motion.div>
);
})}
</div>
</div>
</section>
{/* Equipo */}
<section
style={{ background: "var(--background)" }}
className="px-8 pb-24 md:px-14 md:pb-28"
>
<div className="mx-auto max-w-7xl">
<motion.h2
className="text-headline mb-16"
style={{ color: "var(--foreground)" }}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
>
El equipo detrás del código.
</motion.h2>
{loadingTeam ? (
<Loader className="py-16" />
) : departments.length === 0 ? (
<p className="text-sm" style={{ color: "var(--muted-foreground)" }}>
Sin miembros registrados.
</p>
) : (
<div className="flex flex-col gap-14">
{departments.map((dept, di) => (
<motion.div
key={dept.name}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{
duration: 0.5,
delay: di * 0.08,
ease: [0.22, 1, 0.36, 1],
}}
>
{/* Dept header */}
<div className="flex items-center gap-3 mb-0">
<span
style={{
width: 3,
height: 16,
background: "#3b82f6",
borderRadius: 99,
flexShrink: 0,
display: "block",
}}
/>
<p
className="text-xs font-semibold uppercase tracking-widest"
style={{ color: "var(--muted-foreground)" }}
>
{dept.name}
</p>
</div>
{/* Hierarchy connector */}
<div
style={{
position: "relative",
paddingLeft: 20,
marginLeft: 1,
}}
>
{/* Vertical line */}
<span
style={{
position: "absolute",
left: 0,
top: 0,
bottom: 24,
width: 1,
background: "var(--border)",
}}
/>
{/* Horizontal branch */}
<span
style={{
position: "absolute",
left: 0,
top: 24,
width: 16,
height: 1,
background: "var(--border)",
}}
/>
<div className="grid gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 pt-6">
{dept.members.map((m, mi) => (
<motion.div
key={m.id}
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{
duration: 0.4,
delay: mi * 0.05,
ease: [0.22, 1, 0.36, 1],
}}
whileHover={{ y: -2 }}
className="transition-shadow duration-200 hover:shadow-md"
style={{
background: "var(--card)",
border: "1px solid var(--border)",
borderRadius: 14,
padding: "20px",
display: "flex",
alignItems: "center",
gap: 14,
}}
>
<div
style={{
width: 44,
height: 44,
borderRadius: "50%",
background:
"linear-gradient(135deg,#3b82f6,#1d4ed8)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<span
style={{
color: "#fff",
fontWeight: 700,
fontSize: 14,
letterSpacing: "0.02em",
}}
>
{m.firstName[0]}
{m.lastName[0]}
</span>
</div>
<div style={{ minWidth: 0 }}>
<p
className="font-display font-bold text-sm"
style={{
color: "var(--foreground)",
lineHeight: 1.3,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{m.firstName} {m.lastName}
</p>
{m.positionName && (
<p
className="text-xs mt-0.5"
style={{ color: "var(--muted-foreground)" }}
>
{m.positionName}
</p>
)}
</div>
</motion.div>
))}
</div>
</div>
</motion.div>
))}
</div>
)}
</div>
</section>
</main>
);
}
export function SobreNosotrosPage() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return <SobreNosotrosContent />;
}
@@ -0,0 +1,9 @@
import Loader from '@/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>
);
}
File renamed without changes.
@@ -0,0 +1,40 @@
import { useQuery } from "@tanstack/react-query";
import { apiFetch } from '@/app/(landing)/services/api';
export interface TeamMember {
id: string;
firstName: string;
lastName: string;
positionName?: string;
departmentName?: string;
departmentOrder?: number;
positionOrder?: number;
}
export interface Department {
name: string;
order: number;
members: TeamMember[];
}
export function useTeam() {
return useQuery<TeamMember[]>({
queryKey: ["team"],
queryFn: () => apiFetch<TeamMember[]>("/api/employees/public"),
staleTime: 1000 * 60 * 5,
});
}
export function groupByDepartment(members: TeamMember[]): Department[] {
const map = new Map<string, Department>();
for (const m of members) {
const key = m.departmentName ?? "Sin departamento";
if (!map.has(key)) {
map.set(key, { name: key, order: m.departmentOrder ?? 99, members: [] });
}
map.get(key)!.members.push(m);
}
return Array.from(map.values()).sort((a, b) => a.order - b.order);
}
@@ -1,5 +1,3 @@
import type React from 'react';
export interface ThemeProps { export interface ThemeProps {
isNavyBg: boolean; isNavyBg: boolean;
} }
@@ -22,7 +20,7 @@ export interface ServiceItem {
title: string; title: string;
description: string; description: string;
features: string[]; features: string[];
icon: React.ElementType; icon: string;
accent: 'blue' | 'red'; accent: 'blue' | 'red';
gridCols: string; gridCols: string;
} }
-47
View File
@@ -1,47 +0,0 @@
'use client';
import { useEffect } from 'react';
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';
export function NotFoundPage() {
useEffect(() => {
document.title = '404 — Página no encontrada | Rynext Technologies';
}, []);
return (
<main
id="main-content"
className="flex min-h-screen flex-col items-center justify-center px-8 text-center"
style={{ background: '#f9f9f7' }}
>
<p
className="font-display font-black mb-6"
style={{ fontSize: 'clamp(6rem, 20vw, 12rem)', lineHeight: 1, color: '#3b82f6', letterSpacing: '-0.06em', opacity: 0.15 }}
aria-hidden="true"
>
404
</p>
<div style={{ marginTop: '-2rem' }}>
<h1
className="font-display font-black mb-4"
style={{ fontSize: 'clamp(1.5rem, 4vw, 2.5rem)', color: '#0a0a0a', letterSpacing: '-0.03em' }}
>
Página no encontrada.
</h1>
<p className="text-base mb-10 max-w-sm mx-auto" style={{ color: '#6a6866', lineHeight: 1.7 }}>
La página que buscas no existe o fue movida. Regresa al inicio para continuar.
</p>
<Link
href="/"
className="inline-flex items-center gap-2 px-6 py-3 text-sm font-bold text-white transition hover:opacity-90"
style={{ background: '#3b82f6', borderRadius: 6 }}
>
<ArrowLeft size={15} />
Volver al inicio
</Link>
</div>
</main>
);
}
+109
View File
@@ -0,0 +1,109 @@
/* Estilos propios del panel admin (Mod-Rynext).
No redefine tokens compartidos (--background, --foreground, --primary, etc.)
para no chocar con el resto de la Landing. */
:root {
--nav-bg: #001f5b;
--nav-hover: rgba(255, 255, 255, 0.08);
--nav-active: rgba(255, 255, 255, 0.15);
--nav-border: rgba(255, 255, 255, 0.12);
}
/* Dropdown animation */
@keyframes dropdown-in {
from {
opacity: 0;
transform: translateY(-6px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes dropdown-out {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(-6px) scale(0.97);
}
}
@keyframes dropdown-in-up {
from {
opacity: 0;
transform: translateY(calc(-100% + 6px)) scale(0.97);
}
to {
opacity: 1;
transform: translateY(-100%) scale(1);
}
}
@keyframes dropdown-out-up {
from {
opacity: 1;
transform: translateY(-100%) scale(1);
}
to {
opacity: 0;
transform: translateY(calc(-100% + 6px)) scale(0.97);
}
}
.animate-dropdown {
animation: dropdown-in 0.15s ease-out forwards;
}
.animate-dropdown-out {
animation: dropdown-out 0.12s ease-in forwards;
}
.animate-dropdown-up {
animation: dropdown-in-up 0.15s ease-out forwards;
}
.animate-dropdown-out-up {
animation: dropdown-out-up 0.12s ease-in forwards;
}
/* Custom scrollbar (solo dentro del admin) */
.admin-root .scrollbar-thin::-webkit-scrollbar {
height: 6px;
width: 6px;
}
.admin-root .scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.admin-root .scrollbar-thin::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 9999px;
}
.admin-root .scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Hide number input spinners (solo dentro del admin) */
.admin-root input[type="number"]::-webkit-inner-spin-button,
.admin-root input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.admin-root input[type="number"] {
-moz-appearance: textfield;
}
.admin-root {
font-family: var(--font-quicksand), var(--font-dm-sans), sans-serif;
}
Loaded 100 of 349 files, more files were not shown because too many files have changed in this diff. Show more