Add basic navigation
This commit is contained in:
parent
5da5af2812
commit
cae57e81e3
4 changed files with 367 additions and 3 deletions
|
|
@ -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 />} />
|
||||
|
|
|
|||
16
ui/src/components/Layout.tsx
Normal file
16
ui/src/components/Layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
342
ui/src/components/NavigationBar.tsx
Normal file
342
ui/src/components/NavigationBar.tsx
Normal 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>
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue