replace the registration modal with the registration page
This commit is contained in:
15
package-lock.json
generated
15
package-lock.json
generated
@@ -2257,6 +2257,7 @@
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-9.2.0.tgz",
|
||||
"integrity": "sha512-YSzLC0t6VS9MDdPTynSMqU8IxrItFUjkDORALFT6sSMR/XZ5Vgm3RDp/Gk7z727MC4A9s4MFVel0gF0c7+kdrg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.16"
|
||||
}
|
||||
@@ -3097,6 +3098,7 @@
|
||||
"integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -3107,6 +3109,7 @@
|
||||
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -3117,6 +3120,7 @@
|
||||
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
@@ -3386,7 +3390,8 @@
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/embla-carousel-react": {
|
||||
"version": "8.6.0",
|
||||
@@ -3981,6 +3986,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4038,6 +4044,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -4064,6 +4071,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -4099,6 +4107,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -4301,7 +4310,8 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -4564,6 +4574,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
|
||||
@@ -36,6 +36,7 @@ import { PaymentCancelPage } from './pages/PaymentCancelPage';
|
||||
import { ItineraryViewPage } from './pages/ItineraryViewPage';
|
||||
import { CheckoutPage } from './pages/CheckoutPage';
|
||||
import { CreateMagicItineraryPage } from './pages/CreateMagicIternaryPage';
|
||||
import RegisterPage from './components/RegisterPage';
|
||||
|
||||
// User type definition
|
||||
interface User {
|
||||
@@ -283,6 +284,11 @@ export function AppRouter({
|
||||
<CheckoutPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
<Route path="/register" element={
|
||||
<motion.div key="register" {...pageTransition}>
|
||||
<RegisterPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/payment/:bookingId" element={
|
||||
<motion.div key="super-savings" {...pageTransition}>
|
||||
|
||||
@@ -7,7 +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';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface LoginModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -24,6 +24,7 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
const [showRegisterModal, setShowRegisterModal] = useState(false);
|
||||
|
||||
const { login } = useAuth();
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [sendOtp, { isLoading: isSendingOtp }] = useLoginMutation();
|
||||
const [verifyOtp, { isLoading: isVerifying }] = useVerifyOtpMutation();
|
||||
@@ -155,7 +156,11 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
};
|
||||
|
||||
login(userData);
|
||||
toast.success("User Logged in successfully")
|
||||
if (!response?.data?.userExists) {
|
||||
navigate("/register")
|
||||
} else {
|
||||
toast.success("User Logged in successfully")
|
||||
}
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err?.data?.message || 'Invalid OTP. Please try again.');
|
||||
@@ -234,7 +239,7 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
</Button>
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => setShowRegisterModal(true)}
|
||||
onClick={() => navigate("/register")}
|
||||
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>
|
||||
@@ -314,15 +319,6 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
)
|
||||
}
|
||||
</AnimatePresence >
|
||||
<RegisterModal
|
||||
isOpen={showRegisterModal}
|
||||
onClose={() => setShowRegisterModal(false)}
|
||||
onLoginClick={() => {
|
||||
setShowRegisterModal(false);
|
||||
setStep('email');
|
||||
setEmail('');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
226
src/components/RegisterPage.tsx
Normal file
226
src/components/RegisterPage.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { useRegisterMutation } from '../Redux/services/auth.service';
|
||||
import { toast } from 'sonner';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function RegisterPage() {
|
||||
const { login, user } = useAuth();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
emailAddress: user?.email ?? "",
|
||||
isdCode: '',
|
||||
mobileNumber: '',
|
||||
address1: '',
|
||||
address2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
country: '',
|
||||
postalCode: ''
|
||||
});
|
||||
|
||||
const [helperText, setHelperText] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [register, { isLoading: isRegistering }] = useRegisterMutation();
|
||||
|
||||
const emailAddress = user?.email
|
||||
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
if (!formData.firstName.trim()) return toast.error('First name is required'), false;
|
||||
if (!formData.lastName.trim()) return toast.error('Last name is required'), false;
|
||||
if (!formData.emailAddress.includes('@')) return toast.error('Invalid email address'), false;
|
||||
if (!formData.isdCode.startsWith("+")) toast.error("ISD code must start with '+'"), false;
|
||||
if (!/^\+\d+$/.test(formData.isdCode)) toast.error("ISD code must contain only numbers after '+'"), false;
|
||||
if (!/^\d+$/.test(formData.mobileNumber)) return toast.error('Invalid mobile number'), false;
|
||||
if (!formData.address1.trim()) return toast.error('Address required'), false;
|
||||
if (!formData.city.trim()) return toast.error('City required'), false;
|
||||
if (!formData.state.trim()) return toast.error('State required'), false;
|
||||
if (!/^\d+$/.test(formData.postalCode)) return toast.error('Postal code should only contain numbers'), false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setHelperText('');
|
||||
|
||||
try {
|
||||
const response = await register(formData).unwrap();
|
||||
toast.success('Registration successful!');
|
||||
const userData = {
|
||||
userId: response?.user?.id,
|
||||
email: response?.email || formData.emailAddress,
|
||||
name: response?.name || formData.emailAddress.split('@')[0].charAt(0).toUpperCase() + formData.emailAddress.split('@')[0].slice(1),
|
||||
accessToken: response?.accessToken,
|
||||
};
|
||||
|
||||
login(userData);
|
||||
navigate("/")
|
||||
} catch (err: any) {
|
||||
const msg = err?.data?.message || 'Registration failed';
|
||||
toast.error(msg);
|
||||
setHelperText(msg);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-gray-50 w-full">
|
||||
{/* Navbar */}
|
||||
<Navbar activeCity="" />
|
||||
{/* Main Content */}
|
||||
<div className="flex-grow w-full px-6 md:px-10 py-10 mt-20">
|
||||
|
||||
<div className="w-full max-w-5xl mx-auto">
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h2 className="font-merchant text-3xl font-semibold text-gray-900 mb-2">
|
||||
Create Account
|
||||
</h2>
|
||||
<p className="font-poppins text-gray-600">
|
||||
Register to get started with City Cards
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form Container */}
|
||||
<div className="bg-white rounded-2xl border border-gray-200 p-8 space-y-8">
|
||||
|
||||
{/* Personal Info */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-poppins font-semibold text-gray-800 text-lg">
|
||||
Personal Information
|
||||
</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Input
|
||||
placeholder="First Name"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||
className="h-12 bg-gray-50 border-0 rounded-xl"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Last Name"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||
className="h-12 bg-gray-50 border-0 rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Email Address"
|
||||
value={emailAddress}
|
||||
onChange={(e) => handleInputChange('emailAddress', e.target.value)}
|
||||
className="h-12 bg-gray-50 border-0 rounded-xl"
|
||||
/>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
<Input
|
||||
placeholder="ISD Code"
|
||||
value={formData.isdCode}
|
||||
onChange={(e) => handleInputChange('isdCode', e.target.value)}
|
||||
className="h-12 bg-gray-50 border-0 rounded-xl"
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<Input
|
||||
placeholder="Mobile Number"
|
||||
value={formData.mobileNumber}
|
||||
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
|
||||
className="h-12 bg-gray-50 border-0 rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-poppins font-semibold text-gray-800 text-lg">
|
||||
Address Information
|
||||
</h3>
|
||||
|
||||
<Input
|
||||
placeholder="Address Line 1"
|
||||
value={formData.address1}
|
||||
onChange={(e) => handleInputChange('address1', e.target.value)}
|
||||
className="h-12 bg-gray-50 border-0 rounded-xl"
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="Address Line 2"
|
||||
value={formData.address2}
|
||||
onChange={(e) => handleInputChange('address2', e.target.value)}
|
||||
className="h-12 bg-gray-50 border-0 rounded-xl"
|
||||
/>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Input
|
||||
placeholder="City"
|
||||
value={formData.city}
|
||||
onChange={(e) => handleInputChange('city', e.target.value)}
|
||||
className="h-12 bg-gray-50 border-0 rounded-xl"
|
||||
/>
|
||||
<Input
|
||||
placeholder="State"
|
||||
value={formData.state}
|
||||
onChange={(e) => handleInputChange('state', e.target.value)}
|
||||
className="h-12 bg-gray-50 border-0 rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Input
|
||||
placeholder="Country"
|
||||
value={formData.country}
|
||||
onChange={(e) => handleInputChange('country', e.target.value)}
|
||||
className="h-12 bg-gray-50 border-0 rounded-xl"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Postal Code"
|
||||
value={formData.postalCode}
|
||||
onChange={(e) => handleInputChange('postalCode', e.target.value)}
|
||||
className="h-12 bg-gray-50 border-0 rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{helperText && (
|
||||
<p className="text-sm text-red-500">{helperText}</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleRegister}
|
||||
disabled={isLoading || isRegistering}
|
||||
className="w-full cursor-pointer bg-gray-800 hover:bg-gray-900 md:px-10 h-12 text-white rounded-xl"
|
||||
>
|
||||
{isLoading || isRegistering ? 'Registering...' : 'Register'}
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-auto">
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -60,71 +60,6 @@ interface ProfilePageProps {
|
||||
currentPage: string;
|
||||
}
|
||||
|
||||
// Mock passes data
|
||||
const mockPasses = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Melbourne Unlimited Card',
|
||||
city: 'Melbourne',
|
||||
type: 'Unlimited Pass',
|
||||
status: 'active',
|
||||
price: 149.00,
|
||||
originalPrice: 249.00,
|
||||
discount: 40,
|
||||
attractions: 25,
|
||||
validFrom: '2024-01-15',
|
||||
validUntil: '2024-01-22',
|
||||
daysRemaining: 3,
|
||||
image: 'https://images.unsplash.com/photo-1514395462725-fb4566210144?w=400',
|
||||
usedAttractions: 8
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Melbourne Selective Card',
|
||||
city: 'Melbourne',
|
||||
type: 'Flexi Pass',
|
||||
status: 'active',
|
||||
price: 89.00,
|
||||
originalPrice: 149.00,
|
||||
discount: 40,
|
||||
attractions: 12,
|
||||
validFrom: '2024-02-01',
|
||||
validUntil: '2024-02-08',
|
||||
daysRemaining: 12,
|
||||
image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?w=400',
|
||||
usedAttractions: 3
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Sydney Explorer Pass',
|
||||
city: 'Sydney',
|
||||
type: 'Standard Pass',
|
||||
status: 'expired',
|
||||
price: 89.00,
|
||||
originalPrice: 149.00,
|
||||
discount: 40,
|
||||
attractions: 15,
|
||||
validFrom: '2023-12-01',
|
||||
validUntil: '2023-12-08',
|
||||
daysRemaining: 0,
|
||||
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400',
|
||||
usedAttractions: 12
|
||||
}
|
||||
];
|
||||
|
||||
// Mock itineraries data
|
||||
const mockItineraries = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Melbourne Unlimited Card',
|
||||
city: 'Melbourne',
|
||||
duration: '7 days',
|
||||
attractions: 25,
|
||||
createdDate: '2024-01-15',
|
||||
status: 'active'
|
||||
}
|
||||
];
|
||||
|
||||
export function ProfilePage({
|
||||
onBackClick,
|
||||
onHomeClick,
|
||||
@@ -342,21 +277,6 @@ export function ProfilePage({
|
||||
|
||||
<div>
|
||||
<Label htmlFor="country" className="font-poppins font-light">Country</Label>
|
||||
{/* <Select value={formData.country} onValueChange={(value) => handleInputChange('country', value)}>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue placeholder="Select country" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="us">United States</SelectItem>
|
||||
<SelectItem value="au">Australia</SelectItem>
|
||||
<SelectItem value="uk">United Kingdom</SelectItem>
|
||||
<SelectItem value="ca">Canada</SelectItem>
|
||||
<SelectItem value="de">Germany</SelectItem>
|
||||
<SelectItem value="fr">France</SelectItem>
|
||||
<SelectItem value="in">India</SelectItem>
|
||||
<SelectItem value="jp">Japan</SelectItem>
|
||||
</SelectContent>
|
||||
</Select> */}
|
||||
<Input
|
||||
id="country"
|
||||
value={formData.country}
|
||||
|
||||
Reference in New Issue
Block a user