diff --git a/src/components/LoginModal.tsx b/src/components/LoginModal.tsx index 5eca180..bff54d3 100644 --- a/src/components/LoginModal.tsx +++ b/src/components/LoginModal.tsx @@ -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 ( - - {isOpen && ( - <> - + <> + + {isOpen && ( + <> + - -
-
- + +
+
+ -

- Login -

-

- Enter your email and verify with OTP -

-
+

+ Login +

+

+ Enter your email and verify with OTP +

+
-
- {step === 'email' ? ( - // ... Email step (unchanged) -
-
- - 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 &&

{error}

} - {helperText &&

{helperText}

} -
- - -
- ) : ( -
- {/* Email Display */} -
- -
- {email} -
-
- - {/* OTP Inputs with Paste Support */} -
- -
- {otp.map((digit, index) => ( - 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" - /> - ))} +
+ {step === 'email' ? ( + // ... Email step (unchanged) +
+
+ + 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 &&

{error}

} + {helperText &&

{helperText}

}
- {countdown > 0 && ( -

- Resend OTP in {formatCountdown(countdown)} -

+ +
+ +
+
+ ) : ( +
+ {/* Email Display */} +
+ +
+ {email} +
+
+ + {/* OTP Inputs with Paste Support */} +
+ +
+ {otp.map((digit, index) => ( + 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" + /> + ))} +
+ + {countdown > 0 && ( +

+ Resend OTP in {formatCountdown(countdown)} +

+ )} +
+ + {error &&

{error}

} + + + + {countdown === 0 && ( + )}
- - {error &&

{error}

} - - - - {countdown === 0 && ( - - )} -
- )} + )} +
-
- - - )} - + + + ) + } + + setShowRegisterModal(false)} + onLoginClick={() => { + setShowRegisterModal(false); + setStep('email'); + setEmail(''); + }} + /> + ); } \ No newline at end of file diff --git a/src/components/RegisterModal.tsx b/src/components/RegisterModal.tsx new file mode 100644 index 0000000..e690a89 --- /dev/null +++ b/src/components/RegisterModal.tsx @@ -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 ( + + {isOpen && ( + <> + + + +
+
+ + +

+ Create Account +

+

+ Register to get started with City Cards +

+
+ +
+
+ {/* Personal Information */} +
+

Personal Information

+
+
+ + handleInputChange('firstName', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+ +
+ + handleInputChange('lastName', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+
+ +
+ + handleInputChange('emailAddress', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+ +
+
+ + +
+ +
+ + handleInputChange('mobileNumber', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+
+
+ + {/* Address Information */} +
+

Address Information

+ +
+ + handleInputChange('address1', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+ +
+ + handleInputChange('address2', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+ +
+
+ + handleInputChange('city', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+ +
+ + handleInputChange('state', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+
+ +
+
+ + +
+ +
+ + handleInputChange('postalCode', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+
+
+ + {helperText && ( +

+ {helperText} +

+ )} + + + +
+ +
+
+
+
+
+ + )} +
+ ); +} diff --git a/src/global.d.ts b/src/global.d.ts index 903f3f8..eb5a13c 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -32,3 +32,5 @@ declare module '*.mp4' { const src: string; export default src; } + +declare module "*.css"; diff --git a/src/main.tsx b/src/main.tsx index 9cf6d51..6e91c69 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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(