PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
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.13, at resources/js/pages/account/Profile.tsx

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