306 lines
No EOL
10 KiB
TypeScript
306 lines
No EOL
10 KiB
TypeScript
import { Link } from "wouter"
|
|
import { SearchIcon, SettingsIcon, TagsIcon, ArchiveIcon, UserIcon, LayoutListIcon, PanelLeftIcon, PanelLeftCloseIcon, MenuIcon, XIcon } from "lucide-react"
|
|
import { Button, buttonVariants } from "@/components/ui/button.tsx"
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip.tsx"
|
|
import { useLogin } from "@/state/user.ts";
|
|
import { useNavItems } from "./navAtom";
|
|
import { Separator } from "../ui/separator";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface NavItemProps {
|
|
icon: React.ReactNode;
|
|
to: string;
|
|
name: string;
|
|
}
|
|
|
|
export function NavItem({
|
|
icon,
|
|
to,
|
|
name
|
|
}: NavItemProps) {
|
|
return <Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Link
|
|
href={to}
|
|
className={buttonVariants({ variant: "ghost" })}
|
|
>
|
|
{icon}
|
|
<span className="sr-only">{name}</span>
|
|
</Link>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">{name}</TooltipContent>
|
|
</Tooltip>
|
|
}
|
|
|
|
interface NavItemButtonProps {
|
|
icon: React.ReactNode;
|
|
onClick: () => void;
|
|
name: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function NavItemButton({
|
|
icon,
|
|
onClick,
|
|
name,
|
|
className
|
|
}: NavItemButtonProps) {
|
|
return <Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
onClick={onClick}
|
|
variant="ghost"
|
|
className={className}
|
|
>
|
|
{icon}
|
|
<span className="sr-only">{name}</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">{name}</TooltipContent>
|
|
</Tooltip>
|
|
}
|
|
|
|
export function NavList() {
|
|
const loginInfo = useLogin();
|
|
const navItems = useNavItems();
|
|
|
|
return <aside className="h-dvh flex flex-col">
|
|
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5 flex-1">
|
|
{navItems && <>{navItems} <Separator /> </>}
|
|
<NavItem icon={<SearchIcon className="h-5 w-5" />} to="/search" name="Search" />
|
|
<NavItem icon={<TagsIcon className="h-5 w-5" />} to="/tags" name="Tags" />
|
|
<NavItem icon={<ArchiveIcon className="h-5 w-5" />} to="/difference" name="Difference" />
|
|
</nav>
|
|
<nav className="mt-auto flex flex-col items-center gap-4 px-2 sm:py-5 flex-grow-0">
|
|
<NavItem icon={<UserIcon className="h-5 w-5" />} to={loginInfo ? "/profile" : "/login"} name={loginInfo ? "Profiles" : "Login"} />
|
|
<NavItem icon={<SettingsIcon className="h-5 w-5" />} to="/setting" name="Settings" />
|
|
</nav>
|
|
</aside>
|
|
}
|
|
|
|
// 사이드바 토글 버튼
|
|
export function SidebarToggle({ isOpen, onToggle }: { isOpen: boolean; onToggle: () => void }) {
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={onToggle}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
{isOpen ? (
|
|
<PanelLeftCloseIcon className="h-4 w-4" />
|
|
) : (
|
|
<PanelLeftIcon className="h-4 w-4" />
|
|
)}
|
|
<span className="sr-only">{isOpen ? "Close sidebar" : "Open sidebar"}</span>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">
|
|
{isOpen ? "Close sidebar" : "Open sidebar"}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
// 모바일용 사이드바 토글 버튼
|
|
export function MobileSidebarToggle({ isOpen, onToggle }: { isOpen: boolean; onToggle: () => void }) {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={onToggle}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
{isOpen ? (
|
|
<XIcon className="h-5 w-5" />
|
|
) : (
|
|
<MenuIcon className="h-5 w-5" />
|
|
)}
|
|
<span className="sr-only">{isOpen ? "Close menu" : "Open menu"}</span>
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
// 데스크탑용 사이드바 네비게이션
|
|
export function SidebarNav({ isCollapsed, onNavigate }: { isCollapsed: boolean; onNavigate?: () => void }) {
|
|
const loginInfo = useLogin();
|
|
const navItems = useNavItems();
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<nav className="flex flex-col gap-2 p-3 flex-1 min-h-0">
|
|
{navItems && (
|
|
<>
|
|
<div className={cn("space-y-2", isCollapsed && "items-center")}>
|
|
{navItems}
|
|
</div>
|
|
<Separator className="my-3" />
|
|
</>
|
|
)}
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<SidebarNavItem
|
|
icon={<SearchIcon className="h-5 w-5" />}
|
|
to="/search"
|
|
name="Search"
|
|
isCollapsed={isCollapsed}
|
|
onNavigate={onNavigate}
|
|
/>
|
|
<SidebarNavItem
|
|
icon={<TagsIcon className="h-5 w-5" />}
|
|
to="/tags"
|
|
name="Tags"
|
|
isCollapsed={isCollapsed}
|
|
onNavigate={onNavigate}
|
|
/>
|
|
<SidebarNavItem
|
|
icon={<ArchiveIcon className="h-5 w-5" />}
|
|
to="/difference"
|
|
name="Difference"
|
|
isCollapsed={isCollapsed}
|
|
onNavigate={onNavigate}
|
|
/>
|
|
<SidebarNavItem
|
|
icon={<LayoutListIcon className="h-5 w-5" />}
|
|
to="/queue"
|
|
name="Task Queue"
|
|
isCollapsed={isCollapsed}
|
|
onNavigate={onNavigate}
|
|
/>
|
|
</div>
|
|
</nav>
|
|
|
|
<div className="border-t p-3 flex flex-col gap-2 flex-shrink-0">
|
|
<SidebarNavItem
|
|
icon={<UserIcon className="h-5 w-5" />}
|
|
to={loginInfo ? "/profile" : "/login"}
|
|
name={loginInfo ? "Profiles" : "Login"}
|
|
isCollapsed={isCollapsed}
|
|
onNavigate={onNavigate}
|
|
/>
|
|
<SidebarNavItem
|
|
icon={<SettingsIcon className="h-5 w-5" />}
|
|
to="/setting"
|
|
name="Settings"
|
|
isCollapsed={isCollapsed}
|
|
onNavigate={onNavigate}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 사이드바 네비게이션 아이템
|
|
interface SidebarNavItemProps {
|
|
icon: React.ReactNode;
|
|
to: string;
|
|
name: string;
|
|
isCollapsed: boolean;
|
|
onNavigate?: () => void;
|
|
}
|
|
|
|
function SidebarNavItem({ icon, to, name, isCollapsed, onNavigate }: SidebarNavItemProps) {
|
|
if (isCollapsed) {
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Link
|
|
href={to}
|
|
className={cn(
|
|
buttonVariants({ variant: "ghost", size: "sm" }),
|
|
"justify-center h-10 w-10 p-0"
|
|
)}
|
|
onClick={onNavigate}
|
|
>
|
|
{icon}
|
|
<span className="sr-only">{name}</span>
|
|
</Link>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">{name}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Link
|
|
href={to}
|
|
className={cn(
|
|
buttonVariants({ variant: "ghost", size: "sm" }),
|
|
"justify-start gap-3 h-10 px-3"
|
|
)}
|
|
onClick={onNavigate}
|
|
>
|
|
{icon}
|
|
<span>{name}</span>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
// 모바일용 하단 네비게이션
|
|
export function BottomNav() {
|
|
const loginInfo = useLogin();
|
|
const navItems = useNavItems();
|
|
|
|
return (
|
|
<nav className="mb-1">
|
|
<div className="flex justify-around items-center max-w-md mx-auto
|
|
overflow-hidden
|
|
bg-background/50 backdrop-blur-md border rounded-full">
|
|
<BottomNavItem
|
|
icon={<SearchIcon className="h-5 w-5" />}
|
|
to="/search"
|
|
name="Search"
|
|
className="flex-1"
|
|
/>
|
|
{navItems ? navItems : <>
|
|
<BottomNavItem
|
|
icon={<TagsIcon className="h-5 w-5" />}
|
|
to="/tags"
|
|
name="Tags"
|
|
className="flex-1"
|
|
/>
|
|
<BottomNavItem
|
|
icon={<ArchiveIcon className="h-5 w-5" />}
|
|
to="/difference"
|
|
name="Diff"
|
|
className="flex-1"
|
|
/>
|
|
<BottomNavItem
|
|
icon={<UserIcon className="h-5 w-5" />}
|
|
to={loginInfo ? "/profile" : "/login"}
|
|
name={loginInfo ? "Profile" : "Login"}
|
|
className="flex-1"
|
|
/>
|
|
<BottomNavItem
|
|
icon={<SettingsIcon className="h-5 w-5" />}
|
|
to="/setting"
|
|
name="Settings"
|
|
className="flex-1"
|
|
/>
|
|
</>}
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
// 하단 네비게이션 아이템
|
|
interface BottomNavItemProps {
|
|
icon: React.ReactNode;
|
|
to: string;
|
|
name: string;
|
|
className?: string;
|
|
}
|
|
|
|
function BottomNavItem({ icon, to, name, className }: BottomNavItemProps) {
|
|
return (
|
|
<Link
|
|
href={to}
|
|
className={cn("flex flex-col items-center gap-1 p-2 hover:bg-accent text-xs min-w-0", className)}
|
|
>
|
|
{icon}
|
|
<span className="text-xs truncate leading-normal">{name}</span>
|
|
</Link>
|
|
);
|
|
} |