Compare commits
6 Commits
41c60ab832
...
5d213d14d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d213d14d8 | ||
|
|
0a60ba58a3 | ||
|
|
a09d53db7d | ||
|
|
b3e1c0faf4 | ||
|
|
3b920c2461 | ||
|
|
1f28171893 |
@@ -117,7 +117,7 @@ export function AppRouter({
|
||||
<Route path="/attractions/:attractionId" element={
|
||||
<motion.div key="attraction-details" {...pageTransition}>
|
||||
<AttractionDetailsPage
|
||||
attractionId={attractionId || ''}
|
||||
// attractionId={attractionId || ''}
|
||||
{...commonNavHandlers}
|
||||
onBackClick={() => navigate(-1)}
|
||||
onCheckoutClick={() => navigate('/checkout')}
|
||||
@@ -274,12 +274,6 @@ export function AppRouter({
|
||||
} />
|
||||
</Routes>
|
||||
</AnimatePresence>
|
||||
|
||||
<LoginModal
|
||||
isOpen={showLoginModal}
|
||||
onClose={onCloseLoginModal}
|
||||
onLoginSuccess={onLoginSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { fakeApi } from "./services/fakeapi.service";
|
||||
import { fakeApi } from "./services/fakeApi.service";
|
||||
import { attractionsApi } from "./services/attractions.service";
|
||||
import { citiesApi } from "./services/cities.service";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
[fakeApi.reducerPath]:fakeApi.reducer
|
||||
[fakeApi.reducerPath]:fakeApi.reducer,
|
||||
[attractionsApi.reducerPath]:attractionsApi.reducer,
|
||||
[citiesApi.reducerPath]:citiesApi.reducer
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -11,7 +16,8 @@ export const store = configureStore({
|
||||
getDefaultMiddleware().concat(
|
||||
|
||||
fakeApi.middleware,
|
||||
|
||||
attractionsApi.middleware,
|
||||
citiesApi.middleware
|
||||
),
|
||||
});
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
17
src/Redux/baseQuery.ts
Normal file
17
src/Redux/baseQuery.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// src/store/baseQuery.ts
|
||||
import { fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
|
||||
export const baseQuery = fetchBaseQuery({
|
||||
baseUrl: import.meta.env.VITE_BASE_URL,
|
||||
// credentials: "include",
|
||||
prepareHeaders: (headers) => {
|
||||
const token = localStorage.getItem("accessToken");
|
||||
if (token) {
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
// headers.set("access-token", token);
|
||||
}
|
||||
// headers.set("Content-Type", "application/json");
|
||||
return headers;
|
||||
},
|
||||
});
|
||||
|
||||
41
src/Redux/services/attractions.service.ts
Normal file
41
src/Redux/services/attractions.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import { baseQuery } from "../baseQuery";
|
||||
|
||||
export const attractionsApi = createApi({
|
||||
reducerPath: 'attractionsApi',
|
||||
// baseQuery: fetchBaseQuery({
|
||||
// baseUrl: 'https://testingapi.citycards.betadelivery.com',
|
||||
// }),
|
||||
baseQuery,
|
||||
endpoints: (builder) => ({
|
||||
getAttractionFilters: builder.query({
|
||||
// cityId is passed as the query param
|
||||
query: (cityId) => `/attractions/customer/filters?cityXid=${cityId}`,
|
||||
}),
|
||||
|
||||
getCustomerAttractions: builder.query({
|
||||
// cityId is required, others optional
|
||||
query: ({ cityId, categoryId, isBookingRequired, cardType, search }) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// required
|
||||
params.append('cityXid', cityId);
|
||||
|
||||
// optional
|
||||
if (categoryId) params.append('categoryXid', categoryId);
|
||||
if (isBookingRequired !== undefined) params.append('isBookingRequired', isBookingRequired);
|
||||
if (cardType) params.append('cardType', cardType);
|
||||
if (search) params.append('search', search);
|
||||
|
||||
return `/attractions/customer/customer-attractions?${params.toString()}`;
|
||||
},
|
||||
}),
|
||||
|
||||
getAttractionDetailsById: builder.query({
|
||||
query: (id: number) => `/attractions/customer/${id}`,
|
||||
}),
|
||||
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetAttractionFiltersQuery,useGetCustomerAttractionsQuery,useGetAttractionDetailsByIdQuery } = attractionsApi;
|
||||
30
src/Redux/services/cities.service.ts
Normal file
30
src/Redux/services/cities.service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import { baseQuery } from "../baseQuery";
|
||||
|
||||
export const citiesApi = createApi({
|
||||
reducerPath: 'citiesApi',
|
||||
// baseQuery: fetchBaseQuery({
|
||||
// baseUrl: 'https://testingapi.citycards.betadelivery.com',
|
||||
// }),
|
||||
baseQuery,
|
||||
endpoints: (builder) => ({
|
||||
|
||||
getCityListWithBanner: builder.query({
|
||||
query: ({ search }) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (search) params.append('search', search);
|
||||
|
||||
return `/cities/list/customer/cities?${params.toString()}`
|
||||
}
|
||||
}),
|
||||
|
||||
getUpcomingCities: builder.query({
|
||||
|
||||
query: (listType) => `/cities/list/all?listType=${listType}`,
|
||||
|
||||
})
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetCityListWithBannerQuery,useGetUpcomingCitiesQuery } = citiesApi;
|
||||
@@ -6,6 +6,8 @@ import { Badge } from './ui/badge';
|
||||
import { Card, } from './ui/card';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useGetAttractionDetailsByIdQuery } from '../Redux/services/attractions.service';
|
||||
|
||||
interface AttractionDetailsPageProps {
|
||||
onBackClick: () => void;
|
||||
@@ -13,7 +15,7 @@ interface AttractionDetailsPageProps {
|
||||
onSignInClick: () => void;
|
||||
onSignOutClick?: () => void;
|
||||
user?: { email: string; name: string } | null;
|
||||
attractionId: string;
|
||||
// attractionId: string;
|
||||
}
|
||||
|
||||
export function AttractionDetailsPage({
|
||||
@@ -23,74 +25,33 @@ export function AttractionDetailsPage({
|
||||
onSignOutClick,
|
||||
user,
|
||||
}: AttractionDetailsPageProps) {
|
||||
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||
|
||||
// Featured attraction for the main display
|
||||
const featuredAttraction = {
|
||||
id: 'phi-phi',
|
||||
name: 'Phi Phi Islands Adventure Day Trip with Seaview Lunch by V. Marine Tour',
|
||||
badges: ['Bestseller', 'Free cancellation', 'Reservation Required'],
|
||||
images: {
|
||||
main: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
gallery: [
|
||||
'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxpc2xhbmQlMjB0b3VyJTIwYWRvJTIwdHJvcGljYWx8ZW58MXx8fHwxNzU4MTA0OTEwfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZCUyMGF1c3RyYWxpYXxlbnwxfHx8fDE3NTgxMDQ5Mzd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhdHYlMjBkZXNlcnQlMjB0b3VyfGVufDF8fHx8MTc1ODEwNDg5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
|
||||
]
|
||||
},
|
||||
overview: {
|
||||
duration: '3 days',
|
||||
groupSize: '10 people',
|
||||
ages: '18-99 yrs',
|
||||
languages: 'English, Japanese'
|
||||
},
|
||||
description: 'The Phi Phi archipelago is a must-visit while in Phuket, and this speedboat trip whisks you around the islands in one day. Swim over the coral reefs of Pileh Lagoon, have lunch at Phi Phi Leh, snorkel at Bamboo Island, and visit Monkey Beach and Maya Bay, immortalized in "The Beach." Boat transfers, snacks, buffet lunch, snorkeling equipment, and Phuket hotel pickup and drop-off all included.',
|
||||
highlights: [
|
||||
'Experience the thrill of a speedboat to the stunning Phi Phi Islands',
|
||||
'Be amazed by the variety of marine life in the archepelago',
|
||||
'Enjoy relaxing in paradise with white sand beaches and azure turquoise water',
|
||||
'Feel the comfort of a tour limited to 35 passengers',
|
||||
'Catch a glimpse of the wild monkeys around Monkey Beach'
|
||||
],
|
||||
included: [
|
||||
'Beverages, drinking water, morning tea and buffet lunch',
|
||||
'Local taxes',
|
||||
'Hotel pickup and drop-off by air-conditioned minivan',
|
||||
'Insurance Transfer to a private pier',
|
||||
'Soft drinks',
|
||||
'Tour Guide'
|
||||
],
|
||||
notIncluded: [
|
||||
'Towel',
|
||||
'Tips',
|
||||
'Alcoholic Beverages'
|
||||
],
|
||||
bookingOptions: [
|
||||
'By Calling on 022 2645675',
|
||||
'Email your details at islands.booking@mail.com',
|
||||
'Via CityCards Portal'
|
||||
]
|
||||
};
|
||||
const { attractionId } = useParams()
|
||||
|
||||
const { data: attraction, isLoading } = useGetAttractionDetailsByIdQuery(Number(attractionId));
|
||||
|
||||
if (isLoading) {
|
||||
return <div>loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
activeCity=""
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
showCitySubmenu={false}
|
||||
>
|
||||
activeCity=""
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
// showCitySubmenu={false}
|
||||
>
|
||||
<div className="container mx-auto px-4 pt-40 pb-16 max-w-6xl">
|
||||
{/* Back Button */}
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="mb-8"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onBackClick}
|
||||
className="font-poppins font-medium text-base text-gray-600 hover:text-primary transition-colors duration-200"
|
||||
>
|
||||
@@ -102,27 +63,26 @@ export function AttractionDetailsPage({
|
||||
{/* Title and Badges Section */}
|
||||
<div className="mb-8">
|
||||
<div className="flex flex-wrap gap-3 mb-6">
|
||||
{featuredAttraction.badges.map((badge, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
{attraction.attractionBadges.map((badge: any, index: number) => (
|
||||
<Badge
|
||||
key={badge.badgeXid}
|
||||
variant={index === 0 ? "default" : "secondary"}
|
||||
className={`px-6 py-2 rounded-full text-sm transition-all duration-200 ${
|
||||
index === 0
|
||||
? 'bg-primary text-white shadow-lg'
|
||||
: 'bg-primary/10 text-primary border border-primary/20'
|
||||
}`}
|
||||
className={`px-6 py-2 rounded-full text-sm transition-all duration-200 ${index === 0
|
||||
? 'bg-primary text-white shadow-lg'
|
||||
: 'bg-primary/10 text-primary border border-primary/20'
|
||||
}`}
|
||||
>
|
||||
{badge}
|
||||
{badge.badge.badgeName}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<h1 className="text-4xl font-bold text-[#2d3134] leading-tight">
|
||||
<span className="bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent">
|
||||
Phi Phi Islands Adventure
|
||||
{attraction.title}
|
||||
</span>{' '}
|
||||
<span className="text-[#2d3134]">
|
||||
Day Trip with Seaview Lunch by V. Marine Tour
|
||||
Day Trip by {attraction.partner.businessName}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
@@ -132,18 +92,18 @@ export function AttractionDetailsPage({
|
||||
{/* Main large image */}
|
||||
<div className="col-span-2 row-span-2">
|
||||
<ImageWithFallback
|
||||
src={featuredAttraction.images.main}
|
||||
src={attraction.attractionGalleries[0].filePathUrl}
|
||||
alt="Main attraction image"
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Gallery images */}
|
||||
{featuredAttraction.images.gallery.slice(0, 4).map((image, index) => (
|
||||
<div key={index} className="col-span-1 row-span-1">
|
||||
{attraction.attractionGalleries.slice().map((image:any) => (
|
||||
<div key={image.id} className="col-span-1 row-span-1">
|
||||
<ImageWithFallback
|
||||
src={image}
|
||||
alt={`Gallery image ${index + 1}`}
|
||||
src={image.filePathUrl}
|
||||
alt={`Gallery image ${image.id}`}
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
@@ -156,20 +116,43 @@ export function AttractionDetailsPage({
|
||||
<div className="lg:col-span-2 space-y-12">
|
||||
{/* Overview Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{Object.entries(featuredAttraction.overview).map(([key, value]) => (
|
||||
<Card key={key} className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
|
||||
{key === 'duration' && <Clock className="w-6 h-6 text-primary" />}
|
||||
{key === 'groupSize' && <Users className="w-6 h-6 text-primary" />}
|
||||
{key === 'ages' && <Users className="w-6 h-6 text-primary" />}
|
||||
{key === 'languages' && <MapPin className="w-6 h-6 text-primary" />}
|
||||
</div>
|
||||
<h3 className="font-normal text-primary capitalize mb-1">
|
||||
{key === 'groupSize' ? 'Group Size' : key}
|
||||
</h3>
|
||||
<p className="text-sm text-[#717171] font-light">{value}</p>
|
||||
</Card>
|
||||
))}
|
||||
{/* Duration */}
|
||||
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<Clock className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-normal text-primary capitalize mb-1">Duration</h3>
|
||||
<p className="text-sm text-[#717171] font-light">{attraction.durations} mins</p>
|
||||
</Card>
|
||||
|
||||
{/* Group Size */}
|
||||
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<Users className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-normal text-primary capitalize mb-1">Group Size</h3>
|
||||
<p className="text-sm text-[#717171] font-light">{attraction.groupSize}</p>
|
||||
</Card>
|
||||
|
||||
{/* Age Range */}
|
||||
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<Users className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-normal text-primary capitalize mb-1">Age Range</h3>
|
||||
<p className="text-sm text-[#717171] font-light">{attraction.ageRange}</p>
|
||||
</Card>
|
||||
|
||||
{/* Languages */}
|
||||
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<MapPin className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-normal text-primary capitalize mb-1">Languages</h3>
|
||||
<p className="text-sm text-[#717171] font-light">
|
||||
{attraction.attractionLanguages.map((lang: any) => lang.language.name).join(", ")}
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tour Overview */}
|
||||
@@ -181,7 +164,7 @@ export function AttractionDetailsPage({
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-[#2d3134] leading-relaxed text-lg font-light">
|
||||
{featuredAttraction.description}
|
||||
{attraction.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -194,12 +177,12 @@ export function AttractionDetailsPage({
|
||||
</h3>
|
||||
</div>
|
||||
<ul className="space-y-4">
|
||||
{featuredAttraction.highlights.map((highlight, index) => (
|
||||
<li key={index} className="flex items-start gap-3 group">
|
||||
{attraction.attractionHighlights.map((highlight: any) => (
|
||||
<li key={highlight.id} className="flex items-start gap-3 group">
|
||||
<div className="w-6 h-6 bg-primary/10 rounded-full mt-1 flex items-center justify-center flex-shrink-0 group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<div className="w-2 h-2 bg-primary rounded-full"></div>
|
||||
</div>
|
||||
<span className="text-[#2d3134] leading-relaxed font-light">{highlight}</span>
|
||||
<span className="text-[#2d3134] leading-relaxed font-light">{highlight.title}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -220,30 +203,32 @@ export function AttractionDetailsPage({
|
||||
<Check className="w-5 h-5" />
|
||||
Included
|
||||
</h4>
|
||||
{featuredAttraction.included.map((item, index) => (
|
||||
<div key={index} className="flex items-start gap-3 group">
|
||||
<div className="w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0 mt-1 group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<Check className="w-3 h-3 text-primary" />
|
||||
{attraction.attractionInclusions.filter((inclusion: any) => inclusion.isInclusion === true)
|
||||
.map((inclusion: any) => (
|
||||
<div key={inclusion.id} className="flex items-start gap-3 group">
|
||||
<div className="w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0 mt-1 group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<Check className="w-3 h-3 text-primary" />
|
||||
</div>
|
||||
<span className="text-[#2d3134] font-light">{inclusion.title}</span>
|
||||
</div>
|
||||
<span className="text-[#2d3134] font-light">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Not Included */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-medium text-gray-600 mb-4 flex items-center gap-2">
|
||||
<X className="w-5 h-5" />
|
||||
Not Included
|
||||
</h4>
|
||||
{featuredAttraction.notIncluded.map((item, index) => (
|
||||
<div key={index} className="flex items-start gap-3 group">
|
||||
<div className="w-6 h-6 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0 mt-1 group-hover:bg-gray-200 transition-colors duration-200">
|
||||
<X className="w-3 h-3 text-gray-500" />
|
||||
{attraction.attractionInclusions.filter((inclusion: any) => inclusion.isInclusion === false)
|
||||
.map((inclusion: any) => (
|
||||
<div key={inclusion.id} className="flex items-start gap-3 group">
|
||||
<div className="w-6 h-6 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0 mt-1 group-hover:bg-gray-200 transition-colors duration-200">
|
||||
<X className="w-3 h-3 text-gray-500" />
|
||||
</div>
|
||||
<span className="text-[#2d3134] font-light">{inclusion.title}</span>
|
||||
</div>
|
||||
<span className="text-[#2d3134] font-light">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,7 +247,8 @@ export function AttractionDetailsPage({
|
||||
<MapPin className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<p className="text-lg font-medium text-primary mb-2">Interactive Map</p>
|
||||
<p className="text-sm text-gray-600 font-light">Phi Phi Islands, Thailand</p>
|
||||
<p className="text-sm text-gray-600 font-light">{attraction.title}</p>
|
||||
<p className="text-sm text-gray-600 font-light">{attraction.address} </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -276,7 +262,7 @@ export function AttractionDetailsPage({
|
||||
<h3 className="text-xl font-bold text-primary mb-1">Select Date</h3>
|
||||
<p className="text-sm text-gray-600">Choose your preferred visit date</p>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Custom Calendar Design */}
|
||||
<div className="space-y-4">
|
||||
{/* Calendar Header */}
|
||||
@@ -305,7 +291,7 @@ export function AttractionDetailsPage({
|
||||
<div className="grid grid-cols-7 gap-1">
|
||||
{/* Previous month */}
|
||||
<button className="h-10 w-10 text-sm text-gray-300 hover:bg-gray-50 rounded">31</button>
|
||||
|
||||
|
||||
{/* Current month */}
|
||||
{Array.from({ length: 30 }, (_, i) => {
|
||||
const day = i + 1;
|
||||
@@ -314,13 +300,12 @@ export function AttractionDetailsPage({
|
||||
return (
|
||||
<button
|
||||
key={day}
|
||||
className={`h-10 w-10 text-sm rounded font-medium transition-all duration-200 ${
|
||||
isSelected
|
||||
? 'bg-primary text-white shadow-lg scale-105'
|
||||
: isToday
|
||||
className={`h-10 w-10 text-sm rounded font-medium transition-all duration-200 ${isSelected
|
||||
? 'bg-primary text-white shadow-lg scale-105'
|
||||
: isToday
|
||||
? 'bg-primary/10 text-primary border border-primary/20'
|
||||
: 'text-gray-700 hover:bg-primary/5 hover:text-primary'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{day}
|
||||
</button>
|
||||
@@ -356,7 +341,7 @@ export function AttractionDetailsPage({
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">Adult Ticket</span>
|
||||
<span className="font-bold text-xl text-primary">$89</span>
|
||||
<span className="font-bold text-xl text-primary">{attraction.ticketPriceAdult}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">Service Fee</span>
|
||||
@@ -365,14 +350,14 @@ export function AttractionDetailsPage({
|
||||
<div className="border-t border-primary/20 pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-semibold text-gray-900">Total</span>
|
||||
<span className="font-bold text-2xl text-primary">$94</span>
|
||||
<span className="font-bold text-2xl text-primary">${attraction.ticketPriceAdult + 5}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Confirm Booking Button */}
|
||||
<Button
|
||||
<Button
|
||||
className="w-full bg-primary text-white hover:bg-primary/90 py-6 text-lg rounded-xl font-semibold transition-all duration-200 shadow-lg hover:shadow-xl hover:scale-[1.02] relative overflow-hidden group"
|
||||
onClick={() => onCheckoutClick()}
|
||||
>
|
||||
@@ -398,7 +383,7 @@ export function AttractionDetailsPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { Search, Star, Clock } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
@@ -9,12 +9,11 @@ import { Badge } from './ui/badge';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
import { useGetAttractionFiltersQuery, useGetCustomerAttractionsQuery } from '../Redux/services/attractions.service';
|
||||
interface User {
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Attraction {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -30,191 +29,187 @@ interface Attraction {
|
||||
passType: string;
|
||||
}
|
||||
|
||||
const attractions: Attraction[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Centipede Tour - Guided Arizona Desert Tour by ATV',
|
||||
description: 'Experience the thrill of off-road adventure through the stunning Arizona desert landscape',
|
||||
image: 'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhdHYlMjBkZXNlcnQlMjB0b3VyfGVufDF8fHx8MTc1ODEwNDg5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'Paris, France',
|
||||
duration: '4 days',
|
||||
rating: 4.8,
|
||||
price: 189.25,
|
||||
category: 'adventure',
|
||||
hasReservation: true,
|
||||
reviewCount: 243,
|
||||
passType: 'unlimited'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Molokini and Turtle Town Snorkeling Adventure Aboard',
|
||||
description: 'Snorkel in crystal-clear waters and swim alongside sea turtles in this unforgettable marine adventure',
|
||||
image: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'New York, USA',
|
||||
duration: '4 days',
|
||||
rating: 4.8,
|
||||
price: 225,
|
||||
category: 'adventure',
|
||||
hasReservation: false,
|
||||
reviewCount: 167,
|
||||
passType: 'selective'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Westminster Walking Tour & Westminster Abbey Entry',
|
||||
description: 'Explore the heart of London with guided tours of historic Westminster and the famous Abbey',
|
||||
image: 'https://images.unsplash.com/photo-1533929736458-ca588d08c8be?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx3ZXN0bWluc3RlciUyMGFiYmV5JTIwbG9uZG9ufGVufDF8fHx8MTc1ODEwNDkwNnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'London, UK',
|
||||
duration: '4 days',
|
||||
rating: 4.8,
|
||||
price: 343,
|
||||
category: 'culture',
|
||||
hasReservation: true,
|
||||
reviewCount: 343,
|
||||
passType: 'unlimited'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'All Inclusive Ultimate Circle Island Day Tour with Lunch',
|
||||
description: 'Comprehensive island tour including all major attractions, lunch, and transportation',
|
||||
image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxpc2xhbmQlMjB0b3VyJTIwYWRvJTIwdHJvcGljYWx8ZW58MXx8fHwxNzU4MTA0OTEwfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'New York, USA',
|
||||
duration: '4 days',
|
||||
rating: 4.8,
|
||||
price: 225,
|
||||
category: 'adventure',
|
||||
hasReservation: false,
|
||||
reviewCount: 243,
|
||||
passType: 'unlimited'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Space Center Houston Admission Ticket',
|
||||
description: 'Explore NASA\'s Johnson Space Center and discover the wonders of space exploration',
|
||||
image: 'https://images.unsplash.com/photo-1446776653964-20c1d3a81b06?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzcGFjZSUyMGNlbnRlciUyMG5hc2ElMjBob3VzdG9ufGVufDF8fHx8MTc1ODEwNDkxM3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'Paris, France',
|
||||
duration: '4 days',
|
||||
rating: 4.8,
|
||||
price: 225,
|
||||
category: 'family',
|
||||
hasReservation: true,
|
||||
reviewCount: 243,
|
||||
passType: 'selective'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Melbourne Skydeck Observatory',
|
||||
description: 'Experience breathtaking 360-degree views from the Southern Hemisphere\'s highest viewing platform',
|
||||
image: 'https://images.unsplash.com/photo-1677200922658-d0df5b2ac91e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBhdHRyYWN0aW9ucyUyMGZhbW91cyUyMGxhbmRtYXJrc3xlbnwxfHx8fDE3NTc0MDEwODV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'Melbourne CBD',
|
||||
duration: '2 hours',
|
||||
rating: 4.5,
|
||||
price: 25,
|
||||
category: 'adventure',
|
||||
hasReservation: true,
|
||||
reviewCount: 892,
|
||||
passType: 'selective'
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: 'Royal Botanic Gardens Melbourne',
|
||||
description: 'Explore 38 hectares of stunning gardens featuring over 8,500 species of plants',
|
||||
image: 'https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zfGVufDF8fHx8MTc1NzMzNzc4OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'South Yarra',
|
||||
duration: '3 hours',
|
||||
rating: 4.7,
|
||||
price: 0,
|
||||
category: 'nature',
|
||||
hasReservation: false,
|
||||
reviewCount: 1245,
|
||||
passType: 'selective'
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: 'Federation Square Cultural Precinct',
|
||||
description: 'Melbourne\'s cultural precinct featuring galleries, museums, and unique architecture',
|
||||
image: 'https://images.unsplash.com/photo-1580688027085-8220709e3d84?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxmZWRlcmF0aW9uJTIwc3F1YXJlJTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5Mnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'Melbourne CBD',
|
||||
duration: '3 hours',
|
||||
rating: 4.3,
|
||||
price: 0,
|
||||
category: 'culture',
|
||||
hasReservation: true,
|
||||
reviewCount: 672,
|
||||
passType: 'unlimited'
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
name: 'St Kilda Pier & Little Penguins',
|
||||
description: 'Watch little penguins return home at sunset while enjoying the scenic pier',
|
||||
image: 'https://images.unsplash.com/photo-1597889790884-2bb63cfbd4f6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzdCUyMGtpbGRhJTIwcGllciUyMG1lbGJvdXJuZXxlbnwxfHx8fDE3NTc0MDEwOTV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'St Kilda',
|
||||
duration: '2 hours',
|
||||
rating: 4.4,
|
||||
price: 0,
|
||||
category: 'nature',
|
||||
hasReservation: false,
|
||||
reviewCount: 543,
|
||||
passType: 'unlimited'
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
name: 'Queen Victoria Market Experience',
|
||||
description: 'Historic market offering fresh produce, gourmet foods, and unique souvenirs',
|
||||
image: 'https://images.unsplash.com/photo-1676454953709-e0be46f62490?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0JTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5OHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'Melbourne CBD',
|
||||
duration: '2 hours',
|
||||
rating: 4.6,
|
||||
price: 0,
|
||||
category: 'culture',
|
||||
hasReservation: true,
|
||||
reviewCount: 987,
|
||||
passType: 'selective'
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
name: 'Melbourne Zoo Adventure',
|
||||
description: 'Meet over 320 animal species from around the world in naturalistic habitats',
|
||||
image: 'https://images.unsplash.com/photo-1681429477985-30dc7b88dd5b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjB6b28lMjBhbmltYWxzfGVufDF8fHx8MTc1NzMzNzgxMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'Parkville',
|
||||
duration: '4 hours',
|
||||
rating: 4.5,
|
||||
price: 40,
|
||||
category: 'family',
|
||||
hasReservation: false,
|
||||
reviewCount: 1156,
|
||||
passType: 'selective'
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
name: 'Great Ocean Road Day Tour',
|
||||
description: 'Scenic coastal drive featuring the famous Twelve Apostles and stunning ocean views',
|
||||
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZCUyMGF1c3RyYWxpYXxlbnwxfHx8fDE3NTgxMDQ5Mzd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
location: 'Great Ocean Road',
|
||||
duration: '12 hours',
|
||||
rating: 4.9,
|
||||
price: 85,
|
||||
category: 'adventure',
|
||||
hasReservation: true,
|
||||
reviewCount: 678,
|
||||
passType: 'unlimited'
|
||||
}
|
||||
];
|
||||
|
||||
const filterCategories = [
|
||||
{ value: 'with-reservation', label: 'With Reservation', count: 3 },
|
||||
{ value: 'without-reservation', label: 'Without Reservation', count: 3 },
|
||||
{ value: 'beach', label: 'Beach', count: 3 },
|
||||
{ value: 'adventure', label: 'Adventure', count: 3 },
|
||||
{ value: 'mountains', label: 'Mountains', count: 3 },
|
||||
{ value: 'family', label: 'Family Friendly', count: 3 }
|
||||
];
|
||||
|
||||
const passTypeCategories = [
|
||||
{ value: 'selective', label: 'Flexi Pass', count: 6 },
|
||||
{ value: 'unlimited', label: 'Unlimited Pass', count: 6 }
|
||||
];
|
||||
|
||||
// {
|
||||
// id: '1',
|
||||
// name: 'Centipede Tour - Guided Arizona Desert Tour by ATV',
|
||||
// description: 'Experience the thrill of off-road adventure through the stunning Arizona desert landscape',
|
||||
// image: 'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhdHYlMjBkZXNlcnQlMjB0b3VyfGVufDF8fHx8MTc1ODEwNDg5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'Paris, France',
|
||||
// duration: '4 days',
|
||||
// rating: 4.8,
|
||||
// price: 189.25,
|
||||
// category: 'adventure',
|
||||
// hasReservation: true,
|
||||
// reviewCount: 243,
|
||||
// passType: 'unlimited'
|
||||
// },
|
||||
// {
|
||||
// id: '2',
|
||||
// name: 'Molokini and Turtle Town Snorkeling Adventure Aboard',
|
||||
// description: 'Snorkel in crystal-clear waters and swim alongside sea turtles in this unforgettable marine adventure',
|
||||
// image: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'New York, USA',
|
||||
// duration: '4 days',
|
||||
// rating: 4.8,
|
||||
// price: 225,
|
||||
// category: 'adventure',
|
||||
// hasReservation: false,
|
||||
// reviewCount: 167,
|
||||
// passType: 'selective'
|
||||
// },
|
||||
// {
|
||||
// id: '3',
|
||||
// name: 'Westminster Walking Tour & Westminster Abbey Entry',
|
||||
// description: 'Explore the heart of London with guided tours of historic Westminster and the famous Abbey',
|
||||
// image: 'https://images.unsplash.com/photo-1533929736458-ca588d08c8be?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx3ZXN0bWluc3RlciUyMGFiYmV5JTIwbG9uZG9ufGVufDF8fHx8MTc1ODEwNDkwNnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'London, UK',
|
||||
// duration: '4 days',
|
||||
// rating: 4.8,
|
||||
// price: 343,
|
||||
// category: 'culture',
|
||||
// hasReservation: true,
|
||||
// reviewCount: 343,
|
||||
// passType: 'unlimited'
|
||||
// },
|
||||
// {
|
||||
// id: '4',
|
||||
// name: 'All Inclusive Ultimate Circle Island Day Tour with Lunch',
|
||||
// description: 'Comprehensive island tour including all major attractions, lunch, and transportation',
|
||||
// image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxpc2xhbmQlMjB0b3VyJTIwYWRvJTIwdHJvcGljYWx8ZW58MXx8fHwxNzU4MTA0OTEwfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'New York, USA',
|
||||
// duration: '4 days',
|
||||
// rating: 4.8,
|
||||
// price: 225,
|
||||
// category: 'adventure',
|
||||
// hasReservation: false,
|
||||
// reviewCount: 243,
|
||||
// passType: 'unlimited'
|
||||
// },
|
||||
// {
|
||||
// id: '5',
|
||||
// name: 'Space Center Houston Admission Ticket',
|
||||
// description: 'Explore NASA\'s Johnson Space Center and discover the wonders of space exploration',
|
||||
// image: 'https://images.unsplash.com/photo-1446776653964-20c1d3a81b06?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzcGFjZSUyMGNlbnRlciUyMG5hc2ElMjBob3VzdG9ufGVufDF8fHx8MTc1ODEwNDkxM3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'Paris, France',
|
||||
// duration: '4 days',
|
||||
// rating: 4.8,
|
||||
// price: 225,
|
||||
// category: 'family',
|
||||
// hasReservation: true,
|
||||
// reviewCount: 243,
|
||||
// passType: 'selective'
|
||||
// },
|
||||
// {
|
||||
// id: '6',
|
||||
// name: 'Melbourne Skydeck Observatory',
|
||||
// description: 'Experience breathtaking 360-degree views from the Southern Hemisphere\'s highest viewing platform',
|
||||
// image: 'https://images.unsplash.com/photo-1677200922658-d0df5b2ac91e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBhdHRyYWN0aW9ucyUyMGZhbW91cyUyMGxhbmRtYXJrc3xlbnwxfHx8fDE3NTc0MDEwODV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'Melbourne CBD',
|
||||
// duration: '2 hours',
|
||||
// rating: 4.5,
|
||||
// price: 25,
|
||||
// category: 'adventure',
|
||||
// hasReservation: true,
|
||||
// reviewCount: 892,
|
||||
// passType: 'selective'
|
||||
// },
|
||||
// {
|
||||
// id: '7',
|
||||
// name: 'Royal Botanic Gardens Melbourne',
|
||||
// description: 'Explore 38 hectares of stunning gardens featuring over 8,500 species of plants',
|
||||
// image: 'https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zfGVufDF8fHx8MTc1NzMzNzc4OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'South Yarra',
|
||||
// duration: '3 hours',
|
||||
// rating: 4.7,
|
||||
// price: 0,
|
||||
// category: 'nature',
|
||||
// hasReservation: false,
|
||||
// reviewCount: 1245,
|
||||
// passType: 'selective'
|
||||
// },
|
||||
// {
|
||||
// id: '8',
|
||||
// name: 'Federation Square Cultural Precinct',
|
||||
// description: 'Melbourne\'s cultural precinct featuring galleries, museums, and unique architecture',
|
||||
// image: 'https://images.unsplash.com/photo-1580688027085-8220709e3d84?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxmZWRlcmF0aW9uJTIwc3F1YXJlJTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5Mnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'Melbourne CBD',
|
||||
// duration: '3 hours',
|
||||
// rating: 4.3,
|
||||
// price: 0,
|
||||
// category: 'culture',
|
||||
// hasReservation: true,
|
||||
// reviewCount: 672,
|
||||
// passType: 'unlimited'
|
||||
// },
|
||||
// {
|
||||
// id: '9',
|
||||
// name: 'St Kilda Pier & Little Penguins',
|
||||
// description: 'Watch little penguins return home at sunset while enjoying the scenic pier',
|
||||
// image: 'https://images.unsplash.com/photo-1597889790884-2bb63cfbd4f6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzdCUyMGtpbGRhJTIwcGllciUyMG1lbGJvdXJuZXxlbnwxfHx8fDE3NTc0MDEwOTV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'St Kilda',
|
||||
// duration: '2 hours',
|
||||
// rating: 4.4,
|
||||
// price: 0,
|
||||
// category: 'nature',
|
||||
// hasReservation: false,
|
||||
// reviewCount: 543,
|
||||
// passType: 'unlimited'
|
||||
// },
|
||||
// {
|
||||
// id: '10',
|
||||
// name: 'Queen Victoria Market Experience',
|
||||
// description: 'Historic market offering fresh produce, gourmet foods, and unique souvenirs',
|
||||
// image: 'https://images.unsplash.com/photo-1676454953709-e0be46f62490?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0JTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5OHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'Melbourne CBD',
|
||||
// duration: '2 hours',
|
||||
// rating: 4.6,
|
||||
// price: 0,
|
||||
// category: 'culture',
|
||||
// hasReservation: true,
|
||||
// reviewCount: 987,
|
||||
// passType: 'selective'
|
||||
// },
|
||||
// {
|
||||
// id: '11',
|
||||
// name: 'Melbourne Zoo Adventure',
|
||||
// description: 'Meet over 320 animal species from around the world in naturalistic habitats',
|
||||
// image: 'https://images.unsplash.com/photo-1681429477985-30dc7b88dd5b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjB6b28lMjBhbmltYWxzfGVufDF8fHx8MTc1NzMzNzgxMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'Parkville',
|
||||
// duration: '4 hours',
|
||||
// rating: 4.5,
|
||||
// price: 40,
|
||||
// category: 'family',
|
||||
// hasReservation: false,
|
||||
// reviewCount: 1156,
|
||||
// passType: 'selective'
|
||||
// },
|
||||
// {
|
||||
// id: '12',
|
||||
// name: 'Great Ocean Road Day Tour',
|
||||
// description: 'Scenic coastal drive featuring the famous Twelve Apostles and stunning ocean views',
|
||||
// image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZCUyMGF1c3RyYWxpYXxlbnwxfHx8fDE3NTgxMDQ5Mzd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
|
||||
// location: 'Great Ocean Road',
|
||||
// duration: '12 hours',
|
||||
// rating: 4.9,
|
||||
// price: 85,
|
||||
// category: 'adventure',
|
||||
// hasReservation: true,
|
||||
// reviewCount: 678,
|
||||
// passType: 'unlimited'
|
||||
// }
|
||||
// ];
|
||||
// const filterCategories = [
|
||||
// { value: 'with-reservation', label: 'With Reservation', count: 3 },
|
||||
// { value: 'without-reservation', label: 'Without Reservation', count: 3 },
|
||||
// { value: 'beach', label: 'Beach', count: 3 },
|
||||
// { value: 'adventure', label: 'Adventure', count: 3 },
|
||||
// { value: 'mountains', label: 'Mountains', count: 3 },
|
||||
// { value: 'family', label: 'Family Friendly', count: 3 }
|
||||
// ];
|
||||
// const passTypeCategories = [
|
||||
// { value: 'selective', label: 'Flexi Pass', count: 6 },
|
||||
// { value: 'unlimited', label: 'Unlimited Pass', count: 6 }
|
||||
// ];
|
||||
interface AttractionsPageProps {
|
||||
onSignInClick: () => void;
|
||||
onSignOutClick?: () => void;
|
||||
@@ -226,55 +221,73 @@ export function AttractionsPage({
|
||||
onSignOutClick,
|
||||
user
|
||||
}: AttractionsPageProps) {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [selectedPassTypes, setSelectedPassTypes] = useState<string[]>([]);
|
||||
|
||||
const filteredAttractions = attractions.filter(attraction => {
|
||||
const matchesSearch = attraction.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
attraction.description.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const [search, setSearch] = useState("");
|
||||
const [isBookingRequired, setIsBookingRequired] = useState<boolean | undefined>(undefined)
|
||||
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
|
||||
const [selectedPassType, setSelectedPassType] = useState<string | null>(null);
|
||||
|
||||
const matchesCategory = selectedCategories.length === 0 ||
|
||||
selectedCategories.some(cat => {
|
||||
if (cat === 'with-reservation') return attraction.hasReservation;
|
||||
if (cat === 'without-reservation') return !attraction.hasReservation;
|
||||
return attraction.category === cat;
|
||||
});
|
||||
|
||||
const matchesPassType = selectedPassTypes.length === 0 ||
|
||||
selectedPassTypes.includes(attraction.passType);
|
||||
|
||||
return matchesSearch && matchesCategory && matchesPassType;
|
||||
const cityId = 1
|
||||
|
||||
const { data: filterData, isLoading } = useGetAttractionFiltersQuery(cityId)
|
||||
const { data: attractions } = useGetCustomerAttractionsQuery({
|
||||
cityId, // required
|
||||
categoryId: selectedCategory, // optional
|
||||
isBookingRequired, // optional
|
||||
cardType: selectedPassType, // optional
|
||||
search, // optional
|
||||
});
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
setSelectedCategories(prev =>
|
||||
prev.includes(category)
|
||||
? prev.filter(c => c !== category)
|
||||
: [...prev, category]
|
||||
);
|
||||
};
|
||||
|
||||
const togglePassType = (passType: string) => {
|
||||
setSelectedPassTypes(prev =>
|
||||
prev.includes(passType)
|
||||
? prev.filter(p => p !== passType)
|
||||
: [...prev, passType]
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
const handleAttractionClick = (attractionId: string) => {
|
||||
navigate(`/attractions/${attractionId}`);
|
||||
};
|
||||
|
||||
const handleCheckoutClick = () => {
|
||||
navigate('/checkout');
|
||||
};
|
||||
|
||||
const showingFrom = 1;
|
||||
const showingTo = Math.min(12, filteredAttractions.length);
|
||||
const totalItems = filteredAttractions.length;
|
||||
const showingTo = Math.min(12, attractions?.length);
|
||||
const totalItems = attractions?.length;
|
||||
|
||||
function handlePassTypeSelection(key: string, checked: boolean) {
|
||||
if (checked) {
|
||||
setSelectedPassType(key); // only keep the newly selected one
|
||||
} else {
|
||||
setSelectedPassType(null); // reset if unchecked
|
||||
}
|
||||
}
|
||||
|
||||
function handleCategorySelection(id: number, checked: boolean) {
|
||||
if (checked) {
|
||||
if (id === 50) {
|
||||
setIsBookingRequired(true);
|
||||
setSelectedCategory(null); // clear normal category
|
||||
} else if (id === 51) {
|
||||
setIsBookingRequired(false);
|
||||
setSelectedCategory(null); // clear normal category
|
||||
} else {
|
||||
setSelectedCategory(id);
|
||||
setIsBookingRequired(undefined); // clear booking filter
|
||||
}
|
||||
} else {
|
||||
// reset if unchecked
|
||||
if (id === 50 || id === 51) {
|
||||
setIsBookingRequired(undefined);
|
||||
} else {
|
||||
setSelectedCategory(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
@@ -296,7 +309,6 @@ export function AttractionsPage({
|
||||
Skip the lines and explore Melbourne's most iconic destinations with your CityCard pass
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* City Card Promotional Banner */}
|
||||
<div className="mb-8">
|
||||
<Card className="bg-gradient-to-r from-primary to-primary/80 text-white p-8 rounded-xl border-none shadow-lg overflow-hidden relative">
|
||||
@@ -304,20 +316,18 @@ export function AttractionsPage({
|
||||
<h2 className="font-merchant text-2xl leading-tight font-bold text-white">
|
||||
Find Your Perfect Adventure
|
||||
</h2>
|
||||
|
||||
{/* Search Bar and Button Container */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
|
||||
{/* Search Bar */}
|
||||
<div className="relative flex-1 max-w-lg">
|
||||
<Input
|
||||
placeholder="Search An Attraction"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
className="pl-4 pr-12 h-[44px] bg-white/95 backdrop-blur-sm border-0 rounded-lg text-gray-800 placeholder:text-gray-500 font-poppins shadow-lg"
|
||||
/>
|
||||
<Search className="absolute right-4 top-1/2 transform -translate-y-1/2 text-gray-500 w-5 h-5" />
|
||||
</div>
|
||||
|
||||
{/* Call-to-Action Button */}
|
||||
<Button
|
||||
className="bg-white/90 hover:bg-white text-primary border-2 border-primary hover:border-primary/80 px-8 h-[44px] rounded-lg font-semibold transition-all duration-200 hover:scale-105 font-poppins shadow-lg"
|
||||
@@ -327,13 +337,11 @@ export function AttractionsPage({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative background elements */}
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -translate-y-16 translate-x-16"></div>
|
||||
<div className="absolute bottom-0 left-0 w-24 h-24 bg-white/5 rounded-full translate-y-12 -translate-x-12"></div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-8">
|
||||
{/* Left Sidebar */}
|
||||
<div className="w-64 flex-shrink-0">
|
||||
@@ -344,22 +352,29 @@ export function AttractionsPage({
|
||||
<div className="h-0 w-6 border-t-[3px] border-[#2d2d2d] rotate-90"></div>
|
||||
<h3 className="font-merchant text-[22px] text-[#2d2d2d] font-normal">Search by</h3>
|
||||
</div>
|
||||
|
||||
{/* Filter categories */}
|
||||
<div className="space-y-4">
|
||||
{filterCategories.map(category => (
|
||||
<div key={category.value} className="flex items-center gap-3">
|
||||
{filterData && filterData.categories.map((category: any) => (
|
||||
<div key={category.id} className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
id={category.value}
|
||||
checked={selectedCategories.includes(category.value)}
|
||||
onCheckedChange={() => toggleCategory(category.value)}
|
||||
id={String(category.id)}
|
||||
checked={
|
||||
category.id === 50
|
||||
? isBookingRequired === true
|
||||
: category.id === 51
|
||||
? isBookingRequired === false
|
||||
: selectedCategory === category.id
|
||||
}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
handleCategorySelection(category.id, checked)
|
||||
}
|
||||
className="border-[#bebebe]"
|
||||
/>
|
||||
<label
|
||||
htmlFor={category.value}
|
||||
htmlFor={String(category.id)}
|
||||
className="font-poppins text-sm text-[#414141] cursor-pointer flex-1 font-normal"
|
||||
>
|
||||
{category.label} ({category.count})
|
||||
{category.categoryName} ({category.count})
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
@@ -367,51 +382,49 @@ export function AttractionsPage({
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t border-[#e5e5e5]"></div>
|
||||
|
||||
{/* Pass Type header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-0 w-6 border-t-[3px] border-[#2d2d2d] rotate-90"></div>
|
||||
<h3 className="font-merchant text-[22px] text-[#2d2d2d] font-normal">Pass Type</h3>
|
||||
</div>
|
||||
|
||||
{/* Pass Type filters */}
|
||||
<div className="space-y-4">
|
||||
{passTypeCategories.map(passType => (
|
||||
<div key={passType.value} className="flex items-center gap-3">
|
||||
{filterData && Object.entries(filterData.passType).map(([key, count]) => (
|
||||
<div key={key} className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
id={passType.value}
|
||||
checked={selectedPassTypes.includes(passType.value)}
|
||||
onCheckedChange={() => togglePassType(passType.value)}
|
||||
id={key}
|
||||
checked={selectedPassType === key}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
handlePassTypeSelection(key, checked as boolean)
|
||||
}
|
||||
className="border-[#bebebe]"
|
||||
/>
|
||||
<label
|
||||
htmlFor={passType.value}
|
||||
htmlFor={key}
|
||||
className="font-poppins text-sm text-[#414141] cursor-pointer flex-1 font-normal"
|
||||
>
|
||||
{passType.label} ({passType.count})
|
||||
{key==="selective_pass" ?"Selective":"Unlimited"} ({count as number})
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-[48px] font-medium text-[#2d2d2d] mb-6">Attractions in Melbourne</h1>
|
||||
|
||||
{/* Results count */}
|
||||
<p className="text-[16px] text-[#414141] mb-2">
|
||||
Showing {showingFrom}-{showingTo} of {totalItems} Item(s)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Attractions Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr">
|
||||
{filteredAttractions.slice(0, 12).map((attraction) => (
|
||||
{attractions && attractions.map((attraction: any) => (
|
||||
<motion.div
|
||||
key={attraction.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@@ -435,70 +448,52 @@ export function AttractionsPage({
|
||||
<Badge className="bg-primary text-white px-3 py-1 font-poppins font-semibold shadow-lg">
|
||||
FREE
|
||||
</Badge>
|
||||
) : attraction.passType === 'unlimited' ? (
|
||||
<Badge className="bg-gradient-to-r from-amber-500 to-orange-500 text-white px-3 py-1 font-poppins font-semibold shadow-lg border-0">
|
||||
Unlimited Pass Exclusive
|
||||
</Badge>
|
||||
) : (
|
||||
) : attraction.cards[0].cardType.cardTypeDisplayName === "Flexi card" ? (
|
||||
<Badge className="bg-gradient-to-r from-blue-500 to-cyan-500 text-white px-3 py-1 font-poppins font-semibold shadow-lg border-0">
|
||||
Flexi Pass
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="bg-gradient-to-r from-amber-500 to-orange-500 text-white px-3 py-1 font-poppins font-semibold shadow-lg border-0">
|
||||
Unlimited Pass Exclusive
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CardContent className="p-4 flex-1 flex flex-col">
|
||||
<div className="text-sm text-muted-foreground mb-2 font-medium font-poppins">
|
||||
{/* <div className="text-sm text-muted-foreground mb-2 font-medium font-poppins">
|
||||
{attraction.location}
|
||||
</div>
|
||||
</div> */}
|
||||
<h3 className="font-semibold text-foreground mb-3 line-clamp-2 leading-tight min-h-[2.5rem] font-poppins">
|
||||
{attraction.name}
|
||||
{attraction.title}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="flex items-center gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-4 h-4 ${i < Math.floor(attraction.rating)
|
||||
? 'fill-primary text-primary'
|
||||
: 'text-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
<span className="text-sm font-medium ml-1 text-gray-700 font-poppins">
|
||||
{attraction.rating} ({attraction.reviewCount})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing and Pass Info */}
|
||||
<div className="mt-auto pt-2 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<Clock className="w-4 h-4 text-primary" />
|
||||
<span className="font-poppins">{attraction.duration}</span>
|
||||
<span className="font-poppins">{attraction.durations} minutes</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-muted-foreground font-poppins font-normal">Normal visit price</div>
|
||||
<div className="text-lg font-bold text-gray-400 line-through font-poppins">
|
||||
${attraction.price}
|
||||
${attraction.ticketPriceAdult}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Included with Pass CTA */}
|
||||
<div className="bg-gradient-to-r from-primary/10 to-secondary/10 border border-primary/20 rounded-lg p-2.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-poppins font-semibold text-primary uppercase">
|
||||
✓ Included with {attraction.passType === 'unlimited' ? 'Unlimited' : 'Selective'} Pass
|
||||
✓ Included with {attraction.cards[0].cardType.cardTypeDisplayName === "Flexi card" ? 'Flexi' : 'Unlimited'} Pass
|
||||
</p>
|
||||
<p className="text-xs font-poppins font-normal text-gray-600 mt-0.5">
|
||||
Save ${attraction.price}
|
||||
Save ${attraction.cards[0].adultPrice}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-primary hover:bg-primary/90 text-white font-poppins font-semibold text-xs px-4 min-h-[44px] min-w-[44px] h-[44px] whitespace-nowrap"
|
||||
onClick={(e) => {
|
||||
onClick={(e:React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
handleCheckoutClick();
|
||||
}}
|
||||
|
||||
@@ -79,7 +79,7 @@ export function CTAButton({ user, onClick, className = "" }: CTAButtonProps) {
|
||||
<motion.div
|
||||
key={user ? user.email : 'logged-out'}
|
||||
className="w-full h-full"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
// initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// CitySelectionDialog.tsx
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Dialog, DialogContent, DialogTitle, DialogDescription } from './ui/dialog';
|
||||
import { ArrowLeft, Search } from 'lucide-react';
|
||||
import { Input } from './ui/input';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { useGetCityListWithBannerQuery } from '../Redux/services/cities.service';
|
||||
|
||||
interface City {
|
||||
id: string;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
id: number;
|
||||
cityName: string;
|
||||
bannerImage: string;
|
||||
}
|
||||
|
||||
interface CitySelectionDialogProps {
|
||||
@@ -19,43 +20,39 @@ interface CitySelectionDialogProps {
|
||||
onCitySelect?: (cityId: string) => void; // ✅ Updated to pass cityId
|
||||
}
|
||||
|
||||
const cities: City[] = [
|
||||
{ id: 'melbourne', name: 'Melbourne', imageUrl: 'https://images.unsplash.com/photo-1624341373902-70e3a8dc9acc?...' },
|
||||
{ id: 'new-york', name: 'New York', imageUrl: 'https://images.unsplash.com/photo-1514565131-fce0801e5785?...' },
|
||||
{ id: 'abu-dhabi', name: 'Abu Dhabi', imageUrl: 'https://images.unsplash.com/photo-1584551246679-0daf3d275d0f?...' },
|
||||
{ id: 'dubai', name: 'Dubai', imageUrl: 'https://images.unsplash.com/photo-1518684079-3c830dcef090?...' },
|
||||
{ id: 'tokyo', name: 'Tokyo', imageUrl: 'https://images.unsplash.com/photo-1613487897980-50cc440ce118?...' },
|
||||
{ id: 'ontario', name: 'Ontario', imageUrl: 'https://images.unsplash.com/photo-1542704792-e30dac463c90?...' },
|
||||
{ id: 'mumbai', name: 'Mumbai', imageUrl: 'https://images.unsplash.com/photo-1600867161422-79f8f6e08c84?...' },
|
||||
{ id: 'louisiana', name: 'Louisiana', imageUrl: 'https://images.unsplash.com/photo-1646508262200-455d62c22182?...' },
|
||||
];
|
||||
|
||||
export function CitySelectionDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onCitySelect
|
||||
export function CitySelectionDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onCitySelect
|
||||
}: CitySelectionDialogProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const filteredCities = cities.filter(city =>
|
||||
city.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
const { data: cities, isLoading } = useGetCityListWithBannerQuery({ search })
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
|
||||
const handleCityClick = (city: City) => {
|
||||
console.log('Selected city:', city.name);
|
||||
|
||||
console.log('Selected city:', city.cityName);
|
||||
|
||||
// ✅ Call the onCitySelect callback if provided (passing cityId)
|
||||
if (onCitySelect) {
|
||||
onCitySelect(city.id);
|
||||
onCitySelect(String(city.id));
|
||||
} else {
|
||||
// ✅ Default behavior: navigate to passes page
|
||||
navigate(`/passes?city=${encodeURIComponent(city.name)}`);
|
||||
navigate(`/passes?city=${encodeURIComponent(city.cityName)}`);
|
||||
}
|
||||
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md w-full p-0 gap-0 font-poppins">
|
||||
@@ -83,8 +80,8 @@ export function CitySelectionDialog({
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search Cities"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
className="pl-10 bg-input border-0 rounded-lg h-11 font-poppins placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
@@ -94,27 +91,26 @@ export function CitySelectionDialog({
|
||||
<div className="px-6 pb-6 max-h-[60vh] overflow-y-auto">
|
||||
<AnimatePresence>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{filteredCities.map((city, index) => (
|
||||
{cities && cities.map((city: City) => (
|
||||
<motion.button
|
||||
key={city.id}
|
||||
onClick={() => handleCityClick(city)}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
|
||||
className="relative h-28 rounded-2xl overflow-hidden group cursor-pointer"
|
||||
>
|
||||
<ImageWithFallback
|
||||
src={city.imageUrl}
|
||||
alt={city.name}
|
||||
src={city.bannerImage}
|
||||
alt={city.cityName}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
|
||||
<div className="absolute bottom-3 left-3 right-3">
|
||||
<h3 className="font-poppins font-semibold text-white text-left">
|
||||
{city.name}
|
||||
{city.cityName}
|
||||
</h3>
|
||||
</div>
|
||||
</motion.button>
|
||||
@@ -122,10 +118,10 @@ export function CitySelectionDialog({
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
|
||||
{filteredCities.length === 0 && (
|
||||
{cities?.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500 font-poppins">
|
||||
No cities found matching "{searchQuery}"
|
||||
No cities found matching "{search}"
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,102 +3,103 @@ import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from './ui/button';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import Image592Traced from '../imports/Image592Traced-5025-559';
|
||||
import { useGetUpcomingCitiesQuery } from '../Redux/services/cities.service';
|
||||
|
||||
const upcomingCities = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Boston',
|
||||
country: 'USA',
|
||||
launchDate: 'Spring 2025',
|
||||
attractions: 65,
|
||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.',
|
||||
image: 'https://images.unsplash.com/photo-1568271667303-14b2a1a36da1?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Rome',
|
||||
country: 'Italy',
|
||||
launchDate: 'Summer 2025',
|
||||
attractions: 80,
|
||||
image: 'https://images.unsplash.com/photo-1552832230-c0197dd311b5?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Paris',
|
||||
country: 'France',
|
||||
launchDate: 'Fall 2025',
|
||||
attractions: 95,
|
||||
image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Dubai',
|
||||
country: 'UAE',
|
||||
launchDate: 'Winter 2025',
|
||||
attractions: 70,
|
||||
image: 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false,
|
||||
badge: 'New'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Tokyo',
|
||||
country: 'Japan',
|
||||
launchDate: 'Early 2026',
|
||||
attractions: 120,
|
||||
image: 'https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Sydney',
|
||||
country: 'Australia',
|
||||
launchDate: 'Spring 2026',
|
||||
attractions: 85,
|
||||
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'New York',
|
||||
country: 'USA',
|
||||
launchDate: 'Summer 2026',
|
||||
attractions: 150,
|
||||
image: 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false,
|
||||
badge: 'Most Requested'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Singapore',
|
||||
country: 'Singapore',
|
||||
launchDate: 'Fall 2026',
|
||||
attractions: 75,
|
||||
image: 'https://images.unsplash.com/photo-1525625293386-3f8f99389edd?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Amsterdam',
|
||||
country: 'Netherlands',
|
||||
launchDate: 'Winter 2026',
|
||||
attractions: 90,
|
||||
image: 'https://images.unsplash.com/photo-1534351590666-13e3e96b5017?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Barcelona',
|
||||
country: 'Spain',
|
||||
launchDate: 'Early 2027',
|
||||
attractions: 110,
|
||||
image: 'https://images.unsplash.com/photo-1583422409516-2895a77efded?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
showHoverState: false
|
||||
}
|
||||
];
|
||||
// const upcomingCities = [
|
||||
// {
|
||||
// id: 1,
|
||||
// name: 'Boston',
|
||||
// country: 'USA',
|
||||
// launchDate: 'Spring 2025',
|
||||
// attractions: 65,
|
||||
// description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.',
|
||||
// image: 'https://images.unsplash.com/photo-1568271667303-14b2a1a36da1?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
// showHoverState: true
|
||||
// },
|
||||
// {
|
||||
// id: 2,
|
||||
// name: 'Rome',
|
||||
// country: 'Italy',
|
||||
// launchDate: 'Summer 2025',
|
||||
// attractions: 80,
|
||||
// image: 'https://images.unsplash.com/photo-1552832230-c0197dd311b5?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
// showHoverState: false
|
||||
// },
|
||||
// {
|
||||
// id: 3,
|
||||
// name: 'Paris',
|
||||
// country: 'France',
|
||||
// launchDate: 'Fall 2025',
|
||||
// attractions: 95,
|
||||
// image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
// showHoverState: false
|
||||
// },
|
||||
// {
|
||||
// id: 4,
|
||||
// name: 'Dubai',
|
||||
// country: 'UAE',
|
||||
// launchDate: 'Winter 2025',
|
||||
// attractions: 70,
|
||||
// image: 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
// showHoverState: false,
|
||||
// badge: 'New'
|
||||
// },
|
||||
// {
|
||||
// id: 5,
|
||||
// name: 'Tokyo',
|
||||
// country: 'Japan',
|
||||
// launchDate: 'Early 2026',
|
||||
// attractions: 120,
|
||||
// image: 'https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
// showHoverState: false
|
||||
// },
|
||||
// {
|
||||
// id: 6,
|
||||
// name: 'Sydney',
|
||||
// country: 'Australia',
|
||||
// launchDate: 'Spring 2026',
|
||||
// attractions: 85,
|
||||
// image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
// showHoverState: false
|
||||
// },
|
||||
// {
|
||||
// id: 7,
|
||||
// name: 'New York',
|
||||
// country: 'USA',
|
||||
// launchDate: 'Summer 2026',
|
||||
// attractions: 150,
|
||||
// image: 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
// showHoverState: false,
|
||||
// badge: 'Most Requested'
|
||||
// },
|
||||
// {
|
||||
// id: 8,
|
||||
// name: 'Singapore',
|
||||
// country: 'Singapore',
|
||||
// launchDate: 'Fall 2026',
|
||||
// attractions: 75,
|
||||
// image: 'https://images.unsplash.com/photo-1525625293386-3f8f99389edd?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
// showHoverState: false
|
||||
// },
|
||||
// {
|
||||
// id: 9,
|
||||
// name: 'Amsterdam',
|
||||
// country: 'Netherlands',
|
||||
// launchDate: 'Winter 2026',
|
||||
// attractions: 90,
|
||||
// image: 'https://images.unsplash.com/photo-1534351590666-13e3e96b5017?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
// showHoverState: false
|
||||
// },
|
||||
// {
|
||||
// id: 10,
|
||||
// name: 'Barcelona',
|
||||
// country: 'Spain',
|
||||
// launchDate: 'Early 2027',
|
||||
// attractions: 110,
|
||||
// image: 'https://images.unsplash.com/photo-1583422409516-2895a77efded?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
// showHoverState: false
|
||||
// }
|
||||
// ];
|
||||
|
||||
export function LandingUpcomingCities() {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -107,6 +108,15 @@ export function LandingUpcomingCities() {
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
const [showDragHint, setShowDragHint] = useState(false);
|
||||
|
||||
const listType = "upcomingCity"
|
||||
// const[listType,setListType]=useState("upcomingCity")
|
||||
|
||||
const { data, isLoading } = useGetUpcomingCitiesQuery(listType)
|
||||
|
||||
if(isLoading){
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (!scrollContainerRef.current) return;
|
||||
// Only start dragging if not clicking on a button or interactive element
|
||||
@@ -143,11 +153,11 @@ export function LandingUpcomingCities() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalMouseUp = () => setIsDragging(false);
|
||||
document.addEventListener('mouseup', handleGlobalMouseUp);
|
||||
return () => document.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// const handleGlobalMouseUp = () => setIsDragging(false);
|
||||
// document.addEventListener('mouseup', handleGlobalMouseUp);
|
||||
// return () => document.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-gray-50">
|
||||
@@ -172,11 +182,11 @@ export function LandingUpcomingCities() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={`flex gap-6 overflow-x-auto scrollbar-hide pb-2 ${isDragging ? 'cursor-grabbing dragging select-none' : 'cursor-grab'}`}
|
||||
style={{
|
||||
scrollbarWidth: 'none',
|
||||
style={{
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none',
|
||||
scrollBehavior: isDragging ? 'auto' : 'smooth',
|
||||
paddingLeft: 'max(1rem, calc((100vw - 1280px) / 2 + 1rem))',
|
||||
@@ -188,112 +198,112 @@ export function LandingUpcomingCities() {
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
>
|
||||
{upcomingCities.map((city) => (
|
||||
<div
|
||||
key={city.id}
|
||||
className="flex-shrink-0 w-72 md:w-80 group relative h-[420px] rounded-3xl overflow-hidden shadow-lg hover:shadow-xl transition-all duration-500"
|
||||
>
|
||||
{/* Background - Either solid color or image */}
|
||||
{city.showHoverState ? (
|
||||
// Boston card with image background and same layout as other cards
|
||||
<>
|
||||
<ImageWithFallback
|
||||
src={city.image!}
|
||||
alt={city.name}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
|
||||
{/* Dark overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent group-hover:from-black/80 transition-all duration-500" />
|
||||
|
||||
{/* City name overlay - matching Rome card layout */}
|
||||
<div className="absolute bottom-6 left-6 right-6 text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.name}</h3>
|
||||
<div className="flex items-center justify-between text-sm text-white/80">
|
||||
<span>{city.country}</span>
|
||||
<span>{city.launchDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
{data && data?.upcomingCities?.map((city: any) => (
|
||||
<div
|
||||
key={city.id}
|
||||
className="flex-shrink-0 w-72 md:w-80 group relative h-[420px] rounded-3xl overflow-hidden shadow-lg hover:shadow-xl transition-all duration-500"
|
||||
>
|
||||
{/* Background - Either solid color or image */}
|
||||
{true ? (
|
||||
// Boston card with image background and same layout as other cards
|
||||
<>
|
||||
<ImageWithFallback
|
||||
src={city.imgPathName!}
|
||||
alt={city.cityName}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
|
||||
{/* Hover state overlay - same as other cards */}
|
||||
<div className="absolute inset-0 bg-warm-coral/90 opacity-0 group-hover:opacity-100 transition-all duration-500 flex items-center justify-center">
|
||||
<div className="text-center text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.name}</h3>
|
||||
<p className="text-white/90 mb-4">{city.attractions}+ attractions</p>
|
||||
<p className="text-sm text-white/80 mb-6">Coming {city.launchDate}</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-white/20 hover:bg-white/30 text-white border-white/30 hover:border-white/50 backdrop-blur-sm"
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('Notify Me button clicked');
|
||||
}}
|
||||
>
|
||||
Notify Me
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// Image background for other cards
|
||||
<>
|
||||
<ImageWithFallback
|
||||
src={city.image!}
|
||||
alt={city.name}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
|
||||
{/* Dark overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent group-hover:from-black/80 transition-all duration-500" />
|
||||
|
||||
{/* Badge (if present) */}
|
||||
{city.badge && (
|
||||
<div className="absolute top-4 right-4 bg-white text-gray-900 px-3 py-1 rounded-full text-sm font-medium shadow-lg">
|
||||
{city.badge}
|
||||
</div>
|
||||
)}
|
||||
{/* Dark overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent group-hover:from-black/80 transition-all duration-500" />
|
||||
|
||||
{/* City name overlay */}
|
||||
<div className="absolute bottom-6 left-6 right-6 text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.name}</h3>
|
||||
<div className="flex items-center justify-between text-sm text-white/80">
|
||||
<span>{city.country}</span>
|
||||
<span>{city.launchDate}</span>
|
||||
</div>
|
||||
{/* City name overlay - matching Rome card layout */}
|
||||
<div className="absolute bottom-6 left-6 right-6 text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.cityName}</h3>
|
||||
<div className="flex items-center justify-between text-sm text-white/80">
|
||||
{/* <span>{city.country}</span>
|
||||
<span>{city.launchDate}</span> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover state overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/90 to-secondary/90 opacity-0 group-hover:opacity-100 transition-all duration-500 flex items-center justify-center">
|
||||
<div className="text-center text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.name}</h3>
|
||||
<p className="text-white/90 mb-4">{city.attractions}+ attractions</p>
|
||||
<p className="text-sm text-white/80 mb-6">Coming {city.launchDate}</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-white/20 hover:bg-white/30 text-white border-white/30 hover:border-white/50 backdrop-blur-sm"
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('Notify Me button clicked');
|
||||
}}
|
||||
>
|
||||
Notify Me
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Hover state overlay - same as other cards */}
|
||||
<div className="absolute inset-0 bg-warm-coral/90 opacity-0 group-hover:opacity-100 transition-all duration-500 flex items-center justify-center">
|
||||
<div className="text-center text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.cityName}</h3>
|
||||
{/* <p className="text-white/90 mb-4">{city.attractions}+ attractions</p>
|
||||
<p className="text-sm text-white/80 mb-6">Coming {city.launchDate}</p> */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-white/20 hover:bg-white/30 text-white border-white/30 hover:border-white/50 backdrop-blur-sm"
|
||||
onMouseDown={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
console.log('Notify Me button clicked');
|
||||
}}
|
||||
>
|
||||
Notify Me
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// Image background for other cards
|
||||
<>
|
||||
<ImageWithFallback
|
||||
src={city.image!}
|
||||
alt={city.name}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
|
||||
{/* Dark overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent group-hover:from-black/80 transition-all duration-500" />
|
||||
|
||||
{/* Badge (if present) */}
|
||||
{/* {city.badge && (
|
||||
<div className="absolute top-4 right-4 bg-white text-gray-900 px-3 py-1 rounded-full text-sm font-medium shadow-lg">
|
||||
{city.badge}
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* City name overlay */}
|
||||
{/* <div className="absolute bottom-6 left-6 right-6 text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.name}</h3>
|
||||
<div className="flex items-center justify-between text-sm text-white/80">
|
||||
<span>{city.country}</span>
|
||||
<span>{city.launchDate}</span>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Hover state overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/90 to-secondary/90 opacity-0 group-hover:opacity-100 transition-all duration-500 flex items-center justify-center">
|
||||
<div className="text-center text-white">
|
||||
<h3 className="text-2xl font-bold mb-2">{city.cityName}</h3>
|
||||
{/* <p className="text-white/90 mb-4">{city.attractions}+ attractions</p> */}
|
||||
{/* <p className="text-sm text-white/80 mb-6">Coming {city.launchDate}</p> */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-white/20 hover:bg-white/30 text-white border-white/30 hover:border-white/50 backdrop-blur-sm"
|
||||
onMouseDown={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
console.log('Notify Me button clicked');
|
||||
}}
|
||||
>
|
||||
Notify Me
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { MobileAppPromotion } from './MobileAppPromotion';
|
||||
import { MelbourneFAQ } from './MelbourneFAQ';
|
||||
import { Footer } from './Footer';
|
||||
import { MinimalHeroBanner } from './MinimalHeroBanner';
|
||||
// import { MinimalHeroBanner } from './MinimalHeroBanner';
|
||||
import { Layout } from '../Layout';
|
||||
import { HeroBannerCarousel } from './HeroBannerCarousel';
|
||||
import { HotelEsimOffers } from './HotelEsimOffers';
|
||||
@@ -257,12 +257,12 @@ export function MelbournePage({
|
||||
|
||||
{/* Attractions Section */}
|
||||
<div id="attractions" className="scroll-mt-32">
|
||||
<MelbourneAttractions onAttractionClick={() => { }} />
|
||||
<MelbourneAttractions />
|
||||
</div>
|
||||
|
||||
{/* Pass Comparison */}
|
||||
<div id="passes" className="scroll-mt-32">
|
||||
<MelbourneCardComparison onSelectPass={() => { }} />
|
||||
<MelbourneCardComparison />
|
||||
</div>
|
||||
|
||||
{/* Tour Overview */}
|
||||
@@ -280,7 +280,7 @@ export function MelbournePage({
|
||||
|
||||
{/* Blogs */}
|
||||
<div id="blogs" className="scroll-mt-32">
|
||||
<MelbourneBlogs onBlogClick={() => { }} />
|
||||
<MelbourneBlogs />
|
||||
</div>
|
||||
|
||||
{/* Custom Postcards */}
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function Navbar({
|
||||
|
||||
const { user, login, logout } = useAuth(); // from AuthContext
|
||||
|
||||
const protectedPaths = ["/passes", "/whats-included", "/","/melbourne"];
|
||||
const protectedPaths = ["/passes", "/whats-included", "/", "/melbourne"];
|
||||
|
||||
const handleOpenLoginModal = () => {
|
||||
if (!user && protectedPaths.includes(location.pathname)) {
|
||||
@@ -289,7 +289,7 @@ export default function Navbar({
|
||||
console.log('City selected from navbar:', cityId);
|
||||
onCityChange(cityId);
|
||||
|
||||
if (cityId.toLowerCase() === 'melbourne') {
|
||||
if (cityId.toLowerCase() === '1') {
|
||||
setNavigationSource('melbourne');
|
||||
navigate('/melbourne');
|
||||
} else {
|
||||
@@ -472,7 +472,7 @@ export default function Navbar({
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{title && (
|
||||
<div className="px-5 py-4 border-b border-gray-100/50">
|
||||
<div className="px-5 py-4 border-b border-gray-200/50">
|
||||
<h3 className="font-merchant font-semibold text-gray-900 text-base">{title}</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
8
src/vite-env.d.ts
vendored
Normal file
8
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_BASE_URL: string
|
||||
readonly VITE_GOOGLE_MAP: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
@@ -84,7 +84,7 @@ import * as path from 'path';
|
||||
outDir: 'build',
|
||||
},
|
||||
server: {
|
||||
port: 4007,
|
||||
port: 4008,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user