Spaces:
Running
Running
| import React, { useState, useEffect } from 'react'; | |
| import { motion } from 'framer-motion'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Input } from '@/components/ui/input'; | |
| import { Label } from '@/components/ui/label'; | |
| import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Link, useNavigate } from 'react-router-dom'; | |
| import { KeyRound, Mail, LogIn, Phone, Shield, ArrowRight, AlertCircle, CheckCircle2 } from 'lucide-react'; | |
| import { useToast } from '@/components/ui/use-toast'; | |
| import { supabase } from '@/lib/supabaseClient'; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; | |
| // Alert and Progress components are not available in the UI components directory | |
| // We'll implement alternative solutions | |
| const LoginPage = ({ setSession }) => { | |
| const navigate = useNavigate(); | |
| const { toast } = useToast(); | |
| // Basic auth states | |
| const [email, setEmail] = useState(''); | |
| const [password, setPassword] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| // Login flow states | |
| const [loginStep, setLoginStep] = useState('credentials'); // credentials, otp, success | |
| const [loginMethod, setLoginMethod] = useState('email'); // email or phone | |
| // OTP verification states | |
| const [phoneNumber, setPhoneNumber] = useState(''); | |
| const [otpCode, setOtpCode] = useState(''); | |
| const [otpSent, setOtpSent] = useState(false); | |
| const [otpExpiry, setOtpExpiry] = useState(null); | |
| const [timeRemaining, setTimeRemaining] = useState(0); | |
| const [otpAttempts, setOtpAttempts] = useState(0); | |
| const [userId, setUserId] = useState(null); | |
| const [sessionData, setSessionData] = useState(null); | |
| const [otpError, setOtpError] = useState('') | |
| // Demo credentials for testing | |
| const demoCredentials = { | |
| email: 'demo@example.com', | |
| password: 'demo123' | |
| }; | |
| // Timer for OTP expiration | |
| useEffect(() => { | |
| let timer; | |
| if (otpExpiry && timeRemaining > 0) { | |
| timer = setInterval(() => { | |
| const remaining = Math.max(0, Math.floor((otpExpiry - Date.now()) / 1000)); | |
| setTimeRemaining(remaining); | |
| if (remaining === 0) { | |
| setOtpSent(false); | |
| setOtpError('OTP has expired. Please request a new one.'); | |
| } | |
| }, 1000); | |
| } | |
| return () => clearInterval(timer); | |
| }, [otpExpiry, timeRemaining]); | |
| const handleSignIn = async (e) => { | |
| e.preventDefault(); | |
| setIsLoading(true); | |
| setOtpError(''); | |
| try { | |
| const { data, error } = await supabase.auth.signInWithPassword({ | |
| email, | |
| password, | |
| }); | |
| if (error) throw error; | |
| if (data.session) { | |
| // Store session data for after OTP verification | |
| setSessionData(data.session); | |
| setUserId(data.user.id); | |
| // In a real app, you would fetch the user's phone number from the database | |
| // For demo purposes, we'll use a placeholder or let them enter it | |
| const { data: profileData } = await supabase | |
| .from('profiles') | |
| .select('phone') | |
| .eq('id', data.user.id) | |
| .single(); | |
| if (profileData && profileData.phone) { | |
| setPhoneNumber(profileData.phone); | |
| } | |
| // Move to OTP verification step | |
| setLoginStep('otp'); | |
| toast({ | |
| title: "Verification Required", | |
| description: "Please verify your identity with a one-time code.", | |
| duration: 5000, | |
| }); | |
| } | |
| } catch (error) { | |
| toast({ | |
| title: "Error", | |
| description: error.message || "Invalid credentials. Please try again.", | |
| variant: "destructive", | |
| duration: 5000, | |
| }); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const sendOTP = async () => { | |
| if (!phoneNumber || phoneNumber.length < 10) { | |
| setOtpError('Please enter a valid phone number'); | |
| return; | |
| } | |
| setIsLoading(true); | |
| setOtpError(''); | |
| try { | |
| // In a real app, this would call an API to send an SMS via Twilio/AWS SNS | |
| // For demo purposes, we'll simulate sending an OTP | |
| // Generate a random 6-digit code | |
| const generatedOTP = Math.floor(100000 + Math.random() * 900000).toString(); | |
| console.log('Generated OTP (for demo):', generatedOTP); // In production, remove this log | |
| // Set expiry time (2 minutes from now) | |
| const expiryTime = Date.now() + 2 * 60 * 1000; | |
| setOtpExpiry(expiryTime); | |
| setTimeRemaining(Math.floor((expiryTime - Date.now()) / 1000)); | |
| // In a real app, you would store the OTP hash in the database with the user ID and expiry | |
| // For demo, we'll just set it in state (not secure for production) | |
| setOtpSent(true); | |
| toast({ | |
| title: "Success", | |
| description: `A verification code has been sent to ${phoneNumber}. It will expire in 2 minutes.`, | |
| variant: "default", | |
| duration: 5000, | |
| }); | |
| // In a real app, the actual OTP would be sent via SMS and not exposed in the code | |
| // For demo purposes only: | |
| setOtpCode(generatedOTP); | |
| } catch (error) { | |
| setOtpError('Failed to send verification code. Please try again.'); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const verifyOTP = async (e) => { | |
| e.preventDefault(); | |
| setIsLoading(true); | |
| setOtpError(''); | |
| try { | |
| // In a real app, this would verify the OTP against a stored hash | |
| // For demo purposes, we'll do a direct comparison (not secure for production) | |
| // Increment attempt counter | |
| const newAttemptCount = otpAttempts + 1; | |
| setOtpAttempts(newAttemptCount); | |
| // Check if max attempts reached | |
| if (newAttemptCount >= 3 && otpCode !== e.target.otp.value) { | |
| setOtpError('Maximum verification attempts reached. Please request a new code.'); | |
| setOtpSent(false); | |
| return; | |
| } | |
| // Check if OTP matches | |
| if (otpCode === e.target.otp.value) { | |
| // OTP verified successfully | |
| setLoginStep('success'); | |
| // Complete the login process with the stored session | |
| setSession(sessionData); | |
| setTimeout(() => { | |
| toast({ | |
| title: "Welcome Back!", | |
| description: "👋 You have successfully signed in.", | |
| variant: "default", | |
| duration: 5000, | |
| }); | |
| navigate('/'); | |
| }, 1500); | |
| } else { | |
| setOtpError(`Invalid verification code. ${3 - newAttemptCount} attempts remaining.`); | |
| } | |
| } catch (error) { | |
| setOtpError('Verification failed. Please try again.'); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleUseDemo = () => { | |
| setEmail(demoCredentials.email); | |
| setPassword(demoCredentials.password); | |
| }; | |
| // Helper function to format remaining time | |
| const formatTime = (seconds) => { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = seconds % 60; | |
| return `${mins}:${secs < 10 ? '0' : ''}${secs}`; | |
| }; | |
| return ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.5 }} | |
| className="min-h-[calc(100vh-280px)] flex items-center justify-center py-12 px-4" | |
| > | |
| <Card className="w-full max-w-md bg-amber-50/70 backdrop-blur-md shadow-2xl border-amber-600/30"> | |
| {loginStep === 'credentials' && ( | |
| <> | |
| <CardHeader className="text-center"> | |
| <CardTitle className="text-3xl font-bold text-amber-800">Welcome Back!</CardTitle> | |
| <CardDescription className="text-stone-600" style={{ fontFamily: "'Lato', sans-serif"}}> | |
| Sign in to continue your spice journey. | |
| </CardDescription> | |
| </CardHeader> | |
| <form onSubmit={handleSignIn}> | |
| <CardContent className="space-y-6"> | |
| <Tabs defaultValue="email" className="w-full" onValueChange={setLoginMethod}> | |
| <TabsList className="grid w-full grid-cols-2 mb-4"> | |
| <TabsTrigger value="email" className="text-sm"> | |
| <Mail className="mr-2 h-4 w-4" /> Email Login | |
| </TabsTrigger> | |
| <TabsTrigger value="phone" className="text-sm" disabled> | |
| <Phone className="mr-2 h-4 w-4" /> Phone Login | |
| <span className="ml-1 text-xs bg-amber-200 text-amber-800 px-1 rounded">Soon</span> | |
| </TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="email" className="space-y-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="email-login" className="text-stone-700">Email Address</Label> | |
| <div className="relative"> | |
| <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-stone-500" /> | |
| <Input | |
| id="email-login" | |
| type="email" | |
| placeholder="you@example.com" | |
| value={email} | |
| onChange={(e) => setEmail(e.target.value)} | |
| required | |
| className="pl-10 bg-white/50 border-amber-600/40 focus:border-amber-600 text-stone-800 placeholder:text-stone-500" | |
| disabled={isLoading} | |
| /> | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="password-login" className="text-stone-700">Password</Label> | |
| <div className="relative"> | |
| <KeyRound className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-stone-500" /> | |
| <Input | |
| id="password-login" | |
| type="password" | |
| placeholder="••••••••" | |
| value={password} | |
| onChange={(e) => setPassword(e.target.value)} | |
| required | |
| className="pl-10 bg-white/50 border-amber-600/40 focus:border-amber-600 text-stone-800 placeholder:text-stone-500" | |
| disabled={isLoading} | |
| /> | |
| </div> | |
| </div> | |
| </TabsContent> | |
| <TabsContent value="phone"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="phone-login" className="text-stone-700">Phone Number</Label> | |
| <div className="relative"> | |
| <Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-stone-500" /> | |
| <Input | |
| id="phone-login" | |
| type="tel" | |
| placeholder="+91 9999999999" | |
| disabled={true} | |
| className="pl-10 bg-white/50 border-amber-600/40 focus:border-amber-600 text-stone-800 placeholder:text-stone-500" | |
| /> | |
| </div> | |
| <p className="text-xs text-amber-700">Phone login coming soon!</p> | |
| </div> | |
| </TabsContent> | |
| </Tabs> | |
| <div className="flex items-center space-x-2"> | |
| <div className="flex-grow h-px bg-amber-200"></div> | |
| <div className="text-xs text-stone-500">Quick Access</div> | |
| <div className="flex-grow h-px bg-amber-200"></div> | |
| </div> | |
| <Button | |
| type="button" | |
| variant="outline" | |
| className="w-full border-amber-300 hover:bg-amber-100 text-amber-800" | |
| onClick={handleUseDemo} | |
| > | |
| Use Demo Credentials | |
| </Button> | |
| </CardContent> | |
| <CardFooter className="flex flex-col space-y-4"> | |
| <Button | |
| type="submit" | |
| className="w-full bg-amber-600 hover:bg-amber-700 text-white font-semibold flex items-center justify-center" | |
| style={{ fontFamily: "'Lato', sans-serif", letterSpacing: '0.05em' }} | |
| disabled={isLoading} | |
| > | |
| {isLoading ? ( | |
| <> | |
| <motion.div | |
| animate={{ rotate: 360 }} | |
| transition={{ duration: 1, repeat: Infinity, ease: "linear" }} | |
| className="w-5 h-5 border-2 border-white border-t-transparent rounded-full mr-2" | |
| ></motion.div> | |
| Signing In... | |
| </> | |
| ) : ( | |
| <> | |
| <LogIn className="mr-2 h-5 w-5" /> Sign In | |
| </> | |
| )} | |
| </Button> | |
| <p className="text-sm text-stone-600 text-center" style={{ fontFamily: "'Lato', sans-serif"}}> | |
| Don't have an account?{' '} | |
| <Link to="/signup" className="font-semibold text-amber-700 hover:text-amber-800 underline"> | |
| Sign Up | |
| </Link> | |
| </p> | |
| </CardFooter> | |
| </form> | |
| </> | |
| )} | |
| {loginStep === 'otp' && ( | |
| <> | |
| <CardHeader className="text-center"> | |
| <CardTitle className="text-2xl font-bold text-amber-800">Verify Your Identity</CardTitle> | |
| <CardDescription className="text-stone-600" style={{ fontFamily: "'Lato', sans-serif"}}> | |
| Enter the verification code sent to your phone | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-6"> | |
| {otpError && ( | |
| <div className="bg-red-50 border border-red-200 text-red-800 p-4 rounded-md flex items-start"> | |
| <AlertCircle className="h-4 w-4 mt-0.5 mr-2 flex-shrink-0" /> | |
| <div> | |
| <div className="font-medium">Error</div> | |
| <div className="text-sm">{otpError}</div> | |
| </div> | |
| </div> | |
| )} | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center"> | |
| <Phone className="h-5 w-5 text-amber-600 mr-2" /> | |
| <span className="text-sm font-medium">{phoneNumber || 'Enter your phone number'}</span> | |
| </div> | |
| {!otpSent && ( | |
| <Button | |
| type="button" | |
| variant="outline" | |
| size="sm" | |
| className="text-xs" | |
| onClick={() => setLoginStep('credentials')} | |
| > | |
| Change | |
| </Button> | |
| )} | |
| </div> | |
| {!otpSent ? ( | |
| <div className="space-y-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="phone-number" className="text-stone-700">Phone Number</Label> | |
| <div className="relative"> | |
| <Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-stone-500" /> | |
| <Input | |
| id="phone-number" | |
| type="tel" | |
| placeholder="Enter your phone number" | |
| value={phoneNumber} | |
| onChange={(e) => setPhoneNumber(e.target.value)} | |
| className="pl-10 bg-white/50 border-amber-600/40 focus:border-amber-600 text-stone-800 placeholder:text-stone-500" | |
| disabled={isLoading} | |
| /> | |
| </div> | |
| </div> | |
| <Button | |
| type="button" | |
| className="w-full bg-amber-600 hover:bg-amber-700 text-white" | |
| onClick={sendOTP} | |
| disabled={isLoading} | |
| > | |
| {isLoading ? ( | |
| <> | |
| <motion.div | |
| animate={{ rotate: 360 }} | |
| transition={{ duration: 1, repeat: Infinity, ease: "linear" }} | |
| className="w-5 h-5 border-2 border-white border-t-transparent rounded-full mr-2" | |
| ></motion.div> | |
| Sending Code... | |
| </> | |
| ) : ( | |
| <>Send Verification Code</> | |
| )} | |
| </Button> | |
| </div> | |
| ) : ( | |
| <form onSubmit={verifyOTP} className="space-y-4"> | |
| <div className="space-y-2"> | |
| <div className="flex justify-between items-center"> | |
| <Label htmlFor="otp" className="text-stone-700">Verification Code</Label> | |
| <span className="text-xs text-amber-700"> | |
| Expires in {formatTime(timeRemaining)} | |
| </span> | |
| </div> | |
| <div className="relative"> | |
| <Shield className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-stone-500" /> | |
| <Input | |
| id="otp" | |
| name="otp" | |
| type="text" | |
| placeholder="Enter 6-digit code" | |
| maxLength={6} | |
| className="pl-10 bg-white/50 border-amber-600/40 focus:border-amber-600 text-stone-800 placeholder:text-stone-500 text-center tracking-widest font-mono text-lg" | |
| disabled={isLoading} | |
| required | |
| /> | |
| </div> | |
| </div> | |
| <div className="flex justify-between items-center"> | |
| <Button | |
| type="button" | |
| variant="ghost" | |
| size="sm" | |
| className="text-amber-700 hover:text-amber-800 hover:bg-amber-100 text-xs" | |
| onClick={sendOTP} | |
| disabled={isLoading || timeRemaining > 90} // Prevent spam by requiring 30s wait | |
| > | |
| Resend Code | |
| </Button> | |
| <Button | |
| type="submit" | |
| className="bg-amber-600 hover:bg-amber-700 text-white" | |
| disabled={isLoading} | |
| > | |
| {isLoading ? ( | |
| <> | |
| <motion.div | |
| animate={{ rotate: 360 }} | |
| transition={{ duration: 1, repeat: Infinity, ease: "linear" }} | |
| className="w-5 h-5 border-2 border-white border-t-transparent rounded-full mr-2" | |
| ></motion.div> | |
| Verifying... | |
| </> | |
| ) : ( | |
| <>Verify <ArrowRight className="ml-2 h-4 w-4" /></> | |
| )} | |
| </Button> | |
| </div> | |
| </form> | |
| )} | |
| </div> | |
| <div className="pt-4"> | |
| <div className="bg-amber-50 border border-amber-200 p-4 rounded-md"> | |
| <div className="text-amber-800 font-medium flex items-center"> | |
| <Shield className="h-4 w-4 mr-2" /> Security Information | |
| </div> | |
| <div className="text-stone-600 text-sm mt-1"> | |
| For your security, we use two-factor authentication. The verification code is valid for 2 minutes and can only be used once. | |
| </div> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </> | |
| )} | |
| {loginStep === 'success' && ( | |
| <> | |
| <CardHeader className="text-center"> | |
| <motion.div | |
| initial={{ scale: 0 }} | |
| animate={{ scale: 1 }} | |
| transition={{ duration: 0.5 }} | |
| className="mx-auto bg-green-100 text-green-700 rounded-full p-3 mb-4" | |
| > | |
| <CheckCircle2 className="h-12 w-12" /> | |
| </motion.div> | |
| <CardTitle className="text-2xl font-bold text-green-700">Login Successful!</CardTitle> | |
| <CardDescription className="text-stone-600" style={{ fontFamily: "'Lato', sans-serif"}}> | |
| You have been securely authenticated | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-6 text-center"> | |
| <motion.div | |
| initial={{ width: 0 }} | |
| animate={{ width: '100%' }} | |
| transition={{ duration: 1.5 }} | |
| className="w-full bg-green-100 h-2 rounded-full overflow-hidden" | |
| > | |
| <div className="h-full bg-green-500 rounded-full" style={{ width: '100%' }}></div> | |
| </motion.div> | |
| <p className="text-stone-600">Redirecting to your dashboard...</p> | |
| </CardContent> | |
| </> | |
| )} | |
| </Card> | |
| </motion.div> | |
| ); | |
| }; | |
| export default LoginPage; |