first commit
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user