enable the download itinerary feature in itinerary summary page

This commit is contained in:
aryabenade
2026-04-24 16:44:43 +05:30
parent c3d3d0c751
commit 668a183123

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'motion/react'; import { motion, AnimatePresence } from 'motion/react';
import { import {
MapPin, MapPin,
@@ -7,11 +7,12 @@ import {
Share2, Share2,
Download, Download,
ChevronRight, ChevronRight,
Loader2,
} from 'lucide-react'; } from 'lucide-react';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
import { Card, CardContent } from '../components/ui/card'; import { Card, CardContent } from '../components/ui/card';
import { ImageWithFallback } from '../components/figma/ImageWithFallback'; import { ImageWithFallback } from '../components/figma/ImageWithFallback';
import { useCreateMagicItineraryMutation, useGetItineraryDetailsByIdQuery } from '../Redux/services/itinerary.service'; import { useDownloadItineraryQuery, useGetItineraryDetailsByIdQuery } from '../Redux/services/itinerary.service';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import Navbar from '../components/Navbar'; import Navbar from '../components/Navbar';
@@ -26,6 +27,37 @@ const ItinerarySummaryPage = () => {
const { itineraryId } = useParams() const { itineraryId } = useParams()
const { data: itineraryDetails, isLoading: itineraryDetailsLoading } = useGetItineraryDetailsByIdQuery(itineraryId); const { data: itineraryDetails, isLoading: itineraryDetailsLoading } = useGetItineraryDetailsByIdQuery(itineraryId);
// Download logic using standard query with manual trigger
const [shouldDownload, setShouldDownload] = useState(false);
const { data: pdfBlob, isFetching: isDownloading, refetch } = useDownloadItineraryQuery
(itineraryId!, {
skip: !shouldDownload || !itineraryId,
});
useEffect(() => {
if (shouldDownload && pdfBlob) {
// Create download link
const url = window.URL.createObjectURL(pdfBlob);
const link = document.createElement('a');
link.href = url;
link.download = `itinerary-${itineraryId}.pdf`;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
toast.success('Itinerary downloaded successfully!');
setShouldDownload(false); // reset trigger
}
}, [pdfBlob, shouldDownload, itineraryId]);
const handleDownloadItinerary = useCallback(() => {
if (!itineraryId) {
toast.error('Itinerary ID not found');
return;
}
setShouldDownload(true);
refetch(); // manually trigger the download query
}, [itineraryId, refetch]);
const generatedItinerary = itineraryDetails ?? null; const generatedItinerary = itineraryDetails ?? null;
const days = generatedItinerary?.days ?? []; const days = generatedItinerary?.days ?? [];
@@ -35,305 +67,312 @@ const ItinerarySummaryPage = () => {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
{/* Navbar */} {/* Navbar */}
<Navbar <Navbar
/> />
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="space-y-8 max-w-3xl mx-auto mt-25" className="space-y-8 max-w-3xl mx-auto mt-25"
> >
{/* Title */} {/* Title */}
<div className="text-center space-y-1"> <div className="text-center space-y-1">
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight"> <h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
<span className="font-normal">Your</span> <span className="font-normal">Your</span>
</h1> </h1>
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight"> <h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
<span className="font-bold text-primary italic">{generatedItinerary?.title}</span> <span className="font-bold text-primary italic">{generatedItinerary?.title}</span>
</h1> </h1>
</div> </div>
{/* Trip Details Card */} {/* Trip Details Card */}
<div className="relative overflow-hidden rounded-2xl border border-gray-100 shadow-sm"> <div className="relative overflow-hidden rounded-2xl border border-gray-100 shadow-sm">
{/* Background Image */} {/* Background Image */}
<div className="relative h-40 md:h-48"> <div className="relative h-40 md:h-48">
<ImageWithFallback <ImageWithFallback
src={generatedItinerary?.cityBanner} src={generatedItinerary?.cityBanner}
alt={generatedItinerary?.city} alt={generatedItinerary?.city}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent" />
<div className="absolute bottom-4 left-5 right-5"> <div className="absolute bottom-4 left-5 right-5">
<p className="font-poppins text-xs font-medium text-white/70 uppercase tracking-wider mb-1">Your Trip</p> <p className="font-poppins text-xs font-medium text-white/70 uppercase tracking-wider mb-1">Your Trip</p>
<h3 className="font-merchant text-2xl md:text-3xl text-white leading-snug font-semibold">{generatedItinerary?.city}</h3> <h3 className="font-merchant text-2xl md:text-3xl text-white leading-snug font-semibold">{generatedItinerary?.city}</h3>
</div>
</div>
{/* Stats Row */}
<div className="grid grid-cols-3 divide-x divide-gray-100 bg-white">
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalDays}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Days</span>
</div>
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalStops}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Stops</span>
</div>
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.days[0]?.date}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Start Date</span>
</div>
</div> </div>
</div> </div>
{/* Stats Row */}
<div className="grid grid-cols-3 divide-x divide-gray-100 bg-white">
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalDays}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Days</span>
</div>
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.totalStops}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Stops</span>
</div>
<div className="flex flex-col items-center py-4">
<span className="font-merchant text-2xl text-primary">{generatedItinerary?.days[0]?.date}</span>
<span className="font-poppins text-xs font-normal text-gray-500 mt-0.5">Start Date</span>
</div>
</div>
</div>
{/* Share & Download Buttons */} {/* Share & Download Buttons */}
<div className="flex gap-3"> <div className="flex gap-3">
<Button <Button
variant="outline" variant="outline"
className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3" className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3"
>
<Share2 className="w-4 h-4 mr-2" />
Share
</Button>
<Button
variant="outline"
className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3"
>
<Download className="w-4 h-4 mr-2" />
Download
</Button>
</div>
{/* View Toggle */}
<div className="flex justify-center">
<div className="bg-gray-100 p-1 rounded-full inline-flex">
<button
onClick={() => setViewMode('daily')}
className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'daily'
? 'bg-white shadow-sm text-gray-900'
: 'text-gray-500 hover:text-gray-700'
}`}
> >
Daily View <Share2 className="w-4 h-4 mr-2" />
</button> Share
<button </Button>
onClick={() => setViewMode('summary')} <Button
className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'summary' onClick={handleDownloadItinerary}
? 'bg-white shadow-sm text-gray-900' disabled={isDownloading}
: 'text-gray-500 hover:text-gray-700' variant="outline"
}`} className="flex-1 border-2 border-primary/20 text-primary hover:bg-primary/5 font-poppins font-medium rounded-xl py-3"
> >
Summary {isDownloading ? (
</button> <Loader2 className="w-5 h-5 mr-2 animate-spin" />
</div> ) : (
</div> <Download className="w-5 h-5 mr-2" />
{/* Daily View */}
{viewMode === 'daily' && (
<div className="space-y-6">
{/* Day Tabs */}
<div className="flex items-center gap-2 overflow-x-auto pb-2">
{days?.map((day: any) => (
<button
key={day.dayNumber}
onClick={() => setSelectedDayTab(day.dayNumber)}
className={`px-5 py-2.5 rounded-xl whitespace-nowrap font-poppins text-base transition-all ${selectedDayTab === day.dayNumber
? 'text-primary font-semibold bg-primary/10 border border-primary/20'
: 'text-gray-400 font-medium hover:text-gray-600'
}`}
>
Day {day.dayNumber}
</button>
))}
{days?.length > 4 && (
<button className="p-2 text-gray-400 hover:text-gray-600">
<ChevronRight className="w-4 h-4" />
</button>
)} )}
</div>
{/* Activities for selected day */} Download
{selectedDayPlan && ( </Button>
<AnimatePresence mode="wait">
<motion.div key={`day-${selectedDayTab}`} initial={{ opacity: 0, x: 10 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -10 }} transition={{ duration: 0.3 }} className="space-y-8">
{selectedDayPlan?.items?.map((activity: any, actIndex: number) => {
const activityKey = `day${selectedDayPlan.day}-act${actIndex}`;
return (
<motion.div
key={actIndex}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: actIndex * 0.08 }}
className="space-y-4"
>
{/* Time Label */}
<p className="font-poppins text-sm font-medium text-gray-500 text-center uppercase tracking-wider">
{activity.timeSlot}
</p>
{/* Activity Card */}
<Card className="overflow-hidden border border-gray-100 shadow-sm hover:shadow-lg transition-shadow duration-300 rounded-2xl">
<CardContent className="p-0">
{/* Image */}
<div className="relative h-56 md:h-64 bg-gray-200">
<ImageWithFallback
src={activity.imageUrl}
alt={activity.title}
className="w-full h-full object-cover"
/>
{/* TODO: Get Directions Badge redirect it to lat,long */}
<div className="absolute bottom-3 left-3">
<button className="flex items-center gap-1.5 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold shadow-lg">
<MapPin className="w-3.5 h-3.5" />
Get Directions
</button>
</div>
</div>
{/* Content */}
<div className="p-5 space-y-3">
<h4 className="font-poppins text-lg font-semibold text-gray-900 leading-snug">
{activity.title}
</h4>
<p className="font-poppins text-sm font-normal text-gray-500 leading-relaxed">
{activity.locationName}
</p>
{/* Category Tags */}
<div className="flex flex-wrap gap-2">
{activity.categories?.map((cat: string, ci: number) => (
<span
key={ci}
className="font-poppins text-xs font-medium px-3 py-1.5 rounded-full border border-primary/20 text-primary bg-primary/5"
>
{cat}
</span>
))}
</div>
{/* Bullet Points */}
<div className="space-y-1.5 pt-1">
<div className="flex items-baseline gap-2">
<span className="text-primary flex-shrink-0 text-sm leading-relaxed"></span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">{activity.description}</span>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
);
})}
</motion.div>
</AnimatePresence>
)}
</div> </div>
)}
{/* Summary View */} {/* View Toggle */}
{viewMode === 'summary' && ( <div className="flex justify-center">
<div className="space-y-6"> <div className="bg-gray-100 p-1 rounded-full inline-flex">
{days?.map((day: any, dayIndex: number) => { <button
const dayDate = days[0]?.date onClick={() => setViewMode('daily')}
? new Date( className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'daily'
new Date(days[0].date).setDate( ? 'bg-white shadow-sm text-gray-900'
new Date(days[0].date).getDate() + dayIndex : 'text-gray-500 hover:text-gray-700'
) }`}
).toLocaleDateString('en-AU', { >
day: '2-digit', Daily View
month: '2-digit', </button>
year: 'numeric', <button
}) onClick={() => setViewMode('summary')}
: ''; className={`px-6 py-2.5 rounded-full font-poppins font-medium text-sm transition-all ${viewMode === 'summary'
? 'bg-white shadow-sm text-gray-900'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Summary
</button>
</div>
</div>
// ✅ Find the matching summary for this day {/* Daily View */}
const daySummary = summaries.find((s: any) => s.dayNumber === day.dayNumber); {viewMode === 'daily' && (
<div className="space-y-6">
{/* Day Tabs */}
<div className="flex items-center gap-2 overflow-x-auto pb-2">
{days?.map((day: any) => (
<button
key={day.dayNumber}
onClick={() => setSelectedDayTab(day.dayNumber)}
className={`px-5 py-2.5 rounded-xl whitespace-nowrap font-poppins text-base transition-all ${selectedDayTab === day.dayNumber
? 'text-primary font-semibold bg-primary/10 border border-primary/20'
: 'text-gray-400 font-medium hover:text-gray-600'
}`}
>
Day {day.dayNumber}
</button>
))}
{days?.length > 4 && (
<button className="p-2 text-gray-400 hover:text-gray-600">
<ChevronRight className="w-4 h-4" />
</button>
)}
</div>
return ( {/* Activities for selected day */}
<div key={dayIndex} className="border-l-4 border-primary/20 pl-5 space-y-3"> {selectedDayPlan && (
{/* Day Header */} <AnimatePresence mode="wait">
<div className="flex items-center justify-between"> <motion.div key={`day-${selectedDayTab}`} initial={{ opacity: 0, x: 10 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -10 }} transition={{ duration: 0.3 }} className="space-y-8">
<h3 className="font-poppins text-lg font-semibold text-gray-900"> {selectedDayPlan?.items?.map((activity: any, actIndex: number) => {
Day {day.dayNumber}: const activityKey = `day${selectedDayPlan.day}-act${actIndex}`;
</h3>
<div className="flex items-center gap-1.5 text-primary">
<Calendar className="w-3.5 h-3.5" />
<span className="font-poppins text-sm font-medium">{dayDate}</span>
</div>
</div>
{/* Activity List */}
<div className="space-y-2">
{daySummary?.items?.map((item: any, actIndex: number) => {
const activityKey = `summary-day${day.dayNumber}-act${actIndex}`;
const isExpanded = selectedActivity === activityKey;
return ( return (
<div key={actIndex} className="bg-gray-50 rounded-xl overflow-hidden"> <motion.div
<div key={actIndex}
className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-gray-100 transition-colors" initial={{ opacity: 0, y: 20 }}
onClick={() => animate={{ opacity: 1, y: 0 }}
setSelectedActivity(isExpanded ? null : activityKey) transition={{ duration: 0.4, delay: actIndex * 0.08 }}
} className="space-y-4"
> >
<p className="font-poppins text-sm font-medium text-gray-800"> {/* Time Label */}
{item.timeSlot}: {item.title} <p className="font-poppins text-sm font-medium text-gray-500 text-center uppercase tracking-wider">
</p> {activity.timeSlot}
<ChevronDown </p>
className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
/>
</div>
<AnimatePresence> {/* Activity Card */}
{isExpanded && ( <Card className="overflow-hidden border border-gray-100 shadow-sm hover:shadow-lg transition-shadow duration-300 rounded-2xl">
<motion.div <CardContent className="p-0">
initial={{ height: 0, opacity: 0 }} {/* Image */}
animate={{ height: 'auto', opacity: 1 }} <div className="relative h-56 md:h-64 bg-gray-200">
exit={{ height: 0, opacity: 0 }} <ImageWithFallback
transition={{ duration: 0.3, ease: 'easeInOut' }} src={activity.imageUrl}
> alt={activity.title}
<div className="px-4 pb-4 space-y-2"> className="w-full h-full object-cover"
<div className="flex items-baseline gap-2"> />
<span className="text-primary flex-shrink-0 text-sm leading-relaxed"></span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed"> {/* TODO: Get Directions Badge redirect it to lat,long */}
{item.description} <div className="absolute bottom-3 left-3">
</span> <button className="flex items-center gap-1.5 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold shadow-lg">
</div>
<button className="flex items-center gap-1.5 mt-2 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold">
<MapPin className="w-3.5 h-3.5" /> <MapPin className="w-3.5 h-3.5" />
Get directions Get Directions
</button> </button>
</div> </div>
</motion.div> </div>
)}
</AnimatePresence> {/* Content */}
</div> <div className="p-5 space-y-3">
<h4 className="font-poppins text-lg font-semibold text-gray-900 leading-snug">
{activity.title}
</h4>
<p className="font-poppins text-sm font-normal text-gray-500 leading-relaxed">
{activity.locationName}
</p>
{/* Category Tags */}
<div className="flex flex-wrap gap-2">
{activity.categories?.map((cat: string, ci: number) => (
<span
key={ci}
className="font-poppins text-xs font-medium px-3 py-1.5 rounded-full border border-primary/20 text-primary bg-primary/5"
>
{cat}
</span>
))}
</div>
{/* Bullet Points */}
<div className="space-y-1.5 pt-1">
<div className="flex items-baseline gap-2">
<span className="text-primary flex-shrink-0 text-sm leading-relaxed"></span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">{activity.description}</span>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
); );
})} })}
</div> </motion.div>
</div> </AnimatePresence>
); )}
})} </div>
</div> )}
)}
{/* Bottom Action */} {/* Summary View */}
<div className="flex justify-center pt-4 pb-8"> {viewMode === 'summary' && (
<Button <div className="space-y-6">
onClick={() => navigate('/create-itinerary')} {days?.map((day: any, dayIndex: number) => {
className="w-full font-poppins font-semibold px-8 py-3 rounded-xl bg-primary hover:bg-primary/90 text-white shadow-md shadow-primary/20" const dayDate = days[0]?.date
> ? new Date(
Create Another Itinerary new Date(days[0].date).setDate(
</Button> new Date(days[0].date).getDate() + dayIndex
</div> )
</motion.div> ).toLocaleDateString('en-AU', {
<Footer day: '2-digit',
/> month: '2-digit',
</div> year: 'numeric',
})
: '';
// ✅ Find the matching summary for this day
const daySummary = summaries.find((s: any) => s.dayNumber === day.dayNumber);
return (
<div key={dayIndex} className="border-l-4 border-primary/20 pl-5 space-y-3">
{/* Day Header */}
<div className="flex items-center justify-between">
<h3 className="font-poppins text-lg font-semibold text-gray-900">
Day {day.dayNumber}:
</h3>
<div className="flex items-center gap-1.5 text-primary">
<Calendar className="w-3.5 h-3.5" />
<span className="font-poppins text-sm font-medium">{dayDate}</span>
</div>
</div>
{/* Activity List */}
<div className="space-y-2">
{daySummary?.items?.map((item: any, actIndex: number) => {
const activityKey = `summary-day${day.dayNumber}-act${actIndex}`;
const isExpanded = selectedActivity === activityKey;
return (
<div key={actIndex} className="bg-gray-50 rounded-xl overflow-hidden">
<div
className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-gray-100 transition-colors"
onClick={() =>
setSelectedActivity(isExpanded ? null : activityKey)
}
>
<p className="font-poppins text-sm font-medium text-gray-800">
{item.timeSlot}: {item.title}
</p>
<ChevronDown
className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
/>
</div>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
<div className="px-4 pb-4 space-y-2">
<div className="flex items-baseline gap-2">
<span className="text-primary flex-shrink-0 text-sm leading-relaxed"></span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">
{item.description}
</span>
</div>
<button className="flex items-center gap-1.5 mt-2 bg-primary text-white px-4 py-2 rounded-full font-poppins text-xs font-semibold">
<MapPin className="w-3.5 h-3.5" />
Get directions
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
</div>
);
})}
</div>
)}
{/* Bottom Action */}
<div className="flex justify-center pt-4 pb-8">
<Button
onClick={() => navigate('/create-itinerary')}
className="w-full font-poppins font-semibold px-8 py-3 rounded-xl bg-primary hover:bg-primary/90 text-white shadow-md shadow-primary/20"
>
Create Another Itinerary
</Button>
</div>
</motion.div>
<Footer
/>
</div>
); );
} }