From 193fe3e722322fc13bc28b03139631632205bcb9 Mon Sep 17 00:00:00 2001 From: Hemant Vishwakarma Date: Fri, 20 Mar 2026 14:41:18 +0530 Subject: [PATCH 01/19] remove unused code --- src/components/AttractionDetailsPage.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/AttractionDetailsPage.tsx b/src/components/AttractionDetailsPage.tsx index d92c3b1..4962d84 100644 --- a/src/components/AttractionDetailsPage.tsx +++ b/src/components/AttractionDetailsPage.tsx @@ -1,13 +1,12 @@ -import { useState } from 'react'; +import { ArrowLeft, Calendar, Check, ChevronLeft, ChevronRight, Clock, MapPin, Users, X } from 'lucide-react'; import { motion } from 'motion/react'; -import { ArrowLeft, Clock, Users, Calendar, MapPin, Star, Check, X, ChevronLeft, ChevronRight } from 'lucide-react'; -import { Button } from './ui/button'; -import { Badge } from './ui/badge'; -import { Card, } from './ui/card'; -import { ImageWithFallback } from './figma/ImageWithFallback'; -import { Layout } from '../Layout'; import { useParams } from 'react-router-dom'; +import { Layout } from '../Layout'; import { useGetAttractionDetailsByIdQuery } from '../Redux/services/attractions.service'; +import { ImageWithFallback } from './figma/ImageWithFallback'; +import { Badge } from './ui/badge'; +import { Button } from './ui/button'; +import { Card, } from './ui/card'; interface AttractionDetailsPageProps { onBackClick: () => void; From dbdf974475cdc34d54301f71918e9ea2eda548e4 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Tue, 14 Apr 2026 18:14:13 +0530 Subject: [PATCH 02/19] save the selected city to localstorage --- src/AppRouter.tsx | 6 +- src/components/CitySelectionDialog.tsx | 11 +- src/components/Navbar.tsx | 26 ++-- src/pages/AttractionsPage.tsx | 192 +------------------------ 4 files changed, 33 insertions(+), 202 deletions(-) diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 28f26f5..52bf4f0 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -88,11 +88,11 @@ export function AppRouter({ } /> {/* Home Route */} - - } /> + } /> {/* Passes Route */} {/* Attractions Routes */} - diff --git a/src/components/CitySelectionDialog.tsx b/src/components/CitySelectionDialog.tsx index 3ede930..e4d6503 100644 --- a/src/components/CitySelectionDialog.tsx +++ b/src/components/CitySelectionDialog.tsx @@ -20,6 +20,9 @@ interface CitySelectionDialogProps { onCitySelect?: (cityId: string) => void; // ✅ Updated to pass cityId } +export const slugify = (name: string | null) => + name?.toLowerCase().replace(/\s+/g, '-'); + export function CitySelectionDialog({ isOpen, onClose, @@ -41,13 +44,17 @@ export function CitySelectionDialog({ ); } - const handleCityClick = (city: City) => { console.log('Selected city:', city.cityName); // ✅ Call the onCitySelect callback if provided (passing cityId) if (onCitySelect) { - onCitySelect(String(city.cityName)); + // onCitySelect(String(city.cityName)); + // navigate(`/${city.cityName}/${city.id}`) + + navigate(`/${slugify(city.cityName)}/${city.id}`); + localStorage.setItem("cityId", String(city.id)) + localStorage.setItem("cityName", String(city.cityName)) } else { // ✅ Default behavior: navigate to passes page navigate(`/passes?city=${encodeURIComponent(city.cityName)}`); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index ca6e863..36980ca 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -8,7 +8,7 @@ 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 } from './CitySelectionDialog'; +import { CitySelectionDialog, slugify } from './CitySelectionDialog'; import { useAuth } from '../context/AuthContext'; import { LoginModal } from './LoginModal'; @@ -104,6 +104,8 @@ export default function Navbar({ } }; + const cityId = localStorage.getItem("cityId") + const cityName = localStorage.getItem("cityName") // More flexible navigation configuration const navigationConfig = { @@ -129,20 +131,20 @@ export default function Navbar({ isShared: false }, // Position 4 - Shared item - { - label: 'Your Card', - path: '/passes', - isShared: true, - landingLabel: 'Your Card', - melbourneLabel: 'Your Card' - }, + // { + // label: 'Your Card', + // path: '/passes', + // isShared: true, + // landingLabel: 'Your Card', + // melbourneLabel: 'Your Card' + // }, // Position 5 { label: 'FAQ', path: '/faq', isShared: false }, - { + { label: 'Your Postcard', path: '/postcards', isShared: true, @@ -154,7 +156,7 @@ export default function Navbar({ // Position 1 { label: 'Attractions', - path: '/attractions', + path: `/${slugify(cityName)}/${cityId}/attractions`, isShared: false }, // Position 2 @@ -185,7 +187,7 @@ export default function Navbar({ landingLabel: 'Your Card', melbourneLabel: 'Your Card' }, - { + { label: 'Your Postcard', path: '/postcards', isShared: true, @@ -559,7 +561,7 @@ export default function Navbar({ whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > - + void; onSignOutClick?: () => void; @@ -229,7 +48,10 @@ export function AttractionsPage({ const [selectedCategory, setSelectedCategory] = useState(null); const [selectedPassType, setSelectedPassType] = useState(null); - const cityId = 1 + const { cityId } = useParams() + const cityName = localStorage.getItem("cityName") + + console.log(cityName) const { data: filterData, isLoading } = useGetAttractionFiltersQuery(cityId) const { data: attractions } = useGetCustomerAttractionsQuery({ @@ -308,12 +130,12 @@ export function AttractionsPage({

Discover{' '} - Melbourne's{' '} + {cityName}'s{' '} Best{' '} Attractions

- Skip the lines and explore Melbourne's most iconic destinations with your CityCard pass + Skip the lines and explore {cityName}'s most iconic destinations with your CityCard pass

{/* City Card Promotional Banner */} @@ -423,7 +245,7 @@ export function AttractionsPage({
{/* Header */}
-

Attractions in Melbourne

+

Attractions in {cityName}

{/* Results count */}

Showing {showingFrom}-{showingTo} of {totalItems} Item(s) From 743462d088ab14f73b2c465d9bae2150933874ad Mon Sep 17 00:00:00 2001 From: aryabenade Date: Wed, 15 Apr 2026 11:07:13 +0530 Subject: [PATCH 03/19] make the card comparison section in selected city page dynamic --- src/Redux/services/cities.service.ts | 6 +- src/components/MelbourneCardComparison.tsx | 156 ++++++++++++++------- src/components/Navbar.tsx | 19 ++- src/pages/MelbournePage.tsx | 26 ++-- 4 files changed, 137 insertions(+), 70 deletions(-) diff --git a/src/Redux/services/cities.service.ts b/src/Redux/services/cities.service.ts index e1dfb06..9754016 100644 --- a/src/Redux/services/cities.service.ts +++ b/src/Redux/services/cities.service.ts @@ -23,8 +23,12 @@ export const citiesApi = createApi({ query: (listType) => `/cities/list/all?listType=${listType}`, + }), + + getSelectedCityDetails:builder.query({ + query: (cityId) => `/website/${cityId}`, }) }), }); -export const { useGetCityListWithBannerQuery,useGetUpcomingCitiesQuery } = citiesApi; \ No newline at end of file +export const { useGetCityListWithBannerQuery,useGetUpcomingCitiesQuery,useGetSelectedCityDetailsQuery } = citiesApi; \ No newline at end of file diff --git a/src/components/MelbourneCardComparison.tsx b/src/components/MelbourneCardComparison.tsx index d12ac03..e32bdaa 100644 --- a/src/components/MelbourneCardComparison.tsx +++ b/src/components/MelbourneCardComparison.tsx @@ -3,52 +3,52 @@ import { Check, X, Star, Users, MapPin, Calendar, Clock, Zap, Eye } from 'lucide import { Button } from './ui/button'; import { motion } from 'motion/react'; -const cardOptions = [ - { - id: 'selective', - name: 'Flexi Card', - subtitle: 'Pick 5-10 things to do from a choice of 102 attractions tours and activities', - priceRange: '$89-159', - duration: '3-7 days', - popular: false, - color: 'from-blue-500 to-cyan-500', - features: { - passCategory: 'Selective Card', - accessToAttractions: true, - entryToAttractions: true, - accessToExperiences: true, - entryToSites: true, - accessToVenues: false, - entryToEvents: 'Pass Category', - accessToLocations: 'Pass Category', - entryToActivities: true, - accessToExhibits: true, - entryToActivitiesSecond: true - } - }, - { - id: 'unlimited', - name: 'Melbourne Unlimited Card', - subtitle: 'Pick 5-30 things to do from a choice of 102 attractions tours and activities', - priceRange: '$159-299', - duration: '3-7 days', - popular: true, - color: 'from-purple-500 to-pink-500', - features: { - passCategory: 'Pass Category', - accessToAttractions: true, - entryToAttractions: true, - accessToExperiences: true, - entryToSites: true, - accessToVenues: true, - entryToEvents: 'Pass Category', - accessToLocations: 'Pass Category', - entryToActivities: true, - accessToExhibits: true, - entryToActivitiesSecond: true - } - } -]; +// const cardOptions = [ +// { +// id: 'selective', +// name: 'Flexi Card', +// subtitle: 'Pick 5-10 things to do from a choice of 102 attractions tours and activities', +// priceRange: '$89-159', +// duration: '3-7 days', +// popular: false, +// color: 'from-blue-500 to-cyan-500', +// features: { +// passCategory: 'Selective Card', +// accessToAttractions: true, +// entryToAttractions: true, +// accessToExperiences: true, +// entryToSites: true, +// accessToVenues: false, +// entryToEvents: 'Pass Category', +// accessToLocations: 'Pass Category', +// entryToActivities: true, +// accessToExhibits: true, +// entryToActivitiesSecond: true +// } +// }, +// { +// id: 'unlimited', +// name: 'Melbourne Unlimited Card', +// subtitle: 'Pick 5-30 things to do from a choice of 102 attractions tours and activities', +// priceRange: '$159-299', +// duration: '3-7 days', +// popular: true, +// color: 'from-purple-500 to-pink-500', +// features: { +// passCategory: 'Pass Category', +// accessToAttractions: true, +// entryToAttractions: true, +// accessToExperiences: true, +// entryToSites: true, +// accessToVenues: true, +// entryToEvents: 'Pass Category', +// accessToLocations: 'Pass Category', +// entryToActivities: true, +// accessToExhibits: true, +// entryToActivitiesSecond: true +// } +// } +// ]; const features = [ { key: 'passCategory', label: 'Pass Category', icon: Star }, @@ -71,11 +71,59 @@ const FeatureIcon = ({ feature }: { feature: typeof features[0] }) => { interface MelbourneCardComparisonProps { onCheckoutClick?: () => void; + cards: any[] } -export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardComparisonProps) { +export function MelbourneCardComparison({ onCheckoutClick,cards }: MelbourneCardComparisonProps) { const [selectedCard, setSelectedCard] = useState('unlimited'); + const cardOptions = [ + { + id: cards[0]?.id, + name: cards[0]?.title, + subtitle: cards[0]?.description, + priceRange: `$${cards[0]?.adultPrice}`, + duration: '3-7 days', + popular: false, + color: 'from-blue-500 to-cyan-500', + features: { + passCategory: 'Selective Card', + accessToAttractions: true, + entryToAttractions: true, + accessToExperiences: true, + entryToSites: true, + accessToVenues: false, + entryToEvents: 'Pass Category', + accessToLocations: 'Pass Category', + entryToActivities: true, + accessToExhibits: true, + entryToActivitiesSecond: true + } + }, + { + id: cards[1]?.id, + name: cards[1]?.title, + subtitle: cards[1]?.description, + priceRange: `$${cards[1]?.adultPrice}`, + duration: '3-7 days', + popular: true, + color: 'from-purple-500 to-pink-500', + features: { + passCategory: 'Pass Category', + accessToAttractions: true, + entryToAttractions: true, + accessToExperiences: true, + entryToSites: true, + accessToVenues: true, + entryToEvents: 'Pass Category', + accessToLocations: 'Pass Category', + entryToActivities: true, + accessToExhibits: true, + entryToActivitiesSecond: true + } + } + ]; + const renderFeatureValue = (value: boolean | string, cardId: string) => { if (typeof value === 'boolean') { return value ? ( @@ -92,7 +140,7 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar

); } - + return (
{value} @@ -122,17 +170,17 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar Choose Your Adventure
- +

Buy {' '} Now

- +

- Melbourne is a must-visit cultural epicenter, and this spectacular trip unlocks - your access around the city in one easy. Save over the cost of visiting Melbourne's + Melbourne is a must-visit cultural epicenter, and this spectacular trip unlocks + your access around the city in one easy. Save over the cost of visiting Melbourne's landmarks, have lunch at Phi Phi Leh, snorkel at Bamboo Island, and visit Monkey Beach.

@@ -179,7 +227,7 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar {feature.label}
- + {cardOptions.map((card) => (
{renderFeatureValue(card.features[feature.key as keyof typeof card.features], card.id)} @@ -196,7 +244,7 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
Ready to explore?
Compare features above
- + {cardOptions.map((card) => (
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 36980ca..9190dfc 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -91,6 +91,10 @@ export default function Navbar({ const { user, login, logout } = useAuth(); // from AuthContext + const cityLogo = sessionStorage.getItem("cityLogo") || "" + + const baseUrl = import.meta.env.VITE_BASE_URL; + const protectedPaths = ["/passes", "/whats-included", "/", "/melbourne"]; const handleOpenLoginModal = () => { @@ -563,8 +567,7 @@ export default function Navbar({ >
{/* Enhanced Navigation Items with source tracking */} {navigationItems.map((item) => { @@ -625,12 +628,13 @@ export default function Navbar({ onClick={handleOpenCityDialogFromNavbar} > - {!activeCity || activeCity === 'shared' + {/* {!activeCity || activeCity === 'shared' ? 'City' : ['landing', 'landingpage'].includes(activeCity.toLowerCase()) ? 'City' : activeCity.charAt(0).toUpperCase() + activeCity.slice(1) - } + } */} + {cityName ? cityName : "City"} @@ -786,10 +790,11 @@ export default function Navbar({ trigger={
- {activeCity && activeCity !== 'shared' ? + {/* {activeCity && activeCity !== 'shared' ? activeCity.charAt(0).toUpperCase() + activeCity.slice(1) : currentSource === 'melbourne' ? 'Melbourne' : 'Select City' - } + } */} + {cityName ? cityName : "City"}
diff --git a/src/pages/MelbournePage.tsx b/src/pages/MelbournePage.tsx index a4dbe82..64c8c11 100644 --- a/src/pages/MelbournePage.tsx +++ b/src/pages/MelbournePage.tsx @@ -1,8 +1,7 @@ import { motion, useAnimationControls, AnimatePresence } from 'motion/react'; import { Button } from '../components/ui/button'; import { ArrowRight, Calendar, Thermometer, Eye, MapPin, Clock, Users, Ticket, Wand2, Plane, Sparkles } from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; -import Navbar from '../components/Navbar'; +import { useState } from 'react'; import { ImageWithFallback } from '../components/figma/ImageWithFallback'; import { MelbourneAttractions } from '../components/MelbourneAttractions'; import { MelbourneCardComparison } from '../components/MelbourneCardComparison'; @@ -12,11 +11,10 @@ import { CustomPostcards } from '../components/CustomPostcards'; import { EnhancedTestimonials } from '../components/EnhancedTestimonials'; import { MobileAppPromotion } from '../components/MobileAppPromotion'; import { MelbourneFAQ } from '../components/MelbourneFAQ'; -import { Footer } from '../components/Footer'; -// import { MinimalHeroBanner } from './MinimalHeroBanner'; import { Layout } from '../Layout'; import { HeroBannerCarousel } from '../components/HeroBannerCarousel'; import { HotelEsimOffers } from '../components/HotelEsimOffers'; +import { useGetSelectedCityDetailsQuery } from '../Redux/services/cities.service'; interface User { email: string; @@ -149,6 +147,18 @@ export function MelbournePage({ const [currentCardIndex, setCurrentCardIndex] = useState(0); const [isAnimating, setIsAnimating] = useState(false); + const cityId = localStorage.getItem("cityId") + + const { data: cityDetails, isLoading: loadingCityDetails } = useGetSelectedCityDetailsQuery(cityId) + + + if (loadingCityDetails) { + return
Loading...
+ } + + const cards = cityDetails?.city?.cards + sessionStorage.setItem("cityLogo", String(cityDetails?.city?.cityIconPath)) + const currentCard = itineraryCards[currentCardIndex]; const nextCard = itineraryCards[(currentCardIndex + 1) % itineraryCards.length]; const thirdCard = itineraryCards[(currentCardIndex + 2) % itineraryCards.length]; @@ -257,12 +267,12 @@ export function MelbournePage({ {/* Attractions Section */}
- +
{/* Pass Comparison */}
- +
{/* Tour Overview */} @@ -740,8 +750,8 @@ export function MelbournePage({ }, 400); }} className={`font-poppins group relative transition-all duration-300 px-4 py-2 rounded-full font-medium ${idx === currentCardIndex - ? 'bg-gradient-to-r from-primary to-orange-500 text-white shadow-lg scale-110' - : 'bg-white/80 backdrop-blur-sm text-gray-600 hover:text-primary hover:bg-white border border-gray-200 hover:border-primary/30 hover:scale-105' + ? 'bg-gradient-to-r from-primary to-orange-500 text-white shadow-lg scale-110' + : 'bg-white/80 backdrop-blur-sm text-gray-600 hover:text-primary hover:bg-white border border-gray-200 hover:border-primary/30 hover:scale-105' }`} whileHover={{ y: -2 }} whileTap={{ scale: 0.95 }} From 929359d7e214b74e4884575450a9dc32cef1446a Mon Sep 17 00:00:00 2001 From: aryabenade Date: Wed, 15 Apr 2026 11:34:44 +0530 Subject: [PATCH 04/19] make loading spinner component and pass it to all loading states --- src/components/CitySelectionDialog.tsx | 8 ++------ src/components/LandingUpcomingCities.tsx | 8 ++------ src/components/LoadingSpinner.tsx | 12 ++++++++++++ src/components/MelbourneCardComparison.tsx | 2 +- src/pages/AttractionDetailsPage.tsx | 8 ++------ src/pages/AttractionsPage.tsx | 8 ++------ src/pages/MelbournePage.tsx | 9 ++++++--- src/pages/ProfilePage.tsx | 8 ++------ 8 files changed, 29 insertions(+), 34 deletions(-) create mode 100644 src/components/LoadingSpinner.tsx diff --git a/src/components/CitySelectionDialog.tsx b/src/components/CitySelectionDialog.tsx index e4d6503..e087e4e 100644 --- a/src/components/CitySelectionDialog.tsx +++ b/src/components/CitySelectionDialog.tsx @@ -7,6 +7,7 @@ import { Input } from './ui/input'; import { motion, AnimatePresence } from 'motion/react'; import { ImageWithFallback } from './figma/ImageWithFallback'; import { useGetCityListWithBannerQuery } from '../Redux/services/cities.service'; +import LoadingSpinner from './LoadingSpinner'; interface City { id: number; @@ -35,12 +36,7 @@ export function CitySelectionDialog({ if (isLoading) { return ( -
-
-
-

Loading...

-
-
+ ); } diff --git a/src/components/LandingUpcomingCities.tsx b/src/components/LandingUpcomingCities.tsx index cd15393..c5c70a1 100644 --- a/src/components/LandingUpcomingCities.tsx +++ b/src/components/LandingUpcomingCities.tsx @@ -4,6 +4,7 @@ import { Button } from './ui/button'; import { useRef, useState, useEffect } from 'react'; import Image592Traced from '../imports/Image592Traced-5025-559'; import { useGetUpcomingCitiesQuery } from '../Redux/services/cities.service'; +import LoadingSpinner from './LoadingSpinner'; // const upcomingCities = [ // { @@ -115,12 +116,7 @@ export function LandingUpcomingCities() { if (isLoading) { return ( -
-
-
-

Loading...

-
-
+ ); } diff --git a/src/components/LoadingSpinner.tsx b/src/components/LoadingSpinner.tsx new file mode 100644 index 0000000..bde2706 --- /dev/null +++ b/src/components/LoadingSpinner.tsx @@ -0,0 +1,12 @@ +const LoadingSpinner = () => { + return ( +
+
+
+

Loading...

+
+
+ ) +} + +export default LoadingSpinner \ No newline at end of file diff --git a/src/components/MelbourneCardComparison.tsx b/src/components/MelbourneCardComparison.tsx index e32bdaa..5258954 100644 --- a/src/components/MelbourneCardComparison.tsx +++ b/src/components/MelbourneCardComparison.tsx @@ -253,7 +253,7 @@ export function MelbourneCardComparison({ onCheckoutClick,cards }: MelbourneCard
- -
+ Start Saving Now + +
+ - {/* Decorative elements */} -
-
- + {/* Decorative elements */} +
+
+ - {/* Trusted By Companies Section */} -
-
-
-
-

- Trusted by the - world's best -

-

- Join thousands of savvy travelers enjoying massive savings on premium experiences -

-
- -
-
-
- - {/* Featured Super Savings Section */} -
-
- -

- Featured{' '} - - Super Savings - + {/* Trusted By Companies Section */} +
+
+
+
+

+ Trusted by the + world's best

-

- Check out our biggest discounts and start saving on premium experiences +

+ Join thousands of savvy travelers enjoying massive savings on premium experiences

- +
+ +
+
+
-
-
- {/* Left Sidebar - Filters */} -
- -
- {/* Search by header */} -
-
-

Search by

-
+ {/* Featured Super Savings Section */} +
+
+ +

+ Featured{' '} + + Super Savings + +

+

+ Check out our biggest discounts and start saving on premium experiences +

+
- {/* Filter categories */} -
- {filterCategories.map(category => ( -
- toggleCategory(category.value)} - className="border-gray-400" - /> - -
- ))} -
+
+
+ {/* Left Sidebar - Filters */} +
+ +
+ {/* Search by header */} +
+
+

Search by

- -
- {/* Main Content */} -
- {/* Breadcrumb */} -
-

- {fromSource === 'passes' ? ( - <> - My Profile{'>'}My passes{'>'} - Super Savings - - ) : ( - <> - Our Products{'>'} - Super Savings - - )} -

-
- - {/* Header Section */} -
-

- Super Savings -

-

- Exclusive discounts up to 65% off on premium experiences -

-
- - {/* Savings Grid */} -
- {displayedSavings.map((saving, index) => ( - - - {/* Image */} -
- - - - {/* Discount Badge */} -
- {saving.discount} -
-
- - - {/* Business Name */} -
-
- {saving.business} -
- - {/* Title */} -

- {saving.title} -

- - {/* Saved Amount Display */} -
-
- - - {saving.savedAmount} - -
-
-
-
-
- ))} -
- - {/* Minimal Pagination */} -
-
- - -
- {[1, 2, 3].map((page) => ( - - ))} -
- - + {category.categoryName} ({category.offerCount}) + +
+ ))}
+
+
+ + {/* Main Content */} +
+ {/* Breadcrumb */} +
+

+ {fromSource === 'passes' ? ( + <> + My Profile{'>'}My passes{'>'} + Super Savings + + ) : ( + <> + Our Products{'>'} + Super Savings + + )} +

+
+ + {/* Header Section */} +
+

+ Super Savings +

+

+ Exclusive discounts up to 65% off on premium experiences +

+
+ + {/* Offers Grid */} +
+ {offers.map((offer: any, index: number) => ( + + + {/* Image */} +
+ + {/* */} + + {/* Discount Badge */} +
+ {offer.offerCode} +
+
+ + + {/* Business Name */} +
+
+ {offer.partnerName} +
+ + {/* Title */} +

+ {offer.description} +

+ + {/* Saved Amount Display */} +
+
+ {/* */} + + {offer.title} + +
+
+
+
+
+ ))} +
+ + {/* Minimal Pagination */} +
+
+ {/* Previous button */} + + + {/* Page numbers */} +
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map(p => ( + + ))} +
+ + {/* Next button */} + +
- -
- -
-
- {/* How It Works Section */} -
- {/* Background decorative elements */} -
-
- -
- +
-
+
+

- {/* Categories Section */} -
- {/* Abstract Travel Patterns */} -
+ {/* How It Works Section */} +
+ {/* Background decorative elements */} +
+
-
- {/* Section Header */} -
- -
-
- Explore Collections -
-

- Curated for the Modern Traveler -

-

- Discover exclusive savings across our most sought-after travel categories. -

-
- - - - +
+ +
+
+ Simple Process +
+

+ Start Saving in Minutes +

+

+ Your journey to smarter travel and bigger savings begins with three simple steps. +

+
- {/* Bento Grid Layout */} -
- {categoriesData.map((category, index) => ( +
+ {/* Connecting line for desktop */} +
+ +
+ {[ + { + step: '01', + title: 'Unlock Access', + description: 'Get your CityCards pass to instantly activate membership perks.', + icon: MapPinned + }, + { + step: '02', + title: 'Discover Deals', + description: 'Browse exclusive offers on hotels, flights, and experiences.', + icon: Search + }, + { + step: '03', + title: 'Enjoy Savings', + description: 'Redeem discounts instantly and watch your travel budget grow.', + icon: Percent + } + ].map((item, index) => ( -
+ {/* Icon Container */} +
+
+
+ +
+
+ {index + 1} +
+
+ +

+ {item.title} +

+

+ {item.description} +

+
+
+ ))} +
+
+ + +
+
+ + {/* Categories Section */} +
+ {/* Abstract Travel Patterns */} +
+ +
+ {/* Section Header */} +
+ +
+
+ Explore Collections +
+

+ Curated for the Modern Traveler +

+

+ Discover exclusive savings across our most sought-after travel categories. +

+
+ + + + +
+ + {/* Bento Grid Layout */} +
+ {categoriesData.map((category, index) => ( + +
- {/* Background Gradient Hover */} -
+ > + {/* Background Gradient Hover */} +
- {/* Large Watermark Icon for visual depth */} - + {/* Large Watermark Icon for visual depth */} + -
-
-
+
+
- -
- - {category.savings} - +
- -

- {category.title} -

-

- {category.description} -

+ + {category.savings} +
-
- Explore Deals - -
+

+ {category.title} +

+

+ {category.description} +

- - ))} -
- {/* Mobile View All Button */} -
- -
+
+ Explore Deals + +
+
+
+ ))}
-
- {/* Access Your CityCards Section */} -
- -
+ {/* Mobile View All Button */} +
+ +
+ +
+ + {/* Access Your CityCards Section */} +
+ +
+ + + + ); - - - ); - } } From ce3c095727b588b728fc7f1038deb51346c95343 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Fri, 17 Apr 2026 14:45:03 +0530 Subject: [PATCH 07/19] show cards on the passes page coming from backend --- src/AppRouter.tsx | 4 +- src/components/Navbar.tsx | 12 +- src/pages/CheckoutPage.tsx | 2 - src/pages/PassesPage.tsx | 288 ++++++++++++++++++++++--------------- 4 files changed, 184 insertions(+), 122 deletions(-) diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index b3e28a0..12945be 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -106,7 +106,7 @@ export function AppRouter({ } /> {/* Attractions Routes */} - @@ -265,7 +265,7 @@ export function AppRouter({ } /> - diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 5568776..7e421a3 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -162,25 +162,25 @@ export default function Navbar({ // Position 1 { label: 'Attractions', - path: `/${slugify(cityName)}/attractions`, + path: `/attractions`, isShared: false }, // Position 2 { label: 'Magic Itinerary', - path: `/${slugify(cityName)}/magic-itinerary`, + path: `/magic-itinerary`, isShared: false }, // Position 3 { label: 'Super Savings', - path: `/${slugify(cityName)}/super-savings`, + path: `/super-savings`, isShared: false }, // Position 4 - Shared item { label: 'How It Works', - path: `/${slugify(cityName)}/how-it-works`, + path: `/how-it-works`, isShared: true, landingLabel: 'Discover', melbourneLabel: 'How It Works' @@ -188,14 +188,14 @@ export default function Navbar({ // Position 5 - Shared item { label: 'Your Card', - path: `/${slugify(cityName)}/passes`, + path: `/passes`, isShared: true, landingLabel: 'Your Card', melbourneLabel: 'Your Card' }, { label: 'Your Postcard', - path: `/${slugify(cityName)}/postcards`, + path: `/postcards`, isShared: true, landingLabel: 'Your Postcard', melbourneLabel: 'Your Postcard' diff --git a/src/pages/CheckoutPage.tsx b/src/pages/CheckoutPage.tsx index f34b555..235aa7e 100644 --- a/src/pages/CheckoutPage.tsx +++ b/src/pages/CheckoutPage.tsx @@ -12,8 +12,6 @@ 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 Navbar from './Navbar'; -import { Footer } from './Footer'; import { ImageWithFallback } from '../components/figma/ImageWithFallback'; import { Layout } from '../Layout'; diff --git a/src/pages/PassesPage.tsx b/src/pages/PassesPage.tsx index 154ac1e..171ca2e 100644 --- a/src/pages/PassesPage.tsx +++ b/src/pages/PassesPage.tsx @@ -11,6 +11,8 @@ import { LoginModal } from '../components/LoginModal'; import { ImageWithFallback } from '../components/figma/ImageWithFallback'; import { useAuth } from '../context/AuthContext'; import { useNavigate } from 'react-router-dom'; +import { useGetSelectedCityDetailsQuery } from '../Redux/services/cities.service'; +import LoadingSpinner from '../components/LoadingSpinner'; interface PassesPageProps { onCheckoutClick?: () => void; @@ -148,21 +150,29 @@ export function PassesPage({ onSignInClick, onSignOutClick, }: PassesPageProps) { - const [selectedPass, setSelectedPass] = useState('unlimited'); + const [selectedPass, setSelectedPass] = useState(passTypes[1].id); const [isLoginOpen, setIsLoginOpen] = useState(false); const { user } = useAuth(); // from AuthContext - const navigate= useNavigate() + const navigate = useNavigate() + const cityId = localStorage.getItem("cityId") + + const { data: cityDetails, isLoading: loadingCityDetails } = useGetSelectedCityDetailsQuery(cityId) + const cards = cityDetails?.city?.cards ?? [] + console.log(cards) + + if (loadingCityDetails) { + return () + } - - const handleCheckoutClick = () => { + const handleCheckoutClick = () => { console.log('Proceeding to checkout for user:', user); // Add your checkout logic here navigate('/checkout'); }; - const handleSignInClick = () => { + const handleSignInClick = () => { setIsLoginOpen(true); }; @@ -189,7 +199,7 @@ export function PassesPage({

- Buy Passes + Buy Cards

Skip the lines, save money, and explore more with our flexible city cards designed for modern travelers @@ -200,130 +210,184 @@ export function PassesPage({ {/* Pass Comparison Section */}

- {passTypes.map((pass) => ( -
- + {/* Flexi Pass Card */} +
+ setSelectedPass(passTypes[0].id)} + > +
+ {/* */} +
- {/* Popular Badge */} - {pass.popular && ( -
- - Most Popular - -
- )} + + + {cards[0].title} + + + {cards[0].description} + + - {/* Radio Button */} -
- + {/* Pricing */} +
+
+ + ${cards[0].adultPrice} + + + / {passTypes[0].period} +
- - {/* Header - Fixed Height */} - - - {pass.title} - - - {pass.description} - - - - {/* Attraction Images Grid */} -
-
-
- +
+ {cards[0].adultPrice && ( +
+ {/* Strikethrough price = originalPrice + $5 */} + + ${parseFloat(cards[0].adultPrice) + 5} + + + Save{" "} + {Math.round( + ((5) / (parseFloat(cards[0].adultPrice) + 5)) * 100 + )} + % +
-
- -
-
- -
-
- -
-
+ )}
+
- {/* Pricing Section - Fixed Height */} -
-
- {pass.price} - / {pass.period} -
-
- {pass.originalPrice && ( -
- {pass.originalPrice} - Save {Math.round(((parseFloat(pass.originalPrice.slice(1)) - parseFloat(pass.price.slice(1))) / parseFloat(pass.originalPrice.slice(1))) * 100)}% + + +
+
+ {passTypes[0].features.map((feature, index) => ( +
+ + {feature}
- )} + ))}
- {/* Content - Flexible Height with Fixed Features Area */} - - {/* Features List - Fixed height */} -
-
- {pass.features.slice(0, 6).map((feature, index) => ( -
- - {feature} -
- ))} +
+ +

+ ✓ Free cancellation up to 24 hours • Instant delivery +

+
+ + +
+ + {/* Unlimited Pass Card */} +
+ setSelectedPass(passTypes[1].id)} + + > + {passTypes[1].popular && ( +
+ + Most Popular + +
+ )} + +
+ {/* */} +
+ + + + {cards[1].title} + + + {cards[1].description} + + + + {/* Pricing */} +
+
+ ${cards[1].adultPrice} + / {passTypes[1].period} +
+
+ {cards[1].adultPrice && ( +
+ {/* Strikethrough price = originalPrice + $5 */} + + ${parseFloat(cards[1].adultPrice) + 5} + + + Save{" "} + {Math.round( + ((5) / (parseFloat(cards[1].adultPrice) + 5)) * 100 + )} + % +
-
+ )} +
+
- {/* CTA Button - Pushed to bottom */} -
- - -

- ✓ Free cancellation up to 24 hours • Instant delivery -

+ +
+
+ {passTypes[1].features.map((feature, index) => ( +
+ + {feature} +
+ ))}
- - -
- ))} +
+ +
+ +

+ ✓ Free cancellation up to 24 hours • Instant delivery +

+
+ + +
+ {/* Good to Know Section */}
From cdadee5df40960af2263c2e5e6f4a8f234b7d5b0 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Fri, 17 Apr 2026 15:56:35 +0530 Subject: [PATCH 08/19] remove calendar from attraction details page --- src/pages/AttractionDetailsPage.tsx | 126 +--------------------------- 1 file changed, 1 insertion(+), 125 deletions(-) diff --git a/src/pages/AttractionDetailsPage.tsx b/src/pages/AttractionDetailsPage.tsx index 53770f6..4c9f90c 100644 --- a/src/pages/AttractionDetailsPage.tsx +++ b/src/pages/AttractionDetailsPage.tsx @@ -258,131 +258,7 @@ export function AttractionDetailsPage({
{/* Right Sidebar - Calendar and Booking */} -
- {/* Calendar Widget with Custom Design */} - -
-

Select Date

-

Choose your preferred visit date

-
- - {/* Custom Calendar Design */} -
- {/* Calendar Header */} -
- - September 2025 - -
- - {/* Days of week */} -
-
Su
-
Mo
-
Tu
-
We
-
Th
-
Fr
-
Sa
-
- - {/* Calendar Grid */} -
- {/* Previous month */} - - - {/* Current month */} - {Array.from({ length: 30 }, (_, i) => { - const day = i + 1; - const isSelected = day === 27; - const isToday = day === 15; - return ( - - ); - })} - - {/* Next month */} - {Array.from({ length: 4 }, (_, i) => ( - - ))} -
-
- - {/* Selected Date Display */} -
-
- -
-

Selected Date

-

September 27, 2025

-
-
-
-
- - {/* Pricing Card */} - -
-
- Adult Ticket - {attraction.ticketPriceAdult} -
-
- Service Fee - $5 -
-
-
- Total - ${attraction.ticketPriceAdult + 5} -
-
-
-
- - {/* Confirm Booking Button */} - - - {/* Trust Indicators */} -
-
- - Instant Confirmation -
-
- - Free Cancellation -
-
-
+
From 27d73fa90c371e74f2eb88757f3ef78e65434b0c Mon Sep 17 00:00:00 2001 From: Hemant Vishwakarma Date: Fri, 17 Apr 2026 19:13:43 +0530 Subject: [PATCH 09/19] super savings details page added --- src/AppRouter.tsx | 8 + src/Redux/services/cities.service.ts | 8 +- src/pages/AttractionDetailsPage.tsx | 13 +- src/pages/SuperSavingsDetailsPage.tsx | 387 ++++++++++++++++++++++++++ src/pages/SuperSavingsPage.tsx | 5 +- 5 files changed, 407 insertions(+), 14 deletions(-) create mode 100644 src/pages/SuperSavingsDetailsPage.tsx diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 12945be..0683ff4 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -32,6 +32,7 @@ import { SuperSavingsPage } from './pages/SuperSavingsPage'; import { WhatsIncluded } from './pages/WhatsIncluded'; import { LandingMagicItineraryPage } from './pages/LandingMagicItineraryPage'; import { DiscoverPage } from './pages/DiscoverPage'; +import { SuperSavingsDetailsPage } from './pages/SuperSavingsDetailsPage'; // User type definition interface User { @@ -270,6 +271,13 @@ export function AppRouter({ } /> + + + navigate(-1)} /> + + } /> diff --git a/src/Redux/services/cities.service.ts b/src/Redux/services/cities.service.ts index acc5b31..c064904 100644 --- a/src/Redux/services/cities.service.ts +++ b/src/Redux/services/cities.service.ts @@ -41,7 +41,13 @@ export const citiesApi = createApi({ return `/website/super-savings/list/offers?${params.toString()}`; } }), + + getOfferDetailsById: builder.query({ + query: (id: number) => `/website/super-savings/list/offers/${id}`, + }), + + }), }); -export const { useGetCityListWithBannerQuery, useGetUpcomingCitiesQuery, useGetSelectedCityDetailsQuery, useGetSelectedCityOffersQuery } = citiesApi; \ No newline at end of file +export const { useGetCityListWithBannerQuery, useGetUpcomingCitiesQuery, useGetSelectedCityDetailsQuery, useGetSelectedCityOffersQuery, useGetOfferDetailsByIdQuery } = citiesApi; \ No newline at end of file diff --git a/src/pages/AttractionDetailsPage.tsx b/src/pages/AttractionDetailsPage.tsx index 64223b9..4c9f90c 100644 --- a/src/pages/AttractionDetailsPage.tsx +++ b/src/pages/AttractionDetailsPage.tsx @@ -1,25 +1,14 @@ -import { ArrowLeft, Calendar, Check, ChevronLeft, ChevronRight, Clock, MapPin, Users, X } from 'lucide-react'; +import { useState } from 'react'; import { motion } from 'motion/react'; -<<<<<<< HEAD:src/components/AttractionDetailsPage.tsx -======= import { ArrowLeft, Clock, Users, Calendar, MapPin, Star, Check, X, ChevronLeft, ChevronRight } from 'lucide-react'; import { Button } from '../components/ui/button'; import { Badge } from '../components/ui/badge'; import { Card, } from '../components/ui/card'; import { ImageWithFallback } from '../components/figma/ImageWithFallback'; import { Layout } from '../Layout'; ->>>>>>> cdadee5df40960af2263c2e5e6f4a8f234b7d5b0:src/pages/AttractionDetailsPage.tsx import { useParams } from 'react-router-dom'; -import { Layout } from '../Layout'; import { useGetAttractionDetailsByIdQuery } from '../Redux/services/attractions.service'; -<<<<<<< HEAD:src/components/AttractionDetailsPage.tsx -import { ImageWithFallback } from './figma/ImageWithFallback'; -import { Badge } from './ui/badge'; -import { Button } from './ui/button'; -import { Card, } from './ui/card'; -======= import LoadingSpinner from '../components/LoadingSpinner'; ->>>>>>> cdadee5df40960af2263c2e5e6f4a8f234b7d5b0:src/pages/AttractionDetailsPage.tsx interface AttractionDetailsPageProps { onBackClick: () => void; diff --git a/src/pages/SuperSavingsDetailsPage.tsx b/src/pages/SuperSavingsDetailsPage.tsx new file mode 100644 index 0000000..23c3988 --- /dev/null +++ b/src/pages/SuperSavingsDetailsPage.tsx @@ -0,0 +1,387 @@ +import { ArrowLeft, Check, Clock, MapPin, Users, X } from 'lucide-react'; +import { motion } from 'motion/react'; +import { useParams } from 'react-router-dom'; +import { ImageWithFallback } from '../components/figma/ImageWithFallback'; +import LoadingSpinner from '../components/LoadingSpinner'; +import { Badge } from '../components/ui/badge'; +import { Button } from '../components/ui/button'; +import { Card } from '../components/ui/card'; +import { Layout } from '../Layout'; +import { useGetOfferDetailsByIdQuery } from '../Redux/services/cities.service'; + +interface SuperSavingsDetailsPageProps { + onBackClick: () => void; + onCheckoutClick: () => void; + onSignInClick: () => void; + onSignOutClick?: () => void; + user?: { email: string; name: string } | null; +} + +export function SuperSavingsDetailsPage({ + onBackClick, + onCheckoutClick, + onSignInClick, + onSignOutClick, + user, +}: SuperSavingsDetailsPageProps) { + const { id } = useParams(); + const { data: offer, isLoading } = useGetOfferDetailsByIdQuery(Number(id)); + const baseUrl = import.meta.env.VITE_BASE_URL; + + + if (isLoading) { + return ; + } + + // Guard against missing data – but keep all UI elements + const safeOffer = offer || { + id: 0, + title: 'Offer Details', + description: 'No description available.', + cityXid: 0, + cardXid: 0, + cardTypeXid: 0, + categoryXid: 0, + partnerName: '', + offerCode: '', + websiteBannerImage: '', + mobileBannerImage: '', + redemptionLink: '', + passType: '', + startDateTime: null, + endDateTime: null, + applyToPasses: false, + stepsForBooking: null, + offerStatus: '', + isActive: true, + createdAt: '', + updatedAt: '', + city: { id: 0, cityName: 'Unknown City' }, + card: { id: 0, title: 'Unknown Card' }, + cardType: { id: 0, cardTypeDisplayName: 'Standard' }, + category: { id: 0, categoryName: 'General' }, + }; + + // Build badges from available API data (preserves the badge UI section) + const superSavingsBadges = [ + safeOffer.category && { badgeXid: safeOffer.category.id, badge: { badgeName: safeOffer.category.categoryName } }, + safeOffer.cardType && { badgeXid: safeOffer.cardType.id, badge: { badgeName: safeOffer.cardType.cardTypeDisplayName } }, + safeOffer.offerCode && { badgeXid: -1, badge: { badgeName: `Code: ${safeOffer.offerCode}` } }, + safeOffer.offerStatus && { badgeXid: -2, badge: { badgeName: safeOffer.offerStatus.toUpperCase() } }, + ].filter(Boolean); + + // Build gallery array from banner images (original expected superSavingsGalleries) + const superSavingsGalleries = []; + if (safeOffer.websiteBannerImage) { + superSavingsGalleries.push({ id: 1, filePathUrl: safeOffer.websiteBannerImage }); + } + if (safeOffer.mobileBannerImage) { + superSavingsGalleries.push({ id: 2, filePathUrl: safeOffer.mobileBannerImage }); + } + // If no images, add a placeholder + if (superSavingsGalleries.length === 0) { + superSavingsGalleries.push({ id: 0, filePathUrl: 'https://placehold.co/1200x800?text=No+Image' }); + } + + // Mock data for sections not present in API (preserve structure but show empty/fallback) + const durations = safeOffer.startDateTime && safeOffer.endDateTime + ? Math.round((new Date(safeOffer.endDateTime).getTime() - new Date(safeOffer.startDateTime).getTime()) / (1000 * 60)) + : 'Not specified'; + const groupSize = 'Not specified'; + const ageRange = 'All ages'; + const superSavingsLanguages: any[] = []; // API has no language data + const superSavingsHighlights: any[] = []; // API has no highlights + // Inclusions: API has none, so show empty state (or we could derive from redemptionLink etc.) + const superSavingsInclusions: any[] = []; + const address = safeOffer.city?.cityName || 'Location not specified'; + + return ( + +
+ {/* Back Button */} + + + + + {/* Title and Badges Section */} +
+
+ {superSavingsBadges.map((badge: any, index: number) => ( + + {badge.badge.badgeName} + + ))} +
+ +

+ + {safeOffer.title} + {' '} + + Day Trip by {safeOffer.partnerName || safeOffer.card?.title || 'Partner'} + +

+
+ + {/* Image Gallery Section - preserved exactly as original */} +
+ {/* Main large image */} +
+ +
+ + {/* Gallery images - use remaining images or repeat first if needed */} + {superSavingsGalleries.slice(1, 5).map((image: any) => ( +
+ +
+ ))} + {/* If less than 4 extra images, fill with placeholders to maintain grid */} + {superSavingsGalleries.slice(1, 5).length < 4 && + Array(4 - superSavingsGalleries.slice(1, 5).length) + .fill(null) + .map((_, idx) => ( +
+
+ No Image +
+
+ ))} +
+ + {/* Main Content Grid */} +
+ {/* Left Content - Tour Details */} +
+ {/* Overview Cards - preserved */} +
+ {/* Duration */} + +
+ +
+

Duration

+

+ {typeof durations === 'number' ? `${durations} mins` : durations} +

+
+ + {/* Group Size */} + +
+ +
+

Group Size

+

{groupSize}

+
+ + {/* Age Range */} + +
+ +
+

Age Range

+

{ageRange}

+
+ + {/* Languages */} + +
+ +
+

Languages

+

+ {superSavingsLanguages?.length > 0 + ? superSavingsLanguages?.map((lang: any) => lang.language.name).join(', ') + : 'English (default)'} +

+
+
+ + {/* Tour Overview */} +
+
+
+

+ Tour Overview +

+
+

+ {safeOffer.description} +

+
+ + {/* Tour Highlights - preserved even if empty */} +
+
+
+

+ Tour Highlights +

+
+ {superSavingsHighlights.length > 0 ? ( +
    + {superSavingsHighlights.map((highlight: any) => ( +
  • +
    +
    +
    + {highlight.title} +
  • + ))} +
+ ) : ( +

No highlights listed for this offer.

+ )} +
+ + {/* What's Included/Not Included - preserved */} +
+
+
+

+ What's included +

+
+
+ {/* Included */} +
+

+ + Included +

+ {superSavingsInclusions.filter((inc: any) => inc.isInclusion === true).length > 0 ? ( + superSavingsInclusions + .filter((inclusion: any) => inclusion.isInclusion === true) + .map((inclusion: any) => ( +
+
+ +
+ {inclusion.title} +
+ )) + ) : ( +

No included items specified.

+ )} +
+ + {/* Not Included */} +
+

+ + Not Included +

+ {superSavingsInclusions.filter((inc: any) => inc.isInclusion === false).length > 0 ? ( + superSavingsInclusions + .filter((inclusion: any) => inclusion.isInclusion === false) + .map((inclusion: any) => ( +
+
+ +
+ {inclusion.title} +
+ )) + ) : ( +

No excluded items specified.

+ )} +
+
+
+ + {/* Location on map - preserved */} +
+
+
+

+ Location on map +

+
+
+
+
+ +
+

Interactive Map

+

{safeOffer.title}

+

{address}

+
+
+
+
+ + {/* Right Sidebar - Calendar and Booking (preserved, but you can add a real calendar if needed) */} +
+ +

Book This Offer

+
+
+ Availability + + {safeOffer.offerStatus === 'active' ? 'Available' : 'Unavailable'} + +
+ {safeOffer.startDateTime && ( +
+ Valid from + + {new Date(safeOffer.startDateTime).toLocaleDateString()} + +
+ )} + {safeOffer.endDateTime && ( +
+ Valid until + + {new Date(safeOffer.endDateTime).toLocaleDateString()} + +
+ )} +
+ +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/SuperSavingsPage.tsx b/src/pages/SuperSavingsPage.tsx index 8b9e680..40be9f3 100644 --- a/src/pages/SuperSavingsPage.tsx +++ b/src/pages/SuperSavingsPage.tsx @@ -17,6 +17,7 @@ import { TrustedCompanies } from '../components/TrustedCompanies'; import { Layout } from '../Layout'; import { useGetSelectedCityOffersQuery } from '../Redux/services/cities.service'; import LoadingSpinner from '../components/LoadingSpinner'; +import { useNavigate } from 'react-router-dom'; interface SuperSavingsPageProps { onBackClick: () => void; @@ -113,6 +114,7 @@ export function SuperSavingsPage({ user }: SuperSavingsPageProps) { + const navigate = useNavigate(); const [categoryId, setCategoryId] = useState(null) const [page, setPage] = useState(1) const [limit, setLimit] = useState(4) @@ -302,7 +304,8 @@ export function SuperSavingsPage({ animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: index * 0.1 }} > - + navigate(`/super-savings/${offer.id}`)}> {/* Image */}
Date: Sun, 19 Apr 2026 11:46:13 +0530 Subject: [PATCH 10/19] import cart page and payment page code from figma --- src/AppRouter.tsx | 13 + src/components/Navbar.tsx | 7 +- src/pages/CartPage.tsx | 877 +++++++++++++++++++++++++++++++ src/pages/PaymentDetailsPage.tsx | 494 +++++++++++++++++ 4 files changed, 1388 insertions(+), 3 deletions(-) create mode 100644 src/pages/CartPage.tsx create mode 100644 src/pages/PaymentDetailsPage.tsx diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 12945be..aa6c388 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -32,6 +32,8 @@ import { SuperSavingsPage } from './pages/SuperSavingsPage'; import { WhatsIncluded } from './pages/WhatsIncluded'; import { LandingMagicItineraryPage } from './pages/LandingMagicItineraryPage'; import { DiscoverPage } from './pages/DiscoverPage'; +import { CartPage } from './pages/CartPage'; +import { PaymentDetailsPage } from './pages/PaymentDetailsPage'; // User type definition interface User { @@ -270,6 +272,17 @@ export function AppRouter({ } /> + + + + + } /> + + + + } /> diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 7e421a3..e1444e5 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -656,7 +656,7 @@ export default function Navbar({ /> {/* Shopping Cart */} - setActiveCartDropdown(prev => !prev)} @@ -674,7 +674,8 @@ export default function Navbar({
} - /> + /> */} + navigate("/cart-page")} /> {/* Enhanced City Card Button with Source Tracking */}
@@ -690,7 +691,7 @@ export default function Navbar({ label: 'My Profile', icon: , action: () => { - navigate(citySelected?`/${slugify(cityName)}/profile`:'/profile'); + navigate(citySelected ? `/${slugify(cityName)}/profile` : '/profile'); setActiveUserDropdown(false); } }, diff --git a/src/pages/CartPage.tsx b/src/pages/CartPage.tsx new file mode 100644 index 0000000..497f6ec --- /dev/null +++ b/src/pages/CartPage.tsx @@ -0,0 +1,877 @@ +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 './Navbar'; +import { Footer } from './Footer'; +import { ImageWithFallback } from './figma/ImageWithFallback'; +import { CheckoutStepper } from './CheckoutStepper'; +import imgRectangle26 from "figma:asset/2496f45326066d3adf0d5494c1dc1595575894ff.png"; + +/* ─── 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 CartPageProps { + 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 CartPage({ + 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, +}: CartPageProps) { + 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/PaymentDetailsPage.tsx b/src/pages/PaymentDetailsPage.tsx new file mode 100644 index 0000000..b863f0d --- /dev/null +++ b/src/pages/PaymentDetailsPage.tsx @@ -0,0 +1,494 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'motion/react'; +import { + ArrowLeft, User, MapPin, Lock, Shield, ChevronDown, + Check, AlertCircle, Pencil, UserCheck, Gift +} from 'lucide-react'; +import Navbar from '../components/Navbar'; +import { Footer } from '../components/Footer'; +import { Card, CardContent, CardHeader } from '../components/ui/card'; +import { Separator } from '../components/ui/separator'; + +export interface CheckoutOrderItem { + city: string; + cardType: 'Flexi' | 'Unlimited'; + days: number; + adults: number; + children: number; + quantity: number; + pricePerUnit: number; +} + +interface PaymentDetailsPageProps { + checkoutOrder?: CheckoutOrderItem | null; + onBackClick: () => void; + onPaymentComplete: () => void; + onHomeClick: () => void; + onPassesClick: () => 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; + onCheckoutClick?: () => void; + onSignInClick: () => void; + onSignOutClick?: () => void; + currentPage?: string; + user?: { email: string; name: string } | null; +} + +/* ─── Profile data ─── */ +const profileData = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + phone: '+1 (555) 123-4567', + address: '123 Main Street', + city: 'New York', + state: 'NY', + postcode: '10001', + country: 'United States', +}; + +/* ─── Editable field ─── */ +function Field({ + label, value, onChange, placeholder, type = 'text', + error, maxLength, inputMode, prefilled, +}: { + label: string; value: string; onChange: (v: string) => void; placeholder?: string; + type?: string; error?: string; maxLength?: number; + inputMode?: React.HTMLAttributes['inputMode']; prefilled?: boolean; +}) { + const [focused, setFocused] = useState(false); + return ( +
+ +
+ onChange(e.target.value)} + onFocus={() => setFocused(true)} + onBlur={() => setFocused(false)} + placeholder={placeholder} + maxLength={maxLength} + inputMode={inputMode} + className={`w-full border rounded-xl px-4 py-3 pr-10 font-poppins text-base font-normal text-[#2a2a2a] outline-none transition-all duration-200 placeholder:text-[#ccc] ${ + error + ? 'border-red-300 focus:border-red-400 bg-red-50/30' + : focused + ? 'border-[#F95F62] ring-2 ring-[#F95F62]/10' + : prefilled + ? 'border-[#F95F62]/25 bg-[#F95F62]/[0.02]' + : 'border-gray-200' + }`} + /> + {prefilled && !focused && ( + + )} +
+ {error && ( + + {error} + + )} +
+ ); +} + +/* ─── Card type badge ─── */ +function CardTypeBadge({ cardType }: { cardType: 'Flexi' | 'Unlimited' }) { + return ( + + {cardType} Card + + ); +} + +/* ─── Main component ─── */ +export function PaymentDetailsPage({ + checkoutOrder, + onBackClick, + onPaymentComplete, + onHomeClick, + onPassesClick, + onAttractionsClick, + onBlogsClick, + onHowItWorksClick, + onFAQClick, + onPrivacyPolicyClick, + onAboutUsClick, + onProfileClick, + onCityCardsClick, + onMagicItineraryClick, + onPostCardsClick, + onOffersClick, + onSuperSavingsClick, + onEsimsClick, + onHotelDiscountsClick, + onContactUsClick, + onCartClick, + onCheckoutClick, + onSignInClick, + onSignOutClick, + currentPage, + user, +}: PaymentDetailsPageProps) { + + /* ── Purchase type ── */ + const [selectedTab, setSelectedTab] = useState<'myself' | 'gift'>('myself'); + const [giftName, setGiftName] = useState(''); + const [giftEmail, setGiftEmail] = useState(''); + + /* ── Personal Info ── */ + const [firstName, setFirstName] = useState(profileData.firstName); + const [lastName, setLastName] = useState(profileData.lastName); + const [email, setEmail] = useState(user?.email || profileData.email); + const [phone, setPhone] = useState(profileData.phone); + + /* ── Billing Address ── */ + const [address, setAddress] = useState(profileData.address); + const [billingCity, setBillingCity] = useState(profileData.city); + const [state, setState] = useState(profileData.state); + const [postcode, setPostcode] = useState(profileData.postcode); + const [country, setCountry] = useState(profileData.country); + + /* ── Validation ── */ + const [errors, setErrors] = useState>({}); + const [submitting, setSubmitting] = useState(false); + + const order = checkoutOrder || { + city: 'Melbourne', cardType: 'Flexi' as const, + days: 3, adults: 2, children: 0, quantity: 1, pricePerUnit: 49.50, + }; + + const subtotal = order.pricePerUnit * order.quantity; + const tax = subtotal * 0.1; + const total = subtotal + tax; + + const validate = () => { + const e: Record = {}; + if (!firstName.trim()) e.firstName = 'Required'; + if (!lastName.trim()) e.lastName = 'Required'; + if (!email.trim() || !/\S+@\S+\.\S+/.test(email)) e.email = 'Valid email required'; + if (!phone.trim()) e.phone = 'Required'; + if (!address.trim()) e.address = 'Required'; + if (!billingCity.trim()) e.billingCity = 'Required'; + if (!postcode.trim()) e.postcode = 'Required'; + return e; + }; + + const handleSubmit = () => { + const e = validate(); + setErrors(e); + if (Object.keys(e).length > 0) return; + setSubmitting(true); + setTimeout(() => { + setSubmitting(false); + onPaymentComplete(); + }, 1800); + }; + + 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} + /> + +
+ + {/* Back */} + + + {/* Page heading */} + +
+

+ Secure{' '} + Checkout +

+
+ + SSL Secured +
+
+

+ Complete your purchase securely. Your payment information is protected. +

+
+ +
+ + {/* ── LEFT: Forms ── */} + + + + {/* Purchase type tabs */} + +
+ + +
+
+ + + + {/* Pre-filled notice banner */} + +
+ +
+

+ Details pre-filled from your profile.{' '} + All fields are editable — just tap to make changes. +

+
+ + + + {/* 1. Personal Information */} + +
+
+ 1 +
+

Personal Information

+
+
+ + +
+
+ + +
+
+ + {/* Gift recipient section */} + + {selectedTab === 'gift' && ( + +
+
+ +

Gift Recipient Details

+
+
+ + +
+
+
+ )} +
+ + + + {/* 2. Billing Address */} + +
+
+ 2 +
+

Billing Address

+
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+
+
+ +
+
+
+ + {/* ── RIGHT: Order Summary ── */} + +
+ +
+
+

Order Summary

+
+ + {/* Card info */} +
+
+
+ {order.cardType} +
+
+
+

{order.city}

+ +
+

+ {order.cardType === 'Flexi' ? `${order.days} Attractions` : `${order.days} Days`} +

+
+
+
+ {[ + { label: 'Adults', value: order.adults }, + { label: 'Children', value: order.children }, + { label: 'Qty', value: order.quantity }, + ].map(({ label, value }) => ( +
+ {label} + {value} +
+ ))} +
+
+ + {/* Pricing breakdown */} +
+
+
+ Subtotal + ${subtotal.toFixed(2)} +
+
+ GST (10%) + ${tax.toFixed(2)} +
+
+ Booking fee + Free +
+
+ Total + ${total.toFixed(2)} +
+
+
+
+ + {/* CTA */} + + {submitting ? ( + <> + + Processing… + + ) : ( + <> + + Complete Payment · ${total.toFixed(2)} + + )} + + +

+ By completing your purchase you agree to our Terms of Service and Privacy Policy +

+
+
+ +
+
+ +
+
+ ); +} From 99811b1e8738025c9e389e65f7a6d8b281476864 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Sun, 19 Apr 2026 20:14:44 +0530 Subject: [PATCH 11/19] integrate showCartItems api in the CartPage --- src/Redux/Store.tsx | 7 +- src/Redux/services/cards.service.ts | 37 + src/pages/CartPage.tsx | 1586 ++++++++++++++------------- 3 files changed, 841 insertions(+), 789 deletions(-) create mode 100644 src/Redux/services/cards.service.ts diff --git a/src/Redux/Store.tsx b/src/Redux/Store.tsx index ecb7f5d..725a87a 100644 --- a/src/Redux/Store.tsx +++ b/src/Redux/Store.tsx @@ -3,13 +3,15 @@ import { attractionsApi } from "./services/attractions.service"; import { citiesApi } from "./services/cities.service"; import { authApi } from "./services/auth.service"; import { profileApi } from "./services/profile.service"; +import { cardsApi } from "./services/cards.service"; export const store = configureStore({ reducer: { [attractionsApi.reducerPath]: attractionsApi.reducer, [citiesApi.reducerPath]: citiesApi.reducer, [authApi.reducerPath]: authApi.reducer, - [profileApi.reducerPath]: profileApi.reducer + [profileApi.reducerPath]: profileApi.reducer, + [cardsApi.reducerPath]:cardsApi.reducer }, @@ -18,7 +20,8 @@ export const store = configureStore({ attractionsApi.middleware, citiesApi.middleware, authApi.middleware, - profileApi.middleware + profileApi.middleware, + cardsApi.middleware ), }); export type RootState = ReturnType; diff --git a/src/Redux/services/cards.service.ts b/src/Redux/services/cards.service.ts new file mode 100644 index 0000000..689e9cf --- /dev/null +++ b/src/Redux/services/cards.service.ts @@ -0,0 +1,37 @@ + +import { createApi } from "@reduxjs/toolkit/query/react"; +import { baseQuery } from "../baseQuery"; + +export const cardsApi = createApi({ + reducerPath: "cardsApi", + baseQuery, + + tagTypes: ["cardsInCart"], + + endpoints: (builder) => ({ + + getCardsinCart: builder.query({ + query: (cityId) => { + const params = new URLSearchParams() + params.append('cityXid', cityId); + return `/website/passes/cart/passes?${params.toString()}` + }, + providesTags: ["cardsInCart"] + }), + + getUpcomingCities: builder.query({ + query: (listType) => `/cities/list/all?listType=${listType}`, + + }), + + getCheckoutPageData:builder.query({ + query: (cityId) => `/website/pass/${cityId}`, + + }), + }) +}); + +export const { + useGetCardsinCartQuery, + useGetCheckoutPageDataQuery +} = cardsApi; \ No newline at end of file diff --git a/src/pages/CartPage.tsx b/src/pages/CartPage.tsx index 497f6ec..3f06370 100644 --- a/src/pages/CartPage.tsx +++ b/src/pages/CartPage.tsx @@ -1,137 +1,140 @@ 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 + 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 './Navbar'; -import { Footer } from './Footer'; -import { ImageWithFallback } from './figma/ImageWithFallback'; -import { CheckoutStepper } from './CheckoutStepper'; -import imgRectangle26 from "figma:asset/2496f45326066d3adf0d5494c1dc1595575894ff.png"; +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'; +// import { CheckoutStepper } from './CheckoutStepper'; +// import imgRectangle26 from "figma:asset/2496f45326066d3adf0d5494c1dc1595575894ff.png"; /* ─── Types ─── */ export interface CartItem { - id: string; - city: string; - cardType: 'Flexi' | 'Unlimited'; - days: number; - adults: number; - children: number; - quantity: number; - pricePerUnit: number; - image: string; + 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; + id: string; + name: string; + image: string; + category: string; + included: boolean; } interface CartPageProps { - 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; + 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', - }, + { + 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 }, - ], - }, + 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' }, - ], + 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 }, + Flexi: { 3: 49.5, 6: 69, 12: 99, 18: 129, 24: 159 }, + Unlimited: { 3: 79, 6: 109, 12: 149, 18: 189, 24: 229 }, }; /* ═══════════════════════════════════════════ @@ -139,101 +142,99 @@ const priceTable: Record> = { ═══════════════════════════════════════════ */ 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 + 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 && ( +
+ +
+ )}
-
- 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 + 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 && ( +
+ +
+ )}
-
- and - ${childPrice} - /Child -
-
- {/* Description */} -
-

- Dive into an extensive selection of thrilling destinations! -

-
- {/* Side tab - Unlimited (coral) */} -
- Card - Unlimited -
- {/* Selected checkmark */} - {isSelected && ( -
- -
- )} -
- ); + ); } /* ═══════════════════════════════════════════ @@ -241,150 +242,148 @@ function UnlimitedCardPreview({ city, adultPrice, childPrice, isSelected }: { ci ═══════════════════════════════════════════ */ function CheckoutConfigCard({ - item, - onChange, - onProceed, + item, + onChange, + onProceed, }: { - item: CartItem; - onChange: (updates: Partial) => void; - onProceed: () => void; + 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; + 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 - -
-
+ const navigate = useNavigate() - {/* Configuration rows */} -
- {/* No. of Adults */} -
- No. of Adults -
- - {item.adults} - -
-
+ return ( +
+ {/* City header */} +
+

{item.city}

+
+ + {item.cardType} Card + +
+
- {/* No. of Children */} -
- No. of Children -
- - {item.children} - -
-
+ {/* Configuration rows */} +
+ {/* No. of Adults */} +
+ No. of Adults +
+ + {item.adults} + +
+
- {/* No. of Days (dropdown) */} -
- - {item.cardType === 'Flexi' ? 'No. of Attractions' : 'No. of Days'} - -
- - - {daysOpen && ( - + 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 */} +
+ navigate("/payment")} + className="w-full py-4 rounded-full bg-[#f95f62] text-white font-poppins text-base font-medium hover:bg-[#e8545a] transition-colors shadow-lg shadow-[#f95f62]/20" > - {dayOptions.map((d) => ( - - ))} - - )} - -
+ Proceed to Pay + +
- - {/* You Pay */} -
- You Pay -
- - ${originalPrice.toFixed(0)} - - - ${totalPrice.toFixed(0)} - -
-
-
- - {/* Proceed button */} -
- - Proceed to Pay - -
-
- ); + ); } /* ═══════════════════════════════════════════ @@ -392,486 +391,499 @@ function CheckoutConfigCard({ ═══════════════════════════════════════════ */ export function CartPage({ - 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, + 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, }: CartPageProps) { - 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 [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 navigate = useNavigate() - const handleSelectCard = (id: string) => { - setSelectedCardId(prev => (prev === id ? null : id)); - }; + const cityId = localStorage.getItem("cityId") - const handleGoToCheckout = () => { - const item = cartItems.find(i => i.id === selectedCardId); - if (item) { - setCheckoutItem({ ...item }); - setView('checkout'); - window.scrollTo({ top: 0, behavior: 'smooth' }); + const { data, isLoading } = useGetCardsinCartQuery(cityId) + + const CartItems = data?.cartItems ?? [] + + if (isLoading) { + return ( + + ) } - }; - const handleBackToCart = () => { - setView('cart'); - setCheckoutItem(null); - }; + const handleRemoveItem = (id: string) => { + setCartItems(prev => prev.filter(item => item.id !== id)); + if (selectedCardId === id) setSelectedCardId(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 handleSelectCard = (id: string) => { + setSelectedCardId(prev => (prev === id ? null : id)); + }; - 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] || []) : []; + const handleGoToCheckout = () => { + // const item = cartItems.find(i => i.id === selectedCardId); + // if (item) { + // setCheckoutItem({ ...item }); + // setView('checkout'); + // window.scrollTo({ top: 0, behavior: 'smooth' }); + // } + navigate("/payment") + }; - 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} - /> + const handleBackToCart = () => { + setView('cart'); + setCheckoutItem(null); + }; - - {view === 'cart' ? ( - /* ─── CART VIEW ─── */ - - {/* Header */} -
-

- Your{' '} - Cart -

-

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

-
+ 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); + }; - {/* Tab switcher */} - {/* Cards listed directly below */} + const isEmpty = cartItems.length === 0; + const selectedItem = CartItems.find((i: any) => 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} + /> - {/* 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
-
-
+ {view === 'cart' ? ( + /* ─── CART VIEW ─── */ + + {/* Header */} +
+

+ Your{' '} + Cart +

+

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

+
- - {cartItems.map((item) => { - const isSelected = selectedCardId === item.id; - const totalPrice = item.pricePerUnit * item.quantity; + {/* Tab switcher */} + {/* Cards listed directly below */} - 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 && ( - - - - )} - + {/* 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
+
+
- {/* 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} -
-
-
-
+ + {CartItems.map((item: any) => { + const isSelected = selectedCardId === item.id; + const totalPrice = item.pricePerUnit * item.quantity; - {/* 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} -
-
- -
-
- - ); - })} -
+ 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 && ( + + + + )} + - {/* Bottom checkout bar */} - -
- {selectedItem ? ( + {/* Mobile layout */} +
+
+ +
+
+
+
+
{item.city?.cityName}
+
+ {item.displayCardMode} + {item.cardMode === 'flexi' ? `${item.noOfAttractions} ${item.noOfAttractions === 1 ? 'attraction' : 'attractions'}` : `${item.noOfDays} ${item.noOfDays === 1 ? 'day' : 'days'}`} +
+
+ {/* */} +
+
+ {item.totalAdult}A · {item.totalChild}C +
+ ${item.totalAmount} + {/* {item.quantity > 1 && ${item.pricePerUnit.toFixed(2)}/ea} */} +
+
+
+
+ + {/* Desktop layout */} +
+
+
+ +
+
+
{item.city?.cityName}
+
+ {item.displayCardMode} + {item.cardMode === 'flexi' ? `${item.noOfAttractions} ${item.noOfAttractions === 1 ? 'attraction' : 'attractions'}` : `${item.noOfDays} ${item.noOfDays === 1 ? 'day' : 'days'}`} +
+
+
+
+
+ {item.totalAdult} + {item.totalChild} +
+
+ {/*
+ {item.quantity} +
*/} +
+ ${item.totalAmount} + {/* {item.quantity > 1 && ${item.pricePerUnit.toFixed(2)} per unit} */} +
+ {/*
+ +
*/} +
+ + ); + })} + + + {/* Bottom checkout bar */} + +
+ {selectedItem ? ( + <> +

+ Selected: {selectedItem.city.cityName} {selectedItem.displayCardMode} · {selectedItem.cardMode === 'flexi' ? `${selectedItem.noOfAttractions} ${selectedItem.noOfAttractions === 1 ? 'attraction' : 'attractions'}` : `${selectedItem.noOfDays} ${selectedItem.noOfDays === 1 ? 'day' : 'days'}`} +

+

+ ${selectedItem.totalAmount} +

+ + ) : ( +

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 && ( <> -

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

-

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

+ {/* 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)} + /> +
+
+
- ) : ( -

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; + icon: React.ReactNode; title: string; description: string; actionLabel: string; onAction?: () => void; }) { - return ( - - {icon} -

{title}

-

{description}

- {actionLabel} -
- ); + return ( + + {icon} +

{title}

+

{description}

+ {actionLabel} +
+ ); } \ No newline at end of file From fca33f55a76d23063a54f9383745d8a1c4f062e9 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Mon, 20 Apr 2026 12:10:46 +0530 Subject: [PATCH 12/19] integrate api in the checkoutpage2 component --- src/AppRouter.tsx | 18 +- src/components/Navbar.tsx | 5 +- src/pages/CartPage.tsx | 6 +- src/pages/CheckoutPage2.tsx | 805 ++++++++++++++++++++++++++++++++++++ 4 files changed, 826 insertions(+), 8 deletions(-) create mode 100644 src/pages/CheckoutPage2.tsx diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index aa6c388..b3227f4 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -34,6 +34,8 @@ 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'; // User type definition interface User { @@ -126,11 +128,11 @@ export function AppRouter({ } /> {/* Checkout Routes */} -
- } /> + } /> */} @@ -278,7 +280,17 @@ export function AppRouter({ } /> - + + + } /> + + + + } /> + diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index e1444e5..f090f9e 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -95,7 +95,8 @@ export default function Navbar({ const cityId = localStorage.getItem("cityId") const cityName = localStorage.getItem("cityName") - const citySelected = location.pathname.includes(slugify(cityName) || "") + // const citySelected = location.pathname.includes(slugify(cityName) || "") + const citySelected = cityName const baseUrl = import.meta.env.VITE_BASE_URL; @@ -675,7 +676,7 @@ export default function Navbar({
} /> */} - navigate("/cart-page")} /> + navigate("/cart")} /> {/* Enhanced City Card Button with Source Tracking */}
diff --git a/src/pages/CartPage.tsx b/src/pages/CartPage.tsx index 3f06370..dc61438 100644 --- a/src/pages/CartPage.tsx +++ b/src/pages/CartPage.tsx @@ -446,14 +446,14 @@ export function CartPage({ setSelectedCardId(prev => (prev === id ? null : id)); }; - const handleGoToCheckout = () => { + const handleGoToCheckout = (selectedItemId:number) => { // const item = cartItems.find(i => i.id === selectedCardId); // if (item) { // setCheckoutItem({ ...item }); // setView('checkout'); // window.scrollTo({ top: 0, behavior: 'smooth' }); // } - navigate("/payment") + navigate(`/payment/${selectedItemId}`) }; const handleBackToCart = () => { @@ -641,7 +641,7 @@ export function CartPage({ handleGoToCheckout(selectedItem.id)} disabled={!selectedItem} className={`sm:w-auto px-8 py-3.5 rounded-xl font-poppins text-base font-medium flex items-center justify-center gap-2 transition-all duration-200 ${selectedItem ? 'bg-[#F95F62] text-white hover:bg-[#e8545a] shadow-lg shadow-[#F95F62]/20' : 'bg-gray-100 text-gray-400 cursor-not-allowed' }`} diff --git a/src/pages/CheckoutPage2.tsx b/src/pages/CheckoutPage2.tsx new file mode 100644 index 0000000..d8d6abe --- /dev/null +++ b/src/pages/CheckoutPage2.tsx @@ -0,0 +1,805 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'motion/react'; +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 { useNavigate } from 'react-router-dom'; +import { useGetCheckoutPageDataQuery } 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; +} + +/* ─── Data (Same as Original) ─── */ +const dayOptions = [3, 6, 12, 18, 24]; + +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 }, +}; + +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' }, + ], +}; + +/* ─── FIGMA CARD PREVIEWS (Exact Copy) ─── */ +function FlexiCardPreview({ city, adultPrice, childPrice, isSelected }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean }) { + return ( +
+
+
+ {/* */} +
+

{city}

+
+
+
+ From + ${adultPrice} + /Adult +
+
+ and + ${childPrice} + /Child +
+
+
+

+ Dive into an extensive selection of thrilling destinations! +

+
+
+ Card + Flexi +
+ {isSelected && ( +
+ +
+ )} +
+ ); +} + +function UnlimitedCardPreview({ city, adultPrice, childPrice, isSelected }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean }) { + return ( +
+
+
+ {/* */} +
+

{city}

+
+
+
+ From + ${adultPrice} + /Adult +
+
+ and + ${childPrice} + /Child +
+
+
+

+ Dive into an extensive selection of thrilling destinations! +

+
+
+ Card + Unlimited +
+ {isSelected && ( +
+ +
+ )} +
+ ); +} + +/* ─── CheckoutConfigCard (Exact Copy) ─── */ +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; + const navigate = useNavigate(); + + return ( +
+
+

{item.city}

+
+ + {item.cardType}3 + +
+
+
+
+ No. of Adults +
+ + {item.adults} + +
+
+ +
+ No. of Children +
+ + {item.children} + +
+
+ +
+ + {item.cardType === 'Flexi' ? 'No. of Attractions' : 'No. of Days'} + +
+ + + {daysOpen && ( + + {dayOptions.map((d) => ( + + ))} + + )} + +
+
+ +
+ You Pay +
+ ${originalPrice.toFixed(0)} + ${totalPrice.toFixed(0)} +
+
+
+ +
+ + Proceed to Pay + +
+
+ ); +} + +/* ─── MAIN CHECKOUT PAGE 2 ─── */ +// export function CheckoutPage2({ +// onHomeClick, +// onPassesClick, +// onAttractionsClick, +// onBlogsClick, +// onHowItWorksClick, +// onFAQClick, +// onPrivacyPolicyClick, +// onAboutUsClick, +// onContactUsClick, +// onSignInClick, +// onSignOutClick, +// onProfileClick, +// user, +// currentPage, +// }: any) { +// const navigate = useNavigate(); + +// // Default item (you can pass via props later) +// const [checkoutItem, setCheckoutItem] = useState({ +// id: '1', +// city: 'Melbourne', +// cardType: 'Flexi', +// days: 3, +// adults: 2, +// children: 1, +// quantity: 1, +// pricePerUnit: 49.5, +// image: '', +// }); + +// const cityId = localStorage.getItem("cityId") +// const { data: checkoutPageData, isLoading } = useGetCheckoutPageDataQuery(cityId) + +// const cards = checkoutPageData?.cards ?? [] + +// if (isLoading) { +// return +// } else { +// console.log(checkoutPageData) +// } + +// const handleCheckoutItemChange = (updates: Partial) => { +// const updated = { ...checkoutItem, ...updates }; +// const prices = priceTable[updated.cardType]; +// if (prices && prices[updated.days] !== undefined) { +// updated.pricePerUnit = prices[updated.days]; +// } +// setCheckoutItem(updated); +// }; + +// const attractions = attractionsData[checkoutItem.city]?.[checkoutItem.cardType] || []; +// const offers = offersData[checkoutItem.cardType] || []; + +// return ( +//
+// { }} +// onSignInClick={onSignInClick} +// onSignOutClick={onSignOutClick} +// onPassesClick={onPassesClick} +// onCheckoutClick={() => { }} +// onHomeClick={onHomeClick} +// onAttractionsClick={onAttractionsClick} +// onBlogsClick={onBlogsClick} +// onHowItWorksClick={onHowItWorksClick} +// onFAQClick={onFAQClick} +// onPrivacyPolicyClick={onPrivacyPolicyClick} +// onAboutUsClick={onAboutUsClick} +// onProfileClick={onProfileClick} +// onCityCardsClick={() => { }} +// onMagicItineraryClick={() => { }} +// onPostCardsClick={() => { }} +// onOffersClick={() => { }} +// onSuperSavingsClick={() => { }} +// onEsimsClick={() => { }} +// onHotelDiscountsClick={() => { }} +// onCartClick={() => { }} +// currentPage={currentPage} +// user={user} +// /> + +//
+// + +//
+//

+// Checkout{' '} +// {checkoutItem.city} +//

+//
+ +//
+// {/* Left Column */} +//
+// {/* Card Type Selection */} +//
+//

Choose Your Card

+//

Select the card type that best suits your travel style

+//
+// +// +//
+ +// {/* Features Comparison (Exact Copy) */} +//
+//
+//

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 Section (Exact) */} +//
+//

