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 28f26f5..eba004f 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -32,6 +32,13 @@ 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'; +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 { @@ -88,7 +95,7 @@ export function AppRouter({ } /> {/* Home Route */} - @@ -124,11 +131,11 @@ export function AppRouter({ } /> {/* Checkout Routes */} - - } /> + } /> */} @@ -270,6 +277,59 @@ export function AppRouter({ } /> + + + + + } /> + + + + } /> + + + + } /> + + + + } /> + + navigate(-1)} /> + + } /> + + + + + + } /> + + + + } /> 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..efac3eb --- /dev/null +++ b/src/Redux/services/cards.service.ts @@ -0,0 +1,78 @@ + +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"] + }), + + getCheckoutPageData: builder.query({ + query: (cityId) => `/website/pass/${cityId}`, + }), + + 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 + }), + }), + 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 + }), + }), + + 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 { + useGetCardsinCartQuery, + useGetCheckoutPageDataQuery, + useGetCardBookingDetailsQuery, + useStoreRecipientDetailsMutation, + useAddCardToCartMutation, + usePayForCardMutation, + useConfirmCardPaymentMutation + +} = cardsApi; \ No newline at end of file diff --git a/src/Redux/services/cities.service.ts b/src/Redux/services/cities.service.ts index e1dfb06..c064904 100644 --- a/src/Redux/services/cities.service.ts +++ b/src/Redux/services/cities.service.ts @@ -20,11 +20,34 @@ export const citiesApi = createApi({ }), getUpcomingCities: builder.query({ - query: (listType) => `/cities/list/all?listType=${listType}`, - }) + }), + + getSelectedCityDetails: builder.query({ + query: (cityId) => `/website/${cityId}`, + }), + + getSelectedCityOffers: builder.query({ + query: ({ cityId, categoryId, page, limit }) => { + const params = new URLSearchParams() + + params.append('cityXid', cityId); + + if (categoryId) params.append('categoryXid', categoryId); + if (page) params.append('page', page); + if (limit) params.append('limit', limit); + + return `/website/super-savings/list/offers?${params.toString()}`; + } + }), + + getOfferDetailsById: builder.query({ + query: (id: number) => `/website/super-savings/list/offers/${id}`, + }), + + }), }); -export const { useGetCityListWithBannerQuery,useGetUpcomingCitiesQuery } = 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/components/CitySelectionDialog.tsx b/src/components/CitySelectionDialog.tsx index 3ede930..188d4a0 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; @@ -20,6 +21,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, @@ -32,27 +36,15 @@ export function CitySelectionDialog({ if (isLoading) { return ( -
-
-
-

Loading...

