diff --git a/index.html b/index.html index daa01f8..7ce70fd 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,16 @@ + + - - - - - - CityCards Customer-web - + + + + + CityCards Customer-web + - -
- - - - \ No newline at end of file + +
+ + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index abfa862..af5b3cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index a8e8300..ed1dd57 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -6,17 +6,12 @@ import { MelbournePage } from './pages/MelbournePage'; import { PassesPage } from './pages/PassesPage'; import { AttractionsPage } from './pages/AttractionsPage'; import { AttractionDetailsPage } from './pages/AttractionDetailsPage'; -import { CheckoutPage } from './pages/CheckoutPage'; -import { SecureCheckoutPage } from './pages/SecureCheckoutPage'; import { BlogsPage } from './pages/BlogsPage'; import { BlogDetailsPage } from './pages/BlogDetailsPage'; -import { HowItWorksPage } from './components/HowItWorksPage'; import { FAQPage } from './components/FAQPage'; import { PrivacyPolicyPage } from './pages/PrivacyPolicyPage'; import { AboutUsPage } from './pages/AboutUsPage'; import { ProfilePage } from './pages/ProfilePage'; -import { CreateMagicItineraryPage } from './pages/CreateMagicItineraryPage'; -import { ItineraryViewPage } from './pages/ItineraryViewPage'; import { OffersPage } from './pages/OffersPage'; import { CityCardsPage } from './pages/CityCardsPage'; import { MagicItineraryPage } from './pages/MagicItineraryPage'; @@ -24,7 +19,6 @@ import { PostCardsPage } from './pages/PostCardsPage'; import { DownloadAppPage } from './pages/DownloadAppPage'; import { HotelDiscountsPage } from './pages/HotelDiscountsPage'; import { ContactUsPage } from './pages/ContactUsPage'; - import { pageTransition } from './utils/animations'; import { LandingPage } from './pages/landingPage'; import ComingSoonPage from './pages/ComingSoonPage'; @@ -34,15 +28,15 @@ import { LandingMagicItineraryPage } from './pages/LandingMagicItineraryPage'; import { DiscoverPage } from './pages/DiscoverPage'; import { CartPage } from './pages/CartPage'; import { PaymentDetailsPage } from './pages/PaymentDetailsPage'; -import { CartPageDesign } from './pages/CartPageDesign'; -import { CheckoutPage2 } from './pages/CheckoutPage2'; import { SuperSavingsDetailsPage } from './pages/SuperSavingsDetailsPage'; -import { ViewCardDetailsPage } from './pages/ViewCardDetailsPageDesign'; -import { CreateMagicItineraryPageDesign } from './pages/CreateMagicIternaryPageDesign'; -import { ItineraryViewPageDesign } from './pages/ItineraryViewPageDesign'; +import { ViewCardDetailsPage } from './pages/ViewCardDetailsPage'; import ItinerarySummaryPage from './pages/ItinerarySummaryPage'; import { PaymentSuccessPage } from './pages/PaymentSuccessPage'; 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 { @@ -134,19 +128,6 @@ export function AppRouter({ } /> - {/* Checkout Routes */} - {/* - - - } /> */} - - - - - } /> - {/* Blog Routes */} @@ -200,7 +181,7 @@ export function AppRouter({ } /> - @@ -213,22 +194,12 @@ export function AppRouter({ } /> - - - - } /> - } /> - - - - } /> @@ -310,14 +281,15 @@ export function AppRouter({ } /> - + } /> - - + + } /> + diff --git a/src/Redux/services/profile.service.ts b/src/Redux/services/profile.service.ts index fe266fe..ab343fd 100644 --- a/src/Redux/services/profile.service.ts +++ b/src/Redux/services/profile.service.ts @@ -25,9 +25,11 @@ export const profileApi = createApi({ }), getUserCards: builder.query({ - query: (sort) => { + query: ({sort,cityId}) => { const params = new URLSearchParams() + params.append('cityXid', cityId); + if (sort) params.append('sort', sort); return `/website/passes/all?${params.toString()}` diff --git a/src/assets/citycards customer app.png b/src/assets/citycards customer app.png new file mode 100644 index 0000000..7caa18f Binary files /dev/null and b/src/assets/citycards customer app.png differ diff --git a/src/components/CitySelectionDialog.tsx b/src/components/CitySelectionDialog.tsx index 188d4a0..a972fc5 100644 --- a/src/components/CitySelectionDialog.tsx +++ b/src/components/CitySelectionDialog.tsx @@ -45,6 +45,7 @@ export function CitySelectionDialog({ navigate(`/${slugify(city.cityName)}`); localStorage.setItem("cityId", String(city.id)) localStorage.setItem("cityName", String(city.cityName)) + sessionStorage.setItem("citySelected", String(city.cityName)) onClose(); }; diff --git a/src/components/LoginModal.tsx b/src/components/LoginModal.tsx index b15cdc6..be1c4e5 100644 --- a/src/components/LoginModal.tsx +++ b/src/components/LoginModal.tsx @@ -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; @@ -21,9 +21,9 @@ 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(); + const navigate = useNavigate() const [sendOtp, { isLoading: isSendingOtp }] = useLoginMutation(); const [verifyOtp, { isLoading: isVerifying }] = useVerifyOtpMutation(); @@ -147,15 +147,21 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) { otp: otpString }).unwrap(); - const userData = { - userId: response?.user?.id, - email: response?.email || email, - name: response?.name || email.split('@')[0].charAt(0).toUpperCase() + email.split('@')[0].slice(1), - accessToken: response?.accessToken, - }; + + if (!response?.userExists) { + localStorage.setItem("userEmail",email) + navigate("/register") + } else { + const userData = { + userId: response?.user?.id, + email: response?.email || email, + name: response?.name || email.split('@')[0].charAt(0).toUpperCase() + email.split('@')[0].slice(1), + accessToken: response?.accessToken, + }; - login(userData); - toast.success("User Logged in successfully") + login(userData); + toast.success("User Logged in successfully") + } onClose(); } catch (err: any) { setError(err?.data?.message || 'Invalid OTP. Please try again.'); @@ -232,14 +238,7 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) { > {isSendingOtp ? 'Sending OTP...' : 'Send OTP'} -
- -
+ ) : (
@@ -314,15 +313,6 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) { ) } - setShowRegisterModal(false)} - onLoginClick={() => { - setShowRegisterModal(false); - setStep('email'); - setEmail(''); - }} - /> ); } \ No newline at end of file diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3a7d78c..193529a 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -2,12 +2,10 @@ import { useState, useEffect, useRef, forwardRef } from 'react'; import { Menu, X, ShoppingBag, ChevronDown, Globe, User, Settings, LogOut } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; -import Frame1597884853 from '../imports/Frame1597884853'; import { Button } from './ui/button'; import { ImageWithFallback } from './figma/ImageWithFallback'; import { CTAButton } from './CTAButton'; import logoImage from '../assets/cit-logo.png'; -import melbourneLogo from '../assets/melbourne-logo.png'; import { CitySelectionDialog, slugify } from './CitySelectionDialog'; import { useAuth } from '../context/AuthContext'; import { LoginModal } from './LoginModal'; @@ -62,15 +60,12 @@ interface NavigationItem { export default function Navbar({ activeCity, onCityChange, - onSignInClick, - onSignOutClick, isUserSignedIn = false, // user }: NavbarProps) { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); const [activeLanguageDropdown, setActiveLanguageDropdown] = useState(false); - const [activeCartDropdown, setActiveCartDropdown] = useState(false); const [activeUserDropdown, setActiveUserDropdown] = useState(false); const [activeCityDropdown, setActiveCityDropdown] = useState(false); const [isCityDialogOpen, setIsCityDialogOpen] = useState(false); @@ -79,7 +74,6 @@ export default function Navbar({ const [dialogSource, setDialogSource] = useState<'navbar' | 'cta'>('navbar'); const languageRef = useRef(null); - const cartRef = useRef(null); const userRef = useRef(null); const cityRef = useRef(null); @@ -96,7 +90,7 @@ export default function Navbar({ const cityName = localStorage.getItem("cityName") // const citySelected = location.pathname.includes(slugify(cityName) || "") - const citySelected = cityName + const citySelected = sessionStorage.getItem("citySelected") const baseUrl = import.meta.env.VITE_BASE_URL; @@ -137,15 +131,6 @@ export default function Navbar({ path: '/whats-included', isShared: false }, - // Position 4 - Shared item - // { - // label: 'Your Card', - // path: '/passes', - // isShared: true, - // landingLabel: 'Your Card', - // melbourneLabel: 'Your Card' - // }, - // Position 5 { label: 'FAQ', path: '/faq', @@ -159,7 +144,7 @@ export default function Navbar({ melbourneLabel: 'Your Postcard' } ], - melbourne: [ + citySelected: [ // Position 1 { label: 'Attractions', @@ -188,11 +173,11 @@ export default function Navbar({ }, // Position 5 - Shared item { - label: 'Your Card', + label: 'Buy Cards', path: `/passes`, isShared: true, - landingLabel: 'Your Card', - melbourneLabel: 'Your Card' + landingLabel: 'Buy Cards', + melbourneLabel: 'Buy Cards' }, { label: 'Your Postcard', @@ -240,20 +225,20 @@ export default function Navbar({ }, [location.pathname]); // ✅ Determine which navbar to show - const getAutoNavigationSource = (): 'landing' | 'melbourne' => { + const getAutoNavigationSource = () => { const path = location.pathname; // Explicit routes - if (path.startsWith('/melbourne')) return 'melbourne'; + // if (path.startsWith('/melbourne')) return 'melbourne'; if (path === '/' || path.startsWith('/explore')) return 'landing'; // Shared routes - if (['/passes', '/how-it-works'].includes(path)) { - return lastKnownCity; // ← remembers where user came from - } + // if (['/passes', '/how-it-works'].includes(path)) { + // return lastKnownCity; // ← remembers where user came from + // } // Fallback - return lastKnownCity; + return citySelected; }; @@ -261,7 +246,7 @@ export default function Navbar({ const getNavigationItems = (): NavigationItem[] => { const currentSource = getAutoNavigationSource(); const items = currentSource === 'landing' ? - navigationConfig.landing : navigationConfig.melbourne; + navigationConfig.landing : navigationConfig.citySelected; return items.map((item, index) => ({ ...item, @@ -370,34 +355,6 @@ export default function Navbar({ { id: '2', name: 'Melbourne Premium Pass', price: '$129', quantity: 1 }, ]; - // Calculate cart total - const cartTotal = cartItems.reduce((total, item) => { - const price = parseFloat(item.price.replace('$', '')); - return total + (price * item.quantity); - }, 0); - - // Cart dropdown items with proper navigation for checkout - const cartDropdownItems: DropdownItem[] = [ - ...cartItems.map(item => ({ - id: item.id, - label: `${item.name} - ${item.price}`, - badge: `${item.quantity}x` - })), - { - id: 'total', - label: `Total: $${cartTotal.toFixed(2)}`, - icon: - }, - { - id: 'checkout', - label: 'Proceed to Checkout', - action: () => { - navigate('/checkout'); - setActiveCartDropdown(false); - } - } - ]; - const closeMobileMenu = () => { setIsMobileMenuOpen(false); }; diff --git a/src/components/RegisterModal.tsx b/src/components/RegisterModal.tsx deleted file mode 100644 index e690a89..0000000 --- a/src/components/RegisterModal.tsx +++ /dev/null @@ -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 ( - - {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/components/RegisterPage.tsx b/src/components/RegisterPage.tsx new file mode 100644 index 0000000..f6ea833 --- /dev/null +++ b/src/components/RegisterPage.tsx @@ -0,0 +1,260 @@ +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'; +import { Label } from './ui/label'; + +export default function RegisterPage() { + const { login } = useAuth(); + const email = localStorage.getItem("userEmail") + const [formData, setFormData] = useState({ + firstName: '', + lastName: '', + emailAddress: 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 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); + localStorage.removeItem("userEmail") + navigate("/") + } catch (err: any) { + const msg = err?.data?.message || 'Registration failed'; + toast.error(msg); + setHelperText(msg); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* Navbar */} + + + {/* Main Content */} +
+
+ + {/* Header */} +
+

+ Create Account +

+

+ Register to get started with City Cards +

+
+ + {/* Form Container */} +
+ + {/* Personal Info */} +
+

+ Personal Information +

+ +
+
+ + handleInputChange('firstName', e.target.value)} + className="h-12 bg-gray-50 border-0 rounded-xl mt-1" + /> +
+ +
+ + handleInputChange('lastName', e.target.value)} + className="h-12 bg-gray-50 border-0 rounded-xl mt-1" + /> +
+
+ +
+ + handleInputChange('emailAddress', e.target.value)} + className="h-12 bg-gray-50 border-0 rounded-xl mt-1" + /> +
+ +
+
+ + handleInputChange('isdCode', e.target.value)} + className="h-12 bg-gray-50 border-0 rounded-xl mt-1" + /> +
+ +
+ + handleInputChange('mobileNumber', e.target.value)} + className="h-12 bg-gray-50 border-0 rounded-xl mt-1" + /> +
+
+
+ + {/* Address */} +
+

