main #6

Merged
Hemant.Vishwakarma merged 16 commits from main into testing 2026-04-14 12:25:05 +00:00
48 changed files with 1188 additions and 948 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

@@ -31,7 +31,14 @@ export function CitySelectionDialog({
const { data: cities, isLoading } = useGetCityListWithBannerQuery({ search })
if (isLoading) {
return <div>Loading...</div>
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>
);
}
@@ -40,7 +47,7 @@ export function CitySelectionDialog({
// ✅ Call the onCitySelect callback if provided (passing cityId)
if (onCitySelect) {
onCitySelect(String(city.id));
onCitySelect(String(city.cityName));
} else {
// ✅ Default behavior: navigate to passes page
navigate(`/passes?city=${encodeURIComponent(city.cityName)}`);

View File

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

View File

@@ -113,8 +113,15 @@ export function LandingUpcomingCities() {
const { data, isLoading } = useGetUpcomingCitiesQuery(listType)
if(isLoading){
return <div>Loading...</div>
if (isLoading) {
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>
);
}
const handleMouseDown = (e: React.MouseEvent) => {

View File

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

View File

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

View File

@@ -143,11 +143,11 @@ export default function Navbar({
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: [
@@ -186,11 +186,11 @@ export default function Navbar({
melbourneLabel: 'Your Card'
},
{
label: 'Your PostCard',
label: 'Your Postcard',
path: '/postcards',
isShared: true,
landingLabel: 'Your PostCard',
melbourneLabel: 'Your PostCard'
landingLabel: 'Your Postcard',
melbourneLabel: 'Your Postcard'
}
]
};
@@ -299,11 +299,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 +334,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 +533,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 }}

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

View File

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

@@ -32,3 +32,5 @@ declare module '*.mp4' {
const src: string;
export default src;
}
declare module "*.css";

View File

@@ -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 (H1H6), 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
-->

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Clock, Users, Calendar, MapPin, Star, Check, X, ChevronLeft, ChevronRight } from 'lucide-react';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { Card, } from './ui/card';
import { ImageWithFallback } from './figma/ImageWithFallback';
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';
import { useParams } from 'react-router-dom';
import { useGetAttractionDetailsByIdQuery } from '../Redux/services/attractions.service';
@@ -31,7 +31,14 @@ export function AttractionDetailsPage({
const { data: attraction, isLoading } = useGetAttractionDetailsByIdQuery(Number(attractionId));
if (isLoading) {
return <div>loading...</div>
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>
);
}
return (
@@ -40,7 +47,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 */}
@@ -82,7 +89,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>
@@ -99,10 +106,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"
/>

View File

@@ -2,12 +2,12 @@ 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';
interface User {
@@ -230,7 +230,7 @@ export function AttractionsPage({
const [selectedPassType, setSelectedPassType] = useState<string | null>(null);
const cityId = 1
const { data: filterData, isLoading } = useGetAttractionFiltersQuery(cityId)
const { data: attractions } = useGetCustomerAttractionsQuery({
cityId, // required
@@ -239,9 +239,16 @@ export function AttractionsPage({
cardType: selectedPassType, // optional
search, // optional
});
if (isLoading) {
return <div>Loading...</div>
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>
);
}
const handleAttractionClick = (attractionId: string) => {
@@ -254,7 +261,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
@@ -403,7 +410,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>
))}
@@ -493,7 +500,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();
}}

View File

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

View File

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

View File

@@ -1,20 +1,20 @@
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 { 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 Navbar from './Navbar';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
import { Layout } from '../Layout';
interface CheckoutPageProps {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,22 @@
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 Navbar from '../components/Navbar';
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 { Footer } from '../components/Footer';
// import { MinimalHeroBanner } from './MinimalHeroBanner';
import { Layout } from '../Layout';
import { HeroBannerCarousel } from './HeroBannerCarousel';
import { HotelEsimOffers } from './HotelEsimOffers';
import { HeroBannerCarousel } from '../components/HeroBannerCarousel';
import { HotelEsimOffers } from '../components/HotelEsimOffers';
interface User {
email: string;

View File

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

View File

@@ -1,14 +1,14 @@
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';

View File

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

View File

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

View File

@@ -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,20 @@ 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';
interface ProfilePageProps {
onBackClick: () => void;
@@ -55,18 +58,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 +77,7 @@ const mockPasses = [
usedAttractions: 8
},
{
id: '2',
id: '2',
name: 'Melbourne Selective Card',
city: 'Melbourne',
type: 'Flexi Pass',
@@ -160,55 +151,92 @@ 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 (
<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>
);
}
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 +253,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 +340,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 +354,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 +408,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 +430,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 +440,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 +469,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 +495,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 +524,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 +550,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 +581,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 +618,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 +642,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 +662,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 +706,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 +735,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 +758,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 +774,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 +795,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 +812,7 @@ export function ProfilePage({
</section>
{/* Footer */}
<Footer
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}

View File

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

View File

@@ -1,19 +1,19 @@
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 { 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';
interface SuperSavingsPageProps {

View File

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

View File

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