Implemented mobile-optimized navigation structure: 1. Bottom Navigation (Mobile Only) - 5 items: Dashboard, Orders, Products, Customers, More - Fixed at bottom, always visible - Thumb-friendly positioning - Active state indication - Hidden on desktop (md:hidden) 2. More Menu Page - Overflow menu for Coupons and Settings - Clean list layout with icons - Descriptions for each item - Chevron indicators 3. FAB (Floating Action Button) - Context-aware system via FABContext - Fixed bottom-right (72px from bottom) - Hidden on desktop (md:hidden) - Ready for contextual actions per page 4. FAB Context System - Global state for FAB configuration - setFAB() / clearFAB() methods - Supports icon, label, onClick, visibility - Allows pages to control FAB behavior 5. Layout Updates - Added pb-14 to main for bottom nav spacing - BottomNav and FAB in mobile fullscreen layout - Wrapped app with FABProvider Structure (Mobile): ┌─────────────────────────────────┐ │ App Bar (will hide on scroll) │ ├─────────────────────────────────┤ │ Page Header (sticky, contextual)│ ├─────────────────────────────────┤ │ Submenu (sticky) │ ├─────────────────────────────────┤ │ Content (scrollable) │ │ [+] FAB │ ├─────────────────────────────────┤ │ Bottom Nav (fixed) │ └─────────────────────────────────┘ Next Steps: - Implement scroll-hide for app bar - Add contextual FAB per page - Test on real devices Files Created: - BottomNav.tsx: Bottom navigation component - More/index.tsx: More menu page - FABContext.tsx: FAB state management - FAB.tsx: Floating action button component - useScrollDirection.ts: Scroll detection hook Files Modified: - App.tsx: Added bottom nav, FAB, More route, providers
554 lines
22 KiB
TypeScript
554 lines
22 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { HashRouter, Routes, Route, NavLink, useLocation, useParams, Navigate, Link } from 'react-router-dom';
|
|
import { Login } from './routes/Login';
|
|
import Dashboard from '@/routes/Dashboard';
|
|
import DashboardRevenue from '@/routes/Dashboard/Revenue';
|
|
import DashboardOrders from '@/routes/Dashboard/Orders';
|
|
import DashboardProducts from '@/routes/Dashboard/Products';
|
|
import DashboardCustomers from '@/routes/Dashboard/Customers';
|
|
import DashboardCoupons from '@/routes/Dashboard/Coupons';
|
|
import DashboardTaxes from '@/routes/Dashboard/Taxes';
|
|
import OrdersIndex from '@/routes/Orders';
|
|
import OrderNew from '@/routes/Orders/New';
|
|
import OrderEdit from '@/routes/Orders/Edit';
|
|
import OrderDetail from '@/routes/Orders/Detail';
|
|
import ProductsIndex from '@/routes/Products';
|
|
import ProductNew from '@/routes/Products/New';
|
|
import ProductCategories from '@/routes/Products/Categories';
|
|
import ProductTags from '@/routes/Products/Tags';
|
|
import ProductAttributes from '@/routes/Products/Attributes';
|
|
import CouponsIndex from '@/routes/Coupons';
|
|
import CouponNew from '@/routes/Coupons/New';
|
|
import CustomersIndex from '@/routes/Customers';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { LayoutDashboard, ReceiptText, Package, Tag, Users, Settings as SettingsIcon, Maximize2, Minimize2, Loader2 } from 'lucide-react';
|
|
import { Toaster } from 'sonner';
|
|
import { useShortcuts } from "@/hooks/useShortcuts";
|
|
import { CommandPalette } from "@/components/CommandPalette";
|
|
import { useCommandStore } from "@/lib/useCommandStore";
|
|
import SubmenuBar from './components/nav/SubmenuBar';
|
|
import DashboardSubmenuBar from './components/nav/DashboardSubmenuBar';
|
|
import { DashboardProvider } from '@/contexts/DashboardContext';
|
|
import { PageHeaderProvider } from '@/contexts/PageHeaderContext';
|
|
import { FABProvider } from '@/contexts/FABContext';
|
|
import { PageHeader } from '@/components/PageHeader';
|
|
import { BottomNav } from '@/components/nav/BottomNav';
|
|
import { FAB } from '@/components/FAB';
|
|
import { useActiveSection } from '@/hooks/useActiveSection';
|
|
import { NAV_TREE_VERSION } from '@/nav/tree';
|
|
import { __ } from '@/lib/i18n';
|
|
|
|
function useFullscreen() {
|
|
const [on, setOn] = useState<boolean>(() => {
|
|
try { return localStorage.getItem('wnwFullscreen') === '1'; } catch { return false; }
|
|
});
|
|
|
|
useEffect(() => {
|
|
const id = 'wnw-fullscreen-style';
|
|
let style = document.getElementById(id);
|
|
if (!style) {
|
|
style = document.createElement('style');
|
|
style.id = id;
|
|
style.textContent = `
|
|
/* Hide WP admin chrome when fullscreen */
|
|
.wnw-fullscreen #wpadminbar,
|
|
.wnw-fullscreen #adminmenumain,
|
|
.wnw-fullscreen #screen-meta,
|
|
.wnw-fullscreen #screen-meta-links,
|
|
.wnw-fullscreen #wpfooter { display:none !important; }
|
|
.wnw-fullscreen #wpcontent { margin-left:0 !important; }
|
|
.wnw-fullscreen #wpbody-content { padding-bottom:0 !important; }
|
|
.wnw-fullscreen html, .wnw-fullscreen body { height: 100%; overflow: hidden; }
|
|
.wnw-fullscreen .woonoow-fullscreen-root {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 999;
|
|
background: var(--background, #fff);
|
|
height: 100dvh; /* ensure full viewport height on mobile/desktop */
|
|
overflow: hidden; /* prevent double scrollbars; inner <main> handles scrolling */
|
|
overscroll-behavior: contain;
|
|
display: flex;
|
|
flex-direction: column;
|
|
contain: layout paint size; /* prevent WP wrappers from affecting layout */
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
document.body.classList.toggle('wnw-fullscreen', on);
|
|
try { localStorage.setItem('wnwFullscreen', on ? '1' : '0'); } catch { /* ignore localStorage errors */ }
|
|
return () => { /* do not remove style to avoid flicker between reloads */ };
|
|
}, [on]);
|
|
|
|
return { on, setOn } as const;
|
|
}
|
|
|
|
function ActiveNavLink({ to, startsWith, children, className, end }: any) {
|
|
// Use the router location hook instead of reading from NavLink's className args
|
|
const location = useLocation();
|
|
const starts = typeof startsWith === 'string' && startsWith.length > 0 ? startsWith : undefined;
|
|
return (
|
|
<NavLink
|
|
to={to}
|
|
end={end}
|
|
className={(nav) => {
|
|
// Special case: Dashboard should also match root path "/"
|
|
const isDashboard = starts === '/dashboard' && location.pathname === '/';
|
|
const activeByPath = starts ? (location.pathname.startsWith(starts) || isDashboard) : false;
|
|
const mergedActive = nav.isActive || activeByPath;
|
|
if (typeof className === 'function') {
|
|
// Preserve caller pattern: className receives { isActive }
|
|
return className({ isActive: mergedActive });
|
|
}
|
|
return `${className ?? ''} ${mergedActive ? '' : ''}`.trim();
|
|
}}
|
|
>
|
|
{children}
|
|
</NavLink>
|
|
);
|
|
}
|
|
|
|
function Sidebar() {
|
|
const link = "flex items-center gap-2 rounded-md px-3 py-2 hover:bg-accent hover:text-accent-foreground shadow-none hover:shadow-none focus:shadow-none focus:outline-none focus:ring-0";
|
|
const active = "bg-secondary";
|
|
return (
|
|
<aside className="w-56 flex-shrink-0 p-3 border-r border-border sticky top-16 h-[calc(100vh-64px)] overflow-y-auto bg-background">
|
|
<nav className="flex flex-col gap-1">
|
|
<ActiveNavLink to="/dashboard" startsWith="/dashboard" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<LayoutDashboard className="w-4 h-4" />
|
|
<span>{__("Dashboard")}</span>
|
|
</ActiveNavLink>
|
|
<ActiveNavLink to="/orders" startsWith="/orders" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<ReceiptText className="w-4 h-4" />
|
|
<span>{__("Orders")}</span>
|
|
</ActiveNavLink>
|
|
<ActiveNavLink to="/products" startsWith="/products" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<Package className="w-4 h-4" />
|
|
<span>{__("Products")}</span>
|
|
</ActiveNavLink>
|
|
<ActiveNavLink to="/coupons" startsWith="/coupons" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<Tag className="w-4 h-4" />
|
|
<span>{__("Coupons")}</span>
|
|
</ActiveNavLink>
|
|
<ActiveNavLink to="/customers" startsWith="/customers" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<Users className="w-4 h-4" />
|
|
<span>{__("Customers")}</span>
|
|
</ActiveNavLink>
|
|
<ActiveNavLink to="/settings" startsWith="/settings" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<SettingsIcon className="w-4 h-4" />
|
|
<span>{__("Settings")}</span>
|
|
</ActiveNavLink>
|
|
</nav>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
function TopNav({ fullscreen = false }: { fullscreen?: boolean }) {
|
|
const link = "inline-flex items-center gap-2 rounded-md px-3 py-2 hover:bg-accent hover:text-accent-foreground shadow-none hover:shadow-none focus:shadow-none focus:outline-none focus:ring-0";
|
|
const active = "bg-secondary";
|
|
const topClass = fullscreen ? 'top-16' : 'top-[calc(4rem+32px)]';
|
|
return (
|
|
<div className={`border-b border-border sticky ${topClass} z-30 bg-background md:bg-background/95 md:backdrop-blur md:supports-[backdrop-filter]:bg-background/60`}>
|
|
<div className="px-4 h-12 flex flex-nowrap overflow-auto items-center gap-2">
|
|
<ActiveNavLink to="/dashboard" startsWith="/dashboard" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<LayoutDashboard className="w-4 h-4" />
|
|
<span>{__("Dashboard")}</span>
|
|
</ActiveNavLink>
|
|
<ActiveNavLink to="/orders" startsWith="/orders" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<ReceiptText className="w-4 h-4" />
|
|
<span>{__("Orders")}</span>
|
|
</ActiveNavLink>
|
|
<ActiveNavLink to="/products" startsWith="/products" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<Package className="w-4 h-4" />
|
|
<span>{__("Products")}</span>
|
|
</ActiveNavLink>
|
|
<ActiveNavLink to="/coupons" startsWith="/coupons" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<Tag className="w-4 h-4" />
|
|
<span>{__("Coupons")}</span>
|
|
</ActiveNavLink>
|
|
<ActiveNavLink to="/customers" startsWith="/customers" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<Users className="w-4 h-4" />
|
|
<span>{__("Customers")}</span>
|
|
</ActiveNavLink>
|
|
<ActiveNavLink to="/settings" startsWith="/settings" className={({ isActive }: any) => `${link} ${isActive ? active : ''}`}>
|
|
<SettingsIcon className="w-4 h-4" />
|
|
<span>{__("Settings")}</span>
|
|
</ActiveNavLink>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
function useIsDesktop(minWidth = 1024) { // lg breakpoint
|
|
const [isDesktop, setIsDesktop] = useState<boolean>(() => {
|
|
if (typeof window === 'undefined') return false;
|
|
return window.matchMedia(`(min-width: ${minWidth}px)`).matches;
|
|
});
|
|
useEffect(() => {
|
|
const mq = window.matchMedia(`(min-width: ${minWidth}px)`);
|
|
const onChange = () => setIsDesktop(mq.matches);
|
|
try { mq.addEventListener('change', onChange); } catch { mq.addListener(onChange); }
|
|
return () => { try { mq.removeEventListener('change', onChange); } catch { mq.removeListener(onChange); } };
|
|
}, [minWidth]);
|
|
return isDesktop;
|
|
}
|
|
|
|
import SettingsIndex from '@/routes/Settings';
|
|
import SettingsStore from '@/routes/Settings/Store';
|
|
import SettingsPayments from '@/routes/Settings/Payments';
|
|
import SettingsShipping from '@/routes/Settings/Shipping';
|
|
import MorePage from '@/routes/More';
|
|
|
|
// Addon Route Component - Dynamically loads addon components
|
|
function AddonRoute({ config }: { config: any }) {
|
|
const [Component, setComponent] = React.useState<any>(null);
|
|
const [loading, setLoading] = React.useState(true);
|
|
const [error, setError] = React.useState<string | null>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (!config.component_url) {
|
|
setError('No component URL provided');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
// Dynamically import the addon component
|
|
import(/* @vite-ignore */ config.component_url)
|
|
.then((mod) => {
|
|
setComponent(() => mod.default || mod);
|
|
setLoading(false);
|
|
})
|
|
.catch((err) => {
|
|
console.error('[AddonRoute] Failed to load component:', err);
|
|
setError(err.message || 'Failed to load addon component');
|
|
setLoading(false);
|
|
});
|
|
}, [config.component_url]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center p-8">
|
|
<div className="text-center">
|
|
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-2 opacity-50" />
|
|
<p className="text-sm opacity-70">{__('Loading addon...')}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
|
|
<h3 className="font-semibold text-red-900 mb-2">{__('Failed to Load Addon')}</h3>
|
|
<p className="text-sm text-red-700">{error}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!Component) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4">
|
|
<p className="text-sm text-yellow-700">{__('Addon component not found')}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Render the addon component with props
|
|
return <Component {...(config.props || {})} />;
|
|
}
|
|
|
|
function Header({ onFullscreen, fullscreen, showToggle = true }: { onFullscreen: () => void; fullscreen: boolean; showToggle?: boolean }) {
|
|
const [siteTitle, setSiteTitle] = React.useState((window as any).wnw?.siteTitle || 'WooNooW');
|
|
const isStandalone = window.WNW_CONFIG?.standaloneMode ?? false;
|
|
|
|
// Listen for store settings updates
|
|
React.useEffect(() => {
|
|
const handleStoreUpdate = (event: CustomEvent) => {
|
|
if (event.detail?.store_name) {
|
|
setSiteTitle(event.detail.store_name);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('woonoow:store:updated' as any, handleStoreUpdate);
|
|
return () => window.removeEventListener('woonoow:store:updated' as any, handleStoreUpdate);
|
|
}, []);
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
await fetch((window.WNW_CONFIG?.restUrl || '') + '/auth/logout', {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
window.location.reload();
|
|
} catch (err) {
|
|
console.error('Logout failed:', err);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<header className={`h-16 border-b border-border flex items-center px-4 justify-between sticky ${fullscreen ? `top-0` : `top-[32px]`} z-40 bg-background md:bg-background/95 md:backdrop-blur md:supports-[backdrop-filter]:bg-background/60`}>
|
|
<div className="font-semibold">{siteTitle}</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="text-sm opacity-70 hidden sm:block">{window.WNW_API?.isDev ? 'Dev Server' : 'Production'}</div>
|
|
{isStandalone && (
|
|
<>
|
|
<a
|
|
href={window.WNW_CONFIG?.wpAdminUrl || '/wp-admin'}
|
|
className="inline-flex items-center gap-2 border rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
|
|
title="Go to WordPress Admin"
|
|
>
|
|
<span>{__('WordPress')}</span>
|
|
</a>
|
|
<button
|
|
onClick={handleLogout}
|
|
className="inline-flex items-center gap-2 border rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
|
|
title="Logout"
|
|
>
|
|
<span>{__('Logout')}</span>
|
|
</button>
|
|
</>
|
|
)}
|
|
{showToggle && (
|
|
<button
|
|
onClick={onFullscreen}
|
|
className="inline-flex items-center gap-2 border rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
|
|
title={fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
>
|
|
{fullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
|
<span className="hidden sm:inline">{fullscreen ? 'Exit' : 'Fullscreen'}</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|
|
|
|
const qc = new QueryClient();
|
|
|
|
function ShortcutsBinder({ onToggle }: { onToggle: () => void }) {
|
|
useShortcuts({ toggleFullscreen: onToggle });
|
|
return null;
|
|
}
|
|
|
|
// Centralized route controller so we don't duplicate <Routes> in each layout
|
|
function AppRoutes() {
|
|
const addonRoutes = (window as any).WNW_ADDON_ROUTES || [];
|
|
|
|
return (
|
|
<Routes>
|
|
{/* Dashboard */}
|
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
<Route path="/dashboard" element={<Dashboard />} />
|
|
<Route path="/dashboard/revenue" element={<DashboardRevenue />} />
|
|
<Route path="/dashboard/orders" element={<DashboardOrders />} />
|
|
<Route path="/dashboard/products" element={<DashboardProducts />} />
|
|
<Route path="/dashboard/customers" element={<DashboardCustomers />} />
|
|
<Route path="/dashboard/coupons" element={<DashboardCoupons />} />
|
|
<Route path="/dashboard/taxes" element={<DashboardTaxes />} />
|
|
|
|
{/* Products */}
|
|
<Route path="/products" element={<ProductsIndex />} />
|
|
<Route path="/products/new" element={<ProductNew />} />
|
|
<Route path="/products/:id/edit" element={<ProductNew />} />
|
|
<Route path="/products/:id" element={<ProductNew />} />
|
|
<Route path="/products/categories" element={<ProductCategories />} />
|
|
<Route path="/products/tags" element={<ProductTags />} />
|
|
<Route path="/products/attributes" element={<ProductAttributes />} />
|
|
|
|
{/* Orders */}
|
|
<Route path="/orders" element={<OrdersIndex />} />
|
|
<Route path="/orders/new" element={<OrderNew />} />
|
|
<Route path="/orders/:id" element={<OrderDetail />} />
|
|
<Route path="/orders/:id/edit" element={<OrderEdit />} />
|
|
|
|
{/* Coupons */}
|
|
<Route path="/coupons" element={<CouponsIndex />} />
|
|
<Route path="/coupons/new" element={<CouponNew />} />
|
|
|
|
{/* Customers */}
|
|
<Route path="/customers" element={<CustomersIndex />} />
|
|
|
|
{/* More */}
|
|
<Route path="/more" element={<MorePage />} />
|
|
|
|
{/* Settings */}
|
|
<Route path="/settings" element={<SettingsIndex />} />
|
|
<Route path="/settings/store" element={<SettingsStore />} />
|
|
<Route path="/settings/payments" element={<SettingsPayments />} />
|
|
<Route path="/settings/shipping" element={<SettingsShipping />} />
|
|
<Route path="/settings/taxes" element={<SettingsIndex />} />
|
|
<Route path="/settings/checkout" element={<SettingsIndex />} />
|
|
<Route path="/settings/customers" element={<SettingsIndex />} />
|
|
<Route path="/settings/notifications" element={<SettingsIndex />} />
|
|
<Route path="/settings/brand" element={<SettingsIndex />} />
|
|
|
|
{/* Dynamic Addon Routes */}
|
|
{addonRoutes.map((route: any) => (
|
|
<Route
|
|
key={route.path}
|
|
path={route.path}
|
|
element={<AddonRoute config={route} />}
|
|
/>
|
|
))}
|
|
</Routes>
|
|
);
|
|
}
|
|
|
|
function Shell() {
|
|
const { on, setOn } = useFullscreen();
|
|
const { main } = useActiveSection();
|
|
const toggle = () => setOn(v => !v);
|
|
const isDesktop = useIsDesktop();
|
|
const location = useLocation();
|
|
|
|
// Check if standalone mode - force fullscreen and hide toggle
|
|
const isStandalone = window.WNW_CONFIG?.standaloneMode ?? false;
|
|
const fullscreen = isStandalone ? true : on;
|
|
|
|
// Check if current route is dashboard
|
|
const isDashboardRoute = location.pathname === '/' || location.pathname.startsWith('/dashboard');
|
|
|
|
return (
|
|
<>
|
|
{!isStandalone && <ShortcutsBinder onToggle={toggle} />}
|
|
{!isStandalone && <CommandPalette toggleFullscreen={toggle} />}
|
|
<div className={`flex flex-col min-h-screen ${fullscreen ? 'woonoow-fullscreen-root' : ''}`}>
|
|
<Header onFullscreen={toggle} fullscreen={fullscreen} showToggle={!isStandalone} />
|
|
{fullscreen ? (
|
|
isDesktop ? (
|
|
<div className="flex flex-1 min-h-0">
|
|
<Sidebar />
|
|
<main className="flex-1 flex flex-col min-h-0 min-w-0">
|
|
{isDashboardRoute ? (
|
|
<DashboardSubmenuBar items={main.children} fullscreen={true} />
|
|
) : (
|
|
<SubmenuBar items={main.children} fullscreen={true} />
|
|
)}
|
|
<PageHeader fullscreen={true} />
|
|
<div className="flex-1 overflow-auto p-4 min-w-0">
|
|
<AppRoutes />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-1 flex-col min-h-0">
|
|
<TopNav fullscreen />
|
|
{isDashboardRoute ? (
|
|
<DashboardSubmenuBar items={main.children} fullscreen={true} />
|
|
) : (
|
|
<SubmenuBar items={main.children} fullscreen={true} />
|
|
)}
|
|
<main className="flex-1 flex flex-col min-h-0 min-w-0 pb-14">
|
|
<PageHeader fullscreen={true} />
|
|
<div className="flex-1 overflow-auto p-4 min-w-0">
|
|
<AppRoutes />
|
|
</div>
|
|
</main>
|
|
<BottomNav />
|
|
<FAB />
|
|
</div>
|
|
)
|
|
) : (
|
|
<div className="flex flex-1 flex-col min-h-0">
|
|
<TopNav />
|
|
{isDashboardRoute ? (
|
|
<DashboardSubmenuBar items={main.children} fullscreen={false} />
|
|
) : (
|
|
<SubmenuBar items={main.children} fullscreen={false} />
|
|
)}
|
|
<PageHeader fullscreen={false} />
|
|
<main className="flex-1 flex flex-col min-h-0 min-w-0">
|
|
<div className="flex-1 overflow-auto p-4 min-w-0">
|
|
<AppRoutes />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function AuthWrapper() {
|
|
const [isAuthenticated, setIsAuthenticated] = useState(
|
|
window.WNW_CONFIG?.isAuthenticated ?? true
|
|
);
|
|
const [isChecking, setIsChecking] = useState(window.WNW_CONFIG?.standaloneMode ?? false);
|
|
const location = useLocation();
|
|
|
|
useEffect(() => {
|
|
console.log('[AuthWrapper] Initial config:', {
|
|
standaloneMode: window.WNW_CONFIG?.standaloneMode,
|
|
isAuthenticated: window.WNW_CONFIG?.isAuthenticated,
|
|
currentUser: window.WNW_CONFIG?.currentUser
|
|
});
|
|
|
|
// In standalone mode, trust the initial PHP auth check
|
|
// PHP uses wp_signon which sets proper WordPress cookies
|
|
const checkAuth = () => {
|
|
if (window.WNW_CONFIG?.standaloneMode) {
|
|
setIsAuthenticated(window.WNW_CONFIG.isAuthenticated ?? false);
|
|
setIsChecking(false);
|
|
} else {
|
|
// In wp-admin mode, always authenticated
|
|
setIsChecking(false);
|
|
}
|
|
};
|
|
checkAuth();
|
|
}, []);
|
|
|
|
if (isChecking) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<Loader2 className="w-12 h-12 animate-spin text-primary" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (window.WNW_CONFIG?.standaloneMode && !isAuthenticated && location.pathname !== '/login') {
|
|
return <Navigate to="/login" replace />;
|
|
}
|
|
|
|
if (location.pathname === '/login' && isAuthenticated) {
|
|
return <Navigate to="/" replace />;
|
|
}
|
|
|
|
return (
|
|
<FABProvider>
|
|
<PageHeaderProvider>
|
|
<DashboardProvider>
|
|
<Shell />
|
|
</DashboardProvider>
|
|
</PageHeaderProvider>
|
|
</FABProvider>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<QueryClientProvider client={qc}>
|
|
<HashRouter>
|
|
<Routes>
|
|
{window.WNW_CONFIG?.standaloneMode && (
|
|
<Route path="/login" element={<Login />} />
|
|
)}
|
|
<Route path="/*" element={<AuthWrapper />} />
|
|
</Routes>
|
|
<Toaster
|
|
richColors
|
|
theme="light"
|
|
position="bottom-right"
|
|
closeButton
|
|
visibleToasts={3}
|
|
duration={4000}
|
|
offset="20px"
|
|
/>
|
|
</HashRouter>
|
|
</QueryClientProvider>
|
|
);
|
|
} |