Merge pull request 'main' (#7) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 25s

Reviewed-on: #7
This commit is contained in:
2026-04-22 11:23:27 +00:00
26 changed files with 4986 additions and 1163 deletions

54
package-lock.json generated
View File

@@ -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",

View File

@@ -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": "*",

View File

@@ -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 */}
<Route path="/melbourne" element={
<Route path="/:cityName" element={
<motion.div key="home" {...pageTransition}>
<MelbournePage {...commonNavHandlers} />
</motion.div>
@@ -124,11 +131,11 @@ export function AppRouter({
} />
{/* Checkout Routes */}
<Route path="/checkout" element={
{/* <Route path="/checkout" element={
<motion.div key="checkout" {...pageTransition}>
<CheckoutPage {...commonNavHandlers} />
</motion.div>
} />
} /> */}
<Route path="/secure-checkout" element={
<motion.div key="secure-checkout" {...pageTransition}>
@@ -270,6 +277,59 @@ export function AppRouter({
<SuperSavingsPage {...commonNavHandlers} />
</motion.div>
} />
<Route path="/cart" element={
<motion.div key="super-savings" {...pageTransition}>
<CartPage {...commonNavHandlers} />
</motion.div>
} />
<Route path="/checkout" element={
<motion.div key="super-savings" {...pageTransition}>
<CheckoutPage2 {...commonNavHandlers} />
</motion.div>
} />
<Route path="/cart-design" element={
<motion.div key="super-savings" {...pageTransition}>
<CartPageDesign {...commonNavHandlers} />
</motion.div>
} />
<Route path="/payment/:bookingId" element={
<motion.div key="super-savings" {...pageTransition}>
<PaymentDetailsPage {...commonNavHandlers} />
</motion.div>
} />
<Route path="/super-savings/:id" element={
<motion.div key="super-savings" {...pageTransition}>
<SuperSavingsDetailsPage {...commonNavHandlers}
onBackClick={() => navigate(-1)} />
</motion.div>
} />
<Route path="/success" element={
<motion.div key="super-savings" {...pageTransition}>
<PaymentSuccessPage
// onHomeClick={onHomeClick}
// onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
currentPage="success"
user={user}
/>
</motion.div>
} />
<Route path="/cancel" element={
<motion.div key="super-savings" {...pageTransition}>
<PaymentCancelPage
// onHomeClick={onHomeClick}
// onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
currentPage="cancel"
user={user}
/>
</motion.div>
} />
</Routes>
</AnimatePresence>
</>

View File

@@ -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<typeof store.getState>;

View File

@@ -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;

View File

@@ -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;
export const { useGetCityListWithBannerQuery, useGetUpcomingCitiesQuery, useGetSelectedCityDetailsQuery, useGetSelectedCityOffersQuery, useGetOfferDetailsByIdQuery } = citiesApi;

View File

@@ -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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#F95F62] mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
<LoadingSpinner/>
);
}
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();
};

View File

@@ -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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#F95F62] mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
<LoadingSpinner/>
);
}

View File

@@ -0,0 +1,12 @@
const LoadingSpinner = () => {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#F95F62] mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
)
}
export default LoadingSpinner

View File

