PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / account / SavedTrips.tsx

SavedTrips.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.3, at resources/js/pages/account/SavedTrips.tsx

288 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React from "react";
2 import { Heart, MapPin as MapPinIcon } from "lucide-react";
3 import { __ } from "../../lib/i18n";
4 import { formatPrice } from "./utils";
5 import { apiClient } from "../../lib/api-client";
6 import { useToast } from "../../components/ui/toast";
7
8 interface SavedTripsProps {
9 savedTrips: any[];
10 isLoading: boolean;
11 }
12
13 const SavedTrips: React.FC<SavedTripsProps> = ({ savedTrips, isLoading }) => {
14 const { showToast } = useToast();
15
16 const handleRemoveTrip = async (tripId: number) => {
17 try {
18 await apiClient.delete(`/saved-trips/${tripId}`);
19 // Refetch saved trips
20 window.location.reload();
21 } catch (error) {
22 console.error("Error removing trip:", error);
23 showToast(__("Failed to remove trip from wishlist.", "yatra"), "error");
24 }
25 };
26
27 return (
28 <div className="space-y-6">
29 <div className="flex items-center justify-between">
30 <div>
31 <h2 className="text-2xl font-bold text-gray-900 dark:text-white">
32 {__("Saved Trips", "yatra")}
33 </h2>
34 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
35 {__("Your wishlist of favorite trips", "yatra")}
36 </p>
37 </div>
38 </div>
39
40 {isLoading ? (
41 <div className="yatra-trip-grid grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
42 {[1, 2, 3].map((i) => (
43 <div key={i} className="yatra-trip-card">
44 <div
45 className="yatra-trip-image"
46 style={{ backgroundColor: "#f3f4f6", minHeight: "200px" }}
47 >
48 <div className="animate-pulse bg-gray-200 dark:bg-gray-700 w-full h-full"></div>
49 </div>
50 <div className="yatra-trip-content p-4">
51 <div className="animate-pulse space-y-3">
52 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
53 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
54 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-2/3"></div>
55 </div>
56 </div>
57 </div>
58 ))}
59 </div>
60 ) : savedTrips.length === 0 ? (
61 <div className="text-center py-12">
62 <Heart className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
63 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
64 {__("No saved trips yet", "yatra")}
65 </h3>
66 <p className="text-gray-500 dark:text-gray-400 mb-6">
67 {__(
68 "Start exploring and save your favorite trips to your wishlist!",
69 "yatra",
70 )}
71 </p>
72 <a
73 href="/trip/"
74 className="inline-flex items-center gap-2 px-4 py-2 bg-yatra-primary text-white rounded-lg hover:bg-yatra-primary-dark transition-colors"
75 >
76 <MapPinIcon className="w-4 h-4" />
77 {__("Browse Trips", "yatra")}
78 </a>
79 </div>
80 ) : (
81 <div className="yatra-trip-grid grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
82 {savedTrips.map((trip: any) => {
83 const tripUrl =
84 trip.permalink || `/trip/${trip.trip_slug || trip.trip_id}`;
85 const siteBase =
86 (typeof window !== "undefined" &&
87 ((window as unknown as { yatraAdmin?: { siteUrl?: string } })
88 .yatraAdmin?.siteUrl ||
89 (
90 window as unknown as {
91 yatraAccountPage?: { siteUrl?: string };
92 }
93 ).yatraAccountPage?.siteUrl)) ||
94 (typeof window !== "undefined" ? window.location.origin : "");
95 const imageUrl =
96 trip.trip_image ||
97 `${siteBase}/wp-content/plugins/yatra/assets/images/trip-placeholder.svg`;
98 const hasDiscount =
99 trip.discount_percent && trip.discount_percent > 0;
100 const displayPrice =
101 trip.price !== undefined && trip.price !== null
102 ? trip.price
103 : trip.sale_price !== undefined &&
104 trip.sale_price !== null &&
105 trip.sale_price > 0
106 ? trip.sale_price
107 : trip.original_price !== undefined &&
108 trip.original_price !== null &&
109 trip.original_price > 0
110 ? trip.original_price
111 : 0;
112 const originalPrice = trip.original_price;
113 const avgRating =
114 trip.rating !== undefined && trip.rating !== null
115 ? trip.rating
116 : trip.average_rating !== undefined &&
117 trip.average_rating !== null
118 ? trip.average_rating
119 : 0;
120 const reviewCount =
121 trip.reviews !== undefined && trip.reviews !== null
122 ? trip.reviews
123 : trip.review_count || trip.reviews_count || 0;
124
125 return (
126 <div
127 key={trip.id || trip.trip_id}
128 className="yatra-trip-card flex h-full flex-col overflow-hidden rounded-xl border border-gray-100 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800"
129 data-price={displayPrice}
130 data-rating={avgRating}
131 data-duration={trip.duration_days || 0}
132 >
133 <div className="yatra-trip-image">
134 <img src={imageUrl} alt={trip.trip_title} />
135 {hasDiscount && (
136 <div className="yatra-discount-badge">
137 {trip.discount_percent}% {__("OFF", "yatra")}
138 </div>
139 )}
140 <button
141 className="yatra-favorite-btn saved is-saved"
142 title={__("Remove from favorites", "yatra")}
143 onClick={() => handleRemoveTrip(trip.trip_id)}
144 style={{ color: "#ef4444" }}
145 >
146 <svg
147 width="20"
148 height="20"
149 fill="currentColor"
150 stroke="currentColor"
151 viewBox="0 0 24 24"
152 >
153 <path
154 strokeLinecap="round"
155 strokeLinejoin="round"
156 strokeWidth="2"
157 d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
158 />
159 </svg>
160 </button>
161 </div>
162
163 <div className="yatra-trip-content flex flex-1 flex-col p-4">
164 <div className="yatra-trip-meta">
165 {trip.location && (
166 <>
167 <span className="yatra-trip-location">
168 {trip.location}
169 </span>
170 <span className="yatra-trip-separator">•</span>
171 </>
172 )}
173 <span className="yatra-trip-duration">
174 {trip.duration || __("Flexible", "yatra")}
175 </span>
176 {trip.difficulty && (
177 <>
178 <span className="yatra-trip-separator">•</span>
179 <span className="yatra-trip-difficulty">
180 {trip.difficulty}
181 </span>
182 </>
183 )}
184 </div>
185
186 <h3 className="yatra-trip-title mb-2 text-lg font-semibold leading-snug text-gray-900 dark:text-white">
187 <a
188 href={tripUrl}
189 className="text-yatra-primary-dark hover:text-yatra-primary-darker hover:underline dark:text-yatra-on-dark dark:hover:text-yatra-primary-light focus-visible:outline focus-visible:ring-2 focus-visible:ring-yatra-primary focus-visible:ring-offset-2 rounded-sm"
190 >
191 {trip.trip_title}
192 </a>
193 </h3>
194
195 {trip.highlights && trip.highlights.length > 0 && (
196 <div className="yatra-trip-highlights">
197 {trip.highlights.map((highlight: any, idx: number) => {
198 const highlightText =
199 typeof highlight === "string"
200 ? highlight
201 : highlight.text || highlight;
202 const highlightLink =
203 typeof highlight === "object" && highlight.link
204 ? highlight.link
205 : null;
206
207 if (highlightLink) {
208 return (
209 <a
210 key={idx}
211 href={highlightLink}
212 className="yatra-highlight-badge yatra-highlight-link"
213 >
214 {highlightText}
215 </a>
216 );
217 }
218 return (
219 <span key={idx} className="yatra-highlight-badge">
220 {highlightText}
221 </span>
222 );
223 })}
224 </div>
225 )}
226
227 <div className="yatra-trip-rating">
228 <div className="yatra-rating-stars">
229 <svg
230 width="16"
231 height="16"
232 fill="#fbbf24"
233 viewBox="0 0 24 24"
234 >
235 <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
236 </svg>
237 <span className="yatra-rating-value">
238 {avgRating > 0 ? avgRating.toFixed(1) : "0.0"}
239 </span>
240 </div>
241 {reviewCount > 0 && (
242 <span className="yatra-reviews-count">
243 ({reviewCount} {__("reviews", "yatra")})
244 </span>
245 )}
246 </div>
247
248 <div className="yatra-trip-footer mt-auto flex flex-row items-center justify-between gap-4 border-t border-gray-100 pt-4 dark:border-gray-700">
249 <div className="yatra-trip-price flex min-w-0 flex-col items-start gap-1 text-left">
250 {hasDiscount &&
251 originalPrice &&
252 originalPrice > displayPrice &&
253 !trip.is_traveler_based && (
254 <div className="yatra-original-price text-sm text-gray-500 line-through">
255 {formatPrice(originalPrice)}
256 </div>
257 )}
258 {trip.is_traveler_based ? (
259 <span className="yatra-starting-from text-xs font-normal leading-tight text-gray-500 dark:text-gray-400">
260 {__("Starting from", "yatra")}
261 </span>
262 ) : null}
263 <div className="yatra-current-price text-xl font-bold leading-none text-yatra-primary tabular-nums dark:text-yatra-on-dark">
264 {formatPrice(displayPrice)}
265 </div>
266 <div className="yatra-price-note text-xs leading-tight text-gray-400 dark:text-gray-500">
267 {__("Per person", "yatra")}
268 </div>
269 </div>
270 <a
271 href={tripUrl}
272 className="yatra-card-view-btn inline-flex shrink-0 items-center justify-center self-center rounded-lg bg-yatra-primary px-4 py-2 text-sm font-medium text-white no-underline transition-colors hover:bg-yatra-primary-dark"
273 >
274 {__("View Details", "yatra")}
275 </a>
276 </div>
277 </div>
278 </div>
279 );
280 })}
281 </div>
282 )}
283 </div>
284 );
285 };
286
287 export default SavedTrips;
288