Merge pull request 'main' (#20) from main into testing
All checks were successful
CityCards-Website / Build-CityCards-Website (push) Successful in 22s

Reviewed-on: #20
This commit is contained in:
2026-04-23 14:44:18 +00:00
10 changed files with 274 additions and 277 deletions

View File

@@ -293,7 +293,7 @@ export function AppRouter({
<Route path="/payment/:bookingId" element={
<motion.div key="super-savings" {...pageTransition}>
<PaymentDetailsPage {...commonNavHandlers} />
</motion.div>
</motion.div>
} />
<Route path="/super-savings/:id" element={
<motion.div key="super-savings" {...pageTransition}>

View File

@@ -8,8 +8,6 @@ export const itineraryApi = createApi({
endpoints: (builder) => ({
createMagicItinerary: builder.mutation({
query: (itineraryDetails) => ({ // keep the name of the variables being passed here same as when calling the mutation hook
url: `/website/itinerary`,
@@ -24,7 +22,11 @@ export const itineraryApi = createApi({
}),
getUserItineraries: builder.query({
query: () => `/website/itinerary/all-initineraries`,
query: (cityId) => {
const params = new URLSearchParams()
params.append('cityId', cityId);
return `/website/itinerary/all-initineraries?${params.toString()}`
}
}),
})

View File

@@ -17,13 +17,13 @@ export function FooterBottom({ onPrivacyPolicyClick }: FooterBottomProps) {
<div className="flex flex-col lg:flex-row justify-between items-center space-y-6 lg:space-y-0">
{/* Copyright */}
<p className="text-white/60 text-sm">
© 2024 CityCards. All rights reserved.
© 2026 CityCards. All rights reserved.
</p>
{/* Right Section - Legal Links and Social Icons */}
<div className="flex flex-col md:flex-row items-center space-y-4 md:space-y-0 md:space-x-8">
{/* Legal Links */}
<div className="flex space-x-6 text-sm">
{/* <div className="flex space-x-6 text-sm">
<motion.button
onClick={onPrivacyPolicyClick}
className="text-white/70 hover:text-white transition-colors duration-200"
@@ -48,7 +48,7 @@ export function FooterBottom({ onPrivacyPolicyClick }: FooterBottomProps) {
>
Cookie Policy
</motion.a>
</div>
</div> */}
{/* Social Icons - Horizontal Layout */}
<div className="flex space-x-3">

View File

@@ -1,37 +1,21 @@
import { motion } from 'motion/react';
import { footerSections } from '../utils/footerConstants';
import { Link } from 'react-router-dom';
interface FooterNavigationProps {
onHomeClick?: () => void;
onMelbourneClick?: () => void;
onPassesClick?: () => void;
onSignInClick?: () => void;
onAttractionsClick?: () => void;
onBlogsClick?: () => void;
onHowItWorksClick?: () => void;
onFAQClick?: () => void;
onPrivacyPolicyClick?: () => void;
onAboutUsClick?: () => void;
onContactUsClick?: () => void;
currentPage?: string;
}
const linkRoutes: Record<string, string> = {
'Home': '/',
// 'Cancellation policy': '/cancellation-policy',
'How It Works': '/how-it-works',
'FAQ': '/faq',
'Blog': '/blogs',
'Contact Us': '/contact-us',
'Privacy Policy': '/privacy-policy',
// 'Terms of Service': '/terms',
};
export function FooterNavigation({
onHomeClick,
onMelbourneClick,
onPassesClick,
onSignInClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onContactUsClick,
currentPage
}: FooterNavigationProps) {
export function FooterNavigation() {
return (
<div className="lg:col-span-8 grid grid-cols-2 md:grid-cols-4 gap-8">
<div className="lg:col-span-8 grid grid-cols-2 md:grid-cols-3 gap-8">
{Object.entries(footerSections).map(([key, section]) => (
<motion.div
key={key}
@@ -45,50 +29,20 @@ export function FooterNavigation({
}}
>
<h4 className="font-semibold text-white">{section.title}</h4>
<ul className="space-y-3">
{section.links.map((link, index) => {
const getClickHandler = () => {
switch (link) {
case 'Home': return onHomeClick;
case 'Melbourne': return onMelbourneClick;
case 'Passes': return onPassesClick;
case 'Sign In': return onSignInClick;
case 'Attractions': return onAttractionsClick;
case 'Blog': return onBlogsClick;
case 'How It Works': return onHowItWorksClick;
case 'FAQ': return onFAQClick;
case 'Privacy Policy': return onPrivacyPolicyClick;
case 'Contact Us': return onContactUsClick;
default: return undefined;
}
};
const clickHandler = getClickHandler();
return (
<li key={link}>
{clickHandler ? (
<motion.button
onClick={(e) => {
e.preventDefault();
clickHandler();
}}
className="text-white/80 hover:text-white transition-colors duration-200 text-sm text-left"
whileHover={{ x: 4 }}
transition={{ duration: 0.2 }}
>
{link}
</motion.button>
) : (
<motion.span
className="text-white/80 cursor-default text-sm"
>
{link}
</motion.span>
)}
</li>
);
})}
{section.links.map((link) => (
<li key={link}>
<motion.div whileHover={{ x: 4 }} transition={{ duration: 0.2 }}>
<Link
to={linkRoutes[link] || ""}
className="text-white/80 hover:text-white transition-colors duration-200 text-sm"
>
{link}
</Link>
</motion.div>
</li>
))}
</ul>
</motion.div>
))}

View File

@@ -19,12 +19,12 @@ import {
} from "./ui/accordion";
const faqData = [
{
id: "refund",
question: "Can I get a refund on my Melbourne CityCard?",
answer: "Yes, you can cancel your Melbourne CityCard and receive a full refund if you cancel at least 24 hours in advance of your selected start date. For cancellations within 24 hours, refunds are subject to our cancellation policy. Digital cards can be refunded instantly through your account.",
icon: CreditCard
},
// {
// id: "refund",
// question: "Can I get a refund on my Melbourne CityCard?",
// answer: "Yes, you can cancel your Melbourne CityCard and receive a full refund if you cancel at least 24 hours in advance of your selected start date. For cancellations within 24 hours, refunds are subject to our cancellation policy. Digital cards can be refunded instantly through your account.",
// icon: CreditCard
// },
{
id: "duration",
question: "How long is my Melbourne CityCard valid?",

View File

@@ -38,19 +38,56 @@ export default function RegisterPage() {
};
const validateForm = () => {
// First Name
if (!formData.firstName.trim()) return toast.error('First name is required'), false;
if (!/^[A-Za-z]+$/.test(formData.firstName)) return toast.error('First name must contain only alphabets'), false;
if (formData.firstName.length < 2 || formData.firstName.length > 50) return toast.error('First name must be between 2 and 50 characters'), false;
// Last Name
if (!formData.lastName.trim()) return toast.error('Last name is required'), false;
if (!/^[A-Za-z]+$/.test(formData.lastName)) return toast.error('Last name must contain only alphabets'), false;
if (formData.lastName.length < 2 || formData.lastName.length > 50) return toast.error('Last name must be between 2 and 50 characters'), false;
// Email
if (!formData.emailAddress.includes('@')) return toast.error('Invalid email address'), false;
if (!formData.isdCode.startsWith("+")) toast.error("ISD code must start with '+'"), false;
if (!/^\+\d+$/.test(formData.isdCode)) toast.error("ISD code must contain only numbers after '+'"), false;
// ISD Code
if (!formData.isdCode.startsWith("+")) return toast.error("ISD code must start with '+'"), false;
if (!/^\+\d+$/.test(formData.isdCode)) return toast.error("ISD code must contain only numbers after '+'"), false;
// Mobile Number
if (!/^\d+$/.test(formData.mobileNumber)) return toast.error('Invalid mobile number'), false;
if (formData.mobileNumber.length < 7 || formData.mobileNumber.length > 15) return toast.error('Mobile number must be between 7 and 15 digits'), false;
// Address Line 1
if (!formData.address1.trim()) return toast.error('Address required'), false;
if (!/^[A-Za-z0-9\s]+$/.test(formData.address1)) return toast.error('Address must be alphanumeric'), false;
if (formData.address1.length < 5 || formData.address1.length > 100) return toast.error('Address must be between 5 and 100 characters'), false;
// City
if (!formData.city.trim()) return toast.error('City required'), false;
if (!/^[A-Za-z\s]+$/.test(formData.city)) return toast.error('City must contain only alphabets'), false;
if (formData.city.length < 2 || formData.city.length > 50) return toast.error('City must be between 2 and 50 characters'), false;
// State
if (!formData.state.trim()) return toast.error('State required'), false;
if (!/^[A-Za-z\s]+$/.test(formData.state)) return toast.error('State must contain only alphabets'), false;
if (formData.state.length < 2 || formData.state.length > 50) return toast.error('State must be between 2 and 50 characters'), false;
// Country
if (!formData.country.trim()) return toast.error('Country required'), false;
if (!/^[A-Za-z\s]+$/.test(formData.country)) return toast.error('Country must contain only alphabets'), false;
if (formData.country.length < 2 || formData.country.length > 50) return toast.error('Country must be between 2 and 50 characters'), false;
// Postal Code
if (!/^\d+$/.test(formData.postalCode)) return toast.error('Postal code should only contain numbers'), false;
if (formData.postalCode.length < 4 || formData.postalCode.length > 10) return toast.error('Postal code must be between 4 and 10 digits'), false;
return true;
};
const handleRegister = async () => {
if (!validateForm()) return;
@@ -79,182 +116,182 @@ export default function RegisterPage() {
};
return (
<div className="min-h-screen flex flex-col bg-gray-50 w-full">
{/* Navbar */}
<Navbar activeCity="" />
<div className="min-h-screen flex flex-col bg-gray-50 w-full">
{/* Navbar */}
<Navbar activeCity="" />
{/* Main Content */}
<div className="flex-grow w-full px-6 md:px-10 py-10 mt-20">
<div className="w-full max-w-5xl mx-auto">
{/* Main Content */}
<div className="flex-grow w-full px-6 md:px-10 py-10 mt-20">
<div className="w-full max-w-5xl mx-auto">
{/* Header */}
<div className="mb-8">
<h2 className="font-merchant text-3xl font-semibold text-gray-900 mb-2">
Create Account
</h2>
<p className="font-poppins text-gray-600">
Register to get started with City Cards
</p>
{/* Header */}
<div className="mb-8">
<h2 className="font-merchant text-3xl font-semibold text-gray-900 mb-2">
Create Account
</h2>
<p className="font-poppins text-gray-600">
Register to get started with City Cards
</p>
</div>
{/* Form Container */}
<div className="bg-white rounded-2xl border border-gray-200 p-8 space-y-8">
{/* Personal Info */}
<div className="space-y-4">
<h3 className="font-poppins font-semibold text-gray-800 text-lg">
Personal Information
</h3>
<div className="grid md:grid-cols-2 gap-6">
<div>
<Label htmlFor="firstName" className="font-poppins font-light">First Name</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div>
<Label htmlFor="lastName" className="font-poppins font-light">Last Name</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
</div>
<div>
<Label htmlFor="emailAddress" className="font-poppins font-light">Email Address</Label>
<Input
id="emailAddress"
type="email"
value={formData.emailAddress}
disabled
onChange={(e) => handleInputChange('emailAddress', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div className="grid md:grid-cols-3 gap-6">
<div>
<Label htmlFor="isdCode" className="font-poppins font-light">ISD Code</Label>
<Input
id="isdCode"
placeholder="example: +91"
value={formData.isdCode}
onChange={(e) => handleInputChange('isdCode', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div className="md:col-span-2">
<Label htmlFor="mobileNumber" className="font-poppins font-light">Mobile Number</Label>
<Input
id="mobileNumber"
value={formData.mobileNumber}
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
</div>
</div>
{/* Address */}
<div className="space-y-4">
<h3 className="font-poppins font-semibold text-gray-800 text-lg">
Address Information
</h3>
<div>
<Label htmlFor="address1" className="font-poppins font-light">Address Line 1</Label>
<Input
id="address1"
value={formData.address1}
onChange={(e) => handleInputChange('address1', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div>
<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="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div>
<Label htmlFor="city" className="font-poppins font-light">City</Label>
<Input
id="city"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div>
<Label htmlFor="state" className="font-poppins font-light">State</Label>
<Input
id="state"
value={formData.state}
onChange={(e) => handleInputChange('state', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div>
<Label htmlFor="country" className="font-poppins font-light">Country</Label>
<Input
id="country"
value={formData.country}
onChange={(e) => handleInputChange('country', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div>
<Label htmlFor="postalCode" className="font-poppins font-light">Postal Code</Label>
<Input
id="postalCode"
value={formData.postalCode}
onChange={(e) => handleInputChange('postalCode', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
</div>
</div>
{helperText && (
<p className="text-sm text-red-500">{helperText}</p>
)}
<Button
onClick={handleRegister}
disabled={isLoading || isRegistering}
className="w-full cursor-pointer bg-gray-800 hover:bg-gray-900 md:px-10 h-12 text-white rounded-xl"
>
{isLoading || isRegistering ? 'Registering...' : 'Register'}
</Button>
</div>
</div>
</div>
{/* Form Container */}
<div className="bg-white rounded-2xl border border-gray-200 p-8 space-y-8">
{/* Personal Info */}
<div className="space-y-4">
<h3 className="font-poppins font-semibold text-gray-800 text-lg">
Personal Information
</h3>
<div className="grid md:grid-cols-2 gap-6">
<div>
<Label htmlFor="firstName" className="font-poppins font-light">First Name</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div>
<Label htmlFor="lastName" className="font-poppins font-light">Last Name</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
</div>
<div>
<Label htmlFor="emailAddress" className="font-poppins font-light">Email Address</Label>
<Input
id="emailAddress"
type="email"
value={formData.emailAddress}
disabled
onChange={(e) => handleInputChange('emailAddress', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div className="grid md:grid-cols-3 gap-6">
<div>
<Label htmlFor="isdCode" className="font-poppins font-light">ISD Code</Label>
<Input
id="isdCode"
placeholder="example: +91"
value={formData.isdCode}
onChange={(e) => handleInputChange('isdCode', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div className="md:col-span-2">
<Label htmlFor="mobileNumber" className="font-poppins font-light">Mobile Number</Label>
<Input
id="mobileNumber"
value={formData.mobileNumber}
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
</div>
</div>
{/* Address */}
<div className="space-y-4">
<h3 className="font-poppins font-semibold text-gray-800 text-lg">
Address Information
</h3>
<div>
<Label htmlFor="address1" className="font-poppins font-light">Address Line 1</Label>
<Input
id="address1"
value={formData.address1}
onChange={(e) => handleInputChange('address1', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div>
<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="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div>
<Label htmlFor="city" className="font-poppins font-light">City</Label>
<Input
id="city"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div>
<Label htmlFor="state" className="font-poppins font-light">State</Label>
<Input
id="state"
value={formData.state}
onChange={(e) => handleInputChange('state', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div>
<Label htmlFor="country" className="font-poppins font-light">Country</Label>
<Input
id="country"
value={formData.country}
onChange={(e) => handleInputChange('country', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
<div>
<Label htmlFor="postalCode" className="font-poppins font-light">Postal Code</Label>
<Input
id="postalCode"
value={formData.postalCode}
onChange={(e) => handleInputChange('postalCode', e.target.value)}
className="h-12 bg-gray-50 border-0 rounded-xl mt-1"
/>
</div>
</div>
</div>
{helperText && (
<p className="text-sm text-red-500">{helperText}</p>
)}
<Button
onClick={handleRegister}
disabled={isLoading || isRegistering}
className="w-full cursor-pointer bg-gray-800 hover:bg-gray-900 md:px-10 h-12 text-white rounded-xl"
>
{isLoading || isRegistering ? 'Registering...' : 'Register'}
</Button>
{/* Footer */}
<div className="mt-auto">
<Footer />
</div>
</div>
</div>
{/* Footer */}
<div className="mt-auto">
<Footer />
</div>
</div>
);
}

View File

@@ -150,9 +150,9 @@ function CheckoutConfigCard({
const cardMode = item?.cardType?.name === "selective_pass" ? "flexi" : "unlimited"
const adultPrice = item?.adultPrice * noOfAdults
const childPrice = item?.childPrice * noOfChildren
const basePrice = adultPrice + childPrice
const taxAmount = basePrice * 0.1
const strikedPrice = basePrice + 20
const basePrice = Math.round(adultPrice + childPrice)
const taxAmount = Math.round(basePrice * 0.1)
const strikedPrice = Math.round(basePrice + 20)
const [addCardToCart] = useAddCardToCartMutation()

View File

@@ -149,7 +149,7 @@ export function CreateMagicItineraryPage({
const [selectedActivity, setSelectedActivity] = useState<string | null>(null);
const [createMagicItinerary] = useCreateMagicItineraryMutation();
const navigate= useNavigate()
const navigate = useNavigate()
const toggleFavorite = (activityKey: string) => {
setFavorites(prev => {
@@ -213,17 +213,17 @@ export function CreateMagicItineraryPage({
const generateItinerary = async () => {
try {
console.log("creating itinerary...", itineraryDetails);
setIsGenerating(true);
const response = await createMagicItinerary(itineraryDetails);
console.log(response)
setGeneratedItinerary(response);
setShowResults(true);
toast.success("Itinerary created successfully!");
navigate(`/itinerary-summary/${response?.data?.id}`)
} catch (error) {
console.error("Error creating itinerary:", error);
toast.error("Failed to create itinerary. Please try again.");
if (response?.data?.id) {
navigate(`/itinerary-summary/${response?.data?.id}`)
toast.success("Itinerary created successfully!");
} else {
throw new Error(response?.error?.data?.message)
}
} catch (error: any) {
toast.error(error.message);
} finally {
setIsGenerating(false);
}

View File

@@ -107,7 +107,7 @@ export function ProfilePage({
const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId)
const [updateUserProfileDetails, { isLoading: savingChanges }] = useUpdateUserProfileDetailsMutation();
const { data, isLoading: loadingCards } = useGetUserCardsQuery({sort,cityId})
const { data: userItineraries, isLoading: loadingItineraries } = useGetUserItinerariesQuery({})
const { data: userItineraries, isLoading: loadingItineraries } = useGetUserItinerariesQuery(cityId)
const cards = data ?? []
const itineraries = userItineraries?.itineraries ?? []

View File

@@ -1,16 +1,20 @@
export const footerSections = {
explore: {
title: 'Explore',
links: ['Home', 'My Adventures', 'Cancellation policy']
links: ['Home',
'Cancellation policy'
]
},
learn: {
title: 'Learn',
links: ['How It Works', 'Safety Tips', 'FAQ', 'Blog']
},
community: {
title: 'Community',
links: ['Testimonials', 'Partner Stories', 'Events & Meetups', 'Newsletter']
links: ['How It Works',
// 'Safety Tips',
'FAQ', 'Blog']
},
// community: {
// title: 'Community',
// links: ['Testimonials', 'Partner Stories', 'Events & Meetups', 'Newsletter']
// },
support: {
title: 'Support',
links: ['Contact Us', 'Privacy Policy', 'Terms of Service']