main #6
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CityCards Travel 22-8-2025</title>
|
||||
<title>CityCards Customer-web</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -54,7 +54,7 @@
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"recharts": "^2.15.2",
|
||||
"sonner": "^2.0.3",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "*",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"vaul": "^1.1.2"
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"recharts": "^2.15.2",
|
||||
"sonner": "^2.0.3",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "*",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"vaul": "^1.1.2"
|
||||
|
||||
@@ -2,38 +2,36 @@ import { Routes, Route, useParams, useLocation, useNavigate } from 'react-router
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
|
||||
// Import all your pages
|
||||
import { LoginModal } from './components/LoginModal';
|
||||
import { MelbournePage } from './components/MelbournePage';
|
||||
import { PassesPage } from './components/PassesPage';
|
||||
import { AttractionsPage } from './components/AttractionsPage';
|
||||
import { AttractionDetailsPage } from './components/AttractionDetailsPage';
|
||||
import { CheckoutPage } from './components/CheckoutPage';
|
||||
import { SecureCheckoutPage } from './components/SecureCheckoutPage';
|
||||
import { BlogsPage } from './components/BlogsPage';
|
||||
import { BlogDetailsPage } from './components/BlogDetailsPage';
|
||||
import { MelbournePage } from './pages/MelbournePage';
|
||||
import { PassesPage } from './pages/PassesPage';
|
||||
import { AttractionsPage } from './pages/AttractionsPage';
|
||||
import { AttractionDetailsPage } from './pages/AttractionDetailsPage';
|
||||
import { CheckoutPage } from './pages/CheckoutPage';
|
||||
import { SecureCheckoutPage } from './pages/SecureCheckoutPage';
|
||||
import { BlogsPage } from './pages/BlogsPage';
|
||||
import { BlogDetailsPage } from './pages/BlogDetailsPage';
|
||||
import { HowItWorksPage } from './components/HowItWorksPage';
|
||||
import { FAQPage } from './components/FAQPage';
|
||||
import { PrivacyPolicyPage } from './components/PrivacyPolicyPage';
|
||||
import { AboutUsPage } from './components/AboutUsPage';
|
||||
import { ProfilePage } from './components/ProfilePage';
|
||||
import { CreateMagicItineraryPage } from './components/CreateMagicItineraryPage';
|
||||
import { ItineraryViewPage } from './components/ItineraryViewPage';
|
||||
import { OffersPage } from './components/OffersPage';
|
||||
import { CityCardsPage } from './components/CityCardsPage';
|
||||
import { MagicItineraryPage } from './components/MagicItineraryPage';
|
||||
import { PostCardsPage } from './components/PostCardsPage';
|
||||
import { DownloadAppPage } from './components/DownloadAppPage';
|
||||
import { EsimsPage } from './components/EsimsPage';
|
||||
import { HotelDiscountsPage } from './components/HotelDiscountsPage';
|
||||
import { ContactUsPage } from './components/ContactUsPage';
|
||||
import { PrivacyPolicyPage } from './pages/PrivacyPolicyPage';
|
||||
import { AboutUsPage } from './pages/AboutUsPage';
|
||||
import { ProfilePage } from './pages/ProfilePage';
|
||||
import { CreateMagicItineraryPage } from './pages/CreateMagicItineraryPage';
|
||||
import { ItineraryViewPage } from './pages/ItineraryViewPage';
|
||||
import { OffersPage } from './pages/OffersPage';
|
||||
import { CityCardsPage } from './pages/CityCardsPage';
|
||||
import { MagicItineraryPage } from './pages/MagicItineraryPage';
|
||||
import { PostCardsPage } from './pages/PostCardsPage';
|
||||
import { DownloadAppPage } from './pages/DownloadAppPage';
|
||||
import { HotelDiscountsPage } from './pages/HotelDiscountsPage';
|
||||
import { ContactUsPage } from './pages/ContactUsPage';
|
||||
|
||||
import { pageTransition } from './utils/animations';
|
||||
import { LandingPage } from './pages/landingPage';
|
||||
import ComingSoonPage from './pages/ComingSoonPage';
|
||||
import { SuperSavingsPage } from './components/SuperSavingsPage';
|
||||
import { WhatsIncluded } from './components/WhatsIncluded';
|
||||
import { LandingMagicItineraryPage } from './components/LandingMagicItineraryPage';
|
||||
import { DiscoverPage } from './components/DiscoverPage';
|
||||
import { SuperSavingsPage } from './pages/SuperSavingsPage';
|
||||
import { WhatsIncluded } from './pages/WhatsIncluded';
|
||||
import { LandingMagicItineraryPage } from './pages/LandingMagicItineraryPage';
|
||||
import { DiscoverPage } from './pages/DiscoverPage';
|
||||
|
||||
// User type definition
|
||||
interface User {
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { fakeApi } from "./services/fakeApi.service";
|
||||
import { attractionsApi } from "./services/attractions.service";
|
||||
import { citiesApi } from "./services/cities.service";
|
||||
import { authApi } from "./services/auth.service";
|
||||
import { profileApi } from "./services/profile.service";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
[fakeApi.reducerPath]:fakeApi.reducer,
|
||||
[attractionsApi.reducerPath]:attractionsApi.reducer,
|
||||
[citiesApi.reducerPath]:citiesApi.reducer
|
||||
[attractionsApi.reducerPath]: attractionsApi.reducer,
|
||||
[citiesApi.reducerPath]: citiesApi.reducer,
|
||||
[authApi.reducerPath]: authApi.reducer,
|
||||
[profileApi.reducerPath]: profileApi.reducer
|
||||
|
||||
},
|
||||
|
||||
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(
|
||||
|
||||
fakeApi.middleware,
|
||||
attractionsApi.middleware,
|
||||
citiesApi.middleware
|
||||
attractionsApi.middleware,
|
||||
citiesApi.middleware,
|
||||
authApi.middleware,
|
||||
profileApi.middleware
|
||||
),
|
||||
});
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
@@ -8,10 +8,9 @@ export const baseQuery = fetchBaseQuery({
|
||||
const token = localStorage.getItem("accessToken");
|
||||
if (token) {
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
// headers.set("access-token", token);
|
||||
headers.set("access-token", token);
|
||||
}
|
||||
// headers.set("Content-Type", "application/json");
|
||||
return headers;
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
53
src/Redux/services/auth.service.ts
Normal file
53
src/Redux/services/auth.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import { baseQuery } from "../baseQuery";
|
||||
|
||||
export const authApi = createApi({
|
||||
reducerPath: "authApi",
|
||||
baseQuery: baseQuery,
|
||||
|
||||
tagTypes: ["profile", "Transaction"],
|
||||
|
||||
|
||||
endpoints: (builder) => ({
|
||||
// Login
|
||||
|
||||
login: builder.mutation({
|
||||
query: (credentials) => ({
|
||||
url: "/website/send-otp",
|
||||
method: "POST",
|
||||
body: credentials,
|
||||
}),
|
||||
}),
|
||||
|
||||
verifyOtp: builder.mutation({
|
||||
query: (credentials) => ({
|
||||
url: "/website/user/verify-otp",
|
||||
method: "POST",
|
||||
body: credentials,
|
||||
}),
|
||||
}),
|
||||
|
||||
register: builder.mutation({
|
||||
query: (credentials) => ({
|
||||
url: "/website/user/register",
|
||||
method: "POST",
|
||||
body: credentials,
|
||||
}),
|
||||
}),
|
||||
|
||||
logoutUser: builder.mutation({
|
||||
query: () => ({
|
||||
url: "/website/user/logout",
|
||||
method: "POST"
|
||||
})
|
||||
})
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useLoginMutation,
|
||||
useVerifyOtpMutation,
|
||||
useRegisterMutation,
|
||||
useLogoutUserMutation
|
||||
} = authApi;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||
|
||||
export const fakeApi = createApi({
|
||||
reducerPath: 'fakeApi',
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: " https://fakestoreapi.com",
|
||||
|
||||
}),
|
||||
endpoints: (builder) => ({
|
||||
getProducts: builder.query<any, void>({
|
||||
query: () => ({
|
||||
url: 'products',
|
||||
method: 'GET',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export const { useGetProductsQuery} = fakeApi
|
||||
45
src/Redux/services/profile.service.ts
Normal file
45
src/Redux/services/profile.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import { baseQuery } from "../baseQuery";
|
||||
|
||||
export const profileApi = createApi({
|
||||
reducerPath: "profileApi",
|
||||
baseQuery,
|
||||
|
||||
tagTypes: ["userDetails"],
|
||||
|
||||
endpoints: (builder) => ({
|
||||
|
||||
getUserProfileDetails: builder.query({
|
||||
query: (id) => `/website/user/${id}`,
|
||||
providesTags: ["userDetails"]
|
||||
}),
|
||||
|
||||
updateUserProfileDetails: builder.mutation({
|
||||
query: ({ userDetails, userId }) => ({ // keep the name of the variables being passed here same as when calling the mutation hook
|
||||
url: `/website/user/${userId}`,
|
||||
method: "PUT",
|
||||
body: userDetails
|
||||
}),
|
||||
invalidatesTags: ["userDetails"]
|
||||
}),
|
||||
|
||||
getUserPasses: builder.query({
|
||||
query: ({ cardMode, sort }) => {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if(cardMode) params.append('cardMode',cardMode);
|
||||
if(sort) params.append('sort',sort);
|
||||
|
||||
return `/website/passes/all?${params.toString()}`
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetUserProfileDetailsQuery,
|
||||
useUpdateUserProfileDetailsMutation,
|
||||
useGetUserPassesQuery
|
||||
} = profileApi;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { motion } from 'motion/react';
|
||||
import BeforeLogin from '../imports/BeforeLogin';
|
||||
import AfterLogin from '../imports/AfterLogin';
|
||||
import BeforeLogin from './BeforeLogin';
|
||||
import AfterLogin from './AfterLogin';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
|
||||
@@ -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)}`);
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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('');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
391
src/components/RegisterModal.tsx
Normal file
391
src/components/RegisterModal.tsx
Normal file
@@ -0,0 +1,391 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { X } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { useRegisterMutation } from '../Redux/services/auth.service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface RegisterModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onLoginClick: () => void;
|
||||
}
|
||||
|
||||
export function RegisterModal({ isOpen, onClose, onLoginClick }: RegisterModalProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
emailAddress: '',
|
||||
isdCode: '+91',
|
||||
mobileNumber: '',
|
||||
address1: '',
|
||||
address2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
country: 'Australia',
|
||||
postalCode: ''
|
||||
});
|
||||
const [helperText, setHelperText] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [register, { isLoading: isRegistering }] = useRegisterMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setFormData({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
emailAddress: '',
|
||||
isdCode: '+91',
|
||||
mobileNumber: '',
|
||||
address1: '',
|
||||
address2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
country: 'Australia',
|
||||
postalCode: ''
|
||||
});
|
||||
setHelperText('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
if (!formData.firstName.trim()) {
|
||||
toast.error('First name is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.lastName.trim()) {
|
||||
toast.error('Last name is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.emailAddress.trim() || !formData.emailAddress.includes('@')) {
|
||||
toast.error('Please enter a valid email address');
|
||||
return false;
|
||||
}
|
||||
if (!formData.mobileNumber.trim()) {
|
||||
toast.error('Mobile number is required');
|
||||
return false;
|
||||
}
|
||||
if (!/^\d+$/.test(formData.mobileNumber.trim())) {
|
||||
toast.error('Mobile number must contain only digits');
|
||||
return false;
|
||||
}
|
||||
if (!formData.address1.trim()) {
|
||||
toast.error('Address is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.city.trim()) {
|
||||
toast.error('City is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.state.trim()) {
|
||||
toast.error('State is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.postalCode.trim()) {
|
||||
toast.error('Postal code is required');
|
||||
return false;
|
||||
}
|
||||
if (!/^\d+$/.test(formData.postalCode.trim())) {
|
||||
toast.error('Postal code must contain only digits');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHelperText('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await register(formData).unwrap();
|
||||
console.log('Registration response:', response);
|
||||
|
||||
toast.success('Registration successful! Please login.');
|
||||
setTimeout(() => {
|
||||
onLoginClick();
|
||||
onClose();
|
||||
}, 2000);
|
||||
} catch (error: any) {
|
||||
console.error('Registration error:', error);
|
||||
const errorMessage = error?.data?.message || 'Registration failed. Please try again.';
|
||||
toast.error(errorMessage);
|
||||
setHelperText(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleRegister();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
className="fixed inset-0 flex items-center justify-center z-50 p-4 overflow-y-auto"
|
||||
>
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl mx-auto overflow-hidden max-h-[90vh] overflow-y-auto">
|
||||
<div className="relative px-8 pt-8 pb-4 top-0 bg-white z-10">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
|
||||
<h2 className="font-merchant text-2xl font-semibold text-gray-900 mb-2">
|
||||
Create Account
|
||||
</h2>
|
||||
<p className="font-poppins text-sm text-gray-600">
|
||||
Register to get started with City Cards
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="px-8 pb-8">
|
||||
<div className="space-y-6">
|
||||
{/* Personal Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-poppins text-base font-semibold text-gray-800">Personal Information</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
First Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter your first name"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Last Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter your last name"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Email Address <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter your email address"
|
||||
value={formData.emailAddress}
|
||||
onChange={(e) => handleInputChange('emailAddress', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
ISD Code
|
||||
</Label>
|
||||
<Select value={formData.isdCode} onValueChange={(value: any) => handleInputChange('isdCode', value)}>
|
||||
<SelectTrigger className="h-12 bg-gray-50 border-0 rounded-xl cursor-pointer">
|
||||
<SelectValue placeholder="Select code" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="+1">+1 (USA)</SelectItem>
|
||||
<SelectItem value="+44">+44 (UK)</SelectItem>
|
||||
<SelectItem value="+61">+61 (Australia)</SelectItem>
|
||||
<SelectItem value="+91">+91 (India)</SelectItem>
|
||||
<SelectItem value="+86">+86 (China)</SelectItem>
|
||||
<SelectItem value="+81">+81 (Japan)</SelectItem>
|
||||
<SelectItem value="+49">+49 (Germany)</SelectItem>
|
||||
<SelectItem value="+33">+33 (France)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Mobile Number <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="Enter your mobile number"
|
||||
value={formData.mobileNumber}
|
||||
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Address Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-poppins text-base font-semibold text-gray-800">Address Information</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Address Line 1 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter street address"
|
||||
value={formData.address1}
|
||||
onChange={(e) => handleInputChange('address1', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Address Line 2
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter apartment, suite, unit (optional)"
|
||||
value={formData.address2}
|
||||
onChange={(e) => handleInputChange('address2', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
City <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter city name"
|
||||
value={formData.city}
|
||||
onChange={(e) => handleInputChange('city', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
State <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter state name"
|
||||
value={formData.state}
|
||||
onChange={(e) => handleInputChange('state', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Country <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select value={formData.country} onValueChange={(value: any) => handleInputChange('country', value)}>
|
||||
<SelectTrigger className="h-12 bg-gray-50 border-0 rounded-xl cursor-pointer">
|
||||
<SelectValue placeholder="Select country" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Australia">Australia</SelectItem>
|
||||
<SelectItem value="United States">United States</SelectItem>
|
||||
<SelectItem value="United Kingdom">United Kingdom</SelectItem>
|
||||
<SelectItem value="Canada">Canada</SelectItem>
|
||||
<SelectItem value="India">India</SelectItem>
|
||||
<SelectItem value="Germany">Germany</SelectItem>
|
||||
<SelectItem value="France">France</SelectItem>
|
||||
<SelectItem value="Japan">Japan</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Postal Code <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter postal code"
|
||||
value={formData.postalCode}
|
||||
onChange={(e) => handleInputChange('postalCode', e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{helperText && (
|
||||
<p className={`font-poppins text-xs ${helperText.includes('successful') ? 'text-green-600' : 'text-red-500'}`}>
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleRegister}
|
||||
disabled={isLoading || isRegistering}
|
||||
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl transition-colors cursor-pointer"
|
||||
>
|
||||
{isLoading || isRegistering ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
|
||||
Creating Account...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
Register
|
||||
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
onLoginClick();
|
||||
onClose();
|
||||
}}
|
||||
className="font-poppins text-sm text-gray-600 hover:text-gray-800 transition-colors cursor-pointer"
|
||||
>
|
||||
Already have an account? <span className="text-primary font-semibold">Login</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
name: string
|
||||
name: string;
|
||||
accessToken:string;
|
||||
userId:string;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
@@ -29,11 +31,15 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const login = (userData: User) => {
|
||||
setUser(userData)
|
||||
localStorage.setItem("user", JSON.stringify(userData))
|
||||
localStorage.setItem("accessToken", userData?.accessToken)
|
||||
localStorage.setItem("userId", userData?.userId)
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
setUser(null)
|
||||
localStorage.removeItem("user")
|
||||
localStorage.removeItem("accessToken")
|
||||
localStorage.removeItem("userId")
|
||||
navigate("/")
|
||||
}
|
||||
|
||||
|
||||
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@@ -32,3 +32,5 @@ declare module '*.mp4' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module "*.css";
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
# Image Reference Guidelines
|
||||
|
||||
**IMPORTANT**: When I provide an image for reference, it is for design reference only. Do NOT use that image inside any image section in the code. The image is provided to help understand the layout, styling, and visual direction - not to be embedded as an actual image in the application.
|
||||
|
||||
# CityCards Typography Guidelines
|
||||
|
||||
**Project**: Typography Guidelines for CityCards Travel Website
|
||||
|
||||
## Font System
|
||||
|
||||
### Primary Font
|
||||
- **Poppins**: Used for all text including headings (H1–H6), body text, buttons, labels, and forms - Clean, readable, and consistent throughout
|
||||
|
||||
### Font Weight Scale & Usage
|
||||
- **font-light (300)**: Hero headings only - For creating dynamic contrast in H1/H2
|
||||
- **font-normal (400)**: Standard body text - Default weight for paragraphs
|
||||
- **font-medium (500)**: Buttons, navigation links, subtle emphasis
|
||||
- **font-semibold (600)**: Section headings, primary buttons
|
||||
- **font-bold (700)**: Hero keywords, strong emphasis
|
||||
|
||||
## Heading Typography Specifications
|
||||
|
||||
### H1 - Hero/Main Page Headings
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-5xl md:text-6xl lg:text-7xl` (48px/60px/72px - targeting ~64px)
|
||||
- **Line Height**: `leading-tight`
|
||||
- **Pattern**: Dynamic multi-weight with gradient/italic accents (max 2 emphasis styles)
|
||||
|
||||
```jsx
|
||||
<h1 className="font-poppins text-5xl md:text-6xl lg:text-7xl leading-tight">
|
||||
<span className="font-light">Discover</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Melbourne's
|
||||
</span>{' '}
|
||||
<span className="font-normal">Best Experiences</span>
|
||||
</h1>
|
||||
```
|
||||
|
||||
### H2 - Section Headings
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-2xl md:text-3xl lg:text-4xl` (24px/36px/48px)
|
||||
- **Line Height**: `leading-tight`
|
||||
- **Pattern**: Mixed weights with gradient emphasis
|
||||
|
||||
```jsx
|
||||
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
|
||||
<span className="font-light">Explore</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Amazing
|
||||
</span>{' '}
|
||||
<span className="font-normal">Cities</span>
|
||||
</h2>
|
||||
```
|
||||
|
||||
### H3 - Subsection Headings
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-xl md:text-2xl` (24px/30px)
|
||||
- **Line Height**: `leading-snug`
|
||||
- **Weight**: `font-semibold`
|
||||
|
||||
```jsx
|
||||
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-semibold">
|
||||
Feature Title
|
||||
</h3>
|
||||
```
|
||||
|
||||
### H4 - Component Headings
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-lg md:text-xl` (20px/24px)
|
||||
- **Line Height**: `leading-snug`
|
||||
- **Weight**: `font-medium` or `font-semibold`
|
||||
|
||||
```jsx
|
||||
<h4 className="font-poppins text-lg md:text-xl leading-snug font-medium">
|
||||
Component Heading
|
||||
</h4>
|
||||
```
|
||||
|
||||
### H5 - Card/Item Titles
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-lg` (18px)
|
||||
- **Line Height**: `leading-snug`
|
||||
- **Weight**: `font-medium`
|
||||
|
||||
```jsx
|
||||
<h5 className="font-poppins text-lg leading-snug font-medium">
|
||||
Card Title
|
||||
</h5>
|
||||
```
|
||||
|
||||
### H6 - Small Headings
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-base` (16px)
|
||||
- **Line Height**: `leading-snug`
|
||||
- **Weight**: `font-medium`
|
||||
|
||||
```jsx
|
||||
<h6 className="font-poppins text-base leading-snug font-medium">
|
||||
Small Heading
|
||||
</h6>
|
||||
```
|
||||
|
||||
## Body Typography Specifications
|
||||
|
||||
### Large Body Text
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-xl` (20px)
|
||||
- **Line Height**: `leading-relaxed`
|
||||
- **Weight**: `font-normal`
|
||||
|
||||
```jsx
|
||||
<p className="font-poppins text-xl leading-relaxed font-normal">
|
||||
Large descriptive text for important sections
|
||||
</p>
|
||||
```
|
||||
|
||||
### Regular Body Text
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-base` (16px)
|
||||
- **Line Height**: `leading-relaxed`
|
||||
- **Weight**: `font-normal`
|
||||
|
||||
```jsx
|
||||
<p className="font-poppins text-base leading-relaxed font-normal">
|
||||
Regular body text content
|
||||
</p>
|
||||
```
|
||||
|
||||
### Small Body Text
|
||||
- **Font**: Poppins
|
||||
- **Size**: `text-sm` (14px)
|
||||
- **Line Height**: `leading-relaxed`
|
||||
- **Weight**: `font-normal` or `font-light`
|
||||
|
||||
```jsx
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal">
|
||||
Caption or meta information
|
||||
</p>
|
||||
```
|
||||
|
||||
## Interactive Element Typography
|
||||
|
||||
### Buttons
|
||||
- **Font**: Poppins
|
||||
- **Primary Weight**: `font-semibold`
|
||||
- **Secondary Weight**: `font-medium`
|
||||
- **Min Size**: 16px
|
||||
|
||||
```jsx
|
||||
// Primary Button
|
||||
<Button className="font-poppins font-semibold">
|
||||
Primary Action
|
||||
</Button>
|
||||
|
||||
// Secondary Button
|
||||
<Button className="font-poppins font-medium">
|
||||
Secondary Action
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Navigation Links
|
||||
- **Font**: Poppins
|
||||
- **Weight**: `font-medium`
|
||||
- **Size**: `text-base` (16px)
|
||||
|
||||
```jsx
|
||||
<a className="font-poppins font-medium text-base">
|
||||
Navigation Link
|
||||
</a>
|
||||
```
|
||||
|
||||
### Form Labels
|
||||
- **Font**: Poppins
|
||||
- **Weight**: `font-light` or `font-normal`
|
||||
- **Size**: `text-sm` or `text-base` (14px/16px)
|
||||
|
||||
```jsx
|
||||
<Label className="font-poppins font-light text-sm">
|
||||
Form Label
|
||||
</Label>
|
||||
```
|
||||
|
||||
### Form Inputs
|
||||
- **Font**: Poppins
|
||||
- **Weight**: `font-normal`
|
||||
- **Size**: `text-base` (16px)
|
||||
|
||||
```jsx
|
||||
<Input className="font-poppins font-normal text-base" />
|
||||
```
|
||||
|
||||
## Accessibility Standards
|
||||
|
||||
### Text Size Requirements
|
||||
- **Minimum Text Size**: 14px
|
||||
- **Interactive Minimum Size**: 16px
|
||||
- **Contrast**: WCAG AA or higher
|
||||
- **Heading Hierarchy**: Maintain semantic order (H1 → H2 → H3 etc.)
|
||||
|
||||
### Implementation Requirements
|
||||
```jsx
|
||||
// Always include explicit font and size classes to override defaults
|
||||
<p className="font-poppins text-base font-normal leading-relaxed">
|
||||
Content with explicit styling
|
||||
</p>
|
||||
```
|
||||
|
||||
## Typography Rules
|
||||
|
||||
### DO ✅
|
||||
- Use Poppins for all text (headings and body)
|
||||
- Apply max 2 emphasis styles per heading
|
||||
- Use gradient effects sparingly
|
||||
- Keep line-heights consistent
|
||||
- Always specify explicit font classes to override component defaults
|
||||
|
||||
### DON'T ❌
|
||||
- Don't use font-light in small text
|
||||
- Don't mix more than 3 weights in one heading
|
||||
- Don't go below 14px for captions
|
||||
- Don't override font sizes without Tailwind classes
|
||||
- Don't break semantic heading hierarchy
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Component Styling Override
|
||||
**IMPORTANT**: Always explicitly set typography classes to override component defaults:
|
||||
|
||||
```jsx
|
||||
// ✅ CORRECT - Explicit typography classes
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="font-poppins text-xl font-semibold">
|
||||
Card Title
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="font-poppins text-base font-normal leading-relaxed">
|
||||
Card content with explicit styling
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
// ❌ INCORRECT - Relying on defaults
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Card content without explicit styling</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Dynamic Heading Patterns
|
||||
```jsx
|
||||
// Pattern 1: Light → Bold (H1/H2 only)
|
||||
<h1 className="font-poppins text-4xl md:text-5xl lg:text-6xl leading-tight">
|
||||
<span className="font-light">Discover</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Amazing
|
||||
</span>{' '}
|
||||
<span className="font-normal">Destinations</span>
|
||||
</h1>
|
||||
|
||||
// Pattern 2: Normal → Semibold (H3/H4)
|
||||
<h3 className="font-poppins text-xl md:text-2xl leading-snug">
|
||||
<span className="font-normal">Experience</span>{' '}
|
||||
<span className="font-semibold">Melbourne's Culture</span>
|
||||
</h3>
|
||||
```
|
||||
|
||||
### Responsive Typography
|
||||
```jsx
|
||||
// Mobile-first responsive scaling
|
||||
<h1 className="font-poppins text-3xl sm:text-4xl md:text-5xl lg:text-6xl leading-tight">
|
||||
Responsive Heading
|
||||
</h1>
|
||||
|
||||
<p className="font-poppins text-sm sm:text-base md:text-lg leading-relaxed font-normal">
|
||||
Responsive body text
|
||||
</p>
|
||||
```
|
||||
|
||||
**Add your own guidelines here**
|
||||
<!--
|
||||
|
||||
System Guidelines
|
||||
|
||||
Use this file to provide the AI with rules and guidelines you want it to follow.
|
||||
This template outlines a few examples of things you can add. You can add your own sections and format it to suit your needs
|
||||
|
||||
TIP: More context isn't always better. It can confuse the LLM. Try and add the most important rules you need
|
||||
|
||||
# General guidelines
|
||||
|
||||
Any general rules you want the AI to follow.
|
||||
For example:
|
||||
|
||||
* Only use absolute positioning when necessary. Opt for responsive and well structured layouts that use flexbox and grid by default
|
||||
* Refactor code as you go to keep code clean
|
||||
* Keep file sizes small and put helper functions and components in their own files.
|
||||
|
||||
--------------
|
||||
|
||||
# Design system guidelines
|
||||
Rules for how the AI should make generations look like your company's design system
|
||||
|
||||
Additionally, if you select a design system to use in the prompt box, you can reference
|
||||
your design system's components, tokens, variables and components.
|
||||
For example:
|
||||
|
||||
* Use a base font-size of 14px
|
||||
* Date formats should always be in the format “Jun 10”
|
||||
* The bottom toolbar should only ever have a maximum of 4 items
|
||||
* Never use the floating action button with the bottom toolbar
|
||||
* Chips should always come in sets of 3 or more
|
||||
* Don't use a dropdown if there are 2 or fewer options
|
||||
|
||||
You can also create sub sections and add more specific details
|
||||
For example:
|
||||
|
||||
|
||||
## Button
|
||||
The Button component is a fundamental interactive element in our design system, designed to trigger actions or navigate
|
||||
users through the application. It provides visual feedback and clear affordances to enhance user experience.
|
||||
|
||||
### Usage
|
||||
Buttons should be used for important actions that users need to take, such as form submissions, confirming choices,
|
||||
or initiating processes. They communicate interactivity and should have clear, action-oriented labels.
|
||||
|
||||
### Variants
|
||||
* Primary Button
|
||||
* Purpose : Used for the main action in a section or page
|
||||
* Visual Style : Bold, filled with the primary brand color
|
||||
* Usage : One primary button per section to guide users toward the most important action
|
||||
* Secondary Button
|
||||
* Purpose : Used for alternative or supporting actions
|
||||
* Visual Style : Outlined with the primary color, transparent background
|
||||
* Usage : Can appear alongside a primary button for less important actions
|
||||
* Tertiary Button
|
||||
* Purpose : Used for the least important actions
|
||||
* Visual Style : Text-only with no border, using primary color
|
||||
* Usage : For actions that should be available but not emphasized
|
||||
-->
|
||||
@@ -4,10 +4,12 @@ import App from "./App";
|
||||
import "./index.css";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "./Redux/Store";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<Toaster position="top-right" richColors duration={2000} closeButton />
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Heart, MapPin, Zap, Globe, Users, Camera, Coffee } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
// import { Button } from './ui/button';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { ReviewsSection } from '../components/ReviewsSection';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -1,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"
|
||||
/>
|
||||
@@ -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();
|
||||
}}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Calendar, User, Clock, Share2, BookmarkPlus, ThumbsUp, MessageSquare, Tag, MapPin } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Separator } from './ui/separator';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Separator } from '../components/ui/separator';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { Calendar, User, Clock, ArrowRight, Search, Tag, CreditCard, MapPin, Check, Smartphone, Star, Heart, Share2 } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import Navbar from './Navbar';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Input } from '../components/ui/input';
|
||||
import Navbar from '../components/Navbar';
|
||||
// import { CitySubmenu } from './CitySubmenu';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { WhyChooseCityCards } from './WhyChooseCityCards';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { WhyChooseCityCards } from '../components/WhyChooseCityCards';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { ReviewsSection } from '../components/ReviewsSection';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import imgFrame1597884939 from "figma:asset/5da1b0444c0d21bc7ee776c49e36e2a8ea4d3e12.png";
|
||||
|
||||
// Blog Mobile App Section Component
|
||||
@@ -1,20 +1,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 {
|
||||
@@ -1,18 +1,15 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Star, MapPin, Clock, CreditCard, Users, Shield, Smartphone, Check } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
// import SubNavbar from './SubNavbar';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { FAQPage } from './FAQPage';
|
||||
import { HowItWorks } from './HowItWorks';
|
||||
import { WhyChooseCityCards } from './WhyChooseCityCards';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { HowItWorks } from '../components/HowItWorks';
|
||||
import { WhyChooseCityCards } from '../components/WhyChooseCityCards';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface User {
|
||||
@@ -12,13 +12,13 @@ import {
|
||||
ChevronRight,
|
||||
CheckCircle
|
||||
} from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Textarea } from './ui/textarea';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Label } from '../components/ui/label';
|
||||
import { Textarea } from '../components/ui/textarea';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
|
||||
interface ContactUsPageProps {
|
||||
onBackClick: () => void;
|
||||
@@ -18,17 +18,17 @@ import {
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Progress } from './ui/progress';
|
||||
import { Calendar as CalendarComponent } from './ui/calendar';
|
||||
import { Input } from './ui/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Progress } from '../components/ui/progress';
|
||||
import { Calendar as CalendarComponent } from '../components/ui/calendar';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../components/ui/popover';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../components/ui/collapsible';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
|
||||
|
||||
interface User {
|
||||
@@ -2,10 +2,10 @@ import { ArrowRight, Check, CreditCard, DollarSign, MapPin, Palette, Sparkles, T
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Layout } from '../Layout';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { TrustSection } from './TrustSection';
|
||||
import { Button } from './ui/button';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { TrustSection } from '../components/TrustSection';
|
||||
import { Button } from '../components/ui/button';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, ChevronRight, QrCode, CreditCard, Calendar, MapPin, Star, CheckCircle, Sparkles, Users, Clock, Gift, Ticket } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -1,10 +1,6 @@
|
||||
import { motion } from 'motion/react';
|
||||
import { BadgePercent, Clock, Crown, Check, ArrowRight } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import Navbar from './Navbar';
|
||||
// import { SubNavbar } from './SubNavbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import cityCardsLogo from '../assets/cityLogo.png';
|
||||
import marriottHotelImage from '../assets/marriott-hotel.png';
|
||||
import { Layout } from '../Layout';
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Calendar, Clock, MapPin, Users, Star, Heart, Share2, Download, CheckCircle, Navigation, Cloud, Sun } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
|
||||
interface ItineraryViewPageProps {
|
||||
onBackClick: () => void;
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Sparkles, MapPin, Clock, Users, Calendar, Star, Zap, Heart, Camera } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { HowItWorks } from './HowItWorks';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { PersonalizedTourHero } from './PersonalizedTourHero';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { HowItWorks } from '../components/HowItWorks';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { PersonalizedTourHero } from '../components/PersonalizedTourHero';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface User {
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Sparkles, MapPin, Clock, Users, Calendar, Star, Zap, Heart, Camera } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { HowItWorks } from './HowItWorks';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { PersonalizedTourHero } from './PersonalizedTourHero';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { HowItWorks } from '../components/HowItWorks';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { PersonalizedTourHero } from '../components/PersonalizedTourHero';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface User {
|
||||
@@ -1,22 +1,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;
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Search, Filter, Star, MapPin, Clock, Tag, Heart, Share2, Copy, ChevronDown, ChevronRight, Check, Hotel, Plane, Building2, MapPinned, Home } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Separator } from './ui/separator';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import Navbar from './Navbar';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Separator } from '../components/ui/separator';
|
||||
import { Checkbox } from '../components/ui/checkbox';
|
||||
import Navbar from '../components/Navbar';
|
||||
// import SubNavbar from './SubNavbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { TrustSection } from './TrustSection';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
import { TrustedCompanies } from './TrustedCompanies';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { TrustSection } from '../components/TrustSection';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { ReviewsSection } from '../components/ReviewsSection';
|
||||
import { TrustedCompanies } from '../components/TrustedCompanies';
|
||||
import { Layout } from '../Layout';
|
||||
|
||||
interface OffersPageProps {
|
||||
@@ -1,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';
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ArrowLeft, Camera, Edit3, Upload, Heart, Star, Download, Share2 } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import Navbar from './Navbar';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import Navbar from '../components/Navbar';
|
||||
// import SubNavbar from './SubNavbar';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { CustomPostcards } from './CustomPostcards';
|
||||
import { HowItWorks } from './HowItWorks';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { CustomPostcards } from '../components/CustomPostcards';
|
||||
import { HowItWorks } from '../components/HowItWorks';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { Layout } from '../Layout';
|
||||
import front from '../assets/front.jpg'
|
||||
// import front from '../assets/front.jpg'
|
||||
|
||||
|
||||
interface User {
|
||||
@@ -89,7 +89,7 @@ export function PostCardsPage({
|
||||
|
||||
<div className="flex mx-auto items-center px-4 relative z-10">
|
||||
<motion.div
|
||||
className="max-w-2xl mx-auto text-left"
|
||||
className="max-w-2xl mx-auto text-center"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
@@ -111,7 +111,7 @@ export function PostCardsPage({
|
||||
Start Creating Postcards
|
||||
</Button> */}
|
||||
</motion.div>
|
||||
< img src={front} alt='Postcard image' />
|
||||
{/* < img src={front} alt='Postcard image' /> */}
|
||||
</div>
|
||||
|
||||
{/* Decorative elements */}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, useScroll, useTransform } from 'motion/react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import Navbar from './Navbar';
|
||||
import Navbar from '../components/Navbar';
|
||||
// import { CitySubmenu } from './CitySubmenu';
|
||||
import { Footer } from './Footer';
|
||||
import { MobileAppSection } from './MobileAppSection';
|
||||
import { WhyChooseCityCards } from './WhyChooseCityCards';
|
||||
import { EnhancedTestimonials } from './EnhancedTestimonials';
|
||||
import { ReviewsSection } from './ReviewsSection';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { MobileAppSection } from '../components/MobileAppSection';
|
||||
import { WhyChooseCityCards } from '../components/WhyChooseCityCards';
|
||||
import { EnhancedTestimonials } from '../components/EnhancedTestimonials';
|
||||
import { ReviewsSection } from '../components/ReviewsSection';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import {
|
||||
ArrowLeft,
|
||||
User,
|
||||
CreditCard,
|
||||
Calendar,
|
||||
MapPin,
|
||||
import {
|
||||
ArrowLeft,
|
||||
User,
|
||||
CreditCard,
|
||||
Calendar,
|
||||
MapPin,
|
||||
Settings,
|
||||
Download,
|
||||
QrCode,
|
||||
@@ -15,17 +15,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}
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ArrowRight, Hotel, Mail, MapPin, Sparkles, Ticket, Wifi } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
import { Layout } from '../Layout';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { SmartSaving } from './SmartSaving';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Button } from './ui/button';
|
||||
import { WhatsIncludedHero } from './WhatsIncludedHero';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { SmartSaving } from '../components/SmartSaving';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { WhatsIncludedHero } from '../components/WhatsIncludedHero';
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
@@ -5,12 +5,12 @@
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
|
||||
Reference in New Issue
Block a user