Taste_Rider_Web / src /pages /LoginPage.jsx
Harry9233's picture
Upload 20790 files
5c05829 verified
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;