Primer vistaso
This commit is contained in:
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal 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
5
AGENTS.md
Normal 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 -->
|
||||||
70
README.md
Normal file
70
README.md
Normal 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**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 🚀 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
18
eslint.config.mjs
Normal 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
7
next.config.ts
Normal 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
6003
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal 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
1
public/file.svg
Normal 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
1
public/globe.svg
Normal 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
1
public/next.svg
Normal 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
1
public/vercel.svg
Normal 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
1
public/window.svg
Normal 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
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
314
src/app/globals.css
Normal file
314
src/app/globals.css
Normal 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
26
src/app/layout.tsx
Normal 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
21
src/app/page.tsx
Normal 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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
367
src/components/ContactForm/ContactForm.module.css
Normal file
367
src/components/ContactForm/ContactForm.module.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
402
src/components/ContactForm/ContactForm.tsx
Normal file
402
src/components/ContactForm/ContactForm.tsx
Normal 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}>
|
||||||
|
“FlowSync transformó la forma en que colaboramos. Lo que antes
|
||||||
|
tomaba días, ahora lo hacemos en horas.”
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
150
src/components/Features/Features.module.css
Normal file
150
src/components/Features/Features.module.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
149
src/components/Features/Features.tsx
Normal file
149
src/components/Features/Features.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
230
src/components/Footer/Footer.module.css
Normal file
230
src/components/Footer/Footer.module.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
138
src/components/Footer/Footer.tsx
Normal file
138
src/components/Footer/Footer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
370
src/components/Hero/Hero.module.css
Normal file
370
src/components/Hero/Hero.module.css
Normal 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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
177
src/components/Hero/Hero.tsx
Normal file
177
src/components/Hero/Hero.tsx
Normal 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 · 14 días gratis · 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
177
src/components/Navbar/Navbar.module.css
Normal file
177
src/components/Navbar/Navbar.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/components/Navbar/Navbar.tsx
Normal file
111
src/components/Navbar/Navbar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
278
src/components/Pricing/Pricing.module.css
Normal file
278
src/components/Pricing/Pricing.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
212
src/components/Pricing/Pricing.tsx
Normal file
212
src/components/Pricing/Pricing.tsx
Normal 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?
|
||||||
|
<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
34
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user