91 Commits

Author SHA1 Message Date
2918cfd33c Merge pull request 'main' (#42) from main into testing
Some checks failed
CityCards-Website / Build-CityCards-Website (push) Failing after 2m26s
Reviewed-on: #42
2026-04-27 12:01:33 +00:00
5abda5b6cb Merge pull request 'arya-branch' (#41) from arya-branch into main
Reviewed-on: #41
2026-04-27 11:59:50 +00:00
aryabenade
a03d1999bf remove the vertical white spaces from Melbourne page 2026-04-27 16:33:04 +05:30
aryabenade
201e8b86d4 reduce the size of the testimonial cards in LandingTrustSection component 2026-04-27 16:23:40 +05:30
aryabenade
39e63deca2 change the css of LandingMagicItinerary Component
Co-authored-by: Copilot <copilot@github.com>
2026-04-27 16:23:10 +05:30
aryabenade
c7af20fac7 change the color to red when attraction category selected on home page 2026-04-27 15:58:56 +05:30
aryabenade
e0c314d3af show validation errors below the fields instead of toasters 2026-04-27 15:48:34 +05:30
aryabenade
4c985e5177 remove ai word from magic itinerary from city home page 2026-04-27 14:57:51 +05:30
d271b3b64c Merge pull request 'main' (#39) from main into testing
Some checks failed
CityCards-Website / Build-CityCards-Website (push) Failing after 2m27s
Reviewed-on: #39
2026-04-27 06:17:15 +00:00
7fc7f1b433 Merge pull request 'push code after npm install' (#38) from arya-branch into main
Reviewed-on: #38
2026-04-27 06:16:20 +00:00
aryabenade
34223f1c81 push code after npm install 2026-04-27 11:43:32 +05:30
eea8eb52f3 Merge pull request 'main' (#36) from main into testing
Some checks failed
CityCards-Website / Build-CityCards-Website (push) Has been cancelled
Reviewed-on: #36
2026-04-27 06:07:32 +00:00
3a5d6b0724 Merge pull request 'arya-branch' (#35) from arya-branch into main
Reviewed-on: #35
2026-04-27 06:07:12 +00:00
aryabenade
05f134fdba change the padding of navbar 2026-04-27 11:22:54 +05:30
aryabenade
13780803ba add the ScrollToTop Component
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 23:43:33 +05:30
aryabenade
0dbba7f80e select the same card on checkout page which we select on the buy cards page
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 19:52:31 +05:30
7cdaa43e5b Merge pull request 'main' (#34) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 22s
Reviewed-on: #34
2026-04-24 16:42:03 +00:00
7b3833e5f7 Merge pull request 'arya-branch' (#32) from arya-branch into main
Reviewed-on: #32
2026-04-24 16:40:46 +00:00
aryabenade
d8976a29b4 remove the booking section in supersavings details page 2026-04-24 22:03:36 +05:30
aryabenade
1be37e098b show login modal when clicked on ctabutton if logged out 2026-04-24 22:03:11 +05:30
aryabenade
e3fde4bb17 add validations for spaces in the forms 2026-04-24 21:58:34 +05:30
aryabenade
33a782ca54 show cityName on basis of citySelected 2026-04-24 21:45:49 +05:30
aryabenade
a651186276 add validations in forms 2026-04-24 21:30:04 +05:30
cc9bc18bef Merge pull request 'main' (#30) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 24s
Reviewed-on: #30
2026-04-24 14:32:20 +00:00
b276dec0f5 Merge pull request 'arya-branch' (#29) from arya-branch into main
Reviewed-on: #29
2026-04-24 14:30:49 +00:00
aryabenade
e9ccc78bb0 add protected routes 2026-04-24 19:59:02 +05:30
aryabenade
962d4283e6 remove commented code 2026-04-24 19:09:20 +05:30
aryabenade
67d7f977b7 show attractions from backend on cityHomePage 2026-04-24 19:02:51 +05:30
566afcfd75 Merge pull request 'main' (#28) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 22s
Reviewed-on: #28
2026-04-24 12:41:52 +00:00
e9f404e4df Merge pull request 'fix issues according to client feedback' (#27) from arya-branch into main
Reviewed-on: #27
2026-04-24 12:41:32 +00:00
aryabenade
848c33edbd fix issues according to client feedback 2026-04-24 18:10:03 +05:30
1438178535 Merge pull request 'main' (#26) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 22s
Reviewed-on: #26
2026-04-24 11:26:40 +00:00
c8729848fb Merge pull request 'enable the download itinerary feature in itinerary summary page' (#25) from arya-branch into main
Reviewed-on: #25
2026-04-24 11:26:03 +00:00
aryabenade
668a183123 enable the download itinerary feature in itinerary summary page 2026-04-24 16:44:43 +05:30
d0c02f4fb9 Merge pull request 'main' (#24) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 21s
Reviewed-on: #24
2026-04-24 11:10:06 +00:00
dd5e49bcc1 Merge pull request 'arya-branch' (#23) from arya-branch into main
Reviewed-on: #23
2026-04-24 11:09:38 +00:00
aryabenade
c3d3d0c751 Merge branch 'hemant' of http://git.wdipl.com/CityCards/CityCards-Website into arya-branch 2026-04-24 16:38:03 +05:30
aryabenade
340de94a5d add blogs from backend in the homepage after city selected 2026-04-24 16:37:32 +05:30
Hemant Vishwakarma
54f33c2d34 Implement download itinerary pdf in my profile 2026-04-24 16:32:35 +05:30
ebff7f1887 Merge pull request 'main' (#22) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 24s
Reviewed-on: #22
2026-04-24 10:00:23 +00:00
103511eadb Merge pull request 'arya-branch' (#21) from arya-branch into main
Reviewed-on: #21
2026-04-24 09:59:15 +00:00
aryabenade
8a462b599e change the border and field colors of recipient details in payment page 2026-04-24 15:11:44 +05:30
aryabenade
627137427d add proper navigations in melbourneAttractions and Profile Page 2026-04-24 14:51:27 +05:30
aryabenade
f01a5e0630 remove blank spaces 2026-04-24 14:41:58 +05:30
aryabenade
40ff761104 add navbar and footer in itinerary summary page 2026-04-24 14:23:24 +05:30
aryabenade
ff76d9a370 navigate to create-itinerary 2026-04-24 14:23:01 +05:30
aryabenade
ecd2fb2719 add optional operators to prevent crashing when no cards for a city 2026-04-24 13:52:56 +05:30
aryabenade
c0a2c448e5 replace melbourne with the selected cityname 2026-04-24 13:52:27 +05:30
a2e0eb5e14 Merge pull request 'main' (#20) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 22s
Reviewed-on: #20
2026-04-23 14:44:18 +00:00
aryabenade
191aa3fd54 Merge branch 'hemant' of http://git.wdipl.com/CityCards/CityCards-Website into arya-branch 2026-04-23 20:12:44 +05:30
1575690684 Merge pull request 'change card service again' (#19) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 21s
Reviewed-on: #19
2026-04-23 14:26:01 +00:00
Hemant Vishwakarma
6a23429131 change card service again 2026-04-23 19:55:21 +05:30
132ceccbdd Merge pull request 'change card service file' (#18) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 23s
Reviewed-on: #18
2026-04-23 14:21:57 +00:00
aryabenade
2c9bf7da83 show toast error when not able to create itinerary with the error msg from backend 2026-04-23 19:50:48 +05:30
Hemant Vishwakarma
8279715f2c change card service file 2026-04-23 19:50:43 +05:30
aryabenade
37601dd51d round off the basePrice and strikedPrice 2026-04-23 19:31:26 +05:30
aryabenade
16fe56913d debug the cityId issue in getting user itineraries 2026-04-23 19:29:18 +05:30
46f5533028 Merge pull request 'change payment success page and endpoint' (#17) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 22s
Reviewed-on: #17
2026-04-23 13:59:11 +00:00
Hemant Vishwakarma
a1a5c839dd change payment success page and endpoint 2026-04-23 19:28:13 +05:30
87a8749f10 Merge pull request 'Again rechange in payment success' (#16) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 23s
Reviewed-on: #16
2026-04-23 13:35:28 +00:00
Hemant Vishwakarma
d3bedeb56d Again rechange in payment success 2026-04-23 19:02:42 +05:30
c1b50de2b6 Merge pull request 'change payment success file and cards endpoints' (#15) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 23s
Reviewed-on: #15
2026-04-23 13:04:26 +00:00
Hemant Vishwakarma
cd7c6ffbaf change payment success file and cards endpoints 2026-04-23 18:33:33 +05:30
aryabenade
cdfcc70e45 integrate api to show itineraries in profile acc to the city selected 2026-04-23 18:24:34 +05:30
aryabenade
977eecd4d8 add indentation in register page 2026-04-23 17:51:12 +05:30
aryabenade
420d2038f2 add validations in register page 2026-04-23 17:48:48 +05:30
aryabenade
27382c45e3 round off the tax amount before adding to cart 2026-04-23 17:00:41 +05:30
aryabenade
0e8045d9a2 add navigation to the footer links 2026-04-23 16:50:09 +05:30
5efc22d150 Merge pull request 'main' (#14) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 25s
Reviewed-on: #14
2026-04-23 10:01:47 +00:00
b3bb9f08cd Merge pull request 'arya-branch' (#13) from arya-branch into main
Reviewed-on: #13
2026-04-23 10:01:05 +00:00
aryabenade
b1fb01a2a6 refactor the api call by passing cityId when getting user city cards 2026-04-23 15:25:52 +05:30
aryabenade
ef1408fe2a change the label of passes route to Buy Cards 2026-04-23 14:43:07 +05:30
aryabenade
23d1eaa43a add favicon to the app 2026-04-23 14:02:57 +05:30
aryabenade
cee671cf32 replace passes with cards in profile page 2026-04-23 13:56:30 +05:30
aryabenade
517a7c0446 remove the register button from logout modal 2026-04-23 13:34:58 +05:30
aryabenade
0d83118938 remove from local and session storage when signing out 2026-04-23 13:31:46 +05:30
aryabenade
0cb3d6c326 replace the registration modal with the registration page 2026-04-23 12:38:39 +05:30
aryabenade
0c7667bf26 remove console logs from checkout page 2026-04-23 00:13:37 +05:30
aryabenade
4c15fa597d remove the unused secure checkout file 2026-04-22 23:52:06 +05:30
aryabenade
617b494249 rename and replace the design files with actual page names 2026-04-22 23:49:59 +05:30
aryabenade
351c767104 replace the citySelected condition from local to session storage 2026-04-22 23:20:26 +05:30
2fa8f86d62 Merge pull request 'main' (#12) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 25s
Reviewed-on: #12
2026-04-22 14:26:44 +00:00
3ee552dccc Merge pull request 'main' (#11) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 23s
Reviewed-on: #11
2026-04-22 12:31:47 +00:00
11714bc1e4 Merge pull request 'main' (#10) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 22s
Reviewed-on: #10
2026-04-22 12:21:36 +00:00
c9ed8f5628 Merge pull request 'main' (#9) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 21s
Reviewed-on: #9
2026-04-22 12:15:33 +00:00
473902a2ae Merge pull request 'main' (#8) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 22s
Reviewed-on: #8
2026-04-22 11:47:22 +00:00
fdd86a44b7 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
2026-04-22 11:23:27 +00:00
943bdd6407 Merge pull request 'main' (#6) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 33s
Reviewed-on: #6
2026-04-14 12:25:05 +00:00
652d5e8a33 Merge pull request 'main' (#4) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 29s
Reviewed-on: #4
2026-04-02 06:40:37 +00:00
dfb1b83352 Merge pull request 'main branch merge to testing' (#1) from main into testing
Some checks failed
CityCards-Website / Build-CityCards-Website (push) Failing after 10s
Reviewed-on: #1
2026-03-24 07:22:43 +00:00
e4796f862d Add .gitea/workflows/deploy.yml
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 24s
2026-03-20 09:54:23 +00:00
60 changed files with 3819 additions and 8598 deletions

View File

@@ -0,0 +1,67 @@
name: CityCards-Website
on:
push:
branches:
- main
- beta
- testing
- uat-beta
- staging
- production
jobs:
Build-CityCards-Website:
runs-on: ubuntu-latest
steps:
- name: Checkout Code in Runner
uses: actions/checkout@v3
- name: Branch and Folder Selection for Deployment
run: |
BRANCH_NAME=${{ gitea.ref_name }}
case $BRANCH_NAME in
#beta)
#echo "PROJECT_FOLDER=/home/citycards/citycards-superadmin" >> $GITHUB_ENV
#PROJECT_FOLDER="/home/citycards/citycards-superadmin"
#;;
testing)
echo "PROJECT_FOLDER=/home/citycards/Test-Release/citycards-frontend/CityCards-Website" >> $GITHUB_ENV
PROJECT_FOLDER="/home/citycards/Test-Release/citycards-frontend/CityCards-Website"
;;
#client)
#echo "PROJECT_FOLDER=/home/citycards/Client-Release/citycards-frontend/CityCards-AdminPanel" >> $GITHUB_ENV
#PROJECT_FOLDER="/home/citycards/Client-Release/citycards-frontend/CityCards-AdminPanel"
#;;
#uat-beta)
#echo "PROJECT_FOLDER=/home/citycards/UAT-Release/citycards-frontend/CityCards-AdminPanel" >> $GITHUB_ENV
#PROJECT_FOLDER="/home/citycards/UAT-Release/citycards-frontend/CityCards-AdminPanel"
#;;
*)
echo "Unknown Branch"
exit 1
;;
esac
echo "BRANCH_NAME=${{ gitea.ref_name }}" >> $GITHUB_ENV
echo "SELECTED BRANCH : $BRANCH_NAME"
echo "SELECTED FOLDER : $PROJECT_FOLDER"
- name: Deployment to Server SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.BETA_SERVER_HOST }}
username: ${{ secrets.BETA_SERVER_USERNAME }}
password: ${{ secrets.BETA_SERVER_PASSWORD }}
port: ${{ secrets.BETA_SERVER_PORT }}
envs: BRANCH_NAME,PROJECT_FOLDER
script: |
set -xe
echo $BRANCH_NAME
echo $PROJECT_FOLDER
cd $PROJECT_FOLDER
git fetch
git reset --hard origin/$BRANCH_NAME
git pull origin $BRANCH_NAME
echo "BUILDING..... "
npm i
npm run build

View File

@@ -1,15 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CityCards Customer-web</title>
</head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/src/assets/citycards customer app.png" />
<title>CityCards Customer-web</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -2,21 +2,17 @@ import { Routes, Route, useParams, useLocation, useNavigate } from 'react-router
import { motion, AnimatePresence } from 'motion/react';
// Import all your pages
import { ProtectedRoute } from './components/ProtectedRoute';
import { MelbournePage } from './pages/MelbournePage';
import { PassesPage } from './pages/PassesPage';
import { AttractionsPage } from './pages/AttractionsPage';
import { AttractionDetailsPage } from './pages/AttractionDetailsPage';
import { CheckoutPage } from './pages/CheckoutPage';
import { SecureCheckoutPage } from './pages/SecureCheckoutPage';
import { BlogsPage } from './pages/BlogsPage';
import { BlogDetailsPage } from './pages/BlogDetailsPage';
import { HowItWorksPage } from './components/HowItWorksPage';
import { FAQPage } from './components/FAQPage';
import { PrivacyPolicyPage } from './pages/PrivacyPolicyPage';
import { AboutUsPage } from './pages/AboutUsPage';
import { ProfilePage } from './pages/ProfilePage';
import { CreateMagicItineraryPage } from './pages/CreateMagicItineraryPage';
import { ItineraryViewPage } from './pages/ItineraryViewPage';
import { OffersPage } from './pages/OffersPage';
import { CityCardsPage } from './pages/CityCardsPage';
import { MagicItineraryPage } from './pages/MagicItineraryPage';
@@ -24,7 +20,6 @@ import { PostCardsPage } from './pages/PostCardsPage';
import { DownloadAppPage } from './pages/DownloadAppPage';
import { HotelDiscountsPage } from './pages/HotelDiscountsPage';
import { ContactUsPage } from './pages/ContactUsPage';
import { pageTransition } from './utils/animations';
import { LandingPage } from './pages/landingPage';
import ComingSoonPage from './pages/ComingSoonPage';
@@ -34,15 +29,15 @@ import { LandingMagicItineraryPage } from './pages/LandingMagicItineraryPage';
import { DiscoverPage } from './pages/DiscoverPage';
import { CartPage } from './pages/CartPage';
import { PaymentDetailsPage } from './pages/PaymentDetailsPage';
import { CartPageDesign } from './pages/CartPageDesign';
import { CheckoutPage2 } from './pages/CheckoutPage2';
import { SuperSavingsDetailsPage } from './pages/SuperSavingsDetailsPage';
import { ViewCardDetailsPage } from './pages/ViewCardDetailsPageDesign';
import { CreateMagicItineraryPageDesign } from './pages/CreateMagicIternaryPageDesign';
import { ItineraryViewPageDesign } from './pages/ItineraryViewPageDesign';
import { ViewCardDetailsPage } from './pages/ViewCardDetailsPage';
import ItinerarySummaryPage from './pages/ItinerarySummaryPage';
import { PaymentSuccessPage } from './pages/PaymentSuccessPage';
import { PaymentCancelPage } from './pages/PaymentCancelPage';
import { ItineraryViewPage } from './pages/ItineraryViewPage';
import { CheckoutPage } from './pages/CheckoutPage';
import { CreateMagicItineraryPage } from './pages/CreateMagicIternaryPage';
import RegisterPage from './components/RegisterPage';
// User type definition
interface User {
@@ -134,19 +129,6 @@ export function AppRouter({
</motion.div>
} />
{/* Checkout Routes */}
{/* <Route path="/checkout" element={
<motion.div key="checkout" {...pageTransition}>
<CheckoutPage {...commonNavHandlers} />
</motion.div>
} /> */}
<Route path="/secure-checkout" element={
<motion.div key="secure-checkout" {...pageTransition}>
<SecureCheckoutPage {...commonNavHandlers} />
</motion.div>
} />
{/* Blog Routes */}
<Route path="/blogs" element={
<motion.div key="blogs" {...pageTransition}>
@@ -197,41 +179,43 @@ export function AppRouter({
{/* User Routes */}
<Route path="/profile" element={
<motion.div key="profile" {...pageTransition}>
<ProfilePage {...commonNavHandlers} />
<ProtectedRoute>
<ProfilePage {...commonNavHandlers} />
</ProtectedRoute>
</motion.div>
} />
<Route path="/view-card-design/:cardId" element={
<Route path="/view-card-details/:cardId" element={
<motion.div key="profile" {...pageTransition}>
<ViewCardDetailsPage {...commonNavHandlers} />
<ProtectedRoute>
<ViewCardDetailsPage {...commonNavHandlers} />
</ProtectedRoute>
</motion.div>
} />
{/* Itinerary Routes */}
<Route path="/create-itinerary" element={
<motion.div key="create-itinerary" {...pageTransition}>
<CreateMagicItineraryPage {...commonNavHandlers} />
</motion.div>
} />
<Route path="/create-itinerary-design" element={
<motion.div key="create-itinerary" {...pageTransition}>
<CreateMagicItineraryPageDesign {...commonNavHandlers} />
<ProtectedRoute>
<CreateMagicItineraryPage {...commonNavHandlers} />
</ProtectedRoute>
</motion.div>
} />
<Route path="/itinerary-view" element={
<Route path="/view-itinerary/:itineraryId" element={
<motion.div key="itinerary-view" {...pageTransition}>
<ItineraryViewPage {...commonNavHandlers} />
</motion.div>
} />
<Route path="/itinerary-view-design/:itineraryId" element={
<motion.div key="itinerary-view" {...pageTransition}>
<ItineraryViewPageDesign {...commonNavHandlers} />
<ProtectedRoute>
<ItineraryViewPage {...commonNavHandlers} />
</ProtectedRoute>
</motion.div>
} />
<Route path="/itinerary-summary/:itineraryId" element={
<motion.div key="itinerary-summary" {...pageTransition}>
<ItinerarySummaryPage {...commonNavHandlers} />
<ProtectedRoute>
<ItinerarySummaryPage {...commonNavHandlers} />
</ProtectedRoute>
</motion.div>
} />
@@ -305,22 +289,30 @@ export function AppRouter({
<Route path="/cart" element={
<motion.div key="super-savings" {...pageTransition}>
<CartPage {...commonNavHandlers} />
<ProtectedRoute>
<CartPage {...commonNavHandlers} />
</ProtectedRoute>
</motion.div>
} />
<Route path="/checkout" element={
<motion.div key="super-savings" {...pageTransition}>
<CheckoutPage2 {...commonNavHandlers} />
<ProtectedRoute>
<CheckoutPage {...commonNavHandlers} />
</ProtectedRoute>
</motion.div>
} />
<Route path="/cart-design" element={
<motion.div key="super-savings" {...pageTransition}>
<CartPageDesign {...commonNavHandlers} />
<Route path="/register" element={
<motion.div key="register" {...pageTransition}>
<RegisterPage {...commonNavHandlers} />
</motion.div>
} />
<Route path="/payment/:bookingId" element={
<motion.div key="super-savings" {...pageTransition}>
<PaymentDetailsPage {...commonNavHandlers} />
<ProtectedRoute>
<PaymentDetailsPage {...commonNavHandlers} />
</ProtectedRoute>
</motion.div>
} />
<Route path="/super-savings/:id" element={
@@ -333,26 +325,32 @@ export function AppRouter({
<Route path="/success" element={
<motion.div key="super-savings" {...pageTransition}>
<PaymentSuccessPage
// onHomeClick={onHomeClick}
// onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
currentPage="success"
user={user}
/>
<ProtectedRoute>
<PaymentSuccessPage
// onHomeClick={onHomeClick}
// onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
currentPage="success"
user={user}
/>
</ProtectedRoute>
</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}
/>
<ProtectedRoute>
<PaymentCancelPage
// onHomeClick={onHomeClick}
// onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
currentPage="cancel"
user={user}
/>
</ProtectedRoute>
</motion.div>
} />
</Routes>

View File

@@ -5,6 +5,7 @@ import { authApi } from "./services/auth.service";
import { profileApi } from "./services/profile.service";
import { cardsApi } from "./services/cards.service";
import { itineraryApi } from "./services/itinerary.service";
import { blogsApi } from "./services/blogs.service";
export const store = configureStore({
reducer: {
@@ -13,7 +14,8 @@ export const store = configureStore({
[authApi.reducerPath]: authApi.reducer,
[profileApi.reducerPath]: profileApi.reducer,
[cardsApi.reducerPath]:cardsApi.reducer,
[itineraryApi.reducerPath]:itineraryApi.reducer
[itineraryApi.reducerPath]:itineraryApi.reducer,
[blogsApi.reducerPath]:blogsApi.reducer
},
@@ -24,7 +26,8 @@ export const store = configureStore({
authApi.middleware,
profileApi.middleware,
cardsApi.middleware,
itineraryApi.middleware
itineraryApi.middleware,
blogsApi.middleware
),
});
export type RootState = ReturnType<typeof store.getState>;

View File

@@ -30,6 +30,20 @@ export const attractionsApi = createApi({
return `/attractions/customer/customer-attractions?${params.toString()}`;
},
}),
getAttractionsForHomePage: builder.query({
// cityId is required, others optional
query: ({ cityId, categoryId}) => {
const params = new URLSearchParams();
// required
params.append('cityXid', cityId);
// optional
if (categoryId) params.append('categoryXid', categoryId);
return `/attractions/list/city-attractions?${params.toString()}`;
},
}),
getAttractionDetailsById: builder.query({
query: (id: number) => `/attractions/customer/${id}`,
@@ -38,4 +52,4 @@ export const attractionsApi = createApi({
}),
});
export const { useGetAttractionFiltersQuery,useGetCustomerAttractionsQuery,useGetAttractionDetailsByIdQuery } = attractionsApi;
export const { useGetAttractionFiltersQuery,useGetCustomerAttractionsQuery,useGetAttractionDetailsByIdQuery,useGetAttractionsForHomePageQuery } = attractionsApi;

View File

@@ -0,0 +1,28 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { baseQuery } from "../baseQuery";
export const blogsApi = createApi({
reducerPath: 'blogsApi',
baseQuery,
endpoints: (builder) => ({
getBlogsForCity: builder.query({
// cityId is required, others optional
query: ({ cityId, categoryId }) => {
const params = new URLSearchParams();
// required
params.append('cityXid', cityId);
// optional
if (categoryId) params.append('categoryXid', categoryId);
return `/website/list/blogs?${params.toString()}`;
},
}),
}),
});
export const { useGetBlogsForCityQuery } = blogsApi;

View File

@@ -43,7 +43,7 @@ export const cardsApi = createApi({
}),
payForCard: builder.mutation({
query: (id) => ({
query: (id) => ({
url: `/website/passes/${id}/pay`,
method: "POST",
body: {},
@@ -51,10 +51,9 @@ export const cardsApi = createApi({
}),
confirmCardPayment: builder.mutation({
query: (id) => ({
url: `/website/passes/${id}/confirm-payment`,
query: (payload: { id: string; checkoutSessionId: string }) => ({
url: `/website/passes/${payload.id}/${payload.checkoutSessionId}/confirm-payment/`,
method: "POST",
// body: id,
}),
}),

View File

@@ -1,11 +1,8 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { createApi } from '@reduxjs/toolkit/query/react';
import { baseQuery } from "../baseQuery";
export const citiesApi = createApi({
reducerPath: 'citiesApi',
// baseQuery: fetchBaseQuery({
// baseUrl: 'https://testingapi.citycards.betadelivery.com',
// }),
baseQuery,
endpoints: (builder) => ({

View File

@@ -8,8 +8,6 @@ export const itineraryApi = createApi({
endpoints: (builder) => ({
createMagicItinerary: builder.mutation({
query: (itineraryDetails) => ({ // keep the name of the variables being passed here same as when calling the mutation hook
url: `/website/itinerary`,
@@ -24,7 +22,19 @@ export const itineraryApi = createApi({
}),
getUserItineraries: builder.query({
query: () => `/website/itinerary/all-initineraries`,
query: (cityId) => {
const params = new URLSearchParams()
params.append('cityId', cityId);
return `/website/itinerary/all-initineraries?${params.toString()}`
}
}),
downloadItinerary: builder.query<Blob, string>({
query: (id) => ({
url: `/mobile/itinerary/${id}/download`,
method: 'GET',
responseHandler: (response) => response.blob(),
}),
}),
})
@@ -33,5 +43,8 @@ export const itineraryApi = createApi({
export const {
useCreateMagicItineraryMutation,
useGetItineraryDetailsByIdQuery,
useGetUserItinerariesQuery
useGetUserItinerariesQuery,
useDownloadItineraryQuery,
} = itineraryApi;

View File

@@ -25,9 +25,11 @@ export const profileApi = createApi({
}),
getUserCards: builder.query({
query: (sort) => {
query: ({sort,cityId}) => {
const params = new URLSearchParams()
params.append('cityXid', cityId);
if (sort) params.append('sort', sort);
return `/website/passes/all?${params.toString()}`

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -45,6 +45,7 @@ export function CitySelectionDialog({
navigate(`/${slugify(city.cityName)}`);
localStorage.setItem("cityId", String(city.id))
localStorage.setItem("cityName", String(city.cityName))
sessionStorage.setItem("citySelected", String(city.cityName))
onClose();
};

View File

@@ -20,11 +20,11 @@ interface Testimonial {
company: string;
signature: string;
}
const cityName = localStorage.getItem('cityName') || 'the city';
const testimonials: Testimonial[] = [
{
id: 1,
quote: "CityCards transformed our Melbourne trip into an unforgettable adventure. The seamless access to attractions and insider recommendations made every moment magical.",
quote: `CityCards transformed our ${cityName} trip into an unforgettable adventure. The seamless access to attractions and insider recommendations made every moment magical.`,
name: "Sarah Chen",
company: "Travel Blogger",
signature: "Sarah"
@@ -126,7 +126,7 @@ export function EnhancedTestimonials() {
style={{
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
transformOrigin: 'center center',
minHeight: '480px',
minHeight: '360px',
background: `
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),

View File

@@ -45,10 +45,10 @@ export function Footer({
/>
{/* Enhanced White Gradient Overlay at Top */}
<div className="absolute top-0 left-0 right-0 h-48 bg-gradient-to-b from-white via-white/95 via-white/80 via-white/60 via-white/40 to-transparent z-10" />
<div className="absolute top-0 left-0 right-0 h-24 bg-gradient-to-b from-white via-white/95 via-white/80 via-white/60 via-white/40 to-transparent z-10" />
{/* Additional Smooth Transition Layer */}
<div className="absolute top-0 left-0 right-0 h-24 bg-gradient-to-b from-white via-white/90 to-white/70 z-10" />
<div className="absolute top-0 left-0 right-0 h-4 bg-gradient-to-b from-white via-white/90 to-white/70 z-10" />
{/* Dark overlay for text readability */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-black/30 to-black/70 z-20" />
@@ -61,7 +61,7 @@ export function Footer({
</div>
{/* Content Overlay */}
<div className="relative z-30 py-24">
<div className="relative z-30 py-4">
<div className="container mx-auto px-4">
{/* Footer Content Grid */}
<div className="w-full mt-48 bg-primary/10 backdrop-blur-lg rounded-[10px] border border-white/10 p-12">

View File

@@ -17,13 +17,13 @@ export function FooterBottom({ onPrivacyPolicyClick }: FooterBottomProps) {
<div className="flex flex-col lg:flex-row justify-between items-center space-y-6 lg:space-y-0">
{/* Copyright */}
<p className="text-white/60 text-sm">
© 2024 CityCards. All rights reserved.
© 2026 CityCards. All rights reserved.
</p>
{/* Right Section - Legal Links and Social Icons */}
<div className="flex flex-col md:flex-row items-center space-y-4 md:space-y-0 md:space-x-8">
{/* Legal Links */}
<div className="flex space-x-6 text-sm">
{/* <div className="flex space-x-6 text-sm">
<motion.button
onClick={onPrivacyPolicyClick}
className="text-white/70 hover:text-white transition-colors duration-200"
@@ -48,7 +48,7 @@ export function FooterBottom({ onPrivacyPolicyClick }: FooterBottomProps) {
>
Cookie Policy
</motion.a>
</div>
</div> */}
{/* Social Icons - Horizontal Layout */}
<div className="flex space-x-3">

View File

@@ -1,37 +1,21 @@
import { motion } from 'motion/react';
import { footerSections } from '../utils/footerConstants';
import { Link } from 'react-router-dom';
interface FooterNavigationProps {
onHomeClick?: () => void;
onMelbourneClick?: () => void;
onPassesClick?: () => void;
onSignInClick?: () => void;
onAttractionsClick?: () => void;
onBlogsClick?: () => void;
onHowItWorksClick?: () => void;
onFAQClick?: () => void;
onPrivacyPolicyClick?: () => void;
onAboutUsClick?: () => void;
onContactUsClick?: () => void;
currentPage?: string;
}
const linkRoutes: Record<string, string> = {
'Home': '/',
// 'Cancellation policy': '/cancellation-policy',
'How It Works': '/how-it-works',
'FAQ': '/faq',
'Blog': '/blogs',
'Contact Us': '/contact-us',
'Privacy Policy': '/privacy-policy',
// 'Terms of Service': '/terms',
};
export function FooterNavigation({
onHomeClick,
onMelbourneClick,
onPassesClick,
onSignInClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onContactUsClick,
currentPage
}: FooterNavigationProps) {
export function FooterNavigation() {
return (
<div className="lg:col-span-8 grid grid-cols-2 md:grid-cols-4 gap-8">
<div className="lg:col-span-8 grid grid-cols-2 md:grid-cols-3 gap-8">
{Object.entries(footerSections).map(([key, section]) => (
<motion.div
key={key}
@@ -45,50 +29,20 @@ export function FooterNavigation({
}}
>
<h4 className="font-semibold text-white">{section.title}</h4>
<ul className="space-y-3">
{section.links.map((link, index) => {
const getClickHandler = () => {
switch (link) {
case 'Home': return onHomeClick;
case 'Melbourne': return onMelbourneClick;
case 'Passes': return onPassesClick;
case 'Sign In': return onSignInClick;
case 'Attractions': return onAttractionsClick;
case 'Blog': return onBlogsClick;
case 'How It Works': return onHowItWorksClick;
case 'FAQ': return onFAQClick;
case 'Privacy Policy': return onPrivacyPolicyClick;
case 'Contact Us': return onContactUsClick;
default: return undefined;
}
};
const clickHandler = getClickHandler();
return (
<li key={link}>
{clickHandler ? (
<motion.button
onClick={(e) => {
e.preventDefault();
clickHandler();
}}
className="text-white/80 hover:text-white transition-colors duration-200 text-sm text-left"
whileHover={{ x: 4 }}
transition={{ duration: 0.2 }}
>
{link}
</motion.button>
) : (
<motion.span
className="text-white/80 cursor-default text-sm"
>
{link}
</motion.span>
)}
</li>
);
})}
{section.links.map((link) => (
<li key={link}>
<motion.div whileHover={{ x: 4 }} transition={{ duration: 0.2 }}>
<Link
to={linkRoutes[link] || ""}
className="text-white/80 hover:text-white transition-colors duration-200 text-sm"
>
{link}
</Link>
</motion.div>
</li>
))}
</ul>
</motion.div>
))}

View File

@@ -19,13 +19,15 @@ export function HeroBannerCarousel({
const [currentSlide, setCurrentSlide] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const cityName = localStorage.getItem("cityName")
const slides = [
{
id: 1,
title: "Discover",
highlight: "Melbourne",
highlight: cityName,
subtitle: "Ultimate Guide to Iconic City",
description: "From Flinders Street to St Kilda Beach: explore the best of Melbourne's landmarks, culture, food and more!",
description: cityName === "Melbourne" ? "From Flinders Street to St Kilda Beach: explore the best of Melbourne's landmarks, culture, food and more!" : "From the Sydney Opera House to Bondi Beach: explore the best of Sydneys landmarks, culture, food and more!",
image: "https://images.unsplash.com/photo-1757470238279-0e9f331d02c9?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBza3lsaW5lJTIwc3Vuc2V0fGVufDF8fHx8MTc2MDUwOTIyMHww&ixlib=rb-4.1.0&q=80&w=1080",
cta: "Get Started",
onClick: onCheckoutClick

View File

@@ -7,6 +7,7 @@ interface HotelEsimOffersProps {
}
export function HotelEsimOffers({ onEsimsClick, onHotelDiscountsClick }: HotelEsimOffersProps) {
const cityName = localStorage.getItem("cityName")
return (
<div>
<div className="space-y-0">
@@ -64,7 +65,7 @@ export function HotelEsimOffers({ onEsimsClick, onHotelDiscountsClick }: HotelEs
>
<Wifi className="w-4 h-4 text-primary" />
<span className="font-poppins text-sm font-medium text-primary">
Stay Connected in Melbourne
Stay Connected in {cityName}
</span>
</motion.div>
<h2 className="font-poppins text-3xl md:text-5xl lg:text-6xl leading-tight text-foreground mb-6">
@@ -72,7 +73,7 @@ export function HotelEsimOffers({ onEsimsClick, onHotelDiscountsClick }: HotelEs
<span className="font-bold text-primary italic">Connected</span>
</h2>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-700 max-w-2xl mx-auto">
Get instant e-SIM connectivity across Australia. Stay online from the moment you land in Melbourne.
Get instant e-SIM connectivity across Australia. Stay online from the moment you land in {cityName}.
</p>
</motion.div>
@@ -105,10 +106,10 @@ export function HotelEsimOffers({ onEsimsClick, onHotelDiscountsClick }: HotelEs
<span className="font-poppins text-xs font-semibold text-white">🇦🇺 AUSTRALIA-WIDE COVERAGE</span>
</div>
<h3 className="font-poppins text-3xl md:text-4xl font-semibold text-white mb-4 leading-tight">
Exclusive e-SIM Offers for Melbourne Visitors
Exclusive e-SIM Offers for {cityName} Visitors
</h3>
<p className="font-poppins text-base text-white/90 mb-6">
No more hunting for local SIM cards at the airport. Activate your e-SIM instantly and explore Melbourne with seamless connectivity.
No more hunting for local SIM cards at the airport. Activate your e-SIM instantly and explore {cityName} with seamless connectivity.
</p>
<motion.div
@@ -207,7 +208,7 @@ export function HotelEsimOffers({ onEsimsClick, onHotelDiscountsClick }: HotelEs
>
<Hotel className="w-4 h-4 text-primary" />
<span className="font-poppins text-sm font-medium text-primary">
Premium Melbourne Hotels
Premium {cityName} Hotels
</span>
</motion.div>
<h2 className="font-poppins text-3xl md:text-5xl lg:text-6xl leading-tight text-foreground mb-6">
@@ -215,7 +216,7 @@ export function HotelEsimOffers({ onEsimsClick, onHotelDiscountsClick }: HotelEs
<span className="font-bold text-primary italic">Luxury</span>
</h2>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-700 max-w-2xl mx-auto">
Unlock exclusive rates at Melbourne's finest hotels. Your CityCard membership opens doors to premium CBD and waterfront stays.
Unlock exclusive rates at {cityName}'s finest hotels. Your CityCard membership opens doors to premium CBD and waterfront stays.
</p>
</motion.div>
@@ -236,10 +237,10 @@ export function HotelEsimOffers({ onEsimsClick, onHotelDiscountsClick }: HotelEs
<span className="font-poppins text-xs font-semibold text-white">🔥 MARRIOTT BONVOY PARTNER</span>
</div>
<h3 className="font-poppins text-3xl md:text-4xl font-semibold text-foreground mb-4 leading-tight">
Melbourne Premium Stays at <span className="text-primary italic">Unbeatable Prices</span>
{cityName} Premium Stays at <span className="text-primary italic">Unbeatable Prices</span>
</h3>
<p className="font-poppins text-base text-gray-700 mb-6">
Access exclusive member rates at Melbourne's top hotels including Crown Towers, W Melbourne, and premium CBD properties. Enjoy complimentary upgrades and special amenities.
Access exclusive member rates at {cityName}'s top hotels including Crown Towers, W {cityName}, and premium CBD properties. Enjoy complimentary upgrades and special amenities.
</p>
</div>
@@ -253,7 +254,7 @@ export function HotelEsimOffers({ onEsimsClick, onHotelDiscountsClick }: HotelEs
<div className="bg-gradient-to-br from-primary to-orange-500 rounded-3xl p-8 text-center shadow-xl">
<div className="font-poppins text-6xl font-bold text-white mb-2">25%</div>
<div className="font-poppins text-lg text-white/90 mb-1">Average Savings</div>
<div className="font-poppins text-sm text-white/70">on Melbourne hotels</div>
<div className="font-poppins text-sm text-white/70">on {cityName} hotels</div>
</div>
<motion.div
className="absolute -top-2 -right-2 bg-yellow-400 rounded-full px-3 py-1"

File diff suppressed because it is too large Load Diff

View File

@@ -259,7 +259,7 @@ export function LandingBookAttractionSection() {
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={`px-6 py-4 h-14 rounded-full font-medium transition-all duration-300 ${activeCategory === category
? 'bg-warm-coral text-white shadow-xl shadow-warm-coral/25 ring-2 ring-warm-coral/20'
? 'bg-red-400 text-white shadow-xl shadow-warm-coral/25 ring-2 ring-warm-coral/20'
: 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-warm-coral/20 hover:bg-white'
}`}
>

View File

@@ -6,6 +6,7 @@ import { Button } from './ui/button';
// Import your video from assets
import cityTourVideo from '../assets/itinenary-animation-vid.mp4';
import { useNavigate } from 'react-router-dom';
interface ItineraryCard {
id: number;
@@ -22,6 +23,8 @@ export function LandingMagicItinerary() {
const [isPlaying, setIsPlaying] = useState(true);
const [videoLoaded, setVideoLoaded] = useState(false);
const navigate = useNavigate()
const handleVideoLoad = () => {
setVideoLoaded(true);
};
@@ -31,7 +34,7 @@ export function LandingMagicItinerary() {
};
return (
<section className="relative py-20 lg:py-32 overflow-hidden -mt-20 pt-32 z-[49]">
<section className="relative py-20 lg:py-15 overflow-hidden -mt-20 z-[49]">
{/* Dynamic Background */}
<div className="absolute inset-0 overflow-hidden pointer-events-none z-[5]">
{/* Background Image as fallback */}
@@ -97,7 +100,7 @@ export function LandingMagicItinerary() {
{/* Header */}
<div className="text-center mb-16 max-w-5xl w-full">
<motion.div
className="inline-flex items-center gap-3 bg-gradient-to-r from-warm-coral/10 to-orange-100/50 backdrop-blur-sm px-6 py-3 rounded-full border-2 border-warm-coral/30 shadow-xl mb-8"
className="inline-flex items-center gap-3 bg-gradient-to-r from-warm-coral/10 to-orange-100/50 backdrop-blur-sm pl-6 py-3 rounded-full border-2 border-warm-coral/30 shadow-xl mb-8"
initial={{ opacity: 0, scale: 0.8, y: 20 }}
whileInView={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 0.7, ease: [0.34, 1.56, 0.64, 1] }}
@@ -112,7 +115,7 @@ export function LandingMagicItinerary() {
>
<Wand2 className="w-6 h-6 text-warm-coral drop-shadow-lg" />
</motion.div>
<span className="font-semibold text-gray-800">AI-Powered Magic Itinerary</span>
<span className="font-semibold text-gray-800">Magic Itinerary</span>
<motion.div
className="w-2 h-2 bg-warm-coral rounded-full"
animate={{
@@ -131,7 +134,7 @@ export function LandingMagicItinerary() {
viewport={{ once: true }}
>
<span className="font-light">Plan Your</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-warm-coral via-orange-500 to-rose-500 bg-clip-text pr-2 text-transparent drop-shadow-lg">
<span className="font-bold italic bg-gradient-to-r from-red-500 via-orange-500 to-rose-500 bg-clip-text pr-2 text-transparent drop-shadow-lg">
Dream Journey
</span>
<br />
@@ -250,7 +253,8 @@ export function LandingMagicItinerary() {
>
<Button
withShine={true}
className="py-6 px-14 rounded-full text-lg font-bold bg-gradient-to-r from-warm-coral via-orange-500 to-rose-500 hover:from-warm-coral/90 hover:via-orange-500/90 hover:to-rose-500/90 shadow-2xl hover:shadow-warm-coral/50 transition-all hover:scale-105 hover:-translate-y-1"
onClick={() => navigate('/landing-magic-itinerary')}
className="py-6 px-14 rounded-full text-lg font-bold bg-gradient-to-r via-orange-500 to-rose-500 hover:from-warm-coral/90 hover:via-orange-500/90 hover:to-rose-500/90 shadow-2xl hover:shadow-warm-coral/50 transition-all hover:scale-105 hover:-translate-y-1"
>
<span className="flex items-center gap-3">
<Wand2 className="w-5 h-5" />
@@ -258,11 +262,11 @@ export function LandingMagicItinerary() {
</span>
</Button>
<p className="text-gray-600 text-sm flex items-center gap-2">
{/* <p className="text-gray-600 text-sm flex items-center gap-2">
<Sparkles className="w-4 h-4 text-warm-coral" />
<span>Free to use • No credit card required</span>
<Sparkles className="w-4 h-4 text-warm-coral" />
</p>
</p> */}
</motion.div>
</div>
</div>

View File

@@ -204,7 +204,7 @@ export function LandingTrustSection() {
style={{
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
transformOrigin: 'center center',
minHeight: '480px',
minHeight: '360px',
background: `
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),

View File

@@ -150,10 +150,10 @@ export function LandingVarietyOfAdventures() {
const extendedCategories = [...melbourneCategories, ...melbourneCategories, ...melbourneCategories];
return (
<section className="py-20 lg:py-28 bg-white overflow-hidden">
<section className="lg: bg-white overflow-hidden">
<div className="container mx-auto px-4">
{/* Header */}
<div className="text-center mb-16 max-w-4xl mx-auto">
<div className="text-center mb-2 max-w-4xl mx-auto">
<motion.h2
className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight text-foreground mb-6"
initial={{ opacity: 0, y: 30 }}

View File

@@ -1,3 +1,4 @@
// LoginModal.tsx
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { X } from 'lucide-react';
@@ -7,7 +8,7 @@ import { Label } from './ui/label';
import { useAuth } from '../context/AuthContext';
import { useLoginMutation, useVerifyOtpMutation } from '../Redux/services/auth.service';
import { toast } from 'sonner';
import { RegisterModal } from './RegisterModal';
import { useNavigate } from 'react-router-dom';
interface LoginModalProps {
isOpen: boolean;
@@ -21,9 +22,9 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) {
const [countdown, setCountdown] = useState(0);
const [helperText, setHelperText] = useState('');
const [error, setError] = useState('');
const [showRegisterModal, setShowRegisterModal] = useState(false);
const { login } = useAuth();
const navigate = useNavigate()
const [sendOtp, { isLoading: isSendingOtp }] = useLoginMutation();
const [verifyOtp, { isLoading: isVerifying }] = useVerifyOtpMutation();
@@ -147,15 +148,22 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) {
otp: otpString
}).unwrap();
const userData = {
userId: response?.user?.id,
email: response?.email || email,
name: response?.name || email.split('@')[0].charAt(0).toUpperCase() + email.split('@')[0].slice(1),
accessToken: response?.accessToken,
};
if (!response?.userExists) {
localStorage.setItem("userEmail",email)
navigate("/register")
} else {
const userData = {
userId: response?.user?.id,
email: response?.email || email,
name: response?.name || email.split('@')[0].charAt(0).toUpperCase() + email.split('@')[0].slice(1),
accessToken: response?.accessToken,
};
login(userData);
toast.success("User Logged in successfully")
login(userData);
toast.success("User Logged in successfully")
navigate("/passes")
}
onClose();
} catch (err: any) {
setError(err?.data?.message || 'Invalid OTP. Please try again.');
@@ -232,14 +240,7 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) {
>
{isSendingOtp ? 'Sending OTP...' : 'Send OTP'}
</Button>
<div className="text-center">
<button
onClick={() => setShowRegisterModal(true)}
className="font-poppins text-sm text-gray-600 hover:text-gray-800 transition-colors cursor-pointer"
>
Don't have an account? <span className="text-primary font-semibold">Register</span>
</button>
</div>
</div>
) : (
<div className="space-y-6">
@@ -314,15 +315,6 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) {
)
}
</AnimatePresence >
<RegisterModal
isOpen={showRegisterModal}
onClose={() => setShowRegisterModal(false)}
onLoginClick={() => {
setShowRegisterModal(false);
setStep('email');
setEmail('');
}}
/>
</>
);
}

View File

@@ -2,269 +2,168 @@ import { useState } from 'react';
import { ChevronLeft, ChevronRight, Clock, Users, Star, Zap, CheckCircle, MapPin, Volume2, Camera, Coffee, Palette, Eye } from 'lucide-react';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { motion } from 'motion/react';
const melbourneAttractions = [
{
id: 1,
name: "Royal Botanic Gardens",
city: "Melbourne",
country: "Australia",
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",
rating: 4.8,
reviews: "15,600+",
category: "Gardens",
originalPrice: "Free",
includedValue: "$25",
perks: [
{ icon: Volume2, label: "Audio garden tour", color: "text-green-600" },
{ icon: MapPin, label: "Garden maps", color: "text-blue-600" },
{ icon: Camera, label: "Photo spots guide", color: "text-purple-600" }
]
},
{
id: 2,
name: "Federation Square",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1639655001512-e4b58d4874b8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBmZWRlcmF0aW9uJTIwc3F1YXJlfGVufDF8fHx8MTc1NzMzNzc5Mnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.6,
reviews: "22,400+",
category: "Landmarks",
originalPrice: "Free",
includedValue: "$35",
perks: [
{ icon: Volume2, label: "Cultural tours", color: "text-orange-600" },
{ icon: Eye, label: "Gallery access", color: "text-blue-600" },
{ icon: Users, label: "Event access", color: "text-purple-600" }
]
},
{
id: 3,
name: "Queen Victoria Market",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1676454953709-e0be46f62490?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0fGVufDF8fHx8MTc1NzMzNzc5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.7,
reviews: "18,200+",
category: "Markets",
originalPrice: "$45",
includedValue: "$45",
perks: [
{ icon: Users, label: "Food tours", color: "text-orange-600" },
{ icon: Coffee, label: "Tastings", color: "text-brown-600" },
{ icon: Volume2, label: "History guide", color: "text-blue-600" }
]
},
{
id: 4,
name: "Eureka Skydeck",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1629677713183-29248e1268d7?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBldXJla2ElMjB0b3dlcnxlbnwxfHx8fDE3NTczMzc4MDB8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.9,
reviews: "11,800+",
category: "Views",
originalPrice: "$32",
includedValue: "$32",
perks: [
{ icon: Zap, label: "Skip-the-line", color: "text-green-600" },
{ icon: Eye, label: "360° views", color: "text-purple-600" },
{ icon: Camera, label: "Photo experiences", color: "text-blue-600" }
]
},
{
id: 5,
name: "St Kilda Beach & Pier",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1674732954456-159835c0a46b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBzdCUyMGtpbGRhJTIwYmVhY2h8ZW58MXx8fHwxNzU3MzM3ODAzfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.5,
reviews: "14,300+",
category: "Beach",
originalPrice: "Free",
includedValue: "$20",
perks: [
{ icon: Users, label: "Penguin tours", color: "text-blue-600" },
{ icon: MapPin, label: "Beach activities", color: "text-green-600" },
{ icon: Camera, label: "Sunset spots", color: "text-purple-600" }
]
},
{
id: 6,
name: "Melbourne Laneways",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1705120624704-0970afc29fea?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBsYW5ld2F5cyUyMHN0cmVldCUyMGFydHxlbnwxfHx8fDE3NTczMzc4MDd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.8,
reviews: "19,500+",
category: "Street Art",
originalPrice: "$55",
includedValue: "$55",
perks: [
{ icon: Palette, label: "Art tours", color: "text-pink-600" },
{ icon: Coffee, label: "Café stops", color: "text-brown-600" },
{ icon: Camera, label: "Photo walks", color: "text-purple-600" }
]
},
{
id: 7,
name: "Melbourne Zoo",
city: "Melbourne",
country: "Australia",
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",
rating: 4.7,
reviews: "13,900+",
category: "Wildlife",
originalPrice: "$42",
includedValue: "$42",
perks: [
{ icon: Zap, label: "Skip-the-line", color: "text-green-600" },
{ icon: Users, label: "Animal encounters", color: "text-orange-600" },
{ icon: Volume2, label: "Keeper talks", color: "text-blue-600" }
]
},
{
id: 8,
name: "Royal Exhibition Building",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1720523794299-c3b445d71a51?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGV4aGliaXRpb24lMjBidWlsZGluZ3xlbnwxfHx8fDE3NTczMzc4MTR8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.6,
reviews: "8,700+",
category: "Heritage",
originalPrice: "$25",
includedValue: "$25",
perks: [
{ icon: Volume2, label: "Audio tours", color: "text-blue-600" },
{ icon: Eye, label: "Exhibitions", color: "text-purple-600" },
{ icon: MapPin, label: "Heritage walks", color: "text-green-600" }
]
}
];
const categories = ["All", "Landmarks", "Gardens", "Markets", "Views", "Beach", "Street Art", "Wildlife", "Heritage"];
import { useNavigate } from 'react-router-dom';
import { useGetAttractionsForHomePageQuery } from '../Redux/services/attractions.service';
export function MelbourneAttractions() {
const [activeCategory, setActiveCategory] = useState("All");
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null);
const navigate = useNavigate();
const cityName = localStorage.getItem("cityName");
const cityId = localStorage.getItem("cityId");
const filteredAttractions = activeCategory === "All"
? melbourneAttractions
: melbourneAttractions.filter(attraction => attraction.category === activeCategory);
const { data: homePageAttractionsData } = useGetAttractionsForHomePageQuery({ cityId });
const AttractionCard = ({ attraction, index }: { attraction: typeof melbourneAttractions[0], index: number }) => (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
className="group cursor-pointer flex-shrink-0 w-[280px] md:w-auto md:flex-shrink h-96 flip-card-container"
>
{/* 3D Flip Container */}
<div className="flip-card-inner group-hover:[transform:rotateY(180deg)] relative w-full h-full">
{/* FRONT FACE */}
<div className="flip-card-face absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg">
{/* Background Image */}
<ImageWithFallback
src={attraction.image}
alt={attraction.name}
className="w-full h-full object-cover"
/>
const apiAttractions = homePageAttractionsData?.attractions || [];
const apiCategories = homePageAttractionsData?.categories || [];
{/* Rating Badge */}
{/* <div className="absolute top-4 right-4 bg-white/95 backdrop-blur-sm rounded-full px-3 py-1.5 flex items-center gap-1 shadow-lg z-10">
<div className="w-4 h-4 bg-gradient-to-r from-yellow-400 to-yellow-500 rounded-full flex items-center justify-center">
<span className="text-white text-xs">★</span>
</div>
<span className="text-sm font-medium text-gray-900">{attraction.rating}</span>
</div> */}
// Filter attractions by selected category
const filteredAttractions = selectedCategoryId === null
? apiAttractions
: apiAttractions.filter((attraction: any) =>
attraction.categories?.some((cat: any) => cat.id === selectedCategoryId)
);
{/* Front Content - Clean Title & Location */}
<div className="absolute bottom-0 left-0 right-0">
<div className="bg-gradient-to-t from-black/80 via-black/50 to-transparent p-6">
<h3 className="font-bold text-xl text-white mb-1">{attraction.name}</h3>
<p className="text-white/90 text-sm">
{attraction.city}, {attraction.country}
</p>
const AttractionCard = ({ attraction, index }: { attraction: any; index: number }) => {
// Get cover image or first image from galleries
const coverImage = attraction.galleries?.find((g: any) => g.isCoverImage)?.filePathUrl
|| attraction.galleries?.[0]?.filePathUrl
|| '';
// Filter only inclusions (isInclusion: true)
const inclusions = attraction.inclusions?.filter((inc: any) => inc.isInclusion) || [];
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
className="group cursor-pointer flex-shrink-0 w-[280px] md:w-auto md:flex-shrink h-96 flip-card-container"
>
<div className="flip-card-inner group-hover:[transform:rotateY(180deg)] relative w-full h-full">
{/* FRONT FACE */}
<div className="flip-card-face absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg">
<ImageWithFallback
src={coverImage}
alt={attraction.title}
className="w-full h-full object-cover"
/>
<div className="absolute bottom-0 left-0 right-0">
<div className="bg-gradient-to-t from-black/80 via-black/50 to-transparent p-6">
<h3 className="font-bold text-xl text-white mb-1">{attraction.title}</h3>
<p className="text-white/90 text-sm">{attraction.city?.cityName}, Australia</p>
</div>
</div>
</div>
</div>
{/* BACK FACE */}
<div className="flip-card-face flip-card-back absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg bg-gradient-to-br from-gray-900 to-black">
{/* Back Content Container */}
<div className="relative w-full h-full p-6 flex flex-col justify-center text-white">
{/* Included Value Section */}
<div className="mb-4">
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-3 py-1.5 rounded-full text-sm font-medium mb-3">
<CheckCircle className="w-4 h-4" />
<span>Included Value</span>
{/* BACK FACE */}
<div className="flip-card-face flip-card-back absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg bg-gradient-to-br from-gray-900 to-black">
<div className="relative w-full h-full p-6 flex flex-col justify-center text-white">
{/* Pricing Section */}
<div className="mb-4">
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-3 py-1.5 rounded-full text-sm font-medium mb-3">
<CheckCircle className="w-4 h-4" />
<span>Included Value</span>
</div>
<div className="text-2xl font-bold mb-1">
${attraction.ticketPriceAdult}
{attraction.ticketPriceChild && (
<span className="text-sm font-normal text-white/70 ml-2">
/ Child ${attraction.ticketPriceChild}
</span>
)}
</div>
<p className="text-white/80 text-sm">
{attraction.isBookingRequired ? 'Booking required' : 'No booking required'}
</p>
</div>
<div className="text-2xl font-bold mb-1">{attraction.includedValue}</div>
<p className="text-white/80 text-sm">
{attraction.originalPrice === "Free"
? "Premium access included"
: "Save money with CityCard"}
</p>
</div>
{/* What's Included List */}
<div className="mb-4">
<h4 className="font-semibold text-sm mb-3">What's Included:</h4>
<div className="space-y-2">
{attraction.perks.slice(0, 3).map((perk, perkIndex) => (
<div key={perkIndex} className="flex items-center gap-3 text-white/90">
<div className="w-6 h-6 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center">
<perk.icon className="w-3 h-3 text-white" />
</div>
<span className="text-sm">{perk.label}</span>
{/* Inclusions List */}
{inclusions.length > 0 && (
<div className="mb-4">
<h4 className="font-semibold text-sm mb-3">What's Included:</h4>
<div className="space-y-2">
{inclusions.slice(0, 3).map((inc: any) => (
<div key={inc.id} className="flex items-center gap-3 text-white/90">
<div className="w-6 h-6 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center flex-shrink-0">
<CheckCircle className="w-3 h-3 text-white" />
</div>
<span className="text-sm">{inc.title}</span>
</div>
))}
</div>
))}
</div>
</div>
{/* Duration & Meta Info */}
<div className="mb-4">
<div className="flex items-center gap-4 text-white/80 text-sm">
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>2-3 hours</span>
</div>
<div className="flex items-center gap-1">
<Users className="w-4 h-4" />
<span>All ages</span>
)}
{/* Duration & Group Info */}
<div className="mb-4">
<div className="flex items-center gap-4 text-white/80 text-sm">
{attraction.durations && (
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>{attraction.durations} mins</span>
</div>
)}
{attraction.groupSize && (
<div className="flex items-center gap-1">
<Users className="w-4 h-4" />
<span>Max {attraction.groupSize}</span>
</div>
)}
{attraction.ageRange && (
<div className="flex items-center gap-1">
<Star className="w-4 h-4" />
<span>{attraction.ageRange}</span>
</div>
)}
</div>
</div>
</div>
{/* Footer Features */}
<div className="border-t border-white/20 pt-4">
<div className="flex items-center justify-between text-white/80 text-xs">
<div className="flex items-center gap-2">
<MapPin className="w-3 h-3" />
<span>Mobile ticket</span>
{/* Categories */}
{attraction.categories?.length > 0 && (
<div className="flex flex-wrap gap-1 mb-4">
{attraction.categories.slice(0, 2).map((cat: any) => (
<span
key={cat.id}
className="text-xs bg-white/20 text-white/90 px-2 py-0.5 rounded-full"
>
{cat.categoryName}
</span>
))}
</div>
<div className="flex items-center gap-2">
<CheckCircle className="w-3 h-3" />
<span>Instant confirmation</span>
)}
{/* Footer */}
<div className="border-t border-white/20 pt-4">
<div className="flex items-center justify-between text-white/80 text-xs">
<div className="flex items-center gap-2">
<MapPin className="w-3 h-3" />
<span>Mobile ticket</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle className="w-3 h-3" />
<span>Instant confirmation</span>
</div>
</div>
</div>
</div>
{/* Decorative Elements */}
<div className="absolute top-4 right-4 w-16 h-16 bg-gradient-to-br from-primary/20 to-secondary/20 rounded-full blur-xl"></div>
<div className="absolute bottom-4 left-4 w-12 h-12 bg-gradient-to-tr from-secondary/15 to-primary/15 rounded-full blur-lg"></div>
{/* Decorative Elements */}
<div className="absolute top-4 right-4 w-16 h-16 bg-gradient-to-br from-primary/20 to-secondary/20 rounded-full blur-xl"></div>
<div className="absolute bottom-4 left-4 w-12 h-12 bg-gradient-to-tr from-secondary/15 to-primary/15 rounded-full blur-lg"></div>
</div>
</div>
</div>
</div>
</motion.div>
);
</div>
</motion.div>
);
};
return (
<section className="py-20 bg-gradient-to-br from-gray-50 to-white relative overflow-hidden">
<div className="container mx-auto px-4">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
@@ -276,19 +175,19 @@ export function MelbourneAttractions() {
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-primary/10 to-secondary/10 px-4 py-2 rounded-full mb-6">
<div className="w-2 h-2 bg-gradient-to-r from-primary to-secondary rounded-full"></div>
<span className="text-sm font-medium bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Melbourne Must-Sees
{cityName} Must-Sees
</span>
</div>
<h2 className="heading-dynamic text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-4">
<span className="font-light">Discover</span>{' '}
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic pr-1">
Melbourne's
{cityName}'s
</span>{' '}
<span className="font-normal">Best</span>{' '}
<span className="font-semibold text-emphasis">Experiences</span>
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Discover Melbourne's iconic landmarks, vibrant culture, world-class dining, and hidden gems - all included with your Melbourne CityCard
Discover {cityName}'s iconic landmarks, vibrant culture, world-class dining, and hidden gems all included with your {cityName} CityCard
</p>
</motion.div>
@@ -300,23 +199,41 @@ export function MelbourneAttractions() {
viewport={{ once: true }}
className="flex flex-wrap justify-center gap-3 mb-12"
>
{categories.map((category, index) => (
{/* "All" button */}
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
onClick={() => setSelectedCategoryId(null)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={`px-6 py-4 h-14 rounded-2xl font-medium transition-all duration-300 ${
selectedCategoryId === null
? 'bg-gradient-to-r from-primary to-secondary text-white shadow-xl shadow-primary/25 ring-2 ring-primary/20'
: 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-primary/20 hover:bg-white'
}`}
>
All
</motion.button>
{/* Dynamic category buttons from API */}
{apiCategories.map((category: any, index: number) => (
<motion.button
key={category}
key={category.id}
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
viewport={{ once: true }}
onClick={() => setActiveCategory(category)}
onClick={() => setSelectedCategoryId(category.id)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={`px-6 py-4 h-14 rounded-2xl font-medium transition-all duration-300 ${
activeCategory === category
selectedCategoryId === category.id
? 'bg-gradient-to-r from-primary to-secondary text-white shadow-xl shadow-primary/25 ring-2 ring-primary/20'
: 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-primary/20 hover:bg-white'
}`}
>
{category}
{category.categoryName}
</motion.button>
))}
</motion.div>
@@ -324,52 +241,44 @@ export function MelbourneAttractions() {
{/* Mobile Horizontal Carousel */}
<div className="block md:hidden mb-8">
<div className="relative">
{/* Scroll Container */}
<div className="flex gap-6 overflow-x-auto scrollbar-hide pb-4 px-4 -mx-4">
{filteredAttractions.map((attraction, index) => (
{filteredAttractions.map((attraction: any, index: number) => (
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
))}
</div>
{/* Scroll Indicators */}
<div className="flex justify-center mt-6 gap-2">
{Array.from({ length: Math.ceil(filteredAttractions.length / 2) }).map((_, index) => (
<div
key={index}
className="w-2 h-2 rounded-full bg-gray-300"
/>
{Array.from({ length: Math.ceil(filteredAttractions.length / 2) }).map((_: any, index: number) => (
<div key={index} className="w-2 h-2 rounded-full bg-gray-300" />
))}
</div>
{/* Mobile Hint Text */}
<div className="text-center mt-4">
<p className="text-sm text-gray-500">
Swipe to explore more Melbourne attractions
</p>
<p className="text-sm text-gray-500">Swipe to explore more {cityName} attractions</p>
</div>
</div>
</div>
{/* Desktop Bento Grid */}
<div className="hidden md:block w-full">
{/* Top Row - 3 equal cards */}
<div className="grid grid-cols-3 gap-6">
{filteredAttractions.slice(0, 3).map((attraction, index) => (
{filteredAttractions.slice(0, 3).map((attraction: any, index: number) => (
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
))}
</div>
{/* Consistent Vertical Spacing */}
<div className="h-6"></div>
{/* Bottom Row - 2 larger cards */}
<div className="grid grid-cols-2 gap-6">
{filteredAttractions.slice(3, 5).map((attraction, index) => (
{filteredAttractions.slice(3, 5).map((attraction: any, index: number) => (
<AttractionCard key={attraction.id} attraction={attraction} index={index + 3} />
))}
</div>
</div>
{/* Empty State */}
{filteredAttractions.length === 0 && (
<div className="text-center py-16 text-gray-500">
<p className="text-lg">No attractions found for this category.</p>
</div>
)}
{/* Call to Action */}
<motion.div
initial={{ opacity: 0, y: 30 }}
@@ -382,15 +291,15 @@ export function MelbourneAttractions() {
whileHover={{ scale: 1.05, boxShadow: "0 20px 40px rgba(99,102,241,0.3)" }}
whileTap={{ scale: 0.95 }}
className="relative bg-gradient-to-r from-primary to-secondary text-white py-4 px-12 rounded-lg text-lg shadow-xl transition-all duration-300 overflow-hidden group"
onClick={() => navigate('/passes')}
>
<span className="relative z-10">Get Your Melbourne Card</span>
{/* Shine animation */}
<span className="relative z-10">Get Your {cityName} Card</span>
<div className="absolute inset-0 opacity-30">
<div className="h-full bg-gradient-to-r from-transparent via-white to-transparent animate-shine"></div>
</div>
</motion.button>
</motion.div>
</div>
</section>
);

View File

@@ -2,6 +2,9 @@ import { motion } from 'motion/react';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { Calendar, Clock, User, ArrowRight, Coffee, Camera, MapPin, Star } from 'lucide-react';
import { Button } from './ui/button';
import { useRef, useState } from "react";
import { useNavigate } from 'react-router-dom';
import { useGetBlogsForCityQuery } from '../Redux/services/blogs.service';
const blogPosts = [
{
@@ -46,7 +49,7 @@ const blogPosts = [
excerpt: "From the iconic MCG to Formula 1 racing, discover why Melbourne holds the title of Australia's sporting capital and home to major international events.",
image: "https://images.unsplash.com/photo-1720347247737-9252d85d3027?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjaXR5JTIwc2t5bGluZSUyMGZsaW5kZXJzJTIwc3RyZWV0fGVufDF8fHx8MTc1NzMzOTAyNHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
author: "Sports Fan",
date: "Dec 8, 2024",
date: "Dec 8, 2024",
readTime: "6 min read",
category: "Sports",
featured: false,
@@ -88,12 +91,31 @@ const categories = [
];
export function MelbourneBlogs() {
const sectionRef = useRef(null);
const navigate = useNavigate();
const cityId = localStorage.getItem('cityId');
const [categoryId, setCategoryId] = useState("");
const { data: blogsData, error, isLoading } = useGetBlogsForCityQuery({ cityId, categoryId });
const featuredPost = blogPosts.find(post => post.featured);
const regularPosts = blogPosts.filter(post => !post.featured);
const cityName = localStorage.getItem('cityName');
const baseUrl = import.meta.env.VITE_BASE_URL;
const blogss = blogsData?.blogs ?? [];
const categoriess = blogsData?.categories ?? []
const handleCategoryClick = (id: string) => {
// toggle logic: if already selected, reset to empty
setCategoryId(prev => (prev === id ? "" : id));
};
return (
<section className="py-20 bg-gradient-to-br from-gray-50 via-white to-gray-50 relative overflow-hidden">
{/* Background Pattern */}
<section
ref={sectionRef}
className="py-20 bg-gradient-to-br from-gray-50 via-white to-gray-50 relative overflow-hidden"
> {/* Background Pattern */}
<div className="absolute inset-0 opacity-[0.02]">
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-br from-primary/20 via-secondary/20 to-primary/20"></div>
</div>
@@ -110,21 +132,21 @@ export function MelbourneBlogs() {
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-primary/10 to-secondary/10 px-4 py-2 rounded-full mb-6">
<div className="w-2 h-2 bg-gradient-to-r from-primary to-secondary rounded-full"></div>
<span className="text-sm font-medium bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Melbourne Stories
{cityName} Stories
</span>
</div>
<h2 className="font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-6">
<span className="font-normal">Melbourne</span>{' '}
<span className="font-normal">{cityName}</span>{' '}
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic pr-2">
Blogs
</span>
</h2>
<p className="text-xl text-gray-600 max-w-4xl mx-auto leading-relaxed">
Dive deep into Melbourne's rich cultural tapestry, from hidden laneway treasures to world-renowned
coffee culture. Discover insider stories, local secrets, and expert guides to Australia's cultural capital
that will transform your Melbourne experience into an unforgettable journey.
Dive deep into {cityName}'s rich cultural tapestry, from hidden laneway treasures to world-renowned
coffee culture. Discover insider stories, local secrets, and expert guides to Australia's cultural capital
that will transform your {cityName} experience into an unforgettable journey.
</p>
</motion.div>
@@ -136,29 +158,42 @@ export function MelbourneBlogs() {
viewport={{ once: true }}
className="flex flex-wrap justify-center gap-3 mb-16"
>
{categories.map((category, index) => (
<motion.button
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setCategoryId("")}
className={`cursor-pointer px-6 py-3 rounded-full font-medium shadow-lg hover:shadow-xl transition-all duration-300 group
${categoryId === "" ? "bg-gradient-to-r from-primary to-secondary text-white" : "bg-white text-gray-700"}`}
>
<span className="flex items-center gap-2">All</span>
</motion.button>
{categoriess.map((category: any, index) => (
<motion.button
key={category.name}
key={category.id}
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.2 + index * 0.05 }}
viewport={{ once: true }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={`px-6 py-3 rounded-full bg-gradient-to-r ${category.color} text-white font-medium shadow-lg hover:shadow-xl transition-all duration-300 group`}
onClick={() => handleCategoryClick(category.id)}
className={`cursor-pointer px-6 py-3 rounded-full font-medium shadow-lg hover:shadow-xl transition-all duration-300 group
${categoryId === category.id ? "bg-gradient-to-r from-primary to-secondary text-white" : "bg-white text-gray-700"}`}
>
<span className="flex items-center gap-2">
{category.name}
<span className="text-xs bg-white/20 px-2 py-1 rounded-full group-hover:bg-white/30 transition-colors duration-200">
{category.count}
</span>
{category.categoryName}
</span>
</motion.button>
))}
</motion.div>
{/* Featured Post */}
{featuredPost && (
{/* {featuredPost && (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -168,13 +203,13 @@ export function MelbourneBlogs() {
>
</motion.div>
)}
)} */}
{/* Regular Blog Posts Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{regularPosts.map((post, index) => (
{blogss && blogss?.map((blog: any, index) => (
<motion.article
key={post.id}
key={blog.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 + index * 0.1 }}
@@ -184,47 +219,52 @@ export function MelbourneBlogs() {
{/* Post Image */}
<div className="relative overflow-hidden h-48">
<ImageWithFallback
src={post.image}
alt={post.title}
src={`${baseUrl}${blog?.coverImage}`}
alt={blog?.blogTitle}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{/* Category Badge */}
<div className="absolute top-4 left-4 bg-white/95 backdrop-blur-sm text-gray-900 px-3 py-1 rounded-full text-xs font-medium">
{post.category}
{blog?.category?.categoryName}
</div>
</div>
{/* Post Content */}
<div className="p-6 flex-1 flex flex-col justify-between">
<div className="flex items-center gap-3 text-xs text-gray-500 mb-3">
<div className="flex items-center gap-1">
{/* <div className="flex items-center gap-1">
<User className="w-3 h-3" />
{post.author}
</div>
{blog?.author}
</div> */}
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{post.date}
{blog?.createdAt && new Date(blog.createdAt).toLocaleDateString(
'en-US',
{ month: 'short', day: 'numeric', year: 'numeric' }
)}
</div>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{post.readTime}
5 min read
</div>
</div>
<div className="flex-1 flex flex-col">
<h3 className="font-merchant text-xl font-semibold text-gray-900 mb-3 leading-tight group-hover:text-primary transition-colors duration-200 line-clamp-2">
{post.title}
{blog?.blogTitle}
</h3>
<p className="text-gray-600 leading-relaxed mb-4 text-sm flex-1 line-clamp-3">
{post.excerpt}
</p>
<p
className="text-gray-600 leading-relaxed mb-4 text-sm flex-1 line-clamp-3"
dangerouslySetInnerHTML={{ __html: blog?.content }}
/>
{/* Tags */}
<div className="flex flex-wrap gap-1 mb-4">
{post.tags.slice(0, 2).map((tag, tagIndex) => (
{/* <div className="flex flex-wrap gap-1 mb-4">
{blog?.tags?.slice(0, 2).map((tag, tagIndex) => (
<span
key={tagIndex}
className="px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-medium"
@@ -232,12 +272,12 @@ export function MelbourneBlogs() {
{tag}
</span>
))}
{post.tags.length > 2 && (
{blog?.tags?.length > 2 && (
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs">
+{post.tags.length - 2}
+{blog?.tags?.length - 2}
</span>
)}
</div>
</div> */}
</div>
<div className="flex items-center justify-between mt-auto">
@@ -261,23 +301,30 @@ export function MelbourneBlogs() {
>
<div className="bg-gradient-to-br from-primary/5 via-secondary/5 to-primary/5 rounded-3xl p-8 md:p-12 border border-gray-100">
<h3 className="font-merchant text-2xl md:text-3xl font-semibold text-gray-900 mb-4">
Want to explore Melbourne yourself?
Want to explore {cityName} yourself?
</h3>
<p className="text-gray-600 text-lg mb-8 max-w-2xl mx-auto">
Get your Melbourne CityCard and unlock access to all these incredible experiences and more.
Get your {cityName} CityCard and unlock access to all these incredible experiences and more.
Start your adventure today with exclusive deals and insider access.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
className="bg-gradient-to-r from-primary to-secondary text-white font-semibold px-8 py-4 rounded-2xl hover:scale-105 transition-all duration-300 shadow-lg hover:shadow-xl"
onClick={() => { navigate(`/${cityName.toLowerCase()}`), window.scrollTo({ top: 0, behavior: 'smooth' }); }}
>
<MapPin className="w-5 h-5 mr-2" />
Explore Melbourne
Explore {cityName}
</Button>
<Button
variant="outline"
className="border-2 border-gray-300 text-gray-700 font-semibold px-8 py-4 rounded-2xl hover:border-primary hover:text-primary hover:scale-105 transition-all duration-300"
onClick={() => {
sectionRef.current?.scrollIntoView({
behavior: "smooth",
block: "start",
});
}}
>
<Coffee className="w-5 h-5 mr-2" />
View All Blogs

View File

@@ -2,8 +2,8 @@ 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';
import { useNavigate } from 'react-router';
// const cardOptions = [
// {
// id: 'selective',
// name: 'Flexi Card',
@@ -76,7 +76,10 @@ interface MelbourneCardComparisonProps {
export function MelbourneCardComparison({ onCheckoutClick, cards }: MelbourneCardComparisonProps) {
const [selectedCard, setSelectedCard] = useState<string>('unlimited');
const navigate = useNavigate();
const cityName=localStorage.getItem('cityName');
const cardOptions = [
{
id: cards[0]?.id,
@@ -179,9 +182,18 @@ export function MelbourneCardComparison({ onCheckoutClick, cards }: MelbourneCar
</h2>
<p className="text-xl text-gray-600 max-w-4xl mx-auto leading-relaxed">
Melbourne is a must-visit cultural epicenter, and this spectacular trip unlocks
your access around the city in one easy. Save over the cost of visiting Melbourne's
landmarks, have lunch at Phi Phi Leh, snorkel at Bamboo Island, and visit Monkey Beach.
{cityName === 'Melbourne' && (
<span>
Melbourne is a must-visit cultural epicenter, and this spectacular trip unlocks
your access around the city in one easy. Save over the cost of visiting Melbourne's
landmarks, have lunch at Phi Phi Leh, snorkel at Bamboo Island, and visit Monkey Beach.
</span>
)}
{cityName === 'Sydney' && (
<span>
Sydney is a dazzling harbor city that blends iconic landmarks with vibrant coastal escapes. This unforgettable trip gives you seamless access across Sydney in one easy pass. Save on the cost of visiting Sydneys worldfamous attractions, cruise past the Sydney Opera House and Harbour Bridge, relax on Bondi Beach, snorkel at Manly, and explore the wildlife at Taronga Zoo.
</span>
)}
</p>
</motion.div>
@@ -255,7 +267,7 @@ export function MelbourneCardComparison({ onCheckoutClick, cards }: MelbourneCar
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 cursor-pointer"
style={{ backgroundColor: '#F95F62' }}
onClick={onCheckoutClick}
onClick={()=>navigate("/passes")}
>
Buy {card.name}
</Button>

View File

@@ -18,59 +18,61 @@ import {
AccordionTrigger,
} from "./ui/accordion";
const cityName = localStorage.getItem('cityName') || 'the city';
const faqData = [
{
id: "refund",
question: "Can I get a refund on my Melbourne CityCard?",
answer: "Yes, you can cancel your Melbourne CityCard and receive a full refund if you cancel at least 24 hours in advance of your selected start date. For cancellations within 24 hours, refunds are subject to our cancellation policy. Digital cards can be refunded instantly through your account.",
icon: CreditCard
},
// {
// id: "refund",
// question: "Can I get a refund on my Melbourne CityCard?",
// answer: "Yes, you can cancel your Melbourne CityCard and receive a full refund if you cancel at least 24 hours in advance of your selected start date. For cancellations within 24 hours, refunds are subject to our cancellation policy. Digital cards can be refunded instantly through your account.",
// icon: CreditCard
// },
{
id: "duration",
question: "How long is my Melbourne CityCard valid?",
answer: "Melbourne CityCards are available in 1, 2, 3, and 5-day options. Your card activates on the first attraction you visit and is valid for consecutive days only. The 5-day Melbourne Unlimited Card provides the best value for extended stays with access to over 40 premium attractions.",
question: `How long is my ${cityName} CityCard valid?`,
answer: `${cityName} CityCards are available in 1, 2, 3, and 5-day options. Your card activates on the first attraction you visit and is valid for consecutive days only. The 5-day ${cityName} Unlimited Card provides the best value for extended stays with access to over 40 premium attractions.`,
icon: Calendar
},
{
id: "transportation",
question: "Does the Melbourne CityCard include public transport?",
answer: "The Melbourne Unlimited Card includes a complimentary Myki card loaded with travel credit for trams, trains, and buses within Melbourne's CBD and inner suburbs. The Selective Card focuses on attractions only, but we provide detailed transport guides for each venue.",
question: `Does the ${cityName} CityCard include public transport?`,
answer: `The ${cityName} Unlimited Card includes a complimentary Myki card loaded with travel credit for trams, trains, and buses within ${cityName}'s CBD and inner suburbs. The Selective Card focuses on attractions only, but we provide detailed transport guides for each venue.`,
icon: Train
},
{
id: "attractions",
question: "What are the must-visit attractions included with my card?",
answer: "Your Melbourne CityCard includes iconic experiences like Eureka Tower SkyDeck, Royal Botanic Gardens tours, Melbourne Zoo, SEA LIFE Melbourne Aquarium, and Melbourne Star observation wheel. Plus unique local experiences like laneways art tours, coffee culture walks, and rooftop dining discounts.",
question: `What are the must-visit attractions included with my ${cityName} CityCard?`,
answer: `Your ${cityName} CityCard includes iconic experiences like Eureka Tower SkyDeck, Royal Botanic Gardens tours, ${cityName} Zoo, SEA LIFE ${cityName} Aquarium, and ${cityName} Star observation wheel. Plus unique local experiences like laneways art tours, coffee culture walks, and rooftop dining discounts.`,
icon: Camera
},
{
id: "best-time",
question: "When is the best time to visit Melbourne?",
answer: "Melbourne is fantastic year-round! Spring (Sep-Nov) offers perfect weather and blooming gardens. Summer (Dec-Feb) brings outdoor festivals and rooftop season. Autumn (Mar-May) showcases beautiful foliage and harvest events. Winter (Jun-Aug) is ideal for cozy cafes, indoor attractions, and cultural experiences.",
question: `When is the best time to visit ${cityName}?`,
answer: `${cityName} is fantastic year-round! Spring (Sep-Nov) offers perfect weather and blooming gardens. Summer (Dec-Feb) brings outdoor festivals and rooftop season. Autumn (Mar-May) showcases beautiful foliage and harvest events. Winter (Jun-Aug) is ideal for cozy cafes, indoor attractions, and cultural experiences.`,
icon: Clock
},
{
id: "coffee-culture",
question: "How can I experience Melbourne's famous coffee culture?",
answer: "Your Melbourne CityCard includes guided coffee tours through famous laneways, visits to historic coffee roasters, and discounts at award-winning cafes. We've partnered with local baristas to offer exclusive tastings and behind-the-scenes experiences at Melbourne's most beloved coffee institutions.",
question: `How can I experience ${cityName}'s famous coffee culture?`,
answer: `Your ${cityName} CityCard includes guided coffee tours through famous laneways, visits to historic coffee roasters, and discounts at award-winning cafes. We've partnered with local baristas to offer exclusive tastings and behind-the-scenes experiences at ${cityName}'s most beloved coffee institutions.`,
icon: Coffee
},
{
id: "group-bookings",
question: "Do you offer group discounts for families or friends?",
answer: "Yes! Groups of 4+ receive automatic discounts, and families with children under 16 get special pricing. School groups and corporate bookings receive additional benefits. Contact our Melbourne team for custom packages that can include private tours and exclusive venue access.",
question: `Do you offer group discounts for families or friends?`,
answer: `Yes! Groups of 4+ receive automatic discounts, and families with children under 16 get special pricing. School groups and corporate bookings receive additional benefits. Contact our ${cityName} team for custom packages that can include private tours and exclusive venue access.`,
icon: Users
},
{
id: "mobile-app",
question: "Do I need the mobile app to use my Melbourne CityCard?",
answer: "While not required, our mobile app enhances your Melbourne experience with interactive maps, real-time attraction wait times, insider tips from locals, and the ability to skip lines at participating venues. Download it for offline access to your itinerary and exclusive app-only deals.",
question: `Do I need the mobile app to use my ${cityName} CityCard?`,
answer: `While not required, our mobile app enhances your ${cityName} experience with interactive maps, real-time attraction wait times, insider tips from locals, and the ability to skip lines at participating venues. Download it for offline access to your itinerary and exclusive app-only deals.`,
icon: Smartphone
},
{
id: "neighborhoods",
question: "Which Melbourne neighborhoods should I explore?",
answer: "Your CityCard provides access to experiences across Melbourne's diverse neighborhoods: Fitzroy for street art and vintage shopping, St. Kilda for beaches and nightlife, Southbank for dining and culture, CBD for iconic attractions, and Richmond for authentic Vietnamese food and shopping.",
question: `Which ${cityName} neighborhoods should I explore?`,
answer: `Your CityCard provides access to experiences across ${cityName}'s diverse neighborhoods: Fitzroy for street art and vintage shopping, St. Kilda for beaches and nightlife, Southbank for dining and culture, CBD for iconic attractions, and Richmond for authentic Vietnamese food and shopping.`,
icon: MapPin
}
];
@@ -95,7 +97,7 @@ export function MelbourneFAQ() {
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-primary/10 to-secondary/10 px-4 py-2 rounded-full mb-6">
<HelpCircle className="w-4 h-4 text-primary" />
<span className="text-sm font-medium bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Melbourne Guide
{cityName} Guide
</span>
</div>
@@ -107,8 +109,8 @@ export function MelbourneFAQ() {
</h2>
<p className="text-xl text-gray-600 max-w-4xl mx-auto leading-relaxed">
Everything you need to know about exploring Melbourne with your CityCard. From iconic attractions
to hidden local gems, we've got your Melbourne adventure covered.
Everything you need to know about exploring {cityName} with your CityCard. From iconic attractions
to hidden local gems, we've got your {cityName} adventure covered.
</p>
</motion.div>
@@ -161,7 +163,7 @@ export function MelbourneFAQ() {
</div>
{/* Call to Action */}
<motion.div
{/* <motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
@@ -205,7 +207,7 @@ export function MelbourneFAQ() {
</motion.button>
</div>
</div>
</motion.div>
</motion.div> */}
</div>
</section>
);

View File

@@ -36,7 +36,7 @@ export function MelbourneTourOverview() {
}
];
const tourHighlights = [
const MelbourneTourHighlights = [
{
icon: Star,
text: "Experience the cultural capital's vibrant arts scene and hidden laneways",
@@ -69,6 +69,43 @@ export function MelbourneTourOverview() {
}
];
const SydneyTourHighlights = [
{
icon: Star,
text: "Experience Sydneys iconic harbour lifestyle with Opera House and Harbour Bridge views",
color: "text-yellow-600"
},
{
icon: Coffee,
text: "Discover Sydneys vibrant café culture, waterfront dining, and buzzing food markets",
color: "text-amber-600"
},
{
icon: Camera,
text: "Capture panoramic views from Sydney Tower Eye and scenic harbour cruises",
color: "text-purple-600"
},
{
icon: Users,
text: "Enjoy beachside vibes at Bondi and Manly, plus lively nightlife in Darling Harbour",
color: "text-green-600"
},
{
icon: MapPin,
text: "Explore historic charm in The Rocks and coastal walks like Bondi to Coogee",
color: "text-blue-600"
},
{
icon: Calendar,
text: "Access year-round festivals, cultural events, and dynamic harbour celebrations",
color: "text-rose-600"
}
];
const cityName = localStorage.getItem("cityName")
const selectedHighlights = cityName === 'Melbourne' ? MelbourneTourHighlights : SydneyTourHighlights;
return (
<section className="py-20 bg-gradient-to-br from-white via-gray-50/30 to-white relative overflow-hidden">
{/* Background Pattern */}
@@ -88,7 +125,7 @@ export function MelbourneTourOverview() {
className="mb-16"
>
<h2 className="heading-dynamic font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-8">
<span className="font-light">Melbourne</span>{' '}
<span className="font-light">{cityName}</span>{' '}
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic pr-2">
Tour
</span>{' '}
@@ -108,11 +145,20 @@ export function MelbourneTourOverview() {
viewport={{ once: true }}
>
<p className="text-lg md:text-xl text-gray-700 leading-relaxed">
Melbourne is a must-visit cultural epicenter, and this spectacular experience unlocks
your access around the city in one easy pass. Save over the cost of visiting Melbourne's
landmarks, explore famous laneways and street art, enjoy world-class dining in hidden bars,
and immerse yourself in the sports capital's vibrant atmosphere. From Royal Botanic Gardens
to Federation Square, hotel pickup and drop-off all included.
{ cityName === 'Melbourne' && (
<span>
Melbourne is a must-visit cultural epicenter, and this spectacular experience unlocks
your access around the city in one easy pass. Save over the cost of visiting Melbourne's
landmarks, explore famous laneways and street art, enjoy world-class dining in hidden bars,
and immerse yourself in the sports capital's vibrant atmosphere. From Royal Botanic Gardens
to Federation Square, hotel pickup and drop-off all included.
</span>
)}
{ cityName === 'Sydney' && (
<span>
Sydney is a must-visit global destination, blending iconic landmarks with vibrant coastal charm, and this all-in-one experience gives you seamless access across the city. Save more while exploring Sydneys top attractions, wander through historic neighborhoods like The Rocks, admire world-famous sights such as the Sydney Opera House and Harbour Bridge, and relax along stunning beaches like Bondi and Manly. Enjoy diverse dining from waterfront restaurants to hidden cafés, soak in the lively cultural scene, and experience the energy of Australias most dynamic harbor cityall with convenient access to key locations and experiences included.
</span>
)}
</p>
</motion.div>
@@ -226,7 +272,7 @@ export function MelbourneTourOverview() {
</h3>
<div className="space-y-6">
{tourHighlights.map((highlight, index) => (
{selectedHighlights.map((highlight, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}

View File

@@ -2,12 +2,10 @@ import { useState, useEffect, useRef, forwardRef } from 'react';
import { Menu, X, ShoppingBag, ChevronDown, Globe, User, Settings, LogOut } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import Frame1597884853 from '../imports/Frame1597884853';
import { Button } from './ui/button';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { CTAButton } from './CTAButton';
import logoImage from '../assets/cit-logo.png';
import melbourneLogo from '../assets/melbourne-logo.png';
import { CitySelectionDialog, slugify } from './CitySelectionDialog';
import { useAuth } from '../context/AuthContext';
import { LoginModal } from './LoginModal';
@@ -62,15 +60,12 @@ interface NavigationItem {
export default function Navbar({
activeCity,
onCityChange,
onSignInClick,
onSignOutClick,
isUserSignedIn = false,
// user
}: NavbarProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const [activeLanguageDropdown, setActiveLanguageDropdown] = useState(false);
const [activeCartDropdown, setActiveCartDropdown] = useState(false);
const [activeUserDropdown, setActiveUserDropdown] = useState(false);
const [activeCityDropdown, setActiveCityDropdown] = useState(false);
const [isCityDialogOpen, setIsCityDialogOpen] = useState(false);
@@ -79,7 +74,6 @@ export default function Navbar({
const [dialogSource, setDialogSource] = useState<'navbar' | 'cta'>('navbar');
const languageRef = useRef<HTMLDivElement>(null);
const cartRef = useRef<HTMLDivElement>(null);
const userRef = useRef<HTMLDivElement>(null);
const cityRef = useRef<HTMLDivElement>(null);
@@ -96,14 +90,12 @@ export default function Navbar({
const cityName = localStorage.getItem("cityName")
// const citySelected = location.pathname.includes(slugify(cityName) || "")
const citySelected = cityName
const citySelected = sessionStorage.getItem("citySelected")
const baseUrl = import.meta.env.VITE_BASE_URL;
const protectedPaths = ["/passes", "/whats-included", "/", "/melbourne"];
const handleOpenLoginModal = () => {
if (!user && protectedPaths.includes(location.pathname)) {
if (!user) {
setLoginOpen(true);
}
else if (!user) {
@@ -126,26 +118,17 @@ export default function Navbar({
melbourneLabel: 'How It Works'
},
// Position 2
{
label: 'Magic Itinerary',
path: '/landing-magic-itinerary',
isShared: false
},
// Position 3
{
label: 'Whats Included',
path: '/whats-included',
isShared: false
},
// Position 4 - Shared item
// {
// label: 'Your Card',
// path: '/passes',
// isShared: true,
// landingLabel: 'Your Card',
// melbourneLabel: 'Your Card'
// },
// Position 5
{
label: 'Magic Itinerary',
path: '/landing-magic-itinerary',
isShared: false
},
{
label: 'FAQ',
path: '/faq',
@@ -159,7 +142,7 @@ export default function Navbar({
melbourneLabel: 'Your Postcard'
}
],
melbourne: [
citySelected: [
// Position 1
{
label: 'Attractions',
@@ -188,11 +171,11 @@ export default function Navbar({
},
// Position 5 - Shared item
{
label: 'Your Card',
label: 'Buy Cards',
path: `/passes`,
isShared: true,
landingLabel: 'Your Card',
melbourneLabel: 'Your Card'
landingLabel: 'Buy Cards',
melbourneLabel: 'Buy Cards'
},
{
label: 'Your Postcard',
@@ -240,20 +223,20 @@ export default function Navbar({
}, [location.pathname]);
// ✅ Determine which navbar to show
const getAutoNavigationSource = (): 'landing' | 'melbourne' => {
const getAutoNavigationSource = () => {
const path = location.pathname;
// Explicit routes
if (path.startsWith('/melbourne')) return 'melbourne';
// if (path.startsWith('/melbourne')) return 'melbourne';
if (path === '/' || path.startsWith('/explore')) return 'landing';
// Shared routes
if (['/passes', '/how-it-works'].includes(path)) {
return lastKnownCity; // ← remembers where user came from
}
// if (['/passes', '/how-it-works'].includes(path)) {
// return lastKnownCity; // ← remembers where user came from
// }
// Fallback
return lastKnownCity;
return citySelected;
};
@@ -261,7 +244,7 @@ export default function Navbar({
const getNavigationItems = (): NavigationItem[] => {
const currentSource = getAutoNavigationSource();
const items = currentSource === 'landing' ?
navigationConfig.landing : navigationConfig.melbourne;
navigationConfig.landing : navigationConfig.citySelected;
return items.map((item, index) => ({
...item,
@@ -370,34 +353,6 @@ export default function Navbar({
{ id: '2', name: 'Melbourne Premium Pass', price: '$129', quantity: 1 },
];
// Calculate cart total
const cartTotal = cartItems.reduce((total, item) => {
const price = parseFloat(item.price.replace('$', ''));
return total + (price * item.quantity);
}, 0);
// Cart dropdown items with proper navigation for checkout
const cartDropdownItems: DropdownItem[] = [
...cartItems.map(item => ({
id: item.id,
label: `${item.name} - ${item.price}`,
badge: `${item.quantity}x`
})),
{
id: 'total',
label: `Total: $${cartTotal.toFixed(2)}`,
icon: <ShoppingBag className="w-4 h-4" />
},
{
id: 'checkout',
label: 'Proceed to Checkout',
action: () => {
navigate('/checkout');
setActiveCartDropdown(false);
}
}
];
const closeMobileMenu = () => {
setIsMobileMenuOpen(false);
};
@@ -546,7 +501,7 @@ export default function Navbar({
>
<div className="">
<motion.div
className={`w-full transition-all duration-500 ease-out px-8 py-4 bg-white backdrop-blur-[20px] border border-white/20 ${isScrolled
className={`w-full transition-all duration-500 ease-out px-3 py-3 bg-white backdrop-blur-[20px] border border-white/20 ${isScrolled
? 'shadow-[0_10px_15px_-3px_rgba(0,0,0,0.08),0_4px_6px_-2px_rgba(0,0,0,0.05)]'
: 'shadow-lg shadow-black/5'
}`}
@@ -572,7 +527,7 @@ export default function Navbar({
? 'Melbourne CityCards Logo'
: 'CityCards Logo'
}
className="h-14 w-auto"
className="h-17 w-auto"
/>
</Link>
</motion.div>
@@ -656,27 +611,7 @@ export default function Navbar({
}
/>
{/* Shopping Cart */}
{/* <Dropdown
ref={cartRef}
isOpen={activeCartDropdown}
onToggle={() => setActiveCartDropdown(prev => !prev)}
items={cartDropdownItems}
title="Shopping Cart"
trigger={
<div className="relative text-gray-700 hover:text-gray-900 transition-colors duration-200 rounded-lg hover:bg-gray-50/50 cursor-pointer p-2">
<ShoppingBag className="w-6 h-6" />
<motion.div
className="absolute -top-1 -right-1 w-5 h-5 bg-primary rounded-full flex items-center justify-center"
animate={{ scale: [1, 1.1, 1] }}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
>
<span className="text-xs text-primary-foreground font-bold">{cartItems.length}</span>
</motion.div>
</div>
}
/> */}
<ShoppingBag className="w-6 h-6" onClick={() => navigate("/cart")} />
<ShoppingBag className="w-6 h-6 cursor-pointer" onClick={() => navigate("/cart")} />
{/* Enhanced City Card Button with Source Tracking */}
<div className="flex items-center gap-3 pl-2">

View File

@@ -3,6 +3,7 @@ import { motion, AnimatePresence } from 'motion/react';
import { Sparkles, MapPin, Calendar, Wand2, Clock } from 'lucide-react';
// import { ImageWithFallback } from './figma/ImageWithFallback';
import cityTourVideo from '../assets/citycards-vid.mp4';
import { useNavigate } from 'react-router-dom';
interface PersonalizedTourHeroProps {
onCreateItineraryClick?: () => void;
@@ -17,6 +18,7 @@ interface AttractionCard {
}
export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTourHeroProps) {
const navigate = useNavigate();
const attractionCards: AttractionCard[] = [
{
id: 1,
@@ -71,6 +73,7 @@ export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTou
const nextCard = attractionCards[(currentCardIndex + 1) % attractionCards.length];
const thirdCard = attractionCards[(currentCardIndex + 2) % attractionCards.length];
const cityName = localStorage.getItem("cityName")
return (
<div className="relative w-full min-h-[90vh] overflow-hidden flex items-center bg-gradient-to-br from-orange-50 via-white to-rose-50">
{/* Gradient Background Elements */}
@@ -107,7 +110,7 @@ export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTou
>
<Wand2 className="w-5 h-5 text-primary drop-shadow-lg" />
</motion.div>
<span className="font-poppins font-semibold text-gray-800">AI-Powered Planning</span>
<span className="font-poppins font-semibold text-gray-800">Smart Planning</span>
<motion.div
className="w-1.5 h-1.5 bg-primary rounded-full"
animate={{
@@ -128,14 +131,14 @@ export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTou
</h1>
<p className="font-poppins text-lg md:text-xl font-normal leading-relaxed text-gray-600 mb-8">
Let AI craft a personalized journey tailored to your interests, timeline, and travel style. Get the perfect itinerary in minutes.
Craft a personalized journey tailored to your interests, timeline, and travel style. Get the perfect itinerary in minutes.
</p>
{/* Quick Features */}
<div className="space-y-3 mb-8">
{[
{ icon: <Sparkles className="w-5 h-5" />, text: 'AI-powered smart suggestions' },
{ icon: <MapPin className="w-5 h-5" />, text: '40+ top Melbourne attractions' },
{ icon: <Sparkles className="w-5 h-5" />, text: 'Smart suggestions' },
{ icon: <MapPin className="w-5 h-5" />, text: `40+ top ${cityName} attractions` },
{ icon: <Calendar className="w-5 h-5" />, text: 'Flexible & customizable plans' }
].map((feature, index) => (
<motion.div
@@ -163,18 +166,18 @@ export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTou
className="flex flex-col sm:flex-row gap-4 items-start sm:items-center"
>
<button
onClick={onCreateItineraryClick}
className="group relative px-8 py-4 rounded-lg flex items-center gap-2 overflow-hidden transition-all duration-300 hover:scale-105 shadow-lg hover:shadow-xl font-poppins font-semibold text-base text-white bg-gradient-to-r from-primary via-orange-500 to-rose-500 hover:from-primary/90 hover:via-orange-500/90 hover:to-rose-500/90"
onClick={() => navigate('/create-itinerary')}
className="group cursor-pointer px-8 py-4 rounded-lg flex items-center gap-2 overflow-hidden transition-all duration-300 hover:scale-105 shadow-lg hover:shadow-xl font-poppins font-semibold text-base text-white bg-gradient-to-r from-primary via-orange-500 to-rose-500 hover:from-primary/90 hover:via-orange-500/90 hover:to-rose-500/90"
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-700" />
<Wand2 className="w-5 h-5 relative z-10 group-hover:rotate-12 transition-transform duration-300" />
<span className="relative z-10">Create My Itinerary</span>
</button>
<p className="font-poppins text-sm text-gray-600 font-normal flex items-center gap-2">
{/* <p className="font-poppins text-sm text-gray-600 font-normal flex items-center gap-2">
<Sparkles className="w-4 h-4 text-primary" />
<span>Free • Takes less than 2 minutes</span>
</p>
</p> */}
</motion.div>
</motion.div>

View File

@@ -0,0 +1,29 @@
// ProtectedRoute.tsx
import { useState } from 'react';
import { useAuth } from '../context/AuthContext';
import { LoginModal } from './LoginModal';
import { useNavigate } from 'react-router-dom';
interface ProtectedRouteProps {
children: React.ReactNode;
}
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { user } = useAuth();
const navigate = useNavigate();
const [isLoginOpen, setIsLoginOpen] = useState(!user);
if (!user) {
return (
<LoginModal
isOpen={isLoginOpen}
onClose={() => {
setIsLoginOpen(false);
navigate(-1);
}}
/>
);
}
return <>{children}</>;
}

View File

@@ -1,391 +0,0 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { X } from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { useRegisterMutation } from '../Redux/services/auth.service';
import { toast } from 'sonner';
interface RegisterModalProps {
isOpen: boolean;
onClose: () => void;
onLoginClick: () => void;
}
export function RegisterModal({ isOpen, onClose, onLoginClick }: RegisterModalProps) {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
emailAddress: '',
isdCode: '+91',
mobileNumber: '',
address1: '',
address2: '',
city: '',
state: '',
country: 'Australia',
postalCode: ''
});
const [helperText, setHelperText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [register, { isLoading: isRegistering }] = useRegisterMutation();
useEffect(() => {
if (!isOpen) {
setFormData({
firstName: '',
lastName: '',
emailAddress: '',
isdCode: '+91',
mobileNumber: '',
address1: '',
address2: '',
city: '',
state: '',
country: 'Australia',
postalCode: ''
});
setHelperText('');
}
}, [isOpen]);
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const validateForm = () => {
if (!formData.firstName.trim()) {
toast.error('First name is required');
return false;
}
if (!formData.lastName.trim()) {
toast.error('Last name is required');
return false;
}
if (!formData.emailAddress.trim() || !formData.emailAddress.includes('@')) {
toast.error('Please enter a valid email address');
return false;
}
if (!formData.mobileNumber.trim()) {
toast.error('Mobile number is required');
return false;
}
if (!/^\d+$/.test(formData.mobileNumber.trim())) {
toast.error('Mobile number must contain only digits');
return false;
}
if (!formData.address1.trim()) {
toast.error('Address is required');
return false;
}
if (!formData.city.trim()) {
toast.error('City is required');
return false;
}
if (!formData.state.trim()) {
toast.error('State is required');
return false;
}
if (!formData.postalCode.trim()) {
toast.error('Postal code is required');
return false;
}
if (!/^\d+$/.test(formData.postalCode.trim())) {
toast.error('Postal code must contain only digits');
return false;
}
return true;
};
const handleRegister = async () => {
if (!validateForm()) {
return;
}
setHelperText('');
setIsLoading(true);
try {
const response = await register(formData).unwrap();
console.log('Registration response:', response);
toast.success('Registration successful! Please login.');
setTimeout(() => {
onLoginClick();
onClose();
}, 2000);
} catch (error: any) {
console.error('Registration error:', error);
const errorMessage = error?.data?.message || 'Registration failed. Please try again.';
toast.error(errorMessage);
setHelperText(errorMessage);
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleRegister();
}
};
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
onClick={onClose}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="fixed inset-0 flex items-center justify-center z-50 p-4 overflow-y-auto"
>
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl mx-auto overflow-hidden max-h-[90vh] overflow-y-auto">
<div className="relative px-8 pt-8 pb-4 top-0 bg-white z-10">
<button
onClick={onClose}
className="absolute top-6 right-6 w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors cursor-pointer"
>
<X className="w-4 h-4 text-gray-600" />
</button>
<h2 className="font-merchant text-2xl font-semibold text-gray-900 mb-2">
Create Account
</h2>
<p className="font-poppins text-sm text-gray-600">
Register to get started with City Cards
</p>
</div>
<div className="px-8 pb-8">
<div className="space-y-6">
{/* Personal Information */}
<div className="space-y-4">
<h3 className="font-poppins text-base font-semibold text-gray-800">Personal Information</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
First Name <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter your first name"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Last Name <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter your last name"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
</div>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Email Address <span className="text-red-500">*</span>
</Label>
<Input
type="email"
placeholder="Enter your email address"
value={formData.emailAddress}
onChange={(e) => handleInputChange('emailAddress', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
ISD Code
</Label>
<Select value={formData.isdCode} onValueChange={(value: any) => handleInputChange('isdCode', value)}>
<SelectTrigger className="h-12 bg-gray-50 border-0 rounded-xl cursor-pointer">
<SelectValue placeholder="Select code" />
</SelectTrigger>
<SelectContent>
<SelectItem value="+1">+1 (USA)</SelectItem>
<SelectItem value="+44">+44 (UK)</SelectItem>
<SelectItem value="+61">+61 (Australia)</SelectItem>
<SelectItem value="+91">+91 (India)</SelectItem>
<SelectItem value="+86">+86 (China)</SelectItem>
<SelectItem value="+81">+81 (Japan)</SelectItem>
<SelectItem value="+49">+49 (Germany)</SelectItem>
<SelectItem value="+33">+33 (France)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="md:col-span-2 space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Mobile Number <span className="text-red-500">*</span>
</Label>
<Input
type="tel"
placeholder="Enter your mobile number"
value={formData.mobileNumber}
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
</div>
</div>
{/* Address Information */}
<div className="space-y-4">
<h3 className="font-poppins text-base font-semibold text-gray-800">Address Information</h3>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Address Line 1 <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter street address"
value={formData.address1}
onChange={(e) => handleInputChange('address1', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Address Line 2
</Label>
<Input
placeholder="Enter apartment, suite, unit (optional)"
value={formData.address2}
onChange={(e) => handleInputChange('address2', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
City <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter city name"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
State <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter state name"
value={formData.state}
onChange={(e) => handleInputChange('state', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Country <span className="text-red-500">*</span>
</Label>
<Select value={formData.country} onValueChange={(value: any) => handleInputChange('country', value)}>
<SelectTrigger className="h-12 bg-gray-50 border-0 rounded-xl cursor-pointer">
<SelectValue placeholder="Select country" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Australia">Australia</SelectItem>
<SelectItem value="United States">United States</SelectItem>
<SelectItem value="United Kingdom">United Kingdom</SelectItem>
<SelectItem value="Canada">Canada</SelectItem>
<SelectItem value="India">India</SelectItem>
<SelectItem value="Germany">Germany</SelectItem>
<SelectItem value="France">France</SelectItem>
<SelectItem value="Japan">Japan</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Postal Code <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Enter postal code"
value={formData.postalCode}
onChange={(e) => handleInputChange('postalCode', e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
/>
</div>
</div>
</div>
{helperText && (
<p className={`font-poppins text-xs ${helperText.includes('successful') ? 'text-green-600' : 'text-red-500'}`}>
{helperText}
</p>
)}
<Button
onClick={handleRegister}
disabled={isLoading || isRegistering}
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl transition-colors cursor-pointer"
>
{isLoading || isRegistering ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
Creating Account...
</div>
) : (
<>
Register
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</>
)}
</Button>
<div className="text-center">
<button
onClick={() => {
onLoginClick();
onClose();
}}
className="font-poppins text-sm text-gray-600 hover:text-gray-800 transition-colors cursor-pointer"
>
Already have an account? <span className="text-primary font-semibold">Login</span>
</button>
</div>
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}

View File

@@ -0,0 +1,403 @@
//RegisterPage.tsx
import { useState } from 'react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { useRegisterMutation } from '../Redux/services/auth.service';
import { toast } from 'sonner';
import { useAuth } from '../context/AuthContext';
import Navbar from './Navbar';
import { Footer } from './Footer';
import { useNavigate } from 'react-router-dom';
import { Label } from './ui/label';
import { useEffect } from 'react';
import { AlertCircle } from 'lucide-react';
export default function RegisterPage() {
const { login, user } = useAuth();
const email = localStorage.getItem("userEmail")
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
emailAddress: email ?? "",
isdCode: '',
mobileNumber: '',
address1: '',
address2: '',
city: '',
state: '',
country: '',
postalCode: ''
});
const [helperText, setHelperText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const navigate = useNavigate()
useEffect(() => {
const pendingEmail = localStorage.getItem("userEmail");
if (user || !pendingEmail) {
navigate("/");
}
}, [user]);
const [register, { isLoading: isRegistering }] = useRegisterMutation();
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
// const validateForm = () => {
// // First Name
// if (!formData.firstName.trim()) return toast.error('First name is required'), false;
// if (/\s/.test(formData.firstName)) return toast.error('First name must not contain spaces'), false;
// if (!/^[A-Za-z]+$/.test(formData.firstName)) return toast.error('First name must contain only letters (AZ)'), false;
// if (formData.firstName.length < 2 || formData.firstName.length > 50) return toast.error('First name must be between 2 and 50 characters'), false;
// // Last Name
// if (!formData.lastName.trim()) return toast.error('Last name is required'), false;
// if (/\s/.test(formData.lastName)) return toast.error('Last name must not contain spaces'), false;
// if (!/^[A-Za-z]+$/.test(formData.lastName)) return toast.error('Last name must contain only letters (AZ)'), false;
// if (formData.lastName.length < 2 || formData.lastName.length > 50) return toast.error('Last name must be between 2 and 50 characters'), false;
// // Email
// if (!formData.emailAddress.trim()) return toast.error('Email address is required'), false;
// if (!/\S+@\S+\.\S+/.test(formData.emailAddress)) return toast.error('Enter a valid email (e.g. name@example.com)'), false;
// // ISD
// if (!formData.isdCode.trim()) return toast.error('ISD code is required'), false;
// if (/\s/.test(formData.isdCode)) return toast.error('ISD code must not contain spaces'), false;
// if (!formData.isdCode.startsWith('+')) return toast.error("ISD code must start with '+' (e.g. +91)"), false;
// if (!/^\+\d+$/.test(formData.isdCode)) return toast.error("ISD code must contain only digits after '+'"), false;
// // Phone
// if (!formData.mobileNumber.trim()) return toast.error('Mobile number is required'), false;
// if (/\s/.test(formData.mobileNumber)) return toast.error('Mobile number must not contain spaces'), false;
// if (!/^\d+$/.test(formData.mobileNumber)) return toast.error('Mobile number must contain only digits (09)'), false;
// if (formData.mobileNumber.length < 7 || formData.mobileNumber.length > 15) return toast.error('Mobile number must be between 7 and 15 digits'), false;
// // Address
// if (!formData.address1.trim()) return toast.error('Address is required'), false;
// if (!/^[A-Za-z0-9\s,\-.]+$/.test(formData.address1)) return toast.error('Address can only contain letters, numbers, spaces, commas, dots, and hyphens'), false;
// if (formData.address1.length < 5 || formData.address1.length > 100) return toast.error('Address must be between 5 and 100 characters'), false;
// // City
// if (!formData.city.trim()) return toast.error('City is required'), false;
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.city)) return toast.error('City can only contain letters and spaces'), false;
// if (/\s{2,}/.test(formData.city)) return toast.error('City must not contain multiple consecutive spaces'), false;
// // State
// if (!formData.state.trim()) return toast.error('State is required'), false;
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.state)) return toast.error('State can only contain letters and spaces'), false;
// if (/\s{2,}/.test(formData.state)) return toast.error('State must not contain multiple consecutive spaces'), false;
// // Country
// if (!formData.country.trim()) return toast.error('Country is required'), false;
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.country)) return toast.error('Country can only contain letters and spaces'), false;
// // Postal Code
// if (!formData.postalCode.trim()) return toast.error('Postal code is required'), false;
// if (/\s/.test(formData.postalCode)) return toast.error('Postal code must not contain spaces'), false;
// if (!/^[A-Za-z0-9]+$/.test(formData.postalCode)) return toast.error('Postal code must contain only letters and numbers'), false;
// if (formData.postalCode.length < 4 || formData.postalCode.length > 10) return toast.error('Postal code must be between 4 and 10 characters'), false;
// return true;
// };
const validateForm = () => {
const e: Record<string, string> = {};
if (!formData.firstName.trim()) e.firstName = 'First name is required';
else if (/\s/.test(formData.firstName)) e.firstName = 'First name must not contain spaces';
else if (!/^[A-Za-z]+$/.test(formData.firstName)) e.firstName = 'First name must contain only letters (AZ)';
else if (formData.firstName.length < 2 || formData.firstName.length > 50) e.firstName = 'First name must be between 2 and 50 characters';
if (!formData.lastName.trim()) e.lastName = 'Last name is required';
else if (/\s/.test(formData.lastName)) e.lastName = 'Last name must not contain spaces';
else if (!/^[A-Za-z]+$/.test(formData.lastName)) e.lastName = 'Last name must contain only letters (AZ)';
else if (formData.lastName.length < 2 || formData.lastName.length > 50) e.lastName = 'Last name must be between 2 and 50 characters';
if (!formData.emailAddress.trim()) e.emailAddress = 'Email address is required';
else if (!/\S+@\S+\.\S+/.test(formData.emailAddress)) e.emailAddress = 'Enter a valid email (e.g. name@example.com)';
if (!formData.isdCode.trim()) e.isdCode = 'ISD code is required';
else if (/\s/.test(formData.isdCode)) e.isdCode = 'ISD code must not contain spaces';
else if (!formData.isdCode.startsWith('+')) e.isdCode = "ISD code must start with '+' (e.g. +91)";
else if (!/^\+\d+$/.test(formData.isdCode)) e.isdCode = "ISD code must contain only digits after '+'";
if (!formData.mobileNumber.trim()) e.mobileNumber = 'Mobile number is required';
else if (/\s/.test(formData.mobileNumber)) e.mobileNumber = 'Mobile number must not contain spaces';
else if (!/^\d+$/.test(formData.mobileNumber)) e.mobileNumber = 'Mobile number must contain only digits (09)';
else if (formData.mobileNumber.length < 7 || formData.mobileNumber.length > 15) e.mobileNumber = 'Mobile number must be between 7 and 15 digits';
if (!formData.address1.trim()) e.address1 = 'Address is required';
else if (!/^[A-Za-z0-9\s,\-.]+$/.test(formData.address1)) e.address1 = 'Address can only contain letters, numbers, spaces, commas, dots, and hyphens';
else if (formData.address1.length < 5 || formData.address1.length > 100) e.address1 = 'Address must be between 5 and 100 characters';
if (!formData.city.trim()) e.city = 'City is required';
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.city)) e.city = 'City can only contain letters and spaces';
else if (/\s{2,}/.test(formData.city)) e.city = 'City must not contain multiple consecutive spaces';
if (!formData.state.trim()) e.state = 'State is required';
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.state)) e.state = 'State can only contain letters and spaces';
else if (/\s{2,}/.test(formData.state)) e.state = 'State must not contain multiple consecutive spaces';
if (!formData.country.trim()) e.country = 'Country is required';
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.country)) e.country = 'Country can only contain letters and spaces';
else if (formData.country.length < 2 || formData.country.length > 50) e.country = 'Country must be between 2 and 50 characters';
if (!formData.postalCode.trim()) e.postalCode = 'Postal code is required';
else if (/\s/.test(formData.postalCode)) e.postalCode = 'Postal code must not contain spaces';
else if (!/^[A-Za-z0-9]+$/.test(formData.postalCode)) e.postalCode = 'Postal code must contain only letters and numbers';
else if (formData.postalCode.length < 4 || formData.postalCode.length > 10) e.postalCode = 'Postal code must be between 4 and 10 characters';
setFieldErrors(e);
return Object.keys(e).length === 0;
};
// Helper to render inline error (add once near top of return or as a small component)
const FieldError = ({ name }: { name: string }) =>
fieldErrors[name] ? (
<p className="text-xs text-red-500 mt-1 flex items-center gap-1">
<AlertCircle className="w-3 h-3" />{fieldErrors[name]}
</p>
) : null;
const handleRegister = async () => {
if (!validateForm()) return;
setIsLoading(true);
setHelperText('');
try {
const response = await register(formData).unwrap();
toast.success('Registration successful!');
const userData = {
userId: response?.user?.id,
email: response?.email || formData.emailAddress,
name: response?.name || formData.emailAddress.split('@')[0].charAt(0).toUpperCase() + formData.emailAddress.split('@')[0].slice(1),
accessToken: response?.accessToken,
};
login(userData);
localStorage.removeItem("userEmail")
navigate("/")
} catch (err: any) {
const msg = err?.data?.message || 'Registration failed';
toast.error(msg);
setHelperText(msg);
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex flex-col bg-gray-50 w-full">
{/* Navbar */}
<Navbar activeCity="" />
{/* Main Content */}
<div className="flex-grow w-full px-6 md:px-10 py-10 mt-20">
<div className="w-full max-w-5xl mx-auto">
{/* Header */}
<div className="mb-8">
<h2 className="font-merchant text-3xl font-semibold text-gray-900 mb-2">
Create Account
</h2>
<p className="font-poppins text-gray-600">
Register to get started with City Cards
</p>
</div>
{/* Form Container */}
<div className="bg-white rounded-2xl border border-gray-200 p-8 space-y-8">
{/* Personal Info */}
<div className="space-y-4">
<h3 className="font-poppins font-semibold text-gray-800 text-lg">
Personal Information
</h3>
<div className="grid md:grid-cols-2 gap-6">
<div>
<Label htmlFor="firstName" className="font-poppins font-light">
First Name <span className="text-red-500">*</span>
</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.firstName ? 'border border-red-400' : ''}`}
/>
<FieldError name="firstName" />
</div>
<div>
<Label htmlFor="lastName" className="font-poppins font-light">
Last Name <span className="text-red-500">*</span>
</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.lastName ? 'border border-red-400' : ''}`}
/>
<FieldError name="lastName" />
</div>
</div>
<div>
<Label htmlFor="emailAddress" className="font-poppins font-light">
Email Address <span className="text-red-500">*</span>
</Label>
<Input
id="emailAddress"
type="email"
value={formData.emailAddress}
disabled
onChange={(e) => handleInputChange('emailAddress', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
<FieldError name="emailAddress" />
</div>
<div className="grid md:grid-cols-3 gap-6">
<div>
<Label htmlFor="isdCode" className="font-poppins font-light">
ISD Code <span className="text-red-500">*</span>
</Label>
<Input
id="isdCode"
placeholder="example: +91"
value={formData.isdCode}
onChange={(e) => handleInputChange('isdCode', e.target.value)}
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.isdCode ? 'border border-red-400' : ''}`}
/>
<FieldError name="isdCode" />
</div>
<div className="md:col-span-2">
<Label htmlFor="mobileNumber" className="font-poppins font-light">
Mobile Number <span className="text-red-500">*</span>
</Label>
<Input
id="mobileNumber"
value={formData.mobileNumber}
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.mobileNumber ? 'border border-red-400' : ''}`}
/>
<FieldError name="mobileNumber" />
</div>
</div>
</div>
{/* Address */}
<div className="space-y-4">
<h3 className="font-poppins font-semibold text-gray-800 text-lg">
Address Information
</h3>
<div>
<Label htmlFor="address1" className="font-poppins font-light">
Address Line 1 <span className="text-red-500">*</span>
</Label>
<Input
id="address1"
value={formData.address1}
onChange={(e) => handleInputChange('address1', e.target.value)}
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.address1 ? 'border border-red-400' : ''}`}
/>
<FieldError name="address1" />
</div>
<div>
<Label htmlFor="address2" className="font-poppins font-light">Address Line 2</Label>
<Input
id="address2"
value={formData.address2}
onChange={(e) => handleInputChange('address2', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div>
<Label htmlFor="city" className="font-poppins font-light">
City <span className="text-red-500">*</span>
</Label>
<Input
id="city"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.city ? 'border border-red-400' : ''}`}
/>
<FieldError name="city" />
</div>
<div>
<Label htmlFor="state" className="font-poppins font-light">
State <span className="text-red-500">*</span>
</Label>
<Input
id="state"
value={formData.state}
onChange={(e) => handleInputChange('state', e.target.value)}
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.state ? 'border border-red-400' : ''}`}
/>
<FieldError name="state" />
</div>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div>
<Label htmlFor="country" className="font-poppins font-light">
Country <span className="text-red-500">*</span>
</Label>
<Input
id="country"
value={formData.country}
onChange={(e) => handleInputChange('country', e.target.value)}
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.country ? 'border border-red-400' : ''}`}
/>
<FieldError name="country" />
</div>
<div>
<Label htmlFor="postalCode" className="font-poppins font-light">
Postal Code <span className="text-red-500">*</span>
</Label>
<Input
id="postalCode"
value={formData.postalCode}
onChange={(e) => handleInputChange('postalCode', e.target.value)}
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.postalCode ? 'border border-red-400' : ''}`}
/>
<FieldError name="postalCode" />
</div>
</div>
</div>
{helperText && (
<p className="text-sm text-red-500">{helperText}</p>
)}
<Button
onClick={handleRegister}
disabled={isLoading || isRegistering}
className="w-full cursor-pointer bg-gray-800 hover:bg-gray-900 md:px-10 h-12 text-white rounded-xl"
>
{isLoading || isRegistering ? 'Registering...' : 'Register'}
</Button>
</div>
</div>
</div>
{/* Footer */}
<div className="mt-auto">
<Footer />
</div>
</div>
);
}

View File

@@ -0,0 +1,12 @@
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}

View File

@@ -204,7 +204,7 @@ export function TrustSection() {
style={{
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
transformOrigin: 'center center',
minHeight: '480px',
minHeight: '360px',
background: `
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),

View File

@@ -100,7 +100,7 @@ export function WhatsIncludedHero({ onCreateItineraryClick }: WhatsIncludedHeroP
>
{/* Main Heading */}
<h1 className="font-poppins text-4xl sm:text-5xl md:text-6xl w-full leading-tight mb-6">
<span className="font-light">One pass.</span>{' '}
<span className="font-light">One CityCard</span>{' '}
<span className="font-bold italic pr-2 bg-gradient-to-r from-primary via-orange-500 to-rose-500 bg-clip-text text-transparent">
Everything you
</span>{' '}

View File

@@ -1,3 +1,4 @@
// AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom';
@@ -40,6 +41,8 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
localStorage.removeItem("user")
localStorage.removeItem("accessToken")
localStorage.removeItem("userId")
localStorage.removeItem("userEmail")
sessionStorage.removeItem("citySelected")
navigate("/")
}

View File

@@ -5,11 +5,13 @@ import "./index.css";
import { Provider } from "react-redux";
import { store } from "./Redux/Store";
import { Toaster } from "sonner";
import { ScrollToTop } from "./components/ScrollToTop";
createRoot(document.getElementById("root")!).render(
<Provider store={store}>
<BrowserRouter>
<Toaster position="top-right" richColors duration={2000} closeButton />
<ScrollToTop />
<App />
</BrowserRouter>
</Provider>

View File

@@ -504,7 +504,7 @@ export function CartPage({
<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>
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent pr-2">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`}

View File

@@ -1,878 +0,0 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import {
Users, Baby, ShoppingBag, Trash2, Check, CreditCard, Mail,
ChevronRight, ChevronDown, Minus, Plus, Calendar, ArrowLeft, MapPin,
Zap, Shield, Clock, Percent, Sparkles
} from 'lucide-react';
import Navbar from '../components/Navbar';
import { Footer } from '../components/Footer';
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
import { useNavigate } from 'react-router-dom';
import { useGetCardsinCartQuery } from '../Redux/services/cards.service';
import LoadingSpinner from '../components/LoadingSpinner'
/* ─── Types ─── */
export interface CartItem {
id: string;
city: string;
cardType: 'Flexi' | 'Unlimited';
days: number;
adults: number;
children: number;
quantity: number;
pricePerUnit: number;
image: string;
}
interface Attraction {
id: string;
name: string;
image: string;
category: string;
included: boolean;
}
interface CartPageDesignProps {
onBackClick: () => void;
onHomeClick: () => void;
onPassesClick: () => void;
onCheckoutClick?: () => void;
onSecureCheckoutClick?: (item: CartItem) => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onAttractionsClick?: () => void;
onBlogsClick?: () => void;
onHowItWorksClick?: () => void;
onFAQClick?: () => void;
onPrivacyPolicyClick?: () => void;
onAboutUsClick?: () => void;
onProfileClick?: () => void;
onCityCardsClick?: () => void;
onMagicItineraryClick?: () => void;
onPostCardsClick?: () => void;
onOffersClick?: () => void;
onSuperSavingsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
onContactUsClick?: () => void;
onCartClick?: () => void;
currentPage?: string;
user?: { email: string; name: string } | null;
}
/* ─── Data ─── */
const initialCartItems: CartItem[] = [
{
id: '1', city: 'Melbourne', cardType: 'Flexi', days: 3, adults: 3, children: 3, quantity: 2, pricePerUnit: 49.50,
image: 'https://images.unsplash.com/photo-1655963754904-2cf2b562a681?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBmbGluZGVycyUyMHN0YXRpb24lMjBzdW5zZXR8ZW58MXx8fHwxNzc2MzE5NDgzfDA&ixlib=rb-4.1.0&q=80&w=1080',
},
{
id: '2', city: 'Sydney', cardType: 'Flexi', days: 3, adults: 3, children: 3, quantity: 2, pricePerUnit: 49.50,
image: 'https://images.unsplash.com/photo-1695018228065-2e0026c654af?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxTeWRuZXklMjBvcGVyYSUyMGhvdXNlJTIwaGFyYm91ciUyMGJyaWRnZXxlbnwxfHx8fDE3NzYzMTk0ODN8MA&ixlib=rb-4.1.0&q=80&w=1080',
},
{
id: '3', city: 'Melbourne', cardType: 'Unlimited', days: 6, adults: 2, children: 1, quantity: 1, pricePerUnit: 79.00,
image: 'https://images.unsplash.com/photo-1705120624704-0970afc29fea?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBzdHJlZXQlMjBhcnQlMjBsYW5ld2F5c3xlbnwxfHx8fDE3NzYzMTk0ODR8MA&ixlib=rb-4.1.0&q=80&w=1080',
},
];
const dayOptions = [3, 6, 12, 18, 24];
const attractionsData: Record<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>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,886 +0,0 @@
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

@@ -56,7 +56,7 @@ interface User {
name: string;
}
interface CreateMagicItineraryPageDesignProps {
interface CreateMagicItineraryPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
@@ -94,49 +94,7 @@ interface ItineraryData {
}
const destinations = [
{
id: 'melbourne',
name: 'Melbourne',
country: 'Australia',
image: 'https://images.unsplash.com/photo-1595434971780-79d5c20c5090?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBza3lsaW5lJTIwYXVzdHJhbGlhfGVufDF8fHx8MTc1ODk2MzkxOHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
},
{
id: 'sydney',
name: 'Sydney',
country: 'Australia',
image: 'https://images.unsplash.com/photo-1595563382617-fe6fa4fd0394?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzeWRuZXklMjBoYXJib3VyJTIwYnJpZGdlJTIwYXVzdHJhbGlhfGVufDF8fHx8MTc1ODk2MzkyMXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
},
{
id: 'brisbane',
name: 'Brisbane',
country: 'Australia',
image: 'https://images.unsplash.com/photo-1729904987421-12490733e013?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxicmlzYmFuZSUyMGNpdHklMjByaXZlciUyMGF1c3RyYWxpYXxlbnwxfHx8fDE3NTg5NjM5Mjd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
}
];
const energyOptions = [
{
id: 'adventurous',
name: 'Adventurous',
description: 'High energy activities and exploration',
image: 'https://images.unsplash.com/photo-1587502537147-2ba64a62e3d3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhZHZlbnR1cmUlMjBvdXRkb29yJTIwaGlraW5nfGVufDF8fHx8MTc1ODk2NDQ2N3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
},
{
id: 'relaxed',
name: 'Relaxed',
description: 'Peaceful and laid-back experiences',
image: 'https://images.unsplash.com/photo-1716893933701-73d59789bba7?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHxyZWxheGVkJTIwc3BhJTIwcGVhY2VmdWx8ZW58MXx8fHwxNzU4OTY0NDcxfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
}
];
const questionImages = {
museums: 'https://images.unsplash.com/photo-1747918157024-a1e1c77336fa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtdXNldW0lMjBhcnQlMjBnYWxsZXJ5fGVufDF8fHx8MTc1ODkwOTYwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
};
const dietaryOptions = ['No Restrictions', 'Vegetarian', 'Vegan', 'Pescatarian', 'Halal', 'Kosher'];
export function CreateMagicItineraryPageDesign({
export function CreateMagicItineraryPage({
onBackClick,
onHomeClick,
onMelbourneClick,
@@ -160,7 +118,7 @@ export function CreateMagicItineraryPageDesign({
onHotelDiscountsClick,
currentPage,
user
}: CreateMagicItineraryPageDesignProps) {
}: CreateMagicItineraryPageProps) {
const [currentStep, setCurrentStep] = useState(1);
const [isGenerating, setIsGenerating] = useState(false);
const [showResults, setShowResults] = useState(false);
@@ -191,7 +149,7 @@ export function CreateMagicItineraryPageDesign({
const [selectedActivity, setSelectedActivity] = useState<string | null>(null);
const [createMagicItinerary] = useCreateMagicItineraryMutation();
const navigate= useNavigate()
const navigate = useNavigate()
const toggleFavorite = (activityKey: string) => {
setFavorites(prev => {
@@ -255,17 +213,17 @@ export function CreateMagicItineraryPageDesign({
const generateItinerary = async () => {
try {
console.log("creating itinerary...", itineraryDetails);
setIsGenerating(true);
const response = await createMagicItinerary(itineraryDetails);
console.log(response)
setGeneratedItinerary(response);
setShowResults(true);
toast.success("Itinerary created successfully!");
navigate(`/itinerary-summary/${response?.data?.id}`)
} catch (error) {
console.error("Error creating itinerary:", error);
toast.error("Failed to create itinerary. Please try again.");
if (response?.data?.id) {
navigate(`/itinerary-summary/${response?.data?.id}`)
toast.success("Itinerary created successfully!");
} else {
throw new Error(response?.error?.data?.message)
}
} catch (error: any) {
toast.error(error.message);
} finally {
setIsGenerating(false);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
import { ArrowRight, Check, CreditCard, DollarSign, MapPin, Palette, Sparkles, Ticket, Zap } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import { useEffect, useState } from 'react';
import { use, useEffect, useState } from 'react';
import { Layout } from '../Layout';
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
import { MobileAppSection } from '../components/MobileAppSection';
import { TrustSection } from '../components/TrustSection';
import { Button } from '../components/ui/button';
import { useNavigate } from 'react-router-dom';
interface User {
email: string;
@@ -68,6 +69,8 @@ export function DiscoverPage({
const [direction, setDirection] = useState(0);
const [activeStep, setActiveStep] = useState(0);
const navigate =useNavigate();
const handleStepInView = (index: number) => {
setActiveStep(index);
};
@@ -687,8 +690,8 @@ export function DiscoverPage({
</div>
<Button
onClick={onPassesClick}
className="w-full py-6 rounded-full font-poppins font-semibold text-lg bg-gray-900 hover:bg-black text-white transition-all duration-300"
onClick={()=>navigate('/passes')}
className="cursor-pointer w-full py-6 rounded-full font-poppins font-semibold text-lg bg-gray-900 hover:bg-black text-white transition-all duration-300"
>
VIEW FLEXI OPTIONS
</Button>
@@ -743,8 +746,8 @@ export function DiscoverPage({
</div>
<Button
onClick={onPassesClick}
className="w-full py-6 rounded-full font-poppins font-semibold text-lg bg-primary hover:bg-primary/90 text-white transition-all duration-300"
onClick={()=>navigate('/passes')}
className=" cursor-pointer w-full py-6 rounded-full font-poppins font-semibold text-lg bg-primary hover:bg-primary/90 text-white transition-all duration-300"
>
VIEW UNLIMITED OPTIONS
</Button>

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import {
MapPin,
@@ -7,13 +7,16 @@ import {
Share2,
Download,
ChevronRight,
Loader2,
} from 'lucide-react';
import { Button } from '../components/ui/button';
import { Card, CardContent } from '../components/ui/card';
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
import { useCreateMagicItineraryMutation, useGetItineraryDetailsByIdQuery } from '../Redux/services/itinerary.service';
import { useDownloadItineraryQuery, useGetItineraryDetailsByIdQuery } from '../Redux/services/itinerary.service';
import { toast } from 'sonner';
import { useNavigate, useParams } from 'react-router-dom';
import Navbar from '../components/Navbar';
import { Footer } from '../components/Footer';
const ItinerarySummaryPage = () => {
const [viewMode, setViewMode] = useState<'daily' | 'summary'>('daily');
@@ -24,6 +27,37 @@ const ItinerarySummaryPage = () => {
const { itineraryId } = useParams()
const { data: itineraryDetails, isLoading: itineraryDetailsLoading } = useGetItineraryDetailsByIdQuery(itineraryId);
// Download logic using standard query with manual trigger
const [shouldDownload, setShouldDownload] = useState(false);
const { data: pdfBlob, isFetching: isDownloading, refetch } = useDownloadItineraryQuery
(itineraryId!, {
skip: !shouldDownload || !itineraryId,
});
useEffect(() => {
if (shouldDownload && pdfBlob) {
// Create download link
const url = window.URL.createObjectURL(pdfBlob);
const link = document.createElement('a');
link.href = url;
link.download = `itinerary-${itineraryId}.pdf`;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
toast.success('Itinerary downloaded successfully!');
setShouldDownload(false); // reset trigger
}
}, [pdfBlob, shouldDownload, itineraryId]);
const handleDownloadItinerary = useCallback(() => {
if (!itineraryId) {
toast.error('Itinerary ID not found');
return;
}
setShouldDownload(true);
refetch(); // manually trigger the download query
}, [itineraryId, refetch]);
const generatedItinerary = itineraryDetails ?? null;
const days = generatedItinerary?.days ?? [];
@@ -33,298 +67,312 @@ const ItinerarySummaryPage = () => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-8 max-w-3xl mx-auto"
>
{/* Title */}
<div className="text-center space-y-1">
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
<span className="font-normal">Your</span>
</h1>
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
<span className="font-bold text-primary italic">{generatedItinerary?.title}</span>
</h1>
</div>
<div className="min-h-screen bg-background">
{/* Navbar */}
<Navbar
/>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-8 max-w-3xl mx-auto mt-25"
>
{/* Title */}
<div className="text-center space-y-1">
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
<span className="font-normal">Your</span>
</h1>
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
<span className="font-bold text-primary italic">{generatedItinerary?.title}</span>
</h1>
</div>
{/* Trip Details Card */}
<div className="relative overflow-hidden rounded-2xl border border-gray-100 shadow-sm">
{/* Background Image */}
<div className="relative h-40 md:h-48">
<ImageWithFallback
src={generatedItinerary?.cityBanner}
alt={generatedItinerary?.city}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent" />
<div className="absolute bottom-4 left-5 right-5">
<p className="font-poppins text-xs font-medium text-white/70 uppercase tracking-wider mb-1">Your Trip</p>
<h3 className="font-merchant text-2xl md:text-3xl text-white leading-snug font-semibold">{generatedItinerary?.city}</h3>
{/* Trip Details Card */}
<div className="relative overflow-hidden rounded-2xl border border-gray-100 shadow-sm">
{/* Background Image */}
<div className="relative h-40 md:h-48">
<ImageWithFallback
src={generatedItinerary?.cityBanner}
alt={generatedItinerary?.city}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent" />
<div className="absolute bottom-4 left-5 right-5">
<p className="font-poppins text-xs font-medium text-white/70 uppercase tracking-wider mb-1">Your Trip</p>
<h3 className="font-merchant text-2xl md:text-3xl text-white leading-snug font-semibold">{generatedItinerary?.city}</h3>
</div>
</div>
{/* Stats Row */}
<div className="grid grid-cols-3 divide-x divide-gray-100 bg-white">
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalDays}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Days</span>
</div>
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalStops}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Stops</span>
</div>
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.days[0]?.date}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Start Date</span>
</div>
</div>
</div>
{/* Stats Row */}
<div className="grid grid-cols-3 divide-x divide-gray-100 bg-white">
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalDays}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Days</span>
</div>
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalStops}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Stops</span>
</div>
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.days[0]?.date}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Start Date</span>
</div>
</div>
</div>
{/* Share & Download Buttons */}
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3"
>
<Share2 className="w-4 h-4 mr-2" />
Share
</Button>
<Button
variant="outline"
className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3"
>
<Download className="w-4 h-4 mr-2" />
Download
</Button>
</div>
{/* View Toggle */}
<div className="flex justify-center">
<div className="bg-gray-100 p-1 rounded-full inline-flex">
<button
onClick={() => setViewMode('daily')}
className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'daily'
? 'bg-white shadow-sm text-gray-900'
: 'text-gray-500 hover:text-gray-700'
}`}
{/* Share & Download Buttons */}
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3"
>
Daily View
</button>
<button
onClick={() => setViewMode('summary')}
className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'summary'
? 'bg-white shadow-sm text-gray-900'
: 'text-gray-500 hover:text-gray-700'
}`}
<Share2 className="w-4 h-4 mr-2" />
Share
</Button>
<Button
onClick={handleDownloadItinerary}
disabled={isDownloading}
variant="outline"
className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3"
>
Summary
</button>
</div>
</div>
{/* Daily View */}
{viewMode === 'daily' && (
<div className="space-y-6">
{/* Day Tabs */}
<div className="flex items-center gap-2 overflow-x-auto pb-2">
{days?.map((day: any) => (
<button
key={day.dayNumber}
onClick={() => setSelectedDayTab(day.dayNumber)}
className={`px-5 py-2.5 rounded-xl whitespace-nowrap font-poppins text-base transition-all ${selectedDayTab === day.dayNumber
? 'text-primary font-semibold bg-primary/10 border border-primary/20'
: 'text-gray-400 font-medium hover:text-gray-600'
}`}
>
Day {day.dayNumber}
</button>
))}
{days?.length > 4 && (
<button className="p-2 text-gray-400 hover:text-gray-600">
<ChevronRight className="w-4 h-4" />
</button>
{isDownloading ? (
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
) : (
<Download className="w-5 h-5 mr-2" />
)}
</div>
{/* Activities for selected day */}
{selectedDayPlan && (
<AnimatePresence mode="wait">
<motion.div key={`day-${selectedDayTab}`} initial={{ opacity: 0, x: 10 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -10 }} transition={{ duration: 0.3 }} className="space-y-8">
{selectedDayPlan?.items?.map((activity: any, actIndex: number) => {
const activityKey = `day${selectedDayPlan.day}-act${actIndex}`;
return (
<motion.div
key={actIndex}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: actIndex * 0.08 }}
className="space-y-4"
>
{/* Time Label */}
<p className="font-poppins text-sm font-medium text-gray-500 text-center uppercase tracking-wider">
{activity.timeSlot}
</p>
{/* Activity Card */}
<Card className="overflow-hidden border border-gray-100 shadow-sm hover:shadow-lg transition-shadow duration-300 rounded-2xl">
<CardContent className="p-0">
{/* Image */}
<div className="relative h-56 md:h-64 bg-gray-200">
<ImageWithFallback
src={activity.imageUrl}
alt={activity.title}
className="w-full h-full object-cover"
/>
{/* TODO: Get Directions Badge redirect it to lat,long */}
<div className="absolute bottom-3 left-3">
<button className="flex items-center gap-1.5 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold shadow-lg">
<MapPin className="w-3.5 h-3.5" />
Get Directions
</button>
</div>
</div>
{/* Content */}
<div className="p-5 space-y-3">
<h4 className="font-poppins text-lg font-semibold text-gray-900 leading-snug">
{activity.title}
</h4>
<p className="font-poppins text-sm font-normal text-gray-500 leading-relaxed">
{activity.locationName}
</p>
{/* Category Tags */}
<div className="flex flex-wrap gap-2">
{activity.categories?.map((cat: string, ci: number) => (
<span
key={ci}
className="font-poppins text-xs font-medium px-3 py-1.5 rounded-full border border-primary/20 text-primary bg-primary/5"
>
{cat}
</span>
))}
</div>
{/* Bullet Points */}
<div className="space-y-1.5 pt-1">
<div className="flex items-baseline gap-2">
<span className="text-primary flex-shrink-0 text-sm leading-relaxed"></span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">{activity.description}</span>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
);
})}
</motion.div>
</AnimatePresence>
)}
Download
</Button>
</div>
)}
{/* Summary View */}
{viewMode === 'summary' && (
<div className="space-y-6">
{days?.map((day: any, dayIndex: number) => {
const dayDate = days[0]?.date
? new Date(
new Date(days[0].date).setDate(
new Date(days[0].date).getDate() + dayIndex
)
).toLocaleDateString('en-AU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
: '';
{/* View Toggle */}
<div className="flex justify-center">
<div className="bg-gray-100 p-1 rounded-full inline-flex">
<button
onClick={() => setViewMode('daily')}
className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'daily'
? 'bg-white shadow-sm text-gray-900'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Daily View
</button>
<button
onClick={() => setViewMode('summary')}
className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'summary'
? 'bg-white shadow-sm text-gray-900'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Summary
</button>
</div>
</div>
// ✅ Find the matching summary for this day
const daySummary = summaries.find((s: any) => s.dayNumber === day.dayNumber);
{/* Daily View */}
{viewMode === 'daily' && (
<div className="space-y-6">
{/* Day Tabs */}
<div className="flex items-center gap-2 overflow-x-auto pb-2">
{days?.map((day: any) => (
<button
key={day.dayNumber}
onClick={() => setSelectedDayTab(day.dayNumber)}
className={`px-5 py-2.5 rounded-xl whitespace-nowrap font-poppins text-base transition-all ${selectedDayTab === day.dayNumber
? 'text-primary font-semibold bg-primary/10 border border-primary/20'
: 'text-gray-400 font-medium hover:text-gray-600'
}`}
>
Day {day.dayNumber}
</button>
))}
{days?.length > 4 && (
<button className="p-2 text-gray-400 hover:text-gray-600">
<ChevronRight className="w-4 h-4" />
</button>
)}
</div>
return (
<div key={dayIndex} className="border-l-4 border-primary/20 pl-5 space-y-3">
{/* Day Header */}
<div className="flex items-center justify-between">
<h3 className="font-poppins text-lg font-semibold text-gray-900">
Day {day.dayNumber}:
</h3>
<div className="flex items-center gap-1.5 text-primary">
<Calendar className="w-3.5 h-3.5" />
<span className="font-poppins text-sm font-medium">{dayDate}</span>
</div>
</div>
{/* Activity List */}
<div className="space-y-2">
{daySummary?.items?.map((item: any, actIndex: number) => {
const activityKey = `summary-day${day.dayNumber}-act${actIndex}`;
const isExpanded = selectedActivity === activityKey;
{/* Activities for selected day */}
{selectedDayPlan && (
<AnimatePresence mode="wait">
<motion.div key={`day-${selectedDayTab}`} initial={{ opacity: 0, x: 10 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -10 }} transition={{ duration: 0.3 }} className="space-y-8">
{selectedDayPlan?.items?.map((activity: any, actIndex: number) => {
const activityKey = `day${selectedDayPlan.day}-act${actIndex}`;
return (
<div key={actIndex} className="bg-gray-50 rounded-xl overflow-hidden">
<div
className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-gray-100 transition-colors"
onClick={() =>
setSelectedActivity(isExpanded ? null : activityKey)
}
>
<p className="font-poppins text-sm font-medium text-gray-800">
{item.timeSlot}: {item.title}
</p>
<ChevronDown
className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
/>
</div>
<motion.div
key={actIndex}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: actIndex * 0.08 }}
className="space-y-4"
>
{/* Time Label */}
<p className="font-poppins text-sm font-medium text-gray-500 text-center uppercase tracking-wider">
{activity.timeSlot}
</p>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
<div className="px-4 pb-4 space-y-2">
<div className="flex items-baseline gap-2">
<span className="text-primary flex-shrink-0 text-sm leading-relaxed"></span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">
{item.description}
</span>
</div>
<button className="flex items-center gap-1.5 mt-2 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold">
{/* Activity Card */}
<Card className="overflow-hidden border border-gray-100 shadow-sm hover:shadow-lg transition-shadow duration-300 rounded-2xl">
<CardContent className="p-0">
{/* Image */}
<div className="relative h-56 md:h-64 bg-gray-200">
<ImageWithFallback
src={activity.imageUrl}
alt={activity.title}
className="w-full h-full object-cover"
/>
{/* TODO: Get Directions Badge redirect it to lat,long */}
<div className="absolute bottom-3 left-3">
<button className="flex items-center gap-1.5 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold shadow-lg">
<MapPin className="w-3.5 h-3.5" />
Get directions
Get Directions
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* Content */}
<div className="p-5 space-y-3">
<h4 className="font-poppins text-lg font-semibold text-gray-900 leading-snug">
{activity.title}
</h4>
<p className="font-poppins text-sm font-normal text-gray-500 leading-relaxed">
{activity.locationName}
</p>
{/* Category Tags */}
<div className="flex flex-wrap gap-2">
{activity.categories?.map((cat: string, ci: number) => (
<span
key={ci}
className="font-poppins text-xs font-medium px-3 py-1.5 rounded-full border border-primary/20 text-primary bg-primary/5"
>
{cat}
</span>
))}
</div>
{/* Bullet Points */}
<div className="space-y-1.5 pt-1">
<div className="flex items-baseline gap-2">
<span className="text-primary flex-shrink-0 text-sm leading-relaxed"></span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">{activity.description}</span>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
);
})}
</div>
</div>
);
})}
</div>
)}
</motion.div>
</AnimatePresence>
)}
</div>
)}
{/* Bottom Action */}
<div className="flex justify-center pt-4 pb-8">
<Button
onClick={() => navigate('/create-itinerary-design')}
className="w-full font-poppins font-semibold px-8 py-3 rounded-xl bg-primary hover:bg-primary/90 text-white shadow-md shadow-primary/20"
>
Create Another Itinerary
</Button>
</div>
</motion.div>
{/* Summary View */}
{viewMode === 'summary' && (
<div className="space-y-6">
{days?.map((day: any, dayIndex: number) => {
const dayDate = days[0]?.date
? new Date(
new Date(days[0].date).setDate(
new Date(days[0].date).getDate() + dayIndex
)
).toLocaleDateString('en-AU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
: '';
// ✅ Find the matching summary for this day
const daySummary = summaries.find((s: any) => s.dayNumber === day.dayNumber);
return (
<div key={dayIndex} className="border-l-4 border-primary/20 pl-5 space-y-3">
{/* Day Header */}
<div className="flex items-center justify-between">
<h3 className="font-poppins text-lg font-semibold text-gray-900">
Day {day.dayNumber}:
</h3>
<div className="flex items-center gap-1.5 text-primary">
<Calendar className="w-3.5 h-3.5" />
<span className="font-poppins text-sm font-medium">{dayDate}</span>
</div>
</div>
{/* Activity List */}
<div className="space-y-2">
{daySummary?.items?.map((item: any, actIndex: number) => {
const activityKey = `summary-day${day.dayNumber}-act${actIndex}`;
const isExpanded = selectedActivity === activityKey;
return (
<div key={actIndex} className="bg-gray-50 rounded-xl overflow-hidden">
<div
className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-gray-100 transition-colors"
onClick={() =>
setSelectedActivity(isExpanded ? null : activityKey)
}
>
<p className="font-poppins text-sm font-medium text-gray-800">
{item.timeSlot}: {item.title}
</p>
<ChevronDown
className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
/>
</div>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
<div className="px-4 pb-4 space-y-2">
<div className="flex items-baseline gap-2">
<span className="text-primary flex-shrink-0 text-sm leading-relaxed"></span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">
{item.description}
</span>
</div>
<button className="flex items-center gap-1.5 mt-2 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold">
<MapPin className="w-3.5 h-3.5" />
Get directions
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
</div>
);
})}
</div>
)}
{/* Bottom Action */}
<div className="flex justify-center pt-4 pb-8">
<Button
onClick={() => navigate('/create-itinerary')}
className="w-full font-poppins font-semibold px-8 py-3 rounded-xl bg-primary hover:bg-primary/90 text-white shadow-md shadow-primary/20"
>
Create Another Itinerary
</Button>
</div>
</motion.div>
<Footer
/>
</div>
);
}

View File

@@ -1,12 +1,16 @@
import React, { useState } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Calendar, Clock, MapPin, Users, Star, Heart, Share2, Download, CheckCircle, Navigation, Cloud, Sun } from 'lucide-react';
import { ArrowLeft, Calendar, MapPin, Users, Star, Heart, Share2, Download, CheckCircle, Navigation, Cloud, Sun, Loader2 } from 'lucide-react';
import { Button } from '../components/ui/button';
import { Card, CardContent } from '../components/ui/card';
import { Badge } from '../components/ui/badge';
import Navbar from '../components/Navbar';
import { Footer } from '../components/Footer';
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
import { useGetItineraryDetailsByIdQuery, useDownloadItineraryQuery } from '../Redux/services/itinerary.service';
import { useNavigate, useParams } from 'react-router-dom';
import LoadingSpinner from '../components/LoadingSpinner';
import { toast } from 'sonner'; // optional, install if not present
interface ItineraryViewPageProps {
onBackClick: () => void;
@@ -35,20 +39,7 @@ interface ItineraryViewPageProps {
user?: { email: string; name: string; } | null;
}
// Enhanced activity type with more details
interface Activity {
time: string;
activity: string;
location: string;
address: string;
image: string;
categories: string[];
description: string[];
isFavorite?: boolean;
}
export function ItineraryViewPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
@@ -68,310 +59,55 @@ export function ItineraryViewPage({
onOffersClick,
onCreateItineraryClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: ItineraryViewPageProps) {
const [viewMode, setViewMode] = useState<'daily' | 'summary'>('daily');
const [favorites, setFavorites] = useState<Set<string>>(new Set());
const navigate = useNavigate();
const toggleFavorite = (activityKey: string) => {
setFavorites(prev => {
const newSet = new Set(prev);
if (newSet.has(activityKey)) {
newSet.delete(activityKey);
} else {
newSet.add(activityKey);
}
return newSet;
});
};
// ── API Integration ──────────────────────────────────────────────────────────
const { itineraryId } = useParams();
const { data: itineraryDetails, isLoading } = useGetItineraryDetailsByIdQuery(itineraryId);
// Enhanced itinerary data with images, addresses, and detailed info
const generatedItinerary = {
destination: {
name: 'Melbourne',
country: 'Australia',
weather: '18°C, Sunny',
image: 'https://images.unsplash.com/photo-1514395462725-fb4566210144?w=400&h=300&fit=crop'
},
totalDays: 3,
estimatedCost: '$450 AUD',
includedActivities: 18,
dailyPlans: [
{
day: 1,
title: "City Center & Culture",
activities: [
{
time: '8:00 am',
activity: 'The Langham Melbourne',
location: 'The Langham Melbourne',
address: '1 Southgate Avenue, Southbank VIC 3006',
image: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=800&h=600&fit=crop',
categories: ['Accommodation', 'Luxury'],
description: [
'Check-in at luxury riverside hotel',
'Enjoy complimentary breakfast',
'Relax at the spa facilities',
'Explore the surrounding Southbank area'
]
},
{
time: '10:00 am',
activity: 'Federation Square',
location: 'Federation Square',
address: 'Corner Swanston & Flinders Streets, Melbourne VIC 3000',
image: 'https://images.unsplash.com/photo-1514395462725-fb4566210144?w=800&h=600&fit=crop',
categories: ['Culture', 'Landmark'],
description: [
'Explore Melbourne\'s cultural precinct',
'Visit the ACMI museum',
'Enjoy street performances',
'Take photos at iconic locations'
]
},
{
time: '12:00 pm',
activity: 'Degrave Street Café',
location: 'Degrave Street Espresso Bar',
address: '23-25 Degraves Street, Melbourne VIC 3000',
image: 'https://images.unsplash.com/photo-1554118811-1e0d58224f24?w=800&h=600&fit=crop',
categories: ['Food', 'Drinks', 'Culture'],
description: [
'Coffee at Pellegrini\'s Espresso Bar (iconic old-school cafe)',
'Try the famous jam doughnuts',
'Shop for fresh produce in the Deli Hall',
'Pick up unique souvenirs in the General Merchandise section'
]
},
{
time: '2:00 pm',
activity: 'Royal Botanic Gardens',
location: 'Royal Botanic Gardens Victoria',
address: 'Birdwood Avenue, South Yarra VIC 3141',
image: 'https://images.unsplash.com/photo-1585320806297-9794b3e4eeae?w=800&h=600&fit=crop',
categories: ['Nature', 'Culture'],
description: [
'Stroll through stunning landscaped gardens',
'Visit the Australian Forest Walk',
'Relax by the Ornamental Lake',
'Join a free guided walking tour'
]
},
{
time: '4:00 pm',
activity: 'National Gallery of Victoria',
location: 'NGV International',
address: '180 St Kilda Road, Melbourne VIC 3006',
image: 'https://images.unsplash.com/photo-1564399577149-749794d74eee?w=800&h=600&fit=crop',
categories: ['Culture', 'Art'],
description: [
'Explore Australia\'s oldest art museum',
'View international and Australian art collections',
'Visit the stunning water wall entrance',
'Browse the NGV design store'
]
},
{
time: '7:00 pm',
activity: 'Dinner at Chin Chin',
location: 'Chin Chin Restaurant',
address: '125 Flinders Lane, Melbourne VIC 3000',
image: 'https://images.unsplash.com/photo-1552566626-52f8b828add9?w=800&h=600&fit=crop',
categories: ['Food', 'Drinks'],
description: [
'Experience modern Thai cuisine',
'Try signature dishes like the Betel Leaf',
'Enjoy the vibrant atmosphere',
'Book ahead or walk-in for bar seating'
]
}
]
},
{
day: 2,
title: "Markets & Neighborhoods",
activities: [
{
time: '8:00 am',
activity: 'Queen Victoria Market',
location: 'Queen Victoria Market',
address: 'Queen Street, Melbourne VIC 3000',
image: 'https://images.unsplash.com/photo-1555939594-58d7cb561ad1?w=800&h=600&fit=crop',
categories: ['Food', 'Shopping', 'Culture'],
description: [
'Explore Melbourne\'s historic market (since 1878)',
'Sample fresh local produce',
'Shop for artisan goods and souvenirs',
'Grab breakfast at the Deli Hall'
]
},
{
time: '10:30 am',
activity: 'Fitzroy Street Art Tour',
location: 'Fitzroy Arts Precinct',
address: 'Gertrude Street, Fitzroy VIC 3065',
image: 'https://images.unsplash.com/photo-1499781350541-7783f6c6a0c8?w=800&h=600&fit=crop',
categories: ['Culture', 'Art'],
description: [
'Walk through famous street art laneways',
'Discover works by renowned artists',
'Visit independent galleries',
'Explore vintage and record stores'
]
},
{
time: '12:30 pm',
activity: 'Brunswick Street Lunch',
location: 'Brunswick Street Precinct',
address: 'Brunswick Street, Fitzroy VIC 3065',
image: 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&h=600&fit=crop',
categories: ['Food', 'Drinks'],
description: [
'Choose from diverse dining options',
'Try local cafes and restaurants',
'Explore bookshops and boutiques',
'Enjoy the vibrant neighborhood atmosphere'
]
},
{
time: '2:30 pm',
activity: 'Carlton Gardens',
location: 'Carlton Gardens',
address: 'Carlton Gardens, Carlton VIC 3053',
image: 'https://images.unsplash.com/photo-1519331379826-f10be5486c6f?w=800&h=600&fit=crop',
categories: ['Nature', 'Culture', 'Landmark'],
description: [
'Visit the UNESCO World Heritage site',
'See the Royal Exhibition Building',
'Stroll through Victorian-era gardens',
'Relax by the ornamental fountains'
]
},
{
time: '4:00 pm',
activity: 'Melbourne Museum',
location: 'Melbourne Museum',
address: '11 Nicholson Street, Carlton VIC 3053',
image: 'https://images.unsplash.com/photo-1566127992631-137a642a90f4?w=800&h=600&fit=crop',
categories: ['Culture', 'Museum'],
description: [
'Explore natural and cultural history',
'Visit the Bunjilaka Aboriginal Centre',
'See the Forest Gallery living ecosystem',
'Discover Melbourne\'s story exhibition'
]
},
{
time: '7:00 pm',
activity: 'Rooftop Bar Experience',
location: 'Naked for Satan',
address: '285 Brunswick Street, Fitzroy VIC 3065',
image: 'https://images.unsplash.com/photo-1514933651103-005eec06c04b?w=800&h=600&fit=crop',
categories: ['Drinks', 'Food'],
description: [
'Enjoy sunset views from the rooftop',
'Try Spanish-style pintxos',
'Sample craft cocktails and local beers',
'Experience Melbourne\'s bar culture'
]
}
]
},
{
day: 3,
title: "Coastal Adventure",
activities: [
{
time: '8:00 am',
activity: 'St. Kilda Beach',
location: 'St. Kilda Beach',
address: 'Jacka Boulevard, St Kilda VIC 3182',
image: 'https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800&h=600&fit=crop',
categories: ['Nature', 'Beach'],
description: [
'Morning walk along the iconic beach',
'Visit the historic St Kilda Pier',
'See the little penguins at sunset',
'Explore the Sunday Esplanade Market (weekends)'
]
},
{
time: '10:00 am',
activity: 'Acland Street Cafes',
location: 'Acland Street',
address: 'Acland Street, St Kilda VIC 3182',
image: 'https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=800&h=600&fit=crop',
categories: ['Food', 'Drinks'],
description: [
'Brunch at famous cake shops',
'Try traditional European pastries',
'Visit Lentil as Anything (pay-as-you-feel)',
'Browse vintage shops and bookstores'
]
},
{
time: '12:00 pm',
activity: 'Luna Park Melbourne',
location: 'Luna Park Melbourne',
address: '18 Lower Esplanade, St Kilda VIC 3182',
image: 'https://images.unsplash.com/photo-1513026705753-bc3fffca8bf4?w=800&h=600&fit=crop',
categories: ['Entertainment', 'Landmark'],
description: [
'Visit Melbourne\'s iconic amusement park',
'Ride the historic Scenic Railway (1912)',
'Take photos at Mr Moon entrance',
'Enjoy carnival games and rides'
]
},
{
time: '2:00 pm',
activity: 'Brighton Beach Boxes',
location: 'Brighton Beach',
address: 'Esplanade, Brighton VIC 3186',
image: 'https://images.unsplash.com/photo-1520208422220-d12a3c588e6c?w=800&h=600&fit=crop',
categories: ['Culture', 'Landmark'],
description: [
'Photograph the famous colorful bathing boxes',
'Walk along the pristine beach',
'Learn about the heritage structures',
'Relax in the beachside atmosphere'
]
},
{
time: '4:00 pm',
activity: 'Southbank Promenade',
location: 'Southbank',
address: 'Southbank Promenade, Southbank VIC 3006',
image: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=800&h=600&fit=crop',
categories: ['Culture', 'Shopping'],
description: [
'Stroll along the Yarra River',
'Visit arts and craft markets',
'Explore restaurants and cafes',
'Enjoy river views and street performers'
]
},
{
time: '7:00 pm',
activity: 'Farewell Dinner at Vue de Monde',
location: 'Vue de Monde',
address: 'Level 55, Rialto, 525 Collins Street, Melbourne VIC 3000',
image: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=800&h=600&fit=crop',
categories: ['Food', 'Drinks', 'Luxury'],
description: [
'Experience fine dining at 55th floor',
'Enjoy panoramic Melbourne views',
'Taste modern Australian cuisine',
'Celebrate the end of your journey'
]
}
]
}
]
};
// Download logic using standard query with manual trigger
const [shouldDownload, setShouldDownload] = useState(false);
const { data: pdfBlob, isFetching: isDownloading, refetch } = useDownloadItineraryQuery(itineraryId!, {
skip: !shouldDownload || !itineraryId,
});
useEffect(() => {
if (shouldDownload && pdfBlob) {
// Create download link
const url = window.URL.createObjectURL(pdfBlob);
const link = document.createElement('a');
link.href = url;
link.download = `itinerary-${itineraryId}.pdf`;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
toast.success('Itinerary downloaded successfully!');
setShouldDownload(false); // reset trigger
}
}, [pdfBlob, shouldDownload, itineraryId]);
const handleDownloadItinerary = useCallback(() => {
if (!itineraryId) {
toast.error('Itinerary ID not found');
return;
}
setShouldDownload(true);
refetch(); // manually trigger the download query
}, [itineraryId, refetch]);
const generatedItinerary = itineraryDetails ?? null;
const days = generatedItinerary?.days ?? [];
const summaries = generatedItinerary?.summary ?? [];
// ── Loading State ─────────────────────────────────────────────────────────────
if (isLoading) {
return <LoadingSpinner />;
}
return (
<div className="min-h-screen bg-background">
@@ -401,7 +137,7 @@ export function ItineraryViewPage({
/>
{/* Header Section */}
<section className="pt-32 pb-8 bg-gradient-to-br from-primary/5 to-secondary/5">
<section className="pb-8 bg-gradient-to-br from-primary/5 to-secondary/5">
<div className="container mx-auto px-4 pt-32">
<motion.div
initial={{ opacity: 0, y: 30 }}
@@ -411,8 +147,8 @@ export function ItineraryViewPage({
>
<Button
variant="ghost"
onClick={onBackClick}
className="mb-6 hover:bg-primary/5 font-poppins font-medium"
onClick={() => navigate(-1)}
className="mb-6 hover:bg-primary/5 font-poppins font-medium cursor-pointer"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Magic Itinerary
@@ -422,12 +158,14 @@ export function ItineraryViewPage({
<Star className="w-6 h-6 fill-current" />
<h1 className="font-merchant text-4xl md:text-5xl lg:text-6xl leading-tight">
<span className="font-light">Your</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">Magic Itinerary</span>
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pr-3">
{generatedItinerary?.title ?? 'Magic Itinerary'}
</span>
</h1>
<Star className="w-6 h-6 fill-current" />
</div>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-2xl mx-auto">
Here's your personalized {generatedItinerary.totalDays}-day adventure in {generatedItinerary.destination.name}!
Here's your personalized {generatedItinerary?.totalDays}-day adventure in {generatedItinerary?.city}!
</p>
</motion.div>
</div>
@@ -437,6 +175,7 @@ export function ItineraryViewPage({
<section className="py-8">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto space-y-8">
{/* View Toggle */}
<div className="flex justify-center">
<div className="bg-muted p-1 rounded-lg">
@@ -459,31 +198,45 @@ export function ItineraryViewPage({
</div>
</div>
{/* Itinerary Overview */}
{/* Itinerary Overview Card */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<Card className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center">
<div className="font-merchant text-3xl text-primary mb-2">{generatedItinerary.totalDays}</div>
<div className="font-poppins text-sm text-muted-foreground font-normal">Days</div>
<Card className="overflow-hidden">
<div className="relative h-40 md:h-48">
<ImageWithFallback
src={generatedItinerary?.cityBanner}
alt={generatedItinerary?.city}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent" />
<div className="absolute bottom-4 left-6">
<p className="font-poppins text-xs font-medium text-white/70 uppercase tracking-wider mb-1">Your Trip</p>
<h3 className="font-merchant text-2xl md:text-3xl text-white font-semibold">{generatedItinerary?.city}</h3>
</div>
<div className="text-center">
<div className="font-merchant text-3xl text-primary mb-2">{generatedItinerary.includedActivities}</div>
<div className="font-poppins text-sm text-muted-foreground font-normal">Activities</div>
</div>
{/* Stats Row */}
<div className="grid grid-cols-3 divide-x divide-gray-100 bg-white">
<div className="flex flex-col items-center py-5">
<span className="font-merchant text-3xl text-primary">{generatedItinerary?.totalDays}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-1">Days</span>
</div>
<div className="text-center">
<div className="font-merchant text-3xl text-primary mb-2">{generatedItinerary.estimatedCost}</div>
<div className="font-poppins text-sm text-muted-foreground font-normal">Estimated Cost</div>
<div className="flex flex-col items-center py-5">
<span className="font-merchant text-3xl text-primary">{generatedItinerary?.totalStops}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-1">Stops</span>
</div>
<div className="flex flex-col items-center py-5">
<span className="font-merchant text-3xl text-primary">{days[0]?.date}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-1">Start Date</span>
</div>
</div>
</Card>
</motion.div>
{/* Daily Plans - Enhanced View */}
{/* ── Daily View ──────────────────────────────────────────────────── */}
{viewMode === 'daily' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -491,29 +244,24 @@ export function ItineraryViewPage({
transition={{ duration: 0.6, delay: 0.3 }}
className="space-y-12"
>
{generatedItinerary.dailyPlans.map((day, dayIndex) => (
{days.map((day: any, dayIndex: number) => (
<div key={dayIndex} className="space-y-6">
{/* Location Header with Weather - Only show for first day or when location changes */}
{(dayIndex === 0 ||
(dayIndex > 0 &&
generatedItinerary.dailyPlans[dayIndex - 1] &&
(generatedItinerary.dailyPlans[dayIndex - 1] as any).destination?.name !== generatedItinerary.destination.name)) && (
{/* City / Weather header — only on first day */}
{dayIndex === 0 && (
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.4 + dayIndex * 0.1 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="bg-gray-50 rounded-2xl p-6 shadow-sm border border-gray-200"
>
<div className="flex items-center justify-between">
<div>
<h2 className="font-merchant text-3xl md:text-4xl leading-tight mb-2">
{generatedItinerary.destination.name}, {generatedItinerary.destination.country}
<h2 className="font-poppins text-3xl md:text-4xl leading-tight mb-2">
{generatedItinerary?.city}, Australia
</h2>
<p className="font-poppins text-base text-primary font-medium">{generatedItinerary.destination.weather}</p>
</div>
<div className="flex items-center gap-2">
<Sun className="w-10 h-10 text-amber-500" />
</div>
<Sun className="w-10 h-10 text-amber-500" />
</div>
</motion.div>
)}
@@ -526,122 +274,118 @@ export function ItineraryViewPage({
className="flex items-center gap-4 pl-2"
>
<div className="bg-gradient-to-br from-primary to-secondary text-white w-16 h-16 rounded-full flex items-center justify-center shadow-lg">
<span className="font-merchant text-2xl font-semibold">{day.day}</span>
<span className="font-merchant text-2xl font-semibold">{day.dayNumber}</span>
</div>
<div>
<h3 className="font-merchant text-2xl md:text-3xl leading-snug font-semibold">Day {day.day}</h3>
<h3 className="font-merchant text-2xl md:text-3xl leading-snug font-semibold">
Day {day.dayNumber}
</h3>
<p className="font-poppins text-base text-muted-foreground font-normal">{day.title}</p>
</div>
</motion.div>
{/* GMT Label */}
{/* Time-zone label */}
<div className="pl-2">
<p className="font-poppins text-sm text-gray-500 font-normal">GMT</p>
<p className="font-poppins text-sm text-gray-500 font-normal">
{day.date}
</p>
</div>
{/* Activity Cards - Desktop Grid Layout */}
{/* Activity Cards */}
<div className="space-y-8">
{day.activities.map((activity, actIndex) => {
const activityKey = `day${day.day}-act${actIndex}`;
const isFavorite = favorites.has(activityKey);
return (
<motion.div
key={actIndex}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.6 + dayIndex * 0.1 + actIndex * 0.05 }}
className="flex gap-6"
>
{/* Time Column */}
<div className="flex-shrink-0 w-24 pt-2">
<div className="font-poppins text-base font-medium text-gray-700">{activity.time}</div>
{day.items?.map((activity: any, actIndex: number) => (
<motion.div
key={actIndex}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.6 + dayIndex * 0.1 + actIndex * 0.05 }}
className="flex gap-6"
>
{/* Time Column */}
<div className="flex-shrink-0 w-24 pt-2">
<div className="font-poppins text-base font-medium text-gray-700">
{activity.timeSlot}
</div>
</div>
{/* Activity Card */}
<div className="flex-1">
<Card className="overflow-hidden hover:shadow-xl transition-shadow duration-300 border-2 border-gray-100">
<CardContent className="p-0">
{/* Hero Image with Overlay Buttons */}
<div className="relative h-64 md:h-72 bg-gray-200">
<ImageWithFallback
src={activity.image}
alt={activity.activity}
className="w-full h-full object-cover"
/>
{/* Favorite Heart Button - Top Right */}
<div className="absolute top-4 right-4">
<Button
size="icon"
{/* Activity Card */}
<div className="flex-1">
<Card className="overflow-hidden hover:shadow-xl transition-shadow duration-300 border-2 border-gray-100">
<CardContent className="p-0">
{/* Hero Image */}
<div className="relative h-64 md:h-72 bg-gray-200">
<ImageWithFallback
src={activity.imageUrl}
alt={activity.title}
className="w-full h-full object-cover"
/>
{/* Get Directions */}
<div className="absolute bottom-4 left-4">
<Button
className="bg-primary hover:bg-primary/90 text-white font-poppins font-semibold shadow-lg px-6 py-3 rounded-xl"
onClick={() =>
window.open(
`https://www.google.com/maps?q=${activity.latitude},${activity.longitude}`,
'_blank'
)
}
>
<Navigation className="w-4 h-4 mr-2" />
Get Directions
</Button>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-4">
<div className="space-y-2">
<h4 className="font-merchant text-xl md:text-2xl leading-snug font-semibold text-gray-900">
{activity.title}
</h4>
<div className="flex items-start gap-2 text-gray-600">
<MapPin className="w-4 h-4 mt-1 flex-shrink-0 text-primary" />
<span className="font-poppins text-sm font-normal leading-relaxed">
{activity.locationName}
</span>
</div>
</div>
{/* Category Badges */}
<div className="flex flex-wrap gap-2">
{activity.categories?.map((cat: string, ci: number) => (
<Badge
key={ci}
variant="secondary"
className="bg-white/95 hover:bg-white shadow-lg backdrop-blur-sm rounded-full w-12 h-12"
onClick={() => toggleFavorite(activityKey)}
className="font-poppins font-normal text-sm bg-primary/10 text-primary hover:bg-primary/20 px-3 py-1"
>
<Heart className={`w-5 h-5 ${isFavorite ? 'fill-primary text-primary' : 'text-gray-700'}`} />
</Button>
</div>
{/* Get Directions Button - Bottom Left */}
<div className="absolute bottom-4 left-4">
<Button
className="bg-primary hover:bg-primary/90 text-white font-poppins font-semibold shadow-lg px-6 py-3 rounded-xl"
>
<Navigation className="w-4 h-4 mr-2" />
Get Directions
</Button>
</div>
{cat}
</Badge>
))}
</div>
{/* Content Section */}
<div className="p-6 space-y-4">
{/* Location Name & Address */}
<div className="space-y-2">
<h4 className="font-merchant text-xl md:text-2xl leading-snug font-semibold text-gray-900">
{activity.activity}
</h4>
<div className="flex items-start gap-2 text-gray-600">
<MapPin className="w-4 h-4 mt-1 flex-shrink-0 text-primary" />
<span className="font-poppins text-sm font-normal leading-relaxed">{activity.address}</span>
</div>
</div>
{/* Category Badges */}
<div className="flex flex-wrap gap-2">
{activity.categories.map((category, catIndex) => (
<Badge
key={catIndex}
variant="secondary"
className="font-poppins font-normal text-sm bg-primary/10 text-primary hover:bg-primary/20 px-3 py-1"
>
{category}
</Badge>
))}
</div>
{/* Activity Details - Bullet Points */}
<div className="space-y-2 pt-2">
{activity.description.map((detail, detailIndex) => (
<div key={detailIndex} className="flex items-start gap-3">
<span className="text-primary font-semibold mt-1.5 flex-shrink-0">•</span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">{detail}</span>
</div>
))}
{/* Description */}
<div className="space-y-2 pt-2">
<div className="flex items-center gap-3">
<span className="text-primary font-semibold mt-1 flex-shrink-0">•</span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">
{activity.description}
</span>
</div>
</div>
</CardContent>
</Card>
</div>
</motion.div>
);
})}
</div>
</CardContent>
</Card>
</div>
</motion.div>
))}
</div>
</div>
))}
</motion.div>
)}
{/* Summary View */}
{/* ── Summary View ─────────────────────────────────────────────────── */}
{viewMode === 'summary' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -649,32 +393,55 @@ export function ItineraryViewPage({
transition={{ duration: 0.6, delay: 0.3 }}
className="space-y-4"
>
<h3 className="font-merchant text-2xl md:text-3xl text-center mb-8 leading-tight font-semibold">Trip Summary</h3>
<h3 className="font-merchant text-2xl md:text-3xl text-center mb-8 leading-tight font-semibold">
Trip Summary
</h3>
<Card className="p-6">
<div className="space-y-6">
{generatedItinerary.dailyPlans.map((day, index) => (
<div key={index} className="border-l-4 border-primary pl-6">
<div className="flex items-center gap-2 mb-3">
<Calendar className="w-5 h-5 text-primary" />
<h4 className="font-merchant text-lg md:text-xl leading-snug font-semibold">Day {day.day}: {day.title}</h4>
</div>
<div className="space-y-2">
{day.activities.map((activity, actIndex) => (
<div key={actIndex} className="flex items-start gap-3 text-sm">
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<span className="font-poppins text-gray-700 font-medium">{activity.activity}</span>
<div className="flex items-center gap-2 mt-1">
<span className="font-poppins text-gray-500 text-xs font-normal">{activity.time}</span>
<span className="text-gray-400"></span>
<span className="font-poppins text-gray-500 text-xs font-normal">{activity.location}</span>
{days.map((day: any, dayIndex: number) => {
const daySummary = summaries.find((s: any) => s.dayNumber === day.dayNumber);
const dayDate = days[0]?.date
? new Date(
new Date(days[0].date).setDate(
new Date(days[0].date).getDate() + dayIndex
)
).toLocaleDateString('en-AU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
: '';
return (
<div key={dayIndex} className="border-l-4 border-primary pl-6">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Calendar className="w-5 h-5 text-primary" />
<h4 className="font-merchant text-lg md:text-xl leading-snug font-semibold">
Day {day.dayNumber}: {daySummary?.title ?? day.title}
</h4>
</div>
<span className="font-poppins text-sm text-primary font-medium">{dayDate}</span>
</div>
<div className="space-y-2">
{daySummary?.items?.map((item: any, actIndex: number) => (
<div key={actIndex} className="flex items-start gap-3 text-sm">
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<span className="font-poppins text-gray-700 font-medium">{item.title}</span>
<div className="flex items-center gap-2 mt-1">
<span className="font-poppins text-gray-500 text-xs font-normal">{item.timeSlot}</span>
<span className="text-gray-400"></span>
<span className="font-poppins text-gray-500 text-xs font-normal">{item.locationName}</span>
</div>
</div>
</div>
</div>
))}
))}
</div>
</div>
</div>
))}
);
})}
</div>
</Card>
</motion.div>
@@ -689,22 +456,27 @@ export function ItineraryViewPage({
>
<Button
variant="outline"
onClick={onCreateItineraryClick}
onClick={() => navigate(`/create-itinerary`)}
className="font-poppins font-medium px-8 py-3 text-lg"
>
<Heart className="w-5 h-5 mr-2" />
Create Another
</Button>
<Button
onClick={handleDownloadItinerary}
disabled={isDownloading}
className="bg-primary hover:bg-primary/90 font-poppins font-semibold px-8 py-3 text-lg"
>
<Download className="w-5 h-5 mr-2" />
{isDownloading ? (
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
) : (
<Download className="w-5 h-5 mr-2" />
)}
Save Itinerary
</Button>
<Button
variant="outline"
className="font-poppins font-medium px-8 py-3 text-lg"
>
<Button variant="outline" className="font-poppins font-medium px-8 py-3 text-lg">
<Share2 className="w-5 h-5 mr-2" />
Share Trip
</Button>
@@ -714,7 +486,7 @@ export function ItineraryViewPage({
</section>
{/* Footer */}
<Footer
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}

View File

@@ -1,501 +0,0 @@
import React, { useState } from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Calendar, Clock, MapPin, Users, Star, Heart, Share2, Download, CheckCircle, Navigation, Cloud, Sun } from 'lucide-react';
import { Button } from '../components/ui/button';
import { Card, CardContent } from '../components/ui/card';
import { Badge } from '../components/ui/badge';
import Navbar from '../components/Navbar';
import { Footer } from '../components/Footer';
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
import { useGetItineraryDetailsByIdQuery } from '../Redux/services/itinerary.service';
import { useParams } from 'react-router-dom';
import LoadingSpinner from '../components/LoadingSpinner';
interface ItineraryViewPageDesignProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onCreateItineraryClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user?: { email: string; name: string; } | null;
}
export function ItineraryViewPageDesign({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onCreateItineraryClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: ItineraryViewPageDesignProps) {
const [viewMode, setViewMode] = useState<'daily' | 'summary'>('daily');
// const [favorites, setFavorites] = useState<Set<string>>(new Set());
// ── API Integration ──────────────────────────────────────────────────────────
const { itineraryId } = useParams();
const { data: itineraryDetails, isLoading } = useGetItineraryDetailsByIdQuery(itineraryId);
const generatedItinerary = itineraryDetails ?? null;
const days = generatedItinerary?.days ?? [];
const summaries = generatedItinerary?.summary ?? [];
// ─────────────────────────────────────────────────────────────────────────────
// const toggleFavorite = (activityKey: string) => {
// setFavorites(prev => {
// const newSet = new Set(prev);
// if (newSet.has(activityKey)) {
// newSet.delete(activityKey);
// } else {
// newSet.add(activityKey);
// }
// return newSet;
// });
// };
// ── Loading State ─────────────────────────────────────────────────────────────
if (isLoading) {
return (
<LoadingSpinner/>
);
}
// ─────────────────────────────────────────────────────────────────────────────
return (
<div className="min-h-screen bg-background">
{/* Navbar */}
<Navbar
activeCity=""
onCityChange={() => {}}
onSignInClick={onSignInClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
currentPage="itinerary-view"
isUserSignedIn={!!user}
user={user}
/>
{/* Header Section */}
<section className="pt-32 pb-8 bg-gradient-to-br from-primary/5 to-secondary/5">
<div className="container mx-auto px-4 pt-32">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center"
>
<Button
variant="ghost"
onClick={onBackClick}
className="mb-6 hover:bg-primary/5 font-poppins font-medium"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Magic Itinerary
</Button>
<div className="flex items-center justify-center gap-2 text-primary mb-4">
<Star className="w-6 h-6 fill-current" />
<h1 className="font-merchant text-4xl md:text-5xl lg:text-6xl leading-tight">
<span className="font-light">Your</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pr-3">
{generatedItinerary?.title ?? 'Magic Itinerary'}
</span>
</h1>
<Star className="w-6 h-6 fill-current" />
</div>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-2xl mx-auto">
Here's your personalized {generatedItinerary?.totalDays}-day adventure in {generatedItinerary?.city}!
</p>
</motion.div>
</div>
</section>
{/* Main Content */}
<section className="py-8">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto space-y-8">
{/* View Toggle */}
<div className="flex justify-center">
<div className="bg-muted p-1 rounded-lg">
<Button
variant={viewMode === 'daily' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('daily')}
className="rounded-md font-poppins font-medium"
>
Daily View
</Button>
<Button
variant={viewMode === 'summary' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('summary')}
className="rounded-md font-poppins font-medium"
>
Summary
</Button>
</div>
</div>
{/* Itinerary Overview Card */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{/* Banner Image */}
<Card className="overflow-hidden">
<div className="relative h-40 md:h-48">
<ImageWithFallback
src={generatedItinerary?.cityBanner}
alt={generatedItinerary?.city}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent" />
<div className="absolute bottom-4 left-6">
<p className="font-poppins text-xs font-medium text-white/70 uppercase tracking-wider mb-1">Your Trip</p>
<h3 className="font-merchant text-2xl md:text-3xl text-white font-semibold">{generatedItinerary?.city}</h3>
</div>
</div>
{/* Stats Row */}
<div className="grid grid-cols-3 divide-x divide-gray-100 bg-white">
<div className="flex flex-col items-center py-5">
<span className="font-merchant text-3xl text-primary">{generatedItinerary?.totalDays}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-1">Days</span>
</div>
<div className="flex flex-col items-center py-5">
<span className="font-merchant text-3xl text-primary">{generatedItinerary?.totalStops}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-1">Stops</span>
</div>
<div className="flex flex-col items-center py-5">
<span className="font-merchant text-3xl text-primary">{days[0]?.date}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-1">Start Date</span>
</div>
</div>
</Card>
</motion.div>
{/* ── Daily View ──────────────────────────────────────────────────── */}
{viewMode === 'daily' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="space-y-12"
>
{days.map((day: any, dayIndex: number) => (
<div key={dayIndex} className="space-y-6">
{/* City / Weather header — only on first day */}
{dayIndex === 0 && (
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="bg-gray-50 rounded-2xl p-6 shadow-sm border border-gray-200"
>
<div className="flex items-center justify-between">
<div>
<h2 className="font-poppins text-3xl md:text-4xl leading-tight mb-2">
{generatedItinerary?.city}, Australia
</h2>
</div>
<Sun className="w-10 h-10 text-amber-500" />
</div>
</motion.div>
)}
{/* Day Header */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.5 + dayIndex * 0.1 }}
className="flex items-center gap-4 pl-2"
>
<div className="bg-gradient-to-br from-primary to-secondary text-white w-16 h-16 rounded-full flex items-center justify-center shadow-lg">
<span className="font-merchant text-2xl font-semibold">{day.dayNumber}</span>
</div>
<div>
<h3 className="font-merchant text-2xl md:text-3xl leading-snug font-semibold">
Day {day.dayNumber}
</h3>
<p className="font-poppins text-base text-muted-foreground font-normal">{day.title}</p>
</div>
</motion.div>
{/* Time-zone label */}
<div className="pl-2">
<p className="font-poppins text-sm text-gray-500 font-normal">
{day.date}
</p>
</div>
{/* Activity Cards */}
<div className="space-y-8">
{day.items?.map((activity: any, actIndex: number) => {
const activityKey = `day${day.dayNumber}-act${actIndex}`;
// const isFavorite = favorites.has(activityKey);
return (
<motion.div
key={actIndex}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.6 + dayIndex * 0.1 + actIndex * 0.05 }}
className="flex gap-6"
>
{/* Time Column */}
<div className="flex-shrink-0 w-24 pt-2">
<div className="font-poppins text-base font-medium text-gray-700">
{activity.timeSlot}
</div>
</div>
{/* Activity Card */}
<div className="flex-1">
<Card className="overflow-hidden hover:shadow-xl transition-shadow duration-300 border-2 border-gray-100">
<CardContent className="p-0">
{/* Hero Image */}
<div className="relative h-64 md:h-72 bg-gray-200">
<ImageWithFallback
src={activity.imageUrl}
alt={activity.title}
className="w-full h-full object-cover"
/>
{/* Favourite Button */}
{/* <div className="absolute top-4 right-4">
<Button
size="icon"
variant="secondary"
className="bg-white/95 hover:bg-white shadow-lg backdrop-blur-sm rounded-full w-12 h-12"
onClick={() => toggleFavorite(activityKey)}
>
<Heart className={`w-5 h-5 ${isFavorite ? 'fill-primary text-primary' : 'text-gray-700'}`} />
</Button>
</div> */}
{/* Get Directions — links to Google Maps via lat/lng */}
<div className="absolute bottom-4 left-4">
<Button
className="bg-primary hover:bg-primary/90 text-white font-poppins font-semibold shadow-lg px-6 py-3 rounded-xl"
onClick={() =>
window.open(
`https://www.google.com/maps?q=${activity.latitude},${activity.longitude}`,
'_blank'
)
}
>
<Navigation className="w-4 h-4 mr-2" />
Get Directions
</Button>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-4">
<div className="space-y-2">
<h4 className="font-merchant text-xl md:text-2xl leading-snug font-semibold text-gray-900">
{activity.title}
</h4>
<div className="flex items-start gap-2 text-gray-600">
<MapPin className="w-4 h-4 mt-1 flex-shrink-0 text-primary" />
<span className="font-poppins text-sm font-normal leading-relaxed">
{activity.locationName}
</span>
</div>
</div>
{/* Category Badges */}
<div className="flex flex-wrap gap-2">
{activity.categories?.map((cat: string, ci: number) => (
<Badge
key={ci}
variant="secondary"
className="font-poppins font-normal text-sm bg-primary/10 text-primary hover:bg-primary/20 px-3 py-1"
>
{cat}
</Badge>
))}
</div>
{/* Description */}
<div className="space-y-2 pt-2">
<div className="flex items-center gap-3">
<span className="text-primary font-semibold mt-1 flex-shrink-0">•</span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">
{activity.description}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</motion.div>
);
})}
</div>
</div>
))}
</motion.div>
)}
{/* ── Summary View ─────────────────────────────────────────────────── */}
{viewMode === 'summary' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="space-y-4"
>
<h3 className="font-merchant text-2xl md:text-3xl text-center mb-8 leading-tight font-semibold">
Trip Summary
</h3>
<Card className="p-6">
<div className="space-y-6">
{days.map((day: any, dayIndex: number) => {
// ✅ Match summary to the correct day by dayNumber
const daySummary = summaries.find((s: any) => s.dayNumber === day.dayNumber);
const dayDate = days[0]?.date
? new Date(
new Date(days[0].date).setDate(
new Date(days[0].date).getDate() + dayIndex
)
).toLocaleDateString('en-AU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
: '';
return (
<div key={dayIndex} className="border-l-4 border-primary pl-6">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Calendar className="w-5 h-5 text-primary" />
<h4 className="font-merchant text-lg md:text-xl leading-snug font-semibold">
Day {day.dayNumber}: {daySummary?.title ?? day.title}
</h4>
</div>
<span className="font-poppins text-sm text-primary font-medium">{dayDate}</span>
</div>
<div className="space-y-2">
{daySummary?.items?.map((item: any, actIndex: number) => (
<div key={actIndex} className="flex items-start gap-3 text-sm">
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<span className="font-poppins text-gray-700 font-medium">{item.title}</span>
<div className="flex items-center gap-2 mt-1">
<span className="font-poppins text-gray-500 text-xs font-normal">{item.timeSlot}</span>
<span className="text-gray-400"></span>
<span className="font-poppins text-gray-500 text-xs font-normal">{item.locationName}</span>
</div>
</div>
</div>
))}
</div>
</div>
);
})}
</div>
</Card>
</motion.div>
)}
{/* Action Buttons */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="flex flex-col sm:flex-row gap-4 justify-center pt-8"
>
<Button
variant="outline"
onClick={onCreateItineraryClick}
className="font-poppins font-medium px-8 py-3 text-lg"
>
<Heart className="w-5 h-5 mr-2" />
Create Another
</Button>
<Button className="bg-primary hover:bg-primary/90 font-poppins font-semibold px-8 py-3 text-lg">
<Download className="w-5 h-5 mr-2" />
Save Itinerary
</Button>
<Button variant="outline" className="font-poppins font-medium px-8 py-3 text-lg">
<Share2 className="w-5 h-5 mr-2" />
Share Trip
</Button>
</motion.div>
</div>
</div>
</section>
{/* Footer */}
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -75,6 +75,7 @@ export function MagicItineraryPage({
currentPage,
user
}: MagicItineraryPageProps) {
const cityName = localStorage.getItem("cityName") || "your city";
return (
<Layout activeCity="Landingpage" onSignInClick={onSignInClick} onSignOutClick={onSignOutClick} user={user}>
@@ -190,7 +191,7 @@ export function MagicItineraryPage({
</h3>
</div>
<p className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">
A perfectly planned Melbourne adventure
A perfectly planned {cityName} adventure
</p>
</div>

View File

@@ -16,6 +16,7 @@ import { HeroBannerCarousel } from '../components/HeroBannerCarousel';
import { HotelEsimOffers } from '../components/HotelEsimOffers';
import { useGetSelectedCityDetailsQuery } from '../Redux/services/cities.service';
import LoadingSpinner from '../components/LoadingSpinner';
import { useNavigate } from 'react-router-dom';
interface User {
email: string;
@@ -147,8 +148,10 @@ export function MelbournePage({
// Magic Itinerary state
const [currentCardIndex, setCurrentCardIndex] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
const navigate= useNavigate();
const cityId = localStorage.getItem("cityId")
const cityName = localStorage.getItem("cityName")
const { data: cityDetails, isLoading: loadingCityDetails } = useGetSelectedCityDetailsQuery(cityId)
@@ -170,7 +173,7 @@ export function MelbournePage({
<div className="min-h-screen bg-white">
{/* Navigation */}
<Layout
activeCity="Melbourne"
// activeCity="Melbourne"
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
user={user}
@@ -219,7 +222,7 @@ export function MelbournePage({
</div> */}
<div className="container mx-auto px-4 py-12 space-y-24">
<div className="container mx-auto px-4 py-12">
{/* Features Grid */}
<motion.section
id="overview"
@@ -231,7 +234,7 @@ export function MelbournePage({
{[
{
title: "50+ Top Attractions",
description: "Unlimited access to Melbourne's finest museums, zoos, and observation decks.",
description: `Unlimited access to ${cityName}'s finest museums, zoos, and observation decks.`,
icon: MapPin,
color: "text-blue-500",
bg: "bg-blue-50"
@@ -389,7 +392,7 @@ export function MelbournePage({
>
<Wand2 className="w-6 h-6 text-primary drop-shadow-lg" />
</motion.div>
<span className="font-poppins font-semibold text-gray-800">AI-Powered Magic Itinerary</span>
<span className="font-poppins font-semibold text-gray-800">Magic Itinerary</span>
<motion.div
className="w-2 h-2 bg-primary rounded-full"
animate={{
@@ -779,7 +782,7 @@ export function MelbournePage({
viewport={{ once: true }}
>
<Button
onClick={onCreateItineraryClick}
onClick={() => {navigate('/create-itinerary');}}
className="font-poppins py-7 px-16 rounded-full text-xl font-bold bg-gradient-to-r from-primary via-orange-500 to-rose-500 hover:from-primary/90 hover:via-orange-500/90 hover:to-rose-500/90 shadow-2xl hover:shadow-primary/50 transition-all hover:scale-105 hover:-translate-y-1"
>
<span className="flex items-center gap-3">
@@ -788,18 +791,18 @@ export function MelbournePage({
</span>
</Button>
<p className="font-poppins text-gray-600 text-sm flex items-center gap-2">
{/* <p className="font-poppins text-gray-600 text-sm flex items-center gap-2">
<Sparkles className="w-4 h-4 text-primary" />
<span>Free to use • No credit card required</span>
<Sparkles className="w-4 h-4 text-primary" />
</p>
</p> */}
</motion.div>
</div>
</div>
</section>
</div>
<div className="container mx-auto px-4 py-12 space-y-24">
<div className="container mx-auto px-4 py-12">
{/* Testimonials */}
<EnhancedTestimonials />

View File

@@ -156,20 +156,19 @@ export function PassesPage({
const navigate = useNavigate()
const cityId = localStorage.getItem("cityId")
const cityName = localStorage.getItem("cityName")
const { data: cityDetails, isLoading: loadingCityDetails } = useGetSelectedCityDetailsQuery(cityId)
const cards = cityDetails?.city?.cards ?? []
console.log(cards)
if (loadingCityDetails) {
return (<LoadingSpinner />)
}
const handleCheckoutClick = () => {
const handleCheckoutClick = (cardTypeName:string) => {
console.log('Proceeding to checkout for user:', user);
// Add your checkout logic here
navigate('/checkout');
navigate('/checkout', { state: { selectedCard: cardTypeName } });
};
const handleSignInClick = () => {
@@ -227,10 +226,10 @@ export function PassesPage({
<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[0].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">
{cards[0].description}
{cards[0]?.description}
</CardDescription>
</CardHeader>
@@ -238,23 +237,23 @@ export function PassesPage({
<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[0].adultPrice}
${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">
{cards[0].adultPrice && (
{cards[0]?.adultPrice && (
<div className="text-sm text-gray-500 font-poppins">
{/* Strikethrough price = originalPrice + $5 */}
<span className="line-through mr-2">
${parseFloat(cards[0].adultPrice) + 5}
${parseFloat(cards[0]?.adultPrice) + 5}
</span>
<span className="text-green-600 font-medium">
Save{" "}
{Math.round(
((5) / (parseFloat(cards[0].adultPrice) + 5)) * 100
((5) / (parseFloat(cards[0]?.adultPrice) + 5)) * 100
)}
%
</span>
@@ -282,7 +281,7 @@ export function PassesPage({
? "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}
onClick={() => user ? handleCheckoutClick(cards[0]?.cardType?.cardTypeName) : handleSignInClick}
disabled={selectedPass !== passTypes[0].id}
>
@@ -299,14 +298,14 @@ export function PassesPage({
{/* 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
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)}
onClick={() => setSelectedPass(passTypes[1]?.id)}
>
{passTypes[1].popular && (
{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
@@ -320,30 +319,30 @@ export function PassesPage({
<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}
{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}
{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>
<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 && (
{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}
${parseFloat(cards[1]?.adultPrice) + 5}
</span>
<span className="text-green-600 font-medium">
Save{" "}
{Math.round(
((5) / (parseFloat(cards[1].adultPrice) + 5)) * 100
((5) / (parseFloat(cards[1]?.adultPrice) + 5)) * 100
)}
%
</span>
@@ -355,7 +354,7 @@ export function PassesPage({
<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) => (
{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>
@@ -372,7 +371,7 @@ export function PassesPage({
}`}
disabled={selectedPass !== passTypes[1].id}
onClick={user ? handleCheckoutClick : handleSignInClick}
onClick={() => user ? handleCheckoutClick(cards[1]?.cardType?.cardTypeName) : handleSignInClick}
>
{user ? 'PURCHASE NOW' : 'LOGIN TO BUY PASS'}
@@ -415,7 +414,7 @@ export function PassesPage({
<Clock className="w-7 h-7" strokeWidth={1.5} />
</div>
<div className="flex-1 w-full">
<h3 className="font-merchant text-2xl text-gray-900 mb-3">Calendar Days Policy</h3>
<h3 className="font-merchant text-xl text-gray-900 mb-3">Calendar Days Policy</h3>
<p className="font-poppins text-gray-600 leading-relaxed mb-6">
Unlimited passes work on a <span className="font-medium text-gray-900">consecutive calendar day basis</span>, not 24-hour periods. Your pass expires at 11:59 PM on your final day.
</p>
@@ -452,7 +451,7 @@ export function PassesPage({
<Shield className="w-7 h-7" strokeWidth={1.5} />
</div>
<div className="flex-1">
<h3 className="font-merchant text-2xl text-gray-900 mb-3">60-Minute Adventure Gap</h3>
<h3 className="font-merchant text-xl text-gray-900 mb-3">60-Minute Adventure Gap</h3>
<p className="font-poppins text-gray-600 leading-relaxed">
To keep the journey smooth for everyone, there's a simple <span className="font-medium text-gray-900">60-minute wait</span> between scanning your pass at attractions.
</p>
@@ -597,7 +596,7 @@ export function PassesPage({
</div>
<div>
<h3 className="text-white font-semibold text-lg">CityCards</h3>
<p className="text-white/70 text-sm">Melbourne Explorer</p>
<p className="text-white/70 text-sm">{cityName} Explorer</p>
</div>
</div>
</div>
@@ -770,7 +769,7 @@ export function PassesPage({
<h3 className="heading-dynamic text-4xl mb-4">
<span className="font-light">Ready to</span>{' '}
<span className="font-bold italic text-emphasis">explore</span>{' '}
<span className="font-semibold">Melbourne?</span>
<span className="font-semibold">{cityName}?</span>
</h3>
<p className="text-xl mb-8 max-w-2xl mx-auto opacity-90 font-light">
Choose your pass and start discovering amazing attractions with skip-the-line access.

View File

@@ -85,7 +85,7 @@ function Field({
prefilled,
disabled = false,
}: {
label: string;
label: React.ReactNode;
value: string;
onChange: (v: string) => void;
placeholder?: string;
@@ -123,7 +123,7 @@ function Field({
? 'border-[#F95F62] ring-2 ring-[#F95F62]/10'
: prefilled
? 'border-[#F95F62]/25 bg-[#F95F62]/[0.02]'
: 'border-gray-200'
: 'border-[#E4AFB1] bg-[#FFF5F5]'
}`}
/>
{prefilled && !focused && !disabled && (
@@ -232,30 +232,62 @@ export function PaymentDetailsPage({
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';
// First Name
if (!giftFirstName.trim()) e.giftFirstName = 'First name is required';
else if (/\s/.test(giftFirstName)) e.giftFirstName = 'First name must not contain spaces';
else if (!/^[A-Za-z]+$/.test(giftFirstName)) e.giftFirstName = 'First name must contain only letters (AZ)';
else if (giftFirstName.length < 2 || giftFirstName.length > 50) e.giftFirstName = 'First name must be between 2 and 50 characters';
// Last Name
if (!giftLastName.trim()) e.giftLastName = 'Last name is required';
else if (/\s/.test(giftLastName)) e.giftLastName = 'Last name must not contain spaces';
else if (!/^[A-Za-z]+$/.test(giftLastName)) e.giftLastName = 'Last name must contain only letters (AZ)';
else if (giftLastName.length < 2 || giftLastName.length > 50) e.giftLastName = 'Last name must be between 2 and 50 characters';
// ISD Code
if (!giftIsd.trim()) e.giftIsd = 'ISD code is required';
else if (/\s/.test(giftIsd)) e.giftIsd = 'ISD code must not contain spaces';
else if (!giftIsd.startsWith('+')) e.giftIsd = "ISD code must start with '+' (e.g. +91)";
else if (!/^\+\d+$/.test(giftIsd)) e.giftIsd = "ISD code must contain only digits after '+'";
// Email
if (!giftEmail.trim()) e.giftEmail = 'Email address is required';
else if (!/\S+@\S+\.\S+/.test(giftEmail)) e.giftEmail = 'Enter a valid email (e.g. name@example.com)';
// Phone
if (!giftPhone.trim()) e.giftPhone = 'Phone number is required';
else if (/\s/.test(giftPhone)) e.giftPhone = 'Phone number must not contain spaces';
else if (!/^\d+$/.test(giftPhone)) e.giftPhone = 'Phone number must contain only digits (09)';
else if (giftPhone.length < 7 || giftPhone.length > 15) e.giftPhone = 'Phone number must be between 7 and 15 digits';
// Message
if (!giftMessage.trim()) e.giftMessage = 'Message is required';
else if (giftMessage.length < 5) e.giftMessage = 'Message must be at least 5 characters long';
else if (giftMessage.length > 500) e.giftMessage = 'Message must not exceed 500 characters';
// City
if (!giftCity.trim()) e.giftCity = 'City is required';
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(giftCity)) e.giftCity = 'City can only contain letters and spaces';
else if (/\s{2,}/.test(giftCity)) e.giftCity = 'City must not contain multiple consecutive spaces';
else if (giftCity.length < 2 || giftCity.length > 50) e.giftCity = 'City must be between 2 and 50 characters';
// Country
if (!giftCountry.trim()) e.giftCountry = 'Country is required';
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(giftCountry)) e.giftCountry = 'Country can only contain letters and spaces';
else if (giftCountry.length < 2 || giftCountry.length > 50) e.giftCountry = 'Country must be between 2 and 50 characters';
}
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');
// toast.error('Please fill all required fields');
return;
}
@@ -381,8 +413,8 @@ export function PaymentDetailsPage({
<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'
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
}`}
>
<User className="w-4 h-4" />
@@ -391,8 +423,8 @@ export function PaymentDetailsPage({
<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'
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
}`}
>
<Gift className="w-4 h-4" />
@@ -448,70 +480,20 @@ export function PaymentDetailsPage({
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="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}
/>
<Field label={<>Recipient First Name <span className="text-red-500">*</span></>} value={giftFirstName} onChange={setGiftFirstName} placeholder="Enter recipient's first name" error={errors.giftFirstName} />
<Field label={<>Recipient Last Name <span className="text-red-500">*</span></>} value={giftLastName} onChange={setGiftLastName} placeholder="Enter recipient's last name" error={errors.giftLastName} />
<Field label={<>Recipient ISD Code <span className="text-red-500">*</span></>} value={giftIsd} onChange={setGiftIsd} placeholder="e.g., +61" error={errors.giftIsd} />
<Field label={<>Recipient Phone <span className="text-red-500">*</span></>} value={giftPhone} onChange={setGiftPhone} type="tel" placeholder="Enter recipient's phone number" error={errors.giftPhone} />
<Field label={<>Recipient Email <span className="text-red-500">*</span></>} value={giftEmail} onChange={setGiftEmail} type="email" placeholder="Enter recipient's email" error={errors.giftEmail} />
<Field label={<>Recipient City <span className="text-red-500">*</span></>} value={giftCity} onChange={setGiftCity} placeholder="Enter recipient's city" error={errors.giftCity} />
<Field label={<>Recipient Country <span className="text-red-500">*</span></>} value={giftCountry} onChange={setGiftCountry} placeholder="Enter recipient's country" error={errors.giftCountry} />
<Field label={<>Gift Message <span className="text-red-500">*</span></>} value={giftMessage} onChange={setGiftMessage} placeholder="Write a heartfelt message" error={errors.giftMessage} />
</div>
</div>
</motion.div>
@@ -557,8 +539,8 @@ export function PaymentDetailsPage({
<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]'
? '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>

View File

@@ -39,18 +39,22 @@ export function PaymentSuccessPage({
useEffect(() => {
const confirm = async () => {
// Try all possible sources
// 1. Retrieve bookingId from storage (cookie, localStorage, sessionStorage, or query param)
let bookingId = getCookie('pendingBookingId');
if (!bookingId) bookingId = localStorage.getItem('pendingBookingId');
if (!bookingId) bookingId = sessionStorage.getItem('pendingBookingId');
if (!bookingId) bookingId = searchParams.get('bookingId');
console.log('Retrieved bookingId from sources:', {
// 2. Get checkoutSessionId from URL query parameter
const checkoutSessionId = searchParams.get('session_id');
console.log('Retrieved data:', {
bookingId: bookingId,
checkoutSessionId: checkoutSessionId,
cookie: getCookie('pendingBookingId'),
localStorage: localStorage.getItem('pendingBookingId'),
sessionStorage: sessionStorage.getItem('pendingBookingId'),
queryParam: searchParams.get('bookingId'),
final: bookingId,
queryBookingId: searchParams.get('bookingId'),
});
if (!bookingId) {
@@ -62,10 +66,20 @@ export function PaymentSuccessPage({
return;
}
if (!checkoutSessionId) {
setStatus('error');
setErrorMsg(
'Missing session ID. Please contact support with your order details.'
);
return;
}
try {
await confirmPayment(bookingId).unwrap();
// Call API with both id and checkoutSessionId
await confirmPayment({ id: bookingId, checkoutSessionId }).unwrap();
setStatus('success');
toast.success('Payment confirmed! Your order is complete.');
// Clean up all storage
document.cookie = 'pendingBookingId=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
localStorage.removeItem('pendingBookingId');

View File

@@ -13,7 +13,8 @@ import {
Clock,
Star,
Badge as BadgeIcon,
Camera
Camera,
AlertCircle
} from 'lucide-react';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
@@ -21,7 +22,6 @@ import { Label } from '../components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
import { Separator } from '../components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../components/ui/select';
import { Badge } from '../components/ui/badge';
import Navbar from '../components/Navbar';
import { Footer } from '../components/Footer';
@@ -60,71 +60,6 @@ interface ProfilePageProps {
currentPage: string;
}
// Mock passes data
const mockPasses = [
{
id: '1',
name: 'Melbourne Unlimited Card',
city: 'Melbourne',
type: 'Unlimited Pass',
status: 'active',
price: 149.00,
originalPrice: 249.00,
discount: 40,
attractions: 25,
validFrom: '2024-01-15',
validUntil: '2024-01-22',
daysRemaining: 3,
image: 'https://images.unsplash.com/photo-1514395462725-fb4566210144?w=400',
usedAttractions: 8
},
{
id: '2',
name: 'Melbourne Selective Card',
city: 'Melbourne',
type: 'Flexi Pass',
status: 'active',
price: 89.00,
originalPrice: 149.00,
discount: 40,
attractions: 12,
validFrom: '2024-02-01',
validUntil: '2024-02-08',
daysRemaining: 12,
image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?w=400',
usedAttractions: 3
},
{
id: '3',
name: 'Sydney Explorer Pass',
city: 'Sydney',
type: 'Standard Pass',
status: 'expired',
price: 89.00,
originalPrice: 149.00,
discount: 40,
attractions: 15,
validFrom: '2023-12-01',
validUntil: '2023-12-08',
daysRemaining: 0,
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400',
usedAttractions: 12
}
];
// Mock itineraries data
const mockItineraries = [
{
id: '1',
name: 'Melbourne Unlimited Card',
city: 'Melbourne',
duration: '7 days',
attractions: 25,
createdDate: '2024-01-15',
status: 'active'
}
];
export function ProfilePage({
onBackClick,
onHomeClick,
@@ -153,6 +88,7 @@ export function ProfilePage({
currentPage
}: ProfilePageProps) {
const [activeTab, setActiveTab] = useState('profile');
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
@@ -168,10 +104,11 @@ export function ProfilePage({
const [sort, setSort] = useState("latest")
const navigate = useNavigate()
const userId = localStorage.getItem("userId")
const cityId = localStorage.getItem("cityId")
const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId)
const [updateUserProfileDetails, { isLoading: savingChanges }] = useUpdateUserProfileDetailsMutation();
const { data, isLoading: loadingCards } = useGetUserCardsQuery(sort)
const { data: userItineraries, isLoading: loadingItineraries } = useGetUserItinerariesQuery({})
const { data, isLoading: loadingCards } = useGetUserCardsQuery({ sort, cityId })
const { data: userItineraries, isLoading: loadingItineraries } = useGetUserItinerariesQuery(cityId)
const cards = data ?? []
const itineraries = userItineraries?.itineraries ?? []
@@ -193,19 +130,87 @@ export function ProfilePage({
}, [userDetails])
// const validateForm = () => {
// if (!formData.firstName.trim()) return toast.error('First name is required'), false;
// if (/\s/.test(formData.firstName)) return toast.error('First name must not contain spaces'), false;
// if (!/^[A-Za-z]+$/.test(formData.firstName)) return toast.error('First name must contain only letters'), false;
// if (!formData.lastName.trim()) return toast.error('Last name is required'), false;
// if (/\s/.test(formData.lastName)) return toast.error('Last name must not contain spaces'), false;
// if (!/^[A-Za-z]+$/.test(formData.lastName)) return toast.error('Last name must contain only letters'), false;
// if (!formData.phone.trim()) return toast.error('Mobile number is required'), false;
// if (/\s/.test(formData.phone)) return toast.error('Mobile number must not contain spaces'), false;
// if (!/^\d+$/.test(formData.phone)) return toast.error('Mobile number must contain only digits'), false;
// if (!formData.address1.trim()) return toast.error('Address is required'), false;
// if (!/^[A-Za-z0-9\s,\-.]+$/.test(formData.address1)) return toast.error('Address contains invalid characters'), false;
// if (!formData.city.trim()) return toast.error('City is required'), false;
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.city)) return toast.error('City can only contain letters and spaces'), false;
// if (/\s{2,}/.test(formData.city)) return toast.error('City must not contain multiple consecutive spaces'), false;
// if (!formData.country.trim()) return toast.error('Country is required'), false;
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.country)) return toast.error('Country can only contain letters and spaces'), false;
// if (!formData.postalCode.trim()) return toast.error('Postal code is required'), false;
// if (/\s/.test(formData.postalCode)) return toast.error('Postal code must not contain spaces'), false;
// if (!/^[A-Za-z0-9]+$/.test(formData.postalCode)) return toast.error('Postal code must contain only letters and numbers'), false;
// return true;
// };
const validateForm = () => {
const e: Record<string, string> = {};
if (!formData.firstName.trim()) e.firstName = 'First name is required';
else if (/\s/.test(formData.firstName)) e.firstName = 'First name must not contain spaces';
else if (!/^[A-Za-z]+$/.test(formData.firstName)) e.firstName = 'First name must contain only letters';
if (!formData.lastName.trim()) e.lastName = 'Last name is required';
else if (/\s/.test(formData.lastName)) e.lastName = 'Last name must not contain spaces';
else if (!/^[A-Za-z]+$/.test(formData.lastName)) e.lastName = 'Last name must contain only letters';
if (!formData.phone.trim()) e.phone = 'Mobile number is required';
else if (/\s/.test(formData.phone)) e.phone = 'Mobile number must not contain spaces';
else if (!/^\d+$/.test(formData.phone)) e.phone = 'Mobile number must contain only digits';
if (!formData.address1.trim()) e.address1 = 'Address is required';
else if (!/^[A-Za-z0-9\s,\-.]+$/.test(formData.address1)) e.address1 = 'Address contains invalid characters';
if (!formData.city.trim()) e.city = 'City is required';
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.city)) e.city = 'City can only contain letters and spaces';
else if (/\s{2,}/.test(formData.city)) e.city = 'City must not contain multiple consecutive spaces';
if (!formData.country.trim()) e.country = 'Country is required';
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.country)) e.country = 'Country can only contain letters and spaces';
if (!formData.postalCode.trim()) e.postalCode = 'Postal code is required';
else if (/\s/.test(formData.postalCode)) e.postalCode = 'Postal code must not contain spaces';
else if (!/^[A-Za-z0-9]+$/.test(formData.postalCode)) e.postalCode = 'Postal code must contain only letters and numbers';
setFieldErrors(e);
return Object.keys(e).length === 0;
};
// inside ProfilePage function body:
const FieldError = ({ name }: { name: string }) =>
fieldErrors[name] ? (
<p className="text-xs text-red-500 mt-1 flex items-center gap-1">
<AlertCircle className="w-3 h-3" />{fieldErrors[name]}
</p>
) : null;
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSaveProfile = async () => {
if (!validateForm()) return;
try {
console.log("Saving profile...", formData);
const response = await updateUserProfileDetails({ userDetails: formData, userId });
console.log(response)
toast.success("Profile updated successfully!");
} catch (error) {
console.error("Error saving profile:", error);
toast.error("Failed to update profile. Please try again.");
}
};
@@ -258,7 +263,7 @@ export function ProfilePage({
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pr-2">Profile</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600">
Manage your account, passes, and travel itineraries
Manage your account, cards, and travel itineraries
</p>
</motion.div>
</div>
@@ -271,7 +276,7 @@ export function ProfilePage({
{/* Tab Navigation */}
<TabsList className="grid w-full grid-cols-3 lg:w-[400px]">
<TabsTrigger value="profile" className="font-poppins font-light">My Profile</TabsTrigger>
<TabsTrigger value="passes" className="font-poppins font-light">My Passes</TabsTrigger>
<TabsTrigger value="passes" className="font-poppins font-light">My Cards</TabsTrigger>
<TabsTrigger value="itineraries" className="font-poppins font-light">My Itineraries</TabsTrigger>
</TabsList>
@@ -295,45 +300,57 @@ export function ProfilePage({
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="firstName" className="font-poppins font-light">First Name</Label>
<Label htmlFor="firstName" className="font-poppins font-light">
First Name <span className="text-red-500">*</span>
</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
className="mt-1 font-poppins font-light"
className={`mt-1 font-poppins font-light ${fieldErrors.firstName ? 'border-red-400' : ''}`}
/>
<FieldError name="firstName" />
</div>
<div>
<Label htmlFor="lastName" className="font-poppins font-light">Last Name</Label>
<Label htmlFor="lastName" className="font-poppins font-light">
Last Name <span className="text-red-500">*</span>
</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
className="mt-1 font-poppins font-light"
className={`mt-1 font-poppins font-light ${fieldErrors.lastName ? 'border-red-400' : ''}`}
/>
<FieldError name="lastName" />
</div>
</div>
<div>
<Label htmlFor="email" className="font-poppins font-light">Email Address</Label>
<Label htmlFor="email" className="font-poppins font-light">
Email Address
</Label>
<Input
id="email"
type="email"
value={formData.email}
disabled
onChange={(e) => handleInputChange('email', e.target.value)}
className="mt-1 font-poppins font-light"
/>
</div>
<div>
<Label htmlFor="phone" className="font-poppins font-light">Phone Number</Label>
<Label htmlFor="phone" className="font-poppins font-light">
Phone Number <span className="text-red-500">*</span>
</Label>
<Input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
className="mt-1 font-poppins font-light"
className={`mt-1 font-poppins font-light ${fieldErrors.phone ? 'border-red-400' : ''}`}
/>
<FieldError name="phone" />
</div>
<Separator />
@@ -341,44 +358,31 @@ export function ProfilePage({
<h3 className="font-poppins font-normal">Billing Address</h3>
<div>
<Label htmlFor="country" className="font-poppins font-light">Country</Label>
{/* <Select value={formData.country} onValueChange={(value) => handleInputChange('country', value)}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select country" />
</SelectTrigger>
<SelectContent>
<SelectItem value="us">United States</SelectItem>
<SelectItem value="au">Australia</SelectItem>
<SelectItem value="uk">United Kingdom</SelectItem>
<SelectItem value="ca">Canada</SelectItem>
<SelectItem value="de">Germany</SelectItem>
<SelectItem value="fr">France</SelectItem>
<SelectItem value="in">India</SelectItem>
<SelectItem value="jp">Japan</SelectItem>
</SelectContent>
</Select> */}
<Label htmlFor="country" className="font-poppins font-light">
Country <span className="text-red-500">*</span>
</Label>
<Input
id="country"
value={formData.country}
onChange={(e) => handleInputChange('country', e.target.value)}
className="mt-1 font-poppins font-light"
className={`mt-1 font-poppins font-light ${fieldErrors.country ? 'border-red-400' : ''}`}
/>
<FieldError name="country" />
</div>
<div>
<Label htmlFor="address1" className="font-poppins font-light">
Address Line 1
Address Line 1 <span className="text-red-500">*</span>
</Label>
<Input
id="address1"
value={formData.address1}
onChange={(e) => handleInputChange('address1', e.target.value)}
className="mt-1 font-poppins font-light mb-4"
className={`mt-1 font-poppins font-light mb-4 ${fieldErrors.address1 ? 'border-red-400' : ''}`}
/>
<FieldError name="address1" />
<Label htmlFor="address2" className="font-poppins font-light">
Address Line 2
</Label>
<Label htmlFor="address2" className="font-poppins font-light">Address Line 2</Label>
<Input
id="address2"
value={formData.address2}
@@ -389,22 +393,28 @@ export function ProfilePage({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="city" className="font-poppins font-light">City</Label>
<Label htmlFor="city" className="font-poppins font-light">
City <span className="text-red-500">*</span>
</Label>
<Input
id="city"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
className="mt-1 font-poppins font-light"
className={`mt-1 font-poppins font-light ${fieldErrors.city ? 'border-red-400' : ''}`}
/>
<FieldError name="city" />
</div>
<div>
<Label htmlFor="postalCode" className="font-poppins font-light">Postal Code</Label>
<Label htmlFor="postalCode" className="font-poppins font-light">
Postal Code <span className="text-red-500">*</span>
</Label>
<Input
id="postalCode"
value={formData.postalCode}
onChange={(e) => handleInputChange('postalCode', e.target.value)}
className="mt-1 font-poppins font-light"
className={`mt-1 font-poppins font-light ${fieldErrors.postalCode ? 'border-red-400' : ''}`}
/>
<FieldError name="postalCode" />
</div>
</div>
@@ -419,216 +429,30 @@ export function ProfilePage({
</motion.div>
</div>
{/* App Download Section */}
<div className="lg:col-span-1">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
<Card className="border border-gray-200 shadow-sm">
<CardContent className="p-8 space-y-6">
{(() => {
// Determine which pass type to show
const hasUnlimitedPass = activeCards.some((card: any) => card.cardType.cardTypeName === 'selective_pass');
const hasSelectivePass = activeCards.some((card: any) => card.cardType.cardTypeName === 'unlimited_card');
if (hasUnlimitedPass) {
return (
<>
<div className="space-y-3">
<h3 className="font-poppins text-xl font-normal">
Get{' '}
<span className="text-primary">Melbourne Unlimited Card</span>
</h3>
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
Unlimited access to 25+ attractions. Visit as many places as you want with one simple card.
Save up to 40% compared to individual tickets.
</p>
</div>
{/* Card Benefits */}
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<CreditCard className="w-3 h-3 text-white" />
</div>
<span>Unlimited entries to all attractions</span>
</div>
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<Calendar className="w-3 h-3 text-white" />
</div>
<span>Valid for 7 consecutive days</span>
</div>
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<MapPin className="w-3 h-3 text-white" />
</div>
<span>Skip the queue at major venues</span>
</div>
</div>
{/* Purchase CTA */}
<div className="space-y-3">
<Button
onClick={onPassesClick}
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
>
Purchase Unlimited Card
</Button>
<Button
variant="outline"
onClick={onCityCardsClick}
className="w-full font-poppins font-normal"
>
Learn More
</Button>
</div>
</>
);
} else if (hasSelectivePass) {
return (
<>
<div className="space-y-3">
<h3 className="font-poppins text-xl font-normal">
Get{' '}
<span className="text-primary">Selective Card</span>
{' '}now
</h3>
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
Choose your own adventure with 12 hand-picked attractions. Perfect for visitors
who want flexibility and value.
</p>
</div>
{/* Card Benefits */}
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<CreditCard className="w-3 h-3 text-white" />
</div>
<span>Choose from 12 curated attractions</span>
</div>
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<Calendar className="w-3 h-3 text-white" />
</div>
<span>Flexible 7-day validity period</span>
</div>
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<Star className="w-3 h-3 text-white" />
</div>
<span>Save 40% on combined ticket price</span>
</div>
</div>
{/* Purchase CTA */}
<div className="space-y-3">
<Button
onClick={onPassesClick}
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
>
Purchase Selective Card
</Button>
<Button
variant="outline"
onClick={onCityCardsClick}
className="w-full font-poppins font-normal"
>
View All Attractions
</Button>
</div>
</>
);
} else {
return (
<>
<div className="space-y-3">
<h3 className="font-poppins text-xl font-normal">
Get{' '}
<span className="text-primary">CityCards</span>
{' '}now
</h3>
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
Explore Melbourne's best attractions with our flexible card options.
Choose unlimited access or select your favorites.
</p>
</div>
{/* Card Options */}
<div className="space-y-3">
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<div>
<h4 className="font-poppins text-base font-medium mb-1">Unlimited Card</h4>
<p className="text-xs text-gray-600 font-poppins font-light">25+ attractions, unlimited visits</p>
</div>
<Badge className="bg-primary text-white">Popular</Badge>
</div>
<div className="text-2xl font-poppins font-semibold text-primary">$149</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<div>
<h4 className="font-poppins text-base font-medium mb-1">Selective Card</h4>
<p className="text-xs text-gray-600 font-poppins font-light">12 attractions of your choice</p>
</div>
</div>
<div className="text-2xl font-poppins font-semibold text-primary">$89</div>
</div>
</div>
{/* Purchase CTA */}
<div className="space-y-3">
<Button
onClick={onPassesClick}
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
>
Explore All Cards
</Button>
<Button
variant="outline"
onClick={onCityCardsClick}
className="w-full font-poppins font-normal"
>
Learn More
</Button>
</div>
</>
);
}
})()}
</CardContent>
</Card>
</motion.div>
</div>
</div>
</TabsContent>
{/* My Passes Tab */}
{/* My Cards Tab */}
<TabsContent value="passes" className="space-y-8">
{/* Active Passes */}
{/* Active Cards */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<h2 className="font-poppins text-2xl mb-6 font-normal">Active Passes</h2>
<h2 className="font-poppins text-2xl mb-6 font-normal">Active Cards</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{activeCards.map((card: any) => (
<Card key={card.id} className="overflow-hidden">
<div
className="flex cursor-pointer hover:bg-gray-50 transition-colors duration-200 rounded-lg p-2 -m-2"
onClick={() => navigate(`/view-card-design/${card.id}`)}
onClick={() => navigate(`/view-card-details/${card.id}`)}
>
<div className="w-32 h-32 flex-shrink-0">
<ImageWithFallback
src={card.city.bannerImage}
alt={card.city.name}
className="w-full h-full object-cover"
className="w-full object-cover"
/>
</div>
<div className="flex-1 p-6">
@@ -693,14 +517,14 @@ export function ProfilePage({
</div>
</motion.div>
{/* Expired Passes */}
{/* Expired Cards */}
{expiredCards.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
<h2 className="font-poppins text-2xl mb-6 font-normal">Expired Passes</h2>
<h2 className="font-poppins text-2xl mb-6 font-normal">Expired Cards</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{expiredCards.map((card: any) => (
<Card key={card.id} className="overflow-hidden opacity-60">
@@ -765,7 +589,7 @@ export function ProfilePage({
<h2 className="font-poppins text-2xl font-normal">My Itineraries</h2>
<Button
className="bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-normal"
onClick={() => navigate("/create-itinerary-design")}
onClick={() => navigate("/create-itinerary")}
>
<Plus className="w-4 h-4 mr-2" />
Create Itinerary
@@ -805,7 +629,7 @@ export function ProfilePage({
<Button
variant="outline"
className="w-full mt-4 font-poppins font-normal"
onClick={()=>navigate(`/itinerary-view-design/${itinerary.id}`)}
onClick={() => navigate(`/view-itinerary/${itinerary.id}`)}
>
View Itinerary
</Button>
@@ -825,7 +649,7 @@ export function ProfilePage({
</p>
<Button
className="bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-normal"
onClick={onCreateItineraryClick}
onClick={() => navigate("/create-itinerary")}
>
<Plus className="w-4 h-4 mr-2" />
Create Itinerary

View File

@@ -1,527 +0,0 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Lock, Shield, CreditCard, Check, X, Tag } from 'lucide-react';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Label } from '../components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
import { Separator } from '../components/ui/separator';
import { Badge } from '../components/ui/badge';
import Navbar from '../components/Navbar';
import { Footer } from '../components/Footer';
interface User {
email: string;
name: string;
}
interface SecureCheckoutPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage?: string;
user: User | null;
}
export function SecureCheckoutPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: SecureCheckoutPageProps) {
const [selectedTab, setSelectedTab] = useState('myself');
const [formData, setFormData] = useState({
firstName: 'Frank',
lastName: 'Adam S',
email: 'frank2023@mail.com',
phone: '',
travelDate: '',
billingAddress: '',
city: '',
country: 'England',
state: '',
zipCode: '000000'
});
const [discountCode, setDiscountCode] = useState('');
const [isEmailVerified, setIsEmailVerified] = useState(false);
const [showPaymentSuccess, setShowPaymentSuccess] = useState(false);
const orderSummary = {
item: 'Melbourne - Unlimited Card',
duration: '2 Days',
adults: 3,
kids: 3,
quantity: 1,
price: 49.80,
discount: 7.20,
tax: 2.24
};
const subtotal = orderSummary.price;
const discount = orderSummary.discount;
const total = subtotal - discount;
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handlePayment = () => {
setShowPaymentSuccess(true);
setTimeout(() => {
setShowPaymentSuccess(false);
onHomeClick();
}, 2000);
};
return (
<div className="min-h-screen bg-background">
<Navbar
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
currentPage={currentPage}
isUserSignedIn={!!user}
user={user}
/>
<div className="container mx-auto px-4 py-12">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<Button
variant="ghost"
onClick={onBackClick}
className="mb-6 p-0 hover:bg-transparent"
>
<ArrowLeft className="w-5 h-5 mr-2" />
Back
</Button>
<div className="flex items-center gap-4 mb-2">
<h1 className="font-merchant text-3xl md:text-4xl leading-tight">
Secure <span className="text-primary">Checkout</span>
</h1>
<div className="flex items-center gap-2 text-green-600">
<Shield className="w-5 h-5" />
<span className="font-poppins text-sm font-medium">SSL Secured</span>
</div>
</div>
<p className="text-muted-foreground">
Complete your purchase securely. Your payment information is protected.
</p>
</motion.div>
<div className="grid lg:grid-cols-3 gap-8">
{/* Checkout Form */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="lg:col-span-2"
>
<Card className="shadow-lg border-0">
<CardHeader className="pb-6">
<Tabs value={selectedTab} onValueChange={setSelectedTab} className="w-full">
<TabsList className="grid w-fit grid-cols-2 bg-muted/50">
<TabsTrigger value="myself" className="px-6">For myself</TabsTrigger>
<TabsTrigger value="gift" className="px-6">To gift Someone</TabsTrigger>
</TabsList>
</Tabs>
</CardHeader>
<CardContent className="space-y-8">
{/* Personal Information */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
className="space-y-6"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary text-primary-foreground rounded-full flex items-center justify-center font-medium">
1
</div>
<h2 className="font-merchant text-xl font-medium leading-snug">Personal Information</h2>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">First Name</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Last Name</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
className="h-12"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<div className="relative">
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
className="h-12 pr-20"
/>
<Button
size="sm"
variant={isEmailVerified ? "default" : "outline"}
className="absolute right-2 top-1/2 -translate-y-1/2 h-8"
onClick={() => setIsEmailVerified(!isEmailVerified)}
>
{isEmailVerified ? (
<>
<Check className="w-3 h-3 mr-1" />
Verified
</>
) : (
'Verify'
)}
</Button>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="phone">Phone Number</Label>
<Input
id="phone"
type="tel"
placeholder="Enter mobile number"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="travelDate">Select Travel Date</Label>
<Input
id="travelDate"
type="date"
value={formData.travelDate}
onChange={(e) => handleInputChange('travelDate', e.target.value)}
className="h-12"
/>
</div>
</div>
</motion.div>
<Separator />
{/* Billing Information */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="space-y-6"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary text-primary-foreground rounded-full flex items-center justify-center font-medium">
2
</div>
<h2 className="font-merchant text-xl font-medium leading-snug">Billing Information</h2>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="billingAddress">Billing Address</Label>
<Input
id="billingAddress"
placeholder="Enter Address"
value={formData.billingAddress}
onChange={(e) => handleInputChange('billingAddress', e.target.value)}
className="h-12"
/>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="city">City</Label>
<Input
id="city"
placeholder="Enter City"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="country">Country</Label>
<Input
id="country"
value={formData.country}
onChange={(e) => handleInputChange('country', e.target.value)}
className="h-12"
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="state">State</Label>
<Input
id="state"
placeholder="State"
value={formData.state}
onChange={(e) => handleInputChange('state', e.target.value)}
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="zipCode">Zip Code</Label>
<Input
id="zipCode"
value={formData.zipCode}
onChange={(e) => handleInputChange('zipCode', e.target.value)}
className="h-12"
/>
</div>
</div>
</div>
</motion.div>
</CardContent>
</Card>
</motion.div>
{/* Order Summary */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className="lg:col-span-1"
>
<Card className="shadow-lg border-0 sticky top-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CreditCard className="w-5 h-5" />
Order Summary
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Item Details */}
<div className="space-y-4">
<div className="flex justify-between items-start">
<div className="space-y-2">
<h3 className="font-poppins font-medium">{orderSummary.item}</h3>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>{orderSummary.duration}</span>
<span></span>
<span>Adults-{orderSummary.adults}</span>
<span></span>
<span>Kids-{orderSummary.kids}</span>
</div>
<div className="text-sm text-muted-foreground">
Qty: {orderSummary.quantity}
</div>
</div>
<div className="font-poppins text-lg font-medium">
${orderSummary.price.toFixed(2)}
</div>
</div>
</div>
<Separator />
{/* Discount Code */}
<div className="space-y-3">
<div className="flex gap-2">
<Input
placeholder="Gift or discount code"
value={discountCode}
onChange={(e) => setDiscountCode(e.target.value)}
className="flex-1"
/>
<Button variant="outline" className="px-6">
Apply
</Button>
</div>
</div>
<Separator />
{/* Price Breakdown */}
<div className="space-y-3">
<div className="flex justify-between">
<span>Subtotal</span>
<span>${subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between text-green-600">
<span>Discount</span>
<span>-${discount.toFixed(2)}</span>
</div>
</div>
<Separator />
{/* Total */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<div>
<div className="font-medium">Total</div>
<div className="text-sm text-muted-foreground">
Including ${orderSummary.tax.toFixed(2)} in taxes
</div>
</div>
<div className="text-2xl font-bold text-primary">
${total.toFixed(2)}
</div>
</div>
</div>
{/* Payment Button */}
<Button
onClick={handlePayment}
className="w-full h-14 text-lg bg-gradient-to-r from-primary to-primary/90 hover:from-primary/90 hover:to-primary/80 relative overflow-hidden group"
disabled={showPaymentSuccess}
>
{showPaymentSuccess ? (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="flex items-center gap-2"
>
<Check className="w-5 h-5" />
Payment Successful!
</motion.div>
) : (
<>
<Lock className="w-5 h-5 mr-2" />
Pay ${total.toFixed(2)}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent transform -skew-x-12 -translate-x-full group-hover:translate-x-full transition-transform duration-700" />
</>
)}
</Button>
{/* Security Notice */}
<div className="text-xs text-muted-foreground leading-relaxed">
Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our privacy policy.
</div>
{/* Trust Badges */}
<div className="flex items-center justify-center gap-4 pt-4">
<div className="flex items-center gap-2 text-green-600">
<Shield className="w-4 h-4" />
<span className="text-xs">256-bit SSL</span>
</div>
<div className="flex items-center gap-2 text-blue-600">
<Lock className="w-4 h-4" />
<span className="text-xs">Secure Payment</span>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
/>
{/* Success Overlay */}
{showPaymentSuccess && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center"
>
<motion.div
initial={{ scale: 0, rotate: 180 }}
animate={{ scale: 1, rotate: 0 }}
className="bg-white p-8 rounded-2xl shadow-2xl text-center max-w-sm mx-4"
>
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Check className="w-8 h-8 text-green-600" />
</div>
<h3 className="text-xl font-medium mb-2">Payment Successful!</h3>
<p className="text-muted-foreground">
Your Melbourne CityCard has been purchased successfully.
</p>
</motion.div>
</motion.div>
)}
</div>
);
}

View File

@@ -345,7 +345,7 @@ export function SuperSavingsDetailsPage({
</div>
{/* Right Sidebar - Calendar and Booking (preserved, but you can add a real calendar if needed) */}
<div className="lg:col-span-1">
{/* <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">
@@ -379,7 +379,7 @@ export function SuperSavingsDetailsPage({
Proceed to Checkout
</Button>
</Card>
</div>
</div> */}
</div>
</div>
</Layout>

View File

@@ -188,10 +188,10 @@ export function SuperSavingsPage({
</section>
{/* Trusted By Companies Section */}
<section className="py-12 bg-background">
<section className="py-10 bg-background">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto text-center">
<div className="mb-10">
<div className="mb-1">
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight mb-4">
<span>Trusted by the </span>
<span className="font-semibold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">world's best</span>
@@ -206,7 +206,7 @@ export function SuperSavingsPage({
</section>
{/* Featured Super Savings Section */}
<section className="py-20">
<section className="">
<div className="container mx-auto px-4">
<motion.div
className="text-center mb-12"
@@ -225,7 +225,7 @@ export function SuperSavingsPage({
</p>
</motion.div>
<div className="container mx-auto px-4 pt-51 pb-16">
<div className="container mx-auto px-4 pt-11 pb-16">
<div className="flex gap-8">
{/* Left Sidebar - Filters */}
<div className="w-64 flex-shrink-0">
@@ -269,7 +269,7 @@ export function SuperSavingsPage({
{/* Main Content */}
<div className="flex-1">
{/* Breadcrumb */}
<div className="mb-8">
{/* <div className="mb-8">
<p className="font-poppins text-sm text-gray-800">
{fromSource === 'passes' ? (
<>
@@ -283,7 +283,7 @@ export function SuperSavingsPage({
</>
)}
</p>
</div>
</div> */}
{/* Header Section */}
<div className="mb-8">
@@ -395,7 +395,7 @@ export function SuperSavingsPage({
</div>
</div>
<div className="text-center">
{/* <div className="text-center">
<Button
onClick={onSignInClick}
variant="outline"
@@ -403,7 +403,7 @@ export function SuperSavingsPage({
>
View All Super Savings
</Button>
</div>
</div> */}
</div>
</section>

View File

@@ -1,6 +1,5 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, ChevronRight, QrCode, CreditCard, Calendar, MapPin, Star, CheckCircle, Sparkles, Users, Clock, Gift, Ticket } from 'lucide-react';
import { ArrowLeft, ChevronRight, Calendar, MapPin, Star, CheckCircle, Sparkles, Users, Clock, Gift, Ticket } from 'lucide-react';
import { Button } from '../components/ui/button';
import { Card, CardContent } from '../components/ui/card';
import { Badge } from '../components/ui/badge';
@@ -62,13 +61,10 @@ export function ViewCardDetailsPage({
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: ViewCardDetailsPageProps) {
// Card type state
const [cardType, setCardType] = useState<'unlimited' | 'flexi'>('unlimited');
const { cardId } = useParams()
const { data, isLoading } = useGetUserCardDetailsQuery(cardId)
const navigate = useNavigate()
@@ -86,16 +82,6 @@ export function ViewCardDetailsPage({
)
}
// Mock data for the current pass
const currentPass = {
name: 'Melbourne- Unlimited Card',
status: 'Active',
date: '22/12/2024',
duration: '2 Days',
adults: 3,
kids: 3
};
// Generate QR code pattern
const generateQRPattern = () => {
const size = 17;

View File

@@ -1,16 +1,20 @@
export const footerSections = {
explore: {
title: 'Explore',
links: ['Home', 'My Adventures', 'Cancellation policy']
links: ['Home',
'Cancellation policy'
]
},
learn: {
title: 'Learn',
links: ['How It Works', 'Safety Tips', 'FAQ', 'Blog']
},
community: {
title: 'Community',
links: ['Testimonials', 'Partner Stories', 'Events & Meetups', 'Newsletter']
links: ['How It Works',
// 'Safety Tips',
'FAQ', 'Blog']
},
// community: {
// title: 'Community',
// links: ['Testimonials', 'Partner Stories', 'Events & Meetups', 'Newsletter']
// },
support: {
title: 'Support',
links: ['Contact Us', 'Privacy Policy', 'Terms of Service']