import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Crown, ExternalLink, Save, Loader2, Lock, Image as ImageIcon, X, Eye, EyeOff, Palette, Sparkles, Layout as LayoutIcon, PanelLeft, GripVertical, Wrench, Building2, ChevronDown, ChevronRight, } from "lucide-react"; import { apiClient } from "../lib/api-client"; import { __ } from "../lib/i18n"; import { useToast } from "../components/ui/toast"; import { useWordPressMedia } from "../hooks/useWordPressMedia"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { ModulePageSkeleton } from "../components/ui/module-skeleton"; import { Label } from "../components/ui/label"; import { IconPicker } from "../components/ui/icon-picker"; import type { IconPickerValue } from "../lib/icon-picker-types"; import { PageHeader } from "../components/common/PageHeader"; import { MenuIcon } from "../lib/menu-icon"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../components/ui/card"; import { DEFAULT_MENU_ITEMS, type MenuIconValue, type MenuItemOverride, type MenuOrderMap, type MenuOverrides, type UiChromeFlags, } from "../lib/sidebar-menu-defaults"; import { ICON_MAP } from "../lib/icon-map"; /** * Coerce the stored override icon into an IconPickerValue for the picker. * Lucide-string overrides display as the IconPicker's empty state — users * see the default Lucide icon in the row's preview but the picker itself * starts blank, which is the expected behavior since IconPicker doesn't * know about Lucide. */ function toPickerValue( icon: MenuIconValue | undefined, ): IconPickerValue | null { if (!icon) return null; if (typeof icon === "string") return null; return icon; } interface ThemeSurfaces { sidebar_bg?: string; sidebar_text?: string; topbar_bg?: string; topbar_text?: string; } interface WhiteLabelSettings { plugin_name: string; company_name: string; website_url: string; support_url: string; logo_url: string; primary_color: string; menu_overrides: MenuOverrides; /** * Per-parent ordered list. The "" key is the top-level order; each * top-level slug key holds that parent's ordered children. Backwards * compatible with the older flat array form (handled on read). */ menu_order: MenuOrderMap | string[]; ui_chrome: UiChromeFlags; /** * Soft "theme" overrides — sidebar + topbar background and text * colors. Empty / missing keys fall back to Yatra's default * light/dark palette. */ theme_surfaces: ThemeSurfaces; } /** Quick-pick theme palettes for the Theme card. */ const THEME_PRESETS: Array<{ label: string; surfaces: Required; }> = [ { label: "Light", surfaces: { sidebar_bg: "#ffffff", sidebar_text: "#374151", topbar_bg: "#ffffff", topbar_text: "#111827", }, }, { label: "Dark", surfaces: { sidebar_bg: "#111827", sidebar_text: "#e5e7eb", topbar_bg: "#111827", topbar_text: "#f9fafb", }, }, { label: "Midnight", surfaces: { sidebar_bg: "#0f172a", sidebar_text: "#94a3b8", topbar_bg: "#1e293b", topbar_text: "#e2e8f0", }, }, { label: "Ocean", surfaces: { sidebar_bg: "#0c4a6e", sidebar_text: "#e0f2fe", topbar_bg: "#075985", topbar_text: "#f0f9ff", }, }, { label: "Sand", surfaces: { sidebar_bg: "#fef3c7", sidebar_text: "#78350f", topbar_bg: "#fef9c3", topbar_text: "#854d0e", }, }, { label: "Forest", surfaces: { sidebar_bg: "#064e3b", sidebar_text: "#d1fae5", topbar_bg: "#065f46", topbar_text: "#ecfdf5", }, }, ]; interface WhiteLabelMeta { is_pro_active: boolean; is_agency_active: boolean; is_module_enabled: boolean; agency: { is_agency?: boolean; tier?: string; price_id?: number | null; agency_price_ids?: number[]; }; upgrade_url: string; license_page_url: string; } interface WhiteLabelResponse { data: WhiteLabelSettings; meta: WhiteLabelMeta; } type SectionId = "brand" | "theme" | "menu" | "chrome"; interface SectionDef { id: SectionId; label: string; icon: React.ComponentType<{ className?: string }>; description: string; } const SECTIONS: SectionDef[] = [ { id: "brand", label: "Brand Identity", icon: Building2, description: "Plugin name, company, URLs, and logo shown across the admin and PDFs.", }, { id: "theme", label: "Theme", icon: Palette, description: "Brand accent color, plus surface theme — sidebar and top bar background + text colors. Pick a preset or fine-tune each color.", }, { id: "menu", label: "Sidebar Menu", icon: PanelLeft, description: "Reorder, rename, re-icon, or hide entries in the Yatra sidebar.", }, { id: "chrome", label: "UI Chrome", icon: LayoutIcon, description: "Show or hide the version number, Back to WordPress link, and Join Community button.", }, ]; const DEFAULT_SETTINGS: WhiteLabelSettings = { plugin_name: "", company_name: "", website_url: "", support_url: "", logo_url: "", primary_color: "", menu_overrides: {}, menu_order: {}, ui_chrome: {}, theme_surfaces: {}, }; /** Normalize stored order (flat array OR map) to a map keyed by parent. */ function toOrderMap(raw: MenuOrderMap | string[] | undefined): MenuOrderMap { if (!raw) return {}; if (Array.isArray(raw)) return { "": [...raw] }; return Object.fromEntries( Object.entries(raw).map(([k, v]) => [k, Array.isArray(v) ? [...v] : []]), ); } /* -------------------------------------------------------------------------- */ /* Plan tier badge */ /* -------------------------------------------------------------------------- */ /** * Pill rendered in the White Label page header so operators see at a * glance which license tier unlocks this page. Mirrors the badge style * used on the Modules page card for visual consistency. */ const PlanBadge: React.FC = () => ( {__("Scale plan", "yatra")} ); /* -------------------------------------------------------------------------- */ /* Upgrade card (non-Agency) */ /* -------------------------------------------------------------------------- */ const UpgradeCard: React.FC<{ meta?: WhiteLabelMeta }> = ({ meta }) => { const upgradeUrl = meta?.upgrade_url || "https://wpyatra.com/pricing?module=white-label"; const licensePageUrl = meta?.license_page_url || "admin.php?page=yatra&subpage=license"; return (
{__("White Label", "yatra")}
{__( "White Label is exclusive to the Scale plan (Yearly or Lifetime). Upgrade to remove all Yatra / MantraBrain references from your clients' admin.", "yatra", )}

