| @@ -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"> |