cambios
This commit is contained in:
1 parent
9dfced04f1
commit
d7f584059e
12 files changed
+524
-30
No files matched your search
Vendored
+2
-2
@@ -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.
|
||||||
+34
-18
@@ -1,23 +1,33 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from "react";
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from "next/dynamic";
|
||||||
import { Hero } from '../sections/Hero';
|
import { Hero } from "../sections/Hero";
|
||||||
import { Problem } from '../sections/Problem';
|
import { Problem } from "../sections/Problem";
|
||||||
import { Machinery } from '../sections/Machinery';
|
import { Machinery } from "../sections/Machinery";
|
||||||
import { Showcase } from '../sections/Showcase';
|
import { Showcase } from "../sections/Showcase";
|
||||||
import { PlatformAdvantage } from '../sections/PlatformAdvantage';
|
import { PlatformAdvantage } from "../sections/PlatformAdvantage";
|
||||||
import { Services } from '../sections/Services';
|
import { Services } from "../sections/Services";
|
||||||
import { Testimonial } from '../sections/Testimonial';
|
import { Testimonial } from "../sections/Testimonial";
|
||||||
import { Anatomy } from '../sections/Anatomy';
|
import { Anatomy } from "../sections/Anatomy";
|
||||||
import { Process } from '../sections/Process';
|
import { Process } from "../sections/Process";
|
||||||
import { Principles } from '../sections/Principles';
|
import { Principles } from "../sections/Principles";
|
||||||
import { FAQ } from '../sections/FAQ';
|
import { FAQ } from "../sections/FAQ";
|
||||||
import { CtaBanner } from '../sections/CtaBanner';
|
import { CtaBanner } from "../sections/CtaBanner";
|
||||||
import { ContactSection } from '../sections/ContactSection';
|
import { ContactSection } from "../sections/ContactSection";
|
||||||
|
|
||||||
const Clients = dynamic(
|
const Clients = dynamic(
|
||||||
() => import('../sections/Clients').then(m => ({ default: m.Clients })),
|
() => import("../sections/Clients").then((m) => ({ default: m.Clients })),
|
||||||
{ ssr: false }
|
{ ssr: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
const Partners = dynamic(
|
||||||
|
() => import("../sections/Partners").then((m) => ({ default: m.Partners })),
|
||||||
|
{ ssr: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
const Portfolio = dynamic(
|
||||||
|
() => import("../sections/Portfolio").then((m) => ({ default: m.Portfolio })),
|
||||||
|
{ ssr: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
@@ -30,6 +40,9 @@ export function HomePage() {
|
|||||||
<Problem isNavyBg={false} />
|
<Problem isNavyBg={false} />
|
||||||
<Machinery isNavyBg={false} />
|
<Machinery isNavyBg={false} />
|
||||||
<Showcase isNavyBg={false} />
|
<Showcase isNavyBg={false} />
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<Portfolio isNavyBg={false} />
|
||||||
|
</Suspense>
|
||||||
<PlatformAdvantage isNavyBg={false} />
|
<PlatformAdvantage isNavyBg={false} />
|
||||||
<Services isNavyBg={false} />
|
<Services isNavyBg={false} />
|
||||||
<Testimonial isNavyBg={false} />
|
<Testimonial isNavyBg={false} />
|
||||||
@@ -37,6 +50,9 @@ export function HomePage() {
|
|||||||
<Process isNavyBg={false} />
|
<Process isNavyBg={false} />
|
||||||
<Principles isNavyBg={false} />
|
<Principles isNavyBg={false} />
|
||||||
<FAQ isNavyBg={false} />
|
<FAQ isNavyBg={false} />
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<Partners isNavyBg={false} />
|
||||||
|
</Suspense>
|
||||||
<CtaBanner isNavyBg={false} />
|
<CtaBanner isNavyBg={false} />
|
||||||
<ContactSection isNavyBg={false} />
|
<ContactSection isNavyBg={false} />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import Loader from '../../components/Loader';
|
||||||
|
import { usePortfolio } from '../../services/portfolio';
|
||||||
|
import { assetUrl } from '../../services/api';
|
||||||
|
|
||||||
|
const bg = '#f9f9f7';
|
||||||
|
const text = '#0a0a0a';
|
||||||
|
const sub = '#6a6866';
|
||||||
|
const border = 'rgba(0,0,0,0.08)';
|
||||||
|
const card = '#ffffff';
|
||||||
|
const tagBg = 'rgba(0,0,0,0.05)';
|
||||||
|
|
||||||
|
function PortafolioContent() {
|
||||||
|
const { data, isLoading } = usePortfolio();
|
||||||
|
const projects = data ?? [];
|
||||||
|
|
||||||
|
if (isLoading) return <Loader className="py-32" />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main id="main-content">
|
||||||
|
{/* Hero */}
|
||||||
|
<section
|
||||||
|
style={{ background: '#ffffff', borderBottom: `1px solid ${border}`, paddingTop: 140, paddingBottom: 80 }}
|
||||||
|
className="px-8 md:px-14"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
>
|
||||||
|
<span className="mb-3 block text-xs font-semibold uppercase tracking-[0.14em]" style={{ color: '#3b82f6' }}>
|
||||||
|
Casos de éxito
|
||||||
|
</span>
|
||||||
|
<h1 className="text-display mb-6" style={{ color: text, maxWidth: '22ch' }}>
|
||||||
|
Construimos el sistema.{' '}
|
||||||
|
<span style={{ color: '#3b82f6' }}>Tú operas el negocio.</span>
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-2xl text-lg leading-relaxed" style={{ color: sub }}>
|
||||||
|
Proyectos reales, resultados medibles. Cada implementación parte de entender la operación antes de escribir una sola línea de código.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<section style={{ background: bg }} className="px-8 py-20 md:px-14">
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
{projects.length === 0 ? (
|
||||||
|
<p className="text-center py-20 text-sm" style={{ color: sub }}>Próximamente casos de éxito.</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{projects.map((p, i) => {
|
||||||
|
const cover = p.images.find((img) => img.isCover) ?? p.images.sort((a, b) => a.order - b.order)[0];
|
||||||
|
return (
|
||||||
|
<motion.article
|
||||||
|
key={p.id}
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, margin: '-40px' }}
|
||||||
|
transition={{ duration: 0.55, delay: i * 0.08, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
style={{
|
||||||
|
borderRadius: 14,
|
||||||
|
border: `1px solid ${border}`,
|
||||||
|
background: card,
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Cover */}
|
||||||
|
<div style={{ aspectRatio: '16/9', overflow: 'hidden', background: 'rgba(0,0,0,0.03)', borderBottom: `1px solid ${border}` }}>
|
||||||
|
{cover ? (
|
||||||
|
<img
|
||||||
|
src={assetUrl(cover.url)}
|
||||||
|
alt={p.title}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke={sub} strokeWidth="1.5">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||||
|
<path d="m21 15-5-5L5 21" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div style={{ padding: '24px 24px 28px', display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||||
|
{p.clientCompanyName && (
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wider mb-2 block" style={{ color: '#3b82f6' }}>
|
||||||
|
{p.clientCompanyName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<h2 className="font-display font-bold mb-3" style={{ fontSize: '1.1rem', color: text, letterSpacing: '-0.01em' }}>
|
||||||
|
{p.title}
|
||||||
|
</h2>
|
||||||
|
{p.publicSummary && (
|
||||||
|
<p className="text-sm leading-relaxed mb-5" style={{ color: sub }}>
|
||||||
|
{p.publicSummary}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{p.technologies.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mt-auto">
|
||||||
|
{p.technologies.map((t) => (
|
||||||
|
<span
|
||||||
|
key={t.id}
|
||||||
|
style={{
|
||||||
|
fontSize: 10.5,
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: '3px 9px',
|
||||||
|
borderRadius: 99,
|
||||||
|
background: tagBg,
|
||||||
|
color: t.color ?? sub,
|
||||||
|
letterSpacing: '0.01em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PortafolioPage() {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
if (!mounted) return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center" style={{ background: bg }}>
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return <PortafolioContent />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Loader from '../../components/Loader';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center" style={{ background: '#f9f9f7' }}>
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { PortafolioPage } from './PortafolioPage';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Portafolio | Rynext Technologies',
|
||||||
|
description: 'Casos de éxito reales. Proyectos que transformaron operaciones empresariales con software diseñado a medida.',
|
||||||
|
alternates: { canonical: 'https://rynext.com.mx/portafolio' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <PortafolioPage />;
|
||||||
|
}
|
||||||
@@ -10,7 +10,8 @@ import { pageLinks } from '../data/navigationData';
|
|||||||
export const Navbar = () => {
|
export const Navbar = () => {
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const pathname = usePathname();
|
const rawPathname = usePathname();
|
||||||
|
const pathname = rawPathname?.replace(/\/$/, '') || '/';
|
||||||
|
|
||||||
const scrollToContact = () => {
|
const scrollToContact = () => {
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
@@ -66,7 +67,7 @@ export const Navbar = () => {
|
|||||||
|
|
||||||
{/* Desktop nav */}
|
{/* Desktop nav */}
|
||||||
<nav className="hidden items-center gap-8 md:flex" aria-label="Navegación principal">
|
<nav className="hidden items-center gap-8 md:flex" aria-label="Navegación principal">
|
||||||
{pageLinks.map((link) => {
|
{pageLinks.filter(l => l.href !== '/privacidad').map((link) => {
|
||||||
const active = pathname === link.href;
|
const active = pathname === link.href;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
export const pageLinks = [
|
export const pageLinks = [
|
||||||
{ label: 'Sobre Nosotros', href: '/sobre-nosotros' },
|
{ label: 'Sobre Nosotros', href: '/sobre-nosotros' },
|
||||||
{ label: 'Productos', href: '/productos' },
|
{ label: 'Productos', href: '/productos' },
|
||||||
|
{ 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: 'Sistemas', href: '#machinery' },
|
{ label: 'Soluciones', href: '#showcase' },
|
||||||
{ label: 'Anatomía', href: '#anatomy' },
|
{ label: 'Portafolio', href: '#portafolio' },
|
||||||
{ label: 'Proceso', href: '#proceso' },
|
{ label: 'Sistemas', href: '#machinery' },
|
||||||
{ label: 'Principios', href: '#principios' },
|
{ label: 'Proceso', href: '#proceso' },
|
||||||
|
{ label: 'Principios', href: '#principios' },
|
||||||
];
|
];
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import Loader from "../components/Loader";
|
||||||
|
import { useCompanies } from "../services/companies";
|
||||||
|
import { assetUrl } from "../services/api";
|
||||||
|
import type { ThemeProps } from "../types";
|
||||||
|
|
||||||
|
function PartnersInner({ isNavyBg }: ThemeProps) {
|
||||||
|
const { data, isLoading } = useCompanies();
|
||||||
|
|
||||||
|
const bg = isNavyBg ? "#071233" : "#f7f6f3";
|
||||||
|
const border = isNavyBg ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.07)";
|
||||||
|
const label = isNavyBg ? "rgba(255,255,255,0.28)" : "rgba(0,0,0,0.28)";
|
||||||
|
|
||||||
|
const partners = (data ?? []).filter((c) => c.isPartner && c.logoUrl);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
style={{ background: bg, borderBottom: `1px solid ${border}` }}
|
||||||
|
className="px-8 py-14 md:px-14"
|
||||||
|
>
|
||||||
|
<Loader className="py-8" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partners.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-label="Partners tecnológicos de Rynext"
|
||||||
|
style={{ background: bg, borderBottom: `1px solid ${border}` }}
|
||||||
|
className="px-8 py-14 md:px-14 transition-colors duration-500"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="mx-auto max-w-5xl"
|
||||||
|
initial={{ opacity: 0, y: 14 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="mb-10 text-center text-xs font-semibold uppercase tracking-[0.18em]"
|
||||||
|
style={{ color: label }}
|
||||||
|
>
|
||||||
|
Partners tecnológicos
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-8 md:gap-12">
|
||||||
|
{partners.map((p, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={p.id}
|
||||||
|
title={p.name}
|
||||||
|
initial={{ opacity: 0, scale: 0.92 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.4,
|
||||||
|
delay: i * 0.07,
|
||||||
|
ease: [0.22, 1, 0.36, 1],
|
||||||
|
}}
|
||||||
|
whileHover={{ scale: 1.06 }}
|
||||||
|
style={{ lineHeight: 1 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={assetUrl(p.logoUrl!)}
|
||||||
|
alt={p.name}
|
||||||
|
style={{
|
||||||
|
height: "auto",
|
||||||
|
width: "auto",
|
||||||
|
maxHeight: 150,
|
||||||
|
maxWidth: 200,
|
||||||
|
objectFit: "contain",
|
||||||
|
filter: "grayscale(1)",
|
||||||
|
opacity: 0.55,
|
||||||
|
transition: "filter 300ms, opacity 300ms",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
(e.currentTarget as HTMLImageElement).style.filter =
|
||||||
|
"grayscale(0)";
|
||||||
|
(e.currentTarget as HTMLImageElement).style.opacity = "1";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
(e.currentTarget as HTMLImageElement).style.filter =
|
||||||
|
"grayscale(1)";
|
||||||
|
(e.currentTarget as HTMLImageElement).style.opacity = "0.55";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Partners = (props: ThemeProps) => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
if (!mounted) return null;
|
||||||
|
return <PartnersInner {...props} />;
|
||||||
|
};
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import Loader from "../components/Loader";
|
||||||
|
import { usePortfolio } from "../services/portfolio";
|
||||||
|
import { assetUrl } from "../services/api";
|
||||||
|
import type { ThemeProps } from "../types";
|
||||||
|
|
||||||
|
function PortfolioInner({ isNavyBg }: ThemeProps) {
|
||||||
|
const { data, isLoading } = usePortfolio();
|
||||||
|
|
||||||
|
const bg = isNavyBg ? "#080c14" : "#f0efec";
|
||||||
|
const text = isNavyBg ? "#ffffff" : "#0a0a0a";
|
||||||
|
const sub = isNavyBg ? "rgba(255,255,255,0.45)" : "#6a6866";
|
||||||
|
const border = isNavyBg ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)";
|
||||||
|
const card = isNavyBg ? "rgba(255,255,255,0.03)" : "#ffffff";
|
||||||
|
const tagBg = isNavyBg ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.05)";
|
||||||
|
|
||||||
|
const projects = data ?? [];
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<section style={{ background: bg, borderBottom: `1px solid ${border}` }} className="px-8 py-28 md:px-14">
|
||||||
|
<Loader className="py-20" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projects.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="portafolio"
|
||||||
|
aria-labelledby="portfolio-heading"
|
||||||
|
style={{ background: bg, borderBottom: `1px solid ${border}` }}
|
||||||
|
className="px-8 py-28 md:px-14 transition-colors duration-500"
|
||||||
|
>
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<motion.div
|
||||||
|
className="mb-16"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="mb-3 block text-xs font-semibold uppercase tracking-[0.14em]"
|
||||||
|
style={{ color: "#3b82f6" }}
|
||||||
|
>
|
||||||
|
Casos de éxito
|
||||||
|
</span>
|
||||||
|
<h2
|
||||||
|
id="portfolio-heading"
|
||||||
|
className="text-headline"
|
||||||
|
style={{ maxWidth: "28ch", color: text }}
|
||||||
|
>
|
||||||
|
Construimos el sistema. <span style={{ color: "#3b82f6" }}>Tú operas el negocio.</span>
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 max-w-xl text-base leading-relaxed" style={{ color: sub }}>
|
||||||
|
Proyectos reales, resultados medibles. Cada implementación parte de entender la operación antes de escribir una sola línea de código.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{projects.map((p, i) => {
|
||||||
|
const cover = p.images.find((img) => img.isCover) ?? p.images.sort((a, b) => a.order - b.order)[0];
|
||||||
|
return (
|
||||||
|
<motion.article
|
||||||
|
key={p.id}
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, margin: "-40px" }}
|
||||||
|
transition={{ duration: 0.55, delay: i * 0.1, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
style={{
|
||||||
|
borderRadius: 14,
|
||||||
|
border: `1px solid ${border}`,
|
||||||
|
background: card,
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Cover image */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
aspectRatio: "16/9",
|
||||||
|
overflow: "hidden",
|
||||||
|
background: isNavyBg ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.03)",
|
||||||
|
borderBottom: `1px solid ${border}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cover ? (
|
||||||
|
<img
|
||||||
|
src={assetUrl(cover.url)}
|
||||||
|
alt={p.title}
|
||||||
|
style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke={sub} strokeWidth="1.5">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||||
|
<path d="m21 15-5-5L5 21" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div style={{ padding: "24px 24px 28px", display: "flex", flexDirection: "column", flex: 1 }}>
|
||||||
|
{p.clientCompanyName && (
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wider mb-2 block" style={{ color: "#3b82f6" }}>
|
||||||
|
{p.clientCompanyName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<h3
|
||||||
|
className="font-display font-bold mb-3"
|
||||||
|
style={{ fontSize: "1.05rem", color: text, letterSpacing: "-0.01em" }}
|
||||||
|
>
|
||||||
|
{p.title}
|
||||||
|
</h3>
|
||||||
|
{p.publicSummary && (
|
||||||
|
<p className="text-sm leading-relaxed mb-5" style={{ color: sub }}>
|
||||||
|
{p.publicSummary}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{p.technologies.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mt-auto">
|
||||||
|
{p.technologies.map((t) => (
|
||||||
|
<span
|
||||||
|
key={t.id}
|
||||||
|
style={{
|
||||||
|
fontSize: 10.5,
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: "3px 9px",
|
||||||
|
borderRadius: 99,
|
||||||
|
background: tagBg,
|
||||||
|
color: t.color ?? sub,
|
||||||
|
letterSpacing: "0.01em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Portfolio = (props: ThemeProps) => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
if (!mounted) return null;
|
||||||
|
return <PortfolioInner {...props} />;
|
||||||
|
};
|
||||||
@@ -39,21 +39,20 @@ export const Showcase = ({ isNavyBg }: ThemeProps) => {
|
|||||||
className="mb-3 block text-xs font-semibold uppercase tracking-[0.14em]"
|
className="mb-3 block text-xs font-semibold uppercase tracking-[0.14em]"
|
||||||
style={{ color: "#3b82f6" }}
|
style={{ color: "#3b82f6" }}
|
||||||
>
|
>
|
||||||
Lo que construimos
|
Nuestras soluciones
|
||||||
</span>
|
</span>
|
||||||
<h2
|
<h2
|
||||||
id="showcase-heading"
|
id="showcase-heading"
|
||||||
className="text-headline"
|
className="text-headline"
|
||||||
style={{ maxWidth: "28ch" }}
|
style={{ maxWidth: "28ch" }}
|
||||||
>
|
>
|
||||||
Sistemas reales para operaciones reales.
|
Software diseñado para tu operación.
|
||||||
</h2>
|
</h2>
|
||||||
<p
|
<p
|
||||||
className="mt-4 max-w-xl text-base leading-relaxed"
|
className="mt-4 max-w-xl text-base leading-relaxed"
|
||||||
style={{ color: sub }}
|
style={{ color: sub }}
|
||||||
>
|
>
|
||||||
No plantillas ni demos genéricos. Cada sistema sale de entender la
|
Cada módulo resuelve un problema real. Sin funciones de relleno, sin licencias por usuario, sin límites artificiales.
|
||||||
operación y construir la arquitectura correcta desde cero.
|
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface Company {
|
|||||||
name: string;
|
name: string;
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
showInLanding?: boolean;
|
showInLanding?: boolean;
|
||||||
|
isPartner?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCompanies() {
|
export function useCompanies() {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { apiFetch } from './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,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user