+ Address Information +

+ +
+ + handleInputChange('address1', e.target.value)} + className="h-12 bg-gray-50 border-0 rounded-xl mt-1" + /> +
+ +
+ + handleInputChange('address2', e.target.value)} + className="h-12 bg-gray-50 border-0 rounded-xl mt-1" + /> +
+ +
+
+ + handleInputChange('city', e.target.value)} + className="h-12 bg-gray-50 border-0 rounded-xl mt-1" + /> +
+ +
+ + handleInputChange('state', e.target.value)} + className="h-12 bg-gray-50 border-0 rounded-xl mt-1" + /> +
+
+ +
+
+ + handleInputChange('country', e.target.value)} + className="h-12 bg-gray-50 border-0 rounded-xl mt-1" + /> +
+ +
+ + handleInputChange('postalCode', e.target.value)} + className="h-12 bg-gray-50 border-0 rounded-xl mt-1" + /> +
+
+
+ + {helperText && ( +

{helperText}

+ )} + + + +
+
+
+ + {/* Footer */} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 50dba2a..b3cdda3 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -40,6 +40,8 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { localStorage.removeItem("user") localStorage.removeItem("accessToken") localStorage.removeItem("userId") + localStorage.removeItem("userEmail") + sessionStorage.removeItem("citySelected") navigate("/") } diff --git a/src/pages/CartPageDesign.tsx b/src/pages/CartPageDesign.tsx deleted file mode 100644 index e51725c..0000000 --- a/src/pages/CartPageDesign.tsx +++ /dev/null @@ -1,878 +0,0 @@ -import React, { useState } from 'react'; -import { motion, AnimatePresence } from 'motion/react'; -import { - Users, Baby, ShoppingBag, Trash2, Check, CreditCard, Mail, - ChevronRight, ChevronDown, Minus, Plus, Calendar, ArrowLeft, MapPin, - Zap, Shield, Clock, Percent, Sparkles -} from 'lucide-react'; -import Navbar from '../components/Navbar'; -import { Footer } from '../components/Footer'; -import { ImageWithFallback } from '../components/figma/ImageWithFallback'; -import { useNavigate } from 'react-router-dom'; -import { useGetCardsinCartQuery } from '../Redux/services/cards.service'; -import LoadingSpinner from '../components/LoadingSpinner' - -/* ─── Types ─── */ -export interface CartItem { - id: string; - city: string; - cardType: 'Flexi' | 'Unlimited'; - days: number; - adults: number; - children: number; - quantity: number; - pricePerUnit: number; - image: string; -} - -interface Attraction { - id: string; - name: string; - image: string; - category: string; - included: boolean; -} - -interface CartPageDesignProps { - onBackClick: () => void; - onHomeClick: () => void; - onPassesClick: () => void; - onCheckoutClick?: () => void; - onSecureCheckoutClick?: (item: CartItem) => void; - onSignInClick: () => void; - onSignOutClick?: () => void; - onAttractionsClick?: () => void; - onBlogsClick?: () => void; - onHowItWorksClick?: () => void; - onFAQClick?: () => void; - onPrivacyPolicyClick?: () => void; - onAboutUsClick?: () => void; - onProfileClick?: () => void; - onCityCardsClick?: () => void; - onMagicItineraryClick?: () => void; - onPostCardsClick?: () => void; - onOffersClick?: () => void; - onSuperSavingsClick?: () => void; - onEsimsClick?: () => void; - onHotelDiscountsClick?: () => void; - onContactUsClick?: () => void; - onCartClick?: () => void; - currentPage?: string; - user?: { email: string; name: string } | null; -} - -/* ─── Data ─── */ -const initialCartItems: CartItem[] = [ - { - id: '1', city: 'Melbourne', cardType: 'Flexi', days: 3, adults: 3, children: 3, quantity: 2, pricePerUnit: 49.50, - image: 'https://images.unsplash.com/photo-1655963754904-2cf2b562a681?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBmbGluZGVycyUyMHN0YXRpb24lMjBzdW5zZXR8ZW58MXx8fHwxNzc2MzE5NDgzfDA&ixlib=rb-4.1.0&q=80&w=1080', - }, - { - id: '2', city: 'Sydney', cardType: 'Flexi', days: 3, adults: 3, children: 3, quantity: 2, pricePerUnit: 49.50, - image: 'https://images.unsplash.com/photo-1695018228065-2e0026c654af?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxTeWRuZXklMjBvcGVyYSUyMGhvdXNlJTIwaGFyYm91ciUyMGJyaWRnZXxlbnwxfHx8fDE3NzYzMTk0ODN8MA&ixlib=rb-4.1.0&q=80&w=1080', - }, - { - id: '3', city: 'Melbourne', cardType: 'Unlimited', days: 6, adults: 2, children: 1, quantity: 1, pricePerUnit: 79.00, - image: 'https://images.unsplash.com/photo-1705120624704-0970afc29fea?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBzdHJlZXQlMjBhcnQlMjBsYW5ld2F5c3xlbnwxfHx8fDE3NzYzMTk0ODR8MA&ixlib=rb-4.1.0&q=80&w=1080', - }, -]; - -const dayOptions = [3, 6, 12, 18, 24]; - -const attractionsData: Record> = { - Melbourne: { - Flexi: [ - { id: 'mel-1', name: 'SEA LIFE Aquarium', image: 'https://images.unsplash.com/photo-1536845111858-bb269af65cb6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBhcXVhcml1bSUyMHVuZGVyd2F0ZXJ8ZW58MXx8fHwxNzc2MzE5OTcwfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true }, - { id: 'mel-2', name: 'Melbourne Zoo', image: 'https://images.unsplash.com/photo-1730074888490-31239540bacf?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjB6b28lMjB3aWxkbGlmZXxlbnwxfHx8fDE3NzYzMTk5NzB8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true }, - { id: 'mel-3', name: 'Royal Botanic Gardens', image: 'https://images.unsplash.com/photo-1585894507208-eeead8cb9a56?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBib3RhbmljYWwlMjBnYXJkZW4lMjBncmVlbnxlbnwxfHx8fDE3NzYzMTk5NzF8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Nature', included: true }, - { id: 'mel-4', name: 'NGV Art Gallery', image: 'https://images.unsplash.com/photo-1752429242469-55ba7ec210d2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnQlMjBnYWxsZXJ5JTIwbXVzZXVtJTIwaW50ZXJpb3J8ZW58MXx8fHwxNzc2MzE5OTczfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Culture', included: true }, - ], - Unlimited: [ - { id: 'mel-1', name: 'SEA LIFE Aquarium', image: 'https://images.unsplash.com/photo-1536845111858-bb269af65cb6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBhcXVhcml1bSUyMHVuZGVyd2F0ZXJ8ZW58MXx8fHwxNzc2MzE5OTcwfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true }, - { id: 'mel-2', name: 'Melbourne Zoo', image: 'https://images.unsplash.com/photo-1730074888490-31239540bacf?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjB6b28lMjB3aWxkbGlmZXxlbnwxfHx8fDE3NzYzMTk5NzB8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true }, - { id: 'mel-3', name: 'Royal Botanic Gardens', image: 'https://images.unsplash.com/photo-1585894507208-eeead8cb9a56?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBib3RhbmljYWwlMjBnYXJkZW4lMjBncmVlbnxlbnwxfHx8fDE3NzYzMTk5NzF8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Nature', included: true }, - { id: 'mel-4', name: 'NGV Art Gallery', image: 'https://images.unsplash.com/photo-1752429242469-55ba7ec210d2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnQlMjBnYWxsZXJ5JTIwbXVzZXVtJTIwaW50ZXJpb3J8ZW58MXx8fHwxNzc2MzE5OTczfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Culture', included: true }, - { id: 'mel-5', name: 'Melbourne Star Wheel', image: 'https://images.unsplash.com/photo-1769880659692-fa77e04c5ffa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxvYnNlcnZhdGlvbiUyMHdoZWVsJTIwYW11c2VtZW50JTIwbmlnaHR8ZW58MXx8fHwxNzc2MzE5OTc2fDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Experience', included: true }, - { id: 'mel-6', name: 'Penguin Parade', image: 'https://images.unsplash.com/photo-1670391050251-d1cfbc3891c4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxwZW5ndWlucyUyMHdpbGRsaWZlJTIwbmF0dXJlfGVufDF8fHx8MTc3NjMxOTk3Nnww&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true }, - { id: 'mel-7', name: 'Yarra River Cruise', image: 'https://images.unsplash.com/photo-1562003914-018a4a6c2171?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyaXZlciUyMGNydWlzZSUyMGJvYXQlMjBjaXR5fGVufDF8fHx8MTc3NjMxOTk3M3ww&ixlib=rb-4.1.0&q=80&w=1080', category: 'Experience', included: true }, - ], - }, - Sydney: { - Flexi: [ - { id: 'syd-1', name: 'Harbour Bridge Climb', image: 'https://images.unsplash.com/photo-1767974062666-2685a670e353?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxTeWRuZXklMjBoYXJib3VyJTIwYnJpZGdlJTIwY2xpbWJ8ZW58MXx8fHwxNzc2MzE5OTcxfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Adventure', included: true }, - { id: 'syd-2', name: 'Taronga Zoo', image: 'https://images.unsplash.com/photo-1704852168456-b70e08441917?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxTeWRuZXklMjB0YXJvbmdhJTIwem9vJTIwYW5pbWFsc3xlbnwxfHx8fDE3NzYzMTk5NzJ8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true }, - { id: 'syd-3', name: 'Art Gallery NSW', image: 'https://images.unsplash.com/photo-1752429242469-55ba7ec210d2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnQlMjBnYWxsZXJ5JTIwbXVzZXVtJTIwaW50ZXJpb3J8ZW58MXx8fHwxNzc2MzE5OTczfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Culture', included: true }, - ], - Unlimited: [ - { id: 'syd-1', name: 'Harbour Bridge Climb', image: 'https://images.unsplash.com/photo-1767974062666-2685a670e353?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxTeWRuZXklMjBoYXJib3VyJTIwYnJpZGdlJTIwY2xpbWJ8ZW58MXx8fHwxNzc2MzE5OTcxfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Adventure', included: true }, - { id: 'syd-2', name: 'Taronga Zoo', image: 'https://images.unsplash.com/photo-1704852168456-b70e08441917?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxTeWRuZXklMjB0YXJvbmdhJTIwem9vJTIwYW5pbWFsc3xlbnwxfHx8fDE3NzYzMTk5NzJ8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true }, - { id: 'syd-3', name: 'Art Gallery NSW', image: 'https://images.unsplash.com/photo-1752429242469-55ba7ec210d2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnQlMjBnYWxsZXJ5JTIwbXVzZXVtJTIwaW50ZXJpb3J8ZW58MXx8fHwxNzc2MzE5OTczfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Culture', included: true }, - { id: 'syd-4', name: 'Sydney Harbour Cruise', image: 'https://images.unsplash.com/photo-1562003914-018a4a6c2171?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyaXZlciUyMGNydWlzZSUyMGJvYXQlMjBjaXR5fGVufDF8fHx8MTc3NjMxOTk3M3ww&ixlib=rb-4.1.0&q=80&w=1080', category: 'Experience', included: true }, - { id: 'syd-5', name: 'SEA LIFE Aquarium', image: 'https://images.unsplash.com/photo-1536845111858-bb269af65cb6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBhcXVhcml1bSUyMHVuZGVyd2F0ZXJ8ZW58MXx8fHwxNzc2MzE5OTcwfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true }, - ], - }, -}; - -const offersData: Record = { - Flexi: [ - { title: 'Astor Hotels Ultra Deluxe', description: '15% Discount on all treatments for first-time clients', image: 'https://images.unsplash.com/photo-1715191904112-4a5d9c3089fa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsdXh1cnklMjBob3RlbCUyMHJlc29ydCUyMGV4dGVyaW9yfGVufDF8fHx8MTc3NjMyMTM2MXww&ixlib=rb-4.1.0&q=80&w=1080' }, - { title: 'Green Valley Spa Lux', description: '20% Off on membership plans for new members', image: 'https://images.unsplash.com/photo-1759216853079-831ef8c8b327?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzcGElMjB3ZWxsbmVzcyUyMHRyZWF0bWVudCUyMGludGVyaW9yfGVufDF8fHx8MTc3NjMyMTM2M3ww&ixlib=rb-4.1.0&q=80&w=1080' }, - { title: 'Harbour Dining Co.', description: '10% Off your first dining experience at waterfront', image: 'https://images.unsplash.com/photo-1676471932681-45fa972d848a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyZXN0YXVyYW50JTIwZmluZSUyMGRpbmluZ3xlbnwxfHx8fDE3NzYzMTkxNDl8MA&ixlib=rb-4.1.0&q=80&w=1080' }, - { title: 'National Gallery Exhibition', description: 'Free audio guide with every gallery visit', image: 'https://images.unsplash.com/photo-1569342380852-035f42d9ca41?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtdXNldW0lMjBnYWxsZXJ5JTIwZXhoaWJpdGlvbnxlbnwxfHx8fDE3NzYyNDYwMjh8MA&ixlib=rb-4.1.0&q=80&w=1080' }, - { title: 'Sunset Harbour Cruise', description: 'Complimentary drink on every sunset cruise booking', image: 'https://images.unsplash.com/photo-1765783800962-83d99ff7b158?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjcnVpc2UlMjBib2F0JTIwaGFyYm9yJTIwdG91cnxlbnwxfHx8fDE3NzYzMjE2MDd8MA&ixlib=rb-4.1.0&q=80&w=1080' }, - ], - Unlimited: [ - { title: 'SkyView Ferris Wheel', description: 'Complimentary second ride for all pass holders', image: 'https://images.unsplash.com/photo-1626209025747-b41ee6ec191f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxmZXJyaXMlMjB3aGVlbCUyMGFtdXNlbWVudCUyMHBhcmt8ZW58MXx8fHwxNzc2MzE3NDI2fDA&ixlib=rb-4.1.0&q=80&w=1080' }, - { title: 'City Mall Boutique', description: '15% Off at select boutique stores with your pass', image: 'https://images.unsplash.com/photo-1567966689299-819568579d36?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzaG9wcGluZyUyMG1hbGwlMjBib3V0aXF1ZSUyMHJldGFpbHxlbnwxfHx8fDE3NzYzMjEzNjN8MA&ixlib=rb-4.1.0&q=80&w=1080' }, - { title: 'Adventure Outfitters', description: 'Free gear rental on outdoor adventure bookings', image: 'https://images.unsplash.com/photo-1761131221577-0716baffc6ef?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhZHZlbnR1cmUlMjBzcG9ydHMlMjBvdXRkb29yJTIwYWN0aXZpdHl8ZW58MXx8fHwxNzc2MzIxMzYzfDA&ixlib=rb-4.1.0&q=80&w=1080' }, - { title: 'Skyline Rooftop Lounge', description: 'Buy one get one free on signature cocktails', image: 'https://images.unsplash.com/photo-1642114955097-8f3d0e141641?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyb29mdG9wJTIwYmFyJTIwY2l0eSUyMHNreWxpbmUlMjBuaWdodHxlbnwxfHx8fDE3NzYyNDU2NTl8MA&ixlib=rb-4.1.0&q=80&w=1080' }, - { title: 'Yarra Valley Wines', description: 'Exclusive wine tasting tour with pass holders discount', image: 'https://images.unsplash.com/photo-1764649841527-c8852b63cc53?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx3aW5lJTIwdGFzdGluZyUyMHZpbmV5YXJkJTIwY2VsbGFyfGVufDF8fHx8MTc3NjMyMTYwOHww&ixlib=rb-4.1.0&q=80&w=1080' }, - ], -}; - -const priceTable: Record> = { - Flexi: { 3: 49.5, 6: 69, 12: 99, 18: 129, 24: 159 }, - Unlimited: { 3: 79, 6: 109, 12: 149, 18: 189, 24: 229 }, -}; - -/* ═══════════════════════════════════════════ - FIGMA CARD TYPE COMPONENTS - ═══════════════════════════════════════════ */ - -function FlexiCardPreview({ city, adultPrice, childPrice, isSelected }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean }) { - return ( -
- {/* Card bg */} -
- {/* City image */} -
- {/* */} -
- {/* City name - left aligned */} -
-

{city}

-
- {/* Pricing */} -
-
- From - ${adultPrice} - /Adult -
-
- and - ${childPrice} - /Child -
-
- {/* Description */} -
-

- Dive into an extensive selection of thrilling destinations! -

-
- {/* Side tab - Flexi (pink) */} -
- Card - Flexi -
- {/* Selected checkmark */} - {isSelected && ( -
- -
- )} -
- ); -} - -function UnlimitedCardPreview({ city, adultPrice, childPrice, isSelected }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean }) { - return ( -
- {/* Card bg */} -
- {/* City image */} -
- {/* */} -
- {/* City name - left aligned */} -
-

{city}

-
- {/* Pricing */} -
-
- From - ${adultPrice} - /Adult -
-
- and - ${childPrice} - /Child -
-
- {/* Description */} -
-

- Dive into an extensive selection of thrilling destinations! -

-
- {/* Side tab - Unlimited (coral) */} -
- Card - Unlimited -
- {/* Selected checkmark */} - {isSelected && ( -
- -
- )} -
- ); -} - -/* ═══════════════════════════════════════════ - CHECKOUT CONFIGURATION CARD (Mobile-first) - ═══════════════════════════════════════════ */ - -function CheckoutConfigCard({ - item, - onChange, - onProceed, -}: { - item: CartItem; - onChange: (updates: Partial) => void; - onProceed: () => void; -}) { - const [daysOpen, setDaysOpen] = useState(false); - const originalPrice = (item.pricePerUnit * item.quantity * 1.35); - const totalPrice = item.pricePerUnit * item.quantity; - - return ( -
- {/* City header */} -
-

{item.city}

-
- - {item.cardType} Card - -
-
- - {/* Configuration rows */} -
- {/* No. of Adults */} -
- No. of Adults -
- - {item.adults} - -
-
- - {/* No. of Children */} -
- No. of Children -
- - {item.children} - -
-
- - {/* No. of Days (dropdown) */} -
- - {item.cardType === 'Flexi' ? 'No. of Attractions' : 'No. of Days'} - -
- - - {daysOpen && ( - - {dayOptions.map((d) => ( - - ))} - - )} - -
-
- - {/* You Pay */} -
- You Pay -
- - ${originalPrice.toFixed(0)} - - - ${totalPrice.toFixed(0)} - -
-
-
- - {/* Proceed button */} -
- - Proceed to Pay - -
-
- ); -} - -/* ═══════════════════════════════════════════ - MAIN CART PAGE - ═══════════════════════════════════════════ */ - -export function CartPageDesign({ - onBackClick, - onHomeClick, - onPassesClick, - onCheckoutClick, - onSecureCheckoutClick, - onSignInClick, - onSignOutClick, - onAttractionsClick, - onBlogsClick, - onHowItWorksClick, - onFAQClick, - onPrivacyPolicyClick, - onAboutUsClick, - onProfileClick, - onCityCardsClick, - onMagicItineraryClick, - onPostCardsClick, - onOffersClick, - onSuperSavingsClick, - onEsimsClick, - onHotelDiscountsClick, - onContactUsClick, - onCartClick, - currentPage, - user, -}: CartPageDesignProps) { - const [activeTab, setActiveTab] = useState<'cards' | 'postcards'>('cards'); - const [cartItems, setCartItems] = useState(initialCartItems); - const [selectedCardId, setSelectedCardId] = useState(null); - const [view, setView] = useState<'cart' | 'checkout'>('cart'); - const [checkoutItem, setCheckoutItem] = useState(null); - - const handleRemoveItem = (id: string) => { - setCartItems(prev => prev.filter(item => item.id !== id)); - if (selectedCardId === id) setSelectedCardId(null); - }; - - const handleSelectCard = (id: string) => { - setSelectedCardId(prev => (prev === id ? null : id)); - }; - - const handleGoToCheckout = () => { - const item = cartItems.find(i => i.id === selectedCardId); - if (item) { - setCheckoutItem({ ...item }); - setView('checkout'); - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - }; - - const handleBackToCart = () => { - setView('cart'); - setCheckoutItem(null); - }; - - const handleCheckoutItemChange = (updates: Partial) => { - if (!checkoutItem) return; - const updated = { ...checkoutItem, ...updates }; - const prices = priceTable[updated.cardType]; - if (prices && prices[updated.days] !== undefined) { - updated.pricePerUnit = prices[updated.days]; - } - setCheckoutItem(updated); - }; - - const isEmpty = cartItems.length === 0; - const selectedItem = cartItems.find(i => i.id === selectedCardId); - const attractions = checkoutItem ? (attractionsData[checkoutItem.city]?.[checkoutItem.cardType] || []) : []; - const offers = checkoutItem ? (offersData[checkoutItem.cardType] || []) : []; - - return ( -
- {}} onSignInClick={onSignInClick} onSignOutClick={onSignOutClick} - onPassesClick={onPassesClick} onCheckoutClick={onCheckoutClick} onHomeClick={onHomeClick} - onAttractionsClick={onAttractionsClick} onBlogsClick={onBlogsClick} onHowItWorksClick={onHowItWorksClick} - onFAQClick={onFAQClick} onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick} - onProfileClick={onProfileClick} onCityCardsClick={onCityCardsClick} onMagicItineraryClick={onMagicItineraryClick} - onPostCardsClick={onPostCardsClick} onOffersClick={onOffersClick} onSuperSavingsClick={onSuperSavingsClick} - onEsimsClick={onEsimsClick} onHotelDiscountsClick={onHotelDiscountsClick} onCartClick={onCartClick} - currentPage={currentPage as any} user={user} - /> - - - {view === 'cart' ? ( - /* ─── CART VIEW ─── */ - - {/* Header */} -
-

- Your{' '} - Cart -

-

- {isEmpty ? 'Your cart is empty' : `${cartItems.length} ${cartItems.length === 1 ? 'item' : 'items'} in your cart`} -

-
- - {/* Tab switcher */} - {/* Cards listed directly below */} - - {/* Content */} - - {activeTab === 'cards' ? ( - - {isEmpty ? ( - } title="No cards in your cart" description="Browse our city passes to unlock amazing experiences and savings on your next adventure" actionLabel="Explore Passes" onAction={onPassesClick} /> - ) : ( -
- {/* Table header (desktop) */} -
-
City Cards
-
Travellers
-
Qty
-
Price
-
-
- - - {cartItems.map((item) => { - const isSelected = selectedCardId === item.id; - const totalPrice = item.pricePerUnit * item.quantity; - - return ( - handleSelectCard(item.id)} - className={`relative bg-white rounded-2xl overflow-hidden cursor-pointer transition-all duration-300 ${ - isSelected ? 'ring-2 ring-[#F95F62] shadow-lg shadow-[#F95F62]/8' : 'ring-1 ring-gray-100 hover:ring-gray-200 hover:shadow-md' - }`} - > - {/* Selected badge */} - - {isSelected && ( - - - - )} - - - {/* Mobile layout */} -
-
- -
-
-
-
-
{item.city}
-
- {item.cardType} - {item.days}d -
-
- -
-
- {item.adults}A · {item.children}C · Qty {item.quantity} -
- ${totalPrice.toFixed(2)} - {item.quantity > 1 && ${item.pricePerUnit.toFixed(2)}/ea} -
-
-
-
- - {/* Desktop layout */} -
-
-
- -
-
-
{item.city}
-
- {item.cardType} Card - {item.days} days -
-
-
-
-
- {item.adults} - {item.children} -
-
-
- {item.quantity} -
-
- ${totalPrice.toFixed(2)} - {item.quantity > 1 && ${item.pricePerUnit.toFixed(2)} per unit} -
-
- -
-
-
- ); - })} -
- - {/* Bottom checkout bar */} - -
- {selectedItem ? ( - <> -

- Selected: {selectedItem.city} {selectedItem.cardType} · {selectedItem.days}d · Qty {selectedItem.quantity} -

-

- ${(selectedItem.pricePerUnit * selectedItem.quantity).toFixed(2)} -

- - ) : ( -

Tap a card above to select it for checkout

- )} -
- - Secure Checkout - -
-
- )} - - ) : ( - - } title="No post cards yet" description="Send beautiful digital post cards to friends and family from your favourite destinations around the world" actionLabel="Browse Post Cards" onAction={onPostCardsClick} /> - - )} - - - ) : ( - /* ─── CHECKOUT VIEW ─── */ - - {checkoutItem && ( - <> - {/* Back */} - - - {/* Stepper */} - {/* */} - - {/* Checkout heading */} -
-

- Checkout{' '} - {checkoutItem.city} -

- -
- -
- {/* Left column */} -
- - {/* ── Card Type Selection (Figma cards) ── */} -
-

- Choose Your Card -

-

- Select the card type that best suits your travel style -

-
- {/* Flexi */} - - - {/* Unlimited */} - -
- - {/* ── Config Card (mobile only) — right after card selection ── */} -
- checkoutItem && onSecureCheckoutClick?.(checkoutItem)} - /> -
- - {/* Features Comparison */} -
-
- {/* Header */} -

Features

-

Flexi

-

Unlimited

- {[ - { feature: 'Access to attractions', flexi: true, unlimited: true }, - { feature: 'Entry to attractions', flexi: true, unlimited: true }, - { feature: 'Access to experiences', flexi: true, unlimited: true }, - { feature: 'Entry to sites', flexi: false, unlimited: true }, - { feature: 'Access to venues', flexi: true, unlimited: true }, - { feature: 'Entry to events', flexi: true, unlimited: true }, - { feature: 'Access to experiences', flexi: false, unlimited: true }, - { feature: 'Access to Itinerary creation', flexi: false, unlimited: true }, - { feature: 'Access to postcard creation', flexi: false, unlimited: true }, - ].map((row, i) => ( - -

- {row.feature} -

-
- {row.flexi ? ( -
- -
- ) : ( - - )} -
-
- {row.unlimited ? ( -
- -
- ) : ( - - )} -
-
- ))} -
-
-
- - {/* ── Offers ── */} -
-

- {checkoutItem.cardType} Card Offers -

-

- Exclusive deals and discounts included with your {checkoutItem.cardType} pass -

-
- {offers.map((offer, idx) => ( -
-
-
- -
-
-

- {offer.title} -

-
-
-

- {offer.description} -

-
-
-
-
- ))} -
-
- - {/* ── Available Attractions ── */} -
-
-

Available Attractions

- {attractions.length} included -
-

- Explore all the experiences you can enjoy with your pass -

-
- {attractions.map((a) => ( -
-
- -
-
- {a.category} -
-
-
{a.name}
-
- -
-
- ))} -
-
-
- - {/* Right column: Config card (desktop only, sticky) */} -
-
- checkoutItem && onSecureCheckoutClick?.(checkoutItem)} - /> -
-
-
- - )} - - )} - - -
-
- ); -} - -/* ─── Empty state ─── */ -function EmptyState({ icon, title, description, actionLabel, onAction }: { - icon: React.ReactNode; title: string; description: string; actionLabel: string; onAction?: () => void; -}) { - return ( - - {icon} -

{title}

-

{description}

- {actionLabel} -
- ); -} \ No newline at end of file diff --git a/src/pages/CheckoutPage.tsx b/src/pages/CheckoutPage.tsx index 235aa7e..3464eca 100644 --- a/src/pages/CheckoutPage.tsx +++ b/src/pages/CheckoutPage.tsx @@ -1,733 +1,514 @@ -import { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { motion, AnimatePresence } from 'motion/react'; -import { ArrowLeft, CreditCard, Users, Calendar, MapPin, Shield, Truck, Clock, ChevronRight, Check, ChevronDown, X, Mail, Smartphone } from 'lucide-react'; -import { Button } from '../components/ui/button'; -import { Input } from '../components/ui/input'; -import { Label } from '../components/ui/label'; -import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card'; -import { Separator } from '../components/ui/separator'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../components/ui/dialog'; -import { RadioGroup, RadioGroupItem } from '../components/ui/radio-group'; -import { Checkbox } from '../components/ui/checkbox'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../components/ui/select'; -import { Badge } from '../components/ui/badge'; -import { Textarea } from '../components/ui/textarea'; +import { + ArrowLeft, Check, Minus, Plus, ChevronDown +} from 'lucide-react'; +import Navbar from '../components/Navbar'; +import { Footer } from '../components/Footer'; import { ImageWithFallback } from '../components/figma/ImageWithFallback'; -import { Layout } from '../Layout'; +import { useNavigate } from 'react-router-dom'; +import { useAddCardToCartMutation, useGetCheckoutPageDataQuery } from '../Redux/services/cards.service'; +import LoadingSpinner from '../components/LoadingSpinner'; +import { toast } from 'sonner'; -interface CheckoutPageProps { - onBackClick?: () => void; - onHomeClick?: () => void; - onMelbourneClick?: () => void; - onPassesClick?: () => void; - onCheckoutClick?: () => void; - onSignInClick?: () => void; - onSignOutClick?: () => void; - onAttractionsClick?: () => void; - onBlogsClick?: () => void; - onHowItWorksClick?: () => void; - onFAQClick?: () => void; - onPrivacyPolicyClick?: () => void; - onAboutUsClick?: () => void; - onProfileClick?: () => void; - onCityCardsClick?: () => void; - onMagicItineraryClick?: () => void; - onPostCardsClick?: () => void; - onOffersClick?: () => void; - onSecureCheckoutClick?: () => void; - onContactUsClick?: () => void; - onEsimsClick?: () => void; - onHotelDiscountsClick?: () => void; - currentPage?: string; - user?: { email: string; name: string } | null; +/* ─── Types ─── */ +export interface CartItem { + id: string; + city: string; + cardType: 'Flexi' | 'Unlimited'; + days: number; + adults: number; + children: number; + quantity: number; + pricePerUnit: number; + image: string; } +interface Attraction { + id: string; + name: string; + image: string; + category: string; + included: boolean; +} -// Mock cart data -const mockCartItems = [ - { - id: '1', - name: 'Paris Unlimited Pass', - type: '7-Day Pass', - price: 79, - originalPrice: 149, - discount: 47, - attractions: 45, - validity: '7 days', - image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?w=400', - features: ['Skip-the-line access', 'Mobile voucher', 'Free cancellation'] - } -]; +/* ─── FIGMA CARD PREVIEWS (Exact Copy) ─── */ +function FlexiCardPreview({ city, adultPrice, childPrice, isSelected, image }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean, image: string; }) { + return ( +
+ {/* Card bg */} +
+ {/* City image */} +
+ +
+ {/* City name - left aligned */} +
+

{city}

+
+ {/* Pricing */} +
+
+ From + ${adultPrice} + /Adult +
+
+ and + ${childPrice} + /Child +
+
+ {/* Description */} +
+

+ Dive into an extensive selection of thrilling destinations! +

+
+ {/* Side tab - Flexi (pink) */} +
+ Card + Flexi +
+ {/* Selected checkmark */} + {isSelected && ( +
+ +
+ )} +
+ ); +} -export function CheckoutPage({ - onBackClick, - onHomeClick, - onMelbourneClick, - onPassesClick, - onCheckoutClick, - onSignInClick, - onSignOutClick, - onAttractionsClick, - onBlogsClick, - onHowItWorksClick, - onFAQClick, - onPrivacyPolicyClick, - onAboutUsClick, - onProfileClick, - onCityCardsClick, - onMagicItineraryClick, - onPostCardsClick, - onOffersClick, - onSecureCheckoutClick, - onContactUsClick, - onEsimsClick, - onHotelDiscountsClick, - currentPage, - user, -}: CheckoutPageProps) { - const [purchaseType, setPurchaseType] = useState<'self' | 'gift'>('self'); - const [selectedPayment, setSelectedPayment] = useState('credit-card'); - const [showEmailVerification, setShowEmailVerification] = useState(false); - const [verificationCode, setVerificationCode] = useState(''); - const [isEmailVerified, setIsEmailVerified] = useState(false); - const [formData, setFormData] = useState({ - email: '', - firstName: '', - lastName: '', - phone: '', - country: '', - address: '', - city: '', - postalCode: '', - cardNumber: '', - expiry: '', - cvv: '', - cardName: '', - agreeTerms: false, - subscribeNewsletter: false - }); - const [giftData, setGiftData] = useState({ - recipientName: '', - recipientPhone: '', - recipientEmail: '', - personalizedMessage: '' - }); +function UnlimitedCardPreview({ city, adultPrice, childPrice, isSelected, image }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean, image: string; }) { + return ( +
+ {/* Card bg */} +
+ {/* City image */} +
+ +
+ {/* City name - left aligned */} +
+

{city}

+
+ {/* Pricing */} +
+
+ From + ${adultPrice} + /Adult +
+
+ and + ${childPrice} + /Child +
+
+ {/* Description */} +
+

+ Dive into an extensive selection of thrilling destinations! +

+
+ {/* Side tab - Unlimited (coral) */} +
+ Card + Unlimited +
+ {/* Selected checkmark */} + {isSelected && ( +
+ +
+ )} +
+ ); +} - const subtotal = mockCartItems.reduce((sum, item) => sum + item.price, 0); - const tax = Math.round(subtotal * 0.1); - const total = subtotal + tax; - const totalSavings = mockCartItems.reduce((sum, item) => sum + (item.originalPrice - item.price), 0); +/* ─── CheckoutConfigCard (Exact Copy) ─── */ +function CheckoutConfigCard({ + item, + onProceed, +}: { + item: any; + onProceed: () => void; +}) { + const [dropdownOpen, setDropdownOpen] = useState(false); + const [noOfAdults, setNoOfAdults] = useState(1) + const [noOfChildren, setNoOfChildren] = useState(0) + const [noOfAttractions, setNoOfAttractions] = useState(item?.minNumber); + const [noOfDays, setNoOfDays] = useState(item?.minNumber) - const handleInputChange = (field: string, value: string | boolean) => { - setFormData(prev => ({ ...prev, [field]: value })); - - // Trigger email verification when email is complete - if (field === 'email' && typeof value === 'string' && value.includes('@') && value.includes('.') && !isEmailVerified) { - setTimeout(() => { - setShowEmailVerification(true); - }, 1000); - } - }; + const cityId = localStorage.getItem("cityId") + const cityName = localStorage.getItem("cityName") + const cardTypeId = item?.cardType?.id + const cardId = item?.id + const cardMode = item?.cardType?.name === "selective_pass" ? "flexi" : "unlimited" + const adultPrice = item?.adultPrice * noOfAdults + const childPrice = item?.childPrice * noOfChildren + const basePrice = adultPrice + childPrice + const taxAmount = basePrice * 0.1 + const strikedPrice = basePrice + 20 - const handleGiftInputChange = (field: string, value: string) => { - setGiftData(prev => ({ ...prev, [field]: value })); - }; + const [addCardToCart] = useAddCardToCartMutation() - const handleEmailVerification = () => { - if (verificationCode === '123456') { - setIsEmailVerified(true); - setShowEmailVerification(false); - } - }; + useEffect(() => { + setNoOfAttractions(item?.minNumber) + setNoOfDays(item?.minNumber) + }, [item]) - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!isEmailVerified) { - setShowEmailVerification(true); - return; - } - const checkoutData = { - purchaseType, - formData, - ...(purchaseType === 'gift' && { giftData }), - selectedPayment, - cartItems: mockCartItems + const numberArray = Array.from( + { length: item?.maxNumber - item?.minNumber + 1 }, + (_, i) => item?.minNumber + i + ); + const navigate = useNavigate(); + + const cardBookingDetails = { + cityXid: cityId, + cardTypeXid: cardTypeId, + cardXid: cardId, + cardMode, // stays as-is + totalAdult: noOfAdults, + baseAmount: basePrice, // static value + taxAmount, + totalChild: noOfChildren, + noOfAttractions, + noOfDays }; - console.log('Processing checkout...', checkoutData); - }; - const paymentMethods = [ - { - id: 'credit-card', - name: 'Credit Card', - icon: , - description: 'Visa, Mastercard, American Express' - }, - { - id: 'paypal', - name: 'PayPal', - icon:
P
, - description: 'Pay with your PayPal account' - }, - { - id: 'google-pay', - name: 'Google Pay', - icon:
G
, - description: 'Pay with Google Pay' + const handleProceedToPayment = async () => { + try { + const response = await addCardToCart(cardBookingDetails); + const bookingId = response?.data?.id + if (bookingId) { + navigate(`/payment/${bookingId}`) + } else { + throw new Error(response?.error?.data?.message) + } + } catch (error:any) { + toast.error(error.message); + } } - ]; - return ( -
- - - {/* Header Section */} -
-
- {/* Back Button */} - - - Back to Cart - - - {/* Page Title */} - -

- Secure{' '} - Checkout -

-

- Complete your purchase and start exploring Paris -

-
-
-
- - {/* Main Checkout Content */} -
-
-
- {/* Left Column - Form Inputs (3/5 width) */} -
- {/* Purchase Type Selection */} - - - - Purchase Type - - - setPurchaseType(value as 'self' | 'gift')} - className="grid grid-cols-1 md:grid-cols-2 gap-4" - > -
- - -
-
- - -
-
-
-
-
- - {/* Gift Recipient Information - Only shown when gift is selected */} - {purchaseType === 'gift' && ( - - - - - - Gift Recipient Details - - - -
- - handleGiftInputChange('recipientName', e.target.value)} - placeholder="Jane Smith" - required={purchaseType === 'gift'} - className="mt-1 font-poppins" - /> -
-
- - handleGiftInputChange('recipientEmail', e.target.value)} - placeholder="recipient@email.com" - required={purchaseType === 'gift'} - className="mt-1 font-poppins" - /> -
-
- - handleGiftInputChange('recipientPhone', e.target.value)} - placeholder="+1 (555) 123-4567" - required={purchaseType === 'gift'} - className="mt-1 font-poppins" - /> -
-
- -