v3/frontend/src/components/app_sidebar.tsx

253 lines
10 KiB
TypeScript

import Icon from "@mdi/react";
import { BookPlus, Calendar, ChevronsUpDown, Home, Inbox, ListTodo, LogOut, Monitor, Moon, Palette, PanelLeft, Plus, Search, Sun, Waypoints } from "lucide-react";
import { Button } from "~/components/ui/button";
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, useSidebar } from "~/components/ui/sidebar";
import { type ColorName, colors } from "~/lib/color";
import { type IconName, icons } from "~/lib/icon";
import { NewNoteDialog } from "~/components/note/new_note_dialog";
import { NewCollectionDialog } from "~/components/collection/new_collection_dialog";
import { cn } from "~/lib/utils";
import { useCollectionNotesMetadata, useCollections } from "~/hooks/use-metadata";
import { useIsMobile } from "~/hooks/use-mobile";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "~/components/ui/dropdown-menu";
import { UserAvatar } from "~/components/user/user_avatar";
import { Link } from "@tanstack/react-router";
import { useTheme } from "~/hooks/use-theme";
import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
export function AppSidebar() {
const collections = useCollections();
return (
<Sidebar variant="inset" collapsible="icon">
<SidebarContent>
<HeaderButtons>
<NewNoteDialog>
<Button variant="ghost"><Plus /></Button>
</NewNoteDialog>
<NewCollectionDialog>
<Button variant="ghost"><BookPlus /></Button>
</NewCollectionDialog>
</HeaderButtons>
<SidebarGroup>
<SidebarMenu>
<NavButton href="/app/" label="Overview" icon={<Home />} />
<NavButton href="/app/search" label="Search" icon={<Search />} />
<InboxButton />
<NavButton href="/app/graph" label="Graph" icon={<Waypoints />} />
<NavButton href="/app/calendar" label="Calendar" icon={<Calendar />} />
<NavButton href="/app/todo" label="Todo" icon={<ListTodo />} />
</SidebarMenu>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Collections</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{collections.map((collection) => (<CollectionButton
key={collection.get("id")}
id={collection.get("id")}
name={collection.get("name")}
icon={collection.get("icon")}
color={collection.get("color")}
/>))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarToggle />
<NavUser />
</SidebarMenu>
</SidebarFooter>
</Sidebar>
);
}
type NavButtonProps = {
href: string;
label: string;
icon?: React.ReactNode;
children?: React.ReactNode;
}
function NavButton(props: NavButtonProps) {
const { setOpenMobile } = useSidebar();
return (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link to={props.href} onClick={() => setOpenMobile(false)}>
{props.icon}
<span>{props.label}</span>
</Link>
</SidebarMenuButton>
{props.children}
</SidebarMenuItem>
);
}
function HeaderButtons(props: { children: React.ReactNode }) {
return (
<div className={cn(
"flex flex-row justify-between",
"transition-[marign,opa] duration-200 ease-linear mt-0",
"group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:-mt-9")}>
{props.children}
</div>
);
}
type CollectionButtonProps = {
id: string;
name: string;
icon: IconName | "";
color: ColorName | "";
}
function CollectionButton(props: CollectionButtonProps) {
const icon = props.icon && icons[props.icon];
const color = props.color ? colors[props.color] : colors.white;
return (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link
to="/app/collection/$id"
params={{ id: props.id }}
className="w-full flex items-center gap-2 group/item"
>
<div className="flex items-center justify-center h-4 w-4">
{icon && <Icon path={icon.path} size={0.75} color={color.base} />}
</div>
<span
className="group-hover/item:text-[var(--hover-color)]"
style={{ "--hover-color": color.hover, }}
>
{props.name}
</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
function SidebarToggle() {
const isMobile = useIsMobile();
const { toggleSidebar } = useSidebar()
if (isMobile) {
return null;
}
return (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Button
data-sidebar="trigger"
variant="ghost"
size="icon"
onClick={toggleSidebar}
className="justify-start"
>
<PanelLeft />
<span>Toggle Sidebar</span>
</Button>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
function InboxButton() {
const notes = useCollectionNotesMetadata("");
const count = notes.length;
return (
<NavButton href="/app/inbox" label="Inbox" icon={<Inbox />}>
<SidebarMenuBadge>{count}</SidebarMenuBadge>
</NavButton>
);
}
function NavUser() {
const isMobile = useIsMobile();
const [theme, setTheme] = useTheme();
// const { data: session } = useSession();
// const user = session?.user;
const user = {
name: "Kalle Struik",
email: "kalle@kallestruik.nl"
};
const email = user?.email ?? undefined;
const name = user?.name ?? undefined
function handleSignOut() {
// // End the session
// signOut();
// // Clear all locally stored data
// localStorage.clear();
// indexedDB.databases().then((dbs) => {
// dbs.forEach((db) => {
// db.name && indexedDB.deleteDatabase(db.name);
// });
// });
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<UserAvatar user={user} className="h-8 w-8 rounded-lg" />
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{name}</span>
<span className="truncate text-xs">{email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "top" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<UserAvatar user={user} className="h-8 w-8 rounded-lg" />
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{name}</span>
<span className="truncate text-xs">{email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuLabel className="py-0">
<div className="flex flex-row place-content-between w-full items-center">
<div className="flex flex-row items-center gap-2">
<Palette className="size-4 text-muted-foreground" />
Theme
</div>
<ToggleGroup type="single" size="sm" defaultValue={theme}>
<ToggleGroupItem value="system" onClick={() => setTheme("system")}><Monitor /></ToggleGroupItem>
<ToggleGroupItem value="light" onClick={() => setTheme("light")}><Sun /></ToggleGroupItem>
<ToggleGroupItem value="dark" onClick={() => setTheme("dark")}><Moon /></ToggleGroupItem>
</ToggleGroup>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSignOut}>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}