show the created itinerary summary on the summary page
This commit is contained in:
@@ -39,6 +39,8 @@ import { CheckoutPage2 } from './pages/CheckoutPage2';
|
||||
import { SuperSavingsDetailsPage } from './pages/SuperSavingsDetailsPage';
|
||||
import { ViewCardDetailsPage } from './pages/ViewCardDetailsPageDesign';
|
||||
import { CreateMagicItineraryPageDesign } from './pages/CreateMagicIternaryPageDesign';
|
||||
import { ItineraryViewPageDesign } from './pages/ItineraryViewPageDesign';
|
||||
import ItinerarySummaryPage from './pages/ItinerarySummaryPage';
|
||||
|
||||
// User type definition
|
||||
interface User {
|
||||
@@ -222,7 +224,12 @@ export function AppRouter({
|
||||
} />
|
||||
<Route path="/itinerary-view-design" element={
|
||||
<motion.div key="itinerary-view" {...pageTransition}>
|
||||
<ItineraryViewPage {...commonNavHandlers} />
|
||||
<ItineraryViewPageDesign {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
<Route path="/itinerary-summary/:itineraryId" element={
|
||||
<motion.div key="itinerary-summary" {...pageTransition}>
|
||||
<ItinerarySummaryPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { createApi} from "@reduxjs/toolkit/query/react";
|
||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||
import { baseQuery } from "../baseQuery";
|
||||
|
||||
export const itineraryApi = createApi({
|
||||
@@ -19,10 +19,15 @@ export const itineraryApi = createApi({
|
||||
|
||||
}),
|
||||
|
||||
getItineraryDetailsById: builder.query({
|
||||
query: (itineraryId: number) => `/website/itinerary/${itineraryId}`,
|
||||
}),
|
||||
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
export const {
|
||||
useCreateMagicItineraryMutation,
|
||||
useGetItineraryDetailsByIdQuery,
|
||||
} = itineraryApi;
|
||||
@@ -48,6 +48,7 @@ import MuseumSoundsGoodIcon from '../imports/NounRelax65970551-9923-633';
|
||||
import MuseumLoveItIcon from '../imports/NounRelax65970551-9923-637';
|
||||
import { useCreateMagicItineraryMutation } from '../Redux/services/itinerary.service';
|
||||
import { toast } from 'sonner';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
|
||||
interface User {
|
||||
@@ -190,6 +191,8 @@ export function CreateMagicItineraryPageDesign({
|
||||
const [selectedActivity, setSelectedActivity] = useState<string | null>(null);
|
||||
const [createMagicItinerary] = useCreateMagicItineraryMutation();
|
||||
|
||||
const navigate= useNavigate()
|
||||
|
||||
const toggleFavorite = (activityKey: string) => {
|
||||
setFavorites(prev => {
|
||||
const newSet = new Set(prev);
|
||||
@@ -259,6 +262,7 @@ export function CreateMagicItineraryPageDesign({
|
||||
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.");
|
||||
|
||||
331
src/pages/ItinerarySummaryPage.tsx
Normal file
331
src/pages/ItinerarySummaryPage.tsx
Normal file
@@ -0,0 +1,331 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import {
|
||||
MapPin,
|
||||
Calendar,
|
||||
ChevronDown,
|
||||
Share2,
|
||||
Download,
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardContent } from '../components/ui/card';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import { useCreateMagicItineraryMutation, useGetItineraryDetailsByIdQuery } from '../Redux/services/itinerary.service';
|
||||
import { toast } from 'sonner';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
const ItinerarySummaryPage = () => {
|
||||
const [viewMode, setViewMode] = useState<'daily' | 'summary'>('daily');
|
||||
const [selectedDayTab, setSelectedDayTab] = useState(1);
|
||||
const [selectedActivity, setSelectedActivity] = useState<string | null>(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { itineraryId } = useParams()
|
||||
const { data: itineraryDetails, isLoading: itineraryDetailsLoading } = useGetItineraryDetailsByIdQuery(itineraryId);
|
||||
|
||||
const generatedItinerary = itineraryDetails ?? null;
|
||||
const days = generatedItinerary?.days ?? [];
|
||||
const summaries = generatedItinerary?.summary ?? [];
|
||||
|
||||
const selectedDayPlan = days?.find((d: any) => d.dayNumber === selectedDayTab);
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="space-y-8 max-w-3xl mx-auto"
|
||||
>
|
||||
{/* Title */}
|
||||
<div className="text-center space-y-1">
|
||||
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
|
||||
<span className="font-normal">Your</span>
|
||||
</h1>
|
||||
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight">
|
||||
<span className="font-bold text-primary italic">{generatedItinerary?.title}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Trip Details Card */}
|
||||
<div className="relative overflow-hidden rounded-2xl border border-gray-100 shadow-sm">
|
||||
{/* Background Image */}
|
||||
<div className="relative h-40 md:h-48">
|
||||
<ImageWithFallback
|
||||
src={generatedItinerary?.cityBanner}
|
||||
alt={generatedItinerary?.city}
|
||||
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 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>
|
||||
<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>
|
||||
|
||||
{/* Share & Download Buttons */}
|
||||
<div className="flex gap-3">
|
||||
<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"
|
||||
>
|
||||
<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
|
||||
</button>
|
||||
<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>
|
||||
|
||||
{/* 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 */}
|
||||
{selectedDayPlan && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Summary View */}
|
||||
{viewMode === 'summary' && (
|
||||
<div className="space-y-6">
|
||||
{days?.map((day: any, dayIndex: number) => {
|
||||
const dayDate = days[0]?.date
|
||||
? new Date(
|
||||
new Date(days[0].date).setDate(
|
||||
new Date(days[0].date).getDate() + dayIndex
|
||||
)
|
||||
).toLocaleDateString('en-AU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
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-design')}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default ItinerarySummaryPage
|
||||
Reference in New Issue
Block a user