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/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/AppRouter.tsx b/src/AppRouter.tsx index a58d8bd..1a3c767 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -117,7 +117,7 @@ export function AppRouter({ navigate(-1)} onCheckoutClick={() => navigate('/checkout')} @@ -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 new file mode 100644 index 0000000..7d03f11 --- /dev/null +++ b/src/Redux/Store.tsx @@ -0,0 +1,24 @@ +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, + [citiesApi.reducerPath]:citiesApi.reducer + + }, + + + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat( + +fakeApi.middleware, +attractionsApi.middleware, +citiesApi.middleware + ), +}); +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; \ No newline at end of file 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 new file mode 100644 index 0000000..01b7dee --- /dev/null +++ b/src/Redux/services/attractions.service.ts @@ -0,0 +1,41 @@ +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, + 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/Redux/services/cities.service.ts b/src/Redux/services/cities.service.ts new file mode 100644 index 0000000..e1dfb06 --- /dev/null +++ b/src/Redux/services/cities.service.ts @@ -0,0 +1,30 @@ +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, + endpoints: (builder) => ({ + + getCityListWithBanner: builder.query({ + query: ({ search }) => { + const params = new URLSearchParams(); + + if (search) params.append('search', search); + + return `/cities/list/customer/cities?${params.toString()}` + } + }), + + getUpcomingCities: builder.query({ + + query: (listType) => `/cities/list/all?listType=${listType}`, + + }) + }), +}); + +export const { useGetCityListWithBannerQuery,useGetUpcomingCitiesQuery } = citiesApi; \ 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/components/AttractionDetailsPage.tsx b/src/components/AttractionDetailsPage.tsx index ceac7f1..d92c3b1 100644 --- a/src/components/AttractionDetailsPage.tsx +++ b/src/components/AttractionDetailsPage.tsx @@ -6,6 +6,8 @@ import { Badge } from './ui/badge'; import { Card, } from './ui/card'; import { ImageWithFallback } from './figma/ImageWithFallback'; import { Layout } from '../Layout'; +import { useParams } from 'react-router-dom'; +import { useGetAttractionDetailsByIdQuery } from '../Redux/services/attractions.service'; interface AttractionDetailsPageProps { onBackClick: () => void; @@ -13,7 +15,7 @@ interface AttractionDetailsPageProps { onSignInClick: () => void; onSignOutClick?: () => void; user?: { email: string; name: string } | null; - attractionId: string; + // attractionId: string; } export function AttractionDetailsPage({ @@ -23,74 +25,33 @@ export function AttractionDetailsPage({ onSignOutClick, user, }: AttractionDetailsPageProps) { - const [date, setDate] = useState(new Date()); - // Featured attraction for the main display - const featuredAttraction = { - id: 'phi-phi', - name: 'Phi Phi Islands Adventure Day Trip with Seaview Lunch by V. Marine Tour', - badges: ['Bestseller', 'Free cancellation', 'Reservation Required'], - images: { - main: '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', - gallery: [ - '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', - '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', - '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', - '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' - ] - }, - overview: { - duration: '3 days', - groupSize: '10 people', - ages: '18-99 yrs', - languages: 'English, Japanese' - }, - description: 'The Phi Phi archipelago is a must-visit while in Phuket, and this speedboat trip whisks you around the islands in one day. Swim over the coral reefs of Pileh Lagoon, have lunch at Phi Phi Leh, snorkel at Bamboo Island, and visit Monkey Beach and Maya Bay, immortalized in "The Beach." Boat transfers, snacks, buffet lunch, snorkeling equipment, and Phuket hotel pickup and drop-off all included.', - highlights: [ - 'Experience the thrill of a speedboat to the stunning Phi Phi Islands', - 'Be amazed by the variety of marine life in the archepelago', - 'Enjoy relaxing in paradise with white sand beaches and azure turquoise water', - 'Feel the comfort of a tour limited to 35 passengers', - 'Catch a glimpse of the wild monkeys around Monkey Beach' - ], - included: [ - 'Beverages, drinking water, morning tea and buffet lunch', - 'Local taxes', - 'Hotel pickup and drop-off by air-conditioned minivan', - 'Insurance Transfer to a private pier', - 'Soft drinks', - 'Tour Guide' - ], - notIncluded: [ - 'Towel', - 'Tips', - 'Alcoholic Beverages' - ], - bookingOptions: [ - 'By Calling on 022 2645675', - 'Email your details at islands.booking@mail.com', - 'Via CityCards Portal' - ] - }; + const { attractionId } = useParams() + + const { data: attraction, isLoading } = useGetAttractionDetailsByIdQuery(Number(attractionId)); + + if (isLoading) { + return
loading...
+ } return ( + activeCity="" + onSignInClick={onSignInClick} + onSignOutClick={onSignOutClick} + user={user} + // showCitySubmenu={false} + >
{/* Back Button */} - -
    - {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 66fea7c..7df14cb 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.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}

@@ -94,27 +91,26 @@ export function CitySelectionDialog({
- {filteredCities.map((city, index) => ( + {cities && cities.map((city: City) => ( 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" >

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

@@ -122,10 +118,10 @@ export function CitySelectionDialog({
- {filteredCities.length === 0 && ( + {cities?.length === 0 && (

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

)} 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 ( (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/LoginModal.tsx b/src/components/LoginModal.tsx index 3d94411..e044045 100644 --- a/src/components/LoginModal.tsx +++ b/src/components/LoginModal.tsx @@ -4,14 +4,16 @@ import { X } from 'lucide-react'; 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(['', '', '', '', '', '']); @@ -19,6 +21,8 @@ export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) const [isLoading, setIsLoading] = useState(false); const [helperText, setHelperText] = useState(''); + const { login } = useAuth(); // from AuthContext + // Reset modal state when closed useEffect(() => { if (!isOpen) { @@ -46,7 +50,7 @@ export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) setIsLoading(true); setHelperText(''); - + // Simulate API call setTimeout(() => { setStep('otp'); @@ -58,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); @@ -92,12 +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") onClose(); }, 1500); }; @@ -139,7 +142,7 @@ export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) > - +

Login

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

diff --git a/src/components/MelbournePage.tsx b/src/components/MelbournePage.tsx index abbf7c0..2471181 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'; @@ -254,12 +254,12 @@ export function MelbournePage({ {/* Attractions Section */}

- { }} /> +
{/* Pass Comparison */}
- { }} /> +
{/* Tour Overview */} @@ -277,7 +277,7 @@ export function MelbournePage({ {/* Blogs */}
- { }} /> +
{/* Custom Postcards */} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 5893b72..900f3b1 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 @@ -271,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 { @@ -454,7 +472,7 @@ export default function Navbar({ onClick={(e) => e.stopPropagation()} > {title && ( -
+

{title}

)} @@ -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 diff --git a/src/components/PassesPage.tsx b/src/components/PassesPage.tsx index 24314e1..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); @@ -192,10 +195,10 @@ export function PassesPage({ return (
{/* Page Header */} @@ -759,12 +762,6 @@ export function PassesPage({
- setIsLoginOpen(false)} - onLoginSuccess={handleLoginSuccess} - /> - ); } 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 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..6e0fc0f 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 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 diff --git a/vite.config.ts b/vite.config.ts index 5a178b2..551b307 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -84,7 +84,7 @@ import * as path from 'path'; outDir: 'build', }, server: { - port: 4007, + port: 4008, open: true, }, }); \ No newline at end of file