Primer vistaso

This commit is contained in:
unknown
2026-05-26 23:00:14 -04:00
commit bd93fb3bf2
30 changed files with 9330 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

5
AGENTS.md Normal file
View File

@@ -0,0 +1,5 @@
<!-- 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/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
@AGENTS.md

70
README.md Normal file
View File

@@ -0,0 +1,70 @@
# FlowSync Landing Page
> Repositorio principal de la landing page pública de FlowSync. Construida con **Next.js 16 + TypeScript + CSS Modules**.
![FlowSync Dashboard Mockup](https://raw.githubusercontent.com/flowsync/assets/main/mockup-preview.png)
## 🚀 Inicio rápido
```bash
# Instalar dependencias
npm install
# Iniciar servidor de desarrollo
npm run dev
```
Abre [http://localhost:3000](http://localhost:3000) en tu navegador para ver el resultado.
---
## 🛠 Stack tecnológico
- **Framework:** Next.js 16.2.6 (App Router)
- **Lenguaje:** TypeScript 5
- **Estilos:** CSS Modules + Vanilla CSS (Custom Properties)
- **Componentes:** React 19 (Server + Client Components)
---
## 📁 Arquitectura del proyecto
```text
landing-page/
├── src/
│ ├── app/
│ │ ├── globals.css # Design system tokens & utility classes
│ │ ├── layout.tsx # Root layout con importación de Inter font
│ │ └── page.tsx # Composición de página principal
│ │
│ └── components/
│ ├── Navbar/ # Navegación con scroll spy y menú móvil
│ ├── Hero/ # Sección principal con mockup de UI animado
│ ├── Features/ # Cuadrícula de características
│ ├── Pricing/ # Tabla de precios con toggle mensual/anual
│ ├── ContactForm/ # Formulario de lead con validación local
│ └── Footer/ # Enlaces secundarios y CTA final
```
---
## 🎨 Design System
El diseño sigue una estética B2B SaaS moderna:
- **Colores:** Blanco/Negro alto contraste con acento azul (`#0066ff`).
- **Tipografía:** Inter (sans-serif) optimizada para legibilidad.
- **Micro-interacciones:** Animaciones CSS nativas para entrada de elementos (`IntersectionObserver`), hovers táctiles en botones y cards.
- **Responsive:** Mobile-first approach asegurando legibilidad en cualquier dispositivo.
---
## 📝 Scripts disponibles
- `npm run dev`: Inicia el entorno de desarrollo con Turbopack.
- `npm run build`: Construye la aplicación para producción.
- `npm run start`: Inicia el servidor de producción con la build generada.
- `npm run lint`: Ejecuta ESLint para analizar el código.
---
*Desarrollado internamente por el equipo de ingeniería de FlowSync.*

18
eslint.config.mjs Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

7
next.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

6003
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "landing-page",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"next": "16.2.6",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.6",
"typescript": "^5"
}
}

1
public/file.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

314
src/app/globals.css Normal file
View File

@@ -0,0 +1,314 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
/* ============================================
DESIGN SYSTEM TOKENS
============================================ */
:root {
/* Colors */
--color-black: #0a0a0a;
--color-dark: #111111;
--color-gray-900: #1a1a1a;
--color-gray-800: #2d2d2d;
--color-gray-600: #555555;
--color-gray-400: #888888;
--color-gray-200: #e5e5e5;
--color-gray-100: #f5f5f5;
--color-gray-50: #fafafa;
--color-white: #ffffff;
/* Accent */
--color-accent: #0066ff;
--color-accent-dark: #0052cc;
--color-accent-light: #e8f0fe;
/* Status */
--color-success: #00c853;
--color-error: #ff3b3b;
/* Typography */
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--text-5xl: 3rem;
--text-6xl: 3.75rem;
--text-7xl: 4.5rem;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--space-16: 4rem;
--space-20: 5rem;
--space-24: 6rem;
--space-32: 8rem;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.10);
--shadow-xl: 0 24px 64px rgba(0, 0, 0, 0.12);
/* Transitions */
--transition-fast: 150ms ease;
--transition-base: 250ms ease;
--transition-slow: 400ms ease;
/* Max widths */
--max-width-sm: 640px;
--max-width-md: 768px;
--max-width-lg: 1024px;
--max-width-xl: 1280px;
--max-width-2xl: 1440px;
}
/* ============================================
RESET & BASE
============================================ */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--font-sans);
font-size: var(--text-base);
color: var(--color-dark);
background-color: var(--color-white);
line-height: 1.6;
overflow-x: hidden;
}
img, video {
max-width: 100%;
height: auto;
display: block;
}
a {
color: inherit;
text-decoration: none;
}
button {
cursor: pointer;
font-family: var(--font-sans);
border: none;
outline: none;
background: none;
}
input, textarea, select {
font-family: var(--font-sans);
}
/* ============================================
UTILITIES
============================================ */
.container {
width: 100%;
max-width: var(--max-width-xl);
margin: 0 auto;
padding: 0 var(--space-6);
}
.section {
padding: var(--space-24) 0;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-6);
font-size: var(--text-base);
font-weight: 600;
border-radius: var(--radius-md);
transition: all var(--transition-base);
letter-spacing: -0.01em;
white-space: nowrap;
}
.btn-primary {
background: var(--color-black);
color: var(--color-white);
border: 2px solid var(--color-black);
}
.btn-primary:hover {
background: var(--color-gray-900);
border-color: var(--color-gray-900);
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.btn-secondary {
background: transparent;
color: var(--color-dark);
border: 2px solid var(--color-gray-200);
}
.btn-secondary:hover {
border-color: var(--color-dark);
transform: translateY(-1px);
}
.btn-accent {
background: var(--color-accent);
color: var(--color-white);
border: 2px solid var(--color-accent);
}
.btn-accent:hover {
background: var(--color-accent-dark);
border-color: var(--color-accent-dark);
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(0, 102, 255, 0.3);
}
.btn-lg {
padding: var(--space-4) var(--space-8);
font-size: var(--text-lg);
border-radius: var(--radius-lg);
}
/* Badges */
.badge {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
border-radius: var(--radius-full);
}
.badge-dark {
background: var(--color-black);
color: var(--color-white);
}
.badge-accent {
background: var(--color-accent-light);
color: var(--color-accent);
}
/* ============================================
SCROLLBAR
============================================ */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-gray-100);
}
::-webkit-scrollbar-thumb {
background: var(--color-gray-400);
border-radius: var(--radius-full);
}
/* ============================================
ANIMATIONS
============================================ */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-fadeInUp {
animation: fadeInUp 0.6s ease forwards;
}
.animate-fadeIn {
animation: fadeIn 0.5s ease forwards;
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 768px) {
.container {
padding: 0 var(--space-4);
}
.section {
padding: var(--space-16) 0;
}
}

26
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,26 @@
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'FlowSync — El SaaS que impulsa tu equipo',
description:
'Automatiza flujos de trabajo, conecta equipos y escala tu negocio con FlowSync. La plataforma SaaS todo-en-uno para equipos modernos.',
keywords: ['SaaS', 'productividad', 'automatización', 'equipos', 'gestión de proyectos'],
openGraph: {
title: 'FlowSync — El SaaS que impulsa tu equipo',
description: 'Automatiza flujos de trabajo y escala tu negocio con FlowSync.',
type: 'website',
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="es">
<body>{children}</body>
</html>
);
}

21
src/app/page.tsx Normal file
View File

