diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 5671705..bd664f6 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -2,6 +2,7 @@ import { Routes, Route, useParams, useLocation, useNavigate } from 'react-router import { motion, AnimatePresence } from 'motion/react'; // Import all your pages +import { ProtectedRoute } from './components/ProtectedRoute'; import { MelbournePage } from './pages/MelbournePage'; import { PassesPage } from './pages/PassesPage'; import { AttractionsPage } from './pages/AttractionsPage'; @@ -178,31 +179,43 @@ export function AppRouter({ {/* User Routes */} - + + + } /> - + + + } /> - + {/* Itinerary Routes */} - + + + + } /> - + + + } /> - + + + + } /> @@ -276,12 +289,17 @@ export function AppRouter({ - + + + + } /> - + + + } /> } /> - + - - + + + + } /> @@ -305,26 +325,32 @@ export function AppRouter({ - + + + + } /> - + + + + } /> diff --git a/src/Redux/services/attractions.service.ts b/src/Redux/services/attractions.service.ts index 01b7dee..2695346 100644 --- a/src/Redux/services/attractions.service.ts +++ b/src/Redux/services/attractions.service.ts @@ -30,6 +30,20 @@ export const attractionsApi = createApi({ return `/attractions/customer/customer-attractions?${params.toString()}`; }, }), + getAttractionsForHomePage: builder.query({ + // cityId is required, others optional + query: ({ cityId, categoryId}) => { + const params = new URLSearchParams(); + + // required + params.append('cityXid', cityId); + + // optional + if (categoryId) params.append('categoryXid', categoryId); + + return `/attractions/list/city-attractions?${params.toString()}`; + }, + }), getAttractionDetailsById: builder.query({ query: (id: number) => `/attractions/customer/${id}`, @@ -38,4 +52,4 @@ export const attractionsApi = createApi({ }), }); -export const { useGetAttractionFiltersQuery,useGetCustomerAttractionsQuery,useGetAttractionDetailsByIdQuery } = attractionsApi; \ No newline at end of file +export const { useGetAttractionFiltersQuery,useGetCustomerAttractionsQuery,useGetAttractionDetailsByIdQuery,useGetAttractionsForHomePageQuery } = attractionsApi; \ No newline at end of file diff --git a/src/Redux/services/cities.service.ts b/src/Redux/services/cities.service.ts index c064904..9d9132b 100644 --- a/src/Redux/services/cities.service.ts +++ b/src/Redux/services/cities.service.ts @@ -1,11 +1,8 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { createApi } 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) => ({ diff --git a/src/components/LoginModal.tsx b/src/components/LoginModal.tsx index 32ca669..9abd21b 100644 --- a/src/components/LoginModal.tsx +++ b/src/components/LoginModal.tsx @@ -1,3 +1,4 @@ +// LoginModal.tsx import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { X } from 'lucide-react'; diff --git a/src/components/MelbourneAttractions.tsx b/src/components/MelbourneAttractions.tsx index b76a998..f531a81 100644 --- a/src/components/MelbourneAttractions.tsx +++ b/src/components/MelbourneAttractions.tsx @@ -3,271 +3,167 @@ import { ChevronLeft, ChevronRight, Clock, Users, Star, Zap, CheckCircle, MapPin import { ImageWithFallback } from './figma/ImageWithFallback'; import { motion } from 'motion/react'; import { useNavigate } from 'react-router-dom'; - -const melbourneAttractions = [ - { - id: 1, - name: "Royal Botanic Gardens", - city: "Melbourne", - country: "Australia", - image: "https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zfGVufDF8fHx8MTc1NzMzNzc4OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral", - rating: 4.8, - reviews: "15,600+", - category: "Gardens", - originalPrice: "Free", - includedValue: "$25", - perks: [ - { icon: Volume2, label: "Audio garden tour", color: "text-green-600" }, - { icon: MapPin, label: "Garden maps", color: "text-blue-600" }, - { icon: Camera, label: "Photo spots guide", color: "text-purple-600" } - ] - }, - { - id: 2, - name: "Federation Square", - city: "Melbourne", - country: "Australia", - image: "https://images.unsplash.com/photo-1639655001512-e4b58d4874b8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBmZWRlcmF0aW9uJTIwc3F1YXJlfGVufDF8fHx8MTc1NzMzNzc5Mnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral", - rating: 4.6, - reviews: "22,400+", - category: "Landmarks", - originalPrice: "Free", - includedValue: "$35", - perks: [ - { icon: Volume2, label: "Cultural tours", color: "text-orange-600" }, - { icon: Eye, label: "Gallery access", color: "text-blue-600" }, - { icon: Users, label: "Event access", color: "text-purple-600" } - ] - }, - { - id: 3, - name: "Queen Victoria Market", - city: "Melbourne", - country: "Australia", - image: "https://images.unsplash.com/photo-1676454953709-e0be46f62490?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0fGVufDF8fHx8MTc1NzMzNzc5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral", - rating: 4.7, - reviews: "18,200+", - category: "Markets", - originalPrice: "$45", - includedValue: "$45", - perks: [ - { icon: Users, label: "Food tours", color: "text-orange-600" }, - { icon: Coffee, label: "Tastings", color: "text-brown-600" }, - { icon: Volume2, label: "History guide", color: "text-blue-600" } - ] - }, - { - id: 4, - name: "Eureka Skydeck", - city: "Melbourne", - country: "Australia", - image: "https://images.unsplash.com/photo-1629677713183-29248e1268d7?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBldXJla2ElMjB0b3dlcnxlbnwxfHx8fDE3NTczMzc4MDB8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral", - rating: 4.9, - reviews: "11,800+", - category: "Views", - originalPrice: "$32", - includedValue: "$32", - perks: [ - { icon: Zap, label: "Skip-the-line", color: "text-green-600" }, - { icon: Eye, label: "360° views", color: "text-purple-600" }, - { icon: Camera, label: "Photo experiences", color: "text-blue-600" } - ] - }, - { - id: 5, - name: "St Kilda Beach & Pier", - city: "Melbourne", - country: "Australia", - image: "https://images.unsplash.com/photo-1674732954456-159835c0a46b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBzdCUyMGtpbGRhJTIwYmVhY2h8ZW58MXx8fHwxNzU3MzM3ODAzfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral", - rating: 4.5, - reviews: "14,300+", - category: "Beach", - originalPrice: "Free", - includedValue: "$20", - perks: [ - { icon: Users, label: "Penguin tours", color: "text-blue-600" }, - { icon: MapPin, label: "Beach activities", color: "text-green-600" }, - { icon: Camera, label: "Sunset spots", color: "text-purple-600" } - ] - }, - { - id: 6, - name: "Melbourne Laneways", - city: "Melbourne", - country: "Australia", - image: "https://images.unsplash.com/photo-1705120624704-0970afc29fea?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBsYW5ld2F5cyUyMHN0cmVldCUyMGFydHxlbnwxfHx8fDE3NTczMzc4MDd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral", - rating: 4.8, - reviews: "19,500+", - category: "Street Art", - originalPrice: "$55", - includedValue: "$55", - perks: [ - { icon: Palette, label: "Art tours", color: "text-pink-600" }, - { icon: Coffee, label: "Café stops", color: "text-brown-600" }, - { icon: Camera, label: "Photo walks", color: "text-purple-600" } - ] - }, - { - id: 7, - name: "Melbourne Zoo", - city: "Melbourne", - country: "Australia", - image: "https://images.unsplash.com/photo-1681429477985-30dc7b88dd5b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjB6b28lMjBhbmltYWxzfGVufDF8fHx8MTc1NzMzNzgxMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral", - rating: 4.7, - reviews: "13,900+", - category: "Wildlife", - originalPrice: "$42", - includedValue: "$42", - perks: [ - { icon: Zap, label: "Skip-the-line", color: "text-green-600" }, - { icon: Users, label: "Animal encounters", color: "text-orange-600" }, - { icon: Volume2, label: "Keeper talks", color: "text-blue-600" } - ] - }, - { - id: 8, - name: "Royal Exhibition Building", - city: "Melbourne", - country: "Australia", - image: "https://images.unsplash.com/photo-1720523794299-c3b445d71a51?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGV4aGliaXRpb24lMjBidWlsZGluZ3xlbnwxfHx8fDE3NTczMzc4MTR8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral", - rating: 4.6, - reviews: "8,700+", - category: "Heritage", - originalPrice: "$25", - includedValue: "$25", - perks: [ - { icon: Volume2, label: "Audio tours", color: "text-blue-600" }, - { icon: Eye, label: "Exhibitions", color: "text-purple-600" }, - { icon: MapPin, label: "Heritage walks", color: "text-green-600" } - ] - } -]; - -const categories = ["All", "Landmarks", "Gardens", "Markets", "Views", "Beach", "Street Art", "Wildlife", "Heritage"]; +import { useGetAttractionsForHomePageQuery } from '../Redux/services/attractions.service'; export function MelbourneAttractions() { - const [activeCategory, setActiveCategory] = useState("All"); + const [selectedCategoryId, setSelectedCategoryId] = useState(null); const navigate = useNavigate(); - const cityName = localStorage.getItem("cityName") + const cityName = localStorage.getItem("cityName"); + const cityId = localStorage.getItem("cityId"); - const filteredAttractions = activeCategory === "All" - ? melbourneAttractions - : melbourneAttractions.filter(attraction => attraction.category === activeCategory); + const { data: homePageAttractionsData } = useGetAttractionsForHomePageQuery({ cityId }); - const AttractionCard = ({ attraction, index }: { attraction: typeof melbourneAttractions[0], index: number }) => ( - - {/* 3D Flip Container */} -
- - {/* FRONT FACE */} -
- {/* Background Image */} - + const apiAttractions = homePageAttractionsData?.attractions || []; + const apiCategories = homePageAttractionsData?.categories || []; - {/* Rating Badge */} - {/*
-
- -
- {attraction.rating} -
*/} + // Filter attractions by selected category + const filteredAttractions = selectedCategoryId === null + ? apiAttractions + : apiAttractions.filter((attraction: any) => + attraction.categories?.some((cat: any) => cat.id === selectedCategoryId) + ); - {/* Front Content - Clean Title & Location */} -
-
-