{checkoutItem.cardType} Card Offers

+//

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

+//
+// {offers.map((offer, idx) => ( +//
+//
+//
+// +//
+//
+//

{offer.title}

+//
+//
+//

{offer.description}

+//
+//
+//
+//
+// ))} +//
+//
+ +// {/* Attractions Section (Exact) */} +//
+//
+//

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 */} +//
+//
+// navigate("/payment")} +// /> +//
+//
+ +// {/* Mobile Config Card */} +//
+// navigate("/payment")} +// /> +//
+//
+//
+ +//
+//
+// ); +// } + +export function CheckoutPage2({ + onHomeClick, + onPassesClick, + onAttractionsClick, + onBlogsClick, + onHowItWorksClick, + onFAQClick, + onPrivacyPolicyClick, + onAboutUsClick, + onContactUsClick, + onSignInClick, + onSignOutClick, + onProfileClick, + user, + currentPage, +}: any) { + const navigate = useNavigate(); + + const cityId = localStorage.getItem("cityId"); + const { data: checkoutPageData, isLoading } = useGetCheckoutPageDataQuery(cityId); + + const city = checkoutPageData?.city; + const allCards = checkoutPageData?.cards ?? []; + const allAttractions = checkoutPageData?.attractions ?? []; + const allOffers = checkoutPageData?.offers ?? []; + + const baseUrl = import.meta.env.VITE_BASE_URL; + + + // Initialize with first card (Flexi) as default + const defaultCard = allCards[0] || null; + + const [checkoutItem, setCheckoutItem] = useState({ + id: defaultCard?.id?.toString() || '1', + city: city?.name || 'Melbourne', + cardType: defaultCard?.cardType?.displayName || 'Flexi', + days: defaultCard?.validityDuration || 3, + adults: 2, + children: 1, + quantity: 1, + pricePerUnit: defaultCard?.adultPrice || 49.5, + image: city?.heroBanner?.image || '', + }); + + if (isLoading) { + return ; + } + + const handleCheckoutItemChange = (updates: Partial) => { + const updated = { ...checkoutItem, ...updates }; + + // If card type changes, update with real card data + if (updates.cardType) { + const selectedCard = allCards.find( + c => c.cardType?.displayName === updates.cardType + ); + if (selectedCard) { + updated.id = selectedCard.id.toString(); + updated.days = selectedCard.validityDuration; + updated.pricePerUnit = selectedCard.adultPrice; + } + } + + setCheckoutItem(updated); + }; + + // Get currently selected card + const selectedCard = allCards.find(c => + c.cardType?.displayName === checkoutItem.cardType + ) || allCards[0]; + + // Offers for selected card (fallback to global offers) + const currentOffers = selectedCard?.offers?.length + ? selectedCard.offers + : allOffers; + + return ( +
+ { }} + onSignInClick={onSignInClick} + onSignOutClick={onSignOutClick} + onPassesClick={onPassesClick} + onCheckoutClick={() => { }} + onHomeClick={onHomeClick} + onAttractionsClick={onAttractionsClick} + onBlogsClick={onBlogsClick} + onHowItWorksClick={onHowItWorksClick} + onFAQClick={onFAQClick} + onPrivacyPolicyClick={onPrivacyPolicyClick} + onAboutUsClick={onAboutUsClick} + onProfileClick={onProfileClick} + onCityCardsClick={() => { }} + onMagicItineraryClick={() => { }} + onPostCardsClick={() => { }} + onOffersClick={() => { }} + onSuperSavingsClick={() => { }} + onEsimsClick={() => { }} + onHotelDiscountsClick={() => { }} + onCartClick={() => { }} + currentPage={currentPage} + user={user} + /> + +
+ + +
+