@@ -0,0 +1,21 @@
import Navbar from '@/components/Navbar/Navbar';
import Hero from '@/components/Hero/Hero';
import Features from '@/components/Features/Features';
import Pricing from '@/components/Pricing/Pricing';
import ContactForm from '@/components/ContactForm/ContactForm';
import Footer from '@/components/Footer/Footer';
export default function Home() {
return (
<>
<Navbar />
<main id="main-content">
<Hero />
<Features />
<Pricing />
<ContactForm />
</main>
<Footer />
</>
);
}

View File

@@ -0,0 +1,367 @@
.contactSection {
background: var(--color-white);
}
/* Layout */
.layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-16);
align-items: start;
}
/* Reveal */
.reveal {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.65s ease, transform 0.65s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
/* Info panel */
.infoPanel {
display: flex;
flex-direction: column;
gap: var(--space-6);
position: sticky;
top: calc(72px + var(--space-8));
}
.title {
font-size: clamp(var(--text-3xl), 5vw, var(--text-5xl));
font-weight: 900;
letter-spacing: -0.04em;
line-height: 1.05;
color: var(--color-black);
}
.subtitle {
font-size: var(--text-lg);
color: var(--color-gray-600);
line-height: 1.6;
}
/* Contact details */
.contactDetails {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-6);
background: var(--color-gray-50);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-xl);
}
.contactItem {
display: flex;
align-items: center;
gap: var(--space-4);
}
.contactIcon {
width: 40px;
height: 40px;
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--color-black);
}
.contactLabel {
font-size: var(--text-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-gray-400);
}
.contactValue {
font-size: var(--text-base);
font-weight: 600;
color: var(--color-black);
}
/* Testimonial */
.testimonial {
background: var(--color-black);
color: var(--color-white);
border-radius: var(--radius-xl);
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.testimonialText {
font-size: var(--text-base);
line-height: 1.65;
color: rgba(255,255,255,0.85);
font-style: italic;
}
.testimonialAuthor {
display: flex;
align-items: center;
gap: var(--space-3);
}
.authorAvatar {
width: 40px;
height: 40px;
background: rgba(255,255,255,0.15);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.02em;
flex-shrink: 0;
}
.authorName {
font-size: var(--text-sm);
font-weight: 700;
}
.authorRole {
font-size: var(--text-xs);
color: rgba(255,255,255,0.5);
}
/* Form panel */
.formPanel {
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-2xl);
padding: var(--space-10);
box-shadow: var(--shadow-lg);
}
/* Form */
.form {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.formHeader {
margin-bottom: var(--space-2);
}
.formTitle {
font-size: var(--text-2xl);
font-weight: 800;
letter-spacing: -0.03em;
color: var(--color-black);
}
.formSubtitle {
font-size: var(--text-sm);
color: var(--color-gray-400);
margin-top: var(--space-1);
}
/* Row */
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-4);
}
/* Field */
.fieldGroup {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-dark);
}
.required {
color: var(--color-error);
}
.optional {
font-weight: 400;
color: var(--color-gray-400);
}
/* Inputs */
.input,
.textarea {
width: 100%;
padding: var(--space-3) var(--space-4);
font-size: var(--text-base);
font-family: var(--font-sans);
color: var(--color-dark);
background: var(--color-white);
border: 1.5px solid var(--color-gray-200);
border-radius: var(--radius-lg);
transition: all var(--transition-fast);
outline: none;
}
.input::placeholder,
.textarea::placeholder {
color: var(--color-gray-400);
}
.input:focus,
.textarea:focus {
border-color: var(--color-black);
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.06);
}
.inputError {
border-color: var(--color-error) !important;
box-shadow: 0 0 0 3px rgba(255, 59, 59, 0.08) !important;
}
.textarea {
resize: vertical;
min-height: 120px;
}
.textareaFooter {
display: flex;
justify-content: space-between;
align-items: center;
}
/* Error */
.errorMsg {
font-size: var(--text-xs);
color: var(--color-error);
font-weight: 500;
display: flex;
align-items: center;
gap: var(--space-1);
}
.charCount {
font-size: var(--text-xs);
color: var(--color-gray-400);
margin-left: auto;
}
/* Submit */
.submitBtn {
width: 100%;
justify-content: center;
margin-top: var(--space-2);
}
.submitBtn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none !important;
}
/* Spinner */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.7s linear infinite;
flex-shrink: 0;
}
/* Privacy note */
.privacyNote {
font-size: var(--text-xs);
color: var(--color-gray-400);
text-align: center;
line-height: 1.5;
}
.privacyLink {
color: var(--color-gray-600);
text-decoration: underline;
text-underline-offset: 2px;
transition: color var(--transition-fast);
}
.privacyLink:hover {
color: var(--color-black);
}
/* Success state */
.successState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: var(--space-4);
padding: var(--space-16) var(--space-8);
animation: scaleIn 0.4s ease;
}
.successIcon {
width: 72px;
height: 72px;
background: var(--color-black);
color: var(--color-white);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.successTitle {
font-size: var(--text-2xl);
font-weight: 800;
letter-spacing: -0.03em;
color: var(--color-black);
}
.successDesc {
font-size: var(--text-base);
color: var(--color-gray-600);
max-width: 320px;
line-height: 1.6;
}
/* Responsive */
@media (max-width: 1024px) {
.layout {
grid-template-columns: 1fr;
gap: var(--space-12);
}
.infoPanel {
position: static;
}
.formPanel {
padding: var(--space-8);
}
}
@media (max-width: 640px) {
.row {
grid-template-columns: 1fr;
}
.formPanel {
padding: var(--space-6);
}
}

View File

@@ -0,0 +1,402 @@
'use client';
import { useState, useRef, useEffect, FormEvent } from 'react';
import styles from './ContactForm.module.css';
type FormData = {
name: string;
email: string;
company: string;
phone: string;
message: string;
};
type FormErrors = Partial<Record<keyof FormData, string>>;
type SubmitStatus = 'idle' | 'loading' | 'success' | 'error';
const initialData: FormData = {
name: '',
email: '',
company: '',
phone: '',
message: '',
};
function validateForm(data: FormData): FormErrors {
const errors: FormErrors = {};
if (!data.name.trim()) errors.name = 'El nombre es requerido';
if (!data.email.trim()) {
errors.email = 'El email es requerido';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.email = 'Ingresa un email válido';
}
if (!data.company.trim()) errors.company = 'La empresa es requerida';
if (data.phone && !/^[+\d\s\-().]{7,20}$/.test(data.phone)) {
errors.phone = 'Ingresa un teléfono válido';
}
if (!data.message.trim()) {
errors.message = 'El mensaje es requerido';
} else if (data.message.trim().length < 10) {
errors.message = 'El mensaje debe tener al menos 10 caracteres';
}
return errors;
}
export default function ContactForm() {
const [formData, setFormData] = useState<FormData>(initialData);
const [errors, setErrors] = useState<FormErrors>({});
const [touched, setTouched] = useState<Partial<Record<keyof FormData, boolean>>>({});
const [status, setStatus] = useState<SubmitStatus>('idle');
const sectionRef = useRef<HTMLElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) entry.target.classList.add(styles.visible);
});
},
{ threshold: 0.1 }
);
const elements = sectionRef.current?.querySelectorAll(`.${styles.reveal}`);
elements?.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
if (touched[name as keyof FormData]) {
const newErrors = validateForm({ ...formData, [name]: value });
setErrors((prev) => ({ ...prev, [name]: newErrors[name as keyof FormData] }));
}
};
const handleBlur = (
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name } = e.target;
setTouched((prev) => ({ ...prev, [name]: true }));
const newErrors = validateForm(formData);
setErrors((prev) => ({ ...prev, [name]: newErrors[name as keyof FormData] }));
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const allTouched = Object.keys(formData).reduce(
(acc, k) => ({ ...acc, [k]: true }),
{} as Record<keyof FormData, boolean>
);
setTouched(allTouched);
const validationErrors = validateForm(formData);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setStatus('loading');
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1800));
setStatus('success');
setFormData(initialData);
setTouched({});
setErrors({});
};
const handleReset = () => {
setStatus('idle');
setFormData(initialData);
setErrors({});
setTouched({});
};
return (
<section
className={`section ${styles.contactSection}`}
id="contact"
ref={sectionRef}
aria-labelledby="contact-heading"
>
<div className="container">
<div className={styles.layout}>
{/* Left info panel */}
<div className={`${styles.reveal} ${styles.infoPanel}`}>
<span className="badge badge-dark">Contacto</span>
<h2 id="contact-heading" className={styles.title}>
Hablemos de
<br />
tu negocio
</h2>
<p className={styles.subtitle}>
Cuéntanos qué necesitas. Nuestro equipo responderá en menos de 24 horas
con una propuesta personalizada.
</p>
{/* Contact details */}
<div className={styles.contactDetails}>
<div className={styles.contactItem}>
<div className={styles.contactIcon}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" stroke="currentColor" strokeWidth="2" />
<polyline points="22,6 12,13 2,6" stroke="currentColor" strokeWidth="2" />
</svg>
</div>
<div>
<div className={styles.contactLabel}>Email</div>
<div className={styles.contactValue}>hola@flowsync.io</div>
</div>
</div>
<div className={styles.contactItem}>
<div className={styles.contactIcon}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.62a19.79 19.79 0 01-3.07-8.63A2 2 0 012.18 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.91 7.91a16 16 0 006.18 6.18l1.28-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z" stroke="currentColor" strokeWidth="2" />
</svg>
</div>
<div>
<div className={styles.contactLabel}>Teléfono</div>
<div className={styles.contactValue}>+1 (800) 123-4567</div>
</div>
</div>
<div className={styles.contactItem}>
<div className={styles.contactIcon}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
<polyline points="12 6 12 12 16 14" stroke="currentColor" strokeWidth="2" />
</svg>
</div>
<div>
<div className={styles.contactLabel}>Respuesta</div>
<div className={styles.contactValue}>Menos de 24 horas</div>
</div>
</div>
</div>
{/* Testimonial */}
<blockquote className={styles.testimonial}>
<p className={styles.testimonialText}>
&ldquo;FlowSync transformó la forma en que colaboramos. Lo que antes
tomaba días, ahora lo hacemos en horas.&rdquo;
</p>
<footer className={styles.testimonialAuthor}>
<div className={styles.authorAvatar}>MR</div>
<div>
<div className={styles.authorName}>María Rodríguez</div>
<div className={styles.authorRole}>CTO en TechLatam</div>
</div>
</footer>
</blockquote>
</div>
{/* Form panel */}
<div className={`${styles.reveal} ${styles.formPanel}`} style={{ transitionDelay: '0.15s' }}>
{status === 'success' ? (
<div className={styles.successState} role="alert" aria-live="polite">
<div className={styles.successIcon}>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<path d="M6 16l7 7L26 9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<h3 className={styles.successTitle}>¡Mensaje enviado!</h3>
<p className={styles.successDesc}>
Gracias por contactarnos. Nuestro equipo te responderá en menos de 24 horas.
</p>
<button
className="btn btn-secondary"
onClick={handleReset}
id="contact-send-another-btn"
>
Enviar otro mensaje
</button>
</div>
) : (
<form
className={styles.form}
onSubmit={handleSubmit}
noValidate
aria-label="Formulario de contacto"
id="contact-form"
>
<div className={styles.formHeader}>
<h3 className={styles.formTitle}>Envíanos un mensaje</h3>
<p className={styles.formSubtitle}>Todos los campos marcados con * son requeridos</p>
</div>
{/* Row: Name + Email */}
<div className={styles.row}>
<div className={styles.fieldGroup}>
<label htmlFor="contact-name" className={styles.label}>
Nombre completo <span className={styles.required}>*</span>
</label>
<input
id="contact-name"
name="name"
type="text"
className={`${styles.input} ${errors.name && touched.name ? styles.inputError : ''}`}
placeholder="Juan García"
value={formData.name}
onChange={handleChange}
onBlur={handleBlur}
autoComplete="name"
aria-required="true"
aria-describedby={errors.name && touched.name ? 'name-error' : undefined}
aria-invalid={!!(errors.name && touched.name)}
/>
{errors.name && touched.name && (
<span id="name-error" className={styles.errorMsg} role="alert">
{errors.name}
</span>
)}
</div>
<div className={styles.fieldGroup}>
<label htmlFor="contact-email" className={styles.label}>
Email <span className={styles.required}>*</span>
</label>
<input
id="contact-email"
name="email"
type="email"
className={`${styles.input} ${errors.email && touched.email ? styles.inputError : ''}`}
placeholder="juan@empresa.com"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
autoComplete="email"
aria-required="true"
aria-describedby={errors.email && touched.email ? 'email-error' : undefined}
aria-invalid={!!(errors.email && touched.email)}
/>
{errors.email && touched.email && (
<span id="email-error" className={styles.errorMsg} role="alert">
{errors.email}
</span>
)}
</div>
</div>
{/* Row: Company + Phone */}
<div className={styles.row}>
<div className={styles.fieldGroup}>
<label htmlFor="contact-company" className={styles.label}>
Empresa <span className={styles.required}>*</span>
</label>
<input
id="contact-company"
name="company"
type="text"
className={`${styles.input} ${errors.company && touched.company ? styles.inputError : ''}`}
placeholder="Mi Empresa S.A."
value={formData.company}
onChange={handleChange}
onBlur={handleBlur}
autoComplete="organization"
aria-required="true"
aria-describedby={errors.company && touched.company ? 'company-error' : undefined}
aria-invalid={!!(errors.company && touched.company)}
/>
{errors.company && touched.company && (
<span id="company-error" className={styles.errorMsg} role="alert">
{errors.company}
</span>
)}
</div>
<div className={styles.fieldGroup}>
<label htmlFor="contact-phone" className={styles.label}>
Teléfono
<span className={styles.optional}> (opcional)</span>
</label>
<input
id="contact-phone"
name="phone"
type="tel"
className={`${styles.input} ${errors.phone && touched.phone ? styles.inputError : ''}`}
placeholder="+52 55 1234 5678"
value={formData.phone}
onChange={handleChange}
onBlur={handleBlur}
autoComplete="tel"
aria-describedby={errors.phone && touched.phone ? 'phone-error' : undefined}
aria-invalid={!!(errors.phone && touched.phone)}
/>
{errors.phone && touched.phone && (
<span id="phone-error" className={styles.errorMsg} role="alert">
{errors.phone}
</span>
)}
</div>
</div>
{/* Message */}
<div className={styles.fieldGroup}>
<label htmlFor="contact-message" className={styles.label}>
Mensaje <span className={styles.required}>*</span>
</label>
<textarea
id="contact-message"
name="message"
className={`${styles.textarea} ${errors.message && touched.message ? styles.inputError : ''}`}
placeholder="Cuéntanos sobre tu proyecto, equipo y qué quieres lograr con FlowSync..."
rows={5}
value={formData.message}
onChange={handleChange}
onBlur={handleBlur}
aria-required="true"
aria-describedby={errors.message && touched.message ? 'message-error' : undefined}
aria-invalid={!!(errors.message && touched.message)}
/>
<div className={styles.textareaFooter}>
{errors.message && touched.message ? (
<span id="message-error" className={styles.errorMsg} role="alert">
{errors.message}
</span>
) : (
<span />
)}
<span className={styles.charCount}>
{formData.message.length} caracteres
</span>
</div>
</div>
{/* Submit */}
<button
type="submit"
className={`btn btn-primary btn-lg ${styles.submitBtn}`}
disabled={status === 'loading'}
id="contact-submit-btn"
aria-busy={status === 'loading'}
>
{status === 'loading' ? (
<>
<span className={styles.spinner} aria-hidden="true" />
Enviando...
</>
) : (
<>
Enviar mensaje
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M2 8h12M10 4l4 4-4 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</>
)}
</button>
<p className={styles.privacyNote}>
Al enviar, aceptas nuestra{' '}
<a href="#" className={styles.privacyLink}>política de privacidad</a>.
Nunca compartiremos tu información.
</p>
</form>
)}
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,150 @@
.features {
background: var(--color-gray-50);
border-top: 1px solid var(--color-gray-200);
border-bottom: 1px solid var(--color-gray-200);
}
/* Reveal */
.reveal {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.65s ease, transform 0.65s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
/* Header */
.header {
text-align: center;
max-width: 600px;
margin: 0 auto var(--space-16);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
}
.title {
font-size: clamp(var(--text-3xl), 5vw, var(--text-5xl));
font-weight: 900;
letter-spacing: -0.04em;
line-height: 1.1;
color: var(--color-black);
}
.titleAccent {
color: var(--color-gray-600);
}
.subtitle {
font-size: var(--text-lg);
color: var(--color-gray-600);
line-height: 1.6;
}
/* Grid */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-4);
}
/* Card */
.card {
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-xl);
padding: var(--space-8);
display: flex;
flex-direction: column;
gap: var(--space-4);
transition: all var(--transition-base);
cursor: default;
}
.card:hover {
border-color: var(--color-black);
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.cardIcon {
width: 48px;
height: 48px;
background: var(--color-black);
color: var(--color-white);
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform var(--transition-base);
}
.card:hover .cardIcon {
transform: scale(1.05) rotate(-2deg);
}
.cardContent {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.cardTag {
font-size: var(--text-xs);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-gray-400);
}
.cardTitle {
font-size: var(--text-xl);
font-weight: 800;
letter-spacing: -0.03em;
color: var(--color-black);
line-height: 1.2;
}
.cardDesc {
font-size: var(--text-sm);
color: var(--color-gray-600);
line-height: 1.65;
}
/* Bottom CTA */
.bottomCta {
text-align: center;
margin-top: var(--space-16);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
}
.ctaText {
font-size: var(--text-xl);
font-weight: 700;
color: var(--color-black);
letter-spacing: -0.02em;
}
/* Responsive */
@media (max-width: 1024px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.grid {
grid-template-columns: 1fr;
}
.card {
padding: var(--space-6);
}
}

View File

@@ -0,0 +1,149 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import styles from './Features.module.css';
const features = [
{
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
),
title: 'Automatización inteligente',
description:
'Crea flujos de trabajo sin código. Conecta apps, dispara acciones y elimina tareas manuales con nuestra IA incorporada.',
tag: 'IA',
},
{
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zM23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
),
title: 'Colaboración en tiempo real',
description:
'Tu equipo siempre sincronizado. Comenta, asigna tareas y sigue el progreso de cada proyecto en tiempo real.',
tag: 'Equipos',
},
{
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
),
title: 'Analíticas avanzadas',
description:
'Paneles personalizables con métricas de negocio en tiempo real. Toma decisiones basadas en datos, no en suposiciones.',
tag: 'Analytics',
},
{
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" stroke="currentColor" strokeWidth="2" />
<path d="M7 11V7a5 5 0 0110 0v4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
),
title: 'Seguridad enterprise',
description:
'Cifrado end-to-end, SSO, auditoría de accesos y cumplimiento SOC 2 Tipo II. Tu información, siempre protegida.',
tag: 'Seguridad',
},
{
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
<path d="M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" stroke="currentColor" strokeWidth="2" />
</svg>
),
title: '200+ integraciones',
description:
'Conecta con Slack, Notion, GitHub, Salesforce y más. FlowSync se adapta a tu stack, no al revés.',
tag: 'Integraciones',
},
{
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
),
title: 'Soporte 24/7',
description:
'Equipo de soporte dedicado disponible en cualquier zona horaria. Tiempo de respuesta promedio: menos de 2 minutos.',
tag: 'Soporte',
},
];
export default function Features() {
const sectionRef = useRef<HTMLElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add(styles.visible);
}
});
},
{ threshold: 0.1 }
);
const elements = sectionRef.current?.querySelectorAll(`.${styles.reveal}`);
elements?.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
return (
<section className={`section ${styles.features}`} id="features" ref={sectionRef} aria-labelledby="features-heading">
<div className="container">
{/* Header */}
<div className={`${styles.reveal} ${styles.header}`}>
<span className="badge badge-accent">Características</span>
<h2 id="features-heading" className={styles.title}>
Todo lo que necesitas,
<br />
<span className={styles.titleAccent}>nada que no necesitas</span>
</h2>
<p className={styles.subtitle}>
Diseñado para equipos que quieren mover rápido sin romper cosas.
Potente cuando lo necesitas, simple cuando no.
</p>
</div>
{/* Grid */}
<div className={styles.grid}>
{features.map((feature, i) => (
<article
key={feature.title}
className={`${styles.reveal} ${styles.card}`}
style={{ transitionDelay: `${i * 0.08}s` }}
aria-label={feature.title}
>
<div className={styles.cardIcon}>{feature.icon}</div>
<div className={styles.cardContent}>
<div className={styles.cardTag}>{feature.tag}</div>
<h3 className={styles.cardTitle}>{feature.title}</h3>
<p className={styles.cardDesc}>{feature.description}</p>
</div>
</article>
))}
</div>
{/* Bottom CTA */}
<div className={`${styles.reveal} ${styles.bottomCta}`}>
<p className={styles.ctaText}>
¿Listo para transformar cómo trabaja tu equipo?
</p>
<button
className="btn btn-primary btn-lg"
id="features-cta-btn"
onClick={() => document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' })}
>
Ver todas las funciones
</button>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,230 @@
.footer {
background: var(--color-black);
color: var(--color-white);
}
/* CTA Banner */
.ctaBanner {
background: var(--color-gray-900);
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.ctaInner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-8);
padding: var(--space-12) 0;
flex-wrap: wrap;
}
.ctaText {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.ctaTitle {
font-size: clamp(var(--text-2xl), 4vw, var(--text-4xl));
font-weight: 900;
letter-spacing: -0.04em;
color: var(--color-white);
}
.ctaSubtitle {
font-size: var(--text-base);
color: rgba(255,255,255,0.5);
}
.ctaBtns {
display: flex;
gap: var(--space-3);
flex-shrink: 0;
flex-wrap: wrap;
}
/* Main footer */
.main {
padding: var(--space-16) 0 var(--space-8);
}
.topRow {
display: grid;
grid-template-columns: 280px 1fr;
gap: var(--space-16);
margin-bottom: var(--space-12);
}
/* Brand */
.brand {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.brandLogo {
display: flex;
align-items: center;
gap: var(--space-2);
}
.logoMark {
width: 36px;
height: 36px;
background: var(--color-white);
color: var(--color-black);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
font-weight: 800;
letter-spacing: -0.02em;
}
.logoText {
font-size: var(--text-lg);
font-weight: 800;
letter-spacing: -0.04em;
color: var(--color-white);
}
.brandDesc {
font-size: var(--text-sm);
color: rgba(255,255,255,0.45);
line-height: 1.6;
}
/* Socials */
.socials {
display: flex;
gap: var(--space-2);
}
.socialLink {
width: 36px;
height: 36px;
background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.6);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-fast);
}
.socialLink:hover {
background: rgba(255,255,255,0.15);
color: var(--color-white);
}
/* Links grid */
.linksGrid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-8);
}
.linkColumn {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.linkCategory {
font-size: var(--text-xs);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(255,255,255,0.35);
}
.linkList {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.footerLink {
font-size: var(--text-sm);
color: rgba(255,255,255,0.55);
text-decoration: none;
transition: color var(--transition-fast);
}
.footerLink:hover {
color: var(--color-white);
}
/* Bottom bar */
.bottomBar {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: var(--space-8);
border-top: 1px solid rgba(255,255,255,0.08);
flex-wrap: wrap;
gap: var(--space-4);
}
.copyright {
font-size: var(--text-sm);
color: rgba(255,255,255,0.35);
}
.statusBadge {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-xs);
font-weight: 500;
color: rgba(255,255,255,0.45);
}
.statusDot {
width: 8px;
height: 8px;
background: var(--color-success);
border-radius: 50%;
animation: pulse 2s ease infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(0, 200, 83, 0.4); }
50% { opacity: 0.8; box-shadow: 0 0 0 4px rgba(0, 200, 83, 0); }
}
/* Responsive */
@media (max-width: 1024px) {
.topRow {
grid-template-columns: 1fr;
gap: var(--space-12);
}
.linksGrid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.ctaInner {
flex-direction: column;
text-align: center;
}
.ctaBtns {
width: 100%;
flex-direction: column;
}
.ctaBtns .btn {
width: 100%;
justify-content: center;
}
.linksGrid {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-6);
}
}

