Compare commits

6 Commits

Author SHA1 Message Date
aryabenade
5d213d14d8 show upcomingCities from backend on landingPage 2026-03-20 13:58:39 +05:30
aryabenade
0a60ba58a3 remove the useMemo hook from CitySelectionDialog 2026-03-20 11:49:11 +05:30
aryabenade
a09d53db7d integrate cities api in the CitySelectionDialog 2026-03-20 11:45:14 +05:30
aryabenade
b3e1c0faf4 show AttractionDetails from backend on AttractionDetails page 2026-03-19 19:21:39 +05:30
aryabenade
3b920c2461 show all attractions with filter and search functionalities 2026-03-19 15:12:44 +05:30
aryabenade
1f28171893 fix the slow loading of CitySelectDialog 2026-03-17 17:04:59 +05:30
14 changed files with 751 additions and 669 deletions

View File

@@ -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}
/>
</>
);
}

View File

@@ -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
View 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;
},
});

View 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;

View 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;

View File

@@ -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>
);
}

View File

@@ -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();
}}

View File

@@ -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 }}
>

View File

@@ -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>
)}

View File

@@ -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>

View File

@@ -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 */}

View File

@@ -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
View File

@@ -0,0 +1,8 @@
interface ImportMetaEnv {
readonly VITE_BASE_URL: string
readonly VITE_GOOGLE_MAP: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@@ -84,7 +84,7 @@ import * as path from 'path';
outDir: 'build',
},
server: {
port: 4007,
port: 4008,
open: true,
},
});