640 lines
29 KiB
TypeScript
640 lines
29 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import { motion, AnimatePresence } from 'motion/react';
|
||
import {
|
||
ArrowLeft, User, Lock, Shield, Pencil, UserCheck, Gift, AlertCircle
|
||
} from 'lucide-react';
|
||
import Navbar from '../components/Navbar';
|
||
import { Footer } from '../components/Footer';
|
||
import { Card, CardContent, CardHeader } from '../components/ui/card';
|
||
import { Separator } from '../components/ui/separator';
|
||
import { useGetUserProfileDetailsQuery } from '../Redux/services/profile.service';
|
||
import LoadingSpinner from '../components/LoadingSpinner';
|
||
import { useNavigate, useParams } from 'react-router-dom';
|
||
import {
|
||
useGetCardBookingDetailsQuery,
|
||
useStoreRecipientDetailsMutation,
|
||
usePayForCardMutation,
|
||
} from '../Redux/services/cards.service';
|
||
import { toast } from 'sonner';
|
||
|
||
import countries from 'i18n-iso-countries';
|
||
import enLocale from 'i18n-iso-countries/langs/en.json';
|
||
|
||
export interface CheckoutOrderItem {
|
||
city: string;
|
||
cardType: 'Flexi' | 'Unlimited';
|
||
days: number;
|
||
adults: number;
|
||
children: number;
|
||
quantity: number;
|
||
pricePerUnit: number;
|
||
}
|
||
|
||
interface PaymentDetailsPageProps {
|
||
checkoutOrder?: CheckoutOrderItem | null;
|
||
onBackClick: () => void;
|
||
onPaymentComplete: () => void;
|
||
onHomeClick: () => void;
|
||
onPassesClick: () => void;
|
||
onAttractionsClick?: () => void;
|
||
onBlogsClick?: () => void;
|
||
onHowItWorksClick?: () => void;
|
||
onFAQClick?: () => void;
|
||
onPrivacyPolicyClick?: () => void;
|
||
onAboutUsClick?: () => void;
|
||
onProfileClick?: () => void;
|
||
onCityCardsClick?: () => void;
|
||
onMagicItineraryClick?: () => void;
|
||
onPostCardsClick?: () => void;
|
||
onOffersClick?: () => void;
|
||
onSuperSavingsClick?: () => void;
|
||
onEsimsClick?: () => void;
|
||
onHotelDiscountsClick?: () => void;
|
||
onContactUsClick?: () => void;
|
||
onCartClick?: () => void;
|
||
onCheckoutClick?: () => void;
|
||
onSignInClick: () => void;
|
||
onSignOutClick?: () => void;
|
||
currentPage?: string;
|
||
user?: { email: string; name: string } | null;
|
||
}
|
||
|
||
// Register English locale for country codes
|
||
countries.registerLocale(enLocale);
|
||
|
||
const getCountryCode = (countryName: string): string => {
|
||
const code = countries.getAlpha2Code(countryName, 'en');
|
||
if (code) return code;
|
||
if (countryName.length === 2 && /^[A-Z]{2}$/i.test(countryName)) {
|
||
return countryName.toUpperCase();
|
||
}
|
||
console.warn(`Unknown country name: ${countryName}, defaulting to 'AU'`);
|
||
return 'AU';
|
||
};
|
||
|
||
/* ─── Editable field component ─── */
|
||
function Field({
|
||
label,
|
||
value,
|
||
onChange,
|
||
placeholder,
|
||
type = 'text',
|
||
error,
|
||
maxLength,
|
||
inputMode,
|
||
prefilled,
|
||
disabled = false,
|
||
}: {
|
||
label: React.ReactNode;
|
||
value: string;
|
||
onChange: (v: string) => void;
|
||
placeholder?: string;
|
||
type?: string;
|
||
error?: string;
|
||
maxLength?: number;
|
||
inputMode?: React.HTMLAttributes<HTMLInputElement>['inputMode'];
|
||
prefilled?: boolean;
|
||
disabled?: boolean;
|
||
}) {
|
||
const [focused, setFocused] = useState(false);
|
||
|
||
return (
|
||
<div className="flex flex-col gap-1 w-full">
|
||
<label className="font-poppins text-sm font-normal text-[#555] leading-relaxed">
|
||
{label}
|
||
</label>
|
||
<div className="relative">
|
||
<input
|
||
type={type}
|
||
value={value}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
onFocus={() => setFocused(true)}
|
||
onBlur={() => setFocused(false)}
|
||
placeholder={placeholder}
|
||
maxLength={maxLength}
|
||
inputMode={inputMode}
|
||
disabled={disabled}
|
||
className={`w-full border rounded-xl px-4 py-3 pr-10 font-poppins text-base font-normal text-[#2a2a2a] outline-none transition-all duration-200 placeholder:text-[#ccc]
|
||
${disabled
|
||
? 'bg-gray-100 text-gray-500 cursor-not-allowed border-gray-300'
|
||
: error
|
||
? 'border-red-300 focus:border-red-400 bg-red-50/30'
|
||
: focused
|
||
? 'border-[#F95F62] ring-2 ring-[#F95F62]/10'
|
||
: prefilled
|
||
? 'border-[#F95F62]/25 bg-[#F95F62]/[0.02]'
|
||
: 'border-[#E4AFB1] bg-[#FFF5F5]'
|
||
}`}
|
||
/>
|
||
{prefilled && !focused && !disabled && (
|
||
<Pencil className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[#F95F62]/40" />
|
||
)}
|
||
</div>
|
||
{error && (
|
||
<span className="font-poppins text-xs font-normal text-red-500 flex items-center gap-1">
|
||
<AlertCircle className="w-3 h-3" />{error}
|
||
</span>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── Card type badge ─── */
|
||
function CardTypeBadge({ cardType }: { cardType: 'Flexi' | 'Unlimited' }) {
|
||
return (
|
||
<span className={`inline-flex items-center px-3 py-1 rounded-full font-poppins text-xs font-medium ${cardType === 'Flexi'
|
||
? 'bg-[#f95faf]/10 text-[#f95faf]'
|
||
: 'bg-[#F95F62]/10 text-[#F95F62]'
|
||
}`}>
|
||
{cardType} Card
|
||
</span>
|
||
);
|
||
}
|
||
|
||
/* ─── Main Component ─── */
|
||
export function PaymentDetailsPage({
|
||
onHomeClick,
|
||
onPassesClick,
|
||
onAttractionsClick,
|
||
onBlogsClick,
|
||
onHowItWorksClick,
|
||
onFAQClick,
|
||
onPrivacyPolicyClick,
|
||
onAboutUsClick,
|
||
onProfileClick,
|
||
onCityCardsClick,
|
||
onMagicItineraryClick,
|
||
onPostCardsClick,
|
||
onOffersClick,
|
||
onSuperSavingsClick,
|
||
onEsimsClick,
|
||
onHotelDiscountsClick,
|
||
onContactUsClick,
|
||
onCartClick,
|
||
onCheckoutClick,
|
||
onSignInClick,
|
||
onSignOutClick,
|
||
currentPage,
|
||
user,
|
||
}: PaymentDetailsPageProps) {
|
||
const [selectedTab, setSelectedTab] = useState<'myself' | 'gift'>('myself');
|
||
|
||
// Gift fields
|
||
const [giftFirstName, setGiftFirstName] = useState('');
|
||
const [giftLastName, setGiftLastName] = useState('');
|
||
const [giftEmail, setGiftEmail] = useState('');
|
||
const [giftPhone, setGiftPhone] = useState('');
|
||
const [giftCity, setGiftCity] = useState('');
|
||
const [giftCountry, setGiftCountry] = useState('');
|
||
const [giftIsd, setGiftIsd] = useState('');
|
||
const [giftMessage, setGiftMessage] = useState('');
|
||
|
||
// Profile data
|
||
const [formData, setFormData] = useState({
|
||
firstName: '',
|
||
lastName: '',
|
||
email: '',
|
||
phone: '',
|
||
country: '',
|
||
address1: '',
|
||
address2: '',
|
||
city: '',
|
||
postalCode: '',
|
||
});
|
||
|
||
const navigate = useNavigate();
|
||
const userId = localStorage.getItem('userId');
|
||
const { bookingId } = useParams();
|
||
const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId);
|
||
const { data } = useGetCardBookingDetailsQuery(bookingId);
|
||
const [storeRecipientDetails] = useStoreRecipientDetailsMutation();
|
||
const [payForCard, { isLoading: isCreatingPayment }] = usePayForCardMutation();
|
||
|
||
const bookingDetails = data?.bookingDetails ?? null;
|
||
|
||
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 [errors, setErrors] = useState<Record<string, string>>({});
|
||
|
||
const validate = () => {
|
||
const e: Record<string, string> = {};
|
||
|
||
if (selectedTab === 'gift') {
|
||
// First Name
|
||
if (!giftFirstName.trim()) e.giftFirstName = 'First name is required';
|
||
else if (/\s/.test(giftFirstName)) e.giftFirstName = 'First name must not contain spaces';
|
||
else if (!/^[A-Za-z]+$/.test(giftFirstName)) e.giftFirstName = 'First name must contain only letters (A–Z)';
|
||
else if (giftFirstName.length < 2 || giftFirstName.length > 50) e.giftFirstName = 'First name must be between 2 and 50 characters';
|
||
|
||
// Last Name
|
||
if (!giftLastName.trim()) e.giftLastName = 'Last name is required';
|
||
else if (/\s/.test(giftLastName)) e.giftLastName = 'Last name must not contain spaces';
|
||
else if (!/^[A-Za-z]+$/.test(giftLastName)) e.giftLastName = 'Last name must contain only letters (A–Z)';
|
||
else if (giftLastName.length < 2 || giftLastName.length > 50) e.giftLastName = 'Last name must be between 2 and 50 characters';
|
||
|
||
// ISD Code
|
||
if (!giftIsd.trim()) e.giftIsd = 'ISD code is required';
|
||
else if (/\s/.test(giftIsd)) e.giftIsd = 'ISD code must not contain spaces';
|
||
else if (!giftIsd.startsWith('+')) e.giftIsd = "ISD code must start with '+' (e.g. +91)";
|
||
else if (!/^\+\d+$/.test(giftIsd)) e.giftIsd = "ISD code must contain only digits after '+'";
|
||
|
||
// Email
|
||
if (!giftEmail.trim()) e.giftEmail = 'Email address is required';
|
||
else if (!/\S+@\S+\.\S+/.test(giftEmail)) e.giftEmail = 'Enter a valid email (e.g. name@example.com)';
|
||
|
||
// Phone
|
||
if (!giftPhone.trim()) e.giftPhone = 'Phone number is required';
|
||
else if (/\s/.test(giftPhone)) e.giftPhone = 'Phone number must not contain spaces';
|
||
else if (!/^\d+$/.test(giftPhone)) e.giftPhone = 'Phone number must contain only digits (0–9)';
|
||
else if (giftPhone.length < 7 || giftPhone.length > 15) e.giftPhone = 'Phone number must be between 7 and 15 digits';
|
||
|
||
// Message
|
||
if (!giftMessage.trim()) e.giftMessage = 'Message is required';
|
||
else if (giftMessage.length < 5) e.giftMessage = 'Message must be at least 5 characters long';
|
||
else if (giftMessage.length > 500) e.giftMessage = 'Message must not exceed 500 characters';
|
||
|
||
// City
|
||
if (!giftCity.trim()) e.giftCity = 'City is required';
|
||
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(giftCity)) e.giftCity = 'City can only contain letters and spaces';
|
||
else if (/\s{2,}/.test(giftCity)) e.giftCity = 'City must not contain multiple consecutive spaces';
|
||
else if (giftCity.length < 2 || giftCity.length > 50) e.giftCity = 'City must be between 2 and 50 characters';
|
||
|
||
// Country
|
||
if (!giftCountry.trim()) e.giftCountry = 'Country is required';
|
||
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(giftCountry)) e.giftCountry = 'Country can only contain letters and spaces';
|
||
else if (giftCountry.length < 2 || giftCountry.length > 50) e.giftCountry = 'Country must be between 2 and 50 characters';
|
||
}
|
||
|
||
return e;
|
||
};
|
||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||
|
||
const handlePayment = async () => {
|
||
const validationErrors = validate();
|
||
setErrors(validationErrors);
|
||
if (Object.keys(validationErrors).length > 0) {
|
||
// toast.error('Please fill all required fields');
|
||
return;
|
||
}
|
||
|
||
if (selectedTab === 'gift') {
|
||
const recipientDetails = {
|
||
isForSelf: true,
|
||
recipientFirstName: giftFirstName,
|
||
recipientLastName: giftLastName,
|
||
recipientEmail: giftEmail,
|
||
recipientIsdCode: `+${giftIsd}`,
|
||
recipientPhone: giftPhone,
|
||
recipientCity: giftCity,
|
||
recipientCountry: giftCountry,
|
||
giftMessage: giftMessage,
|
||
};
|
||
try {
|
||
await storeRecipientDetails({ recipientDetails, bookingId }).unwrap();
|
||
toast.success('Gift details saved!');
|
||
} catch (err) {
|
||
console.error('Failed to save gift details:', err);
|
||
toast.error('Failed to save gift details. Please try again.');
|
||
return;
|
||
}
|
||
}
|
||
|
||
setIsRedirecting(true);
|
||
|
||
try {
|
||
const payResponse = await payForCard(bookingId).unwrap();
|
||
console.log('payForCard response:', payResponse);
|
||
|
||
const { checkoutPageUrl } = payResponse;
|
||
|
||
|
||
const setCookie = (name: string, value: string, days = 1) => {
|
||
const expires = new Date();
|
||
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
|
||
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
|
||
};
|
||
setCookie('pendingBookingId', bookingId);
|
||
localStorage.setItem('pendingBookingId', bookingId);
|
||
sessionStorage.setItem('pendingBookingId', bookingId);
|
||
|
||
if (!checkoutPageUrl || typeof checkoutPageUrl !== 'string') {
|
||
throw new Error('Invalid checkout URL received from server');
|
||
}
|
||
|
||
if (!checkoutPageUrl.startsWith('http://') && !checkoutPageUrl.startsWith('https://')) {
|
||
throw new Error('Checkout URL must start with http:// or https://');
|
||
}
|
||
|
||
window.location.href = checkoutPageUrl;
|
||
} catch (err: any) {
|
||
console.error('Payment initiation error:', err);
|
||
const errorMsg = err?.data?.message || err?.message || 'Failed to initiate payment. Please try again.';
|
||
toast.error(errorMsg);
|
||
setIsRedirecting(false);
|
||
}
|
||
};
|
||
|
||
if (isLoading) {
|
||
return <LoadingSpinner />;
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#fafafa] font-poppins">
|
||
<Navbar
|
||
activeCity="Melbourne"
|
||
onCityChange={() => { }}
|
||
onSignInClick={onSignInClick}
|
||
onSignOutClick={onSignOutClick}
|
||
onPassesClick={onPassesClick}
|
||
onCheckoutClick={onCheckoutClick}
|
||
onHomeClick={onHomeClick}
|
||
onAttractionsClick={onAttractionsClick}
|
||
onBlogsClick={onBlogsClick}
|
||
onHowItWorksClick={onHowItWorksClick}
|
||
onFAQClick={onFAQClick}
|
||
onPrivacyPolicyClick={onPrivacyPolicyClick}
|
||
onAboutUsClick={onAboutUsClick}
|
||
onProfileClick={onProfileClick}
|
||
onCityCardsClick={onCityCardsClick}
|
||
onMagicItineraryClick={onMagicItineraryClick}
|
||
onPostCardsClick={onPostCardsClick}
|
||
onOffersClick={onOffersClick}
|
||
onSuperSavingsClick={onSuperSavingsClick}
|
||
onEsimsClick={onEsimsClick}
|
||
onHotelDiscountsClick={onHotelDiscountsClick}
|
||
onCartClick={onCartClick}
|
||
currentPage={currentPage as any}
|
||
user={user}
|
||
/>
|
||
|
||
<div className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto">
|
||
<button
|
||
onClick={() => navigate(-1)}
|
||
className="flex items-center gap-2 text-[#8e8e8e] hover:text-[#2a2a2a] transition-colors font-poppins text-sm font-normal mb-8"
|
||
>
|
||
<ArrowLeft className="w-4 h-4" /> Back
|
||
</button>
|
||
|
||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
||
<div className="flex items-center gap-4 mb-2">
|
||
<h1 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
|
||
<span className="font-light">Review & </span>
|
||
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent pr-2">Pay</span>
|
||
</h1>
|
||
<div className="flex items-center gap-2 text-[#22a86b]">
|
||
<Shield className="w-4 h-4" />
|
||
<span className="font-poppins text-sm font-medium">SSL Secured</span>
|
||
</div>
|
||
</div>
|
||
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e]">
|
||
Complete your purchase securely. You will be redirected to Stripe to enter your card details.
|
||
</p>
|
||
</motion.div>
|
||
|
||
<div className="grid lg:grid-cols-3 gap-8">
|
||
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="lg:col-span-2">
|
||
<Card className="shadow-lg border-0 overflow-hidden">
|
||
<CardHeader className="pb-0 pt-6 px-6 border-b border-gray-100">
|
||
<div className="flex gap-2 mb-6">
|
||
<button
|
||
onClick={() => setSelectedTab('myself')}
|
||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${selectedTab === 'myself'
|
||
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
|
||
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
|
||
}`}
|
||
>
|
||
<User className="w-4 h-4" />
|
||
<span>For myself</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setSelectedTab('gift')}
|
||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${selectedTab === 'gift'
|
||
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
|
||
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
|
||
}`}
|
||
>
|
||
<Gift className="w-4 h-4" />
|
||
<span>To gift Someone</span>
|
||
</button>
|
||
</div>
|
||
</CardHeader>
|
||
|
||
<CardContent className="px-6 py-6 space-y-6">
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -8 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.3 }}
|
||
className="flex items-center gap-3 bg-[#F95F62]/6 border border-[#F95F62]/20 rounded-xl px-4 py-3"
|
||
>
|
||
<div className="w-8 h-8 rounded-lg bg-[#F95F62]/10 flex items-center justify-center flex-shrink-0">
|
||
<UserCheck className="w-4 h-4 text-[#F95F62]" />
|
||
</div>
|
||
<p className="font-poppins text-sm font-normal text-[#555] leading-relaxed">
|
||
<span className="font-medium text-[#2a2a2a]">Details pre-filled from your profile.</span>{' '}
|
||
{selectedTab === 'myself' ? 'Personal & billing details are locked.' : 'Only gift recipient details are editable.'}
|
||
</p>
|
||
</motion.div>
|
||
|
||
<Separator />
|
||
|
||
{/* Personal Information */}
|
||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.1 }} className="space-y-5">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-8 h-8 bg-[#F95F62] text-white rounded-full flex items-center justify-center font-poppins text-sm font-semibold flex-shrink-0">
|
||
1
|
||
</div>
|
||
<h2 className="font-poppins text-xl leading-snug font-semibold text-[#2a2a2a]">Personal Information</h2>
|
||
</div>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
<Field label="First Name" value={formData.firstName} onChange={() => { }} prefilled disabled />
|
||
<Field label="Last Name" value={formData.lastName} onChange={() => { }} prefilled disabled />
|
||
</div>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
<Field label="Email Address" value={formData.email} onChange={() => { }} type="email" prefilled disabled />
|
||
<Field label="Phone Number" value={formData.phone} onChange={() => { }} type="tel" prefilled disabled />
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* Gift Section */}
|
||
<AnimatePresence>
|
||
{selectedTab === 'gift' && (
|
||
<motion.div
|
||
key="gift-section"
|
||
initial={{ opacity: 0, height: 0 }}
|
||
animate={{ opacity: 1, height: 'auto' }}
|
||
exit={{ opacity: 0, height: 0 }}
|
||
transition={{ duration: 0.25 }}
|
||
className="overflow-hidden"
|
||
>
|
||
<div className="border border-[#F95F62]/15 rounded-xl px-5 py-4 space-y-4">
|
||
<div className="flex items-center gap-2">
|
||
<Gift className="w-4 h-4 text-[#F95F62]" />
|
||
<h3 className="font-poppins text-base font-semibold text-[#2a2a2a]">Gift Recipient Details</h3>
|
||
</div>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
<Field label={<>Recipient First Name <span className="text-red-500">*</span></>} value={giftFirstName} onChange={setGiftFirstName} placeholder="Enter recipient's first name" error={errors.giftFirstName} />
|
||
<Field label={<>Recipient Last Name <span className="text-red-500">*</span></>} value={giftLastName} onChange={setGiftLastName} placeholder="Enter recipient's last name" error={errors.giftLastName} />
|
||
<Field label={<>Recipient ISD Code <span className="text-red-500">*</span></>} value={giftIsd} onChange={setGiftIsd} placeholder="e.g., +61" error={errors.giftIsd} />
|
||
<Field label={<>Recipient Phone <span className="text-red-500">*</span></>} value={giftPhone} onChange={setGiftPhone} type="tel" placeholder="Enter recipient's phone number" error={errors.giftPhone} />
|
||
<Field label={<>Recipient Email <span className="text-red-500">*</span></>} value={giftEmail} onChange={setGiftEmail} type="email" placeholder="Enter recipient's email" error={errors.giftEmail} />
|
||
<Field label={<>Recipient City <span className="text-red-500">*</span></>} value={giftCity} onChange={setGiftCity} placeholder="Enter recipient's city" error={errors.giftCity} />
|
||
<Field label={<>Recipient Country <span className="text-red-500">*</span></>} value={giftCountry} onChange={setGiftCountry} placeholder="Enter recipient's country" error={errors.giftCountry} />
|
||
<Field label={<>Gift Message <span className="text-red-500">*</span></>} value={giftMessage} onChange={setGiftMessage} placeholder="Write a heartfelt message" error={errors.giftMessage} />
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
<Separator />
|
||
|
||
{/* Billing Address */}
|
||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.15 }} className="space-y-5">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-8 h-8 bg-[#F95F62] text-white rounded-full flex items-center justify-center font-poppins text-sm font-semibold flex-shrink-0">
|
||
2
|
||
</div>
|
||
<h2 className="font-poppins text-xl leading-snug font-semibold text-[#2a2a2a]">Billing Address</h2>
|
||
</div>
|
||
<div className="space-y-4">
|
||
<Field label="Address 1" value={formData.address1} onChange={() => { }} prefilled disabled />
|
||
<Field label="Address 2" value={formData.address2} onChange={() => { }} prefilled disabled />
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
<Field label="City / Suburb" value={formData.city} onChange={() => { }} prefilled disabled />
|
||
<Field label="State" value="Victoria" onChange={() => { }} prefilled disabled />
|
||
</div>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
<Field label="Postcode" value={formData.postalCode} onChange={() => { }} inputMode="numeric" prefilled disabled />
|
||
<Field label="Country" value={formData.country} onChange={() => { }} prefilled disabled />
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</CardContent>
|
||
</Card>
|
||
</motion.div>
|
||
|
||
{/* Right Column: Order Summary & Payment Button */}
|
||
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="lg:col-span-1">
|
||
<div className="lg:sticky lg:top-28 space-y-4">
|
||
<div className="bg-white rounded-2xl shadow-[0px_2px_16px_0px_rgba(0,0,0,0.06)] overflow-hidden">
|
||
<div className="px-6 py-4 border-b border-gray-100">
|
||
<h3 className="font-poppins text-lg leading-snug font-semibold text-[#2a2a2a]">Order Summary</h3>
|
||
</div>
|
||
|
||
<div className="px-6 py-5 border-b border-gray-100">
|
||
<div className="flex items-start gap-4">
|
||
<div
|
||
className={`w-16 h-10 rounded-lg flex-shrink-0 flex items-center justify-center ${bookingDetails?.cardMode?.toLowerCase() === 'flexi'
|
||
? 'bg-gradient-to-br from-[#f95faf] to-[#F95F62]'
|
||
: 'bg-gradient-to-br from-[#F95F62] to-[#c94245]'
|
||
}`}
|
||
>
|
||
<span className="font-poppins text-[10px] font-semibold text-white">{bookingDetails?.cardMode}</span>
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<h4 className="font-poppins text-base font-semibold text-[#2a2a2a]">{bookingDetails?.name}</h4>
|
||
<CardTypeBadge cardType={bookingDetails?.cardMode} />
|
||
</div>
|
||
<p className="font-poppins text-sm font-normal text-[#8e8e8e] mt-0.5">
|
||
{bookingDetails?.cardMode?.toLowerCase() === 'flexi'
|
||
? `${bookingDetails?.noOfAttractions} Attractions`
|
||
: `${bookingDetails?.noOfDays} Days`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4 space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-poppins text-sm font-normal text-[#8e8e8e]">Adults</span>
|
||
<span className="font-poppins text-sm font-medium text-[#2a2a2a]">{bookingDetails?.totalAdult}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-poppins text-sm font-normal text-[#8e8e8e]">Children</span>
|
||
<span className="font-poppins text-sm font-medium text-[#2a2a2a]">{bookingDetails?.totalChild}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="px-6 py-5">
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-poppins text-sm font-normal text-[#555]">Subtotal</span>
|
||
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">${bookingDetails?.baseAmount}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-poppins text-sm font-normal text-[#555]">GST (10%)</span>
|
||
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">${bookingDetails?.totalTaxAmount}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-poppins text-sm font-normal text-[#555]">Booking fee</span>
|
||
<span className="font-poppins text-sm font-normal text-[#22a86b]">Free</span>
|
||
</div>
|
||
<div className="pt-3 border-t border-gray-100 flex items-center justify-between">
|
||
<span className="font-poppins text-base font-semibold text-[#2a2a2a]">Total</span>
|
||
<span className="font-poppins text-2xl font-semibold text-[#F95F62]">${bookingDetails?.totalAmount}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<motion.button
|
||
whileHover={{ scale: 1.01 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
onClick={handlePayment}
|
||
disabled={isRedirecting || isCreatingPayment}
|
||
className="w-full py-4 rounded-2xl bg-[#F95F62] text-white font-poppins text-base font-semibold hover:bg-[#e8545a] transition-colors shadow-lg shadow-[#F95F62]/20 disabled:opacity-70 flex items-center justify-center gap-2"
|
||
>
|
||
{isRedirecting ? (
|
||
<>
|
||
<motion.div
|
||
animate={{ rotate: 360 }}
|
||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
||
className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full"
|
||
/>
|
||
Redirecting to Stripe...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Lock className="w-4 h-4" />
|
||
Proceed to Payment · ${bookingDetails?.totalAmount}
|
||
</>
|
||
)}
|
||
</motion.button>
|
||
|
||
<p className="font-poppins text-xs font-normal text-[#aaa] text-center">
|
||
You will be redirected to Stripe’s secure checkout page to enter your card details.
|
||
By completing your purchase you agree to our Terms of Service and Privacy Policy.
|
||
</p>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
</div>
|
||
|
||
<Footer
|
||
onHomeClick={onHomeClick}
|
||
onPassesClick={onPassesClick}
|
||
onAttractionsClick={onAttractionsClick}
|
||
onBlogsClick={onBlogsClick}
|
||
onHowItWorksClick={onHowItWorksClick}
|
||
onFAQClick={onFAQClick}
|
||
onPrivacyPolicyClick={onPrivacyPolicyClick}
|
||
onAboutUsClick={onAboutUsClick}
|
||
onContactUsClick={onContactUsClick}
|
||
/>
|
||
</div>
|
||
);
|
||
} |