show all attractions with filter and search functionalities
This commit is contained in:
@@ -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
|
||||
|
||||
),
|
||||
});
|
||||
|
||||
40
src/Redux/services/attractions.service.ts
Normal file
40
src/Redux/services/attractions.service.ts
Normal 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;
|
||||
@@ -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();
|
||||
}}
|
||||
|
||||
@@ -84,7 +84,7 @@ import * as path from 'path';
|
||||
outDir: 'build',
|
||||
},
|
||||
server: {
|
||||
port: 4007,
|
||||
port: 4008,
|
||||
open: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user