View File

@@ -0,0 +1,138 @@
'use client';
import styles from './Footer.module.css';
const footerLinks = {
Producto: ['Características', 'Precios', 'Integraciones', 'Changelog', 'Roadmap'],
Empresa: ['Sobre nosotros', 'Blog', 'Carreras', 'Prensa', 'Partners'],
Recursos: ['Documentación', 'API', 'Comunidad', 'Webinars', 'Templates'],
Legal: ['Privacidad', 'Términos', 'Cookies', 'Seguridad', 'GDPR'],
};
const socials = [
{
name: 'Twitter',
href: '#',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
),
},
{
name: 'LinkedIn',
href: '#',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
),
},
{
name: 'GitHub',
href: '#',
icon: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z" />
</svg>
),
},
];
export default function Footer() {
return (
<footer className={styles.footer} role="contentinfo">
{/* CTA Banner */}
<div className={styles.ctaBanner}>
<div className="container">
<div className={styles.ctaInner}>
<div className={styles.ctaText}>
<h2 className={styles.ctaTitle}>
Empieza hoy mismo es gratis
</h2>
<p className={styles.ctaSubtitle}>
Únete a más de 10,000 equipos que ya usan FlowSync
</p>
</div>
<div className={styles.ctaBtns}>
<button
className="btn btn-accent btn-lg"
id="footer-cta-btn"
onClick={() => document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' })}
>
Prueba gratis 14 días
</button>
<button
className="btn btn-secondary btn-lg"
id="footer-demo-btn"
style={{ background: 'rgba(255,255,255,0.1)', borderColor: 'rgba(255,255,255,0.2)', color: 'white' }}
>
Ver demo
</button>
</div>
</div>
</div>
</div>
{/* Main footer */}
<div className={styles.main}>
<div className="container">
<div className={styles.topRow}>
{/* Brand */}
<div className={styles.brand}>
<div className={styles.brandLogo}>
<span className={styles.logoMark}>FS</span>
<span className={styles.logoText}>FlowSync</span>
</div>
<p className={styles.brandDesc}>
La plataforma todo-en-uno para equipos que quieren moverse rápido sin perder el control.
</p>
<div className={styles.socials}>
{socials.map((s) => (
<a
key={s.name}
href={s.href}
className={styles.socialLink}
aria-label={s.name}
id={`footer-social-${s.name.toLowerCase()}`}
>
{s.icon}
</a>
))}
</div>
</div>
{/* Links */}
<nav className={styles.linksGrid} aria-label="Footer navigation">
{Object.entries(footerLinks).map(([category, links]) => (
<div key={category} className={styles.linkColumn}>
<h3 className={styles.linkCategory}>{category}</h3>
<ul className={styles.linkList} role="list">
{links.map((link) => (
<li key={link}>
<a href="#" className={styles.footerLink}>
{link}
</a>
</li>
))}
</ul>
</div>
))}
</nav>
</div>
{/* Bottom bar */}
<div className={styles.bottomBar}>
<p className={styles.copyright}>
© {new Date().getFullYear()} FlowSync, Inc. Todos los derechos reservados.
</p>
<div className={styles.statusBadge}>
<span className={styles.statusDot} />
Todos los sistemas operativos
</div>
</div>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,370 @@
.hero {
padding-top: 72px; /* navbar height */
background: var(--color-white);
overflow: hidden;
}
.inner {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding-top: var(--space-20);
padding-bottom: var(--space-16);
gap: var(--space-6);
}
/* Reveal animation */
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.7s ease, transform 0.7s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
.reveal:nth-child(2) { transition-delay: 0.1s; }
.reveal:nth-child(3) { transition-delay: 0.2s; }
.reveal:nth-child(4) { transition-delay: 0.3s; }
.reveal:nth-child(5) { transition-delay: 0.4s; }
.reveal:nth-child(6) { transition-delay: 0.5s; }
.reveal:nth-child(7) { transition-delay: 0.6s; }
.reveal:nth-child(8) { transition-delay: 0.7s; }
/* Badge */
.badgeWrap {
margin-bottom: var(--space-2);
}
/* Heading */
.heading {
font-size: clamp(2.5rem, 7vw, 5rem);
font-weight: 900;
letter-spacing: -0.04em;
line-height: 1.05;
color: var(--color-black);
max-width: 800px;
}
.emphasis {
font-style: italic;
font-weight: 900;
}
/* Subheading */
.subheading {
font-size: clamp(var(--text-base), 2vw, var(--text-xl));
color: var(--color-gray-600);
max-width: 520px;
line-height: 1.6;
margin-top: var(--space-2);
}
/* CTAs */
.ctas {
display: flex;
gap: var(--space-3);
flex-wrap: wrap;
justify-content: center;
margin-top: var(--space-2);
}
/* Trust note */
.trustNote {
font-size: var(--text-sm);
color: var(--color-gray-400);
margin-top: var(--space-1);
}
/* Dashboard Mockup */
.mockupWrap {
width: 100%;
max-width: 900px;
margin-top: var(--space-8);
perspective: 1200px;
}
.mockup {
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl), 0 0 0 1px rgba(0, 0, 0, 0.03);
overflow: hidden;
transform: rotateX(4deg);
transition: transform 0.4s ease;
}
.mockup:hover {
transform: rotateX(0deg);
}
/* Window bar */
.mockupBar {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-gray-50);
border-bottom: 1px solid var(--color-gray-200);
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.mockupUrl {
flex: 1;
text-align: center;
font-size: var(--text-xs);
color: var(--color-gray-400);
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-full);
padding: 2px var(--space-3);
max-width: 280px;
margin: 0 auto;
}
/* Mockup body */
.mockupBody {
display: flex;
height: 380px;
}
/* Sidebar */
.mockupSidebar {
width: 56px;
background: var(--color-gray-50);
border-right: 1px solid var(--color-gray-200);
padding: var(--space-4) var(--space-3);
display: flex;
flex-direction: column;
gap: var(--space-3);
flex-shrink: 0;
}
.sidebarLogo {
width: 32px;
height: 32px;
background: var(--color-black);
border-radius: var(--radius-md);
margin-bottom: var(--space-4);
}
.sidebarItem {
width: 32px;
height: 32px;
background: var(--color-gray-200);
border-radius: var(--radius-md);
}
.sidebarActive {
background: var(--color-black) !important;
}
/* Main dashboard */
.mockupMain {
flex: 1;
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-4);
overflow: hidden;
}
.mockupHeader {
display: flex;
justify-content: space-between;
align-items: center;
}
.mockupTitle {
height: 20px;
width: 160px;
background: var(--color-gray-200);
border-radius: var(--radius-sm);
}
.mockupBtn {
height: 32px;
width: 100px;
background: var(--color-black);
border-radius: var(--radius-md);
}
/* Metric cards */
.metricsRow {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-3);
}
.metricCard {
background: var(--color-gray-50);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
padding: var(--space-3);
display: flex;
gap: var(--space-2);
align-items: center;
}
.metricIcon {
width: 32px;
height: 32px;
border-radius: var(--radius-md);
flex-shrink: 0;
}
.metricLines {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
}
.metricValue {
height: 14px;
background: var(--color-gray-300);
border-radius: var(--radius-sm);
width: 70%;
}
.metricLabel {
height: 10px;
background: var(--color-gray-200);
border-radius: var(--radius-sm);
width: 90%;
}
/* Chart */
.chartArea {
flex: 1;
background: var(--color-gray-50);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
padding: var(--space-4);
overflow: hidden;
}
.chartBars {
display: flex;
align-items: flex-end;
gap: var(--space-2);
height: 100%;
}
@keyframes growBar {
from { height: 0; }
to { height: var(--target-height); }
}
.bar {
flex: 1;
background: var(--color-black);
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
opacity: 0.12;
animation: fadeIn 0.5s ease forwards;
}
.bar:nth-child(odd) {
opacity: 0.08;
}
.bar:nth-child(4),
.bar:nth-child(8) {
opacity: 0.9;
background: var(--color-black);
}
/* Task list */
.taskList {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.taskRow {
display: flex;
align-items: center;
gap: var(--space-3);
}
.taskCheck {
width: 16px;
height: 16px;
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-sm);
flex-shrink: 0;
}
.taskLine {
height: 10px;
background: var(--color-gray-200);
border-radius: var(--radius-sm);
}
/* Stats */
.stats {
display: flex;
gap: var(--space-12);
margin-top: var(--space-8);
flex-wrap: wrap;
justify-content: center;
}
.statItem {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-1);
}
.statValue {
font-size: var(--text-3xl);
font-weight: 900;
letter-spacing: -0.04em;
color: var(--color-black);
}
.statLabel {
font-size: var(--text-sm);
color: var(--color-gray-500, #666);
}
/* Responsive */
@media (max-width: 768px) {
.mockupBody {
height: 260px;
}
.mockupSidebar {
display: none;
}
.metricsRow {
grid-template-columns: repeat(2, 1fr);
}
.stats {
gap: var(--space-8);
}
}
@media (max-width: 480px) {
.ctas {
flex-direction: column;
width: 100%;
max-width: 320px;
}
.ctas .btn {
width: 100%;
}
}

View File

@@ -0,0 +1,177 @@
'use client';
import { useEffect, useRef } from 'react';
import styles from './Hero.module.css';
const stats = [
{ value: '10K+', label: 'Equipos activos' },
{ value: '99.9%', label: 'Uptime garantizado' },
{ value: '3x', label: 'Más productividad' },
{ value: '140+', label: 'Países' },
];
export default function Hero() {
const heroRef = useRef<HTMLElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add(styles.visible);
}
});
},
{ threshold: 0.1 }
);
const elements = heroRef.current?.querySelectorAll(`.${styles.reveal}`);
elements?.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
const handleScrollToContact = () => {
document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' });
};
const handleScrollToFeatures = () => {
document.querySelector('#features')?.scrollIntoView({ behavior: 'smooth' });
};
return (
<section className={styles.hero} id="hero" ref={heroRef} aria-label="Sección principal">
<div className={`container ${styles.inner}`}>
{/* Badge */}
<div className={`${styles.reveal} ${styles.badgeWrap}`}>
<span className="badge badge-dark">
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
<circle cx="4" cy="4" r="4" fill="#00c853" />
</svg>
Nuevo Ahora con IA integrada
</span>
</div>
{/* Heading */}
<h1 className={`${styles.reveal} ${styles.heading}`}>
El flujo de trabajo
<br />
<em className={styles.emphasis}>que tu equipo</em>
<br />
siempre necesitó
</h1>
{/* Subheading */}
<p className={`${styles.reveal} ${styles.subheading}`}>
FlowSync conecta a tu equipo, automatiza tareas repetitivas y te da
visibilidad total de cada proyecto todo en un solo lugar.
</p>
{/* CTA Buttons */}
<div className={`${styles.reveal} ${styles.ctas}`}>
<button
className="btn btn-primary btn-lg"
id="hero-cta-primary"
onClick={handleScrollToContact}
>
Empieza gratis
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<button
className="btn btn-secondary btn-lg"
id="hero-cta-secondary"
onClick={handleScrollToFeatures}
>
Ver características
</button>
</div>
{/* Trust note */}
<p className={`${styles.reveal} ${styles.trustNote}`}>
Sin tarjeta de crédito &nbsp;·&nbsp; 14 días gratis &nbsp;·&nbsp; Cancela cuando quieras
</p>
{/* Dashboard mockup */}
<div className={`${styles.reveal} ${styles.mockupWrap}`}>
<div className={styles.mockup} role="img" aria-label="Vista previa del dashboard de FlowSync">
{/* Window chrome */}
<div className={styles.mockupBar}>
<span className={styles.dot} style={{ background: '#ff5f57' }} />
<span className={styles.dot} style={{ background: '#febc2e' }} />
<span className={styles.dot} style={{ background: '#28c840' }} />
<span className={styles.mockupUrl}>app.flowsync.io/dashboard</span>
</div>
{/* Mock dashboard content */}
<div className={styles.mockupBody}>
{/* Sidebar */}
<nav className={styles.mockupSidebar} aria-hidden="true">
<div className={styles.sidebarLogo} />
{[...Array(5)].map((_, i) => (
<div key={i} className={`${styles.sidebarItem} ${i === 0 ? styles.sidebarActive : ''}`} />
))}
</nav>
{/* Main content */}
<main className={styles.mockupMain} aria-hidden="true">
{/* Header row */}
<div className={styles.mockupHeader}>
<div className={styles.mockupTitle} />
<div className={styles.mockupBtn} />
</div>
{/* Metric cards */}
<div className={styles.metricsRow}>
{['#0066ff', '#00c853', '#ff9500', '#6c5ce7'].map((color, i) => (
<div key={i} className={styles.metricCard}>
<div className={styles.metricIcon} style={{ background: `${color}18`, color }} />
<div className={styles.metricLines}>
<div className={styles.metricValue} />
<div className={styles.metricLabel} />
</div>
</div>
))}
</div>
{/* Chart area */}
<div className={styles.chartArea}>
<div className={styles.chartBars}>
{[60, 80, 50, 90, 70, 85, 65, 95, 75, 88].map((h, i) => (
<div
key={i}
className={styles.bar}
style={{ height: `${h}%`, animationDelay: `${i * 0.05}s` }}
/>
))}
</div>
</div>
{/* Task list */}
<div className={styles.taskList}>
{[85, 60, 95, 70].map((w, i) => (
<div key={i} className={styles.taskRow}>
<div className={styles.taskCheck} />
<div className={styles.taskLine} style={{ width: `${w}%` }} />
</div>
))}
</div>
</main>
</div>
</div>
</div>
{/* Stats */}
<div className={`${styles.reveal} ${styles.stats}`}>
{stats.map((stat) => (
<div key={stat.label} className={styles.statItem}>
<span className={styles.statValue}>{stat.value}</span>
<span className={styles.statLabel}>{stat.label}</span>
</div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,177 @@
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid transparent;
transition: all var(--transition-base);
}
.scrolled {
border-bottom-color: var(--color-gray-200);
box-shadow: var(--shadow-sm);
}
.inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-8);
height: 72px;
}
/* Logo */
.logo {
display: flex;
align-items: center;
gap: var(--space-2);
text-decoration: none;
flex-shrink: 0;
}
.logoMark {
width: 36px;
height: 36px;
background: var(--color-black);
color: var(--color-white);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
font-weight: 800;
letter-spacing: -0.02em;
}
.logoText {
font-size: var(--text-lg);
font-weight: 800;
color: var(--color-black);
letter-spacing: -0.04em;
}
/* Desktop links */
.links {
display: flex;
align-items: center;
gap: var(--space-1);
list-style: none;
flex: 1;
justify-content: center;
}
.link {
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-gray-600);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
background: none;
border: none;
cursor: pointer;
font-family: var(--font-sans);
}
.link:hover {
color: var(--color-black);
background: var(--color-gray-100);
}
/* Desktop actions */
.actions {
display: flex;
align-items: center;
gap: var(--space-3);
flex-shrink: 0;
}
/* Mobile toggle */
.menuToggle {
display: none;
flex-direction: column;
gap: 5px;
padding: var(--space-2);
border-radius: var(--radius-md);
background: none;
border: none;
cursor: pointer;
}
.bar {
display: block;
width: 22px;
height: 2px;
background: var(--color-black);
border-radius: 2px;
transition: all var(--transition-fast);
transform-origin: center;
}
.bar.open:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.bar.open:nth-child(2) {
opacity: 0;
transform: scaleX(0);
}
.bar.open:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}
/* Mobile menu */
.mobileMenu {
background: var(--color-white);
border-top: 1px solid var(--color-gray-200);
padding: var(--space-4) var(--space-6);
animation: fadeInUp 0.2s ease;
}
.mobileLinks {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-1);
margin-bottom: var(--space-4);
}
.mobileLink {
display: block;
width: 100%;
text-align: left;
padding: var(--space-3) var(--space-4);
font-size: var(--text-base);
font-weight: 500;
color: var(--color-dark);
border-radius: var(--radius-md);
background: none;
border: none;
cursor: pointer;
font-family: var(--font-sans);
transition: background var(--transition-fast);
}
.mobileLink:hover {
background: var(--color-gray-100);
}
.mobileActions {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
@media (max-width: 768px) {
.links,
.actions {
display: none;
}
.menuToggle {
display: flex;
}
}

View File

@@ -0,0 +1,111 @@
'use client';
import { useState, useEffect } from 'react';
import styles from './Navbar.module.css';
const navLinks = [
{ label: 'Características', href: '#features' },
{ label: 'Precios', href: '#pricing' },
{ label: 'Contacto', href: '#contact' },
];
export default function Navbar() {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 24);
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const handleNavClick = (href: string) => {
setMenuOpen(false);
const el = document.querySelector(href);
if (el) el.scrollIntoView({ behavior: 'smooth' });
};
return (
<header className={`${styles.navbar} ${scrolled ? styles.scrolled : ''}`} role="banner">
<nav className={`${styles.inner} container`} aria-label="Navegación principal">
{/* Logo */}
<a href="#" className={styles.logo} aria-label="FlowSync - inicio">
<span className={styles.logoMark}>FS</span>
<span className={styles.logoText}>FlowSync</span>
</a>
{/* Desktop links */}
<ul className={styles.links} role="list">
{navLinks.map((link) => (
<li key={link.href}>
<button
className={styles.link}
onClick={() => handleNavClick(link.href)}
aria-label={`Ir a sección ${link.label}`}
>
{link.label}
</button>
</li>
))}
</ul>
{/* Desktop CTA */}
<div className={styles.actions}>
<button className="btn btn-secondary" id="nav-login-btn">
Iniciar sesión
</button>
<button
className="btn btn-primary"
id="nav-cta-btn"
onClick={() => handleNavClick('#contact')}
>
Prueba gratis
</button>
</div>
{/* Mobile toggle */}
<button
className={styles.menuToggle}
onClick={() => setMenuOpen(!menuOpen)}
aria-label={menuOpen ? 'Cerrar menú' : 'Abrir menú'}
aria-expanded={menuOpen}
id="nav-menu-toggle"
>
<span className={`${styles.bar} ${menuOpen ? styles.open : ''}`} />
<span className={`${styles.bar} ${menuOpen ? styles.open : ''}`} />
<span className={`${styles.bar} ${menuOpen ? styles.open : ''}`} />
</button>
</nav>
{/* Mobile menu */}
{menuOpen && (
<div className={styles.mobileMenu} role="dialog" aria-label="Menú móvil">
<ul className={styles.mobileLinks} role="list">
{navLinks.map((link) => (
<li key={link.href}>
<button
className={styles.mobileLink}
onClick={() => handleNavClick(link.href)}
>
{link.label}
</button>
</li>
))}
</ul>
<div className={styles.mobileActions}>
<button className="btn btn-secondary" style={{ width: '100%' }}>
Iniciar sesión
</button>
<button
className="btn btn-primary"
style={{ width: '100%' }}
onClick={() => handleNavClick('#contact')}
>
Prueba gratis
</button>
</div>
</div>
)}
</header>
);
}

