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 { AppProvider } from '@/contexts/AppContext'; 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'; import { ThemeToggle } from '@/components/ThemeToggle'; function useFullscreen() { const [on, setOn] = useState(() => { 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
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 ( { // 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} ); } 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 ( ); } 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 (
`${link} ${isActive ? active : ''}`}> {__("Dashboard")} `${link} ${isActive ? active : ''}`}> {__("Orders")} `${link} ${isActive ? active : ''}`}> {__("Products")} `${link} ${isActive ? active : ''}`}> {__("Coupons")} `${link} ${isActive ? active : ''}`}> {__("Customers")} `${link} ${isActive ? active : ''}`}> {__("Settings")}
); } function useIsDesktop(minWidth = 1024) { // lg breakpoint const [isDesktop, setIsDesktop] = useState(() => { 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 SettingsTax from '@/routes/Settings/Tax'; import SettingsCustomers from '@/routes/Settings/Customers'; import SettingsLocalPickup from '@/routes/Settings/LocalPickup'; import SettingsNotifications from '@/routes/Settings/Notifications'; import StaffNotifications from '@/routes/Settings/Notifications/Staff'; import CustomerNotifications from '@/routes/Settings/Notifications/Customer'; import EditTemplate from '@/routes/Settings/Notifications/EditTemplate'; import SettingsDeveloper from '@/routes/Settings/Developer'; import MorePage from '@/routes/More'; // Addon Route Component - Dynamically loads addon components function AddonRoute({ config }: { config: any }) { const [Component, setComponent] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(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 (

{__('Loading addon...')}

); } if (error) { return (

{__('Failed to Load Addon')}

{error}

); } if (!Component) { return (

{__('Addon component not found')}

); } // Render the addon component with props return ; } function Header({ onFullscreen, fullscreen, showToggle = true, scrollContainerRef, onVisibilityChange }: { onFullscreen: () => void; fullscreen: boolean; showToggle?: boolean; scrollContainerRef?: React.RefObject; onVisibilityChange?: (visible: boolean) => void }) { const [siteTitle, setSiteTitle] = React.useState((window as any).wnw?.siteTitle || 'WooNooW'); const [storeLogo, setStoreLogo] = React.useState(''); const [storeLogoDark, setStoreLogoDark] = React.useState(''); const [isVisible, setIsVisible] = React.useState(true); const lastScrollYRef = React.useRef(0); const isStandalone = window.WNW_CONFIG?.standaloneMode ?? false; const [isDark, setIsDark] = React.useState(false); // Detect dark mode React.useEffect(() => { const checkDarkMode = () => { const htmlEl = document.documentElement; setIsDark(htmlEl.classList.contains('dark')); }; checkDarkMode(); // Watch for theme changes const observer = new MutationObserver(checkDarkMode); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); return () => observer.disconnect(); }, []); // Notify parent of visibility changes React.useEffect(() => { onVisibilityChange?.(isVisible); }, [isVisible, onVisibilityChange]); // Fetch store branding on mount React.useEffect(() => { const fetchBranding = async () => { try { const response = await fetch((window.WNW_CONFIG?.restUrl || '') + '/store/branding'); if (response.ok) { const data = await response.json(); if (data.store_logo) setStoreLogo(data.store_logo); if (data.store_logo_dark) setStoreLogoDark(data.store_logo_dark); if (data.store_name) setSiteTitle(data.store_name); } } catch (err) { console.error('Failed to fetch branding:', err); } }; fetchBranding(); }, []); // Listen for store settings updates React.useEffect(() => { const handleStoreUpdate = (event: CustomEvent) => { if (event.detail?.store_logo) setStoreLogo(event.detail.store_logo); if (event.detail?.store_logo_dark) setStoreLogoDark(event.detail.store_logo_dark); 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); }, []); // Hide/show header on scroll (mobile only) React.useEffect(() => { const scrollContainer = scrollContainerRef?.current; if (!scrollContainer) return; const handleScroll = () => { const currentScrollY = scrollContainer.scrollTop; // Only apply on mobile (check window width) if (window.innerWidth >= 768) { setIsVisible(true); return; } if (currentScrollY > lastScrollYRef.current && currentScrollY > 50) { // Scrolling down & past threshold setIsVisible(false); } else if (currentScrollY < lastScrollYRef.current) { // Scrolling up setIsVisible(true); } lastScrollYRef.current = currentScrollY; }; scrollContainer.addEventListener('scroll', handleScroll, { passive: true }); return () => { scrollContainer.removeEventListener('scroll', handleScroll); }; }, [scrollContainerRef]); 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); } }; // Hide header completely on mobile in fullscreen mode (both standalone and wp-admin fullscreen) if (fullscreen && typeof window !== 'undefined' && window.innerWidth < 768) { return null; } // Choose logo based on theme const currentLogo = isDark && storeLogoDark ? storeLogoDark : storeLogo; return (
{currentLogo ? ( {siteTitle} ) : (
{siteTitle}
)}
{window.WNW_API?.isDev ? 'Dev Server' : 'Production'}
{isStandalone && ( <> {__('WordPress')} )} {showToggle && ( )}
); } const qc = new QueryClient(); function ShortcutsBinder({ onToggle }: { onToggle: () => void }) { useShortcuts({ toggleFullscreen: onToggle }); return null; } // Centralized route controller so we don't duplicate in each layout function AppRoutes() { const addonRoutes = (window as any).WNW_ADDON_ROUTES || []; return ( {/* Dashboard */} } /> } /> } /> } /> } /> } /> } /> } /> {/* Products */} } /> } /> } /> } /> } /> } /> } /> {/* Orders */} } /> } /> } /> } /> {/* Coupons */} } /> } /> {/* Customers */} } /> {/* More */} } /> {/* Settings */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Dynamic Addon Routes */} {addonRoutes.map((route: any) => ( } /> ))} ); } function Shell() { const { on, setOn } = useFullscreen(); const { main } = useActiveSection(); const toggle = () => setOn(v => !v); const exitFullscreen = () => setOn(false); const isDesktop = useIsDesktop(); const location = useLocation(); const scrollContainerRef = React.useRef(null); // 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'); // Check if current route is More page (no submenu needed) const isMorePage = location.pathname === '/more'; const submenuTopClass = fullscreen ? 'top-0' : 'top-[calc(7rem+32px)]'; const submenuZIndex = fullscreen ? 'z-50' : 'z-40'; return ( {!isStandalone && } {!isStandalone && }
{fullscreen ? ( isDesktop ? (
{/* Flex wrapper: desktop = col-reverse (SubmenuBar first, PageHeader second) */}
{isDashboardRoute ? ( ) : ( )}
) : (
{/* Flex wrapper: mobile = col (PageHeader first), desktop = col-reverse (SubmenuBar first) */}
{!isMorePage && (isDashboardRoute ? ( ) : ( ))}
) ) : (
{/* Flex wrapper: mobile = col (PageHeader first), desktop = col-reverse (SubmenuBar first) */}
{isDashboardRoute ? ( ) : ( )}
)}
); } 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 (
); } if (window.WNW_CONFIG?.standaloneMode && !isAuthenticated && location.pathname !== '/login') { return ; } if (location.pathname === '/login' && isAuthenticated) { return ; } return ( ); } export default function App() { return ( {window.WNW_CONFIG?.standaloneMode && ( } /> )} } /> ); }