PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5
Yatra – Travel Booking & Tour Operator Software v3.0.5
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 / components / Layout.tsx

Layout.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.5, at resources/js/components/Layout.tsx

1,585 lines 63.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, {
2 useState,
3 useMemo,
4 useEffect,
5 useRef,
6 useCallback,
7 } from "react";
8 import { useMutation } from "@tanstack/react-query";
9 import {
10 LayoutDashboard,
11 MapPin,
12 Calendar,
13 CalendarDays,
14 Star,
15 BarChart3,
16 Settings,
17 Wrench,
18 Moon,
19 FileText,
20 CreditCard,
21 Package,
22 UserCircle,
23 FolderTree,
24 Tag,
25 TrendingUp,
26 List,
27 Activity,
28 Crown,
29 Sparkles,
30 ChevronDown,
31 ChevronRight,
32 Mail,
33 Key,
34 FileSignature,
35 Route,
36 BadgePercent,
37 Plane,
38 MessageSquare,
39 MessageCircle,
40 Network,
41 Webhook,
42 Users,
43 Puzzle,
44 ArrowLeft,
45 Gift,
46 Loader2,
47 RotateCcw,
48 Sun,
49 User,
50 } from "lucide-react";
51 import { __ } from "../lib/i18n";
52 import { Button } from "../components/ui/button";
53 import { Tooltip } from "../components/ui/tooltip";
54 import { useToast } from "../components/ui/toast";
55 import { postFlushRewriteRules } from "../api/settings-api";
56
57 // Helper function to extract Gravatar URL from WordPress get_avatar HTML
58 function extractGravatarUrl(avatarHtml: string, size: number): string {
59 if (!avatarHtml) {
60 return `https://www.gravatar.com/avatar/00000000000000000000000000000000?s=${size}&d=identicon&r=pg`;
61 }
62
63 // Extract src attribute from img tag
64 const imgMatch = avatarHtml.match(/<img[^>]+src=["']([^"']+)["']/i);
65 if (imgMatch && imgMatch[1]) {
66 // Replace size parameter if needed
67 return imgMatch[1].replace(/s=\d+/, `s=${size}`);
68 }
69
70 // Fallback to default
71 return `https://www.gravatar.com/avatar/00000000000000000000000000000000?s=${size}&d=identicon&r=pg`;
72 }
73
74 // Helper function to get Gravatar URL using WordPress data
75 function getGravatarUrl(size: number): string {
76 const avatarHtml = (window as any)?.yatraAdmin?.currentUserAvatar || "";
77 return extractGravatarUrl(avatarHtml, size);
78 }
79
80 import { ConditionalRender } from "../components/ui/conditional-render";
81 import {
82 readMenuOverrides,
83 readMenuOrder,
84 readUiChrome,
85 type MenuOverrides,
86 } from "../lib/sidebar-menu-defaults";
87 import { MenuIcon } from "../lib/menu-icon";
88 import {
89 Card,
90 CardContent,
91 CardHeader,
92 CardTitle,
93 CardDescription,
94 } from "../components/ui/card";
95 import { Badge } from "../components/ui/badge";
96 import {
97 useModulesQuery,
98 useToggleModule,
99 type ModuleDefinition,
100 } from "../hooks/useModules";
101 import { isProPluginActive, isModuleActive } from "../lib/plugin-utils";
102 import { navigateMenu } from "../hooks/useNavigate";
103 import { canCap } from "../hooks/useCapabilities";
104 import { InlineNotices } from "./notices/InlineNotices";
105
106 interface LayoutProps {
107 children: React.ReactNode;
108 }
109
110 const Layout: React.FC<LayoutProps> = ({ children }) => {
111 // Load dark mode preference from localStorage
112 const [darkMode, setDarkMode] = useState(() => {
113 const saved = localStorage.getItem("yatra-dark-mode");
114 return saved === "true";
115 });
116
117 // Apply dark mode to document on mount and when it changes
118 useEffect(() => {
119 const root = document.documentElement;
120 if (darkMode) {
121 root.classList.add("dark");
122 localStorage.setItem("yatra-dark-mode", "true");
123 } else {
124 root.classList.remove("dark");
125 localStorage.setItem("yatra-dark-mode", "false");
126 }
127 }, [darkMode]);
128
129 const [isModulesPanelOpen, setIsModulesPanelOpen] = useState(false);
130 const [isUserDropdownOpen, setIsUserDropdownOpen] = useState(false);
131 const modulesPanelRef = useRef<HTMLDivElement | null>(null);
132 const userDropdownRef = useRef<HTMLDivElement | null>(null);
133
134 // License status state for real-time updates
135 const [licenseStatus, setLicenseStatus] = useState<string | null>(
136 (window as any).yatraAdmin?.licenseStatus || null,
137 );
138
139 const { showToast } = useToast();
140 const flushRewriteRulesMutation = useMutation({
141 mutationFn: async () => postFlushRewriteRules(),
142 onSuccess: () => {
143 showToast(__("Rewrite rules flushed successfully", "yatra"), "success");
144 },
145 onError: (error: any) => {
146 showToast(
147 error?.message || __("Failed to flush rewrite rules", "yatra"),
148 "error",
149 );
150 },
151 });
152 const { data: modulesData, isLoading: isLoadingModules } = useModulesQuery({
153 enabled: isModulesPanelOpen,
154 });
155 const toggleModuleMutation = useToggleModule();
156 // Ensure modulesData is always an array before slicing
157 // eslint-disable-next-line react-hooks/exhaustive-deps
158 const safeModulesData = Array.isArray(modulesData) ? modulesData : [];
159 const modulesPreview = useMemo<ModuleDefinition[]>(
160 () => safeModulesData.slice(0, 3),
161 [safeModulesData],
162 );
163 const handleQuickToggle = (module: ModuleDefinition, enabled: boolean) => {
164 toggleModuleMutation.mutate({
165 slug: module.slug,
166 enabled,
167 name: module.name,
168 });
169 };
170
171 // Track URL changes to update menu state
172 const [urlKey, setUrlKey] = useState(0);
173 const [navRefreshKey, setNavRefreshKey] = useState(0);
174
175 /** SPA: avoid full reload — keeps PHP boot splash from showing on every sidebar click */
176 const handleMenuNavClick = useCallback(
177 (e: React.MouseEvent<HTMLAnchorElement>, subpage: string, tab?: string) => {
178 if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey || e.button !== 0) {
179 return;
180 }
181 e.preventDefault();
182 navigateMenu(subpage, tab);
183 },
184 [],
185 );
186
187 // Close dropdowns when clicking outside
188 useEffect(() => {
189 const handleClickOutside = (event: MouseEvent) => {
190 if (
191 modulesPanelRef.current &&
192 !modulesPanelRef.current.contains(event.target as Node)
193 ) {
194 setIsModulesPanelOpen(false);
195 }
196 if (
197 userDropdownRef.current &&
198 !userDropdownRef.current.contains(event.target as Node)
199 ) {
200 setIsUserDropdownOpen(false);
201 }
202 };
203
204 document.addEventListener("mousedown", handleClickOutside);
205 return () => document.removeEventListener("mousedown", handleClickOutside);
206 }, []);
207
208 useEffect(() => {
209 const handleLocationChange = () => {
210 setUrlKey((prev) => prev + 1);
211 };
212
213 // Listen for popstate (back/forward button)
214 window.addEventListener("popstate", handleLocationChange);
215
216 // Also check periodically (fallback for direct navigation)
217 const interval = setInterval(() => {
218 const currentSearch = window.location.search;
219 if (currentSearch !== (window as any).__lastSearch) {
220 (window as any).__lastSearch = currentSearch;
221 handleLocationChange();
222 }
223 }, 100);
224
225 return () => {
226 window.removeEventListener("popstate", handleLocationChange);
227 clearInterval(interval);
228 };
229 }, []);
230
231 // Listen for module updates to refresh navigation
232 useEffect(() => {
233 const handleModuleUpdate = () => {
234 // Force re-render of menu items by updating navRefreshKey
235 setNavRefreshKey((prev) => prev + 1);
236 // Also update urlKey to ensure all memoized values refresh
237 setUrlKey((prev) => prev + 1);
238 };
239
240 const handleForceRefresh = () => {
241 setNavRefreshKey((prev) => prev + 1);
242 setUrlKey((prev) => prev + 1);
243 };
244
245 const handleLicenseStatusUpdate = (event: any) => {
246 const newStatus = event.detail?.status;
247 if (newStatus) {
248 setLicenseStatus(newStatus);
249 }
250 };
251
252 window.addEventListener("yatra-modules-updated", handleModuleUpdate);
253 window.addEventListener("yatra-force-nav-refresh", handleForceRefresh);
254 window.addEventListener(
255 "yatra-license-status-updated",
256 handleLicenseStatusUpdate,
257 );
258
259 return () => {
260 window.removeEventListener("yatra-modules-updated", handleModuleUpdate);
261 window.removeEventListener("yatra-force-nav-refresh", handleForceRefresh);
262 window.removeEventListener(
263 "yatra-license-status-updated",
264 handleLicenseStatusUpdate,
265 );
266 };
267 }, []);
268
269 useEffect(() => {
270 if (!isModulesPanelOpen) return;
271
272 const handleClickOutside = (event: MouseEvent) => {
273 if (
274 modulesPanelRef.current &&
275 !modulesPanelRef.current.contains(event.target as Node)
276 ) {
277 setIsModulesPanelOpen(false);
278 }
279 };
280
281 document.addEventListener("mousedown", handleClickOutside);
282 return () => document.removeEventListener("mousedown", handleClickOutside);
283 }, [isModulesPanelOpen]);
284
285 // Get current subpage and tab from URL
286 const currentSubpage = useMemo(() => {
287 const params = new URLSearchParams(window.location.search);
288 return params.get("subpage") || "dashboard";
289 // eslint-disable-next-line react-hooks/exhaustive-deps
290 }, [urlKey]);
291
292 const currentTab = useMemo(() => {
293 const params = new URLSearchParams(window.location.search);
294 return params.get("tab") || "all";
295 // eslint-disable-next-line react-hooks/exhaustive-deps
296 }, [urlKey]);
297
298 const currentAction = useMemo(() => {
299 const params = new URLSearchParams(window.location.search);
300 return params.get("action");
301 // eslint-disable-next-line react-hooks/exhaustive-deps
302 }, [urlKey]);
303
304 // Check if we're on the trip form page
305 const isTripFormPage = useMemo(() => {
306 return (
307 currentSubpage === "trips" &&
308 (currentTab === "all" || !currentTab) &&
309 (currentAction === "create" || currentAction === "edit")
310 );
311 // eslint-disable-next-line react-hooks/exhaustive-deps
312 }, [currentSubpage, currentTab, currentAction, urlKey]);
313
314 // Track expanded submenus - initialize based on current subpage
315 const [expandedMenus, setExpandedMenus] = useState<string[]>(() => {
316 const params = new URLSearchParams(window.location.search);
317 const subpage = params.get("subpage") || "dashboard";
318 const menus: string[] = [];
319
320 if (subpage === "trips") {
321 menus.push("trips");
322 }
323
324 if (subpage === "itinerary") {
325 menus.push("itinerary");
326 }
327
328 return menus;
329 });
330
331 // Auto-expand menu when on submenu pages
332 useEffect(() => {
333 const menusToExpand: string[] = [];
334
335 if (currentSubpage === "trips") {
336 menusToExpand.push("trips");
337 }
338
339 if (currentSubpage === "itinerary") {
340 menusToExpand.push("itinerary");
341 }
342
343 setExpandedMenus((prev) => {
344 // Only update if the menus to expand are different
345 const newMenus = [...new Set([...prev, ...menusToExpand])];
346 if (
347 newMenus.length !== prev.length ||
348 !newMenus.every((m) => prev.includes(m))
349 ) {
350 return newMenus;
351 }
352 return prev;
353 });
354 }, [currentSubpage, urlKey]);
355
356 // Get base admin URL
357 const baseUrl = useMemo(() => {
358 return window.yatraAdmin?.siteUrl
359 ? `${window.yatraAdmin.siteUrl}/wp-admin/admin.php?page=yatra`
360 : "/wp-admin/admin.php?page=yatra";
361 }, []);
362
363 const menuItems = useMemo(
364 // Every menu label is wrapped in __() with a literal string so
365 // gettext extraction (scripts/extract-js-pot.mjs) picks them up
366 // and Loco / Poedit / wp i18n can translate them. Previously most
367 // were raw English literals — translations existed in the .po but
368 // never reached the UI because there was no translation lookup at
369 // render time. Wrapping is the WordPress-canonical way; the
370 // pre_load_script_translations filter then delivers the locale
371 // data to wp.i18n on every admin page load.
372 () =>
373 [
374 {
375 subpage: "dashboard",
376 label: __("Dashboard", "yatra"),
377 icon: LayoutDashboard,
378 cap: "yatra_access_admin",
379 },
380 {
381 subpage: "trips",
382 label: __("Trips", "yatra"),
383 icon: MapPin,
384 cap: "yatra_view_trips",
385 submenu: [
386 { tab: "all", label: __("All Trips", "yatra"), icon: List },
387 {
388 tab: "activities",
389 label: __("Activities", "yatra"),
390 icon: Activity,
391 },
392 {
393 tab: "destinations",
394 label: __("Destinations", "yatra"),
395 icon: Route,
396 },
397 {
398 tab: "categories",
399 label: __("Categories", "yatra"),
400 icon: FolderTree,
401 },
402 {
403 tab: "difficulty-levels",
404 label: __("Difficulty Levels", "yatra"),
405 icon: TrendingUp,
406 },
407 // Availability - FREE feature, always show
408 {
409 tab: "availability",
410 label: __("Availability", "yatra"),
411 icon: CalendarDays,
412 },
413 // Attributes - FREE feature, always show
414 { tab: "attributes", label: __("Attributes", "yatra"), icon: Tag },
415 // Additional Services - show only if Pro plugin is active and module is enabled
416 ...(isProPluginActive() && isModuleActive("additional_services")
417 ? [
418 {
419 tab: "additional-services",
420 label: __("Additional Services", "yatra"),
421 icon: Package,
422 isPremium: true,
423 },
424 ]
425 : []),
426 // Trip Consent - show only if Pro plugin is active and module is enabled
427 ...(isProPluginActive() && isModuleActive("trip_consent")
428 ? [
429 {
430 tab: "trip-consent",
431 label: __("Trip Consent", "yatra"),
432 icon: FileSignature,
433 isPremium: true,
434 },
435 ]
436 : []),
437 ],
438 },
439 {
440 subpage: "traveler-categories",
441 label: __("Traveler Categories", "yatra"),
442 icon: UserCircle,
443 cap: "yatra_manage_trip_taxonomies",
444 },
445 {
446 subpage: "itinerary",
447 label: __("Itinerary", "yatra"),
448 icon: FileText,
449 cap: "yatra_edit_trips",
450 submenu: [
451 { tab: "item-types", label: __("Item Types", "yatra"), icon: Tag },
452 { tab: "items", label: __("Items", "yatra"), icon: Route },
453 {
454 tab: "itinerary",
455 label: __("Itinerary", "yatra"),
456 icon: FileText,
457 },
458 ],
459 },
460 // Departures - FREE feature, always show
461 {
462 subpage: "departures",
463 label: __("Departures", "yatra"),
464 icon: Calendar,
465 cap: "yatra_view_departures",
466 },
467 {
468 subpage: "discounts",
469 label: __("Discounts", "yatra"),
470 icon: BadgePercent,
471 cap: "yatra_manage_discounts",
472 },
473 {
474 subpage: "payments",
475 label: __("Payments", "yatra"),
476 icon: CreditCard,
477 cap: "yatra_view_financial_reports",
478 },
479 {
480 subpage: "bookings",
481 label: __("Bookings", "yatra"),
482 icon: Calendar,
483 cap: "yatra_view_bookings",
484 },
485 {
486 subpage: "customers",
487 label: __("Customers", "yatra"),
488 icon: UserCircle,
489 cap: "yatra_view_customers",
490 },
491 {
492 subpage: "travelers",
493 label: __("Travelers", "yatra"),
494 icon: Plane,
495 cap: "yatra_view_customers",
496 },
497 {
498 subpage: "enquiries",
499 label: __("Enquiries", "yatra"),
500 icon: MessageSquare,
501 cap: "yatra_view_enquiries",
502 },
503 {
504 subpage: "reviews",
505 label: __("Reviews", "yatra"),
506 icon: Star,
507 cap: "yatra_view_reviews",
508 },
509 {
510 subpage: "reports",
511 label: __("Reports", "yatra"),
512 icon: BarChart3,
513 cap: "yatra_view_operational_reports",
514 },
515 // Email — SMTP & transactional for all; Pro adds automation tabs on the same screen
516 {
517 subpage: "email-automation",
518 label: __("Email", "yatra"),
519 icon: Mail,
520 isPremium: false,
521 cap: "yatra_manage_emails",
522 },
523 // Abandoned Booking Recovery - show only if Pro plugin is active and module is enabled
524 ...(isProPluginActive() && isModuleActive("abandoned_booking_recovery")
525 ? [
526 {
527 subpage: "abandoned-recovery",
528 label: __("Abandoned Recovery", "yatra"),
529 icon: RotateCcw,
530 isPremium: true,
531 cap: "yatra_manage_email_automation",
532 },
533 ]
534 : []),
535 // Dynamic Pricing - show only if Pro plugin is active and module is enabled
536 ...(isProPluginActive() && isModuleActive("dynamic_pricing")
537 ? [
538 {
539 subpage: "dynamic-pricing",
540 label: __("Dynamic Pricing", "yatra"),
541 icon: TrendingUp,
542 isPremium: true,
543 cap: "yatra_edit_trips",
544 },
545 ]
546 : []),
547 {
548 subpage: "modules",
549 label: __("Modules", "yatra"),
550 icon: Puzzle,
551 cap: "yatra_manage_modules",
552 },
553 // White Label — only when the Agency-tier module is actually enabled.
554 // (Agency license alone is not enough; the module toggle must be on,
555 // otherwise users would see a link to a disabled feature. To get
556 // here for the first time, an Agency admin enables the module from
557 // Yatra → Modules.) The `whiteLabelEnabled` flag from
558 // AdminAssetsProvider already implies Agency-active.
559 // White Label — needs Agency-tier license AND module toggle on.
560 // Mirrors the AI Assistant gate below so both menus appear /
561 // disappear instantly when their module toggles, while staying
562 // license-safe if the client-side `whiteLabelEnabled` flag is
563 // ever stale or over-eager. AdminAssetsProvider seeds both
564 // `isAgency` and `whiteLabelEnabled`; useModules.ts updates the
565 // latter on toggle and fires `yatra-modules-updated` which bumps
566 // navRefreshKey → this memo recomputes → menu appears.
567 ...((window as any).yatraAdmin?.isAgency &&
568 (window as any).yatraAdmin?.whiteLabelEnabled
569 ? [
570 {
571 subpage: "white-label",
572 label: __("White Label", "yatra"),
573 icon: Crown,
574 isPremium: true,
575 cap: "yatra_manage_white_label",
576 },
577 ]
578 : []),
579 // AI Assistant — only when BOTH the license tier qualifies
580 // (Growth or Agency) AND the module toggle is on. Operators who
581 // disable the module shouldn't keep seeing the menu item — it
582 // would lead to a settings page they've explicitly opted out of.
583 // The toggle handler in useModules.ts updates
584 // window.yatraAdmin.aiAssistantEnabled and fires
585 // `yatra-modules-updated`, which bumps navRefreshKey so this
586 // memo recomputes immediately without a page reload.
587 ...((window as any).yatraAdmin?.isAiEligible &&
588 (window as any).yatraAdmin?.aiAssistantEnabled
589 ? [
590 {
591 subpage: "ai-assistant",
592 label: __("AI Assistant", "yatra"),
593 icon: Sparkles,
594 isPremium: true,
595 cap: "yatra_manage_ai",
596 },
597 ]
598 : []),
599 // WhatsApp Notifications — Growth/Agency module. Same gate
600 // pattern as AI Assistant: needs the AI-eligibility flag (which
601 // is the Growth-or-Agency tier check) AND the module toggle on.
602 // useModules.ts updates `whatsappEnabled` on toggle + fires
603 // `yatra-modules-updated` so this menu appears instantly without
604 // a page reload.
605 ...((window as any).yatraAdmin?.isAiEligible &&
606 (window as any).yatraAdmin?.whatsappEnabled
607 ? [
608 {
609 subpage: "whatsapp",
610 label: __("WhatsApp", "yatra"),
611 icon: MessageCircle,
612 isPremium: true,
613 cap: "yatra_manage_whatsapp",
614 },
615 ]
616 : []),
617 // Channel Manager — Agency-tier OTA distribution hub. Same gate
618 // pattern as White Label: needs the Agency license AND the module
619 // toggle on. useModules.ts updates `channelManagerEnabled` on
620 // toggle + fires `yatra-modules-updated` so this menu appears
621 // instantly without a page reload.
622 ...((window as any).yatraAdmin?.isAgency &&
623 (window as any).yatraAdmin?.channelManagerEnabled
624 ? [
625 {
626 subpage: "channel-manager",
627 label: __("Channel Manager", "yatra"),
628 icon: Network,
629 isPremium: true,
630 cap: "yatra_manage_channel_manager",
631 },
632 ]
633 : []),
634 // Webhooks — Agency-tier outbound integration hub. Same gate
635 // as Channel Manager / White Label: Agency license + module on.
636 ...((window as any).yatraAdmin?.isAgency &&
637 (window as any).yatraAdmin?.webhooksEnabled
638 ? [
639 {
640 subpage: "webhooks",
641 label: __("Webhooks", "yatra"),
642 icon: Webhook,
643 isPremium: true,
644 cap: "yatra_manage_webhooks",
645 },
646 ]
647 : []),
648 // Team & Access — Agency-tier granular roles + capability-level
649 // access + audit log. Gate matches Webhooks: Agency license +
650 // module on. The page itself is also cap-gated server-side; the
651 // sidebar entry is just UX.
652 ...((window as any).yatraAdmin?.isAgency &&
653 (window as any).yatraAdmin?.teamEnabled
654 ? [
655 {
656 subpage: "team",
657 label: __("Team & Access", "yatra"),
658 icon: Users,
659 isPremium: true,
660 cap: "yatra_manage_team",
661 },
662 ]
663 : []),
664 {
665 subpage: "license",
666 label: __("License", "yatra"),
667 icon: Key,
668 cap: "yatra_manage_settings",
669 },
670 {
671 subpage: "settings",
672 label: __("Settings", "yatra"),
673 icon: Settings,
674 cap: "yatra_manage_settings",
675 },
676 ]
677 // Cap-gate every entry. canCap() returns true for WP admins and
678 // when the Team module isn't enabled (= admin-only mode), so this
679 // is a no-op on every site without team management. With team
680 // management active, non-admin team members only see the menus
681 // their role's caps permit.
682 //
683 // Items without a `cap` field are always shown (default-allow).
684 .filter((item: any) => !item.cap || canCap(item.cap)),
685 // eslint-disable-next-line react-hooks/exhaustive-deps
686 [navRefreshKey],
687 ); // Re-calculate when navRefreshKey changes
688
689 /**
690 * Apply White Label customizations on top of the base menuItems.
691 *
692 * Implementation: factor menuItems into a flat atom registry, apply
693 * label/icon/hidden overrides, then rebuild the menu tree honoring
694 * the saved parent override (cross-parent moves) and per-parent
695 * order map. URLs always come from the atom's intrinsic identity —
696 * promoting a submenu to top-level does NOT change its route, so
697 * App.tsx routing keeps working unchanged.
698 */
699 const brandedMenuItems = useMemo(() => {
700 const overrides: MenuOverrides = readMenuOverrides();
701 const orderMap = readMenuOrder();
702
703 interface Atom {
704 key: string; // "trips" or "trips.activities"
705 defaultParent: string; // "" for top-level, parent slug for submenu
706 label: string;
707 iconOverride?: unknown;
708 icon: any;
709 // Bag of routing/render props copied from the original menu item.
710 subpage: string;
711 tab?: string;
712 isPremium?: boolean;
713 }
714
715 const atoms: Atom[] = [];
716 const atomByKey = new Map<string, Atom>();
717
718 const apply = (
719 key: string,
720 defaultParent: string,
721 base: any,
722 fallbackLabel: string,
723 ): Atom | null => {
724 const ov = overrides[key];
725 if (ov?.hidden) return null;
726 const labelOverride =
727 ov?.label && typeof ov.label === "string" && ov.label.trim() !== ""
728 ? ov.label
729 : null;
730 const atom: Atom = {
731 key,
732 defaultParent,
733 label: labelOverride ?? fallbackLabel,
734 iconOverride: ov?.icon,
735 icon: base.icon,
736 subpage: base.subpage ?? defaultParent ?? key,
737 tab: base.tab,
738 isPremium: base.isPremium,
739 };
740 atoms.push(atom);
741 atomByKey.set(key, atom);
742 return atom;
743 };
744
745 for (const item of menuItems) {
746 apply(item.subpage, "", item, item.label);
747 if (item.submenu) {
748 for (const sub of item.submenu as any[]) {
749 apply(
750 `${item.subpage}.${sub.tab}`,
751 item.subpage,
752 { icon: sub.icon, subpage: item.subpage, tab: sub.tab },
753 sub.label,
754 );
755 }
756 }
757 }
758
759 // Resolve each atom's effective parent (override wins; "" = top-level).
760 const effectiveParent = (atom: Atom): string => {
761 const ov = overrides[atom.key];
762 if (ov && typeof ov.parent === "string") {
763 // Only accept the override if it points at a known top-level atom
764 // (or empty for top-level). Otherwise fall back to the default.
765 if (ov.parent === "") return "";
766 if (atomByKey.get(ov.parent)?.defaultParent === "") return ov.parent;
767 }
768 return atom.defaultParent;
769 };
770
771 // Group atoms by effective parent and order each group.
772 const groups = new Map<string, Atom[]>();
773 for (const atom of atoms) {
774 const parent = effectiveParent(atom);
775 // Self-parenting guard — should never happen but stay defensive.
776 if (parent === atom.key) continue;
777 const list = groups.get(parent) ?? [];
778 list.push(atom);
779 groups.set(parent, list);
780 }
781
782 const orderGroup = (parent: string, group: Atom[]): Atom[] => {
783 const saved = orderMap[parent] ?? [];
784 if (saved.length === 0) return group;
785 const byKey = new Map(group.map((a) => [a.key, a]));
786 const seen = new Set<string>();
787 const ordered: Atom[] = [];
788 for (const k of saved) {
789 const atom = byKey.get(k);
790 if (atom && !seen.has(k)) {
791 ordered.push(atom);
792 seen.add(k);
793 }
794 }
795 for (const atom of group) {
796 if (!seen.has(atom.key)) ordered.push(atom);
797 }
798 return ordered;
799 };
800
801 const topLevel = orderGroup("", groups.get("") ?? []);
802 return topLevel.map((atom) => {
803 const childAtoms = orderGroup(atom.key, groups.get(atom.key) ?? []);
804 return {
805 subpage: atom.subpage,
806 label: atom.label,
807 icon: atom.icon,
808 iconOverride: atom.iconOverride,
809 isPremium: atom.isPremium,
810 submenu: childAtoms.length
811 ? childAtoms.map((child) => ({
812 // Submenu rendering supports both legacy intra-parent items
813 // (matching parent's subpage) and promoted/demoted items
814 // that point at their own subpage.
815 tab: child.defaultParent === atom.subpage ? child.tab : undefined,
816 label: child.label,
817 icon: child.icon,
818 iconOverride: child.iconOverride,
819 // Promoted top-level items now-as-children get an absolute href.
820 href:
821 child.defaultParent === atom.subpage
822 ? undefined
823 : `${baseUrl}&subpage=${child.subpage}${child.tab ? `&tab=${child.tab}` : ""}`,
824 subpage: child.subpage,
825 tab_orig: child.tab,
826 }))
827 : undefined,
828 };
829 });
830 }, [menuItems, baseUrl]);
831
832 /** UI chrome visibility flags (version, Back to WP, Join Community). */
833 const uiChrome = useMemo(() => readUiChrome(), []);
834
835 const isActive = (subpage: string, tab?: string) => {
836 if (tab) {
837 return currentSubpage === subpage && currentTab === tab;
838 }
839 // For parent menu items, check if current subpage matches
840 // or if any submenu item is active
841 if (currentSubpage === subpage) {
842 const menuItem = menuItems.find((item) => item.subpage === subpage);
843 if (menuItem?.submenu) {
844 // If it has submenu, check if any submenu item is active
845 return menuItem.submenu.some((sub) => sub.tab === currentTab);
846 }
847 return true;
848 }
849 return false;
850 };
851
852 const isMenuExpanded = (subpage: string) => {
853 return expandedMenus.includes(subpage);
854 };
855
856 const toggleMenu = (subpage: string) => {
857 setExpandedMenus((prev) =>
858 prev.includes(subpage)
859 ? prev.filter((m) => m !== subpage)
860 : [...prev, subpage],
861 );
862 };
863
864 const getUrl = (subpage: string, tab?: string) => {
865 if (subpage === "dashboard") {
866 return baseUrl;
867 }
868 if (tab) {
869 return `${baseUrl}&subpage=${subpage}&tab=${tab}`;
870 }
871 return `${baseUrl}&subpage=${subpage}`;
872 };
873
874 return (
875 <div
876 className={`min-h-screen ${darkMode ? "dark bg-gray-900" : "bg-gray-50"}`}
877 >
878 <div className="flex h-screen overflow-hidden">
879 {/* Sidebar */}
880 <aside className="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
881 {/* Logo */}
882 <div className="h-16 px-6 flex items-center justify-center border-b border-gray-200 dark:border-gray-700">
883 <div className="flex flex-col gap-1">
884 <div className="flex items-center gap-3">
885 {(() => {
886 const brandName =
887 (window as any).yatraAdmin?.brandName || "Yatra";
888 const brandInitial = brandName.charAt(0).toUpperCase() || "Y";
889 return (window as any).yatraAdmin?.brandLogoUrl ? (
890 <img
891 src={(window as any).yatraAdmin.brandLogoUrl}
892 alt={brandName}
893 width={32}
894 height={32}
895 className="w-8 h-8 rounded-lg object-contain shrink-0 bg-blue-600 p-1 border border-blue-700 dark:border-blue-500"
896 />
897 ) : (
898 <div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center shrink-0">
899 <span className="text-white font-bold text-lg">
900 {brandInitial}
901 </span>
902 </div>
903 );
904 })()}
905 <div className="flex flex-col">
906 <span className="text-xl font-bold text-gray-900 dark:text-white">
907 {(window as any).yatraAdmin?.brandName || "Yatra"}
908 </span>
909 {!uiChrome.hideVersion && (
910 <div className="flex items-center gap-2 text-[10px] text-gray-500 dark:text-gray-400">
911 <span>v{window.yatraAdmin?.version || "1.0.0"}</span>
912 {(window as any).yatraAdmin?.proVersion && (
913 <span className="px-1.5 py-0.5 bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 rounded font-medium">
914 Pro v{(window as any).yatraAdmin?.proVersion}
915 </span>
916 )}
917 </div>
918 )}
919 </div>
920 </div>
921 </div>
922 </div>
923
924 {/* Navigation + Bottom Actions */}
925 <nav className="flex-1 p-4 space-y-1 overflow-y-auto">
926 {brandedMenuItems.map((item) => {
927 const Icon = item.icon;
928 const hasSubmenu = item.submenu && item.submenu.length > 0;
929 const isExpanded = hasSubmenu && isMenuExpanded(item.subpage);
930 const active = isActive(item.subpage);
931
932 return (
933 <div key={item.subpage}>
934 {hasSubmenu ? (
935 <>
936 <button
937 onClick={() => toggleMenu(item.subpage)}
938 className={`w-full flex items-center justify-between gap-3 px-4 py-3 rounded-lg text-sm transition-colors ${
939 active
940 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium"
941 : "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
942 }`}
943 >
944 <div className="flex items-center gap-3">
945 <MenuIcon
946 icon={(item as any).iconOverride}
947 fallback={Icon}
948 className="w-5 h-5"
949 />
950 <span>{item.label}</span>
951 </div>
952 {isExpanded ? (
953 <ChevronDown className="w-4 h-4" />
954 ) : (
955 <ChevronRight className="w-4 h-4" />
956 )}
957 </button>
958 {isExpanded && item.submenu && (
959 <div className="ml-4 mt-1 space-y-1">
960 {item.submenu.map((subItem: any) => {
961 // Promoted / demoted child carries its own
962 // routing target via subpage + tab_orig; the
963 // default in-parent child uses the parent's
964 // subpage with its tab.
965 const navSubpage = subItem.subpage ?? item.subpage;
966 const navTab = subItem.tab ?? subItem.tab_orig;
967 const subActive = navTab
968 ? isActive(navSubpage, navTab)
969 : isActive(navSubpage);
970 const SubIcon = subItem.icon;
971 return (
972 <a
973 key={`${navSubpage}.${navTab ?? ""}`}
974 href={getUrl(navSubpage, navTab)}
975 onClick={(e) =>
976 handleMenuNavClick(e, navSubpage, navTab)
977 }
978 className={`flex items-center gap-3 px-4 py-2 rounded-lg transition-colors relative ${
979 subActive
980 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium"
981 : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
982 }`}
983 >
984 <div className="flex items-center gap-3">
985 {SubIcon && (
986 <div className="w-4 h-4">
987 <MenuIcon
988 icon={(subItem as any).iconOverride}
989 fallback={SubIcon}
990 className="w-4 h-4"
991 />
992 </div>
993 )}
994 <span className="text-sm">
995 {subItem.label}
996 </span>
997 </div>
998 {subItem.isPremium && !isProPluginActive() && (
999 <div className="absolute inset-y-0 right-2 flex items-center justify-center">
1000 <div className="w-4 h-4 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 text-white flex items-center justify-center">
1001 <Crown className="w-2.5 h-2.5" />
1002 </div>
1003 </div>
1004 )}
1005 </a>
1006 );
1007 })}
1008 </div>
1009 )}
1010 </>
1011 ) : (
1012 <a
1013 href={getUrl(item.subpage)}
1014 onClick={(e) => handleMenuNavClick(e, item.subpage)}
1015 className={`flex items-center gap-3 px-4 py-3 rounded-lg text-sm transition-colors relative ${
1016 active
1017 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium"
1018 : "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
1019 }`}
1020 >
1021 <div className="flex items-center gap-3">
1022 <MenuIcon
1023 icon={(item as any).iconOverride}
1024 fallback={Icon}
1025 className="w-5 h-5"
1026 />
1027 <span>{item.label}</span>
1028 </div>
1029 {item.isPremium && !isProPluginActive() && (
1030 <div className="absolute inset-y-0 right-2 flex items-center justify-center">
1031 <div className="w-4 h-4 rounded-full bg-gradient-to-r from-amber-500 to-orange-500 text-white flex items-center justify-center">
1032 <Crown className="w-2.5 h-2.5" />
1033 </div>
1034 </div>
1035 )}
1036 {item.subpage === "license" &&
1037 isProPluginActive() &&
1038 licenseStatus && (
1039 <Badge
1040 variant={
1041 licenseStatus === "active"
1042 ? "success"
1043 : licenseStatus === "expired"
1044 ? "error"
1045 : licenseStatus === "invalid"
1046 ? "error"
1047 : "error"
1048 }
1049 className="text-[10px] px-2 py-0.5"
1050 >
1051 {licenseStatus === "active"
1052 ? "Active"
1053 : licenseStatus === "expired"
1054 ? "Expired"
1055 : licenseStatus === "invalid"
1056 ? "Invalid"
1057 : "Inactive"}
1058 </Badge>
1059 )}
1060 </a>
1061 )}
1062 </div>
1063 );
1064 })}
1065 </nav>
1066
1067 {!isProPluginActive() && (
1068 <div className="shrink-0 px-4 pb-3">
1069 <a
1070 href="https://wpyatra.com/pricing/"
1071 target="_blank"
1072 rel="noopener noreferrer"
1073 className="group flex flex-col gap-0.5 rounded-lg border border-amber-200/90 dark:border-amber-800/40 bg-amber-50/60 dark:bg-amber-950/25 px-3 py-2.5 text-left transition-colors hover:bg-amber-50 dark:hover:bg-amber-950/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-800"
1074 >
1075 <span className="inline-flex items-center gap-1.5 text-xs font-semibold text-amber-900 dark:text-amber-200">
1076 <Crown
1077 className="w-3.5 h-3.5 shrink-0 text-amber-600 dark:text-amber-400"
1078 aria-hidden
1079 />
1080 {__("Upgrade to Pro", "yatra")}
1081 </span>
1082 <span className="text-[11px] leading-snug text-amber-800/75 dark:text-amber-300/70">
1083 {__("Premium features & integrations — view plans.", "yatra")}
1084 </span>
1085 </a>
1086 </div>
1087 )}
1088
1089 {/* Sticky bottom back link */}
1090 {!uiChrome.hideBackToWp && (
1091 <div className="border-t border-gray-200 dark:border-gray-700 p-4">
1092 <a
1093 href={
1094 window.yatraAdmin?.siteUrl
1095 ? `${window.yatraAdmin.siteUrl}/wp-admin/`
1096 : "/wp-admin/"
1097 }
1098 className="flex items-center justify-between gap-2 px-3 py-2 rounded-lg text-xs font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
1099 >
1100 <span className="inline-flex items-center gap-2">
1101 <ArrowLeft className="w-3 h-3" />
1102 <span>{__("Back to WordPress", "yatra")}</span>
1103 </span>
1104 </a>
1105 </div>
1106 )}
1107 </aside>
1108
1109 {/* Main Content */}
1110 <div
1111 className={`flex-1 flex flex-col ${isTripFormPage ? "overflow-hidden" : "overflow-y-auto"}`}
1112 >
1113 {/* Top Bar */}
1114 <header className="h-16 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 flex items-center">
1115 <div className="flex items-center justify-between w-full">
1116 <div className="flex items-center gap-3 flex-wrap min-w-0">
1117 <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
1118 {(() => {
1119 // Show specific text for trip form page
1120 if (isTripFormPage) {
1121 return currentAction === "create"
1122 ? "Create Trip"
1123 : "Edit Trip";
1124 }
1125
1126 const activeItem = brandedMenuItems.find((item) =>
1127 isActive(item.subpage),
1128 );
1129 if (activeItem?.submenu && currentTab) {
1130 const activeSubItem = activeItem.submenu.find(
1131 (sub) => sub.tab === currentTab,
1132 );
1133 return activeSubItem?.label || activeItem.label;
1134 }
1135 return activeItem?.label || "Dashboard";
1136 })()}
1137 </h1>
1138
1139 {/* Conversion pill — visible only on free-plugin installs. */}
1140 {/* Hidden as soon as the Pro plugin is active. Single edit */}
1141 {/* in this top-bar surfaces it on every admin page. */}
1142 {/* Gift icon leads on the "free" promise (operators read */}
1143 {/* the icon before the text). Real Tooltip component */}
1144 {/* instead of the browser `title` attribute so the hint */}
1145 {/* matches the rest of the admin's hover-help style. */}
1146 {!isProPluginActive() && (
1147 <Tooltip
1148 side="bottom"
1149 content={__(
1150 "Spin up a free Pro trial site in one click — no credit card required.",
1151 "yatra",
1152 )}
1153 >
1154 <a
1155 href="https://try.wpyatra.com/try-yatra-pro/"
1156 target="_blank"
1157 rel="noopener noreferrer"
1158 className="inline-flex items-center gap-1.5 rounded-full bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-900 ring-1 ring-amber-200 hover:bg-amber-100 hover:ring-amber-300 dark:bg-amber-950/40 dark:text-amber-100 dark:ring-amber-800 dark:hover:bg-amber-900/50 transition-colors"
1159 >
1160 <Gift className="h-3.5 w-3.5" />
1161 {__("Try Yatra Pro for free", "yatra")}
1162 </a>
1163 </Tooltip>
1164 )}
1165 </div>
1166
1167 <div className="flex items-center gap-4">
1168 {/* Back to WordPress button */}
1169 {!uiChrome.hideBackToWp && (
1170 <a
1171 href={
1172 window.yatraAdmin?.siteUrl
1173 ? `${window.yatraAdmin.siteUrl}/wp-admin/`
1174 : "/wp-admin/"
1175 }
1176 target="_blank"
1177 rel="noopener noreferrer"
1178 className="inline-flex items-center gap-2 px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
1179 >
1180 <ArrowLeft className="w-4 h-4" />
1181 {__("Back to WordPress", "yatra")}
1182 </a>
1183 )}
1184
1185 {!uiChrome.hideJoinCommunity && (
1186 <a
1187 href="https://www.facebook.com/groups/yatrawordpressplugin"
1188 target="_blank"
1189 rel="noopener noreferrer"
1190 className="inline-flex items-center gap-2 px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
1191 >
1192 <MessageSquare className="w-4 h-4 shrink-0" aria-hidden />
1193 {__("Join Community", "yatra")}
1194 </a>
1195 )}
1196
1197 {/* Flush permalink (same API as Settings → Permalink); icon + native title for tooltip */}
1198 <Button
1199 type="button"
1200 variant="outline"
1201 size="icon"
1202 onClick={() => flushRewriteRulesMutation.mutate()}
1203 disabled={flushRewriteRulesMutation.isPending}
1204 className="h-9 w-9 shrink-0 border-gray-300 dark:border-gray-600"
1205 title={
1206 flushRewriteRulesMutation.isPending
1207 ? __("Flushing...", "yatra")
1208 : __("Flush permalink", "yatra")
1209 }
1210 aria-label={
1211 flushRewriteRulesMutation.isPending
1212 ? __("Flushing rewrite rules", "yatra")
1213 : __("Flush permalink", "yatra")
1214 }
1215 >
1216 <RotateCcw
1217 className={`h-4 w-4 ${flushRewriteRulesMutation.isPending ? "animate-spin" : ""}`}
1218 aria-hidden
1219 />
1220 </Button>
1221
1222 <ConditionalRender capability="yatra_edit_trips">
1223 <Button
1224 variant={currentSubpage === "tools" ? "default" : "outline"}
1225 onClick={() => {
1226 navigateMenu("tools");
1227 }}
1228 className={`flex items-center gap-2 ${
1229 currentSubpage === "tools"
1230 ? "bg-blue-600 hover:bg-blue-700 text-white border-blue-600"
1231 : ""
1232 }`}
1233 >
1234 <Wrench className="w-4 h-4" />
1235 {__("Tools", "yatra")}
1236 </Button>
1237 </ConditionalRender>
1238 <ConditionalRender capability="yatra_edit_trips">
1239 <div className="relative">
1240 <button
1241 onClick={() => setIsModulesPanelOpen((prev) => !prev)}
1242 className={`p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400 ${isModulesPanelOpen ? "bg-gray-100 dark:bg-gray-700" : ""}`}
1243 aria-label={__("Toggle modules panel", "yatra")}
1244 >
1245 <Puzzle className="w-5 h-5" />
1246 </button>
1247 {isModulesPanelOpen && (
1248 <div
1249 ref={modulesPanelRef}
1250 className="absolute right-0 top-12 z-50 w-80"
1251 >
1252 <Card className="shadow-xl border border-gray-200 dark:border-gray-700">
1253 <CardHeader className="pb-3">
1254 <div className="flex items-center justify-between">
1255 <div>
1256 <CardTitle className="text-base">
1257 {__("Modules", "yatra")}
1258 </CardTitle>
1259 <CardDescription>
1260 {__(
1261 "Quickly enable or disable feature packs.",
1262 "yatra",
1263 )}
1264 </CardDescription>
1265 </div>
1266 <Button
1267 size="sm"
1268 variant="ghost"
1269 onClick={() => {
1270 setIsModulesPanelOpen(false);
1271 navigateMenu("modules");
1272 }}
1273 >
1274 {__("Open", "yatra")}
1275 </Button>
1276 </div>
1277 </CardHeader>
1278 <CardContent className="space-y-3">
1279 {isLoadingModules && (
1280 <div className="flex items-center gap-2 text-gray-500 dark:text-gray-400 text-sm">
1281 <Loader2 className="w-4 h-4 animate-spin" />
1282 {__("Loading modules…", "yatra")}
1283 </div>
1284 )}
1285 {!isLoadingModules &&
1286 modulesPreview.length === 0 && (
1287 <div className="text-sm text-gray-500 dark:text-gray-400">
1288 {__("No modules found.", "yatra")}
1289 </div>
1290 )}
1291 {!isLoadingModules && modulesPreview.length > 0 && (
1292 <div className="space-y-3">
1293 {modulesPreview.map((module) => (
1294 <div
1295 key={module.slug}
1296 className="flex items-center justify-between border border-gray-100 dark:border-gray-800 rounded-lg p-2"
1297 >
1298 <div>
1299 <p className="text-sm font-medium text-gray-900 dark:text-white flex items-center gap-2">
1300 {module.name}
1301 {module.is_core && (
1302 <Badge
1303 variant="outline"
1304 className="text-[10px]"
1305 >
1306 {__("Core", "yatra")}
1307 </Badge>
1308 )}
1309 </p>
1310 <p className="text-xs text-gray-500 dark:text-gray-400">
1311 {module.enabled
1312 ? __("Enabled", "yatra")
1313 : __("Disabled", "yatra")}
1314 </p>
1315 </div>
1316 <button
1317 onClick={() =>
1318 handleQuickToggle(
1319 module,
1320 !module.enabled,
1321 )
1322 }
1323 disabled={
1324 module.is_core ||
1325 toggleModuleMutation.isPending
1326 }
1327 className={`relative inline-flex h-6 w-11 items-center rounded-full transition ${
1328 module.enabled
1329 ? "bg-blue-600"
1330 : "bg-gray-300 dark:bg-gray-600"
1331 } ${module.is_core ? "opacity-60 cursor-not-allowed" : "cursor-pointer"}`}
1332 aria-pressed={module.enabled}
1333 aria-label={
1334 module.enabled
1335 ? __("Disable module", "yatra")
1336 : __("Enable module", "yatra")
1337 }
1338 >
1339 <span
1340 className={`inline-block h-5 w-5 transform rounded-full bg-white transition ${
1341 module.enabled
1342 ? "translate-x-5"
1343 : "translate-x-1"
1344 }`}
1345 />
1346 </button>
1347 </div>
1348 ))}
1349 </div>
1350 )}
1351 <div className="pt-2">
1352 <Button
1353 variant="outline"
1354 size="sm"
1355 className="w-full"
1356 onClick={() => {
1357 setIsModulesPanelOpen(false);
1358 navigateMenu("modules");
1359 }}
1360 >
1361 {__("Manage all modules", "yatra")}
1362 </Button>
1363 </div>
1364 </CardContent>
1365 </Card>
1366 </div>
1367 )}
1368 </div>
1369 </ConditionalRender>
1370 <button
1371 onClick={() => {
1372 setDarkMode(!darkMode);
1373 }}
1374 className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400"
1375 aria-label={
1376 darkMode ? "Switch to light mode" : "Switch to dark mode"
1377 }
1378 >
1379 {darkMode ? (
1380 <Sun className="w-5 h-5" />
1381 ) : (
1382 <Moon className="w-5 h-5" />
1383 )}
1384 </button>
1385
1386 <div className="relative">
1387 <button
1388 onClick={() => setIsUserDropdownOpen((prev) => !prev)}
1389 className="flex items-center gap-3 pl-4 border-l border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700 p-2 transition-colors"
1390 aria-label={__("User menu", "yatra")}
1391 >
1392 <div className="w-8 h-8 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700">
1393 <img
1394 src={getGravatarUrl(32)}
1395 alt={__("User avatar", "yatra")}
1396 className="w-full h-full object-cover"
1397 onError={(e) => {
1398 e.currentTarget.style.display = "none";
1399 e.currentTarget.parentElement!.innerHTML =
1400 '<svg class="w-5 h-5 text-gray-600 dark:text-gray-400 m-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>';
1401 }}
1402 />
1403 </div>
1404 </button>
1405
1406 {isUserDropdownOpen && (
1407 <div
1408 ref={userDropdownRef}
1409 className="absolute right-0 top-12 z-50 w-64"
1410 >
1411 <Card className="shadow-xl border border-gray-200 dark:border-gray-700">
1412 <CardContent className="p-0">
1413 <div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
1414 <div className="flex items-center gap-3">
1415 <div className="w-10 h-10 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700">
1416 <img
1417 src={getGravatarUrl(40)}
1418 alt={__("User avatar", "yatra")}
1419 className="w-full h-full object-cover"
1420 onError={(e) => {
1421 e.currentTarget.style.display = "none";
1422 e.currentTarget.parentElement!.innerHTML =
1423 '<svg class="w-6 h-6 text-gray-600 dark:text-gray-400 m-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>';
1424 }}
1425 />
1426 </div>
1427 <div className="flex-1 min-w-0">
1428 <div className="text-sm font-medium text-gray-900 dark:text-white truncate">
1429 {(window as any)?.yatraAdmin
1430 ?.currentUserDisplayName ||
1431 (window as any)?.yatraAdmin
1432 ?.currentUserLogin ||
1433 "Admin"}
1434 </div>
1435 <div className="text-xs text-gray-500 dark:text-gray-400 truncate">
1436 {(window as any)?.yatraAdmin
1437 ?.currentUserEmail || ""}
1438 </div>
1439 </div>
1440 </div>
1441 </div>
1442
1443 <a
1444 href={`${(window as any)?.yatraAdmin?.siteUrl || ""}/wp-admin/profile.php`}
1445 target="_blank"
1446 rel="noopener noreferrer"
1447 className="w-full text-left px-4 py-3 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors"
1448 >
1449 <User className="w-4 h-4" />
1450 {__("Edit Profile", "yatra")}
1451 </a>
1452
1453 <a
1454 href={`${(window as any)?.yatraAdmin?.siteUrl || ""}/wp-admin/`}
1455 target="_blank"
1456 rel="noopener noreferrer"
1457 className="w-full text-left px-4 py-3 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors"
1458 >
1459 <svg
1460 className="w-4 h-4"
1461 fill="none"
1462 stroke="currentColor"
1463 viewBox="0 0 24 24"
1464 >
1465 <path
1466 strokeLinecap="round"
1467 strokeLinejoin="round"
1468 strokeWidth={2}
1469 d="M10 19l-7-7m0 0l7-7m-7 7h18"
1470 />
1471 </svg>
1472 {__("Back to WordPress", "yatra")}
1473 </a>
1474
1475 <button
1476 onClick={() => {
1477 const admin = (window as any)?.yatraAdmin;
1478 const siteUrl = admin?.siteUrl || "";
1479 window.location.href = `${siteUrl}/wp-login.php?action=logout`;
1480 }}
1481 className="w-full text-left px-4 py-3 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/20 flex items-center gap-3 transition-colors"
1482 >
1483 <svg
1484 className="w-4 h-4"
1485 fill="none"
1486 stroke="currentColor"
1487 viewBox="0 0 24 24"
1488 >
1489 <path
1490 strokeLinecap="round"
1491 strokeLinejoin="round"
1492 strokeWidth={2}
1493 d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
1494 />
1495 </svg>
1496 {__("Logout", "yatra")}
1497 </button>
1498 </CardContent>
1499 </Card>
1500 </div>
1501 )}
1502 </div>
1503 </div>
1504 </div>
1505 </header>
1506
1507 {/* Admin notices (Review / Buy Pro, etc.) */}
1508 <InlineNotices />
1509
1510 {/* License Warning Banner */}
1511 {isProPluginActive() &&
1512 licenseStatus &&
1513 licenseStatus !== "active" && (
1514 <div className="bg-red-50 dark:bg-red-950/30 border-l-4 border-b-2 border-red-500">
1515 <div className="px-6 py-3">
1516 <div className="flex items-center gap-4">
1517 <div className="flex-shrink-0">
1518 <div className="w-10 h-10 rounded-lg bg-red-500 flex items-center justify-center">
1519 <svg
1520 className="w-6 h-6 text-white"
1521 fill="currentColor"
1522 viewBox="0 0 20 20"
1523 >
1524 <path
1525 fillRule="evenodd"
1526 d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
1527 clipRule="evenodd"
1528 />
1529 </svg>
1530 </div>
1531 </div>
1532 <div className="flex-1 min-w-0">
1533 <p className="text-sm font-medium text-red-800 dark:text-red-200">
1534 <span className="font-semibold">
1535 {licenseStatus === "expired"
1536 ? "License Expired: "
1537 : licenseStatus === "invalid"
1538 ? "Invalid License: "
1539 : "License Not Activated: "}
1540 </span>
1541 {licenseStatus === "expired"
1542 ? "Renew your license to continue receiving updates and support."
1543 : licenseStatus === "invalid"
1544 ? "Please check your license key."
1545 : "Activate your license to receive updates and support."}
1546 </p>
1547 </div>
1548 <a
1549 href={getUrl("license")}
1550 onClick={(e) => handleMenuNavClick(e, "license")}
1551 className="flex-shrink-0 px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-md transition-colors"
1552 >
1553 {licenseStatus === "expired"
1554 ? "Renew Now"
1555 : "Activate Now"}
1556 </a>
1557 </div>
1558 </div>
1559 </div>
1560 )}
1561
1562 {/* Page Content */}
1563 <main
1564 className={`flex-1 ${isTripFormPage ? "p-0 overflow-hidden flex flex-col" : "p-6 overflow-y-auto"}`}
1565 >
1566 <div
1567 className={
1568 isTripFormPage
1569 ? "flex-1 min-h-0 overflow-hidden flex flex-col"
1570 : currentSubpage === "tools"
1571 ? ""
1572 : "space-y-6"
1573 }
1574 >
1575 {children}
1576 </div>
1577 </main>
1578 </div>
1579 </div>
1580 </div>
1581 );
1582 };
1583
1584 export default Layout;
1585