149 lines
3.9 KiB
TypeScript
149 lines
3.9 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import { MessageCircle, User, Settings, Home, Search } from 'lucide-react';
|
|
import { cn } from '@/shared/lib/utils';
|
|
import type { BottomNavItem } from './mobile-layout';
|
|
|
|
/**
|
|
* EN: Native-style Bottom Navigation Component
|
|
* VI: Component Bottom Navigation theo phong cách Native
|
|
*
|
|
* Pre-configured bottom navigation for common app patterns.
|
|
* Bottom navigation được cấu hình sẵn cho các pattern app thông dụng.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <MobileBottomNav
|
|
* activeItem="chat"
|
|
* onItemPress={handleNavPress}
|
|
* showBadges={{ chat: 3 }}
|
|
* />
|
|
* ```
|
|
*/
|
|
|
|
export interface MobileBottomNavProps {
|
|
/** Active navigation item ID */
|
|
activeItem?: string;
|
|
/** Callback when item is pressed */
|
|
onItemPress?: (itemId: string) => void;
|
|
/** Show badges on items */
|
|
showBadges?: Record<string, number>;
|
|
/** Custom navigation items */
|
|
customItems?: BottomNavItem[];
|
|
/** Hide labels on inactive items */
|
|
hideLabels?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Pre-configured bottom navigation items for common apps
|
|
*/
|
|
const DEFAULT_NAV_ITEMS: BottomNavItem[] = [
|
|
{
|
|
id: 'home',
|
|
label: 'Home',
|
|
icon: <Home className="w-6 h-6" />,
|
|
},
|
|
{
|
|
id: 'search',
|
|
label: 'Search',
|
|
icon: <Search className="w-6 h-6" />,
|
|
},
|
|
{
|
|
id: 'chat',
|
|
label: 'Chat',
|
|
icon: <MessageCircle className="w-6 h-6" />,
|
|
},
|
|
{
|
|
id: 'profile',
|
|
label: 'Profile',
|
|
icon: <User className="w-6 h-6" />,
|
|
},
|
|
{
|
|
id: 'settings',
|
|
label: 'Settings',
|
|
icon: <Settings className="w-6 h-6" />,
|
|
},
|
|
];
|
|
|
|
export function MobileBottomNav({
|
|
activeItem,
|
|
onItemPress,
|
|
showBadges = {},
|
|
customItems,
|
|
hideLabels = false,
|
|
}: MobileBottomNavProps) {
|
|
const navItems = customItems || DEFAULT_NAV_ITEMS;
|
|
|
|
return (
|
|
<nav className="flex items-center justify-around w-full h-full px-2">
|
|
{navItems.map((item) => {
|
|
const badge = showBadges[item.id];
|
|
const isActive = activeItem === item.id;
|
|
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => onItemPress?.(item.id)}
|
|
className={cn(
|
|
'flex flex-col items-center justify-center',
|
|
'min-w-[60px] h-12 rounded-lg',
|
|
'transition-all duration-200',
|
|
'relative group',
|
|
'active:scale-95',
|
|
isActive
|
|
? 'text-white'
|
|
: 'text-white/50 hover:text-white/80'
|
|
)}
|
|
>
|
|
<div className={cn(
|
|
'relative mb-1 transition-transform',
|
|
isActive ? 'scale-110' : 'group-hover:scale-105'
|
|
)}>
|
|
{item.icon}
|
|
{badge && badge > 0 && (
|
|
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full min-w-[18px] h-[18px] flex items-center justify-center px-1 font-medium">
|
|
{badge > 99 ? '99+' : badge}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{(!hideLabels || isActive) && (
|
|
<span className={cn(
|
|
'text-xs font-medium transition-all',
|
|
isActive ? 'opacity-100' : 'opacity-70'
|
|
)}>
|
|
{item.label}
|
|
</span>
|
|
)}
|
|
{isActive && (
|
|
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-6 h-0.5 bg-white rounded-full animate-fadeIn" />
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* EN: Hook for managing bottom navigation state
|
|
* VI: Hook để quản lý trạng thái bottom navigation
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const { activeItem, setActiveItem, handleNavPress } = useBottomNav('home');
|
|
* ```
|
|
*/
|
|
export function useBottomNav(initialItem: string = 'home') {
|
|
const [activeItem, setActiveItem] = React.useState(initialItem);
|
|
|
|
const handleNavPress = React.useCallback((itemId: string) => {
|
|
setActiveItem(itemId);
|
|
}, []);
|
|
|
|
return {
|
|
activeItem,
|
|
setActiveItem,
|
|
handleNavPress,
|
|
};
|
|
} |