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:
dwindown
2025-11-06 20:21:12 +07:00
parent 4be283c4a4
commit 76624bb473
6 changed files with 261 additions and 6 deletions

View 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>
);
}

View 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>
);
}