import React, { useMemo, useState, useEffect, Suspense, lazy } from "react"; import Layout from "./components/Layout"; import { Skeleton } from "./components/ui/skeleton"; import { useMarkSectionSeen, SUBPAGE_TO_SECTION, } from "./hooks/useNotificationCounts"; /* * Page-loading strategy. * * Pages >1500 lines are split into separate chunks via React.lazy so they * land in the JS bundle on demand instead of inflating the initial paint. * Vite picks these up automatically as separate output files — operators * who never visit the Whatsapp tab never download its ~80KB of code. * * Pages that ship eagerly: * - Dashboard: the default landing route, almost always rendered first * - Smaller list pages (Trips, Bookings, Customers, etc.) where the * lazy-chunk overhead (extra HTTP round-trip on first nav) outweighs * the bytes saved on initial paint * * Pages that ship lazily (in line-count order, biggest savings first): * - TripForm (9.6k loc) * - Settings (8.6k loc) * - Team (3.5k loc) * - Reports (3.1k loc) * - DiscountForm (2.7k loc) * - Itinerary (2.7k loc) * - RecurringRuleForm (2.6k loc) * - Whatsapp (2.6k loc) * - ChannelManager (2.5k loc) * - Webhooks (2.5k loc) * - TripConsentForm (2.1k loc) * - BookingForm (1.8k loc) * - DynamicPricing (1.8k loc) * - AvailabilityForm (1.7k loc) * - AiAssistant (1.6k loc) * - AbandonedRecovery (1.6k loc) * - WhiteLabel (1.6k loc) * - DynamicPricingRuleForm (1.5k loc) * - ViewBooking (1.5k loc) * * If you add a new heavy page, lazy-import it here too; cheap to add and * the only thing to remember is that lazy components must be rendered * inside a boundary (we wrap the whole renderPage() output). */ import Dashboard from "./pages/Dashboard"; import Trips from "./pages/Trips"; import Activities from "./pages/Activities"; import ActivityForm from "./pages/ActivityForm"; import Destinations from "./pages/Destinations"; import DestinationForm from "./pages/DestinationForm"; import Categories from "./pages/Categories"; import CategoryForm from "./pages/CategoryForm"; import DifficultyLevels from "./pages/DifficultyLevels"; import DifficultyLevelForm from "./pages/DifficultyLevelForm"; import Bookings from "./pages/Bookings"; import Customers from "./pages/Customers"; import CustomerForm from "./pages/CustomerForm"; import ViewCustomer from "./pages/ViewCustomer"; import Reviews from "./pages/Reviews"; import ReviewForm from "./pages/ReviewForm"; import ViewReview from "./pages/ViewReview"; import Tools from "./components/Tools"; import ItemTypes from "./pages/ItemTypes"; import ItemTypeForm from "./pages/ItemTypeForm"; import Items from "./pages/Items"; import ItemForm from "./pages/ItemForm"; import ItineraryForm from "./pages/ItineraryForm"; import Discounts from "./pages/Discounts"; import Payments from "./pages/Payments"; import PaymentForm from "./pages/PaymentForm"; import ViewPayment from "./pages/ViewPayment"; import TravelerCategories from "./pages/TravelerCategories"; import TravelerCategoryForm from "./pages/TravelerCategoryForm"; import Availability from "./pages/Availability"; import Departures from "./pages/Departures"; import DepartureForm from "./pages/DepartureForm"; import ViewDeparture from "./pages/ViewDeparture"; import Enquiries from "./pages/Enquiries"; import ViewEnquiry from "./pages/ViewEnquiry"; import EnquiryForm from "./pages/EnquiryForm"; import Modules from "./pages/Modules"; import Travelers from "./pages/Travelers"; import GoogleCalendar from "./pages/GoogleCalendar"; import AdditionalServices from "./pages/AdditionalServices"; import AdditionalServicesForm from "./pages/AdditionalServicesForm"; import TripConsent from "./pages/TripConsent"; import EmailAutomation from "./pages/EmailAutomation"; import EmailTemplateForm from "./pages/EmailTemplateForm"; import EmailSequenceForm from "./pages/EmailSequenceForm"; import Attributes from "./pages/Attributes"; import AttributeForm from "./pages/AttributeForm"; import License from "./pages/License"; // Lazy-loaded heavy pages — each becomes its own JS chunk. const TripForm = lazy(() => import("./pages/TripForm")); const Settings = lazy(() => import("./pages/Settings")); const Team = lazy(() => import("./pages/Team")); const Reports = lazy(() => import("./pages/Reports")); const DiscountForm = lazy(() => import("./pages/DiscountForm")); const Itinerary = lazy(() => import("./pages/Itinerary")); const RecurringRuleForm = lazy(() => import("./pages/RecurringRuleForm")); const Whatsapp = lazy(() => import("./pages/Whatsapp")); const ChannelManager = lazy(() => import("./pages/ChannelManager")); const Webhooks = lazy(() => import("./pages/Webhooks")); const TripConsentForm = lazy(() => import("./pages/TripConsentForm")); const BookingForm = lazy(() => import("./pages/BookingForm")); const ViewBooking = lazy(() => import("./pages/ViewBooking")); const AvailabilityForm = lazy(() => import("./pages/AvailabilityForm")); const DynamicPricing = lazy(() => import("./pages/DynamicPricing")); const DynamicPricingRuleForm = lazy( () => import("./pages/DynamicPricingRuleForm"), ); const AiAssistant = lazy(() => import("./pages/AiAssistant")); const AbandonedRecovery = lazy(() => import("./pages/AbandonedRecovery")); const WhiteLabel = lazy(() => import("./pages/WhiteLabel")); /** * Suspense fallback used between an old route unmounting and the new * lazy chunk's JS arriving. Stays compact — a skeleton matching the * common page-header / table shape so layout doesn't jump. */ const PageLoadingFallback: React.FC = () => (
); const App: React.FC = () => { // Force re-render on URL change const [urlKey, setUrlKey] = useState(0); useEffect(() => { const handleLocationChange = () => { setUrlKey((prev) => prev + 1); }; // Listen for popstate (back/forward button) window.addEventListener("popstate", handleLocationChange); // Also check periodically (fallback for direct navigation) const interval = setInterval(() => { const currentSearch = window.location.search; if (currentSearch !== (window as any).__lastSearch) { (window as any).__lastSearch = currentSearch; handleLocationChange(); } }, 100); return () => { window.removeEventListener("popstate", handleLocationChange); clearInterval(interval); }; }, []); // Get subpage, tab, and action from URL query parameters const subpage = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("subpage") || "dashboard"; // eslint-disable-next-line react-hooks/exhaustive-deps }, [urlKey]); // Mark a badged section seen when its page is opened, so the sidebar badge // clears (bumps the wp_options marker server-side, then the shared counts // query refreshes). Fires once per navigation into the page. const markSectionSeen = useMarkSectionSeen(); useEffect(() => { const section = SUBPAGE_TO_SECTION[subpage.toLowerCase()]; if (section) { markSectionSeen.mutate(section); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [subpage]); const tab = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("tab") || "all"; // eslint-disable-next-line react-hooks/exhaustive-deps }, [urlKey]); const action = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get("action"); // eslint-disable-next-line react-hooks/exhaustive-deps }, [urlKey, subpage]); // Render the appropriate page based on subpage, tab, and action parameters const renderPage = () => { switch (subpage.toLowerCase()) { case "trips": // Handle trips tabs switch (tab.toLowerCase()) { case "activities": // Check if we're creating or editing an activity if (action === "create" || action === "edit") { return ; } return ; case "destinations": // Check if we're creating or editing a destination if (action === "create" || action === "edit") { return ; } return ; case "categories": // Check if we're creating or editing a category if (action === "create" || action === "edit") { return ; } return ; case "difficulty-levels": // Check if we're creating or editing a difficulty level if (action === "create" || action === "edit") { return ; } return ; case "availability": // Check if we're creating or editing a recurring rule if (action === "create-rule" || action === "edit-rule") { return ; } // Check if we're creating or editing an availability date if (action === "create" || action === "edit") { return ; } return ; case "additional-services": // Check if we're creating or editing a service if (action === "create" || action === "edit") { return ; } return ; case "trip-consent": // Check if we're creating or editing a consent form if (action === "create" || action === "edit") { return ; } return ; case "attributes": // Check if we're creating or editing an attribute if (action === "create" || action === "edit") { return ; } return ; case "all": default: // Check if we're creating or editing a trip if (action === "create" || action === "edit") { return ; } return ; } case "bookings": // Check if we're viewing, creating, or editing a booking if (action === "view") { return ; } if (action === "create" || action === "edit") { return ; } return ; case "customers": // Check if we're viewing, creating, or editing a customer if (action === "view") { return ; } if (action === "create" || action === "edit") { return ; } return ; case "travelers": return ; case "enquiries": // Check if we're viewing or editing an enquiry if (action === "view") { return ; } if (action === "edit" || action === "new") { return ; } return ; case "reviews": // Check if we're viewing, creating, or editing a review if (action === "view") { return ; } if (action === "create" || action === "edit") { return ; } return ; case "email-automation": // Check if we're creating or editing a template or sequence if (action === "create" && tab === "template") { return ; } if (action === "edit" && tab === "template") { return ; } if (action === "create" && tab === "sequence") { return ; } if (action === "edit" && tab === "sequence") { return ; } // Default: if action is edit without tab, assume template if (action === "edit") { return ; } return ; case "itinerary": // Handle itinerary tabs switch (tab.toLowerCase()) { case "item-types": // Check if we're creating or editing an item type if (action === "create" || action === "edit") { return ; } return ; case "items": // Check if we're creating or editing an item if (action === "create" || action === "edit") { return ; } return ; case "itinerary": // Check if we're creating or editing an itinerary entry if (action === "create" || action === "edit") { return ; } return ; default: return ; } case "departures": // Check if we're viewing, creating, or editing a departure if (action === "view") { return ; } if (action === "create" || action === "edit") { return ; } return ; case "discounts": // Check if we're creating or editing a discount if (action === "create" || action === "edit") { return ; } return ; case "payments": // Check if we're viewing, creating, or editing a payment if (action === "view") { return ; } if (action === "create" || action === "edit") { return ; } return ; case "traveler-categories": // Check if we're creating or editing a traveler category if (action === "create" || action === "edit") { return ; } return ; case "reports": return ; case "tools": return ; case "settings": return ; case "modules": return ; case "google-calendar": return ; case "additional-services": // Check if we're creating or editing a service if (action === "create" || action === "edit") { return ; } return ; case "trip-consent": // Check if we're creating or editing a consent form if (action === "create" || action === "edit") { return ; } return ; case "abandoned-recovery": return ; case "dynamic-pricing": if ( action === "create-pricing-rule" || action === "edit-pricing-rule" ) { return ; } return ; case "license": return ; case "white-label": return ; case "ai-assistant": return ; case "whatsapp": return ; case "channel-manager": return ; case "webhooks": return ; case "team": return ; case "dashboard": default: return ; } }; return ( }>{renderPage()} ); }; export default App;