Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | 'use client'; /** * MobileBottomNav Component * * A bottom navigation bar optimized for mobile devices. * Features: * - 44px minimum tap targets * - Cart badge with item count * - Active state indication * - Safe area inset support for notched devices * - Hidden on desktop (md:hidden) * * @example * ```tsx * <MobileBottomNav /> * ``` */ import { usePathname } from 'next/navigation'; import Link from 'next/link'; import { useSelector } from 'react-redux'; import { Icon, IconName } from '@/components/ui/icons'; import { selectTotalItemCount } from '@/redux/features/cartSlice'; import { useAppSelector } from '@/redux/store'; import { cn } from '@/lib/core'; interface NavItem { href: string; icon: IconName; label: string; showCartBadge?: boolean; showWishlistBadge?: boolean; } const navItems: NavItem[] = [ { href: '/', icon: 'home', label: 'Home' }, { href: '/shop', icon: 'grid', label: 'Shop' }, { href: '/cart', icon: 'cart', label: 'Cart', showCartBadge: true }, { href: '/wishlist', icon: 'heart', label: 'Wishlist', showWishlistBadge: true }, { href: '/my-account', icon: 'user', label: 'Account' }, ]; export function MobileBottomNav() { const pathname = usePathname(); const cartItemCount = useSelector(selectTotalItemCount); const wishlistItemCount = useAppSelector((state) => state.wishlistReducer.items?.length ?? 0); // Don't show on certain pages (checkout, admin, etc.) const hiddenPaths = ['/checkout', '/admin', '/signin', '/signup']; if (hiddenPaths.some((path) => pathname.startsWith(path))) { return null; } return ( <nav role="navigation" aria-label="Mobile navigation" className={cn( 'fixed bottom-0 left-0 right-0 z-50', 'bg-white dark:bg-gray-900', 'border-t border-gray-200 dark:border-gray-800', 'pb-[env(safe-area-inset-bottom)]', 'md:hidden' // Hide on desktop )} > <div className="flex items-center justify-around h-16"> {navItems.map((item) => { const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href)); const badgeCount = item.showCartBadge ? cartItemCount : item.showWishlistBadge ? wishlistItemCount : 0; return ( <Link key={item.href} href={item.href} className={cn( 'flex flex-col items-center justify-center', 'w-full h-full', 'min-h-[44px] min-w-[44px]', // Minimum tap target 'transition-colors duration-200', 'touch-manipulation', // Optimize touch response isActive ? 'text-primary-600 dark:text-primary-400' : 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200' )} aria-current={isActive ? 'page' : undefined} > <div className="relative"> <Icon name={item.icon} size={24} /> {badgeCount > 0 && ( <span className={cn( 'absolute -top-1.5 -right-1.5', 'min-w-[18px] h-[18px] px-1', 'flex items-center justify-center', 'bg-red-500 text-white text-[10px] font-bold rounded-full' )} aria-label={`${badgeCount} items`} > {badgeCount > 99 ? '99+' : badgeCount} </span> )} </div> <span className="text-xs mt-1 font-medium">{item.label}</span> </Link> ); })} </div> </nav> ); } export default MobileBottomNav; |