{attraction.name}

-

- {attraction.city}, {attraction.country} -

+ const AttractionCard = ({ attraction, index }: { attraction: any; index: number }) => { + // Get cover image or first image from galleries + const coverImage = attraction.galleries?.find((g: any) => g.isCoverImage)?.filePathUrl + || attraction.galleries?.[0]?.filePathUrl + || ''; + + // Filter only inclusions (isInclusion: true) + const inclusions = attraction.inclusions?.filter((inc: any) => inc.isInclusion) || []; + + return ( + +
+ + {/* FRONT FACE */} +
+ +
+
+

{attraction.title}

+

{attraction.city?.cityName}, Australia

+
-
- {/* BACK FACE */} -
- {/* Back Content Container */} -
- - {/* Included Value Section */} -
-
- - Included Value + {/* BACK FACE */} +
+
+ + {/* Pricing Section */} +
+
+ + Included Value +
+
+ ${attraction.ticketPriceAdult} + {attraction.ticketPriceChild && ( + + / Child ${attraction.ticketPriceChild} + + )} +
+

+ {attraction.isBookingRequired ? 'Booking required' : 'No booking required'} +

-
{attraction.includedValue}
-

- {attraction.originalPrice === "Free" - ? "Premium access included" - : "Save money with CityCard"} -

-
- {/* What's Included List */} -
-

What's Included:

-
- {attraction.perks.slice(0, 3).map((perk, perkIndex) => ( -
-
- -
- {perk.label} + {/* Inclusions List */} + {inclusions.length > 0 && ( +
+

What's Included:

+
+ {inclusions.slice(0, 3).map((inc: any) => ( +
+
+ +
+ {inc.title} +
+ ))}
- ))} -
-
- - {/* Duration & Meta Info */} -
-
-
- - 2-3 hours
-
- - All ages + )} + + {/* Duration & Group Info */} +
+
+ {attraction.durations && ( +
+ + {attraction.durations} mins +
+ )} + {attraction.groupSize && ( +
+ + Max {attraction.groupSize} +
+ )} + {attraction.ageRange && ( +
+ + {attraction.ageRange} +
+ )}
-
- {/* Footer Features */} -
-
-
- - Mobile ticket + {/* Categories */} + {attraction.categories?.length > 0 && ( +
+ {attraction.categories.slice(0, 2).map((cat: any) => ( + + {cat.categoryName} + + ))}
-
- - Instant confirmation + )} + + {/* Footer */} +
+
+
+ + Mobile ticket +
+
+ + Instant confirmation +
-
- {/* Decorative Elements */} -
-
+ {/* Decorative Elements */} +
+
+
-
-
- - ); +
+ + ); + }; return (
+ {/* Header */} Experiences

- Discover {cityName}'s iconic landmarks, vibrant culture, world-class dining, and hidden gems - all included with your {cityName} CityCard + Discover {cityName}'s iconic landmarks, vibrant culture, world-class dining, and hidden gems — all included with your {cityName} CityCard

@@ -303,23 +199,41 @@ export function MelbourneAttractions() { viewport={{ once: true }} className="flex flex-wrap justify-center gap-3 mb-12" > - {categories.map((category, index) => ( + {/* "All" button */} + setSelectedCategoryId(null)} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + className={`px-6 py-4 h-14 rounded-2xl font-medium transition-all duration-300 ${ + selectedCategoryId === null + ? 'bg-gradient-to-r from-primary to-secondary text-white shadow-xl shadow-primary/25 ring-2 ring-primary/20' + : 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-primary/20 hover:bg-white' + }`} + > + All + + + {/* Dynamic category buttons from API */} + {apiCategories.map((category: any, index: number) => ( setActiveCategory(category)} + onClick={() => setSelectedCategoryId(category.id)} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} className={`px-6 py-4 h-14 rounded-2xl font-medium transition-all duration-300 ${ - activeCategory === category + selectedCategoryId === category.id ? 'bg-gradient-to-r from-primary to-secondary text-white shadow-xl shadow-primary/25 ring-2 ring-primary/20' : 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-primary/20 hover:bg-white' }`} > - {category} + {category.categoryName} ))} @@ -327,52 +241,44 @@ export function MelbourneAttractions() { {/* Mobile Horizontal Carousel */}
- {/* Scroll Container */}
- {filteredAttractions.map((attraction, index) => ( + {filteredAttractions.map((attraction: any, index: number) => ( ))}
- - {/* Scroll Indicators */}
- {Array.from({ length: Math.ceil(filteredAttractions.length / 2) }).map((_, index) => ( -
+ {Array.from({ length: Math.ceil(filteredAttractions.length / 2) }).map((_: any, index: number) => ( +
))}
- - {/* Mobile Hint Text */}
-

- Swipe to explore more Melbourne attractions -

+

Swipe to explore more {cityName} attractions

{/* Desktop Bento Grid */}
- {/* Top Row - 3 equal cards */}
- {filteredAttractions.slice(0, 3).map((attraction, index) => ( + {filteredAttractions.slice(0, 3).map((attraction: any, index: number) => ( ))}
- - {/* Consistent Vertical Spacing */}
- - {/* Bottom Row - 2 larger cards */}
- {filteredAttractions.slice(3, 5).map((attraction, index) => ( + {filteredAttractions.slice(3, 5).map((attraction: any, index: number) => ( ))}
+ {/* Empty State */} + {filteredAttractions.length === 0 && ( +
+

No attractions found for this category.

+
+ )} + {/* Call to Action */} navigate('/passes')} + onClick={() => navigate('/passes')} > Get Your {cityName} Card - - {/* Shine animation */}
+
); diff --git a/src/components/MelbourneCardComparison.tsx b/src/components/MelbourneCardComparison.tsx index 19d20bf..25ad8ce 100644 --- a/src/components/MelbourneCardComparison.tsx +++ b/src/components/MelbourneCardComparison.tsx @@ -4,7 +4,6 @@ import { Button } from './ui/button'; import { motion } from 'motion/react'; import { useNavigate } from 'react-router'; -// const cardOptions = [ // { // id: 'selective', // name: 'Flexi Card', diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..dc9cb39 --- /dev/null +++ b/src/components/ProtectedRoute.tsx @@ -0,0 +1,29 @@ +// ProtectedRoute.tsx +import { useState } from 'react'; +import { useAuth } from '../context/AuthContext'; +import { LoginModal } from './LoginModal'; +import { useNavigate } from 'react-router-dom'; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export function ProtectedRoute({ children }: ProtectedRouteProps) { + const { user } = useAuth(); + const navigate = useNavigate(); + const [isLoginOpen, setIsLoginOpen] = useState(!user); + + if (!user) { + return ( + { + setIsLoginOpen(false); + navigate(-1); + }} + /> + ); + } + + return <>{children}; +} \ No newline at end of file diff --git a/src/components/RegisterPage.tsx b/src/components/RegisterPage.tsx index 89adc60..31ae9c2 100644 --- a/src/components/RegisterPage.tsx +++ b/src/components/RegisterPage.tsx @@ -1,3 +1,4 @@ +//RegisterPage.tsx import { useState } from 'react'; import { Button } from './ui/button'; import { Input } from './ui/input'; @@ -8,9 +9,10 @@ import Navbar from './Navbar'; import { Footer } from './Footer'; import { useNavigate } from 'react-router-dom'; import { Label } from './ui/label'; +import { useEffect } from 'react'; export default function RegisterPage() { - const { login } = useAuth(); + const { login, user } = useAuth(); const email = localStorage.getItem("userEmail") const [formData, setFormData] = useState({ firstName: '', @@ -31,6 +33,13 @@ export default function RegisterPage() { const navigate = useNavigate() + useEffect(() => { + const pendingEmail = localStorage.getItem("userEmail"); + if (user || !pendingEmail) { + navigate("/"); + } + }, [user]); + const [register, { isLoading: isRegistering }] = useRegisterMutation(); const handleInputChange = (field: string, value: string) => { diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index b3cdda3..2fb643b 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,3 +1,4 @@ +// AuthContext.tsx import React, { createContext, useContext, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom';