PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 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 All 85 releases
← All changes | resources/js/pages/AccountPage.tsx +64 -15 3.0.2.9 → 3.0.17 View file →
@@ -2,8 +2,9 @@
2 2 import { useQuery } from "@tanstack/react-query";
3 3 import { apiClient } from "../lib/api-client";
4 4 import { API_ENDPOINTS } from "../lib/api-endpoints";
5 5 import { __ } from "../lib/i18n";
6 +import { toDateValue } from "../lib/dateFormat";
6 7 import {
7 8 LayoutDashboard,
8 9 Calendar,
9 10 CreditCard,
@@ -25,8 +26,9 @@
25 26 import {
26 27 formatDate,
27 28 getYatraAccountPageGlobals,
28 29 phoneToTelHref,
30 + currency as formatAccountCurrency,
29 31 } from "./account/utils";
30 32 import Dashboard from "./account/Dashboard";
31 33 import Bookings from "./account/Bookings";
32 34 import Payments from "./account/Payments";
@@ -32,8 +34,9 @@
32 34 import Payments from "./account/Payments";
33 35 import Documents from "./account/Documents";
34 36 import Profile from "./account/Profile";
35 37 import SavedTrips from "./account/SavedTrips";
38 +import { useToast } from "../components/ui/toast";
36 39
37 40 const navigation: Array<{
38 41 id: Section;
39 42 label: string;
@@ -48,8 +51,9 @@
48 51 ];
49 52
50 53 const AccountPage: React.FC = () => {
51 54 const accountShell = useMemo(() => getYatraAccountPageGlobals(), []);
55 + const { showToast } = useToast();
52 56
53 57 const accountNavigation = React.useMemo(() => {
54 58 const wl =
55 59 typeof window !== "undefined" &&
@@ -141,8 +145,15 @@
141 145 };
142 146
143 147 const [section, setSection] = useState<Section>(getSectionFromUrl);
144 148
149 + // Cross-section booking selection. Lifted here from Bookings.tsx so
150 + // Dashboard's upcoming-trip cards can deep-link straight into the
151 + // booking detail screen — click on the dashboard card sets this id
152 + // AND switches to the "bookings" section; the Bookings component
153 + // honours `initialBookingId` on mount/update to open the detail view.
154 + const [pendingBookingId, setPendingBookingId] = useState<number | null>(null);
155 +
145 156 // Update section when URL changes
146 157 React.useEffect(() => {
147 158 const newSection = getSectionFromUrl();
148 159 setSection(newSection);
@@ -147,8 +158,42 @@
147 158 const newSection = getSectionFromUrl();
148 159 setSection(newSection);
149 160 }, [urlKey]);
150 161
162 + // Surface the result of an email-change confirmation. The confirmation link
163 + // (from WordPress-style verification email) redirects here with
164 + // ?email_change=success|error; show feedback, open the Profile tab, then
165 + // strip the param so a refresh doesn't repeat the toast.
166 + React.useEffect(() => {
167 + if (typeof window === "undefined") {
168 + return;
169 + }
170 + const params = new URLSearchParams(window.location.search);
171 + const outcome = params.get("email_change");
172 + if (outcome !== "success" && outcome !== "error") {
173 + return;
174 + }
175 + if (outcome === "success") {
176 + showToast(__("Your email address has been updated.", "yatra"), "success");
177 + setSection("profile");
178 + } else {
179 + showToast(
180 + __(
181 + "This email confirmation link is invalid or has expired.",
182 + "yatra",
183 + ),
184 + "error",
185 + );
186 + }
187 + params.delete("email_change");
188 + const query = params.toString();
189 + const cleanUrl = `${window.location.pathname}${query ? `?${query}` : ""}`;
190 + window.history.replaceState({}, "", cleanUrl);
191 + (window as any).__lastAccountSearch = window.location.search;
192 + // Run once on mount.
193 + // eslint-disable-next-line react-hooks/exhaustive-deps
194 + }, []);
195 +
151 196 // Update URL and localStorage when section changes
152 197 const handleSectionChange = (newSection: Section) => {
153 198 setSection(newSection);
154 199
@@ -166,8 +211,16 @@
166 211 window.dispatchEvent(new PopStateEvent("popstate"));
167 212 }
168 213 };
169 214
215 + // Open a specific booking from anywhere (currently used by the
216 + // Dashboard's "Upcoming Trips" cards). Switches to the bookings tab
217 + // and seeds the id so Bookings opens straight into the detail view.
218 + const handleBookingOpen = (bookingId: number) => {
219 + setPendingBookingId(bookingId);
220 + handleSectionChange("bookings");
221 + };
222 +
170 223 // State moved to individual components
171 224
172 225 const unwrapArrayResponse = (response: any): any[] => {
173 226 if (!response) return [];
@@ -380,22 +433,16 @@
380 433 const savedTrips = Array.isArray(savedTripsData) ? savedTripsData : [];
381 434
382 435 // Booking details fetching moved to Bookings component
383 436
384 - const currency = (value: number) =>
385 - new Intl.NumberFormat(undefined, {
386 - style: "currency",
387 - currency: "USD",
388 - }).format(value);
437 + // formatDate / site currency from ./account/utils
389 438
390 - // formatDate imported from ./account/utils
391 -
392 439 const stats = useMemo(() => {
393 440 const outstanding = payments
394 441 .filter((p: Payment) => p.status === "pending")
395 442 .reduce((sum: number, payment: Payment) => sum + payment.amount, 0);
396 443 const upcoming = bookings.filter(
397 - (b: Booking) => new Date(b.travel_date) > new Date(),
444 + (b: Booking) => toDateValue(b.travel_date) > new Date(),
398 445 ).length;
399 446
400 447 // Calculate from real data
401 448 const totalSpent = displayProfile?.total_spent ?? 0;
@@ -414,14 +461,14 @@
414 461 icon: Calendar,
415 462 },
416 463 {
417 464 label: __("Outstanding Balance", "yatra"),
418 - value: currency(outstanding),
465 + value: formatAccountCurrency(outstanding),
419 466 icon: DollarSign,
420 467 },
421 468 {
422 469 label: __("Total Spent", "yatra"),
423 - value: currency(totalSpent),
470 + value: formatAccountCurrency(totalSpent),
424 471 icon: ShieldCheck,
425 472 },
426 473 ];
427 474 }, [bookings, payments, displayProfile]);
@@ -444,8 +491,9 @@
444 491 conciergeEmail={accountShell.companyEmail}
445 492 onSectionChange={(section: string) =>
446 493 handleSectionChange(section as Section)
447 494 }
495 + onBookingOpen={handleBookingOpen}
448 496 />
449 497 );
450 498 case "bookings":
451 499 return (
@@ -455,8 +503,10 @@
455 503 conciergeEmail={accountShell.companyEmail}
456 504 onSectionChange={(section: string) =>
457 505 handleSectionChange(section as Section)
458 506 }
507 + initialBookingId={pendingBookingId}
508 + onBookingIdConsumed={() => setPendingBookingId(null)}
459 509 />
460 510 );
461 511 case "payments":
462 512 return (
@@ -490,9 +540,9 @@
490 540 };
491 541
492 542 return (
493 543 <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-10 pb-16 px-4 sm:px-6 lg:px-8">
494 - <div className="max-w-7xl mx-auto space-y-6 pb-8">
544 + <div className="w-full max-w-[var(--yatra-container-max-width,80rem)] mx-auto space-y-6 pb-8">
495 545 <div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-800 rounded-xl shadow-sm p-6">
496 546 <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
497 547 <div>
498 548 <p className="text-sm text-gray-500 dark:text-gray-400">
@@ -549,9 +599,9 @@
549 599 tabIndex={0}
550 600 onClick={() => handleSectionChange(item.id)}
551 601 className={`yatra-nav-item yatra-nav-item-${item.id} w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition cursor-pointer ${
552 602 section === item.id
553 - ? "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-200"
603 + ? "bg-yatra-soft text-yatra-primary-dark dark:bg-yatra-surface-dark dark:text-yatra-primary-light"
554 604 : "text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-900/40"
555 605 }`}
556 606 >
557 607 <item.icon className="w-4 h-4" />
@@ -561,13 +611,12 @@
561 611 </div>
562 612 </nav>
563 613
564 614 <div
565 - className="bg-gradient-to-br from-blue-600 to-indigo-600 rounded-xl p-6 text-white space-y-2 shadow-xl"
615 + className="rounded-xl p-6 text-white space-y-2 shadow-xl"
566 616 style={{
567 - backgroundColor: "#2563eb",
568 617 backgroundImage:
569 - "linear-gradient(to bottom right, #2563eb, #4f46e5)",
618 + "linear-gradient(to bottom right, var(--yatra-primary-dark, #2563eb), var(--yatra-primary-darker, #1e40af))",
570 619 }}
571 620 >
572 621 <ShieldCheck className="w-6 h-6 text-white" />
573 622 <p className="text-sm font-medium text-white">