feat: Implement mobile-first navigation with bottom bar and FAB
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
This commit is contained in:
23
admin-spa/src/components/FAB.tsx
Normal file
23
admin-spa/src/components/FAB.tsx
Normal file
@@ -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 (
|
||||
<Button
|
||||
onClick={config.onClick}
|
||||
size="lg"
|
||||
className="fixed bottom-[72px] right-4 z-40 w-14 h-14 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-200 md:hidden"
|
||||
aria-label={config.label}
|
||||
>
|
||||
{config.icon || <Plus className="w-6 h-6" />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
82
admin-spa/src/components/nav/BottomNav.tsx
Normal file
82
admin-spa/src/components/nav/BottomNav.tsx
Normal file
@@ -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: <LayoutDashboard className="w-5 h-5" />,
|
||||
label: __('Dashboard'),
|
||||
startsWith: '/dashboard'
|
||||
},
|
||||
{
|
||||
to: '/orders',
|
||||
icon: <ReceiptText className="w-5 h-5" />,
|
||||
label: __('Orders'),
|
||||
startsWith: '/orders'
|
||||
},
|
||||
{
|
||||
to: '/products',
|
||||
icon: <Package className="w-5 h-5" />,
|
||||
label: __('Products'),
|
||||
startsWith: '/products'
|
||||
},
|
||||
{
|
||||
to: '/customers',
|
||||
icon: <Users className="w-5 h-5" />,
|
||||
label: __('Customers'),
|
||||
startsWith: '/customers'
|
||||
},
|
||||
{
|
||||
to: '/more',
|
||||
icon: <MoreHorizontal className="w-5 h-5" />,
|
||||
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 (
|
||||
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-background border-t border-border safe-area-inset-bottom md:hidden">
|
||||
<div className="flex items-center justify-around h-14">
|
||||
{navItems.map((item) => {
|
||||
const active = isActive(item);
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={`flex flex-col items-center justify-center flex-1 h-full gap-0.5 transition-colors ${
|
||||
active
|
||||
? 'text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
<span className="text-[10px] font-medium leading-none">
|
||||
{item.label}
|
||||
</span>
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user