View File

@@ -0,0 +1,278 @@
.reveal {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.65s ease, transform 0.65s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
/* Header */
.header {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-16);
}
.title {
font-size: clamp(var(--text-3xl), 5vw, var(--text-5xl));
font-weight: 900;
letter-spacing: -0.04em;
line-height: 1.1;
color: var(--color-black);
}
.subtitle {
font-size: var(--text-lg);
color: var(--color-gray-600);
}
/* Toggle */
.toggle {
display: flex;
align-items: center;
gap: var(--space-3);
margin-top: var(--space-2);
}
.toggleLabel {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-gray-400);
transition: color var(--transition-fast);
}
.toggleLabelActive {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-black);
}
.toggleSwitch {
width: 48px;
height: 26px;
background: var(--color-gray-200);
border-radius: var(--radius-full);
border: none;
cursor: pointer;
position: relative;
transition: background var(--transition-base);
}
.toggleSwitch[aria-pressed='true'] {
background: var(--color-black);
}
.toggleKnob {
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background: var(--color-white);
border-radius: 50%;
transition: transform var(--transition-base);
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
.toggleKnobOn {
transform: translateX(22px);
}
.saveBadge {
display: inline-block;
margin-left: var(--space-2);
padding: 2px var(--space-2);
background: #dcfce7;
color: #15803d;
font-size: var(--text-xs);
font-weight: 700;
border-radius: var(--radius-full);
}
/* Plans grid */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-6);
align-items: start;
}
/* Card */
.card {
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-2xl);
padding: var(--space-8);
display: flex;
flex-direction: column;
gap: var(--space-6);
position: relative;
transition: all var(--transition-base);
}
.card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.cardHighlight {
background: var(--color-black);
border-color: var(--color-black);
color: var(--color-white);
}
.cardHighlight:hover {
box-shadow: 0 24px 64px rgba(0,0,0,0.2);
}
/* Popular badge */
.popularBadge {
position: absolute;
top: -14px;
left: 50%;
transform: translateX(-50%);
background: var(--color-accent);
color: var(--color-white);
font-size: var(--text-xs);
font-weight: 700;
padding: var(--space-1) var(--space-4);
border-radius: var(--radius-full);
white-space: nowrap;
letter-spacing: 0.04em;
}
/* Plan header */
.planHeader {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.planName {
font-size: var(--text-2xl);
font-weight: 800;
letter-spacing: -0.03em;
}
.cardHighlight .planName {
color: var(--color-white);
}
.planDesc {
font-size: var(--text-sm);
color: var(--color-gray-600);
line-height: 1.5;
}
.cardHighlight .planDesc {
color: rgba(255,255,255,0.6);
}
/* Price */
.priceRow {
display: flex;
align-items: flex-end;
gap: 2px;
}
.priceCurrency {
font-size: var(--text-xl);
font-weight: 700;
padding-bottom: 4px;
}
.priceValue {
font-size: clamp(var(--text-4xl), 6vw, 3.5rem);
font-weight: 900;
letter-spacing: -0.05em;
line-height: 1;
}
.priceUnit {
font-size: var(--text-sm);
color: var(--color-gray-400);
padding-bottom: 4px;
margin-left: var(--space-1);
}
.cardHighlight .priceUnit {
color: rgba(255,255,255,0.5);
}
.annualNote {
font-size: var(--text-xs);
color: var(--color-gray-400);
margin-top: calc(var(--space-6) * -1);
}
.cardHighlight .annualNote {
color: rgba(255,255,255,0.4);
}
/* Feature list */
.featureList {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.featureItem {
display: flex;
align-items: center;
gap: var(--space-3);
font-size: var(--text-sm);
color: var(--color-gray-700, #444);
}
.cardHighlight .featureItem {
color: rgba(255,255,255,0.8);
}
.checkIcon {
flex-shrink: 0;
color: var(--color-black);
}
.cardHighlight .checkIcon {
color: var(--color-white);
}
/* Enterprise note */
.enterpriseNote {
text-align: center;
margin-top: var(--space-12);
font-size: var(--text-base);
color: var(--color-gray-600);
}
.inlineLink {
background: none;
border: none;
cursor: pointer;
font-family: var(--font-sans);
font-size: var(--text-base);
font-weight: 600;
color: var(--color-black);
text-decoration: underline;
text-underline-offset: 3px;
transition: opacity var(--transition-fast);
}
.inlineLink:hover {
opacity: 0.6;
}
/* Responsive */
@media (max-width: 1024px) {
.grid {
grid-template-columns: 1fr;
max-width: 480px;
margin: 0 auto;
}
}

View File

@@ -0,0 +1,212 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import styles from './Pricing.module.css';
const plans = [
{
id: 'starter',
name: 'Starter',
price: { monthly: 0, annual: 0 },
description: 'Para equipos pequeños que están comenzando.',
cta: 'Empieza gratis',
highlight: false,
features: [
'Hasta 5 usuarios',
'3 proyectos activos',
'1 GB de almacenamiento',
'Integraciones básicas (5)',
'Soporte por email',
'Analíticas básicas',
],
},
{
id: 'pro',
name: 'Pro',
price: { monthly: 29, annual: 23 },
description: 'Para equipos en crecimiento que necesitan más poder.',
cta: 'Empieza prueba gratis',
highlight: true,
badge: 'Más popular',
features: [
'Hasta 25 usuarios',
'Proyectos ilimitados',
'50 GB de almacenamiento',
'Integraciones avanzadas (50+)',
'Soporte prioritario 24/7',
'Analíticas avanzadas',
'Automatización de flujos',
'Roles y permisos',
],
},
{
id: 'enterprise',
name: 'Enterprise',
price: { monthly: 99, annual: 79 },
description: 'Para organizaciones que necesitan control total.',
cta: 'Contactar ventas',
highlight: false,
features: [
'Usuarios ilimitados',
'Proyectos ilimitados',
'1 TB de almacenamiento',
'Todas las integraciones',
'Soporte dedicado + SLA',
'Analíticas enterprise',
'IA avanzada',
'SSO & SAML',
'Auditoría y compliance',
'Infraestructura dedicada',
],
},
];
export default function Pricing() {
const [annual, setAnnual] = useState(false);
const sectionRef = useRef<HTMLElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) entry.target.classList.add(styles.visible);
});
},
{ threshold: 0.1 }
);
const elements = sectionRef.current?.querySelectorAll(`.${styles.reveal}`);
elements?.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
const handleContactScroll = () => {
document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' });
};
return (
<section className="section" id="pricing" ref={sectionRef} aria-labelledby="pricing-heading">
<div className="container">
{/* Header */}
<div className={`${styles.reveal} ${styles.header}`}>
<span className="badge badge-dark">Precios</span>
<h2 id="pricing-heading" className={styles.title}>
Transparente. Simple.
<br />
Sin sorpresas.
</h2>
<p className={styles.subtitle}>
Comienza gratis. Escala cuando estés listo. Sin contratos a largo plazo.
</p>
{/* Toggle */}
<div className={styles.toggle}>
<span className={!annual ? styles.toggleLabelActive : styles.toggleLabel}>Mensual</span>
<button
className={styles.toggleSwitch}
onClick={() => setAnnual(!annual)}
aria-pressed={annual}
aria-label="Cambiar a facturación anual"
id="pricing-toggle-btn"
>
<span className={`${styles.toggleKnob} ${annual ? styles.toggleKnobOn : ''}`} />
</button>
<span className={annual ? styles.toggleLabelActive : styles.toggleLabel}>
Anual
<span className={styles.saveBadge}>Ahorra 20%</span>
</span>
</div>
</div>
{/* Plans */}
<div className={styles.grid}>
{plans.map((plan, i) => (
<article
key={plan.id}
className={`${styles.reveal} ${styles.card} ${plan.highlight ? styles.cardHighlight : ''}`}
style={{ transitionDelay: `${i * 0.1}s` }}
aria-label={`Plan ${plan.name}`}
>
{plan.badge && (
<div className={styles.popularBadge} aria-label="Plan más popular">
{plan.badge}
</div>
)}
<div className={styles.planHeader}>
<h3 className={styles.planName}>{plan.name}</h3>
<p className={styles.planDesc}>{plan.description}</p>
</div>
<div className={styles.priceRow}>
{plan.price.monthly === 0 ? (
<span className={styles.priceValue}>Gratis</span>
) : (
<>
<span className={styles.priceCurrency}>$</span>
<span className={styles.priceValue}>
{annual ? plan.price.annual : plan.price.monthly}
</span>
<span className={styles.priceUnit}>/mes</span>
</>
)}
</div>
{annual && plan.price.monthly > 0 && (
<p className={styles.annualNote}>
Facturado anualmente ${plan.price.annual * 12}/año
</p>
)}
<button
className={`btn btn-lg ${plan.highlight ? 'btn-accent' : 'btn-secondary'}`}
id={`pricing-${plan.id}-btn`}
onClick={handleContactScroll}
aria-label={`${plan.cta} — Plan ${plan.name}`}
>
{plan.cta}
</button>
<ul className={styles.featureList} role="list">
{plan.features.map((f) => (
<li key={f} className={styles.featureItem}>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
className={styles.checkIcon}
>
<path
d="M3 8l3.5 3.5L13 4.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{f}
</li>
))}
</ul>
</article>
))}
</div>
{/* Enterprise note */}
<div className={`${styles.reveal} ${styles.enterpriseNote}`}>
<p>
¿Necesitas algo personalizado? &nbsp;
<button
className={styles.inlineLink}
onClick={handleContactScroll}
id="pricing-enterprise-contact-btn"
>
Habla con nuestro equipo
</button>
</p>
</div>
</div>
</section>
);
}

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}