684 lines
31 KiB
TypeScript
684 lines
31 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { motion } from 'motion/react';
|
|
import {
|
|
ArrowLeft,
|
|
User,
|
|
CreditCard,
|
|
Calendar,
|
|
MapPin,
|
|
Settings,
|
|
Download,
|
|
QrCode,
|
|
Plus,
|
|
Clock,
|
|
Star,
|
|
Badge as BadgeIcon,
|
|
Camera,
|
|
AlertCircle
|
|
} from 'lucide-react';
|
|
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 { Badge } from '../components/ui/badge';
|
|
import Navbar from '../components/Navbar';
|
|
import { Footer } from '../components/Footer';
|
|
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
|
import { useGetUserCardsQuery, useGetUserProfileDetailsQuery, useUpdateUserProfileDetailsMutation } from '../Redux/services/profile.service';
|
|
import { toast } from 'sonner';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import LoadingSpinner from '../components/LoadingSpinner';
|
|
import { useGetUserItinerariesQuery } from '../Redux/services/itinerary.service';
|
|
|
|
interface ProfilePageProps {
|
|
onBackClick: () => void;
|
|
onHomeClick: () => void;
|
|
onMelbourneClick: () => void;
|
|
onPassesClick: () => void;
|
|
onCheckoutClick: () => void;
|
|
onSignInClick: () => void;
|
|
onSignOutClick?: () => void;
|
|
onAttractionsClick: () => void;
|
|
onBlogsClick: () => void;
|
|
onHowItWorksClick: () => void;
|
|
onFAQClick: () => void;
|
|
onPrivacyPolicyClick: () => void;
|
|
onAboutUsClick: () => void;
|
|
onProfileClick: () => void;
|
|
onCityCardsClick: () => void;
|
|
onMagicItineraryClick: () => void;
|
|
onPostCardsClick: () => void;
|
|
onCreateItineraryClick: () => void;
|
|
onViewItineraryClick?: () => void;
|
|
onOffersClick: () => void;
|
|
onDownloadAppClick?: () => void;
|
|
onContactUsClick?: () => void;
|
|
onEsimsClick?: () => void;
|
|
onHotelDiscountsClick?: () => void;
|
|
currentPage: string;
|
|
}
|
|
|
|
export function ProfilePage({
|
|
onBackClick,
|
|
onHomeClick,
|
|
onMelbourneClick,
|
|
onPassesClick,
|
|
onCheckoutClick,
|
|
onSignInClick,
|
|
onSignOutClick,
|
|
onAttractionsClick,
|
|
onBlogsClick,
|
|
onHowItWorksClick,
|
|
onFAQClick,
|
|
onPrivacyPolicyClick,
|
|
onAboutUsClick,
|
|
onProfileClick,
|
|
onCityCardsClick,
|
|
onMagicItineraryClick,
|
|
onPostCardsClick,
|
|
onCreateItineraryClick,
|
|
onViewItineraryClick,
|
|
onOffersClick,
|
|
onDownloadAppClick,
|
|
onContactUsClick,
|
|
onEsimsClick,
|
|
onHotelDiscountsClick,
|
|
currentPage
|
|
}: ProfilePageProps) {
|
|
const [activeTab, setActiveTab] = useState('profile');
|
|
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
|
const [formData, setFormData] = useState({
|
|
firstName: '',
|
|
lastName: '',
|
|
email: '',
|
|
phone: '',
|
|
country: '',
|
|
address1: '',
|
|
address2: '',
|
|
city: '',
|
|
postalCode: ''
|
|
});
|
|
|
|
const [sort, setSort] = useState("latest")
|
|
const navigate = useNavigate()
|
|
const userId = localStorage.getItem("userId")
|
|
const cityId = localStorage.getItem("cityId")
|
|
const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId)
|
|
const [updateUserProfileDetails, { isLoading: savingChanges }] = useUpdateUserProfileDetailsMutation();
|
|
const { data, isLoading: loadingCards } = useGetUserCardsQuery({ sort, cityId })
|
|
const { data: userItineraries, isLoading: loadingItineraries } = useGetUserItinerariesQuery(cityId)
|
|
|
|
const cards = data ?? []
|
|
const itineraries = userItineraries?.itineraries ?? []
|
|
|
|
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 validateForm = () => {
|
|
// if (!formData.firstName.trim()) return toast.error('First name is required'), false;
|
|
// if (/\s/.test(formData.firstName)) return toast.error('First name must not contain spaces'), false;
|
|
// if (!/^[A-Za-z]+$/.test(formData.firstName)) return toast.error('First name must contain only letters'), false;
|
|
|
|
// if (!formData.lastName.trim()) return toast.error('Last name is required'), false;
|
|
// if (/\s/.test(formData.lastName)) return toast.error('Last name must not contain spaces'), false;
|
|
// if (!/^[A-Za-z]+$/.test(formData.lastName)) return toast.error('Last name must contain only letters'), false;
|
|
|
|
// if (!formData.phone.trim()) return toast.error('Mobile number is required'), false;
|
|
// if (/\s/.test(formData.phone)) return toast.error('Mobile number must not contain spaces'), false;
|
|
// if (!/^\d+$/.test(formData.phone)) return toast.error('Mobile number must contain only digits'), false;
|
|
|
|
// if (!formData.address1.trim()) return toast.error('Address is required'), false;
|
|
// if (!/^[A-Za-z0-9\s,\-.]+$/.test(formData.address1)) return toast.error('Address contains invalid characters'), false;
|
|
|
|
// if (!formData.city.trim()) return toast.error('City is required'), false;
|
|
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.city)) return toast.error('City can only contain letters and spaces'), false;
|
|
// if (/\s{2,}/.test(formData.city)) return toast.error('City must not contain multiple consecutive spaces'), false;
|
|
|
|
// if (!formData.country.trim()) return toast.error('Country is required'), false;
|
|
// if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.country)) return toast.error('Country can only contain letters and spaces'), false;
|
|
|
|
// if (!formData.postalCode.trim()) return toast.error('Postal code is required'), false;
|
|
// if (/\s/.test(formData.postalCode)) return toast.error('Postal code must not contain spaces'), false;
|
|
// if (!/^[A-Za-z0-9]+$/.test(formData.postalCode)) return toast.error('Postal code must contain only letters and numbers'), false;
|
|
|
|
// return true;
|
|
// };
|
|
|
|
const validateForm = () => {
|
|
const e: Record<string, string> = {};
|
|
|
|
if (!formData.firstName.trim()) e.firstName = 'First name is required';
|
|
else if (/\s/.test(formData.firstName)) e.firstName = 'First name must not contain spaces';
|
|
else if (!/^[A-Za-z]+$/.test(formData.firstName)) e.firstName = 'First name must contain only letters';
|
|
|
|
if (!formData.lastName.trim()) e.lastName = 'Last name is required';
|
|
else if (/\s/.test(formData.lastName)) e.lastName = 'Last name must not contain spaces';
|
|
else if (!/^[A-Za-z]+$/.test(formData.lastName)) e.lastName = 'Last name must contain only letters';
|
|
|
|
if (!formData.phone.trim()) e.phone = 'Mobile number is required';
|
|
else if (/\s/.test(formData.phone)) e.phone = 'Mobile number must not contain spaces';
|
|
else if (!/^\d+$/.test(formData.phone)) e.phone = 'Mobile number must contain only digits';
|
|
|
|
if (!formData.address1.trim()) e.address1 = 'Address is required';
|
|
else if (!/^[A-Za-z0-9\s,\-.]+$/.test(formData.address1)) e.address1 = 'Address contains invalid characters';
|
|
|
|
if (!formData.city.trim()) e.city = 'City is required';
|
|
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.city)) e.city = 'City can only contain letters and spaces';
|
|
else if (/\s{2,}/.test(formData.city)) e.city = 'City must not contain multiple consecutive spaces';
|
|
|
|
if (!formData.country.trim()) e.country = 'Country is required';
|
|
else if (!/^[A-Za-z\s\-'À-ÿ]+$/.test(formData.country)) e.country = 'Country can only contain letters and spaces';
|
|
|
|
if (!formData.postalCode.trim()) e.postalCode = 'Postal code is required';
|
|
else if (/\s/.test(formData.postalCode)) e.postalCode = 'Postal code must not contain spaces';
|
|
else if (!/^[A-Za-z0-9]+$/.test(formData.postalCode)) e.postalCode = 'Postal code must contain only letters and numbers';
|
|
|
|
setFieldErrors(e);
|
|
return Object.keys(e).length === 0;
|
|
};
|
|
|
|
// inside ProfilePage function body:
|
|
const FieldError = ({ name }: { name: string }) =>
|
|
fieldErrors[name] ? (
|
|
<p className="text-xs text-red-500 mt-1 flex items-center gap-1">
|
|
<AlertCircle className="w-3 h-3" />{fieldErrors[name]}
|
|
</p>
|
|
) : null;
|
|
|
|
const handleInputChange = (field: string, value: string) => {
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
const handleSaveProfile = async () => {
|
|
if (!validateForm()) return;
|
|
try {
|
|
const response = await updateUserProfileDetails({ userDetails: formData, userId });
|
|
toast.success("Profile updated successfully!");
|
|
} catch (error) {
|
|
toast.error("Failed to update profile. Please try again.");
|
|
}
|
|
};
|
|
|
|
const activeCards = cards.filter((card: any) => card.isActive === true);
|
|
const expiredCards = cards.filter((card: any) => card.isActive === false);
|
|
|
|
if (isLoading && loadingCards) {
|
|
return (
|
|
<LoadingSpinner />
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
{/* Navbar */}
|
|
<Navbar
|
|
activeCity=""
|
|
onSignInClick={onSignInClick}
|
|
onSignOutClick={onSignOutClick}
|
|
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={() => 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 }}
|
|
>
|
|
<ArrowLeft className="w-5 h-5" />
|
|
<span className="font-normal">Back</span>
|
|
</motion.button>
|
|
|
|
{/* Page Title */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.1 }}
|
|
>
|
|
<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 pr-2">Profile</span>
|
|
</h1>
|
|
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600">
|
|
Manage your account, cards, and travel itineraries
|
|
</p>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Main Content */}
|
|
<section className="py-12">
|
|
<div className="container mx-auto px-4">
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-8">
|
|
{/* Tab Navigation */}
|
|
<TabsList className="grid w-full grid-cols-3 lg:w-[400px]">
|
|
<TabsTrigger value="profile" className="font-poppins font-light">My Profile</TabsTrigger>
|
|
<TabsTrigger value="passes" className="font-poppins font-light">My Cards</TabsTrigger>
|
|
<TabsTrigger value="itineraries" className="font-poppins font-light">My Itineraries</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* My Profile Tab */}
|
|
<TabsContent value="profile" className="space-y-8">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Profile Form */}
|
|
<div className="lg:col-span-2">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.2 }}
|
|
>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 font-poppins font-normal">
|
|
<User className="w-5 h-5" />
|
|
Personal Information
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="firstName" className="font-poppins font-light">
|
|
First Name <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="firstName"
|
|
value={formData.firstName}
|
|
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
|
className={`mt-1 font-poppins font-light ${fieldErrors.firstName ? 'border-red-400' : ''}`}
|
|
/>
|
|
<FieldError name="firstName" />
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="lastName" className="font-poppins font-light">
|
|
Last Name <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="lastName"
|
|
value={formData.lastName}
|
|
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
|
className={`mt-1 font-poppins font-light ${fieldErrors.lastName ? 'border-red-400' : ''}`}
|
|
/>
|
|
<FieldError name="lastName" />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="email" className="font-poppins font-light">
|
|
Email Address
|
|
</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
value={formData.email}
|
|
disabled
|
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
|
className="mt-1 font-poppins font-light"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="phone" className="font-poppins font-light">
|
|
Phone Number <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="phone"
|
|
type="tel"
|
|
value={formData.phone}
|
|
onChange={(e) => handleInputChange('phone', e.target.value)}
|
|
className={`mt-1 font-poppins font-light ${fieldErrors.phone ? 'border-red-400' : ''}`}
|
|
/>
|
|
<FieldError name="phone" />
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<h3 className="font-poppins font-normal">Billing Address</h3>
|
|
|
|
<div>
|
|
<Label htmlFor="country" className="font-poppins font-light">
|
|
Country <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="country"
|
|
value={formData.country}
|
|
onChange={(e) => handleInputChange('country', e.target.value)}
|
|
className={`mt-1 font-poppins font-light ${fieldErrors.country ? 'border-red-400' : ''}`}
|
|
/>
|
|
<FieldError name="country" />
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="address1" className="font-poppins font-light">
|
|
Address Line 1 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="address1"
|
|
value={formData.address1}
|
|
onChange={(e) => handleInputChange('address1', e.target.value)}
|
|
className={`mt-1 font-poppins font-light mb-4 ${fieldErrors.address1 ? 'border-red-400' : ''}`}
|
|
/>
|
|
<FieldError name="address1" />
|
|
|
|
<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>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="city" className="font-poppins font-light">
|
|
City <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="city"
|
|
value={formData.city}
|
|
onChange={(e) => handleInputChange('city', e.target.value)}
|
|
className={`mt-1 font-poppins font-light ${fieldErrors.city ? 'border-red-400' : ''}`}
|
|
/>
|
|
<FieldError name="city" />
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="postalCode" className="font-poppins font-light">
|
|
Postal Code <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="postalCode"
|
|
value={formData.postalCode}
|
|
onChange={(e) => handleInputChange('postalCode', e.target.value)}
|
|
className={`mt-1 font-poppins font-light ${fieldErrors.postalCode ? 'border-red-400' : ''}`}
|
|
/>
|
|
<FieldError name="postalCode" />
|
|
</div>
|
|
</div>
|
|
|
|
<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 cursor-pointer"
|
|
>
|
|
{savingChanges ? "Saving Changes..." : "Save Changes"}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
</div>
|
|
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* My Cards Tab */}
|
|
<TabsContent value="passes" className="space-y-8">
|
|
{/* Active Cards */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.2 }}
|
|
>
|
|
<h2 className="font-poppins text-2xl mb-6 font-normal">Active Cards</h2>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{activeCards.map((card: any) => (
|
|
<Card key={card.id} className="overflow-hidden">
|
|
<div
|
|
className="flex cursor-pointer hover:bg-gray-50 transition-colors duration-200 rounded-lg p-2 -m-2"
|
|
onClick={() => navigate(`/view-card-details/${card.id}`)}
|
|
>
|
|
<div className="w-32 h-32 flex-shrink-0">
|
|
<ImageWithFallback
|
|
src={card.city.bannerImage}
|
|
alt={card.city.name}
|
|
className="w-full object-cover"
|
|
/>
|
|
</div>
|
|
<div className="flex-1 p-6">
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div>
|
|
<h3 className="font-normal font-poppins">{card.card.title}</h3>
|
|
<Badge variant={card.isActive === true ? 'default' : 'secondary'} className="mt-1">
|
|
{card.isActive && "Active"}
|
|
</Badge>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="font-semibold text-lg font-poppins">${card.totalAmount}</div>
|
|
<div className="text-sm text-gray-500 line-through font-poppins font-light">${card.totalAmount + 50}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2 text-sm font-poppins font-light">
|
|
<div className="flex justify-between">
|
|
{card.cardMode === "flexi" ? (
|
|
<>
|
|
<span>Attractions:</span>
|
|
<span>{card.noOfAttractions}</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<span>Days:</span>
|
|
<span>{card.noOfDays}</span>
|
|
</>
|
|
)
|
|
}
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Valid until:</span>
|
|
<span>{new Date(card.validUpto).toLocaleDateString()}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Days remaining:</span>
|
|
<span className="text-primary font-normal">{Math.max(
|
|
0,
|
|
Math.ceil(
|
|
(new Date(card.validUpto).getTime() - new Date().getTime()) /
|
|
(1000 * 60 * 60 * 24)
|
|
)
|
|
)} days</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* Offers Button */}
|
|
<div className="mt-8 text-center">
|
|
<Button
|
|
onClick={() => navigate("/super-savings")}
|
|
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"
|
|
>
|
|
<Star className="w-4 h-4 mr-2" />
|
|
View Special Offers
|
|
</Button>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Expired Cards */}
|
|
{expiredCards.length > 0 && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.4 }}
|
|
>
|
|
<h2 className="font-poppins text-2xl mb-6 font-normal">Expired Cards</h2>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{expiredCards.map((card: any) => (
|
|
<Card key={card.id} className="overflow-hidden opacity-60">
|
|
<div className="flex">
|
|
<div className="w-32 h-32 flex-shrink-0">
|
|
<ImageWithFallback
|
|
src={card.city.bannerImage}
|
|
alt={card.city.name}
|
|
className="w-full h-full object-cover grayscale"
|
|
/>
|
|
</div>
|
|
<div className="flex-1 p-6">
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div>
|
|
<h3 className="font-normal font-poppins">{card.card.title}</h3>
|
|
<Badge variant="secondary" className="mt-1">
|
|
{!card.isActive && "Expired"}
|
|
</Badge>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="font-semibold text-lg font-poppins">${card.price}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2 text-sm font-poppins font-light">
|
|
<div className="flex justify-between">
|
|
{card.cardMode === "flexi" ? (
|
|
<>
|
|
<span>Attractions:</span>
|
|
<span>{card.noOfAttractions}</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<span>Days:</span>
|
|
<span>{card.noOfDays}</span>
|
|
</>
|
|
)
|
|
}
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Expired on:</span>
|
|
<span>{new Date(card.validUpto).toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</TabsContent>
|
|
|
|
{/* My Itineraries Tab */}
|
|
<TabsContent value="itineraries" className="space-y-8">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.2 }}
|
|
>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="font-poppins text-2xl font-normal">My Itineraries</h2>
|
|
<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={() => navigate("/create-itinerary")}
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Create Itinerary
|
|
</Button>
|
|
</div>
|
|
|
|
{itineraries?.length > 0 ? (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{itineraries.map((itinerary: any) => (
|
|
<Card key={itinerary.id}>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div>
|
|
<h3 className="font-normal font-poppins">{ }</h3>
|
|
<p className="text-sm text-gray-600 font-poppins font-light">{itinerary.city.cityName} Travel Plan</p>
|
|
</div>
|
|
<Badge variant={itinerary.isActive === true ? 'default' : 'secondary'}>
|
|
{itinerary.isActive ? "Active" : "Inactive"}
|
|
</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" />
|
|
<span>{itinerary.totalDays}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<MapPin className="w-4 h-4 text-gray-500" />
|
|
<span>{itinerary?.attractions} attractions</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Clock className="w-4 h-4 text-gray-500" />
|
|
<span>Created {new Date(itinerary.createdAt).toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
className="w-full mt-4 font-poppins font-normal"
|
|
onClick={() => navigate(`/view-itinerary/${itinerary.id}`)}
|
|
>
|
|
View Itinerary
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-16">
|
|
<div className="max-w-md mx-auto">
|
|
<div className="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
<Calendar className="w-12 h-12 text-gray-400" />
|
|
</div>
|
|
<h3 className="font-poppins text-xl mb-4 font-normal">You don't have an itinerary yet</h3>
|
|
<p className="text-gray-600 mb-6 font-poppins font-light">
|
|
Create your first itinerary to plan your perfect trip
|
|
</p>
|
|
<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={() => navigate("/create-itinerary")}
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Create Itinerary
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Footer */}
|
|
<Footer
|
|
onHomeClick={onHomeClick}
|
|
onMelbourneClick={onMelbourneClick}
|
|
onPassesClick={onPassesClick}
|
|
onSignInClick={onSignInClick}
|
|
onAttractionsClick={onAttractionsClick}
|
|
onBlogsClick={onBlogsClick}
|
|
onHowItWorksClick={onHowItWorksClick}
|
|
onFAQClick={onFAQClick}
|
|
onPrivacyPolicyClick={onPrivacyPolicyClick}
|
|
onContactUsClick={onContactUsClick}
|
|
currentPage={currentPage}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ProfilePage; |