+ Checkout{' '} + + {city?.name || checkoutItem.city} + +

+
+ +
+ {/* Left Column */} +
+ {/* Card Type Selection */} +
+

Choose Your Card

+

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

+
+ {allCards.map((card) => ( + + ))} +
+ + {/* Features Comparison - Kept as is (no CSS change) */} +
+
+ {/* 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 Section */} +
+

+ {checkoutItem.cardType} Card Offers +

+

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

+
+ {currentOffers.map((offer, idx) => ( +
+
+
+ +
+
+

+ {offer.title} +

+
+
+

+ {offer.description} +

+
+
+
+
+ ))} +
+
+ + {/* Attractions Section */} +
+
+

Available Attractions

+ + {allAttractions.length} included + +
+

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

+
+ {allAttractions.map((attraction) => ( +
+
+ +
+
+
+ {attraction.title} +
+
+
+
+ ))} +
+
+
+ + {/* Right Column - Config Card */} +
+
+ navigate("/payment")} + /> +
+
+ + {/* Mobile Config Card */} +
+ navigate("/payment")} + /> +
+
+
+ +
+
+ ); +} \ No newline at end of file From 326d5e58718a5489a31df75962615820f38ea43b Mon Sep 17 00:00:00 2001 From: aryabenade Date: Mon, 20 Apr 2026 12:11:12 +0530 Subject: [PATCH 13/19] integrate userdetails api in the payment page --- src/Redux/services/cards.service.ts | 13 +- src/pages/PaymentDetailsPage.tsx | 318 ++++++++++++++++++---------- 2 files changed, 208 insertions(+), 123 deletions(-) diff --git a/src/Redux/services/cards.service.ts b/src/Redux/services/cards.service.ts index 689e9cf..c2850cf 100644 --- a/src/Redux/services/cards.service.ts +++ b/src/Redux/services/cards.service.ts @@ -19,19 +19,18 @@ export const cardsApi = createApi({ providesTags: ["cardsInCart"] }), - getUpcomingCities: builder.query({ - query: (listType) => `/cities/list/all?listType=${listType}`, - + getCheckoutPageData: builder.query({ + query: (cityId) => `/website/pass/${cityId}`, }), - getCheckoutPageData:builder.query({ - query: (cityId) => `/website/pass/${cityId}`, - + getCardBookingDetails: builder.query({ + query: (bookingId) => `/website/passes/${bookingId}/details`, }), }) }); export const { useGetCardsinCartQuery, - useGetCheckoutPageDataQuery + useGetCheckoutPageDataQuery, + useGetCardBookingDetailsQuery } = cardsApi; \ No newline at end of file diff --git a/src/pages/PaymentDetailsPage.tsx b/src/pages/PaymentDetailsPage.tsx index b863f0d..49c4912 100644 --- a/src/pages/PaymentDetailsPage.tsx +++ b/src/pages/PaymentDetailsPage.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { ArrowLeft, User, MapPin, Lock, Shield, ChevronDown, @@ -8,6 +8,10 @@ import Navbar from '../components/Navbar'; import { Footer } from '../components/Footer'; import { Card, CardContent, CardHeader } from '../components/ui/card'; import { Separator } from '../components/ui/separator'; +import { useGetUserProfileDetailsQuery } from '../Redux/services/profile.service'; +import LoadingSpinner from '../components/LoadingSpinner'; +import { useParams } from 'react-router-dom'; +import { useGetCardBookingDetailsQuery } from '../Redux/services/cards.service'; export interface CheckoutOrderItem { city: string; @@ -48,32 +52,37 @@ interface PaymentDetailsPageProps { user?: { email: string; name: string } | null; } -/* ─── Profile data ─── */ -const profileData = { - firstName: 'John', - lastName: 'Doe', - email: 'john.doe@example.com', - phone: '+1 (555) 123-4567', - address: '123 Main Street', - city: 'New York', - state: 'NY', - postcode: '10001', - country: 'United States', -}; - /* ─── Editable field ─── */ function Field({ - label, value, onChange, placeholder, type = 'text', - error, maxLength, inputMode, prefilled, + label, + value, + onChange, + placeholder, + type = 'text', + error, + maxLength, + inputMode, + prefilled, + disabled = false, // ← New prop }: { - label: string; value: string; onChange: (v: string) => void; placeholder?: string; - type?: string; error?: string; maxLength?: number; - inputMode?: React.HTMLAttributes['inputMode']; prefilled?: boolean; + label: string; + value: string; + onChange: (v: string) => void; + placeholder?: string; + type?: string; + error?: string; + maxLength?: number; + inputMode?: React.HTMLAttributes['inputMode']; + prefilled?: boolean; + disabled?: boolean; // ← Added }) { const [focused, setFocused] = useState(false); + return (
- +
- {prefilled && !focused && ( + + {/* Pencil icon only when prefilled AND not disabled AND not focused */} + {prefilled && !focused && !disabled && ( )}
+ {error && ( {error} @@ -110,11 +125,10 @@ function Field({ /* ─── Card type badge ─── */ function CardTypeBadge({ cardType }: { cardType: 'Flexi' | 'Unlimited' }) { return ( - + {cardType} Card ); @@ -152,21 +166,50 @@ export function PaymentDetailsPage({ /* ── Purchase type ── */ const [selectedTab, setSelectedTab] = useState<'myself' | 'gift'>('myself'); - const [giftName, setGiftName] = useState(''); + + /* ── Gift Recipient Details (Only editable fields) ── */ + const [giftFirstName, setGiftFirstName] = useState(''); + const [giftLastName, setGiftLastName] = useState(''); const [giftEmail, setGiftEmail] = useState(''); + const [giftPhone, setGiftPhone] = useState(''); + const [giftCity, setGiftCity] = useState(''); + const [giftCountry, setGiftCountry] = useState(''); - /* ── Personal Info ── */ - const [firstName, setFirstName] = useState(profileData.firstName); - const [lastName, setLastName] = useState(profileData.lastName); - const [email, setEmail] = useState(user?.email || profileData.email); - const [phone, setPhone] = useState(profileData.phone); + /* ── Profile Data (Same as ProfilePage) ── */ + const [formData, setFormData] = useState({ + firstName: '', + lastName: '', + email: '', + phone: '', + country: '', + address1: '', + address2: '', + city: '', + postalCode: '' + }); - /* ── Billing Address ── */ - const [address, setAddress] = useState(profileData.address); - const [billingCity, setBillingCity] = useState(profileData.city); - const [state, setState] = useState(profileData.state); - const [postcode, setPostcode] = useState(profileData.postcode); - const [country, setCountry] = useState(profileData.country); + const userId = localStorage.getItem("userId"); + const { bookingId } = useParams() + const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId); + const { data: bookingDetails } = useGetCardBookingDetailsQuery(bookingId); + console.log(bookingDetails) + + // Populate formData from API (exactly like ProfilePage) + useEffect(() => { + if (userDetails) { + setFormData({ + firstName: userDetails?.firstName, + lastName: userDetails?.lastName, + email: userDetails?.emailAddress, + phone: userDetails?.mobileNumber, + country: userDetails?.country, + address1: userDetails?.address1, + address2: userDetails?.address2, + city: userDetails?.cityName, + postalCode: userDetails?.zipCode, + }); + } + }, [userDetails]); /* ── Validation ── */ const [errors, setErrors] = useState>({}); @@ -183,20 +226,32 @@ export function PaymentDetailsPage({ const validate = () => { const e: Record = {}; - if (!firstName.trim()) e.firstName = 'Required'; - if (!lastName.trim()) e.lastName = 'Required'; - if (!email.trim() || !/\S+@\S+\.\S+/.test(email)) e.email = 'Valid email required'; - if (!phone.trim()) e.phone = 'Required'; - if (!address.trim()) e.address = 'Required'; - if (!billingCity.trim()) e.billingCity = 'Required'; - if (!postcode.trim()) e.postcode = 'Required'; + + if (selectedTab === 'gift') { + if (!giftFirstName.trim()) e.giftFirstName = 'Required'; + if (!giftLastName.trim()) e.giftLastName = 'Required'; + + if (!giftEmail.trim() || !/\S+@\S+\.\S+/.test(giftEmail)) { + e.giftEmail = 'Valid email required'; + } + + if (!giftPhone.trim() || !/^\+?[0-9]{7,15}$/.test(giftPhone)) { + e.giftPhone = 'Valid phone required'; + } + + if (!giftCity.trim()) e.giftCity = 'Required'; + if (!giftCountry.trim()) e.giftCountry = 'Required'; + } + return e; }; + const handleSubmit = () => { const e = validate(); setErrors(e); if (Object.keys(e).length > 0) return; + setSubmitting(true); setTimeout(() => { setSubmitting(false); @@ -204,10 +259,14 @@ export function PaymentDetailsPage({ }, 1800); }; + if (isLoading) { + return ; + } + return (
{}} onSignInClick={onSignInClick} onSignOutClick={onSignOutClick} + activeCity="Melbourne" onCityChange={() => { }} onSignInClick={onSignInClick} onSignOutClick={onSignOutClick} onPassesClick={onPassesClick} onCheckoutClick={onCheckoutClick} onHomeClick={onHomeClick} onAttractionsClick={onAttractionsClick} onBlogsClick={onBlogsClick} onHowItWorksClick={onHowItWorksClick} onFAQClick={onFAQClick} onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick} @@ -219,7 +278,7 @@ export function PaymentDetailsPage({
- {/* Back */} + {/* Back Button */}
)} - - {/* 2. Billing Address */} + {/* Billing Address */}
@@ -352,26 +456,17 @@ export function PaymentDetailsPage({

Billing Address

- + { }} prefilled disabled={true} /> + { }} prefilled disabled={true} />
- - + { }} prefilled disabled={true} /> + { }} prefilled disabled={true} />
- + { }} inputMode="numeric" prefilled disabled={true} />
-
- - + { }} prefilled disabled={true} />
@@ -382,40 +477,37 @@ export function PaymentDetailsPage({ - {/* ── RIGHT: Order Summary ── */} + {/* RIGHT: Order Summary (unchanged) */}
-

Order Summary

- {/* Card info */}
-
- {order.cardType} +
+ {bookingDetails?.city?.cardMode}
-

{order.city}

- +

{bookingDetails?.city?.name}

+

- {order.cardType === 'Flexi' ? `${order.days} Attractions` : `${order.days} Days`} + {bookingDetails?.city?.cardMode.toLowerCase() === 'flexi' ? `${bookingDetails?.city?.noOfAttractions} Attractions` : `${bookingDetails?.city?.noOfDays} Days`}

{[ - { label: 'Adults', value: order.adults }, - { label: 'Children', value: order.children }, - { label: 'Qty', value: order.quantity }, + { label: 'Adults', value: bookingDetails?.city?.totalAdult }, + { label: 'Children', value: bookingDetails?.city?.totalChild }, + // { label: 'Qty', value: order.quantity }, ].map(({ label, value }) => (
{label} @@ -425,7 +517,6 @@ export function PaymentDetailsPage({
- {/* Pricing breakdown */}
@@ -448,7 +539,6 @@ export function PaymentDetailsPage({
- {/* CTA */} {submitting ? ( <> - + Processing… ) : ( From b19d3194cf50fa22580f1e5d979c433dd72b5db0 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Mon, 20 Apr 2026 16:08:50 +0530 Subject: [PATCH 14/19] integrate storeRecipentData api in the payment page --- src/Redux/services/cards.service.ts | 11 ++- src/pages/PaymentDetailsPage.tsx | 105 ++++++++++++++++++++-------- 2 files changed, 87 insertions(+), 29 deletions(-) diff --git a/src/Redux/services/cards.service.ts b/src/Redux/services/cards.service.ts index c2850cf..08116c9 100644 --- a/src/Redux/services/cards.service.ts +++ b/src/Redux/services/cards.service.ts @@ -26,11 +26,20 @@ export const cardsApi = createApi({ getCardBookingDetails: builder.query({ query: (bookingId) => `/website/passes/${bookingId}/details`, }), + + storeRecipientDetails: builder.mutation({ + query: ({ recipientDetails, bookingId }) => ({ // keep the name of the variables being passed here same as when calling the mutation hook + url: `/website/passes/${bookingId}/store-gift-details`, + method: "PUT", + body: recipientDetails + }), + }), }) }); export const { useGetCardsinCartQuery, useGetCheckoutPageDataQuery, - useGetCardBookingDetailsQuery + useGetCardBookingDetailsQuery, + useStoreRecipientDetailsMutation } = cardsApi; \ No newline at end of file diff --git a/src/pages/PaymentDetailsPage.tsx b/src/pages/PaymentDetailsPage.tsx index 49c4912..7054559 100644 --- a/src/pages/PaymentDetailsPage.tsx +++ b/src/pages/PaymentDetailsPage.tsx @@ -11,7 +11,8 @@ import { Separator } from '../components/ui/separator'; import { useGetUserProfileDetailsQuery } from '../Redux/services/profile.service'; import LoadingSpinner from '../components/LoadingSpinner'; import { useParams } from 'react-router-dom'; -import { useGetCardBookingDetailsQuery } from '../Redux/services/cards.service'; +import { useGetCardBookingDetailsQuery, useStoreRecepientDetailsMutation, useStoreRecipientDetailsMutation } from '../Redux/services/cards.service'; +import { toast } from 'sonner'; export interface CheckoutOrderItem { city: string; @@ -174,6 +175,8 @@ export function PaymentDetailsPage({ const [giftPhone, setGiftPhone] = useState(''); const [giftCity, setGiftCity] = useState(''); const [giftCountry, setGiftCountry] = useState(''); + const [giftIsd, setGiftIsd] = useState("") + const [giftMessage, setGiftMessage] = useState("") /* ── Profile Data (Same as ProfilePage) ── */ const [formData, setFormData] = useState({ @@ -191,8 +194,10 @@ export function PaymentDetailsPage({ const userId = localStorage.getItem("userId"); const { bookingId } = useParams() const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId); - const { data: bookingDetails } = useGetCardBookingDetailsQuery(bookingId); - console.log(bookingDetails) + const { data } = useGetCardBookingDetailsQuery(bookingId); + const [storeRecipientDetails, { isLoading: savingChanges }] = useStoreRecipientDetailsMutation(); + + const bookingDetails = data?.bookingDetails ?? null // Populate formData from API (exactly like ProfilePage) useEffect(() => { @@ -230,6 +235,8 @@ export function PaymentDetailsPage({ if (selectedTab === 'gift') { if (!giftFirstName.trim()) e.giftFirstName = 'Required'; if (!giftLastName.trim()) e.giftLastName = 'Required'; + if (!giftIsd.trim()) e.giftIsd = 'Required'; + if (!giftMessage.trim()) e.giftMessage = 'Required'; if (!giftEmail.trim() || !/\S+@\S+\.\S+/.test(giftEmail)) { e.giftEmail = 'Valid email required'; @@ -246,17 +253,44 @@ export function PaymentDetailsPage({ return e; }; + const recipientDetails = { + isForSelf: true, + recipientFirstName: giftFirstName, + recipientLastName: giftLastName, + recipientEmail: giftEmail, + recipientIsdCode: `+${giftIsd}`, + recipientPhone: giftPhone, + recipientCity: giftCity, + recipientCountry: giftCountry, + giftMessage: giftMessage, + }; - const handleSubmit = () => { + + const handleSaveProfile = async () => { + try { + console.log("Saving profile...", recipientDetails); + const response = await storeRecipientDetails({ recipientDetails, bookingId }); + console.log(response) + toast.success("gift details saved successfully!"); + } catch (error) { + console.error("Error saving profile:", error); + toast.error("Failed to update profile. Please try again."); + } + }; + + const handleSubmit = async () => { const e = validate(); setErrors(e); if (Object.keys(e).length > 0) return; - - setSubmitting(true); - setTimeout(() => { - setSubmitting(false); - onPaymentComplete(); - }, 1800); + try { + console.log("Saving profile...", recipientDetails); + const response = await storeRecipientDetails({ recipientDetails, bookingId }); + console.log(response) + toast.success("gift details saved successfully!"); + } catch (error) { + console.error("Error saving profile:", error); + toast.error("Failed to update profile. Please try again."); + } }; if (isLoading) { @@ -406,16 +440,15 @@ export function PaymentDetailsPage({ placeholder="Enter recipient's last name" error={errors.giftLastName} /> - + + + +
@@ -487,26 +536,26 @@ export function PaymentDetailsPage({
-
- {bookingDetails?.city?.cardMode} + {bookingDetails?.cardMode}
-

{bookingDetails?.city?.name}

- +

{bookingDetails?.name}

+

- {bookingDetails?.city?.cardMode.toLowerCase() === 'flexi' ? `${bookingDetails?.city?.noOfAttractions} Attractions` : `${bookingDetails?.city?.noOfDays} Days`} + {bookingDetails?.cardMode.toLowerCase() === 'flexi' ? `${bookingDetails?.noOfAttractions} Attractions` : `${bookingDetails?.noOfDays} Days`}

{[ - { label: 'Adults', value: bookingDetails?.city?.totalAdult }, - { label: 'Children', value: bookingDetails?.city?.totalChild }, + { label: 'Adults', value: bookingDetails?.totalAdult }, + { label: 'Children', value: bookingDetails?.totalChild }, // { label: 'Qty', value: order.quantity }, ].map(({ label, value }) => (
@@ -521,11 +570,11 @@ export function PaymentDetailsPage({
Subtotal - ${subtotal.toFixed(2)} + ${bookingDetails?.baseAmount}
GST (10%) - ${tax.toFixed(2)} + ${bookingDetails?.totalTaxAmount}
Booking fee @@ -533,7 +582,7 @@ export function PaymentDetailsPage({
Total - ${total.toFixed(2)} + ${bookingDetails?.totalAmount}
@@ -554,7 +603,7 @@ export function PaymentDetailsPage({ ) : ( <> - Complete Payment · ${total.toFixed(2)} + Complete Payment · ${bookingDetails.totalAmount} )} From a88958603e0db1710f22f7758aa8282065bdc437 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Mon, 20 Apr 2026 16:25:27 +0530 Subject: [PATCH 15/19] change the heading of paymentPage --- src/pages/CheckoutPage2.tsx | 6 +++--- src/pages/PaymentDetailsPage.tsx | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/pages/CheckoutPage2.tsx b/src/pages/CheckoutPage2.tsx index d8d6abe..32bff5b 100644 --- a/src/pages/CheckoutPage2.tsx +++ b/src/pages/CheckoutPage2.tsx @@ -254,7 +254,7 @@ function CheckoutConfigCard({
- + navigate(`/payment/390`)} className="w-full py-4 rounded-full bg-[#f95f62] text-white font-poppins text-base font-medium hover:bg-[#e8545a] transition-colors shadow-lg shadow-[#f95f62]/20"> Proceed to Pay
@@ -604,13 +604,13 @@ export function CheckoutPage2({ onClick={() => navigate(-1)} className="flex items-center gap-2 text-[#8e8e8e] hover:text-[#2a2a2a] transition-colors font-poppins text-sm font-normal mb-8" > - Back to Cart + Back

Checkout{' '} - + {city?.name || checkoutItem.city}

diff --git a/src/pages/PaymentDetailsPage.tsx b/src/pages/PaymentDetailsPage.tsx index 7054559..0f22825 100644 --- a/src/pages/PaymentDetailsPage.tsx +++ b/src/pages/PaymentDetailsPage.tsx @@ -10,7 +10,7 @@ import { Card, CardContent, CardHeader } from '../components/ui/card'; import { Separator } from '../components/ui/separator'; import { useGetUserProfileDetailsQuery } from '../Redux/services/profile.service'; import LoadingSpinner from '../components/LoadingSpinner'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useGetCardBookingDetailsQuery, useStoreRecepientDetailsMutation, useStoreRecipientDetailsMutation } from '../Redux/services/cards.service'; import { toast } from 'sonner'; @@ -190,6 +190,7 @@ export function PaymentDetailsPage({ city: '', postalCode: '' }); + const navigate = useNavigate() const userId = localStorage.getItem("userId"); const { bookingId } = useParams() @@ -314,7 +315,7 @@ export function PaymentDetailsPage({ {/* Back Button */} + {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/CheckoutPage2.tsx b/src/pages/CheckoutPage2.tsx index 32bff5b..7f8f136 100644 --- a/src/pages/CheckoutPage2.tsx +++ b/src/pages/CheckoutPage2.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { ArrowLeft, Check, Minus, Plus, ChevronDown @@ -91,15 +91,21 @@ const offersData: Record +
+ {/* Card bg */}
-
- {/* */} + {/* City image */} +
+ +
+ {/* City name - left aligned */}

{city}

+ {/* Pricing */}
From @@ -112,15 +118,18 @@ function FlexiCardPreview({ city, adultPrice, childPrice, isSelected }: { city: /Child
+ {/* Description */}

Dive into an extensive selection of thrilling destinations!

+ {/* Side tab - Flexi (pink) */}
Card Flexi
+ {/* Selected checkmark */} {isSelected && (
@@ -130,15 +139,21 @@ function FlexiCardPreview({ city, adultPrice, childPrice, isSelected }: { city: ); } -function UnlimitedCardPreview({ city, adultPrice, childPrice, isSelected }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean }) { +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 @@ -151,15 +166,18 @@ function UnlimitedCardPreview({ city, adultPrice, childPrice, isSelected }: { ci /Child
+ {/* Description */}

Dive into an extensive selection of thrilling destinations!

+ {/* Side tab - Unlimited (coral) */}
Card Unlimited
+ {/* Selected checkmark */} {isSelected && (
@@ -172,25 +190,39 @@ function UnlimitedCardPreview({ city, adultPrice, childPrice, isSelected }: { ci /* ─── CheckoutConfigCard (Exact Copy) ─── */ function CheckoutConfigCard({ item, - onChange, onProceed, }: { - item: CartItem; - onChange: (updates: Partial) => void; + item: any; onProceed: () => void; }) { - const [daysOpen, setDaysOpen] = useState(false); - const originalPrice = (item.pricePerUnit * item.quantity * 1.35); - const totalPrice = item.pricePerUnit * item.quantity; + const [dropdownOpen, setDropdownOpen] = useState(false); + const [noOfAdults, setNoOfAdults] = useState(1) + const [noOfChildren, setNoOfChildren] = useState(0) + const [noOfAttractions, setNoOfAttractions] = useState(item?.minNumber); + + const adultPrice = item?.adultPrice * noOfAdults + const childPrice = item?.childPrice * noOfChildren + const originalPrice = adultPrice + childPrice + const totalPrice = originalPrice - 20 + + useEffect(() => { + setNoOfAttractions(item?.minNumber) + }, [item]) + + const numberArray = Array.from( + { length: item?.maxNumber - item?.minNumber + 1 }, + (_, i) => item?.minNumber + i + ); + const navigate = useNavigate(); return (
-

{item.city}

+

{item?.city}

- - {item.cardType}3 + + {item?.cardType?.displayName}
@@ -198,11 +230,11 @@ function CheckoutConfigCard({
No. of Adults
- - {item.adults} -
@@ -211,11 +243,11 @@ function CheckoutConfigCard({
No. of Children
- - {item.children} -
@@ -223,19 +255,35 @@ function CheckoutConfigCard({
- {item.cardType === 'Flexi' ? 'No. of Attractions' : 'No. of Days'} + {item?.cardType?.name === 'selective_pass' ? 'No. of Attractions' : 'No. of Days'}
- - {daysOpen && ( - - {dayOptions.map((d) => ( - ))} @@ -247,8 +295,8 @@ function CheckoutConfigCard({
You Pay
- ${originalPrice.toFixed(0)} - ${totalPrice.toFixed(0)} + ${originalPrice} + ${totalPrice}
@@ -263,6 +311,233 @@ function CheckoutConfigCard({ } /* ─── MAIN CHECKOUT PAGE 2 ─── */ +export function CheckoutPage2({ + onHomeClick, + onPassesClick, + onAttractionsClick, + onBlogsClick, + onHowItWorksClick, + onFAQClick, + onPrivacyPolicyClick, + onAboutUsClick, + onContactUsClick, + onSignInClick, + onSignOutClick, + onProfileClick, + user, + currentPage, +}: any) { + const navigate = useNavigate(); + + // Default item (you can pass via props later) + + const cityId = localStorage.getItem("cityId") + const { data: checkoutPageData, isLoading } = useGetCheckoutPageDataQuery(cityId) + + const cityName = checkoutPageData?.city?.name ?? "" + const cityImage = checkoutPageData?.city?.heroBanner?.image ?? "" + const cards = checkoutPageData?.cards ?? [] + const flexiCard = checkoutPageData?.cards[0] ?? null + const unlimitedCard = checkoutPageData?.cards[1] ?? null + + const [checkoutItem, setCheckoutItem] = useState(flexiCard); + + useEffect(() => { + setCheckoutItem(flexiCard) + }, [cards]) + + console.log(checkoutItem) + + if (isLoading) { + return + } else { + console.log(flexiCard) + } + + const handleCheckoutItemChange = (cardObject: any) => { + setCheckoutItem(cardObject); + }; + + // const attractions = attractionsData[checkoutItem.city]?.[checkoutItem.cardType] || []; + // const offers = offersData[checkoutItem.cardType] || []; + + return ( +
+ { }} + onSignInClick={onSignInClick} + onSignOutClick={onSignOutClick} + onPassesClick={onPassesClick} + onCheckoutClick={() => { }} + onHomeClick={onHomeClick} + onAttractionsClick={onAttractionsClick} + onBlogsClick={onBlogsClick} + onHowItWorksClick={onHowItWorksClick} + onFAQClick={onFAQClick} + onPrivacyPolicyClick={onPrivacyPolicyClick} + onAboutUsClick={onAboutUsClick} + onProfileClick={onProfileClick} + onCityCardsClick={() => { }} + onMagicItineraryClick={() => { }} + onPostCardsClick={() => { }} + onOffersClick={() => { }} + onSuperSavingsClick={() => { }} + onEsimsClick={() => { }} + onHotelDiscountsClick={() => { }} + onCartClick={() => { }} + currentPage={currentPage} + user={user} + /> + +
+ + +
+

+ Checkout{' '} + {cityName} +

+
+ +
+ {/* Left Column */} +
+ {/* Card Type Selection */} +
+

Choose Your Card

+

Select the card type that best suits your travel style

+
+ + +
+ + {/* Features Comparison (Exact Copy) */} +
+
+

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 Section (Exact) */} + {/*
+

{checkoutItem.cardType} Card Offers

+

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

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

{offer.title}

+
+
+

{offer.description}

+
+
+
+
+ ))} +
+
*/} + + {/* Attractions Section (Exact) */} + {/*
+
+

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 */} +
+
+ navigate("/payment")} + /> +
+
+ + {/* Mobile Config Card */} + {/*
+ navigate("/payment")} + /> +
*/} +
+
+ +
+
+ ); +} + // export function CheckoutPage2({ // onHomeClick, // onPassesClick, @@ -281,46 +556,68 @@ function CheckoutConfigCard({ // }: any) { // const navigate = useNavigate(); -// // Default item (you can pass via props later) +// const cityId = localStorage.getItem("cityId"); +// const { data: checkoutPageData, isLoading } = useGetCheckoutPageDataQuery(cityId); + +// const city = checkoutPageData?.city; +// const allCards = checkoutPageData?.cards ?? []; +// const allAttractions = checkoutPageData?.attractions ?? []; +// const allOffers = checkoutPageData?.offers ?? []; + +// const baseUrl = import.meta.env.VITE_BASE_URL; + + +// // Initialize with first card (Flexi) as default +// const defaultCard = allCards[0] || null; + // const [checkoutItem, setCheckoutItem] = useState({ -// id: '1', -// city: 'Melbourne', -// cardType: 'Flexi', -// days: 3, +// id: defaultCard?.id?.toString() || '1', +// city: city?.name || 'Melbourne', +// cardType: defaultCard?.cardType?.displayName || 'Flexi', +// days: defaultCard?.validityDuration || 3, // adults: 2, // children: 1, // quantity: 1, -// pricePerUnit: 49.5, -// image: '', +// pricePerUnit: defaultCard?.adultPrice || 49.5, +// image: city?.heroBanner?.image || '', // }); -// const cityId = localStorage.getItem("cityId") -// const { data: checkoutPageData, isLoading } = useGetCheckoutPageDataQuery(cityId) - -// const cards = checkoutPageData?.cards ?? [] - // if (isLoading) { -// return -// } else { -// console.log(checkoutPageData) +// return ; // } // const handleCheckoutItemChange = (updates: Partial) => { // const updated = { ...checkoutItem, ...updates }; -// const prices = priceTable[updated.cardType]; -// if (prices && prices[updated.days] !== undefined) { -// updated.pricePerUnit = prices[updated.days]; + +// // If card type changes, update with real card data +// if (updates.cardType) { +// const selectedCard = allCards.find( +// c => c.cardType?.displayName === updates.cardType +// ); +// if (selectedCard) { +// updated.id = selectedCard.id.toString(); +// updated.days = selectedCard.validityDuration; +// updated.pricePerUnit = selectedCard.adultPrice; +// } // } + // setCheckoutItem(updated); // }; -// const attractions = attractionsData[checkoutItem.city]?.[checkoutItem.cardType] || []; -// const offers = offersData[checkoutItem.cardType] || []; +// // Get currently selected card +// const selectedCard = allCards.find(c => +// c.cardType?.displayName === checkoutItem.cardType +// ) || allCards[0]; + +// // Offers for selected card (fallback to global offers) +// const currentOffers = selectedCard?.offers?.length +// ? selectedCard.offers +// : allOffers; // return ( //
// { }} // onSignInClick={onSignInClick} // onSignOutClick={onSignOutClick} @@ -347,14 +644,19 @@ function CheckoutConfigCard({ // /> //
-// //
//

// Checkout{' '} -// {checkoutItem.city} +// +// {city?.name || checkoutItem.city} +// //

//
@@ -364,19 +666,40 @@ function CheckoutConfigCard({ // {/* Card Type Selection */} //
//

Choose Your Card

-//

Select the card type that best suits your travel style

+//

+// Select the card type that best suits your travel style +//

//
-// -// +// {allCards.map((card) => ( +// +// ))} //
-// {/* Features Comparison (Exact Copy) */} +// {/* Features Comparison - Kept as is (no CSS change) */} //
//
+// {/* Header */} //

Features

//

Flexi

//

Unlimited

@@ -396,10 +719,22 @@ function CheckoutConfigCard({ // {row.feature} //

//
-// {row.flexi ?
: } +// {row.flexi ? ( +//
+// +//
+// ) : ( +// +// )} //
//
-// {row.unlimited ?
: } +// {row.unlimited ? ( +//
+// +//
+// ) : ( +// +// )} //
// // ))} @@ -407,22 +742,34 @@ function CheckoutConfigCard({ //
//
-// {/* Offers Section (Exact) */} +// {/* Offers Section */} //
-//

{checkoutItem.cardType} Card Offers

-//

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

+//

+// {checkoutItem.cardType} Card Offers +//

+//

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

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

{offer.title}

+//

+// {offer.title} +//

//
//
-//

{offer.description}

+//

+// {offer.description} +//

//
//
//
@@ -431,24 +778,31 @@ function CheckoutConfigCard({ //
//
-// {/* Attractions Section (Exact) */} +// {/* Attractions Section */} //
//
//

Available Attractions

-// {attractions.length} included +// +// {allAttractions.length} included +// //
-//

Explore all the experiences you can enjoy with your pass

+//

+// Explore all the experiences you can enjoy with your pass +//

//
-// {attractions.map((a) => ( -//
+// {allAttractions.map((attraction) => ( +//
//
-// +// //
-//
-// {a.category} -//
//
-//
{a.name}
+//
+// {attraction.title} +//
//
//
//
@@ -492,314 +846,4 @@ function CheckoutConfigCard({ // /> //
// ); -// } - -export function CheckoutPage2({ - onHomeClick, - onPassesClick, - onAttractionsClick, - onBlogsClick, - onHowItWorksClick, - onFAQClick, - onPrivacyPolicyClick, - onAboutUsClick, - onContactUsClick, - onSignInClick, - onSignOutClick, - onProfileClick, - user, - currentPage, -}: any) { - const navigate = useNavigate(); - - const cityId = localStorage.getItem("cityId"); - const { data: checkoutPageData, isLoading } = useGetCheckoutPageDataQuery(cityId); - - const city = checkoutPageData?.city; - const allCards = checkoutPageData?.cards ?? []; - const allAttractions = checkoutPageData?.attractions ?? []; - const allOffers = checkoutPageData?.offers ?? []; - - const baseUrl = import.meta.env.VITE_BASE_URL; - - - // Initialize with first card (Flexi) as default - const defaultCard = allCards[0] || null; - - const [checkoutItem, setCheckoutItem] = useState({ - id: defaultCard?.id?.toString() || '1', - city: city?.name || 'Melbourne', - cardType: defaultCard?.cardType?.displayName || 'Flexi', - days: defaultCard?.validityDuration || 3, - adults: 2, - children: 1, - quantity: 1, - pricePerUnit: defaultCard?.adultPrice || 49.5, - image: city?.heroBanner?.image || '', - }); - - if (isLoading) { - return ; - } - - const handleCheckoutItemChange = (updates: Partial) => { - const updated = { ...checkoutItem, ...updates }; - - // If card type changes, update with real card data - if (updates.cardType) { - const selectedCard = allCards.find( - c => c.cardType?.displayName === updates.cardType - ); - if (selectedCard) { - updated.id = selectedCard.id.toString(); - updated.days = selectedCard.validityDuration; - updated.pricePerUnit = selectedCard.adultPrice; - } - } - - setCheckoutItem(updated); - }; - - // Get currently selected card - const selectedCard = allCards.find(c => - c.cardType?.displayName === checkoutItem.cardType - ) || allCards[0]; - - // Offers for selected card (fallback to global offers) - const currentOffers = selectedCard?.offers?.length - ? selectedCard.offers - : allOffers; - - return ( -
- { }} - onSignInClick={onSignInClick} - onSignOutClick={onSignOutClick} - onPassesClick={onPassesClick} - onCheckoutClick={() => { }} - onHomeClick={onHomeClick} - onAttractionsClick={onAttractionsClick} - onBlogsClick={onBlogsClick} - onHowItWorksClick={onHowItWorksClick} - onFAQClick={onFAQClick} - onPrivacyPolicyClick={onPrivacyPolicyClick} - onAboutUsClick={onAboutUsClick} - onProfileClick={onProfileClick} - onCityCardsClick={() => { }} - onMagicItineraryClick={() => { }} - onPostCardsClick={() => { }} - onOffersClick={() => { }} - onSuperSavingsClick={() => { }} - onEsimsClick={() => { }} - onHotelDiscountsClick={() => { }} - onCartClick={() => { }} - currentPage={currentPage} - user={user} - /> - -
- - -
-

- Checkout{' '} - - {city?.name || checkoutItem.city} - -

-
- -
- {/* Left Column */} -
- {/* Card Type Selection */} -
-

Choose Your Card

-

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

-
- {allCards.map((card) => ( - - ))} -
- - {/* Features Comparison - Kept as is (no CSS change) */} -
-
- {/* 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 Section */} -
-

- {checkoutItem.cardType} Card Offers -

-

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

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

- {offer.title} -

-
-
-

- {offer.description} -

-
-
-
-
- ))} -
-
- - {/* Attractions Section */} -
-
-

Available Attractions

- - {allAttractions.length} included - -
-

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

-
- {allAttractions.map((attraction) => ( -
-
- -
-
-
- {attraction.title} -
-
-
-
- ))} -
-
-
- - {/* Right Column - Config Card */} -
-
- navigate("/payment")} - /> -
-
- - {/* Mobile Config Card */} -
- navigate("/payment")} - /> -
-
-
- -
-
- ); -} \ No newline at end of file +// } \ No newline at end of file diff --git a/src/pages/PaymentDetailsPage.tsx b/src/pages/PaymentDetailsPage.tsx index 0f22825..08b5ad3 100644 --- a/src/pages/PaymentDetailsPage.tsx +++ b/src/pages/PaymentDetailsPage.tsx @@ -11,7 +11,7 @@ import { Separator } from '../components/ui/separator'; import { useGetUserProfileDetailsQuery } from '../Redux/services/profile.service'; import LoadingSpinner from '../components/LoadingSpinner'; import { useNavigate, useParams } from 'react-router-dom'; -import { useGetCardBookingDetailsQuery, useStoreRecepientDetailsMutation, useStoreRecipientDetailsMutation } from '../Redux/services/cards.service'; +import { useGetCardBookingDetailsQuery, useStoreRecipientDetailsMutation } from '../Redux/services/cards.service'; import { toast } from 'sonner'; export interface CheckoutOrderItem { From 0543627f426970845e4ca926f2e933f60ca6bdd0 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Mon, 20 Apr 2026 23:46:22 +0530 Subject: [PATCH 17/19] render the offers and attractions in checkoutPage2 --- src/pages/CheckoutPage2.tsx | 72 ++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/src/pages/CheckoutPage2.tsx b/src/pages/CheckoutPage2.tsx index 7f8f136..4897c2f 100644 --- a/src/pages/CheckoutPage2.tsx +++ b/src/pages/CheckoutPage2.tsx @@ -199,23 +199,44 @@ function CheckoutConfigCard({ const [noOfAdults, setNoOfAdults] = useState(1) const [noOfChildren, setNoOfChildren] = useState(0) const [noOfAttractions, setNoOfAttractions] = useState(item?.minNumber); + const [noOfDays, setNoOfDays] = useState(item?.minNumber) + const cityId = localStorage.getItem("cityId") + + 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 originalPrice = adultPrice + childPrice - const totalPrice = originalPrice - 20 + const basePrice = adultPrice + childPrice + const taxAmount = 10 + const strikedPrice = basePrice - 20 useEffect(() => { setNoOfAttractions(item?.minNumber) + setNoOfDays(item?.minNumber) }, [item]) const numberArray = Array.from( { length: item?.maxNumber - item?.minNumber + 1 }, (_, i) => item?.minNumber + i ); - const navigate = useNavigate(); + const cardBookingDetails = { + cityId, + cardTypeId, + cardId, + noOfAdults, + cardMode, + noOfChildren, + taxAmount, + noOfAttractions, + noOfDays + } + + + return (
@@ -259,7 +280,7 @@ function CheckoutConfigCard({
@@ -275,13 +296,14 @@ function CheckoutConfigCard({ @@ -295,8 +317,8 @@ function CheckoutConfigCard({
You Pay
- ${originalPrice} - ${totalPrice} + ${basePrice} + ${strikedPrice}
@@ -330,6 +352,7 @@ export function CheckoutPage2({ const navigate = useNavigate(); // Default item (you can pass via props later) + const baseUrl = import.meta.env.VITE_BASE_URL; const cityId = localStorage.getItem("cityId") const { data: checkoutPageData, isLoading } = useGetCheckoutPageDataQuery(cityId) @@ -339,6 +362,7 @@ export function CheckoutPage2({ const cards = checkoutPageData?.cards ?? [] const flexiCard = checkoutPageData?.cards[0] ?? null const unlimitedCard = checkoutPageData?.cards[1] ?? null + const attractions = checkoutPageData?.attractions ?? []; const [checkoutItem, setCheckoutItem] = useState(flexiCard); @@ -351,15 +375,13 @@ export function CheckoutPage2({ if (isLoading) { return } else { - console.log(flexiCard) + // console.log(flexiCard) } const handleCheckoutItemChange = (cardObject: any) => { setCheckoutItem(cardObject); }; - // const attractions = attractionsData[checkoutItem.city]?.[checkoutItem.cardType] || []; - // const offers = offersData[checkoutItem.cardType] || []; return (
@@ -452,15 +474,15 @@ export function CheckoutPage2({
{/* Offers Section (Exact) */} - {/*
-

{checkoutItem.cardType} Card Offers

-

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

+
+

{checkoutItem?.cardType?.displayName} Offers

+

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

- {offers.map((offer, idx) => ( -
+ {checkoutItem?.offers.map((offer: any) => ( +
- +

{offer.title}

@@ -473,32 +495,32 @@ export function CheckoutPage2({
))}
-
*/} +
{/* Attractions Section (Exact) */} - {/*
+

Available Attractions

{attractions.length} included

Explore all the experiences you can enjoy with your pass

- {attractions.map((a) => ( + {attractions.map((a: any) => (
- +
{a.category}
-
{a.name}
+
{a.title}
))}
-
*/} +
{/* Right Column - Config Card */} From c490a820e4907399cd3dd24148b4c8a1519bb3d6 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Tue, 21 Apr 2026 11:51:35 +0530 Subject: [PATCH 18/19] integrate add to cart api in the checkoutConfig card --- src/Redux/services/cards.service.ts | 10 +++++- src/pages/CheckoutPage2.tsx | 55 ++++++++++++++++++----------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/Redux/services/cards.service.ts b/src/Redux/services/cards.service.ts index 08116c9..fbc43e3 100644 --- a/src/Redux/services/cards.service.ts +++ b/src/Redux/services/cards.service.ts @@ -34,6 +34,13 @@ export const cardsApi = createApi({ body: recipientDetails }), }), + addCardToCart: builder.mutation({ + query: (cardBookingDetails) => ({ // keep the name of the variables being passed here same as when calling the mutation hook + url: `/website/passes/add-to-cart`, + method: "POST", + body: cardBookingDetails + }), + }), }) }); @@ -41,5 +48,6 @@ export const { useGetCardsinCartQuery, useGetCheckoutPageDataQuery, useGetCardBookingDetailsQuery, - useStoreRecipientDetailsMutation + useStoreRecipientDetailsMutation, + useAddCardToCartMutation } = cardsApi; \ No newline at end of file diff --git a/src/pages/CheckoutPage2.tsx b/src/pages/CheckoutPage2.tsx index 4897c2f..4235469 100644 --- a/src/pages/CheckoutPage2.tsx +++ b/src/pages/CheckoutPage2.tsx @@ -7,8 +7,9 @@ import Navbar from '../components/Navbar'; import { Footer } from '../components/Footer'; import { ImageWithFallback } from '../components/figma/ImageWithFallback'; import { useNavigate } from 'react-router-dom'; -import { useGetCheckoutPageDataQuery } from '../Redux/services/cards.service'; +import { useAddCardToCartMutation, useGetCheckoutPageDataQuery } from '../Redux/services/cards.service'; import LoadingSpinner from '../components/LoadingSpinner'; +import { toast } from 'sonner'; /* ─── Types ─── */ export interface CartItem { @@ -202,15 +203,17 @@ function CheckoutConfigCard({ const [noOfDays, setNoOfDays] = useState(item?.minNumber) 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 = 10 - const strikedPrice = basePrice - 20 + const taxAmount = basePrice * 0.1 + const strikedPrice = basePrice + 20 + + const [addCardToCart] = useAddCardToCartMutation() useEffect(() => { setNoOfAttractions(item?.minNumber) @@ -224,23 +227,35 @@ function CheckoutConfigCard({ const navigate = useNavigate(); const cardBookingDetails = { - cityId, - cardTypeId, - cardId, - noOfAdults, - cardMode, - noOfChildren, + cityXid: cityId, + cardTypeXid: cardTypeId, + cardXid: cardId, + cardMode, // stays as-is + totalAdult: noOfAdults, + baseAmount: basePrice, // static value taxAmount, + totalChild: noOfChildren, noOfAttractions, noOfDays + }; + + const handleProceedToPayment = async () => { + try { + console.log("Adding card to cart", cardBookingDetails); + const response = await addCardToCart(cardBookingDetails); + console.log(response) + const bookingId = response?.data?.id + navigate(`/payment/${bookingId}`) + } catch (error) { + console.error("Error adding card to cart:", error); + toast.error("Failed to move forward. Please try again."); + } } - - return (
-

{item?.city}

+

{cityName}

{item?.cardType?.displayName} @@ -296,12 +311,12 @@ function CheckoutConfigCard({
- navigate(`/payment/390`)} className="w-full py-4 rounded-full bg-[#f95f62] text-white font-poppins text-base font-medium hover:bg-[#e8545a] transition-colors shadow-lg shadow-[#f95f62]/20"> + Proceed to Pay
@@ -414,7 +429,7 @@ export function CheckoutPage2({
From 205b19ae50b0cb099deabc465173cd1cd67c2af6 Mon Sep 17 00:00:00 2001 From: Hemant Vishwakarma Date: Wed, 22 Apr 2026 16:51:15 +0530 Subject: [PATCH 19/19] Implement stripe and add success and cancel page --- package-lock.json | 54 ++++- package.json | 3 + src/AppRouter.tsx | 28 +++ src/Redux/services/cards.service.ts | 29 ++- src/pages/PaymentCancelPage.tsx | 89 ++++++++ src/pages/PaymentDetailsPage.tsx | 338 +++++++++++++++------------- src/pages/PaymentSuccessPage.tsx | 153 +++++++++++++ src/vite-env.d.ts | 1 + 8 files changed, 523 insertions(+), 172 deletions(-) create mode 100644 src/pages/PaymentCancelPage.tsx create mode 100644 src/pages/PaymentSuccessPage.tsx diff --git a/package-lock.json b/package-lock.json index 071ee2a..abfa862 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,12 +35,15 @@ "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@reduxjs/toolkit": "^2.11.2", + "@stripe/react-stripe-js": "^6.2.0", + "@stripe/stripe-js": "^9.2.0", "@tailwindcss/postcss": "^4.1.13", "@tailwindcss/vite": "^4.1.14", "class-variance-authority": "^0.7.1", "clsx": "*", "cmdk": "^1.1.1", "embla-carousel-react": "^8.6.0", + "i18n-iso-countries": "^7.14.0", "input-otp": "^1.4.2", "lucide-react": "^0.487.0", "motion": "*", @@ -2237,6 +2240,27 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@stripe/react-stripe-js": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-6.2.0.tgz", + "integrity": "sha512-GSCErjljZEQv9LaxP30xGOwstcMyyUzb5JyihXwvjOU95yrfhbiPG4K2KkwxYxn+WY0/AyHsRhPPoGRw7urBzg==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=9.2.0 <10.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-9.2.0.tgz", + "integrity": "sha512-YSzLC0t6VS9MDdPTynSMqU8IxrItFUjkDORALFT6sSMR/XZ5Vgm3RDp/Gk7z727MC4A9s4MFVel0gF0c7+kdrg==", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@swc/core": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", @@ -3073,7 +3097,6 @@ "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3084,7 +3107,6 @@ "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3095,7 +3117,6 @@ "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -3346,6 +3367,11 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/diacritics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", + "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==" + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -3360,8 +3386,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -3527,6 +3552,17 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/i18n-iso-countries": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz", + "integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==", + "dependencies": { + "diacritics": "1.3.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/immer": { "version": "11.1.4", "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", @@ -3945,7 +3981,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4003,7 +4038,6 @@ "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" }, @@ -4030,7 +4064,6 @@ "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" @@ -4066,7 +4099,6 @@ "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" @@ -4269,8 +4301,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -4533,7 +4564,6 @@ "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/package.json b/package.json index 078328a..544a147 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,15 @@ "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@reduxjs/toolkit": "^2.11.2", + "@stripe/react-stripe-js": "^6.2.0", + "@stripe/stripe-js": "^9.2.0", "@tailwindcss/postcss": "^4.1.13", "@tailwindcss/vite": "^4.1.14", "class-variance-authority": "^0.7.1", "clsx": "*", "cmdk": "^1.1.1", "embla-carousel-react": "^8.6.0", + "i18n-iso-countries": "^7.14.0", "input-otp": "^1.4.2", "lucide-react": "^0.487.0", "motion": "*", diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 0b5fb55..eba004f 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -37,6 +37,8 @@ import { PaymentDetailsPage } from './pages/PaymentDetailsPage'; import { CartPageDesign } from './pages/CartPageDesign'; import { CheckoutPage2 } from './pages/CheckoutPage2'; import { SuperSavingsDetailsPage } from './pages/SuperSavingsDetailsPage'; +import { PaymentSuccessPage } from './pages/PaymentSuccessPage'; +import { PaymentCancelPage } from './pages/PaymentCancelPage'; // User type definition interface User { @@ -302,6 +304,32 @@ export function AppRouter({ onBackClick={() => navigate(-1)} /> } /> + + + + + + } /> + + + + } /> diff --git a/src/Redux/services/cards.service.ts b/src/Redux/services/cards.service.ts index fbc43e3..efac3eb 100644 --- a/src/Redux/services/cards.service.ts +++ b/src/Redux/services/cards.service.ts @@ -41,7 +41,29 @@ export const cardsApi = createApi({ body: cardBookingDetails }), }), - }) + + payForCard: builder.mutation({ + query: (id) => ({ + url: `/website/passes/${id}/pay`, + method: "POST", + body: {}, + }), + }), + + confirmCardPayment: builder.mutation({ + query: (id) => ({ + url: `/website/passes/${id}/confirm-payment`, + method: "POST", + // body: id, + }), + }), + + + + + + + }), }); export const { @@ -49,5 +71,8 @@ export const { useGetCheckoutPageDataQuery, useGetCardBookingDetailsQuery, useStoreRecipientDetailsMutation, - useAddCardToCartMutation + useAddCardToCartMutation, + usePayForCardMutation, + useConfirmCardPaymentMutation + } = cardsApi; \ No newline at end of file diff --git a/src/pages/PaymentCancelPage.tsx b/src/pages/PaymentCancelPage.tsx new file mode 100644 index 0000000..108cf7b --- /dev/null +++ b/src/pages/PaymentCancelPage.tsx @@ -0,0 +1,89 @@ +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { XCircle } from 'lucide-react'; +import Navbar from '../components/Navbar'; +import { Footer } from '../components/Footer'; + +interface PaymentCancelPageProps { + onHomeClick: () => void; + onPassesClick: () => void; + onSignInClick: () => void; + onSignOutClick?: () => void; + currentPage?: string; + user?: { email: string; name: string } | null; +} + +export function PaymentCancelPage({ + onHomeClick, + onPassesClick, + onSignInClick, + onSignOutClick, + currentPage, + user, +}: PaymentCancelPageProps) { + const navigate = useNavigate(); + + // ✅ Clear pending booking ID when user cancels + useEffect(() => { + localStorage.removeItem('pendingBookingId'); + }, []); + + return ( +
+ {}} + onSignInClick={onSignInClick} + onSignOutClick={onSignOutClick} + onHomeClick={onHomeClick} + onPassesClick={onPassesClick} + onCheckoutClick={() => {}} + onAttractionsClick={() => {}} + onBlogsClick={() => {}} + onHowItWorksClick={() => {}} + onFAQClick={() => {}} + onPrivacyPolicyClick={() => {}} + onAboutUsClick={() => {}} + onProfileClick={() => {}} + onCityCardsClick={() => {}} + onMagicItineraryClick={() => {}} + onPostCardsClick={() => {}} + onOffersClick={() => {}} + onSuperSavingsClick={() => {}} + onEsimsClick={() => {}} + onHotelDiscountsClick={() => {}} + onCartClick={() => {}} + currentPage={currentPage as any} + user={user} + /> + +
+
+ +

Payment Cancelled

+

+ You cancelled the payment process. No charges have been made. +

+ +
+
+ +
{}} + onBlogsClick={() => {}} + onHowItWorksClick={() => {}} + onFAQClick={() => {}} + onPrivacyPolicyClick={() => {}} + onAboutUsClick={() => {}} + onContactUsClick={() => {}} + /> +
+ ); +} \ No newline at end of file diff --git a/src/pages/PaymentDetailsPage.tsx b/src/pages/PaymentDetailsPage.tsx index 08b5ad3..ed315a6 100644 --- a/src/pages/PaymentDetailsPage.tsx +++ b/src/pages/PaymentDetailsPage.tsx @@ -1,8 +1,7 @@ import React, { useEffect, useState } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { - ArrowLeft, User, MapPin, Lock, Shield, ChevronDown, - Check, AlertCircle, Pencil, UserCheck, Gift + ArrowLeft, User, Lock, Shield, Pencil, UserCheck, Gift, AlertCircle } from 'lucide-react'; import Navbar from '../components/Navbar'; import { Footer } from '../components/Footer'; @@ -11,9 +10,16 @@ import { Separator } from '../components/ui/separator'; import { useGetUserProfileDetailsQuery } from '../Redux/services/profile.service'; import LoadingSpinner from '../components/LoadingSpinner'; import { useNavigate, useParams } from 'react-router-dom'; -import { useGetCardBookingDetailsQuery, useStoreRecipientDetailsMutation } from '../Redux/services/cards.service'; +import { + useGetCardBookingDetailsQuery, + useStoreRecipientDetailsMutation, + usePayForCardMutation, +} from '../Redux/services/cards.service'; import { toast } from 'sonner'; +import countries from 'i18n-iso-countries'; +import enLocale from 'i18n-iso-countries/langs/en.json'; + export interface CheckoutOrderItem { city: string; cardType: 'Flexi' | 'Unlimited'; @@ -53,7 +59,20 @@ interface PaymentDetailsPageProps { user?: { email: string; name: string } | null; } -/* ─── Editable field ─── */ +// Register English locale for country codes +countries.registerLocale(enLocale); + +const getCountryCode = (countryName: string): string => { + const code = countries.getAlpha2Code(countryName, 'en'); + if (code) return code; + if (countryName.length === 2 && /^[A-Z]{2}$/i.test(countryName)) { + return countryName.toUpperCase(); + } + console.warn(`Unknown country name: ${countryName}, defaulting to 'AU'`); + return 'AU'; +}; + +/* ─── Editable field component ─── */ function Field({ label, value, @@ -64,7 +83,7 @@ function Field({ maxLength, inputMode, prefilled, - disabled = false, // ← New prop + disabled = false, }: { label: string; value: string; @@ -75,7 +94,7 @@ function Field({ maxLength?: number; inputMode?: React.HTMLAttributes['inputMode']; prefilled?: boolean; - disabled?: boolean; // ← Added + disabled?: boolean; }) { const [focused, setFocused] = useState(false); @@ -94,7 +113,7 @@ function Field({ placeholder={placeholder} maxLength={maxLength} inputMode={inputMode} - disabled={disabled} // ← Applied + disabled={disabled} className={`w-full border rounded-xl px-4 py-3 pr-10 font-poppins text-base font-normal text-[#2a2a2a] outline-none transition-all duration-200 placeholder:text-[#ccc] ${disabled ? 'bg-gray-100 text-gray-500 cursor-not-allowed border-gray-300' @@ -107,13 +126,10 @@ function Field({ : 'border-gray-200' }`} /> - - {/* Pencil icon only when prefilled AND not disabled AND not focused */} {prefilled && !focused && !disabled && ( )}
- {error && ( {error} @@ -135,11 +151,8 @@ function CardTypeBadge({ cardType }: { cardType: 'Flexi' | 'Unlimited' }) { ); } -/* ─── Main component ─── */ +/* ─── Main Component ─── */ export function PaymentDetailsPage({ - checkoutOrder, - onBackClick, - onPaymentComplete, onHomeClick, onPassesClick, onAttractionsClick, @@ -164,21 +177,19 @@ export function PaymentDetailsPage({ currentPage, user, }: PaymentDetailsPageProps) { - - /* ── Purchase type ── */ const [selectedTab, setSelectedTab] = useState<'myself' | 'gift'>('myself'); - /* ── Gift Recipient Details (Only editable fields) ── */ + // Gift fields const [giftFirstName, setGiftFirstName] = useState(''); const [giftLastName, setGiftLastName] = useState(''); const [giftEmail, setGiftEmail] = useState(''); const [giftPhone, setGiftPhone] = useState(''); const [giftCity, setGiftCity] = useState(''); const [giftCountry, setGiftCountry] = useState(''); - const [giftIsd, setGiftIsd] = useState("") - const [giftMessage, setGiftMessage] = useState("") + const [giftIsd, setGiftIsd] = useState(''); + const [giftMessage, setGiftMessage] = useState(''); - /* ── Profile Data (Same as ProfilePage) ── */ + // Profile data const [formData, setFormData] = useState({ firstName: '', lastName: '', @@ -188,19 +199,19 @@ export function PaymentDetailsPage({ address1: '', address2: '', city: '', - postalCode: '' + postalCode: '', }); - const navigate = useNavigate() - const userId = localStorage.getItem("userId"); - const { bookingId } = useParams() + const navigate = useNavigate(); + const userId = localStorage.getItem('userId'); + const { bookingId } = useParams(); const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId); const { data } = useGetCardBookingDetailsQuery(bookingId); - const [storeRecipientDetails, { isLoading: savingChanges }] = useStoreRecipientDetailsMutation(); + const [storeRecipientDetails] = useStoreRecipientDetailsMutation(); + const [payForCard, { isLoading: isCreatingPayment }] = usePayForCardMutation(); - const bookingDetails = data?.bookingDetails ?? null + const bookingDetails = data?.bookingDetails ?? null; - // Populate formData from API (exactly like ProfilePage) useEffect(() => { if (userDetails) { setFormData({ @@ -217,80 +228,83 @@ export function PaymentDetailsPage({ } }, [userDetails]); - /* ── Validation ── */ const [errors, setErrors] = useState>({}); - const [submitting, setSubmitting] = useState(false); - - const order = checkoutOrder || { - city: 'Melbourne', cardType: 'Flexi' as const, - days: 3, adults: 2, children: 0, quantity: 1, pricePerUnit: 49.50, - }; - - const subtotal = order.pricePerUnit * order.quantity; - const tax = subtotal * 0.1; - const total = subtotal + tax; const validate = () => { const e: Record = {}; - if (selectedTab === 'gift') { if (!giftFirstName.trim()) e.giftFirstName = 'Required'; if (!giftLastName.trim()) e.giftLastName = 'Required'; if (!giftIsd.trim()) e.giftIsd = 'Required'; if (!giftMessage.trim()) e.giftMessage = 'Required'; - if (!giftEmail.trim() || !/\S+@\S+\.\S+/.test(giftEmail)) { e.giftEmail = 'Valid email required'; } - if (!giftPhone.trim() || !/^\+?[0-9]{7,15}$/.test(giftPhone)) { e.giftPhone = 'Valid phone required'; } - if (!giftCity.trim()) e.giftCity = 'Required'; if (!giftCountry.trim()) e.giftCountry = 'Required'; } - return e; }; - const recipientDetails = { - isForSelf: true, - recipientFirstName: giftFirstName, - recipientLastName: giftLastName, - recipientEmail: giftEmail, - recipientIsdCode: `+${giftIsd}`, - recipientPhone: giftPhone, - recipientCity: giftCity, - recipientCountry: giftCountry, - giftMessage: giftMessage, - }; + const [isRedirecting, setIsRedirecting] = useState(false); - - const handleSaveProfile = async () => { - try { - console.log("Saving profile...", recipientDetails); - const response = await storeRecipientDetails({ recipientDetails, bookingId }); - console.log(response) - toast.success("gift details saved successfully!"); - } catch (error) { - console.error("Error saving profile:", error); - toast.error("Failed to update profile. Please try again."); + const handlePayment = async () => { + const validationErrors = validate(); + setErrors(validationErrors); + if (Object.keys(validationErrors).length > 0) { + toast.error('Please fill all required fields'); + return; } - }; - const handleSubmit = async () => { - const e = validate(); - setErrors(e); - if (Object.keys(e).length > 0) return; + if (selectedTab === 'gift') { + const recipientDetails = { + isForSelf: true, + recipientFirstName: giftFirstName, + recipientLastName: giftLastName, + recipientEmail: giftEmail, + recipientIsdCode: `+${giftIsd}`, + recipientPhone: giftPhone, + recipientCity: giftCity, + recipientCountry: giftCountry, + giftMessage: giftMessage, + }; + try { + await storeRecipientDetails({ recipientDetails, bookingId }).unwrap(); + toast.success('Gift details saved!'); + } catch (err) { + console.error('Failed to save gift details:', err); + toast.error('Failed to save gift details. Please try again.'); + return; + } + } + + setIsRedirecting(true); + try { - console.log("Saving profile...", recipientDetails); - const response = await storeRecipientDetails({ recipientDetails, bookingId }); - console.log(response) - toast.success("gift details saved successfully!"); - } catch (error) { - console.error("Error saving profile:", error); - toast.error("Failed to update profile. Please try again."); + const payResponse = await payForCard(bookingId).unwrap(); + console.log('payForCard response:', payResponse); + + const { checkoutPageUrl } = payResponse; + + localStorage.setItem('pendingBookingId', bookingId); + + if (!checkoutPageUrl || typeof checkoutPageUrl !== 'string') { + throw new Error('Invalid checkout URL received from server'); + } + + if (!checkoutPageUrl.startsWith('http://') && !checkoutPageUrl.startsWith('https://')) { + throw new Error('Checkout URL must start with http:// or https://'); + } + + window.location.href = checkoutPageUrl; + } catch (err: any) { + console.error('Payment initiation error:', err); + const errorMsg = err?.data?.message || err?.message || 'Failed to initiate payment. Please try again.'; + toast.error(errorMsg); + setIsRedirecting(false); } }; @@ -301,19 +315,33 @@ export function PaymentDetailsPage({ 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} + activeCity="Melbourne" + onCityChange={() => {}} + 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} />
- - {/* Back Button */} - {/* Page heading */}

- Review & - {' '} + Review & Pay

@@ -335,35 +361,33 @@ export function PaymentDetailsPage({

- Complete your purchase securely. Your payment information is protected. + Complete your purchase securely. You will be redirected to Stripe to enter your card details.

- - {/* LEFT: Forms */} - - {/* Purchase type tabs */}
- { }} prefilled disabled={true} /> - { }} prefilled disabled={true} /> + {}} prefilled disabled /> + {}} prefilled disabled />
- { }} type="email" prefilled disabled={true} /> - { }} type="tel" prefilled disabled={true} /> + {}} type="email" prefilled disabled /> + {}} type="tel" prefilled disabled />
@@ -426,7 +448,6 @@ export function PaymentDetailsPage({

Gift Recipient Details

- - - - - - -
@@ -496,6 +511,7 @@ export function PaymentDetailsPage({ )} + {/* Billing Address */} @@ -507,28 +523,23 @@ export function PaymentDetailsPage({

Billing Address

- { }} prefilled disabled={true} /> - { }} prefilled disabled={true} /> + {}} prefilled disabled /> + {}} prefilled disabled />
- { }} prefilled disabled={true} /> - { }} prefilled disabled={true} /> + {}} prefilled disabled /> + {}} prefilled disabled />
- { }} inputMode="numeric" prefilled disabled={true} /> -
-
- { }} prefilled disabled={true} /> -
-
+ {}} inputMode="numeric" prefilled disabled /> + {}} prefilled disabled />
- - {/* RIGHT: Order Summary (unchanged) */} + {/* Right Column: Order Summary & Payment Button */}
@@ -538,10 +549,13 @@ export function PaymentDetailsPage({
-
+
{bookingDetails?.cardMode}
@@ -550,21 +564,21 @@ export function PaymentDetailsPage({

- {bookingDetails?.cardMode.toLowerCase() === 'flexi' ? `${bookingDetails?.noOfAttractions} Attractions` : `${bookingDetails?.noOfDays} Days`} + {bookingDetails?.cardMode?.toLowerCase() === 'flexi' + ? `${bookingDetails?.noOfAttractions} Attractions` + : `${bookingDetails?.noOfDays} Days`}

- {[ - { label: 'Adults', value: bookingDetails?.totalAdult }, - { label: 'Children', value: bookingDetails?.totalChild }, - // { label: 'Qty', value: order.quantity }, - ].map(({ label, value }) => ( -
- {label} - {value} -
- ))} +
+ Adults + {bookingDetails?.totalAdult} +
+
+ Children + {bookingDetails?.totalChild} +
@@ -593,39 +607,47 @@ export function PaymentDetailsPage({ - {submitting ? ( + {isRedirecting ? ( <> - - Processing… + + Redirecting to Stripe... ) : ( <> - Complete Payment · ${bookingDetails?.totalAmount} + Proceed to Payment · ${bookingDetails?.totalAmount} )}

- By completing your purchase you agree to our Terms of Service and Privacy Policy + You will be redirected to Stripe’s secure checkout page to enter your card details. + By completing your purchase you agree to our Terms of Service and Privacy Policy.

-
); -} +} \ No newline at end of file diff --git a/src/pages/PaymentSuccessPage.tsx b/src/pages/PaymentSuccessPage.tsx new file mode 100644 index 0000000..b3c4361 --- /dev/null +++ b/src/pages/PaymentSuccessPage.tsx @@ -0,0 +1,153 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { CheckCircle, XCircle, Loader2 } from 'lucide-react'; +import { useConfirmCardPaymentMutation } from '../Redux/services/cards.service'; +import { toast } from 'sonner'; +import Navbar from '../components/Navbar'; +import { Footer } from '../components/Footer'; + +interface PaymentSuccessPageProps { + onHomeClick: () => void; + onPassesClick: () => void; + onSignInClick: () => void; + onSignOutClick?: () => void; + currentPage?: string; + user?: { email: string; name: string } | null; + // Add other handlers if needed (optional) +} + +export function PaymentSuccessPage({ + onHomeClick, + onPassesClick, + onSignInClick, + onSignOutClick, + currentPage, + user, +}: PaymentSuccessPageProps) { + const [searchParams] = useSearchParams(); + const sessionId = searchParams.get('session_id'); + const [confirmPayment, { isLoading }] = useConfirmCardPaymentMutation(); + const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading'); + const [errorMsg, setErrorMsg] = useState(''); + const navigate = useNavigate(); + + useEffect(() => { + const confirm = async () => { + // Retrieve bookingId from localStorage (set before redirect) + const bookingId = localStorage.getItem('pendingBookingId'); + + if (!bookingId) { + setStatus('error'); + setErrorMsg('Missing booking information. Please contact support.'); + return; + } + + if (!sessionId) { + setStatus('error'); + setErrorMsg('Missing session ID. Please contact support.'); + return; + } + + try { + // ✅ Send both bookingId and sessionId to backend + await confirmPayment({ bookingId, sessionId }).unwrap(); + setStatus('success'); + toast.success('Payment confirmed! Your order is complete.'); + // Clear the stored bookingId + localStorage.removeItem('pendingBookingId'); + } catch (err: any) { + console.error('Payment confirmation error:', err); + setStatus('error'); + setErrorMsg(err?.data?.message || 'Payment could not be confirmed. Please contact support.'); + toast.error('Payment confirmation failed'); + // Optionally clear pending booking on error to avoid infinite loops + localStorage.removeItem('pendingBookingId'); + } + }; + + confirm(); + }, [sessionId, confirmPayment]); + + return ( +
+ {}} + onSignInClick={onSignInClick} + onSignOutClick={onSignOutClick} + onHomeClick={onHomeClick} + onPassesClick={onPassesClick} + onCheckoutClick={() => {}} + onAttractionsClick={() => {}} + onBlogsClick={() => {}} + onHowItWorksClick={() => {}} + onFAQClick={() => {}} + onPrivacyPolicyClick={() => {}} + onAboutUsClick={() => {}} + onProfileClick={() => {}} + onCityCardsClick={() => {}} + onMagicItineraryClick={() => {}} + onPostCardsClick={() => {}} + onOffersClick={() => {}} + onSuperSavingsClick={() => {}} + onEsimsClick={() => {}} + onHotelDiscountsClick={() => {}} + onCartClick={() => {}} + currentPage={currentPage as any} + user={user} + /> + +
+
+ {status === 'loading' && ( + <> + +

Confirming your payment...

+

Please wait while we verify your transaction.

+ + )} + + {status === 'success' && ( + <> + +

Payment Successful!

+

Thank you for your purchase. Your order is now confirmed.

+ + + )} + + {status === 'error' && ( + <> + +

Payment Confirmation Failed

+

{errorMsg}

+ + + )} +
+
+ +
+ ); +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 8abf8d3..602f7ff 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,6 +1,7 @@ interface ImportMetaEnv { readonly VITE_BASE_URL: string readonly VITE_GOOGLE_MAP: string + readonly VITE_STRIPE_PUBLISHABLE_KEY: string } interface ImportMeta {