init commit

This commit is contained in:
Cesar Mendivil 2025-11-01 23:13:43 -07:00
commit cc7702686e
45 changed files with 10883 additions and 0 deletions

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals"]
}

72
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,72 @@
# AvanzaCast - Plataforma de Streaming en Vivo
## Descripción del Proyecto
AvanzaCast es una plataforma de streaming en vivo similar a StreamYard que permite:
- Transmisión multistream (YouTube, Facebook, Twitch, LinkedIn)
- Estudio virtual con WebRTC
- Chat en tiempo real
- Grabación y almacenamiento en la nube
- Gestión de invitados y colaboración
- Personalización de escenas con overlays y branding
## Stack Tecnológico
- **Frontend**: React 18 + Next.js 15
- **Styling**: Tailwind CSS
- **TypeScript**: Para tipado estático
- **Linting**: ESLint
- **Streaming**: WebRTC (próximamente)
- **Chat**: Socket.IO (próximamente)
- **Base de Datos**: PostgreSQL + Redis (próximamente)
- **Storage**: AWS S3 / MinIO (próximamente)
- **Transcoding**: FFmpeg (próximamente)
## Estructura del Proyecto
```
src/
├── app/ # App Router (Next.js 13+)
│ ├── auth/ # Páginas de autenticación
│ ├── dashboard/ # Dashboard de usuario
│ ├── studio/ # Estudio virtual de streaming
│ ├── layout.tsx # Layout principal
│ ├── page.tsx # Página de inicio
│ └── globals.css # Estilos globales
├── components/ # Componentes reutilizables
├── hooks/ # Custom hooks de React
└── lib/ # Utilidades y configuraciones
```
## Instrucciones de Desarrollo
- Seguir las convenciones de Next.js 15 (App Router)
- Usar TypeScript para tipado estático
- Implementar componentes modulares y reutilizables
- Priorizar la performance y UX para streaming en tiempo real
- Mantener código limpio y bien documentado
- El proyecto está configurado para ejecutarse con `npm run dev`
## Estado Actual del Proyecto
✅ **Configuración inicial completa**
- Estructura de archivos Next.js creada
- Páginas básicas implementadas (Home, Login, Register, Dashboard, Studio)
- Tailwind CSS configurado
- TypeScript configurado
- ESLint configurado
- Proyecto ejecutándose en http://localhost:3000
## Próximos Pasos de Desarrollo
1. Implementar autenticación real (NextAuth.js)
2. Configurar base de datos (PostgreSQL)
3. Implementar WebRTC para video streaming
4. Agregar Socket.IO para chat en tiempo real
5. Integrar APIs de plataformas de streaming
6. Implementar grabación de sesiones
## Features Principales del MVP
1. ✅ Estructura básica del proyecto
2. 🟡 Autenticación de usuarios
3. 🟡 Creación de transmisiones
4. 🟡 Estudio virtual básico con WebRTC
5. 🟡 Multistream a YouTube + Facebook
6. 🟡 Chat en tiempo real
7. 🟡 Grabación en la nube
Leyenda: ✅ Completado | 🟡 Pendiente | ❌ No iniciado

44
.gitignore vendored Normal file
View File

@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# IDE
.vscode/
.idea/
# OS
Thumbs.db

14
AvanzaCast.code-workspace Normal file
View File

@ -0,0 +1,14 @@
{
"folders": [
{
"path": "."
},
{
"path": "../Templates Backup/vristo-main-1.1.8/html/vristo-html-main"
},
{
"path": "../landing-page"
}
],
"settings": {}
}

82
README.md Normal file
View File

@ -0,0 +1,82 @@
# AvanzaCast - Plataforma de Streaming en Vivo
Una plataforma profesional de streaming en vivo similar a StreamYard, construida con React y Next.js.
## 🚀 Características
- **Multistreaming**: Transmite simultáneamente a YouTube, Facebook, Twitch y LinkedIn
- **Estudio Virtual**: Personaliza tu transmisión con overlays, logos y escenas dinámicas
- **Colaboración en Tiempo Real**: Invita participantes y gestiona el chat en vivo
- **Grabación en la Nube**: Guarda y descarga tus transmisiones
- **WebRTC**: Video chat en tiempo real de alta calidad
- **Autenticación**: Sistema seguro de login con OAuth2
## 🛠️ Stack Tecnológico
- **Frontend**: React 18 + Next.js 15
- **Styling**: Tailwind CSS
- **TypeScript**: Para tipado estático
- **Linting**: ESLint
- **Streaming**: WebRTC (próximamente)
- **Chat**: Socket.IO (próximamente)
## 📦 Instalación
1. Clona el repositorio:
```bash
git clone https://github.com/tu-usuario/avanzacast.git
cd avanzacast
```
2. Instala las dependencias:
```bash
npm install
```
3. Inicia el servidor de desarrollo:
```bash
npm run dev
```
4. Abre [http://localhost:3000](http://localhost:3000) en tu navegador.
## 📁 Estructura del Proyecto
```
src/
├── app/ # App Router (Next.js 13+)
│ ├── auth/ # Páginas de autenticación
│ ├── dashboard/ # Dashboard de usuario
│ ├── studio/ # Estudio virtual de streaming
│ ├── layout.tsx # Layout principal
│ ├── page.tsx # Página de inicio
│ └── globals.css # Estilos globales
├── components/ # Componentes reutilizables
├── hooks/ # Custom hooks de React
└── lib/ # Utilidades y configuraciones
```
## 🏗️ Roadmap
### MVP (Versión 1.0)
- [x] Configuración inicial del proyecto
- [ ] Sistema de autenticación
- [ ] Dashboard de usuario
- [ ] Creación de transmisiones
- [ ] Estudio virtual básico
- [ ] Multistreaming a YouTube y Facebook
### Futuras Versiones
- [ ] Chat en tiempo real
- [ ] Grabación en la nube
- [ ] Soporte para Twitch y LinkedIn
- [ ] Overlays y branding personalizado
- [ ] App móvil
## 📄 Licencia
MIT License - ver el archivo [LICENSE](LICENSE) para más detalles.
## 🤝 Contribuir
Las contribuciones son bienvenidas. Por favor, abre un issue primero para discutir los cambios que te gustaría hacer.

9
next.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['localhost'],
},
outputFileTracingRoot: __dirname,
};
module.exports = nextConfig;

