Add basic navigation

This commit is contained in:
Arthur Belleville 2025-04-03 08:56:41 +02:00
parent 5da5af2812
commit cae57e81e3
No known key found for this signature in database
4 changed files with 367 additions and 3 deletions

View file

@ -11,6 +11,7 @@ import { TabloPage } from "./pages/tablo";
import { SessionProvider } from "./contexts/SessionContext";
import { OAuthSigninPage } from "./pages/oauth-signin";
import { NotFoundPage } from "./pages/NotFoundPage";
import { Layout } from "./components/Layout";
export const App = () => {
return (
@ -24,7 +25,14 @@ export const App = () => {
)}
>
<Routes>
<Route path="/" element={<ProtectedRoute fallback="/login" />}>
<Route
path="/"
element={
<Layout>
<ProtectedRoute fallback="/login" />
</Layout>
}
>
<Route index element={<TabloPage />} />
</Route>
<Route path="login-with-oauth" element={<OAuthSigninPage />} />

View file

@ -0,0 +1,16 @@
import { ReactNode } from "react";
import { SideNavigation, HamburgerMenu } from "./NavigationBar";
interface LayoutProps {
children: ReactNode;
}
export function Layout({ children }: LayoutProps) {
return (
<div className="flex h-screen">
<HamburgerMenu />
<SideNavigation />
<main className="flex-1 overflow-auto">{children}</main>
</div>
);
}

View file

@ -0,0 +1,342 @@
import { twMerge } from "tailwind-merge";
// import { useSession } from "../contexts/SessionContext";
import {
HomeIcon,
TableIcon,
SettingsIcon,
UserIcon,
HelpCircleIcon,
SendIcon,
Menu,
Settings2Icon,
LogOutIcon,
SearchIcon,
MenuIcon,
ChevronRightIcon,
} from "lucide-react";
import {
MenuButton,
MenuItem,
MenuItemLabel,
MenuSeparator,
MenuTrigger,
} from "../ui-library/menu";
import { MenuPopover } from "../ui-library/menu";
import { Link } from "../ui-library/link";
import { Icon } from "../ui-library/icon";
import { Avatar } from "../ui-library/avatar";
import { DialogHeader } from "../ui-library/dialog";
import { Dialog } from "../ui-library/dialog";
import { Button } from "../ui-library/button";
import { Modal } from "../ui-library/modal";
import { DialogTrigger } from "../ui-library/dialog";
import { DialogCloseButton } from "../ui-library/dialog";
import { DialogBody } from "../ui-library/dialog";
import {
DisclosurePanel,
DisclosureControl,
Disclosure,
} from "../ui-library/disclosure";
import { LinkProps } from "react-aria-components";
type NavLinkItem = {
isActive?: boolean;
} & LinkProps;
type NavLinkProps = NavLinkItem | { title: string; items: NavLinkItem[] };
function NavLink(props: NavLinkProps) {
if ("items" in props) {
return (
<Disclosure defaultExpanded>
<DisclosureControl className="group/control [&:not(:hover)]:text-foreground/50 mt-3 w-full ps-2.5 text-xs/6 font-semibold">
{props.title}{" "}
<ChevronRightIcon className="ms-auto hidden size-4 transition-all group-hover/control:flex group-aria-expanded:rotate-90" />
</DisclosureControl>
<DisclosurePanel>
<ul className="grid gap-y-1">
{props.items.map((item) => (
<li key={item.href}>
<NavLink {...item} />
</li>
))}
</ul>
</DisclosurePanel>
</Disclosure>
);
}
const { isActive, ...rest } = props;
return (
<Link
{...rest}
className={twMerge(
"group w-full gap-x-3 overflow-hidden rounded-md px-2.5 py-1 text-nowrap hover:bg-zinc-100/50 hover:no-underline focus-visible:outline-offset-0 dark:hover:bg-zinc-800 [&>[data-ui=icon]:not([class*=size-])]:size-4.5",
"[&>[data-ui=notification-badge]]:text-foreground/70",
"[&>[data-ui=notification-badge]]:rounded-md",
"[&>[data-ui=notification-badge]]:top-1/2",
"[&>[data-ui=notification-badge]]:right-1",
"[&>[data-ui=notification-badge]]:-translate-y-1/2",
"[&>[data-ui=notification-badge]]:bg-zinc-200/40",
"dark:[&>[data-ui=notification-badge]]:bg-zinc-900",
"[&>[data-ui=notification-badge]]:p-3",
"[&>[data-ui=notification-badge]]:text-xs/6",
"[&>[data-ui=notification-badge]]:font-semibold",
isActive
? "bg-zinc-100/50 font-semibold dark:bg-zinc-900 [&>[data-ui=notification-badge]]:bg-transparent"
: [
"font-medium",
"text-foreground/70 [&:not(:hover)>[data-ui=icon]]:text-foreground/35",
],
rest.className
)}
/>
);
}
function AvatarMenuPopover() {
return (
<MenuPopover placement="top left" className="min-w-64">
<Menu>
<MenuItem>Clear status</MenuItem>
<MenuSeparator />
<MenuItem>
<Icon>
<UserIcon />
</Icon>
<MenuItemLabel>My profile</MenuItemLabel>
</MenuItem>
<MenuItem>
<Icon>
<Settings2Icon />
</Icon>
<MenuItemLabel>Settings</MenuItemLabel>
</MenuItem>
<MenuSeparator />
<MenuItem>
<Icon>
<LogOutIcon />
</Icon>
<MenuItemLabel>Sign out</MenuItemLabel>
</MenuItem>
</Menu>
</MenuPopover>
);
}
export function SideNavigation() {
return (
<div className="group isolate hidden w-64 flex-col overflow-y-auto md:flex">
<div className="bg-background sticky top-0 left-0 z-10 flex items-center justify-between gap-x-2.5">
<div className="flex flex-1 items-center overflow-hidden px-4 pt-4 pb-2">
<MenuTrigger>
<MenuButton
variant="outline"
className="flex-1 gap-x-2.5 overflow-hidden rounded-lg font-semibold sm:px-1.5"
>
<Avatar
alt="Acme"
className="size-6 [--border-radius:0.25rem]"
// fallbackBackground="black"
/>
<span className="truncate"> Acme, Inc</span>
</MenuButton>
<MenuPopover placement="bottom left">
<Menu>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
</Menu>
</MenuPopover>
</MenuTrigger>
</div>
</div>
<MainNavigation />
<div className="bg-background sticky bottom-0 left-0 flex px-2 py-4">
<MenuTrigger>
<MenuButton
variant="plain"
className="flex-1 justify-start overflow-hidden font-normal"
>
<Avatar
src="https://i.imgur.com/xIe7Wlb.png"
alt="Marissa Whitaker"
className="size-8"
/>
<span className="text-foreground/70 truncate font-medium">
Marissa Whitaker
</span>
</MenuButton>
<AvatarMenuPopover />
</MenuTrigger>
</div>
</div>
);
}
export function HamburgerMenu() {
return (
<header className="sticky top-0 left-0 flex h-14 items-center px-4 md:hidden">
<DialogTrigger>
<Button variant="plain" isIconOnly className="text-muted lg:hidden">
<Icon aria-label="Open Navigation">
<MenuIcon />
</Icon>
</Button>
<Modal drawer size="xs" isDismissable>
<Dialog className="h-full">
<DialogHeader className="p-0 pt-16">
<div className="flex flex-1 gap-x-2 px-2.5 pb-3">
<MenuTrigger>
<MenuButton
variant="outline"
className="flex-1 gap-x-2.5 overflow-hidden rounded-lg font-semibold sm:px-1.5"
>
<Avatar
alt="Acme"
className="size-6 [--border-radius:0.25rem]"
// fallbackBackground="black"
/>
<span className="truncate"> Acme, Inc</span>
</MenuButton>
<MenuPopover placement="bottom left">
<Menu>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
</Menu>
</MenuPopover>
</MenuTrigger>
</div>
</DialogHeader>
<DialogCloseButton />
<DialogBody className="px-0">
<MainNavigation />
</DialogBody>
</Dialog>
</Modal>
</DialogTrigger>
<div className="ml-auto flex items-center gap-4 px-2">
<Button isIconOnly variant="plain">
<Icon aria-label="Search">
<SearchIcon />
</Icon>
</Button>
<MenuTrigger>
<MenuButton variant="plain" buttonArrow={null}>
<Avatar
className="size-8"
src="https://i.imgur.com/xIe7Wlb.png"
alt="Marissa Whitaker"
/>
</MenuButton>
<AvatarMenuPopover />
</MenuTrigger>
</div>
</header>
);
}
export function MainNavigation() {
// const location = useLocation();
// const { session } = useSession();
// const isActive = (path: string) => {
// return location.pathname === path;
// };
const navItems = [
{ path: "/", label: "Accueil", icon: <HomeIcon className="w-5 h-5" /> },
{
path: "/tablo",
label: "Tableaux",
icon: <TableIcon className="w-5 h-5" />,
},
{
path: "/settings",
label: "Paramètres",
icon: <SettingsIcon className="w-5 h-5" />,
},
];
return (
<nav className="flex flex-1 flex-col">
<ul className="grid gap-y-1 p-4">
{navItems.map(({ path, label, icon }) => (
<li key={label}>
<NavLink to={path}>
<Icon>{icon}</Icon>
{label}
</NavLink>
</li>
))}
</ul>
<ul className="mt-auto grid gap-y-1 p-4">
<li>
<NavLink to="/">
<HelpCircleIcon />
Support
</NavLink>
</li>
<li>
<NavLink to="/">
<SendIcon />
Feedback
</NavLink>
</li>
</ul>
</nav>
);
// return (
// <div className="flex flex-col h-screen w-64 bg-white dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700">
// <div className="p-4">
// <h1 className="text-xl font-bold text-emerald-600">Xtablo</h1>
// </div>
// <nav className="flex-1 px-2 py-4">
// <ul className="space-y-1">
// {navItems.map((item) => (
// <li key={item.path}>
// <Link
// to={item.path}
// className={twMerge(
// "flex items-center px-3 py-2 rounded-md text-sm font-medium",
// isActive(item.path)
// ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
// : "text-slate-700 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700/50"
// )}
// >
// <span className="mr-3">{item.icon}</span>
// {item.label}
// </Link>
// </li>
// ))}
// </ul>
// </nav>
// {session && (
// <div className="p-4 border-t border-slate-200 dark:border-slate-700">
// <div className="flex items-center mb-4">
// <div className="w-8 h-8 rounded-full bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center mr-3">
// <UserIcon className="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
// </div>
// <div>
// <p className="text-sm font-medium text-slate-900 dark:text-white">
// {session.user?.email}
// </p>
// <p className="text-xs text-slate-500 dark:text-slate-400">
// {session.user?.email?.split("@")[0]}
// </p>
// </div>
// </div>
// </div>
// )}
// </div>
// );
// }
}

View file

@ -1,12 +1,10 @@
import { SignOutButton } from "../components/SignOutButton";
import { useSession } from "../contexts/SessionContext";
import { Header } from "../components/header";
export const TabloPage = () => {
const { session } = useSession();
return (
<div className="min-h-screen">
<Header />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Tablo