414 lines
19 KiB
TypeScript
414 lines
19 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useAuth } from '@/context/AuthContext';
|
|
|
|
export default function Login() {
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [firstName, setFirstName] = useState('');
|
|
const [isSignup, setIsSignup] = useState(false);
|
|
const [rememberMe, setRememberMe] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
|
const [forgotEmail, setForgotEmail] = useState('');
|
|
const [forgotLoading, setForgotLoading] = useState(false);
|
|
const [forgotMessage, setForgotMessage] = useState('');
|
|
const [showVerificationBanner, setShowVerificationBanner] = useState(false);
|
|
const [verificationSent, setVerificationSent] = useState(false);
|
|
const [needsVerification, setNeedsVerification] = useState(false);
|
|
const [resendEmail, setResendEmail] = useState('');
|
|
const [resendPassword, setResendPassword] = useState('');
|
|
const [showPasswordPrompt, setShowPasswordPrompt] = useState(false);
|
|
const { login, signup, sendPasswordReset, sendVerificationEmailToUser } = useAuth();
|
|
|
|
// Persist verification state across re-renders
|
|
useEffect(() => {
|
|
const savedEmail = sessionStorage.getItem('needsVerification');
|
|
if (savedEmail) {
|
|
setNeedsVerification(true);
|
|
setResendEmail(savedEmail);
|
|
setError('Please verify your email address before logging in. Check your inbox for the verification link.');
|
|
}
|
|
}, []);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setLoading(true);
|
|
|
|
if (isSignup) {
|
|
if (!firstName.trim()) {
|
|
setError('Please enter your first name');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
if (password !== confirmPassword) {
|
|
setError('Passwords do not match');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
try {
|
|
const newUser = await signup(email, password, firstName);
|
|
// Send verification email to the newly created user
|
|
if (newUser) {
|
|
try {
|
|
await sendVerificationEmailToUser(newUser);
|
|
|
|
// Sign them out immediately - they must verify before login
|
|
const { signOut } = await import('firebase/auth');
|
|
const { auth } = await import('@/lib/firebase');
|
|
await signOut(auth);
|
|
|
|
setVerificationSent(true);
|
|
setShowVerificationBanner(true);
|
|
} catch (err) {
|
|
console.error('Error sending verification email:', err);
|
|
}
|
|
}
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
}
|
|
} else {
|
|
try {
|
|
await login(email, password);
|
|
// Store remember me timestamp if checked
|
|
if (rememberMe) {
|
|
localStorage.setItem('rememberMeTimestamp', Date.now().toString());
|
|
} else {
|
|
localStorage.removeItem('rememberMeTimestamp');
|
|
}
|
|
} catch (err: unknown) {
|
|
const errorMessage = err instanceof Error ? err.message : 'An error occurred';
|
|
const errorCode = err instanceof Error && 'code' in err ? (err as any).code : '';
|
|
|
|
console.log('Login error:', errorMessage, 'Code:', errorCode); // Debug log
|
|
|
|
setError(errorMessage);
|
|
}
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
const handleResendVerification = async () => {
|
|
if (!resendPassword) {
|
|
setShowPasswordPrompt(true);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError('');
|
|
try {
|
|
// Try to login temporarily to send verification
|
|
const { signInWithEmailAndPassword } = await import('firebase/auth');
|
|
const { auth } = await import('@/lib/firebase');
|
|
const userCred = await signInWithEmailAndPassword(auth, resendEmail, resendPassword);
|
|
await sendVerificationEmailToUser(userCred.user);
|
|
|
|
// Sign them back out
|
|
const { signOut } = await import('firebase/auth');
|
|
await signOut(auth);
|
|
|
|
setError('');
|
|
setNeedsVerification(false);
|
|
sessionStorage.removeItem('needsVerification');
|
|
setShowVerificationBanner(true);
|
|
setVerificationSent(true);
|
|
setResendPassword('');
|
|
setShowPasswordPrompt(false);
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : 'Failed to resend verification email. Check your password.');
|
|
setResendPassword('');
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
const handleForgotPassword = async () => {
|
|
if (!forgotEmail) {
|
|
setForgotMessage('Please enter your email address');
|
|
return;
|
|
}
|
|
setForgotLoading(true);
|
|
setForgotMessage('');
|
|
try {
|
|
await sendPasswordReset(forgotEmail);
|
|
setForgotMessage('Password reset email sent! Check your inbox.');
|
|
} catch (err: unknown) {
|
|
setForgotMessage(err instanceof Error ? err.message : 'Failed to send reset email');
|
|
}
|
|
setForgotLoading(false);
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-900 to-black text-white flex items-center justify-center p-4">
|
|
<div className="w-full max-w-md">
|
|
<div className="text-center mb-8">
|
|
<h1 className="text-4xl font-bold mb-2">Haver</h1>
|
|
<p className="text-gray-400">Your prayer companion</p>
|
|
</div>
|
|
<form onSubmit={handleSubmit} className="bg-gray-800 p-8 rounded-xl shadow-2xl space-y-6">
|
|
<h2 className="text-2xl font-semibold text-center">
|
|
{isSignup ? 'Create Account' : 'Welcome Back'}
|
|
</h2>
|
|
{error && (
|
|
<div className="bg-red-900 border border-red-700 text-red-200 px-4 py-3 rounded-lg">
|
|
{error}
|
|
</div>
|
|
)}
|
|
{needsVerification && (
|
|
<div className="bg-yellow-900 border-2 border-yellow-600 text-yellow-100 px-5 py-4 rounded-lg">
|
|
<div className="flex items-start gap-3">
|
|
<svg className="w-6 h-6 text-yellow-400 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
</svg>
|
|
<div className="flex-1">
|
|
<p className="font-bold text-lg mb-2">⚠️ Email Not Verified</p>
|
|
<p className="mb-2">Your account exists, but <strong>you haven't verified your email yet</strong>.</p>
|
|
<p className="text-sm text-yellow-300 mb-3">Check your inbox for <strong>{resendEmail}</strong> and click the verification link. Don't forget to check your spam folder!</p>
|
|
<div className="bg-yellow-950 border border-yellow-700 rounded p-3 mb-3 text-sm">
|
|
<p className="font-semibold mb-1">📧 Can't find the email?</p>
|
|
<p className="text-yellow-300 text-xs">Click the button below to send a new verification email.</p>
|
|
</div>
|
|
{showPasswordPrompt && (
|
|
<div className="mb-3">
|
|
<label className="block text-sm font-medium text-yellow-300 mb-2">Enter your password to resend:</label>
|
|
<input
|
|
type="password"
|
|
value={resendPassword}
|
|
onChange={(e) => setResendPassword(e.target.value)}
|
|
placeholder="Your password"
|
|
className="w-full p-2 bg-yellow-950 border border-yellow-700 rounded-lg text-white placeholder-yellow-600 focus:ring-2 focus:ring-yellow-500 focus:border-transparent"
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
handleResendVerification();
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={handleResendVerification}
|
|
disabled={loading}
|
|
className="w-full px-4 py-2 bg-yellow-600 hover:bg-yellow-700 disabled:bg-yellow-800 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors"
|
|
>
|
|
{loading ? 'Sending...' : '📨 Resend Verification Email'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{isSignup && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">First Name</label>
|
|
<input
|
|
type="text"
|
|
value={firstName}
|
|
onChange={(e) => setFirstName(e.target.value)}
|
|
className="w-full p-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
|
placeholder="Enter your first name"
|
|
required={isSignup}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">Email</label>
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
autoFocus
|
|
className="w-full p-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
|
placeholder="Enter your email"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">Password</label>
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="w-full p-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
|
placeholder="Enter your password"
|
|
required
|
|
/>
|
|
</div>
|
|
{isSignup && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">Confirm Password</label>
|
|
<input
|
|
type="password"
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
className="w-full p-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
|
placeholder="Confirm your password"
|
|
required
|
|
/>
|
|
</div>
|
|
)}
|
|
{!isSignup && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="rememberMe"
|
|
checked={rememberMe}
|
|
onChange={(e) => setRememberMe(e.target.checked)}
|
|
className="w-4 h-4 rounded border-gray-600 bg-gray-700 text-blue-600 focus:ring-2 focus:ring-blue-500 cursor-pointer"
|
|
/>
|
|
<label htmlFor="rememberMe" className="ml-2 text-sm text-gray-300 cursor-pointer">
|
|
Remember me for 15 days
|
|
</label>
|
|
</div>
|
|
<div className="text-right">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowForgotPassword(true);
|
|
setForgotEmail(email);
|
|
setForgotMessage('');
|
|
}}
|
|
className="text-sm text-blue-400 hover:text-blue-300 transition-colors"
|
|
>
|
|
Forgot password?
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full p-3 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed rounded-lg font-medium transition-colors flex items-center justify-center"
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" 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>
|
|
Processing...
|
|
</>
|
|
) : (
|
|
isSignup ? 'Create Account' : 'Sign In'
|
|
)}
|
|
</button>
|
|
{showVerificationBanner && (
|
|
<div className="bg-blue-900 border-2 border-blue-600 text-blue-100 px-5 py-4 rounded-lg">
|
|
<div className="flex items-start gap-3 mb-3">
|
|
<svg className="w-6 h-6 text-blue-400 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
|
</svg>
|
|
<div className="flex-1">
|
|
<p className="font-bold text-lg mb-2">⚠️ Email Verification Required</p>
|
|
<p className="mb-2">Your account has been created, but <strong>you must verify your email before you can sign in</strong>.</p>
|
|
<p className="text-sm text-blue-300 mb-3">A verification email has been sent to <strong>{email || resendEmail}</strong>. Please check your inbox (and spam folder) and click the verification link.</p>
|
|
<div className="bg-blue-950 border border-blue-700 rounded p-3 mb-3 text-sm">
|
|
<p className="font-semibold mb-1">📧 Didn't receive the email?</p>
|
|
<p className="text-blue-300 text-xs">It may take a few minutes to arrive. Check your spam/junk folder.</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={async () => {
|
|
setLoading(true);
|
|
try {
|
|
const { signInWithEmailAndPassword, signOut } = await import('firebase/auth');
|
|
const { auth } = await import('@/lib/firebase');
|
|
const userCred = await signInWithEmailAndPassword(auth, email, password);
|
|
await sendVerificationEmailToUser(userCred.user);
|
|
await signOut(auth);
|
|
setError('');
|
|
alert('✓ Verification email resent! Check your inbox.');
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : 'Failed to resend email');
|
|
}
|
|
setLoading(false);
|
|
}}
|
|
disabled={loading}
|
|
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors"
|
|
>
|
|
{loading ? 'Resending...' : '📨 Resend Verification Email'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="text-center">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setIsSignup(!isSignup);
|
|
setError('');
|
|
setConfirmPassword('');
|
|
setFirstName('');
|
|
}}
|
|
className="text-blue-400 hover:text-blue-300 transition-colors"
|
|
>
|
|
{isSignup ? 'Already have an account? Sign In' : 'Need an account? Sign Up'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{/* Forgot Password Modal */}
|
|
{showForgotPassword && (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
<div className="bg-gray-800 rounded-xl p-6 max-w-sm w-full">
|
|
<h2 className="text-xl font-bold text-white mb-2">Reset Password</h2>
|
|
<p className="text-gray-400 mb-4">Enter your email and we'll send you a link to reset your password.</p>
|
|
|
|
{forgotMessage && (
|
|
<div className={`px-4 py-2 rounded-lg mb-4 text-sm ${
|
|
forgotMessage.includes('sent')
|
|
? 'bg-green-900 border border-green-700 text-green-200'
|
|
: 'bg-red-900 border border-red-700 text-red-200'
|
|
}`}>
|
|
{forgotMessage}
|
|
</div>
|
|
)}
|
|
|
|
<input
|
|
type="email"
|
|
value={forgotEmail}
|
|
onChange={(e) => setForgotEmail(e.target.value)}
|
|
placeholder="Enter your email"
|
|
className="w-full p-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors mb-4"
|
|
/>
|
|
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={() => {
|
|
setShowForgotPassword(false);
|
|
setForgotEmail('');
|
|
setForgotMessage('');
|
|
}}
|
|
className="flex-1 px-4 py-2 rounded-lg bg-gray-700 text-white font-medium hover:bg-gray-600 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleForgotPassword}
|
|
disabled={forgotLoading || !forgotEmail}
|
|
className="flex-1 px-4 py-2 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed transition-colors flex items-center justify-center"
|
|
>
|
|
{forgotLoading ? (
|
|
<>
|
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" 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>
|
|
Sending...
|
|
</>
|
|
) : (
|
|
'Send Reset Link'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |