From de80760cd44dcdaaa820c77b7954ea1a8e004889 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Mon, 16 Feb 2026 12:11:26 +0530 Subject: [PATCH 01/12] add rtk setup --- package-lock.json | 112 +++++++++++++++- package.json | 2 + src/Redux/Store.tsx | 18 +++ src/Redux/services/fakeApi.service.ts | 19 +++ src/main.tsx | 10 +- src/pages/landingPage.tsx | 181 +++++++++++++------------- 6 files changed, 250 insertions(+), 92 deletions(-) create mode 100644 src/Redux/Store.tsx create mode 100644 src/Redux/services/fakeApi.service.ts diff --git a/package-lock.json b/package-lock.json index 5f4b827..13d84a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/postcss": "^4.1.13", "@tailwindcss/vite": "^4.1.14", "class-variance-authority": "^0.7.1", @@ -49,6 +50,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.55.0", + "react-redux": "^9.2.0", "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.9.4", "recharts": "^2.15.2", @@ -1917,6 +1919,32 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -2197,6 +2225,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/core": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", @@ -3033,6 +3073,7 @@ "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3043,6 +3084,7 @@ "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3053,10 +3095,17 @@ "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", @@ -3311,7 +3360,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -3477,6 +3527,16 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/input-otp": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", @@ -3885,6 +3945,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3942,6 +4003,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3968,6 +4030,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3998,6 +4061,30 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-remove-scroll": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", @@ -4178,6 +4265,28 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.50.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", @@ -4424,6 +4533,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/package.json b/package.json index 5e2c057..2e11959 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/postcss": "^4.1.13", "@tailwindcss/vite": "^4.1.14", "class-variance-authority": "^0.7.1", @@ -44,6 +45,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.55.0", + "react-redux": "^9.2.0", "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.9.4", "recharts": "^2.15.2", diff --git a/src/Redux/Store.tsx b/src/Redux/Store.tsx new file mode 100644 index 0000000..1a71269 --- /dev/null +++ b/src/Redux/Store.tsx @@ -0,0 +1,18 @@ +import { configureStore } from "@reduxjs/toolkit"; +import { fakeApi } from "./services/fakeapi.service"; + +export const store = configureStore({ + reducer: { + [fakeApi.reducerPath]:fakeApi.reducer + }, + + + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat( + +fakeApi.middleware, + + ), +}); +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; \ No newline at end of file diff --git a/src/Redux/services/fakeApi.service.ts b/src/Redux/services/fakeApi.service.ts new file mode 100644 index 0000000..b5f90ae --- /dev/null +++ b/src/Redux/services/fakeApi.service.ts @@ -0,0 +1,19 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + +export const fakeApi = createApi({ + reducerPath: 'fakeApi', + baseQuery: fetchBaseQuery({ + baseUrl: " https://fakestoreapi.com", + + }), + endpoints: (builder) => ({ + getProducts: builder.query({ + query: () => ({ + url: 'products', + method: 'GET', + }), + }), + }), +}) + +export const { useGetProductsQuery} = fakeApi diff --git a/src/main.tsx b/src/main.tsx index b348863..241b0b3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,9 +2,13 @@ import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import App from "./App"; import "./index.css"; +import { Provider } from "react-redux"; +import { store } from "./Redux/Store"; createRoot(document.getElementById("root")!).render( - - - + + + + + ); \ No newline at end of file diff --git a/src/pages/landingPage.tsx b/src/pages/landingPage.tsx index fc9a777..0338ecb 100644 --- a/src/pages/landingPage.tsx +++ b/src/pages/landingPage.tsx @@ -19,95 +19,100 @@ import { LandingNewsletterSection } from '../components/LandingNewsletterSection import { CustomPostcards } from '../components/CustomPostcards'; import { Layout } from '../Layout'; import { getAutoNavigationSource } from '../utils/getAutoNavigationSource'; +import { useGetProductsQuery } from '../Redux/services/fakeapi.service'; const melbourneImage = - "https://images.unsplash.com/photo-1551836022-d5d88e9218df?auto=format&fit=crop&w=1920&q=80"; // Melbourne + "https://images.unsplash.com/photo-1551836022-d5d88e9218df?auto=format&fit=crop&w=1920&q=80"; // Melbourne const sydneyImage = - "https://images.unsplash.com/photo-1506976785307-8732e854ad03?auto=format&fit=crop&w=1920&q=80"; // Sydney Opera House + "https://images.unsplash.com/photo-1506976785307-8732e854ad03?auto=format&fit=crop&w=1920&q=80"; // Sydney Opera House const brisbaneImage = - "https://images.unsplash.com/photo-1604644363101-03f3d7cbecb6?auto=format&fit=crop&w=1920&q=80"; // Brisbane skyline + "https://images.unsplash.com/photo-1604644363101-03f3d7cbecb6?auto=format&fit=crop&w=1920&q=80"; // Brisbane skyline interface User { - email: string; - name: string; + email: string; + name: string; } interface LandingPageProps { - onSignInClick: () => void; - onSignOutClick?: () => void; - user?: User | null; + onSignInClick: () => void; + onSignOutClick?: () => void; + user?: User | null; } export function LandingPage({ onSignInClick, - onSignOutClick, - user }: LandingPageProps) { - const [currentCityIndex, setCurrentCityIndex] = useState(0); - const location = useLocation(); - const activeCity = getAutoNavigationSource(location); + onSignOutClick, + user }: LandingPageProps) { + const [currentCityIndex, setCurrentCityIndex] = useState(0); + const [isCityDialogOpen, setIsCityDialogOpen] = useState(Boolean) + const location = useLocation(); + const activeCity = getAutoNavigationSource(location); - const cities = [ - { - id: 'melbourne', - name: 'Melbourne', - description: 'Cultural capital with world-class attractions', - image: melbourneImage, - attractions: 45, - savings: '30%', - path: '/melbourne' - }, - { - id: 'sydney', - name: 'Sydney', - description: 'Iconic landmarks and harbor views', - image: sydneyImage, - attractions: 38, - savings: '25%', - path: '/sydney' - }, - { - id: 'brisbane', - name: 'Brisbane', - description: 'Sunshine, riverside dining, and adventure', - image: brisbaneImage, - attractions: 32, - savings: '28%', - path: '/brisbane' - } - ]; + const { data } = useGetProductsQuery() + console.log(data) - // Auto-rotate cities - useEffect(() => { - const interval = setInterval(() => { - setCurrentCityIndex((prev) => (prev + 1) % cities.length); - }, 4000); - return () => clearInterval(interval); - }, []); + const cities = [ + { + id: 'melbourne', + name: 'Melbourne', + description: 'Cultural capital with world-class attractions', + image: melbourneImage, + attractions: 45, + savings: '30%', + path: '/melbourne' + }, + { + id: 'sydney', + name: 'Sydney', + description: 'Iconic landmarks and harbor views', + image: sydneyImage, + attractions: 38, + savings: '25%', + path: '/sydney' + }, + { + id: 'brisbane', + name: 'Brisbane', + description: 'Sunshine, riverside dining, and adventure', + image: brisbaneImage, + attractions: 32, + savings: '28%', + path: '/brisbane' + } + ]; - const scrollToCities = () => { - document.getElementById('cities-section')?.scrollIntoView({ - behavior: 'smooth' - }); - }; + // Auto-rotate cities + useEffect(() => { + const interval = setInterval(() => { + setCurrentCityIndex((prev) => (prev + 1) % cities.length); + }, 4000); + return () => clearInterval(interval); + }, []); - return ( -
- {/* Navbar */} - + const scrollToCities = () => { + document.getElementById('cities-section')?.scrollIntoView({ + behavior: 'smooth' + }); + }; - {/* City Submenu */} - {/* + {/* Navbar */} + + + {/* City Submenu */} + {/* { }} /> */} - {/* Hero Section */} -
@@ -162,34 +167,34 @@ export function LandingPage({ onSignInClick,
- {/* Features Section */} - + {/* Features Section */} + - {/* LandingVarietyOfAdventures Section */} - + {/* LandingVarietyOfAdventures Section */} + - {/* MagicItinerary Section */} - + {/* MagicItinerary Section */} + - {/* BookAttractionSection Section */} - + {/* BookAttractionSection Section */} + - {/* CustomPostcards Section */} - + {/* CustomPostcards Section */} + - {/* UpcomingCities Section */} - + {/* UpcomingCities Section */} + - {/* TrustSection Section */} - + {/* TrustSection Section */} + - {/* MobileAppSection Section */} - + {/* MobileAppSection Section */} + - {/* Newsletter Section */} - + {/* Newsletter Section */} + - - - ); + + + ); } \ No newline at end of file From 8e83c3d90d1c24e724fe5a6749e5d067fc8f4550 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Mon, 23 Feb 2026 11:37:38 +0530 Subject: [PATCH 02/12] redirect to melbourne route after login --- src/components/DiscoverPage.tsx | 2 +- src/components/LoginModal.tsx | 4 ++++ src/components/PassesPage.tsx | 2 +- src/pages/landingPage.tsx | 6 +++--- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/DiscoverPage.tsx b/src/components/DiscoverPage.tsx index c20d852..1ccebfa 100644 --- a/src/components/DiscoverPage.tsx +++ b/src/components/DiscoverPage.tsx @@ -174,7 +174,7 @@ export function DiscoverPage({ return ( { if (!isOpen) { @@ -98,6 +101,7 @@ export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) name: name.length > 8 ? name.substring(0, 8) : name }); setIsLoading(false); + navigate("/melbourne") onClose(); }, 1500); }; diff --git a/src/components/PassesPage.tsx b/src/components/PassesPage.tsx index 24314e1..581efb9 100644 --- a/src/components/PassesPage.tsx +++ b/src/components/PassesPage.tsx @@ -192,7 +192,7 @@ export function PassesPage({ return ( Date: Tue, 17 Mar 2026 11:00:42 +0530 Subject: [PATCH 03/12] feat(auth): add AuthContext and wrap App with Authprovider --- src/App.tsx | 72 +++++++++++++++++++------------------ src/context/AuthContext.tsx | 51 ++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 35 deletions(-) create mode 100644 src/context/AuthContext.tsx diff --git a/src/App.tsx b/src/App.tsx index d3c705b..ea15e93 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import { easeOutExpo, easeOutCubic } from './utils/animations'; +import { AuthProvider } from './context/AuthContext'; // User type definition interface User { @@ -23,11 +24,11 @@ function App() { const [showQRCard, setShowQRCard] = useState(false); const [offersSource, setOffersSource] = useState<'products' | 'passes'>('products'); const [stickyCardType, setStickyCardType] = useState<'unlimited' | 'selective'>('unlimited'); - + // ✅ Authentication state management const [user, setUser] = useState(null); const [showLoginModal, setShowLoginModal] = useState(false); - + // ✅ City state management const [activeCity, setActiveCity] = useState(''); @@ -73,7 +74,7 @@ function App() { const checkMobile = () => { setIsMobile(window.innerWidth < 768); }; - + checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); @@ -83,27 +84,27 @@ function App() { const generateQRPattern = () => { const size = 27; const pattern = []; - + for (let i = 0; i < size * size; i++) { const row = Math.floor(i / size); const col = i % size; - - const isCornerSquare = + + const isCornerSquare = (row < 7 && col < 7) || (row < 7 && col >= 20) || (row >= 20 && col < 7); - + const isFinderPattern = isCornerSquare && ( (row === 0 || row === 6 || col === 0 || col === 6) || (row >= 2 && row <= 4 && col >= 2 && col <= 4) ); - + const isTimingPattern = (row === 6 && col >= 8 && col <= 18) || (col === 6 && row >= 8 && row <= 18); const isDataPattern = !isCornerSquare && !isTimingPattern && Math.random() > 0.38; - + pattern.push(isFinderPattern || isTimingPattern || isDataPattern); } - + return pattern; }; @@ -120,24 +121,26 @@ function App() { return (
{/* Global Animation Context Provider */} - - + + + {/* Sticky Widget */} @@ -152,11 +155,10 @@ function App() { whileHover={{ scale: 1.05, y: -2 }} whileTap={{ scale: 0.95 }} onClick={handleStickyWidgetClick} - className={`relative shadow-2xl flex items-center justify-center rounded-2xl transition-all duration-300 overflow-hidden group ${ - location.pathname === '/attractions' + className={`relative shadow-2xl flex items-center justify-center rounded-2xl transition-all duration-300 overflow-hidden group ${location.pathname === '/attractions' ? 'w-[244px] h-36' : 'w-36 h-36 bg-black text-white' - }`} + }`} aria-label={location.pathname === '/attractions' ? 'Get CityCard' : 'Show QR Code'} > {location.pathname === '/attractions' ? ( @@ -169,10 +171,10 @@ function App() {
- + {/* GET NOW Text */}

GET NOW

- + {/* Dashed Line Separator */}
@@ -181,7 +183,7 @@ function App() {
- + {/* Card Title in Orange */}

{stickyCardType === 'unlimited' ? ( @@ -191,7 +193,7 @@ function App() { )}

- + {/* Orange Border */} @@ -240,8 +242,8 @@ function App() { className={`aspect-square ${filled ? 'bg-black' : 'bg-transparent'} rounded-[0.5px]`} initial={{ opacity: 0, scale: 0 }} animate={{ opacity: 1, scale: 1 }} - transition={{ - duration: 0.01, + transition={{ + duration: 0.01, delay: index * 0.001, ease: "easeOut" }} @@ -251,9 +253,9 @@ function App() {
- CityCards
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 0000000..8cabeb8 --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,51 @@ +import React, { createContext, useContext, useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom'; + +interface User { + email: string; + name: string +} + +interface AuthContextType { + user: User | null; + login: (userData: User) => void; + logout: () => void +} + +const AuthContext = createContext(null) + +export const AuthProvider = ({ children }: { children: React.ReactNode }) => { + const [user, setUser] = useState(null) + + const navigate = useNavigate() + + useEffect(() => { + const storedUser = localStorage.getItem("user") + if (storedUser) { + setUser(JSON.parse(storedUser)) + } + }, []) + + const login = (userData: User) => { + setUser(userData) + localStorage.setItem("user", JSON.stringify(userData)) + } + + const logout = () => { + setUser(null) + localStorage.removeItem("user") + navigate("/") + } + + return ( + + {children} + + ) +} + +export const useAuth = () => { + const ctx = useContext(AuthContext) + if (!ctx) throw new Error("useAuth must be used inside AuthProvider") + return ctx +} \ No newline at end of file From 61c8e1833fa800a968b0e3d3a5efc82a52d08c3f Mon Sep 17 00:00:00 2001 From: aryabenade Date: Tue, 17 Mar 2026 11:01:47 +0530 Subject: [PATCH 04/12] refactor(auth): integrate AuthContext into LoginModal --- src/components/LoginModal.tsx | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/components/LoginModal.tsx b/src/components/LoginModal.tsx index 4dbb253..e044045 100644 --- a/src/components/LoginModal.tsx +++ b/src/components/LoginModal.tsx @@ -5,14 +5,15 @@ import { Button } from './ui/button'; import { Input } from './ui/input'; import { Label } from './ui/label'; import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; interface LoginModalProps { isOpen: boolean; onClose: () => void; - onLoginSuccess: (userData: { email: string; name: string }) => void; + // onLoginSuccess: (userData: { email: string; name: string }) => void; } -export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) { +export function LoginModal({ isOpen, onClose, }: LoginModalProps) { const [step, setStep] = useState<'email' | 'otp'>('email'); const [email, setEmail] = useState(''); const [otp, setOtp] = useState(['', '', '', '', '', '']); @@ -20,7 +21,7 @@ export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) const [isLoading, setIsLoading] = useState(false); const [helperText, setHelperText] = useState(''); - const navigate = useNavigate() + const { login } = useAuth(); // from AuthContext // Reset modal state when closed useEffect(() => { @@ -49,7 +50,7 @@ export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) setIsLoading(true); setHelperText(''); - + // Simulate API call setTimeout(() => { setStep('otp'); @@ -61,7 +62,7 @@ export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) const handleOTPChange = (index: number, value: string) => { if (value.length > 1) return; // Only allow single digit - + const newOtp = [...otp]; newOtp[index] = value; setOtp(newOtp); @@ -95,13 +96,11 @@ export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) // Generate name from email for demo const emailParts = email.split('@')[0]; const name = emailParts.charAt(0).toUpperCase() + emailParts.slice(1); - - onLoginSuccess({ - email, - name: name.length > 8 ? name.substring(0, 8) : name - }); + + login({ email, name }) + setIsLoading(false); - navigate("/melbourne") + // navigate("/melbourne") onClose(); }, 1500); }; @@ -143,7 +142,7 @@ export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) > - +

Login

@@ -235,7 +234,7 @@ export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) /> ))}
- + {/* Countdown */} {countdown > 0 && (

From 001b0311578e4e0cd1c7d80cb6cdcb33d2b7bd3e Mon Sep 17 00:00:00 2001 From: aryabenade Date: Tue, 17 Mar 2026 11:03:33 +0530 Subject: [PATCH 05/12] feat(navbar): render LoginModal conditionally based on route --- src/components/Navbar.tsx | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 5893b72..bdd2a3a 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -9,6 +9,8 @@ import { CTAButton } from './CTAButton'; import logoImage from '../assets/cit-logo.png'; import melbourneLogo from '../assets/melbourne-logo.png'; import { CitySelectionDialog } from './CitySelectionDialog'; +import { useAuth } from '../context/AuthContext'; +import { LoginModal } from './LoginModal'; interface NavbarProps { activeCity: string; @@ -63,7 +65,7 @@ export default function Navbar({ onSignInClick, onSignOutClick, isUserSignedIn = false, - user + // user }: NavbarProps) { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); @@ -85,6 +87,22 @@ export default function Navbar({ const navigate = useNavigate(); const [lastKnownCity, setLastKnownCity] = useState<'landing' | 'melbourne'>('landing'); + const [isLoginOpen, setLoginOpen] = useState(false); + + const { user, login, logout } = useAuth(); // from AuthContext + + const protectedPaths = ["/passes", "/whats-included", "/","/melbourne"]; + + const handleOpenLoginModal = () => { + if (!user && protectedPaths.includes(location.pathname)) { + setLoginOpen(true); + } + else if (!user) { + setIsCityDialogOpen(true); // normal city selection + } else if (user) { + setActiveUserDropdown(true) + } + }; // More flexible navigation configuration @@ -642,7 +660,7 @@ export default function Navbar({ {/* Enhanced City Card Button with Source Tracking */}

- {isUserSignedIn && user ? ( + {user ? ( , action: () => { - if (onSignOutClick) { - onSignOutClick(); - } + logout() setActiveUserDropdown(false); } } @@ -698,10 +714,10 @@ export default function Navbar({ ) : (
{ }} className="hover:scale-105 transition-transform duration-200" /> @@ -887,6 +903,13 @@ export default function Navbar({ onClose={handleCloseCityDialog} onCitySelect={handleCitySelect} /> + + { + setLoginOpen(false); + }} + /> ); } \ No newline at end of file From 41c60ab8323cddf6796fcf2fb0bc34acf4f8a53e Mon Sep 17 00:00:00 2001 From: aryabenade Date: Tue, 17 Mar 2026 11:04:31 +0530 Subject: [PATCH 06/12] feat(passes): render data conditionally based on auth state --- src/components/PassesPage.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/components/PassesPage.tsx b/src/components/PassesPage.tsx index 581efb9..25313f4 100644 --- a/src/components/PassesPage.tsx +++ b/src/components/PassesPage.tsx @@ -11,6 +11,7 @@ import { ReviewsSection } from './ReviewsSection'; import { Layout } from '../Layout'; import { LoginModal } from './LoginModal'; import { ImageWithFallback } from './figma/ImageWithFallback'; +import { useAuth } from '../context/AuthContext'; interface PassesPageProps { onCheckoutClick?: () => void; @@ -149,16 +150,18 @@ export function PassesPage({ onCheckoutClick, onSignInClick, onSignOutClick, - user, + // user, onLoginSuccess }: PassesPageProps) { const [selectedPass, setSelectedPass] = useState('unlimited'); const [isLoginOpen, setIsLoginOpen] = useState(false); - const [userData, setUserData] = useState<{ email: string; name: string } | null>(user || null); + // const [userData, setUserData] = useState<{ email: string; name: string } | null>(user || null); + const { user } = useAuth(); // from AuthContext + // ✅ Handle purchase button click const handlePurchaseClick = () => { - if (!userData) { + if (!user) { // User not logged in - show login modal setIsLoginOpen(true); } else { @@ -169,7 +172,7 @@ export function PassesPage({ // ✅ Handle successful login const handleLoginSuccess = (data: { email: string; name: string }) => { - setUserData(data); + // setUserData(data); setIsLoginOpen(false); console.log('Logged in user:', data); @@ -195,7 +198,7 @@ export function PassesPage({ activeCity={sessionStorage.getItem("lastKnownCity")||"shared"} onSignInClick={onSignInClick} onSignOutClick={onSignOutClick} - user={userData} // ✅ Pass the updated user data + user={user} // ✅ Pass the updated user data >
{/* Page Header */} @@ -759,12 +762,6 @@ export function PassesPage({
- setIsLoginOpen(false)} - onLoginSuccess={handleLoginSuccess} - /> - ); } From 1f2817189314245f3aa9a716b632fa9272a4f07a Mon Sep 17 00:00:00 2001 From: aryabenade Date: Tue, 17 Mar 2026 17:04:59 +0530 Subject: [PATCH 07/12] fix the slow loading of CitySelectDialog --- src/AppRouter.tsx | 6 ----- src/Redux/Store.tsx | 2 +- src/components/CTAButton.tsx | 2 +- src/components/CitySelectionDialog.tsx | 33 +++++++++++++------------- src/components/MelbournePage.tsx | 8 +++---- src/components/Navbar.tsx | 2 +- 6 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index a58d8bd..9769ff2 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -274,12 +274,6 @@ export function AppRouter({ } /> - - ); } \ No newline at end of file diff --git a/src/Redux/Store.tsx b/src/Redux/Store.tsx index 1a71269..fde51a7 100644 --- a/src/Redux/Store.tsx +++ b/src/Redux/Store.tsx @@ -1,5 +1,5 @@ import { configureStore } from "@reduxjs/toolkit"; -import { fakeApi } from "./services/fakeapi.service"; +import { fakeApi } from "./services/fakeApi.service"; export const store = configureStore({ reducer: { diff --git a/src/components/CTAButton.tsx b/src/components/CTAButton.tsx index ff5d97d..9b80d9f 100644 --- a/src/components/CTAButton.tsx +++ b/src/components/CTAButton.tsx @@ -79,7 +79,7 @@ export function CTAButton({ user, onClick, className = "" }: CTAButtonProps) { diff --git a/src/components/CitySelectionDialog.tsx b/src/components/CitySelectionDialog.tsx index 377e035..4921f9e 100644 --- a/src/components/CitySelectionDialog.tsx +++ b/src/components/CitySelectionDialog.tsx @@ -1,5 +1,5 @@ // CitySelectionDialog.tsx -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Dialog, DialogContent, DialogTitle, DialogDescription } from './ui/dialog'; import { ArrowLeft, Search } from 'lucide-react'; @@ -30,21 +30,23 @@ const cities: City[] = [ { id: 'louisiana', name: 'Louisiana', imageUrl: 'https://images.unsplash.com/photo-1646508262200-455d62c22182?...' }, ]; -export function CitySelectionDialog({ - isOpen, - onClose, - onCitySelect +export function CitySelectionDialog({ + isOpen, + onClose, + onCitySelect }: CitySelectionDialogProps) { const [searchQuery, setSearchQuery] = useState(''); const navigate = useNavigate(); - const filteredCities = cities.filter(city => - city.name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const filteredCities = useMemo(() => + cities.filter(city => + city.name.toLowerCase().includes(searchQuery.toLowerCase()) + ), [searchQuery]); + const handleCityClick = (city: City) => { console.log('Selected city:', city.name); - + // ✅ Call the onCitySelect callback if provided (passing cityId) if (onCitySelect) { onCitySelect(city.id); @@ -52,7 +54,7 @@ export function CitySelectionDialog({ // ✅ Default behavior: navigate to passes page navigate(`/passes?city=${encodeURIComponent(city.name)}`); } - + onClose(); }; @@ -98,16 +100,15 @@ export function CitySelectionDialog({ handleCityClick(city)} - initial={{ opacity: 0, scale: 0.9 }} - animate={{ opacity: 1, scale: 1 }} - exit={{ opacity: 0, scale: 0.9 }} - transition={{ delay: index * 0.05 }} + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + transition={{ duration: 0.2 }} whileHover={{ scale: 1.03 }} - whileTap={{ scale: 0.98 }} + className="relative h-28 rounded-2xl overflow-hidden group cursor-pointer" > diff --git a/src/components/MelbournePage.tsx b/src/components/MelbournePage.tsx index 64abd05..96f0ca6 100644 --- a/src/components/MelbournePage.tsx +++ b/src/components/MelbournePage.tsx @@ -13,7 +13,7 @@ import { EnhancedTestimonials } from './EnhancedTestimonials'; import { MobileAppPromotion } from './MobileAppPromotion'; import { MelbourneFAQ } from './MelbourneFAQ'; import { Footer } from './Footer'; -import { MinimalHeroBanner } from './MinimalHeroBanner'; +// import { MinimalHeroBanner } from './MinimalHeroBanner'; import { Layout } from '../Layout'; import { HeroBannerCarousel } from './HeroBannerCarousel'; import { HotelEsimOffers } from './HotelEsimOffers'; @@ -257,12 +257,12 @@ export function MelbournePage({ {/* Attractions Section */}
- { }} /> +
{/* Pass Comparison */}
- { }} /> +
{/* Tour Overview */} @@ -280,7 +280,7 @@ export function MelbournePage({ {/* Blogs */}
- { }} /> +
{/* Custom Postcards */} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index bdd2a3a..d35d2eb 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -472,7 +472,7 @@ export default function Navbar({ onClick={(e) => e.stopPropagation()} > {title && ( -
+

{title}

)} From 3b920c2461870962291733c495410a78823db44c Mon Sep 17 00:00:00 2001 From: aryabenade Date: Thu, 19 Mar 2026 15:12:44 +0530 Subject: [PATCH 08/12] show all attractions with filter and search functionalities --- src/Redux/Store.tsx | 6 +- src/Redux/services/attractions.service.ts | 40 ++ src/components/AttractionsPage.tsx | 561 +++++++++++----------- vite.config.ts | 2 +- 4 files changed, 324 insertions(+), 285 deletions(-) create mode 100644 src/Redux/services/attractions.service.ts diff --git a/src/Redux/Store.tsx b/src/Redux/Store.tsx index fde51a7..f4aab57 100644 --- a/src/Redux/Store.tsx +++ b/src/Redux/Store.tsx @@ -1,9 +1,12 @@ import { configureStore } from "@reduxjs/toolkit"; import { fakeApi } from "./services/fakeApi.service"; +import { attractionsApi } from "./services/attractions.service"; export const store = configureStore({ reducer: { - [fakeApi.reducerPath]:fakeApi.reducer + [fakeApi.reducerPath]:fakeApi.reducer, + [attractionsApi.reducerPath]:attractionsApi.reducer + }, @@ -11,6 +14,7 @@ export const store = configureStore({ getDefaultMiddleware().concat( fakeApi.middleware, +attractionsApi.middleware ), }); diff --git a/src/Redux/services/attractions.service.ts b/src/Redux/services/attractions.service.ts new file mode 100644 index 0000000..ac73733 --- /dev/null +++ b/src/Redux/services/attractions.service.ts @@ -0,0 +1,40 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; + +export const attractionsApi = createApi({ + reducerPath: 'attractionsApi', + baseQuery: fetchBaseQuery({ + baseUrl: 'https://testingapi.citycards.betadelivery.com', + }), + endpoints: (builder) => ({ + getAttractionFilters: builder.query({ + // cityId is passed as the query param + query: (cityId) => `/attractions/customer/filters?cityXid=${cityId}`, + }), + + getCustomerAttractions: builder.query({ + // cityId is required, others optional + query: ({ cityId, categoryId, isBookingRequired, cardType, search }) => { + const params = new URLSearchParams(); + + // required + params.append('cityXid', cityId); + + // optional + if (categoryId) params.append('categoryXid', categoryId); + if (isBookingRequired !== undefined) params.append('isBookingRequired', isBookingRequired); + if (cardType) params.append('cardType', cardType); + if (search) params.append('search', search); + + return `/attractions/customer/customer-attractions?${params.toString()}`; + }, + }), + + getAttractionDetailsById: builder.query({ + query: (id: number) => `/attractions/customer/${id}`, + }), + + + }), +}); + +export const { useGetAttractionFiltersQuery,useGetCustomerAttractionsQuery,useGetAttractionDetailsByIdQuery } = attractionsApi; \ No newline at end of file diff --git a/src/components/AttractionsPage.tsx b/src/components/AttractionsPage.tsx index 66fea7c..c7a97a8 100644 --- a/src/components/AttractionsPage.tsx +++ b/src/components/AttractionsPage.tsx @@ -1,7 +1,7 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { motion } from 'motion/react'; import { Search, Star, Clock } from 'lucide-react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Card, CardContent } from './ui/card'; @@ -9,12 +9,11 @@ import { Badge } from './ui/badge'; import { Checkbox } from './ui/checkbox'; import { ImageWithFallback } from './figma/ImageWithFallback'; import { Layout } from '../Layout'; - +import { useGetAttractionFiltersQuery, useGetCustomerAttractionsQuery } from '../Redux/services/attractions.service'; interface User { email: string; name: string; } - interface Attraction { id: string; name: string; @@ -30,191 +29,187 @@ interface Attraction { passType: string; } -const attractions: Attraction[] = [ - { - id: '1', - name: 'Centipede Tour - Guided Arizona Desert Tour by ATV', - description: 'Experience the thrill of off-road adventure through the stunning Arizona desert landscape', - image: 'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhdHYlMjBkZXNlcnQlMjB0b3VyfGVufDF8fHx8MTc1ODEwNDg5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'Paris, France', - duration: '4 days', - rating: 4.8, - price: 189.25, - category: 'adventure', - hasReservation: true, - reviewCount: 243, - passType: 'unlimited' - }, - { - id: '2', - name: 'Molokini and Turtle Town Snorkeling Adventure Aboard', - description: 'Snorkel in crystal-clear waters and swim alongside sea turtles in this unforgettable marine adventure', - image: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'New York, USA', - duration: '4 days', - rating: 4.8, - price: 225, - category: 'adventure', - hasReservation: false, - reviewCount: 167, - passType: 'selective' - }, - { - id: '3', - name: 'Westminster Walking Tour & Westminster Abbey Entry', - description: 'Explore the heart of London with guided tours of historic Westminster and the famous Abbey', - image: 'https://images.unsplash.com/photo-1533929736458-ca588d08c8be?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx3ZXN0bWluc3RlciUyMGFiYmV5JTIwbG9uZG9ufGVufDF8fHx8MTc1ODEwNDkwNnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'London, UK', - duration: '4 days', - rating: 4.8, - price: 343, - category: 'culture', - hasReservation: true, - reviewCount: 343, - passType: 'unlimited' - }, - { - id: '4', - name: 'All Inclusive Ultimate Circle Island Day Tour with Lunch', - description: 'Comprehensive island tour including all major attractions, lunch, and transportation', - image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxpc2xhbmQlMjB0b3VyJTIwYWRvJTIwdHJvcGljYWx8ZW58MXx8fHwxNzU4MTA0OTEwfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'New York, USA', - duration: '4 days', - rating: 4.8, - price: 225, - category: 'adventure', - hasReservation: false, - reviewCount: 243, - passType: 'unlimited' - }, - { - id: '5', - name: 'Space Center Houston Admission Ticket', - description: 'Explore NASA\'s Johnson Space Center and discover the wonders of space exploration', - image: 'https://images.unsplash.com/photo-1446776653964-20c1d3a81b06?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzcGFjZSUyMGNlbnRlciUyMG5hc2ElMjBob3VzdG9ufGVufDF8fHx8MTc1ODEwNDkxM3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'Paris, France', - duration: '4 days', - rating: 4.8, - price: 225, - category: 'family', - hasReservation: true, - reviewCount: 243, - passType: 'selective' - }, - { - id: '6', - name: 'Melbourne Skydeck Observatory', - description: 'Experience breathtaking 360-degree views from the Southern Hemisphere\'s highest viewing platform', - image: 'https://images.unsplash.com/photo-1677200922658-d0df5b2ac91e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBhdHRyYWN0aW9ucyUyMGZhbW91cyUyMGxhbmRtYXJrc3xlbnwxfHx8fDE3NTc0MDEwODV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'Melbourne CBD', - duration: '2 hours', - rating: 4.5, - price: 25, - category: 'adventure', - hasReservation: true, - reviewCount: 892, - passType: 'selective' - }, - { - id: '7', - name: 'Royal Botanic Gardens Melbourne', - description: 'Explore 38 hectares of stunning gardens featuring over 8,500 species of plants', - image: 'https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zfGVufDF8fHx8MTc1NzMzNzc4OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'South Yarra', - duration: '3 hours', - rating: 4.7, - price: 0, - category: 'nature', - hasReservation: false, - reviewCount: 1245, - passType: 'selective' - }, - { - id: '8', - name: 'Federation Square Cultural Precinct', - description: 'Melbourne\'s cultural precinct featuring galleries, museums, and unique architecture', - image: 'https://images.unsplash.com/photo-1580688027085-8220709e3d84?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxmZWRlcmF0aW9uJTIwc3F1YXJlJTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5Mnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'Melbourne CBD', - duration: '3 hours', - rating: 4.3, - price: 0, - category: 'culture', - hasReservation: true, - reviewCount: 672, - passType: 'unlimited' - }, - { - id: '9', - name: 'St Kilda Pier & Little Penguins', - description: 'Watch little penguins return home at sunset while enjoying the scenic pier', - image: 'https://images.unsplash.com/photo-1597889790884-2bb63cfbd4f6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzdCUyMGtpbGRhJTIwcGllciUyMG1lbGJvdXJuZXxlbnwxfHx8fDE3NTc0MDEwOTV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'St Kilda', - duration: '2 hours', - rating: 4.4, - price: 0, - category: 'nature', - hasReservation: false, - reviewCount: 543, - passType: 'unlimited' - }, - { - id: '10', - name: 'Queen Victoria Market Experience', - description: 'Historic market offering fresh produce, gourmet foods, and unique souvenirs', - image: 'https://images.unsplash.com/photo-1676454953709-e0be46f62490?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0JTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5OHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'Melbourne CBD', - duration: '2 hours', - rating: 4.6, - price: 0, - category: 'culture', - hasReservation: true, - reviewCount: 987, - passType: 'selective' - }, - { - id: '11', - name: 'Melbourne Zoo Adventure', - description: 'Meet over 320 animal species from around the world in naturalistic habitats', - image: 'https://images.unsplash.com/photo-1681429477985-30dc7b88dd5b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjB6b28lMjBhbmltYWxzfGVufDF8fHx8MTc1NzMzNzgxMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'Parkville', - duration: '4 hours', - rating: 4.5, - price: 40, - category: 'family', - hasReservation: false, - reviewCount: 1156, - passType: 'selective' - }, - { - id: '12', - name: 'Great Ocean Road Day Tour', - description: 'Scenic coastal drive featuring the famous Twelve Apostles and stunning ocean views', - image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZCUyMGF1c3RyYWxpYXxlbnwxfHx8fDE3NTgxMDQ5Mzd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', - location: 'Great Ocean Road', - duration: '12 hours', - rating: 4.9, - price: 85, - category: 'adventure', - hasReservation: true, - reviewCount: 678, - passType: 'unlimited' - } -]; - -const filterCategories = [ - { value: 'with-reservation', label: 'With Reservation', count: 3 }, - { value: 'without-reservation', label: 'Without Reservation', count: 3 }, - { value: 'beach', label: 'Beach', count: 3 }, - { value: 'adventure', label: 'Adventure', count: 3 }, - { value: 'mountains', label: 'Mountains', count: 3 }, - { value: 'family', label: 'Family Friendly', count: 3 } -]; - -const passTypeCategories = [ - { value: 'selective', label: 'Flexi Pass', count: 6 }, - { value: 'unlimited', label: 'Unlimited Pass', count: 6 } -]; - +// { +// id: '1', +// name: 'Centipede Tour - Guided Arizona Desert Tour by ATV', +// description: 'Experience the thrill of off-road adventure through the stunning Arizona desert landscape', +// image: 'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhdHYlMjBkZXNlcnQlMjB0b3VyfGVufDF8fHx8MTc1ODEwNDg5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'Paris, France', +// duration: '4 days', +// rating: 4.8, +// price: 189.25, +// category: 'adventure', +// hasReservation: true, +// reviewCount: 243, +// passType: 'unlimited' +// }, +// { +// id: '2', +// name: 'Molokini and Turtle Town Snorkeling Adventure Aboard', +// description: 'Snorkel in crystal-clear waters and swim alongside sea turtles in this unforgettable marine adventure', +// image: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'New York, USA', +// duration: '4 days', +// rating: 4.8, +// price: 225, +// category: 'adventure', +// hasReservation: false, +// reviewCount: 167, +// passType: 'selective' +// }, +// { +// id: '3', +// name: 'Westminster Walking Tour & Westminster Abbey Entry', +// description: 'Explore the heart of London with guided tours of historic Westminster and the famous Abbey', +// image: 'https://images.unsplash.com/photo-1533929736458-ca588d08c8be?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx3ZXN0bWluc3RlciUyMGFiYmV5JTIwbG9uZG9ufGVufDF8fHx8MTc1ODEwNDkwNnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'London, UK', +// duration: '4 days', +// rating: 4.8, +// price: 343, +// category: 'culture', +// hasReservation: true, +// reviewCount: 343, +// passType: 'unlimited' +// }, +// { +// id: '4', +// name: 'All Inclusive Ultimate Circle Island Day Tour with Lunch', +// description: 'Comprehensive island tour including all major attractions, lunch, and transportation', +// image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxpc2xhbmQlMjB0b3VyJTIwYWRvJTIwdHJvcGljYWx8ZW58MXx8fHwxNzU4MTA0OTEwfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'New York, USA', +// duration: '4 days', +// rating: 4.8, +// price: 225, +// category: 'adventure', +// hasReservation: false, +// reviewCount: 243, +// passType: 'unlimited' +// }, +// { +// id: '5', +// name: 'Space Center Houston Admission Ticket', +// description: 'Explore NASA\'s Johnson Space Center and discover the wonders of space exploration', +// image: 'https://images.unsplash.com/photo-1446776653964-20c1d3a81b06?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzcGFjZSUyMGNlbnRlciUyMG5hc2ElMjBob3VzdG9ufGVufDF8fHx8MTc1ODEwNDkxM3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'Paris, France', +// duration: '4 days', +// rating: 4.8, +// price: 225, +// category: 'family', +// hasReservation: true, +// reviewCount: 243, +// passType: 'selective' +// }, +// { +// id: '6', +// name: 'Melbourne Skydeck Observatory', +// description: 'Experience breathtaking 360-degree views from the Southern Hemisphere\'s highest viewing platform', +// image: 'https://images.unsplash.com/photo-1677200922658-d0df5b2ac91e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBhdHRyYWN0aW9ucyUyMGZhbW91cyUyMGxhbmRtYXJrc3xlbnwxfHx8fDE3NTc0MDEwODV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'Melbourne CBD', +// duration: '2 hours', +// rating: 4.5, +// price: 25, +// category: 'adventure', +// hasReservation: true, +// reviewCount: 892, +// passType: 'selective' +// }, +// { +// id: '7', +// name: 'Royal Botanic Gardens Melbourne', +// description: 'Explore 38 hectares of stunning gardens featuring over 8,500 species of plants', +// image: 'https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zfGVufDF8fHx8MTc1NzMzNzc4OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'South Yarra', +// duration: '3 hours', +// rating: 4.7, +// price: 0, +// category: 'nature', +// hasReservation: false, +// reviewCount: 1245, +// passType: 'selective' +// }, +// { +// id: '8', +// name: 'Federation Square Cultural Precinct', +// description: 'Melbourne\'s cultural precinct featuring galleries, museums, and unique architecture', +// image: 'https://images.unsplash.com/photo-1580688027085-8220709e3d84?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxmZWRlcmF0aW9uJTIwc3F1YXJlJTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5Mnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'Melbourne CBD', +// duration: '3 hours', +// rating: 4.3, +// price: 0, +// category: 'culture', +// hasReservation: true, +// reviewCount: 672, +// passType: 'unlimited' +// }, +// { +// id: '9', +// name: 'St Kilda Pier & Little Penguins', +// description: 'Watch little penguins return home at sunset while enjoying the scenic pier', +// image: 'https://images.unsplash.com/photo-1597889790884-2bb63cfbd4f6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzdCUyMGtpbGRhJTIwcGllciUyMG1lbGJvdXJuZXxlbnwxfHx8fDE3NTc0MDEwOTV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'St Kilda', +// duration: '2 hours', +// rating: 4.4, +// price: 0, +// category: 'nature', +// hasReservation: false, +// reviewCount: 543, +// passType: 'unlimited' +// }, +// { +// id: '10', +// name: 'Queen Victoria Market Experience', +// description: 'Historic market offering fresh produce, gourmet foods, and unique souvenirs', +// image: 'https://images.unsplash.com/photo-1676454953709-e0be46f62490?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0JTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5OHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'Melbourne CBD', +// duration: '2 hours', +// rating: 4.6, +// price: 0, +// category: 'culture', +// hasReservation: true, +// reviewCount: 987, +// passType: 'selective' +// }, +// { +// id: '11', +// name: 'Melbourne Zoo Adventure', +// description: 'Meet over 320 animal species from around the world in naturalistic habitats', +// image: 'https://images.unsplash.com/photo-1681429477985-30dc7b88dd5b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjB6b28lMjBhbmltYWxzfGVufDF8fHx8MTc1NzMzNzgxMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'Parkville', +// duration: '4 hours', +// rating: 4.5, +// price: 40, +// category: 'family', +// hasReservation: false, +// reviewCount: 1156, +// passType: 'selective' +// }, +// { +// id: '12', +// name: 'Great Ocean Road Day Tour', +// description: 'Scenic coastal drive featuring the famous Twelve Apostles and stunning ocean views', +// image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZCUyMGF1c3RyYWxpYXxlbnwxfHx8fDE3NTgxMDQ5Mzd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral', +// location: 'Great Ocean Road', +// duration: '12 hours', +// rating: 4.9, +// price: 85, +// category: 'adventure', +// hasReservation: true, +// reviewCount: 678, +// passType: 'unlimited' +// } +// ]; +// const filterCategories = [ +// { value: 'with-reservation', label: 'With Reservation', count: 3 }, +// { value: 'without-reservation', label: 'Without Reservation', count: 3 }, +// { value: 'beach', label: 'Beach', count: 3 }, +// { value: 'adventure', label: 'Adventure', count: 3 }, +// { value: 'mountains', label: 'Mountains', count: 3 }, +// { value: 'family', label: 'Family Friendly', count: 3 } +// ]; +// const passTypeCategories = [ +// { value: 'selective', label: 'Flexi Pass', count: 6 }, +// { value: 'unlimited', label: 'Unlimited Pass', count: 6 } +// ]; interface AttractionsPageProps { onSignInClick: () => void; onSignOutClick?: () => void; @@ -226,55 +221,73 @@ export function AttractionsPage({ onSignOutClick, user }: AttractionsPageProps) { + const navigate = useNavigate(); - const [searchQuery, setSearchQuery] = useState(''); - const [selectedCategories, setSelectedCategories] = useState([]); - const [selectedPassTypes, setSelectedPassTypes] = useState([]); - const filteredAttractions = attractions.filter(attraction => { - const matchesSearch = attraction.name.toLowerCase().includes(searchQuery.toLowerCase()) || - attraction.description.toLowerCase().includes(searchQuery.toLowerCase()); + const [search, setSearch] = useState(""); + const [isBookingRequired, setIsBookingRequired] = useState(undefined) + const [selectedCategory, setSelectedCategory] = useState(null); + const [selectedPassType, setSelectedPassType] = useState(null); - const matchesCategory = selectedCategories.length === 0 || - selectedCategories.some(cat => { - if (cat === 'with-reservation') return attraction.hasReservation; - if (cat === 'without-reservation') return !attraction.hasReservation; - return attraction.category === cat; - }); - - const matchesPassType = selectedPassTypes.length === 0 || - selectedPassTypes.includes(attraction.passType); - - return matchesSearch && matchesCategory && matchesPassType; + const cityId = 1 + + const { data: filterData, isLoading } = useGetAttractionFiltersQuery(cityId) + const { data: attractions } = useGetCustomerAttractionsQuery({ + cityId, // required + categoryId: selectedCategory, // optional + isBookingRequired, // optional + cardType: selectedPassType, // optional + search, // optional }); - - const toggleCategory = (category: string) => { - setSelectedCategories(prev => - prev.includes(category) - ? prev.filter(c => c !== category) - : [...prev, category] - ); - }; - - const togglePassType = (passType: string) => { - setSelectedPassTypes(prev => - prev.includes(passType) - ? prev.filter(p => p !== passType) - : [...prev, passType] - ); - }; + + if (isLoading) { + return
Loading...
+ } const handleAttractionClick = (attractionId: string) => { navigate(`/attractions/${attractionId}`); }; - const handleCheckoutClick = () => { navigate('/checkout'); }; const showingFrom = 1; - const showingTo = Math.min(12, filteredAttractions.length); - const totalItems = filteredAttractions.length; + const showingTo = Math.min(12, attractions?.length); + const totalItems = attractions?.length; + + function handlePassTypeSelection(key: string, checked: boolean) { + if (checked) { + setSelectedPassType(key); // only keep the newly selected one + } else { + setSelectedPassType(null); // reset if unchecked + } + } + + function handleCategorySelection(id: number, checked: boolean) { + if (checked) { + if (id === 50) { + setIsBookingRequired(true); + setSelectedCategory(null); // clear normal category + } else if (id === 51) { + setIsBookingRequired(false); + setSelectedCategory(null); // clear normal category + } else { + setSelectedCategory(id); + setIsBookingRequired(undefined); // clear booking filter + } + } else { + // reset if unchecked + if (id === 50 || id === 51) { + setIsBookingRequired(undefined); + } else { + setSelectedCategory(null); + } + } + } + + const handleSearchChange = (e: React.ChangeEvent) => { + setSearch(e.target.value) + } return (
- {/* City Card Promotional Banner */}
@@ -304,20 +316,18 @@ export function AttractionsPage({

Find Your Perfect Adventure

- {/* Search Bar and Button Container */}
{/* Search Bar */}
setSearchQuery(e.target.value)} + value={search} + onChange={handleSearchChange} className="pl-4 pr-12 h-[44px] bg-white/95 backdrop-blur-sm border-0 rounded-lg text-gray-800 placeholder:text-gray-500 font-poppins shadow-lg" />
- {/* Call-to-Action Button */}
- {/* Decorative background elements */}
-
{/* Left Sidebar */}
@@ -344,22 +352,29 @@ export function AttractionsPage({

Search by

- {/* Filter categories */}
- {filterCategories.map(category => ( -
+ {filterData && filterData.categories.map((category: any) => ( +
toggleCategory(category.value)} + id={String(category.id)} + checked={ + category.id === 50 + ? isBookingRequired === true + : category.id === 51 + ? isBookingRequired === false + : selectedCategory === category.id + } + onCheckedChange={(checked: boolean) => + handleCategorySelection(category.id, checked) + } className="border-[#bebebe]" />
))} @@ -367,51 +382,49 @@ export function AttractionsPage({ {/* Divider */}
- {/* Pass Type header */}

Pass Type

- {/* Pass Type filters */}
- {passTypeCategories.map(passType => ( -
+ {filterData && Object.entries(filterData.passType).map(([key, count]) => ( +
togglePassType(passType.value)} + id={key} + checked={selectedPassType === key} + onCheckedChange={(checked: boolean) => + handlePassTypeSelection(key, checked as boolean) + } className="border-[#bebebe]" />
))}
+
- {/* Main Content */}
{/* Header */}

Attractions in Melbourne

- {/* Results count */}

Showing {showingFrom}-{showingTo} of {totalItems} Item(s)

- {/* Attractions Grid */}
- {filteredAttractions.slice(0, 12).map((attraction) => ( + {attractions && attractions.map((attraction: any) => ( FREE - ) : attraction.passType === 'unlimited' ? ( - - Unlimited Pass Exclusive - - ) : ( + ) : attraction.cards[0].cardType.cardTypeDisplayName === "Flexi card" ? ( Flexi Pass + ) : ( + + Unlimited Pass Exclusive + )}
- {attraction.location} + {/* {attraction.location} */}

- {attraction.name} + {attraction.title}

-
-
- {[...Array(5)].map((_, i) => ( - - ))} - - {attraction.rating} ({attraction.reviewCount}) - -
-
- {/* Pricing and Pass Info */}
- {attraction.duration} + {attraction.durations} minutes
Normal visit price
- ${attraction.price} + ${attraction.ticketPriceAdult}
- {/* Included with Pass CTA */}

- ✓ Included with {attraction.passType === 'unlimited' ? 'Unlimited' : 'Selective'} Pass + ✓ Included with {attraction.cards[0].cardType.cardTypeDisplayName === "Flexi card" ? 'Flexi' : 'Unlimited'} Pass

- Save ${attraction.price} + Save ${attraction.cards[0].adultPrice}

    - {featuredAttraction.highlights.map((highlight, index) => ( -
  • + {attraction.attractionHighlights.map((highlight: any) => ( +
  • - {highlight} + {highlight.title}
  • ))}
@@ -220,30 +203,32 @@ export function AttractionDetailsPage({ Included - {featuredAttraction.included.map((item, index) => ( -
-
- + {attraction.attractionInclusions.filter((inclusion: any) => inclusion.isInclusion === true) + .map((inclusion: any) => ( +
+
+ +
+ {inclusion.title}
- {item} -
- ))} + ))}
- + {/* Not Included */}

Not Included

- {featuredAttraction.notIncluded.map((item, index) => ( -
-
- + {attraction.attractionInclusions.filter((inclusion: any) => inclusion.isInclusion === false) + .map((inclusion: any) => ( +
+
+ +
+ {inclusion.title}
- {item} -
- ))} + ))}
@@ -262,7 +247,8 @@ export function AttractionDetailsPage({

Interactive Map

-

Phi Phi Islands, Thailand

+

{attraction.title}

+

{attraction.address}

@@ -276,7 +262,7 @@ export function AttractionDetailsPage({

Select Date

Choose your preferred visit date

- + {/* Custom Calendar Design */}
{/* Calendar Header */} @@ -305,7 +291,7 @@ export function AttractionDetailsPage({
{/* Previous month */} - + {/* Current month */} {Array.from({ length: 30 }, (_, i) => { const day = i + 1; @@ -314,13 +300,12 @@ export function AttractionDetailsPage({ return ( @@ -356,7 +341,7 @@ export function AttractionDetailsPage({
Adult Ticket - $89 + {attraction.ticketPriceAdult}
Service Fee @@ -365,14 +350,14 @@ export function AttractionDetailsPage({
Total - $94 + ${attraction.ticketPriceAdult + 5}
{/* Confirm Booking Button */} -
- + ); } \ No newline at end of file diff --git a/src/components/AttractionsPage.tsx b/src/components/AttractionsPage.tsx index c7a97a8..7df14cb 100644 --- a/src/components/AttractionsPage.tsx +++ b/src/components/AttractionsPage.tsx @@ -403,7 +403,7 @@ export function AttractionsPage({ htmlFor={key} className="font-poppins text-sm text-[#414141] cursor-pointer flex-1 font-normal" > - {key} ({count as number}) + {key==="selective_pass" ?"Selective":"Unlimited"} ({count as number})
))} @@ -460,9 +460,9 @@ export function AttractionsPage({ -
- {/* {attraction.location} */} -
+ {/*
+ {attraction.location} +
*/}

{attraction.title}

From a09d53db7df3be2ddc860fde311c75d42c04030d Mon Sep 17 00:00:00 2001 From: aryabenade Date: Fri, 20 Mar 2026 11:45:14 +0530 Subject: [PATCH 10/12] integrate cities api in the CitySelectionDialog --- src/Redux/Store.tsx | 8 +-- src/Redux/services/attractions.service.ts | 1 - src/Redux/services/cities.service.ts | 22 ++++++++ src/components/CitySelectionDialog.tsx | 63 ++++++++++++----------- 4 files changed, 60 insertions(+), 34 deletions(-) create mode 100644 src/Redux/services/cities.service.ts diff --git a/src/Redux/Store.tsx b/src/Redux/Store.tsx index f4aab57..7d03f11 100644 --- a/src/Redux/Store.tsx +++ b/src/Redux/Store.tsx @@ -1,11 +1,13 @@ import { configureStore } from "@reduxjs/toolkit"; import { fakeApi } from "./services/fakeApi.service"; import { attractionsApi } from "./services/attractions.service"; +import { citiesApi } from "./services/cities.service"; export const store = configureStore({ reducer: { [fakeApi.reducerPath]:fakeApi.reducer, - [attractionsApi.reducerPath]:attractionsApi.reducer + [attractionsApi.reducerPath]:attractionsApi.reducer, + [citiesApi.reducerPath]:citiesApi.reducer }, @@ -14,8 +16,8 @@ export const store = configureStore({ getDefaultMiddleware().concat( fakeApi.middleware, -attractionsApi.middleware - +attractionsApi.middleware, +citiesApi.middleware ), }); export type RootState = ReturnType; diff --git a/src/Redux/services/attractions.service.ts b/src/Redux/services/attractions.service.ts index ac73733..dcbae38 100644 --- a/src/Redux/services/attractions.service.ts +++ b/src/Redux/services/attractions.service.ts @@ -33,7 +33,6 @@ export const attractionsApi = createApi({ query: (id: number) => `/attractions/customer/${id}`, }), - }), }); diff --git a/src/Redux/services/cities.service.ts b/src/Redux/services/cities.service.ts new file mode 100644 index 0000000..44caf81 --- /dev/null +++ b/src/Redux/services/cities.service.ts @@ -0,0 +1,22 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; + +export const citiesApi = createApi({ + reducerPath: 'citiesApi', + baseQuery: fetchBaseQuery({ + baseUrl: 'https://testingapi.citycards.betadelivery.com', + }), + endpoints: (builder) => ({ + + getCityListWithBanner: builder.query({ + query: ({ search }) => { + const params = new URLSearchParams(); + + if (search) params.append('search', search); + + return `/cities/list/customer/cities?${params.toString()}` + } + }) + }), +}); + +export const { useGetCityListWithBannerQuery} = citiesApi; \ No newline at end of file diff --git a/src/components/CitySelectionDialog.tsx b/src/components/CitySelectionDialog.tsx index 4921f9e..ec0efff 100644 --- a/src/components/CitySelectionDialog.tsx +++ b/src/components/CitySelectionDialog.tsx @@ -6,11 +6,12 @@ import { ArrowLeft, Search } from 'lucide-react'; import { Input } from './ui/input'; import { motion, AnimatePresence } from 'motion/react'; import { ImageWithFallback } from './figma/ImageWithFallback'; +import { useGetCityListWithBannerQuery } from '../Redux/services/cities.service'; interface City { - id: string; - name: string; - imageUrl: string; + id: number; + cityName: string; + bannerImage: string; } interface CitySelectionDialogProps { @@ -19,45 +20,47 @@ interface CitySelectionDialogProps { onCitySelect?: (cityId: string) => void; // ✅ Updated to pass cityId } -const cities: City[] = [ - { id: 'melbourne', name: 'Melbourne', imageUrl: 'https://images.unsplash.com/photo-1624341373902-70e3a8dc9acc?...' }, - { id: 'new-york', name: 'New York', imageUrl: 'https://images.unsplash.com/photo-1514565131-fce0801e5785?...' }, - { id: 'abu-dhabi', name: 'Abu Dhabi', imageUrl: 'https://images.unsplash.com/photo-1584551246679-0daf3d275d0f?...' }, - { id: 'dubai', name: 'Dubai', imageUrl: 'https://images.unsplash.com/photo-1518684079-3c830dcef090?...' }, - { id: 'tokyo', name: 'Tokyo', imageUrl: 'https://images.unsplash.com/photo-1613487897980-50cc440ce118?...' }, - { id: 'ontario', name: 'Ontario', imageUrl: 'https://images.unsplash.com/photo-1542704792-e30dac463c90?...' }, - { id: 'mumbai', name: 'Mumbai', imageUrl: 'https://images.unsplash.com/photo-1600867161422-79f8f6e08c84?...' }, - { id: 'louisiana', name: 'Louisiana', imageUrl: 'https://images.unsplash.com/photo-1646508262200-455d62c22182?...' }, -]; - export function CitySelectionDialog({ isOpen, onClose, onCitySelect }: CitySelectionDialogProps) { - const [searchQuery, setSearchQuery] = useState(''); + const [search, setSearch] = useState(''); const navigate = useNavigate(); - const filteredCities = useMemo(() => - cities.filter(city => - city.name.toLowerCase().includes(searchQuery.toLowerCase()) - ), [searchQuery]); + const { data: cities, isLoading } = useGetCityListWithBannerQuery({ search }) + + if (isLoading) { + return
Loading...
+ } const handleCityClick = (city: City) => { - console.log('Selected city:', city.name); + console.log('Selected city:', city.cityName); // ✅ Call the onCitySelect callback if provided (passing cityId) if (onCitySelect) { - onCitySelect(city.id); + onCitySelect(String(city.id)); } else { // ✅ Default behavior: navigate to passes page - navigate(`/passes?city=${encodeURIComponent(city.name)}`); + navigate(`/passes?city=${encodeURIComponent(city.cityName)}`); } onClose(); }; + const handleSearchChange = (e: React.ChangeEvent) => { + setSearch(e.target.value) + } + + const filteredCities = useMemo(() => + cities?.filter((city: City) => + city.cityName.toLowerCase().includes(search.toLowerCase()) + ) ?? [], + [cities, search] + ); + + return ( @@ -85,8 +88,8 @@ export function CitySelectionDialog({ setSearchQuery(e.target.value)} + value={search} + onChange={handleSearchChange} className="pl-10 bg-input border-0 rounded-lg h-11 font-poppins placeholder:text-gray-400" /> @@ -96,7 +99,7 @@ export function CitySelectionDialog({
- {filteredCities.map((city, index) => ( + {filteredCities && filteredCities.map((city: City) => ( handleCityClick(city)} @@ -108,14 +111,14 @@ export function CitySelectionDialog({ className="relative h-28 rounded-2xl overflow-hidden group cursor-pointer" >

- {city.name} + {city.cityName}

@@ -123,10 +126,10 @@ export function CitySelectionDialog({
- {filteredCities.length === 0 && ( + {filteredCities?.length === 0 && (

- No cities found matching "{searchQuery}" + No cities found matching "{search}"

)} From 0a60ba58a3f2ed0cfa58fe3924aa6f88d32d2b63 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Fri, 20 Mar 2026 11:49:11 +0530 Subject: [PATCH 11/12] remove the useMemo hook from CitySelectionDialog --- src/components/CitySelectionDialog.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/CitySelectionDialog.tsx b/src/components/CitySelectionDialog.tsx index ec0efff..203aa72 100644 --- a/src/components/CitySelectionDialog.tsx +++ b/src/components/CitySelectionDialog.tsx @@ -53,14 +53,6 @@ export function CitySelectionDialog({ setSearch(e.target.value) } - const filteredCities = useMemo(() => - cities?.filter((city: City) => - city.cityName.toLowerCase().includes(search.toLowerCase()) - ) ?? [], - [cities, search] - ); - - return ( @@ -99,7 +91,7 @@ export function CitySelectionDialog({
- {filteredCities && filteredCities.map((city: City) => ( + {cities && cities.map((city: City) => ( handleCityClick(city)} @@ -126,7 +118,7 @@ export function CitySelectionDialog({
- {filteredCities?.length === 0 && ( + {cities?.length === 0 && (

No cities found matching "{search}" From 5d213d14d846598ff6b29e0b4f4cd4143d4cd564 Mon Sep 17 00:00:00 2001 From: aryabenade Date: Fri, 20 Mar 2026 13:58:39 +0530 Subject: [PATCH 12/12] show upcomingCities from backend on landingPage --- src/Redux/baseQuery.ts | 17 + src/Redux/services/attractions.service.ts | 8 +- src/Redux/services/cities.service.ts | 16 +- src/components/LandingUpcomingCities.tsx | 418 +++++++++++----------- src/components/Navbar.tsx | 4 +- src/vite-env.d.ts | 8 + 6 files changed, 258 insertions(+), 213 deletions(-) create mode 100644 src/Redux/baseQuery.ts create mode 100644 src/vite-env.d.ts diff --git a/src/Redux/baseQuery.ts b/src/Redux/baseQuery.ts new file mode 100644 index 0000000..37c41cc --- /dev/null +++ b/src/Redux/baseQuery.ts @@ -0,0 +1,17 @@ +// src/store/baseQuery.ts +import { fetchBaseQuery } from "@reduxjs/toolkit/query/react"; + +export const baseQuery = fetchBaseQuery({ + baseUrl: import.meta.env.VITE_BASE_URL, + // credentials: "include", + prepareHeaders: (headers) => { + const token = localStorage.getItem("accessToken"); + if (token) { + headers.set("Authorization", `Bearer ${token}`); + // headers.set("access-token", token); + } + // headers.set("Content-Type", "application/json"); + return headers; + }, +}); + diff --git a/src/Redux/services/attractions.service.ts b/src/Redux/services/attractions.service.ts index dcbae38..01b7dee 100644 --- a/src/Redux/services/attractions.service.ts +++ b/src/Redux/services/attractions.service.ts @@ -1,10 +1,12 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { baseQuery } from "../baseQuery"; export const attractionsApi = createApi({ reducerPath: 'attractionsApi', - baseQuery: fetchBaseQuery({ - baseUrl: 'https://testingapi.citycards.betadelivery.com', - }), + // baseQuery: fetchBaseQuery({ + // baseUrl: 'https://testingapi.citycards.betadelivery.com', + // }), + baseQuery, endpoints: (builder) => ({ getAttractionFilters: builder.query({ // cityId is passed as the query param diff --git a/src/Redux/services/cities.service.ts b/src/Redux/services/cities.service.ts index 44caf81..e1dfb06 100644 --- a/src/Redux/services/cities.service.ts +++ b/src/Redux/services/cities.service.ts @@ -1,10 +1,12 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { baseQuery } from "../baseQuery"; export const citiesApi = createApi({ reducerPath: 'citiesApi', - baseQuery: fetchBaseQuery({ - baseUrl: 'https://testingapi.citycards.betadelivery.com', - }), + // baseQuery: fetchBaseQuery({ + // baseUrl: 'https://testingapi.citycards.betadelivery.com', + // }), + baseQuery, endpoints: (builder) => ({ getCityListWithBanner: builder.query({ @@ -15,8 +17,14 @@ export const citiesApi = createApi({ return `/cities/list/customer/cities?${params.toString()}` } + }), + + getUpcomingCities: builder.query({ + + query: (listType) => `/cities/list/all?listType=${listType}`, + }) }), }); -export const { useGetCityListWithBannerQuery} = citiesApi; \ No newline at end of file +export const { useGetCityListWithBannerQuery,useGetUpcomingCitiesQuery } = citiesApi; \ No newline at end of file diff --git a/src/components/LandingUpcomingCities.tsx b/src/components/LandingUpcomingCities.tsx index 0322168..2d724a0 100644 --- a/src/components/LandingUpcomingCities.tsx +++ b/src/components/LandingUpcomingCities.tsx @@ -3,102 +3,103 @@ import { ImageWithFallback } from './figma/ImageWithFallback'; import { Button } from './ui/button'; import { useRef, useState, useEffect } from 'react'; import Image592Traced from '../imports/Image592Traced-5025-559'; +import { useGetUpcomingCitiesQuery } from '../Redux/services/cities.service'; -const upcomingCities = [ - { - id: 1, - name: 'Boston', - country: 'USA', - launchDate: 'Spring 2025', - attractions: 65, - description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.', - image: 'https://images.unsplash.com/photo-1568271667303-14b2a1a36da1?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', - showHoverState: true - }, - { - id: 2, - name: 'Rome', - country: 'Italy', - launchDate: 'Summer 2025', - attractions: 80, - image: 'https://images.unsplash.com/photo-1552832230-c0197dd311b5?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', - showHoverState: false - }, - { - id: 3, - name: 'Paris', - country: 'France', - launchDate: 'Fall 2025', - attractions: 95, - image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', - showHoverState: false - }, - { - id: 4, - name: 'Dubai', - country: 'UAE', - launchDate: 'Winter 2025', - attractions: 70, - image: 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', - showHoverState: false, - badge: 'New' - }, - { - id: 5, - name: 'Tokyo', - country: 'Japan', - launchDate: 'Early 2026', - attractions: 120, - image: 'https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', - showHoverState: false - }, - { - id: 6, - name: 'Sydney', - country: 'Australia', - launchDate: 'Spring 2026', - attractions: 85, - image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', - showHoverState: false - }, - { - id: 7, - name: 'New York', - country: 'USA', - launchDate: 'Summer 2026', - attractions: 150, - image: 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', - showHoverState: false, - badge: 'Most Requested' - }, - { - id: 8, - name: 'Singapore', - country: 'Singapore', - launchDate: 'Fall 2026', - attractions: 75, - image: 'https://images.unsplash.com/photo-1525625293386-3f8f99389edd?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', - showHoverState: false - }, - { - id: 9, - name: 'Amsterdam', - country: 'Netherlands', - launchDate: 'Winter 2026', - attractions: 90, - image: 'https://images.unsplash.com/photo-1534351590666-13e3e96b5017?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', - showHoverState: false - }, - { - id: 10, - name: 'Barcelona', - country: 'Spain', - launchDate: 'Early 2027', - attractions: 110, - image: 'https://images.unsplash.com/photo-1583422409516-2895a77efded?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', - showHoverState: false - } -]; +// const upcomingCities = [ +// { +// id: 1, +// name: 'Boston', +// country: 'USA', +// launchDate: 'Spring 2025', +// attractions: 65, +// description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.', +// image: 'https://images.unsplash.com/photo-1568271667303-14b2a1a36da1?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', +// showHoverState: true +// }, +// { +// id: 2, +// name: 'Rome', +// country: 'Italy', +// launchDate: 'Summer 2025', +// attractions: 80, +// image: 'https://images.unsplash.com/photo-1552832230-c0197dd311b5?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', +// showHoverState: false +// }, +// { +// id: 3, +// name: 'Paris', +// country: 'France', +// launchDate: 'Fall 2025', +// attractions: 95, +// image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', +// showHoverState: false +// }, +// { +// id: 4, +// name: 'Dubai', +// country: 'UAE', +// launchDate: 'Winter 2025', +// attractions: 70, +// image: 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', +// showHoverState: false, +// badge: 'New' +// }, +// { +// id: 5, +// name: 'Tokyo', +// country: 'Japan', +// launchDate: 'Early 2026', +// attractions: 120, +// image: 'https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', +// showHoverState: false +// }, +// { +// id: 6, +// name: 'Sydney', +// country: 'Australia', +// launchDate: 'Spring 2026', +// attractions: 85, +// image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', +// showHoverState: false +// }, +// { +// id: 7, +// name: 'New York', +// country: 'USA', +// launchDate: 'Summer 2026', +// attractions: 150, +// image: 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', +// showHoverState: false, +// badge: 'Most Requested' +// }, +// { +// id: 8, +// name: 'Singapore', +// country: 'Singapore', +// launchDate: 'Fall 2026', +// attractions: 75, +// image: 'https://images.unsplash.com/photo-1525625293386-3f8f99389edd?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', +// showHoverState: false +// }, +// { +// id: 9, +// name: 'Amsterdam', +// country: 'Netherlands', +// launchDate: 'Winter 2026', +// attractions: 90, +// image: 'https://images.unsplash.com/photo-1534351590666-13e3e96b5017?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', +// showHoverState: false +// }, +// { +// id: 10, +// name: 'Barcelona', +// country: 'Spain', +// launchDate: 'Early 2027', +// attractions: 110, +// image: 'https://images.unsplash.com/photo-1583422409516-2895a77efded?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', +// showHoverState: false +// } +// ]; export function LandingUpcomingCities() { const scrollContainerRef = useRef(null); @@ -107,6 +108,15 @@ export function LandingUpcomingCities() { const [scrollLeft, setScrollLeft] = useState(0); const [showDragHint, setShowDragHint] = useState(false); + const listType = "upcomingCity" + // const[listType,setListType]=useState("upcomingCity") + + const { data, isLoading } = useGetUpcomingCitiesQuery(listType) + + if(isLoading){ + return

Loading...
+ } + const handleMouseDown = (e: React.MouseEvent) => { if (!scrollContainerRef.current) return; // Only start dragging if not clicking on a button or interactive element @@ -143,11 +153,11 @@ export function LandingUpcomingCities() { } }; - useEffect(() => { - const handleGlobalMouseUp = () => setIsDragging(false); - document.addEventListener('mouseup', handleGlobalMouseUp); - return () => document.removeEventListener('mouseup', handleGlobalMouseUp); - }, []); + // useEffect(() => { + // const handleGlobalMouseUp = () => setIsDragging(false); + // document.addEventListener('mouseup', handleGlobalMouseUp); + // return () => document.removeEventListener('mouseup', handleGlobalMouseUp); + // }, []); return (
@@ -172,11 +182,11 @@ export function LandingUpcomingCities() {
)} -
- {upcomingCities.map((city) => ( -
- {/* Background - Either solid color or image */} - {city.showHoverState ? ( - // Boston card with image background and same layout as other cards - <> - - - {/* Dark overlay */} -
- - {/* City name overlay - matching Rome card layout */} -
-

{city.name}

-
- {city.country} - {city.launchDate} -
-
+ {data && data?.upcomingCities?.map((city: any) => ( +
+ {/* Background - Either solid color or image */} + {true ? ( + // Boston card with image background and same layout as other cards + <> + - {/* Hover state overlay - same as other cards */} -
-
-

{city.name}

-

{city.attractions}+ attractions

-

Coming {city.launchDate}

- -
-
- - ) : ( - // Image background for other cards - <> - - - {/* Dark overlay */} -
- - {/* Badge (if present) */} - {city.badge && ( -
- {city.badge} -
- )} + {/* Dark overlay */} +
- {/* City name overlay */} -
-

{city.name}

-
- {city.country} - {city.launchDate} -
+ {/* City name overlay - matching Rome card layout */} +
+

{city.cityName}

+
+ {/* {city.country} + {city.launchDate} */}
+
- {/* Hover state overlay */} -
-
-

{city.name}

-

{city.attractions}+ attractions

-

Coming {city.launchDate}

- -
+ {/* Hover state overlay - same as other cards */} +
+
+

{city.cityName}

+ {/*

{city.attractions}+ attractions

+

Coming {city.launchDate}

*/} +
- - )} -
- ))} +
+ + ) : ( + // Image background for other cards + <> + + + {/* Dark overlay */} +
+ + {/* Badge (if present) */} + {/* {city.badge && ( +
+ {city.badge} +
+ )} */} + + {/* City name overlay */} + {/*
+

{city.name}

+
+ {city.country} + {city.launchDate} +
+
*/} + + {/* Hover state overlay */} +
+
+

{city.cityName}

+ {/*

{city.attractions}+ attractions

*/} + {/*

Coming {city.launchDate}

*/} + +
+
+ + )} +
+ ))}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index d35d2eb..900f3b1 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -91,7 +91,7 @@ export default function Navbar({ const { user, login, logout } = useAuth(); // from AuthContext - const protectedPaths = ["/passes", "/whats-included", "/","/melbourne"]; + const protectedPaths = ["/passes", "/whats-included", "/", "/melbourne"]; const handleOpenLoginModal = () => { if (!user && protectedPaths.includes(location.pathname)) { @@ -289,7 +289,7 @@ export default function Navbar({ console.log('City selected from navbar:', cityId); onCityChange(cityId); - if (cityId.toLowerCase() === 'melbourne') { + if (cityId.toLowerCase() === '1') { setNavigationSource('melbourne'); navigate('/melbourne'); } else { diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..8abf8d3 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,8 @@ +interface ImportMetaEnv { + readonly VITE_BASE_URL: string + readonly VITE_GOOGLE_MAP: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} \ No newline at end of file