| @@ -1,9 +1,53 @@ | ||
| 1 | -import React, { useMemo, useState, useEffect } from "react"; | |
| 1 | +import React, { useMemo, useState, useEffect, Suspense, lazy } from "react"; | |
| 2 | 2 | import Layout from "./components/Layout"; |
| 3 | +import { Skeleton } from "./components/ui/skeleton"; | |
| 4 | +import { | |
| 5 | + useMarkSectionSeen, | |
| 6 | + SUBPAGE_TO_SECTION, | |
| 7 | +} from "./hooks/useNotificationCounts"; | |
| 8 | + | |
| 9 | +/* | |
| 10 | + * Page-loading strategy. | |
| 11 | + * | |
| 12 | + * Pages >1500 lines are split into separate chunks via React.lazy so they | |
| 13 | + * land in the JS bundle on demand instead of inflating the initial paint. | |
| 14 | + * Vite picks these up automatically as separate output files — operators | |
| 15 | + * who never visit the Whatsapp tab never download its ~80KB of code. | |
| 16 | + * | |
| 17 | + * Pages that ship eagerly: | |
| 18 | + * - Dashboard: the default landing route, almost always rendered first | |
| 19 | + * - Smaller list pages (Trips, Bookings, Customers, etc.) where the | |
| 20 | + * lazy-chunk overhead (extra HTTP round-trip on first nav) outweighs | |
| 21 | + * the bytes saved on initial paint | |
| 22 | + * | |
| 23 | + * Pages that ship lazily (in line-count order, biggest savings first): | |
| 24 | + * - TripForm (9.6k loc) | |
| 25 | + * - Settings (8.6k loc) | |
| 26 | + * - Team (3.5k loc) | |
| 27 | + * - Reports (3.1k loc) | |
| 28 | + * - DiscountForm (2.7k loc) | |
| 29 | + * - Itinerary (2.7k loc) | |
| 30 | + * - RecurringRuleForm (2.6k loc) | |
| 31 | + * - Whatsapp (2.6k loc) | |
| 32 | + * - ChannelManager (2.5k loc) | |
| 33 | + * - Webhooks (2.5k loc) | |
| 34 | + * - TripConsentForm (2.1k loc) | |
| 35 | + * - BookingForm (1.8k loc) | |
| 36 | + * - DynamicPricing (1.8k loc) | |
| 37 | + * - AvailabilityForm (1.7k loc) | |
| 38 | + * - AiAssistant (1.6k loc) | |
| 39 | + * - AbandonedRecovery (1.6k loc) | |
| 40 | + * - WhiteLabel (1.6k loc) | |
| 41 | + * - DynamicPricingRuleForm (1.5k loc) | |
| 42 | + * - ViewBooking (1.5k loc) | |
| 43 | + * | |
| 44 | + * If you add a new heavy page, lazy-import it here too; cheap to add and | |
| 45 | + * the only thing to remember is that lazy components must be rendered | |
| 46 | + * inside a <Suspense> boundary (we wrap the whole renderPage() output). | |
| 47 | + */ | |
| 3 | 48 | import Dashboard from "./pages/Dashboard"; |
| 4 | 49 | import Trips from "./pages/Trips"; |
| 5 | -import TripForm from "./pages/TripForm"; | |
| 6 | 50 | import Activities from "./pages/Activities"; |
| 7 | 51 | import ActivityForm from "./pages/ActivityForm"; |
| 8 | 52 | import Destinations from "./pages/Destinations"; |
| 9 | 53 | import DestinationForm from "./pages/DestinationForm"; |
| @@ -11,10 +55,8 @@ | ||
| 11 | 55 | import CategoryForm from "./pages/CategoryForm"; |
| 12 | 56 | import DifficultyLevels from "./pages/DifficultyLevels"; |
| 13 | 57 | import DifficultyLevelForm from "./pages/DifficultyLevelForm"; |
| 14 | 58 | import Bookings from "./pages/Bookings"; |
| 15 | -import BookingForm from "./pages/BookingForm"; | |
| 16 | -import ViewBooking from "./pages/ViewBooking"; | |
| 17 | 59 | import Customers from "./pages/Customers"; |
| 18 | 60 | import CustomerForm from "./pages/CustomerForm"; |
| 19 | 61 | import ViewCustomer from "./pages/ViewCustomer"; |
| 20 | 62 | import Reviews from "./pages/Reviews"; |
| @@ -19,19 +61,15 @@ | ||
| 19 | 61 | import ViewCustomer from "./pages/ViewCustomer"; |
| 20 | 62 | import Reviews from "./pages/Reviews"; |
| 21 | 63 | import ReviewForm from "./pages/ReviewForm"; |
| 22 | 64 | import ViewReview from "./pages/ViewReview"; |
| 23 | -import Reports from "./pages/Reports"; | |
| 24 | 65 | import Tools from "./components/Tools"; |
| 25 | -import Settings from "./pages/Settings"; | |
| 26 | 66 | import ItemTypes from "./pages/ItemTypes"; |
| 27 | 67 | import ItemTypeForm from "./pages/ItemTypeForm"; |
| 28 | 68 | import Items from "./pages/Items"; |
| 29 | 69 | import ItemForm from "./pages/ItemForm"; |
| 30 | -import Itinerary from "./pages/Itinerary"; | |
| 31 | 70 | import ItineraryForm from "./pages/ItineraryForm"; |
| 32 | 71 | import Discounts from "./pages/Discounts"; |
| 33 | -import DiscountForm from "./pages/DiscountForm"; | |
| 34 | 72 | import Payments from "./pages/Payments"; |
| 35 | 73 | import PaymentForm from "./pages/PaymentForm"; |
| 36 | 74 | import ViewPayment from "./pages/ViewPayment"; |
| 37 | 75 | import TravelerCategories from "./pages/TravelerCategories"; |
| @@ -36,10 +74,8 @@ | ||
| 36 | 74 | import ViewPayment from "./pages/ViewPayment"; |
| 37 | 75 | import TravelerCategories from "./pages/TravelerCategories"; |
| 38 | 76 | import TravelerCategoryForm from "./pages/TravelerCategoryForm"; |
| 39 | 77 | import Availability from "./pages/Availability"; |
| 40 | -import AvailabilityForm from "./pages/AvailabilityForm"; | |
| 41 | -import RecurringRuleForm from "./pages/RecurringRuleForm"; | |
| 42 | 78 | import Departures from "./pages/Departures"; |
| 43 | 79 | import DepartureForm from "./pages/DepartureForm"; |
| 44 | 80 | import ViewDeparture from "./pages/ViewDeparture"; |
| 45 | 81 | import Enquiries from "./pages/Enquiries"; |
| @@ -50,19 +86,55 @@ | ||
| 50 | 86 | import GoogleCalendar from "./pages/GoogleCalendar"; |
| 51 | 87 | import AdditionalServices from "./pages/AdditionalServices"; |
| 52 | 88 | import AdditionalServicesForm from "./pages/AdditionalServicesForm"; |
| 53 | 89 | import TripConsent from "./pages/TripConsent"; |
| 54 | -import TripConsentForm from "./pages/TripConsentForm"; | |
| 55 | 90 | import EmailAutomation from "./pages/EmailAutomation"; |
| 56 | 91 | import EmailTemplateForm from "./pages/EmailTemplateForm"; |
| 57 | 92 | import EmailSequenceForm from "./pages/EmailSequenceForm"; |
| 58 | -import AbandonedRecovery from "./pages/AbandonedRecovery"; | |
| 59 | -import DynamicPricing from "./pages/DynamicPricing"; | |
| 60 | -import DynamicPricingRuleForm from "./pages/DynamicPricingRuleForm"; | |
| 61 | 93 | import Attributes from "./pages/Attributes"; |
| 62 | 94 | import AttributeForm from "./pages/AttributeForm"; |
| 63 | 95 | import License from "./pages/License"; |
| 64 | 96 | |
| 97 | +// Lazy-loaded heavy pages — each becomes its own JS chunk. | |
| 98 | +const TripForm = lazy(() => import("./pages/TripForm")); | |
| 99 | +const Settings = lazy(() => import("./pages/Settings")); | |
| 100 | +const Team = lazy(() => import("./pages/Team")); | |
| 101 | +const Reports = lazy(() => import("./pages/Reports")); | |
| 102 | +const DiscountForm = lazy(() => import("./pages/DiscountForm")); | |
| 103 | +const Itinerary = lazy(() => import("./pages/Itinerary")); | |
| 104 | +const RecurringRuleForm = lazy(() => import("./pages/RecurringRuleForm")); | |
| 105 | +const Whatsapp = lazy(() => import("./pages/Whatsapp")); | |
| 106 | +const ChannelManager = lazy(() => import("./pages/ChannelManager")); | |
| 107 | +const Webhooks = lazy(() => import("./pages/Webhooks")); | |
| 108 | +const ScheduledPayments = lazy(() => import("./pages/ScheduledPayments")); | |
| 109 | +const TripConsentForm = lazy(() => import("./pages/TripConsentForm")); | |
| 110 | +const BookingForm = lazy(() => import("./pages/BookingForm")); | |
| 111 | +const ViewBooking = lazy(() => import("./pages/ViewBooking")); | |
| 112 | +const AvailabilityForm = lazy(() => import("./pages/AvailabilityForm")); | |
| 113 | +const DynamicPricing = lazy(() => import("./pages/DynamicPricing")); | |
| 114 | +const DynamicPricingRuleForm = lazy( | |
| 115 | + () => import("./pages/DynamicPricingRuleForm"), | |
| 116 | +); | |
| 117 | +const AiAssistant = lazy(() => import("./pages/AiAssistant")); | |
| 118 | +const AbandonedRecovery = lazy(() => import("./pages/AbandonedRecovery")); | |
| 119 | +const WhiteLabel = lazy(() => import("./pages/WhiteLabel")); | |
| 120 | + | |
| 121 | +/** | |
| 122 | + * Suspense fallback used between an old route unmounting and the new | |
| 123 | + * lazy chunk's JS arriving. Stays compact — a skeleton matching the | |
| 124 | + * common page-header / table shape so layout doesn't jump. | |
| 125 | + */ | |
| 126 | +const PageLoadingFallback: React.FC = () => ( | |
| 127 | + <div className="space-y-6"> | |
| 128 | + <div className="space-y-2"> | |
| 129 | + <Skeleton className="h-6 w-1/3" /> | |
| 130 | + <Skeleton className="h-4 w-2/3" /> | |
| 131 | + </div> | |
| 132 | + <Skeleton className="h-12 w-full" /> | |
| 133 | + <Skeleton className="h-64 w-full" /> | |
| 134 | + </div> | |
| 135 | +); | |
| 136 | + | |
| 65 | 137 | const App: React.FC = () => { |
| 66 | 138 | // Force re-render on URL change |
| 67 | 139 | const [urlKey, setUrlKey] = useState(0); |
| 68 | 140 | |
| @@ -92,18 +164,33 @@ | ||
| 92 | 164 | // Get subpage, tab, and action from URL query parameters |
| 93 | 165 | const subpage = useMemo(() => { |
| 94 | 166 | const params = new URLSearchParams(window.location.search); |
| 95 | 167 | return params.get("subpage") || "dashboard"; |
| 168 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 96 | 169 | }, [urlKey]); |
| 97 | 170 | |
| 171 | + // Mark a badged section seen when its page is opened, so the sidebar badge | |
| 172 | + // clears (bumps the wp_options marker server-side, then the shared counts | |
| 173 | + // query refreshes). Fires once per navigation into the page. | |
| 174 | + const markSectionSeen = useMarkSectionSeen(); | |
| 175 | + useEffect(() => { | |
| 176 | + const section = SUBPAGE_TO_SECTION[subpage.toLowerCase()]; | |
| 177 | + if (section) { | |
| 178 | + markSectionSeen.mutate(section); | |
| 179 | + } | |
| 180 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 181 | + }, [subpage]); | |
| 182 | + | |
| 98 | 183 | const tab = useMemo(() => { |
| 99 | 184 | const params = new URLSearchParams(window.location.search); |
| 100 | 185 | return params.get("tab") || "all"; |
| 186 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 101 | 187 | }, [urlKey]); |
| 102 | 188 | |
| 103 | 189 | const action = useMemo(() => { |
| 104 | 190 | const params = new URLSearchParams(window.location.search); |
| 105 | 191 | return params.get("action"); |
| 192 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 106 | 193 | }, [urlKey, subpage]); |
| 107 | 194 | |
| 108 | 195 | // Render the appropriate page based on subpage, tab, and action parameters |
| 109 | 196 | const renderPage = () => { |
| @@ -267,8 +354,13 @@ | ||
| 267 | 354 | return <DiscountForm />; |
| 268 | 355 | } |
| 269 | 356 | return <Discounts />; |
| 270 | 357 | case "payments": |
| 358 | + // Scheduled balance payments (Pro module) live under Payments as a tab. | |
| 359 | + // Checked before the action branches because this tab has no forms. | |
| 360 | + if (tab.toLowerCase() === "scheduled") { | |
| 361 | + return <ScheduledPayments />; | |
| 362 | + } | |
| 271 | 363 | // Check if we're viewing, creating, or editing a payment |
| 272 | 364 | if (action === "view") { |
| 273 | 365 | return <ViewPayment />; |
| 274 | 366 | } |
| @@ -316,8 +408,20 @@ | ||
| 316 | 408 | |
| 317 | 409 | return <DynamicPricing />; |
| 318 | 410 | case "license": |
| 319 | 411 | return <License />; |
| 412 | + case "white-label": | |
| 413 | + return <WhiteLabel />; | |
| 414 | + case "ai-assistant": | |
| 415 | + return <AiAssistant />; | |
| 416 | + case "whatsapp": | |
| 417 | + return <Whatsapp />; | |
| 418 | + case "channel-manager": | |
| 419 | + return <ChannelManager />; | |
| 420 | + case "webhooks": | |
| 421 | + return <Webhooks />; | |
| 422 | + case "team": | |
| 423 | + return <Team />; | |
| 320 | 424 | case "dashboard": |
| 321 | 425 | default: |
| 322 | 426 | return <Dashboard />; |
| 323 | 427 | } |
| @@ -322,8 +426,12 @@ | ||
| 322 | 426 | return <Dashboard />; |
| 323 | 427 | } |
| 324 | 428 | }; |
| 325 | 429 | |
| 326 | - return <Layout>{renderPage()}</Layout>; | |
| 430 | + return ( | |
| 431 | + <Layout> | |
| 432 | + <Suspense fallback={<PageLoadingFallback />}>{renderPage()}</Suspense> | |
| 433 | + </Layout> | |
| 434 | + ); | |
| 327 | 435 | }; |
| 328 | 436 | |
| 329 | 437 | export default App; |