show all attractions with filter and search functionalities

This commit is contained in:
aryabenade
2026-03-19 15:12:44 +05:30
parent 1f28171893
commit 3b920c2461
4 changed files with 324 additions and 285 deletions

View File

@@ -1,9 +1,12 @@
import { configureStore } from "@reduxjs/toolkit";
import { fakeApi } from "./services/fakeApi.service";
import { attractionsApi } from "./services/attractions.service";
export const store = configureStore({
reducer: {
[fakeApi.reducerPath]:fakeApi.reducer
[fakeApi.reducerPath]:fakeApi.reducer,
[attractionsApi.reducerPath]:attractionsApi.reducer
},
@@ -11,6 +14,7 @@ export const store = configureStore({
getDefaultMiddleware().concat(
fakeApi.middleware,
attractionsApi.middleware
),
});

View File

@@ -0,0 +1,40 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const attractionsApi = createApi({
reducerPath: 'attractionsApi',
baseQuery: fetchBaseQuery({
baseUrl: 'https://testingapi.citycards.betadelivery.com',
}),
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

@@ -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} ({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">
{attraction.location}
{/* {attraction.location} */}
</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

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