first commit

This commit is contained in:
Michaël
2026-01-29 14:13:11 -03:00
commit 2302748c87
105 changed files with 93301 additions and 0 deletions
@@ -0,0 +1,172 @@
'use client';
import { useState, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext';
import { useAuth } from '@/context/AuthContext';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import { authApi } from '@/lib/api';
import toast from 'react-hot-toast';
function ClaimAccountContent() {
const router = useRouter();
const searchParams = useSearchParams();
const { locale: language } = useLanguage();
const { setAuthData } = useAuth();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
password: '',
confirmPassword: '',
});
const token = searchParams.get('token');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!token) {
toast.error(language === 'es' ? 'Token no válido' : 'Invalid token');
return;
}
if (formData.password !== formData.confirmPassword) {
toast.error(language === 'es' ? 'Las contraseñas no coinciden' : 'Passwords do not match');
return;
}
if (formData.password.length < 10) {
toast.error(
language === 'es'
? 'La contraseña debe tener al menos 10 caracteres'
: 'Password must be at least 10 characters'
);
return;
}
setLoading(true);
try {
const result = await authApi.confirmClaimAccount(token, { password: formData.password });
setAuthData({ user: result.user, token: result.token });
toast.success(language === 'es' ? '¡Cuenta activada!' : 'Account activated!');
router.push('/dashboard');
} catch (error: any) {
toast.error(error.message || (language === 'es' ? 'Error' : 'Failed'));
} finally {
setLoading(false);
}
};
if (!token) {
return (
<div className="section-padding min-h-[70vh] flex items-center">
<div className="container-page">
<div className="max-w-md mx-auto">
<Card className="p-8 text-center">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 className="text-xl font-semibold mb-2">
{language === 'es' ? 'Enlace Inválido' : 'Invalid Link'}
</h2>
<p className="text-gray-600 mb-6">
{language === 'es'
? 'Este enlace de activación no es válido o ha expirado.'
: 'This activation link is invalid or has expired.'}
</p>
<Link href="/login">
<Button>
{language === 'es' ? 'Ir a Iniciar Sesión' : 'Go to Login'}
</Button>
</Link>
</Card>
</div>
</div>
</div>
);
}
return (
<div className="section-padding min-h-[70vh] flex items-center">
<div className="container-page">
<div className="max-w-md mx-auto">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold">
{language === 'es' ? 'Activar tu Cuenta' : 'Activate Your Account'}
</h1>
<p className="mt-2 text-gray-600">
{language === 'es'
? 'Configura una contraseña para acceder a tu cuenta'
: 'Set a password to access your account'}
</p>
</div>
<Card className="p-8">
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-800">
{language === 'es'
? 'Tu cuenta fue creada durante una reservación. Establece una contraseña para acceder a tu historial de entradas y más.'
: 'Your account was created during a booking. Set a password to access your ticket history and more.'}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<Input
id="password"
label={language === 'es' ? 'Contraseña' : 'Password'}
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required
/>
<p className="text-xs text-gray-500 -mt-4">
{language === 'es' ? 'Mínimo 10 caracteres' : 'Minimum 10 characters'}
</p>
<Input
id="confirmPassword"
label={language === 'es' ? 'Confirmar Contraseña' : 'Confirm Password'}
type="password"
value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
required
/>
<Button type="submit" className="w-full" size="lg" isLoading={loading}>
{language === 'es' ? 'Activar Cuenta' : 'Activate Account'}
</Button>
</form>
<p className="mt-6 text-center text-sm text-gray-600">
{language === 'es' ? '¿Ya tienes acceso?' : 'Already have access?'}{' '}
<Link href="/login" className="text-secondary-blue hover:underline font-medium">
{language === 'es' ? 'Iniciar Sesión' : 'Log In'}
</Link>
</p>
</Card>
</div>
</div>
</div>
);
}
function LoadingFallback() {
return (
<div className="section-padding min-h-[70vh] flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-secondary-blue"></div>
</div>
);
}
export default function ClaimAccountPage() {
return (
<Suspense fallback={<LoadingFallback />}>
<ClaimAccountContent />
</Suspense>
);
}
@@ -0,0 +1,103 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import { authApi } from '@/lib/api';
import toast from 'react-hot-toast';
export default function ForgotPasswordPage() {
const { locale: language } = useLanguage();
const [loading, setLoading] = useState(false);
const [sent, setSent] = useState(false);
const [email, setEmail] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
await authApi.requestPasswordReset(email);
setSent(true);
toast.success(
language === 'es'
? 'Revisa tu correo para el enlace de restablecimiento'
: 'Check your email for the reset link'
);
} catch (error: any) {
toast.error(error.message || (language === 'es' ? 'Error' : 'Failed'));
} finally {
setLoading(false);
}
};
return (
<div className="section-padding min-h-[70vh] flex items-center">
<div className="container-page">
<div className="max-w-md mx-auto">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold">
{language === 'es' ? 'Restablecer Contraseña' : 'Reset Password'}
</h1>
<p className="mt-2 text-gray-600">
{language === 'es'
? 'Ingresa tu email para recibir un enlace de restablecimiento'
: 'Enter your email to receive a reset link'}
</p>
</div>
<Card className="p-8">
{sent ? (
<div className="text-center py-4">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-lg font-semibold mb-2">
{language === 'es' ? 'Revisa tu Email' : 'Check Your Email'}
</h3>
<p className="text-gray-600 text-sm mb-4">
{language === 'es'
? `Si existe una cuenta con ${email}, recibirás un enlace para restablecer tu contraseña.`
: `If an account exists with ${email}, you'll receive a password reset link.`}
</p>
<button
onClick={() => setSent(false)}
className="text-secondary-blue hover:underline text-sm"
>
{language === 'es' ? 'Intentar con otro email' : 'Try a different email'}
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<Input
id="email"
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<Button type="submit" className="w-full" size="lg" isLoading={loading}>
{language === 'es' ? 'Enviar Enlace' : 'Send Reset Link'}
</Button>
</form>
)}
<p className="mt-6 text-center text-sm text-gray-600">
{language === 'es' ? '¿Recordaste tu contraseña?' : 'Remember your password?'}{' '}
<Link href="/login" className="text-secondary-blue hover:underline font-medium">
{language === 'es' ? 'Iniciar Sesión' : 'Log In'}
</Link>
</p>
</Card>
</div>
</div>
</div>
);
}
@@ -0,0 +1,119 @@
'use client';
import { useEffect, useState, Suspense, useRef } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useLanguage } from '@/context/LanguageContext';
import { useAuth } from '@/context/AuthContext';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import toast from 'react-hot-toast';
function MagicLinkContent() {
const router = useRouter();
const searchParams = useSearchParams();
const { locale: language } = useLanguage();
const { loginWithMagicLink } = useAuth();
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [error, setError] = useState('');
const verificationAttempted = useRef(false);
const token = searchParams.get('token');
useEffect(() => {
// Prevent duplicate verification attempts (React StrictMode double-invokes effects)
if (verificationAttempted.current) return;
if (token) {
verificationAttempted.current = true;
verifyToken();
} else {
setStatus('error');
setError(language === 'es' ? 'Token no encontrado' : 'Token not found');
}
}, [token]);
const verifyToken = async () => {
try {
await loginWithMagicLink(token!);
setStatus('success');
toast.success(language === 'es' ? '¡Bienvenido!' : 'Welcome!');
setTimeout(() => {
router.push('/dashboard');
}, 1500);
} catch (err: any) {
setStatus('error');
setError(err.message || (language === 'es' ? 'Enlace inválido o expirado' : 'Invalid or expired link'));
}
};
return (
<div className="section-padding min-h-[70vh] flex items-center">
<div className="container-page">
<div className="max-w-md mx-auto">
<Card className="p-8 text-center">
{status === 'loading' && (
<>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-secondary-blue mx-auto mb-4"></div>
<h2 className="text-xl font-semibold mb-2">
{language === 'es' ? 'Verificando...' : 'Verifying...'}
</h2>
<p className="text-gray-600">
{language === 'es' ? 'Por favor espera' : 'Please wait'}
</p>
</>
)}
{status === 'success' && (
<>
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-xl font-semibold mb-2">
{language === 'es' ? '¡Inicio de sesión exitoso!' : 'Login successful!'}
</h2>
<p className="text-gray-600">
{language === 'es' ? 'Redirigiendo...' : 'Redirecting...'}
</p>
</>
)}
{status === 'error' && (
<>
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 className="text-xl font-semibold mb-2">
{language === 'es' ? 'Error de Verificación' : 'Verification Error'}
</h2>
<p className="text-gray-600 mb-6">{error}</p>
<Button onClick={() => router.push('/login')}>
{language === 'es' ? 'Ir a Iniciar Sesión' : 'Go to Login'}
</Button>
</>
)}
</Card>
</div>
</div>
</div>
);
}
function LoadingFallback() {
return (
<div className="section-padding min-h-[70vh] flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-secondary-blue"></div>
</div>
);
}
export default function MagicLinkPage() {
return (
<Suspense fallback={<LoadingFallback />}>
<MagicLinkContent />
</Suspense>
);
}
@@ -0,0 +1,178 @@
'use client';
import { useState, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import { authApi } from '@/lib/api';
import toast from 'react-hot-toast';
function ResetPasswordContent() {
const router = useRouter();
const searchParams = useSearchParams();
const { locale: language } = useLanguage();
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [formData, setFormData] = useState({
password: '',
confirmPassword: '',
});
const token = searchParams.get('token');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!token) {
toast.error(language === 'es' ? 'Token no válido' : 'Invalid token');
return;
}
if (formData.password !== formData.confirmPassword) {
toast.error(language === 'es' ? 'Las contraseñas no coinciden' : 'Passwords do not match');
return;
}
if (formData.password.length < 10) {
toast.error(
language === 'es'
? 'La contraseña debe tener al menos 10 caracteres'
: 'Password must be at least 10 characters'
);
return;
}
setLoading(true);
try {
await authApi.confirmPasswordReset(token, formData.password);
setSuccess(true);
toast.success(language === 'es' ? 'Contraseña actualizada' : 'Password updated');
} catch (error: any) {
toast.error(error.message || (language === 'es' ? 'Error' : 'Failed'));
} finally {
setLoading(false);
}
};
if (!token) {
return (
<div className="section-padding min-h-[70vh] flex items-center">
<div className="container-page">
<div className="max-w-md mx-auto">
<Card className="p-8 text-center">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 className="text-xl font-semibold mb-2">
{language === 'es' ? 'Enlace Inválido' : 'Invalid Link'}
</h2>
<p className="text-gray-600 mb-6">
{language === 'es'
? 'Este enlace de restablecimiento no es válido o ha expirado.'
: 'This reset link is invalid or has expired.'}
</p>
<Link href="/auth/forgot-password">
<Button>
{language === 'es' ? 'Solicitar Nuevo Enlace' : 'Request New Link'}
</Button>
</Link>
</Card>
</div>
</div>
</div>
);
}
return (
<div className="section-padding min-h-[70vh] flex items-center">
<div className="container-page">
<div className="max-w-md mx-auto">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold">
{language === 'es' ? 'Nueva Contraseña' : 'New Password'}
</h1>
<p className="mt-2 text-gray-600">
{language === 'es'
? 'Ingresa tu nueva contraseña'
: 'Enter your new password'}
</p>
</div>
<Card className="p-8">
{success ? (
<div className="text-center py-4">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h3 className="text-lg font-semibold mb-2">
{language === 'es' ? '¡Contraseña Actualizada!' : 'Password Updated!'}
</h3>
<p className="text-gray-600 text-sm mb-6">
{language === 'es'
? 'Tu contraseña ha sido cambiada exitosamente.'
: 'Your password has been successfully changed.'}
</p>
<Link href="/login">
<Button>
{language === 'es' ? 'Iniciar Sesión' : 'Log In'}
</Button>
</Link>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<Input
id="password"
label={language === 'es' ? 'Nueva Contraseña' : 'New Password'}
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required
/>
<p className="text-xs text-gray-500 -mt-4">
{language === 'es' ? 'Mínimo 10 caracteres' : 'Minimum 10 characters'}
</p>
<Input
id="confirmPassword"
label={language === 'es' ? 'Confirmar Contraseña' : 'Confirm Password'}
type="password"
value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
required
/>
<Button type="submit" className="w-full" size="lg" isLoading={loading}>
{language === 'es' ? 'Actualizar Contraseña' : 'Update Password'}
</Button>
</form>
)}
</Card>
</div>
</div>
</div>
);
}
function LoadingFallback() {
return (
<div className="section-padding min-h-[70vh] flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-secondary-blue"></div>
</div>
);
}
export default function ResetPasswordPage() {
return (
<Suspense fallback={<LoadingFallback />}>
<ResetPasswordContent />
</Suspense>
);
}