Files
CityCards-Website/src/components/LoginModal.tsx
2026-03-24 12:44:28 +05:30

288 lines
11 KiB
TypeScript

import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { X } from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
interface LoginModalProps {
isOpen: boolean;
onClose: () => void;
// onLoginSuccess: (userData: { email: string; name: string }) => void;
}
export function LoginModal({ isOpen, onClose, }: LoginModalProps) {
const [step, setStep] = useState<'email' | 'otp'>('email');
const [email, setEmail] = useState('');
const [otp, setOtp] = useState(['', '', '', '', '', '']);
const [countdown, setCountdown] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [helperText, setHelperText] = useState('');
const { login } = useAuth(); // from AuthContext
// Reset modal state when closed
useEffect(() => {
if (!isOpen) {
setStep('email');
setEmail('');
setOtp(['', '', '', '', '', '']);
setCountdown(0);
setHelperText('');
}
}, [isOpen]);
// Countdown timer for OTP resend
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
return () => clearTimeout(timer);
}
}, [countdown]);
const handleSendOTP = async () => {
if (!email || !email.includes('@')) {
setHelperText('Please enter a valid email address');
return;
}
setIsLoading(true);
setHelperText('');
// Simulate API call
setTimeout(() => {
setStep('otp');
setCountdown(120); // 2 minutes countdown
setIsLoading(false);
setHelperText('OTP sent successfully');
}, 1500);
};
const handleOTPChange = (index: number, value: string) => {
if (value.length > 1) return; // Only allow single digit
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
// Auto-focus next input
if (value && index < 5) {
const nextInput = document.querySelector(`input[data-otp-index="${index + 1}"]`) as HTMLInputElement;
nextInput?.focus();
}
};
const handleOTPKeyDown = (index: number, e: React.KeyboardEvent) => {
if (e.key === 'Backspace' && !otp[index] && index > 0) {
const prevInput = document.querySelector(`input[data-otp-index="${index - 1}"]`) as HTMLInputElement;
prevInput?.focus();
}
};
const handleVerifyLogin = async () => {
const otpString = otp.join('');
if (otpString.length !== 6) {
setHelperText('Please enter complete OTP');
return;
}
setIsLoading(true);
setHelperText('');
// Simulate API call
setTimeout(() => {
// Generate name from email for demo
const emailParts = email.split('@')[0];
const name = emailParts.charAt(0).toUpperCase() + emailParts.slice(1);
login({ email, name })
setIsLoading(false);
// navigate("/melbourne")
onClose();
}, 1500);
};
const formatCountdown = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
onClick={onClose}
/>
{/* Modal */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="fixed inset-0 flex items-center justify-center z-50 p-4"
>
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md mx-auto overflow-hidden">
{/* Header */}
<div className="relative px-8 pt-8 pb-4">
<button
onClick={onClose}
className="absolute top-6 right-6 w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
>
<X className="w-4 h-4 text-gray-600" />
</button>
<h2 className="font-merchant text-2xl font-semibold text-gray-900 mb-2">
Login
</h2>
<p className="font-poppins text-sm text-gray-600">
Enter your email Id and verify with OTP sent on it.
</p>
</div>
{/* Content */}
<div className="px-8 pb-8">
{step === 'email' ? (
<div className="space-y-6">
{/* Email Input */}
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Email
</Label>
<Input
type="email"
placeholder="Name@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
onKeyDown={(e) => e.key === 'Enter' && handleSendOTP()}
/>
{helperText && (
<p className={`font-poppins text-xs ${helperText.includes('success') ? 'text-green-600' : 'text-red-500'}`}>
{helperText}
</p>
)}
</div>
{/* Send OTP Button */}
<Button
onClick={handleSendOTP}
disabled={isLoading}
className="w-full h-12 bg-gray-800 hover:bg-gray-900 cursor-pointer text-white font-poppins font-semibold rounded-xl transition-colors"
>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
Sending OTP...
</div>
) : (
<>
Send OTP
<svg className="w-4 h-4 ml-2" 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>
) : (
<div className="space-y-6">
{/* Email Display */}
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Email
</Label>
<div className="h-12 bg-gray-50 rounded-xl flex items-center px-4">
<span className="font-poppins text-base text-gray-600">{email}</span>
</div>
{helperText && (
<p className={`font-poppins text-xs ${helperText.includes('success') ? 'text-green-600' : 'text-red-500'}`}>
{helperText}
</p>
)}
</div>
{/* OTP Input */}
<div className="space-y-3">
<Label className="font-poppins text-sm font-medium text-gray-700">
Enter OTP
</Label>
<div className="flex gap-3 justify-between">
{otp.map((digit, index) => (
<input
key={index}
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={1}
value={digit}
onChange={(e) => handleOTPChange(index, e.target.value.replace(/\D/g, ''))}
onKeyDown={(e) => handleOTPKeyDown(index, e)}
data-otp-index={index}
className="w-12 h-12 text-center font-poppins font-semibold text-lg bg-gray-300 border-0 rounded-xl focus:bg-white focus:ring-2 focus:ring-primary focus:outline-none transition-colors"
/>
))}
</div>
{/* Countdown */}
{countdown > 0 && (
<p className="font-poppins text-xs text-gray-500 text-center">
{formatCountdown(countdown)}
</p>
)}
</div>
{/* Verify Button */}
<Button
onClick={handleVerifyLogin}
disabled={isLoading || otp.join('').length !== 6}
className="w-full h-12 bg-gray-800 hover:bg-gray-900 cursor-pointer text-white font-poppins font-semibold rounded-xl transition-colors disabled:opacity-50"
>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
Verifying...
</div>
) : (
<>
Verify and Login
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</>
)}
</Button>
{/* Resend OTP */}
{countdown === 0 && (
<button
onClick={() => {
setStep('email');
setOtp(['', '', '', '', '', '']);
}}
className="w-full font-poppins text-sm text-gray-600 hover:text-gray-800 transition-colors"
>
Didn't receive OTP? Send again
</button>
)}
</div>
)}
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}