-
-
+ ); } - const handleCityClick = (city: City) => { console.log('Selected city:', city.cityName); - - // ✅ Call the onCitySelect callback if provided (passing cityId) - if (onCitySelect) { - onCitySelect(String(city.cityName)); - } else { - // ✅ Default behavior: navigate to passes page - navigate(`/passes?city=${encodeURIComponent(city.cityName)}`); - } - + navigate(`/${slugify(city.cityName)}`); + localStorage.setItem("cityId", String(city.id)) + localStorage.setItem("cityName", String(city.cityName)) onClose(); }; 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 d12ac03..5f5c6eb 100644 --- a/src/components/MelbourneCardComparison.tsx +++ b/src/components/MelbourneCardComparison.tsx @@ -1,54 +1,54 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Check, X, Star, Users, MapPin, Calendar, Clock, Zap, Eye } from 'lucide-react'; 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.

@@ -151,8 +199,8 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
Features
- {cardOptions.map((card) => ( -
+ {cardOptions.map((card,index) => ( +
{card.name}
@@ -179,9 +227,9 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar {feature.label}
- - {cardOptions.map((card) => ( -
+ + {cardOptions.map((card, index) => ( +
{renderFeatureValue(card.features[feature.key as keyof typeof card.features], card.id)}
))} @@ -196,16 +244,16 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
Ready to explore?
Compare features above
- - {cardOptions.map((card) => ( - + + {cardOptions.map((card,index) => ( +
{card.priceRange}
{card.duration}
- 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 -
-
- + diff --git a/src/pages/AttractionsPage.tsx b/src/pages/AttractionsPage.tsx index 4a7d35b..b8d3d8c 100644 --- a/src/pages/AttractionsPage.tsx +++ b/src/pages/AttractionsPage.tsx @@ -10,6 +10,7 @@ import { Checkbox } from '../components/ui/checkbox'; import { ImageWithFallback } from '../components/figma/ImageWithFallback'; import { Layout } from '../Layout'; import { useGetAttractionFiltersQuery, useGetCustomerAttractionsQuery } from '../Redux/services/attractions.service'; +import LoadingSpinner from '../components/LoadingSpinner'; interface User { email: string; name: string; @@ -29,187 +30,6 @@ interface Attraction { passType: string; } -// { -// id: '1', -// name: 'Centipede Tour - Guided Arizona Desert Tour by ATV', -// description: 'Experience the thrill of off-road adventure through the stunning Arizona desert landscape', -// image: 'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhdHYlMjBkZXNlcnQlMjB0b3VyfGVufDF8fHx8MTc1ODEwNDg5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'Paris, France', -// duration: '4 days', -// rating: 4.8, -// price: 189.25, -// category: 'adventure', -// hasReservation: true, -// reviewCount: 243, -// passType: 'unlimited' -// }, -// { -// id: '2', -// name: 'Molokini and Turtle Town Snorkeling Adventure Aboard', -// description: 'Snorkel in crystal-clear waters and swim alongside sea turtles in this unforgettable marine adventure', -// image: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'New York, USA', -// duration: '4 days', -// rating: 4.8, -// price: 225, -// category: 'adventure', -// hasReservation: false, -// reviewCount: 167, -// passType: 'selective' -// }, -// { -// id: '3', -// name: 'Westminster Walking Tour & Westminster Abbey Entry', -// description: 'Explore the heart of London with guided tours of historic Westminster and the famous Abbey', -// image: 'https://images.unsplash.com/photo-1533929736458-ca588d08c8be?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx3ZXN0bWluc3RlciUyMGFiYmV5JTIwbG9uZG9ufGVufDF8fHx8MTc1ODEwNDkwNnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'London, UK', -// duration: '4 days', -// rating: 4.8, -// price: 343, -// category: 'culture', -// hasReservation: true, -// reviewCount: 343, -// passType: 'unlimited' -// }, -// { -// id: '4', -// name: 'All Inclusive Ultimate Circle Island Day Tour with Lunch', -// description: 'Comprehensive island tour including all major attractions, lunch, and transportation', -// image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxpc2xhbmQlMjB0b3VyJTIwYWRvJTIwdHJvcGljYWx8ZW58MXx8fHwxNzU4MTA0OTEwfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'New York, USA', -// duration: '4 days', -// rating: 4.8, -// price: 225, -// category: 'adventure', -// hasReservation: false, -// reviewCount: 243, -// passType: 'unlimited' -// }, -// { -// id: '5', -// name: 'Space Center Houston Admission Ticket', -// description: 'Explore NASA\'s Johnson Space Center and discover the wonders of space exploration', -// image: 'https://images.unsplash.com/photo-1446776653964-20c1d3a81b06?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzcGFjZSUyMGNlbnRlciUyMG5hc2ElMjBob3VzdG9ufGVufDF8fHx8MTc1ODEwNDkxM3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'Paris, France', -// duration: '4 days', -// rating: 4.8, -// price: 225, -// category: 'family', -// hasReservation: true, -// reviewCount: 243, -// passType: 'selective' -// }, -// { -// id: '6', -// name: 'Melbourne Skydeck Observatory', -// description: 'Experience breathtaking 360-degree views from the Southern Hemisphere\'s highest viewing platform', -// image: 'https://images.unsplash.com/photo-1677200922658-d0df5b2ac91e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBhdHRyYWN0aW9ucyUyMGZhbW91cyUyMGxhbmRtYXJrc3xlbnwxfHx8fDE3NTc0MDEwODV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'Melbourne CBD', -// duration: '2 hours', -// rating: 4.5, -// price: 25, -// category: 'adventure', -// hasReservation: true, -// reviewCount: 892, -// passType: 'selective' -// }, -// { -// id: '7', -// name: 'Royal Botanic Gardens Melbourne', -// description: 'Explore 38 hectares of stunning gardens featuring over 8,500 species of plants', -// image: 'https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zfGVufDF8fHx8MTc1NzMzNzc4OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'South Yarra', -// duration: '3 hours', -// rating: 4.7, -// price: 0, -// category: 'nature', -// hasReservation: false, -// reviewCount: 1245, -// passType: 'selective' -// }, -// { -// id: '8', -// name: 'Federation Square Cultural Precinct', -// description: 'Melbourne\'s cultural precinct featuring galleries, museums, and unique architecture', -// image: 'https://images.unsplash.com/photo-1580688027085-8220709e3d84?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxmZWRlcmF0aW9uJTIwc3F1YXJlJTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5Mnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'Melbourne CBD', -// duration: '3 hours', -// rating: 4.3, -// price: 0, -// category: 'culture', -// hasReservation: true, -// reviewCount: 672, -// passType: 'unlimited' -// }, -// { -// id: '9', -// name: 'St Kilda Pier & Little Penguins', -// description: 'Watch little penguins return home at sunset while enjoying the scenic pier', -// image: 'https://images.unsplash.com/photo-1597889790884-2bb63cfbd4f6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzdCUyMGtpbGRhJTIwcGllciUyMG1lbGJvdXJuZXxlbnwxfHx8fDE3NTc0MDEwOTV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'St Kilda', -// duration: '2 hours', -// rating: 4.4, -// price: 0, -// category: 'nature', -// hasReservation: false, -// reviewCount: 543, -// passType: 'unlimited' -// }, -// { -// id: '10', -// name: 'Queen Victoria Market Experience', -// description: 'Historic market offering fresh produce, gourmet foods, and unique souvenirs', -// image: 'https://images.unsplash.com/photo-1676454953709-e0be46f62490?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0JTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5OHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'Melbourne CBD', -// duration: '2 hours', -// rating: 4.6, -// price: 0, -// category: 'culture', -// hasReservation: true, -// reviewCount: 987, -// passType: 'selective' -// }, -// { -// id: '11', -// name: 'Melbourne Zoo Adventure', -// description: 'Meet over 320 animal species from around the world in naturalistic habitats', -// image: 'https://images.unsplash.com/photo-1681429477985-30dc7b88dd5b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjB6b28lMjBhbmltYWxzfGVufDF8fHx8MTc1NzMzNzgxMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'Parkville', -// duration: '4 hours', -// rating: 4.5, -// price: 40, -// category: 'family', -// hasReservation: false, -// reviewCount: 1156, -// passType: 'selective' -// }, -// { -// id: '12', -// name: 'Great Ocean Road Day Tour', -// description: 'Scenic coastal drive featuring the famous Twelve Apostles and stunning ocean views', -// image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZCUyMGF1c3RyYWxpYXxlbnwxfHx8fDE3NTgxMDQ5Mzd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', -// location: 'Great Ocean Road', -// duration: '12 hours', -// rating: 4.9, -// price: 85, -// category: 'adventure', -// hasReservation: true, -// reviewCount: 678, -// passType: 'unlimited' -// } -// ]; -// const filterCategories = [ -// { value: 'with-reservation', label: 'With Reservation', count: 3 }, -// { value: 'without-reservation', label: 'Without Reservation', count: 3 }, -// { value: 'beach', label: 'Beach', count: 3 }, -// { value: 'adventure', label: 'Adventure', count: 3 }, -// { value: 'mountains', label: 'Mountains', count: 3 }, -// { value: 'family', label: 'Family Friendly', count: 3 } -// ]; -// const passTypeCategories = [ -// { value: 'selective', label: 'Flexi Pass', count: 6 }, -// { value: 'unlimited', label: 'Unlimited Pass', count: 6 } -// ]; interface AttractionsPageProps { onSignInClick: () => void; onSignOutClick?: () => void; @@ -229,7 +49,10 @@ export function AttractionsPage({ const [selectedCategory, setSelectedCategory] = useState(null); const [selectedPassType, setSelectedPassType] = useState(null); - const cityId = 1 + const cityId = localStorage.getItem("cityId") + const cityName = localStorage.getItem("cityName") + + console.log(cityName) const { data: filterData, isLoading } = useGetAttractionFiltersQuery(cityId) const { data: attractions } = useGetCustomerAttractionsQuery({ @@ -242,12 +65,7 @@ export function AttractionsPage({ if (isLoading) { return ( -
-
-
-

Loading...

-
-
+ ); } @@ -308,12 +126,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 +241,7 @@ export function AttractionsPage({
{/* Header */}
-

Attractions in Melbourne

+

Attractions in {cityName}

{/* Results count */}

Showing {showingFrom}-{showingTo} of {totalItems} Item(s) diff --git a/src/pages/CartPage.tsx b/src/pages/CartPage.tsx new file mode 100644 index 0000000..dc61438 --- /dev/null +++ b/src/pages/CartPage.tsx @@ -0,0 +1,889 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'motion/react'; +import { + Users, Baby, ShoppingBag, Trash2, Check, CreditCard, Mail, + ChevronRight, ChevronDown, Minus, Plus, Calendar, ArrowLeft, MapPin, + Zap, Shield, Clock, Percent, Sparkles +} from 'lucide-react'; +import Navbar from '../components/Navbar'; +import { Footer } from '../components/Footer'; +import { ImageWithFallback } from '../components/figma/ImageWithFallback'; +import { useNavigate } from 'react-router-dom'; +import { useGetCardsinCartQuery } from '../Redux/services/cards.service'; +import LoadingSpinner from '../components/LoadingSpinner'; +// 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; + + const navigate = useNavigate() + + 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 */} +
+ 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" + > + 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 navigate = useNavigate() + + const cityId = localStorage.getItem("cityId") + + const { data, isLoading } = useGetCardsinCartQuery(cityId) + + const CartItems = data?.cartItems ?? [] + + if (isLoading) { + return ( + + ) + } + + 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 = (selectedItemId:number) => { + // const item = cartItems.find(i => i.id === selectedCardId); + // if (item) { + // setCheckoutItem({ ...item }); + // setView('checkout'); + // window.scrollTo({ top: 0, behavior: 'smooth' }); + // } + navigate(`/payment/${selectedItemId}`) + }; + + 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: 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} + /> + + + {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: any) => { + 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?.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

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

{city}

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

+ Dive into an extensive selection of thrilling destinations! +

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

{city}

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

+ Dive into an extensive selection of thrilling destinations! +

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

{item.city}

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

+ Your{' '} + Cart +

+

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

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

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

+

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

+ + ) : ( +

Tap a card above to select it for checkout

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

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

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

+ Choose Your Card +

+

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

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

Features

+

Flexi

+

Unlimited

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

+ {row.feature} +

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

+ {checkoutItem.cardType} Card Offers +

+

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

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

+ {offer.title} +

+
+
+

+ {offer.description} +

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

Available Attractions

+ {attractions.length} included +
+

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

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

{title}

+

{description}

+ {actionLabel} +
+ ); +} \ No newline at end of file diff --git a/src/pages/CheckoutPage.tsx b/src/pages/CheckoutPage.tsx index 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/CheckoutPage2.tsx b/src/pages/CheckoutPage2.tsx new file mode 100644 index 0000000..4235469 --- /dev/null +++ b/src/pages/CheckoutPage2.tsx @@ -0,0 +1,886 @@ +import React, { useEffect, 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 { useAddCardToCartMutation, useGetCheckoutPageDataQuery } from '../Redux/services/cards.service'; +import LoadingSpinner from '../components/LoadingSpinner'; +import { toast } from 'sonner'; + +/* ─── 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, image }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean, image: string; }) { + return ( +
+ {/* Card bg */} +
+ {/* City image */} +
+ +
+ {/* City name - left aligned */} +
+

{city}

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

+ Dive into an extensive selection of thrilling destinations! +

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

{city}

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

+ Dive into an extensive selection of thrilling destinations! +

+
+ {/* Side tab - Unlimited (coral) */} +
+ Card + Unlimited +
+ {/* Selected checkmark */} + {isSelected && ( +
+ +
+ )} +
+ ); +} + +/* ─── CheckoutConfigCard (Exact Copy) ─── */ +function CheckoutConfigCard({ + item, + onProceed, +}: { + item: any; + onProceed: () => void; +}) { + const [dropdownOpen, setDropdownOpen] = useState(false); + const [noOfAdults, setNoOfAdults] = useState(1) + const [noOfChildren, setNoOfChildren] = useState(0) + const [noOfAttractions, setNoOfAttractions] = useState(item?.minNumber); + const [noOfDays, setNoOfDays] = useState(item?.minNumber) + + const cityId = localStorage.getItem("cityId") + const cityName = localStorage.getItem("cityName") + const cardTypeId = item?.cardType?.id + const cardId = item?.id + const cardMode = item?.cardType?.name === "selective_pass" ? "flexi" : "unlimited" + const adultPrice = item?.adultPrice * noOfAdults + const childPrice = item?.childPrice * noOfChildren + const basePrice = adultPrice + childPrice + const taxAmount = basePrice * 0.1 + const strikedPrice = basePrice + 20 + + const [addCardToCart] = useAddCardToCartMutation() + + 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 = { + 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 ( +
+
+

{cityName}

+
+ + {item?.cardType?.displayName} + +
+
+
+
+ No. of Adults +
+ + {noOfAdults} + +
+
+ +
+ No. of Children +
+ + {noOfChildren} + +
+
+ +
+ + {item?.cardType?.name === 'selective_pass' ? 'No. of Attractions' : 'No. of Days'} + +
+ + + {dropdownOpen && ( + + {numberArray.map((i) => ( + + ))} + + )} + +
+
+ +
+ You Pay +
+ ${strikedPrice} + ${basePrice} +
+
+
+ +
+ + 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 baseUrl = import.meta.env.VITE_BASE_URL; + + 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 attractions = checkoutPageData?.attractions ?? []; + + 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); + }; + + + 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?.displayName} Offers

+

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

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

{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: any) => ( +
+
+ +
+
+ {a.category} +
+
+
{a.title}
+
+
+
+ ))} +
+
+
+ + {/* 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 diff --git a/src/pages/MelbournePage.tsx b/src/pages/MelbournePage.tsx index b738ea7..9f2d198 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,11 @@ 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'; +import LoadingSpinner from '../components/LoadingSpinner'; interface User { email: string; @@ -149,6 +148,20 @@ 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 ( + + ) + } + + 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]; @@ -254,12 +267,12 @@ export function MelbournePage({ {/* Attractions Section */}
- +
{/* Pass Comparison */}
- +
{/* Tour Overview */} @@ -737,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 }} 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 */}
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 new file mode 100644 index 0000000..ed315a6 --- /dev/null +++ b/src/pages/PaymentDetailsPage.tsx @@ -0,0 +1,653 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'motion/react'; +import { + ArrowLeft, User, Lock, Shield, Pencil, UserCheck, Gift, AlertCircle +} 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'; +import { useGetUserProfileDetailsQuery } from '../Redux/services/profile.service'; +import LoadingSpinner from '../components/LoadingSpinner'; +import { useNavigate, useParams } from 'react-router-dom'; +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'; + 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; +} + +// 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, + onChange, + placeholder, + type = 'text', + error, + maxLength, + inputMode, + prefilled, + disabled = false, +}: { + label: string; + value: string; + onChange: (v: string) => void; + placeholder?: string; + type?: string; + error?: string; + maxLength?: number; + inputMode?: React.HTMLAttributes['inputMode']; + prefilled?: boolean; + disabled?: boolean; +}) { + const [focused, setFocused] = useState(false); + + return ( +
+ +
+ onChange(e.target.value)} + onFocus={() => setFocused(true)} + onBlur={() => setFocused(false)} + placeholder={placeholder} + maxLength={maxLength} + inputMode={inputMode} + 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' + : 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 && !disabled && ( + + )} +
+ {error && ( + + {error} + + )} +
+ ); +} + +/* ─── Card type badge ─── */ +function CardTypeBadge({ cardType }: { cardType: 'Flexi' | 'Unlimited' }) { + return ( + + {cardType} Card + + ); +} + +/* ─── Main Component ─── */ +export function PaymentDetailsPage({ + onHomeClick, + onPassesClick, + onAttractionsClick, + onBlogsClick, + onHowItWorksClick, + onFAQClick, + onPrivacyPolicyClick, + onAboutUsClick, + onProfileClick, + onCityCardsClick, + onMagicItineraryClick, + onPostCardsClick, + onOffersClick, + onSuperSavingsClick, + onEsimsClick, + onHotelDiscountsClick, + onContactUsClick, + onCartClick, + onCheckoutClick, + onSignInClick, + onSignOutClick, + currentPage, + user, +}: PaymentDetailsPageProps) { + const [selectedTab, setSelectedTab] = useState<'myself' | 'gift'>('myself'); + + // 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(''); + + // Profile data + const [formData, setFormData] = useState({ + firstName: '', + lastName: '', + email: '', + phone: '', + country: '', + address1: '', + address2: '', + city: '', + postalCode: '', + }); + + const navigate = useNavigate(); + const userId = localStorage.getItem('userId'); + const { bookingId } = useParams(); + const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId); + const { data } = useGetCardBookingDetailsQuery(bookingId); + const [storeRecipientDetails] = useStoreRecipientDetailsMutation(); + const [payForCard, { isLoading: isCreatingPayment }] = usePayForCardMutation(); + + const bookingDetails = data?.bookingDetails ?? null; + + 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]); + + const [errors, setErrors] = useState>({}); + + 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 [isRedirecting, setIsRedirecting] = useState(false); + + const handlePayment = async () => { + const validationErrors = validate(); + setErrors(validationErrors); + if (Object.keys(validationErrors).length > 0) { + toast.error('Please fill all required fields'); + 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 { + 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); + } + }; + + if (isLoading) { + return ; + } + + 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} + /> + +
+ + + +
+

+ Review & + Pay +

+
+ + SSL Secured +
+
+

+ Complete your purchase securely. You will be redirected to Stripe to enter your card details. +

+
+ +
+ + + +
+ + +
+
+ + + +
+ +
+

+ Details pre-filled from your profile.{' '} + {selectedTab === 'myself' ? 'Personal & billing details are locked.' : 'Only gift recipient details are editable.'} +

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

Personal Information

+
+
+ {}} prefilled disabled /> + {}} prefilled disabled /> +
+
+ {}} type="email" prefilled disabled /> + {}} type="tel" prefilled disabled /> +
+
+ + {/* Gift Section */} + + {selectedTab === 'gift' && ( + +
+
+ +

Gift Recipient Details

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

Billing Address

+
+
+ {}} prefilled disabled /> + {}} prefilled disabled /> +
+ {}} prefilled disabled /> + {}} prefilled disabled /> +
+
+ {}} inputMode="numeric" prefilled disabled /> + {}} prefilled disabled /> +
+
+
+
+
+
+ + {/* Right Column: Order Summary & Payment Button */} + +
+
+
+

