142 lines
4.7 KiB
TypeScript
142 lines
4.7 KiB
TypeScript
"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");
|
|
};
|
|
} |