Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5abda5b6cb | |||
|
|
a03d1999bf | ||
|
|
201e8b86d4 | ||
|
|
39e63deca2 | ||
|
|
c7af20fac7 | ||
|
|
e0c314d3af | ||
|
|
4c985e5177 | ||
| 7fc7f1b433 | |||
|
|
34223f1c81 | ||
| 3a5d6b0724 | |||
|
|
05f134fdba | ||
|
|
13780803ba | ||
|
|
0dbba7f80e | ||
| 7b3833e5f7 | |||
|
|
d8976a29b4 | ||
|
|
1be37e098b | ||
|
|
e3fde4bb17 | ||
|
|
33a782ca54 | ||
|
|
a651186276 | ||
| b276dec0f5 | |||
|
|
e9ccc78bb0 | ||
|
|
962d4283e6 | ||
|
|
67d7f977b7 | ||
| e9f404e4df | |||
|
|
848c33edbd | ||
| c8729848fb | |||
|
|
668a183123 | ||
| dd5e49bcc1 | |||
|
|
c3d3d0c751 | ||
|
|
340de94a5d |
15
package-lock.json
generated
15
package-lock.json
generated
@@ -2257,7 +2257,6 @@
|
|||||||
"version": "9.2.0",
|
"version": "9.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-9.2.0.tgz",
|
||||||
"integrity": "sha512-YSzLC0t6VS9MDdPTynSMqU8IxrItFUjkDORALFT6sSMR/XZ5Vgm3RDp/Gk7z727MC4A9s4MFVel0gF0c7+kdrg==",
|
"integrity": "sha512-YSzLC0t6VS9MDdPTynSMqU8IxrItFUjkDORALFT6sSMR/XZ5Vgm3RDp/Gk7z727MC4A9s4MFVel0gF0c7+kdrg==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.16"
|
"node": ">=12.16"
|
||||||
}
|
}
|
||||||
@@ -3098,7 +3097,6 @@
|
|||||||
"integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==",
|
"integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -3109,7 +3107,6 @@
|
|||||||
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
|
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -3120,7 +3117,6 @@
|
|||||||
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
|
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
@@ -3390,8 +3386,7 @@
|
|||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||||
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/embla-carousel-react": {
|
"node_modules/embla-carousel-react": {
|
||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
@@ -3986,7 +3981,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -4044,7 +4038,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -4071,7 +4064,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -4107,7 +4099,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/use-sync-external-store": "^0.0.6",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"use-sync-external-store": "^1.4.0"
|
"use-sync-external-store": "^1.4.0"
|
||||||
@@ -4310,8 +4301,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/redux-thunk": {
|
"node_modules/redux-thunk": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@@ -4574,7 +4564,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Routes, Route, useParams, useLocation, useNavigate } from 'react-router
|
|||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
|
||||||
// Import all your pages
|
// Import all your pages
|
||||||
|
import { ProtectedRoute } from './components/ProtectedRoute';
|
||||||
import { MelbournePage } from './pages/MelbournePage';
|
import { MelbournePage } from './pages/MelbournePage';
|
||||||
import { PassesPage } from './pages/PassesPage';
|
import { PassesPage } from './pages/PassesPage';
|
||||||
import { AttractionsPage } from './pages/AttractionsPage';
|
import { AttractionsPage } from './pages/AttractionsPage';
|
||||||
@@ -178,12 +179,16 @@ export function AppRouter({
|
|||||||
{/* User Routes */}
|
{/* User Routes */}
|
||||||
<Route path="/profile" element={
|
<Route path="/profile" element={
|
||||||
<motion.div key="profile" {...pageTransition}>
|
<motion.div key="profile" {...pageTransition}>
|
||||||
<ProfilePage {...commonNavHandlers} />
|
<ProtectedRoute>
|
||||||
|
<ProfilePage {...commonNavHandlers} />
|
||||||
|
</ProtectedRoute>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
} />
|
} />
|
||||||
<Route path="/view-card-details/:cardId" element={
|
<Route path="/view-card-details/:cardId" element={
|
||||||
<motion.div key="profile" {...pageTransition}>
|
<motion.div key="profile" {...pageTransition}>
|
||||||
<ViewCardDetailsPage {...commonNavHandlers} />
|
<ProtectedRoute>
|
||||||
|
<ViewCardDetailsPage {...commonNavHandlers} />
|
||||||
|
</ProtectedRoute>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
@@ -191,18 +196,26 @@ export function AppRouter({
|
|||||||
{/* Itinerary Routes */}
|
{/* Itinerary Routes */}
|
||||||
<Route path="/create-itinerary" element={
|
<Route path="/create-itinerary" element={
|
||||||
<motion.div key="create-itinerary" {...pageTransition}>
|
<motion.div key="create-itinerary" {...pageTransition}>
|
||||||
<CreateMagicItineraryPage {...commonNavHandlers} />
|
<ProtectedRoute>
|
||||||
|
|
||||||
|
<CreateMagicItineraryPage {...commonNavHandlers} />
|
||||||
|
</ProtectedRoute>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/view-itinerary/:itineraryId" element={
|
<Route path="/view-itinerary/:itineraryId" element={
|
||||||
<motion.div key="itinerary-view" {...pageTransition}>
|
<motion.div key="itinerary-view" {...pageTransition}>
|
||||||
<ItineraryViewPage {...commonNavHandlers} />
|
<ProtectedRoute>
|
||||||
|
<ItineraryViewPage {...commonNavHandlers} />
|
||||||
|
</ProtectedRoute>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
} />
|
} />
|
||||||
<Route path="/itinerary-summary/:itineraryId" element={
|
<Route path="/itinerary-summary/:itineraryId" element={
|
||||||
<motion.div key="itinerary-summary" {...pageTransition}>
|
<motion.div key="itinerary-summary" {...pageTransition}>
|
||||||
<ItinerarySummaryPage {...commonNavHandlers} />
|
<ProtectedRoute>
|
||||||
|
|
||||||
|
<ItinerarySummaryPage {...commonNavHandlers} />
|
||||||
|
</ProtectedRoute>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
@@ -276,12 +289,17 @@ export function AppRouter({
|
|||||||
|
|
||||||
<Route path="/cart" element={
|
<Route path="/cart" element={
|
||||||
<motion.div key="super-savings" {...pageTransition}>
|
<motion.div key="super-savings" {...pageTransition}>
|
||||||
<CartPage {...commonNavHandlers} />
|
<ProtectedRoute>
|
||||||
|
|
||||||
|
<CartPage {...commonNavHandlers} />
|
||||||
|
</ProtectedRoute>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
} />
|
} />
|
||||||
<Route path="/checkout" element={
|
<Route path="/checkout" element={
|
||||||
<motion.div key="super-savings" {...pageTransition}>
|
<motion.div key="super-savings" {...pageTransition}>
|
||||||
<CheckoutPage {...commonNavHandlers} />
|
<ProtectedRoute>
|
||||||
|
<CheckoutPage {...commonNavHandlers} />
|
||||||
|
</ProtectedRoute>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
} />
|
} />
|
||||||
<Route path="/register" element={
|
<Route path="/register" element={
|
||||||
@@ -292,8 +310,10 @@ export function AppRouter({
|
|||||||
|
|
||||||
<Route path="/payment/:bookingId" element={
|
<Route path="/payment/:bookingId" element={
|
||||||
<motion.div key="super-savings" {...pageTransition}>
|
<motion.div key="super-savings" {...pageTransition}>
|
||||||
<PaymentDetailsPage {...commonNavHandlers} />
|
<ProtectedRoute>
|
||||||
</motion.div>
|
<PaymentDetailsPage {...commonNavHandlers} />
|
||||||
|
</ProtectedRoute>
|
||||||
|
</motion.div>
|
||||||
} />
|
} />
|
||||||
<Route path="/super-savings/:id" element={
|
<Route path="/super-savings/:id" element={
|
||||||
<motion.div key="super-savings" {...pageTransition}>
|
<motion.div key="super-savings" {...pageTransition}>
|
||||||
@@ -305,26 +325,32 @@ export function AppRouter({
|
|||||||
|
|
||||||
<Route path="/success" element={
|
<Route path="/success" element={
|
||||||
<motion.div key="super-savings" {...pageTransition}>
|
<motion.div key="super-savings" {...pageTransition}>
|
||||||
<PaymentSuccessPage
|
<ProtectedRoute>
|
||||||
// onHomeClick={onHomeClick}
|
|
||||||
// onPassesClick={onPassesClick}
|
<PaymentSuccessPage
|
||||||
onSignInClick={onSignInClick}
|
// onHomeClick={onHomeClick}
|
||||||
onSignOutClick={onSignOutClick}
|
// onPassesClick={onPassesClick}
|
||||||
currentPage="success"
|
onSignInClick={onSignInClick}
|
||||||
user={user}
|
onSignOutClick={onSignOutClick}
|
||||||
/>
|
currentPage="success"
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
</ProtectedRoute>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
} />
|
} />
|
||||||
<Route path="/cancel" element={
|
<Route path="/cancel" element={
|
||||||
<motion.div key="super-savings" {...pageTransition}>
|
<motion.div key="super-savings" {...pageTransition}>
|
||||||
<PaymentCancelPage
|
<ProtectedRoute>
|
||||||
// onHomeClick={onHomeClick}
|
|
||||||
// onPassesClick={onPassesClick}
|
<PaymentCancelPage
|
||||||
onSignInClick={onSignInClick}
|
// onHomeClick={onHomeClick}
|
||||||
onSignOutClick={onSignOutClick}
|
// onPassesClick={onPassesClick}
|
||||||
currentPage="cancel"
|
onSignInClick={onSignInClick}
|
||||||
user={user}
|
onSignOutClick={onSignOutClick}
|
||||||
/>
|
currentPage="cancel"
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
</ProtectedRoute>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
} />
|
} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { authApi } from "./services/auth.service";
|
|||||||
import { profileApi } from "./services/profile.service";
|
import { profileApi } from "./services/profile.service";
|
||||||
import { cardsApi } from "./services/cards.service";
|
import { cardsApi } from "./services/cards.service";
|
||||||
import { itineraryApi } from "./services/itinerary.service";
|
import { itineraryApi } from "./services/itinerary.service";
|
||||||
|
import { blogsApi } from "./services/blogs.service";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
@@ -13,7 +14,8 @@ export const store = configureStore({
|
|||||||
[authApi.reducerPath]: authApi.reducer,
|
[authApi.reducerPath]: authApi.reducer,
|
||||||
[profileApi.reducerPath]: profileApi.reducer,
|
[profileApi.reducerPath]: profileApi.reducer,
|
||||||
[cardsApi.reducerPath]:cardsApi.reducer,
|
[cardsApi.reducerPath]:cardsApi.reducer,
|
||||||
[itineraryApi.reducerPath]:itineraryApi.reducer
|
[itineraryApi.reducerPath]:itineraryApi.reducer,
|
||||||
|
[blogsApi.reducerPath]:blogsApi.reducer
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -24,7 +26,8 @@ export const store = configureStore({
|
|||||||
authApi.middleware,
|
authApi.middleware,
|
||||||
profileApi.middleware,
|
profileApi.middleware,
|
||||||
cardsApi.middleware,
|
cardsApi.middleware,
|
||||||
itineraryApi.middleware
|
itineraryApi.middleware,
|
||||||
|
blogsApi.middleware
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
|||||||
@@ -30,6 +30,20 @@ export const attractionsApi = createApi({
|
|||||||
return `/attractions/customer/customer-attractions?${params.toString()}`;
|
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({
|
getAttractionDetailsById: builder.query({
|
||||||
query: (id: number) => `/attractions/customer/${id}`,
|
query: (id: number) => `/attractions/customer/${id}`,
|
||||||
@@ -38,4 +52,4 @@ export const attractionsApi = createApi({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { useGetAttractionFiltersQuery,useGetCustomerAttractionsQuery,useGetAttractionDetailsByIdQuery } = attractionsApi;
|
export const { useGetAttractionFiltersQuery,useGetCustomerAttractionsQuery,useGetAttractionDetailsByIdQuery,useGetAttractionsForHomePageQuery } = attractionsApi;
|
||||||
28
src/Redux/services/blogs.service.ts
Normal file
28
src/Redux/services/blogs.service.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||||
|
import { baseQuery } from "../baseQuery";
|
||||||
|
|
||||||
|
export const blogsApi = createApi({
|
||||||
|
reducerPath: 'blogsApi',
|
||||||
|
baseQuery,
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
|
||||||
|
getBlogsForCity: builder.query({
|
||||||
|
// cityId is required, others optional
|
||||||
|
query: ({ cityId, categoryId }) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
// required
|
||||||
|
params.append('cityXid', cityId);
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if (categoryId) params.append('categoryXid', categoryId);
|
||||||
|
|
||||||
|
return `/website/list/blogs?${params.toString()}`;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useGetBlogsForCityQuery } = blogsApi;
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||||
import { baseQuery } from "../baseQuery";
|
import { baseQuery } from "../baseQuery";
|
||||||
|
|
||||||
export const citiesApi = createApi({
|
export const citiesApi = createApi({
|
||||||
reducerPath: 'citiesApi',
|
reducerPath: 'citiesApi',
|
||||||
// baseQuery: fetchBaseQuery({
|
|
||||||
// baseUrl: 'https://testingapi.citycards.betadelivery.com',
|
|
||||||
// }),
|
|
||||||
baseQuery,
|
baseQuery,
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export function EnhancedTestimonials() {
|
|||||||
style={{
|
style={{
|
||||||
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
|
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
|
||||||
transformOrigin: 'center center',
|
transformOrigin: 'center center',
|
||||||
minHeight: '480px',
|
minHeight: '360px',
|
||||||
background: `
|
background: `
|
||||||
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
|
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
|
||||||
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),
|
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -259,7 +259,7 @@ export function LandingBookAttractionSection() {
|
|||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className={`px-6 py-4 h-14 rounded-full font-medium transition-all duration-300 ${activeCategory === category
|
className={`px-6 py-4 h-14 rounded-full font-medium transition-all duration-300 ${activeCategory === category
|
||||||
? 'bg-warm-coral text-white shadow-xl shadow-warm-coral/25 ring-2 ring-warm-coral/20'
|
? 'bg-red-400 text-white shadow-xl shadow-warm-coral/25 ring-2 ring-warm-coral/20'
|
||||||
: 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-warm-coral/20 hover:bg-white'
|
: 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-warm-coral/20 hover:bg-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button } from './ui/button';
|
|||||||
|
|
||||||
// Import your video from assets
|
// Import your video from assets
|
||||||
import cityTourVideo from '../assets/itinenary-animation-vid.mp4';
|
import cityTourVideo from '../assets/itinenary-animation-vid.mp4';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface ItineraryCard {
|
interface ItineraryCard {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -22,6 +23,8 @@ export function LandingMagicItinerary() {
|
|||||||
const [isPlaying, setIsPlaying] = useState(true);
|
const [isPlaying, setIsPlaying] = useState(true);
|
||||||
const [videoLoaded, setVideoLoaded] = useState(false);
|
const [videoLoaded, setVideoLoaded] = useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleVideoLoad = () => {
|
const handleVideoLoad = () => {
|
||||||
setVideoLoaded(true);
|
setVideoLoaded(true);
|
||||||
};
|
};
|
||||||
@@ -31,7 +34,7 @@ export function LandingMagicItinerary() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 lg:py-32 overflow-hidden -mt-20 pt-32 z-[49]">
|
<section className="relative py-20 lg:py-15 overflow-hidden -mt-20 z-[49]">
|
||||||
{/* Dynamic Background */}
|
{/* Dynamic Background */}
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none z-[5]">
|
<div className="absolute inset-0 overflow-hidden pointer-events-none z-[5]">
|
||||||
{/* Background Image as fallback */}
|
{/* Background Image as fallback */}
|
||||||
@@ -97,7 +100,7 @@ export function LandingMagicItinerary() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center mb-16 max-w-5xl w-full">
|
<div className="text-center mb-16 max-w-5xl w-full">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="inline-flex items-center gap-3 bg-gradient-to-r from-warm-coral/10 to-orange-100/50 backdrop-blur-sm px-6 py-3 rounded-full border-2 border-warm-coral/30 shadow-xl mb-8"
|
className="inline-flex items-center gap-3 bg-gradient-to-r from-warm-coral/10 to-orange-100/50 backdrop-blur-sm pl-6 py-3 rounded-full border-2 border-warm-coral/30 shadow-xl mb-8"
|
||||||
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||||
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
transition={{ duration: 0.7, ease: [0.34, 1.56, 0.64, 1] }}
|
transition={{ duration: 0.7, ease: [0.34, 1.56, 0.64, 1] }}
|
||||||
@@ -112,7 +115,7 @@ export function LandingMagicItinerary() {
|
|||||||
>
|
>
|
||||||
<Wand2 className="w-6 h-6 text-warm-coral drop-shadow-lg" />
|
<Wand2 className="w-6 h-6 text-warm-coral drop-shadow-lg" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<span className="font-semibold text-gray-800">AI-Powered Magic Itinerary</span>
|
<span className="font-semibold text-gray-800">Magic Itinerary</span>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-2 h-2 bg-warm-coral rounded-full"
|
className="w-2 h-2 bg-warm-coral rounded-full"
|
||||||
animate={{
|
animate={{
|
||||||
@@ -131,7 +134,7 @@ export function LandingMagicItinerary() {
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
>
|
>
|
||||||
<span className="font-light">Plan Your</span>{' '}
|
<span className="font-light">Plan Your</span>{' '}
|
||||||
<span className="font-bold italic bg-gradient-to-r from-warm-coral via-orange-500 to-rose-500 bg-clip-text pr-2 text-transparent drop-shadow-lg">
|
<span className="font-bold italic bg-gradient-to-r from-red-500 via-orange-500 to-rose-500 bg-clip-text pr-2 text-transparent drop-shadow-lg">
|
||||||
Dream Journey
|
Dream Journey
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
@@ -250,7 +253,8 @@ export function LandingMagicItinerary() {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
withShine={true}
|
withShine={true}
|
||||||
className="py-6 px-14 rounded-full text-lg font-bold bg-gradient-to-r from-warm-coral via-orange-500 to-rose-500 hover:from-warm-coral/90 hover:via-orange-500/90 hover:to-rose-500/90 shadow-2xl hover:shadow-warm-coral/50 transition-all hover:scale-105 hover:-translate-y-1"
|
onClick={() => navigate('/landing-magic-itinerary')}
|
||||||
|
className="py-6 px-14 rounded-full text-lg font-bold bg-gradient-to-r via-orange-500 to-rose-500 hover:from-warm-coral/90 hover:via-orange-500/90 hover:to-rose-500/90 shadow-2xl hover:shadow-warm-coral/50 transition-all hover:scale-105 hover:-translate-y-1"
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-3">
|
<span className="flex items-center gap-3">
|
||||||
<Wand2 className="w-5 h-5" />
|
<Wand2 className="w-5 h-5" />
|
||||||
@@ -258,11 +262,11 @@ export function LandingMagicItinerary() {
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<p className="text-gray-600 text-sm flex items-center gap-2">
|
{/* <p className="text-gray-600 text-sm flex items-center gap-2">
|
||||||
<Sparkles className="w-4 h-4 text-warm-coral" />
|
<Sparkles className="w-4 h-4 text-warm-coral" />
|
||||||
<span>Free to use • No credit card required</span>
|
<span>Free to use • No credit card required</span>
|
||||||
<Sparkles className="w-4 h-4 text-warm-coral" />
|
<Sparkles className="w-4 h-4 text-warm-coral" />
|
||||||
</p>
|
</p> */}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export function LandingTrustSection() {
|
|||||||
style={{
|
style={{
|
||||||
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
|
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
|
||||||
transformOrigin: 'center center',
|
transformOrigin: 'center center',
|
||||||
minHeight: '480px',
|
minHeight: '360px',
|
||||||
background: `
|
background: `
|
||||||
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
|
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
|
||||||
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),
|
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),
|
||||||
|
|||||||
@@ -150,10 +150,10 @@ export function LandingVarietyOfAdventures() {
|
|||||||
const extendedCategories = [...melbourneCategories, ...melbourneCategories, ...melbourneCategories];
|
const extendedCategories = [...melbourneCategories, ...melbourneCategories, ...melbourneCategories];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-20 lg:py-28 bg-white overflow-hidden">
|
<section className="lg: bg-white overflow-hidden">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center mb-16 max-w-4xl mx-auto">
|
<div className="text-center mb-2 max-w-4xl mx-auto">
|
||||||
<motion.h2
|
<motion.h2
|
||||||
className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight text-foreground mb-6"
|
className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight text-foreground mb-6"
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// LoginModal.tsx
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
@@ -161,6 +162,7 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
|||||||
|
|
||||||
login(userData);
|
login(userData);
|
||||||
toast.success("User Logged in successfully")
|
toast.success("User Logged in successfully")
|
||||||
|
navigate("/passes")
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -3,271 +3,167 @@ import { ChevronLeft, ChevronRight, Clock, Users, Star, Zap, CheckCircle, MapPin
|
|||||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useGetAttractionsForHomePageQuery } from '../Redux/services/attractions.service';
|
||||||
const melbourneAttractions = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "Royal Botanic Gardens",
|
|
||||||
city: "Melbourne",
|
|
||||||
country: "Australia",
|
|
||||||
image: "https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zfGVufDF8fHx8MTc1NzMzNzc4OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
rating: 4.8,
|
|
||||||
reviews: "15,600+",
|
|
||||||
category: "Gardens",
|
|
||||||
originalPrice: "Free",
|
|
||||||
includedValue: "$25",
|
|
||||||
perks: [
|
|
||||||
{ icon: Volume2, label: "Audio garden tour", color: "text-green-600" },
|
|
||||||
{ icon: MapPin, label: "Garden maps", color: "text-blue-600" },
|
|
||||||
{ icon: Camera, label: "Photo spots guide", color: "text-purple-600" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Federation Square",
|
|
||||||
city: "Melbourne",
|
|
||||||
country: "Australia",
|
|
||||||
image: "https://images.unsplash.com/photo-1639655001512-e4b58d4874b8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBmZWRlcmF0aW9uJTIwc3F1YXJlfGVufDF8fHx8MTc1NzMzNzc5Mnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
rating: 4.6,
|
|
||||||
reviews: "22,400+",
|
|
||||||
category: "Landmarks",
|
|
||||||
originalPrice: "Free",
|
|
||||||
includedValue: "$35",
|
|
||||||
perks: [
|
|
||||||
{ icon: Volume2, label: "Cultural tours", color: "text-orange-600" },
|
|
||||||
{ icon: Eye, label: "Gallery access", color: "text-blue-600" },
|
|
||||||
{ icon: Users, label: "Event access", color: "text-purple-600" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "Queen Victoria Market",
|
|
||||||
city: "Melbourne",
|
|
||||||
country: "Australia",
|
|
||||||
image: "https://images.unsplash.com/photo-1676454953709-e0be46f62490?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0fGVufDF8fHx8MTc1NzMzNzc5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
rating: 4.7,
|
|
||||||
reviews: "18,200+",
|
|
||||||
category: "Markets",
|
|
||||||
originalPrice: "$45",
|
|
||||||
includedValue: "$45",
|
|
||||||
perks: [
|
|
||||||
{ icon: Users, label: "Food tours", color: "text-orange-600" },
|
|
||||||
{ icon: Coffee, label: "Tastings", color: "text-brown-600" },
|
|
||||||
{ icon: Volume2, label: "History guide", color: "text-blue-600" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "Eureka Skydeck",
|
|
||||||
city: "Melbourne",
|
|
||||||
country: "Australia",
|
|
||||||
image: "https://images.unsplash.com/photo-1629677713183-29248e1268d7?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBldXJla2ElMjB0b3dlcnxlbnwxfHx8fDE3NTczMzc4MDB8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
rating: 4.9,
|
|
||||||
reviews: "11,800+",
|
|
||||||
category: "Views",
|
|
||||||
originalPrice: "$32",
|
|
||||||
includedValue: "$32",
|
|
||||||
perks: [
|
|
||||||
{ icon: Zap, label: "Skip-the-line", color: "text-green-600" },
|
|
||||||
{ icon: Eye, label: "360° views", color: "text-purple-600" },
|
|
||||||
{ icon: Camera, label: "Photo experiences", color: "text-blue-600" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: "St Kilda Beach & Pier",
|
|
||||||
city: "Melbourne",
|
|
||||||
country: "Australia",
|
|
||||||
image: "https://images.unsplash.com/photo-1674732954456-159835c0a46b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBzdCUyMGtpbGRhJTIwYmVhY2h8ZW58MXx8fHwxNzU3MzM3ODAzfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
rating: 4.5,
|
|
||||||
reviews: "14,300+",
|
|
||||||
category: "Beach",
|
|
||||||
originalPrice: "Free",
|
|
||||||
includedValue: "$20",
|
|
||||||
perks: [
|
|
||||||
{ icon: Users, label: "Penguin tours", color: "text-blue-600" },
|
|
||||||
{ icon: MapPin, label: "Beach activities", color: "text-green-600" },
|
|
||||||
{ icon: Camera, label: "Sunset spots", color: "text-purple-600" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: "Melbourne Laneways",
|
|
||||||
city: "Melbourne",
|
|
||||||
country: "Australia",
|
|
||||||
image: "https://images.unsplash.com/photo-1705120624704-0970afc29fea?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBsYW5ld2F5cyUyMHN0cmVldCUyMGFydHxlbnwxfHx8fDE3NTczMzc4MDd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
rating: 4.8,
|
|
||||||
reviews: "19,500+",
|
|
||||||
category: "Street Art",
|
|
||||||
originalPrice: "$55",
|
|
||||||
includedValue: "$55",
|
|
||||||
perks: [
|
|
||||||
{ icon: Palette, label: "Art tours", color: "text-pink-600" },
|
|
||||||
{ icon: Coffee, label: "Café stops", color: "text-brown-600" },
|
|
||||||
{ icon: Camera, label: "Photo walks", color: "text-purple-600" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
name: "Melbourne Zoo",
|
|
||||||
city: "Melbourne",
|
|
||||||
country: "Australia",
|
|
||||||
image: "https://images.unsplash.com/photo-1681429477985-30dc7b88dd5b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjB6b28lMjBhbmltYWxzfGVufDF8fHx8MTc1NzMzNzgxMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
rating: 4.7,
|
|
||||||
reviews: "13,900+",
|
|
||||||
category: "Wildlife",
|
|
||||||
originalPrice: "$42",
|
|
||||||
includedValue: "$42",
|
|
||||||
perks: [
|
|
||||||
{ icon: Zap, label: "Skip-the-line", color: "text-green-600" },
|
|
||||||
{ icon: Users, label: "Animal encounters", color: "text-orange-600" },
|
|
||||||
{ icon: Volume2, label: "Keeper talks", color: "text-blue-600" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: "Royal Exhibition Building",
|
|
||||||
city: "Melbourne",
|
|
||||||
country: "Australia",
|
|
||||||
image: "https://images.unsplash.com/photo-1720523794299-c3b445d71a51?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGV4aGliaXRpb24lMjBidWlsZGluZ3xlbnwxfHx8fDE3NTczMzc4MTR8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
rating: 4.6,
|
|
||||||
reviews: "8,700+",
|
|
||||||
category: "Heritage",
|
|
||||||
originalPrice: "$25",
|
|
||||||
includedValue: "$25",
|
|
||||||
perks: [
|
|
||||||
{ icon: Volume2, label: "Audio tours", color: "text-blue-600" },
|
|
||||||
{ icon: Eye, label: "Exhibitions", color: "text-purple-600" },
|
|
||||||
{ icon: MapPin, label: "Heritage walks", color: "text-green-600" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const categories = ["All", "Landmarks", "Gardens", "Markets", "Views", "Beach", "Street Art", "Wildlife", "Heritage"];
|
|
||||||
|
|
||||||
export function MelbourneAttractions() {
|
export function MelbourneAttractions() {
|
||||||
const [activeCategory, setActiveCategory] = useState("All");
|
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const cityName = localStorage.getItem("cityName")
|
const cityName = localStorage.getItem("cityName");
|
||||||
|
const cityId = localStorage.getItem("cityId");
|
||||||
|
|
||||||
const filteredAttractions = activeCategory === "All"
|
const { data: homePageAttractionsData } = useGetAttractionsForHomePageQuery({ cityId });
|
||||||
? melbourneAttractions
|
|
||||||
: melbourneAttractions.filter(attraction => attraction.category === activeCategory);
|
|
||||||
|
|
||||||
const AttractionCard = ({ attraction, index }: { attraction: typeof melbourneAttractions[0], index: number }) => (
|
const apiAttractions = homePageAttractionsData?.attractions || [];
|
||||||
<motion.div
|
const apiCategories = homePageAttractionsData?.categories || [];
|
||||||
initial={{ opacity: 0, y: 30 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
className="group cursor-pointer flex-shrink-0 w-[280px] md:w-auto md:flex-shrink h-96 flip-card-container"
|
|
||||||
>
|
|
||||||
{/* 3D Flip Container */}
|
|
||||||
<div className="flip-card-inner group-hover:[transform:rotateY(180deg)] relative w-full h-full">
|
|
||||||
|
|
||||||
{/* FRONT FACE */}
|
// Filter attractions by selected category
|
||||||
<div className="flip-card-face absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg">
|
const filteredAttractions = selectedCategoryId === null
|
||||||
{/* Background Image */}
|
? apiAttractions
|
||||||
<ImageWithFallback
|
: apiAttractions.filter((attraction: any) =>
|
||||||
src={attraction.image}
|
attraction.categories?.some((cat: any) => cat.id === selectedCategoryId)
|
||||||
alt={attraction.name}
|
);
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Rating Badge */}
|
const AttractionCard = ({ attraction, index }: { attraction: any; index: number }) => {
|
||||||
{/* <div className="absolute top-4 right-4 bg-white/95 backdrop-blur-sm rounded-full px-3 py-1.5 flex items-center gap-1 shadow-lg z-10">
|
// Get cover image or first image from galleries
|
||||||
<div className="w-4 h-4 bg-gradient-to-r from-yellow-400 to-yellow-500 rounded-full flex items-center justify-center">
|
const coverImage = attraction.galleries?.find((g: any) => g.isCoverImage)?.filePathUrl
|
||||||
<span className="text-white text-xs">★</span>
|
|| attraction.galleries?.[0]?.filePathUrl
|
||||||
</div>
|
|| '';
|
||||||
<span className="text-sm font-medium text-gray-900">{attraction.rating}</span>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* Front Content - Clean Title & Location */}
|
// Filter only inclusions (isInclusion: true)
|
||||||
<div className="absolute bottom-0 left-0 right-0">
|
const inclusions = attraction.inclusions?.filter((inc: any) => inc.isInclusion) || [];
|
||||||
<div className="bg-gradient-to-t from-black/80 via-black/50 to-transparent p-6">
|
|
||||||
<h3 className="font-bold text-xl text-white mb-1">{attraction.name}</h3>
|
return (
|
||||||
<p className="text-white/90 text-sm">
|
<motion.div
|
||||||
{attraction.city}, {attraction.country}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
</p>
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="group cursor-pointer flex-shrink-0 w-[280px] md:w-auto md:flex-shrink h-96 flip-card-container"
|
||||||
|
>
|
||||||
|
<div className="flip-card-inner group-hover:[transform:rotateY(180deg)] relative w-full h-full">
|
||||||
|
|
||||||
|
{/* FRONT FACE */}
|
||||||
|
<div className="flip-card-face absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg">
|
||||||
|
<ImageWithFallback
|
||||||
|
src={coverImage}
|
||||||
|
alt={attraction.title}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0">
|
||||||
|
<div className="bg-gradient-to-t from-black/80 via-black/50 to-transparent p-6">
|
||||||
|
<h3 className="font-bold text-xl text-white mb-1">{attraction.title}</h3>
|
||||||
|
<p className="text-white/90 text-sm">{attraction.city?.cityName}, Australia</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* BACK FACE */}
|
{/* BACK FACE */}
|
||||||
<div className="flip-card-face flip-card-back absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg bg-gradient-to-br from-gray-900 to-black">
|
<div className="flip-card-face flip-card-back absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg bg-gradient-to-br from-gray-900 to-black">
|
||||||
{/* Back Content Container */}
|
<div className="relative w-full h-full p-6 flex flex-col justify-center text-white">
|
||||||
<div className="relative w-full h-full p-6 flex flex-col justify-center text-white">
|
|
||||||
|
|
||||||
{/* Included Value Section */}
|
{/* Pricing Section */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-3 py-1.5 rounded-full text-sm font-medium mb-3">
|
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-3 py-1.5 rounded-full text-sm font-medium mb-3">
|
||||||
<CheckCircle className="w-4 h-4" />
|
<CheckCircle className="w-4 h-4" />
|
||||||
<span>Included Value</span>
|
<span>Included Value</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold mb-1">
|
||||||
|
${attraction.ticketPriceAdult}
|
||||||
|
{attraction.ticketPriceChild && (
|
||||||
|
<span className="text-sm font-normal text-white/70 ml-2">
|
||||||
|
/ Child ${attraction.ticketPriceChild}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-white/80 text-sm">
|
||||||
|
{attraction.isBookingRequired ? 'Booking required' : 'No booking required'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold mb-1">{attraction.includedValue}</div>
|
|
||||||
<p className="text-white/80 text-sm">
|
|
||||||
{attraction.originalPrice === "Free"
|
|
||||||
? "Premium access included"
|
|
||||||
: "Save money with CityCard"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* What's Included List */}
|
{/* Inclusions List */}
|
||||||
<div className="mb-4">
|
{inclusions.length > 0 && (
|
||||||
<h4 className="font-semibold text-sm mb-3">What's Included:</h4>
|
<div className="mb-4">
|
||||||
<div className="space-y-2">
|
<h4 className="font-semibold text-sm mb-3">What's Included:</h4>
|
||||||
{attraction.perks.slice(0, 3).map((perk, perkIndex) => (
|
<div className="space-y-2">
|
||||||
<div key={perkIndex} className="flex items-center gap-3 text-white/90">
|
{inclusions.slice(0, 3).map((inc: any) => (
|
||||||
<div className="w-6 h-6 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center">
|
<div key={inc.id} className="flex items-center gap-3 text-white/90">
|
||||||
<perk.icon className="w-3 h-3 text-white" />
|
<div className="w-6 h-6 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center flex-shrink-0">
|
||||||
</div>
|
<CheckCircle className="w-3 h-3 text-white" />
|
||||||
<span className="text-sm">{perk.label}</span>
|
</div>
|
||||||
|
<span className="text-sm">{inc.title}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Duration & Meta Info */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="flex items-center gap-4 text-white/80 text-sm">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Clock className="w-4 h-4" />
|
|
||||||
<span>2-3 hours</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
)}
|
||||||
<Users className="w-4 h-4" />
|
|
||||||
<span>All ages</span>
|
{/* Duration & Group Info */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center gap-4 text-white/80 text-sm">
|
||||||
|
{attraction.durations && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
<span>{attraction.durations} mins</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{attraction.groupSize && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Users className="w-4 h-4" />
|
||||||
|
<span>Max {attraction.groupSize}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{attraction.ageRange && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star className="w-4 h-4" />
|
||||||
|
<span>{attraction.ageRange}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer Features */}
|
{/* Categories */}
|
||||||
<div className="border-t border-white/20 pt-4">
|
{attraction.categories?.length > 0 && (
|
||||||
<div className="flex items-center justify-between text-white/80 text-xs">
|
<div className="flex flex-wrap gap-1 mb-4">
|
||||||
<div className="flex items-center gap-2">
|
{attraction.categories.slice(0, 2).map((cat: any) => (
|
||||||
<MapPin className="w-3 h-3" />
|
<span
|
||||||
<span>Mobile ticket</span>
|
key={cat.id}
|
||||||
|
className="text-xs bg-white/20 text-white/90 px-2 py-0.5 rounded-full"
|
||||||
|
>
|
||||||
|
{cat.categoryName}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
)}
|
||||||
<CheckCircle className="w-3 h-3" />
|
|
||||||
<span>Instant confirmation</span>
|
{/* Footer */}
|
||||||
|
<div className="border-t border-white/20 pt-4">
|
||||||
|
<div className="flex items-center justify-between text-white/80 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MapPin className="w-3 h-3" />
|
||||||
|
<span>Mobile ticket</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-3 h-3" />
|
||||||
|
<span>Instant confirmation</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Decorative Elements */}
|
{/* Decorative Elements */}
|
||||||
<div className="absolute top-4 right-4 w-16 h-16 bg-gradient-to-br from-primary/20 to-secondary/20 rounded-full blur-xl"></div>
|
<div className="absolute top-4 right-4 w-16 h-16 bg-gradient-to-br from-primary/20 to-secondary/20 rounded-full blur-xl"></div>
|
||||||
<div className="absolute bottom-4 left-4 w-12 h-12 bg-gradient-to-tr from-secondary/15 to-primary/15 rounded-full blur-lg"></div>
|
<div className="absolute bottom-4 left-4 w-12 h-12 bg-gradient-to-tr from-secondary/15 to-primary/15 rounded-full blur-lg"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-20 bg-gradient-to-br from-gray-50 to-white relative overflow-hidden">
|
<section className="py-20 bg-gradient-to-br from-gray-50 to-white relative overflow-hidden">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
@@ -291,7 +187,7 @@ export function MelbourneAttractions() {
|
|||||||
<span className="font-semibold text-emphasis">Experiences</span>
|
<span className="font-semibold text-emphasis">Experiences</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||||
Discover {cityName}'s iconic landmarks, vibrant culture, world-class dining, and hidden gems - all included with your {cityName} CityCard
|
Discover {cityName}'s iconic landmarks, vibrant culture, world-class dining, and hidden gems — all included with your {cityName} CityCard
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -303,23 +199,41 @@ export function MelbourneAttractions() {
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="flex flex-wrap justify-center gap-3 mb-12"
|
className="flex flex-wrap justify-center gap-3 mb-12"
|
||||||
>
|
>
|
||||||
{categories.map((category, index) => (
|
{/* "All" button */}
|
||||||
|
<motion.button
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
onClick={() => setSelectedCategoryId(null)}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className={`px-6 py-4 h-14 rounded-2xl font-medium transition-all duration-300 ${
|
||||||
|
selectedCategoryId === null
|
||||||
|
? 'bg-gradient-to-r from-primary to-secondary text-white shadow-xl shadow-primary/25 ring-2 ring-primary/20'
|
||||||
|
: 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-primary/20 hover:bg-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* Dynamic category buttons from API */}
|
||||||
|
{apiCategories.map((category: any, index: number) => (
|
||||||
<motion.button
|
<motion.button
|
||||||
key={category}
|
key={category.id}
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
whileInView={{ opacity: 1, scale: 1 }}
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
onClick={() => setActiveCategory(category)}
|
onClick={() => setSelectedCategoryId(category.id)}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className={`px-6 py-4 h-14 rounded-2xl font-medium transition-all duration-300 ${
|
className={`px-6 py-4 h-14 rounded-2xl font-medium transition-all duration-300 ${
|
||||||
activeCategory === category
|
selectedCategoryId === category.id
|
||||||
? 'bg-gradient-to-r from-primary to-secondary text-white shadow-xl shadow-primary/25 ring-2 ring-primary/20'
|
? 'bg-gradient-to-r from-primary to-secondary text-white shadow-xl shadow-primary/25 ring-2 ring-primary/20'
|
||||||
: 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-primary/20 hover:bg-white'
|
: 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-primary/20 hover:bg-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{category}
|
{category.categoryName}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -327,52 +241,44 @@ export function MelbourneAttractions() {
|
|||||||
{/* Mobile Horizontal Carousel */}
|
{/* Mobile Horizontal Carousel */}
|
||||||
<div className="block md:hidden mb-8">
|
<div className="block md:hidden mb-8">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Scroll Container */}
|
|
||||||
<div className="flex gap-6 overflow-x-auto scrollbar-hide pb-4 px-4 -mx-4">
|
<div className="flex gap-6 overflow-x-auto scrollbar-hide pb-4 px-4 -mx-4">
|
||||||
{filteredAttractions.map((attraction, index) => (
|
{filteredAttractions.map((attraction: any, index: number) => (
|
||||||
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
|
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scroll Indicators */}
|
|
||||||
<div className="flex justify-center mt-6 gap-2">
|
<div className="flex justify-center mt-6 gap-2">
|
||||||
{Array.from({ length: Math.ceil(filteredAttractions.length / 2) }).map((_, index) => (
|
{Array.from({ length: Math.ceil(filteredAttractions.length / 2) }).map((_: any, index: number) => (
|
||||||
<div
|
<div key={index} className="w-2 h-2 rounded-full bg-gray-300" />
|
||||||
key={index}
|
|
||||||
className="w-2 h-2 rounded-full bg-gray-300"
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Hint Text */}
|
|
||||||
<div className="text-center mt-4">
|
<div className="text-center mt-4">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">Swipe to explore more {cityName} attractions</p>
|
||||||
Swipe to explore more Melbourne attractions
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Bento Grid */}
|
{/* Desktop Bento Grid */}
|
||||||
<div className="hidden md:block w-full">
|
<div className="hidden md:block w-full">
|
||||||
{/* Top Row - 3 equal cards */}
|
|
||||||
<div className="grid grid-cols-3 gap-6">
|
<div className="grid grid-cols-3 gap-6">
|
||||||
{filteredAttractions.slice(0, 3).map((attraction, index) => (
|
{filteredAttractions.slice(0, 3).map((attraction: any, index: number) => (
|
||||||
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
|
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Consistent Vertical Spacing */}
|
|
||||||
<div className="h-6"></div>
|
<div className="h-6"></div>
|
||||||
|
|
||||||
{/* Bottom Row - 2 larger cards */}
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
{filteredAttractions.slice(3, 5).map((attraction, index) => (
|
{filteredAttractions.slice(3, 5).map((attraction: any, index: number) => (
|
||||||
<AttractionCard key={attraction.id} attraction={attraction} index={index + 3} />
|
<AttractionCard key={attraction.id} attraction={attraction} index={index + 3} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
{filteredAttractions.length === 0 && (
|
||||||
|
<div className="text-center py-16 text-gray-500">
|
||||||
|
<p className="text-lg">No attractions found for this category.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Call to Action */}
|
{/* Call to Action */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
@@ -385,16 +291,15 @@ export function MelbourneAttractions() {
|
|||||||
whileHover={{ scale: 1.05, boxShadow: "0 20px 40px rgba(99,102,241,0.3)" }}
|
whileHover={{ scale: 1.05, boxShadow: "0 20px 40px rgba(99,102,241,0.3)" }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className="relative bg-gradient-to-r from-primary to-secondary text-white py-4 px-12 rounded-lg text-lg shadow-xl transition-all duration-300 overflow-hidden group"
|
className="relative bg-gradient-to-r from-primary to-secondary text-white py-4 px-12 rounded-lg text-lg shadow-xl transition-all duration-300 overflow-hidden group"
|
||||||
onClick={()=>navigate('/passes')}
|
onClick={() => navigate('/passes')}
|
||||||
>
|
>
|
||||||
<span className="relative z-10">Get Your {cityName} Card</span>
|
<span className="relative z-10">Get Your {cityName} Card</span>
|
||||||
|
|
||||||
{/* Shine animation */}
|
|
||||||
<div className="absolute inset-0 opacity-30">
|
<div className="absolute inset-0 opacity-30">
|
||||||
<div className="h-full bg-gradient-to-r from-transparent via-white to-transparent animate-shine"></div>
|
<div className="h-full bg-gradient-to-r from-transparent via-white to-transparent animate-shine"></div>
|
||||||
</div>
|
</div>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { motion } from 'motion/react';
|
|||||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||||
import { Calendar, Clock, User, ArrowRight, Coffee, Camera, MapPin, Star } from 'lucide-react';
|
import { Calendar, Clock, User, ArrowRight, Coffee, Camera, MapPin, Star } from 'lucide-react';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useGetBlogsForCityQuery } from '../Redux/services/blogs.service';
|
||||||
|
|
||||||
const blogPosts = [
|
const blogPosts = [
|
||||||
{
|
{
|
||||||
@@ -93,10 +94,22 @@ export function MelbourneBlogs() {
|
|||||||
|
|
||||||
const sectionRef = useRef(null);
|
const sectionRef = useRef(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const cityId = localStorage.getItem('cityId');
|
||||||
|
const [categoryId, setCategoryId] = useState("");
|
||||||
|
const { data: blogsData, error, isLoading } = useGetBlogsForCityQuery({ cityId, categoryId });
|
||||||
const featuredPost = blogPosts.find(post => post.featured);
|
const featuredPost = blogPosts.find(post => post.featured);
|
||||||
const regularPosts = blogPosts.filter(post => !post.featured);
|
const regularPosts = blogPosts.filter(post => !post.featured);
|
||||||
|
|
||||||
const cityName = localStorage.getItem('cityName');
|
const cityName = localStorage.getItem('cityName');
|
||||||
|
const baseUrl = import.meta.env.VITE_BASE_URL;
|
||||||
|
|
||||||
|
const blogss = blogsData?.blogs ?? [];
|
||||||
|
const categoriess = blogsData?.categories ?? []
|
||||||
|
|
||||||
|
const handleCategoryClick = (id: string) => {
|
||||||
|
// toggle logic: if already selected, reset to empty
|
||||||
|
setCategoryId(prev => (prev === id ? "" : id));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
@@ -145,29 +158,42 @@ export function MelbourneBlogs() {
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="flex flex-wrap justify-center gap-3 mb-16"
|
className="flex flex-wrap justify-center gap-3 mb-16"
|
||||||
>
|
>
|
||||||
{categories.map((category, index) => (
|
<motion.button
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={() => setCategoryId("")}
|
||||||
|
className={`cursor-pointer px-6 py-3 rounded-full font-medium shadow-lg hover:shadow-xl transition-all duration-300 group
|
||||||
|
${categoryId === "" ? "bg-gradient-to-r from-primary to-secondary text-white" : "bg-white text-gray-700"}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">All</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{categoriess.map((category: any, index) => (
|
||||||
<motion.button
|
<motion.button
|
||||||
key={category.name}
|
key={category.id}
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
whileInView={{ opacity: 1, scale: 1 }}
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.4, delay: 0.2 + index * 0.05 }}
|
transition={{ duration: 0.4, delay: 0.2 + index * 0.05 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className={`px-6 py-3 rounded-full bg-gradient-to-r ${category.color} text-white font-medium shadow-lg hover:shadow-xl transition-all duration-300 group`}
|
onClick={() => handleCategoryClick(category.id)}
|
||||||
|
className={`cursor-pointer px-6 py-3 rounded-full font-medium shadow-lg hover:shadow-xl transition-all duration-300 group
|
||||||
|
${categoryId === category.id ? "bg-gradient-to-r from-primary to-secondary text-white" : "bg-white text-gray-700"}`}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
{category.name}
|
{category.categoryName}
|
||||||
<span className="text-xs bg-white/20 px-2 py-1 rounded-full group-hover:bg-white/30 transition-colors duration-200">
|
|
||||||
{category.count}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|
||||||
{/* Featured Post */}
|
{/* Featured Post */}
|
||||||
{featuredPost && (
|
{/* {featuredPost && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
@@ -177,13 +203,13 @@ export function MelbourneBlogs() {
|
|||||||
>
|
>
|
||||||
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
{/* Regular Blog Posts Grid */}
|
{/* Regular Blog Posts Grid */}
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{regularPosts.map((post, index) => (
|
{blogss && blogss?.map((blog: any, index) => (
|
||||||
<motion.article
|
<motion.article
|
||||||
key={post.id}
|
key={blog.id}
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5, delay: 0.3 + index * 0.1 }}
|
transition={{ duration: 0.5, delay: 0.3 + index * 0.1 }}
|
||||||
@@ -193,47 +219,52 @@ export function MelbourneBlogs() {
|
|||||||
{/* Post Image */}
|
{/* Post Image */}
|
||||||
<div className="relative overflow-hidden h-48">
|
<div className="relative overflow-hidden h-48">
|
||||||
<ImageWithFallback
|
<ImageWithFallback
|
||||||
src={post.image}
|
src={`${baseUrl}${blog?.coverImage}`}
|
||||||
alt={post.title}
|
alt={blog?.blogTitle}
|
||||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||||
|
|
||||||
{/* Category Badge */}
|
{/* Category Badge */}
|
||||||
<div className="absolute top-4 left-4 bg-white/95 backdrop-blur-sm text-gray-900 px-3 py-1 rounded-full text-xs font-medium">
|
<div className="absolute top-4 left-4 bg-white/95 backdrop-blur-sm text-gray-900 px-3 py-1 rounded-full text-xs font-medium">
|
||||||
{post.category}
|
{blog?.category?.categoryName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Post Content */}
|
{/* Post Content */}
|
||||||
<div className="p-6 flex-1 flex flex-col justify-between">
|
<div className="p-6 flex-1 flex flex-col justify-between">
|
||||||
<div className="flex items-center gap-3 text-xs text-gray-500 mb-3">
|
<div className="flex items-center gap-3 text-xs text-gray-500 mb-3">
|
||||||
<div className="flex items-center gap-1">
|
{/* <div className="flex items-center gap-1">
|
||||||
<User className="w-3 h-3" />
|
<User className="w-3 h-3" />
|
||||||
{post.author}
|
{blog?.author}
|
||||||
</div>
|
</div> */}
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Calendar className="w-3 h-3" />
|
<Calendar className="w-3 h-3" />
|
||||||
{post.date}
|
{blog?.createdAt && new Date(blog.createdAt).toLocaleDateString(
|
||||||
|
'en-US',
|
||||||
|
{ month: 'short', day: 'numeric', year: 'numeric' }
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
{post.readTime}
|
5 min read
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<h3 className="font-merchant text-xl font-semibold text-gray-900 mb-3 leading-tight group-hover:text-primary transition-colors duration-200 line-clamp-2">
|
<h3 className="font-merchant text-xl font-semibold text-gray-900 mb-3 leading-tight group-hover:text-primary transition-colors duration-200 line-clamp-2">
|
||||||
{post.title}
|
{blog?.blogTitle}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className="text-gray-600 leading-relaxed mb-4 text-sm flex-1 line-clamp-3">
|
<p
|
||||||
{post.excerpt}
|
className="text-gray-600 leading-relaxed mb-4 text-sm flex-1 line-clamp-3"
|
||||||
</p>
|
dangerouslySetInnerHTML={{ __html: blog?.content }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
<div className="flex flex-wrap gap-1 mb-4">
|
{/* <div className="flex flex-wrap gap-1 mb-4">
|
||||||
{post.tags.slice(0, 2).map((tag, tagIndex) => (
|
{blog?.tags?.slice(0, 2).map((tag, tagIndex) => (
|
||||||
<span
|
<span
|
||||||
key={tagIndex}
|
key={tagIndex}
|
||||||
className="px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-medium"
|
className="px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-medium"
|
||||||
@@ -241,12 +272,12 @@ export function MelbourneBlogs() {
|
|||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{post.tags.length > 2 && (
|
{blog?.tags?.length > 2 && (
|
||||||
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs">
|
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs">
|
||||||
+{post.tags.length - 2}
|
+{blog?.tags?.length - 2}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between mt-auto">
|
<div className="flex items-center justify-between mt-auto">
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Button } from './ui/button';
|
|||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
// const cardOptions = [
|
|
||||||
// {
|
// {
|
||||||
// id: 'selective',
|
// id: 'selective',
|
||||||
// name: 'Flexi Card',
|
// name: 'Flexi Card',
|
||||||
|
|||||||
@@ -94,10 +94,8 @@ export default function Navbar({
|
|||||||
|
|
||||||
const baseUrl = import.meta.env.VITE_BASE_URL;
|
const baseUrl = import.meta.env.VITE_BASE_URL;
|
||||||
|
|
||||||
const protectedPaths = ["/passes", "/whats-included", "/", "/melbourne"];
|
|
||||||
|
|
||||||
const handleOpenLoginModal = () => {
|
const handleOpenLoginModal = () => {
|
||||||
if (!user && protectedPaths.includes(location.pathname)) {
|
if (!user) {
|
||||||
setLoginOpen(true);
|
setLoginOpen(true);
|
||||||
}
|
}
|
||||||
else if (!user) {
|
else if (!user) {
|
||||||
@@ -120,17 +118,17 @@ export default function Navbar({
|
|||||||
melbourneLabel: 'How It Works'
|
melbourneLabel: 'How It Works'
|
||||||
},
|
},
|
||||||
// Position 2
|
// Position 2
|
||||||
{
|
|
||||||
label: 'Magic Itinerary',
|
|
||||||
path: '/landing-magic-itinerary',
|
|
||||||
isShared: false
|
|
||||||
},
|
|
||||||
// Position 3
|
// Position 3
|
||||||
{
|
{
|
||||||
label: 'Whats Included',
|
label: 'Whats Included',
|
||||||
path: '/whats-included',
|
path: '/whats-included',
|
||||||
isShared: false
|
isShared: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Magic Itinerary',
|
||||||
|
path: '/landing-magic-itinerary',
|
||||||
|
isShared: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'FAQ',
|
label: 'FAQ',
|
||||||
path: '/faq',
|
path: '/faq',
|
||||||
@@ -503,7 +501,7 @@ export default function Navbar({
|
|||||||
>
|
>
|
||||||
<div className="">
|
<div className="">
|
||||||
<motion.div
|
<motion.div
|
||||||
className={`w-full transition-all duration-500 ease-out px-8 py-4 bg-white backdrop-blur-[20px] border border-white/20 ${isScrolled
|
className={`w-full transition-all duration-500 ease-out px-3 py-3 bg-white backdrop-blur-[20px] border border-white/20 ${isScrolled
|
||||||
? 'shadow-[0_10px_15px_-3px_rgba(0,0,0,0.08),0_4px_6px_-2px_rgba(0,0,0,0.05)]'
|
? 'shadow-[0_10px_15px_-3px_rgba(0,0,0,0.08),0_4px_6px_-2px_rgba(0,0,0,0.05)]'
|
||||||
: 'shadow-lg shadow-black/5'
|
: 'shadow-lg shadow-black/5'
|
||||||
}`}
|
}`}
|
||||||
@@ -529,7 +527,7 @@ export default function Navbar({
|
|||||||
? 'Melbourne CityCards Logo'
|
? 'Melbourne CityCards Logo'
|
||||||
: 'CityCards Logo'
|
: 'CityCards Logo'
|
||||||
}
|
}
|
||||||
className="h-14 w-auto"
|
className="h-17 w-auto"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -613,27 +611,7 @@ export default function Navbar({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Shopping Cart */}
|
<ShoppingBag className="w-6 h-6 cursor-pointer" onClick={() => navigate("/cart")} />
|
||||||
{/* <Dropdown
|
|
||||||
ref={cartRef}
|
|
||||||
isOpen={activeCartDropdown}
|
|
||||||
onToggle={() => setActiveCartDropdown(prev => !prev)}
|
|
||||||
items={cartDropdownItems}
|
|
||||||
title="Shopping Cart"
|
|
||||||
trigger={
|
|
||||||
<div className="relative text-gray-700 hover:text-gray-900 transition-colors duration-200 rounded-lg hover:bg-gray-50/50 cursor-pointer p-2">
|
|
||||||
<ShoppingBag className="w-6 h-6" />
|
|
||||||
<motion.div
|
|
||||||
className="absolute -top-1 -right-1 w-5 h-5 bg-primary rounded-full flex items-center justify-center"
|
|
||||||
animate={{ scale: [1, 1.1, 1] }}
|
|
||||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
|
||||||
>
|
|
||||||
<span className="text-xs text-primary-foreground font-bold">{cartItems.length}</span>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/> */}
|
|
||||||
<ShoppingBag className="w-6 h-6" onClick={() => navigate("/cart")} />
|
|
||||||
|
|
||||||
{/* Enhanced City Card Button with Source Tracking */}
|
{/* Enhanced City Card Button with Source Tracking */}
|
||||||
<div className="flex items-center gap-3 pl-2">
|
<div className="flex items-center gap-3 pl-2">
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTou
|
|||||||
const nextCard = attractionCards[(currentCardIndex + 1) % attractionCards.length];
|
const nextCard = attractionCards[(currentCardIndex + 1) % attractionCards.length];
|
||||||
const thirdCard = attractionCards[(currentCardIndex + 2) % attractionCards.length];
|
const thirdCard = attractionCards[(currentCardIndex + 2) % attractionCards.length];
|
||||||
|
|
||||||
|
const cityName = localStorage.getItem("cityName")
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full min-h-[90vh] overflow-hidden flex items-center bg-gradient-to-br from-orange-50 via-white to-rose-50">
|
<div className="relative w-full min-h-[90vh] overflow-hidden flex items-center bg-gradient-to-br from-orange-50 via-white to-rose-50">
|
||||||
{/* Gradient Background Elements */}
|
{/* Gradient Background Elements */}
|
||||||
@@ -109,7 +110,7 @@ export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTou
|
|||||||
>
|
>
|
||||||
<Wand2 className="w-5 h-5 text-primary drop-shadow-lg" />
|
<Wand2 className="w-5 h-5 text-primary drop-shadow-lg" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<span className="font-poppins font-semibold text-gray-800">AI-Powered Planning</span>
|
<span className="font-poppins font-semibold text-gray-800">Smart Planning</span>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-1.5 h-1.5 bg-primary rounded-full"
|
className="w-1.5 h-1.5 bg-primary rounded-full"
|
||||||
animate={{
|
animate={{
|
||||||
@@ -130,14 +131,14 @@ export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTou
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="font-poppins text-lg md:text-xl font-normal leading-relaxed text-gray-600 mb-8">
|
<p className="font-poppins text-lg md:text-xl font-normal leading-relaxed text-gray-600 mb-8">
|
||||||
Let AI craft a personalized journey tailored to your interests, timeline, and travel style. Get the perfect itinerary in minutes.
|
Craft a personalized journey tailored to your interests, timeline, and travel style. Get the perfect itinerary in minutes.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Quick Features */}
|
{/* Quick Features */}
|
||||||
<div className="space-y-3 mb-8">
|
<div className="space-y-3 mb-8">
|
||||||
{[
|
{[
|
||||||
{ icon: <Sparkles className="w-5 h-5" />, text: 'AI-powered smart suggestions' },
|
{ icon: <Sparkles className="w-5 h-5" />, text: 'Smart suggestions' },
|
||||||
{ icon: <MapPin className="w-5 h-5" />, text: '40+ top Melbourne attractions' },
|
{ icon: <MapPin className="w-5 h-5" />, text: `40+ top ${cityName} attractions` },
|
||||||
{ icon: <Calendar className="w-5 h-5" />, text: 'Flexible & customizable plans' }
|
{ icon: <Calendar className="w-5 h-5" />, text: 'Flexible & customizable plans' }
|
||||||
].map((feature, index) => (
|
].map((feature, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -173,10 +174,10 @@ export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTou
|
|||||||
<span className="relative z-10">Create My Itinerary</span>
|
<span className="relative z-10">Create My Itinerary</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p className="font-poppins text-sm text-gray-600 font-normal flex items-center gap-2">
|
{/* <p className="font-poppins text-sm text-gray-600 font-normal flex items-center gap-2">
|
||||||
<Sparkles className="w-4 h-4 text-primary" />
|
<Sparkles className="w-4 h-4 text-primary" />
|
||||||
<span>Free • Takes less than 2 minutes</span>
|
<span>Free • Takes less than 2 minutes</span>
|
||||||
</p>
|
</p> */}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
29
src/components/ProtectedRoute.tsx
Normal file
29
src/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// ProtectedRoute.tsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { LoginModal } from './LoginModal';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isLoginOpen, setIsLoginOpen] = useState(!user);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<LoginModal
|
||||||
|
isOpen={isLoginOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsLoginOpen(false);
|
||||||
|
navigate(-1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//RegisterPage.tsx
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Input } from './ui/input';
|
import { Input } from './ui/input';
|
||||||
@@ -8,9 +9,11 @@ import Navbar from './Navbar';
|
|||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Label } from './ui/label';
|
import { Label } from './ui/label';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const { login } = useAuth();
|
const { login, user } = useAuth();
|
||||||
const email = localStorage.getItem("userEmail")
|
const email = localStorage.getItem("userEmail")
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
firstName: '',
|
firstName: '',
|
||||||
@@ -28,64 +31,137 @@ export default function RegisterPage() {
|
|||||||
|
|
||||||
const [helperText, setHelperText] = useState('');
|
const [helperText, setHelperText] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pendingEmail = localStorage.getItem("userEmail");
|
||||||
|
if (user || !pendingEmail) {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
const [register, { isLoading: isRegistering }] = useRegisterMutation();
|
const [register, { isLoading: isRegistering }] = useRegisterMutation();
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: string) => {
|
const handleInputChange = (field: string, value: string) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const validateForm = () => {
|
||||||
|
// // First Name
|
||||||
|
// if (!formData.firstName.trim()) return toast.error('First name is required'), false;
|
||||||
|
// if (/\s/.test(formData.firstName)) return toast.error('First name must not contain spaces'), false;
|
||||||
|
// if (!/^[A-Za-z]+$/.test(formData.firstName)) return toast.error('First name must contain only letters (A–Z)'), false;
|
||||||
|
// if (formData.firstName.length < 2 || formData.firstName.length > 50) return toast.error('First name must be between 2 and 50 characters'), false;
|
||||||
|
|
||||||
|
// // Last Name
|
||||||
|
// if (!formData.lastName.trim()) return toast.error('Last name is required'), false;
|
||||||
|
// if (/\s/.test(formData.lastName)) return toast.error('Last name must not contain spaces'), false;
|
||||||
|
// if (!/^[A-Za-z]+$/.test(formData.lastName)) return toast.error('Last name must contain only letters (A–Z)'), false;
|
||||||
|
// if (formData.lastName.length < 2 || formData.lastName.length > 50) return toast.error('Last name must be between 2 and 50 characters'), false;
|
||||||
|
|
||||||
|
// // Email
|
||||||
|
// if (!formData.emailAddress.trim()) return toast.error('Email address is required'), false;
|
||||||
|
// if (!/\S+@\S+\.\S+/.test(formData.emailAddress)) return toast.error('Enter a valid email (e.g. name@example.com)'), false;
|
||||||
|
|
||||||
|
// // ISD
|
||||||
|
// if (!formData.isdCode.trim()) return toast.error('ISD code is required'), false;
|
||||||
|
// if (/\s/.test(formData.isdCode)) return toast.error('ISD code must not contain spaces'), false;
|
||||||
|
// if (!formData.isdCode.startsWith('+')) return toast.error("ISD code must start with '+' (e.g. +91)"), false;
|
||||||
|
// if (!/^\+\d+$/.test(formData.isdCode)) return toast.error("ISD code must contain only digits after '+'"), false;
|
||||||
|
|
||||||
|
// // Phone
|
||||||
|
// if (!formData.mobileNumber.trim()) return toast.error('Mobile number is required'), false;
|
||||||
|
// if (/\s/.test(formData.mobileNumber)) return toast.error('Mobile number must not contain spaces'), false;
|
||||||
|
// if (!/^\d+$/.test(formData.mobileNumber)) return toast.error('Mobile number must contain only digits (0–9)'), false;
|
||||||
|
// if (formData.mobileNumber.length < 7 || formData.mobileNumber.length > 15) return toast.error('Mobile number must be between 7 and 15 digits'), false;
|
||||||
|
|
||||||
|
// // Address
|
||||||
|
// if (!formData.address1.trim()) return toast.error('Address is required'), false;
|
||||||
|
// if (!/^[A-Za-z0-9\s,\-.]+$/.test(formData.address1)) return toast.error('Address can only contain letters, numbers, spaces, commas, dots, and hyphens'), false;
|
||||||
|
// if (formData.address1.length < 5 || formData.address1.length > 100) return toast.error('Address must be between 5 and 100 characters'), false;
|
||||||
|
|
||||||
|
// // City
|
||||||
|
// if (!formData.city.trim()) return toast.error('City is required'), false;
|
||||||
|
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.city)) return toast.error('City can only contain letters and spaces'), false;
|
||||||
|
// if (/\s{2,}/.test(formData.city)) return toast.error('City must not contain multiple consecutive spaces'), false;
|
||||||
|
|
||||||
|
// // State
|
||||||
|
// if (!formData.state.trim()) return toast.error('State is required'), false;
|
||||||
|
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.state)) return toast.error('State can only contain letters and spaces'), false;
|
||||||
|
// if (/\s{2,}/.test(formData.state)) return toast.error('State must not contain multiple consecutive spaces'), false;
|
||||||
|
|
||||||
|
// // Country
|
||||||
|
// if (!formData.country.trim()) return toast.error('Country is required'), false;
|
||||||
|
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.country)) return toast.error('Country can only contain letters and spaces'), false;
|
||||||
|
|
||||||
|
// // Postal Code
|
||||||
|
// if (!formData.postalCode.trim()) return toast.error('Postal code is required'), false;
|
||||||
|
// if (/\s/.test(formData.postalCode)) return toast.error('Postal code must not contain spaces'), false;
|
||||||
|
// if (!/^[A-Za-z0-9]+$/.test(formData.postalCode)) return toast.error('Postal code must contain only letters and numbers'), false;
|
||||||
|
// if (formData.postalCode.length < 4 || formData.postalCode.length > 10) return toast.error('Postal code must be between 4 and 10 characters'), false;
|
||||||
|
|
||||||
|
// return true;
|
||||||
|
// };
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
// First Name
|
const e: Record<string, string> = {};
|
||||||
if (!formData.firstName.trim()) return toast.error('First name is required'), false;
|
|
||||||
if (!/^[A-Za-z]+$/.test(formData.firstName)) return toast.error('First name must contain only alphabets'), false;
|
|
||||||
if (formData.firstName.length < 2 || formData.firstName.length > 50) return toast.error('First name must be between 2 and 50 characters'), false;
|
|
||||||
|
|
||||||
// Last Name
|
if (!formData.firstName.trim()) e.firstName = 'First name is required';
|
||||||
if (!formData.lastName.trim()) return toast.error('Last name is required'), false;
|
else if (/\s/.test(formData.firstName)) e.firstName = 'First name must not contain spaces';
|
||||||
if (!/^[A-Za-z]+$/.test(formData.lastName)) return toast.error('Last name must contain only alphabets'), false;
|
else if (!/^[A-Za-z]+$/.test(formData.firstName)) e.firstName = 'First name must contain only letters (A–Z)';
|
||||||
if (formData.lastName.length < 2 || formData.lastName.length > 50) return toast.error('Last name must be between 2 and 50 characters'), false;
|
else if (formData.firstName.length < 2 || formData.firstName.length > 50) e.firstName = 'First name must be between 2 and 50 characters';
|
||||||
|
|
||||||
// Email
|
if (!formData.lastName.trim()) e.lastName = 'Last name is required';
|
||||||
if (!formData.emailAddress.includes('@')) return toast.error('Invalid email address'), false;
|
else if (/\s/.test(formData.lastName)) e.lastName = 'Last name must not contain spaces';
|
||||||
|
else if (!/^[A-Za-z]+$/.test(formData.lastName)) e.lastName = 'Last name must contain only letters (A–Z)';
|
||||||
|
else if (formData.lastName.length < 2 || formData.lastName.length > 50) e.lastName = 'Last name must be between 2 and 50 characters';
|
||||||
|
|
||||||
// ISD Code
|
if (!formData.emailAddress.trim()) e.emailAddress = 'Email address is required';
|
||||||
if (!formData.isdCode.startsWith("+")) return toast.error("ISD code must start with '+'"), false;
|
else if (!/\S+@\S+\.\S+/.test(formData.emailAddress)) e.emailAddress = 'Enter a valid email (e.g. name@example.com)';
|
||||||
if (!/^\+\d+$/.test(formData.isdCode)) return toast.error("ISD code must contain only numbers after '+'"), false;
|
|
||||||
|
|
||||||
// Mobile Number
|
if (!formData.isdCode.trim()) e.isdCode = 'ISD code is required';
|
||||||
if (!/^\d+$/.test(formData.mobileNumber)) return toast.error('Invalid mobile number'), false;
|
else if (/\s/.test(formData.isdCode)) e.isdCode = 'ISD code must not contain spaces';
|
||||||
if (formData.mobileNumber.length < 7 || formData.mobileNumber.length > 15) return toast.error('Mobile number must be between 7 and 15 digits'), false;
|
else if (!formData.isdCode.startsWith('+')) e.isdCode = "ISD code must start with '+' (e.g. +91)";
|
||||||
|
else if (!/^\+\d+$/.test(formData.isdCode)) e.isdCode = "ISD code must contain only digits after '+'";
|
||||||
|
|
||||||
// Address Line 1
|
if (!formData.mobileNumber.trim()) e.mobileNumber = 'Mobile number is required';
|
||||||
if (!formData.address1.trim()) return toast.error('Address required'), false;
|
else if (/\s/.test(formData.mobileNumber)) e.mobileNumber = 'Mobile number must not contain spaces';
|
||||||
if (!/^[A-Za-z0-9\s]+$/.test(formData.address1)) return toast.error('Address must be alphanumeric'), false;
|
else if (!/^\d+$/.test(formData.mobileNumber)) e.mobileNumber = 'Mobile number must contain only digits (0–9)';
|
||||||
if (formData.address1.length < 5 || formData.address1.length > 100) return toast.error('Address must be between 5 and 100 characters'), false;
|
else if (formData.mobileNumber.length < 7 || formData.mobileNumber.length > 15) e.mobileNumber = 'Mobile number must be between 7 and 15 digits';
|
||||||
|
|
||||||
// City
|
if (!formData.address1.trim()) e.address1 = 'Address is required';
|
||||||
if (!formData.city.trim()) return toast.error('City required'), false;
|
else if (!/^[A-Za-z0-9\s,\-.]+$/.test(formData.address1)) e.address1 = 'Address can only contain letters, numbers, spaces, commas, dots, and hyphens';
|
||||||
if (!/^[A-Za-z\s]+$/.test(formData.city)) return toast.error('City must contain only alphabets'), false;
|
else if (formData.address1.length < 5 || formData.address1.length > 100) e.address1 = 'Address must be between 5 and 100 characters';
|
||||||
if (formData.city.length < 2 || formData.city.length > 50) return toast.error('City must be between 2 and 50 characters'), false;
|
|
||||||
|
|
||||||
// State
|
if (!formData.city.trim()) e.city = 'City is required';
|
||||||
if (!formData.state.trim()) return toast.error('State required'), false;
|
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.city)) e.city = 'City can only contain letters and spaces';
|
||||||
if (!/^[A-Za-z\s]+$/.test(formData.state)) return toast.error('State must contain only alphabets'), false;
|
else if (/\s{2,}/.test(formData.city)) e.city = 'City must not contain multiple consecutive spaces';
|
||||||
if (formData.state.length < 2 || formData.state.length > 50) return toast.error('State must be between 2 and 50 characters'), false;
|
|
||||||
|
|
||||||
// Country
|
if (!formData.state.trim()) e.state = 'State is required';
|
||||||
if (!formData.country.trim()) return toast.error('Country required'), false;
|
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.state)) e.state = 'State can only contain letters and spaces';
|
||||||
if (!/^[A-Za-z\s]+$/.test(formData.country)) return toast.error('Country must contain only alphabets'), false;
|
else if (/\s{2,}/.test(formData.state)) e.state = 'State must not contain multiple consecutive spaces';
|
||||||
if (formData.country.length < 2 || formData.country.length > 50) return toast.error('Country must be between 2 and 50 characters'), false;
|
|
||||||
|
|
||||||
// Postal Code
|
if (!formData.country.trim()) e.country = 'Country is required';
|
||||||
if (!/^\d+$/.test(formData.postalCode)) return toast.error('Postal code should only contain numbers'), false;
|
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.country)) e.country = 'Country can only contain letters and spaces';
|
||||||
if (formData.postalCode.length < 4 || formData.postalCode.length > 10) return toast.error('Postal code must be between 4 and 10 digits'), false;
|
else if (formData.country.length < 2 || formData.country.length > 50) e.country = 'Country must be between 2 and 50 characters';
|
||||||
|
|
||||||
return true;
|
if (!formData.postalCode.trim()) e.postalCode = 'Postal code is required';
|
||||||
|
else if (/\s/.test(formData.postalCode)) e.postalCode = 'Postal code must not contain spaces';
|
||||||
|
else if (!/^[A-Za-z0-9]+$/.test(formData.postalCode)) e.postalCode = 'Postal code must contain only letters and numbers';
|
||||||
|
else if (formData.postalCode.length < 4 || formData.postalCode.length > 10) e.postalCode = 'Postal code must be between 4 and 10 characters';
|
||||||
|
|
||||||
|
setFieldErrors(e);
|
||||||
|
return Object.keys(e).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to render inline error (add once near top of return or as a small component)
|
||||||
|
const FieldError = ({ name }: { name: string }) =>
|
||||||
|
fieldErrors[name] ? (
|
||||||
|
<p className="text-xs text-red-500 mt-1 flex items-center gap-1">
|
||||||
|
<AlertCircle className="w-3 h-3" />{fieldErrors[name]}
|
||||||
|
</p>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
|
||||||
const handleRegister = async () => {
|
const handleRegister = async () => {
|
||||||
@@ -145,28 +221,36 @@ export default function RegisterPage() {
|
|||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="firstName" className="font-poppins font-light">First Name</Label>
|
<Label htmlFor="firstName" className="font-poppins font-light">
|
||||||
|
First Name <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="firstName"
|
id="firstName"
|
||||||
value={formData.firstName}
|
value={formData.firstName}
|
||||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||||
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
|
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.firstName ? 'border border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="firstName" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="lastName" className="font-poppins font-light">Last Name</Label>
|
<Label htmlFor="lastName" className="font-poppins font-light">
|
||||||
|
Last Name <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="lastName"
|
id="lastName"
|
||||||
value={formData.lastName}
|
value={formData.lastName}
|
||||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||||
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
|
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.lastName ? 'border border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="lastName" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="emailAddress" className="font-poppins font-light">Email Address</Label>
|
<Label htmlFor="emailAddress" className="font-poppins font-light">
|
||||||
|
Email Address <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="emailAddress"
|
id="emailAddress"
|
||||||
type="email"
|
type="email"
|
||||||
@@ -175,28 +259,35 @@ export default function RegisterPage() {
|
|||||||
onChange={(e) => handleInputChange('emailAddress', e.target.value)}
|
onChange={(e) => handleInputChange('emailAddress', e.target.value)}
|
||||||
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
|
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="emailAddress" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-6">
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="isdCode" className="font-poppins font-light">ISD Code</Label>
|
<Label htmlFor="isdCode" className="font-poppins font-light">
|
||||||
|
ISD Code <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="isdCode"
|
id="isdCode"
|
||||||
placeholder="example: +91"
|
placeholder="example: +91"
|
||||||
value={formData.isdCode}
|
value={formData.isdCode}
|
||||||
onChange={(e) => handleInputChange('isdCode', e.target.value)}
|
onChange={(e) => handleInputChange('isdCode', e.target.value)}
|
||||||
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
|
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.isdCode ? 'border border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="isdCode" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Label htmlFor="mobileNumber" className="font-poppins font-light">Mobile Number</Label>
|
<Label htmlFor="mobileNumber" className="font-poppins font-light">
|
||||||
|
Mobile Number <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="mobileNumber"
|
id="mobileNumber"
|
||||||
value={formData.mobileNumber}
|
value={formData.mobileNumber}
|
||||||
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
|
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
|
||||||
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
|
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.mobileNumber ? 'border border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="mobileNumber" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,13 +299,16 @@ export default function RegisterPage() {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="address1" className="font-poppins font-light">Address Line 1</Label>
|
<Label htmlFor="address1" className="font-poppins font-light">
|
||||||
|
Address Line 1 <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="address1"
|
id="address1"
|
||||||
value={formData.address1}
|
value={formData.address1}
|
||||||
onChange={(e) => handleInputChange('address1', e.target.value)}
|
onChange={(e) => handleInputChange('address1', e.target.value)}
|
||||||
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
|
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.address1 ? 'border border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="address1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -229,45 +323,57 @@ export default function RegisterPage() {
|
|||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="city" className="font-poppins font-light">City</Label>
|
<Label htmlFor="city" className="font-poppins font-light">
|
||||||
|
City <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="city"
|
id="city"
|
||||||
value={formData.city}
|
value={formData.city}
|
||||||
onChange={(e) => handleInputChange('city', e.target.value)}
|
onChange={(e) => handleInputChange('city', e.target.value)}
|
||||||
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
|
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.city ? 'border border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="city" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="state" className="font-poppins font-light">State</Label>
|
<Label htmlFor="state" className="font-poppins font-light">
|
||||||
|
State <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="state"
|
id="state"
|
||||||
value={formData.state}
|
value={formData.state}
|
||||||
onChange={(e) => handleInputChange('state', e.target.value)}
|
onChange={(e) => handleInputChange('state', e.target.value)}
|
||||||
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
|
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.state ? 'border border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="state" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="country" className="font-poppins font-light">Country</Label>
|
<Label htmlFor="country" className="font-poppins font-light">
|
||||||
|
Country <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="country"
|
id="country"
|
||||||
value={formData.country}
|
value={formData.country}
|
||||||
onChange={(e) => handleInputChange('country', e.target.value)}
|
onChange={(e) => handleInputChange('country', e.target.value)}
|
||||||
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
|
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.country ? 'border border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="country" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="postalCode" className="font-poppins font-light">Postal Code</Label>
|
<Label htmlFor="postalCode" className="font-poppins font-light">
|
||||||
|
Postal Code <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="postalCode"
|
id="postalCode"
|
||||||
value={formData.postalCode}
|
value={formData.postalCode}
|
||||||
onChange={(e) => handleInputChange('postalCode', e.target.value)}
|
onChange={(e) => handleInputChange('postalCode', e.target.value)}
|
||||||
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
|
className={`h-12 bg-gray-50 border-0 rounded-xl mt-1 ${fieldErrors.postalCode ? 'border border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="postalCode" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
12
src/components/ScrollToTop.tsx
Normal file
12
src/components/ScrollToTop.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
export function ScrollToTop() {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -204,7 +204,7 @@ export function TrustSection() {
|
|||||||
style={{
|
style={{
|
||||||
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
|
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
|
||||||
transformOrigin: 'center center',
|
transformOrigin: 'center center',
|
||||||
minHeight: '480px',
|
minHeight: '360px',
|
||||||
background: `
|
background: `
|
||||||
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
|
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
|
||||||
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),
|
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export function WhatsIncludedHero({ onCreateItineraryClick }: WhatsIncludedHeroP
|
|||||||
>
|
>
|
||||||
{/* Main Heading */}
|
{/* Main Heading */}
|
||||||
<h1 className="font-poppins text-4xl sm:text-5xl md:text-6xl w-full leading-tight mb-6">
|
<h1 className="font-poppins text-4xl sm:text-5xl md:text-6xl w-full leading-tight mb-6">
|
||||||
<span className="font-light">One pass.</span>{' '}
|
<span className="font-light">One CityCard</span>{' '}
|
||||||
<span className="font-bold italic pr-2 bg-gradient-to-r from-primary via-orange-500 to-rose-500 bg-clip-text text-transparent">
|
<span className="font-bold italic pr-2 bg-gradient-to-r from-primary via-orange-500 to-rose-500 bg-clip-text text-transparent">
|
||||||
Everything you
|
Everything you
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// AuthContext.tsx
|
||||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import "./index.css";
|
|||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { store } from "./Redux/Store";
|
import { store } from "./Redux/Store";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
|
import { ScrollToTop } from "./components/ScrollToTop";
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Toaster position="top-right" richColors duration={2000} closeButton />
|
<Toaster position="top-right" richColors duration={2000} closeButton />
|
||||||
|
<ScrollToTop />
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import Navbar from '../components/Navbar';
|
import Navbar from '../components/Navbar';
|
||||||
import { Footer } from '../components/Footer';
|
import { Footer } from '../components/Footer';
|
||||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAddCardToCartMutation, useGetCheckoutPageDataQuery } from '../Redux/services/cards.service';
|
import { useAddCardToCartMutation, useGetCheckoutPageDataQuery } from '../Redux/services/cards.service';
|
||||||
import LoadingSpinner from '../components/LoadingSpinner';
|
import LoadingSpinner from '../components/LoadingSpinner';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -187,9 +187,9 @@ function CheckoutConfigCard({
|
|||||||
if (bookingId) {
|
if (bookingId) {
|
||||||
navigate(`/payment/${bookingId}`)
|
navigate(`/payment/${bookingId}`)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response?.error?.data?.message)
|
throw new Error(response?.error?.data?.message)
|
||||||
}
|
}
|
||||||
} catch (error:any) {
|
} catch (error: any) {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,6 +307,7 @@ export function CheckoutPage({
|
|||||||
currentPage,
|
currentPage,
|
||||||
}: any) {
|
}: any) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
// Default item (you can pass via props later)
|
// Default item (you can pass via props later)
|
||||||
const baseUrl = import.meta.env.VITE_BASE_URL;
|
const baseUrl = import.meta.env.VITE_BASE_URL;
|
||||||
@@ -321,10 +322,11 @@ export function CheckoutPage({
|
|||||||
const unlimitedCard = checkoutPageData?.cards[1] ?? null
|
const unlimitedCard = checkoutPageData?.cards[1] ?? null
|
||||||
const attractions = checkoutPageData?.attractions ?? [];
|
const attractions = checkoutPageData?.attractions ?? [];
|
||||||
|
|
||||||
const [checkoutItem, setCheckoutItem] = useState(flexiCard);
|
const selectedCard = location.state?.selectedCard === "selective_pass" ? flexiCard : unlimitedCard
|
||||||
|
const [checkoutItem, setCheckoutItem] = useState(selectedCard);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCheckoutItem(flexiCard)
|
setCheckoutItem(selectedCard)
|
||||||
}, [cards])
|
}, [cards])
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { ArrowRight, Check, CreditCard, DollarSign, MapPin, Palette, Sparkles, Ticket, Zap } from 'lucide-react';
|
import { ArrowRight, Check, CreditCard, DollarSign, MapPin, Palette, Sparkles, Ticket, Zap } from 'lucide-react';
|
||||||
import { AnimatePresence, motion } from 'motion/react';
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
import { useEffect, useState } from 'react';
|
import { use, useEffect, useState } from 'react';
|
||||||
import { Layout } from '../Layout';
|
import { Layout } from '../Layout';
|
||||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||||
import { MobileAppSection } from '../components/MobileAppSection';
|
import { MobileAppSection } from '../components/MobileAppSection';
|
||||||
import { TrustSection } from '../components/TrustSection';
|
import { TrustSection } from '../components/TrustSection';
|
||||||
import { Button } from '../components/ui/button';
|
import { Button } from '../components/ui/button';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -68,6 +69,8 @@ export function DiscoverPage({
|
|||||||
const [direction, setDirection] = useState(0);
|
const [direction, setDirection] = useState(0);
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
|
|
||||||
|
const navigate =useNavigate();
|
||||||
|
|
||||||
const handleStepInView = (index: number) => {
|
const handleStepInView = (index: number) => {
|
||||||
setActiveStep(index);
|
setActiveStep(index);
|
||||||
};
|
};
|
||||||
@@ -687,8 +690,8 @@ export function DiscoverPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={onPassesClick}
|
onClick={()=>navigate('/passes')}
|
||||||
className="w-full py-6 rounded-full font-poppins font-semibold text-lg bg-gray-900 hover:bg-black text-white transition-all duration-300"
|
className="cursor-pointer w-full py-6 rounded-full font-poppins font-semibold text-lg bg-gray-900 hover:bg-black text-white transition-all duration-300"
|
||||||
>
|
>
|
||||||
VIEW FLEXI OPTIONS
|
VIEW FLEXI OPTIONS
|
||||||
</Button>
|
</Button>
|
||||||
@@ -743,8 +746,8 @@ export function DiscoverPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={onPassesClick}
|
onClick={()=>navigate('/passes')}
|
||||||
className="w-full py-6 rounded-full font-poppins font-semibold text-lg bg-primary hover:bg-primary/90 text-white transition-all duration-300"
|
className=" cursor-pointer w-full py-6 rounded-full font-poppins font-semibold text-lg bg-primary hover:bg-primary/90 text-white transition-all duration-300"
|
||||||
>
|
>
|
||||||
VIEW UNLIMITED OPTIONS
|
VIEW UNLIMITED OPTIONS
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
import {
|
import {
|
||||||
MapPin,
|
MapPin,
|
||||||
@@ -7,11 +7,12 @@ import {
|
|||||||
Share2,
|
Share2,
|
||||||
Download,
|
Download,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Loader2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '../components/ui/button';
|
import { Button } from '../components/ui/button';
|
||||||
import { Card, CardContent } from '../components/ui/card';
|
import { Card, CardContent } from '../components/ui/card';
|
||||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||||
import { useCreateMagicItineraryMutation, useGetItineraryDetailsByIdQuery } from '../Redux/services/itinerary.service';
|
import { useDownloadItineraryQuery, useGetItineraryDetailsByIdQuery } from '../Redux/services/itinerary.service';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import Navbar from '../components/Navbar';
|
import Navbar from '../components/Navbar';
|
||||||
@@ -26,6 +27,37 @@ const ItinerarySummaryPage = () => {
|
|||||||
|
|
||||||
const { itineraryId } = useParams()
|
const { itineraryId } = useParams()
|
||||||
const { data: itineraryDetails, isLoading: itineraryDetailsLoading } = useGetItineraryDetailsByIdQuery(itineraryId);
|
const { data: itineraryDetails, isLoading: itineraryDetailsLoading } = useGetItineraryDetailsByIdQuery(itineraryId);
|
||||||
|
// Download logic using standard query with manual trigger
|
||||||
|
const [shouldDownload, setShouldDownload] = useState(false);
|
||||||
|
const { data: pdfBlob, isFetching: isDownloading, refetch } = useDownloadItineraryQuery
|
||||||
|
(itineraryId!, {
|
||||||
|
skip: !shouldDownload || !itineraryId,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldDownload && pdfBlob) {
|
||||||
|
// Create download link
|
||||||
|
const url = window.URL.createObjectURL(pdfBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `itinerary-${itineraryId}.pdf`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
toast.success('Itinerary downloaded successfully!');
|
||||||
|
setShouldDownload(false); // reset trigger
|
||||||
|
}
|
||||||
|
}, [pdfBlob, shouldDownload, itineraryId]);
|
||||||
|
|
||||||
|
const handleDownloadItinerary = useCallback(() => {
|
||||||
|
if (!itineraryId) {
|
||||||
|
toast.error('Itinerary ID not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShouldDownload(true);
|
||||||
|
refetch(); // manually trigger the download query
|
||||||
|
}, [itineraryId, refetch]);
|
||||||
|
|
||||||
const generatedItinerary = itineraryDetails ?? null;
|
const generatedItinerary = itineraryDetails ?? null;
|
||||||
const days = generatedItinerary?.days ?? [];
|
const days = generatedItinerary?.days ?? [];
|
||||||
@@ -35,305 +67,312 @@ const ItinerarySummaryPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
{/* Navbar */}
|
{/* Navbar */}
|
||||||
<Navbar
|
<Navbar
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="space-y-8 max-w-3xl mx-auto mt-25"
|
className="space-y-8 max-w-3xl mx-auto mt-25"
|
||||||
>
|
>
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="text-center space-y-1">
|
<div className="text-center space-y-1">
|
||||||
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
|
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
|
||||||
<span className="font-normal">Your</span>
|
<span className="font-normal">Your</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
|
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
|
||||||
<span className="font-bold text-primary italic">{generatedItinerary?.title}</span>
|
<span className="font-bold text-primary italic">{generatedItinerary?.title}</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Trip Details Card */}
|
{/* Trip Details Card */}
|
||||||
<div className="relative overflow-hidden rounded-2xl border border-gray-100 shadow-sm">
|
<div className="relative overflow-hidden rounded-2xl border border-gray-100 shadow-sm">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
<div className="relative h-40 md:h-48">
|
<div className="relative h-40 md:h-48">
|
||||||
<ImageWithFallback
|
<ImageWithFallback
|
||||||
src={generatedItinerary?.cityBanner}
|
src={generatedItinerary?.cityBanner}
|
||||||
alt={generatedItinerary?.city}
|
alt={generatedItinerary?.city}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent" />
|
||||||
<div className="absolute bottom-4 left-5 right-5">
|
<div className="absolute bottom-4 left-5 right-5">
|
||||||
<p className="font-poppins text-xs font-medium text-white/70 uppercase tracking-wider mb-1">Your Trip</p>
|
<p className="font-poppins text-xs font-medium text-white/70 uppercase tracking-wider mb-1">Your Trip</p>
|
||||||
<h3 className="font-merchant text-2xl md:text-3xl text-white leading-snug font-semibold">{generatedItinerary?.city}</h3>
|
<h3 className="font-merchant text-2xl md:text-3xl text-white leading-snug font-semibold">{generatedItinerary?.city}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Stats Row */}
|
||||||
|
<div className="grid grid-cols-3 divide-x divide-gray-100 bg-white">
|
||||||
|
<div className="flex flex-col items-center py-4">
|
||||||
|
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalDays}</span>
|
||||||
|
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Days</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center py-4">
|
||||||
|
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalStops}</span>
|
||||||
|
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Stops</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center py-4">
|
||||||
|
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.days[0]?.date}</span>
|
||||||
|
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Start Date</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Stats Row */}
|
|
||||||
<div className="grid grid-cols-3 divide-x divide-gray-100 bg-white">
|
|
||||||
<div className="flex flex-col items-center py-4">
|
|
||||||
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalDays}</span>
|
|
||||||
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Days</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center py-4">
|
|
||||||
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalStops}</span>
|
|
||||||
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Stops</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center py-4">
|
|
||||||
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.days[0]?.date}</span>
|
|
||||||
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Start Date</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Share & Download Buttons */}
|
{/* Share & Download Buttons */}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3"
|
className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3"
|
||||||
>
|
|
||||||
<Share2 className="w-4 h-4 mr-2" />
|
|
||||||
Share
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3"
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4 mr-2" />
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* View Toggle */}
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="bg-gray-100 p-1 rounded-full inline-flex">
|
|
||||||
<button
|
|
||||||
onClick={() => setViewMode('daily')}
|
|
||||||
className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'daily'
|
|
||||||
? 'bg-white shadow-sm text-gray-900'
|
|
||||||
: 'text-gray-500 hover:text-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Daily View
|
<Share2 className="w-4 h-4 mr-2" />
|
||||||
</button>
|
Share
|
||||||
<button
|
</Button>
|
||||||
onClick={() => setViewMode('summary')}
|
<Button
|
||||||
className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'summary'
|
onClick={handleDownloadItinerary}
|
||||||
? 'bg-white shadow-sm text-gray-900'
|
disabled={isDownloading}
|
||||||
: 'text-gray-500 hover:text-gray-700'
|
variant="outline"
|
||||||
}`}
|
className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3"
|
||||||
>
|
>
|
||||||
Summary
|
{isDownloading ? (
|
||||||
</button>
|
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
|
||||||
</div>
|
) : (
|
||||||
</div>
|
<Download className="w-5 h-5 mr-2" />
|
||||||
|
|
||||||
{/* Daily View */}
|
|
||||||
{viewMode === 'daily' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Day Tabs */}
|
|
||||||
<div className="flex items-center gap-2 overflow-x-auto pb-2">
|
|
||||||
{days?.map((day: any) => (
|
|
||||||
<button
|
|
||||||
key={day.dayNumber}
|
|
||||||
onClick={() => setSelectedDayTab(day.dayNumber)}
|
|
||||||
className={`px-5 py-2.5 rounded-xl whitespace-nowrap font-poppins text-base transition-all ${selectedDayTab === day.dayNumber
|
|
||||||
? 'text-primary font-semibold bg-primary/10 border border-primary/20'
|
|
||||||
: 'text-gray-400 font-medium hover:text-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Day {day.dayNumber}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
{days?.length > 4 && (
|
|
||||||
<button className="p-2 text-gray-400 hover:text-gray-600">
|
|
||||||
<ChevronRight className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Activities for selected day */}
|
Download
|
||||||
{selectedDayPlan && (
|
</Button>
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.div key={`day-${selectedDayTab}`} initial={{ opacity: 0, x: 10 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -10 }} transition={{ duration: 0.3 }} className="space-y-8">
|
|
||||||
{selectedDayPlan?.items?.map((activity: any, actIndex: number) => {
|
|
||||||
const activityKey = `day${selectedDayPlan.day}-act${actIndex}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
key={actIndex}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.4, delay: actIndex * 0.08 }}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
{/* Time Label */}
|
|
||||||
<p className="font-poppins text-sm font-medium text-gray-500 text-center uppercase tracking-wider">
|
|
||||||
{activity.timeSlot}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Activity Card */}
|
|
||||||
<Card className="overflow-hidden border border-gray-100 shadow-sm hover:shadow-lg transition-shadow duration-300 rounded-2xl">
|
|
||||||
<CardContent className="p-0">
|
|
||||||
{/* Image */}
|
|
||||||
<div className="relative h-56 md:h-64 bg-gray-200">
|
|
||||||
<ImageWithFallback
|
|
||||||
src={activity.imageUrl}
|
|
||||||
alt={activity.title}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* TODO: Get Directions Badge redirect it to lat,long */}
|
|
||||||
<div className="absolute bottom-3 left-3">
|
|
||||||
<button className="flex items-center gap-1.5 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold shadow-lg">
|
|
||||||
<MapPin className="w-3.5 h-3.5" />
|
|
||||||
Get Directions
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="p-5 space-y-3">
|
|
||||||
<h4 className="font-poppins text-lg font-semibold text-gray-900 leading-snug">
|
|
||||||
{activity.title}
|
|
||||||
</h4>
|
|
||||||
<p className="font-poppins text-sm font-normal text-gray-500 leading-relaxed">
|
|
||||||
{activity.locationName}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Category Tags */}
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{activity.categories?.map((cat: string, ci: number) => (
|
|
||||||
<span
|
|
||||||
key={ci}
|
|
||||||
className="font-poppins text-xs font-medium px-3 py-1.5 rounded-full border border-primary/20 text-primary bg-primary/5"
|
|
||||||
>
|
|
||||||
{cat}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bullet Points */}
|
|
||||||
<div className="space-y-1.5 pt-1">
|
|
||||||
|
|
||||||
<div className="flex items-baseline gap-2">
|
|
||||||
<span className="text-primary flex-shrink-0 text-sm leading-relaxed">•</span>
|
|
||||||
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">{activity.description}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Summary View */}
|
{/* View Toggle */}
|
||||||
{viewMode === 'summary' && (
|
<div className="flex justify-center">
|
||||||
<div className="space-y-6">
|
<div className="bg-gray-100 p-1 rounded-full inline-flex">
|
||||||
{days?.map((day: any, dayIndex: number) => {
|
<button
|
||||||
const dayDate = days[0]?.date
|
onClick={() => setViewMode('daily')}
|
||||||
? new Date(
|
className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'daily'
|
||||||
new Date(days[0].date).setDate(
|
? 'bg-white shadow-sm text-gray-900'
|
||||||
new Date(days[0].date).getDate() + dayIndex
|
: 'text-gray-500 hover:text-gray-700'
|
||||||
)
|
}`}
|
||||||
).toLocaleDateString('en-AU', {
|
>
|
||||||
day: '2-digit',
|
Daily View
|
||||||
month: '2-digit',
|
</button>
|
||||||
year: 'numeric',
|
<button
|
||||||
})
|
onClick={() => setViewMode('summary')}
|
||||||
: '';
|
className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'summary'
|
||||||
|
? 'bg-white shadow-sm text-gray-900'
|
||||||
|
: 'text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Summary
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
// ✅ Find the matching summary for this day
|
{/* Daily View */}
|
||||||
const daySummary = summaries.find((s: any) => s.dayNumber === day.dayNumber);
|
{viewMode === 'daily' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Day Tabs */}
|
||||||
|
<div className="flex items-center gap-2 overflow-x-auto pb-2">
|
||||||
|
{days?.map((day: any) => (
|
||||||
|
<button
|
||||||
|
key={day.dayNumber}
|
||||||
|
onClick={() => setSelectedDayTab(day.dayNumber)}
|
||||||
|
className={`px-5 py-2.5 rounded-xl whitespace-nowrap font-poppins text-base transition-all ${selectedDayTab === day.dayNumber
|
||||||
|
? 'text-primary font-semibold bg-primary/10 border border-primary/20'
|
||||||
|
: 'text-gray-400 font-medium hover:text-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Day {day.dayNumber}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{days?.length > 4 && (
|
||||||
|
<button className="p-2 text-gray-400 hover:text-gray-600">
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
return (
|
{/* Activities for selected day */}
|
||||||
<div key={dayIndex} className="border-l-4 border-primary/20 pl-5 space-y-3">
|
{selectedDayPlan && (
|
||||||
{/* Day Header */}
|
<AnimatePresence mode="wait">
|
||||||
<div className="flex items-center justify-between">
|
<motion.div key={`day-${selectedDayTab}`} initial={{ opacity: 0, x: 10 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -10 }} transition={{ duration: 0.3 }} className="space-y-8">
|
||||||
<h3 className="font-poppins text-lg font-semibold text-gray-900">
|
{selectedDayPlan?.items?.map((activity: any, actIndex: number) => {
|
||||||
Day {day.dayNumber}:
|
const activityKey = `day${selectedDayPlan.day}-act${actIndex}`;
|
||||||
</h3>
|
|
||||||
<div className="flex items-center gap-1.5 text-primary">
|
|
||||||
<Calendar className="w-3.5 h-3.5" />
|
|
||||||
<span className="font-poppins text-sm font-medium">{dayDate}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Activity List */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
{daySummary?.items?.map((item: any, actIndex: number) => {
|
|
||||||
const activityKey = `summary-day${day.dayNumber}-act${actIndex}`;
|
|
||||||
const isExpanded = selectedActivity === activityKey;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={actIndex} className="bg-gray-50 rounded-xl overflow-hidden">
|
<motion.div
|
||||||
<div
|
key={actIndex}
|
||||||
className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-gray-100 transition-colors"
|
initial={{ opacity: 0, y: 20 }}
|
||||||
onClick={() =>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
setSelectedActivity(isExpanded ? null : activityKey)
|
transition={{ duration: 0.4, delay: actIndex * 0.08 }}
|
||||||
}
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<p className="font-poppins text-sm font-medium text-gray-800">
|
{/* Time Label */}
|
||||||
{item.timeSlot}: {item.title}
|
<p className="font-poppins text-sm font-medium text-gray-500 text-center uppercase tracking-wider">
|
||||||
</p>
|
{activity.timeSlot}
|
||||||
<ChevronDown
|
</p>
|
||||||
className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnimatePresence>
|
{/* Activity Card */}
|
||||||
{isExpanded && (
|
<Card className="overflow-hidden border border-gray-100 shadow-sm hover:shadow-lg transition-shadow duration-300 rounded-2xl">
|
||||||
<motion.div
|
<CardContent className="p-0">
|
||||||
initial={{ height: 0, opacity: 0 }}
|
{/* Image */}
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
<div className="relative h-56 md:h-64 bg-gray-200">
|
||||||
exit={{ height: 0, opacity: 0 }}
|
<ImageWithFallback
|
||||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
src={activity.imageUrl}
|
||||||
>
|
alt={activity.title}
|
||||||
<div className="px-4 pb-4 space-y-2">
|
className="w-full h-full object-cover"
|
||||||
<div className="flex items-baseline gap-2">
|
/>
|
||||||
<span className="text-primary flex-shrink-0 text-sm leading-relaxed">•</span>
|
|
||||||
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">
|
{/* TODO: Get Directions Badge redirect it to lat,long */}
|
||||||
{item.description}
|
<div className="absolute bottom-3 left-3">
|
||||||
</span>
|
<button className="flex items-center gap-1.5 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold shadow-lg">
|
||||||
</div>
|
|
||||||
<button className="flex items-center gap-1.5 mt-2 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold">
|
|
||||||
<MapPin className="w-3.5 h-3.5" />
|
<MapPin className="w-3.5 h-3.5" />
|
||||||
Get directions
|
Get Directions
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
{/* Content */}
|
||||||
</div>
|
<div className="p-5 space-y-3">
|
||||||
|
<h4 className="font-poppins text-lg font-semibold text-gray-900 leading-snug">
|
||||||
|
{activity.title}
|
||||||
|
</h4>
|
||||||
|
<p className="font-poppins text-sm font-normal text-gray-500 leading-relaxed">
|
||||||
|
{activity.locationName}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Category Tags */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{activity.categories?.map((cat: string, ci: number) => (
|
||||||
|
<span
|
||||||
|
key={ci}
|
||||||
|
className="font-poppins text-xs font-medium px-3 py-1.5 rounded-full border border-primary/20 text-primary bg-primary/5"
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bullet Points */}
|
||||||
|
<div className="space-y-1.5 pt-1">
|
||||||
|
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="text-primary flex-shrink-0 text-sm leading-relaxed">•</span>
|
||||||
|
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">{activity.description}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
</AnimatePresence>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Bottom Action */}
|
{/* Summary View */}
|
||||||
<div className="flex justify-center pt-4 pb-8">
|
{viewMode === 'summary' && (
|
||||||
<Button
|
<div className="space-y-6">
|
||||||
onClick={() => navigate('/create-itinerary')}
|
{days?.map((day: any, dayIndex: number) => {
|
||||||
className="w-full font-poppins font-semibold px-8 py-3 rounded-xl bg-primary hover:bg-primary/90 text-white shadow-md shadow-primary/20"
|
const dayDate = days[0]?.date
|
||||||
>
|
? new Date(
|
||||||
Create Another Itinerary
|
new Date(days[0].date).setDate(
|
||||||
</Button>
|
new Date(days[0].date).getDate() + dayIndex
|
||||||
</div>
|
)
|
||||||
</motion.div>
|
).toLocaleDateString('en-AU', {
|
||||||
<Footer
|
day: '2-digit',
|
||||||
/>
|
month: '2-digit',
|
||||||
</div>
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// ✅ Find the matching summary for this day
|
||||||
|
const daySummary = summaries.find((s: any) => s.dayNumber === day.dayNumber);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={dayIndex} className="border-l-4 border-primary/20 pl-5 space-y-3">
|
||||||
|
{/* Day Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-poppins text-lg font-semibold text-gray-900">
|
||||||
|
Day {day.dayNumber}:
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-1.5 text-primary">
|
||||||
|
<Calendar className="w-3.5 h-3.5" />
|
||||||
|
<span className="font-poppins text-sm font-medium">{dayDate}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Activity List */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{daySummary?.items?.map((item: any, actIndex: number) => {
|
||||||
|
const activityKey = `summary-day${day.dayNumber}-act${actIndex}`;
|
||||||
|
const isExpanded = selectedActivity === activityKey;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={actIndex} className="bg-gray-50 rounded-xl overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedActivity(isExpanded ? null : activityKey)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p className="font-poppins text-sm font-medium text-gray-800">
|
||||||
|
{item.timeSlot}: {item.title}
|
||||||
|
</p>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{isExpanded && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||||
|
>
|
||||||
|
<div className="px-4 pb-4 space-y-2">
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="text-primary flex-shrink-0 text-sm leading-relaxed">•</span>
|
||||||
|
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">
|
||||||
|
{item.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button className="flex items-center gap-1.5 mt-2 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold">
|
||||||
|
<MapPin className="w-3.5 h-3.5" />
|
||||||
|
Get directions
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bottom Action */}
|
||||||
|
<div className="flex justify-center pt-4 pb-8">
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate('/create-itinerary')}
|
||||||
|
className="w-full font-poppins font-semibold px-8 py-3 rounded-xl bg-primary hover:bg-primary/90 text-white shadow-md shadow-primary/20"
|
||||||
|
>
|
||||||
|
Create Another Itinerary
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
<Footer
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export function MagicItineraryPage({
|
|||||||
currentPage,
|
currentPage,
|
||||||
user
|
user
|
||||||
}: MagicItineraryPageProps) {
|
}: MagicItineraryPageProps) {
|
||||||
|
const cityName = localStorage.getItem("cityName") || "your city";
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<Layout activeCity="Landingpage" onSignInClick={onSignInClick} onSignOutClick={onSignOutClick} user={user}>
|
<Layout activeCity="Landingpage" onSignInClick={onSignInClick} onSignOutClick={onSignOutClick} user={user}>
|
||||||
@@ -190,7 +191,7 @@ export function MagicItineraryPage({
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">
|
<p className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">
|
||||||
A perfectly planned Melbourne adventure
|
A perfectly planned {cityName} adventure
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ export function MelbournePage({
|
|||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
|
|
||||||
<div className="container mx-auto px-4 py-12 space-y-24">
|
<div className="container mx-auto px-4 py-12">
|
||||||
{/* Features Grid */}
|
{/* Features Grid */}
|
||||||
<motion.section
|
<motion.section
|
||||||
id="overview"
|
id="overview"
|
||||||
@@ -392,7 +392,7 @@ export function MelbournePage({
|
|||||||
>
|
>
|
||||||
<Wand2 className="w-6 h-6 text-primary drop-shadow-lg" />
|
<Wand2 className="w-6 h-6 text-primary drop-shadow-lg" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<span className="font-poppins font-semibold text-gray-800">AI-Powered Magic Itinerary</span>
|
<span className="font-poppins font-semibold text-gray-800">Magic Itinerary</span>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-2 h-2 bg-primary rounded-full"
|
className="w-2 h-2 bg-primary rounded-full"
|
||||||
animate={{
|
animate={{
|
||||||
@@ -791,18 +791,18 @@ export function MelbournePage({
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<p className="font-poppins text-gray-600 text-sm flex items-center gap-2">
|
{/* <p className="font-poppins text-gray-600 text-sm flex items-center gap-2">
|
||||||
<Sparkles className="w-4 h-4 text-primary" />
|
<Sparkles className="w-4 h-4 text-primary" />
|
||||||
<span>Free to use • No credit card required</span>
|
<span>Free to use • No credit card required</span>
|
||||||
<Sparkles className="w-4 h-4 text-primary" />
|
<Sparkles className="w-4 h-4 text-primary" />
|
||||||
</p>
|
</p> */}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container mx-auto px-4 py-12 space-y-24">
|
<div className="container mx-auto px-4 py-12">
|
||||||
{/* Testimonials */}
|
{/* Testimonials */}
|
||||||
<EnhancedTestimonials />
|
<EnhancedTestimonials />
|
||||||
|
|
||||||
|
|||||||
@@ -156,20 +156,19 @@ export function PassesPage({
|
|||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const cityId = localStorage.getItem("cityId")
|
const cityId = localStorage.getItem("cityId")
|
||||||
|
const cityName = localStorage.getItem("cityName")
|
||||||
|
|
||||||
const { data: cityDetails, isLoading: loadingCityDetails } = useGetSelectedCityDetailsQuery(cityId)
|
const { data: cityDetails, isLoading: loadingCityDetails } = useGetSelectedCityDetailsQuery(cityId)
|
||||||
const cards = cityDetails?.city?.cards ?? []
|
const cards = cityDetails?.city?.cards ?? []
|
||||||
console.log(cards)
|
|
||||||
|
|
||||||
if (loadingCityDetails) {
|
if (loadingCityDetails) {
|
||||||
return (<LoadingSpinner />)
|
return (<LoadingSpinner />)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCheckoutClick = (cardTypeName:string) => {
|
||||||
const handleCheckoutClick = () => {
|
|
||||||
console.log('Proceeding to checkout for user:', user);
|
console.log('Proceeding to checkout for user:', user);
|
||||||
// Add your checkout logic here
|
// Add your checkout logic here
|
||||||
navigate('/checkout');
|
navigate('/checkout', { state: { selectedCard: cardTypeName } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSignInClick = () => {
|
const handleSignInClick = () => {
|
||||||
@@ -282,7 +281,7 @@ export function PassesPage({
|
|||||||
? "bg-primary hover:bg-primary/90 text-white hover:shadow-lg"
|
? "bg-primary hover:bg-primary/90 text-white hover:shadow-lg"
|
||||||
: "bg-gray-400 hover:bg-gray-400 text-white hover:shadow-md"
|
: "bg-gray-400 hover:bg-gray-400 text-white hover:shadow-md"
|
||||||
}`}
|
}`}
|
||||||
onClick={user ? handleCheckoutClick : handleSignInClick}
|
onClick={() => user ? handleCheckoutClick(cards[0]?.cardType?.cardTypeName) : handleSignInClick}
|
||||||
disabled={selectedPass !== passTypes[0].id}
|
disabled={selectedPass !== passTypes[0].id}
|
||||||
|
|
||||||
>
|
>
|
||||||
@@ -372,7 +371,7 @@ export function PassesPage({
|
|||||||
}`}
|
}`}
|
||||||
disabled={selectedPass !== passTypes[1].id}
|
disabled={selectedPass !== passTypes[1].id}
|
||||||
|
|
||||||
onClick={user ? handleCheckoutClick : handleSignInClick}
|
onClick={() => user ? handleCheckoutClick(cards[1]?.cardType?.cardTypeName) : handleSignInClick}
|
||||||
|
|
||||||
>
|
>
|
||||||
{user ? 'PURCHASE NOW' : 'LOGIN TO BUY PASS'}
|
{user ? 'PURCHASE NOW' : 'LOGIN TO BUY PASS'}
|
||||||
@@ -415,7 +414,7 @@ export function PassesPage({
|
|||||||
<Clock className="w-7 h-7" strokeWidth={1.5} />
|
<Clock className="w-7 h-7" strokeWidth={1.5} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 w-full">
|
<div className="flex-1 w-full">
|
||||||
<h3 className="font-merchant text-2xl text-gray-900 mb-3">Calendar Days Policy</h3>
|
<h3 className="font-merchant text-xl text-gray-900 mb-3">Calendar Days Policy</h3>
|
||||||
<p className="font-poppins text-gray-600 leading-relaxed mb-6">
|
<p className="font-poppins text-gray-600 leading-relaxed mb-6">
|
||||||
Unlimited passes work on a <span className="font-medium text-gray-900">consecutive calendar day basis</span>, not 24-hour periods. Your pass expires at 11:59 PM on your final day.
|
Unlimited passes work on a <span className="font-medium text-gray-900">consecutive calendar day basis</span>, not 24-hour periods. Your pass expires at 11:59 PM on your final day.
|
||||||
</p>
|
</p>
|
||||||
@@ -452,7 +451,7 @@ export function PassesPage({
|
|||||||
<Shield className="w-7 h-7" strokeWidth={1.5} />
|
<Shield className="w-7 h-7" strokeWidth={1.5} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="font-merchant text-2xl text-gray-900 mb-3">60-Minute Adventure Gap</h3>
|
<h3 className="font-merchant text-xl text-gray-900 mb-3">60-Minute Adventure Gap</h3>
|
||||||
<p className="font-poppins text-gray-600 leading-relaxed">
|
<p className="font-poppins text-gray-600 leading-relaxed">
|
||||||
To keep the journey smooth for everyone, there's a simple <span className="font-medium text-gray-900">60-minute wait</span> between scanning your pass at attractions.
|
To keep the journey smooth for everyone, there's a simple <span className="font-medium text-gray-900">60-minute wait</span> between scanning your pass at attractions.
|
||||||
</p>
|
</p>
|
||||||
@@ -597,7 +596,7 @@ export function PassesPage({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-white font-semibold text-lg">CityCards</h3>
|
<h3 className="text-white font-semibold text-lg">CityCards</h3>
|
||||||
<p className="text-white/70 text-sm">Melbourne Explorer</p>
|
<p className="text-white/70 text-sm">{cityName} Explorer</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -770,7 +769,7 @@ export function PassesPage({
|
|||||||
<h3 className="heading-dynamic text-4xl mb-4">
|
<h3 className="heading-dynamic text-4xl mb-4">
|
||||||
<span className="font-light">Ready to</span>{' '}
|
<span className="font-light">Ready to</span>{' '}
|
||||||
<span className="font-bold italic text-emphasis">explore</span>{' '}
|
<span className="font-bold italic text-emphasis">explore</span>{' '}
|
||||||
<span className="font-semibold">Melbourne?</span>
|
<span className="font-semibold">{cityName}?</span>
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xl mb-8 max-w-2xl mx-auto opacity-90 font-light">
|
<p className="text-xl mb-8 max-w-2xl mx-auto opacity-90 font-light">
|
||||||
Choose your pass and start discovering amazing attractions with skip-the-line access.
|
Choose your pass and start discovering amazing attractions with skip-the-line access.
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ function Field({
|
|||||||
prefilled,
|
prefilled,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: React.ReactNode;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (v: string) => void;
|
onChange: (v: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -232,30 +232,62 @@ export function PaymentDetailsPage({
|
|||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
const e: Record<string, string> = {};
|
const e: Record<string, string> = {};
|
||||||
|
|
||||||
if (selectedTab === 'gift') {
|
if (selectedTab === 'gift') {
|
||||||
if (!giftFirstName.trim()) e.giftFirstName = 'Required';
|
// First Name
|
||||||
if (!giftLastName.trim()) e.giftLastName = 'Required';
|
if (!giftFirstName.trim()) e.giftFirstName = 'First name is required';
|
||||||
if (!giftIsd.trim()) e.giftIsd = 'Required';
|
else if (/\s/.test(giftFirstName)) e.giftFirstName = 'First name must not contain spaces';
|
||||||
if (!giftMessage.trim()) e.giftMessage = 'Required';
|
else if (!/^[A-Za-z]+$/.test(giftFirstName)) e.giftFirstName = 'First name must contain only letters (A–Z)';
|
||||||
if (!giftEmail.trim() || !/\S+@\S+\.\S+/.test(giftEmail)) {
|
else if (giftFirstName.length < 2 || giftFirstName.length > 50) e.giftFirstName = 'First name must be between 2 and 50 characters';
|
||||||
e.giftEmail = 'Valid email required';
|
|
||||||
}
|
// Last Name
|
||||||
if (!giftPhone.trim() || !/^\+?[0-9]{7,15}$/.test(giftPhone)) {
|
if (!giftLastName.trim()) e.giftLastName = 'Last name is required';
|
||||||
e.giftPhone = 'Valid phone required';
|
else if (/\s/.test(giftLastName)) e.giftLastName = 'Last name must not contain spaces';
|
||||||
}
|
else if (!/^[A-Za-z]+$/.test(giftLastName)) e.giftLastName = 'Last name must contain only letters (A–Z)';
|
||||||
if (!giftCity.trim()) e.giftCity = 'Required';
|
else if (giftLastName.length < 2 || giftLastName.length > 50) e.giftLastName = 'Last name must be between 2 and 50 characters';
|
||||||
if (!giftCountry.trim()) e.giftCountry = 'Required';
|
|
||||||
|
// ISD Code
|
||||||
|
if (!giftIsd.trim()) e.giftIsd = 'ISD code is required';
|
||||||
|
else if (/\s/.test(giftIsd)) e.giftIsd = 'ISD code must not contain spaces';
|
||||||
|
else if (!giftIsd.startsWith('+')) e.giftIsd = "ISD code must start with '+' (e.g. +91)";
|
||||||
|
else if (!/^\+\d+$/.test(giftIsd)) e.giftIsd = "ISD code must contain only digits after '+'";
|
||||||
|
|
||||||
|
// Email
|
||||||
|
if (!giftEmail.trim()) e.giftEmail = 'Email address is required';
|
||||||
|
else if (!/\S+@\S+\.\S+/.test(giftEmail)) e.giftEmail = 'Enter a valid email (e.g. name@example.com)';
|
||||||
|
|
||||||
|
// Phone
|
||||||
|
if (!giftPhone.trim()) e.giftPhone = 'Phone number is required';
|
||||||
|
else if (/\s/.test(giftPhone)) e.giftPhone = 'Phone number must not contain spaces';
|
||||||
|
else if (!/^\d+$/.test(giftPhone)) e.giftPhone = 'Phone number must contain only digits (0–9)';
|
||||||
|
else if (giftPhone.length < 7 || giftPhone.length > 15) e.giftPhone = 'Phone number must be between 7 and 15 digits';
|
||||||
|
|
||||||
|
// Message
|
||||||
|
if (!giftMessage.trim()) e.giftMessage = 'Message is required';
|
||||||
|
else if (giftMessage.length < 5) e.giftMessage = 'Message must be at least 5 characters long';
|
||||||
|
else if (giftMessage.length > 500) e.giftMessage = 'Message must not exceed 500 characters';
|
||||||
|
|
||||||
|
// City
|
||||||
|
if (!giftCity.trim()) e.giftCity = 'City is required';
|
||||||
|
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(giftCity)) e.giftCity = 'City can only contain letters and spaces';
|
||||||
|
else if (/\s{2,}/.test(giftCity)) e.giftCity = 'City must not contain multiple consecutive spaces';
|
||||||
|
else if (giftCity.length < 2 || giftCity.length > 50) e.giftCity = 'City must be between 2 and 50 characters';
|
||||||
|
|
||||||
|
// Country
|
||||||
|
if (!giftCountry.trim()) e.giftCountry = 'Country is required';
|
||||||
|
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(giftCountry)) e.giftCountry = 'Country can only contain letters and spaces';
|
||||||
|
else if (giftCountry.length < 2 || giftCountry.length > 50) e.giftCountry = 'Country must be between 2 and 50 characters';
|
||||||
}
|
}
|
||||||
|
|
||||||
return e;
|
return e;
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||||
|
|
||||||
const handlePayment = async () => {
|
const handlePayment = async () => {
|
||||||
const validationErrors = validate();
|
const validationErrors = validate();
|
||||||
setErrors(validationErrors);
|
setErrors(validationErrors);
|
||||||
if (Object.keys(validationErrors).length > 0) {
|
if (Object.keys(validationErrors).length > 0) {
|
||||||
toast.error('Please fill all required fields');
|
// toast.error('Please fill all required fields');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,8 +413,8 @@ export function PaymentDetailsPage({
|
|||||||
<button
|
<button
|
||||||
onClick={() => setSelectedTab('myself')}
|
onClick={() => setSelectedTab('myself')}
|
||||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${selectedTab === 'myself'
|
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${selectedTab === 'myself'
|
||||||
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
|
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
|
||||||
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
|
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
@@ -391,8 +423,8 @@ export function PaymentDetailsPage({
|
|||||||
<button
|
<button
|
||||||
onClick={() => setSelectedTab('gift')}
|
onClick={() => setSelectedTab('gift')}
|
||||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${selectedTab === 'gift'
|
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${selectedTab === 'gift'
|
||||||
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
|
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
|
||||||
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
|
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Gift className="w-4 h-4" />
|
<Gift className="w-4 h-4" />
|
||||||
@@ -454,64 +486,14 @@ export function PaymentDetailsPage({
|
|||||||
<h3 className="font-poppins text-base font-semibold text-[#2a2a2a]">Gift Recipient Details</h3>
|
<h3 className="font-poppins text-base font-semibold text-[#2a2a2a]">Gift Recipient Details</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<Field
|
<Field label={<>Recipient First Name <span className="text-red-500">*</span></>} value={giftFirstName} onChange={setGiftFirstName} placeholder="Enter recipient's first name" error={errors.giftFirstName} />
|
||||||
label="Recipient First Name"
|
<Field label={<>Recipient Last Name <span className="text-red-500">*</span></>} value={giftLastName} onChange={setGiftLastName} placeholder="Enter recipient's last name" error={errors.giftLastName} />
|
||||||
value={giftFirstName}
|
<Field label={<>Recipient ISD Code <span className="text-red-500">*</span></>} value={giftIsd} onChange={setGiftIsd} placeholder="e.g., +61" error={errors.giftIsd} />
|
||||||
onChange={setGiftFirstName}
|
<Field label={<>Recipient Phone <span className="text-red-500">*</span></>} value={giftPhone} onChange={setGiftPhone} type="tel" placeholder="Enter recipient's phone number" error={errors.giftPhone} />
|
||||||
placeholder="Enter recipient's first name"
|
<Field label={<>Recipient Email <span className="text-red-500">*</span></>} value={giftEmail} onChange={setGiftEmail} type="email" placeholder="Enter recipient's email" error={errors.giftEmail} />
|
||||||
error={errors.giftFirstName}
|
<Field label={<>Recipient City <span className="text-red-500">*</span></>} value={giftCity} onChange={setGiftCity} placeholder="Enter recipient's city" error={errors.giftCity} />
|
||||||
/>
|
<Field label={<>Recipient Country <span className="text-red-500">*</span></>} value={giftCountry} onChange={setGiftCountry} placeholder="Enter recipient's country" error={errors.giftCountry} />
|
||||||
<Field
|
<Field label={<>Gift Message <span className="text-red-500">*</span></>} value={giftMessage} onChange={setGiftMessage} placeholder="Write a heartfelt message" error={errors.giftMessage} />
|
||||||
label="Recipient Last Name"
|
|
||||||
value={giftLastName}
|
|
||||||
onChange={setGiftLastName}
|
|
||||||
placeholder="Enter recipient's last name"
|
|
||||||
error={errors.giftLastName}
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
label="Recipient ISD Code"
|
|
||||||
value={giftIsd}
|
|
||||||
onChange={setGiftIsd}
|
|
||||||
placeholder="e.g., 61"
|
|
||||||
error={errors.giftIsd}
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
label="Recipient Phone"
|
|
||||||
value={giftPhone}
|
|
||||||
onChange={setGiftPhone}
|
|
||||||
type="tel"
|
|
||||||
placeholder="Enter recipient's phone number"
|
|
||||||
error={errors.giftPhone}
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
label="Recipient Email"
|
|
||||||
value={giftEmail}
|
|
||||||
onChange={setGiftEmail}
|
|
||||||
type="email"
|
|
||||||
placeholder="Enter recipient's email"
|
|
||||||
error={errors.giftEmail}
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
label="Recipient City"
|
|
||||||
value={giftCity}
|
|
||||||
onChange={setGiftCity}
|
|
||||||
placeholder="Enter recipient's city"
|
|
||||||
error={errors.giftCity}
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
label="Recipient Country"
|
|
||||||
value={giftCountry}
|
|
||||||
onChange={setGiftCountry}
|
|
||||||
placeholder="Enter recipient's country"
|
|
||||||
error={errors.giftCountry}
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
label="Gift Message"
|
|
||||||
value={giftMessage}
|
|
||||||
onChange={setGiftMessage}
|
|
||||||
placeholder="Write a heartfelt message"
|
|
||||||
error={errors.giftMessage}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -557,8 +539,8 @@ export function PaymentDetailsPage({
|
|||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
className={`w-16 h-10 rounded-lg flex-shrink-0 flex items-center justify-center ${bookingDetails?.cardMode?.toLowerCase() === 'flexi'
|
className={`w-16 h-10 rounded-lg flex-shrink-0 flex items-center justify-center ${bookingDetails?.cardMode?.toLowerCase() === 'flexi'
|
||||||
? 'bg-gradient-to-br from-[#f95faf] to-[#F95F62]'
|
? 'bg-gradient-to-br from-[#f95faf] to-[#F95F62]'
|
||||||
: 'bg-gradient-to-br from-[#F95F62] to-[#c94245]'
|
: 'bg-gradient-to-br from-[#F95F62] to-[#c94245]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="font-poppins text-[10px] font-semibold text-white">{bookingDetails?.cardMode}</span>
|
<span className="font-poppins text-[10px] font-semibold text-white">{bookingDetails?.cardMode}</span>
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Star,
|
Star,
|
||||||
Badge as BadgeIcon,
|
Badge as BadgeIcon,
|
||||||
Camera
|
Camera,
|
||||||
|
AlertCircle
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '../components/ui/button';
|
import { Button } from '../components/ui/button';
|
||||||
import { Input } from '../components/ui/input';
|
import { Input } from '../components/ui/input';
|
||||||
@@ -21,7 +22,6 @@ import { Label } from '../components/ui/label';
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
||||||
import { Separator } from '../components/ui/separator';
|
import { Separator } from '../components/ui/separator';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../components/ui/select';
|
|
||||||
import { Badge } from '../components/ui/badge';
|
import { Badge } from '../components/ui/badge';
|
||||||
import Navbar from '../components/Navbar';
|
import Navbar from '../components/Navbar';
|
||||||
import { Footer } from '../components/Footer';
|
import { Footer } from '../components/Footer';
|
||||||
@@ -88,6 +88,7 @@ export function ProfilePage({
|
|||||||
currentPage
|
currentPage
|
||||||
}: ProfilePageProps) {
|
}: ProfilePageProps) {
|
||||||
const [activeTab, setActiveTab] = useState('profile');
|
const [activeTab, setActiveTab] = useState('profile');
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
@@ -106,7 +107,7 @@ export function ProfilePage({
|
|||||||
const cityId = localStorage.getItem("cityId")
|
const cityId = localStorage.getItem("cityId")
|
||||||
const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId)
|
const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId)
|
||||||
const [updateUserProfileDetails, { isLoading: savingChanges }] = useUpdateUserProfileDetailsMutation();
|
const [updateUserProfileDetails, { isLoading: savingChanges }] = useUpdateUserProfileDetailsMutation();
|
||||||
const { data, isLoading: loadingCards } = useGetUserCardsQuery({sort,cityId})
|
const { data, isLoading: loadingCards } = useGetUserCardsQuery({ sort, cityId })
|
||||||
const { data: userItineraries, isLoading: loadingItineraries } = useGetUserItinerariesQuery(cityId)
|
const { data: userItineraries, isLoading: loadingItineraries } = useGetUserItinerariesQuery(cityId)
|
||||||
|
|
||||||
const cards = data ?? []
|
const cards = data ?? []
|
||||||
@@ -129,19 +130,87 @@ export function ProfilePage({
|
|||||||
|
|
||||||
}, [userDetails])
|
}, [userDetails])
|
||||||
|
|
||||||
|
// const validateForm = () => {
|
||||||
|
// if (!formData.firstName.trim()) return toast.error('First name is required'), false;
|
||||||
|
// if (/\s/.test(formData.firstName)) return toast.error('First name must not contain spaces'), false;
|
||||||
|
// if (!/^[A-Za-z]+$/.test(formData.firstName)) return toast.error('First name must contain only letters'), false;
|
||||||
|
|
||||||
|
// if (!formData.lastName.trim()) return toast.error('Last name is required'), false;
|
||||||
|
// if (/\s/.test(formData.lastName)) return toast.error('Last name must not contain spaces'), false;
|
||||||
|
// if (!/^[A-Za-z]+$/.test(formData.lastName)) return toast.error('Last name must contain only letters'), false;
|
||||||
|
|
||||||
|
// if (!formData.phone.trim()) return toast.error('Mobile number is required'), false;
|
||||||
|
// if (/\s/.test(formData.phone)) return toast.error('Mobile number must not contain spaces'), false;
|
||||||
|
// if (!/^\d+$/.test(formData.phone)) return toast.error('Mobile number must contain only digits'), false;
|
||||||
|
|
||||||
|
// if (!formData.address1.trim()) return toast.error('Address is required'), false;
|
||||||
|
// if (!/^[A-Za-z0-9\s,\-.]+$/.test(formData.address1)) return toast.error('Address contains invalid characters'), false;
|
||||||
|
|
||||||
|
// if (!formData.city.trim()) return toast.error('City is required'), false;
|
||||||
|
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.city)) return toast.error('City can only contain letters and spaces'), false;
|
||||||
|
// if (/\s{2,}/.test(formData.city)) return toast.error('City must not contain multiple consecutive spaces'), false;
|
||||||
|
|
||||||
|
// if (!formData.country.trim()) return toast.error('Country is required'), false;
|
||||||
|
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.country)) return toast.error('Country can only contain letters and spaces'), false;
|
||||||
|
|
||||||
|
// if (!formData.postalCode.trim()) return toast.error('Postal code is required'), false;
|
||||||
|
// if (/\s/.test(formData.postalCode)) return toast.error('Postal code must not contain spaces'), false;
|
||||||
|
// if (!/^[A-Za-z0-9]+$/.test(formData.postalCode)) return toast.error('Postal code must contain only letters and numbers'), false;
|
||||||
|
|
||||||
|
// return true;
|
||||||
|
// };
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
const e: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (!formData.firstName.trim()) e.firstName = 'First name is required';
|
||||||
|
else if (/\s/.test(formData.firstName)) e.firstName = 'First name must not contain spaces';
|
||||||
|
else if (!/^[A-Za-z]+$/.test(formData.firstName)) e.firstName = 'First name must contain only letters';
|
||||||
|
|
||||||
|
if (!formData.lastName.trim()) e.lastName = 'Last name is required';
|
||||||
|
else if (/\s/.test(formData.lastName)) e.lastName = 'Last name must not contain spaces';
|
||||||
|
else if (!/^[A-Za-z]+$/.test(formData.lastName)) e.lastName = 'Last name must contain only letters';
|
||||||
|
|
||||||
|
if (!formData.phone.trim()) e.phone = 'Mobile number is required';
|
||||||
|
else if (/\s/.test(formData.phone)) e.phone = 'Mobile number must not contain spaces';
|
||||||
|
else if (!/^\d+$/.test(formData.phone)) e.phone = 'Mobile number must contain only digits';
|
||||||
|
|
||||||
|
if (!formData.address1.trim()) e.address1 = 'Address is required';
|
||||||
|
else if (!/^[A-Za-z0-9\s,\-.]+$/.test(formData.address1)) e.address1 = 'Address contains invalid characters';
|
||||||
|
|
||||||
|
if (!formData.city.trim()) e.city = 'City is required';
|
||||||
|
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.city)) e.city = 'City can only contain letters and spaces';
|
||||||
|
else if (/\s{2,}/.test(formData.city)) e.city = 'City must not contain multiple consecutive spaces';
|
||||||
|
|
||||||
|
if (!formData.country.trim()) e.country = 'Country is required';
|
||||||
|
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.country)) e.country = 'Country can only contain letters and spaces';
|
||||||
|
|
||||||
|
if (!formData.postalCode.trim()) e.postalCode = 'Postal code is required';
|
||||||
|
else if (/\s/.test(formData.postalCode)) e.postalCode = 'Postal code must not contain spaces';
|
||||||
|
else if (!/^[A-Za-z0-9]+$/.test(formData.postalCode)) e.postalCode = 'Postal code must contain only letters and numbers';
|
||||||
|
|
||||||
|
setFieldErrors(e);
|
||||||
|
return Object.keys(e).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// inside ProfilePage function body:
|
||||||
|
const FieldError = ({ name }: { name: string }) =>
|
||||||
|
fieldErrors[name] ? (
|
||||||
|
<p className="text-xs text-red-500 mt-1 flex items-center gap-1">
|
||||||
|
<AlertCircle className="w-3 h-3" />{fieldErrors[name]}
|
||||||
|
</p>
|
||||||
|
) : null;
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: string) => {
|
const handleInputChange = (field: string, value: string) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveProfile = async () => {
|
const handleSaveProfile = async () => {
|
||||||
|
if (!validateForm()) return;
|
||||||
try {
|
try {
|
||||||
console.log("Saving profile...", formData);
|
|
||||||
const response = await updateUserProfileDetails({ userDetails: formData, userId });
|
const response = await updateUserProfileDetails({ userDetails: formData, userId });
|
||||||
console.log(response)
|
|
||||||
toast.success("Profile updated successfully!");
|
toast.success("Profile updated successfully!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving profile:", error);
|
|
||||||
toast.error("Failed to update profile. Please try again.");
|
toast.error("Failed to update profile. Please try again.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -231,45 +300,57 @@ export function ProfilePage({
|
|||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="firstName" className="font-poppins font-light">First Name</Label>
|
<Label htmlFor="firstName" className="font-poppins font-light">
|
||||||
|
First Name <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="firstName"
|
id="firstName"
|
||||||
value={formData.firstName}
|
value={formData.firstName}
|
||||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||||
className="mt-1 font-poppins font-light"
|
className={`mt-1 font-poppins font-light ${fieldErrors.firstName ? 'border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="firstName" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="lastName" className="font-poppins font-light">Last Name</Label>
|
<Label htmlFor="lastName" className="font-poppins font-light">
|
||||||
|
Last Name <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="lastName"
|
id="lastName"
|
||||||
value={formData.lastName}
|
value={formData.lastName}
|
||||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||||
className="mt-1 font-poppins font-light"
|
className={`mt-1 font-poppins font-light ${fieldErrors.lastName ? 'border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="lastName" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="email" className="font-poppins font-light">Email Address</Label>
|
<Label htmlFor="email" className="font-poppins font-light">
|
||||||
|
Email Address
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
|
disabled
|
||||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
className="mt-1 font-poppins font-light"
|
className="mt-1 font-poppins font-light"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="phone" className="font-poppins font-light">Phone Number</Label>
|
<Label htmlFor="phone" className="font-poppins font-light">
|
||||||
|
Phone Number <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="phone"
|
id="phone"
|
||||||
type="tel"
|
type="tel"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||||
className="mt-1 font-poppins font-light"
|
className={`mt-1 font-poppins font-light ${fieldErrors.phone ? 'border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="phone" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -277,29 +358,31 @@ export function ProfilePage({
|
|||||||
<h3 className="font-poppins font-normal">Billing Address</h3>
|
<h3 className="font-poppins font-normal">Billing Address</h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="country" className="font-poppins font-light">Country</Label>
|
<Label htmlFor="country" className="font-poppins font-light">
|
||||||
|
Country <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="country"
|
id="country"
|
||||||
value={formData.country}
|
value={formData.country}
|
||||||
onChange={(e) => handleInputChange('country', e.target.value)}
|
onChange={(e) => handleInputChange('country', e.target.value)}
|
||||||
className="mt-1 font-poppins font-light"
|
className={`mt-1 font-poppins font-light ${fieldErrors.country ? 'border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="country" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="address1" className="font-poppins font-light">
|
<Label htmlFor="address1" className="font-poppins font-light">
|
||||||
Address Line 1
|
Address Line 1 <span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="address1"
|
id="address1"
|
||||||
value={formData.address1}
|
value={formData.address1}
|
||||||
onChange={(e) => handleInputChange('address1', e.target.value)}
|
onChange={(e) => handleInputChange('address1', e.target.value)}
|
||||||
className="mt-1 font-poppins font-light mb-4"
|
className={`mt-1 font-poppins font-light mb-4 ${fieldErrors.address1 ? 'border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="address1" />
|
||||||
|
|
||||||
<Label htmlFor="address2" className="font-poppins font-light">
|
<Label htmlFor="address2" className="font-poppins font-light">Address Line 2</Label>
|
||||||
Address Line 2
|
|
||||||
</Label>
|
|
||||||
<Input
|
<Input
|
||||||
id="address2"
|
id="address2"
|
||||||
value={formData.address2}
|
value={formData.address2}
|
||||||
@@ -310,22 +393,28 @@ export function ProfilePage({
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="city" className="font-poppins font-light">City</Label>
|
<Label htmlFor="city" className="font-poppins font-light">
|
||||||
|
City <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="city"
|
id="city"
|
||||||
value={formData.city}
|
value={formData.city}
|
||||||
onChange={(e) => handleInputChange('city', e.target.value)}
|
onChange={(e) => handleInputChange('city', e.target.value)}
|
||||||
className="mt-1 font-poppins font-light"
|
className={`mt-1 font-poppins font-light ${fieldErrors.city ? 'border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="city" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="postalCode" className="font-poppins font-light">Postal Code</Label>
|
<Label htmlFor="postalCode" className="font-poppins font-light">
|
||||||
|
Postal Code <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="postalCode"
|
id="postalCode"
|
||||||
value={formData.postalCode}
|
value={formData.postalCode}
|
||||||
onChange={(e) => handleInputChange('postalCode', e.target.value)}
|
onChange={(e) => handleInputChange('postalCode', e.target.value)}
|
||||||
className="mt-1 font-poppins font-light"
|
className={`mt-1 font-poppins font-light ${fieldErrors.postalCode ? 'border-red-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
<FieldError name="postalCode" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -340,192 +429,6 @@ export function ProfilePage({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* App Download Section */}
|
|
||||||
<div className="lg:col-span-1">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.4 }}
|
|
||||||
>
|
|
||||||
<Card className="border border-gray-200 shadow-sm">
|
|
||||||
<CardContent className="p-8 space-y-6">
|
|
||||||
{(() => {
|
|
||||||
// Determine which pass type to show
|
|
||||||
const hasUnlimitedPass = activeCards.some((card: any) => card.cardType.cardTypeName === 'selective_pass');
|
|
||||||
const hasSelectivePass = activeCards.some((card: any) => card.cardType.cardTypeName === 'unlimited_card');
|
|
||||||
|
|
||||||
if (hasUnlimitedPass) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h3 className="font-poppins text-xl font-normal">
|
|
||||||
Get{' '}
|
|
||||||
<span className="text-primary">Melbourne Unlimited Card</span>
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
|
|
||||||
Unlimited access to 25+ attractions. Visit as many places as you want with one simple card.
|
|
||||||
Save up to 40% compared to individual tickets.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card Benefits */}
|
|
||||||
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg p-4 space-y-2">
|
|
||||||
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
|
|
||||||
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
|
|
||||||
<CreditCard className="w-3 h-3 text-white" />
|
|
||||||
</div>
|
|
||||||
<span>Unlimited entries to all attractions</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
|
|
||||||
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
|
|
||||||
<Calendar className="w-3 h-3 text-white" />
|
|
||||||
</div>
|
|
||||||
<span>Valid for 7 consecutive days</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
|
|
||||||
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
|
|
||||||
<MapPin className="w-3 h-3 text-white" />
|
|
||||||
</div>
|
|
||||||
<span>Skip the queue at major venues</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Purchase CTA */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Button
|
|
||||||
onClick={onPassesClick}
|
|
||||||
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
|
|
||||||
>
|
|
||||||
Purchase Unlimited Card
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onCityCardsClick}
|
|
||||||
className="w-full font-poppins font-normal"
|
|
||||||
>
|
|
||||||
Learn More
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (hasSelectivePass) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h3 className="font-poppins text-xl font-normal">
|
|
||||||
Get{' '}
|
|
||||||
<span className="text-primary">Selective Card</span>
|
|
||||||
{' '}now
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
|
|
||||||
Choose your own adventure with 12 hand-picked attractions. Perfect for visitors
|
|
||||||
who want flexibility and value.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card Benefits */}
|
|
||||||
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg p-4 space-y-2">
|
|
||||||
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
|
|
||||||
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
|
|
||||||
<CreditCard className="w-3 h-3 text-white" />
|
|
||||||
</div>
|
|
||||||
<span>Choose from 12 curated attractions</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
|
|
||||||
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
|
|
||||||
<Calendar className="w-3 h-3 text-white" />
|
|
||||||
</div>
|
|
||||||
<span>Flexible 7-day validity period</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
|
|
||||||
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
|
|
||||||
<Star className="w-3 h-3 text-white" />
|
|
||||||
</div>
|
|
||||||
<span>Save 40% on combined ticket price</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Purchase CTA */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Button
|
|
||||||
onClick={onPassesClick}
|
|
||||||
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
|
|
||||||
>
|
|
||||||
Purchase Selective Card
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onCityCardsClick}
|
|
||||||
className="w-full font-poppins font-normal"
|
|
||||||
>
|
|
||||||
View All Attractions
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h3 className="font-poppins text-xl font-normal">
|
|
||||||
Get{' '}
|
|
||||||
<span className="text-primary">CityCards</span>
|
|
||||||
{' '}now
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
|
|
||||||
Explore Melbourne's best attractions with our flexible card options.
|
|
||||||
Choose unlimited access or select your favorites.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card Options */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg p-4">
|
|
||||||
<div className="flex items-start justify-between mb-2">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-poppins text-base font-medium mb-1">Unlimited Card</h4>
|
|
||||||
<p className="text-xs text-gray-600 font-poppins font-light">25+ attractions, unlimited visits</p>
|
|
||||||
</div>
|
|
||||||
<Badge className="bg-primary text-white">Popular</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-poppins font-semibold text-primary">$149</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-gray-50 rounded-lg p-4">
|
|
||||||
<div className="flex items-start justify-between mb-2">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-poppins text-base font-medium mb-1">Selective Card</h4>
|
|
||||||
<p className="text-xs text-gray-600 font-poppins font-light">12 attractions of your choice</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-poppins font-semibold text-primary">$89</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Purchase CTA */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Button
|
|
||||||
onClick={onPassesClick}
|
|
||||||
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
|
|
||||||
>
|
|
||||||
Explore All Cards
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onCityCardsClick}
|
|
||||||
className="w-full font-poppins font-normal"
|
|
||||||
>
|
|
||||||
Learn More
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -726,7 +629,7 @@ export function ProfilePage({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full mt-4 font-poppins font-normal"
|
className="w-full mt-4 font-poppins font-normal"
|
||||||
onClick={()=>navigate(`/view-itinerary/${itinerary.id}`)}
|
onClick={() => navigate(`/view-itinerary/${itinerary.id}`)}
|
||||||
>
|
>
|
||||||
View Itinerary
|
View Itinerary
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ export function SuperSavingsDetailsPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Sidebar - Calendar and Booking (preserved, but you can add a real calendar if needed) */}
|
{/* Right Sidebar - Calendar and Booking (preserved, but you can add a real calendar if needed) */}
|
||||||
<div className="lg:col-span-1">
|
{/* <div className="lg:col-span-1">
|
||||||
<Card className="sticky top-32 p-6 bg-white border border-primary/20 shadow-xl rounded-2xl">
|
<Card className="sticky top-32 p-6 bg-white border border-primary/20 shadow-xl rounded-2xl">
|
||||||
<h3 className="text-2xl font-bold text-[#2d3134] mb-4">Book This Offer</h3>
|
<h3 className="text-2xl font-bold text-[#2d3134] mb-4">Book This Offer</h3>
|
||||||
<div className="space-y-4 mb-6">
|
<div className="space-y-4 mb-6">
|
||||||
@@ -379,7 +379,7 @@ export function SuperSavingsDetailsPage({
|
|||||||
Proceed to Checkout
|
Proceed to Checkout
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
Reference in New Issue
Block a user