Order Summary

+
+ +
+
+
+ {bookingDetails?.cardMode} +
+
+
+

{bookingDetails?.name}

+ +
+

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

+
+
+
+
+ Adults + {bookingDetails?.totalAdult} +
+
+ Children + {bookingDetails?.totalChild} +
+
+
+ +
+
+
+ Subtotal + ${bookingDetails?.baseAmount} +
+
+ GST (10%) + ${bookingDetails?.totalTaxAmount} +
+
+ Booking fee + Free +
+
+ Total + ${bookingDetails?.totalAmount} +
+
+
+
+ + + {isRedirecting ? ( + <> + + Redirecting to Stripe... + + ) : ( + <> + + Proceed to Payment · ${bookingDetails?.totalAmount} + + )} + + +

+ 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}

+ + + )} +
+
+ +
{}} + onBlogsClick={() => {}} + onHowItWorksClick={() => {}} + onFAQClick={() => {}} + onPrivacyPolicyClick={() => {}} + onAboutUsClick={() => {}} + onContactUsClick={() => {}} + /> +
+ ); +} \ No newline at end of file diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index d8f91c8..2295a7d 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -29,6 +29,7 @@ import { ImageWithFallback } from '../components/figma/ImageWithFallback'; import { useGetUserProfileDetailsQuery, useUpdateUserProfileDetailsMutation } from '../Redux/services/profile.service'; import { toast } from 'sonner'; import { useNavigate } from 'react-router-dom'; +import LoadingSpinner from '../components/LoadingSpinner'; interface ProfilePageProps { onBackClick: () => void; @@ -208,12 +209,7 @@ export function ProfilePage({ if (isLoading && loadingPasses) { return ( -
-
-
-

Loading...

-
-
+ ); } diff --git a/src/pages/SuperSavingsDetailsPage.tsx b/src/pages/SuperSavingsDetailsPage.tsx new file mode 100644 index 0000000..b3b65fb --- /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 eb8263d..40be9f3 100644 --- a/src/pages/SuperSavingsPage.tsx +++ b/src/pages/SuperSavingsPage.tsx @@ -15,6 +15,9 @@ import { MobileAppSection } from '../components/MobileAppSection'; import { ReviewsSection } from '../components/ReviewsSection'; 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; @@ -44,138 +47,6 @@ interface SuperSavingsPageProps { user?: { email: string; name: string; } | null; } -// Mock super savings data -const savingsData = [ - { - id: '1', - business: 'Grand Hotels Melbourne', - title: 'Up to 50% Off on luxury hotel stays across Melbourne', - discount: '50% OFF', - savedAmount: 'Save up to $300', - image: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=400', - category: 'hotels' - }, - { - id: '2', - business: 'Adventure Tours', - title: '40% Off on guided adventure tours and experiences', - discount: '40% OFF', - savedAmount: 'Save up to $150', - image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=400', - category: 'tours' - }, - { - id: '3', - business: 'Premium Spa & Wellness', - title: '45% Off on spa packages and wellness treatments', - discount: '45% OFF', - savedAmount: 'Save up to $200', - image: 'https://images.unsplash.com/photo-1544161515-4ab6ce6db874?w=400', - category: 'wellness' - }, - { - id: '4', - business: 'Culinary Delights', - title: '35% Off on fine dining at Michelin-starred restaurants', - discount: '35% OFF', - savedAmount: 'Save up to $120', - image: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400', - category: 'dining' - }, - { - id: '5', - business: 'Entertainment Pass', - title: '60% Off on theater shows and concert tickets', - discount: '60% OFF', - savedAmount: 'Save up to $250', - image: 'https://images.unsplash.com/photo-1514306191717-452ec28c7814?w=400', - category: 'entertainment' - }, - { - id: '6', - business: 'Museum Pass', - title: '55% Off on museum entries and special exhibitions', - discount: '55% OFF', - savedAmount: 'Save up to $180', - image: 'https://images.unsplash.com/photo-1566127992631-137a642a90f4?w=400', - category: 'museums' - }, - { - id: '7', - business: 'Luxury Shopping', - title: '30% Off on designer boutiques and luxury shopping', - discount: '30% OFF', - savedAmount: 'Save up to $500', - image: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=400', - category: 'shopping' - }, - { - id: '8', - business: 'Water Sports', - title: '45% Off on water sports and beach activities', - discount: '45% OFF', - savedAmount: 'Save up to $175', - image: 'https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=400', - category: 'sports' - }, - { - id: '9', - business: 'Wine Tasting Tours', - title: '40% Off on wine country tours and tastings', - discount: '40% OFF', - savedAmount: 'Save up to $160', - image: 'https://images.unsplash.com/photo-1506377247377-2a5b3b417ebb?w=400', - category: 'tours' - }, - { - id: '10', - business: 'Family Fun Parks', - title: '50% Off on family entertainment and theme parks', - discount: '50% OFF', - savedAmount: 'Save up to $220', - image: 'https://images.unsplash.com/photo-1524850011238-e3d235c7d4c9?w=400', - category: 'entertainment' - }, - { - id: '11', - business: 'Boutique Stays', - title: '55% Off on boutique hotels and bed & breakfasts', - discount: '55% OFF', - savedAmount: 'Save up to $280', - image: 'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=400', - category: 'hotels' - }, - { - id: '12', - business: 'Art Galleries', - title: '35% Off on contemporary art galleries and workshops', - discount: '35% OFF', - savedAmount: 'Save up to $140', - image: 'https://images.unsplash.com/photo-1561214115-f2f134cc4912?w=400', - category: 'museums' - }, - { - id: '13', - business: 'Luxury Cruises', - title: '65% Off on harbor cruises and yacht experiences', - discount: '65% OFF', - savedAmount: 'Save up to $400', - image: 'https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=400', - category: 'tours' - } -]; - -const filterCategories = [ - { value: 'hotels', label: 'Hotels', count: 2 }, - { value: 'tours', label: 'Tours', count: 3 }, - { value: 'wellness', label: 'Wellness', count: 1 }, - { value: 'dining', label: 'Dining', count: 1 }, - { value: 'entertainment', label: 'Entertainment', count: 2 }, - { value: 'museums', label: 'Museums', count: 2 }, - { value: 'shopping', label: 'Shopping', count: 1 }, - { value: 'sports', label: 'Sports', count: 1 } -]; - // Categories data for the Super Savings Categories section const categoriesData = [ { @@ -242,504 +113,506 @@ export function SuperSavingsPage({ currentPage, user }: SuperSavingsPageProps) { - const [searchQuery, setSearchQuery] = useState(''); - const [selectedCategories, setSelectedCategories] = useState([]); - const [currentPage_, setCurrentPage_] = useState(1); - const [showLoadMore, setShowLoadMore] = useState(true); - const toggleCategory = (category: string) => { - setSelectedCategories(prev => - prev.includes(category) - ? prev.filter(c => c !== category) - : [...prev, category] - ); - }; + const navigate = useNavigate(); + const [categoryId, setCategoryId] = useState(null) + const [page, setPage] = useState(1) + const [limit, setLimit] = useState(4) + const cityId = localStorage.getItem("cityId") + const { data, isLoading } = useGetSelectedCityOffersQuery({ cityId, categoryId, page, limit }) - const filteredSavings = savingsData.filter(saving => { - const matchesSearch = saving.title.toLowerCase().includes(searchQuery.toLowerCase()) || - saving.business.toLowerCase().includes(searchQuery.toLowerCase()); - const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(saving.category); + // optional chaining ensures no crash if data is undefined + const offers = data?.offers ?? []; + const categories = data?.categories ?? []; - return matchesSearch && matchesCategory; - }); + const totalOffers = data?.paginationData.total ?? 0; + const totalPages = Math.ceil(totalOffers / limit); - const itemsPerPage = 12; - const displayedSavings = filteredSavings.slice(0, currentPage_ * itemsPerPage); - const hasMoreItems = filteredSavings.length > displayedSavings.length; + const baseUrl = import.meta.env.VITE_BASE_URL; - const handleLoadMore = () => { - setCurrentPage_(prev => prev + 1); - if (!hasMoreItems) setShowLoadMore(false); - }; - - // Show different layouts based on login state - if (!user) { - // Not logged in - show marketing/landing page + if (isLoading) { return ( - -
+ + ); + } - {/* Hero Section */} -
- {/* Background gradient */} -
- -
-
+ return ( + +
-
- + {/* Background gradient */} +
+ +
+
+ +
+ +

+ Unlock{' '} + + Super Savings + +

+

+ Experience incredible discounts up to 65% off on premium experiences, luxury stays, and unforgettable attractions. +

+ -
-
+ 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) => ( + + navigate(`/super-savings/${offer.id}`)}> + {/* 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 */} +
+ +
+ +
+ + ); -
- - ); - } } 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 {