/** * EN: Native App-style Mobile Layout Component * VI: Component Layout Mobile theo phong cách Native App * * A mobile-first layout that mimics native app UX with iOS/Android design patterns. * Layout mobile-first mô phỏng UX native app với design patterns của iOS/Android. * * Features: * - iOS/Android safe areas support * - Native-like bottom navigation * - Pull-to-refresh capability * - Touch-optimized interactions * - App-like scrolling behavior * * @example * ```tsx * * * * ``` */ 'use client'; import React, { useState, useRef, useEffect } from 'react'; import { cn } from '@/shared/lib/utils'; export interface MobileLayoutProps { /** * Content to render inside the layout */ children: React.ReactNode; /** * Custom className for the container */ className?: string; /** * Header content (optional) */ header?: React.ReactNode; /** * Footer content (optional) */ footer?: React.ReactNode; /** * Bottom navigation (optional) */ bottomNav?: React.ReactNode; /** * Whether to show the header */ showHeader?: boolean; /** * Whether to show the footer */ showFooter?: boolean; /** * Enable pull-to-refresh functionality */ enablePullToRefresh?: boolean; /** * Callback for pull-to-refresh */ onRefresh?: () => Promise; /** * Show native-style bottom navigation bar */ showBottomNav?: boolean; /** * Bottom navigation items for native-style nav */ bottomNavItems?: BottomNavItem[]; /** * Active bottom nav item */ activeNavItem?: string; /** * Callback when bottom nav item is pressed */ onNavItemPress?: (itemId: string) => void; } export interface BottomNavItem { id: string; label: string; icon: React.ReactNode; badge?: number; } /** * Native App-style Mobile Layout Component * * Features: * - Full-screen native app experience * - iOS/Android safe areas * - Pull-to-refresh with native feel * - Native-style bottom navigation * - Touch-optimized interactions * - App-like scrolling physics * * Layout structure: * - Header: iOS/Android style with safe areas * - Main: Native scrolling with pull-to-refresh * - BottomNav: Native-style tab bar * * @example * ```tsx * * * * ``` */ export function MobileLayout({ children, className, header, footer, bottomNav, showHeader = true, showFooter = false, enablePullToRefresh = false, onRefresh, showBottomNav = false, bottomNavItems = [], activeNavItem, onNavItemPress, }: MobileLayoutProps) { const [isRefreshing, setIsRefreshing] = useState(false); const [pullDistance, setPullDistance] = useState(0); const [isPulling, setIsPulling] = useState(false); const mainRef = useRef(null); const startYRef = useRef(0); const isDraggingRef = useRef(false); // Pull-to-refresh logic useEffect(() => { if (!enablePullToRefresh || !mainRef.current) return; const handleTouchStart = (e: TouchEvent) => { if (mainRef.current!.scrollTop === 0) { startYRef.current = e.touches[0].clientY; isDraggingRef.current = true; } }; const handleTouchMove = (e: TouchEvent) => { if (!isDraggingRef.current || mainRef.current!.scrollTop > 0) return; const currentY = e.touches[0].clientY; const diff = currentY - startYRef.current; if (diff > 0) { e.preventDefault(); setPullDistance(Math.min(diff * 0.5, 80)); // Max pull distance setIsPulling(diff > 30); // Threshold for refresh trigger } }; const handleTouchEnd = async () => { if (!isDraggingRef.current) return; isDraggingRef.current = false; if (isPulling && onRefresh) { setIsRefreshing(true); setPullDistance(0); setIsPulling(false); try { await onRefresh(); } finally { setIsRefreshing(false); } } else { setPullDistance(0); setIsPulling(false); } }; const element = mainRef.current; element.addEventListener('touchstart', handleTouchStart, { passive: false }); element.addEventListener('touchmove', handleTouchMove, { passive: false }); element.addEventListener('touchend', handleTouchEnd); return () => { element.removeEventListener('touchstart', handleTouchStart); element.removeEventListener('touchmove', handleTouchMove); element.removeEventListener('touchend', handleTouchEnd); }; }, [enablePullToRefresh, isPulling, onRefresh]); return (
{/* Mobile Header - iOS/Android style */} {showHeader && header && (
{header}
)} {/* Main Content - Native scrolling with pull-to-refresh */}
0 ? `translateY(${pullDistance}px)` : undefined, transition: isDraggingRef.current ? 'none' : 'transform 0.3s ease-out' }} > {/* Pull-to-refresh indicator */} {enablePullToRefresh && (
0 ? 'translate-y-0' : '-translate-y-full' )} >
{isRefreshing ? ( <>
Refreshing... ) : ( <>
{isPulling ? 'Release to refresh' : 'Pull to refresh'} )}
)} {/* Content with top padding for pull indicator */}
{children}
{/* Footer (optional) */} {showFooter && footer && (
{footer}
)} {/* Native-style Bottom Navigation */} {showBottomNav && bottomNavItems.length > 0 && ( )} {/* Custom Bottom Navigation (legacy support) */} {bottomNav && !showBottomNav && ( )}
); }