PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 / Profile.tsx

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

652 lines 27.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState, useEffect } from "react";
2 import {
3 User,
4 PenSquare,
5 ShieldCheck,
6 Heart,
7 CheckCircle,
8 XCircle,
9 } from "lucide-react";
10 import { useQueryClient } from "@tanstack/react-query";
11 import { __, sprintf } from "../../lib/i18n";
12 import { currency } from "./utils";
13 import { getCountryName, getCountryOptions } from "../../lib/countries";
14 import { apiClient } from "../../lib/api-client";
15 import { API_ENDPOINTS } from "../../lib/api-endpoints";
16 import { useToast } from "../../components/ui/toast";
17 import type { CustomerProfile } from "./types";
18
19 interface ProfileProps {
20 profile: CustomerProfile | null;
21 savedTrips: any[];
22 wishlistEnabled?: boolean;
23 }
24
25 const Profile: React.FC<ProfileProps> = ({
26 profile,
27 savedTrips,
28 wishlistEnabled = false,
29 }) => {
30 const [isEditing, setIsEditing] = useState(false);
31 const [formData, setFormData] = useState({
32 name: "",
33 email: "",
34 phone: "",
35 address: "",
36 city: "",
37 country: "",
38 });
39
40 const [isChangingPassword, setIsChangingPassword] = useState(false);
41 const [passwordData, setPasswordData] = useState({
42 currentPassword: "",
43 newPassword: "",
44 confirmPassword: "",
45 });
46
47 // Update formData when profile changes
48 useEffect(() => {
49 if (profile && !isEditing) {
50 setFormData({
51 name: profile.name || "",
52 email: profile.email || "",
53 phone: profile.phone || "",
54 address: profile.address || "",
55 city: profile.city || "",
56 country: profile.country || "",
57 });
58 }
59 }, [profile, isEditing]);
60
61 const queryClient = useQueryClient();
62 const { showToast } = useToast();
63 const [isSaving, setIsSaving] = useState(false);
64 const [isSavingPassword, setIsSavingPassword] = useState(false);
65 const [isResendingEmail, setIsResendingEmail] = useState(false);
66 const [isCancellingEmail, setIsCancellingEmail] = useState(false);
67
68 const handleInputChange = (field: string, value: string) => {
69 setFormData((prev) => ({ ...prev, [field]: value }));
70 };
71
72 const handleSave = async () => {
73 setIsSaving(true);
74 try {
75 // The form has a single "Name" field; the backend stores first/last
76 // separately. Split on whitespace (first token = first name, rest = last).
77 const parts = formData.name.trim().split(/\s+/).filter(Boolean);
78 const firstName = parts.shift() || "";
79 const lastName = parts.join(" ");
80
81 // Email is NOT sent to the profile endpoint — it is the account login and
82 // the backend strips it defensively. Changing it follows WordPress core's
83 // pending-change pattern (handled separately below).
84 const payload: Record<string, string> = {
85 first_name: firstName,
86 phone: formData.phone,
87 address: formData.address,
88 city: formData.city,
89 country: formData.country,
90 };
91 // Only send last_name when present (backend rejects an empty last_name).
92 if (lastName) {
93 payload.last_name = lastName;
94 }
95
96 await apiClient.put(API_ENDPOINTS.CUSTOMER_ME, payload);
97
98 // Refetch the parent's profile query so the saved values are reflected.
99 await queryClient.invalidateQueries({ queryKey: ["account-profile"] });
100
101 // If the email was edited, request the change separately. Like WordPress
102 // core, this does NOT change the email now — it emails a confirmation link
103 // to the new address and the change only applies once it is clicked.
104 const currentEmail = (profile?.email || "").trim().toLowerCase();
105 const nextEmail = formData.email.trim();
106 if (nextEmail && nextEmail.toLowerCase() !== currentEmail) {
107 try {
108 const resp: any = await apiClient.post(
109 API_ENDPOINTS.CUSTOMER_CHANGE_EMAIL,
110 { email: nextEmail },
111 );
112 await queryClient.invalidateQueries({ queryKey: ["account-profile"] });
113 showToast(
114 resp?.message ||
115 __(
116 "A confirmation link has been sent to your new email address. The change takes effect once you confirm it.",
117 "yatra",
118 ),
119 "success",
120 );
121 } catch (emailError: any) {
122 showToast(
123 emailError?.message ||
124 __("Could not request the email change.", "yatra"),
125 "error",
126 );
127 }
128 } else {
129 showToast(__("Profile updated successfully.", "yatra"), "success");
130 }
131 setIsEditing(false);
132 } catch (error: any) {
133 showToast(
134 error?.message || __("Failed to update profile.", "yatra"),
135 "error",
136 );
137 } finally {
138 setIsSaving(false);
139 }
140 };
141
142 const handleChangePassword = async () => {
143 if (passwordData.newPassword !== passwordData.confirmPassword) {
144 showToast(__("Passwords do not match.", "yatra"), "error");
145 return;
146 }
147
148 setIsSavingPassword(true);
149 try {
150 await apiClient.post(API_ENDPOINTS.CUSTOMER_CHANGE_PASSWORD, {
151 current_password: passwordData.currentPassword,
152 new_password: passwordData.newPassword,
153 });
154
155 showToast(__("Password changed successfully.", "yatra"), "success");
156 setIsChangingPassword(false);
157 setPasswordData({
158 currentPassword: "",
159 newPassword: "",
160 confirmPassword: "",
161 });
162 // Changing the password rotates the WP auth cookie + REST nonce, so the
163 // current page's nonce is now stale. Reload to pick up a fresh one and
164 // avoid 403s on subsequent actions.
165 setTimeout(() => window.location.reload(), 1200);
166 } catch (error: any) {
167 showToast(
168 error?.message || __("Failed to change password.", "yatra"),
169 "error",
170 );
171 } finally {
172 setIsSavingPassword(false);
173 }
174 };
175
176 const handleResendEmailChange = async () => {
177 setIsResendingEmail(true);
178 try {
179 const resp: any = await apiClient.post(
180 API_ENDPOINTS.CUSTOMER_RESEND_EMAIL_CHANGE,
181 );
182 showToast(
183 resp?.message ||
184 __("Confirmation link re-sent to your new email address.", "yatra"),
185 "success",
186 );
187 } catch (error: any) {
188 showToast(
189 error?.message ||
190 __("Could not re-send the confirmation email.", "yatra"),
191 "error",
192 );
193 } finally {
194 setIsResendingEmail(false);
195 }
196 };
197
198 const handleCancelEmailChange = async () => {
199 setIsCancellingEmail(true);
200 try {
201 const resp: any = await apiClient.delete(
202 API_ENDPOINTS.CUSTOMER_CHANGE_EMAIL,
203 );
204 await queryClient.invalidateQueries({ queryKey: ["account-profile"] });
205 showToast(
206 resp?.message ||
207 __("The pending email change has been cancelled.", "yatra"),
208 "success",
209 );
210 } catch (error: any) {
211 showToast(
212 error?.message ||
213 __("Could not cancel the pending email change.", "yatra"),
214 "error",
215 );
216 } finally {
217 setIsCancellingEmail(false);
218 }
219 };
220
221 const handleCancel = () => {
222 setIsEditing(false);
223 if (profile) {
224 setFormData({
225 name: profile.name || "",
226 email: profile.email || "",
227 phone: profile.phone || "",
228 address: profile.address || "",
229 city: profile.city || "",
230 country: profile.country || "",
231 });
232 }
233 };
234
235 return (
236 <div className="yatra-profile-page space-y-6">
237 {/* Header */}
238 <div className="yatra-profile-header bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
239 <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
240 <div>
241 <h2 className="text-lg font-bold text-gray-900 dark:text-white flex items-center gap-3">
242 <div className="p-2 bg-yatra-soft dark:bg-yatra-surface-dark-muted rounded-lg">
243 <User className="w-6 h-6 text-yatra-primary dark:text-yatra-on-dark" />
244 </div>
245 {__("Profile", "yatra")}
246 </h2>
247 <p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
248 {__(
249 "Update your contact details, address, and communication preferences.",
250 "yatra",
251 )}
252 </p>
253 </div>
254 {!isEditing && (
255 <div
256 role="button"
257 tabIndex={0}
258 onClick={() => setIsEditing(true)}
259 className="yatra-profile-edit-btn inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium cursor-pointer"
260 >
261 <PenSquare className="w-4 h-4" /> {__("Edit Profile", "yatra")}
262 </div>
263 )}
264 </div>
265 </div>
266
267 {/* Profile Form */}
268 <div className="yatra-profile-card bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
269 <div className="yatra-profile-fields space-y-6">
270 <div className="grid gap-6 md:grid-cols-2">
271 <div className="yatra-profile-field">
272 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
273 {__("Full Name", "yatra")}{" "}
274 <span className="text-red-500">*</span>
275 </label>
276 {isEditing ? (
277 <input
278 type="text"
279 value={formData.name}
280 onChange={(e) => handleInputChange("name", e.target.value)}
281 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
282 placeholder={__("Enter your full name", "yatra")}
283 />
284 ) : (
285 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
286 {formData.name || __("Not set", "yatra")}
287 </p>
288 )}
289 </div>
290
291 <div className="yatra-profile-field">
292 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
293 {__("Email Address", "yatra")}
294 </label>
295 {isEditing ? (
296 <>
297 <input
298 type="email"
299 value={formData.email}
300 onChange={(e) => handleInputChange("email", e.target.value)}
301 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
302 placeholder={__("[email protected]", "yatra")}
303 />
304 {/* Mirrors WordPress core: the email is not changed here — a
305 confirmation link is sent to the new address first. */}
306 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
307 {__(
308 "For security, we'll email a confirmation link to the new address. Your email changes only after you click that link.",
309 "yatra",
310 )}
311 </p>
312 </>
313 ) : (
314 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
315 {formData.email || __("Not set", "yatra")}
316 </p>
317 )}
318 {profile?.pending_email ? (
319 <div className="mt-2 text-xs text-amber-600 dark:text-amber-400">
320 <p>
321 {sprintf(
322 /* translators: %s: the pending new email address. */
323 __(
324 "Pending confirmation for %s — check that inbox for the confirmation link.",
325 "yatra",
326 ),
327 profile.pending_email,
328 )}
329 </p>
330 <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1">
331 <button
332 type="button"
333 onClick={handleResendEmailChange}
334 disabled={isResendingEmail || isCancellingEmail}
335 className="font-medium text-yatra-primary underline hover:no-underline disabled:opacity-60"
336 >
337 {isResendingEmail
338 ? __("Sending…", "yatra")
339 : __("Resend confirmation email", "yatra")}
340 </button>
341 <button
342 type="button"
343 onClick={handleCancelEmailChange}
344 disabled={isResendingEmail || isCancellingEmail}
345 className="font-medium text-gray-500 dark:text-gray-400 underline hover:no-underline disabled:opacity-60"
346 >
347 {isCancellingEmail
348 ? __("Cancelling…", "yatra")
349 : __("Cancel change", "yatra")}
350 </button>
351 </div>
352 </div>
353 ) : null}
354 </div>
355
356 <div className="yatra-profile-field">
357 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
358 {__("Phone Number", "yatra")}
359 </label>
360 {isEditing ? (
361 <input
362 type="tel"
363 value={formData.phone}
364 onChange={(e) => handleInputChange("phone", e.target.value)}
365 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
366 placeholder={__("Enter your phone number", "yatra")}
367 />
368 ) : (
369 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
370 {formData.phone || __("Not set", "yatra")}
371 </p>
372 )}
373 </div>
374
375 {/* Logical order: Country → City → Address (broad to specific). */}
376 <div className="yatra-profile-field">
377 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
378 {__("Country", "yatra")}
379 </label>
380 {isEditing ? (
381 <select
382 value={formData.country}
383 onChange={(e) => handleInputChange("country", e.target.value)}
384 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
385 >
386 <option value="">{__("Select your country", "yatra")}</option>
387 {getCountryOptions().map((c) => (
388 <option key={c.code} value={c.code}>
389 {c.name}
390 </option>
391 ))}
392 </select>
393 ) : (
394 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
395 {getCountryName(formData.country) || __("Not set", "yatra")}
396 </p>
397 )}
398 </div>
399
400 <div className="yatra-profile-field">
401 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
402 {__("City", "yatra")}
403 </label>
404 {isEditing ? (
405 <input
406 type="text"
407 value={formData.city}
408 onChange={(e) => handleInputChange("city", e.target.value)}
409 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
410 placeholder={__("Enter your city", "yatra")}
411 />
412 ) : (
413 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
414 {formData.city || __("Not set", "yatra")}
415 </p>
416 )}
417 </div>
418
419 <div className="yatra-profile-field md:col-span-2">
420 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
421 {__("Address", "yatra")}
422 </label>
423 {isEditing ? (
424 <textarea
425 value={formData.address}
426 onChange={(e) => handleInputChange("address", e.target.value)}
427 rows={3}
428 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
429 placeholder={__("Enter your address", "yatra")}
430 />
431 ) : (
432 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
433 {formData.address || __("Not set", "yatra")}
434 </p>
435 )}
436 </div>
437 </div>
438
439 {isEditing && (
440 <div className="yatra-profile-actions flex flex-wrap gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
441 <button
442 type="button"
443 onClick={handleSave}
444 disabled={isSaving}
445 className="inline-flex items-center gap-2 px-6 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
446 >
447 <CheckCircle className="w-4 h-4" />
448 {isSaving
449 ? __("Saving…", "yatra")
450 : __("Save Changes", "yatra")}
451 </button>
452 <div
453 role="button"
454 tabIndex={0}
455 onClick={handleCancel}
456 className="inline-flex items-center gap-2 px-6 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm font-medium cursor-pointer"
457 >
458 <XCircle className="w-4 h-4" />
459 {__("Cancel", "yatra")}
460 </div>
461 </div>
462 )}
463 </div>
464 </div>
465
466 {/* Password Change Section */}
467 <div className="yatra-profile-password bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
468 <div className="flex items-center justify-between mb-6">
469 <div>
470 <h3 className="text-base font-semibold text-gray-900 dark:text-white flex items-center gap-2">
471 <ShieldCheck className="w-5 h-5 text-yatra-primary dark:text-yatra-on-dark" />
472 {__("Change Password", "yatra")}
473 </h3>
474 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
475 {__("Update your password to keep your account secure.", "yatra")}
476 </p>
477 </div>
478 {!isChangingPassword && (
479 <div
480 role="button"
481 tabIndex={0}
482 onClick={() => setIsChangingPassword(true)}
483 className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium cursor-pointer"
484 >
485 <ShieldCheck className="w-4 h-4" />
486 {__("Change Password", "yatra")}
487 </div>
488 )}
489 </div>
490
491 {isChangingPassword && (
492 <div className="space-y-4">
493 <div className="yatra-password-field">
494 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
495 {__("Current Password", "yatra")}{" "}
496 <span className="text-red-500">*</span>
497 </label>
498 <input
499 type="password"
500 value={passwordData.currentPassword}
501 onChange={(e) =>
502 setPasswordData((prev) => ({
503 ...prev,
504 currentPassword: e.target.value,
505 }))
506 }
507 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
508 placeholder={__("Enter your current password", "yatra")}
509 />
510 </div>
511
512 <div className="yatra-password-field">
513 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
514 {__("New Password", "yatra")}{" "}
515 <span className="text-red-500">*</span>
516 </label>
517 <input
518 type="password"
519 value={passwordData.newPassword}
520 onChange={(e) =>
521 setPasswordData((prev) => ({
522 ...prev,
523 newPassword: e.target.value,
524 }))
525 }
526 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
527 placeholder={__("Enter new password", "yatra")}
528 />
529 </div>
530
531 <div className="yatra-password-field">
532 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
533 {__("Confirm New Password", "yatra")}{" "}
534 <span className="text-red-500">*</span>
535 </label>
536 <input
537 type="password"
538 value={passwordData.confirmPassword}
539 onChange={(e) =>
540 setPasswordData((prev) => ({
541 ...prev,
542 confirmPassword: e.target.value,
543 }))
544 }
545 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
546 placeholder={__("Confirm your new password", "yatra")}
547 />
548 {passwordData.newPassword &&
549 passwordData.confirmPassword &&
550 passwordData.newPassword !== passwordData.confirmPassword && (
551 <p className="text-xs text-red-500 mt-1">
552 {__("Passwords do not match", "yatra")}
553 </p>
554 )}
555 </div>
556
557 <div className="yatra-password-actions flex flex-wrap gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
558 <button
559 type="button"
560 onClick={handleChangePassword}
561 disabled={
562 isSavingPassword ||
563 !passwordData.currentPassword ||
564 !passwordData.newPassword ||
565 !passwordData.confirmPassword ||
566 passwordData.newPassword !== passwordData.confirmPassword
567 }
568 className="inline-flex items-center gap-2 px-6 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
569 >
570 <CheckCircle className="w-4 h-4" />
571 {isSavingPassword
572 ? __("Updating…", "yatra")
573 : __("Update Password", "yatra")}
574 </button>
575 <div
576 role="button"
577 tabIndex={0}
578 onClick={() => {
579 setIsChangingPassword(false);
580 setPasswordData({
581 currentPassword: "",
582 newPassword: "",
583 confirmPassword: "",
584 });
585 }}
586 className="inline-flex items-center gap-2 px-6 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm font-medium cursor-pointer"
587 >
588 <XCircle className="w-4 h-4" />
589 {__("Cancel", "yatra")}
590 </div>
591 </div>
592 </div>
593 )}
594 </div>
595
596 {/* Additional Sections.
597 Communication Preferences card was removed: it shipped as four
598 checkboxes with `defaultChecked` and no onChange / no save
599 handler / no API persistence — purely decorative. Showing
600 fake toggles is worse than not shipping the feature because
601 customers expect a "Save" round-trip and refresh-revert.
602 Re-introduce only when there's a real preferences endpoint
603 + service-side honouring (suppress emails when toggled off). */}
604 <div className="yatra-profile-sections grid gap-6 lg:grid-cols-1">
605 {wishlistEnabled && (
606 <div className="yatra-profile-saved-trips bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
607 <h3 className="text-base font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
608 <Heart className="w-5 h-5 text-red-600 dark:text-red-400" />
609 {__("Saved Trips", "yatra")}
610 </h3>
611 <div className="yatra-saved-trips-list space-y-3">
612 {savedTrips.length > 0 ? (
613 savedTrips.map((trip: any) => (
614 <div
615 key={trip.id || trip.trip_id}
616 className="yatra-saved-trip-item flex items-center justify-between border border-gray-100 dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-900/40 transition-colors"
617 >
618 <div className="flex-1 min-w-0">
619 <p className="text-sm font-medium text-gray-900 dark:text-white truncate">
620 {trip.trip_title || trip.title}
621 </p>
622 {(trip.location || trip.duration) && (
623 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
624 {trip.location || trip.duration}
625 </p>
626 )}
627 </div>
628 <div className="text-right ml-4 flex-shrink-0">
629 <p className="text-xs text-gray-500 dark:text-gray-400">
630 {__("From", "yatra")}
631 </p>
632 <p className="text-sm font-semibold text-gray-900 dark:text-white">
633 {currency(trip.price_from || trip.price || 0)}
634 </p>
635 </div>
636 </div>
637 ))
638 ) : (
639 <p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
640 {__("No saved trips yet", "yatra")}
641 </p>
642 )}
643 </div>
644 </div>
645 )}
646 </div>
647 </div>
648 );
649 };
650
651 export default Profile;
652