diff --git a/index.html b/index.html index 000821d..7ce70fd 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,16 @@ + + - - - - - - CityCards Travel 22-8-2025 - + + + + + CityCards Customer-web + - -
- - - - \ No newline at end of file + +
+ + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 13d84a8..af5b3cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,12 +35,15 @@ "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@reduxjs/toolkit": "^2.11.2", + "@stripe/react-stripe-js": "^6.2.0", + "@stripe/stripe-js": "^9.2.0", "@tailwindcss/postcss": "^4.1.13", "@tailwindcss/vite": "^4.1.14", "class-variance-authority": "^0.7.1", "clsx": "*", "cmdk": "^1.1.1", "embla-carousel-react": "^8.6.0", + "i18n-iso-countries": "^7.14.0", "input-otp": "^1.4.2", "lucide-react": "^0.487.0", "motion": "*", @@ -54,7 +57,7 @@ "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.9.4", "recharts": "^2.15.2", - "sonner": "^2.0.3", + "sonner": "^2.0.7", "tailwind-merge": "*", "tailwindcss": "^4.1.14", "vaul": "^1.1.2" @@ -2237,6 +2240,28 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@stripe/react-stripe-js": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-6.2.0.tgz", + "integrity": "sha512-GSCErjljZEQv9LaxP30xGOwstcMyyUzb5JyihXwvjOU95yrfhbiPG4K2KkwxYxn+WY0/AyHsRhPPoGRw7urBzg==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=9.2.0 <10.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-9.2.0.tgz", + "integrity": "sha512-YSzLC0t6VS9MDdPTynSMqU8IxrItFUjkDORALFT6sSMR/XZ5Vgm3RDp/Gk7z727MC4A9s4MFVel0gF0c7+kdrg==", + "peer": true, + "engines": { + "node": ">=12.16" + } + }, "node_modules/@swc/core": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", @@ -3346,6 +3371,11 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/diacritics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", + "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==" + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -3527,6 +3557,17 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/i18n-iso-countries": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz", + "integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==", + "dependencies": { + "diacritics": "1.3.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/immer": { "version": "11.1.4", "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", diff --git a/package.json b/package.json index 2e11959..544a147 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,15 @@ "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@reduxjs/toolkit": "^2.11.2", + "@stripe/react-stripe-js": "^6.2.0", + "@stripe/stripe-js": "^9.2.0", "@tailwindcss/postcss": "^4.1.13", "@tailwindcss/vite": "^4.1.14", "class-variance-authority": "^0.7.1", "clsx": "*", "cmdk": "^1.1.1", "embla-carousel-react": "^8.6.0", + "i18n-iso-countries": "^7.14.0", "input-otp": "^1.4.2", "lucide-react": "^0.487.0", "motion": "*", @@ -49,7 +52,7 @@ "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.9.4", "recharts": "^2.15.2", - "sonner": "^2.0.3", + "sonner": "^2.0.7", "tailwind-merge": "*", "tailwindcss": "^4.1.14", "vaul": "^1.1.2" diff --git a/src/App.tsx b/src/App.tsx index ea15e93..17afb49 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -185,7 +185,7 @@ function App() { {/* Card Title in Orange */} -

+

{stickyCardType === 'unlimited' ? ( <>Melbourne Unlimited Card ) : ( diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 1a3c767..bd664f6 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -2,38 +2,42 @@ import { Routes, Route, useParams, useLocation, useNavigate } from 'react-router import { motion, AnimatePresence } from 'motion/react'; // Import all your pages -import { LoginModal } from './components/LoginModal'; -import { MelbournePage } from './components/MelbournePage'; -import { PassesPage } from './components/PassesPage'; -import { AttractionsPage } from './components/AttractionsPage'; -import { AttractionDetailsPage } from './components/AttractionDetailsPage'; -import { CheckoutPage } from './components/CheckoutPage'; -import { SecureCheckoutPage } from './components/SecureCheckoutPage'; -import { BlogsPage } from './components/BlogsPage'; -import { BlogDetailsPage } from './components/BlogDetailsPage'; -import { HowItWorksPage } from './components/HowItWorksPage'; +import { ProtectedRoute } from './components/ProtectedRoute'; +import { MelbournePage } from './pages/MelbournePage'; +import { PassesPage } from './pages/PassesPage'; +import { AttractionsPage } from './pages/AttractionsPage'; +import { AttractionDetailsPage } from './pages/AttractionDetailsPage'; +import { BlogsPage } from './pages/BlogsPage'; +import { BlogDetailsPage } from './pages/BlogDetailsPage'; import { FAQPage } from './components/FAQPage'; -import { PrivacyPolicyPage } from './components/PrivacyPolicyPage'; -import { AboutUsPage } from './components/AboutUsPage'; -import { ProfilePage } from './components/ProfilePage'; -import { CreateMagicItineraryPage } from './components/CreateMagicItineraryPage'; -import { ItineraryViewPage } from './components/ItineraryViewPage'; -import { OffersPage } from './components/OffersPage'; -import { CityCardsPage } from './components/CityCardsPage'; -import { MagicItineraryPage } from './components/MagicItineraryPage'; -import { PostCardsPage } from './components/PostCardsPage'; -import { DownloadAppPage } from './components/DownloadAppPage'; -import { EsimsPage } from './components/EsimsPage'; -import { HotelDiscountsPage } from './components/HotelDiscountsPage'; -import { ContactUsPage } from './components/ContactUsPage'; - +import { PrivacyPolicyPage } from './pages/PrivacyPolicyPage'; +import { AboutUsPage } from './pages/AboutUsPage'; +import { ProfilePage } from './pages/ProfilePage'; +import { OffersPage } from './pages/OffersPage'; +import { CityCardsPage } from './pages/CityCardsPage'; +import { MagicItineraryPage } from './pages/MagicItineraryPage'; +import { PostCardsPage } from './pages/PostCardsPage'; +import { DownloadAppPage } from './pages/DownloadAppPage'; +import { HotelDiscountsPage } from './pages/HotelDiscountsPage'; +import { ContactUsPage } from './pages/ContactUsPage'; import { pageTransition } from './utils/animations'; import { LandingPage } from './pages/landingPage'; import ComingSoonPage from './pages/ComingSoonPage'; -import { SuperSavingsPage } from './components/SuperSavingsPage'; -import { WhatsIncluded } from './components/WhatsIncluded'; -import { LandingMagicItineraryPage } from './components/LandingMagicItineraryPage'; -import { DiscoverPage } from './components/DiscoverPage'; +import { SuperSavingsPage } from './pages/SuperSavingsPage'; +import { WhatsIncluded } from './pages/WhatsIncluded'; +import { LandingMagicItineraryPage } from './pages/LandingMagicItineraryPage'; +import { DiscoverPage } from './pages/DiscoverPage'; +import { CartPage } from './pages/CartPage'; +import { PaymentDetailsPage } from './pages/PaymentDetailsPage'; +import { SuperSavingsDetailsPage } from './pages/SuperSavingsDetailsPage'; +import { ViewCardDetailsPage } from './pages/ViewCardDetailsPage'; +import ItinerarySummaryPage from './pages/ItinerarySummaryPage'; +import { PaymentSuccessPage } from './pages/PaymentSuccessPage'; +import { PaymentCancelPage } from './pages/PaymentCancelPage'; +import { ItineraryViewPage } from './pages/ItineraryViewPage'; +import { CheckoutPage } from './pages/CheckoutPage'; +import { CreateMagicItineraryPage } from './pages/CreateMagicIternaryPage'; +import RegisterPage from './components/RegisterPage'; // User type definition interface User { @@ -90,7 +94,7 @@ export function AppRouter({ } /> {/* Home Route */} - @@ -125,19 +129,6 @@ export function AppRouter({ } /> - {/* Checkout Routes */} - - - - } /> - - - - - } /> - {/* Blog Routes */} @@ -188,20 +179,43 @@ export function AppRouter({ {/* User Routes */} - + + + } /> + + + + + + } /> + {/* Itinerary Routes */} - + + + + } /> - - + + + + + } /> + + + + + } /> @@ -272,6 +286,73 @@ export function AppRouter({ } /> + + + + + + + + } /> + + + + + + } /> + + + + } /> + + + + + + + } /> + + navigate(-1)} /> + + } /> + + + + + + + + + } /> + + + + + + + } /> diff --git a/src/Redux/Store.tsx b/src/Redux/Store.tsx index 7d03f11..2f46957 100644 --- a/src/Redux/Store.tsx +++ b/src/Redux/Store.tsx @@ -1,23 +1,33 @@ import { configureStore } from "@reduxjs/toolkit"; -import { fakeApi } from "./services/fakeApi.service"; import { attractionsApi } from "./services/attractions.service"; import { citiesApi } from "./services/cities.service"; +import { authApi } from "./services/auth.service"; +import { profileApi } from "./services/profile.service"; +import { cardsApi } from "./services/cards.service"; +import { itineraryApi } from "./services/itinerary.service"; +import { blogsApi } from "./services/blogs.service"; export const store = configureStore({ reducer: { - [fakeApi.reducerPath]:fakeApi.reducer, - [attractionsApi.reducerPath]:attractionsApi.reducer, - [citiesApi.reducerPath]:citiesApi.reducer + [attractionsApi.reducerPath]: attractionsApi.reducer, + [citiesApi.reducerPath]: citiesApi.reducer, + [authApi.reducerPath]: authApi.reducer, + [profileApi.reducerPath]: profileApi.reducer, + [cardsApi.reducerPath]:cardsApi.reducer, + [itineraryApi.reducerPath]:itineraryApi.reducer, + [blogsApi.reducerPath]:blogsApi.reducer }, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat( - -fakeApi.middleware, -attractionsApi.middleware, -citiesApi.middleware + attractionsApi.middleware, + citiesApi.middleware, + authApi.middleware, + profileApi.middleware, + cardsApi.middleware, + itineraryApi.middleware, + blogsApi.middleware ), }); export type RootState = ReturnType; diff --git a/src/Redux/baseQuery.ts b/src/Redux/baseQuery.ts index 37c41cc..36c9339 100644 --- a/src/Redux/baseQuery.ts +++ b/src/Redux/baseQuery.ts @@ -8,10 +8,9 @@ export const baseQuery = fetchBaseQuery({ const token = localStorage.getItem("accessToken"); if (token) { headers.set("Authorization", `Bearer ${token}`); - // headers.set("access-token", token); + headers.set("access-token", token); } // headers.set("Content-Type", "application/json"); return headers; }, -}); - +}); \ No newline at end of file diff --git a/src/Redux/services/attractions.service.ts b/src/Redux/services/attractions.service.ts index 01b7dee..2695346 100644 --- a/src/Redux/services/attractions.service.ts +++ b/src/Redux/services/attractions.service.ts @@ -30,6 +30,20 @@ export const attractionsApi = createApi({ return `/attractions/customer/customer-attractions?${params.toString()}`; }, }), + getAttractionsForHomePage: builder.query({ + // cityId is required, others optional + query: ({ cityId, categoryId}) => { + const params = new URLSearchParams(); + + // required + params.append('cityXid', cityId); + + // optional + if (categoryId) params.append('categoryXid', categoryId); + + return `/attractions/list/city-attractions?${params.toString()}`; + }, + }), getAttractionDetailsById: builder.query({ query: (id: number) => `/attractions/customer/${id}`, @@ -38,4 +52,4 @@ export const attractionsApi = createApi({ }), }); -export const { useGetAttractionFiltersQuery,useGetCustomerAttractionsQuery,useGetAttractionDetailsByIdQuery } = attractionsApi; \ No newline at end of file +export const { useGetAttractionFiltersQuery,useGetCustomerAttractionsQuery,useGetAttractionDetailsByIdQuery,useGetAttractionsForHomePageQuery } = attractionsApi; \ No newline at end of file diff --git a/src/Redux/services/auth.service.ts b/src/Redux/services/auth.service.ts new file mode 100644 index 0000000..100cd07 --- /dev/null +++ b/src/Redux/services/auth.service.ts @@ -0,0 +1,53 @@ + +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; +import { baseQuery } from "../baseQuery"; + +export const authApi = createApi({ + reducerPath: "authApi", + baseQuery: baseQuery, + + tagTypes: ["profile", "Transaction"], + + + endpoints: (builder) => ({ + // Login + + login: builder.mutation({ + query: (credentials) => ({ + url: "/website/send-otp", + method: "POST", + body: credentials, + }), + }), + + verifyOtp: builder.mutation({ + query: (credentials) => ({ + url: "/website/user/verify-otp", + method: "POST", + body: credentials, + }), + }), + + register: builder.mutation({ + query: (credentials) => ({ + url: "/website/user/register", + method: "POST", + body: credentials, + }), + }), + + logoutUser: builder.mutation({ + query: () => ({ + url: "/website/user/logout", + method: "POST" + }) + }) + }), +}); + +export const { + useLoginMutation, + useVerifyOtpMutation, + useRegisterMutation, + useLogoutUserMutation +} = authApi; \ No newline at end of file diff --git a/src/Redux/services/blogs.service.ts b/src/Redux/services/blogs.service.ts new file mode 100644 index 0000000..bcc2a40 --- /dev/null +++ b/src/Redux/services/blogs.service.ts @@ -0,0 +1,28 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { baseQuery } from "../baseQuery"; + +export const blogsApi = createApi({ + reducerPath: 'blogsApi', + baseQuery, + endpoints: (builder) => ({ + + getBlogsForCity: builder.query({ + // cityId is required, others optional + query: ({ cityId, categoryId }) => { + const params = new URLSearchParams(); + + // required + params.append('cityXid', cityId); + + // optional + if (categoryId) params.append('categoryXid', categoryId); + + return `/website/list/blogs?${params.toString()}`; + }, + }), + + + }), +}); + +export const { useGetBlogsForCityQuery } = blogsApi; \ No newline at end of file diff --git a/src/Redux/services/cards.service.ts b/src/Redux/services/cards.service.ts new file mode 100644 index 0000000..9e1ffdd --- /dev/null +++ b/src/Redux/services/cards.service.ts @@ -0,0 +1,77 @@ + +import { createApi } from "@reduxjs/toolkit/query/react"; +import { baseQuery } from "../baseQuery"; + +export const cardsApi = createApi({ + reducerPath: "cardsApi", + baseQuery, + + tagTypes: ["cardsInCart"], + + endpoints: (builder) => ({ + + getCardsinCart: builder.query({ + query: (cityId) => { + const params = new URLSearchParams() + params.append('cityXid', cityId); + return `/website/passes/cart/passes?${params.toString()}` + }, + providesTags: ["cardsInCart"] + }), + + getCheckoutPageData: builder.query({ + query: (cityId) => `/website/pass/${cityId}`, + }), + + getCardBookingDetails: builder.query({ + query: (bookingId) => `/website/passes/${bookingId}/details`, + }), + + storeRecipientDetails: builder.mutation({ + query: ({ recipientDetails, bookingId }) => ({ // keep the name of the variables being passed here same as when calling the mutation hook + url: `/website/passes/${bookingId}/store-gift-details`, + method: "PUT", + body: recipientDetails + }), + }), + addCardToCart: builder.mutation({ + query: (cardBookingDetails) => ({ // keep the name of the variables being passed here same as when calling the mutation hook + url: `/website/passes/add-to-cart`, + method: "POST", + body: cardBookingDetails + }), + }), + + payForCard: builder.mutation({ + query: (id) => ({ + url: `/website/passes/${id}/pay`, + method: "POST", + body: {}, + }), + }), + + confirmCardPayment: builder.mutation({ + query: (payload: { id: string; checkoutSessionId: string }) => ({ + url: `/website/passes/${payload.id}/${payload.checkoutSessionId}/confirm-payment/`, + method: "POST", + }), + }), + + + + + + + }), +}); + +export const { + useGetCardsinCartQuery, + useGetCheckoutPageDataQuery, + useGetCardBookingDetailsQuery, + useStoreRecipientDetailsMutation, + useAddCardToCartMutation, + usePayForCardMutation, + useConfirmCardPaymentMutation + +} = cardsApi; \ No newline at end of file diff --git a/src/Redux/services/cities.service.ts b/src/Redux/services/cities.service.ts index e1dfb06..9d9132b 100644 --- a/src/Redux/services/cities.service.ts +++ b/src/Redux/services/cities.service.ts @@ -1,11 +1,8 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { createApi } from '@reduxjs/toolkit/query/react'; import { baseQuery } from "../baseQuery"; export const citiesApi = createApi({ reducerPath: 'citiesApi', - // baseQuery: fetchBaseQuery({ - // baseUrl: 'https://testingapi.citycards.betadelivery.com', - // }), baseQuery, endpoints: (builder) => ({ @@ -20,11 +17,34 @@ export const citiesApi = createApi({ }), getUpcomingCities: builder.query({ - query: (listType) => `/cities/list/all?listType=${listType}`, - }) + }), + + getSelectedCityDetails: builder.query({ + query: (cityId) => `/website/${cityId}`, + }), + + getSelectedCityOffers: builder.query({ + query: ({ cityId, categoryId, page, limit }) => { + const params = new URLSearchParams() + + params.append('cityXid', cityId); + + if (categoryId) params.append('categoryXid', categoryId); + if (page) params.append('page', page); + if (limit) params.append('limit', limit); + + return `/website/super-savings/list/offers?${params.toString()}`; + } + }), + + getOfferDetailsById: builder.query({ + query: (id: number) => `/website/super-savings/list/offers/${id}`, + }), + + }), }); -export const { useGetCityListWithBannerQuery,useGetUpcomingCitiesQuery } = citiesApi; \ No newline at end of file +export const { useGetCityListWithBannerQuery, useGetUpcomingCitiesQuery, useGetSelectedCityDetailsQuery, useGetSelectedCityOffersQuery, useGetOfferDetailsByIdQuery } = citiesApi; \ No newline at end of file diff --git a/src/Redux/services/fakeApi.service.ts b/src/Redux/services/fakeApi.service.ts deleted file mode 100644 index b5f90ae..0000000 --- a/src/Redux/services/fakeApi.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/Redux/services/itinerary.service.ts b/src/Redux/services/itinerary.service.ts new file mode 100644 index 0000000..364d7dc --- /dev/null +++ b/src/Redux/services/itinerary.service.ts @@ -0,0 +1,50 @@ + +import { createApi } from "@reduxjs/toolkit/query/react"; +import { baseQuery } from "../baseQuery"; + +export const itineraryApi = createApi({ + reducerPath: "itApi", + baseQuery, + + endpoints: (builder) => ({ + + createMagicItinerary: builder.mutation({ + query: (itineraryDetails) => ({ // keep the name of the variables being passed here same as when calling the mutation hook + url: `/website/itinerary`, + method: "POST", + body: itineraryDetails + }), + + }), + + getItineraryDetailsById: builder.query({ + query: (itineraryId: number) => `/website/itinerary/${itineraryId}`, + }), + + getUserItineraries: builder.query({ + query: (cityId) => { + const params = new URLSearchParams() + params.append('cityId', cityId); + return `/website/itinerary/all-initineraries?${params.toString()}` + } + }), + + downloadItinerary: builder.query({ + query: (id) => ({ + url: `/mobile/itinerary/${id}/download`, + method: 'GET', + responseHandler: (response) => response.blob(), + }), + }), + + }) +}); + +export const { + useCreateMagicItineraryMutation, + useGetItineraryDetailsByIdQuery, + useGetUserItinerariesQuery, + useDownloadItineraryQuery, + + +} = itineraryApi; \ No newline at end of file diff --git a/src/Redux/services/profile.service.ts b/src/Redux/services/profile.service.ts new file mode 100644 index 0000000..ab343fd --- /dev/null +++ b/src/Redux/services/profile.service.ts @@ -0,0 +1,51 @@ + +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; +import { baseQuery } from "../baseQuery"; + +export const profileApi = createApi({ + reducerPath: "profileApi", + baseQuery, + + tagTypes: ["userDetails"], + + endpoints: (builder) => ({ + + getUserProfileDetails: builder.query({ + query: (id) => `/website/user/${id}`, + providesTags: ["userDetails"] + }), + + updateUserProfileDetails: builder.mutation({ + query: ({ userDetails, userId }) => ({ // keep the name of the variables being passed here same as when calling the mutation hook + url: `/website/user/${userId}`, + method: "PUT", + body: userDetails + }), + invalidatesTags: ["userDetails"] + }), + + getUserCards: builder.query({ + query: ({sort,cityId}) => { + const params = new URLSearchParams() + + params.append('cityXid', cityId); + + if (sort) params.append('sort', sort); + + return `/website/passes/all?${params.toString()}` + } + }), + + getUserCardDetails: builder.query({ + query: (cardId) => `/website/passes/${cardId}/details`, + }), + + }) +}); + +export const { + useGetUserProfileDetailsQuery, + useUpdateUserProfileDetailsMutation, + useGetUserCardsQuery, + useGetUserCardDetailsQuery +} = profileApi; \ No newline at end of file diff --git a/src/assets/citycards customer app.png b/src/assets/citycards customer app.png new file mode 100644 index 0000000..7caa18f Binary files /dev/null and b/src/assets/citycards customer app.png differ diff --git a/src/assets/front.jpg b/src/assets/front.jpg new file mode 100644 index 0000000..12f06ef Binary files /dev/null and b/src/assets/front.jpg differ diff --git a/src/components/AfterLogin.tsx b/src/components/AfterLogin.tsx new file mode 100644 index 0000000..9ab7400 --- /dev/null +++ b/src/components/AfterLogin.tsx @@ -0,0 +1,42 @@ +function Text() { + return ( +

+
+
+

JD

+
+
+
+ ); +} + +function Container() { + return ( +
+ +
+ ); +} + +function Text1() { + return ( +
+
+

MY CITY CARD

+
+
+ ); +} + +export default function AfterLogin() { + return ( +
+
+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/BeforeLogin.tsx b/src/components/BeforeLogin.tsx new file mode 100644 index 0000000..0b3a144 --- /dev/null +++ b/src/components/BeforeLogin.tsx @@ -0,0 +1,21 @@ +function Text() { + return ( +
+
+

GET A CITY CARD

+
+
+ ); +} + +export default function BeforeLogin() { + return ( +
+
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/CTAButton.tsx b/src/components/CTAButton.tsx index 9b80d9f..0c16431 100644 --- a/src/components/CTAButton.tsx +++ b/src/components/CTAButton.tsx @@ -1,6 +1,6 @@ import { motion } from 'motion/react'; -import BeforeLogin from '../imports/BeforeLogin'; -import AfterLogin from '../imports/AfterLogin'; +import BeforeLogin from './BeforeLogin'; +import AfterLogin from './AfterLogin'; interface User { email: string; diff --git a/src/components/CheckoutPage.tsx b/src/components/CheckoutPage.tsx deleted file mode 100644 index 790e565..0000000 --- a/src/components/CheckoutPage.tsx +++ /dev/null @@ -1,735 +0,0 @@ -import { useState, useEffect } from 'react'; -import { motion, AnimatePresence } from 'motion/react'; -import { ArrowLeft, CreditCard, Users, Calendar, MapPin, Shield, Truck, Clock, ChevronRight, Check, ChevronDown, X, Mail, Smartphone } from 'lucide-react'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { Label } from './ui/label'; -import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; -import { Separator } from './ui/separator'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; -import { RadioGroup, RadioGroupItem } from './ui/radio-group'; -import { Checkbox } from './ui/checkbox'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; -import { Badge } from './ui/badge'; -import { Textarea } from './ui/textarea'; -import Navbar from './Navbar'; -import { Footer } from './Footer'; -import { ImageWithFallback } from './figma/ImageWithFallback'; -import { Layout } from '../Layout'; - -interface CheckoutPageProps { - onBackClick?: () => void; - onHomeClick?: () => void; - onMelbourneClick?: () => void; - onPassesClick?: () => void; - onCheckoutClick?: () => void; - onSignInClick?: () => void; - onSignOutClick?: () => void; - onAttractionsClick?: () => void; - onBlogsClick?: () => void; - onHowItWorksClick?: () => void; - onFAQClick?: () => void; - onPrivacyPolicyClick?: () => void; - onAboutUsClick?: () => void; - onProfileClick?: () => void; - onCityCardsClick?: () => void; - onMagicItineraryClick?: () => void; - onPostCardsClick?: () => void; - onOffersClick?: () => void; - onSecureCheckoutClick?: () => void; - onContactUsClick?: () => void; - onEsimsClick?: () => void; - onHotelDiscountsClick?: () => void; - currentPage?: string; - user?: { email: string; name: string } | null; -} - - -// Mock cart data -const mockCartItems = [ - { - id: '1', - name: 'Paris Unlimited Pass', - type: '7-Day Pass', - price: 79, - originalPrice: 149, - discount: 47, - attractions: 45, - validity: '7 days', - image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?w=400', - features: ['Skip-the-line access', 'Mobile voucher', 'Free cancellation'] - } -]; - -export function CheckoutPage({ - onBackClick, - onHomeClick, - onMelbourneClick, - onPassesClick, - onCheckoutClick, - onSignInClick, - onSignOutClick, - onAttractionsClick, - onBlogsClick, - onHowItWorksClick, - onFAQClick, - onPrivacyPolicyClick, - onAboutUsClick, - onProfileClick, - onCityCardsClick, - onMagicItineraryClick, - onPostCardsClick, - onOffersClick, - onSecureCheckoutClick, - onContactUsClick, - onEsimsClick, - onHotelDiscountsClick, - currentPage, - user, -}: CheckoutPageProps) { - const [purchaseType, setPurchaseType] = useState<'self' | 'gift'>('self'); - const [selectedPayment, setSelectedPayment] = useState('credit-card'); - const [showEmailVerification, setShowEmailVerification] = useState(false); - const [verificationCode, setVerificationCode] = useState(''); - const [isEmailVerified, setIsEmailVerified] = useState(false); - const [formData, setFormData] = useState({ - email: '', - firstName: '', - lastName: '', - phone: '', - country: '', - address: '', - city: '', - postalCode: '', - cardNumber: '', - expiry: '', - cvv: '', - cardName: '', - agreeTerms: false, - subscribeNewsletter: false - }); - const [giftData, setGiftData] = useState({ - recipientName: '', - recipientPhone: '', - recipientEmail: '', - personalizedMessage: '' - }); - - const subtotal = mockCartItems.reduce((sum, item) => sum + item.price, 0); - const tax = Math.round(subtotal * 0.1); - const total = subtotal + tax; - const totalSavings = mockCartItems.reduce((sum, item) => sum + (item.originalPrice - item.price), 0); - - const handleInputChange = (field: string, value: string | boolean) => { - setFormData(prev => ({ ...prev, [field]: value })); - - // Trigger email verification when email is complete - if (field === 'email' && typeof value === 'string' && value.includes('@') && value.includes('.') && !isEmailVerified) { - setTimeout(() => { - setShowEmailVerification(true); - }, 1000); - } - }; - - const handleGiftInputChange = (field: string, value: string) => { - setGiftData(prev => ({ ...prev, [field]: value })); - }; - - const handleEmailVerification = () => { - if (verificationCode === '123456') { - setIsEmailVerified(true); - setShowEmailVerification(false); - } - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!isEmailVerified) { - setShowEmailVerification(true); - return; - } - const checkoutData = { - purchaseType, - formData, - ...(purchaseType === 'gift' && { giftData }), - selectedPayment, - cartItems: mockCartItems - }; - console.log('Processing checkout...', checkoutData); - }; - - const paymentMethods = [ - { - id: 'credit-card', - name: 'Credit Card', - icon: , - description: 'Visa, Mastercard, American Express' - }, - { - id: 'paypal', - name: 'PayPal', - icon:
P
, - description: 'Pay with your PayPal account' - }, - { - id: 'google-pay', - name: 'Google Pay', - icon:
G
, - description: 'Pay with Google Pay' - } - ]; - - return ( -
- - - {/* Header Section */} -
-
- {/* Back Button */} - - - Back to Cart - - - {/* Page Title */} - -

- Secure{' '} - Checkout -

-

- Complete your purchase and start exploring Paris -

-
-
-
- - {/* Main Checkout Content */} -
-
-
- {/* Left Column - Form Inputs (3/5 width) */} -
- {/* Purchase Type Selection */} - - - - Purchase Type - - - setPurchaseType(value as 'self' | 'gift')} - className="grid grid-cols-1 md:grid-cols-2 gap-4" - > -
- - -
-
- - -
-
-
-
-
- - {/* Gift Recipient Information - Only shown when gift is selected */} - {purchaseType === 'gift' && ( - - - - - - Gift Recipient Details - - - -
- - handleGiftInputChange('recipientName', e.target.value)} - placeholder="Jane Smith" - required={purchaseType === 'gift'} - className="mt-1 font-poppins" - /> -
-
- - handleGiftInputChange('recipientEmail', e.target.value)} - placeholder="recipient@email.com" - required={purchaseType === 'gift'} - className="mt-1 font-poppins" - /> -
-
- - handleGiftInputChange('recipientPhone', e.target.value)} - placeholder="+1 (555) 123-4567" - required={purchaseType === 'gift'} - className="mt-1 font-poppins" - /> -
-
- -