From 76624bb47331313d0c4d5664ca87dc4769b1d432 Mon Sep 17 00:00:00 2001 From: dwindown Date: Thu, 6 Nov 2025 20:21:12 +0700 Subject: [PATCH] feat: Implement mobile-first navigation with bottom bar and FAB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- admin-spa/src/App.tsx | 23 ++++-- admin-spa/src/components/FAB.tsx | 23 ++++++ admin-spa/src/components/nav/BottomNav.tsx | 82 ++++++++++++++++++++++ admin-spa/src/contexts/FABContext.tsx | 43 ++++++++++++ admin-spa/src/hooks/useScrollDirection.ts | 30 ++++++++ admin-spa/src/routes/More/index.tsx | 66 +++++++++++++++++ 6 files changed, 261 insertions(+), 6 deletions(-) create mode 100644 admin-spa/src/components/FAB.tsx create mode 100644 admin-spa/src/components/nav/BottomNav.tsx create mode 100644 admin-spa/src/contexts/FABContext.tsx create mode 100644 admin-spa/src/hooks/useScrollDirection.ts create mode 100644 admin-spa/src/routes/More/index.tsx diff --git a/admin-spa/src/App.tsx b/admin-spa/src/App.tsx index d0484c6..ba02008 100644 --- a/admin-spa/src/App.tsx +++ b/admin-spa/src/App.tsx @@ -30,7 +30,10 @@ 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'; @@ -192,6 +195,7 @@ 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 }) { @@ -369,6 +373,9 @@ function AppRoutes() { {/* Customers */} } /> + {/* More */} + } /> + {/* Settings */} } /> } /> @@ -436,12 +443,14 @@ function Shell() { ) : ( )} -
+
+ + ) ) : ( @@ -510,11 +519,13 @@ function AuthWrapper() { } return ( - - - - - + + + + + + + ); } diff --git a/admin-spa/src/components/FAB.tsx b/admin-spa/src/components/FAB.tsx new file mode 100644 index 0000000..3b0a6c0 --- /dev/null +++ b/admin-spa/src/components/FAB.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Plus } from 'lucide-react'; +import { useFAB } from '@/contexts/FABContext'; +import { Button } from '@/components/ui/button'; + +export function FAB() { + const { config } = useFAB(); + + if (!config || config.visible === false) { + return null; + } + + return ( + + ); +} diff --git a/admin-spa/src/components/nav/BottomNav.tsx b/admin-spa/src/components/nav/BottomNav.tsx new file mode 100644 index 0000000..ebf31bd --- /dev/null +++ b/admin-spa/src/components/nav/BottomNav.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { NavLink, useLocation } from 'react-router-dom'; +import { LayoutDashboard, ReceiptText, Package, Users, MoreHorizontal } from 'lucide-react'; +import { __ } from '@/lib/i18n'; + +interface BottomNavItem { + to: string; + icon: React.ReactNode; + label: string; + startsWith?: string; +} + +const navItems: BottomNavItem[] = [ + { + to: '/dashboard', + icon: , + label: __('Dashboard'), + startsWith: '/dashboard' + }, + { + to: '/orders', + icon: , + label: __('Orders'), + startsWith: '/orders' + }, + { + to: '/products', + icon: , + label: __('Products'), + startsWith: '/products' + }, + { + to: '/customers', + icon: , + label: __('Customers'), + startsWith: '/customers' + }, + { + to: '/more', + icon: , + label: __('More'), + startsWith: '/more' + } +]; + +export function BottomNav() { + const location = useLocation(); + + const isActive = (item: BottomNavItem) => { + if (item.startsWith) { + return location.pathname.startsWith(item.startsWith); + } + return location.pathname === item.to; + }; + + return ( + + ); +} diff --git a/admin-spa/src/contexts/FABContext.tsx b/admin-spa/src/contexts/FABContext.tsx new file mode 100644 index 0000000..ab7be87 --- /dev/null +++ b/admin-spa/src/contexts/FABContext.tsx @@ -0,0 +1,43 @@ +import React, { createContext, useContext, useState, ReactNode } from 'react'; + +export interface FABConfig { + icon?: ReactNode; + label: string; + onClick: () => void; + visible?: boolean; + variant?: 'primary' | 'secondary'; +} + +interface FABContextType { + config: FABConfig | null; + setFAB: (config: FABConfig | null) => void; + clearFAB: () => void; +} + +const FABContext = createContext(undefined); + +export function FABProvider({ children }: { children: ReactNode }) { + const [config, setConfig] = useState(null); + + const setFAB = (newConfig: FABConfig | null) => { + setConfig(newConfig); + }; + + const clearFAB = () => { + setConfig(null); + }; + + return ( + + {children} + + ); +} + +export function useFAB() { + const context = useContext(FABContext); + if (!context) { + throw new Error('useFAB must be used within FABProvider'); + } + return context; +} diff --git a/admin-spa/src/hooks/useScrollDirection.ts b/admin-spa/src/hooks/useScrollDirection.ts new file mode 100644 index 0000000..537b8a1 --- /dev/null +++ b/admin-spa/src/hooks/useScrollDirection.ts @@ -0,0 +1,30 @@ +import { useState, useEffect } from 'react'; + +export function useScrollDirection() { + const [scrollDirection, setScrollDirection] = useState<'up' | 'down'>('up'); + const [lastScrollY, setLastScrollY] = useState(0); + + useEffect(() => { + const handleScroll = () => { + const currentScrollY = window.scrollY; + + if (currentScrollY > lastScrollY && currentScrollY > 50) { + // Scrolling down + setScrollDirection('down'); + } else if (currentScrollY < lastScrollY) { + // Scrolling up + setScrollDirection('up'); + } + + setLastScrollY(currentScrollY); + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [lastScrollY]); + + return scrollDirection; +} diff --git a/admin-spa/src/routes/More/index.tsx b/admin-spa/src/routes/More/index.tsx new file mode 100644 index 0000000..28cdbd2 --- /dev/null +++ b/admin-spa/src/routes/More/index.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Tag, Settings as SettingsIcon, ChevronRight } from 'lucide-react'; +import { __ } from '@/lib/i18n'; + +interface MenuItem { + icon: React.ReactNode; + label: string; + description: string; + to: string; +} + +const menuItems: MenuItem[] = [ + { + icon: , + label: __('Coupons'), + description: __('Manage discount codes and promotions'), + to: '/coupons' + }, + { + icon: , + label: __('Settings'), + description: __('Configure your store settings'), + to: '/settings' + } +]; + +export default function MorePage() { + const navigate = useNavigate(); + + return ( +
+ {/* Header */} +
+
+

{__('More')}

+

+ {__('Additional features and settings')} +

+
+
+ + {/* Menu Items */} +
+ {menuItems.map((item) => ( + + ))} +
+
+ ); +}