integrate the register user api

This commit is contained in:
aryabenade
2026-04-09 13:04:08 +05:30
parent f64993a356
commit cdb9c7e734
4 changed files with 540 additions and 123 deletions

View File

@@ -7,6 +7,7 @@ import { Label } from './ui/label';
import { useAuth } from '../context/AuthContext';
import { useLoginMutation, useVerifyOtpMutation } from '../Redux/services/auth.service';
import { toast } from 'sonner';
import { RegisterModal } from './RegisterModal';
interface LoginModalProps {
isOpen: boolean;
@@ -20,6 +21,7 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) {
const [countdown, setCountdown] = useState(0);
const [helperText, setHelperText] = useState('');
const [error, setError] = useState('');
const [showRegisterModal, setShowRegisterModal] = useState(false);
const { login } = useAuth();
@@ -158,136 +160,156 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) {
};
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
onClick={onClose}
/>
<>
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
onClick={onClose}
/>
<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 }}
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">
<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>
<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 }}
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">
<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 cursor-pointer"
>
<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 and verify with OTP
</p>
</div>
<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 and verify with OTP
</p>
</div>
<div className="px-8 pb-8">
{step === 'email' ? (
// ... Email step (unchanged)
<div className="space-y-6">
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Email Address
</Label>
<Input
type="email"
placeholder="name@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSendOTP()}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
{error && <p className="text-red-500 text-xs">{error}</p>}
{helperText && <p className="text-green-600 text-xs">{helperText}</p>}
</div>
<Button
onClick={handleSendOTP}
disabled={isSendingOtp}
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl"
>
{isSendingOtp ? 'Sending OTP...' : 'Send OTP'}
</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 font-poppins text-base text-gray-600">
{email}
</div>
</div>
{/* OTP Inputs with Paste Support */}
<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"
maxLength={1}
value={digit}
onChange={(e) => handleOTPChange(index, e.target.value.replace(/\D/g, ''))}
onKeyDown={(e) => handleOTPKeyDown(index, e)}
onPaste={handlePaste} // ← Paste support added here
data-otp-index={index}
className="w-12 h-12 text-center font-poppins font-semibold text-lg bg-gray-100 border-0 rounded-xl focus:bg-white focus:ring-2 focus:ring-gray-800 focus:outline-none transition-all"
/>
))}
<div className="px-8 pb-8">
{step === 'email' ? (
// ... Email step (unchanged)
<div className="space-y-6">
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Email Address
</Label>
<Input
type="email"
placeholder="name@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSendOTP()}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
{error && <p className="text-red-500 text-xs">{error}</p>}
{helperText && <p className="text-green-600 text-xs">{helperText}</p>}
</div>
{countdown > 0 && (
<p className="text-center text-xs text-gray-500">
Resend OTP in {formatCountdown(countdown)}
</p>
<Button
onClick={handleSendOTP}
disabled={isSendingOtp}
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl cursor-pointer"
>
{isSendingOtp ? 'Sending OTP...' : 'Send OTP'}
</Button>
<div className="text-center">
<button
onClick={() => setShowRegisterModal(true)}
className="font-poppins text-sm text-gray-600 hover:text-gray-800 transition-colors cursor-pointer"
>
Don't have an account? <span className="text-primary font-semibold">Register</span>
</button>
</div>
</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 font-poppins text-base text-gray-600">
{email}
</div>
</div>
{/* OTP Inputs with Paste Support */}
<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"
maxLength={1}
value={digit}
onChange={(e) => handleOTPChange(index, e.target.value.replace(/\D/g, ''))}
onKeyDown={(e) => handleOTPKeyDown(index, e)}
onPaste={handlePaste} // ← Paste support added here
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-gray-800 focus:outline-none transition-all"
/>
))}
</div>
{countdown > 0 && (
<p className="text-center text-xs text-gray-500">
Resend OTP in {formatCountdown(countdown)}
</p>
)}
</div>
{error && <p className="text-red-500 text-xs text-center">{error}</p>}
<Button
onClick={handleVerifyLogin}
disabled={isVerifying || otp.join('').length !== 6}
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl disabled:opacity-50 cursor-pointer"
>
{isVerifying ? 'Verifying...' : 'Verify & Login'}
</Button>
{countdown === 0 && (
<button
onClick={() => {
setStep('email');
setOtp(['', '', '', '', '', '']);
setError('');
}}
className="w-full text-sm text-gray-600 hover:text-gray-800 font-poppins"
>
Didn't receive OTP? Send again
</button>
)}
</div>
{error && <p className="text-red-500 text-xs text-center">{error}</p>}
<Button
onClick={handleVerifyLogin}
disabled={isVerifying || otp.join('').length !== 6}
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl disabled:opacity-50"
>
{isVerifying ? 'Verifying...' : 'Verify & Login'}
</Button>
{countdown === 0 && (
<button
onClick={() => {
setStep('email');
setOtp(['', '', '', '', '', '']);
setError('');
}}
className="w-full text-sm text-gray-600 hover:text-gray-800 font-poppins"
>
Didn't receive OTP? Send again
</button>
)}
</div>
)}
)}
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</motion.div>
</>
)
}
</AnimatePresence >
<RegisterModal
isOpen={showRegisterModal}
onClose={() => setShowRegisterModal(false)}
onLoginClick={() => {
setShowRegisterModal(false);
setStep('email');
setEmail('');
}}
/>
</>
);
}

