haixunMaster/components/layout/mobile-nav.tsx

142 lines
4.7 KiB
TypeScript
Raw Normal View History

2026-06-21 12:50:31 +00:00
"use client";
import { useEffect } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { X } from "lucide-react";
import { BrandLogo, BrandMark } from "@/components/brand/logo";
import { NavLinks } from "@/components/layout/nav-links";
import { SidebarUtilities } from "@/components/layout/sidebar-utilities";
import { MOBILE_BOTTOM_NAV, NAV_GROUPS } from "@/lib/nav";
import { cn } from "@/lib/utils";
interface MobileNavDrawerProps {
open: boolean;
onClose: () => void;
badgeCount?: number;
userEmail?: string | null;
onLogout: () => void;
}
export function MobileNavDrawer({
open,
onClose,
badgeCount = 0,
userEmail,
onLogout,
}: MobileNavDrawerProps) {
useEffect(() => {
if (!open) return;
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", onKey);
return () => {
document.body.style.overflow = prev;
window.removeEventListener("keydown", onKey);
};
}, [open, onClose]);
if (!open) return null;
return (
<div className="fixed inset-0 z-50 lg:hidden">
<button
type="button"
className="absolute inset-0 bg-black/60"
aria-label="關閉選單"
onClick={onClose}
/>
<aside className="app-sidebar absolute inset-y-0 left-0 flex h-full w-[min(280px,88vw)] flex-col shadow-xl">
<div className="flex h-[52px] shrink-0 items-center justify-between border-b border-border px-3">
<div className="flex min-w-0 items-center gap-2">
<BrandLogo size="sm" />
<BrandMark />
</div>
<button
type="button"
onClick={onClose}
className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
aria-label="關閉選單"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-2 py-3 safe-bottom">
<NavLinks groups={NAV_GROUPS} badgeCount={badgeCount} onNavigate={onClose} />
<SidebarUtilities
userEmail={userEmail}
onLogout={() => {
onClose();
onLogout();
}}
/>
</div>
</aside>
</div>
);
}
interface MobileBottomNavProps {
badgeCount?: number;
onOpenMenu: () => void;
}
export function MobileBottomNav({ badgeCount = 0, onOpenMenu }: MobileBottomNavProps) {
const pathname = usePathname();
return (
<nav className="mobile-bottom-nav fixed inset-x-0 bottom-0 z-40 border-t lg:hidden" aria-label="主要導覽">
<div className="mx-auto flex max-w-lg items-stretch justify-around safe-bottom">
{MOBILE_BOTTOM_NAV.map((item) => {
const Icon = item.icon;
const isMenu = item.href === "#menu";
const active = !isMenu && pathname === item.href;
const badge = item.showBadge && badgeCount > 0 ? badgeCount : 0;
const className = cn(
"relative flex min-h-[52px] min-w-0 flex-1 flex-col items-center justify-center gap-0.5 px-1 py-1 text-[10px]",
active ? "text-primary" : "text-muted-foreground"
);
if (isMenu) {
return (
<button key={item.href} type="button" onClick={onOpenMenu} className={className}>
<Icon className="h-5 w-5" strokeWidth={1.75} />
<span>{item.label}</span>
{badgeCount > 0 && (
<span className="absolute right-3 top-1 flex h-4 min-w-4 items-center justify-center rounded bg-primary px-1 text-[9px] font-medium text-primary-foreground">
{badgeCount > 9 ? "9+" : badgeCount}
</span>
)}
</button>
);
}
return (
<Link key={item.href} href={item.href} className={className}>
<Icon className="h-5 w-5" strokeWidth={active ? 2 : 1.75} />
<span>{item.label}</span>
{badge > 0 && (
<span className="absolute right-3 top-1 flex h-4 min-w-4 items-center justify-center rounded bg-primary px-1 text-[9px] font-medium text-primary-foreground">
{badge > 9 ? "9+" : badge}
</span>
)}
</Link>
);
})}
</div>
</nav>
);
}
export function useMobileLogout() {
const router = useRouter();
return async function logout() {
await fetch("/api/auth/logout", { method: "POST" });
router.replace("/login");
};
}