{__( "Rebrand the entire Yatra plugin as your own product — admin menu, plugin list, and PDFs — and remove every reference to Yatra or MantraBrain from what your clients see.", "yatra", )}

{meta?.is_pro_active && ( )}
); }; /* -------------------------------------------------------------------------- */ /* Menu customization row (top-level + submenu) */ /* -------------------------------------------------------------------------- */ interface MenuRowProps { slug: string; defaultLabel: string; defaultIconName: string; override: MenuItemOverride; isSubmenu?: boolean; draggable?: boolean; isDragging?: boolean; /** Where this row would receive the drop relative to itself. */ dropPosition?: "above" | "into" | "below" | null; onDragStart?: (e: React.DragEvent) => void; onDragOver?: (e: React.DragEvent) => void; onDragEnd?: () => void; onDragLeave?: () => void; onDrop?: (e: React.DragEvent) => void; onPatch: (patch: Partial) => void; } const MenuRow: React.FC = ({ slug, defaultLabel, defaultIconName, override, isSubmenu = false, draggable = false, isDragging = false, dropPosition = null, onDragStart, onDragOver, onDragEnd, onDragLeave, onDrop, onPatch, }) => { const FallbackIcon = ICON_MAP[defaultIconName] || ICON_MAP.Settings; const hidden = Boolean(override.hidden); return (
{/* Drop indicator: thin bar above */} {dropPosition === "above" && (