6548
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "avanzacast",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"next": "15.5.3"
},
"devDependencies": {
"typescript": "^5.6.2",
"@types/node": "^22.10.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"postcss": "^8.5.0",
"autoprefixer": "^10.4.20",
"tailwindcss": "^3.4.16",
"eslint": "^9.15.0",
"eslint-config-next": "15.5.3"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,14 @@
<svg width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad4" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:1" />
<stop offset="100%" style="stop-color:#d97706;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="300" height="200" fill="url(#grad4)"/>
<rect x="100" y="60" width="100" height="80" fill="white" opacity="0.9" rx="8"/>
<circle cx="120" cy="80" r="8" fill="#f59e0b"/>
<rect x="110" y="100" width="60" height="4" fill="#f59e0b" rx="2"/>
<rect x="110" y="110" width="40" height="4" fill="#f59e0b" rx="2"/>
<text x="150" y="170" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white" text-anchor="middle">Branding</text>
</svg>

After

Width:  |  Height:  |  Size: 789 B

View File

@ -0,0 +1,14 @@
<svg width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad5" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ec4899;stop-opacity:1" />
<stop offset="100%" style="stop-color:#be185d;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="300" height="200" fill="url(#grad5)"/>
<path d="M110 70 Q150 50 190 70 Q190 100 150 120 Q110 100 110 70" fill="white" opacity="0.9"/>
<circle cx="130" cy="85" r="3" fill="#ec4899"/>
<circle cx="150" cy="80" r="3" fill="#ec4899"/>
<circle cx="170" cy="85" r="3" fill="#ec4899"/>
<text x="150" y="170" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white" text-anchor="middle">Engagement</text>
</svg>

After

Width:  |  Height:  |  Size: 765 B

View File

@ -0,0 +1,14 @@
<svg width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad3" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#10b981;stop-opacity:1" />
<stop offset="100%" style="stop-color:#047857;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="300" height="200" fill="url(#grad3)"/>
<circle cx="130" cy="80" r="20" fill="white" opacity="0.9"/>
<circle cx="170" cy="80" r="20" fill="white" opacity="0.9"/>
<circle cx="130" cy="120" r="20" fill="white" opacity="0.9"/>
<circle cx="170" cy="120" r="20" fill="white" opacity="0.9"/>
<text x="150" y="170" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white" text-anchor="middle">Guests</text>
</svg>

After

Width:  |  Height:  |  Size: 768 B

View File

@ -0,0 +1,14 @@
<svg width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad6" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
<stop offset="100%" style="stop-color:#4338ca;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="300" height="200" fill="url(#grad6)"/>
<ellipse cx="150" cy="100" rx="50" ry="30" fill="white" opacity="0.9"/>
<circle cx="130" cy="90" r="8" fill="#6366f1"/>
<circle cx="150" cy="100" r="10" fill="#6366f1"/>
<circle cx="170" cy="90" r="8" fill="#6366f1"/>
<text x="150" y="170" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white" text-anchor="middle">Podcasts</text>
</svg>

After

Width:  |  Height:  |  Size: 742 B

View File

@ -0,0 +1 @@
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iIzNiODJmNiIvPgogIDxjaXJjbGUgY3g9IjE1MCIgY3k9IjEwMCIgcj0iNDAiIGZpbGw9IndoaXRlIi8+CiAgPGNpcmNsZSBjeD0iMTUwIiBjeT0iMTAwIiByPSIxNSIgZmlsbD0iI2VmNDQ0NCIvPgo8L3N2Zz4=

View File

@ -0,0 +1,12 @@
<svg width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="300" height="200" fill="url(#grad1)"/>
<circle cx="150" cy="100" r="40" fill="white" opacity="0.9"/>
<circle cx="150" cy="100" r="15" fill="#ef4444"/>
<text x="150" y="170" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white" text-anchor="middle">Recording</text>
</svg>

After

Width:  |  Height:  |  Size: 633 B

View File

@ -0,0 +1,14 @@
<svg width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad7" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#06b6d4;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0891b2;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="300" height="200" fill="url(#grad7)"/>
<path d="M120 80 L180 80 L180 120 L120 120 Z" fill="white" opacity="0.9" rx="4"/>
<path d="M140 100 L160 90 L160 110 Z" fill="#06b6d4"/>
<circle cx="200" cy="70" r="15" fill="white" opacity="0.7"/>
<path d="M192 70 L208 62 L208 78 Z" fill="#06b6d4"/>
<text x="150" y="170" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white" text-anchor="middle">Repurpose</text>
</svg>

After

Width:  |  Height:  |  Size: 776 B

View File

@ -0,0 +1,14 @@
<svg width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#8b5cf6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#5b21b6;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="300" height="200" fill="url(#grad2)"/>
<path d="M120 80 L180 100 L120 120 Z" fill="white" opacity="0.9"/>
<circle cx="200" cy="70" r="8" fill="#10b981"/>
<circle cx="220" cy="70" r="8" fill="#f59e0b"/>
<circle cx="240" cy="70" r="8" fill="#ef4444"/>
<text x="150" y="170" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white" text-anchor="middle">Streaming</text>
</svg>

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

122
src/app/auth/login/page.tsx Normal file
View File

@ -0,0 +1,122 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useApi';
export default function LoginPage() {
const [email, setEmail] = useState('nextv.stream@gmail.com');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login, isLoading } = useAuth();
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
const result = await login(email, password);
if (result.success) {
router.push('/dashboard');
} else {
setError(result.error || 'Error al iniciar sesión');
}
};
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="w-full max-w-md">
{/* Login Card */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-8">
{/* Account continuation header */}
<div className="flex items-center space-x-3 mb-6 p-4 bg-gray-50 rounded-xl border border-gray-200">
<div className="w-10 h-10 bg-purple-600 rounded-full flex items-center justify-center">
<span className="text-white font-bold text-sm">N</span>
</div>
<div className="flex-1">
<p className="font-medium text-gray-900">Continuar como Nextv</p>
<p className="text-sm text-gray-600">nextv.stream@gmail.com</p>
</div>
<div className="w-8 h-8 flex items-center justify-center">
<svg className="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
</div>
</div>
{/* Continue with email */}
<div className="text-center mb-6">
<h2 className="text-xl font-medium text-gray-900 mb-4">
O continúa con tu correo electrónico
</h2>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-xl text-sm">
{error}
</div>
)}
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full h-16 px-6 text-lg border-2 border-gray-300 rounded-xl focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 text-gray-900 placeholder-gray-500 transition-all duration-200 hover:border-gray-400 shadow-sm hover:shadow-md"
placeholder="Ingrese la dirección de correo electrónico"
required
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full h-16 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 disabled:from-gray-400 disabled:to-gray-500 text-white font-bold text-lg rounded-xl transition-all duration-200 transform hover:scale-105 hover:shadow-xl active:scale-95 disabled:scale-100 disabled:shadow-none shadow-lg"
>
{isLoading ? 'Cargando...' : '¡Empieza gratis ahora!'}
</button>
<div className="text-center pt-4">
<p className="text-green-600 font-medium text-sm mb-4">
¡Confiado por más de 12,000,000 creadores!
</p>
<div className="text-xs text-gray-600 leading-relaxed">
<p className="mb-2">
Al continuar, aceptas nuestros{' '}
<Link href="/terms" className="text-blue-600 underline hover:text-blue-800">
Términos de Servicio del Usuario
</Link>{' '}
y{' '}
<Link href="/privacy" className="text-blue-600 underline hover:text-blue-800">
Política de Uso del Plan
</Link>{' '}
y reconoces la recepción de nuestra{' '}
<Link href="/privacy-policy" className="text-blue-600 underline hover:text-blue-800">
Política de Privacidad
</Link>
.
</p>
</div>
</div>
<div className="text-center pt-6 border-t border-gray-100">
<p className="text-sm text-gray-600">
¿Ya usas AvanzaCast?{' '}
<Link href="/auth/register" className="text-blue-600 underline hover:text-blue-800">
Inicia sesión
</Link>
.
</p>
</div>
</form>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,287 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useApi';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Card from '@/components/ui/Card';
export default function RegisterPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: ''
});
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [acceptTerms, setAcceptTerms] = useState(false);
const { register, isLoading } = useAuth();
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (formData.password !== formData.confirmPassword) {
setError('Las contraseñas no coinciden');
return;
}
if (!acceptTerms) {
setError('Debes aceptar los términos y condiciones');
return;
}
const result = await register({
name: formData.name,
email: formData.email
});
if (result.success) {
router.push('/dashboard');
} else {
setError(result.error || 'Error al crear la cuenta');
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-blue-50 to-indigo-50 dark:from-gray-900 dark:via-purple-900 dark:to-blue-900 flex items-center justify-center p-4 relative overflow-hidden">
{/* Background decorations */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-gradient-to-br from-purple-400/20 to-blue-600/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-gradient-to-tr from-blue-400/20 to-purple-600/20 rounded-full blur-3xl"></div>
<div className="absolute top-1/3 left-1/3 transform -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-gradient-to-r from-purple-300/10 to-blue-300/10 rounded-full blur-3xl"></div>
</div>
<div className="w-full max-w-md relative z-10">
{/* Logo Section */}
<div className="text-center mb-8">
<div className="relative">
<div className="w-20 h-20 bg-gradient-to-r from-purple-500 to-blue-600 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-xl shadow-purple-500/25">
<span className="text-white font-bold text-3xl">🚀</span>
</div>
<div className="absolute inset-0 w-20 h-20 bg-gradient-to-r from-purple-500 to-blue-600 rounded-2xl mx-auto blur opacity-20 animate-pulse"></div>
</div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-gray-900 to-purple-900 dark:from-white dark:to-purple-300 bg-clip-text text-transparent mb-2">
AvanzaCast
</h1>
<p className="text-gray-600 dark:text-gray-400">
Únete a la revolución del streaming
</p>
</div>
{/* Register Form */}
<Card className="backdrop-blur-sm bg-white/80 dark:bg-gray-800/80 border-0 shadow-2xl shadow-purple-500/10">
<div className="p-8">
<div className="mb-6">
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-2">
Crear Cuenta
</h2>
<p className="text-gray-600 dark:text-gray-400">
Comienza tu experiencia de streaming profesional
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-xl text-sm flex items-center space-x-2">
<span className="text-red-500"></span>
<span>{error}</span>
</div>
)}
<div className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Nombre completo
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<span className="text-gray-400 text-lg">👤</span>
</div>
<input
id="name"
name="name"
type="text"
value={formData.name}
onChange={handleInputChange}
className="pl-12 h-16 text-lg border-2 border-gray-200 dark:border-gray-600 rounded-xl focus:border-purple-500 focus:ring-4 focus:ring-purple-500/20 transition-all duration-200 hover:border-gray-300 shadow-sm hover:shadow-md w-full"
placeholder="Tu nombre completo"
required
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Correo electrónico
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<span className="text-gray-400 text-lg">📧</span>
</div>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
className="pl-12 h-16 text-lg border-2 border-gray-200 dark:border-gray-600 rounded-xl focus:border-purple-500 focus:ring-4 focus:ring-purple-500/20 transition-all duration-200 hover:border-gray-300 shadow-sm hover:shadow-md w-full"
placeholder="tu@email.com"
required
/>
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Contraseña
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<span className="text-gray-400 text-lg">🔒</span>
</div>
<input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
value={formData.password}
onChange={handleInputChange}
className="pl-12 pr-12 h-16 text-lg border-2 border-gray-200 dark:border-gray-600 rounded-xl focus:border-purple-500 focus:ring-4 focus:ring-purple-500/20 transition-all duration-200 hover:border-gray-300 shadow-sm hover:shadow-md w-full"
placeholder="Mínimo 6 caracteres"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<span className="text-lg">{showPassword ? '🙈' : '👁️'}</span>
</button>
</div>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Confirmar contraseña
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<span className="text-gray-400 text-lg">🔐</span>
</div>
<input
id="confirmPassword"
name="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'}
value={formData.confirmPassword}
onChange={handleInputChange}
className="pl-12 pr-12 h-16 text-lg border-2 border-gray-200 dark:border-gray-600 rounded-xl focus:border-purple-500 focus:ring-4 focus:ring-purple-500/20 transition-all duration-200 hover:border-gray-300 shadow-sm hover:shadow-md w-full"
placeholder="Repite tu contraseña"
required
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<span className="text-lg">{showConfirmPassword ? '🙈' : '👁️'}</span>
</button>
</div>
</div>
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="acceptTerms"
type="checkbox"
checked={acceptTerms}
onChange={(e) => setAcceptTerms(e.target.checked)}
className="w-4 h-4 text-purple-600 bg-gray-100 border-gray-300 rounded focus:ring-purple-500 dark:focus:ring-purple-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="acceptTerms" className="text-gray-600 dark:text-gray-400">
Acepto los{' '}
<Link href="/terms" className="text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300">
términos y condiciones
</Link>{' '}
y la{' '}
<Link href="/privacy" className="text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300">
política de privacidad
</Link>
</label>
</div>
</div>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full h-16 bg-gradient-to-r from-purple-500 to-blue-600 hover:from-purple-600 hover:to-blue-700 disabled:from-gray-400 disabled:to-gray-500 text-white font-bold text-xl rounded-xl shadow-xl shadow-purple-500/25 transition-all duration-200 transform hover:scale-105 hover:shadow-2xl active:scale-95 disabled:scale-100 disabled:shadow-lg"
>
{isLoading ? (
<div className="flex items-center justify-center space-x-3">
<div className="w-6 h-6 border-3 border-white border-t-transparent rounded-full animate-spin"></div>
<span className="text-lg">Creando cuenta...</span>
</div>
) : (
'¡Crear Cuenta Gratis!'
)}
</button>
{/* Social Registration */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200 dark:border-gray-600"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-3 bg-white dark:bg-gray-800 text-gray-500">O regístrate con</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<button
type="button"
className="h-14 border-2 border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-xl font-semibold text-lg transition-all duration-200 transform hover:scale-105 hover:shadow-md active:scale-95 flex items-center justify-center"
>
<span className="text-xl mr-3">🌐</span>
Google
</button>
<button
type="button"
className="h-14 border-2 border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-xl font-semibold text-lg transition-all duration-200 transform hover:scale-105 hover:shadow-md active:scale-95 flex items-center justify-center"
>
<span className="text-xl mr-3">📱</span>
GitHub
</button>
</div>
<div className="text-center">
<span className="text-gray-600 dark:text-gray-400">¿Ya tienes cuenta? </span>
<Link
href="/auth/login"
className="text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300 font-semibold"
>
Inicia sesión
</Link>
</div>
</form>
</div>
</Card>
{/* Footer */}
<div className="text-center mt-8 text-sm text-gray-500 dark:text-gray-400">
<p>© 2024 AvanzaCast. Todos los derechos reservados.</p>
</div>
</div>
</div>
);
}

307
src/app/dashboard/page.tsx Normal file
View File

@ -0,0 +1,307 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useAuth, useStreams, usePlatforms } from '@/hooks/useApi';
import MainLayout from '@/components/MainLayout';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { Stream, StreamPlatform } from '@/lib/types';
export default function DashboardPage() {
const { user } = useAuth();
const { streams, isLoading: streamsLoading } = useStreams();
const { platforms } = usePlatforms();
const [stats, setStats] = useState({
totalStreams: 0,
totalViews: 0,
liveStreams: 0,
connectedPlatforms: 0
});
useEffect(() => {
if (streams && platforms) {
const liveStreams = streams.filter(s => s.status === 'live').length;
const totalViews = streams.reduce((sum, s) => sum + s.maxViewers, 0);
const connectedPlatforms = platforms.filter(p => p.isConnected).length;
setStats({
totalStreams: streams.length,
totalViews,
liveStreams,
connectedPlatforms
});
}
}, [streams, platforms]);
const getStatusBadge = (status: Stream['status']) => {
const badges = {
live: 'bg-success text-white',
scheduled: 'bg-warning text-white',
ended: 'bg-secondary text-white',
draft: 'bg-dark text-white'
};
const labels = {
live: 'En Vivo',
scheduled: 'Programado',
ended: 'Finalizado',
draft: 'Borrador'
};
return (
<span className={`px-2 py-1 text-xs font-medium rounded-full ${badges[status]}`}>
{labels[status]}
</span>
);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('es-ES', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
};
return (
<MainLayout>
<div className="space-y-6">
{/* Welcome Section */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-black dark:text-white">
¡Hola, {user?.name || 'Usuario'}! 👋
</h1>
<p className="text-white-dark mt-2">
Bienvenido de vuelta a tu estudio de streaming profesional
</p>
</div>
<div className="flex items-center space-x-3">
<Link href="/studio">
<Button
variant="danger"
size="lg"
icon={
<div className="w-2 h-2 bg-white rounded-full animate-pulse" />
}
>
Ir en Vivo
</Button>
</Link>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="bg-gradient-to-r from-primary to-primary/80 text-white border-0">
<div className="flex items-center justify-between">
<div>
<p className="text-white/80 text-sm">Total Streams</p>
<p className="text-3xl font-bold">{stats.totalStreams}</p>
</div>
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</div>
</div>
</Card>
<Card className="bg-gradient-to-r from-success to-success/80 text-white border-0">
<div className="flex items-center justify-between">
<div>
<p className="text-white/80 text-sm">Total Views</p>
<p className="text-3xl font-bold">{stats.totalViews.toLocaleString()}</p>
</div>
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</div>
</div>
</Card>
<Card className="bg-gradient-to-r from-warning to-warning/80 text-white border-0">
<div className="flex items-center justify-between">
<div>
<p className="text-white/80 text-sm">En Vivo Ahora</p>
<p className="text-3xl font-bold">{stats.liveStreams}</p>
</div>
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center">
<div className="w-3 h-3 bg-white rounded-full animate-pulse"></div>
</div>
</div>
</Card>
<Card className="bg-gradient-to-r from-info to-info/80 text-white border-0">
<div className="flex items-center justify-between">
<div>
<p className="text-white/80 text-sm">Plataformas</p>
<p className="text-3xl font-bold">{stats.connectedPlatforms}/4</p>
</div>
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</Card>
</div>
{/* Quick Actions */}
<Card title="Acciones Rápidas" subtitle="Accesos directos para gestionar tu streaming">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Link href="/studio">
<div className="group p-6 border border-[#e0e6ed] dark:border-[#253b5c] rounded-lg hover:shadow-lg transition-all cursor-pointer">
<div className="w-12 h-12 bg-primary-light dark:bg-primary-dark-light rounded-lg flex items-center justify-center text-primary mb-4 group-hover:scale-110 transition-transform">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-black dark:text-white mb-2">
Ir al Estudio
</h3>
<p className="text-white-dark text-sm">
Accede al estudio virtual para streaming
</p>
</div>
</Link>
<Link href="/streams/create">
<div className="group p-6 border border-[#e0e6ed] dark:border-[#253b5c] rounded-lg hover:shadow-lg transition-all cursor-pointer">
<div className="w-12 h-12 bg-success-light dark:bg-success-dark-light rounded-lg flex items-center justify-center text-success mb-4 group-hover:scale-110 transition-transform">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
<h3 className="text-lg font-semibold text-black dark:text-white mb-2">
Nueva Transmisión
</h3>
<p className="text-white-dark text-sm">
Crear una nueva transmisión programada
</p>
</div>
</Link>
<Link href="/platforms">
<div className="group p-6 border border-[#e0e6ed] dark:border-[#253b5c] rounded-lg hover:shadow-lg transition-all cursor-pointer">
<div className="w-12 h-12 bg-warning-light dark:bg-warning-dark-light rounded-lg flex items-center justify-center text-warning mb-4 group-hover:scale-110 transition-transform">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
</div>
<h3 className="text-lg font-semibold text-black dark:text-white mb-2">
Conectar Plataformas
</h3>
<p className="text-white-dark text-sm">
Gestiona tus conexiones de streaming
</p>
</div>
</Link>
<Link href="/analytics">
<div className="group p-6 border border-[#e0e6ed] dark:border-[#253b5c] rounded-lg hover:shadow-lg transition-all cursor-pointer">
<div className="w-12 h-12 bg-info-light dark:bg-info-dark-light rounded-lg flex items-center justify-center text-info mb-4 group-hover:scale-110 transition-transform">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-black dark:text-white mb-2">
Ver Analytics
</h3>
<p className="text-white-dark text-sm">
Analizar el rendimiento de streams
</p>
</div>
</Link>
</div>
</Card>
{/* Recent Streams */}
<Card
title="Transmisiones Recientes"
subtitle="Tus últimas transmisiones y su estado"
actions={
<Link href="/streams">
<Button variant="outline" size="sm">
Ver Todas
</Button>
</Link>
}
>
{streamsLoading ? (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : streams && streams.length > 0 ? (
<div className="space-y-4">
{streams.slice(0, 3).map((stream) => (
<div key={stream.id} className="flex items-center justify-between p-4 border border-[#e0e6ed] dark:border-[#253b5c] rounded-lg">
<div className="flex items-center space-x-4">
<div className="w-12 h-8 bg-gray-200 dark:bg-gray-700 rounded flex items-center justify-center">
<svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</div>
<div>
<h4 className="font-semibold text-black dark:text-white">
{stream.title}
</h4>
<div className="flex items-center space-x-4 text-sm text-white-dark">
<span>
{stream.startedAt ? formatDate(stream.startedAt) :
stream.scheduledAt ? `Programado: ${formatDate(stream.scheduledAt)}` :
'Borrador'}
</span>
<span>👁 {stream.maxViewers} viewers</span>
<span> {stream.duration || 0} min</span>
</div>
</div>
</div>
<div className="flex items-center space-x-3">
{getStatusBadge(stream.status)}
<div className="flex items-center space-x-1">
{stream.platforms.slice(0, 3).map((platform, index) => (
<div key={index} className="w-6 h-6 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center">
<span className="text-xs">
{platform.name === 'youtube' ? 'YT' :
platform.name === 'facebook' ? 'FB' :
platform.name === 'twitch' ? 'TW' : 'LI'}
</span>
</div>
))}
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12">
<div className="w-24 h-24 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-black dark:text-white mb-2">
No hay transmisiones aún
</h3>
<p className="text-white-dark mb-6">
Crea tu primera transmisión para comenzar
</p>
<Link href="/studio">
<Button variant="primary">
Crear Primera Transmisión
</Button>
</Link>
</div>
)}
</Card>
</div>
</MainLayout>
);
}

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

@ -0,0 +1,159 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom scrollbar styles for feature carousel */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
/* Smooth scrolling behavior */
scroll-behavior: smooth;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Infinite carousel smooth transitions */
.carousel-smooth {
scroll-behavior: smooth;
transition: scroll-left 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
}
/* Line clamp utility */
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Smooth animations */
.transition-all {
transition: all 0.3s ease;
}
/* Hover effects */
.hover-lift:hover {
transform: translateY(-2px);
}
/* Custom focus styles */
.focus-ring:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Custom animations without GPU usage */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-slideDown {
animation: slideDown 0.2s ease-out;
}
/* Smooth transitions */
.transition-all {
transition: all 0.2s ease;
}
.hover\:shadow-lg:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.hover\:shadow-xl:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.hover\:shadow-3xl:hover {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
/* Smooth hover effects */
.hover\:-translate-y-0\.5:hover {
transform: translateY(-2px);
}
.hover\:-translate-y-1:hover {
transform: translateY(-4px);
}
.hover\:-translate-y-2:hover {
transform: translateY(-8px);
}
.hover\:scale-105:hover {
transform: scale(1.05);
}
.hover\:scale-110:hover {
transform: scale(1.1);
}
.hover\:rotate-3:hover {
transform: rotate(3deg);
}
.hover\:rotate-12:hover {
transform: rotate(12deg);
}
.group-hover\:translate-x-1 {
transition: transform 0.2s ease;
}
.group:hover .group-hover\:translate-x-1 {
transform: translateX(4px);
}
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

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

@ -0,0 +1,21 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "AvanzaCast - Plataforma de Streaming en Vivo",
description: "Plataforma profesional de streaming en vivo similar a StreamYard. Transmite a múltiples plataformas simultáneamente.",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="es">
<body className="antialiased">
{children}
</body>
</html>
);
}

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

@ -0,0 +1,21 @@
import Navigation from "@/components/Navigation";
import HeroSection from "@/components/HeroSection";
import FeaturesCarousel from "@/components/FeaturesCarousel";
import TestimonialsSection from "@/components/TestimonialsSection";
import ContentSection from "@/components/ContentSection";
import FeatureHighlights from "@/components/FeatureHighlights";
import CTAAndFooter from "@/components/CTAAndFooter";
export default function HomePage() {
return (
<div className="min-h-screen bg-white">
<Navigation />
<HeroSection />
<FeaturesCarousel />
<TestimonialsSection />
<ContentSection />
<FeatureHighlights />
<CTAAndFooter />
</div>
);
}

299
src/app/studio/page.tsx Normal file
View File

@ -0,0 +1,299 @@
'use client';
import { useState } from 'react';
import { useAuth } from '@/hooks/useApi';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
export default function StudioPage() {
const { user } = useAuth();
const [isLive, setIsLive] = useState(false);
const [selectedScene, setSelectedScene] = useState(0);
const [cameraConnected, setCameraConnected] = useState(false);
const [selectedPlatform, setSelectedPlatform] = useState<string[]>(['youtube']);
const scenes = [
{ id: 0, name: 'Escena Principal', icon: '📺', active: true },
{ id: 1, name: 'Pantalla Completa', icon: '🖥️', active: false },
{ id: 2, name: 'Cámara + Pantalla', icon: '📱', active: false },
{ id: 3, name: 'Solo Audio', icon: '🎤', active: false }
];
const platforms = [
{ id: 'youtube', name: 'YouTube', color: 'bg-red-500', connected: true },
{ id: 'twitch', name: 'Twitch', color: 'bg-purple-500', connected: false },
{ id: 'facebook', name: 'Facebook', color: 'bg-blue-600', connected: true },
{ id: 'linkedin', name: 'LinkedIn', color: 'bg-blue-700', connected: false }
];
const sources = [
{ id: 1, name: 'Cámara Web', type: 'camera', active: cameraConnected, icon: '📹' },
{ id: 2, name: 'Pantalla', type: 'screen', active: false, icon: '🖥️' },
{ id: 3, name: 'Micrófono', type: 'microphone', active: true, icon: '🎤' },
{ id: 4, name: 'Navegador', type: 'browser', active: false, icon: '🌐' }
];
const handleGoLive = () => {
if (!cameraConnected) {
alert('Conecta una cámara primero');
return;
}
setIsLive(!isLive);
};
const handleConnectCamera = () => {
setCameraConnected(true);
};
return (
<div className="min-h-screen bg-gradient-to-br from-white to-blue-50 dark:from-gray-900 dark:to-gray-800">
{/* Studio Header */}
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
<div className="px-6 py-4 flex justify-between items-center">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
<span className="text-white text-lg">🎥</span>
</div>
<div>
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Estudio Virtual</h1>
<div className="flex items-center space-x-2 text-sm">
<span className={`w-2 h-2 rounded-full ${isLive ? 'bg-red-500 animate-pulse' : 'bg-gray-400'}`}></span>
<span className="text-gray-600 dark:text-gray-400">
{isLive ? 'En Vivo' : 'Desconectado'}
</span>
</div>
</div>
</div>
</div>
<div className="flex items-center space-x-3">
<Button
onClick={handleGoLive}
variant={isLive ? 'danger' : 'primary'}
className={`${isLive ? 'bg-red-500 hover:bg-red-600' : 'bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600'} text-white font-semibold px-6`}
>
{isLive ? '🔴 Detener Stream' : '🔴 Ir en Vivo'}
</Button>
<Button variant="outline">
Configuración
</Button>
</div>
</div>
</div>
<div className="flex h-[calc(100vh-73px)]">
{/* Main Studio Area */}
<div className="flex-1 p-6">
<div className="h-full flex flex-col space-y-6">
{/* Preview Area */}
<Card className="flex-1 p-0 overflow-hidden">
<div className="bg-gradient-to-br from-gray-900 to-black h-full flex items-center justify-center relative">
{cameraConnected ? (
<div className="w-full h-full relative">
{/* Simulated camera feed */}
<div className="w-full h-full bg-gradient-to-br from-blue-900/20 to-purple-900/20 flex items-center justify-center">
<div className="text-center text-white">
<div className="w-32 h-32 bg-white/10 rounded-full flex items-center justify-center mb-4 mx-auto">
<span className="text-4xl">👤</span>
</div>
<h3 className="text-2xl font-semibold mb-2">Vista Previa Activa</h3>
<p className="text-white/70">Cámara conectada - Listo para transmitir</p>
</div>
</div>
{/* Live indicator */}
{isLive && (
<div className="absolute top-4 left-4 bg-red-500 text-white px-3 py-1 rounded-full text-sm font-semibold animate-pulse">
🔴 EN VIVO
</div>
)}
{/* Stream stats */}
{isLive && (
<div className="absolute top-4 right-4 bg-black/50 text-white px-3 py-2 rounded-lg text-sm">
<div>👥 {Math.floor(Math.random() * 500) + 100} espectadores</div>
<div> {Math.floor(Math.random() * 30) + 5}:24</div>
</div>
)}
</div>
) : (
<div className="text-center text-white">
<div className="w-32 h-32 bg-white/10 rounded-full flex items-center justify-center mb-6 mx-auto">
<span className="text-6xl">📹</span>
</div>
<h3 className="text-2xl font-semibold mb-2">Vista Previa del Stream</h3>
<p className="text-white/70 mb-8 max-w-md">
Tu transmisión aparecerá aquí cuando conectes una cámara o fuente de video
</p>
<Button
onClick={handleConnectCamera}
className="bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white"
>
📹 Conectar Cámara
</Button>
</div>
)}
</div>
</Card>
{/* Control Bar */}
<div className="flex justify-center space-x-4">
{sources.map((source) => (
<Button
key={source.id}
variant={source.active ? 'primary' : 'outline'}
className={`flex items-center space-x-2 ${source.active ? 'bg-green-500 hover:bg-green-600' : ''}`}
>
<span>{source.icon}</span>
<span>{source.name}</span>
</Button>
))}
</div>
</div>
</div>
{/* Right Sidebar Controls */}
<div className="w-80 bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm border-l border-gray-200 dark:border-gray-700 p-6 overflow-y-auto">
<div className="space-y-6">
{/* Platforms */}
<Card>
<div className="flex items-center justify-between mb-4">
<h4 className="font-semibold text-gray-900 dark:text-white">Plataformas</h4>
<Button variant="outline" size="sm">+ Conectar</Button>
</div>
<div className="space-y-2">
{platforms.map((platform) => (
<div
key={platform.id}
className={`flex items-center justify-between p-3 rounded-lg border-2 cursor-pointer transition-all ${
selectedPlatform.includes(platform.id)
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500'
}`}
onClick={() => {
if (platform.connected) {
setSelectedPlatform(prev =>
prev.includes(platform.id)
? prev.filter(p => p !== platform.id)
: [...prev, platform.id]
);
}
}}
>
<div className="flex items-center space-x-3">
<div className={`w-3 h-3 rounded-full ${platform.color}`}></div>
<span className="font-medium text-gray-900 dark:text-white">{platform.name}</span>
</div>
<div className="flex items-center space-x-2">
{platform.connected ? (
<span className="text-xs text-green-600 dark:text-green-400 font-medium">Conectado</span>
) : (
<span className="text-xs text-gray-400">Desconectado</span>
)}
{selectedPlatform.includes(platform.id) && (
<span className="text-blue-500"></span>
)}
</div>
</div>
))}
</div>
</Card>
{/* Scenes */}
<Card>
<h4 className="font-semibold text-gray-900 dark:text-white mb-4">Escenas</h4>
<div className="space-y-2">
{scenes.map((scene) => (
<div
key={scene.id}
className={`flex items-center space-x-3 p-3 rounded-lg cursor-pointer transition-all ${
selectedScene === scene.id
? 'bg-gradient-to-r from-blue-500 to-purple-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-900 dark:text-white'
}`}
onClick={() => setSelectedScene(scene.id)}
>
<span className="text-lg">{scene.icon}</span>
<span className="font-medium">{scene.name}</span>
</div>
))}
</div>
<Button variant="outline" className="w-full mt-4">
+ Nueva Escena
</Button>
</Card>
{/* Chat Preview */}
<Card>
<h4 className="font-semibold text-gray-900 dark:text-white mb-4">Chat en Vivo</h4>
<div className="space-y-3 max-h-64 overflow-y-auto">
{isLive ? (
<>
<div className="flex items-start space-x-2">
<div className="w-6 h-6 bg-blue-500 rounded-full flex-shrink-0"></div>
<div className="flex-1">
<span className="font-medium text-sm text-gray-900 dark:text-white">Usuario123</span>
<p className="text-sm text-gray-600 dark:text-gray-300">¡Hola! Gran stream 👏</p>
</div>
</div>
<div className="flex items-start space-x-2">
<div className="w-6 h-6 bg-green-500 rounded-full flex-shrink-0"></div>
<div className="flex-1">
<span className="font-medium text-sm text-gray-900 dark:text-white">StreamFan</span>
<p className="text-sm text-gray-600 dark:text-gray-300">Excelente contenido</p>
</div>
</div>
<div className="flex items-start space-x-2">
<div className="w-6 h-6 bg-purple-500 rounded-full flex-shrink-0"></div>
<div className="flex-1">
<span className="font-medium text-sm text-gray-900 dark:text-white">Viewer456</span>
<p className="text-sm text-gray-600 dark:text-gray-300">¿Cuándo será el próximo stream?</p>
</div>
</div>
</>
) : (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
<span className="text-2xl mb-2 block">💬</span>
<p className="text-sm">Los mensajes aparecerán cuando estés en vivo</p>
</div>
)}
</div>
</Card>
{/* Stream Settings */}
<Card>
<h4 className="font-semibold text-gray-900 dark:text-white mb-4">Configuración Rápida</h4>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600 dark:text-gray-300">Calidad</span>
<select className="text-sm border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
<option>1080p 60fps</option>
<option>1080p 30fps</option>
<option>720p 60fps</option>
<option>720p 30fps</option>
</select>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600 dark:text-gray-300">Bitrate</span>
<select className="text-sm border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
<option>6000 kbps</option>
<option>4500 kbps</option>
<option>3000 kbps</option>
<option>2000 kbps</option>
</select>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600 dark:text-gray-300">Grabar Stream</span>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
</Card>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,215 @@
'use client';
import Link from "next/link";
import Image from "next/image";
interface FooterLink {
name: string;
href: string;
}
interface FooterSection {
title: string;
links: FooterLink[];
}
const footerSections: FooterSection[] = [
{
title: "Producto",
links: [
{ name: "Funciones", href: "/features" },
{ name: "Precios", href: "/pricing" },
{ name: "API", href: "/api" },
{ name: "Integraciones", href: "/integrations" }
]
},
{
title: "Soporte",
links: [
{ name: "Documentación", href: "/docs" },
{ name: "Contacto", href: "/contact" },
{ name: "Estado", href: "/status" },
{ name: "Comunidad", href: "/community" }
]
},
{
title: "Empresa",
links: [
{ name: "Acerca de", href: "/about" },
{ name: "Blog", href: "/blog" },
{ name: "Carreras", href: "/careers" },
{ name: "Prensa", href: "/press" }
]
}
];
const platforms = ['YouTube', 'Twitch', 'Facebook', 'LinkedIn', 'TikTok'];
const companies = [
{ name: 'Microsoft', logo: '🏢' },
{ name: 'Adobe', logo: '🎨' },
{ name: 'Netflix', logo: '🎬' },
{ name: 'Spotify', logo: '🎵' },
{ name: 'Google', logo: '🔍' },
{ name: 'Meta', logo: '📘' }
];
export default function CTAAndFooter() {
return (
<>
{/* Final CTA Section */}
<section className="bg-white py-20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl lg:text-5xl font-bold text-gray-900 mb-8">
Transmite profesionalmente desde el primer día
</h2>
<p className="text-xl text-gray-600 mb-10 leading-relaxed">
Con AvanzaCast, no necesitas ser un experto en tecnología para crear transmisiones de alta calidad.
Comienza en minutos y crece con nosotros.
</p>
<div className="flex flex-col sm:flex-row gap-6 justify-center items-center mb-8">
<Link href="/auth/register">
<button className="bg-gradient-to-r from-teal-600 to-teal-700 hover:from-teal-700 hover:to-teal-800 text-white px-10 py-5 text-xl font-bold rounded-xl transition-all duration-200 transform hover:scale-105 hover:shadow-xl active:scale-95 shadow-lg">
Comenzar prueba gratuita
</button>
</Link>
<button className="text-gray-600 hover:text-gray-800 font-semibold underline transition-colors text-lg hover:bg-white px-4 py-3 rounded-lg hover:shadow-sm">
Ver demos en vivo
</button>
</div>
<p className="text-sm text-gray-500">
Sin tarjeta de crédito requerida Configuración en 2 minutos Soporte 24/7
</p>
</div>
</section>
{/* Trusted By Section */}
<section className="bg-gray-50 py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<p className="text-gray-500 text-lg mb-8">Confiado por más de 4 millones de creadores</p>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-8 items-center opacity-60">
{companies.map((company, index) => (
<div key={index} className="flex flex-col items-center space-y-2 hover:opacity-100 transition-opacity">
<div className="text-3xl">{company.logo}</div>
<span className="text-sm font-medium text-gray-400">{company.name}</span>
</div>
))}
</div>
</div>
</section>
{/* Additional CTA Section */}
<section className="py-20 bg-gradient-to-r from-blue-50 to-indigo-50">
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
¿Listo para elevar tu contenido?
</h2>
<p className="text-xl text-gray-600 mb-8">
Únete a miles de creadores que ya confían en AvanzaCast para sus transmisiones profesionales
</p>
<div className="flex flex-col sm:flex-row gap-6 justify-center mb-8">
<Link href="/auth/register">
<button className="w-full sm:w-auto bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white px-10 py-5 text-xl font-bold rounded-xl transition-all duration-200 transform hover:scale-105 hover:shadow-xl active:scale-95 shadow-lg">
🚀 Crear Cuenta Gratis
</button>
</Link>
<button className="w-full sm:w-auto border-2 border-gray-300 hover:border-gray-400 bg-white hover:bg-gray-50 text-gray-700 px-10 py-5 text-xl font-bold rounded-xl transition-all duration-200 transform hover:scale-105 hover:shadow-lg active:scale-95">
📞 Contactar Ventas
</button>
</div>
{/* Trust indicators */}
<div className="pt-8 border-t border-gray-200">
<p className="text-sm text-gray-500 mb-4">Confiado por creadores en todo el mundo</p>
<div className="flex justify-center items-center space-x-8 opacity-60">
{platforms.map((platform, index) => (
<div
key={platform}
className="text-gray-400 font-medium hover:text-gray-600 hover:-translate-y-0.5 transition-all duration-200 cursor-pointer"
>
{platform}
</div>
))}
</div>
</div>
</div>
</section>
{/* Footer */}
<footer className="bg-gray-900 text-white py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid md:grid-cols-4 gap-8">
{/* Logo and description */}
<div>
<div className="mb-4">
<Image
src="/images/logoavanzacast_white.png"
alt="AvanzaCast"
width={200}
height={48}
className="h-12 w-auto"
/>
</div>
<p className="text-gray-400 mb-6">
La plataforma profesional de streaming que necesitas para crear contenido de calidad.
</p>
<div className="flex space-x-4">
{/* Social links */}
<a href="#" className="text-gray-400 hover:text-white transition-colors">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
</svg>
</a>
<a href="#" className="text-gray-400 hover:text-white transition-colors">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M22.46 6c-.77.35-1.6.58-2.46.69.88-.53 1.56-1.37 1.88-2.38-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29 0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15 0 1.49.75 2.81 1.91 3.56-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07 4.28 4.28 0 0 0 4 2.98 8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21 16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56.84-.6 1.56-1.36 2.14-2.23z"/>
</svg>
</a>
<a href="#" className="text-gray-400 hover:text-white transition-colors">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<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.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 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>
</a>
</div>
</div>
{/* Footer sections */}
{footerSections.map((section) => (
<div key={section.title}>
<h4 className="font-semibold mb-4">{section.title}</h4>
<ul className="space-y-2 text-gray-400">
{section.links.map((link) => (
<li key={link.name}>
<Link href={link.href} className="hover:text-white transition-colors">
{link.name}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
<div className="border-t border-gray-700 mt-8 pt-8 flex flex-col md:flex-row justify-between items-center text-gray-400">
<p>© 2024 AvanzaCast. Todos los derechos reservados.</p>
<div className="flex space-x-6 mt-4 md:mt-0">
<Link href="/privacy" className="hover:text-white transition-colors">
Política de Privacidad
</Link>
<Link href="/terms" className="hover:text-white transition-colors">
Términos de Servicio
</Link>
<Link href="/cookies" className="hover:text-white transition-colors">
Cookies
</Link>
</div>
</div>
</div>
</footer>
</>
);
}

View File

@ -0,0 +1,100 @@
'use client';
interface BlogPost {
image: string;
color: string;
title: string;
author: string;
tag: string;
}
const blogPosts: BlogPost[] = [
{
image: "📅",
color: "bg-red-500",
title: "Cómo programar una transmisión en vivo en YouTube",
author: "Kelsey Bentz",
tag: "YouTube"
},
{
image: "🎙️",
color: "bg-blue-600",
title: "7 formas de mejorar el audio de la transmisión en vivo",
author: "AvanzaCast",
tag: "Audio"
},
{
image: "⏱️",
color: "bg-yellow-500",
title: "¿Cuánto tiempo debe durar un podcast? Elegir la duración adecuada del episodio",
author: "AvanzaCast",
tag: "Podcast"
},
{
image: "📺",
color: "bg-blue-700",
title: "Cómo realizar una transmisión en vivo de prueba sin ir en vivo | 3 formas fáciles",
author: "AvanzaCast",
tag: "Tutorial"
}
];
export default function ContentSection() {
return (
<>
{/* Content Header Section */}
<section className="bg-gradient-to-r from-blue-600 via-purple-600 to-green-500 py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl lg:text-4xl font-bold text-white mb-8">
Transmite mejor, crece más rápido
</h2>
<p className="text-xl text-white/90 mb-8 max-w-4xl mx-auto">
Descubre consejos de expertos, guías prácticas y acciones reales diseñadas para ayudarte a
crear contenido de alta calidad, involucrar a tu audiencia y aumentar tu presenciauna
transmisión a la vez.
</p>
<button className="text-blue-600 bg-white hover:bg-gray-50 px-6 py-3 rounded-lg font-medium transition-colors">
Ver todas las publicaciones
</button>
</div>
</section>
{/* Blog Posts Grid */}
<section className="bg-white py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
{blogPosts.map((post, index) => (
<article key={index} className="group cursor-pointer">
<div className={`${post.color} rounded-2xl p-6 mb-4 text-center text-white relative overflow-hidden hover:scale-105 transition-transform`}>
<div className="text-6xl mb-4">{post.image}</div>
<div className="absolute bottom-4 left-4 right-4">
<p className="text-sm opacity-90">avanzacast.com/blog</p>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full">
{post.tag}
</span>
</div>
<p className="text-sm text-gray-500">
Escrito por <span className="text-blue-600 font-medium">{post.author}</span>
</p>
<h3 className="font-bold text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-3">
{post.title}
</h3>
<button className="text-blue-600 text-sm font-medium flex items-center hover:text-blue-800 transition-colors">
Leer más
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</article>
))}
</div>
</div>
</section>
</>
);
}

View File

@ -0,0 +1,105 @@
'use client';
export default function FeatureHighlights() {
return (
<section className="bg-gray-50 py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* First Feature */}
<div className="grid lg:grid-cols-2 gap-16 items-center mb-20">
<div>
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-6">
Transmite en vivo o graba podcasts con invitados remotos
</h2>
<p className="text-xl text-gray-600 leading-relaxed">
Los invitados pueden unirse fácilmente desde su navegador o teléfono en unos pocos clics.
No hace falta descargar ningún software.
</p>
<div className="mt-8">
<button className="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white px-8 py-4 rounded-xl font-bold text-lg transition-all duration-200 transform hover:scale-105 hover:shadow-xl active:scale-95 shadow-lg">
Invitar colaboradores
</button>
</div>
</div>
<div className="bg-pink-100 rounded-3xl p-8">
<div className="bg-white rounded-2xl p-6 shadow-lg">
<div className="flex items-center space-x-4 mb-4">
<div className="w-20 h-16 bg-gray-300 rounded flex items-center justify-center">
<span className="text-xs font-medium">James</span>
</div>
<div className="w-20 h-16 bg-gray-300 rounded flex items-center justify-center">
<span className="text-xs font-medium">Helena</span>
</div>
</div>
<div className="bg-purple-600 rounded-lg p-4 text-center">
<div className="text-white text-lg font-semibold">Melanie Dyann Howe</div>
<div className="text-purple-200 text-sm">Anfitriona</div>
</div>
</div>
</div>
</div>
{/* Second Feature */}
<div className="grid lg:grid-cols-2 gap-16 items-center">
<div className="order-2 lg:order-1">
<div className="bg-gradient-to-r from-purple-600 to-blue-600 rounded-3xl p-8 text-white">
<div className="flex items-center justify-between mb-4">
<span className="text-sm opacity-80">Zoom</span>
<span className="text-sm font-semibold">AvanzaCast</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-black/20 rounded-lg aspect-video flex items-center justify-center">
<span className="text-xs">📹 Video Feed 1</span>
</div>
<div className="bg-black/20 rounded-lg aspect-video flex items-center justify-center">
<span className="text-xs">📹 Video Feed 2</span>
</div>
</div>
<div className="mt-4 text-center">
<div className="inline-flex items-center bg-green-500 text-white px-3 py-1 rounded-full text-xs">
<div className="w-2 h-2 bg-white rounded-full mr-2"></div>
Grabando en HD
</div>
</div>
</div>
</div>
<div className="order-1 lg:order-2">
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-6">
Grabaciones con calidad de estudio, independientemente de tu conexión
</h2>
<p className="text-xl text-gray-600 leading-relaxed mb-8">
¿Te cansaste de que tus podcasts queden arruinados con Zoom y Skype? Con las grabaciones locales,
se graba un archivo de video separado de cada invitado directamente en su computadora, sin importar
qué tan mala sea su conexión a internet.
</p>
<div className="space-y-4">
<div className="flex items-center space-x-3">
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<span className="text-gray-600">Grabación local en cada dispositivo</span>
</div>
<div className="flex items-center space-x-3">
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<span className="text-gray-600">Calidad 4K independiente de la conexión</span>
</div>
<div className="flex items-center space-x-3">
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<span className="text-gray-600">Sincronización automática</span>
</div>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,202 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import Link from 'next/link';
import Image from 'next/image';
interface Feature {
icon: string;
title: string;
description: string;
}
const features: Feature[] = [
{
icon: '/images/carousel_recording.svg',
title: 'Recording',
description: 'Graba contenido de alta calidad'
},
{
icon: '/images/carousel_streaming.svg',
title: 'Streaming',
description: 'Transmite en vivo a múltiples plataformas'
},
{
icon: '/images/carousel_guests.svg',
title: 'Guests',
description: 'Invita colaboradores fácilmente'
},
{
icon: '/images/carousel_branding.svg',
title: 'Branding',
description: 'Personaliza tu transmisión'
},
{
icon: '/images/carousel_engagement.svg',
title: 'Engagement',
description: 'Interactúa con tu audiencia'
},
{
icon: '/images/carousel_podcasts.svg',
title: 'Podcasts',
description: 'Crea podcasts profesionales'
},
{
icon: '/images/carousel_repurpose.svg',
title: 'Repurpose',
description: 'Reutiliza tu contenido'
}
];
export default function FeaturesCarousel() {
const scrollRef = useRef<HTMLDivElement>(null);
// Create many duplicates for truly infinite scroll illusion
const multiplier = 15; // Number of times to repeat the features
const duplicatedFeatures = Array.from({ length: multiplier }, () => features).flat();
const scrollLeft = () => {
if (scrollRef.current) {
const container = scrollRef.current;
const scrollAmount = 320;
container.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
}
};
const scrollRight = () => {
if (scrollRef.current) {
const container = scrollRef.current;
const scrollAmount = 320;
container.scrollBy({ left: scrollAmount, behavior: 'smooth' });
}
};
const handleScroll = () => {
if (scrollRef.current) {
const container = scrollRef.current;
const singleSetWidth = features.length * (288 + 24); // w-72 (288px) + space-x-6 (24px)
const totalWidth = singleSetWidth * multiplier;
const middleStart = singleSetWidth * Math.floor(multiplier / 2);
const tolerance = 50;
// Use requestAnimationFrame to ensure smooth transitions
requestAnimationFrame(() => {
// If scrolled near the beginning, jump to middle area
if (container.scrollLeft <= tolerance) {
container.scrollLeft = middleStart;
}
// If scrolled near the end, jump back to middle area
else if (container.scrollLeft >= totalWidth - container.clientWidth - tolerance) {
container.scrollLeft = middleStart;
}
});
}
};
useEffect(() => {
// Initialize scroll position to middle area for seamless looping
if (scrollRef.current) {
const singleSetWidth = features.length * (288 + 24); // w-72 (288px) + space-x-6 (24px)
const middleStart = singleSetWidth * Math.floor(multiplier / 2);
// Set initial position without animation
scrollRef.current.scrollLeft = middleStart;
}
}, []);
// Debounce scroll handler to prevent excessive calls
useEffect(() => {
let scrollTimeout: NodeJS.Timeout;
const debouncedHandleScroll = () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(handleScroll, 150);
};
const container = scrollRef.current;
if (container) {
container.addEventListener('scroll', debouncedHandleScroll, { passive: true });
return () => {
container.removeEventListener('scroll', debouncedHandleScroll);
clearTimeout(scrollTimeout);
};
}
}, []);
return (
<section className="bg-gray-50 py-16 w-full">
<div className="w-full">
<div className="text-center mb-12 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
<p className="text-gray-600 mb-8">
AvanzaCast es un estudio profesional para grabar y hacer transmisiones en vivo desde tu
<br />
navegador. Graba contenido o transmite en vivo a Facebook, YouTube y otras plataformas.
</p>
</div>
{/* Feature Cards Carousel */}
<div className="relative w-full">
{/* Left Arrow */}
<button
onClick={scrollLeft}
className="absolute left-4 top-1/2 -translate-y-1/2 z-10 w-12 h-12 rounded-full bg-white shadow-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 cursor-pointer transition-all"
>
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
{/* Right Arrow */}
<button
onClick={scrollRight}
className="absolute right-4 top-1/2 -translate-y-1/2 z-10 w-12 h-12 rounded-full bg-white shadow-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 cursor-pointer transition-all"
>
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Carousel Container */}
<div
ref={scrollRef}
className="flex space-x-6 overflow-x-auto scrollbar-hide carousel-smooth pb-4 px-16"
>
{duplicatedFeatures.map((feature, index) => {
const setNumber = Math.floor(index / features.length);
const itemIndex = index % features.length;
return (
<div key={`set-${setNumber}-item-${itemIndex}-${feature.title}`} className="flex-shrink-0 w-72">
<Link
href="/auth/register"
className="block group hover:scale-105 transition-transform duration-200"
>
<div className="bg-white rounded-2xl p-6 text-center shadow-sm hover:shadow-lg transition-all duration-200 border border-gray-100 hover:border-blue-200">
<div className="mb-6 overflow-hidden rounded-xl">
<Image
src={feature.icon}
alt={feature.title}
width={400}
height={160}
className="w-full h-40 object-cover group-hover:scale-110 transition-transform duration-300"
onError={(e) => {
// Fallback to placeholder if image fails to load
const target = e.target as HTMLImageElement;
target.src = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="%23f3f4f6"/><text x="50" y="50" font-family="Arial" font-size="12" fill="%236b7280" text-anchor="middle" dy=".3em">${feature.title}</text></svg>`;
}}
/>
</div>
<h5 className="font-bold text-gray-900 text-lg mb-2 group-hover:text-blue-600 transition-colors">
{feature.title}
</h5>
</div>
</Link>
</div>
);
})}
</div>
</div>
</div>
</section>
);
}

237
src/components/Header.tsx Normal file
View File

@ -0,0 +1,237 @@
'use client';
import React, { useState } from 'react';
import Image from 'next/image';
import { useAuth, useTheme } from '@/hooks/useApi';
import Button from './ui/Button';
interface HeaderProps {
onToggleSidebar: () => void;
}
const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
const { user, logout } = useAuth();
const { theme, toggleTheme } = useTheme();
const [showProfileMenu, setShowProfileMenu] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const notifications = [
{
id: '1',
title: 'Transmisión iniciada',
message: 'Tu stream "Tutorial AvanzaCast" está en vivo',
time: '2 min',
type: 'success'
},
{
id: '2',
title: 'Nueva plataforma conectada',
message: 'YouTube se conectó exitosamente',
time: '1 hora',
type: 'info'
}
];
return (
<header className="sticky top-0 z-40 bg-white dark:bg-[#0e1726] border-b border-[#e0e6ed] dark:border-[#253b5c]">
<div className="flex items-center justify-between px-6 py-4">
{/* Left side */}
<div className="flex items-center space-x-4">
{/* Sidebar toggle */}
<button
onClick={onToggleSidebar}
className="lg:hidden p-2 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
{/* Search */}
<div className="hidden md:block relative">
<input
type="text"
placeholder="Buscar transmisiones..."
className="w-80 pl-10 pr-4 py-2 bg-gray-50 dark:bg-[#1b2e4b] border border-[#e0e6ed] dark:border-[#253b5c] rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary"
/>
<svg
className="w-5 h-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
{/* Right side */}
<div className="flex items-center space-x-4">
{/* Go Live Button */}
<Button
variant="danger"
size="sm"
icon={
<div className="w-2 h-2 bg-white rounded-full animate-pulse" />
}
>
Ir en Vivo
</Button>
{/* Theme toggle */}
<button
onClick={toggleTheme}
className="p-2 text-black dark:text-white hover:text-primary-600 dark:hover:text-primary-400 hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-xl transition-all duration-200 hover:-translate-y-0.5 hover:scale-110 hover:shadow-md"
>
{theme === 'light' ? (
<svg className="w-5 h-5 hover:rotate-12 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
) : (
<svg className="w-5 h-5 hover:rotate-12 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
)}
</button>
{/* Notifications */}
<div className="relative">
<button
onClick={() => setShowNotifications(!showNotifications)}
className="p-2 text-black dark:text-white hover:text-primary-600 dark:hover:text-primary-400 hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-xl relative transition-all duration-200 hover:-translate-y-0.5 hover:scale-110 hover:shadow-md"
>
<svg className="w-5 h-5 hover:rotate-12 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-5 5v-5zM11 19H7a2 2 0 01-2-2V7a2 2 0 012-2h5m2-3v12a2 2 0 01-2 2m-1 1a3 3 0 01-6 0" />
</svg>
<span className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full text-xs animate-pulse"></span>
</button>
{/* Notifications dropdown */}
{showNotifications && (
<div className="absolute right-0 mt-2 w-80 bg-white dark:bg-[#1b2e4b] rounded-xl shadow-xl border border-[#e0e6ed] dark:border-[#253b5c] z-50 animate-slideDown">
<div className="p-4 border-b border-[#e0e6ed] dark:border-[#253b5c]">
<h3 className="font-semibold text-black dark:text-white">Notificaciones</h3>
</div>
<div className="max-h-64 overflow-y-auto">
{notifications.map((notification) => (
<div key={notification.id} className="p-4 hover:bg-primary-50 dark:hover:bg-primary-900/20 border-b border-[#e0e6ed] dark:border-[#253b5c] last:border-0 transition-all duration-200 hover:-translate-y-0.5 cursor-pointer">
<div className="flex items-start space-x-3">
<div className={`w-2 h-2 mt-2 rounded-full ${
notification.type === 'success' ? 'bg-success' : 'bg-info'
}`} />
<div className="flex-1">
<h4 className="text-sm font-medium text-black dark:text-white">
{notification.title}
</h4>
<p className="text-sm text-white-dark mt-1">
{notification.message}
</p>
<span className="text-xs text-white-dark mt-1">
hace {notification.time}
</span>
</div>
</div>
</div>
))}
</div>
<div className="p-4 border-t border-[#e0e6ed] dark:border-[#253b5c]">
<button className="w-full text-center text-sm text-primary-600 hover:text-primary-800 hover:bg-primary-50 py-2 rounded-lg transition-all duration-200">
Ver todas las notificaciones
</button>
</div>
</div>
)}
</div>
{/* Profile menu */}
<div className="relative group">
<button
onClick={() => setShowProfileMenu(!showProfileMenu)}
className="flex items-center space-x-3 p-2 hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-xl transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"
>
<Image
src={user?.avatar || '/api/placeholder/32/32'}
alt={user?.name || 'Usuario'}
width={32}
height={32}
className="w-8 h-8 rounded-full group-hover:scale-110 transition-transform border-2 border-transparent group-hover:border-primary-200"
/>
<div className="hidden md:block text-left">
<p className="text-sm font-medium text-black dark:text-white group-hover:text-primary-600 transition-colors">
{user?.name || 'Usuario'}
</p>
<p className="text-xs text-white-dark group-hover:text-primary-500 transition-colors">
{user?.plan || 'Free'}
</p>
</div>
<svg className={`w-4 h-4 text-white-dark group-hover:text-primary-600 transition-all duration-200 ${showProfileMenu ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Profile dropdown */}
{showProfileMenu && (
<div className="absolute right-0 mt-2 w-56 bg-white dark:bg-[#1b2e4b] rounded-xl shadow-xl border border-[#e0e6ed] dark:border-[#253b5c] z-50 animate-slideDown">
<div className="p-4 border-b border-[#e0e6ed] dark:border-[#253b5c]">
<div className="flex items-center space-x-3">
<Image
src={user?.avatar || '/api/placeholder/40/40'}
alt={user?.name || 'Usuario'}
width={40}
height={40}
className="w-10 h-10 rounded-full"
/>
<div>
<p className="font-medium text-black dark:text-white">
{user?.name || 'Usuario'}
</p>
<p className="text-sm text-white-dark">
{user?.email || 'email@example.com'}
</p>
</div>
</div>
</div>
<div className="py-2">
<a href="/profile" className="flex items-center px-4 py-2 text-sm text-black dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 hover:text-primary-600 transition-all duration-200 hover:-translate-y-0.5">
<svg className="w-4 h-4 mr-3 hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Mi Perfil
</a>
<a href="/settings" className="flex items-center px-4 py-2 text-sm text-black dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 hover:text-primary-600 transition-all duration-200 hover:-translate-y-0.5">
<svg className="w-4 h-4 mr-3 hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Configuración
</a>
<a href="/billing" className="flex items-center px-4 py-2 text-sm text-black dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 hover:text-primary-600 transition-all duration-200 hover:-translate-y-0.5">
<svg className="w-4 h-4 mr-3 hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
Facturación
</a>
</div>
<div className="border-t border-[#e0e6ed] dark:border-[#253b5c] py-2">
<button
onClick={logout}
className="flex items-center w-full px-4 py-2 text-sm text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-700 transition-all duration-200 hover:-translate-y-0.5"
>
<svg className="w-4 h-4 mr-3 hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Cerrar Sesión
</button>
</div>
</div>
)}
</div>
</div>
</div>
</header>
);
};
export default Header;

View File

@ -0,0 +1,178 @@
'use client';
import Link from "next/link";
import { useState } from "react";
export default function HeroSection() {
const [email, setEmail] = useState('');
const [isGoogleAuthenticated, setIsGoogleAuthenticated] = useState(false);
const [googleUser, setGoogleUser] = useState({
name: 'Nextv',
email: 'nextv.stream@gmail.com',
initial: 'N'
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Redirect to register page with email
window.location.href = `/auth/register?email=${encodeURIComponent(email)}`;
};
const handleGoogleLogin = () => {
// TODO: Implement Google OAuth integration
// For demo purposes, simulate login after 1 second
console.log('Google login clicked');
setTimeout(() => {
setIsGoogleAuthenticated(true);
}, 1000);
};
const handleGoogleContinue = () => {
// User is already logged in with Google, proceed to dashboard
console.log('Continuing with Google account');
window.location.href = '/dashboard';
};
return (
<section className="relative bg-white py-20 lg:py-32">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-16 items-center">
{/* Left Content */}
<div className="space-y-8">
<div className="space-y-6">
<h1 className="text-4xl lg:text-6xl font-black text-gray-900 leading-tight">
La manera más sencilla de
<br />
<span className="text-gray-900">grabar y transmitir</span>
<br />
<span className="text-gray-900">en vivo</span>
</h1>
<p className="text-xl text-gray-600 leading-relaxed max-w-lg">
AvanzaCast es un estudio profesional para grabar y hacer transmisiones en vivo desde tu
navegador. Graba contenido o transmite en vivo a Facebook, YouTube y otras plataformas.
</p>
{/* Trial Notice */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start">
<div className="text-blue-600 mr-3 mt-0.5"></div>
<div>
<p className="text-blue-900 font-medium">¡Te refieres a AvanzaCast!</p>
<p className="text-blue-700 text-sm">Recibirás una prueba gratuita de 14 días. No se requiere tarjeta.</p>
</div>
</div>
</div>
</div>
</div>
{/* Right Content - Registration Form */}
<div className="bg-white rounded-2xl shadow-xl border border-gray-200 p-6 max-w-md mx-auto">
{/* Google OAuth Button */}
<div className="mb-4">
{!isGoogleAuthenticated ? (
// Initial Google Sign In Button (not logged in)
<button
onClick={handleGoogleLogin}
className="w-full flex items-center justify-center px-4 py-2.5 border border-gray-300 rounded-md shadow-sm bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
>
<div className="flex items-center space-x-3">
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC04" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
<span className="text-sm font-medium text-gray-700">Continuar con Google</span>
</div>
</button>
) : (
// Authenticated Google User Button (already logged in)
<button
onClick={handleGoogleContinue}
className="w-full flex items-center px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
>
<div className="w-7 h-7 bg-purple-600 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-bold">{googleUser.initial}</span>
</div>
<div className="flex-1 ml-3">
<p className="text-sm font-medium text-gray-900">Continuar como {googleUser.name}</p>
<p className="text-xs text-gray-500">{googleUser.email}</p>
</div>
<svg className="w-4 h-4 text-gray-400" viewBox="0 0 24 24" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
</button>
)}
</div>
{/* Divider */}
<div className="relative mb-4">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-3 bg-white text-gray-500">O continúa con tu correo</span>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Ingresa tu dirección de correo electrónico"
className="w-full px-6 py-4 text-base border-2 border-gray-300 rounded-xl focus:ring-4 focus:ring-blue-500/20 focus:border-blue-500 transition-all duration-200 placeholder:text-gray-500 hover:border-gray-400 shadow-sm hover:shadow-md"
required
/>
</div>
<button
type="submit"
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white py-4 px-6 rounded-xl font-bold text-base transition-all duration-200 transform hover:scale-105 hover:shadow-lg active:scale-95 shadow-md"
>
¡Empiezo gratis ahora!
</button>
<div className="text-center py-2">
<p className="text-green-600 font-medium text-xs">¡Confiado por más de 12,000,000 creadores!</p>
</div>
<div className="text-xs text-gray-500 text-center leading-tight">
<p>
Al continuar, aceptas nuestros{' '}
<Link href="/terms" className="text-blue-600 hover:underline">
Términos de Servicio
</Link>
, {' '}
<Link href="/privacy-policy" className="text-blue-600 hover:underline">
Política de Uso
</Link>
, y reconoces nuestra{' '}
<Link href="/privacy" className="text-blue-600 hover:underline">
Política de Privacidad
</Link>
.
</p>
</div>
<div className="text-center pt-2">
<p className="text-sm text-gray-600">
¿Ya usas AvanzaCast?{' '}
<Link href="/auth/login" className="text-blue-600 hover:underline font-medium">
Inicia sesión
</Link>
.
</p>
</div>
</form>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,37 @@
'use client';
import React, { useState } from 'react';
import Sidebar from './Sidebar';
import Header from './Header';
interface MainLayoutProps {
children: React.ReactNode;
}
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const toggleSidebar = () => {
setSidebarOpen(!sidebarOpen);
};
return (
<div className="min-h-screen bg-[#f1f5f9] dark:bg-[#060818]">
{/* Sidebar */}
<Sidebar isOpen={sidebarOpen} onToggle={toggleSidebar} />
{/* Main content area */}
<div className="lg:pl-64 flex flex-col min-h-screen">
{/* Header */}
<Header onToggleSidebar={toggleSidebar} />
{/* Page content */}
<main className="flex-1 p-6">
{children}
</main>
</div>
</div>
);
};
export default MainLayout;

View File

@ -0,0 +1,105 @@
'use client';
import Link from "next/link";
import Image from "next/image";
import { useState } from "react";
interface NavigationItem {
name: string;
dropdown: boolean;
href?: string;
}
const navigationItems: NavigationItem[] = [
{ name: 'Producto', dropdown: true },
{ name: 'Comunidad', dropdown: false, href: '/community' },
{ name: 'AvanzaCast para', dropdown: false, href: '/for' },
{ name: 'Únete a nosotros', dropdown: false, href: '/join' }
];
export default function Navigation() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<nav className="bg-white shadow-sm border-b border-gray-100 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-20">
{/* Logo */}
<div className="flex-shrink-0 flex items-center">
<Link href="/" className="flex items-center space-x-3">
<Image
src="/images/logoavanzacast_black.png"
alt="AvanzaCast"
width={200}
height={48}
className="h-12 w-auto"
/>
</Link>
</div>
{/* Desktop Navigation */}
<nav className="hidden md:flex space-x-8">
{navigationItems.map((item) => (
<div key={item.name} className="relative">
<Link
href={item.href || '#'}
className="text-gray-700 hover:text-gray-900 px-3 py-2 text-sm font-medium flex items-center transition-colors"
>
{item.name}
{item.dropdown && (
<svg className="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
)}
</Link>
</div>
))}
</nav>
{/* CTA Buttons */}
<div className="flex items-center space-x-4">
<Link href="/auth/login">
<button className="text-gray-700 hover:text-gray-900 px-5 py-3 text-base font-semibold transition-all duration-200 hover:bg-gray-50 rounded-xl">
Accede
</button>
</Link>
<Link href="/auth/register">
<button className="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white px-7 py-3 rounded-xl text-base font-bold transition-all duration-200 transform hover:scale-105 hover:shadow-lg active:scale-95 shadow-md">
Empezamos
</button>
</Link>
{/* Mobile menu button */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden text-gray-700 hover:text-gray-900 p-2"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d={mobileMenuOpen ? "M6 18L18 6M6 6l12 12" : "M4 6h16M4 12h16M4 18h16"} />
</svg>
</button>
</div>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<div className="md:hidden py-4 border-t border-gray-100">
<div className="flex flex-col space-y-2">
{navigationItems.map((item) => (
<Link
key={item.name}
href={item.href || '#'}
className="text-gray-700 hover:text-gray-900 px-3 py-2 text-sm font-medium"
onClick={() => setMobileMenuOpen(false)}
>
{item.name}
</Link>
))}
</div>
</div>
)}
</div>
</nav>
);
}

222
src/components/Sidebar.tsx Normal file
View File

@ -0,0 +1,222 @@
'use client';
import React, { useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface SidebarProps {
isOpen: boolean;
onToggle: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => {
const pathname = usePathname();
const [expandedMenus, setExpandedMenus] = useState<string[]>([]);
const toggleSubmenu = (menuId: string) => {
setExpandedMenus(prev =>
prev.includes(menuId)
? prev.filter(id => id !== menuId)
: [...prev, menuId]
);
};
const menuItems = [
{
id: 'dashboard',
label: 'Dashboard',
href: '/dashboard',
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none">
<path d="M13 9V3H21V9H13ZM3 13V3H11V13H3ZM13 21V11H21V21H13ZM3 21V15H11V21H3Z" fill="currentColor"/>
</svg>
)
},
{
id: 'studio',
label: 'Estudio',
href: '/studio',
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none">
<path d="M17 10.5V7C17 4.24 14.76 2 12 2S7 4.24 7 7V10.5C6.45 10.5 6 10.95 6 11.5V18.5C6 19.05 6.45 19.5 7 19.5H17C17.55 19.5 18 19.05 18 18.5V11.5C18 10.95 17.55 10.5 17 10.5ZM12 15.5C11.17 15.5 10.5 14.83 10.5 14S11.17 12.5 12 12.5S13.5 13.17 13.5 14S12.83 15.5 12 15.5ZM15.1 10.5H8.9V7C8.9 5.29 10.29 3.9 12 3.9S15.1 5.29 15.1 7V10.5Z" fill="currentColor"/>
</svg>
)
},
{
id: 'streams',
label: 'Transmisiones',
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none">
<path d="M21 6H3C2.45 6 2 6.45 2 7V17C2 17.55 2.45 18 3 18H21C21.55 18 22 17.55 22 17V7C22 6.45 21.55 6 21 6ZM20 16H4V8H20V16ZM10 9V15L15 12L10 9Z" fill="currentColor"/>
</svg>
),
submenu: [
{ label: 'Todas las transmisiones', href: '/streams' },
{ label: 'Transmisiones en vivo', href: '/streams/live' },
{ label: 'Programadas', href: '/streams/scheduled' },
{ label: 'Grabaciones', href: '/streams/recordings' }
]
},
{
id: 'platforms',
label: 'Plataformas',
href: '/platforms',
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none">
<path d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 1L13.5 2.5L17.5 6.5H6.5L10.5 2.5L9 1L3 7V9H4L6 21H8L10 9H14L16 21H18L20 9H21Z" fill="currentColor"/>
</svg>
)
},
{
id: 'analytics',
label: 'Analytics',
href: '/analytics',
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none">
<path d="M16 6L18.29 8.29L13.41 13.17L9.41 9.17L2 16.59L3.41 18L9.41 12L13.41 16L19.71 9.71L22 12V6H16Z" fill="currentColor"/>
</svg>
)
}
];
const isActiveRoute = (href: string) => {
return pathname === href || pathname.startsWith(href + '/');
};
return (
<>
{/* Overlay para móvil */}
{isOpen && (
<div
className="fixed inset-0 z-40 bg-black/60 lg:hidden"
onClick={onToggle}
/>
)}
{/* Sidebar */}
<div className={`
fixed top-0 left-0 z-50 h-full w-64 bg-white dark:bg-[#0e1726]
border-r border-[#e0e6ed] dark:border-[#253b5c]
transform transition-transform duration-300 ease-in-out
${isOpen ? 'translate-x-0' : '-translate-x-full'}
lg:translate-x-0 lg:static lg:z-auto
`}>
{/* Header del sidebar */}
<div className="flex items-center justify-between p-6 border-b border-[#e0e6ed] dark:border-[#253b5c]">
<Link href="/dashboard" className="flex items-center space-x-3">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-lg">A</span>
</div>
<span className="text-xl font-bold text-black dark:text-white">
AvanzaCast
</span>
</Link>
{/* Botón cerrar para móvil */}
<button
onClick={onToggle}
className="lg:hidden p-1 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Navegación */}
<nav className="flex-1 px-4 py-6 space-y-2 overflow-y-auto">
{menuItems.map((item) => (
<div key={item.id}>
{item.submenu ? (
<>
<button
onClick={() => toggleSubmenu(item.id)}
className={`
w-full flex items-center justify-between px-4 py-3 text-sm font-medium rounded-lg
transition-colors duration-200
${expandedMenus.includes(item.id)
? 'bg-primary-light dark:bg-primary-dark-light text-primary'
: 'text-black dark:text-white hover:bg-gray-100 dark:hover:bg-[#1b2e4b]'
}
`}
>
<div className="flex items-center space-x-3">
{item.icon}
<span>{item.label}</span>
</div>
<svg
className={`w-4 h-4 transition-transform duration-200 ${
expandedMenus.includes(item.id) ? 'rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Submenu */}
<div className={`
mt-2 space-y-1 pl-12 overflow-hidden transition-all duration-200
${expandedMenus.includes(item.id) ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'}
`}>
{item.submenu.map((subItem, index) => (
<Link
key={index}
href={subItem.href}
className={`
block px-4 py-2 text-sm rounded-lg transition-colors duration-200
${isActiveRoute(subItem.href)
? 'bg-primary text-white'
: 'text-white-dark hover:bg-gray-100 dark:hover:bg-[#1b2e4b] hover:text-black dark:hover:text-white'
}
`}
>
{subItem.label}
</Link>
))}
</div>
</>
) : (
<Link
href={item.href}
className={`
flex items-center space-x-3 px-4 py-3 text-sm font-medium rounded-lg
transition-colors duration-200
${isActiveRoute(item.href)
? 'bg-primary text-white'
: 'text-black dark:text-white hover:bg-gray-100 dark:hover:bg-[#1b2e4b]'
}
`}
>
{item.icon}
<span>{item.label}</span>
</Link>
)}
</div>
))}
</nav>
{/* Footer del sidebar */}
<div className="p-4 border-t border-[#e0e6ed] dark:border-[#253b5c]">
<div className="bg-primary-light dark:bg-primary-dark-light rounded-lg p-4">
<div className="text-center">
<h4 className="font-medium text-primary mb-2">
¿Necesitas ayuda?
</h4>
<p className="text-xs text-white-dark mb-3">
Consulta nuestra documentación
</p>
<button className="w-full bg-primary text-white text-xs py-2 px-3 rounded-md hover:bg-primary/90 transition-colors">
Ver Guías
</button>
</div>
</div>
</div>
</div>
</>
);
};
export default Sidebar;

View File

@ -0,0 +1,202 @@
'use client';
import { useState, useEffect, useRef } from 'react';
interface Testimonial {
text: string;
author: string;
}
const testimonials: Testimonial[] = [
{
text: "Esta probablemente sea la plataforma de transmisión más fácil de usar que conozco. Tengo casi 50 años, así que mucha tecnología puede resultar desafiante. 😅 Pero ustedes sí que lograron diseñar una plataforma supersimple de utilizar. ¡Gracias!🙏",
author: "Bomeca Trotter"
},
{
text: "Uso AvanzaCast desde hace mucho tiempo y sigo eligiéndolo porque es increíble. Es fluido, las transmisiones en vivo nunca se cortan, me encantan todas las herramientas que incluye y puedo usarlo con un equipo sin problemas. ¡Te amo, AvanzaCast!",
author: "Krissy Buck"
},
{
text: "Hace dos años que uso este sistema y me encanta! Nunca hubo problemas del lado de AvanzaCast durante una entrevista. Vale la pena cada centavo que invertí.",
author: "Joy Ann Lajeret"
},
{
text: "La integración con múltiples plataformas es perfecta. Ahorro horas de trabajo cada semana desde que uso AvanzaCast. La calidad del streaming es excepcional.",
author: "Carlos Mendoza"
},
{
text: "Como creadora de contenido, necesitaba una herramienta confiable y profesional. AvanzaCast superó todas mis expectativas. La recomiendo al 100%.",
author: "María González"
}
];
export default function TestimonialsSection() {
const scrollRef = useRef<HTMLDivElement>(null);
const [isAutoPlay, setIsAutoPlay] = useState(true);
// Create many duplicates for truly infinite scroll illusion
const multiplier = 20; // Number of times to repeat the testimonials
const duplicatedTestimonials = Array.from({ length: multiplier }, () => testimonials).flat();
const scrollLeft = () => {
if (scrollRef.current) {
const container = scrollRef.current;
const scrollAmount = 400; // Width of one testimonial card + gap
container.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
}
};
const scrollRight = () => {
if (scrollRef.current) {
const container = scrollRef.current;
const scrollAmount = 400; // Width of one testimonial card + gap
container.scrollBy({ left: scrollAmount, behavior: 'smooth' });
}
};
const handleScroll = () => {
if (scrollRef.current) {
const container = scrollRef.current;
const singleSetWidth = testimonials.length * 400; // Approximate width per testimonial
const totalWidth = singleSetWidth * multiplier;
const middleStart = singleSetWidth * Math.floor(multiplier / 2);
const tolerance = 100;
// Use requestAnimationFrame to ensure smooth transitions
requestAnimationFrame(() => {
// If scrolled near the beginning, jump to middle area
if (container.scrollLeft <= tolerance) {
container.scrollLeft = middleStart;
}
// If scrolled near the end, jump back to middle area
else if (container.scrollLeft >= totalWidth - container.clientWidth - tolerance) {
container.scrollLeft = middleStart;
}
});
}
};
// Auto-scroll functionality
useEffect(() => {
if (!isAutoPlay || !scrollRef.current) return;
const interval = setInterval(() => {
scrollRight();
}, 4000);
return () => clearInterval(interval);
}, [isAutoPlay]);
const pauseAutoPlay = () => setIsAutoPlay(false);
const resumeAutoPlay = () => setIsAutoPlay(true);
// Initialize scroll position to middle area
useEffect(() => {
if (scrollRef.current) {
const singleSetWidth = testimonials.length * 400;
const middleStart = singleSetWidth * Math.floor(multiplier / 2);
scrollRef.current.scrollLeft = middleStart;
}
}, []);
// Debounce scroll handler
useEffect(() => {
let scrollTimeout: NodeJS.Timeout;
const debouncedHandleScroll = () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(handleScroll, 150);
};
const container = scrollRef.current;
if (container) {
container.addEventListener('scroll', debouncedHandleScroll, { passive: true });
return () => {
container.removeEventListener('scroll', debouncedHandleScroll);
clearTimeout(scrollTimeout);
};
}
}, []);
return (
<section className="bg-white py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl lg:text-5xl font-black text-gray-900 mb-16">
Ya se crearon más de 60 millones de transmisiones y grabaciones en AvanzaCast
</h2>
{/* Infinite Carousel Container */}
<div className="relative w-full">
{/* Left Arrow */}
<button
onClick={scrollLeft}
className="absolute left-4 top-1/2 -translate-y-1/2 z-10 w-12 h-12 rounded-full bg-white shadow-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 cursor-pointer transition-all"
>
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
{/* Right Arrow */}
<button
onClick={scrollRight}
className="absolute right-4 top-1/2 -translate-y-1/2 z-10 w-12 h-12 rounded-full bg-white shadow-lg border border-gray-200 flex items-center justify-center hover:bg-gray-50 cursor-pointer transition-all"
>
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Testimonials Carousel */}
<div
ref={scrollRef}
className="flex space-x-8 overflow-x-auto scrollbar-hide carousel-smooth pb-4 px-16"
onMouseEnter={pauseAutoPlay}
onMouseLeave={resumeAutoPlay}
>
{duplicatedTestimonials.map((testimonial, index) => {
const setNumber = Math.floor(index / testimonials.length);
const itemIndex = index % testimonials.length;
return (
<div key={`testimonial-set-${setNumber}-item-${itemIndex}-${testimonial.author}`} className="flex-shrink-0 w-80">
<div className="bg-gray-50 p-8 rounded-2xl h-full">
<p className="text-gray-700 italic mb-6 leading-relaxed text-sm">
&ldquo;{testimonial.text}&rdquo;
</p>
<p className="font-semibold text-gray-900">{testimonial.author}</p>
</div>
</div>
);
})}
</div>
</div>
{/* Auto-play Controls */}
<div className="flex justify-center space-x-4 mt-8">
<button
onClick={() => setIsAutoPlay(!isAutoPlay)}
className="text-sm text-gray-500 hover:text-gray-700 flex items-center space-x-1 transition-colors"
>
{isAutoPlay ? (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 9v6m4-6v6" />
</svg>
<span>Pausar auto-scroll</span>
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M15 14h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Reanudar auto-scroll</span>
</>
)}
</button>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,133 @@
import React from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'outline';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
icon?: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
loading = false,
icon,
className = '',
disabled,
...props
}) => {
const baseClasses = `
inline-flex items-center justify-center
font-semibold rounded-xl
transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500
disabled:opacity-50 disabled:cursor-not-allowed
`;
const variants = {
primary: `
bg-gradient-to-r from-primary-600 to-primary-700
text-white
hover:from-primary-700 hover:to-primary-800
border border-primary-600
hover:border-primary-700
shadow-lg hover:shadow-xl
hover:-translate-y-1
hover:scale-105
`,
secondary: `
bg-gradient-to-r from-secondary to-secondary-600
text-white
hover:from-secondary-600 hover:to-secondary-700
border border-secondary
hover:border-secondary-600
shadow-lg hover:shadow-xl
hover:-translate-y-1
hover:scale-105
`,
success: `
bg-gradient-to-r from-green-500 to-green-600
text-white
hover:from-green-600 hover:to-green-700
border border-green-500
hover:border-green-600
shadow-lg hover:shadow-xl
hover:-translate-y-1
hover:scale-105
`,
danger: `
bg-gradient-to-r from-red-500 to-red-600
text-white
hover:from-red-600 hover:to-red-700
border border-red-500
hover:border-red-600
shadow-lg hover:shadow-xl
hover:-translate-y-1
hover:scale-105
`,
warning: `
bg-gradient-to-r from-yellow-500 to-yellow-600
text-white
hover:from-yellow-600 hover:to-yellow-700
border border-yellow-500
hover:border-yellow-600
shadow-lg hover:shadow-xl
hover:-translate-y-1
hover:scale-105
`,
info: `
bg-gradient-to-r from-blue-500 to-blue-600
text-white
hover:from-blue-600 hover:to-blue-700
border border-blue-500
hover:border-blue-600
shadow-lg hover:shadow-xl
hover:-translate-y-1
hover:scale-105
`,
outline: `
bg-transparent
text-primary-600
border border-primary-300
hover:bg-primary-50
hover:border-primary-400
hover:-translate-y-0.5
hover:shadow-md
`,
ghost: `
bg-transparent
text-gray-700 dark:text-gray-300
border border-transparent
hover:bg-gray-100 dark:hover:bg-gray-800
hover:-translate-y-0.5
`,
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base'
};
const classes = `${baseClasses} ${variants[variant]} ${sizeClasses[size]} ${className}`;
return (
<button
className={classes}
disabled={disabled || loading}
{...props}
>
{loading && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{!loading && icon && <span className="mr-2">{icon}</span>}
{children}
</button>
);
};
export default Button;

View File

@ -0,0 +1,70 @@
import React from 'react';
interface CardProps {
children: React.ReactNode;
title?: string;
subtitle?: string;
className?: string;
bodyClassName?: string;
padding?: boolean;
shadow?: boolean;
actions?: React.ReactNode;
}
const Card: React.FC<CardProps> = ({
children,
title,
subtitle,
className = '',
bodyClassName = '',
padding = true,
shadow = true,
actions
}) => {
const cardClasses = `
bg-white dark:bg-[#1b2e4b]
rounded-2xl
border border-[#e0e6ed] dark:border-[#253b5c]
${shadow ? 'shadow-lg hover:shadow-xl' : ''}
hover:-translate-y-1 hover:border-primary-200 dark:hover:border-primary-600
transition-all duration-300
relative
${className}
`;
const bodyClasses = `
${padding ? 'p-6' : ''}
${bodyClassName}
`;
return (
<div className={cardClasses}>
{(title || subtitle || actions) && (
<div className="flex items-center justify-between p-6 border-b border-[#e0e6ed] dark:border-[#253b5c]">
<div>
{title && (
<h3 className="text-lg font-semibold text-black dark:text-white">
{title}
</h3>
)}
{subtitle && (
<p className="text-sm text-white-dark mt-1">
{subtitle}
</p>
)}
</div>
{actions && (
<div className="flex items-center space-x-2">
{actions}
</div>
)}
</div>
)}
<div className={bodyClasses}>
{children}
</div>
</div>
);
};
export default Card;

View File

@ -0,0 +1,68 @@
import React from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
helper?: string;
icon?: React.ReactNode;
}
const Input: React.FC<InputProps> = ({
label,
error,
helper,
icon,
className = '',
...props
}) => {
const inputClasses = `
form-input
w-full
px-3 py-2
border border-[#e0e6ed] dark:border-[#253b5c]
rounded-lg
bg-white dark:bg-[#1b2e4b]
text-black dark:text-white
placeholder-white-dark
focus:border-primary focus:ring-primary/30
transition-colors duration-200
${error ? 'border-danger focus:border-danger focus:ring-danger/30' : ''}
${icon ? 'pl-10' : ''}
${className}
`;
return (
<div className="w-full">
{label && (
<label className="block text-sm font-medium text-black dark:text-white mb-2">
{label}
</label>
)}
<div className="relative">
{icon && (
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-white-dark">
{icon}
</span>
</div>
)}
<input
className={inputClasses}
{...props}
/>
</div>
{error && (
<p className="mt-1 text-sm text-danger">
{error}
</p>
)}
{helper && !error && (
<p className="mt-1 text-sm text-white-dark">
{helper}
</p>
)}
</div>
);
};
export default Input;

289
src/hooks/useApi.ts Normal file
View File

@ -0,0 +1,289 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { User, Stream, StreamPlatform, StudioScene, ChatMessage, ApiResponse } from '../lib/types';
import { MockAPI } from '../lib/mockApi';
// Hook para autenticación
export function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const login = async (email: string, password: string): Promise<ApiResponse<User>> => {
setIsLoading(true);
try {
const result = await MockAPI.login(email, password);
if (result.success && result.data) {
setUser(result.data);
setIsAuthenticated(true);
// Simular persistencia
localStorage.setItem('user', JSON.stringify(result.data));
}
return result;
} finally {
setIsLoading(false);
}
};
const register = async (userData: Partial<User>): Promise<ApiResponse<User>> => {
setIsLoading(true);
try {
const result = await MockAPI.register(userData);
if (result.success && result.data) {
setUser(result.data);
setIsAuthenticated(true);
localStorage.setItem('user', JSON.stringify(result.data));
}
return result;
} finally {
setIsLoading(false);
}
};
const logout = () => {
setUser(null);
setIsAuthenticated(false);
localStorage.removeItem('user');
};
// Verificar sesión al cargar
useEffect(() => {
const savedUser = localStorage.getItem('user');
if (savedUser) {
try {
const userData = JSON.parse(savedUser);
setUser(userData);
setIsAuthenticated(true);
} catch (error) {
localStorage.removeItem('user');
}
}
}, []);
return {
user,
isAuthenticated,
isLoading,
login,
register,
logout
};
}
// Hook para streams
export function useStreams() {
const [streams, setStreams] = useState<Stream[]>([]);
const [currentStream, setCurrentStream] = useState<Stream | null>(null);
const [isLoading, setIsLoading] = useState(false);
const fetchStreams = async () => {
setIsLoading(true);
try {
const result = await MockAPI.getStreams();
if (result.success && result.data) {
setStreams(result.data);
}
} finally {
setIsLoading(false);
}
};
const createStream = async (streamData: Partial<Stream>): Promise<ApiResponse<Stream>> => {
const result = await MockAPI.createStream(streamData);
if (result.success && result.data) {
setStreams(prev => [...prev, result.data!]);
}
return result;
};
const goLive = async (streamId: string): Promise<ApiResponse<Stream>> => {
const result = await MockAPI.goLive(streamId);
if (result.success && result.data) {
setCurrentStream(result.data);
setStreams(prev => prev.map(s => s.id === streamId ? result.data! : s));
}
return result;
};
useEffect(() => {
fetchStreams();
}, []);
return {
streams,
currentStream,
isLoading,
fetchStreams,
createStream,
goLive,
setCurrentStream
};
}
// Hook para plataformas
export function usePlatforms() {
const [platforms, setPlatforms] = useState<StreamPlatform[]>([]);
const [isLoading, setIsLoading] = useState(false);
const fetchPlatforms = async () => {
setIsLoading(true);
try {
const result = await MockAPI.getPlatforms();
if (result.success && result.data) {
setPlatforms(result.data);
}
} finally {
setIsLoading(false);
}
};
const connectPlatform = async (platformId: string): Promise<ApiResponse<StreamPlatform>> => {
const result = await MockAPI.connectPlatform(platformId);
if (result.success && result.data) {
setPlatforms(prev => prev.map(p => p.id === platformId ? result.data! : p));
}
return result;
};
useEffect(() => {
fetchPlatforms();
}, []);
return {
platforms,
isLoading,
fetchPlatforms,
connectPlatform
};
}
// Hook para escenas del estudio
export function useStudio() {
const [scenes, setScenes] = useState<StudioScene[]>([]);
const [activeScene, setActiveSceneState] = useState<StudioScene | null>(null);
const [isLoading, setIsLoading] = useState(false);
const fetchScenes = async () => {
setIsLoading(true);
try {
const result = await MockAPI.getScenes();
if (result.success && result.data) {
setScenes(result.data);
const active = result.data.find(scene => scene.isActive);
setActiveSceneState(active || null);
}
} finally {
setIsLoading(false);
}
};
const setActiveScene = async (sceneId: string): Promise<ApiResponse<StudioScene>> => {
const result = await MockAPI.setActiveScene(sceneId);
if (result.success && result.data) {
setScenes(prev => prev.map(s => ({ ...s, isActive: s.id === sceneId })));
setActiveSceneState(result.data);
}
return result;
};
useEffect(() => {
fetchScenes();
}, []);
return {
scenes,
activeScene,
isLoading,
fetchScenes,
setActiveScene
};
}
// Hook para chat
export function useChat(streamId: string) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isLoading, setIsLoading] = useState(false);
const fetchMessages = useCallback(async () => {
if (!streamId) return;
setIsLoading(true);
try {
const result = await MockAPI.getChatMessages(streamId);
if (result.success && result.data) {
setMessages(result.data);
}
} finally {
setIsLoading(false);
}
}, [streamId]);
// Simular mensajes en tiempo real
const simulateRealTimeMessages = useCallback(() => {
const interval = setInterval(() => {
const sampleMessages = [
'¡Excelente contenido!',
'¿Cuándo será el próximo stream?',
'Saludos desde México 🇲🇽',
'Me encanta esta plataforma',
'¿Podrías hablar sobre...?'
];
const newMessage: ChatMessage = {
id: Date.now().toString(),
streamId,
username: `Usuario${Math.floor(Math.random() * 1000)}`,
message: sampleMessages[Math.floor(Math.random() * sampleMessages.length)],
timestamp: new Date().toISOString(),
platform: ['youtube', 'facebook', 'twitch'][Math.floor(Math.random() * 3)]
};
setMessages(prev => [...prev, newMessage].slice(-50)); // Mantener solo los últimos 50 mensajes
}, Math.random() * 5000 + 3000); // Entre 3-8 segundos
return () => clearInterval(interval);
}, [streamId]);
useEffect(() => {
fetchMessages();
}, [fetchMessages]);
return {
messages,
isLoading,
fetchMessages,
simulateRealTimeMessages
};
}
// Hook para tema dark/light
export function useTheme() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
if (newTheme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
};
useEffect(() => {
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' || 'light';
setTheme(savedTheme);
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark');
}
}, []);
return {
theme,
toggleTheme
};
}

368
src/lib/mockApi.ts Normal file
View File

@ -0,0 +1,368 @@
import {
User,
Stream,
StreamPlatform,
ChatMessage,
StudioScene,
Analytics,
ApiResponse
} from './types';
// Mock data
export const mockUser: User = {
id: '1',
name: 'César Mendívil',
email: 'cesar@avanzacast.com',
avatar: '/api/placeholder/40/40',
role: 'admin',
createdAt: '2024-01-15T10:00:00Z',
lastLogin: '2024-09-16T08:30:00Z',
isActive: true,
plan: 'pro'
};
export const mockPlatforms: StreamPlatform[] = [
{
id: '1',
name: 'youtube',
displayName: 'YouTube',
isConnected: true,
accountName: 'AvanzaCast Channel',
isLive: false,
viewerCount: 0
},
{
id: '2',
name: 'facebook',
displayName: 'Facebook Live',
isConnected: true,
accountName: 'AvanzaCast Page',
isLive: false,
viewerCount: 0
},
{
id: '3',
name: 'twitch',
displayName: 'Twitch',
isConnected: false,
isLive: false,
viewerCount: 0
},
{
id: '4',
name: 'linkedin',
displayName: 'LinkedIn Live',
isConnected: false,
isLive: false,
viewerCount: 0
}
];
export const mockStreams: Stream[] = [
{
id: '1',
title: 'Tutorial: Cómo usar AvanzaCast',
description: 'Aprende a crear transmisiones profesionales',
status: 'ended',
userId: '1',
startedAt: '2024-09-15T14:00:00Z',
endedAt: '2024-09-15T15:30:00Z',
platforms: mockPlatforms.slice(0, 2),
viewerCount: 0,
maxViewers: 245,
duration: 90,
thumbnailUrl: '/api/placeholder/300/200',
chatEnabled: true,
isPrivate: false,
tags: ['tutorial', 'streaming', 'tech']
},
{
id: '2',
title: 'Webinar: Futuro del Streaming',
description: 'Tendencias y tecnologías emergentes',
status: 'scheduled',
userId: '1',
scheduledAt: '2024-09-18T16:00:00Z',
platforms: mockPlatforms,
viewerCount: 0,
maxViewers: 0,
chatEnabled: true,
isPrivate: false,
tags: ['webinar', 'trends', 'business']
}
];
export const mockScenes: StudioScene[] = [
{
id: '1',
name: 'Escena Principal',
type: 'camera',
isActive: true,
sources: [
{
id: '1',
type: 'camera',
name: 'Cámara Principal',
position: { x: 0, y: 0 },
size: { width: 1280, height: 720 },
isVisible: true,
properties: { deviceId: 'default' }
}
],
layout: {
width: 1280,
height: 720,
backgroundColor: '#000000'
}
},
{
id: '2',
name: 'Presentación',
type: 'screen',
isActive: false,
sources: [
{
id: '2',
type: 'screen',
name: 'Pantalla Compartida',
position: { x: 0, y: 0 },
size: { width: 1280, height: 720 },
isVisible: true,
properties: { displayId: 'primary' }
}
],
layout: {
width: 1280,
height: 720,
backgroundColor: '#1a1a1a'
}
}
];
export const mockChatMessages: ChatMessage[] = [
{
id: '1',
streamId: '1',
username: 'TechFan123',
message: '¡Excelente tutorial!',
timestamp: '2024-09-15T14:15:00Z',
platform: 'youtube'
},
{
id: '2',
streamId: '1',
username: 'StreamerPro',
message: '¿Cuándo será el próximo stream?',
timestamp: '2024-09-15T14:16:00Z',
platform: 'facebook',
isSuper: true
}
];
export const mockAnalytics: Analytics = {
streamId: '1',
totalViews: 1250,
peakViewers: 245,
averageWatchTime: 67.5,
chatMessages: 89,
likes: 156,
shares: 23,
platformBreakdown: {
youtube: 65,
facebook: 35
},
timelineData: Array.from({ length: 20 }, (_, i) => ({
timestamp: new Date(Date.now() - (19 - i) * 5 * 60 * 1000).toISOString(),
viewers: Math.floor(Math.random() * 200) + 50,
chatRate: Math.floor(Math.random() * 10) + 1
}))
};
// Simulación de delay de red
const delay = (ms: number = 500) => new Promise(resolve => setTimeout(resolve, ms));
// Mock API functions
export class MockAPI {
// Autenticación
static async login(email: string, password: string): Promise<ApiResponse<User>> {
await delay();
if (email === mockUser.email && password === 'password') {
return {
success: true,
data: mockUser,
message: 'Inicio de sesión exitoso'
};
}
return {
success: false,
error: 'Credenciales inválidas'
};
}
static async register(userData: Partial<User>): Promise<ApiResponse<User>> {
await delay();
const newUser: User = {
...mockUser,
id: Date.now().toString(),
name: userData.name || '',
email: userData.email || '',
createdAt: new Date().toISOString()
};
return {
success: true,
data: newUser,
message: 'Usuario creado exitosamente'
};
}
// Streams
static async getStreams(userId?: string): Promise<ApiResponse<Stream[]>> {
await delay();
return {
success: true,
data: mockStreams
};
}
static async createStream(streamData: Partial<Stream>): Promise<ApiResponse<Stream>> {
await delay();
const newStream: Stream = {
...mockStreams[0],
id: Date.now().toString(),
title: streamData.title || 'Nueva Transmisión',
description: streamData.description || '',
status: 'draft',
userId: streamData.userId || '1',
platforms: streamData.platforms || [],
viewerCount: 0,
maxViewers: 0,
chatEnabled: streamData.chatEnabled ?? true,
isPrivate: streamData.isPrivate ?? false,
tags: streamData.tags || []
};
return {
success: true,
data: newStream,
message: 'Transmisión creada exitosamente'
};
}
static async goLive(streamId: string): Promise<ApiResponse<Stream>> {
await delay();
const stream = mockStreams.find(s => s.id === streamId);
if (!stream) {
return {
success: false,
error: 'Transmisión no encontrada'
};
}
const liveStream: Stream = {
...stream,
status: 'live',
startedAt: new Date().toISOString(),
viewerCount: Math.floor(Math.random() * 50) + 10
};
return {
success: true,
data: liveStream,
message: 'Transmisión iniciada'
};
}
// Plataformas
static async getPlatforms(): Promise<ApiResponse<StreamPlatform[]>> {
await delay();
return {
success: true,
data: mockPlatforms
};
}
static async connectPlatform(platformId: string): Promise<ApiResponse<StreamPlatform>> {
await delay();
const platform = mockPlatforms.find(p => p.id === platformId);
if (!platform) {
return {
success: false,
error: 'Plataforma no encontrada'
};
}
const connectedPlatform: StreamPlatform = {
...platform,
isConnected: true,
accountName: `Cuenta de ${platform.displayName}`
};
return {
success: true,
data: connectedPlatform,
message: 'Plataforma conectada exitosamente'
};
}
// Escenas del estudio
static async getScenes(): Promise<ApiResponse<StudioScene[]>> {
await delay();
return {
success: true,
data: mockScenes
};
}
static async setActiveScene(sceneId: string): Promise<ApiResponse<StudioScene>> {
await delay();
const scene = mockScenes.find(s => s.id === sceneId);
if (!scene) {
return {
success: false,
error: 'Escena no encontrada'
};
}
// Marcar todas las escenas como inactivas
mockScenes.forEach(s => s.isActive = false);
// Activar la escena seleccionada
scene.isActive = true;
return {
success: true,
data: scene,
message: 'Escena activada'
};
}
// Chat
static async getChatMessages(streamId: string): Promise<ApiResponse<ChatMessage[]>> {
await delay();
return {
success: true,
data: mockChatMessages.filter(msg => msg.streamId === streamId)
};
}
// Analytics
static async getAnalytics(streamId: string): Promise<ApiResponse<Analytics>> {
await delay();
return {
success: true,
data: mockAnalytics
};
}
}

116
src/lib/types.ts Normal file
View File

@ -0,0 +1,116 @@
// Tipos de datos para la aplicación AvanzaCast
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
role: 'admin' | 'streamer' | 'viewer';
createdAt: string;
lastLogin?: string;
isActive: boolean;
plan: 'free' | 'pro' | 'enterprise';
}
export interface Stream {
id: string;
title: string;
description?: string;
status: 'draft' | 'scheduled' | 'live' | 'ended';
userId: string;
scheduledAt?: string;
startedAt?: string;
endedAt?: string;
platforms: StreamPlatform[];
viewerCount: number;
maxViewers: number;
duration?: number; // en minutos
thumbnailUrl?: string;
recordingUrl?: string;
chatEnabled: boolean;
isPrivate: boolean;
tags: string[];
}
export interface StreamPlatform {
id: string;
name: 'youtube' | 'facebook' | 'twitch' | 'linkedin' | 'custom';
displayName: string;
isConnected: boolean;
streamKey?: string;
streamUrl?: string;
accountName?: string;
isLive: boolean;
viewerCount: number;
}
export interface ChatMessage {
id: string;
streamId: string;
username: string;
message: string;
timestamp: string;
platform: string;
isSuper?: boolean;
isModerator?: boolean;
}
export interface StudioScene {
id: string;
name: string;
type: 'camera' | 'screen' | 'overlay' | 'mixed';
isActive: boolean;
sources: SceneSource[];
layout: SceneLayout;
}
export interface SceneSource {
id: string;
type: 'camera' | 'screen' | 'image' | 'text' | 'browser';
name: string;
url?: string;
position: { x: number; y: number };
size: { width: number; height: number };
isVisible: boolean;
properties: Record<string, any>;
}
export interface SceneLayout {
width: number;
height: number;
backgroundColor: string;
}
export interface Analytics {
streamId: string;
totalViews: number;
peakViewers: number;
averageWatchTime: number;
chatMessages: number;
likes: number;
shares: number;
platformBreakdown: Record<string, number>;
timelineData: {
timestamp: string;
viewers: number;
chatRate: number;
}[];
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
// Estados de la aplicación
export interface AppState {
user: User | null;
isAuthenticated: boolean;
currentStream: Stream | null;
connectedPlatforms: StreamPlatform[];
activeScene: StudioScene | null;
isLoading: boolean;
theme: 'light' | 'dark';
}

77
tailwind.config.js Normal file
View File

@ -0,0 +1,77 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#0284c7',
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
light: '#e0f2fe',
'dark-light': 'rgba(2,132,199,.15)',
},
secondary: {
DEFAULT: '#805dca',
light: '#ebe4f7',
'dark-light': 'rgb(128 93 202 / 15%)',
},
success: {
DEFAULT: '#00ab55',
light: '#ddf5f0',
'dark-light': 'rgba(0,171,85,.15)',
},
danger: {
DEFAULT: '#e7515a',
light: '#fff5f5',
'dark-light': 'rgba(231,81,90,.15)',
},
warning: {
DEFAULT: '#e2a03f',
light: '#fff9ed',
'dark-light': 'rgba(226,160,63,.15)',
},
info: {
DEFAULT: '#2196f3',
light: '#e7f7ff',
'dark-light': 'rgba(33,150,243,.15)',
},
dark: {
DEFAULT: '#3b3f5c',
light: '#eaeaec',
'dark-light': 'rgba(59,63,92,.15)',
},
black: {
DEFAULT: '#0e1726',
light: '#e3e4eb',
'dark-light': 'rgba(14,23,38,.15)',
},
white: {
DEFAULT: '#ffffff',
light: '#e0e6ed',
dark: '#888ea8',
},
},
fontFamily: {
nunito: ['Nunito', 'sans-serif'],
},
boxShadow: {
'3xl': '0 2px 2px rgb(224 230 237 / 46%), 1px 6px 7px rgb(224 230 237 / 46%)',
},
},
},
plugins: [],
}

41
tsconfig.json Normal file
View File

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