@@ -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<string>('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 ? (
@@ -151,8 +199,8 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
<div className="font-poppins text-xl font-semibold text-gray-900">
Features
</div>
{cardOptions.map((card) => (
<div key={card.id} className="text-center">
{cardOptions.map((card,index) => (
<div key={card.id ?? index} className="text-center">
<div className="mb-2">
<div className="font-poppins font-semibold text-2xl" style={{ color: '#F95F62' }}>{card.name}</div>
</div>
@@ -180,8 +228,8 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
<span className="font-medium text-gray-900">{feature.label}</span>
</div>
{cardOptions.map((card) => (
<div key={card.id} className="text-center">
{cardOptions.map((card, index) => (
<div key={card.id ?? index} className="text-center">
{renderFeatureValue(card.features[feature.key as keyof typeof card.features], card.id)}
</div>
))}
@@ -197,15 +245,15 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
<div className="text-xs text-gray-500">Compare features above</div>
</div>
{cardOptions.map((card) => (
<motion.div key={card.id} className="text-center">
{cardOptions.map((card,index) => (
<motion.div key={card.id ?? index} className="text-center">
<div className="mb-4">
<div className="text-3xl font-bold text-gray-900">{card.priceRange}</div>
<div className="text-sm text-gray-600">{card.duration}</div>
</div>
<Button
withShine={true}
className="w-full h-14 rounded-2xl text-white font-semibold text-lg hover:scale-105 transition-all duration-300 shadow-lg hover:shadow-xl"
className="w-full h-14 rounded-2xl text-white font-semibold text-lg hover:scale-105 transition-all duration-300 shadow-lg hover:shadow-xl cursor-pointer"
style={{ backgroundColor: '#F95F62' }}
onClick={onCheckoutClick}
>

View File

@@ -8,7 +8,7 @@ import { ImageWithFallback } from './figma/ImageWithFallback';
import { CTAButton } from './CTAButton';
import logoImage from '../assets/cit-logo.png';
import melbourneLogo from '../assets/melbourne-logo.png';
import { CitySelectionDialog } from './CitySelectionDialog';
import { CitySelectionDialog, slugify } from './CitySelectionDialog';
import { useAuth } from '../context/AuthContext';
import { LoginModal } from './LoginModal';
@@ -89,7 +89,16 @@ export default function Navbar({
const [lastKnownCity, setLastKnownCity] = useState<'landing' | 'melbourne'>('landing');
const [isLoginOpen, setLoginOpen] = useState(false);
const { user, login, logout } = useAuth(); // from AuthContext
const { user, logout } = useAuth(); // from AuthContext
const cityLogo = sessionStorage.getItem("cityLogo")
const cityId = localStorage.getItem("cityId")
const cityName = localStorage.getItem("cityName")
// const citySelected = location.pathname.includes(slugify(cityName) || "")
const citySelected = cityName
const baseUrl = import.meta.env.VITE_BASE_URL;
const protectedPaths = ["/passes", "/whats-included", "/", "/melbourne"];
@@ -129,13 +138,13 @@ export default function Navbar({
isShared: false
},
// Position 4 - Shared item
{
label: 'Your Card',
path: '/passes',
isShared: true,
landingLabel: 'Your Card',
melbourneLabel: 'Your Card'
},
// {
// label: 'Your Card',
// path: '/passes',
// isShared: true,
// landingLabel: 'Your Card',
// melbourneLabel: 'Your Card'
// },
// Position 5
{
label: 'FAQ',
@@ -154,25 +163,25 @@ export default function Navbar({
// Position 1
{
label: 'Attractions',
path: '/attractions',
path: `/attractions`,
isShared: false
},
// Position 2
{
label: 'Magic Itinerary',
path: '/magic-itinerary',
path: `/magic-itinerary`,
isShared: false
},
// Position 3
{
label: 'Super Savings',
path: '/super-savings',
path: `/super-savings`,
isShared: false
},
// Position 4 - Shared item
{
label: 'How It Works',
path: '/how-it-works',
path: `/how-it-works`,
isShared: true,
landingLabel: 'Discover',
melbourneLabel: 'How It Works'
@@ -180,14 +189,14 @@ export default function Navbar({
// Position 5 - Shared item
{
label: 'Your Card',
path: '/passes',
path: `/passes`,
isShared: true,
landingLabel: 'Your Card',
melbourneLabel: 'Your Card'
},
{
label: 'Your Postcard',
path: '/postcards',
path: `/postcards`,
isShared: true,
landingLabel: 'Your Postcard',
melbourneLabel: 'Your Postcard'
@@ -195,9 +204,6 @@ export default function Navbar({
]
};
// Check if we're on landing page
const isLandingPage = location.pathname === '/';
// Restore from session on mount
useEffect(() => {
const savedCity = sessionStorage.getItem('lastKnownCity');
@@ -559,10 +565,9 @@ export default function Navbar({
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Link to={currentSource === 'melbourne' ? '/melbourne' : '/'}>
<Link to={citySelected ? `/${slugify(cityName)}` : '/'}>
<ImageWithFallback
src={currentSource === 'melbourne' ? melbourneLogo : logoImage}
alt={
src={citySelected ? `${baseUrl}${cityLogo}` : logoImage} alt={
currentSource === 'melbourne'
? 'Melbourne CityCards Logo'
: 'CityCards Logo'
@@ -573,7 +578,7 @@ export default function Navbar({
</motion.div>
<div className="absolute -translate-x-1/2 flex items-center gap-5"
style={{ left: '45%', }}
style={{ left: '42%', }}
>
{/* Enhanced Navigation Items with source tracking */}
{navigationItems.map((item) => {
@@ -623,12 +628,13 @@ export default function Navbar({
onClick={handleOpenCityDialogFromNavbar}
>
<span>
{!activeCity || activeCity === 'shared'
{/* {!activeCity || activeCity === 'shared'
? 'City'
: ['landing', 'landingpage'].includes(activeCity.toLowerCase())
? 'City'
: activeCity.charAt(0).toUpperCase() + activeCity.slice(1)
}
} */}
{citySelected ? cityName : "City"}
</span>
<ChevronDown className="w-3.5 h-3.5" />
@@ -651,7 +657,7 @@ export default function Navbar({
/>
{/* Shopping Cart */}
<Dropdown
{/* <Dropdown
ref={cartRef}
isOpen={activeCartDropdown}
onToggle={() => setActiveCartDropdown(prev => !prev)}
@@ -669,7 +675,8 @@ export default function Navbar({
</motion.div>
</div>
}
/>
/> */}
<ShoppingBag className="w-6 h-6" onClick={() => navigate("/cart")} />
{/* Enhanced City Card Button with Source Tracking */}
<div className="flex items-center gap-3 pl-2">
@@ -685,7 +692,7 @@ export default function Navbar({
label: 'My Profile',
icon: <User className="w-4 h-4" />,
action: () => {
navigate('/profile');
navigate(citySelected ? `/${slugify(cityName)}/profile` : '/profile');
setActiveUserDropdown(false);
}
},
@@ -784,10 +791,11 @@ export default function Navbar({
trigger={
<div className="flex items-center space-x-1 text-gray-700 hover:text-gray-900 text-sm font-medium transition-colors duration-200 cursor-pointer rounded-lg hover:bg-gray-50/50 px-2 py-1">
<span>
{activeCity && activeCity !== 'shared' ?
{/* {activeCity && activeCity !== 'shared' ?
activeCity.charAt(0).toUpperCase() + activeCity.slice(1) :
currentSource === 'melbourne' ? 'Melbourne' : 'Select City'
}
} */}
{cityName ? cityName : "City"}
</span>
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${activeCityDropdown ? 'rotate-180' : ''}`} />
</div>

View File

@@ -8,6 +8,7 @@ import { ImageWithFallback } from '../components/figma/ImageWithFallback';
import { Layout } from '../Layout';
import { useParams } from 'react-router-dom';
import { useGetAttractionDetailsByIdQuery } from '../Redux/services/attractions.service';
import LoadingSpinner from '../components/LoadingSpinner';
interface AttractionDetailsPageProps {
onBackClick: () => void;
@@ -32,12 +33,7 @@ export function AttractionDetailsPage({
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#F95F62] mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
<LoadingSpinner/>
);
}
@@ -262,131 +258,7 @@ export function AttractionDetailsPage({
</div>
{/* Right Sidebar - Calendar and Booking */}
<div className="lg:sticky lg:top-32 space-y-8 self-start">
{/* Calendar Widget with Custom Design */}
<Card className="p-6 bg-white shadow-lg border border-primary/10">
<div className="mb-6">
<h3 className="text-xl font-bold text-primary mb-1">Select Date</h3>
<p className="text-sm text-gray-600">Choose your preferred visit date</p>
</div>
{/* Custom Calendar Design */}
<div className="space-y-4">
{/* Calendar Header */}
<div className="flex items-center justify-between">
<button className="p-2 hover:bg-primary/10 rounded-lg transition-colors">
<ChevronLeft className="w-5 h-5 text-primary" />
</button>
<span className="font-semibold text-gray-900">September 2025</span>
<button className="p-2 hover:bg-primary/10 rounded-lg transition-colors">
<ChevronRight className="w-5 h-5 text-primary" />
</button>
</div>
{/* Days of week */}
<div className="grid grid-cols-7 gap-1 text-center text-sm font-medium text-gray-500">
<div>Su</div>
<div>Mo</div>
<div>Tu</div>
<div>We</div>
<div>Th</div>
<div>Fr</div>
<div>Sa</div>
</div>
{/* Calendar Grid */}
<div className="grid grid-cols-7 gap-1">
{/* Previous month */}
<button className="h-10 w-10 text-sm text-gray-300 hover:bg-gray-50 rounded">31</button>
{/* Current month */}
{Array.from({ length: 30 }, (_, i) => {
const day = i + 1;
const isSelected = day === 27;
const isToday = day === 15;
return (
<button
key={day}
className={`h-10 w-10 text-sm rounded font-medium transition-all duration-200 ${isSelected
? 'bg-primary text-white shadow-lg scale-105'
: isToday
? 'bg-primary/10 text-primary border border-primary/20'
: 'text-gray-700 hover:bg-primary/5 hover:text-primary'
}`}
>
{day}
</button>
);
})}
{/* Next month */}
{Array.from({ length: 4 }, (_, i) => (
<button
key={`next-${i + 1}`}
className="h-10 w-10 text-sm text-gray-300 hover:bg-gray-50 rounded"
>
{i + 1}
</button>
))}
</div>
</div>
{/* Selected Date Display */}
<div className="mt-6 p-4 bg-primary/5 rounded-lg border border-primary/10">
<div className="flex items-center gap-3">
<Calendar className="w-5 h-5 text-primary" />
<div>
<p className="text-sm font-medium text-gray-900">Selected Date</p>
<p className="text-lg font-semibold text-primary">September 27, 2025</p>
</div>
</div>
</div>
</Card>
{/* Pricing Card */}
<Card className="p-6 bg-gradient-to-br from-primary/5 to-primary/10 border border-primary/20">
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-gray-600">Adult Ticket</span>
<span className="font-bold text-xl text-primary">{attraction.ticketPriceAdult}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600">Service Fee</span>
<span className="font-medium text-gray-900">$5</span>
</div>
<div className="border-t border-primary/20 pt-4">
<div className="flex items-center justify-between">
<span className="font-semibold text-gray-900">Total</span>
<span className="font-bold text-2xl text-primary">${attraction.ticketPriceAdult + 5}</span>
</div>
</div>
</div>
</Card>
{/* Confirm Booking Button */}
<Button
className="w-full bg-primary text-white hover:bg-primary/90 py-6 text-lg rounded-xl font-semibold transition-all duration-200 shadow-lg hover:shadow-xl hover:scale-[1.02] relative overflow-hidden group"
onClick={() => onCheckoutClick()}
>
<span className="relative z-10 flex items-center justify-center gap-2">
<Check className="w-5 h-5" />
Confirm Booking
</span>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -skew-x-12 -translate-x-full group-hover:translate-x-full transition-transform duration-700"></div>
</Button>
{/* Trust Indicators */}
<div className="flex items-center justify-center gap-4 text-sm text-gray-600">
<div className="flex items-center gap-1">
<Check className="w-4 h-4 text-primary" />
<span>Instant Confirmation</span>
</div>
<div className="flex items-center gap-1">
<X className="w-4 h-4 text-primary" />
<span>Free Cancellation</span>
</div>
</div>
</div>
</div>
</div>

View File

@@ -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<number | null>(null);
const [selectedPassType, setSelectedPassType] = useState<string | null>(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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#F95F62] mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
<LoadingSpinner/>
);
}
@@ -308,12 +126,12 @@ export function AttractionsPage({
<div className="text-center mb-12">
<h1 className="font-merchant text-2xl md:text-3xl lg:text-4xl leading-tight text-gray-900 mb-4">
<span className="font-light">Discover</span>{' '}
<span className="font-bold italic text-gradient-primary pr-1">Melbourne's</span>{' '}
<span className="font-bold italic text-gradient-primary pr-1">{cityName}'s</span>{' '}
<span className="font-normal">Best</span>{' '}
<span className="font-semibold text-emphasis">Attractions</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
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
</p>
</div>
{/* City Card Promotional Banner */}
@@ -423,7 +241,7 @@ export function AttractionsPage({
<div className="flex-1">
{/* Header */}
<div className="mb-8">
<h1 className="text-[48px] font-medium text-[#2d2d2d] mb-6">Attractions in Melbourne</h1>
<h1 className="text-[48px] font-medium text-[#2d2d2d] mb-6">Attractions in {cityName}</h1>
{/* Results count */}
<p className="text-[16px] text-[#414141] mb-2">
Showing {showingFrom}-{showingTo} of {totalItems} Item(s)

889
src/pages/CartPage.tsx Normal file
View File

@@ -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<string, Record<string, Attraction[]>> = {
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<string, { title: string; description: string; image: string }[]> = {
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<string, Record<number, number>> = {
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 (
<div className={`relative h-[160px] w-full rounded-lg transition-all duration-200 ${isSelected ? 'ring-2 ring-[#F95F62] shadow-md shadow-[#F95F62]/10' : 'hover:shadow-md'
}`}>
{/* Card bg */}
<div className="absolute inset-0 bg-white border border-[rgba(249,95,175,0.2)] rounded-lg shadow-[0px_4px_20px_0px_rgba(0,0,0,0.06)]" />
{/* City image */}
<div className="absolute h-[158px] left-[1px] top-[1px] w-[103px] rounded-bl-[7px] rounded-tl-[7px] overflow-hidden">
{/* <img alt="" className="absolute inset-0 w-full h-full object-cover" src={imgRectangle26} /> */}
</div>
{/* City name - left aligned */}
<div className="absolute left-[112px] top-[12px]">
<p className="font-['Poppins',sans-serif] font-medium text-[16px] text-[#2a2a2a] leading-[22px] whitespace-nowrap">{city}</p>
</div>
{/* Pricing */}
<div className="absolute left-[112px] top-[40px] flex flex-col gap-[6px]">
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">From</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${adultPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Adult</span>
</div>
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">and</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${childPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Child</span>
</div>
</div>
{/* Description */}
<div className="absolute left-[112px] top-[112px] right-[44px]">
<p className="font-['Poppins',sans-serif] text-[11px] text-left text-[rgba(0,0,0,0.4)] tracking-[0.06px] leading-[14px]">
Dive into an extensive selection of thrilling destinations!
</p>
</div>
{/* Side tab - Flexi (pink) */}
<div className="absolute bg-[#f95faf] h-full right-0 top-0 w-[35px] rounded-br-lg rounded-tr-lg flex flex-col items-center justify-center gap-[2px]">
<span className="font-['Poppins',sans-serif] text-[12px] text-white/70 [writing-mode:vertical-rl] rotate-180">Card</span>
<span className="font-['Poppins',sans-serif] text-[16px] text-white [writing-mode:vertical-rl] rotate-180">Flexi</span>
</div>
{/* Selected checkmark */}
{isSelected && (
<div className="absolute top-2 right-[44px] w-6 h-6 rounded-full bg-[#F95F62] flex items-center justify-center z-10">
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
</div>
)}
</div>
);
}
function UnlimitedCardPreview({ city, adultPrice, childPrice, isSelected }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean }) {
return (
<div className={`relative h-[160px] w-full rounded-lg transition-all duration-200 ${isSelected ? 'ring-2 ring-[#F95F62] shadow-md shadow-[#F95F62]/10' : 'hover:shadow-md'
}`}>
{/* Card bg */}
<div className="absolute inset-0 bg-white border border-[rgba(0,0,0,0.2)] rounded-lg" />
{/* City image */}
<div className="absolute h-[158px] left-[1px] top-[1px] w-[103px] rounded-bl-[7px] rounded-tl-[7px] overflow-hidden">
{/* <img alt="" className="absolute inset-0 w-full h-full object-cover" src={imgRectangle26} /> */}
</div>
{/* City name - left aligned */}
<div className="absolute left-[112px] top-[12px]">
<p className="font-['Poppins',sans-serif] font-medium text-[16px] text-[#2a2a2a] leading-[20px] whitespace-nowrap">{city}</p>
</div>
{/* Pricing */}
<div className="absolute left-[112px] top-[40px] flex flex-col gap-[6px]">
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">From</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${adultPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Adult</span>
</div>
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">and</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${childPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Child</span>
</div>
</div>
{/* Description */}
<div className="absolute left-[112px] top-[112px] right-[44px]">
<p className="font-['Poppins',sans-serif] text-[11px] text-left text-[rgba(0,0,0,0.4)] tracking-[0.06px] leading-[14px]">
Dive into an extensive selection of thrilling destinations!
</p>
</div>
{/* Side tab - Unlimited (coral) */}
<div className="absolute bg-[#f95f62] h-full right-0 top-0 w-[35px] rounded-br-lg rounded-tr-lg flex flex-col items-center justify-center gap-[2px]">
<span className="font-['Poppins',sans-serif] text-[12px] text-white/70 [writing-mode:vertical-rl] rotate-180">Card</span>
<span className="font-['Poppins',sans-serif] text-[16px] text-white [writing-mode:vertical-rl] rotate-180">Unlimited</span>
</div>
{/* Selected checkmark */}
{isSelected && (
<div className="absolute top-2 right-[44px] w-6 h-6 rounded-full bg-[#F95F62] flex items-center justify-center z-10">
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
</div>
)}
</div>
);
}
/* ═══════════════════════════════════════════
CHECKOUT CONFIGURATION CARD (Mobile-first)
═══════════════════════════════════════════ */
function CheckoutConfigCard({
item,
onChange,
onProceed,
}: {
item: CartItem;
onChange: (updates: Partial<CartItem>) => 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 (
<div className="bg-white rounded-2xl shadow-[0px_4px_24px_0px_rgba(0,0,0,0.06)] overflow-hidden w-full max-w-[400px]">
{/* City header */}
<div className="pt-6 pb-2 text-center">
<h4 className="font-poppins text-lg leading-snug font-medium text-[#2a2a2a]">{item.city}</h4>
<div className="mt-2 flex justify-center">
<span className={`inline-flex items-center px-4 py-1 rounded-full font-poppins text-xs font-medium ${item.cardType === 'Flexi'
? 'bg-[#f95faf]/10 text-[#f95faf]'
: 'bg-[#f95f62]/10 text-[#f95f62]'
}`}>
{item.cardType} Card
</span>
</div>
</div>
{/* Configuration rows */}
<div className="px-6 py-4 space-y-0">
{/* No. of Adults */}
<div className="flex items-center justify-between py-4 border-b border-gray-100">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">No. of Adults</span>
<div className="flex items-center gap-3">
<button
onClick={() => item.adults > 1 && onChange({ adults: item.adults - 1 })}
disabled={item.adults <= 1}
className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${item.adults <= 1 ? 'bg-gray-100 text-gray-300 cursor-not-allowed' : 'bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20'
}`}
>
<Minus className="w-4 h-4" />
</button>
<span className="font-poppins text-base font-medium text-[#2a2a2a] w-5 text-center tabular-nums">{item.adults}</span>
<button
onClick={() => onChange({ adults: item.adults + 1 })}
className="w-8 h-8 rounded-full bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20 flex items-center justify-center transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
{/* No. of Children */}
<div className="flex items-center justify-between py-4 border-b border-gray-100">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">No. of Children</span>
<div className="flex items-center gap-3">
<button
onClick={() => item.children > 0 && onChange({ children: item.children - 1 })}
disabled={item.children <= 0}
className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${item.children <= 0 ? 'bg-gray-100 text-gray-300 cursor-not-allowed' : 'bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20'
}`}
>
<Minus className="w-4 h-4" />
</button>
<span className="font-poppins text-base font-medium text-[#2a2a2a] w-5 text-center tabular-nums">{item.children}</span>
<button
onClick={() => onChange({ children: item.children + 1 })}
className="w-8 h-8 rounded-full bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20 flex items-center justify-center transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
{/* No. of Days (dropdown) */}
<div className="flex items-center justify-between py-4 border-b border-gray-100">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">
{item.cardType === 'Flexi' ? 'No. of Attractions' : 'No. of Days'}
</span>
<div className="relative">
<button
onClick={() => setDaysOpen(!daysOpen)}
className="flex items-center gap-2 border border-[#f95f62]/30 rounded-lg px-3 py-1.5 min-w-[72px] justify-between hover:border-[#f95f62] transition-colors"
>
<span className="font-poppins text-base font-medium text-[#f95f62] tabular-nums">{item.days}</span>
<ChevronDown className={`w-4 h-4 text-[#f95f62] transition-transform ${daysOpen ? 'rotate-180' : ''}`} />
</button>
<AnimatePresence>
{daysOpen && (
<motion.div
initial={{ opacity: 0, y: -4, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -4, scale: 0.95 }}
transition={{ duration: 0.15 }}
className="absolute right-0 top-full mt-1 bg-white rounded-lg shadow-lg border border-gray-100 z-30 min-w-[72px] overflow-hidden"
>
{dayOptions.map((d) => (
<button
key={d}
onClick={() => { onChange({ days: d }); setDaysOpen(false); }}
className={`w-full px-3 py-2 text-left font-poppins text-sm transition-colors ${item.days === d
? 'bg-[#f95f62]/10 text-[#f95f62] font-medium'
: 'text-[#2a2a2a] hover:bg-gray-50 font-normal'
}`}
>
{d}
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* You Pay */}
<div className="flex items-center justify-between py-5">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">You Pay</span>
<div className="flex items-center gap-2">
<span className="font-poppins text-sm font-normal text-[#aaa] line-through">
${originalPrice.toFixed(0)}
</span>
<span className="font-poppins text-2xl font-medium text-[#f95f62] tracking-tight">
${totalPrice.toFixed(0)}
</span>
</div>
</div>
</div>
{/* Proceed button */}
<div className="px-6 pb-6">
<motion.button
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.98 }}
onClick={() => 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
</motion.button>
</div>
</div>
);
}
/* ═══════════════════════════════════════════
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<CartItem[]>(initialCartItems);
const [selectedCardId, setSelectedCardId] = useState<string | null>(null);
const [view, setView] = useState<'cart' | 'checkout'>('cart');
const [checkoutItem, setCheckoutItem] = useState<CartItem | null>(null);
const navigate = useNavigate()
const cityId = localStorage.getItem("cityId")
const { data, isLoading } = useGetCardsinCartQuery(cityId)
const CartItems = data?.cartItems ?? []
if (isLoading) {
return (
<LoadingSpinner />
)
}
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<CartItem>) => {
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 (
<div className="min-h-screen bg-[#fafafa] font-poppins">
<Navbar
activeCity="Melbourne" onCityChange={() => { }} onSignInClick={onSignInClick} onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick} onCheckoutClick={onCheckoutClick} onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick} onBlogsClick={onBlogsClick} onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick} onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick} onCityCardsClick={onCityCardsClick} onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick} onOffersClick={onOffersClick} onSuperSavingsClick={onSuperSavingsClick}
onEsimsClick={onEsimsClick} onHotelDiscountsClick={onHotelDiscountsClick} onCartClick={onCartClick}
currentPage={currentPage as any} user={user}
/>
<AnimatePresence mode="wait">
{view === 'cart' ? (
/* ─── CART VIEW ─── */
<motion.div
key="cart-view"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, x: -30 }}
transition={{ duration: 0.3 }}
className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto"
>
{/* Header */}
<div className="mb-8">
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
<span className="font-light">Your</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent">Cart</span>
</h2>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1">
{isEmpty ? 'Your cart is empty' : `${CartItems.length} ${CartItems.length === 1 ? 'item' : 'items'} in your cart`}
</p>
</div>
{/* Tab switcher */}
{/* Cards listed directly below */}
{/* Content */}
<AnimatePresence mode="wait">
{activeTab === 'cards' ? (
<motion.div key="cards-content" initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -12 }} transition={{ duration: 0.2 }}>
{isEmpty ? (
<EmptyState icon={<CreditCard className="w-16 h-16 text-[#F95F62]/20" strokeWidth={1.2} />} 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} />
) : (
<div className="space-y-3">
{/* Table header (desktop) */}
<div className="md:grid md:grid-cols-12 gap-4 px-5 pb-2">
<div className="col-span-5 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider">City Cards</div>
<div className="col-span-2 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider text-center">Travellers</div>
{/* <div className="col-span-1 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider text-center">Qty</div> */}
<div className="col-span-3 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider text-right">Price</div>
<div className="col-span-1" />
</div>
<AnimatePresence>
{CartItems.map((item: any) => {
const isSelected = selectedCardId === item.id;
const totalPrice = item.pricePerUnit * item.quantity;
return (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: -60, transition: { duration: 0.25 } }}
onClick={() => 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 */}
<AnimatePresence>
{isSelected && (
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} className="absolute top-3 left-3 z-20 w-6 h-6 rounded-full bg-[#F95F62] flex items-center justify-center shadow-md">
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
</motion.div>
)}
</AnimatePresence>
{/* Mobile layout */}
<div className="md:hidden flex gap-4 p-4">
<div className="w-20 h-20 rounded-xl overflow-hidden flex-shrink-0 relative">
<ImageWithFallback src={item?.city?.cityBanners[0]?.imageFilePath} alt={item.city?.cityName} className="absolute inset-0 w-full h-full object-cover" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div>
<h5 className="font-poppins text-base leading-snug font-medium text-[#2a2a2a]">{item.city?.cityName}</h5>
<div className="flex items-center gap-2 mt-0.5">
<span className={`inline-flex px-2 py-0.5 rounded-full text-[10px] font-medium ${item.cardMode === 'flexi' ? 'bg-[#F95FAF]/10 text-[#F95FAF]' : 'bg-[#F95F62]/10 text-[#F95F62]'}`}>{item.displayCardMode}</span>
<span className="font-poppins text-xs font-normal text-[#8e8e8e]">{item.cardMode === 'flexi' ? `${item.noOfAttractions} ${item.noOfAttractions === 1 ? 'attraction' : 'attractions'}` : `${item.noOfDays} ${item.noOfDays === 1 ? 'day' : 'days'}`}</span>
</div>
</div>
{/* <button onClick={(e) => { e.stopPropagation(); handleRemoveItem(item.id); }} className="p-1.5 rounded-lg text-gray-300 hover:text-[#F95F62] hover:bg-red-50 transition-colors">
<Trash2 className="w-4 h-4" />
</button> */}
</div>
<div className="flex items-center justify-between mt-2">
<span className="font-poppins text-xs font-normal text-[#8e8e8e]">{item.totalAdult}A · {item.totalChild}C</span>
<div className="text-right">
<span className="font-poppins text-base font-medium text-[#F95F62] tracking-tight">${item.totalAmount}</span>
{/* {item.quantity > 1 && <span className="block font-poppins text-[10px] font-normal text-[#aaa]">${item.pricePerUnit.toFixed(2)}/ea</span>} */}
</div>
</div>
</div>
</div>
{/* Desktop layout */}
<div className="md:grid md:grid-cols-12 gap-4 items-center p-5">
<div className="col-span-5 flex items-center gap-4">
<div className="w-16 h-16 rounded-xl overflow-hidden flex-shrink-0 relative">
<ImageWithFallback src={item?.city?.cityBanners[0]?.imageFilePath} alt={item.city?.cityName} className="absolute inset-0 w-full h-full object-cover" />
</div>
<div>
<h5 className="font-poppins text-base leading-snug font-medium text-[#2a2a2a]">{item.city?.cityName}</h5>
<div className="flex items-center gap-2 mt-1">
<span className={`inline-flex px-2.5 py-0.5 rounded-full text-xs font-medium ${item.cardMode === 'flexi' ? 'bg-[#F95FAF]/10 text-[#F95FAF]' : 'bg-[#F95F62]/10 text-[#F95F62]'}`}>{item.displayCardMode}</span>
<span className="flex items-center gap-1 font-poppins text-xs font-normal text-[#8e8e8e]">{item.cardMode === 'flexi' ? `${item.noOfAttractions} ${item.noOfAttractions === 1 ? 'attraction' : 'attractions'}` : `${item.noOfDays} ${item.noOfDays === 1 ? 'day' : 'days'}`}</span>
</div>
</div>
</div>
<div className="col-span-2 text-center">
<div className="flex items-center justify-center gap-3">
<span className="flex items-center gap-1 font-poppins text-sm font-normal text-[#555]"><Users className="w-3.5 h-3.5 text-[#8e8e8e]" />{item.totalAdult}</span>
<span className="flex items-center gap-1 font-poppins text-sm font-normal text-[#555]"><Baby className="w-3.5 h-3.5 text-[#8e8e8e]" />{item.totalChild}</span>
</div>
</div>
{/* <div className="col-span-1 flex justify-center">
<span className="font-poppins text-sm font-medium text-[#2a2a2a] bg-gray-50 px-4 py-1.5 rounded-full">{item.quantity}</span>
</div> */}
<div className="col-span-3 text-right">
<span className="font-poppins text-lg font-medium text-[#F95F62] tracking-tight">${item.totalAmount}</span>
{/* {item.quantity > 1 && <span className="block font-poppins text-xs font-normal text-[#aaa] mt-0.5">${item.pricePerUnit.toFixed(2)} per unit</span>} */}
</div>
{/* <div className="col-span-1 flex justify-end">
<button onClick={(e) => { e.stopPropagation(); handleRemoveItem(item.id); }} className="p-2 rounded-lg text-gray-300 hover:text-[#F95F62] hover:bg-red-50 transition-all" title="Remove from cart">
<Trash2 className="w-4.5 h-4.5" />
</button>
</div> */}
</div>
</motion.div>
);
})}
</AnimatePresence>
{/* Bottom checkout bar */}
<motion.div layout className="mt-6 bg-white rounded-2xl ring-1 ring-gray-100 p-5 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-center sm:text-left">
{selectedItem ? (
<>
<p className="font-poppins text-xs font-normal text-[#8e8e8e]">
Selected: {selectedItem.city.cityName} {selectedItem.displayCardMode} · {selectedItem.cardMode === 'flexi' ? `${selectedItem.noOfAttractions} ${selectedItem.noOfAttractions === 1 ? 'attraction' : 'attractions'}` : `${selectedItem.noOfDays} ${selectedItem.noOfDays === 1 ? 'day' : 'days'}`}
</p>
<p className="font-poppins text-2xl font-medium text-[#F95F62] tracking-tight mt-0.5">
${selectedItem.totalAmount}
</p>
</>
) : (
<p className="font-poppins text-sm font-normal text-[#8e8e8e]">Tap a card above to select it for checkout</p>
)}
</div>
<motion.button
whileHover={selectedItem ? { scale: 1.02 } : {}}
whileTap={selectedItem ? { scale: 0.98 } : {}}
onClick={()=>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 <ChevronRight className="w-4 h-4" />
</motion.button>
</motion.div>
</div>
)}
</motion.div>
) : (
<motion.div key="postcards-content" initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -12 }} transition={{ duration: 0.2 }}>
<EmptyState icon={<Mail className="w-16 h-16 text-[#F95F62]/20" strokeWidth={1.2} />} 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} />
</motion.div>
)}
</AnimatePresence>
</motion.div>
) : (
/* ─── CHECKOUT VIEW ─── */
<motion.div
key="checkout-view"
initial={{ opacity: 0, x: 40 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 40 }}
transition={{ duration: 0.3 }}
className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto"
>
{checkoutItem && (
<>
{/* Back */}
<button onClick={handleBackToCart} className="flex items-center gap-2 text-[#8e8e8e] hover:text-[#2a2a2a] transition-colors font-poppins text-sm font-normal mb-8">
<ArrowLeft className="w-4 h-4" />Back to Cart
</button>
{/* Stepper */}
{/* <CheckoutStepper currentStep={2} /> */}
{/* Checkout heading */}
<div className="mb-10">
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
<span className="font-light">Checkout</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent">{checkoutItem.city}</span>
</h2>
</div>
<div className="flex flex-col lg:flex-row gap-10">
{/* Left column */}
<div className="flex-1 space-y-8">
{/* ── Card Type Selection (Figma cards) ── */}
<div>
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">
Choose Your Card
</h3>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
Select the card type that best suits your travel style
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-[16px]">
{/* Flexi */}
<button
onClick={() => handleCheckoutItemChange({ cardType: 'Flexi' })}
className="relative transition-all duration-200"
>
<FlexiCardPreview
city={checkoutItem.city}
adultPrice={priceTable.Flexi[checkoutItem.days] || 80}
childPrice={10}
isSelected={checkoutItem.cardType === 'Flexi'}
/>
</button>
{/* Unlimited */}
<button
onClick={() => handleCheckoutItemChange({ cardType: 'Unlimited' })}
className="relative transition-all duration-200"
>
<UnlimitedCardPreview
city={checkoutItem.city}
adultPrice={priceTable.Unlimited[checkoutItem.days] || 120}
childPrice={20}
isSelected={checkoutItem.cardType === 'Unlimited'}
/>
</button>
</div>
{/* ── Config Card (mobile only) — right after card selection ── */}
<div className="lg:hidden mt-6">
<CheckoutConfigCard
item={checkoutItem}
onChange={handleCheckoutItemChange}
onProceed={() => checkoutItem && onSecureCheckoutClick?.(checkoutItem)}
/>
</div>
{/* Features Comparison */}
<div className="mt-6 bg-[#f5f5f5] rounded-xl p-4">
<div className="grid grid-cols-[1fr_70px_70px] gap-y-0 items-center">
{/* Header */}
<p className="font-poppins font-medium text-sm text-[#2a2a2a] py-3">Features</p>
<p className="font-poppins font-medium text-sm text-[#2a2a2a] text-center py-3">Flexi</p>
<p className="font-poppins font-medium text-sm text-[#2a2a2a] text-center py-3">Unlimited</p>
{[
{ 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) => (
<React.Fragment key={i}>
<p className="font-poppins font-normal text-[13px] text-[#2a2a2a] py-2.5 border-t border-[rgba(0,0,0,0.08)] flex items-center gap-1.5">
<span className="text-[#2a2a2a]"></span> {row.feature}
</p>
<div className="flex justify-center py-2.5 border-t border-[rgba(0,0,0,0.08)]">
{row.flexi ? (
<div className="w-5 h-5 rounded-full bg-[#F95F62] flex items-center justify-center">
<Check className="w-3 h-3 text-white" strokeWidth={3} />
</div>
) : (
<span className="font-poppins text-[13px] text-[rgba(0,0,0,0.3)]"></span>
)}
</div>
<div className="flex justify-center py-2.5 border-t border-[rgba(0,0,0,0.08)]">
{row.unlimited ? (
<div className="w-5 h-5 rounded-full bg-[#F95F62] flex items-center justify-center">
<Check className="w-3 h-3 text-white" strokeWidth={3} />
</div>
) : (
<span className="font-poppins text-[13px] text-[rgba(0,0,0,0.3)]"></span>
)}
</div>
</React.Fragment>
))}
</div>
</div>
</div>
{/* ── Offers ── */}
<div>
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">
{checkoutItem.cardType} Card Offers
</h3>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
Exclusive deals and discounts included with your {checkoutItem.cardType} pass
</p>
<div className="flex gap-3 overflow-x-auto pb-2 -mx-4 px-4 snap-x snap-mandatory scrollbar-hide">
{offers.map((offer, idx) => (
<div key={idx} className="relative bg-white rounded-xl shrink-0 w-[180px] h-[260px] snap-start">
<div className="flex flex-col gap-2 items-start overflow-hidden p-3 rounded-xl h-full">
<div className="h-[120px] w-full rounded-lg overflow-hidden shrink-0 relative">
<ImageWithFallback
src={offer.image}
alt={offer.title}
className="absolute inset-0 w-full h-full object-cover rounded-lg"
/>
</div>
<div className="w-full h-[44px] overflow-hidden">
<p className="font-['Poppins',sans-serif] font-normal text-[18px] text-black tracking-[-0.72px] leading-[22px] line-clamp-2">
{offer.title}
</p>
</div>
<div className="w-full flex-1">
<p className="font-['Poppins',sans-serif] font-normal text-[12px] text-[rgba(0,0,0,0.6)] leading-[16px] line-clamp-3">
{offer.description}
</p>
</div>
</div>
<div className="absolute inset-0 border border-[rgba(249,95,98,0.24)] rounded-xl pointer-events-none" />
</div>
))}
</div>
</div>
{/* ── Available Attractions ── */}
<div>
<div className="flex items-center justify-between">
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">Available Attractions</h3>
<span className="font-poppins text-xs font-medium text-[#F95F62] bg-[#F95F62]/10 px-3 py-1 rounded-full">{attractions.length} included</span>
</div>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
Explore all the experiences you can enjoy with your pass
</p>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
{attractions.map((a) => (
<div key={a.id} className="group relative rounded-xl overflow-hidden">
<div className="aspect-[4/3] relative">
<ImageWithFallback src={a.image} alt={a.name} className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/10 to-transparent" />
<div className="absolute top-2 right-2">
<span className="inline-flex px-2 py-0.5 rounded-full bg-white/90 backdrop-blur-sm text-[10px] font-poppins font-medium text-[#555]">{a.category}</span>
</div>
<div className="absolute bottom-2 left-2 right-2">
<h6 className="font-poppins text-sm leading-snug font-medium text-white drop-shadow-sm">{a.name}</h6>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right column: Config card (desktop only, sticky) */}
<div className="hidden lg:block lg:w-[420px] flex-shrink-0">
<div className="lg:sticky lg:top-28">
<CheckoutConfigCard
item={checkoutItem}
onChange={handleCheckoutItemChange}
onProceed={() => checkoutItem && onSecureCheckoutClick?.(checkoutItem)}
/>
</div>
</div>
</div>
</>
)}
</motion.div>
)}
</AnimatePresence>
<Footer
onHomeClick={onHomeClick} onPassesClick={onPassesClick} onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick} onHowItWorksClick={onHowItWorksClick} onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick} onContactUsClick={onContactUsClick}
/>
</div>
);
}
/* ─── Empty state ─── */
function EmptyState({ icon, title, description, actionLabel, onAction }: {
icon: React.ReactNode; title: string; description: string; actionLabel: string; onAction?: () => void;
}) {
return (
<motion.div initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.4 }} className="flex flex-col items-center justify-center py-20 max-w-sm mx-auto text-center">
<motion.div className="w-28 h-28 rounded-3xl bg-[#fee7e7]/50 flex items-center justify-center mb-6" animate={{ y: [0, -6, 0] }} transition={{ duration: 3, repeat: Infinity, ease: 'easeInOut' }}>{icon}</motion.div>
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a] mb-2">{title}</h3>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mb-8">{description}</p>
<motion.button whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} onClick={onAction} className="bg-[#F95F62] text-white font-poppins text-base font-medium px-8 py-3.5 rounded-xl hover:bg-[#e8545a] transition-colors shadow-lg shadow-[#F95F62]/15 w-full">{actionLabel}</motion.button>
</motion.div>
);
}

View File

@@ -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<string, Record<string, Attraction[]>> = {
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<string, { title: string; description: string; image: string }[]> = {
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<string, Record<number, number>> = {
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 (
<div className={`relative h-[160px] w-full rounded-lg transition-all duration-200 ${
isSelected ? 'ring-2 ring-[#F95F62] shadow-md shadow-[#F95F62]/10' : 'hover:shadow-md'
}`}>
{/* Card bg */}
<div className="absolute inset-0 bg-white border border-[rgba(249,95,175,0.2)] rounded-lg shadow-[0px_4px_20px_0px_rgba(0,0,0,0.06)]" />
{/* City image */}
<div className="absolute h-[158px] left-[1px] top-[1px] w-[103px] rounded-bl-[7px] rounded-tl-[7px] overflow-hidden">
{/* <img alt="" className="absolute inset-0 w-full h-full object-cover" src={imgRectangle26} /> */}
</div>
{/* City name - left aligned */}
<div className="absolute left-[112px] top-[12px]">
<p className="font-['Poppins',sans-serif] font-medium text-[16px] text-[#2a2a2a] leading-[22px] whitespace-nowrap">{city}</p>
</div>
{/* Pricing */}
<div className="absolute left-[112px] top-[40px] flex flex-col gap-[6px]">
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">From</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${adultPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Adult</span>
</div>
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">and</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${childPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Child</span>
</div>
</div>
{/* Description */}
<div className="absolute left-[112px] top-[112px] right-[44px]">
<p className="font-['Poppins',sans-serif] text-[11px] text-left text-[rgba(0,0,0,0.4)] tracking-[0.06px] leading-[14px]">
Dive into an extensive selection of thrilling destinations!
</p>
</div>
{/* Side tab - Flexi (pink) */}
<div className="absolute bg-[#f95faf] h-full right-0 top-0 w-[35px] rounded-br-lg rounded-tr-lg flex flex-col items-center justify-center gap-[2px]">
<span className="font-['Poppins',sans-serif] text-[12px] text-white/70 [writing-mode:vertical-rl] rotate-180">Card</span>
<span className="font-['Poppins',sans-serif] text-[16px] text-white [writing-mode:vertical-rl] rotate-180">Flexi</span>
</div>
{/* Selected checkmark */}
{isSelected && (
<div className="absolute top-2 right-[44px] w-6 h-6 rounded-full bg-[#F95F62] flex items-center justify-center z-10">
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
</div>
)}
</div>
);
}
function UnlimitedCardPreview({ city, adultPrice, childPrice, isSelected }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean }) {
return (
<div className={`relative h-[160px] w-full rounded-lg transition-all duration-200 ${
isSelected ? 'ring-2 ring-[#F95F62] shadow-md shadow-[#F95F62]/10' : 'hover:shadow-md'
}`}>
{/* Card bg */}
<div className="absolute inset-0 bg-white border border-[rgba(0,0,0,0.2)] rounded-lg" />
{/* City image */}
<div className="absolute h-[158px] left-[1px] top-[1px] w-[103px] rounded-bl-[7px] rounded-tl-[7px] overflow-hidden">
{/* <img alt="" className="absolute inset-0 w-full h-full object-cover" src={imgRectangle26} /> */}
</div>
{/* City name - left aligned */}
<div className="absolute left-[112px] top-[12px]">
<p className="font-['Poppins',sans-serif] font-medium text-[16px] text-[#2a2a2a] leading-[20px] whitespace-nowrap">{city}</p>
</div>
{/* Pricing */}
<div className="absolute left-[112px] top-[40px] flex flex-col gap-[6px]">
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">From</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${adultPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Adult</span>
</div>
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">and</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${childPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Child</span>
</div>
</div>
{/* Description */}
<div className="absolute left-[112px] top-[112px] right-[44px]">
<p className="font-['Poppins',sans-serif] text-[11px] text-left text-[rgba(0,0,0,0.4)] tracking-[0.06px] leading-[14px]">
Dive into an extensive selection of thrilling destinations!
</p>
</div>
{/* Side tab - Unlimited (coral) */}
<div className="absolute bg-[#f95f62] h-full right-0 top-0 w-[35px] rounded-br-lg rounded-tr-lg flex flex-col items-center justify-center gap-[2px]">
<span className="font-['Poppins',sans-serif] text-[12px] text-white/70 [writing-mode:vertical-rl] rotate-180">Card</span>
<span className="font-['Poppins',sans-serif] text-[16px] text-white [writing-mode:vertical-rl] rotate-180">Unlimited</span>
</div>
{/* Selected checkmark */}
{isSelected && (
<div className="absolute top-2 right-[44px] w-6 h-6 rounded-full bg-[#F95F62] flex items-center justify-center z-10">
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
</div>
)}
</div>
);
}
/* ═══════════════════════════════════════════
CHECKOUT CONFIGURATION CARD (Mobile-first)
═══════════════════════════════════════════ */
function CheckoutConfigCard({
item,
onChange,
onProceed,
}: {
item: CartItem;
onChange: (updates: Partial<CartItem>) => void;
onProceed: () => void;
}) {
const [daysOpen, setDaysOpen] = useState(false);
const originalPrice = (item.pricePerUnit * item.quantity * 1.35);
const totalPrice = item.pricePerUnit * item.quantity;
return (
<div className="bg-white rounded-2xl shadow-[0px_4px_24px_0px_rgba(0,0,0,0.06)] overflow-hidden w-full max-w-[400px]">
{/* City header */}
<div className="pt-6 pb-2 text-center">
<h4 className="font-poppins text-lg leading-snug font-medium text-[#2a2a2a]">{item.city}</h4>
<div className="mt-2 flex justify-center">
<span className={`inline-flex items-center px-4 py-1 rounded-full font-poppins text-xs font-medium ${
item.cardType === 'Flexi'
? 'bg-[#f95faf]/10 text-[#f95faf]'
: 'bg-[#f95f62]/10 text-[#f95f62]'
}`}>
{item.cardType} Card
</span>
</div>
</div>
{/* Configuration rows */}
<div className="px-6 py-4 space-y-0">
{/* No. of Adults */}
<div className="flex items-center justify-between py-4 border-b border-gray-100">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">No. of Adults</span>
<div className="flex items-center gap-3">
<button
onClick={() => item.adults > 1 && onChange({ adults: item.adults - 1 })}
disabled={item.adults <= 1}
className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${
item.adults <= 1 ? 'bg-gray-100 text-gray-300 cursor-not-allowed' : 'bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20'
}`}
>
<Minus className="w-4 h-4" />
</button>
<span className="font-poppins text-base font-medium text-[#2a2a2a] w-5 text-center tabular-nums">{item.adults}</span>
<button
onClick={() => onChange({ adults: item.adults + 1 })}
className="w-8 h-8 rounded-full bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20 flex items-center justify-center transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
{/* No. of Children */}
<div className="flex items-center justify-between py-4 border-b border-gray-100">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">No. of Children</span>
<div className="flex items-center gap-3">
<button
onClick={() => item.children > 0 && onChange({ children: item.children - 1 })}
disabled={item.children <= 0}
className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${
item.children <= 0 ? 'bg-gray-100 text-gray-300 cursor-not-allowed' : 'bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20'
}`}
>
<Minus className="w-4 h-4" />
</button>
<span className="font-poppins text-base font-medium text-[#2a2a2a] w-5 text-center tabular-nums">{item.children}</span>
<button
onClick={() => onChange({ children: item.children + 1 })}
className="w-8 h-8 rounded-full bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20 flex items-center justify-center transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
{/* No. of Days (dropdown) */}
<div className="flex items-center justify-between py-4 border-b border-gray-100">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">
{item.cardType === 'Flexi' ? 'No. of Attractions' : 'No. of Days'}
</span>
<div className="relative">
<button
onClick={() => setDaysOpen(!daysOpen)}
className="flex items-center gap-2 border border-[#f95f62]/30 rounded-lg px-3 py-1.5 min-w-[72px] justify-between hover:border-[#f95f62] transition-colors"
>
<span className="font-poppins text-base font-medium text-[#f95f62] tabular-nums">{item.days}</span>
<ChevronDown className={`w-4 h-4 text-[#f95f62] transition-transform ${daysOpen ? 'rotate-180' : ''}`} />
</button>
<AnimatePresence>
{daysOpen && (
<motion.div
initial={{ opacity: 0, y: -4, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -4, scale: 0.95 }}
transition={{ duration: 0.15 }}
className="absolute right-0 top-full mt-1 bg-white rounded-lg shadow-lg border border-gray-100 z-30 min-w-[72px] overflow-hidden"
>
{dayOptions.map((d) => (
<button
key={d}
onClick={() => { onChange({ days: d }); setDaysOpen(false); }}
className={`w-full px-3 py-2 text-left font-poppins text-sm transition-colors ${
item.days === d
? 'bg-[#f95f62]/10 text-[#f95f62] font-medium'
: 'text-[#2a2a2a] hover:bg-gray-50 font-normal'
}`}
>
{d}
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* You Pay */}
<div className="flex items-center justify-between py-5">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">You Pay</span>
<div className="flex items-center gap-2">
<span className="font-poppins text-sm font-normal text-[#aaa] line-through">
${originalPrice.toFixed(0)}
</span>
<span className="font-poppins text-2xl font-medium text-[#f95f62] tracking-tight">
${totalPrice.toFixed(0)}
</span>
</div>
</div>
</div>
{/* Proceed button */}
<div className="px-6 pb-6">
<motion.button
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.98 }}
onClick={onProceed}
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
</motion.button>
</div>
</div>
);
}
/* ═══════════════════════════════════════════
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<CartItem[]>(initialCartItems);
const [selectedCardId, setSelectedCardId] = useState<string | null>(null);
const [view, setView] = useState<'cart' | 'checkout'>('cart');
const [checkoutItem, setCheckoutItem] = useState<CartItem | null>(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<CartItem>) => {
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 (
<div className="min-h-screen bg-[#fafafa] font-poppins">
<Navbar
activeCity="Melbourne" onCityChange={() => {}} onSignInClick={onSignInClick} onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick} onCheckoutClick={onCheckoutClick} onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick} onBlogsClick={onBlogsClick} onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick} onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick} onCityCardsClick={onCityCardsClick} onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick} onOffersClick={onOffersClick} onSuperSavingsClick={onSuperSavingsClick}
onEsimsClick={onEsimsClick} onHotelDiscountsClick={onHotelDiscountsClick} onCartClick={onCartClick}
currentPage={currentPage as any} user={user}
/>
<AnimatePresence mode="wait">
{view === 'cart' ? (
/* ─── CART VIEW ─── */
<motion.div
key="cart-view"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, x: -30 }}
transition={{ duration: 0.3 }}
className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto"
>
{/* Header */}
<div className="mb-8">
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
<span className="font-light">Your</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent">Cart</span>
</h2>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1">
{isEmpty ? 'Your cart is empty' : `${cartItems.length} ${cartItems.length === 1 ? 'item' : 'items'} in your cart`}
</p>
</div>
{/* Tab switcher */}
{/* Cards listed directly below */}
{/* Content */}
<AnimatePresence mode="wait">
{activeTab === 'cards' ? (
<motion.div key="cards-content" initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -12 }} transition={{ duration: 0.2 }}>
{isEmpty ? (
<EmptyState icon={<CreditCard className="w-16 h-16 text-[#F95F62]/20" strokeWidth={1.2} />} 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} />
) : (
<div className="space-y-3">
{/* Table header (desktop) */}
<div className="md:grid md:grid-cols-12 gap-4 px-5 pb-2">
<div className="col-span-5 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider">City Cards</div>
<div className="col-span-2 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider text-center">Travellers</div>
<div className="col-span-1 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider text-center">Qty</div>
<div className="col-span-3 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider text-right">Price</div>
<div className="col-span-1" />
</div>
<AnimatePresence>
{cartItems.map((item) => {
const isSelected = selectedCardId === item.id;
const totalPrice = item.pricePerUnit * item.quantity;
return (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: -60, transition: { duration: 0.25 } }}
onClick={() => 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 */}
<AnimatePresence>
{isSelected && (
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} className="absolute top-3 left-3 z-20 w-6 h-6 rounded-full bg-[#F95F62] flex items-center justify-center shadow-md">
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
</motion.div>
)}
</AnimatePresence>
{/* Mobile layout */}
<div className="md:hidden flex gap-4 p-4">
<div className="w-20 h-20 rounded-xl overflow-hidden flex-shrink-0 relative">
<ImageWithFallback src={item.image} alt={item.city} className="absolute inset-0 w-full h-full object-cover" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div>
<h5 className="font-poppins text-base leading-snug font-medium text-[#2a2a2a]">{item.city}</h5>
<div className="flex items-center gap-2 mt-0.5">
<span className={`inline-flex px-2 py-0.5 rounded-full text-[10px] font-medium ${item.cardType === 'Flexi' ? 'bg-[#F95FAF]/10 text-[#F95FAF]' : 'bg-[#F95F62]/10 text-[#F95F62]'}`}>{item.cardType}</span>
<span className="font-poppins text-xs font-normal text-[#8e8e8e]">{item.days}d</span>
</div>
</div>
<button onClick={(e) => { e.stopPropagation(); handleRemoveItem(item.id); }} className="p-1.5 rounded-lg text-gray-300 hover:text-[#F95F62] hover:bg-red-50 transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
<div className="flex items-center justify-between mt-2">
<span className="font-poppins text-xs font-normal text-[#8e8e8e]">{item.adults}A · {item.children}C · Qty {item.quantity}</span>
<div className="text-right">
<span className="font-poppins text-base font-medium text-[#F95F62] tracking-tight">${totalPrice.toFixed(2)}</span>
{item.quantity > 1 && <span className="block font-poppins text-[10px] font-normal text-[#aaa]">${item.pricePerUnit.toFixed(2)}/ea</span>}
</div>
</div>
</div>
</div>
{/* Desktop layout */}
<div className="md:grid md:grid-cols-12 gap-4 items-center p-5">
<div className="col-span-5 flex items-center gap-4">
<div className="w-16 h-16 rounded-xl overflow-hidden flex-shrink-0 relative">
<ImageWithFallback src={item.image} alt={item.city} className="absolute inset-0 w-full h-full object-cover" />
</div>
<div>
<h5 className="font-poppins text-base leading-snug font-medium text-[#2a2a2a]">{item.city}</h5>
<div className="flex items-center gap-2 mt-1">
<span className={`inline-flex px-2.5 py-0.5 rounded-full text-xs font-medium ${item.cardType === 'Flexi' ? 'bg-[#F95FAF]/10 text-[#F95FAF]' : 'bg-[#F95F62]/10 text-[#F95F62]'}`}>{item.cardType} Card</span>
<span className="flex items-center gap-1 font-poppins text-xs font-normal text-[#8e8e8e]"><Calendar className="w-3 h-3" />{item.days} days</span>
</div>
</div>
</div>
<div className="col-span-2 text-center">
<div className="flex items-center justify-center gap-3">
<span className="flex items-center gap-1 font-poppins text-sm font-normal text-[#555]"><Users className="w-3.5 h-3.5 text-[#8e8e8e]" />{item.adults}</span>
<span className="flex items-center gap-1 font-poppins text-sm font-normal text-[#555]"><Baby className="w-3.5 h-3.5 text-[#8e8e8e]" />{item.children}</span>
</div>
</div>
<div className="col-span-1 flex justify-center">
<span className="font-poppins text-sm font-medium text-[#2a2a2a] bg-gray-50 px-4 py-1.5 rounded-full">{item.quantity}</span>
</div>
<div className="col-span-3 text-right">
<span className="font-poppins text-lg font-medium text-[#F95F62] tracking-tight">${totalPrice.toFixed(2)}</span>
{item.quantity > 1 && <span className="block font-poppins text-xs font-normal text-[#aaa] mt-0.5">${item.pricePerUnit.toFixed(2)} per unit</span>}
</div>
<div className="col-span-1 flex justify-end">
<button onClick={(e) => { e.stopPropagation(); handleRemoveItem(item.id); }} className="p-2 rounded-lg text-gray-300 hover:text-[#F95F62] hover:bg-red-50 transition-all" title="Remove from cart">
<Trash2 className="w-4.5 h-4.5" />
</button>
</div>
</div>
</motion.div>
);
})}
</AnimatePresence>
{/* Bottom checkout bar */}
<motion.div layout className="mt-6 bg-white rounded-2xl ring-1 ring-gray-100 p-5 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-center sm:text-left">
{selectedItem ? (
<>
<p className="font-poppins text-xs font-normal text-[#8e8e8e]">
Selected: {selectedItem.city} {selectedItem.cardType} · {selectedItem.days}d · Qty {selectedItem.quantity}
</p>
<p className="font-poppins text-2xl font-medium text-[#F95F62] tracking-tight mt-0.5">
${(selectedItem.pricePerUnit * selectedItem.quantity).toFixed(2)}
</p>
</>
) : (
<p className="font-poppins text-sm font-normal text-[#8e8e8e]">Tap a card above to select it for checkout</p>
)}
</div>
<motion.button
whileHover={selectedItem ? { scale: 1.02 } : {}}
whileTap={selectedItem ? { scale: 0.98 } : {}}
onClick={handleGoToCheckout}
disabled={!selectedItem}
className={`w-full 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 <ChevronRight className="w-4 h-4" />
</motion.button>
</motion.div>
</div>
)}
</motion.div>
) : (
<motion.div key="postcards-content" initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -12 }} transition={{ duration: 0.2 }}>
<EmptyState icon={<Mail className="w-16 h-16 text-[#F95F62]/20" strokeWidth={1.2} />} 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} />
</motion.div>
)}
</AnimatePresence>
</motion.div>
) : (
/* ─── CHECKOUT VIEW ─── */
<motion.div
key="checkout-view"
initial={{ opacity: 0, x: 40 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 40 }}
transition={{ duration: 0.3 }}
className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto"
>
{checkoutItem && (
<>
{/* Back */}
<button onClick={handleBackToCart} className="flex items-center gap-2 text-[#8e8e8e] hover:text-[#2a2a2a] transition-colors font-poppins text-sm font-normal mb-8">
<ArrowLeft className="w-4 h-4" />Back to Cart
</button>
{/* Stepper */}
{/* <CheckoutStepper currentStep={2} /> */}
{/* Checkout heading */}
<div className="mb-10">
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
<span className="font-light">Checkout</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent">{checkoutItem.city}</span>
</h2>
</div>
<div className="flex flex-col lg:flex-row gap-10">
{/* Left column */}
<div className="flex-1 space-y-8">
{/* ── Card Type Selection (Figma cards) ── */}
<div>
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">
Choose Your Card
</h3>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
Select the card type that best suits your travel style
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-[16px]">
{/* Flexi */}
<button
onClick={() => handleCheckoutItemChange({ cardType: 'Flexi' })}
className="relative transition-all duration-200"
>
<FlexiCardPreview
city={checkoutItem.city}
adultPrice={priceTable.Flexi[checkoutItem.days] || 80}
childPrice={10}
isSelected={checkoutItem.cardType === 'Flexi'}
/>
</button>
{/* Unlimited */}
<button
onClick={() => handleCheckoutItemChange({ cardType: 'Unlimited' })}
className="relative transition-all duration-200"
>
<UnlimitedCardPreview
city={checkoutItem.city}
adultPrice={priceTable.Unlimited[checkoutItem.days] || 120}
childPrice={20}
isSelected={checkoutItem.cardType === 'Unlimited'}
/>
</button>
</div>
{/* ── Config Card (mobile only) — right after card selection ── */}
<div className="lg:hidden mt-6">
<CheckoutConfigCard
item={checkoutItem}
onChange={handleCheckoutItemChange}
onProceed={() => checkoutItem && onSecureCheckoutClick?.(checkoutItem)}
/>
</div>
{/* Features Comparison */}
<div className="mt-6 bg-[#f5f5f5] rounded-xl p-4">
<div className="grid grid-cols-[1fr_70px_70px] gap-y-0 items-center">
{/* Header */}
<p className="font-poppins font-medium text-sm text-[#2a2a2a] py-3">Features</p>
<p className="font-poppins font-medium text-sm text-[#2a2a2a] text-center py-3">Flexi</p>
<p className="font-poppins font-medium text-sm text-[#2a2a2a] text-center py-3">Unlimited</p>
{[
{ 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) => (
<React.Fragment key={i}>
<p className="font-poppins font-normal text-[13px] text-[#2a2a2a] py-2.5 border-t border-[rgba(0,0,0,0.08)] flex items-center gap-1.5">
<span className="text-[#2a2a2a]"></span> {row.feature}
</p>
<div className="flex justify-center py-2.5 border-t border-[rgba(0,0,0,0.08)]">
{row.flexi ? (
<div className="w-5 h-5 rounded-full bg-[#F95F62] flex items-center justify-center">
<Check className="w-3 h-3 text-white" strokeWidth={3} />
</div>
) : (
<span className="font-poppins text-[13px] text-[rgba(0,0,0,0.3)]"></span>
)}
</div>
<div className="flex justify-center py-2.5 border-t border-[rgba(0,0,0,0.08)]">
{row.unlimited ? (
<div className="w-5 h-5 rounded-full bg-[#F95F62] flex items-center justify-center">
<Check className="w-3 h-3 text-white" strokeWidth={3} />
</div>
) : (
<span className="font-poppins text-[13px] text-[rgba(0,0,0,0.3)]"></span>
)}
</div>
</React.Fragment>
))}
</div>
</div>
</div>
{/* ── Offers ── */}
<div>
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">
{checkoutItem.cardType} Card Offers
</h3>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
Exclusive deals and discounts included with your {checkoutItem.cardType} pass
</p>
<div className="flex gap-3 overflow-x-auto pb-2 -mx-4 px-4 snap-x snap-mandatory scrollbar-hide">
{offers.map((offer, idx) => (
<div key={idx} className="relative bg-white rounded-xl shrink-0 w-[180px] h-[260px] snap-start">
<div className="flex flex-col gap-2 items-start overflow-hidden p-3 rounded-xl h-full">
<div className="h-[120px] w-full rounded-lg overflow-hidden shrink-0 relative">
<ImageWithFallback
src={offer.image}
alt={offer.title}
className="absolute inset-0 w-full h-full object-cover rounded-lg"
/>
</div>
<div className="w-full h-[44px] overflow-hidden">
<p className="font-['Poppins',sans-serif] font-normal text-[18px] text-black tracking-[-0.72px] leading-[22px] line-clamp-2">
{offer.title}
</p>
</div>
<div className="w-full flex-1">
<p className="font-['Poppins',sans-serif] font-normal text-[12px] text-[rgba(0,0,0,0.6)] leading-[16px] line-clamp-3">
{offer.description}
</p>
</div>
</div>
<div className="absolute inset-0 border border-[rgba(249,95,98,0.24)] rounded-xl pointer-events-none" />
</div>
))}
</div>
</div>
{/* ── Available Attractions ── */}
<div>
<div className="flex items-center justify-between">
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">Available Attractions</h3>
<span className="font-poppins text-xs font-medium text-[#F95F62] bg-[#F95F62]/10 px-3 py-1 rounded-full">{attractions.length} included</span>
</div>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
Explore all the experiences you can enjoy with your pass
</p>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
{attractions.map((a) => (
<div key={a.id} className="group relative rounded-xl overflow-hidden">
<div className="aspect-[4/3] relative">
<ImageWithFallback src={a.image} alt={a.name} className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/10 to-transparent" />
<div className="absolute top-2 right-2">
<span className="inline-flex px-2 py-0.5 rounded-full bg-white/90 backdrop-blur-sm text-[10px] font-poppins font-medium text-[#555]">{a.category}</span>
</div>
<div className="absolute bottom-2 left-2 right-2">
<h6 className="font-poppins text-sm leading-snug font-medium text-white drop-shadow-sm">{a.name}</h6>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right column: Config card (desktop only, sticky) */}
<div className="hidden lg:block lg:w-[420px] flex-shrink-0">
<div className="lg:sticky lg:top-28">
<CheckoutConfigCard
item={checkoutItem}
onChange={handleCheckoutItemChange}
onProceed={() => checkoutItem && onSecureCheckoutClick?.(checkoutItem)}
/>
</div>
</div>
</div>
</>
)}
</motion.div>
)}
</AnimatePresence>
<Footer
onHomeClick={onHomeClick} onPassesClick={onPassesClick} onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick} onHowItWorksClick={onHowItWorksClick} onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick} onContactUsClick={onContactUsClick}
/>
</div>
);
}
/* ─── Empty state ─── */
function EmptyState({ icon, title, description, actionLabel, onAction }: {
icon: React.ReactNode; title: string; description: string; actionLabel: string; onAction?: () => void;
}) {
return (
<motion.div initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.4 }} className="flex flex-col items-center justify-center py-20 max-w-sm mx-auto text-center">
<motion.div className="w-28 h-28 rounded-3xl bg-[#fee7e7]/50 flex items-center justify-center mb-6" animate={{ y: [0, -6, 0] }} transition={{ duration: 3, repeat: Infinity, ease: 'easeInOut' }}>{icon}</motion.div>
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a] mb-2">{title}</h3>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mb-8">{description}</p>
<motion.button whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} onClick={onAction} className="bg-[#F95F62] text-white font-poppins text-base font-medium px-8 py-3.5 rounded-xl hover:bg-[#e8545a] transition-colors shadow-lg shadow-[#F95F62]/15 w-full">{actionLabel}</motion.button>
</motion.div>
);
}

View File

@@ -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';

886
src/pages/CheckoutPage2.tsx Normal file
View File

@@ -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<string, Record<number, number>> = {
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<string, Record<string, Attraction[]>> = {
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<string, { title: string; description: string; image: string }[]> = {
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 (
<div className={`relative h-[160px] w-full rounded-lg transition-all duration-200 ${isSelected ? 'ring-2 ring-[#F95F62] shadow-md shadow-[#F95F62]/10' : 'hover:shadow-md'
}`}>
{/* Card bg */}
<div className="absolute inset-0 bg-white border border-[rgba(249,95,175,0.2)] rounded-lg shadow-[0px_4px_20px_0px_rgba(0,0,0,0.06)]" />
{/* City image */}
<div className="absolute h-[158px] left-[1px] top-[1px] w-[103px] rounded-bl-[7px] rounded-tl-[7px] overflow-hidden">
<img alt="" className="absolute inset-0 w-full h-full object-cover" src={image} />
</div>
{/* City name - left aligned */}
<div className="absolute left-[112px] top-[12px]">
<p className="font-['Poppins',sans-serif] font-medium text-[16px] text-[#2a2a2a] leading-[22px] whitespace-nowrap">{city}</p>
</div>
{/* Pricing */}
<div className="absolute left-[112px] top-[40px] flex flex-col gap-[6px]">
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">From</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${adultPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Adult</span>
</div>
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">and</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${childPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Child</span>
</div>
</div>
{/* Description */}
<div className="absolute left-[112px] top-[112px] right-[44px]">
<p className="font-['Poppins',sans-serif] text-[11px] text-left text-[rgba(0,0,0,0.4)] tracking-[0.06px] leading-[14px]">
Dive into an extensive selection of thrilling destinations!
</p>
</div>
{/* Side tab - Flexi (pink) */}
<div className="absolute bg-[#f95faf] h-full right-0 top-0 w-[35px] rounded-br-lg rounded-tr-lg flex flex-col items-center justify-center gap-[2px]">
<span className="font-['Poppins',sans-serif] text-[12px] text-white/70 [writing-mode:vertical-rl] rotate-180">Card</span>
<span className="font-['Poppins',sans-serif] text-[16px] text-white [writing-mode:vertical-rl] rotate-180">Flexi</span>
</div>
{/* Selected checkmark */}
{isSelected && (
<div className="absolute top-2 right-[44px] w-6 h-6 rounded-full bg-[#F95F62] flex items-center justify-center z-10">
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
</div>
)}
</div>
);
}
function UnlimitedCardPreview({ city, adultPrice, childPrice, isSelected, image }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean, image: string; }) {
return (
<div className={`relative h-[160px] w-full rounded-lg transition-all duration-200 ${isSelected ? 'ring-2 ring-[#F95F62] shadow-md shadow-[#F95F62]/10' : 'hover:shadow-md'
}`}>
{/* Card bg */}
<div className="absolute inset-0 bg-white border border-[rgba(0,0,0,0.2)] rounded-lg" />
{/* City image */}
<div className="absolute h-[158px] left-[1px] top-[1px] w-[103px] rounded-bl-[7px] rounded-tl-[7px] overflow-hidden">
<img alt="" className="absolute inset-0 w-full h-full object-cover" src={image} />
</div>
{/* City name - left aligned */}
<div className="absolute left-[112px] top-[12px]">
<p className="font-['Poppins',sans-serif] font-medium text-[16px] text-[#2a2a2a] leading-[20px] whitespace-nowrap">{city}</p>
</div>
{/* Pricing */}
<div className="absolute left-[112px] top-[40px] flex flex-col gap-[6px]">
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">From</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${adultPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Adult</span>
</div>
<div className="flex gap-[2px] items-center">
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">and</span>
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${childPrice}</span>
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Child</span>
</div>
</div>
{/* Description */}
<div className="absolute left-[112px] top-[112px] right-[44px]">
<p className="font-['Poppins',sans-serif] text-[11px] text-left text-[rgba(0,0,0,0.4)] tracking-[0.06px] leading-[14px]">
Dive into an extensive selection of thrilling destinations!
</p>
</div>
{/* Side tab - Unlimited (coral) */}
<div className="absolute bg-[#f95f62] h-full right-0 top-0 w-[35px] rounded-br-lg rounded-tr-lg flex flex-col items-center justify-center gap-[2px]">
<span className="font-['Poppins',sans-serif] text-[12px] text-white/70 [writing-mode:vertical-rl] rotate-180">Card</span>
<span className="font-['Poppins',sans-serif] text-[16px] text-white [writing-mode:vertical-rl] rotate-180">Unlimited</span>
</div>
{/* Selected checkmark */}
{isSelected && (
<div className="absolute top-2 right-[44px] w-6 h-6 rounded-full bg-[#F95F62] flex items-center justify-center z-10">
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
</div>
)}
</div>
);
}
/* ─── 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 (
<div className="bg-white rounded-2xl shadow-[0px_4px_24px_0px_rgba(0,0,0,0.06)] overflow-hidden w-full max-w-[400px]">
<div className="pt-6 pb-2 text-center">
<h4 className="font-poppins text-lg leading-snug font-medium text-[#2a2a2a]">{cityName}</h4>
<div className="mt-2 flex justify-center">
<span className={`inline-flex items-center px-4 py-1 rounded-full font-poppins text-xs font-medium ${item?.cardType?.name === 'selective_pass' ? 'bg-[#f95faf]/10 text-[#f95faf]' : 'bg-[#f95f62]/10 text-[#f95f62]'}`}>
{item?.cardType?.displayName}
</span>
</div>
</div>
<div className="px-6 py-4 space-y-0">
<div className="flex items-center justify-between py-4 border-b border-gray-100">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">No. of Adults</span>
<div className="flex items-center gap-3">
<button onClick={() => noOfAdults > 1 && setNoOfAdults((prev) => prev - 1)} disabled={noOfAdults <= 1} className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${noOfAdults <= 1 ? 'bg-gray-100 text-gray-300 cursor-not-allowed' : 'bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20'}`}>
<Minus className="w-4 h-4" />
</button>
<span className="font-poppins text-base font-medium text-[#2a2a2a] w-5 text-center tabular-nums">{noOfAdults}</span>
<button onClick={() => setNoOfAdults((prev) => prev + 1)} disabled={noOfAdults >= 15} className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${noOfAdults >= 15 ? 'bg-gray-100 text-gray-300 cursor-not-allowed' : 'bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20'}`}>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
<div className="flex items-center justify-between py-4 border-b border-gray-100">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">No. of Children</span>
<div className="flex items-center gap-3">
<button onClick={() => noOfChildren > 0 && setNoOfChildren((prev) => prev - 1)} disabled={noOfChildren <= 0} className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${noOfChildren <= 0 ? 'bg-gray-100 text-gray-300 cursor-not-allowed' : 'bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20'}`}>
<Minus className="w-4 h-4" />
</button>
<span className="font-poppins text-base font-medium text-[#2a2a2a] w-5 text-center tabular-nums">{noOfChildren}</span>
<button onClick={() => setNoOfChildren((prev) => prev + 1)} disabled={noOfChildren >= 10} className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${noOfChildren >= 10 ? 'bg-gray-100 text-gray-300 cursor-not-allowed' : 'bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20'}`}>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
<div className="flex items-center justify-between py-4 border-b border-gray-100">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">
{item?.cardType?.name === 'selective_pass' ? 'No. of Attractions' : 'No. of Days'}
</span>
<div className="relative">
<button onClick={() => setDropdownOpen(!dropdownOpen)} className="flex items-center gap-2 border border-[#f95f62]/30 rounded-lg px-3 py-1.5 min-w-[72px] justify-between hover:border-[#f95f62] transition-colors">
<span className="font-poppins text-base font-medium text-[#f95f62] tabular-nums">{cardMode === "flexi" ? noOfAttractions : noOfDays}</span>
<ChevronDown className={`w-4 h-4 text-[#f95f62] transition-transform ${dropdownOpen ? 'rotate-180' : ''}`} />
</button>
<AnimatePresence>
{dropdownOpen && (
<motion.div
initial={{ opacity: 0, y: -4, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -4, scale: 0.95 }}
className="absolute right-0 top-full mt-1 bg-white rounded-lg shadow-lg border border-gray-100 z-30 min-w-[72px]
max-h-48 overflow-y-auto"
>
{numberArray.map((i) => (
<button
key={i}
onClick={() => {
cardMode === "flexi" ? setNoOfAttractions(i) : setNoOfDays(i);
setDropdownOpen(false);
}}
className={`w-full px-3 py-2 text-left font-poppins text-sm transition-colors ${(cardMode === "flexi" ? noOfAttractions === i : noOfDays === i)
? "bg-[#f95f62]/10 text-[#f95f62] font-medium"
: "text-[#2a2a2a] hover:bg-gray-50 font-normal"
}`}
>
{i}
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<div className="flex items-center justify-between py-5">
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">You Pay</span>
<div className="flex items-center gap-2">
<span className="font-poppins text-sm font-normal text-[#aaa] line-through">${strikedPrice}</span>
<span className="font-poppins text-2xl font-medium text-[#f95f62] tracking-tight">${basePrice}</span>
</div>
</div>
</div>
<div className="px-6 pb-6">
<motion.button whileHover={{ scale: 1.01 }} whileTap={{ scale: 0.98 }} onClick={handleProceedToPayment} 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 cursor-pointer">
Proceed to Pay
</motion.button>
</div>
</div>
);
}
/* ─── 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 <LoadingSpinner />
} else {
// console.log(flexiCard)
}
const handleCheckoutItemChange = (cardObject: any) => {
setCheckoutItem(cardObject);
};
return (
<div className="min-h-screen bg-[#fafafa] font-poppins">
<Navbar
activeCity="Melbourne"
onCityChange={() => { }}
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}
/>
<div className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto">
<button onClick={() => navigate(-1)} className="flex items-center gap-2 text-[#8e8e8e] hover:text-[#2a2a2a] transition-colors font-poppins text-sm font-normal mb-8">
<ArrowLeft className="w-4 h-4" />Back
</button>
<div className="mb-10">
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
<span className="font-light">Checkout</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent pr-2">{cityName}</span>
</h2>
</div>
<div className="flex flex-col lg:flex-row gap-10">
{/* Left Column */}
<div className="flex-1 space-y-8">
{/* Card Type Selection */}
<div>
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">Choose Your Card</h3>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">Select the card type that best suits your travel style</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-[16px]">
<button onClick={() => handleCheckoutItemChange(flexiCard)}>
<FlexiCardPreview city={cityName} image={cityImage} adultPrice={flexiCard.adultPrice} childPrice={flexiCard.childPrice} isSelected={checkoutItem?.cardType.name === 'selective_pass'} />
</button>
<button onClick={() => handleCheckoutItemChange(unlimitedCard)}>
<UnlimitedCardPreview city={cityName} image={cityImage} adultPrice={unlimitedCard.adultPrice} childPrice={unlimitedCard.childPrice} isSelected={checkoutItem?.cardType.name === 'unlimited_card'} />
</button>
</div>
{/* Features Comparison (Exact Copy) */}
<div className="mt-6 bg-[#f5f5f5] rounded-xl p-4">
<div className="grid grid-cols-[1fr_70px_70px] gap-y-0 items-center">
<p className="font-poppins font-medium text-sm text-[#2a2a2a] py-3">Features</p>
<p className="font-poppins font-medium text-sm text-[#2a2a2a] text-center py-3">Flexi</p>
<p className="font-poppins font-medium text-sm text-[#2a2a2a] text-center py-3">Unlimited</p>
{[
{ 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) => (
<React.Fragment key={i}>
<p className="font-poppins font-normal text-[13px] text-[#2a2a2a] py-2.5 border-t border-[rgba(0,0,0,0.08)] flex items-center gap-1.5">
<span className="text-[#2a2a2a]"></span> {row.feature}
</p>
<div className="flex justify-center py-2.5 border-t border-[rgba(0,0,0,0.08)]">
{row.flexi ? <div className="w-5 h-5 rounded-full bg-[#F95F62] flex items-center justify-center"><Check className="w-3 h-3 text-white" strokeWidth={3} /></div> : <span className="font-poppins text-[13px] text-[rgba(0,0,0,0.3)]"></span>}
</div>
<div className="flex justify-center py-2.5 border-t border-[rgba(0,0,0,0.08)]">
{row.unlimited ? <div className="w-5 h-5 rounded-full bg-[#F95F62] flex items-center justify-center"><Check className="w-3 h-3 text-white" strokeWidth={3} /></div> : <span className="font-poppins text-[13px] text-[rgba(0,0,0,0.3)]"></span>}
</div>
</React.Fragment>
))}
</div>
</div>
</div>
{/* Offers Section (Exact) */}
<div>
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">{checkoutItem?.cardType?.displayName} Offers</h3>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">Exclusive deals and discounts included with your {checkoutItem?.cardType.displayName} pass</p>
<div className="flex gap-3 overflow-x-auto pb-2 -mx-4 px-4 snap-x snap-mandatory scrollbar-hide">
{checkoutItem?.offers.map((offer: any) => (
<div key={offer.id} className="relative bg-white rounded-xl shrink-0 w-[180px] h-[260px] snap-start">
<div className="flex flex-col gap-2 items-start overflow-hidden p-3 rounded-xl h-full">
<div className="h-[120px] w-full rounded-lg overflow-hidden shrink-0 relative">
<ImageWithFallback src={`${baseUrl}/${offer.websiteBannerImage}`} alt={offer.title} className="absolute inset-0 w-full h-full object-cover rounded-lg" />
</div>
<div className="w-full h-[44px] overflow-hidden">
<p className="font-['Poppins',sans-serif] font-normal text-[18px] text-black tracking-[-0.72px] leading-[22px] line-clamp-2">{offer.title}</p>
</div>
<div className="w-full flex-1">
<p className="font-['Poppins',sans-serif] font-normal text-[12px] text-[rgba(0,0,0,0.6)] leading-[16px] line-clamp-3">{offer.description}</p>
</div>
</div>
<div className="absolute inset-0 border border-[rgba(249,95,98,0.24)] rounded-xl pointer-events-none" />
</div>
))}
</div>
</div>
{/* Attractions Section (Exact) */}
<div>
<div className="flex items-center justify-between">
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">Available Attractions</h3>
<span className="font-poppins text-xs font-medium text-[#F95F62] bg-[#F95F62]/10 px-3 py-1 rounded-full">{attractions.length} included</span>
</div>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">Explore all the experiences you can enjoy with your pass</p>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
{attractions.map((a: any) => (
<div key={a.id} className="group relative rounded-xl overflow-hidden">
<div className="aspect-[4/3] relative">
<ImageWithFallback src={a.thumbnail} alt={a.title} className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/10 to-transparent" />
<div className="absolute top-2 right-2">
<span className="inline-flex px-2 py-0.5 rounded-full bg-white/90 backdrop-blur-sm text-[10px] font-poppins font-medium text-[#555]">{a.category}</span>
</div>
<div className="absolute bottom-2 left-2 right-2">
<h6 className="font-poppins text-sm leading-snug font-medium text-white drop-shadow-sm">{a.title}</h6>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right Column - Config Card */}
<div className="hidden lg:block lg:w-[420px] flex-shrink-0">
<div className="lg:sticky lg:top-28">
<CheckoutConfigCard
item={checkoutItem}
onChange={handleCheckoutItemChange}
onProceed={() => navigate("/payment")}
/>
</div>
</div>
{/* Mobile Config Card */}
{/* <div className="lg:hidden mt-6">
<CheckoutConfigCard
item={checkoutItem}
onChange={handleCheckoutItemChange}
onProceed={() => navigate("/payment")}
/>
</div> */}
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onContactUsClick={onContactUsClick}
/>
</div>
);
}
// 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<CartItem>({
// 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 <LoadingSpinner />;
// }
// const handleCheckoutItemChange = (updates: Partial<CartItem>) => {
// 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 (
// <div className="min-h-screen bg-[#fafafa] font-poppins">
// <Navbar
// activeCity={city?.name || "Melbourne"}
// onCityChange={() => { }}
// 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}
// />
// <div className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto">
// <button
// onClick={() => navigate(-1)}
// className="flex items-center gap-2 text-[#8e8e8e] hover:text-[#2a2a2a] transition-colors font-poppins text-sm font-normal mb-8"
// >
// <ArrowLeft className="w-4 h-4" />Back
// </button>
// <div className="mb-10">
// <h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
// <span className="font-light">Checkout</span>{' '}
// <span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent pr-3">
// {city?.name || checkoutItem.city}
// </span>
// </h2>
// </div>
// <div className="flex flex-col lg:flex-row gap-10">
// {/* Left Column */}
// <div className="flex-1 space-y-8">
// {/* Card Type Selection */}
// <div>
// <h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">Choose Your Card</h3>
// <p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
// Select the card type that best suits your travel style
// </p>
// <div className="grid grid-cols-1 sm:grid-cols-2 gap-[16px]">
// {allCards.map((card) => (
// <button
// key={card.id}
// onClick={() => handleCheckoutItemChange({
// cardType: card.cardType?.displayName || 'Flexi'
// })}
// >
// {card.cardType?.name === 'selective_pass' ? (
// <FlexiCardPreview
// city={city?.name || checkoutItem.city}
// adultPrice={card.adultPrice}
// childPrice={card.childPrice}
// isSelected={checkoutItem.cardType === card.cardType?.displayName}
// />
// ) : (
// <UnlimitedCardPreview
// city={city?.name || checkoutItem.city}
// adultPrice={card.adultPrice}
// childPrice={card.childPrice}
// isSelected={checkoutItem.cardType === card.cardType?.displayName}
// />
// )}
// </button>
// ))}
// </div>
// {/* Features Comparison - Kept as is (no CSS change) */}
// <div className="mt-6 bg-[#f5f5f5] rounded-xl p-4">
// <div className="grid grid-cols-[1fr_70px_70px] gap-y-0 items-center">
// {/* Header */}
// <p className="font-poppins font-medium text-sm text-[#2a2a2a] py-3">Features</p>
// <p className="font-poppins font-medium text-sm text-[#2a2a2a] text-center py-3">Flexi</p>
// <p className="font-poppins font-medium text-sm text-[#2a2a2a] text-center py-3">Unlimited</p>
// {[
// { 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) => (
// <React.Fragment key={i}>
// <p className="font-poppins font-normal text-[13px] text-[#2a2a2a] py-2.5 border-t border-[rgba(0,0,0,0.08)] flex items-center gap-1.5">
// <span className="text-[#2a2a2a]">•</span> {row.feature}
// </p>
// <div className="flex justify-center py-2.5 border-t border-[rgba(0,0,0,0.08)]">
// {row.flexi ? (
// <div className="w-5 h-5 rounded-full bg-[#F95F62] flex items-center justify-center">
// <Check className="w-3 h-3 text-white" strokeWidth={3} />
// </div>
// ) : (
// <span className="font-poppins text-[13px] text-[rgba(0,0,0,0.3)]"></span>
// )}
// </div>
// <div className="flex justify-center py-2.5 border-t border-[rgba(0,0,0,0.08)]">
// {row.unlimited ? (
// <div className="w-5 h-5 rounded-full bg-[#F95F62] flex items-center justify-center">
// <Check className="w-3 h-3 text-white" strokeWidth={3} />
// </div>
// ) : (
// <span className="font-poppins text-[13px] text-[rgba(0,0,0,0.3)]"></span>
// )}
// </div>
// </React.Fragment>
// ))}
// </div>
// </div>
// </div>
// {/* Offers Section */}
// <div>
// <h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">
// {checkoutItem.cardType} Card Offers
// </h3>
// <p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
// Exclusive deals and discounts included with your {checkoutItem.cardType} pass
// </p>
// <div className="flex gap-3 overflow-x-auto pb-2 -mx-4 px-4 snap-x snap-mandatory scrollbar-hide">
// {currentOffers.map((offer, idx) => (
// <div key={idx} className="relative bg-white rounded-xl shrink-0 w-[180px] h-[260px] snap-start">
// <div className="flex flex-col gap-2 items-start overflow-hidden p-3 rounded-xl h-full">
// <div className="h-[120px] w-full rounded-lg overflow-hidden shrink-0 relative">
// <ImageWithFallback
// src={`${baseUrl}/${offer.websiteBannerImage}` || offer.mobileBannerImage}
// alt={offer.title}
// className="absolute inset-0 w-full h-full object-cover rounded-lg"
// />
// </div>
// <div className="w-full h-[44px] overflow-hidden">
// <p className="font-['Poppins',sans-serif] font-normal text-[18px] text-black tracking-[-0.72px] leading-[22px] line-clamp-2">
// {offer.title}
// </p>
// </div>
// <div className="w-full flex-1">
// <p className="font-['Poppins',sans-serif] font-normal text-[12px] text-[rgba(0,0,0,0.6)] leading-[16px] line-clamp-3">
// {offer.description}
// </p>
// </div>
// </div>
// <div className="absolute inset-0 border border-[rgba(249,95,98,0.24)] rounded-xl pointer-events-none" />
// </div>
// ))}
// </div>
// </div>
// {/* Attractions Section */}
// <div>
// <div className="flex items-center justify-between">
// <h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">Available Attractions</h3>
// <span className="font-poppins text-xs font-medium text-[#F95F62] bg-[#F95F62]/10 px-3 py-1 rounded-full">
// {allAttractions.length} included
// </span>
// </div>
// <p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
// Explore all the experiences you can enjoy with your pass
// </p>
// <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
// {allAttractions.map((attraction) => (
// <div key={attraction.id} className="group relative rounded-xl overflow-hidden">
// <div className="aspect-[4/3] relative">
// <ImageWithFallback
// src={attraction.thumbnail}
// alt={attraction.title}
// className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
// />
// <div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/10 to-transparent" />
// <div className="absolute bottom-2 left-2 right-2">
// <h6 className="font-poppins text-sm leading-snug font-medium text-white drop-shadow-sm">
// {attraction.title}
// </h6>
// </div>
// </div>
// </div>
// ))}
// </div>
// </div>
// </div>
// {/* Right Column - Config Card */}
// <div className="hidden lg:block lg:w-[420px] flex-shrink-0">
// <div className="lg:sticky lg:top-28">
// <CheckoutConfigCard
// item={checkoutItem}
// onChange={handleCheckoutItemChange}
// onProceed={() => navigate("/payment")}
// />
// </div>
// </div>
// {/* Mobile Config Card */}
// <div className="lg:hidden mt-6">
// <CheckoutConfigCard
// item={checkoutItem}
// onChange={handleCheckoutItemChange}
// onProceed={() => navigate("/payment")}
// />
// </div>
// </div>
// </div>
// <Footer
// onHomeClick={onHomeClick}
// onPassesClick={onPassesClick}
// onAttractionsClick={onAttractionsClick}
// onBlogsClick={onBlogsClick}
// onHowItWorksClick={onHowItWorksClick}
// onFAQClick={onFAQClick}
// onPrivacyPolicyClick={onPrivacyPolicyClick}
// onAboutUsClick={onAboutUsClick}
// onContactUsClick={onContactUsClick}
// />
// </div>
// );
// }

View File

@@ -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 (
<LoadingSpinner />
)
}
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];
@@ -259,7 +272,7 @@ export function MelbournePage({
{/* Pass Comparison */}
<div id="passes" className="scroll-mt-32">
<MelbourneCardComparison />
<MelbourneCardComparison cards={cards} />
</div>
{/* Tour Overview */}

View File

@@ -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,12 +150,20 @@ export function PassesPage({
onSignInClick,
onSignOutClick,
}: PassesPageProps) {
const [selectedPass, setSelectedPass] = useState<string>('unlimited');
const [selectedPass, setSelectedPass] = useState<string>(passTypes[1].id);
const [isLoginOpen, setIsLoginOpen] = useState(false);
const { user } = useAuth(); // from AuthContext
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 (<LoadingSpinner />)
}
const handleCheckoutClick = () => {
@@ -189,7 +199,7 @@ export function PassesPage({
<div className="text-center mb-16">
<div className="mb-6">
<h1 className="font-merchant font-light text-4xl md:text-5xl lg:text-6xl mb-4">
Buy <span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pr-1.5">Passes</span>
Buy <span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pr-1.5">Cards</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
Skip the lines, save money, and explore more with our flexible city cards designed for modern travelers
@@ -200,99 +210,64 @@ export function PassesPage({
{/* Pass Comparison Section */}
<div className="mb-20">
<RadioGroup
value={selectedPass}
onValueChange={setSelectedPass}
className="grid md:grid-cols-2 gap-8 max-w-6xl mx-auto"
>
{passTypes.map((pass) => (
<div key={pass.id} className="relative h-full">
<Card className={`relative h-full flex flex-col transition-all duration-300 ${pass.popular
? 'ring-2 ring-primary shadow-xl'
: selectedPass === pass.id
? 'ring-2 ring-primary/50 shadow-lg'
: 'border-gray-200 shadow-md hover:shadow-lg hover:border-primary/30'
}`}>
{/* Popular Badge */}
{pass.popular && (
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2 z-10">
<Badge className="bg-gradient-to-r from-yellow-400 to-orange-500 text-black px-6 py-1.5 font-semibold shadow-lg font-poppins">
Most Popular
</Badge>
</div>
)}
{/* Radio Button */}
{/* Flexi Pass Card */}
<div className="relative h-full">
<Card
className={`relative h-full flex flex-col transition-all duration-300 cursor-pointer ${selectedPass === passTypes[0].id
? "ring-2 ring-red-500 shadow-lg" // 🔴 red border when selected
: "border-gray-200 shadow-md hover:shadow-lg hover:border-primary/30"
}`}
onClick={() => setSelectedPass(passTypes[0].id)}
>
<div className="absolute top-5 right-5 z-10">
<RadioGroupItem value={pass.id} id={pass.id} className="w-5 h-5" />
{/* <RadioGroupItem value={passTypes[0].id} id={passTypes[0].id} className="w-5 h-5" /> */}
</div>
{/* Header - Fixed Height */}
<CardHeader className="text-center pb-4 pt-8 flex-shrink-0">
<CardTitle className="font-merchant text-2xl leading-tight mb-3 text-gray-900">
{pass.title}
{cards[0].title}
</CardTitle>
<CardDescription className="font-poppins text-sm text-gray-600 leading-relaxed font-normal min-h-[48px] flex items-center justify-center px-4">
{pass.description}
{cards[0].description}
</CardDescription>
</CardHeader>
{/* Attraction Images Grid */}
<div className="px-6 pb-4 flex-shrink-0">
<div className="grid grid-cols-4 gap-3">
<div className="aspect-square rounded-xl overflow-hidden shadow-md hover:shadow-lg transition-all duration-300 ring-1 ring-gray-200 hover:ring-primary/50 group">
<ImageWithFallback
src="https://images.unsplash.com/photo-1639655001512-e4b58d4874b8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBGZWRlcmF0aW9uJTIwU3F1YXJlfGVufDF8fHx8MTc2MjQyMzkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="Federation Square"
className="w-full h-full object-cover group-hover:scale-105 group-hover:brightness-110 transition-all duration-500"
/>
</div>
<div className="aspect-square rounded-xl overflow-hidden shadow-md hover:shadow-lg transition-all duration-300 ring-1 ring-gray-200 hover:ring-primary/50 group">
<ImageWithFallback
src="https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBSb3lhbCUyMEJvdGFuaWMlMjBHYXJkZW5zfGVufDF8fHx8MTc2MjQyMzk2NHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="Royal Botanic Gardens"
className="w-full h-full object-cover group-hover:scale-105 group-hover:brightness-110 transition-all duration-500"
/>
</div>
<div className="aspect-square rounded-xl overflow-hidden shadow-md hover:shadow-lg transition-all duration-300 ring-1 ring-gray-200 hover:ring-primary/50 group">
<ImageWithFallback
src="https://images.unsplash.com/photo-1720044109127-0aee490512be?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBFdXJla2ElMjBTa3lkZWNrfGVufDF8fHx8MTc2MjQyMzk2NXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="Eureka Skydeck"
className="w-full h-full object-cover group-hover:scale-105 group-hover:brightness-110 transition-all duration-500"
/>
</div>
<div className="aspect-square rounded-xl overflow-hidden shadow-md hover:shadow-lg transition-all duration-300 ring-1 ring-gray-200 hover:ring-primary/50 group">
<ImageWithFallback
src="https://images.unsplash.com/photo-1705464079585-0975f0aa5013?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBOYXRpb25hbCUyMEdhbGxlcnl8ZW58MXx8fHwxNzYyNDIzOTY0fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="National Gallery"
className="w-full h-full object-cover group-hover:scale-105 group-hover:brightness-110 transition-all duration-500"
/>
</div>
</div>
</div>
{/* Pricing Section - Fixed Height */}
{/* Pricing */}
<div className="px-6 pb-6 flex-shrink-0">
<div className="flex items-baseline justify-center gap-2 mb-2">
<span className="text-5xl font-bold text-gray-900 font-poppins">{pass.price}</span>
<span className="text-gray-500 font-poppins text-base">/ {pass.period}</span>
<span className="text-5xl font-bold text-gray-900 font-poppins">
${cards[0].adultPrice}
</span>
<span className="text-gray-500 font-poppins text-base">
/ {passTypes[0].period}
</span>
</div>
<div className="h-5 flex items-center justify-center">
{pass.originalPrice && (
{cards[0].adultPrice && (
<div className="text-sm text-gray-500 font-poppins">
<span className="line-through mr-2">{pass.originalPrice}</span>
<span className="text-green-600 font-medium">Save {Math.round(((parseFloat(pass.originalPrice.slice(1)) - parseFloat(pass.price.slice(1))) / parseFloat(pass.originalPrice.slice(1))) * 100)}%</span>
{/* Strikethrough price = originalPrice + $5 */}
<span className="line-through mr-2">
${parseFloat(cards[0].adultPrice) + 5}
</span>
<span className="text-green-600 font-medium">
Save{" "}
{Math.round(
((5) / (parseFloat(cards[0].adultPrice) + 5)) * 100
)}
%
</span>
</div>
)}
</div>
</div>
{/* Content - Flexible Height with Fixed Features Area */}
<CardContent className="pt-0 pb-6 px-6 flex-grow flex flex-col">
{/* Features List - Fixed height */}
<div className="flex-grow mb-6">
<div className="space-y-3">
{pass.features.slice(0, 6).map((feature, index) => (
{passTypes[0].features.map((feature, index) => (
<div key={index} className="flex items-start gap-3">
<Check className="w-4 h-4 text-green-500 mt-1 flex-shrink-0" />
<span className="text-sm text-gray-700 font-poppins leading-relaxed font-normal">{feature}</span>
@@ -301,18 +276,18 @@ export function PassesPage({
</div>
</div>
{/* CTA Button - Pushed to bottom */}
<div className="flex-shrink-0 space-y-3">
<Button
className={`w-full h-12 rounded-lg font-semibold transition-all cursor-pointer duration-300 font-poppins ${pass.popular
? 'bg-primary hover:bg-primary/90 text-white shadow-md hover:shadow-lg'
: 'bg-gray-900 hover:bg-gray-800 text-white hover:shadow-md'
className={`w-full h-12 rounded-lg font-semibold transition-all cursor-pointer duration-300 font-poppins ${selectedPass === passTypes[0].id
? "bg-primary hover:bg-primary/90 text-white hover:shadow-lg"
: "bg-gray-400 hover:bg-gray-400 text-white hover:shadow-md"
}`}
onClick={user ? handleCheckoutClick : handleSignInClick}
disabled={selectedPass !== passTypes[0].id}
>
{user ? 'PURCHASE NOW' : 'LOGIN TO BUY PASS'}
</Button>
<p className="text-xs text-gray-500 text-center font-poppins font-normal leading-tight">
Free cancellation up to 24 hours Instant delivery
</p>
@@ -320,10 +295,99 @@ export function PassesPage({
</CardContent>
</Card>
</div>
{/* Unlimited Pass Card */}
<div className="relative h-full">
<Card
className={`relative h-full flex flex-col transition-all duration-300 cursor-pointer ${selectedPass === passTypes[1].id
? "ring-2 ring-red-500 shadow-lg" // 🔴 red border when selected
: "border-gray-200 shadow-md hover:shadow-lg hover:border-primary/30"
}`}
onClick={() => setSelectedPass(passTypes[1].id)}
>
{passTypes[1].popular && (
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2 z-10">
<Badge className="bg-gradient-to-r from-yellow-400 to-orange-500 text-black px-6 py-1.5 font-semibold shadow-lg font-poppins">
Most Popular
</Badge>
</div>
)}
<div className="absolute top-5 right-5 z-10">
{/* <RadioGroupItem value={passTypes[1].id} id={passTypes[1].id} className="w-5 h-5" /> */}
</div>
<CardHeader className="text-center pb-4 pt-8 flex-shrink-0">
<CardTitle className="font-merchant text-2xl leading-tight mb-3 text-gray-900">
{cards[1].title}
</CardTitle>
<CardDescription className="font-poppins text-sm text-gray-600 leading-relaxed font-normal min-h-[48px] flex items-center justify-center px-4">
{cards[1].description}
</CardDescription>
</CardHeader>
{/* Pricing */}
<div className="px-6 pb-6 flex-shrink-0">
<div className="flex items-baseline justify-center gap-2 mb-2">
<span className="text-5xl font-bold text-gray-900 font-poppins">${cards[1].adultPrice}</span>
<span className="text-gray-500 font-poppins text-base">/ {passTypes[1].period}</span>
</div>
<div className="h-5 flex items-center justify-center">
{cards[1].adultPrice && (
<div className="text-sm text-gray-500 font-poppins">
{/* Strikethrough price = originalPrice + $5 */}
<span className="line-through mr-2">
${parseFloat(cards[1].adultPrice) + 5}
</span>
<span className="text-green-600 font-medium">
Save{" "}
{Math.round(
((5) / (parseFloat(cards[1].adultPrice) + 5)) * 100
)}
%
</span>
</div>
)}
</div>
</div>
<CardContent className="pt-0 pb-6 px-6 flex-grow flex flex-col">
<div className="flex-grow mb-6">
<div className="space-y-3">
{passTypes[1].features.map((feature, index) => (
<div key={index} className="flex items-start gap-3">
<Check className="w-4 h-4 text-green-500 mt-1 flex-shrink-0" />
<span className="text-sm text-gray-700 font-poppins leading-relaxed font-normal">{feature}</span>
</div>
))}
</div>
</div>
<div className="flex-shrink-0 space-y-3">
<Button
className={`w-full h-12 rounded-lg font-semibold transition-all cursor-pointer duration-300 font-poppins ${selectedPass === passTypes[1].id
? "bg-primary hover:bg-primary/90 text-white hover:shadow-lg"
: "bg-gray-400 hover:bg-gray-400 text-white hover:shadow-md"
}`}
disabled={selectedPass !== passTypes[1].id}
onClick={user ? handleCheckoutClick : handleSignInClick}
>
{user ? 'PURCHASE NOW' : 'LOGIN TO BUY PASS'}
</Button>
<p className="text-xs text-gray-500 text-center font-poppins font-normal leading-tight">
Free cancellation up to 24 hours Instant delivery
</p>
</div>
</CardContent>
</Card>
</div>
</RadioGroup>
</div>
{/* Good to Know Section */}
<div className="mb-24">
<div className="text-center mb-16">

View File

@@ -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 (
<div className="min-h-screen bg-[#fafafa] font-poppins">
<Navbar
activeCity="Melbourne"
onCityChange={() => {}}
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}
/>
<div className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto flex justify-center items-center min-h-[60vh]">
<div className="bg-white rounded-2xl shadow-lg p-8 max-w-md w-full text-center">
<XCircle className="w-16 h-16 text-yellow-500 mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-[#2a2a2a]">Payment Cancelled</h2>
<p className="text-[#555] mt-2">
You cancelled the payment process. No charges have been made.
</p>
<button
onClick={() => navigate(-1)}
className="mt-6 px-6 py-3 bg-[#F95F62] text-white rounded-xl font-medium hover:bg-[#e8545a] transition"
>
Go Back & Try Again
</button>
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onAttractionsClick={() => {}}
onBlogsClick={() => {}}
onHowItWorksClick={() => {}}
onFAQClick={() => {}}
onPrivacyPolicyClick={() => {}}
onAboutUsClick={() => {}}
onContactUsClick={() => {}}
/>
</div>
);
}

View File

@@ -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<HTMLInputElement>['inputMode'];
prefilled?: boolean;
disabled?: boolean;
}) {
const [focused, setFocused] = useState(false);
return (
<div className="flex flex-col gap-1 w-full">
<label className="font-poppins text-sm font-normal text-[#555] leading-relaxed">
{label}
</label>
<div className="relative">
<input
type={type}
value={value}
onChange={(e) => 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 && (
<Pencil className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[#F95F62]/40" />
)}
</div>
{error && (
<span className="font-poppins text-xs font-normal text-red-500 flex items-center gap-1">
<AlertCircle className="w-3 h-3" />{error}
</span>
)}
</div>
);
}
/* ─── Card type badge ─── */
function CardTypeBadge({ cardType }: { cardType: 'Flexi' | 'Unlimited' }) {
return (
<span className={`inline-flex items-center px-3 py-1 rounded-full font-poppins text-xs font-medium ${cardType === 'Flexi'
? 'bg-[#f95faf]/10 text-[#f95faf]'
: 'bg-[#F95F62]/10 text-[#F95F62]'
}`}>
{cardType} Card
</span>
);
}
/* ─── 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<Record<string, string>>({});
const validate = () => {
const e: Record<string, string> = {};
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 <LoadingSpinner />;
}
return (
<div className="min-h-screen bg-[#fafafa] font-poppins">
<Navbar
activeCity="Melbourne"
onCityChange={() => {}}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onSuperSavingsClick={onSuperSavingsClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
onCartClick={onCartClick}
currentPage={currentPage as any}
user={user}
/>
<div className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto">
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-[#8e8e8e] hover:text-[#2a2a2a] transition-colors font-poppins text-sm font-normal mb-8"
>
<ArrowLeft className="w-4 h-4" /> Back
</button>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
<div className="flex items-center gap-4 mb-2">
<h1 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
<span className="font-light">Review & </span>
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent pr-2">Pay</span>
</h1>
<div className="flex items-center gap-2 text-[#22a86b]">
<Shield className="w-4 h-4" />
<span className="font-poppins text-sm font-medium">SSL Secured</span>
</div>
</div>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e]">
Complete your purchase securely. You will be redirected to Stripe to enter your card details.
</p>
</motion.div>
<div className="grid lg:grid-cols-3 gap-8">
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="lg:col-span-2">
<Card className="shadow-lg border-0 overflow-hidden">
<CardHeader className="pb-0 pt-6 px-6 border-b border-gray-100">
<div className="flex gap-2 mb-6">
<button
onClick={() => setSelectedTab('myself')}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${
selectedTab === 'myself'
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
}`}
>
<User className="w-4 h-4" />
<span>For myself</span>
</button>
<button
onClick={() => setSelectedTab('gift')}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${
selectedTab === 'gift'
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
}`}
>
<Gift className="w-4 h-4" />
<span>To gift Someone</span>
</button>
</div>
</CardHeader>
<CardContent className="px-6 py-6 space-y-6">
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="flex items-center gap-3 bg-[#F95F62]/6 border border-[#F95F62]/20 rounded-xl px-4 py-3"
>
<div className="w-8 h-8 rounded-lg bg-[#F95F62]/10 flex items-center justify-center flex-shrink-0">
<UserCheck className="w-4 h-4 text-[#F95F62]" />
</div>
<p className="font-poppins text-sm font-normal text-[#555] leading-relaxed">
<span className="font-medium text-[#2a2a2a]">Details pre-filled from your profile.</span>{' '}
{selectedTab === 'myself' ? 'Personal & billing details are locked.' : 'Only gift recipient details are editable.'}
</p>
</motion.div>
<Separator />
{/* Personal Information */}
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.1 }} className="space-y-5">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-[#F95F62] text-white rounded-full flex items-center justify-center font-poppins text-sm font-semibold flex-shrink-0">
1
</div>
<h2 className="font-poppins text-xl leading-snug font-semibold text-[#2a2a2a]">Personal Information</h2>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Field label="First Name" value={formData.firstName} onChange={() => {}} prefilled disabled />
<Field label="Last Name" value={formData.lastName} onChange={() => {}} prefilled disabled />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Field label="Email Address" value={formData.email} onChange={() => {}} type="email" prefilled disabled />
<Field label="Phone Number" value={formData.phone} onChange={() => {}} type="tel" prefilled disabled />
</div>
</motion.div>
{/* Gift Section */}
<AnimatePresence>
{selectedTab === 'gift' && (
<motion.div
key="gift-section"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.25 }}
className="overflow-hidden"
>
<div className="bg-[#F95F62]/[0.03] border border-[#F95F62]/15 rounded-xl px-5 py-4 space-y-4">
<div className="flex items-center gap-2">
<Gift className="w-4 h-4 text-[#F95F62]" />
<h3 className="font-poppins text-base font-semibold text-[#2a2a2a]">Gift Recipient Details</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Field
label="Recipient First Name"
value={giftFirstName}
onChange={setGiftFirstName}
placeholder="Enter recipient's first name"
error={errors.giftFirstName}
/>
<Field
label="Recipient Last Name"
value={giftLastName}
onChange={setGiftLastName}
placeholder="Enter recipient's last name"
error={errors.giftLastName}
/>
<Field
label="Recipient ISD Code"
value={giftIsd}
onChange={setGiftIsd}
placeholder="e.g., 61"
error={errors.giftIsd}
/>
<Field
label="Recipient Phone"
value={giftPhone}
onChange={setGiftPhone}
type="tel"
placeholder="Enter recipient's phone number"
error={errors.giftPhone}
/>
<Field
label="Recipient Email"
value={giftEmail}
onChange={setGiftEmail}
type="email"
placeholder="Enter recipient's email"
error={errors.giftEmail}
/>
<Field
label="Recipient City"
value={giftCity}
onChange={setGiftCity}
placeholder="Enter recipient's city"
error={errors.giftCity}
/>
<Field
label="Recipient Country"
value={giftCountry}
onChange={setGiftCountry}
placeholder="Enter recipient's country"
error={errors.giftCountry}
/>
<Field
label="Gift Message"
value={giftMessage}
onChange={setGiftMessage}
placeholder="Write a heartfelt message"
error={errors.giftMessage}
/>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<Separator />
{/* Billing Address */}
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.15 }} className="space-y-5">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-[#F95F62] text-white rounded-full flex items-center justify-center font-poppins text-sm font-semibold flex-shrink-0">
2
</div>
<h2 className="font-poppins text-xl leading-snug font-semibold text-[#2a2a2a]">Billing Address</h2>
</div>
<div className="space-y-4">
<Field label="Address 1" value={formData.address1} onChange={() => {}} prefilled disabled />
<Field label="Address 2" value={formData.address2} onChange={() => {}} prefilled disabled />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Field label="City / Suburb" value={formData.city} onChange={() => {}} prefilled disabled />
<Field label="State" value="Victoria" onChange={() => {}} prefilled disabled />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Field label="Postcode" value={formData.postalCode} onChange={() => {}} inputMode="numeric" prefilled disabled />
<Field label="Country" value={formData.country} onChange={() => {}} prefilled disabled />
</div>
</div>
</motion.div>
</CardContent>
</Card>
</motion.div>
{/* Right Column: Order Summary & Payment Button */}
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="lg:col-span-1">
<div className="lg:sticky lg:top-28 space-y-4">
<div className="bg-white rounded-2xl shadow-[0px_2px_16px_0px_rgba(0,0,0,0.06)] overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100">
<h3 className="font-poppins text-lg leading-snug font-semibold text-[#2a2a2a]">Order Summary</h3>
</div>
<div className="px-6 py-5 border-b border-gray-100">
<div className="flex items-start gap-4">
<div
className={`w-16 h-10 rounded-lg flex-shrink-0 flex items-center justify-center ${
bookingDetails?.cardMode?.toLowerCase() === 'flexi'
? 'bg-gradient-to-br from-[#f95faf] to-[#F95F62]'
: 'bg-gradient-to-br from-[#F95F62] to-[#c94245]'
}`}
>
<span className="font-poppins text-[10px] font-semibold text-white">{bookingDetails?.cardMode}</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h4 className="font-poppins text-base font-semibold text-[#2a2a2a]">{bookingDetails?.name}</h4>
<CardTypeBadge cardType={bookingDetails?.cardMode} />
</div>
<p className="font-poppins text-sm font-normal text-[#8e8e8e] mt-0.5">
{bookingDetails?.cardMode?.toLowerCase() === 'flexi'
? `${bookingDetails?.noOfAttractions} Attractions`
: `${bookingDetails?.noOfDays} Days`}
</p>
</div>
</div>
<div className="mt-4 space-y-2">
<div className="flex items-center justify-between">
<span className="font-poppins text-sm font-normal text-[#8e8e8e]">Adults</span>
<span className="font-poppins text-sm font-medium text-[#2a2a2a]">{bookingDetails?.totalAdult}</span>
</div>
<div className="flex items-center justify-between">
<span className="font-poppins text-sm font-normal text-[#8e8e8e]">Children</span>
<span className="font-poppins text-sm font-medium text-[#2a2a2a]">{bookingDetails?.totalChild}</span>
</div>
</div>
</div>
<div className="px-6 py-5">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="font-poppins text-sm font-normal text-[#555]">Subtotal</span>
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">${bookingDetails?.baseAmount}</span>
</div>
<div className="flex items-center justify-between">
<span className="font-poppins text-sm font-normal text-[#555]">GST (10%)</span>
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">${bookingDetails?.totalTaxAmount}</span>
</div>
<div className="flex items-center justify-between">
<span className="font-poppins text-sm font-normal text-[#555]">Booking fee</span>
<span className="font-poppins text-sm font-normal text-[#22a86b]">Free</span>
</div>
<div className="pt-3 border-t border-gray-100 flex items-center justify-between">
<span className="font-poppins text-base font-semibold text-[#2a2a2a]">Total</span>
<span className="font-poppins text-2xl font-semibold text-[#F95F62]">${bookingDetails?.totalAmount}</span>
</div>
</div>
</div>
</div>
<motion.button
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.98 }}
onClick={handlePayment}
disabled={isRedirecting || isCreatingPayment}
className="w-full py-4 rounded-2xl bg-[#F95F62] text-white font-poppins text-base font-semibold hover:bg-[#e8545a] transition-colors shadow-lg shadow-[#F95F62]/20 disabled:opacity-70 flex items-center justify-center gap-2"
>
{isRedirecting ? (
<>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full"
/>
Redirecting to Stripe...
</>
) : (
<>
<Lock className="w-4 h-4" />
Proceed to Payment · ${bookingDetails?.totalAmount}
</>
)}
</motion.button>
<p className="font-poppins text-xs font-normal text-[#aaa] text-center">
You will be redirected to Stripes secure checkout page to enter your card details.
By completing your purchase you agree to our Terms of Service and Privacy Policy.
</p>
</div>
</motion.div>
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onContactUsClick={onContactUsClick}
/>
</div>
);
}

View File

@@ -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<string>('');
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 (
<div className="min-h-screen bg-[#fafafa] font-poppins">
<Navbar
activeCity="Melbourne"
onCityChange={() => {}}
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}
/>
<div className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto flex justify-center items-center min-h-[60vh]">
<div className="bg-white rounded-2xl shadow-lg p-8 max-w-md w-full text-center">
{status === 'loading' && (
<>
<Loader2 className="w-16 h-16 text-[#F95F62] animate-spin mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-[#2a2a2a]">Confirming your payment...</h2>
<p className="text-[#555] mt-2">Please wait while we verify your transaction.</p>
</>
)}
{status === 'success' && (
<>
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-[#2a2a2a]">Payment Successful!</h2>
<p className="text-[#555] mt-2">Thank you for your purchase. Your order is now confirmed.</p>
<button
onClick={() => navigate('/my-orders')} // adjust to your orders page route
className="mt-6 px-6 py-3 bg-[#F95F62] text-white rounded-xl font-medium hover:bg-[#e8545a] transition"
>
View My Orders
</button>
</>
)}
{status === 'error' && (
<>
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-[#2a2a2a]">Payment Confirmation Failed</h2>
<p className="text-red-500 mt-2">{errorMsg}</p>
<button
onClick={() => navigate('/')}
className="mt-6 px-6 py-3 bg-gray-200 text-[#2a2a2a] rounded-xl font-medium hover:bg-gray-300 transition"
>
Go to Homepage
</button>
</>
)}
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onAttractionsClick={() => {}}
onBlogsClick={() => {}}
onHowItWorksClick={() => {}}
onFAQClick={() => {}}
onPrivacyPolicyClick={() => {}}
onAboutUsClick={() => {}}
onContactUsClick={() => {}}
/>
</div>
);
}

View File

@@ -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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#F95F62] mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
<LoadingSpinner/>
);
}

View File

@@ -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 <LoadingSpinner />;
}
// 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 (
<Layout
activeCity=""
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
user={user}
>
<div className="container mx-auto px-4 pt-40 pb-16 max-w-6xl">
{/* Back Button */}
<motion.div
className="mb-8"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<Button
variant="ghost"
onClick={onBackClick}
className="font-poppins font-medium text-base text-gray-600 hover:text-primary transition-colors duration-200"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Super-Savings Page
</Button>
</motion.div>
{/* Title and Badges Section */}
<div className="mb-8">
<div className="flex flex-wrap gap-3 mb-6">
{superSavingsBadges.map((badge: any, index: number) => (
<Badge
key={badge.badgeXid}
variant={index === 0 ? 'default' : 'secondary'}
className={`px-6 py-2 rounded-full text-sm transition-all duration-200 ${
index === 0
? 'bg-primary text-white shadow-lg'
: 'bg-primary/10 text-primary border border-primary/20'
}`}
>
{badge.badge.badgeName}
</Badge>
))}
</div>
<h1 className="text-4xl font-bold text-[#2d3134] leading-tight">
<span className="bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent">
{safeOffer.title}
</span>{' '}
<span className="text-[#2d3134]">
Day Trip by {safeOffer.partnerName || safeOffer.card?.title || 'Partner'}
</span>
</h1>
</div>
{/* Image Gallery Section - preserved exactly as original */}
<div className="grid grid-cols-4 grid-rows-2 gap-4 h-[510px] mb-12">
{/* Main large image */}
<div className="col-span-2 row-span-2">
<ImageWithFallback
src={ `${baseUrl}/${superSavingsGalleries[0]?.filePathUrl}` }
alt="Main attraction image"
className="w-full h-full object-cover rounded-lg"
/>
</div>
{/* Gallery images - use remaining images or repeat first if needed */}
{superSavingsGalleries.slice(1, 5).map((image: any) => (
<div key={image.id} className="col-span-1 row-span-1">
<ImageWithFallback
src={ `${baseUrl}/${image.filePathUrl}` }
alt={`Gallery image ${image.id}`}
className="w-full h-full object-cover rounded-lg"
/>
</div>
))}
{/* 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) => (
<div key={`placeholder-${idx}`} className="col-span-1 row-span-1">
<div className="w-full h-full bg-gray-100 rounded-lg flex items-center justify-center text-gray-400">
No Image
</div>
</div>
))}
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Left Content - Tour Details */}
<div className="lg:col-span-2 space-y-12">
{/* Overview Cards - preserved */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{/* Duration */}
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
<Clock className="w-6 h-6 text-primary" />
</div>
<h3 className="font-normal text-primary capitalize mb-1">Duration</h3>
<p className="text-sm text-[#717171] font-light">
{typeof durations === 'number' ? `${durations} mins` : durations}
</p>
</Card>
{/* Group Size */}
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
<Users className="w-6 h-6 text-primary" />
</div>
<h3 className="font-normal text-primary capitalize mb-1">Group Size</h3>
<p className="text-sm text-[#717171] font-light">{groupSize}</p>
</Card>
{/* Age Range */}
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
<Users className="w-6 h-6 text-primary" />
</div>
<h3 className="font-normal text-primary capitalize mb-1">Age Range</h3>
<p className="text-sm text-[#717171] font-light">{ageRange}</p>
</Card>
{/* Languages */}
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
<MapPin className="w-6 h-6 text-primary" />
</div>
<h3 className="font-normal text-primary capitalize mb-1">Languages</h3>
<p className="text-sm text-[#717171] font-light">
{superSavingsLanguages?.length > 0
? superSavingsLanguages?.map((lang: any) => lang.language.name).join(', ')
: 'English (default)'}
</p>
</Card>
</div>
{/* Tour Overview */}
<div>
<div className="flex items-center gap-4 mb-6">
<div className="h-1 w-12 bg-primary rounded-full"></div>
<h2 className="text-3xl font-semibold text-[#2d3134]">
Tour <span className="text-primary">Overview</span>
</h2>
</div>
<p className="text-[#2d3134] leading-relaxed text-lg font-light">
{safeOffer.description}
</p>
</div>
{/* Tour Highlights - preserved even if empty */}
<div>
<div className="flex items-center gap-4 mb-6">
<div className="h-1 w-12 bg-primary rounded-full"></div>
<h3 className="text-2xl font-medium text-[#2d3134]">
Tour <span className="text-primary">Highlights</span>
</h3>
</div>
{superSavingsHighlights.length > 0 ? (
<ul className="space-y-4">
{superSavingsHighlights.map((highlight: any) => (
<li key={highlight.id} className="flex items-start gap-3 group">
<div className="w-6 h-6 bg-primary/10 rounded-full mt-1 flex items-center justify-center flex-shrink-0 group-hover:bg-primary/20 transition-colors duration-200">
<div className="w-2 h-2 bg-primary rounded-full"></div>
</div>
<span className="text-[#2d3134] leading-relaxed font-light">{highlight.title}</span>
</li>
))}
</ul>
) : (
<p className="text-gray-500 italic">No highlights listed for this offer.</p>
)}
</div>
{/* What's Included/Not Included - preserved */}
<div>
<div className="flex items-center gap-4 mb-8">
<div className="h-1 w-12 bg-primary rounded-full"></div>
<h3 className="text-3xl font-semibold text-[#2d3134]">
What's <span className="text-primary">included</span>
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Included */}
<div className="space-y-4">
<h4 className="font-medium text-primary mb-4 flex items-center gap-2">
<Check className="w-5 h-5" />
Included
</h4>
{superSavingsInclusions.filter((inc: any) => inc.isInclusion === true).length > 0 ? (
superSavingsInclusions
.filter((inclusion: any) => inclusion.isInclusion === true)
.map((inclusion: any) => (
<div key={inclusion.id} className="flex items-start gap-3 group">
<div className="w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0 mt-1 group-hover:bg-primary/20 transition-colors duration-200">
<Check className="w-3 h-3 text-primary" />
</div>
<span className="text-[#2d3134] font-light">{inclusion.title}</span>
</div>
))
) : (
<p className="text-gray-500 italic">No included items specified.</p>
)}
</div>
{/* Not Included */}
<div className="space-y-4">
<h4 className="font-medium text-gray-600 mb-4 flex items-center gap-2">
<X className="w-5 h-5" />
Not Included
</h4>
{superSavingsInclusions.filter((inc: any) => inc.isInclusion === false).length > 0 ? (
superSavingsInclusions
.filter((inclusion: any) => inclusion.isInclusion === false)
.map((inclusion: any) => (
<div key={inclusion.id} className="flex items-start gap-3 group">
<div className="w-6 h-6 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0 mt-1 group-hover:bg-gray-200 transition-colors duration-200">
<X className="w-3 h-3 text-gray-500" />
</div>
<span className="text-[#2d3134] font-light">{inclusion.title}</span>
</div>
))
) : (
<p className="text-gray-500 italic">No excluded items specified.</p>
)}
</div>
</div>
</div>
{/* Location on map - preserved */}
<div>
<div className="flex items-center gap-4 mb-8">
<div className="h-1 w-12 bg-primary rounded-full"></div>
<h3 className="text-3xl font-semibold text-[#2d3134]">
Location on <span className="text-primary">map</span>
</h3>
</div>
<div className="h-80 bg-gradient-to-br from-primary/5 to-primary/10 rounded-lg flex items-center justify-center border border-primary/10 hover:border-primary/20 transition-colors duration-200">
<div className="text-center">
<div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
<MapPin className="w-8 h-8 text-primary" />
</div>
<p className="text-lg font-medium text-primary mb-2">Interactive Map</p>
<p className="text-sm text-gray-600 font-light">{safeOffer.title}</p>
<p className="text-sm text-gray-600 font-light">{address}</p>
</div>
</div>
</div>
</div>
{/* Right Sidebar - Calendar and Booking (preserved, but you can add a real calendar if needed) */}
<div className="lg:col-span-1">
<Card className="sticky top-32 p-6 bg-white border border-primary/20 shadow-xl rounded-2xl">
<h3 className="text-2xl font-bold text-[#2d3134] mb-4">Book This Offer</h3>
<div className="space-y-4 mb-6">
<div className="flex justify-between items-center border-b pb-2">
<span className="text-gray-600">Availability</span>
<span className="font-medium text-green-600">
{safeOffer.offerStatus === 'active' ? 'Available' : 'Unavailable'}
</span>
</div>
{safeOffer.startDateTime && (
<div className="flex justify-between items-center border-b pb-2">
<span className="text-gray-600">Valid from</span>
<span className="font-medium">
{new Date(safeOffer.startDateTime).toLocaleDateString()}
</span>
</div>
)}
{safeOffer.endDateTime && (
<div className="flex justify-between items-center border-b pb-2">
<span className="text-gray-600">Valid until</span>
<span className="font-medium">
{new Date(safeOffer.endDateTime).toLocaleDateString()}
</span>
</div>
)}
</div>
<Button
onClick={onCheckoutClick}
className="w-full bg-primary hover:bg-primary/90 text-white font-semibold py-6 text-lg rounded-xl transition-all duration-200"
>
Proceed to Checkout
</Button>
</Card>
</div>
</div>
</div>
</Layout>
);
}

View File

@@ -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,39 +113,29 @@ export function SuperSavingsPage({
currentPage,
user
}: SuperSavingsPageProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
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 })
// optional chaining ensures no crash if data is undefined
const offers = data?.offers ?? [];
const categories = data?.categories ?? [];
const totalOffers = data?.paginationData.total ?? 0;
const totalPages = Math.ceil(totalOffers / limit);
const baseUrl = import.meta.env.VITE_BASE_URL;
if (isLoading) {
return (
<LoadingSpinner />
);
};
}
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);
return matchesSearch && matchesCategory;
});
const itemsPerPage = 12;
const displayedSavings = filteredSavings.slice(0, currentPage_ * itemsPerPage);
const hasMoreItems = filteredSavings.length > displayedSavings.length;
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
return (
<Layout
activeCity="Melbourne"
@@ -378,19 +239,25 @@ export function SuperSavingsPage({
{/* Filter categories */}
<div className="space-y-4">
{filterCategories.map(category => (
<div key={category.value} className="flex items-center gap-3">
{categories.map((category: any) => (
<div key={category.id} className="flex items-center gap-3">
<Checkbox
id={category.value}
checked={selectedCategories.includes(category.value)}
onCheckedChange={() => toggleCategory(category.value)}
id={category.id}
checked={categoryId === category.id}
onCheckedChange={(checked: boolean) => {
if (checked) {
setCategoryId(category.id); // select this category
} else {
setCategoryId(null); // unselect if unchecked
}
}}
className="border-gray-400"
/>
<label
htmlFor={category.value}
htmlFor={category.id}
className="font-poppins text-sm text-gray-700 cursor-pointer flex-1"
>
{category.label} ({category.count})
{category.categoryName} ({category.offerCount})
</label>
</div>
))}
@@ -428,30 +295,31 @@ export function SuperSavingsPage({
</p>
</div>
{/* Savings Grid */}
{/* Offers Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6 mb-16">
{displayedSavings.map((saving, index) => (
{offers.map((offer: any, index: number) => (
<motion.div
key={saving.id}
key={offer.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card className="bg-white border border-gray-200 rounded-xl overflow-hidden h-full hover:shadow-lg transition-shadow duration-300 relative">
<Card className="bg-white border border-gray-200 rounded-xl overflow-hidden h-full hover:shadow-lg transition-shadow duration-300 relative cursor-pointer"
onClick={()=> navigate(`/super-savings/${offer.id}`)}>
{/* Image */}
<div className="relative h-52 bg-gray-300">
<ImageWithFallback
src={saving.image}
alt={saving.title}
src={`${baseUrl}/${offer.websiteBannerImage}`}
alt={offer.title}
className="w-full h-full object-cover"
/>
<Button className="absolute bottom-4 right-3 bg-white rounded-full shadow-lg w-9 h-9 p-0 hover:bg-gray-100 transition-colors">
{/* <Button className="absolute bottom-4 right-3 bg-white rounded-full shadow-lg w-9 h-9 p-0 hover:bg-gray-100 transition-colors">
<Heart className="w-4 h-4 text-gray-800" />
</Button>
</Button> */}
{/* Discount Badge */}
<div className="absolute top-4 left-4 bg-primary text-white px-3 py-1.5 rounded-lg">
<span className="font-poppins font-semibold text-sm">{saving.discount}</span>
<span className="font-poppins font-semibold text-sm">{offer.offerCode}</span>
</div>
</div>
@@ -459,20 +327,20 @@ export function SuperSavingsPage({
{/* Business Name */}
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray-300 rounded"></div>
<span className="font-poppins text-sm text-gray-500">{saving.business}</span>
<span className="font-poppins text-sm text-gray-500">{offer.partnerName}</span>
</div>
{/* Title */}
<h3 className="font-poppins font-medium text-gray-900 leading-relaxed min-h-[48px]">
{saving.title}
{offer.description}
</h3>
{/* Saved Amount Display */}
<div className="bg-gradient-to-r from-primary/10 to-secondary/10 h-12 flex items-center justify-center rounded-lg">
<div className="flex items-center gap-2">
<Percent className="w-4 h-4 text-primary" />
{/* <Percent className="w-4 h-4 text-primary" /> */}
<span className="font-poppins font-semibold text-primary">
{saving.savedAmount}
{offer.title}
</span>
</div>
</div>
@@ -485,34 +353,39 @@ export function SuperSavingsPage({
{/* Minimal Pagination */}
<div className="flex justify-center py-8">
<div className="flex items-center gap-2">
{/* Previous button */}
<Button
variant="outline"
size="sm"
className="w-8 h-8 p-0 font-poppins"
disabled={currentPage_ === 1}
disabled={page === 1}
onClick={() => setPage(prev => Math.max(prev - 1, 1))}
>
<ChevronRight className="w-4 h-4 rotate-180" />
</Button>
{/* Page numbers */}
<div className="flex items-center gap-1">
{[1, 2, 3].map((page) => (
{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => (
<Button
key={page}
variant={currentPage_ === page ? "default" : "ghost"}
key={p}
variant={page === p ? "default" : "ghost"}
size="sm"
className={`w-8 h-8 p-0 font-poppins ${currentPage_ === page ? 'bg-primary hover:bg-primary/90' : ''}`}
onClick={() => setCurrentPage_(page)}
className={`w-8 h-8 p-0 font-poppins ${page === p ? 'bg-primary hover:bg-primary/90' : ''}`}
onClick={() => setPage(p)}
>
{page}
{p}
</Button>
))}
</div>
{/* Next button */}
<Button
variant="outline"
size="sm"
className="w-8 h-8 p-0 font-poppins"
disabled={currentPage_ === 3}
disabled={page === totalPages}
onClick={() => setPage(prev => Math.min(prev + 1, totalPages))}
>
<ChevronRight className="w-4 h-4" />
</Button>
@@ -740,6 +613,6 @@ export function SuperSavingsPage({
</div>
</Layout>
);
}
}

1
src/vite-env.d.ts vendored
View File

@@ -1,6 +1,7 @@
interface ImportMetaEnv {
readonly VITE_BASE_URL: string
readonly VITE_GOOGLE_MAP: string
readonly VITE_STRIPE_PUBLISHABLE_KEY: string
}
interface ImportMeta {