main #7
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CityCards Travel 22-8-2025</title>
|
||||
<title>CityCards Customer-web</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -54,7 +54,7 @@
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"recharts": "^2.15.2",
|
||||
"sonner": "^2.0.3",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "*",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"vaul": "^1.1.2"
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"recharts": "^2.15.2",
|
||||
"sonner": "^2.0.3",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "*",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"vaul": "^1.1.2"
|
||||
|
||||
@@ -2,38 +2,36 @@ import { Routes, Route, useParams, useLocation, useNavigate } from 'react-router
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
|
||||
// Import all your pages
|
||||
import { LoginModal } from './components/LoginModal';
|
||||
import { MelbournePage } from './components/MelbournePage';
|
||||
import { PassesPage } from './components/PassesPage';
|
||||
import { AttractionsPage } from './components/AttractionsPage';
|
||||
import { AttractionDetailsPage } from './components/AttractionDetailsPage';
|
||||
import { CheckoutPage } from './components/CheckoutPage';
|
||||
import { SecureCheckoutPage } from './components/SecureCheckoutPage';
|
||||
import { BlogsPage } from './components/BlogsPage';
|
||||
import { BlogDetailsPage } from './components/BlogDetailsPage';
|
||||
import { MelbournePage } from './pages/MelbournePage';
|
||||
import { PassesPage } from './pages/PassesPage';
|
||||
import { AttractionsPage } from './pages/AttractionsPage';
|
||||
import { AttractionDetailsPage } from './pages/AttractionDetailsPage';
|
||||
import { CheckoutPage } from './pages/CheckoutPage';
|
||||
import { SecureCheckoutPage } from './pages/SecureCheckoutPage';
|
||||
import { BlogsPage } from './pages/BlogsPage';
|
||||
import { BlogDetailsPage } from './pages/BlogDetailsPage';
|
||||
import { HowItWorksPage } from './components/HowItWorksPage';
|
||||
import { FAQPage } from './components/FAQPage';
|
||||
import { PrivacyPolicyPage } from './components/PrivacyPolicyPage';
|
||||
import { AboutUsPage } from './components/AboutUsPage';
|
||||
import { ProfilePage } from './components/ProfilePage';
|
||||
import { CreateMagicItineraryPage } from './components/CreateMagicItineraryPage';
|
||||
import { ItineraryViewPage } from './components/ItineraryViewPage';
|
||||
import { OffersPage } from './components/OffersPage';
|
||||
import { CityCardsPage } from './components/CityCardsPage';
|
||||
import { MagicItineraryPage } from './components/MagicItineraryPage';
|
||||
import { PostCardsPage } from './components/PostCardsPage';
|
||||
import { DownloadAppPage } from './components/DownloadAppPage';
|
||||
import { EsimsPage } from './components/EsimsPage';
|
||||
import { HotelDiscountsPage } from './components/HotelDiscountsPage';
|
||||
import { ContactUsPage } from './components/ContactUsPage';
|
||||
import { PrivacyPolicyPage } from './pages/PrivacyPolicyPage';
|
||||
import { AboutUsPage } from './pages/AboutUsPage';
|
||||
import { ProfilePage } from './pages/ProfilePage';
|
||||
import { CreateMagicItineraryPage } from './pages/CreateMagicItineraryPage';
|
||||
import { ItineraryViewPage } from './pages/ItineraryViewPage';
|
||||
import { OffersPage } from './pages/OffersPage';
|
||||
import { CityCardsPage } from './pages/CityCardsPage';
|
||||
import { MagicItineraryPage } from './pages/MagicItineraryPage';
|
||||
import { PostCardsPage } from './pages/PostCardsPage';
|
||||
import { DownloadAppPage } from './pages/DownloadAppPage';
|
||||
import { HotelDiscountsPage } from './pages/HotelDiscountsPage';
|
||||
import { ContactUsPage } from './pages/ContactUsPage';
|
||||
|
||||
import { pageTransition } from './utils/animations';
|
||||
import { LandingPage } from './pages/landingPage';
|
||||
import ComingSoonPage from './pages/ComingSoonPage';
|
||||
import { SuperSavingsPage } from './components/SuperSavingsPage';
|
||||
import { WhatsIncluded } from './components/WhatsIncluded';
|
||||
import { LandingMagicItineraryPage } from './components/LandingMagicItineraryPage';
|
||||
import { DiscoverPage } from './components/DiscoverPage';
|
||||
import { SuperSavingsPage } from './pages/SuperSavingsPage';
|
||||
import { WhatsIncluded } from './pages/WhatsIncluded';
|
||||
import { LandingMagicItineraryPage } from './pages/LandingMagicItineraryPage';
|
||||
import { DiscoverPage } from './pages/DiscoverPage';
|
||||
|
||||
// User type definition
|
||||
interface User {
|
||||
@@ -90,11 +88,11 @@ export function AppRouter({
|
||||
} />
|
||||
|
||||
{/* Home Route */}
|
||||
<Route path="/melbourne" element={
|
||||
<Route path="/:cityName" element={
|
||||
<motion.div key="home" {...pageTransition}>
|
||||
<MelbournePage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
} />
|
||||
|
||||
{/* Passes Route */}
|
||||
<Route path="/passes" element={
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { fakeApi } from "./services/fakeApi.service";
|
||||
import { attractionsApi } from "./services/attractions.service";
|
||||
import { citiesApi } from "./services/cities.service";
|
||||
import { authApi } from "./services/auth.service";
|
||||
import { profileApi } from "./services/profile.service";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
[fakeApi.reducerPath]:fakeApi.reducer,
|
||||
[attractionsApi.reducerPath]:attractionsApi.reducer,
|
||||
[citiesApi.reducerPath]:citiesApi.reducer
|
||||
[attractionsApi.reducerPath]: attractionsApi.reducer,
|
||||
[citiesApi.reducerPath]: citiesApi.reducer,
|
||||
[authApi.reducerPath]: authApi.reducer,
|
||||
[profileApi.reducerPath]: profileApi.reducer
|
||||
|
||||
},
|
||||
|
||||
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(
|
||||
|
||||
fakeApi.middleware,
|
||||
attractionsApi.middleware,
|
||||
citiesApi.middleware
|
||||
attractionsApi.middleware,
|
||||
citiesApi.middleware,
|
||||
authApi.middleware,
|
||||
profileApi.middleware
|
||||
),
|
||||
});
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
@@ -8,10 +8,9 @@ export const baseQuery = fetchBaseQuery({
|
||||
const token = localStorage.getItem("accessToken");
|
||||
if (token) {
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
// headers.set("access-token", token);
|
||||
headers.set("access-token", token);
|
||||
}
|
||||
// headers.set("Content-Type", "application/json");
|
||||
return headers;
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
53
src/Redux/services/auth.service.ts
Normal file
53
src/Redux/services/auth.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import { baseQuery } from "../baseQuery";
|
||||
|
||||
export const authApi = createApi({
|
||||
reducerPath: "authApi",
|
||||
baseQuery: baseQuery,
|
||||
|
||||
tagTypes: ["profile", "Transaction"],
|
||||
|
||||
|
||||
endpoints: (builder) => ({
|
||||
// Login
|
||||
|
||||
login: builder.mutation({
|
||||
query: (credentials) => ({
|
||||
url: "/website/send-otp",
|
||||
method: "POST",
|
||||
body: credentials,
|
||||
}),
|
||||
}),
|
||||
|
||||
verifyOtp: builder.mutation({
|
||||
query: (credentials) => ({
|
||||
url: "/website/user/verify-otp",
|
||||
method: "POST",
|
||||
body: credentials,
|
||||
}),
|
||||
}),
|
||||
|
||||
register: builder.mutation({
|
||||
query: (credentials) => ({
|
||||
url: "/website/user/register",
|
||||
method: "POST",
|
||||
body: credentials,
|
||||
}),
|
||||
}),
|
||||
|
||||
logoutUser: builder.mutation({
|
||||
query: () => ({
|
||||
url: "/website/user/logout",
|
||||
method: "POST"
|
||||
})
|
||||
})
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useLoginMutation,
|
||||
useVerifyOtpMutation,
|
||||
useRegisterMutation,
|
||||
useLogoutUserMutation
|
||||
} = authApi;
|
||||
@@ -20,11 +20,28 @@ export const citiesApi = createApi({
|
||||
}),
|
||||
|
||||
getUpcomingCities: builder.query({
|
||||
|
||||
query: (listType) => `/cities/list/all?listType=${listType}`,
|
||||
|
||||
})
|
||||
}),
|
||||
|
||||
getSelectedCityDetails: builder.query({
|
||||
query: (cityId) => `/website/${cityId}`,
|
||||
}),
|
||||
|
||||
getSelectedCityOffers: builder.query({
|
||||
query: ({ cityId, categoryId, page, limit }) => {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
params.append('cityXid', cityId);
|
||||
|
||||
if (categoryId) params.append('categoryXid', categoryId);
|
||||
if (page) params.append('page', page);
|
||||
if (limit) params.append('limit', limit);
|
||||
|
||||
return `/website/super-savings/list/offers?${params.toString()}`;
|
||||
}
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetCityListWithBannerQuery,useGetUpcomingCitiesQuery } = citiesApi;
|
||||
export const { useGetCityListWithBannerQuery, useGetUpcomingCitiesQuery, useGetSelectedCityDetailsQuery, useGetSelectedCityOffersQuery } = citiesApi;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||
|
||||
export const fakeApi = createApi({
|
||||
reducerPath: 'fakeApi',
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: " https://fakestoreapi.com",
|
||||
|
||||
}),
|
||||
endpoints: (builder) => ({
|
||||
getProducts: builder.query<any, void>({
|
||||
query: () => ({
|
||||
url: 'products',
|
||||
method: 'GET',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export const { useGetProductsQuery} = fakeApi
|
||||
45
src/Redux/services/profile.service.ts
Normal file
45
src/Redux/services/profile.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import { baseQuery } from "../baseQuery";
|
||||
|
||||
export const profileApi = createApi({
|
||||
reducerPath: "profileApi",
|
||||
baseQuery,
|
||||
|
||||
tagTypes: ["userDetails"],
|
||||
|
||||
endpoints: (builder) => ({
|
||||
|
||||
getUserProfileDetails: builder.query({
|
||||
query: (id) => `/website/user/${id}`,
|
||||
providesTags: ["userDetails"]
|
||||
}),
|
||||
|
||||
updateUserProfileDetails: builder.mutation({
|
||||
query: ({ userDetails, userId }) => ({ // keep the name of the variables being passed here same as when calling the mutation hook
|
||||
url: `/website/user/${userId}`,
|
||||
method: "PUT",
|
||||
body: userDetails
|
||||
}),
|
||||
invalidatesTags: ["userDetails"]
|
||||
}),
|
||||
|
||||
getUserPasses: builder.query({
|
||||
query: ({ cardMode, sort }) => {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if(cardMode) params.append('cardMode',cardMode);
|
||||
if(sort) params.append('sort',sort);
|
||||
|
||||
return `/website/passes/all?${params.toString()}`
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetUserProfileDetailsQuery,
|
||||
useUpdateUserProfileDetailsMutation,
|
||||
useGetUserPassesQuery
|
||||
} = profileApi;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { motion } from 'motion/react';
|
||||
import BeforeLogin from '../imports/BeforeLogin';
|
||||
import AfterLogin from '../imports/AfterLogin';
|
||||
import BeforeLogin from './BeforeLogin';
|
||||
import AfterLogin from './AfterLogin';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Input } from './ui/input';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { useGetCityListWithBannerQuery } from '../Redux/services/cities.service';
|
||||
import LoadingSpinner from './LoadingSpinner';
|
||||
|
||||
interface City {
|
||||
id: number;
|
||||
@@ -20,6 +21,9 @@ interface CitySelectionDialogProps {
|
||||
onCitySelect?: (cityId: string) => void; // ✅ Updated to pass cityId
|
||||
}
|
||||
|
||||
export const slugify = (name: string | null) =>
|
||||
name?.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
export function CitySelectionDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -31,21 +35,16 @@ export function CitySelectionDialog({
|
||||
const { data: cities, isLoading } = useGetCityListWithBannerQuery({ search })
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
return (
|
||||
<LoadingSpinner/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const handleCityClick = (city: City) => {
|
||||
console.log('Selected city:', city.cityName);
|
||||
|
||||
// ✅ Call the onCitySelect callback if provided (passing cityId)
|
||||
if (onCitySelect) {
|
||||
onCitySelect(String(city.id));
|
||||
} else {
|
||||
// ✅ Default behavior: navigate to passes page
|
||||
navigate(`/passes?city=${encodeURIComponent(city.cityName)}`);
|
||||
}
|
||||
|
||||
navigate(`/${slugify(city.cityName)}`);
|
||||
localStorage.setItem("cityId", String(city.id))
|
||||
localStorage.setItem("cityName", String(city.cityName))
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export function LandingMagicItinerary() {
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative py-20 lg:py-32 overflow-hidden -mt-20 pt-32 z-[100]">
|
||||
<section className="relative py-20 lg:py-32 overflow-hidden -mt-20 pt-32 z-[49]">
|
||||
{/* Dynamic Background */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none z-[5]">
|
||||
{/* Background Image as fallback */}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
import LoadingSpinner from './LoadingSpinner';
|
||||
|
||||
// const upcomingCities = [
|
||||
// {
|
||||
@@ -113,8 +114,10 @@ export function LandingUpcomingCities() {
|
||||
|
||||
const { data, isLoading } = useGetUpcomingCitiesQuery(listType)
|
||||
|
||||
if(isLoading){
|
||||
return <div>Loading...</div>
|
||||
if (isLoading) {
|
||||
return (
|
||||
<LoadingSpinner/>
|
||||
);
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
|
||||
@@ -154,7 +154,7 @@ export function LandingVarietyOfAdventures() {
|
||||
<div className="container mx-auto px-4">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-16 max-w-4xl mx-auto">
|
||||
<motion.h2
|
||||
<motion.h2
|
||||
className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight text-foreground mb-6"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -164,13 +164,13 @@ export function LandingVarietyOfAdventures() {
|
||||
<span className="font-bold text-primary italic">Experience</span>{' '}
|
||||
<span className="font-light">for Every Traveller</span>
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
<motion.p
|
||||
className="font-poppins text-xl leading-relaxed font-normal text-gray-600"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
>
|
||||
From iconic laneways and world-class coffee to stunning gardens and vibrant markets,
|
||||
From iconic laneways and world-class coffee to stunning gardens and vibrant markets,
|
||||
discover Melbourne's unique character through curated experiences that showcase the city's soul.
|
||||
</motion.p>
|
||||
</div>
|
||||
@@ -180,7 +180,7 @@ export function LandingVarietyOfAdventures() {
|
||||
{/* Carousel Container - Full Width */}
|
||||
<div className="relative w-full overflow-hidden">
|
||||
{/* Scrolling Track */}
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="horizontal-scroll-track flex items-center gap-8 py-8"
|
||||
style={{
|
||||
width: 'max-content',
|
||||
@@ -212,10 +212,10 @@ export function LandingVarietyOfAdventures() {
|
||||
{/* Dark Overlay */}
|
||||
<div className="absolute inset-0 bg-black/30" />
|
||||
</div>
|
||||
|
||||
|
||||
{/* Bottom Content Card */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6">
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="bg-white/95 backdrop-blur-sm rounded-2xl p-4 border border-white/20"
|
||||
whileHover={{ y: -2 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
@@ -230,9 +230,9 @@ export function LandingVarietyOfAdventures() {
|
||||
{category.tourCount}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Icon */}
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="w-12 h-12 bg-warm-coral rounded-xl flex items-center justify-center flex-shrink-0"
|
||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
@@ -254,8 +254,8 @@ export function LandingVarietyOfAdventures() {
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: [0.25, 0.1, 0.25, 1],
|
||||
layout: { duration: 0.2 }
|
||||
}}
|
||||
@@ -283,8 +283,8 @@ export function LandingVarietyOfAdventures() {
|
||||
key={attraction.name}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay: idx * 0.1,
|
||||
ease: [0.25, 0.1, 0.25, 1]
|
||||
}}
|
||||
@@ -298,7 +298,7 @@ export function LandingVarietyOfAdventures() {
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Attraction Info */}
|
||||
<div className="flex-1">
|
||||
<h5 className="font-semibold text-gray-900 mb-1">
|
||||
@@ -332,19 +332,19 @@ export function LandingVarietyOfAdventures() {
|
||||
</motion.div>
|
||||
|
||||
{/* Gradient Fade Edges */}
|
||||
<div className="absolute left-0 top-0 bottom-0 w-32 bg-white/80 pointer-events-none z-10" />
|
||||
<div className="absolute right-0 top-0 bottom-0 w-32 bg-white/80 pointer-events-none z-10" />
|
||||
<div className="absolute left-0 top-0 bottom-0 w-32 bg-white pointer-events-none z-10" style={{ boxShadow: '47px 2px 40px 0px #fff' }} />
|
||||
<div className="absolute right-0 top-0 bottom-0 w-32 bg-white pointer-events-none z-10" style={{ boxShadow: '-47px 2px 40px 0px #fff' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Button */}
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="text-center"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.6, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
>
|
||||
<Button
|
||||
<Button
|
||||
withShine={true}
|
||||
size="xl"
|
||||
className="h-16 rounded-full text-lg px-8"
|
||||
|
||||
12
src/components/LoadingSpinner.tsx
Normal file
12
src/components/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
const LoadingSpinner = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#F95F62] mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoadingSpinner
|
||||
@@ -4,37 +4,47 @@ import { X } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useLoginMutation, useVerifyOtpMutation } from '../Redux/services/auth.service';
|
||||
import { toast } from 'sonner';
|
||||
import { RegisterModal } from './RegisterModal';
|
||||
|
||||
interface LoginModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
// onLoginSuccess: (userData: { email: string; name: string }) => void;
|
||||
}
|
||||
|
||||
export function LoginModal({ isOpen, onClose, }: LoginModalProps) {
|
||||
export function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
const [step, setStep] = useState<'email' | 'otp'>('email');
|
||||
const [email, setEmail] = useState('');
|
||||
const [otp, setOtp] = useState(['', '', '', '', '', '']);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [helperText, setHelperText] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [showRegisterModal, setShowRegisterModal] = useState(false);
|
||||
|
||||
const { login } = useAuth(); // from AuthContext
|
||||
const { login } = useAuth();
|
||||
|
||||
// Reset modal state when closed
|
||||
const [sendOtp, { isLoading: isSendingOtp }] = useLoginMutation();
|
||||
const [verifyOtp, { isLoading: isVerifying }] = useVerifyOtpMutation();
|
||||
|
||||
// Reset modal when closed
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setStep('email');
|
||||
setEmail('');
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
setCountdown(0);
|
||||
setHelperText('');
|
||||
resetModal();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Countdown timer for OTP resend
|
||||
const resetModal = () => {
|
||||
setStep('email');
|
||||
setEmail('');
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
setCountdown(0);
|
||||
setHelperText('');
|
||||
setError('');
|
||||
};
|
||||
|
||||
// Countdown timer
|
||||
useEffect(() => {
|
||||
if (countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||
@@ -42,67 +52,115 @@ export function LoginModal({ isOpen, onClose, }: LoginModalProps) {
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
const handleSendOTP = async () => {
|
||||
if (!email || !email.includes('@')) {
|
||||
setHelperText('Please enter a valid email address');
|
||||
return;
|
||||
// ==================== PASTE OTP FEATURE ====================
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
const pastedData = e.clipboardData.getData('text').trim();
|
||||
|
||||
// Extract only digits
|
||||
const digits = pastedData.replace(/\D/g, '').slice(0, 6);
|
||||
|
||||
if (digits.length === 0) return;
|
||||
|
||||
const newOtp = [...otp];
|
||||
|
||||
// Fill the OTP array with pasted digits
|
||||
for (let i = 0; i < digits.length; i++) {
|
||||
newOtp[i] = digits[i];
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setHelperText('');
|
||||
setOtp(newOtp);
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setStep('otp');
|
||||
setCountdown(120); // 2 minutes countdown
|
||||
setIsLoading(false);
|
||||
setHelperText('OTP sent successfully');
|
||||
}, 1500);
|
||||
// Auto-focus the next empty field or the last one
|
||||
const nextIndex = digits.length < 6 ? digits.length : 5;
|
||||
const nextInput = document.querySelector(
|
||||
`input[data-otp-index="${nextIndex}"]`
|
||||
) as HTMLInputElement;
|
||||
|
||||
nextInput?.focus();
|
||||
};
|
||||
|
||||
const handleOTPChange = (index: number, value: string) => {
|
||||
if (value.length > 1) return; // Only allow single digit
|
||||
if (value.length > 1) return;
|
||||
|
||||
const newOtp = [...otp];
|
||||
newOtp[index] = value;
|
||||
setOtp(newOtp);
|
||||
|
||||
// Auto-focus next input
|
||||
if (value && index < 5) {
|
||||
const nextInput = document.querySelector(`input[data-otp-index="${index + 1}"]`) as HTMLInputElement;
|
||||
const nextInput = document.querySelector(
|
||||
`input[data-otp-index="${index + 1}"]`
|
||||
) as HTMLInputElement;
|
||||
nextInput?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOTPKeyDown = (index: number, e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Backspace' && !otp[index] && index > 0) {
|
||||
const prevInput = document.querySelector(`input[data-otp-index="${index - 1}"]`) as HTMLInputElement;
|
||||
if (e.key === "Backspace" && !otp[index] && index > 0) {
|
||||
const prevInput = document.querySelector(
|
||||
`input[data-otp-index="${index - 1}"]`
|
||||
) as HTMLInputElement;
|
||||
prevInput?.focus();
|
||||
}
|
||||
|
||||
// ✅ Trigger verify on Enter if all 6 digits are filled
|
||||
if (e.key === "Enter") {
|
||||
const otpString = otp.join("");
|
||||
if (otpString.length === 6) {
|
||||
handleVerifyLogin();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Rest of your functions remain the same
|
||||
const handleSendOTP = async () => {
|
||||
if (!email || !email.includes('@')) {
|
||||
setError('Please enter a valid email address');
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setHelperText('');
|
||||
|
||||
try {
|
||||
await sendOtp({ emailAddress: email }).unwrap();
|
||||
setStep('otp');
|
||||
setCountdown(120);
|
||||
setHelperText('OTP sent successfully to your email');
|
||||
} catch (err: any) {
|
||||
setError(err?.data?.message || 'Failed to send OTP. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyLogin = async () => {
|
||||
const otpString = otp.join('');
|
||||
if (otpString.length !== 6) {
|
||||
setHelperText('Please enter complete OTP');
|
||||
setError('Please enter complete 6-digit OTP');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setHelperText('');
|
||||
setError('');
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
// Generate name from email for demo
|
||||
const emailParts = email.split('@')[0];
|
||||
const name = emailParts.charAt(0).toUpperCase() + emailParts.slice(1);
|
||||
try {
|
||||
const response = await verifyOtp({
|
||||
emailAddress: email,
|
||||
otp: otpString
|
||||
}).unwrap();
|
||||
|
||||
login({ email, name })
|
||||
const userData = {
|
||||
userId: response?.user?.id,
|
||||
email: response?.email || email,
|
||||
name: response?.name || email.split('@')[0].charAt(0).toUpperCase() + email.split('@')[0].slice(1),
|
||||
accessToken: response?.accessToken,
|
||||
};
|
||||
|
||||
setIsLoading(false);
|
||||
// navigate("/melbourne")
|
||||
login(userData);
|
||||
toast.success("User Logged in successfully")
|
||||
onClose();
|
||||
}, 1500);
|
||||
} catch (err: any) {
|
||||
setError(err?.data?.message || 'Invalid OTP. Please try again.');
|
||||
toast.error(err?.data?.message)
|
||||
}
|
||||
};
|
||||
|
||||
const formatCountdown = (seconds: number) => {
|
||||
@@ -112,177 +170,159 @@ export function LoginModal({ isOpen, onClose, }: LoginModalProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
className="fixed inset-0 flex items-center justify-center z-50 p-4"
|
||||
>
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md mx-auto overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="relative px-8 pt-8 pb-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
className="fixed inset-0 flex items-center justify-center z-50 p-4"
|
||||
>
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md mx-auto overflow-hidden">
|
||||
<div className="relative px-8 pt-8 pb-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
|
||||
<h2 className="font-merchant text-2xl font-semibold text-gray-900 mb-2">
|
||||
Login
|
||||
</h2>
|
||||
<p className="font-poppins text-sm text-gray-600">
|
||||
Enter your email Id and verify with OTP sent on it.
|
||||
</p>
|
||||
</div>
|
||||
<h2 className="font-merchant text-2xl font-semibold text-gray-900 mb-2">
|
||||
Login
|
||||
</h2>
|
||||
<p className="font-poppins text-sm text-gray-600">
|
||||
Enter your email and verify with OTP
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-8 pb-8">
|
||||
{step === 'email' ? (
|
||||
<div className="space-y-6">
|
||||
{/* Email Input */}
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</Label>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Name@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSendOTP()}
|
||||
/>
|
||||
{helperText && (
|
||||
<p className={`font-poppins text-xs ${helperText.includes('success') ? 'text-green-600' : 'text-red-500'}`}>
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Send OTP Button */}
|
||||
<Button
|
||||
onClick={handleSendOTP}
|
||||
disabled={isLoading}
|
||||
className="w-full h-12 bg-gray-800 hover:bg-gray-900 cursor-pointer text-white font-poppins font-semibold rounded-xl transition-colors"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
|
||||
Sending OTP...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
Send OTP
|
||||
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Email Display */}
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</Label>
|
||||
<div className="h-12 bg-gray-50 rounded-xl flex items-center px-4">
|
||||
<span className="font-poppins text-base text-gray-600">{email}</span>
|
||||
</div>
|
||||
{helperText && (
|
||||
<p className={`font-poppins text-xs ${helperText.includes('success') ? 'text-green-600' : 'text-red-500'}`}>
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* OTP Input */}
|
||||
<div className="space-y-3">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Enter OTP
|
||||
</Label>
|
||||
<div className="flex gap-3 justify-between">
|
||||
{otp.map((digit, index) => (
|
||||
<input
|
||||
key={index}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onChange={(e) => handleOTPChange(index, e.target.value.replace(/\D/g, ''))}
|
||||
onKeyDown={(e) => handleOTPKeyDown(index, e)}
|
||||
data-otp-index={index}
|
||||
className="w-12 h-12 text-center font-poppins font-semibold text-lg bg-gray-300 border-0 rounded-xl focus:bg-white focus:ring-2 focus:ring-primary focus:outline-none transition-colors"
|
||||
/>
|
||||
))}
|
||||
<div className="px-8 pb-8">
|
||||
{step === 'email' ? (
|
||||
// ... Email step (unchanged)
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Email Address
|
||||
</Label>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSendOTP()}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
{error && <p className="text-red-500 text-xs">{error}</p>}
|
||||
{helperText && <p className="text-green-600 text-xs">{helperText}</p>}
|
||||
</div>
|
||||
|
||||
{/* Countdown */}
|
||||
{countdown > 0 && (
|
||||
<p className="font-poppins text-xs text-gray-500 text-center">
|
||||
{formatCountdown(countdown)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Verify Button */}
|
||||
<Button
|
||||
onClick={handleVerifyLogin}
|
||||
disabled={isLoading || otp.join('').length !== 6}
|
||||
className="w-full h-12 bg-gray-800 hover:bg-gray-900 cursor-pointer text-white font-poppins font-semibold rounded-xl transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
|
||||
Verifying...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
Verify and Login
|
||||
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Resend OTP */}
|
||||
{countdown === 0 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setStep('email');
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
}}
|
||||
className="w-full font-poppins text-sm text-gray-600 hover:text-gray-800 transition-colors"
|
||||
<Button
|
||||
onClick={handleSendOTP}
|
||||
disabled={isSendingOtp}
|
||||
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl cursor-pointer"
|
||||
>
|
||||
Didn't receive OTP? Send again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isSendingOtp ? 'Sending OTP...' : 'Send OTP'}
|
||||
</Button>
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => setShowRegisterModal(true)}
|
||||
className="font-poppins text-sm text-gray-600 hover:text-gray-800 transition-colors cursor-pointer"
|
||||
>
|
||||
Don't have an account? <span className="text-primary font-semibold">Register</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Email Display */}
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">Email</Label>
|
||||
<div className="h-12 bg-gray-50 rounded-xl flex items-center px-4 font-poppins text-base text-gray-600">
|
||||
{email}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OTP Inputs with Paste Support */}
|
||||
<div className="space-y-3">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Enter OTP
|
||||
</Label>
|
||||
<div className="flex gap-3 justify-between">
|
||||
{otp.map((digit, index) => (
|
||||
<input
|
||||
key={index}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onChange={(e) => handleOTPChange(index, e.target.value.replace(/\D/g, ''))}
|
||||
onKeyDown={(e) => handleOTPKeyDown(index, e)}
|
||||
onPaste={handlePaste} // ← Paste support added here
|
||||
data-otp-index={index}
|
||||
className="w-12 h-12 text-center font-poppins font-semibold text-lg bg-gray-300 border-0 rounded-xl focus:bg-white focus:ring-2 focus:ring-gray-800 focus:outline-none transition-all"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{countdown > 0 && (
|
||||
<p className="text-center text-xs text-gray-500">
|
||||
Resend OTP in {formatCountdown(countdown)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <p className="text-red-500 text-xs text-center">{error}</p>}
|
||||
|
||||
<Button
|
||||
onClick={handleVerifyLogin}
|
||||
disabled={isVerifying || otp.join('').length !== 6}
|
||||
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
{isVerifying ? 'Verifying...' : 'Verify & Login'}
|
||||
</Button>
|
||||
|
||||
{countdown === 0 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setStep('email');
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
setHelperText("")
|
||||
setError('');
|
||||
}}
|
||||
className="w-full text-sm text-gray-600 hover:text-gray-800 font-poppins cursor-pointer"
|
||||
>
|
||||
Didn't receive OTP?
|
||||
<span className="text-primary font-semibold"> Send again</span>
|
||||
{/* Send again */}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</AnimatePresence >
|
||||
<RegisterModal
|
||||
isOpen={showRegisterModal}
|
||||
onClose={() => setShowRegisterModal(false)}
|
||||
onLoginClick={() => {
|
||||
setShowRegisterModal(false);
|
||||
setStep('email');
|
||||
setEmail('');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +1,54 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Check, X, Star, Users, MapPin, Calendar, Clock, Zap, Eye } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { motion } from 'motion/react';
|
||||
|
||||
const cardOptions = [
|
||||
{
|
||||
id: 'selective',
|
||||
name: 'Flexi Card',
|
||||
subtitle: 'Pick 5-10 things to do from a choice of 102 attractions tours and activities',
|
||||
priceRange: '$89-159',
|
||||
duration: '3-7 days',
|
||||
popular: false,
|
||||
color: 'from-blue-500 to-cyan-500',
|
||||
features: {
|
||||
passCategory: 'Selective Card',
|
||||
accessToAttractions: true,
|
||||
entryToAttractions: true,
|
||||
accessToExperiences: true,
|
||||
entryToSites: true,
|
||||
accessToVenues: false,
|
||||
entryToEvents: 'Pass Category',
|
||||
accessToLocations: 'Pass Category',
|
||||
entryToActivities: true,
|
||||
accessToExhibits: true,
|
||||
entryToActivitiesSecond: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'unlimited',
|
||||
name: 'Melbourne Unlimited Card',
|
||||
subtitle: 'Pick 5-30 things to do from a choice of 102 attractions tours and activities',
|
||||
priceRange: '$159-299',
|
||||
duration: '3-7 days',
|
||||
popular: true,
|
||||
color: 'from-purple-500 to-pink-500',
|
||||
features: {
|
||||
passCategory: 'Pass Category',
|
||||
accessToAttractions: true,
|
||||
entryToAttractions: true,
|
||||
accessToExperiences: true,
|
||||
entryToSites: true,
|
||||
accessToVenues: true,
|
||||
entryToEvents: 'Pass Category',
|
||||
accessToLocations: 'Pass Category',
|
||||
entryToActivities: true,
|
||||
accessToExhibits: true,
|
||||
entryToActivitiesSecond: true
|
||||
}
|
||||
}
|
||||
];
|
||||
// const cardOptions = [
|
||||
// {
|
||||
// id: 'selective',
|
||||
// name: 'Flexi Card',
|
||||
// subtitle: 'Pick 5-10 things to do from a choice of 102 attractions tours and activities',
|
||||
// priceRange: '$89-159',
|
||||
// duration: '3-7 days',
|
||||
// popular: false,
|
||||
// color: 'from-blue-500 to-cyan-500',
|
||||
// features: {
|
||||
// passCategory: 'Selective Card',
|
||||
// accessToAttractions: true,
|
||||
// entryToAttractions: true,
|
||||
// accessToExperiences: true,
|
||||
// entryToSites: true,
|
||||
// accessToVenues: false,
|
||||
// entryToEvents: 'Pass Category',
|
||||
// accessToLocations: 'Pass Category',
|
||||
// entryToActivities: true,
|
||||
// accessToExhibits: true,
|
||||
// entryToActivitiesSecond: true
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// id: 'unlimited',
|
||||
// name: 'Melbourne Unlimited Card',
|
||||
// subtitle: 'Pick 5-30 things to do from a choice of 102 attractions tours and activities',
|
||||
// priceRange: '$159-299',
|
||||
// duration: '3-7 days',
|
||||
// popular: true,
|
||||
// color: 'from-purple-500 to-pink-500',
|
||||
// features: {
|
||||
// passCategory: 'Pass Category',
|
||||
// accessToAttractions: true,
|
||||
// entryToAttractions: true,
|
||||
// accessToExperiences: true,
|
||||
// entryToSites: true,
|
||||
// accessToVenues: true,
|
||||
// entryToEvents: 'Pass Category',
|
||||
// accessToLocations: 'Pass Category',
|
||||
// entryToActivities: true,
|
||||
// accessToExhibits: true,
|
||||
// entryToActivitiesSecond: true
|
||||
// }
|
||||
// }
|
||||
// ];
|
||||
|
||||
const features = [
|
||||
{ key: 'passCategory', label: 'Pass Category', icon: Star },
|
||||
@@ -71,11 +71,59 @@ const FeatureIcon = ({ feature }: { feature: typeof features[0] }) => {
|
||||
|
||||
interface MelbourneCardComparisonProps {
|
||||
onCheckoutClick?: () => void;
|
||||
cards: any[]
|
||||
}
|
||||
|
||||
export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardComparisonProps) {
|
||||
export function MelbourneCardComparison({ onCheckoutClick, cards }: MelbourneCardComparisonProps) {
|
||||
const [selectedCard, setSelectedCard] = useState<string>('unlimited');
|
||||
|
||||
const cardOptions = [
|
||||
{
|
||||
id: cards[0]?.id,
|
||||
name: cards[0]?.title,
|
||||
subtitle: cards[0]?.description,
|
||||
priceRange: `$${cards[0]?.adultPrice}`,
|
||||
duration: '3-7 days',
|
||||
popular: false,
|
||||
color: 'from-blue-500 to-cyan-500',
|
||||
features: {
|
||||
passCategory: 'Selective Card',
|
||||
accessToAttractions: true,
|
||||
entryToAttractions: true,
|
||||
accessToExperiences: true,
|
||||
entryToSites: true,
|
||||
accessToVenues: false,
|
||||
entryToEvents: 'Pass Category',
|
||||
accessToLocations: 'Pass Category',
|
||||
entryToActivities: true,
|
||||
accessToExhibits: true,
|
||||
entryToActivitiesSecond: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: cards[1]?.id,
|
||||
name: cards[1]?.title,
|
||||
subtitle: cards[1]?.description,
|
||||
priceRange: `$${cards[1]?.adultPrice}`,
|
||||
duration: '3-7 days',
|
||||
popular: true,
|
||||
color: 'from-purple-500 to-pink-500',
|
||||
features: {
|
||||
passCategory: 'Pass Category',
|
||||
accessToAttractions: true,
|
||||
entryToAttractions: true,
|
||||
accessToExperiences: true,
|
||||
entryToSites: true,
|
||||
accessToVenues: true,
|
||||
entryToEvents: 'Pass Category',
|
||||
accessToLocations: 'Pass Category',
|
||||
entryToActivities: true,
|
||||
accessToExhibits: true,
|
||||
entryToActivitiesSecond: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const renderFeatureValue = (value: boolean | string, cardId: string) => {
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? (
|
||||
@@ -92,7 +140,7 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="text-center text-sm text-gray-600 px-2">
|
||||
{value}
|
||||
@@ -122,17 +170,17 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
|
||||
Choose Your Adventure
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<h2 className="font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-6">
|
||||
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic pr-2">
|
||||
Buy
|
||||
</span>{' '}
|
||||
<span className="font-normal">Now</span>
|
||||
</h2>
|
||||
|
||||
|
||||
<p className="text-xl text-gray-600 max-w-4xl mx-auto leading-relaxed">
|
||||
Melbourne is a must-visit cultural epicenter, and this spectacular trip unlocks
|
||||
your access around the city in one easy. Save over the cost of visiting Melbourne's
|
||||
Melbourne is a must-visit cultural epicenter, and this spectacular trip unlocks
|
||||
your access around the city in one easy. Save over the cost of visiting Melbourne's
|
||||
landmarks, have lunch at Phi Phi Leh, snorkel at Bamboo Island, and visit Monkey Beach.
|
||||
</p>
|
||||
</motion.div>
|
||||
@@ -151,8 +199,8 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
|
||||
<div className="font-poppins text-xl font-semibold text-gray-900">
|
||||
Features
|
||||
</div>
|
||||
{cardOptions.map((card) => (
|
||||
<div key={card.id} className="text-center">
|
||||
{cardOptions.map((card,index) => (
|
||||
<div key={card.id ?? index} className="text-center">
|
||||
<div className="mb-2">
|
||||
<div className="font-poppins font-semibold text-2xl" style={{ color: '#F95F62' }}>{card.name}</div>
|
||||
</div>
|
||||
@@ -179,9 +227,9 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
|
||||
<FeatureIcon feature={feature} />
|
||||
<span className="font-medium text-gray-900">{feature.label}</span>
|
||||
</div>
|
||||
|
||||
{cardOptions.map((card) => (
|
||||
<div key={card.id} className="text-center">
|
||||
|
||||
{cardOptions.map((card, index) => (
|
||||
<div key={card.id ?? index} className="text-center">
|
||||
{renderFeatureValue(card.features[feature.key as keyof typeof card.features], card.id)}
|
||||
</div>
|
||||
))}
|
||||
@@ -196,16 +244,16 @@ export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardCompar
|
||||
<div className="font-medium text-gray-600 text-sm mb-2">Ready to explore?</div>
|
||||
<div className="text-xs text-gray-500">Compare features above</div>
|
||||
</div>
|
||||
|
||||
{cardOptions.map((card) => (
|
||||
<motion.div key={card.id} className="text-center">
|
||||
|
||||
{cardOptions.map((card,index) => (
|
||||
<motion.div key={card.id ?? index} className="text-center">
|
||||
<div className="mb-4">
|
||||
<div className="text-3xl font-bold text-gray-900">{card.priceRange}</div>
|
||||
<div className="text-sm text-gray-600">{card.duration}</div>
|
||||
</div>
|
||||
<Button
|
||||
withShine={true}
|
||||
className="w-full h-14 rounded-2xl text-white font-semibold text-lg hover:scale-105 transition-all duration-300 shadow-lg hover:shadow-xl"
|
||||
className="w-full h-14 rounded-2xl text-white font-semibold text-lg hover:scale-105 transition-all duration-300 shadow-lg hover:shadow-xl cursor-pointer"
|
||||
style={{ backgroundColor: '#F95F62' }}
|
||||
onClick={onCheckoutClick}
|
||||
>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { CTAButton } from './CTAButton';
|
||||
import logoImage from '../assets/cit-logo.png';
|
||||
import melbourneLogo from '../assets/melbourne-logo.png';
|
||||
import { CitySelectionDialog } from './CitySelectionDialog';
|
||||
import { CitySelectionDialog, slugify } from './CitySelectionDialog';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { LoginModal } from './LoginModal';
|
||||
|
||||
@@ -89,7 +89,15 @@ export default function Navbar({
|
||||
const [lastKnownCity, setLastKnownCity] = useState<'landing' | 'melbourne'>('landing');
|
||||
const [isLoginOpen, setLoginOpen] = useState(false);
|
||||
|
||||
const { user, login, logout } = useAuth(); // from AuthContext
|
||||
const { user, logout } = useAuth(); // from AuthContext
|
||||
|
||||
const cityLogo = sessionStorage.getItem("cityLogo")
|
||||
const cityId = localStorage.getItem("cityId")
|
||||
const cityName = localStorage.getItem("cityName")
|
||||
|
||||
const citySelected = location.pathname.includes(slugify(cityName) || "")
|
||||
|
||||
const baseUrl = import.meta.env.VITE_BASE_URL;
|
||||
|
||||
const protectedPaths = ["/passes", "/whats-included", "/", "/melbourne"];
|
||||
|
||||
@@ -129,50 +137,50 @@ export default function Navbar({
|
||||
isShared: false
|
||||
},
|
||||
// Position 4 - Shared item
|
||||
{
|
||||
label: 'Your Card',
|
||||
path: '/passes',
|
||||
isShared: true,
|
||||
landingLabel: 'Your Card',
|
||||
melbourneLabel: 'Your Card'
|
||||
},
|
||||
// {
|
||||
// label: 'Your Card',
|
||||
// path: '/passes',
|
||||
// isShared: true,
|
||||
// landingLabel: 'Your Card',
|
||||
// melbourneLabel: 'Your Card'
|
||||
// },
|
||||
// Position 5
|
||||
{
|
||||
label: 'FAQ',
|
||||
path: '/faq',
|
||||
isShared: false
|
||||
},
|
||||
{
|
||||
label: 'Your PostCard',
|
||||
{
|
||||
label: 'Your Postcard',
|
||||
path: '/postcards',
|
||||
isShared: true,
|
||||
landingLabel: 'Your PostCard',
|
||||
melbourneLabel: 'Your PostCard'
|
||||
landingLabel: 'Your Postcard',
|
||||
melbourneLabel: 'Your Postcard'
|
||||
}
|
||||
],
|
||||
melbourne: [
|
||||
// Position 1
|
||||
{
|
||||
label: 'Attractions',
|
||||
path: '/attractions',
|
||||
path: `/attractions`,
|
||||
isShared: false
|
||||
},
|
||||
// Position 2
|
||||
{
|
||||
label: 'Magic Itinerary',
|
||||
path: '/magic-itinerary',
|
||||
path: `/magic-itinerary`,
|
||||
isShared: false
|
||||
},
|
||||
// Position 3
|
||||
{
|
||||
label: 'Super Savings',
|
||||
path: '/super-savings',
|
||||
path: `/super-savings`,
|
||||
isShared: false
|
||||
},
|
||||
// Position 4 - Shared item
|
||||
{
|
||||
label: 'How It Works',
|
||||
path: '/how-it-works',
|
||||
path: `/how-it-works`,
|
||||
isShared: true,
|
||||
landingLabel: 'Discover',
|
||||
melbourneLabel: 'How It Works'
|
||||
@@ -180,24 +188,21 @@ export default function Navbar({
|
||||
// Position 5 - Shared item
|
||||
{
|
||||
label: 'Your Card',
|
||||
path: '/passes',
|
||||
path: `/passes`,
|
||||
isShared: true,
|
||||
landingLabel: 'Your Card',
|
||||
melbourneLabel: 'Your Card'
|
||||
},
|
||||
{
|
||||
label: 'Your PostCard',
|
||||
path: '/postcards',
|
||||
{
|
||||
label: 'Your Postcard',
|
||||
path: `/postcards`,
|
||||
isShared: true,
|
||||
landingLabel: 'Your PostCard',
|
||||
melbourneLabel: 'Your PostCard'
|
||||
landingLabel: 'Your Postcard',
|
||||
melbourneLabel: 'Your Postcard'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Check if we're on landing page
|
||||
const isLandingPage = location.pathname === '/';
|
||||
|
||||
// Restore from session on mount
|
||||
useEffect(() => {
|
||||
const savedCity = sessionStorage.getItem('lastKnownCity');
|
||||
@@ -299,11 +304,11 @@ export default function Navbar({
|
||||
setDialogSource('navbar');
|
||||
};
|
||||
|
||||
const handleCitySelectFromNavbar = (cityId: string) => {
|
||||
console.log('City selected from navbar:', cityId);
|
||||
onCityChange(cityId);
|
||||
const handleCitySelectFromNavbar = (cityName: string) => {
|
||||
console.log('City selected from navbar:', cityName);
|
||||
onCityChange(cityName);
|
||||
|
||||
if (cityId.toLowerCase() === '1') {
|
||||
if (cityName.toLowerCase() === 'melbourne') {
|
||||
setNavigationSource('melbourne');
|
||||
navigate('/melbourne');
|
||||
} else {
|
||||
@@ -334,11 +339,11 @@ export default function Navbar({
|
||||
handleCloseCityDialog();
|
||||
};
|
||||
|
||||
const handleCitySelect = (cityId: string) => {
|
||||
const handleCitySelect = (cityName: string) => {
|
||||
if (dialogSource === 'cta') {
|
||||
handleCitySelectFromCTA(cityId);
|
||||
handleCitySelectFromCTA(cityName);
|
||||
} else {
|
||||
handleCitySelectFromNavbar(cityId);
|
||||
handleCitySelectFromNavbar(cityName);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -533,7 +538,7 @@ export default function Navbar({
|
||||
<>
|
||||
{/* Desktop Navbar - Enhanced Glassmorphism */}
|
||||
<motion.nav
|
||||
className="fixed top-0 left-0 right-0 z-50 hidden lg:block"
|
||||
className="fixed -top-1 left-0 right-0 z-50 hidden lg:block"
|
||||
initial={{ y: -100, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.6, ease: [0.25, 0.1, 0.25, 1], delay: 0.2 }}
|
||||
@@ -559,10 +564,9 @@ export default function Navbar({
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Link to={currentSource === 'melbourne' ? '/melbourne' : '/'}>
|
||||
<Link to={citySelected ? `/${slugify(cityName)}` : '/'}>
|
||||
<ImageWithFallback
|
||||
src={currentSource === 'melbourne' ? melbourneLogo : logoImage}
|
||||
alt={
|
||||
src={citySelected ? `${baseUrl}${cityLogo}` : logoImage} alt={
|
||||
currentSource === 'melbourne'
|
||||
? 'Melbourne CityCards Logo'
|
||||
: 'CityCards Logo'
|
||||
@@ -573,7 +577,7 @@ export default function Navbar({
|
||||
</motion.div>
|
||||
|
||||
<div className="absolute -translate-x-1/2 flex items-center gap-5"
|
||||
style={{ left: '45%', }}
|
||||
style={{ left: '42%', }}
|
||||
>
|
||||
{/* Enhanced Navigation Items with source tracking */}
|
||||
{navigationItems.map((item) => {
|
||||
@@ -623,12 +627,13 @@ export default function Navbar({
|
||||
onClick={handleOpenCityDialogFromNavbar}
|
||||
>
|
||||
<span>
|
||||
{!activeCity || activeCity === 'shared'
|
||||
{/* {!activeCity || activeCity === 'shared'
|
||||
? 'City'
|
||||
: ['landing', 'landingpage'].includes(activeCity.toLowerCase())
|
||||
? 'City'
|
||||
: activeCity.charAt(0).toUpperCase() + activeCity.slice(1)
|
||||
}
|
||||
} */}
|
||||
{citySelected ? cityName : "City"}
|
||||
</span>
|
||||
|
||||
<ChevronDown className="w-3.5 h-3.5" />
|
||||
@@ -685,7 +690,7 @@ export default function Navbar({
|
||||
label: 'My Profile',
|
||||
icon: <User className="w-4 h-4" />,
|
||||
action: () => {
|
||||
navigate('/profile');
|
||||
navigate(citySelected?`/${slugify(cityName)}/profile`:'/profile');
|
||||
setActiveUserDropdown(false);
|
||||
}
|
||||
},
|
||||
@@ -784,10 +789,11 @@ export default function Navbar({
|
||||
trigger={
|
||||
<div className="flex items-center space-x-1 text-gray-700 hover:text-gray-900 text-sm font-medium transition-colors duration-200 cursor-pointer rounded-lg hover:bg-gray-50/50 px-2 py-1">
|
||||
<span>
|
||||
{activeCity && activeCity !== 'shared' ?
|
||||
{/* {activeCity && activeCity !== 'shared' ?
|
||||
activeCity.charAt(0).toUpperCase() + activeCity.slice(1) :
|
||||
currentSource === 'melbourne' ? 'Melbourne' : 'Select City'
|
||||
}
|
||||
} */}
|
||||
{cityName ? cityName : "City"}
|
||||
</span>
|
||||
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${activeCityDropdown ? 'rotate-180' : ''}`} />
|
||||
</div>
|
||||
|
||||
391
src/components/RegisterModal.tsx
Normal file
391
src/components/RegisterModal.tsx
Normal file
@@ -0,0 +1,391 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { X } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { useRegisterMutation } from '../Redux/services/auth.service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface RegisterModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onLoginClick: () => void;
|
||||
}
|
||||
|
||||
export function RegisterModal({ isOpen, onClose, onLoginClick }: RegisterModalProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
emailAddress: '',
|
||||
isdCode: '+91',
|
||||
mobileNumber: '',
|
||||
address1: '',
|
||||
address2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
country: 'Australia',
|
||||
postalCode: ''
|
||||
});
|
||||
const [helperText, setHelperText] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [register, { isLoading: isRegistering }] = useRegisterMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setFormData({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
emailAddress: '',
|
||||
isdCode: '+91',
|
||||
mobileNumber: '',
|
||||
address1: '',
|
||||
address2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
country: 'Australia',
|
||||
postalCode: ''
|
||||
});
|
||||
setHelperText('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
if (!formData.firstName.trim()) {
|
||||
toast.error('First name is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.lastName.trim()) {
|
||||
toast.error('Last name is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.emailAddress.trim() || !formData.emailAddress.includes('@')) {
|
||||
toast.error('Please enter a valid email address');
|
||||
return false;
|
||||
}
|
||||
if (!formData.mobileNumber.trim()) {
|
||||
toast.error('Mobile number is required');
|
||||
return false;
|
||||
}
|
||||
if (!/^\d+$/.test(formData.mobileNumber.trim())) {
|
||||
toast.error('Mobile number must contain only digits');
|
||||
return false;
|
||||
}
|
||||
if (!formData.address1.trim()) {
|
||||
toast.error('Address is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.city.trim()) {
|
||||
toast.error('City is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.state.trim()) {
|
||||
toast.error('State is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.postalCode.trim()) {
|
||||
toast.error('Postal code is required');
|
||||
return false;
|
||||
}
|
||||
if (!/^\d+$/.test(formData.postalCode.trim())) {
|
||||
toast.error('Postal code must contain only digits');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHelperText('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await register(formData).unwrap();
|
||||
console.log('Registration response:', response);
|
||||
|
||||
toast.success('Registration successful! Please login.');
|
||||
setTimeout(() => {
|
||||
onLoginClick();
|
||||
onClose();
|
||||
}, 2000);
|
||||
} catch (error: any) {
|
||||
console.error('Registration error:', error);
|
||||
const errorMessage = error?.data?.message || 'Registration failed. Please try again.';
|
||||
toast.error(errorMessage);
|
||||
setHelperText(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleRegister();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
className="fixed inset-0 flex items-center justify-center z-50 p-4 overflow-y-auto"
|
||||
>
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl mx-auto overflow-hidden max-h-[90vh] overflow-y-auto">
|
||||
<div className="relative px-8 pt-8 pb-4 top-0 bg-white z-10">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
|
||||
<h2 className="font-merchant text-2xl font-semibold text-gray-900 mb-2">
|
||||
Create Account
|
||||
</h2>
|
||||
<p className="font-poppins text-sm text-gray-600">
|
||||
Register to get started with City Cards
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="px-8 pb-8">
|
||||
<div className="space-y-6">
|
||||
{/* Personal Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-poppins text-base font-semibold text-gray-800">Personal Information</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
First Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter your first name"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Last Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter your last name"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Email Address <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter your email address"
|
||||
value={formData.emailAddress}
|
||||
onChange={(e) => handleInputChange('emailAddress', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
ISD Code
|
||||
</Label>
|
||||
<Select value={formData.isdCode} onValueChange={(value: any) => handleInputChange('isdCode', value)}>
|
||||
<SelectTrigger className="h-12 bg-gray-50 border-0 rounded-xl cursor-pointer">
|
||||
<SelectValue placeholder="Select code" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="+1">+1 (USA)</SelectItem>
|
||||
<SelectItem value="+44">+44 (UK)</SelectItem>
|
||||
<SelectItem value="+61">+61 (Australia)</SelectItem>
|
||||
<SelectItem value="+91">+91 (India)</SelectItem>
|
||||
<SelectItem value="+86">+86 (China)</SelectItem>
|
||||
<SelectItem value="+81">+81 (Japan)</SelectItem>
|
||||
<SelectItem value="+49">+49 (Germany)</SelectItem>
|
||||
<SelectItem value="+33">+33 (France)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Mobile Number <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="Enter your mobile number"
|
||||
value={formData.mobileNumber}
|
||||
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Address Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-poppins text-base font-semibold text-gray-800">Address Information</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Address Line 1 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter street address"
|
||||
value={formData.address1}
|
||||
onChange={(e) => handleInputChange('address1', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Address Line 2
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter apartment, suite, unit (optional)"
|
||||
value={formData.address2}
|
||||
onChange={(e) => handleInputChange('address2', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
City <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter city name"
|
||||
value={formData.city}
|
||||
onChange={(e) => handleInputChange('city', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
State <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter state name"
|
||||
value={formData.state}
|
||||
onChange={(e) => handleInputChange('state', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Country <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select value={formData.country} onValueChange={(value: any) => handleInputChange('country', value)}>
|
||||
<SelectTrigger className="h-12 bg-gray-50 border-0 rounded-xl cursor-pointer">
|
||||
<SelectValue placeholder="Select country" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Australia">Australia</SelectItem>
|
||||
<SelectItem value="United States">United States</SelectItem>
|
||||
<SelectItem value="United Kingdom">United Kingdom</SelectItem>
|
||||
<SelectItem value="Canada">Canada</SelectItem>
|
||||
<SelectItem value="India">India</SelectItem>
|
||||
<SelectItem value="Germany">Germany</SelectItem>
|
||||
<SelectItem value="France">France</SelectItem>
|
||||
<SelectItem value="Japan">Japan</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Postal Code <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter postal code"
|
||||
value={formData.postalCode}
|
||||
onChange={(e) => handleInputChange('postalCode', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{helperText && (
|
||||
<p className={`font-poppins text-xs ${helperText.includes('successful') ? 'text-green-600' : 'text-red-500'}`}>
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleRegister}
|
||||
disabled={isLoading || isRegistering}
|
||||
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl transition-colors cursor-pointer"
|
||||
>
|
||||
{isLoading || isRegistering ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
|
||||
Creating Account...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
Register
|
||||
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
onLoginClick();
|
||||
onClose();
|
||||
}}
|
||||
className="font-poppins text-sm text-gray-600 hover:text-gray-800 transition-colors cursor-pointer"
|
||||
>
|
||||
Already have an account? <span className="text-primary font-semibold">Login</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -1,745 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Search, Filter, Star, MapPin, Clock, Tag, Heart, Share2, ChevronDown, ChevronRight, Check, Hotel, Plane, Building2, MapPinned, Home, Gift, Percent } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Separator } from './ui/separator';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { TrustSection } from './TrustSection';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
import { TrustedCompanies } from './TrustedCompanies';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface SuperSavingsPageProps {
|
||||
onBackClick: () => void;
|
||||
onHomeClick: () => void;
|
||||
onMelbourneClick: () => void;
|
||||
onPassesClick: () => void;
|
||||
onCheckoutClick: () => void;
|
||||
onSignInClick: () => void;
|
||||
onSignOutClick: () => void;
|
||||
onAttractionsClick: () => void;
|
||||
onBlogsClick: () => void;
|
||||
onHowItWorksClick: () => void;
|
||||
onFAQClick: () => void;
|
||||
onPrivacyPolicyClick: () => void;
|
||||
onAboutUsClick: () => void;
|
||||
onProfileClick: () => void;
|
||||
onCityCardsClick: () => void;
|
||||
onMagicItineraryClick: () => void;
|
||||
onPostCardsClick: () => void;
|
||||
onOffersClick: () => void;
|
||||
onSuperSavingsClick: () => void;
|
||||
onContactUsClick?: () => void;
|
||||
onEsimsClick?: () => void;
|
||||
onHotelDiscountsClick?: () => void;
|
||||
fromSource?: 'products' | 'passes';
|
||||
currentPage: string;
|
||||
user?: { email: string; name: string; } | null;
|
||||
}
|
||||
|
||||
// Mock super savings data
|
||||
const savingsData = [
|
||||
{
|
||||
id: '1',
|
||||
business: 'Grand Hotels Melbourne',
|
||||
title: 'Up to 50% Off on luxury hotel stays across Melbourne',
|
||||
discount: '50% OFF',
|
||||
savedAmount: 'Save up to $300',
|
||||
image: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=400',
|
||||
category: 'hotels'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
business: 'Adventure Tours',
|
||||
title: '40% Off on guided adventure tours and experiences',
|
||||
discount: '40% OFF',
|
||||
savedAmount: 'Save up to $150',
|
||||
image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=400',
|
||||
category: 'tours'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
business: 'Premium Spa & Wellness',
|
||||
title: '45% Off on spa packages and wellness treatments',
|
||||
discount: '45% OFF',
|
||||
savedAmount: 'Save up to $200',
|
||||
image: 'https://images.unsplash.com/photo-1544161515-4ab6ce6db874?w=400',
|
||||
category: 'wellness'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
business: 'Culinary Delights',
|
||||
title: '35% Off on fine dining at Michelin-starred restaurants',
|
||||
discount: '35% OFF',
|
||||
savedAmount: 'Save up to $120',
|
||||
image: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400',
|
||||
category: 'dining'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
business: 'Entertainment Pass',
|
||||
title: '60% Off on theater shows and concert tickets',
|
||||
discount: '60% OFF',
|
||||
savedAmount: 'Save up to $250',
|
||||
image: 'https://images.unsplash.com/photo-1514306191717-452ec28c7814?w=400',
|
||||
category: 'entertainment'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
business: 'Museum Pass',
|
||||
title: '55% Off on museum entries and special exhibitions',
|
||||
discount: '55% OFF',
|
||||
savedAmount: 'Save up to $180',
|
||||
image: 'https://images.unsplash.com/photo-1566127992631-137a642a90f4?w=400',
|
||||
category: 'museums'
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
business: 'Luxury Shopping',
|
||||
title: '30% Off on designer boutiques and luxury shopping',
|
||||
discount: '30% OFF',
|
||||
savedAmount: 'Save up to $500',
|
||||
image: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=400',
|
||||
category: 'shopping'
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
business: 'Water Sports',
|
||||
title: '45% Off on water sports and beach activities',
|
||||
discount: '45% OFF',
|
||||
savedAmount: 'Save up to $175',
|
||||
image: 'https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=400',
|
||||
category: 'sports'
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
business: 'Wine Tasting Tours',
|
||||
title: '40% Off on wine country tours and tastings',
|
||||
discount: '40% OFF',
|
||||
savedAmount: 'Save up to $160',
|
||||
image: 'https://images.unsplash.com/photo-1506377247377-2a5b3b417ebb?w=400',
|
||||
category: 'tours'
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
business: 'Family Fun Parks',
|
||||
title: '50% Off on family entertainment and theme parks',
|
||||
discount: '50% OFF',
|
||||
savedAmount: 'Save up to $220',
|
||||
image: 'https://images.unsplash.com/photo-1524850011238-e3d235c7d4c9?w=400',
|
||||
category: 'entertainment'
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
business: 'Boutique Stays',
|
||||
title: '55% Off on boutique hotels and bed & breakfasts',
|
||||
discount: '55% OFF',
|
||||
savedAmount: 'Save up to $280',
|
||||
image: 'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=400',
|
||||
category: 'hotels'
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
business: 'Art Galleries',
|
||||
title: '35% Off on contemporary art galleries and workshops',
|
||||
discount: '35% OFF',
|
||||
savedAmount: 'Save up to $140',
|
||||
image: 'https://images.unsplash.com/photo-1561214115-f2f134cc4912?w=400',
|
||||
category: 'museums'
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
business: 'Luxury Cruises',
|
||||
title: '65% Off on harbor cruises and yacht experiences',
|
||||
discount: '65% OFF',
|
||||
savedAmount: 'Save up to $400',
|
||||
image: 'https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=400',
|
||||
category: 'tours'
|
||||
}
|
||||
];
|
||||
|
||||
const filterCategories = [
|
||||
{ value: 'hotels', label: 'Hotels', count: 2 },
|
||||
{ value: 'tours', label: 'Tours', count: 3 },
|
||||
{ value: 'wellness', label: 'Wellness', count: 1 },
|
||||
{ value: 'dining', label: 'Dining', count: 1 },
|
||||
{ value: 'entertainment', label: 'Entertainment', count: 2 },
|
||||
{ value: 'museums', label: 'Museums', count: 2 },
|
||||
{ value: 'shopping', label: 'Shopping', count: 1 },
|
||||
{ value: 'sports', label: 'Sports', count: 1 }
|
||||
];
|
||||
|
||||
// Categories data for the Super Savings Categories section
|
||||
const categoriesData = [
|
||||
{
|
||||
icon: Hotel,
|
||||
title: 'Luxury Hotels',
|
||||
description: 'Premium stays at unbeatable prices',
|
||||
savings: 'Up to 50% off',
|
||||
color: 'from-primary to-primary/80'
|
||||
},
|
||||
{
|
||||
icon: Plane,
|
||||
title: 'Travel Tours',
|
||||
description: 'Guided experiences worth your time',
|
||||
savings: 'Up to 45% off',
|
||||
color: 'from-primary to-primary/80'
|
||||
},
|
||||
{
|
||||
icon: MapPinned,
|
||||
title: 'Attractions',
|
||||
description: 'Must-see landmarks and experiences',
|
||||
savings: 'Up to 60% off',
|
||||
color: 'from-primary to-primary/80'
|
||||
},
|
||||
{
|
||||
icon: Building2,
|
||||
title: 'Shopping',
|
||||
description: 'Designer brands and local boutiques',
|
||||
savings: 'Up to 35% off',
|
||||
color: 'from-primary to-primary/80'
|
||||
},
|
||||
{
|
||||
icon: Gift,
|
||||
title: 'Wellness',
|
||||
description: 'Spa treatments and relaxation',
|
||||
savings: 'Up to 45% off',
|
||||
color: 'from-primary to-primary/80'
|
||||
}
|
||||
];
|
||||
|
||||
export function SuperSavingsPage({
|
||||
onBackClick,
|
||||
onHomeClick,
|
||||
onMelbourneClick,
|
||||
onPassesClick,
|
||||
onCheckoutClick,
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
onAttractionsClick,
|
||||
onBlogsClick,
|
||||
onHowItWorksClick,
|
||||
onFAQClick,
|
||||
onPrivacyPolicyClick,
|
||||
onAboutUsClick,
|
||||
onProfileClick,
|
||||
onCityCardsClick,
|
||||
onMagicItineraryClick,
|
||||
onPostCardsClick,
|
||||
onOffersClick,
|
||||
onSuperSavingsClick,
|
||||
onContactUsClick,
|
||||
onEsimsClick,
|
||||
onHotelDiscountsClick,
|
||||
fromSource = 'products',
|
||||
currentPage,
|
||||
user
|
||||
}: SuperSavingsPageProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [currentPage_, setCurrentPage_] = useState(1);
|
||||
const [showLoadMore, setShowLoadMore] = useState(true);
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
setSelectedCategories(prev =>
|
||||
prev.includes(category)
|
||||
? prev.filter(c => c !== category)
|
||||
: [...prev, category]
|
||||
);
|
||||
};
|
||||
|
||||
const filteredSavings = savingsData.filter(saving => {
|
||||
const matchesSearch = saving.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
saving.business.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(saving.category);
|
||||
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
|
||||
const itemsPerPage = 12;
|
||||
const displayedSavings = filteredSavings.slice(0, currentPage_ * itemsPerPage);
|
||||
const hasMoreItems = filteredSavings.length > displayedSavings.length;
|
||||
|
||||
const handleLoadMore = () => {
|
||||
setCurrentPage_(prev => prev + 1);
|
||||
if (!hasMoreItems) setShowLoadMore(false);
|
||||
};
|
||||
|
||||
// Show different layouts based on login state
|
||||
if (!user) {
|
||||
// Not logged in - show marketing/landing page
|
||||
return (
|
||||
<Layout
|
||||
activeCity="Melbourne"
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
>
|
||||
<div className="min-h-screen bg-background">
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-52 pb-20 overflow-hidden">
|
||||
{/* Background gradient */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-secondary/5 to-background">
|
||||
<ImageWithFallback
|
||||
src="https://images.unsplash.com/photo-1758787397603-bbd85bd06a2c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjB0cmF2ZWwlMjBkZXN0aW5hdGlvbnxlbnwxfHx8fDE3NjI0MjQ3NTV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
||||
alt="Melbourne Travel Destination"
|
||||
className="w-full h-full object-cover opacity-20"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-white/40 via-white/60 to-white/90"></div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<motion.div
|
||||
className="max-w-4xl mx-auto text-center"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h1 className="font-poppins text-4xl md:text-5xl lg:text-6xl leading-tight mb-6">
|
||||
<span className="font-light">Unlock</span>{' '}
|
||||
<span className="pr-2 font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Super Savings
|
||||
</span>
|
||||
</h1>
|
||||
<p className="font-poppins text-lg md:text-xl leading-relaxed text-gray-600 mb-8 max-w-2xl mx-auto">
|
||||
Experience incredible discounts up to 65% off on premium experiences, luxury stays, and unforgettable attractions.
|
||||
</p>
|
||||
<Button
|
||||
onClick={onSignInClick}
|
||||
className="bg-primary hover:bg-primary/90 text-white px-8 py-6 font-poppins font-semibold"
|
||||
>
|
||||
Start Saving Now
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute top-20 left-10 w-20 h-20 bg-primary/10 rounded-full blur-xl"></div>
|
||||
<div className="absolute bottom-20 right-10 w-32 h-32 bg-secondary/10 rounded-full blur-xl"></div>
|
||||
</section>
|
||||
|
||||
{/* Trusted By Companies Section */}
|
||||
<section className="py-12 bg-background">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-6xl mx-auto text-center">
|
||||
<div className="mb-10">
|
||||
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight mb-4">
|
||||
<span>Trusted by the </span>
|
||||
<span className="font-semibold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">world's best</span>
|
||||
</h2>
|
||||
<p className="font-poppins leading-relaxed text-muted-foreground max-w-2xl mx-auto">
|
||||
Join thousands of savvy travelers enjoying massive savings on premium experiences
|
||||
</p>
|
||||
</div>
|
||||
<TrustedCompanies />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured Super Savings Section */}
|
||||
<section className="py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="font-poppins text-3xl md:text-4xl lg:text-5xl leading-tight mb-4">
|
||||
<span className="font-light">Featured</span>{' '}
|
||||
<span className="pr-2 font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Super Savings
|
||||
</span>
|
||||
</h2>
|
||||
<p className="font-poppins leading-relaxed text-gray-600 max-w-2xl mx-auto">
|
||||
Check out our biggest discounts and start saving on premium experiences
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="container mx-auto px-4 pt-51 pb-16">
|
||||
<div className="flex gap-8">
|
||||
{/* Left Sidebar - Filters */}
|
||||
<div className="w-64 flex-shrink-0">
|
||||
<Card className="p-8 sticky top-48">
|
||||
<div className="space-y-6">
|
||||
{/* Search by header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-0 w-6 border-t-[3px] border-gray-800 rotate-90"></div>
|
||||
<h3 className="font-poppins font-medium text-gray-800">Search by</h3>
|
||||
</div>
|
||||
|
||||
{/* Filter categories */}
|
||||
<div className="space-y-4">
|
||||
{filterCategories.map(category => (
|
||||
<div key={category.value} className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
id={category.value}
|
||||
checked={selectedCategories.includes(category.value)}
|
||||
onCheckedChange={() => toggleCategory(category.value)}
|
||||
className="border-gray-400"
|
||||
/>
|
||||
<label
|
||||
htmlFor={category.value}
|
||||
className="font-poppins text-sm text-gray-700 cursor-pointer flex-1"
|
||||
>
|
||||
{category.label} ({category.count})
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1">
|
||||
{/* Breadcrumb */}
|
||||
<div className="mb-8">
|
||||
<p className="font-poppins text-sm text-gray-800">
|
||||
{fromSource === 'passes' ? (
|
||||
<>
|
||||
<span>My Profile{'>'}My passes{'>'}</span>
|
||||
<span className="font-semibold">Super Savings</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Our Products{'>'}</span>
|
||||
<span className="font-semibold">Super Savings</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Header Section */}
|
||||
<div className="mb-8">
|
||||
<h1 className="font-poppins md:text-5xl font-medium text-gray-800 leading-tight text-[24px]">
|
||||
Super Savings
|
||||
</h1>
|
||||
<p className="font-poppins text-gray-600 mt-2">
|
||||
Exclusive discounts up to 65% off on premium experiences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Savings Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6 mb-16">
|
||||
{displayedSavings.map((saving, index) => (
|
||||
<motion.div
|
||||
key={saving.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
>
|
||||
<Card className="bg-white border border-gray-200 rounded-xl overflow-hidden h-full hover:shadow-lg transition-shadow duration-300 relative">
|
||||
{/* Image */}
|
||||
<div className="relative h-52 bg-gray-300">
|
||||
<ImageWithFallback
|
||||
src={saving.image}
|
||||
alt={saving.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<Button className="absolute bottom-4 right-3 bg-white rounded-full shadow-lg w-9 h-9 p-0 hover:bg-gray-100 transition-colors">
|
||||
<Heart className="w-4 h-4 text-gray-800" />
|
||||
</Button>
|
||||
|
||||
{/* Discount Badge */}
|
||||
<div className="absolute top-4 left-4 bg-primary text-white px-3 py-1.5 rounded-lg">
|
||||
<span className="font-poppins font-semibold text-sm">{saving.discount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="space-y-4 px-4 py-4">
|
||||
{/* Business Name */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 bg-gray-300 rounded"></div>
|
||||
<span className="font-poppins text-sm text-gray-500">{saving.business}</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-poppins font-medium text-gray-900 leading-relaxed min-h-[48px]">
|
||||
{saving.title}
|
||||
</h3>
|
||||
|
||||
{/* Saved Amount Display */}
|
||||
<div className="bg-gradient-to-r from-primary/10 to-secondary/10 h-12 flex items-center justify-center rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<Percent className="w-4 h-4 text-primary" />
|
||||
<span className="font-poppins font-semibold text-primary">
|
||||
{saving.savedAmount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Minimal Pagination */}
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-8 h-8 p-0 font-poppins"
|
||||
disabled={currentPage_ === 1}
|
||||
>
|
||||
<ChevronRight className="w-4 h-4 rotate-180" />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{[1, 2, 3].map((page) => (
|
||||
<Button
|
||||
key={page}
|
||||
variant={currentPage_ === page ? "default" : "ghost"}
|
||||
size="sm"
|
||||
className={`w-8 h-8 p-0 font-poppins ${currentPage_ === page ? 'bg-primary hover:bg-primary/90' : ''}`}
|
||||
onClick={() => setCurrentPage_(page)}
|
||||
>
|
||||
{page}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-8 h-8 p-0 font-poppins"
|
||||
disabled={currentPage_ === 3}
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Button
|
||||
onClick={onSignInClick}
|
||||
variant="outline"
|
||||
className="font-poppins font-medium border-primary text-primary hover:bg-primary hover:text-white"
|
||||
>
|
||||
View All Super Savings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How It Works Section */}
|
||||
<section className="py-24 relative overflow-hidden bg-[#FFF5F5]/50">
|
||||
{/* Background decorative elements */}
|
||||
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-primary/5 rounded-full blur-[100px] pointer-events-none -mr-40 -mt-40"></div>
|
||||
<div className="absolute bottom-0 left-0 w-[400px] h-[400px] bg-primary/5 rounded-full blur-[80px] pointer-events-none -ml-20 -mb-20"></div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<motion.div
|
||||
className="text-center mb-20"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
<div className="h-px w-8 bg-primary"></div>
|
||||
<span className="text-primary font-semibold tracking-widest uppercase text-xs font-poppins">Simple Process</span>
|
||||
<div className="h-px w-8 bg-primary"></div>
|
||||
</div>
|
||||
<h2 className="font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-6">
|
||||
Start Saving in <span className="text-primary italic">Minutes</span>
|
||||
</h2>
|
||||
<p className="font-poppins text-lg text-gray-500 font-light max-w-xl mx-auto leading-relaxed">
|
||||
Your journey to smarter travel and bigger savings begins with three simple steps.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="relative max-w-6xl mx-auto">
|
||||
{/* Connecting line for desktop */}
|
||||
<div className="hidden md:block absolute top-12 left-[16%] right-[16%] h-0.5 border-t-2 border-dashed border-primary/20 z-0"></div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 md:gap-8">
|
||||
{[
|
||||
{
|
||||
step: '01',
|
||||
title: 'Unlock Access',
|
||||
description: 'Get your CityCards pass to instantly activate membership perks.',
|
||||
icon: MapPinned
|
||||
},
|
||||
{
|
||||
step: '02',
|
||||
title: 'Discover Deals',
|
||||
description: 'Browse exclusive offers on hotels, flights, and experiences.',
|
||||
icon: Search
|
||||
},
|
||||
{
|
||||
step: '03',
|
||||
title: 'Enjoy Savings',
|
||||
description: 'Redeem discounts instantly and watch your travel budget grow.',
|
||||
icon: Percent
|
||||
}
|
||||
].map((item, index) => (
|
||||
<motion.div
|
||||
key={item.step}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.2 }}
|
||||
className="relative z-10 group"
|
||||
>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{/* Icon Container */}
|
||||
<div className="relative mb-8">
|
||||
<div className="w-24 h-24 rounded-[2rem] bg-white flex items-center justify-center shadow-[0_8px_30px_rgb(0,0,0,0.06)] group-hover:scale-110 group-hover:-rotate-3 transition-transform duration-300 border border-primary/10 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-primary/5 group-hover:bg-primary/10 transition-colors duration-300"></div>
|
||||
<item.icon className="w-10 h-10 text-primary relative z-10" />
|
||||
</div>
|
||||
<div className="absolute -top-3 -right-3 w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-bold font-poppins text-sm border-4 border-white shadow-md">
|
||||
{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="font-merchant text-2xl text-gray-900 mb-3 group-hover:text-primary transition-colors">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="font-poppins text-gray-500 font-light leading-relaxed max-w-xs">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Categories Section */}
|
||||
<section className="py-24 bg-gray-50/50 relative overflow-hidden">
|
||||
{/* Abstract Travel Patterns */}
|
||||
<div className="absolute top-0 right-0 w-[800px] h-[800px] bg-gradient-to-bl from-primary/5 via-secondary/5 to-transparent rounded-full blur-3xl pointer-events-none -mr-40 -mt-40"></div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
{/* Section Header */}
|
||||
<div className="flex flex-col md:flex-row items-end justify-between mb-12 gap-6">
|
||||
<motion.div
|
||||
className="max-w-2xl"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="h-px w-8 bg-primary"></div>
|
||||
<span className="text-primary font-semibold tracking-widest uppercase text-xs font-poppins">Explore Collections</span>
|
||||
</div>
|
||||
<h2 className="font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-4 leading-tight">
|
||||
Curated for the <span className="text-primary italic">Modern Traveler</span>
|
||||
</h2>
|
||||
<p className="font-poppins text-lg text-gray-500 font-light max-w-lg leading-relaxed">
|
||||
Discover exclusive savings across our most sought-after travel categories.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="hidden md:block"
|
||||
>
|
||||
<Button
|
||||
onClick={onSignInClick}
|
||||
variant="ghost"
|
||||
className="group gap-2 text-gray-600 hover:text-primary font-poppins text-lg"
|
||||
>
|
||||
View All Categories
|
||||
<span className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-all duration-300">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Bento Grid Layout */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{categoriesData.map((category, index) => (
|
||||
<motion.div
|
||||
key={category.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: index * 0.1 }}
|
||||
className={`${index === 0 ? 'md:col-span-2' : ''} group`}
|
||||
>
|
||||
<div
|
||||
onClick={onSignInClick}
|
||||
className={`
|
||||
relative h-full bg-white rounded-[2rem] p-8 border border-gray-100
|
||||
shadow-[0_4px_20px_-4px_rgba(0,0,0,0.05)] hover:shadow-[0_20px_40px_-10px_rgba(0,0,0,0.1)]
|
||||
transition-all duration-500 cursor-pointer overflow-hidden flex flex-col justify-between
|
||||
group-hover:border-primary/20
|
||||
`}
|
||||
>
|
||||
{/* Background Gradient Hover */}
|
||||
<div className={`absolute inset-0 bg-gradient-to-br ${category.color} opacity-0 group-hover:opacity-[0.03] transition-opacity duration-500`}></div>
|
||||
|
||||
{/* Large Watermark Icon for visual depth */}
|
||||
<category.icon className="absolute -bottom-8 -right-8 w-48 h-48 text-gray-50 group-hover:text-primary/5 transition-colors duration-500 -rotate-12" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className={`
|
||||
w-14 h-14 rounded-2xl flex items-center justify-center shadow-sm transition-all duration-300
|
||||
${index === 0 ? 'bg-primary text-white shadow-primary/20' : 'bg-gray-50 text-gray-600 group-hover:bg-primary group-hover:text-white'}
|
||||
`}>
|
||||
<category.icon className="w-7 h-7" />
|
||||
</div>
|
||||
<Badge className="bg-emerald-50 text-emerald-600 border-emerald-100 font-poppins font-medium px-3 py-1.5">
|
||||
{category.savings}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<h3 className={`font-merchant text-gray-900 mb-2 group-hover:text-primary transition-colors ${index === 0 ? 'text-4xl' : 'text-2xl'}`}>
|
||||
{category.title}
|
||||
</h3>
|
||||
<p className="font-poppins text-gray-500 font-light leading-relaxed max-w-md">
|
||||
{category.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 mt-8 flex items-center gap-2 text-primary font-poppins font-medium text-sm opacity-0 group-hover:opacity-100 transform translate-y-2 group-hover:translate-y-0 transition-all duration-300">
|
||||
<span>Explore Deals</span>
|
||||
<ArrowLeft className="w-4 h-4 rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile View All Button */}
|
||||
<div className="mt-8 md:hidden text-center">
|
||||
<Button
|
||||
onClick={onSignInClick}
|
||||
className="w-full bg-primary hover:bg-primary/90 text-white font-poppins font-semibold py-6 rounded-xl shadow-lg shadow-primary/20"
|
||||
>
|
||||
Browse All Categories
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Access Your CityCards Section */}
|
||||
<section className="py-20 bg-muted/30">
|
||||
<MobileAppSection />
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
name: string
|
||||
name: string;
|
||||
accessToken:string;
|
||||
userId:string;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
@@ -29,11 +31,15 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const login = (userData: User) => {
|
||||
setUser(userData)
|
||||
localStorage.setItem("user", JSON.stringify(userData))
|
||||
localStorage.setItem("accessToken", userData?.accessToken)
|
||||
localStorage.setItem("userId", userData?.userId)
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
setUser(null)
|
||||
localStorage.removeItem("user")
|
||||
localStorage.removeItem("accessToken")
|
||||
localStorage.removeItem("userId")
|
||||
navigate("/")
|
||||
}
|
||||
|
||||
|
||||
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@@ -32,3 +32,5 @@ declare module '*.mp4' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.css";
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
# Image Reference Guidelines
|
||||
|
||||
**IMPORTANT**: When I provide an image for reference, it is for design reference only. Do NOT use that image inside any image section in the code. The image is provided to help understand the layout, styling, and visual direction - not to be embedded as an actual image in the application.
|
||||
|
||||
# CityCards Typography Guidelines
|
||||
|
||||
**Project**: Typography Guidelines for CityCards Travel Website
|
||||
|
||||
## Font System
|
||||
|
||||
### Primary Font
|
||||
- **Poppins**: Used for all text including headings (H1–H6), body text, buttons, labels, and forms - Clean, readable, and consistent throughout
|
||||
|
||||
### Font Weight Scale & Usage
|
||||
- **font-light (300)**: Hero headings only - For creating dynamic contrast in H1/H2
|
||||
- **font-normal (400)**: Standard body text - Default weight for paragraphs
|
||||
- **font-medium (500)**: Buttons, navigation links, subtle emphasis
|
||||
- **font-semibold (600)**: Section headings, primary buttons
|
||||
- **font-bold (700)**: Hero keywords, strong emphasis
|
||||
|
||||
## Heading Typography Specifications
|
||||
|
||||
### H1 - Hero/Main Page Headings
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-5xl md:text-6xl lg:text-7xl` (48px/60px/72px - targeting ~64px)
|
||||
- **Line Height**: `leading-tight`
|
||||
- **Pattern**: Dynamic multi-weight with gradient/italic accents (max 2 emphasis styles)
|
||||
|
||||
```jsx
|
||||
<h1 className="font-poppins text-5xl md:text-6xl lg:text-7xl leading-tight">
|
||||
<span className="font-light">Discover</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Melbourne's
|
||||
</span>{' '}
|
||||
<span className="font-normal">Best Experiences</span>
|
||||
</h1>
|
||||
```
|
||||
|
||||
### H2 - Section Headings
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-2xl md:text-3xl lg:text-4xl` (24px/36px/48px)
|
||||
- **Line Height**: `leading-tight`
|
||||
- **Pattern**: Mixed weights with gradient emphasis
|
||||
|
||||
```jsx
|
||||
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
|
||||
<span className="font-light">Explore</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Amazing
|
||||
</span>{' '}
|
||||
<span className="font-normal">Cities</span>
|
||||
</h2>
|
||||
```
|
||||
|
||||
### H3 - Subsection Headings
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-xl md:text-2xl` (24px/30px)
|
||||
- **Line Height**: `leading-snug`
|
||||
- **Weight**: `font-semibold`
|
||||
|
||||
```jsx
|
||||
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-semibold">
|
||||
Feature Title
|
||||
</h3>
|
||||
```
|
||||
|
||||
### H4 - Component Headings
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-lg md:text-xl` (20px/24px)
|
||||
- **Line Height**: `leading-snug`
|
||||
- **Weight**: `font-medium` or `font-semibold`
|
||||
|
||||
```jsx
|
||||
<h4 className="font-poppins text-lg md:text-xl leading-snug font-medium">
|
||||
Component Heading
|
||||
</h4>
|
||||
```
|
||||
|
||||
### H5 - Card/Item Titles
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-lg` (18px)
|
||||
- **Line Height**: `leading-snug`
|
||||
- **Weight**: `font-medium`
|
||||
|
||||
```jsx
|
||||
<h5 className="font-poppins text-lg leading-snug font-medium">
|
||||
Card Title
|
||||
</h5>
|
||||
```
|
||||
|
||||
### H6 - Small Headings
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-base` (16px)
|
||||
- **Line Height**: `leading-snug`
|
||||
- **Weight**: `font-medium`
|
||||
|
||||
```jsx
|
||||
<h6 className="font-poppins text-base leading-snug font-medium">
|
||||
Small Heading
|
||||
</h6>
|
||||
```
|
||||
|
||||
## Body Typography Specifications
|
||||
|
||||
### Large Body Text
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-xl` (20px)
|
||||
- **Line Height**: `leading-relaxed`
|
||||
- **Weight**: `font-normal`
|
||||
|
||||
```jsx
|
||||
<p className="font-poppins text-xl leading-relaxed font-normal">
|
||||
Large descriptive text for important sections
|
||||
</p>
|
||||
```
|
||||
|
||||
### Regular Body Text
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-base` (16px)
|
||||
- **Line Height**: `leading-relaxed`
|
||||
- **Weight**: `font-normal`
|
||||
|
||||
```jsx
|
||||
<p className="font-poppins text-base leading-relaxed font-normal">
|
||||
Regular body text content
|
||||
</p>
|
||||
```
|
||||
|
||||
### Small Body Text
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-sm` (14px)
|
||||
- **Line Height**: `leading-relaxed`
|
||||
- **Weight**: `font-normal` or `font-light`
|
||||
|
||||
```jsx
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal">
|
||||
Caption or meta information
|
||||
</p>
|
||||
```
|
||||
|
||||
## Interactive Element Typography
|
||||
|
||||
### Buttons
|
||||
- **Font**: Poppins
|
||||
- **Primary Weight**: `font-semibold`
|
||||
- **Secondary Weight**: `font-medium`
|
||||
- **Min Size**: 16px
|
||||
|
||||
```jsx
|
||||
// Primary Button
|
||||
<Button className="font-poppins font-semibold">
|
||||
Primary Action
|
||||
</Button>
|
||||
|
||||
// Secondary Button
|
||||
<Button className="font-poppins font-medium">
|
||||
Secondary Action
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Navigation Links
|
||||
- **Font**: Poppins
|
||||
- **Weight**: `font-medium`
|
||||
- **Size**: `text-base` (16px)
|
||||
|
||||
```jsx
|
||||
<a className="font-poppins font-medium text-base">
|
||||
Navigation Link
|
||||
</a>
|
||||
```
|
||||
|
||||
### Form Labels
|
||||
- **Font**: Poppins
|
||||
- **Weight**: `font-light` or `font-normal`
|
||||
- **Size**: `text-sm` or `text-base` (14px/16px)
|
||||
|
||||
```jsx
|
||||
<Label className="font-poppins font-light text-sm">
|
||||
Form Label
|
||||
</Label>
|
||||
```
|
||||
|
||||
### Form Inputs
|
||||
- **Font**: Poppins
|
||||
- **Weight**: `font-normal`
|
||||
- **Size**: `text-base` (16px)
|
||||
|
||||
```jsx
|
||||
<Input className="font-poppins font-normal text-base" />
|
||||
```
|
||||
|
||||
## Accessibility Standards
|
||||
|
||||
### Text Size Requirements
|
||||
- **Minimum Text Size**: 14px
|
||||
- **Interactive Minimum Size**: 16px
|
||||
- **Contrast**: WCAG AA or higher
|
||||
- **Heading Hierarchy**: Maintain semantic order (H1 → H2 → H3 etc.)
|
||||
|
||||
### Implementation Requirements
|
||||
```jsx
|
||||
// Always include explicit font and size classes to override defaults
|
||||
<p className="font-poppins text-base font-normal leading-relaxed">
|
||||
Content with explicit styling
|
||||
</p>
|
||||
```
|
||||
|
||||
## Typography Rules
|
||||
|
||||
### DO ✅
|
||||
- Use Poppins for all text (headings and body)
|
||||
- Apply max 2 emphasis styles per heading
|
||||
- Use gradient effects sparingly
|
||||
- Keep line-heights consistent
|
||||
- Always specify explicit font classes to override component defaults
|
||||
|
||||
### DON'T ❌
|
||||
- Don't use font-light in small text
|
||||
- Don't mix more than 3 weights in one heading
|
||||
- Don't go below 14px for captions
|
||||
- Don't override font sizes without Tailwind classes
|
||||
- Don't break semantic heading hierarchy
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Component Styling Override
|
||||
**IMPORTANT**: Always explicitly set typography classes to override component defaults:
|
||||
|
||||
```jsx
|
||||
// ✅ CORRECT - Explicit typography classes
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="font-poppins text-xl font-semibold">
|
||||
Card Title
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="font-poppins text-base font-normal leading-relaxed">
|
||||
Card content with explicit styling
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
// ❌ INCORRECT - Relying on defaults
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Card content without explicit styling</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Dynamic Heading Patterns
|
||||
```jsx
|
||||
// Pattern 1: Light → Bold (H1/H2 only)
|
||||
<h1 className="font-poppins text-4xl md:text-5xl lg:text-6xl leading-tight">
|
||||
<span className="font-light">Discover</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Amazing
|
||||
</span>{' '}
|
||||
<span className="font-normal">Destinations</span>
|
||||
</h1>
|
||||
|
||||
// Pattern 2: Normal → Semibold (H3/H4)
|
||||
<h3 className="font-poppins text-xl md:text-2xl leading-snug">
|
||||
<span className="font-normal">Experience</span>{' '}
|
||||
<span className="font-semibold">Melbourne's Culture</span>
|
||||
</h3>
|
||||
```
|
||||
|
||||
### Responsive Typography
|
||||
```jsx
|
||||
// Mobile-first responsive scaling
|
||||
<h1 className="font-poppins text-3xl sm:text-4xl md:text-5xl lg:text-6xl leading-tight">
|
||||
Responsive Heading
|
||||
</h1>
|
||||
|
||||
<p className="font-poppins text-sm sm:text-base md:text-lg leading-relaxed font-normal">
|
||||
Responsive body text
|
||||
</p>
|
||||
```
|
||||
|
||||
**Add your own guidelines here**
|
||||
<!--
|
||||
|
||||
System Guidelines
|
||||
|
||||
Use this file to provide the AI with rules and guidelines you want it to follow.
|
||||
This template outlines a few examples of things you can add. You can add your own sections and format it to suit your needs
|
||||
|
||||
TIP: More context isn't always better. It can confuse the LLM. Try and add the most important rules you need
|
||||
|
||||
# General guidelines
|
||||
|
||||
Any general rules you want the AI to follow.
|
||||
For example:
|
||||
|
||||
* Only use absolute positioning when necessary. Opt for responsive and well structured layouts that use flexbox and grid by default
|
||||
* Refactor code as you go to keep code clean
|
||||
* Keep file sizes small and put helper functions and components in their own files.
|
||||
|
||||
--------------
|
||||
|
||||
# Design system guidelines
|
||||
Rules for how the AI should make generations look like your company's design system
|
||||
|
||||
Additionally, if you select a design system to use in the prompt box, you can reference
|
||||
your design system's components, tokens, variables and components.
|
||||
For example:
|
||||
|
||||
* Use a base font-size of 14px
|
||||
* Date formats should always be in the format “Jun 10”
|
||||
* The bottom toolbar should only ever have a maximum of 4 items
|
||||
* Never use the floating action button with the bottom toolbar
|
||||
* Chips should always come in sets of 3 or more
|
||||
* Don't use a dropdown if there are 2 or fewer options
|
||||
|
||||
You can also create sub sections and add more specific details
|
||||
For example:
|
||||
|
||||
|
||||
## Button
|
||||
The Button component is a fundamental interactive element in our design system, designed to trigger actions or navigate
|
||||
users through the application. It provides visual feedback and clear affordances to enhance user experience.
|
||||
|
||||
### Usage
|
||||
Buttons should be used for important actions that users need to take, such as form submissions, confirming choices,
|
||||
or initiating processes. They communicate interactivity and should have clear, action-oriented labels.
|
||||
|
||||
### Variants
|
||||
* Primary Button
|
||||
* Purpose : Used for the main action in a section or page
|
||||
* Visual Style : Bold, filled with the primary brand color
|
||||
* Usage : One primary button per section to guide users toward the most important action
|
||||
* Secondary Button
|
||||
* Purpose : Used for alternative or supporting actions
|
||||
* Visual Style : Outlined with the primary color, transparent background
|
||||
* Usage : Can appear alongside a primary button for less important actions
|
||||
* Tertiary Button
|
||||
* Purpose : Used for the least important actions
|
||||
* Visual Style : Text-only with no border, using primary color
|
||||
* Usage : For actions that should be available but not emphasized
|
||||
-->
|
||||
@@ -4,10 +4,12 @@ import App from "./App";
|
||||
import "./index.css";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "./Redux/Store";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<Toaster position="top-right" richColors duration={2000} closeButton />
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Heart, MapPin, Zap, Globe, Users, Camera, Coffee } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
// import { Button } from './ui/button';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { ReviewsSection } from '../components/ReviewsSection';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -1,12 +1,25 @@
|
||||
import { ArrowLeft, Calendar, Check, ChevronLeft, ChevronRight, Clock, MapPin, Users, X } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
<<<<<<< HEAD:src/components/AttractionDetailsPage.tsx
|
||||
=======
|
||||
import { ArrowLeft, Clock, Users, Calendar, MapPin, Star, Check, X, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Card, } from '../components/ui/card';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
>>>>>>> cdadee5df40960af2263c2e5e6f4a8f234b7d5b0:src/pages/AttractionDetailsPage.tsx
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Layout } from '../Layout';
|
||||
import { useGetAttractionDetailsByIdQuery } from '../Redux/services/attractions.service';
|
||||
<<<<<<< HEAD:src/components/AttractionDetailsPage.tsx
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, } from './ui/card';
|
||||
=======
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
>>>>>>> cdadee5df40960af2263c2e5e6f4a8f234b7d5b0:src/pages/AttractionDetailsPage.tsx
|
||||
|
||||
interface AttractionDetailsPageProps {
|
||||
onBackClick: () => void;
|
||||
@@ -30,7 +43,9 @@ export function AttractionDetailsPage({
|
||||
const { data: attraction, isLoading } = useGetAttractionDetailsByIdQuery(Number(attractionId));
|
||||
|
||||
if (isLoading) {
|
||||
return <div>loading...</div>
|
||||
return (
|
||||
<LoadingSpinner/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -39,7 +54,7 @@ export function AttractionDetailsPage({
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
// showCitySubmenu={false}
|
||||
// showCitySubmenu={false}
|
||||
>
|
||||
<div className="container mx-auto px-4 pt-40 pb-16 max-w-6xl">
|
||||
{/* Back Button */}
|
||||
@@ -81,7 +96,7 @@ export function AttractionDetailsPage({
|
||||
{attraction.title}
|
||||
</span>{' '}
|
||||
<span className="text-[#2d3134]">
|
||||
Day Trip by {attraction.partner.businessName}
|
||||
Day Trip by {attraction.partner.businessName}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
@@ -98,10 +113,10 @@ export function AttractionDetailsPage({
|
||||
</div>
|
||||
|
||||
{/* Gallery images */}
|
||||
{attraction.attractionGalleries.slice().map((image:any) => (
|
||||
{attraction.attractionGalleries.slice().map((image: any) => (
|
||||
<div key={image.id} className="col-span-1 row-span-1">
|
||||
<ImageWithFallback
|
||||
src={image.filePathUrl}
|
||||
src={image.filePathUrl}
|
||||
alt={`Gallery image ${image.id}`}
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
/>
|
||||
@@ -254,131 +269,7 @@ export function AttractionDetailsPage({
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar - Calendar and Booking */}
|
||||
<div className="lg:sticky lg:top-32 space-y-8 self-start">
|
||||
{/* Calendar Widget with Custom Design */}
|
||||
<Card className="p-6 bg-white shadow-lg border border-primary/10">
|
||||
<div className="mb-6">
|
||||
<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 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<button className="p-2 hover:bg-primary/10 rounded-lg transition-colors">
|
||||
<ChevronLeft className="w-5 h-5 text-primary" />
|
||||
</button>
|
||||
<span className="font-semibold text-gray-900">September 2025</span>
|
||||
<button className="p-2 hover:bg-primary/10 rounded-lg transition-colors">
|
||||
<ChevronRight className="w-5 h-5 text-primary" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Days of week */}
|
||||
<div className="grid grid-cols-7 gap-1 text-center text-sm font-medium text-gray-500">
|
||||
<div>Su</div>
|
||||
<div>Mo</div>
|
||||
<div>Tu</div>
|
||||
<div>We</div>
|
||||
<div>Th</div>
|
||||
<div>Fr</div>
|
||||
<div>Sa</div>
|
||||
</div>
|
||||
|
||||
{/* Calendar Grid */}
|
||||
<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;
|
||||
const isSelected = day === 27;
|
||||
const isToday = day === 15;
|
||||
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
|
||||
? 'bg-primary/10 text-primary border border-primary/20'
|
||||
: 'text-gray-700 hover:bg-primary/5 hover:text-primary'
|
||||
}`}
|
||||
>
|
||||
{day}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Next month */}
|
||||
{Array.from({ length: 4 }, (_, i) => (
|
||||
<button
|
||||
key={`next-${i + 1}`}
|
||||
className="h-10 w-10 text-sm text-gray-300 hover:bg-gray-50 rounded"
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selected Date Display */}
|
||||
<div className="mt-6 p-4 bg-primary/5 rounded-lg border border-primary/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<Calendar className="w-5 h-5 text-primary" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">Selected Date</p>
|
||||
<p className="text-lg font-semibold text-primary">September 27, 2025</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Pricing Card */}
|
||||
<Card className="p-6 bg-gradient-to-br from-primary/5 to-primary/10 border border-primary/20">
|
||||
<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">{attraction.ticketPriceAdult}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">Service Fee</span>
|
||||
<span className="font-medium text-gray-900">$5</span>
|
||||
</div>
|
||||
<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">${attraction.ticketPriceAdult + 5}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Confirm Booking 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()}
|
||||
>
|
||||
<span className="relative z-10 flex items-center justify-center gap-2">
|
||||
<Check className="w-5 h-5" />
|
||||
Confirm Booking
|
||||
</span>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -skew-x-12 -translate-x-full group-hover:translate-x-full transition-transform duration-700"></div>
|
||||
</Button>
|
||||
|
||||
{/* Trust Indicators */}
|
||||
<div className="flex items-center justify-center gap-4 text-sm text-gray-600">
|
||||
<div className="flex items-center gap-1">
|
||||
<Check className="w-4 h-4 text-primary" />
|
||||
<span>Instant Confirmation</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<X className="w-4 h-4 text-primary" />
|
||||
<span>Free Cancellation</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,14 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { Search, Star, Clock } from 'lucide-react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Checkbox } from '../components/ui/checkbox';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
import { useGetAttractionFiltersQuery, useGetCustomerAttractionsQuery } from '../Redux/services/attractions.service';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
interface User {
|
||||
email: string;
|
||||
name: string;
|
||||
@@ -29,187 +30,6 @@ interface Attraction {
|
||||
passType: string;
|
||||
}
|
||||
|
||||
// {
|
||||
// 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;
|
||||
@@ -229,8 +49,11 @@ export function AttractionsPage({
|
||||
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
|
||||
const [selectedPassType, setSelectedPassType] = useState<string | null>(null);
|
||||
|
||||
const cityId = 1
|
||||
|
||||
const cityId = localStorage.getItem("cityId")
|
||||
const cityName = localStorage.getItem("cityName")
|
||||
|
||||
console.log(cityName)
|
||||
|
||||
const { data: filterData, isLoading } = useGetAttractionFiltersQuery(cityId)
|
||||
const { data: attractions } = useGetCustomerAttractionsQuery({
|
||||
cityId, // required
|
||||
@@ -239,9 +62,11 @@ export function AttractionsPage({
|
||||
cardType: selectedPassType, // optional
|
||||
search, // optional
|
||||
});
|
||||
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
return (
|
||||
<LoadingSpinner/>
|
||||
);
|
||||
}
|
||||
|
||||
const handleAttractionClick = (attractionId: string) => {
|
||||
@@ -254,7 +79,7 @@ export function AttractionsPage({
|
||||
const showingFrom = 1;
|
||||
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
|
||||
@@ -301,12 +126,12 @@ export function AttractionsPage({
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="font-merchant text-2xl md:text-3xl lg:text-4xl leading-tight text-gray-900 mb-4">
|
||||
<span className="font-light">Discover</span>{' '}
|
||||
<span className="font-bold italic text-gradient-primary pr-1">Melbourne's</span>{' '}
|
||||
<span className="font-bold italic text-gradient-primary pr-1">{cityName}'s</span>{' '}
|
||||
<span className="font-normal">Best</span>{' '}
|
||||
<span className="font-semibold text-emphasis">Attractions</span>
|
||||
</h1>
|
||||
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
|
||||
Skip the lines and explore Melbourne's most iconic destinations with your CityCard pass
|
||||
Skip the lines and explore {cityName}'s most iconic destinations with your CityCard pass
|
||||
</p>
|
||||
</div>
|
||||
{/* City Card Promotional Banner */}
|
||||
@@ -403,7 +228,7 @@ export function AttractionsPage({
|
||||
htmlFor={key}
|
||||
className="font-poppins text-sm text-[#414141] cursor-pointer flex-1 font-normal"
|
||||
>
|
||||
{key==="selective_pass" ?"Selective":"Unlimited"} ({count as number})
|
||||
{key === "selective_pass" ? "Selective" : "Unlimited"} ({count as number})
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
@@ -416,7 +241,7 @@ export function AttractionsPage({
|
||||
<div className="flex-1">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-[48px] font-medium text-[#2d2d2d] mb-6">Attractions in Melbourne</h1>
|
||||
<h1 className="text-[48px] font-medium text-[#2d2d2d] mb-6">Attractions in {cityName}</h1>
|
||||
{/* Results count */}
|
||||
<p className="text-[16px] text-[#414141] mb-2">
|
||||
Showing {showingFrom}-{showingTo} of {totalItems} Item(s)
|
||||
@@ -493,7 +318,7 @@ export function AttractionsPage({
|
||||
</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:React.MouseEvent<HTMLButtonElement>) => {
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
handleCheckoutClick();
|
||||
}}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Calendar, User, Clock, Share2, BookmarkPlus, ThumbsUp, MessageSquare, Tag, MapPin } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Separator } from './ui/separator';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Separator } from '../components/ui/separator';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { Calendar, User, Clock, ArrowRight, Search, Tag, CreditCard, MapPin, Check, Smartphone, Star, Heart, Share2 } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import Navbar from './Navbar';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Input } from '../components/ui/input';
|
||||
import Navbar from '../components/Navbar';
|
||||
// import { CitySubmenu } from './CitySubmenu';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { WhyChooseCityCards } from './WhyChooseCityCards';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { WhyChooseCityCards } from '../components/WhyChooseCityCards';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { ReviewsSection } from '../components/ReviewsSection';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import imgFrame1597884939 from "figma:asset/5da1b0444c0d21bc7ee776c49e36e2a8ea4d3e12.png";
|
||||
|
||||
// Blog Mobile App Section Component
|
||||
@@ -1,20 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { ArrowLeft, CreditCard, Users, Calendar, MapPin, Shield, Truck, Clock, ChevronRight, Check, ChevronDown, X, Mail, Smartphone } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Separator } from './ui/separator';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
||||
import { RadioGroup, RadioGroupItem } from './ui/radio-group';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Textarea } from './ui/textarea';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Label } from '../components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Separator } from '../components/ui/separator';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../components/ui/dialog';
|
||||
import { RadioGroup, RadioGroupItem } from '../components/ui/radio-group';
|
||||
import { Checkbox } from '../components/ui/checkbox';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../components/ui/select';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Textarea } from '../components/ui/textarea';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface CheckoutPageProps {
|
||||
@@ -1,18 +1,15 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Star, MapPin, Clock, CreditCard, Users, Shield, Smartphone, Check } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
// import SubNavbar from './SubNavbar';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { FAQPage } from './FAQPage';
|
||||
import { HowItWorks } from './HowItWorks';
|
||||
import { WhyChooseCityCards } from './WhyChooseCityCards';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { HowItWorks } from '../components/HowItWorks';
|
||||
import { WhyChooseCityCards } from '../components/WhyChooseCityCards';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface User {
|
||||
@@ -12,13 +12,13 @@ import {
|
||||
ChevronRight,
|
||||
CheckCircle
|
||||
} from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Textarea } from './ui/textarea';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Label } from '../components/ui/label';
|
||||
import { Textarea } from '../components/ui/textarea';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
|
||||
interface ContactUsPageProps {
|
||||
onBackClick: () => void;
|
||||
@@ -18,17 +18,17 @@ import {
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Progress } from './ui/progress';
|
||||
import { Calendar as CalendarComponent } from './ui/calendar';
|
||||
import { Input } from './ui/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Progress } from '../components/ui/progress';
|
||||
import { Calendar as CalendarComponent } from '../components/ui/calendar';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../components/ui/popover';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../components/ui/collapsible';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
|
||||
|
||||
interface User {
|
||||
@@ -2,10 +2,10 @@ import { ArrowRight, Check, CreditCard, DollarSign, MapPin, Palette, Sparkles, T
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Layout } from '../Layout';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { TrustSection } from './TrustSection';
|
||||
import { Button } from './ui/button';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { TrustSection } from '../components/TrustSection';
|
||||
import { Button } from '../components/ui/button';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, ChevronRight, QrCode, CreditCard, Calendar, MapPin, Star, CheckCircle, Sparkles, Users, Clock, Gift, Ticket } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -1,10 +1,6 @@
|
||||
import { motion } from 'motion/react';
|
||||
import { BadgePercent, Clock, Crown, Check, ArrowRight } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import Navbar from './Navbar';
|
||||
// import { SubNavbar } from './SubNavbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import cityCardsLogo from '../assets/cityLogo.png';
|
||||
import marriottHotelImage from '../assets/marriott-hotel.png';
|
||||
import { Layout } from '../Layout';
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Calendar, Clock, MapPin, Users, Star, Heart, Share2, Download, CheckCircle, Navigation, Cloud, Sun } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
|
||||
interface ItineraryViewPageProps {
|
||||
onBackClick: () => void;
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Sparkles, MapPin, Clock, Users, Calendar, Star, Zap, Heart, Camera } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { HowItWorks } from './HowItWorks';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { PersonalizedTourHero } from './PersonalizedTourHero';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { HowItWorks } from '../components/HowItWorks';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { PersonalizedTourHero } from '../components/PersonalizedTourHero';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface User {
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Sparkles, MapPin, Clock, Users, Calendar, Star, Zap, Heart, Camera } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { HowItWorks } from './HowItWorks';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { PersonalizedTourHero } from './PersonalizedTourHero';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { HowItWorks } from '../components/HowItWorks';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { PersonalizedTourHero } from '../components/PersonalizedTourHero';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface User {
|
||||
@@ -1,22 +1,21 @@
|
||||
import { motion, useAnimationControls, AnimatePresence } from 'motion/react';
|
||||
import { Button } from './ui/button';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { ArrowRight, Calendar, Thermometer, Eye, MapPin, Clock, Users, Ticket, Wand2, Plane, Sparkles } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Navbar from './Navbar';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { MelbourneAttractions } from './MelbourneAttractions';
|
||||
import { MelbourneCardComparison } from './MelbourneCardComparison';
|
||||
import { MelbourneTourOverview } from './MelbourneTourOverview';
|
||||
import { MelbourneBlogs } from './MelbourneBlogs';
|
||||
import { CustomPostcards } from './CustomPostcards';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { MobileAppPromotion } from './MobileAppPromotion';
|
||||
import { MelbourneFAQ } from './MelbourneFAQ';
|
||||
import { Footer } from './Footer';
|
||||
// import { MinimalHeroBanner } from './MinimalHeroBanner';
|
||||
import { useState } from 'react';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { MelbourneAttractions } from '../components/MelbourneAttractions';
|
||||
import { MelbourneCardComparison } from '../components/MelbourneCardComparison';
|
||||
import { MelbourneTourOverview } from '../components/MelbourneTourOverview';
|
||||
import { MelbourneBlogs } from '../components/MelbourneBlogs';
|
||||
import { CustomPostcards } from '../components/CustomPostcards';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { MobileAppPromotion } from '../components/MobileAppPromotion';
|
||||
import { MelbourneFAQ } from '../components/MelbourneFAQ';
|
||||
import { Layout } from '../Layout';
|
||||
import { HeroBannerCarousel } from './HeroBannerCarousel';
|
||||
import { HotelEsimOffers } from './HotelEsimOffers';
|
||||
import { HeroBannerCarousel } from '../components/HeroBannerCarousel';
|
||||
import { HotelEsimOffers } from '../components/HotelEsimOffers';
|
||||
import { useGetSelectedCityDetailsQuery } from '../Redux/services/cities.service';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -149,6 +148,20 @@ export function MelbournePage({
|
||||
const [currentCardIndex, setCurrentCardIndex] = useState(0);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
const cityId = localStorage.getItem("cityId")
|
||||
|
||||
const { data: cityDetails, isLoading: loadingCityDetails } = useGetSelectedCityDetailsQuery(cityId)
|
||||
|
||||
|
||||
if (loadingCityDetails) {
|
||||
return (
|
||||
<LoadingSpinner />
|
||||
)
|
||||
}
|
||||
|
||||
const cards = cityDetails?.city?.cards
|
||||
sessionStorage.setItem("cityLogo", String(cityDetails?.city?.cityIconPath))
|
||||
|
||||
const currentCard = itineraryCards[currentCardIndex];
|
||||
const nextCard = itineraryCards[(currentCardIndex + 1) % itineraryCards.length];
|
||||
const thirdCard = itineraryCards[(currentCardIndex + 2) % itineraryCards.length];
|
||||
@@ -254,12 +267,12 @@ export function MelbournePage({
|
||||
|
||||
{/* Attractions Section */}
|
||||
<div id="attractions" className="scroll-mt-32">
|
||||
<MelbourneAttractions />
|
||||
<MelbourneAttractions />
|
||||
</div>
|
||||
|
||||
{/* Pass Comparison */}
|
||||
<div id="passes" className="scroll-mt-32">
|
||||
<MelbourneCardComparison />
|
||||
<MelbourneCardComparison cards={cards} />
|
||||
</div>
|
||||
|
||||
{/* Tour Overview */}
|
||||
@@ -737,8 +750,8 @@ export function MelbournePage({
|
||||
}, 400);
|
||||
}}
|
||||
className={`font-poppins group relative transition-all duration-300 px-4 py-2 rounded-full font-medium ${idx === currentCardIndex
|
||||
? 'bg-gradient-to-r from-primary to-orange-500 text-white shadow-lg scale-110'
|
||||
: 'bg-white/80 backdrop-blur-sm text-gray-600 hover:text-primary hover:bg-white border border-gray-200 hover:border-primary/30 hover:scale-105'
|
||||
? 'bg-gradient-to-r from-primary to-orange-500 text-white shadow-lg scale-110'
|
||||
: 'bg-white/80 backdrop-blur-sm text-gray-600 hover:text-primary hover:bg-white border border-gray-200 hover:border-primary/30 hover:scale-105'
|
||||
}`}
|
||||
whileHover={{ y: -2 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Search, Filter, Star, MapPin, Clock, Tag, Heart, Share2, Copy, ChevronDown, ChevronRight, Check, Hotel, Plane, Building2, MapPinned, Home } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Separator } from './ui/separator';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import Navbar from './Navbar';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Separator } from '../components/ui/separator';
|
||||
import { Checkbox } from '../components/ui/checkbox';
|
||||
import Navbar from '../components/Navbar';
|
||||
// import SubNavbar from './SubNavbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { TrustSection } from './TrustSection';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
import { TrustedCompanies } from './TrustedCompanies';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { TrustSection } from '../components/TrustSection';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { ReviewsSection } from '../components/ReviewsSection';
|
||||
import { TrustedCompanies } from '../components/TrustedCompanies';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface OffersPageProps {
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
import { Check, X, Star, Shield, Clock, Smartphone, Download, QrCode, Heart, Users, Award, Headphones } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
||||
import { RadioGroup, RadioGroupItem } from './ui/radio-group';
|
||||
import { Badge } from './ui/badge';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { RadioGroup, RadioGroupItem } from '../components/ui/radio-group';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { ReviewsSection } from '../components/ReviewsSection';
|
||||
import { Layout } from '../Layout';
|
||||
import { LoginModal } from './LoginModal';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { LoginModal } from '../components/LoginModal';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useGetSelectedCityDetailsQuery } from '../Redux/services/cities.service';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
|
||||
interface PassesPageProps {
|
||||
onCheckoutClick?: () => void;
|
||||
@@ -148,21 +150,29 @@ export function PassesPage({
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
}: PassesPageProps) {
|
||||
const [selectedPass, setSelectedPass] = useState<string>('unlimited');
|
||||
const [selectedPass, setSelectedPass] = useState<string>(passTypes[1].id);
|
||||
const [isLoginOpen, setIsLoginOpen] = useState(false);
|
||||
const { user } = useAuth(); // from AuthContext
|
||||
|
||||
const navigate= useNavigate()
|
||||
const navigate = useNavigate()
|
||||
const cityId = localStorage.getItem("cityId")
|
||||
|
||||
const { data: cityDetails, isLoading: loadingCityDetails } = useGetSelectedCityDetailsQuery(cityId)
|
||||
const cards = cityDetails?.city?.cards ?? []
|
||||
console.log(cards)
|
||||
|
||||
if (loadingCityDetails) {
|
||||
return (<LoadingSpinner />)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleCheckoutClick = () => {
|
||||
const handleCheckoutClick = () => {
|
||||
console.log('Proceeding to checkout for user:', user);
|
||||
// Add your checkout logic here
|
||||
navigate('/checkout');
|
||||
};
|
||||
|
||||
const handleSignInClick = () => {
|
||||
const handleSignInClick = () => {
|
||||
setIsLoginOpen(true);
|
||||
};
|
||||
|
||||
@@ -189,7 +199,7 @@ export function PassesPage({
|
||||
<div className="text-center mb-16">
|
||||
<div className="mb-6">
|
||||
<h1 className="font-merchant font-light text-4xl md:text-5xl lg:text-6xl mb-4">
|
||||
Buy <span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pr-1.5">Passes</span>
|
||||
Buy <span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pr-1.5">Cards</span>
|
||||
</h1>
|
||||
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
|
||||
Skip the lines, save money, and explore more with our flexible city cards designed for modern travelers
|
||||
@@ -200,130 +210,184 @@ export function PassesPage({
|
||||
{/* Pass Comparison Section */}
|
||||
<div className="mb-20">
|
||||
<RadioGroup
|
||||
value={selectedPass}
|
||||
onValueChange={setSelectedPass}
|
||||
className="grid md:grid-cols-2 gap-8 max-w-6xl mx-auto"
|
||||
>
|
||||
{passTypes.map((pass) => (
|
||||
<div key={pass.id} className="relative h-full">
|
||||
<Card className={`relative h-full flex flex-col transition-all duration-300 ${pass.popular
|
||||
? 'ring-2 ring-primary shadow-xl'
|
||||
: selectedPass === pass.id
|
||||
? 'ring-2 ring-primary/50 shadow-lg'
|
||||
: 'border-gray-200 shadow-md hover:shadow-lg hover:border-primary/30'
|
||||
}`}>
|
||||
{/* Flexi Pass Card */}
|
||||
<div className="relative h-full">
|
||||
<Card
|
||||
className={`relative h-full flex flex-col transition-all duration-300 cursor-pointer ${selectedPass === passTypes[0].id
|
||||
? "ring-2 ring-red-500 shadow-lg" // 🔴 red border when selected
|
||||
: "border-gray-200 shadow-md hover:shadow-lg hover:border-primary/30"
|
||||
}`}
|
||||
onClick={() => setSelectedPass(passTypes[0].id)}
|
||||
>
|
||||
<div className="absolute top-5 right-5 z-10">
|
||||
{/* <RadioGroupItem value={passTypes[0].id} id={passTypes[0].id} className="w-5 h-5" /> */}
|
||||
</div>
|
||||
|
||||
{/* Popular Badge */}
|
||||
{pass.popular && (
|
||||
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2 z-10">
|
||||
<Badge className="bg-gradient-to-r from-yellow-400 to-orange-500 text-black px-6 py-1.5 font-semibold shadow-lg font-poppins">
|
||||
Most Popular
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<CardHeader className="text-center pb-4 pt-8 flex-shrink-0">
|
||||
<CardTitle className="font-merchant text-2xl leading-tight mb-3 text-gray-900">
|
||||
{cards[0].title}
|
||||
</CardTitle>
|
||||
<CardDescription className="font-poppins text-sm text-gray-600 leading-relaxed font-normal min-h-[48px] flex items-center justify-center px-4">
|
||||
{cards[0].description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
{/* Radio Button */}
|
||||
<div className="absolute top-5 right-5 z-10">
|
||||
<RadioGroupItem value={pass.id} id={pass.id} className="w-5 h-5" />
|
||||
{/* Pricing */}
|
||||
<div className="px-6 pb-6 flex-shrink-0">
|
||||
<div className="flex items-baseline justify-center gap-2 mb-2">
|
||||
<span className="text-5xl font-bold text-gray-900 font-poppins">
|
||||
${cards[0].adultPrice}
|
||||
</span>
|
||||
<span className="text-gray-500 font-poppins text-base">
|
||||
/ {passTypes[0].period}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Header - Fixed Height */}
|
||||
<CardHeader className="text-center pb-4 pt-8 flex-shrink-0">
|
||||
<CardTitle className="font-merchant text-2xl leading-tight mb-3 text-gray-900">
|
||||
{pass.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="font-poppins text-sm text-gray-600 leading-relaxed font-normal min-h-[48px] flex items-center justify-center px-4">
|
||||
{pass.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
{/* Attraction Images Grid */}
|
||||
<div className="px-6 pb-4 flex-shrink-0">
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="aspect-square rounded-xl overflow-hidden shadow-md hover:shadow-lg transition-all duration-300 ring-1 ring-gray-200 hover:ring-primary/50 group">
|
||||
<ImageWithFallback
|
||||
src="https://images.unsplash.com/photo-1639655001512-e4b58d4874b8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBGZWRlcmF0aW9uJTIwU3F1YXJlfGVufDF8fHx8MTc2MjQyMzkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
||||
alt="Federation Square"
|
||||
className="w-full h-full object-cover group-hover:scale-105 group-hover:brightness-110 transition-all duration-500"
|
||||
/>
|
||||
<div className="h-5 flex items-center justify-center">
|
||||
{cards[0].adultPrice && (
|
||||
<div className="text-sm text-gray-500 font-poppins">
|
||||
{/* Strikethrough price = originalPrice + $5 */}
|
||||
<span className="line-through mr-2">
|
||||
${parseFloat(cards[0].adultPrice) + 5}
|
||||
</span>
|
||||
<span className="text-green-600 font-medium">
|
||||
Save{" "}
|
||||
{Math.round(
|
||||
((5) / (parseFloat(cards[0].adultPrice) + 5)) * 100
|
||||
)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<div className="aspect-square rounded-xl overflow-hidden shadow-md hover:shadow-lg transition-all duration-300 ring-1 ring-gray-200 hover:ring-primary/50 group">
|
||||
<ImageWithFallback
|
||||
src="https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBSb3lhbCUyMEJvdGFuaWMlMjBHYXJkZW5zfGVufDF8fHx8MTc2MjQyMzk2NHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
||||
alt="Royal Botanic Gardens"
|
||||
className="w-full h-full object-cover group-hover:scale-105 group-hover:brightness-110 transition-all duration-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="aspect-square rounded-xl overflow-hidden shadow-md hover:shadow-lg transition-all duration-300 ring-1 ring-gray-200 hover:ring-primary/50 group">
|
||||
<ImageWithFallback
|
||||
src="https://images.unsplash.com/photo-1720044109127-0aee490512be?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBFdXJla2ElMjBTa3lkZWNrfGVufDF8fHx8MTc2MjQyMzk2NXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
||||
alt="Eureka Skydeck"
|
||||
className="w-full h-full object-cover group-hover:scale-105 group-hover:brightness-110 transition-all duration-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="aspect-square rounded-xl overflow-hidden shadow-md hover:shadow-lg transition-all duration-300 ring-1 ring-gray-200 hover:ring-primary/50 group">
|
||||
<ImageWithFallback
|
||||
src="https://images.unsplash.com/photo-1705464079585-0975f0aa5013?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBOYXRpb25hbCUyMEdhbGxlcnl8ZW58MXx8fHwxNzYyNDIzOTY0fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
||||
alt="National Gallery"
|
||||
className="w-full h-full object-cover group-hover:scale-105 group-hover:brightness-110 transition-all duration-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Section - Fixed Height */}
|
||||
<div className="px-6 pb-6 flex-shrink-0">
|
||||
<div className="flex items-baseline justify-center gap-2 mb-2">
|
||||
<span className="text-5xl font-bold text-gray-900 font-poppins">{pass.price}</span>
|
||||
<span className="text-gray-500 font-poppins text-base">/ {pass.period}</span>
|
||||
</div>
|
||||
<div className="h-5 flex items-center justify-center">
|
||||
{pass.originalPrice && (
|
||||
<div className="text-sm text-gray-500 font-poppins">
|
||||
<span className="line-through mr-2">{pass.originalPrice}</span>
|
||||
<span className="text-green-600 font-medium">Save {Math.round(((parseFloat(pass.originalPrice.slice(1)) - parseFloat(pass.price.slice(1))) / parseFloat(pass.originalPrice.slice(1))) * 100)}%</span>
|
||||
|
||||
<CardContent className="pt-0 pb-6 px-6 flex-grow flex flex-col">
|
||||
<div className="flex-grow mb-6">
|
||||
<div className="space-y-3">
|
||||
{passTypes[0].features.map((feature, index) => (
|
||||
<div key={index} className="flex items-start gap-3">
|
||||
<Check className="w-4 h-4 text-green-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-sm text-gray-700 font-poppins leading-relaxed font-normal">{feature}</span>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content - Flexible Height with Fixed Features Area */}
|
||||
<CardContent className="pt-0 pb-6 px-6 flex-grow flex flex-col">
|
||||
{/* Features List - Fixed height */}
|
||||
<div className="flex-grow mb-6">
|
||||
<div className="space-y-3">
|
||||
{pass.features.slice(0, 6).map((feature, index) => (
|
||||
<div key={index} className="flex items-start gap-3">
|
||||
<Check className="w-4 h-4 text-green-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-sm text-gray-700 font-poppins leading-relaxed font-normal">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex-shrink-0 space-y-3">
|
||||
<Button
|
||||
className={`w-full h-12 rounded-lg font-semibold transition-all cursor-pointer duration-300 font-poppins ${selectedPass === passTypes[0].id
|
||||
? "bg-primary hover:bg-primary/90 text-white hover:shadow-lg"
|
||||
: "bg-gray-400 hover:bg-gray-400 text-white hover:shadow-md"
|
||||
}`}
|
||||
onClick={user ? handleCheckoutClick : handleSignInClick}
|
||||
disabled={selectedPass !== passTypes[0].id}
|
||||
|
||||
>
|
||||
{user ? 'PURCHASE NOW' : 'LOGIN TO BUY PASS'}
|
||||
</Button>
|
||||
<p className="text-xs text-gray-500 text-center font-poppins font-normal leading-tight">
|
||||
✓ Free cancellation up to 24 hours • Instant delivery
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Unlimited Pass Card */}
|
||||
<div className="relative h-full">
|
||||
<Card
|
||||
className={`relative h-full flex flex-col transition-all duration-300 cursor-pointer ${selectedPass === passTypes[1].id
|
||||
? "ring-2 ring-red-500 shadow-lg" // 🔴 red border when selected
|
||||
: "border-gray-200 shadow-md hover:shadow-lg hover:border-primary/30"
|
||||
}`}
|
||||
onClick={() => setSelectedPass(passTypes[1].id)}
|
||||
|
||||
>
|
||||
{passTypes[1].popular && (
|
||||
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2 z-10">
|
||||
<Badge className="bg-gradient-to-r from-yellow-400 to-orange-500 text-black px-6 py-1.5 font-semibold shadow-lg font-poppins">
|
||||
Most Popular
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute top-5 right-5 z-10">
|
||||
{/* <RadioGroupItem value={passTypes[1].id} id={passTypes[1].id} className="w-5 h-5" /> */}
|
||||
</div>
|
||||
|
||||
<CardHeader className="text-center pb-4 pt-8 flex-shrink-0">
|
||||
<CardTitle className="font-merchant text-2xl leading-tight mb-3 text-gray-900">
|
||||
{cards[1].title}
|
||||
</CardTitle>
|
||||
<CardDescription className="font-poppins text-sm text-gray-600 leading-relaxed font-normal min-h-[48px] flex items-center justify-center px-4">
|
||||
{cards[1].description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
{/* Pricing */}
|
||||
<div className="px-6 pb-6 flex-shrink-0">
|
||||
<div className="flex items-baseline justify-center gap-2 mb-2">
|
||||
<span className="text-5xl font-bold text-gray-900 font-poppins">${cards[1].adultPrice}</span>
|
||||
<span className="text-gray-500 font-poppins text-base">/ {passTypes[1].period}</span>
|
||||
</div>
|
||||
<div className="h-5 flex items-center justify-center">
|
||||
{cards[1].adultPrice && (
|
||||
<div className="text-sm text-gray-500 font-poppins">
|
||||
{/* Strikethrough price = originalPrice + $5 */}
|
||||
<span className="line-through mr-2">
|
||||
${parseFloat(cards[1].adultPrice) + 5}
|
||||
</span>
|
||||
<span className="text-green-600 font-medium">
|
||||
Save{" "}
|
||||
{Math.round(
|
||||
((5) / (parseFloat(cards[1].adultPrice) + 5)) * 100
|
||||
)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Button - Pushed to bottom */}
|
||||
<div className="flex-shrink-0 space-y-3">
|
||||
<Button
|
||||
className={`w-full h-12 rounded-lg font-semibold transition-all cursor-pointer duration-300 font-poppins ${pass.popular
|
||||
? 'bg-primary hover:bg-primary/90 text-white shadow-md hover:shadow-lg'
|
||||
: 'bg-gray-900 hover:bg-gray-800 text-white hover:shadow-md'
|
||||
}`}
|
||||
onClick={user ? handleCheckoutClick : handleSignInClick}
|
||||
>
|
||||
{user ? 'PURCHASE NOW' : 'LOGIN TO BUY PASS'}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-gray-500 text-center font-poppins font-normal leading-tight">
|
||||
✓ Free cancellation up to 24 hours • Instant delivery
|
||||
</p>
|
||||
<CardContent className="pt-0 pb-6 px-6 flex-grow flex flex-col">
|
||||
<div className="flex-grow mb-6">
|
||||
<div className="space-y-3">
|
||||
{passTypes[1].features.map((feature, index) => (
|
||||
<div key={index} className="flex items-start gap-3">
|
||||
<Check className="w-4 h-4 text-green-500 mt-1 flex-shrink-0" />
|
||||
<span className="text-sm text-gray-700 font-poppins leading-relaxed font-normal">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 space-y-3">
|
||||
<Button
|
||||
className={`w-full h-12 rounded-lg font-semibold transition-all cursor-pointer duration-300 font-poppins ${selectedPass === passTypes[1].id
|
||||
? "bg-primary hover:bg-primary/90 text-white hover:shadow-lg"
|
||||
: "bg-gray-400 hover:bg-gray-400 text-white hover:shadow-md"
|
||||
}`}
|
||||
disabled={selectedPass !== passTypes[1].id}
|
||||
|
||||
onClick={user ? handleCheckoutClick : handleSignInClick}
|
||||
|
||||
>
|
||||
{user ? 'PURCHASE NOW' : 'LOGIN TO BUY PASS'}
|
||||
</Button>
|
||||
<p className="text-xs text-gray-500 text-center font-poppins font-normal leading-tight">
|
||||
✓ Free cancellation up to 24 hours • Instant delivery
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Good to Know Section */}
|
||||
<div className="mb-24">
|
||||
<div className="text-center mb-16">
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Camera, Edit3, Upload, Heart, Star, Download, Share2 } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
// import SubNavbar from './SubNavbar';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { CustomPostcards } from './CustomPostcards';
|
||||
import { HowItWorks } from './HowItWorks';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { CustomPostcards } from '../components/CustomPostcards';
|
||||
import { HowItWorks } from '../components/HowItWorks';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
import front from '../assets/front.jpg'
|
||||
// import front from '../assets/front.jpg'
|
||||
|
||||
|
||||
interface User {
|
||||
@@ -89,7 +89,7 @@ export function PostCardsPage({
|
||||
|
||||
<div className="flex mx-auto items-center px-4 relative z-10">
|
||||
<motion.div
|
||||
className="max-w-2xl mx-auto text-left"
|
||||
className="max-w-2xl mx-auto text-center"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
@@ -111,7 +111,7 @@ export function PostCardsPage({
|
||||
Start Creating Postcards
|
||||
</Button> */}
|
||||
</motion.div>
|
||||
< img src={front} alt='Postcard image' />
|
||||
{/* < img src={front} alt='Postcard image' /> */}
|
||||
</div>
|
||||
|
||||
{/* Decorative elements */}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, useScroll, useTransform } from 'motion/react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import Navbar from './Navbar';
|
||||
import Navbar from '../components/Navbar';
|
||||
// import { CitySubmenu } from './CitySubmenu';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { WhyChooseCityCards } from './WhyChooseCityCards';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { WhyChooseCityCards } from '../components/WhyChooseCityCards';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { ReviewsSection } from '../components/ReviewsSection';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import {
|
||||
ArrowLeft,
|
||||
User,
|
||||
CreditCard,
|
||||
Calendar,
|
||||
MapPin,
|
||||
import {
|
||||
ArrowLeft,
|
||||
User,
|
||||
CreditCard,
|
||||
Calendar,
|
||||
MapPin,
|
||||
Settings,
|
||||
Download,
|
||||
QrCode,
|
||||
@@ -15,17 +15,21 @@ import {
|
||||
Badge as BadgeIcon,
|
||||
Camera
|
||||
} from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Separator } from './ui/separator';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Label } from '../components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Separator } from '../components/ui/separator';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../components/ui/select';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { useGetUserProfileDetailsQuery, useUpdateUserProfileDetailsMutation } from '../Redux/services/profile.service';
|
||||
import { toast } from 'sonner';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
|
||||
interface ProfilePageProps {
|
||||
onBackClick: () => void;
|
||||
@@ -55,18 +59,6 @@ interface ProfilePageProps {
|
||||
currentPage: string;
|
||||
}
|
||||
|
||||
// Mock user data
|
||||
const mockUserData = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@example.com',
|
||||
phone: '+1 (555) 123-4567',
|
||||
country: 'us',
|
||||
address: '123 Main Street',
|
||||
city: 'New York',
|
||||
postalCode: '10001'
|
||||
};
|
||||
|
||||
// Mock passes data
|
||||
const mockPasses = [
|
||||
{
|
||||
@@ -86,7 +78,7 @@ const mockPasses = [
|
||||
usedAttractions: 8
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
id: '2',
|
||||
name: 'Melbourne Selective Card',
|
||||
city: 'Melbourne',
|
||||
type: 'Flexi Pass',
|
||||
@@ -160,55 +152,87 @@ export function ProfilePage({
|
||||
currentPage
|
||||
}: ProfilePageProps) {
|
||||
const [activeTab, setActiveTab] = useState('profile');
|
||||
const [formData, setFormData] = useState(mockUserData);
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
country: '',
|
||||
address1: '',
|
||||
address2: '',
|
||||
city: '',
|
||||
postalCode: ''
|
||||
});
|
||||
|
||||
const navigate = useNavigate()
|
||||
const userId = localStorage.getItem("userId")
|
||||
const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId)
|
||||
const [updateUserProfileDetails, { isLoading: savingChanges }] = useUpdateUserProfileDetailsMutation();
|
||||
const { data: passes, isLoading: loadingPasses } = useGetUserProfileDetailsQuery(userId)
|
||||
|
||||
useEffect(() => {
|
||||
if (userDetails) {
|
||||
setFormData({
|
||||
firstName: userDetails?.firstName,
|
||||
lastName: userDetails?.lastName,
|
||||
email: userDetails?.emailAddress,
|
||||
phone: userDetails?.mobileNumber,
|
||||
country: userDetails?.country,
|
||||
address1: userDetails?.address1,
|
||||
address2: userDetails?.address2,
|
||||
city: userDetails?.cityName,
|
||||
postalCode: userDetails?.zipCode
|
||||
})
|
||||
}
|
||||
|
||||
}, [userDetails])
|
||||
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSaveProfile = () => {
|
||||
console.log('Saving profile...', formData);
|
||||
// Handle profile save
|
||||
const handleSaveProfile = async () => {
|
||||
try {
|
||||
console.log("Saving profile...", formData);
|
||||
const response = await updateUserProfileDetails({ userDetails: formData, userId });
|
||||
console.log(response)
|
||||
toast.success("Profile updated successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error saving profile:", error);
|
||||
toast.error("Failed to update profile. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const activePasses = mockPasses.filter(pass => pass.status === 'active');
|
||||
const expiredPasses = mockPasses.filter(pass => pass.status === 'expired');
|
||||
|
||||
if (isLoading && loadingPasses) {
|
||||
return (
|
||||
<LoadingSpinner/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Navbar */}
|
||||
<Navbar
|
||||
<Navbar
|
||||
activeCity=""
|
||||
onCityChange={() => {}}
|
||||
onHomeClick={onHomeClick}
|
||||
onMelbourneClick={onMelbourneClick}
|
||||
onPassesClick={onPassesClick}
|
||||
onCheckoutClick={onCheckoutClick}
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
onAttractionsClick={onAttractionsClick}
|
||||
onBlogsClick={onBlogsClick}
|
||||
onHowItWorksClick={onHowItWorksClick}
|
||||
onFAQClick={onFAQClick}
|
||||
onPrivacyPolicyClick={onPrivacyPolicyClick}
|
||||
onAboutUsClick={onAboutUsClick}
|
||||
onProfileClick={onProfileClick}
|
||||
onCityCardsClick={onCityCardsClick}
|
||||
onMagicItineraryClick={onMagicItineraryClick}
|
||||
onPostCardsClick={onPostCardsClick}
|
||||
onOffersClick={onOffersClick}
|
||||
currentPage={currentPage}
|
||||
isUserSignedIn={true}
|
||||
user={{ email: "user@example.com", name: "John Doe" }}
|
||||
/>
|
||||
onCityChange={function (city: string): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}} />
|
||||
|
||||
{/* Header Section */}
|
||||
<section className="pt-40 pb-8 bg-gradient-to-br from-muted/30 to-background">
|
||||
<div className="container mx-auto px-4">
|
||||
{/* Back Button */}
|
||||
<motion.button
|
||||
onClick={onBackClick}
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6 transition-colors duration-200"
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6 transition-colors duration-200 cursor-pointer"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
@@ -225,7 +249,7 @@ export function ProfilePage({
|
||||
>
|
||||
<h1 className="font-poppins text-3xl md:text-4xl lg:text-5xl mb-4">
|
||||
<span className="font-light">My</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">Profile</span>
|
||||
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent pr-2">Profile</span>
|
||||
</h1>
|
||||
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600">
|
||||
Manage your account, passes, and travel itineraries
|
||||
@@ -312,7 +336,7 @@ export function ProfilePage({
|
||||
|
||||
<div>
|
||||
<Label htmlFor="country" className="font-poppins font-light">Country</Label>
|
||||
<Select value={formData.country} onValueChange={(value) => handleInputChange('country', value)}>
|
||||
{/* <Select value={formData.country} onValueChange={(value) => handleInputChange('country', value)}>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue placeholder="Select country" />
|
||||
</SelectTrigger>
|
||||
@@ -326,15 +350,33 @@ export function ProfilePage({
|
||||
<SelectItem value="in">India</SelectItem>
|
||||
<SelectItem value="jp">Japan</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Select> */}
|
||||
<Input
|
||||
id="country"
|
||||
value={formData.country}
|
||||
onChange={(e) => handleInputChange('country', e.target.value)}
|
||||
className="mt-1 font-poppins font-light"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="address" className="font-poppins font-light">Street Address</Label>
|
||||
<Label htmlFor="address1" className="font-poppins font-light">
|
||||
Address Line 1
|
||||
</Label>
|
||||
<Input
|
||||
id="address"
|
||||
value={formData.address}
|
||||
onChange={(e) => handleInputChange('address', e.target.value)}
|
||||
id="address1"
|
||||
value={formData.address1}
|
||||
onChange={(e) => handleInputChange('address1', e.target.value)}
|
||||
className="mt-1 font-poppins font-light mb-4"
|
||||
/>
|
||||
|
||||
<Label htmlFor="address2" className="font-poppins font-light">
|
||||
Address Line 2
|
||||
</Label>
|
||||
<Input
|
||||
id="address2"
|
||||
value={formData.address2}
|
||||
onChange={(e) => handleInputChange('address2', e.target.value)}
|
||||
className="mt-1 font-poppins font-light"
|
||||
/>
|
||||
</div>
|
||||
@@ -362,9 +404,9 @@ export function ProfilePage({
|
||||
|
||||
<Button
|
||||
onClick={handleSaveProfile}
|
||||
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-normal py-3 font-poppins"
|
||||
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-normal py-3 font-poppins cursor-pointer"
|
||||
>
|
||||
Save Changes
|
||||
{savingChanges ? "Saving Changes..." : "Save Changes"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -384,7 +426,7 @@ export function ProfilePage({
|
||||
// Determine which pass type to show
|
||||
const hasUnlimitedPass = activePasses.some(pass => pass.type === 'Unlimited Pass');
|
||||
const hasSelectivePass = activePasses.some(pass => pass.type === 'Flexi Pass');
|
||||
|
||||
|
||||
if (hasUnlimitedPass) {
|
||||
return (
|
||||
<>
|
||||
@@ -394,7 +436,7 @@ export function ProfilePage({
|
||||
<span className="text-primary">Melbourne Unlimited Card</span>
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
|
||||
Unlimited access to 25+ attractions. Visit as many places as you want with one simple card.
|
||||
Unlimited access to 25+ attractions. Visit as many places as you want with one simple card.
|
||||
Save up to 40% compared to individual tickets.
|
||||
</p>
|
||||
</div>
|
||||
@@ -423,13 +465,13 @@ export function ProfilePage({
|
||||
|
||||
{/* Purchase CTA */}
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
<Button
|
||||
onClick={onPassesClick}
|
||||
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
|
||||
>
|
||||
Purchase Unlimited Card
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onCityCardsClick}
|
||||
className="w-full font-poppins font-normal"
|
||||
@@ -449,7 +491,7 @@ export function ProfilePage({
|
||||
{' '}now
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
|
||||
Choose your own adventure with 12 hand-picked attractions. Perfect for visitors
|
||||
Choose your own adventure with 12 hand-picked attractions. Perfect for visitors
|
||||
who want flexibility and value.
|
||||
</p>
|
||||
</div>
|
||||
@@ -478,13 +520,13 @@ export function ProfilePage({
|
||||
|
||||
{/* Purchase CTA */}
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
<Button
|
||||
onClick={onPassesClick}
|
||||
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
|
||||
>
|
||||
Purchase Selective Card
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onCityCardsClick}
|
||||
className="w-full font-poppins font-normal"
|
||||
@@ -504,7 +546,7 @@ export function ProfilePage({
|
||||
{' '}now
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
|
||||
Explore Melbourne's best attractions with our flexible card options.
|
||||
Explore Melbourne's best attractions with our flexible card options.
|
||||
Choose unlimited access or select your favorites.
|
||||
</p>
|
||||
</div>
|
||||
@@ -535,13 +577,13 @@ export function ProfilePage({
|
||||
|
||||
{/* Purchase CTA */}
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
<Button
|
||||
onClick={onPassesClick}
|
||||
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
|
||||
>
|
||||
Explore All Cards
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onCityCardsClick}
|
||||
className="w-full font-poppins font-normal"
|
||||
@@ -572,7 +614,7 @@ export function ProfilePage({
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{activePasses.map((pass) => (
|
||||
<Card key={pass.id} className="overflow-hidden">
|
||||
<div
|
||||
<div
|
||||
className="flex cursor-pointer hover:bg-gray-50 transition-colors duration-200 rounded-lg p-2 -m-2"
|
||||
onClick={() => onDownloadAppClick?.()}
|
||||
>
|
||||
@@ -596,7 +638,7 @@ export function ProfilePage({
|
||||
<div className="text-sm text-gray-500 line-through font-poppins font-light">${pass.originalPrice}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2 text-sm font-poppins font-light">
|
||||
<div className="flex justify-between">
|
||||
<span>Attractions:</span>
|
||||
@@ -616,10 +658,10 @@ export function ProfilePage({
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Offers Button */}
|
||||
<div className="mt-8 text-center">
|
||||
<Button
|
||||
<Button
|
||||
onClick={onOffersClick}
|
||||
className="bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins px-8 py-3 font-normal"
|
||||
>
|
||||
@@ -660,7 +702,7 @@ export function ProfilePage({
|
||||
<div className="font-semibold text-lg font-poppins">${pass.price}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2 text-sm font-poppins font-light">
|
||||
<div className="flex justify-between">
|
||||
<span>Attractions visited:</span>
|
||||
@@ -689,7 +731,7 @@ export function ProfilePage({
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="font-poppins text-2xl font-normal">My Itineraries</h2>
|
||||
<Button
|
||||
<Button
|
||||
className="bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-normal"
|
||||
onClick={onCreateItineraryClick}
|
||||
>
|
||||
@@ -712,7 +754,7 @@ export function ProfilePage({
|
||||
{itinerary.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2 text-sm font-poppins font-light">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4 text-gray-500" />
|
||||
@@ -728,8 +770,8 @@ export function ProfilePage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full mt-4 font-poppins font-normal"
|
||||
onClick={onViewItineraryClick}
|
||||
>
|
||||
@@ -749,7 +791,7 @@ export function ProfilePage({
|
||||
<p className="text-gray-600 mb-6 font-poppins font-light">
|
||||
Create your first itinerary to plan your perfect trip
|
||||
</p>
|
||||
<Button
|
||||
<Button
|
||||
className="bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-normal"
|
||||
onClick={onCreateItineraryClick}
|
||||
>
|
||||
@@ -766,7 +808,7 @@ export function ProfilePage({
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer
|
||||
<Footer
|
||||
onHomeClick={onHomeClick}
|
||||
onMelbourneClick={onMelbourneClick}
|
||||
onPassesClick={onPassesClick}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Lock, Shield, CreditCard, Check, X, Tag } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Separator } from './ui/separator';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Label } from '../components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
|
||||
import { Separator } from '../components/ui/separator';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
615
src/pages/SuperSavingsPage.tsx
Normal file
615
src/pages/SuperSavingsPage.tsx
Normal file
@@ -0,0 +1,615 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Search, Filter, Star, MapPin, Clock, Tag, Heart, Share2, ChevronDown, ChevronRight, Check, Hotel, Plane, Building2, MapPinned, Home, Gift, Percent } from 'lucide-react';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Separator } from '../components/ui/separator';
|
||||
import { Checkbox } from '../components/ui/checkbox';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { TrustSection } from '../components/TrustSection';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { ReviewsSection } from '../components/ReviewsSection';
|
||||
import { TrustedCompanies } from '../components/TrustedCompanies';
|
||||
import { Layout } from '../Layout';
|
||||
import { useGetSelectedCityOffersQuery } from '../Redux/services/cities.service';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
|
||||
interface SuperSavingsPageProps {
|
||||
onBackClick: () => void;
|
||||
onHomeClick: () => void;
|
||||
onMelbourneClick: () => void;
|
||||
onPassesClick: () => void;
|
||||
onCheckoutClick: () => void;
|
||||
onSignInClick: () => void;
|
||||
onSignOutClick: () => void;
|
||||
onAttractionsClick: () => void;
|
||||
onBlogsClick: () => void;
|
||||
onHowItWorksClick: () => void;
|
||||
onFAQClick: () => void;
|
||||
onPrivacyPolicyClick: () => void;
|
||||
onAboutUsClick: () => void;
|
||||
onProfileClick: () => void;
|
||||
onCityCardsClick: () => void;
|
||||
onMagicItineraryClick: () => void;
|
||||
onPostCardsClick: () => void;
|
||||
onOffersClick: () => void;
|
||||
onSuperSavingsClick: () => void;
|
||||
onContactUsClick?: () => void;
|
||||
onEsimsClick?: () => void;
|
||||
onHotelDiscountsClick?: () => void;
|
||||
fromSource?: 'products' | 'passes';
|
||||
currentPage: string;
|
||||
user?: { email: string; name: string; } | null;
|
||||
}
|
||||
|
||||
// Categories data for the Super Savings Categories section
|
||||
const categoriesData = [
|
||||
{
|
||||
icon: Hotel,
|
||||
title: 'Luxury Hotels',
|
||||
description: 'Premium stays at unbeatable prices',
|
||||
savings: 'Up to 50% off',
|
||||
color: 'from-primary to-primary/80'
|
||||
},
|
||||
{
|
||||
icon: Plane,
|
||||
title: 'Travel Tours',
|
||||
description: 'Guided experiences worth your time',
|
||||
savings: 'Up to 45% off',
|
||||
color: 'from-primary to-primary/80'
|
||||
},
|
||||
{
|
||||
icon: MapPinned,
|
||||
title: 'Attractions',
|
||||
description: 'Must-see landmarks and experiences',
|
||||
savings: 'Up to 60% off',
|
||||
color: 'from-primary to-primary/80'
|
||||
},
|
||||
{
|
||||
icon: Building2,
|
||||
title: 'Shopping',
|
||||
description: 'Designer brands and local boutiques',
|
||||
savings: 'Up to 35% off',
|
||||
color: 'from-primary to-primary/80'
|
||||
},
|
||||
{
|
||||
icon: Gift,
|
||||
title: 'Wellness',
|
||||
description: 'Spa treatments and relaxation',
|
||||
savings: 'Up to 45% off',
|
||||
color: 'from-primary to-primary/80'
|
||||
}
|
||||
];
|
||||
|
||||
export function SuperSavingsPage({
|
||||
onBackClick,
|
||||
onHomeClick,
|
||||
onMelbourneClick,
|
||||
onPassesClick,
|
||||
onCheckoutClick,
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
onAttractionsClick,
|
||||
onBlogsClick,
|
||||
onHowItWorksClick,
|
||||
onFAQClick,
|
||||
onPrivacyPolicyClick,
|
||||
onAboutUsClick,
|
||||
onProfileClick,
|
||||
onCityCardsClick,
|
||||
onMagicItineraryClick,
|
||||
onPostCardsClick,
|
||||
onOffersClick,
|
||||
onSuperSavingsClick,
|
||||
onContactUsClick,
|
||||
onEsimsClick,
|
||||
onHotelDiscountsClick,
|
||||
fromSource = 'products',
|
||||
currentPage,
|
||||
user
|
||||
}: SuperSavingsPageProps) {
|
||||
|
||||
const [categoryId, setCategoryId] = useState(null)
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(4)
|
||||
const cityId = localStorage.getItem("cityId")
|
||||
const { data, isLoading } = useGetSelectedCityOffersQuery({ cityId, categoryId, page, limit })
|
||||
|
||||
// optional chaining ensures no crash if data is undefined
|
||||
const offers = data?.offers ?? [];
|
||||
const categories = data?.categories ?? [];
|
||||
|
||||
const totalOffers = data?.paginationData.total ?? 0;
|
||||
const totalPages = Math.ceil(totalOffers / limit);
|
||||
|
||||
const baseUrl = import.meta.env.VITE_BASE_URL;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<LoadingSpinner />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
activeCity="Melbourne"
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
>
|
||||
<div className="min-h-screen bg-background">
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-52 pb-20 overflow-hidden">
|
||||
{/* Background gradient */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-secondary/5 to-background">
|
||||
<ImageWithFallback
|
||||
src="https://images.unsplash.com/photo-1758787397603-bbd85bd06a2c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjB0cmF2ZWwlMjBkZXN0aW5hdGlvbnxlbnwxfHx8fDE3NjI0MjQ3NTV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
||||
alt="Melbourne Travel Destination"
|
||||
className="w-full h-full object-cover opacity-20"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-white/40 via-white/60 to-white/90"></div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<motion.div
|
||||
className="max-w-4xl mx-auto text-center"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h1 className="font-poppins text-4xl md:text-5xl lg:text-6xl leading-tight mb-6">
|
||||
<span className="font-light">Unlock</span>{' '}
|
||||
<span className="pr-2 font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Super Savings
|
||||
</span>
|
||||
</h1>
|
||||
<p className="font-poppins text-lg md:text-xl leading-relaxed text-gray-600 mb-8 max-w-2xl mx-auto">
|
||||
Experience incredible discounts up to 65% off on premium experiences, luxury stays, and unforgettable attractions.
|
||||
</p>
|
||||
<Button
|
||||
onClick={onSignInClick}
|
||||
className="bg-primary hover:bg-primary/90 text-white px-8 py-6 font-poppins font-semibold"
|
||||
>
|
||||
Start Saving Now
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute top-20 left-10 w-20 h-20 bg-primary/10 rounded-full blur-xl"></div>
|
||||
<div className="absolute bottom-20 right-10 w-32 h-32 bg-secondary/10 rounded-full blur-xl"></div>
|
||||
</section>
|
||||
|
||||
{/* Trusted By Companies Section */}
|
||||
<section className="py-12 bg-background">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-6xl mx-auto text-center">
|
||||
<div className="mb-10">
|
||||
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight mb-4">
|
||||
<span>Trusted by the </span>
|
||||
<span className="font-semibold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">world's best</span>
|
||||
</h2>
|
||||
<p className="font-poppins leading-relaxed text-muted-foreground max-w-2xl mx-auto">
|
||||
Join thousands of savvy travelers enjoying massive savings on premium experiences
|
||||
</p>
|
||||
</div>
|
||||
<TrustedCompanies />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured Super Savings Section */}
|
||||
<section className="py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="font-poppins text-3xl md:text-4xl lg:text-5xl leading-tight mb-4">
|
||||
<span className="font-light">Featured</span>{' '}
|
||||
<span className="pr-2 font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Super Savings
|
||||
</span>
|
||||
</h2>
|
||||
<p className="font-poppins leading-relaxed text-gray-600 max-w-2xl mx-auto">
|
||||
Check out our biggest discounts and start saving on premium experiences
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="container mx-auto px-4 pt-51 pb-16">
|
||||
<div className="flex gap-8">
|
||||
{/* Left Sidebar - Filters */}
|
||||
<div className="w-64 flex-shrink-0">
|
||||
<Card className="p-8 sticky top-48">
|
||||
<div className="space-y-6">
|
||||
{/* Search by header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-0 w-6 border-t-[3px] border-gray-800 rotate-90"></div>
|
||||
<h3 className="font-poppins font-medium text-gray-800">Search by</h3>
|
||||
</div>
|
||||
|
||||
{/* Filter categories */}
|
||||
<div className="space-y-4">
|
||||
{categories.map((category: any) => (
|
||||
<div key={category.id} className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
id={category.id}
|
||||
checked={categoryId === category.id}
|
||||
onCheckedChange={(checked: boolean) => {
|
||||
if (checked) {
|
||||
setCategoryId(category.id); // select this category
|
||||
} else {
|
||||
setCategoryId(null); // unselect if unchecked
|
||||
}
|
||||
}}
|
||||
className="border-gray-400"
|
||||
/>
|
||||
<label
|
||||
htmlFor={category.id}
|
||||
className="font-poppins text-sm text-gray-700 cursor-pointer flex-1"
|
||||
>
|
||||
{category.categoryName} ({category.offerCount})
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1">
|
||||
{/* Breadcrumb */}
|
||||
<div className="mb-8">
|
||||
<p className="font-poppins text-sm text-gray-800">
|
||||
{fromSource === 'passes' ? (
|
||||
<>
|
||||
<span>My Profile{'>'}My passes{'>'}</span>
|
||||
<span className="font-semibold">Super Savings</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Our Products{'>'}</span>
|
||||
<span className="font-semibold">Super Savings</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Header Section */}
|
||||
<div className="mb-8">
|
||||
<h1 className="font-poppins md:text-5xl font-medium text-gray-800 leading-tight text-[24px]">
|
||||
Super Savings
|
||||
</h1>
|
||||
<p className="font-poppins text-gray-600 mt-2">
|
||||
Exclusive discounts up to 65% off on premium experiences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Offers Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6 mb-16">
|
||||
{offers.map((offer: any, index: number) => (
|
||||
<motion.div
|
||||
key={offer.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
>
|
||||
<Card className="bg-white border border-gray-200 rounded-xl overflow-hidden h-full hover:shadow-lg transition-shadow duration-300 relative">
|
||||
{/* Image */}
|
||||
<div className="relative h-52 bg-gray-300">
|
||||
<ImageWithFallback
|
||||
src={`${baseUrl}/${offer.websiteBannerImage}`}
|
||||
alt={offer.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{/* <Button className="absolute bottom-4 right-3 bg-white rounded-full shadow-lg w-9 h-9 p-0 hover:bg-gray-100 transition-colors">
|
||||
<Heart className="w-4 h-4 text-gray-800" />
|
||||
</Button> */}
|
||||
|
||||
{/* Discount Badge */}
|
||||
<div className="absolute top-4 left-4 bg-primary text-white px-3 py-1.5 rounded-lg">
|
||||
<span className="font-poppins font-semibold text-sm">{offer.offerCode}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="space-y-4 px-4 py-4">
|
||||
{/* Business Name */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 bg-gray-300 rounded"></div>
|
||||
<span className="font-poppins text-sm text-gray-500">{offer.partnerName}</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-poppins font-medium text-gray-900 leading-relaxed min-h-[48px]">
|
||||
{offer.description}
|
||||
</h3>
|
||||
|
||||
{/* Saved Amount Display */}
|
||||
<div className="bg-gradient-to-r from-primary/10 to-secondary/10 h-12 flex items-center justify-center rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* <Percent className="w-4 h-4 text-primary" /> */}
|
||||
<span className="font-poppins font-semibold text-primary">
|
||||
{offer.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Minimal Pagination */}
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Previous button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-8 h-8 p-0 font-poppins"
|
||||
disabled={page === 1}
|
||||
onClick={() => setPage(prev => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronRight className="w-4 h-4 rotate-180" />
|
||||
</Button>
|
||||
|
||||
{/* Page numbers */}
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => (
|
||||
<Button
|
||||
key={p}
|
||||
variant={page === p ? "default" : "ghost"}
|
||||
size="sm"
|
||||
className={`w-8 h-8 p-0 font-poppins ${page === p ? 'bg-primary hover:bg-primary/90' : ''}`}
|
||||
onClick={() => setPage(p)}
|
||||
>
|
||||
{p}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Next button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-8 h-8 p-0 font-poppins"
|
||||
disabled={page === totalPages}
|
||||
onClick={() => setPage(prev => Math.min(prev + 1, totalPages))}
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Button
|
||||
onClick={onSignInClick}
|
||||
variant="outline"
|
||||
className="font-poppins font-medium border-primary text-primary hover:bg-primary hover:text-white"
|
||||
>
|
||||
View All Super Savings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How It Works Section */}
|
||||
<section className="py-24 relative overflow-hidden bg-[#FFF5F5]/50">
|
||||
{/* Background decorative elements */}
|
||||
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-primary/5 rounded-full blur-[100px] pointer-events-none -mr-40 -mt-40"></div>
|
||||
<div className="absolute bottom-0 left-0 w-[400px] h-[400px] bg-primary/5 rounded-full blur-[80px] pointer-events-none -ml-20 -mb-20"></div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<motion.div
|
||||
className="text-center mb-20"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
<div className="h-px w-8 bg-primary"></div>
|
||||
<span className="text-primary font-semibold tracking-widest uppercase text-xs font-poppins">Simple Process</span>
|
||||
<div className="h-px w-8 bg-primary"></div>
|
||||
</div>
|
||||
<h2 className="font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-6">
|
||||
Start Saving in <span className="text-primary italic">Minutes</span>
|
||||
</h2>
|
||||
<p className="font-poppins text-lg text-gray-500 font-light max-w-xl mx-auto leading-relaxed">
|
||||
Your journey to smarter travel and bigger savings begins with three simple steps.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="relative max-w-6xl mx-auto">
|
||||
{/* Connecting line for desktop */}
|
||||
<div className="hidden md:block absolute top-12 left-[16%] right-[16%] h-0.5 border-t-2 border-dashed border-primary/20 z-0"></div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 md:gap-8">
|
||||
{[
|
||||
{
|
||||
step: '01',
|
||||
title: 'Unlock Access',
|
||||
description: 'Get your CityCards pass to instantly activate membership perks.',
|
||||
icon: MapPinned
|
||||
},
|
||||
{
|
||||
step: '02',
|
||||
title: 'Discover Deals',
|
||||
description: 'Browse exclusive offers on hotels, flights, and experiences.',
|
||||
icon: Search
|
||||
},
|
||||
{
|
||||
step: '03',
|
||||
title: 'Enjoy Savings',
|
||||
description: 'Redeem discounts instantly and watch your travel budget grow.',
|
||||
icon: Percent
|
||||
}
|
||||
].map((item, index) => (
|
||||
<motion.div
|
||||
key={item.step}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.2 }}
|
||||
className="relative z-10 group"
|
||||
>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{/* Icon Container */}
|
||||
<div className="relative mb-8">
|
||||
<div className="w-24 h-24 rounded-[2rem] bg-white flex items-center justify-center shadow-[0_8px_30px_rgb(0,0,0,0.06)] group-hover:scale-110 group-hover:-rotate-3 transition-transform duration-300 border border-primary/10 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-primary/5 group-hover:bg-primary/10 transition-colors duration-300"></div>
|
||||
<item.icon className="w-10 h-10 text-primary relative z-10" />
|
||||
</div>
|
||||
<div className="absolute -top-3 -right-3 w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-bold font-poppins text-sm border-4 border-white shadow-md">
|
||||
{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="font-merchant text-2xl text-gray-900 mb-3 group-hover:text-primary transition-colors">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="font-poppins text-gray-500 font-light leading-relaxed max-w-xs">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Categories Section */}
|
||||
<section className="py-24 bg-gray-50/50 relative overflow-hidden">
|
||||
{/* Abstract Travel Patterns */}
|
||||
<div className="absolute top-0 right-0 w-[800px] h-[800px] bg-gradient-to-bl from-primary/5 via-secondary/5 to-transparent rounded-full blur-3xl pointer-events-none -mr-40 -mt-40"></div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
{/* Section Header */}
|
||||
<div className="flex flex-col md:flex-row items-end justify-between mb-12 gap-6">
|
||||
<motion.div
|
||||
className="max-w-2xl"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="h-px w-8 bg-primary"></div>
|
||||
<span className="text-primary font-semibold tracking-widest uppercase text-xs font-poppins">Explore Collections</span>
|
||||
</div>
|
||||
<h2 className="font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-4 leading-tight">
|
||||
Curated for the <span className="text-primary italic">Modern Traveler</span>
|
||||
</h2>
|
||||
<p className="font-poppins text-lg text-gray-500 font-light max-w-lg leading-relaxed">
|
||||
Discover exclusive savings across our most sought-after travel categories.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="hidden md:block"
|
||||
>
|
||||
<Button
|
||||
onClick={onSignInClick}
|
||||
variant="ghost"
|
||||
className="group gap-2 text-gray-600 hover:text-primary font-poppins text-lg"
|
||||
>
|
||||
View All Categories
|
||||
<span className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-all duration-300">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Bento Grid Layout */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{categoriesData.map((category, index) => (
|
||||
<motion.div
|
||||
key={category.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: index * 0.1 }}
|
||||
className={`${index === 0 ? 'md:col-span-2' : ''} group`}
|
||||
>
|
||||
<div
|
||||
onClick={onSignInClick}
|
||||
className={`
|
||||
relative h-full bg-white rounded-[2rem] p-8 border border-gray-100
|
||||
shadow-[0_4px_20px_-4px_rgba(0,0,0,0.05)] hover:shadow-[0_20px_40px_-10px_rgba(0,0,0,0.1)]
|
||||
transition-all duration-500 cursor-pointer overflow-hidden flex flex-col justify-between
|
||||
group-hover:border-primary/20
|
||||
`}
|
||||
>
|
||||
{/* Background Gradient Hover */}
|
||||
<div className={`absolute inset-0 bg-gradient-to-br ${category.color} opacity-0 group-hover:opacity-[0.03] transition-opacity duration-500`}></div>
|
||||
|
||||
{/* Large Watermark Icon for visual depth */}
|
||||
<category.icon className="absolute -bottom-8 -right-8 w-48 h-48 text-gray-50 group-hover:text-primary/5 transition-colors duration-500 -rotate-12" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className={`
|
||||
w-14 h-14 rounded-2xl flex items-center justify-center shadow-sm transition-all duration-300
|
||||
${index === 0 ? 'bg-primary text-white shadow-primary/20' : 'bg-gray-50 text-gray-600 group-hover:bg-primary group-hover:text-white'}
|
||||
`}>
|
||||
<category.icon className="w-7 h-7" />
|
||||
</div>
|
||||
<Badge className="bg-emerald-50 text-emerald-600 border-emerald-100 font-poppins font-medium px-3 py-1.5">
|
||||
{category.savings}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<h3 className={`font-merchant text-gray-900 mb-2 group-hover:text-primary transition-colors ${index === 0 ? 'text-4xl' : 'text-2xl'}`}>
|
||||
{category.title}
|
||||
</h3>
|
||||
<p className="font-poppins text-gray-500 font-light leading-relaxed max-w-md">
|
||||
{category.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 mt-8 flex items-center gap-2 text-primary font-poppins font-medium text-sm opacity-0 group-hover:opacity-100 transform translate-y-2 group-hover:translate-y-0 transition-all duration-300">
|
||||
<span>Explore Deals</span>
|
||||
<ArrowLeft className="w-4 h-4 rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile View All Button */}
|
||||
<div className="mt-8 md:hidden text-center">
|
||||
<Button
|
||||
onClick={onSignInClick}
|
||||
className="w-full bg-primary hover:bg-primary/90 text-white font-poppins font-semibold py-6 rounded-xl shadow-lg shadow-primary/20"
|
||||
>
|
||||
Browse All Categories
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Access Your CityCards Section */}
|
||||
<section className="py-20 bg-muted/30">
|
||||
<MobileAppSection />
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ArrowRight, Hotel, Mail, MapPin, Sparkles, Ticket, Wifi } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
import { Layout } from '../Layout';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { SmartSaving } from './SmartSaving';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Button } from './ui/button';
|
||||
import { WhatsIncludedHero } from './WhatsIncludedHero';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { SmartSaving } from '../components/SmartSaving';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { WhatsIncludedHero } from '../components/WhatsIncludedHero';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -5,12 +5,12 @@
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
|
||||
Reference in New Issue
Block a user