View File

@@ -0,0 +1,391 @@
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { useRegisterMutation } from '../Redux/services/auth.service';
import { toast } from 'sonner';
interface RegisterModalProps {
isOpen: boolean;
onClose: () => void;
onLoginClick: () => void;
}
export function RegisterModal({ isOpen, onClose, onLoginClick }: RegisterModalProps) {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
emailAddress: '',
isdCode: '+91',
mobileNumber: '',
address1: '',
address2: '',
city: '',
state: '',
country: 'Australia',
postalCode: ''
});
const [helperText, setHelperText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [register, { isLoading: isRegistering }] = useRegisterMutation();
useEffect(() => {
if (!isOpen) {
setFormData({
firstName: '',
lastName: '',
emailAddress: '',
isdCode: '+91',
mobileNumber: '',
address1: '',
address2: '',
city: '',
state: '',
country: 'Australia',
postalCode: ''
});
setHelperText('');
}
}, [isOpen]);
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const validateForm = () => {
if (!formData.firstName.trim()) {
toast.error('First name is required');
return false;
}
if (!formData.lastName.trim()) {
toast.error('Last name is required');
return false;
}
if (!formData.emailAddress.trim() || !formData.emailAddress.includes('@')) {
toast.error('Please enter a valid email address');
return false;
}
if (!formData.mobileNumber.trim()) {
toast.error('Mobile number is required');
return false;
}
if (!/^\d+$/.test(formData.mobileNumber.trim())) {
toast.error('Mobile number must contain only digits');
return false;
}
if (!formData.address1.trim()) {
toast.error('Address is required');
return false;
}
if (!formData.city.trim()) {
toast.error('City is required');
return false;
}
if (!formData.state.trim()) {
toast.error('State is required');
return false;
}
if (!formData.postalCode.trim()) {
toast.error('Postal code is required');
return false;
}
if (!/^\d+$/.test(formData.postalCode.trim())) {
toast.error('Postal code must contain only digits');
return false;
}
return true;
};
const handleRegister = async () => {
if (!validateForm()) {
return;
}
setHelperText('');
setIsLoading(true);
try {
const response = await register(formData).unwrap();
console.log('Registration response:', response);
toast.success('Registration successful! Please login.');
setTimeout(() => {
onLoginClick();
onClose();
}, 2000);
} catch (error: any) {
console.error('Registration error:', error);
const errorMessage = error?.data?.message || 'Registration failed. Please try again.';
toast.error(errorMessage);
setHelperText(errorMessage);
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleRegister();
}
};
return (
<AnimatePresence>
{isOpen && (
<>
<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}
/>
<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 overflow-y-auto"
>
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl mx-auto overflow-hidden max-h-[90vh] overflow-y-auto">
<div className="relative px-8 pt-8 pb-4 top-0 bg-white z-10">
<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 cursor-pointer"
>
<X className="w-4 h-4 text-gray-600" />
</button>
<h2 className="font-merchant text-2xl font-semibold text-gray-900 mb-2">
Create Account
</h2>
<p className="font-poppins text-sm text-gray-600">
Register to get started with City Cards
</p>
</div>
<div className="px-8 pb-8">
<div className="space-y-6">
{/* Personal Information */}
<div className="space-y-4">
<h3 className="font-poppins text-base font-semibold text-gray-800">Personal Information</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
First Name <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter your first name"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Last Name <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter your last name"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
</div>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Email Address <span className="text-red-500">*</span>
</Label>
<Input
type="email"
placeholder="Enter your email address"
value={formData.emailAddress}
onChange={(e) => handleInputChange('emailAddress', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
ISD Code
</Label>
<Select value={formData.isdCode} onValueChange={(value: any) => handleInputChange('isdCode', value)}>
<SelectTrigger className="h-12 bg-gray-50 border-0 rounded-xl cursor-pointer">
<SelectValue placeholder="Select code" />
</SelectTrigger>
<SelectContent>
<SelectItem value="+1">+1 (USA)</SelectItem>
<SelectItem value="+44">+44 (UK)</SelectItem>
<SelectItem value="+61">+61 (Australia)</SelectItem>
<SelectItem value="+91">+91 (India)</SelectItem>
<SelectItem value="+86">+86 (China)</SelectItem>
<SelectItem value="+81">+81 (Japan)</SelectItem>
<SelectItem value="+49">+49 (Germany)</SelectItem>
<SelectItem value="+33">+33 (France)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="md:col-span-2 space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Mobile Number <span className="text-red-500">*</span>
</Label>
<Input
type="tel"
placeholder="Enter your mobile number"
value={formData.mobileNumber}
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
</div>
</div>
{/* Address Information */}
<div className="space-y-4">
<h3 className="font-poppins text-base font-semibold text-gray-800">Address Information</h3>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Address Line 1 <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter street address"
value={formData.address1}
onChange={(e) => handleInputChange('address1', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Address Line 2
</Label>
<Input
placeholder="Enter apartment, suite, unit (optional)"
value={formData.address2}
onChange={(e) => handleInputChange('address2', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
City <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter city name"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
State <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter state name"
value={formData.state}
onChange={(e) => handleInputChange('state', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Country <span className="text-red-500">*</span>
</Label>
<Select value={formData.country} onValueChange={(value: any) => handleInputChange('country', value)}>
<SelectTrigger className="h-12 bg-gray-50 border-0 rounded-xl cursor-pointer">
<SelectValue placeholder="Select country" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Australia">Australia</SelectItem>
<SelectItem value="United States">United States</SelectItem>
<SelectItem value="United Kingdom">United Kingdom</SelectItem>
<SelectItem value="Canada">Canada</SelectItem>
<SelectItem value="India">India</SelectItem>
<SelectItem value="Germany">Germany</SelectItem>
<SelectItem value="France">France</SelectItem>
<SelectItem value="Japan">Japan</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Postal Code <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter postal code"
value={formData.postalCode}
onChange={(e) => handleInputChange('postalCode', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
</div>
</div>
{helperText && (
<p className={`font-poppins text-xs ${helperText.includes('successful') ? 'text-green-600' : 'text-red-500'}`}>
{helperText}
</p>
)}
<Button
onClick={handleRegister}
disabled={isLoading || isRegistering}
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl transition-colors cursor-pointer"
>
{isLoading || isRegistering ? (
<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" />
Creating Account...
</div>
) : (
<>
Register
<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 className="text-center">
<button
onClick={() => {
onLoginClick();
onClose();
}}
className="font-poppins text-sm text-gray-600 hover:text-gray-800 transition-colors cursor-pointer"
>
Already have an account? <span className="text-primary font-semibold">Login</span>
</button>
</div>
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}

2
src/global.d.ts vendored
View File

@@ -32,3 +32,5 @@ declare module '*.mp4' {
const src: string;
export default src;
}
declare module "*.css";

View File

@@ -1,10 +1,12 @@
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import "./index.css";
import { Provider } from "react-redux";
import { store } from "./Redux/Store";
import { Toaster } from "sonner";
createRoot(document.getElementById("root")!).render(
<Provider store={store}>
<BrowserRouter>