Make more UI improvements

This commit is contained in:
Arthur Belleville 2025-04-06 18:21:25 +02:00
parent cae57e81e3
commit 0cd28d7394
No known key found for this signature in database
11 changed files with 640 additions and 337 deletions

View file

@ -48,6 +48,8 @@
"@tailwindcss/vite": "^4.0.14",
"@tanstack/react-query": "^5.69.0",
"@types/react-router-dom": "^5.3.3",
"ag-grid-community": "^33.2.1",
"ag-grid-react": "^33.2.1",
"axios": "^1.8.4",
"jwt-decode": "^4.0.0",
"react-router-dom": "^7.3.0",

View file

@ -23,6 +23,12 @@ importers:
'@types/react-router-dom':
specifier: ^5.3.3
version: 5.3.3
ag-grid-community:
specifier: ^33.2.1
version: 33.2.1
ag-grid-react:
specifier: ^33.2.1
version: 33.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
axios:
specifier: ^1.8.4
version: 1.8.4
@ -1469,6 +1475,18 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
ag-charts-types@11.2.1:
resolution: {integrity: sha512-uzN1OUEn5nCFDZ4GTNkYHpg+6hbF+NamIwUOK/aSHBRvJxJU9/sK+K1QkqYpU912mHtpAZ9x0zEddr2sw6pT2Q==}
ag-grid-community@33.2.1:
resolution: {integrity: sha512-eQVRv+x8C3+T2weBux7Y+SN6IMs4lYJjTmNSfm/OX3ANH3GsscvqRiOxoV/R+hQWL7GUXOCLPZHFzZpzSdk0xg==}
ag-grid-react@33.2.1:
resolution: {integrity: sha512-06Jo2fi90Ke/ZXM1kvcWa5MX+9DM3IKVuCgGeTrQn8PzOOccYWBvVPGwLXk1TAQ2b9kQfKwDCU5q5JAJ6+tPUg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@ -4618,6 +4636,19 @@ snapshots:
acorn@8.14.0: {}
ag-charts-types@11.2.1: {}
ag-grid-community@33.2.1:
dependencies:
ag-charts-types: 11.2.1
ag-grid-react@33.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
ag-grid-community: 33.2.1
prop-types: 15.8.1
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3

View file

@ -12,6 +12,14 @@ import { SessionProvider } from "./contexts/SessionContext";
import { OAuthSigninPage } from "./pages/oauth-signin";
import { NotFoundPage } from "./pages/NotFoundPage";
import { Layout } from "./components/Layout";
import { DevisPage } from "./pages/devis";
import { FacturesPage } from "./pages/factures";
import { PlanningPage } from "./pages/planning";
import { ChantiersPage } from "./pages/chantiers";
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
// Register all Community features
ModuleRegistry.registerModules([AllCommunityModule]);
export const App = () => {
return (
@ -25,15 +33,47 @@ export const App = () => {
)}
>
<Routes>
<Route
path="/"
element={
<Layout>
<ProtectedRoute fallback="/login" />
</Layout>
}
>
<Route index element={<TabloPage />} />
<Route path="/" element={<ProtectedRoute fallback="/login" />}>
<Route
index
element={
<Layout>
<TabloPage />
</Layout>
}
/>
<Route
path="devis"
element={
<Layout>
<DevisPage />
</Layout>
}
/>
<Route
path="factures"
element={
<Layout>
<FacturesPage />
</Layout>
}
/>
<Route
path="planning"
element={
<Layout>
<PlanningPage />
</Layout>
}
/>
<Route
path="chantiers"
element={
<Layout>
<ChantiersPage />
</Layout>
}
/>
</Route>
<Route path="login-with-oauth" element={<OAuthSigninPage />} />
<Route path="landing" element={<LandingPage />} />

View file

@ -1,16 +1,41 @@
import { ReactNode } from "react";
import { SideNavigation, HamburgerMenu } from "./NavigationBar";
import { ReactNode, useState } from "react";
import { SideNavigation } from "./NavigationBar";
import { Button } from "../ui-library/button";
import { Icon } from "../ui-library/icon";
import { MenuIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
interface LayoutProps {
children: ReactNode;
}
export function Layout({ children }: LayoutProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
return (
<div className="flex h-screen">
<HamburgerMenu />
<SideNavigation />
<main className="flex-1 overflow-auto">{children}</main>
<Button
variant="plain"
isIconOnly
className="fixed top-4 left-4 z-50 md:hidden"
onPress={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
>
<Icon>
<MenuIcon className="h-6 w-6" />
</Icon>
</Button>
<div
className={twMerge(
"fixed md:relative transition-all duration-300 z-40",
isMobileMenuOpen
? "translate-x-0"
: "-translate-x-full md:translate-x-0"
)}
>
<SideNavigation />
</div>
<main className="flex-1 overflow-auto p-4 md:p-6">{children}</main>
</div>
);
}

View file

@ -1,43 +1,45 @@
import { twMerge } from "tailwind-merge";
// import { useSession } from "../contexts/SessionContext";
import {
HomeIcon,
TableIcon,
SettingsIcon,
UserIcon,
HelpCircleIcon,
SendIcon,
Menu,
Settings2Icon,
LogOutIcon,
SearchIcon,
MenuIcon,
ChevronRightIcon,
ChevronLeftIcon,
Settings,
ConstructionIcon,
PlusIcon,
MinusIcon,
} from "lucide-react";
import { Link as RouterLink } from "react-router-dom";
import {
MenuButton,
MenuItem,
MenuItemLabel,
MenuSeparator,
MenuTrigger,
} from "../ui-library/menu";
import { MenuPopover } from "../ui-library/menu";
Separator,
Switch,
Menu,
} from "react-aria-components";
import { MenuButton, MenuItemLabel, 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 { Avatar, AvatarBadge } from "../ui-library/avatar";
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";
import { Popover } from "../ui-library/popover";
import {
AvailableIcon,
AwayIcon,
BusyIcon,
DoNotDisturbIcon,
} from "../ui-library/icons";
import { useState, useRef } from "react";
import logo from "../assets/icon.jpg";
type NavLinkItem = {
isActive?: boolean;
@ -87,256 +89,270 @@ function NavLink(props: NavLinkProps) {
: [
"font-medium",
"text-foreground/70 [&:not(:hover)>[data-ui=icon]]:text-foreground/35",
],
rest.className
]
)}
/>
>
{props.children}
</Link>
);
}
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 />
export function ControlledOpenState({ isCollapsed }: { isCollapsed: boolean }) {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef(null);
<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"
>
<>
<Button
aria-label="Settings"
onPress={() => setIsOpen(true)}
ref={ref}
className={twMerge(
"flex items-center gap-2",
isCollapsed && "justify-center"
)}
>
<Icon>
<Settings />
</Icon>
<span
className={twMerge(
"transition-all duration-300",
isCollapsed ? "opacity-0 w-0" : "opacity-100"
)}
>
Settings
</span>
</Button>
<Popover
className="min-w-56 rounded-xl"
isOpen={isOpen}
onOpenChange={setIsOpen}
triggerRef={ref}
>
<Dialog aria-label="Settings">
<div className="flex flex-col gap-2 p-3">
<div className="flex gap-4">
<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>
src="https://i.imgur.com/xIe7Wlb.png"
alt="Marissa Whitaker"
>
<AvatarBadge badge={<AvailableIcon aria-label="Available" />} />
</Avatar>
<div className="flex flex-col">
<span className="font-bold">Lisa Wilson</span>
<span className="sm:leading-4">Admin</span>
</div>
</div>
<MainNavigation />
<Separator />
<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"
<MenuTrigger>
<MenuButton
variant="plain"
className="justify-start gap-3 font-medium"
>
<AvailableIcon className="size-3" />
Available
</MenuButton>
<MenuPopover placement="end top">
<Menu>
<MenuItem>
<AvailableIcon className="size-3" />
<MenuItemLabel>Available</MenuItemLabel>
</MenuItem>
<MenuItem>
<BusyIcon className="size-3" />
<MenuItemLabel>Busy</MenuItemLabel>
</MenuItem>
<MenuItem>
<AwayIcon className="size-3" />
<MenuItemLabel>Away</MenuItemLabel>
</MenuItem>
<MenuItem>
<DoNotDisturbIcon className="size-3" />
<MenuItemLabel>Do not disturb</MenuItemLabel>
</MenuItem>
</Menu>
</MenuPopover>
</MenuTrigger>
<div
className={twMerge(
"flex items-center justify-between px-3",
"text-sm font-medium"
)}
>
<span>Notifications</span>
<Switch defaultSelected />
</div>
<div
className={twMerge(
"flex items-center justify-between px-3",
"text-sm font-medium"
)}
>
<span>Badges</span>
<Switch />
</div>
</div>
</Dialog>
</Popover>
</>
);
}
export const SideNavigation = () => {
const [isCollapsed, setIsCollapsed] = useState(false);
return (
<div
className={twMerge(
"group isolate flex flex-col overflow-y-auto bg-gray-200 dark:bg-gray-900 transition-all duration-300",
"fixed md:relative h-screen z-50",
isCollapsed ? "w-14" : "w-52",
"md:flex",
"transform md:transform-none",
isCollapsed ? "-translate-x-0" : "-translate-x-full md:translate-x-0"
)}
>
<div className="relative flex flex-col items-center px-3 py-3">
<RouterLink
to="/"
className={twMerge(
"flex flex-col items-center gap-2",
isCollapsed ? "w-full justify-center" : ""
)}
>
<img
src={logo}
alt="Logo XTablo"
className={twMerge(isCollapsed ? "w-8 h-8" : "w-15 h-15")}
/>
<h1
className={twMerge(
"text-lg font-bold transition-all duration-300 text-slate-900 dark:text-white whitespace-nowrap",
isCollapsed ? "w-0 opacity-0 h-0" : "w-auto opacity-100"
)}
>
<Avatar
src="https://i.imgur.com/xIe7Wlb.png"
alt="Marissa Whitaker"
className="size-8"
/>
XTablo
</h1>
</RouterLink>
<Button
variant="plain"
isIconOnly
onPress={() => setIsCollapsed(!isCollapsed)}
className={twMerge(
isCollapsed ? "relative" : "absolute top-2 right-2",
"size-5 p-1",
"text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100",
"transition-all duration-300",
"bg-gray-200 dark:bg-gray-900",
"rounded-full shadow-md",
"opacity-0 group-hover:opacity-100",
"hover:scale-110"
)}
>
<Icon>{isCollapsed ? <PlusIcon /> : <MinusIcon />}</Icon>
</Button>
</div>
<MainNavigation isCollapsed={isCollapsed} />
<span className="text-foreground/70 truncate font-medium">
Marissa Whitaker
</span>
</MenuButton>
<AvatarMenuPopover />
</MenuTrigger>
<div className="bg-gray-200 dark:bg-gray-900 sticky bottom-0 left-0 flex px-2 py-3">
<ControlledOpenState isCollapsed={isCollapsed} />
</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;
// };
};
export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
const navItems = [
{ path: "/", label: "Accueil", icon: <HomeIcon className="w-5 h-5" /> },
{
path: "/tablo",
label: "Tableaux",
icon: <TableIcon className="w-5 h-5" />,
path: "/",
label: "Tableau de Bord",
icon: <TableIcon className="w-4 h-4" />,
},
{
path: "/settings",
label: "Paramètres",
icon: <SettingsIcon className="w-5 h-5" />,
path: "/devis",
label: "Devis",
icon: <SettingsIcon className="w-4 h-4" />,
},
{
path: "/factures",
label: "Factures",
icon: <SettingsIcon className="w-4 h-4" />,
},
{
path: "/planning",
label: "Planning",
icon: <SettingsIcon className="w-4 h-4" />,
},
{
path: "/chantiers",
label: "Chantiers",
icon: <ConstructionIcon className="w-4 h-4" />,
},
];
return (
<nav className="flex flex-1 flex-col">
<ul className="grid gap-y-1 p-4">
<ul className="grid gap-y-0.5 px-2 py-3">
{navItems.map(({ path, label, icon }) => (
<li key={label}>
<NavLink to={path}>
<Icon>{icon}</Icon>
{label}
<NavLink>
<RouterLink to={path}>
<div className="flex items-center gap-x-2">
<Icon>{icon}</Icon>
<span
className={twMerge(
"text-sm transition-all duration-300",
isCollapsed ? "opacity-0 w-0" : "opacity-100"
)}
>
{label}
</span>
</div>
</RouterLink>
</NavLink>
</li>
))}
</ul>
<ul className="mt-auto grid gap-y-1 p-4">
<ul className="mt-auto grid gap-y-0.5 px-2 py-3">
<li>
<NavLink to="/">
<HelpCircleIcon />
Support
<NavLink>
<RouterLink to="/">
<div className="flex items-center gap-x-2">
<HelpCircleIcon className="w-4 h-4" />
<span
className={twMerge(
"text-sm transition-all duration-300",
isCollapsed ? "opacity-0 w-0" : "opacity-100"
)}
>
Support
</span>
</div>
</RouterLink>
</NavLink>
</li>
<li>
<NavLink to="/">
<SendIcon />
Feedback
<NavLink>
<RouterLink to="/">
<div className="flex items-center gap-x-2">
<SendIcon className="w-4 h-4" />
<span
className={twMerge(
"text-sm transition-all duration-300",
isCollapsed ? "opacity-0 w-0" : "opacity-100"
)}
>
Feedback
</span>
</div>
</RouterLink>
</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

@ -0,0 +1,24 @@
import { useSession } from "../contexts/SessionContext";
export const ChantiersPage = () => {
const { session } = useSession();
return (
<div className="min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Chantiers
</h1>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="container mx-auto px-4 py-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<p className="text-gray-600 dark:text-gray-300">
Gestion des chantiers
</p>
</div>
</div>
</main>
</div>
);
};

106
ui/src/pages/devis.tsx Normal file
View file

@ -0,0 +1,106 @@
import { useSession } from "../contexts/SessionContext";
import { Button } from "../ui-library/button";
import { PlusIcon } from "../ui-library/icons";
import {
AllCommunityModule,
ModuleRegistry,
themeAlpine,
themeQuartz,
} from "ag-grid-community";
import { AgGridReact } from "ag-grid-react";
import { useState } from "react";
ModuleRegistry.registerModules([AllCommunityModule]);
type Devis = {
id: number;
date: string;
client: string;
montant: number;
status: string;
};
export const DevisPage = () => {
const { session } = useSession();
const [devisData, setDevisData] = useState<Devis[]>([
{
id: 1,
date: "2024-01-01",
client: "John Doe",
montant: 1000,
status: "En attente",
},
{
id: 2,
date: "2024-01-01",
client: "John Doe",
montant: 1000,
status: "En attente",
},
{
id: 3,
date: "2024-01-01",
client: "John Doe",
montant: 1000,
status: "En attente",
},
]);
const handleNewDevis = () => {
const newDevis: Devis = {
id: devisData.length + 1,
date: new Date().toISOString().split("T")[0],
client: "Nouveau Client",
montant: 0,
status: "En attente",
};
setDevisData([...devisData, newDevis]);
};
return (
<div className="min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Devis
</h1>
<Button
variant="solid"
color="accent"
className="px-4"
aria-label="Créer un nouveau devis"
onPress={handleNewDevis}
>
<PlusIcon />
Nouveau Devis
</Button>
</div>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="container mx-auto px-4 py-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div
className="ag-theme-alpine dark:ag-theme-alpine-dark"
style={{ height: 700, width: "100%" }}
>
<AgGridReact<Devis>
rowData={devisData}
gridOptions={{
theme: themeQuartz,
}}
columnDefs={[
{ field: "id", headerName: "ID" },
{ field: "date", headerName: "Date" },
{ field: "client", headerName: "Client" },
{ field: "montant", headerName: "Montant" },
{ field: "status", headerName: "Status" },
]}
domLayout="autoHeight"
/>
</div>
</div>
</div>
</main>
</div>
);
};

24
ui/src/pages/factures.tsx Normal file
View file

@ -0,0 +1,24 @@
import { useSession } from "../contexts/SessionContext";
export const FacturesPage = () => {
const { session } = useSession();
return (
<div className="min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Factures
</h1>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="container mx-auto px-4 py-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<p className="text-gray-600 dark:text-gray-300">
Gestion des factures
</p>
</div>
</div>
</main>
</div>
);
};

24
ui/src/pages/planning.tsx Normal file
View file

@ -0,0 +1,24 @@
import { useSession } from "../contexts/SessionContext";
export const PlanningPage = () => {
const { session } = useSession();
return (
<div className="min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Planning
</h1>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="container mx-auto px-4 py-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<p className="text-gray-600 dark:text-gray-300">
Gestion du planning
</p>
</div>
</div>
</main>
</div>
);
};

View file

@ -1,6 +1,11 @@
import { Menu } from "react-aria-components";
import { MenuTrigger } from "react-aria-components";
import { MenuButton } from "../ui-library/menu";
import { MenuItem } from "react-aria-components";
import { SignOutButton } from "../components/SignOutButton";
import { useSession } from "../contexts/SessionContext";
import { MenuPopover } from "../ui-library/menu";
import { ControlledOpenState } from "../components/NavigationBar";
export const TabloPage = () => {
const { session } = useSession();
return (
@ -30,6 +35,7 @@ export const TabloPage = () => {
</p>
</div>
</div>
<ControlledOpenState />
</main>
</div>
);

View file

@ -1,3 +1,4 @@
import React from "react";
import {
Menu as RACMenu,
MenuItem as RACMenuItem,
@ -9,15 +10,15 @@ import {
MenuSectionProps as RACMenuSectionProps,
MenuSection as RACMenuSection,
Collection,
} from 'react-aria-components';
import { twMerge } from 'tailwind-merge';
import { Popover, PopoverProps } from './popover';
import { Button, ButtonProps } from './button';
import { composeTailwindRenderProps } from './utils';
import { Small } from './text';
import { CheckIcon, ChevronDownIcon, ChevronRightIcon } from './icons';
} from "react-aria-components";
import { twMerge } from "tailwind-merge";
import { Popover, PopoverProps } from "./popover";
import { Button, ButtonProps } from "./button";
import { composeTailwindRenderProps } from "./utils";
import { Small } from "./text";
import { CheckIcon, ChevronDownIcon, ChevronRightIcon } from "./icons";
export { MenuTrigger, SubmenuTrigger } from 'react-aria-components';
export { MenuTrigger, SubmenuTrigger } from "react-aria-components";
type MenuButtonProps = ButtonProps & {
buttonArrow?: React.ReactNode;
@ -25,7 +26,7 @@ type MenuButtonProps = ButtonProps & {
export function MenuButton({
buttonArrow = <ChevronDownIcon className="ms-auto" />,
variant = 'outline',
variant = "outline",
children,
...props
}: MenuButtonProps) {
@ -34,7 +35,7 @@ export function MenuButton({
{(renderProps) => {
return (
<>
{typeof children === 'function' ? children(renderProps) : children}
{typeof children === "function" ? children(renderProps) : children}
{buttonArrow}
</>
);
@ -43,29 +44,32 @@ export function MenuButton({
);
}
export function MenuPopover({ className, ...props }: PopoverProps) {
return (
<Popover
{...props}
className={composeTailwindRenderProps(
className,
twMerge(
'max-w-72',
'min-w-[max(--spacing(36),var(--trigger-width))]',
'has-[[data-ui=content]_[data-ui=icon]]:min-w-[max(--spacing(48),var(--trigger-width))]',
'has-[[data-ui=content]_kbd]:min-w-[max(--spacing(11),var(--trigger-width))]',
),
)}
/>
);
}
export const MenuPopover = React.forwardRef(
({ className, ...props }: PopoverProps, ref: React.Ref<HTMLDivElement>) => {
return (
<Popover
{...props}
ref={ref}
className={composeTailwindRenderProps(
className,
twMerge(
"max-w-72",
"min-w-[max(--spacing(36),var(--trigger-width))]",
"has-[[data-ui=content]_[data-ui=icon]]:min-w-[max(--spacing(48),var(--trigger-width))]",
"has-[[data-ui=content]_kbd]:min-w-[max(--spacing(11),var(--trigger-width))]"
)
)}
/>
);
}
);
type MenuProps<T> = RACMenuProps<T> & {
checkIconPlacement?: 'start' | 'end';
checkIconPlacement?: "start" | "end";
};
export function Menu<T extends object>({
checkIconPlacement = 'end',
checkIconPlacement = "end",
...props
}: MenuProps<T>) {
return (
@ -75,57 +79,58 @@ export function Menu<T extends object>({
className={composeTailwindRenderProps(
props.className,
twMerge(
'max-h-[inherit] overflow-auto outline-hidden',
'flex flex-col',
'p-1 has-[header]:pt-0',
"max-h-[inherit] overflow-auto outline-hidden",
"flex flex-col",
"p-1 has-[header]:pt-0",
// Header, Menu item style when has selectable items
'[&_header]:px-2',
"[&_header]:px-2",
checkIconPlacement === 'start' &&
'[&:has(:is([role=menuitemradio],[role=menuitemcheckbox]))_:is(header,[role=menuitem])]:ps-7',
checkIconPlacement === "start" &&
"[&:has(:is([role=menuitemradio],[role=menuitemcheckbox]))_:is(header,[role=menuitem])]:ps-7",
// Menu item content
'**:data-[ui=content]:flex-1',
'**:data-[ui=content]:grid',
'[&_[data-ui=content]:has([data-ui=label])]:grid-cols-[--spacing(4)_1fr_minmax(--spacing(12),max-content)]',
'**:data-[ui=content]:items-center',
'**:data-[ui=content]:gap-x-2',
"**:data-[ui=content]:flex-1",
"**:data-[ui=content]:grid",
"[&_[data-ui=content]:has([data-ui=label])]:grid-cols-[--spacing(4)_1fr_minmax(--spacing(12),max-content)]",
"**:data-[ui=content]:items-center",
"**:data-[ui=content]:gap-x-2",
"**:data-[ui=content]:rtl:text-right",
// Icon
'[&_[data-ui=content]:not(:hover)>[data-ui=icon]:not([class*=text-])]:text-muted',
'[&_[data-ui=content][data-destructive]>[data-ui=icon]]:text-destructive',
'[&_[data-ui=content][data-destructive]:not(:hover)>[data-ui=icon]]:text-destructive/75',
'[&_[data-ui=content]>[data-ui=icon]:not([class*=size-])]:size-4',
'[&_[data-ui=content]>[data-ui=icon]:first-child]:col-start-1',
"[&_[data-ui=content]:not(:hover)>[data-ui=icon]:not([class*=text-])]:text-muted",
"[&_[data-ui=content][data-destructive]>[data-ui=icon]]:text-destructive",
"[&_[data-ui=content][data-destructive]:not(:hover)>[data-ui=icon]]:text-destructive/75",
"[&_[data-ui=content]>[data-ui=icon]:not([class*=size-])]:size-4",
"[&_[data-ui=content]>[data-ui=icon]:first-child]:col-start-1",
// Label
'**:data-[ui=label]:col-span-full',
'[&:has([data-ui=icon]+[data-ui=label])_[data-ui=label]]:col-start-2',
'[&:has([data-ui=kbd])_[data-ui=label]]:-col-end-2',
'[&:has([data-ui=icon]+[data-ui=label])_[data-ui=content]:not(:has(>[data-ui=label]))]:ps-6',
"**:data-[ui=label]:col-span-full",
"[&:has([data-ui=icon]+[data-ui=label])_[data-ui=label]]:col-start-2",
"[&:has([data-ui=kbd])_[data-ui=label]]:-col-end-2",
"[&:has([data-ui=icon]+[data-ui=label])_[data-ui=content]:not(:has(>[data-ui=label]))]:ps-6",
// Kbd
'**:data-[ui=kbd]:col-span-1',
'**:data-[ui=kbd]:row-start-1',
'**:data-[ui=kbd]:col-start-3',
'**:data-[ui=kbd]:justify-self-end',
'**:data-[ui=kbd]:text-xs/6',
'[&_:not([data-destructive])>[data-ui=kbd]:not([class*=bg-])]:text-muted/75',
'[&_[data-destructive]>[data-ui=kbd]]:text-destructive',
"**:data-[ui=kbd]:col-span-1",
"**:data-[ui=kbd]:row-start-1",
"**:data-[ui=kbd]:col-start-3",
"**:data-[ui=kbd]:justify-self-end",
"**:data-[ui=kbd]:text-xs/6",
"[&_:not([data-destructive])>[data-ui=kbd]:not([class*=bg-])]:text-muted/75",
"[&_[data-destructive]>[data-ui=kbd]]:text-destructive",
// Description
'**:data-[ui=description]:col-span-full',
'[&:has([data-ui=kbd])_[data-ui=description]]:-col-end-2',
'[&:has([data-ui=icon]+[data-ui=label])_[data-ui=description]]:col-start-2',
),
"**:data-[ui=description]:col-span-full",
"[&:has([data-ui=kbd])_[data-ui=description]]:-col-end-2",
"[&:has([data-ui=icon]+[data-ui=label])_[data-ui=description]]:col-start-2"
)
)}
/>
);
}
export function SubMenu<T extends object>(
props: MenuProps<T> & { 'aria-label': string },
props: MenuProps<T> & { "aria-label": string }
) {
return <Menu {...props} />;
}
@ -134,8 +139,8 @@ export function MenuSeparator({ className }: { className?: string }) {
return (
<Separator
className={twMerge(
'border-t-border/75 my-1 w-[calc(100%-(--spacing(4)))] self-center border-t',
className,
"border-t-border/50 my-1 w-[calc(100%-(--spacing(4)))] self-center border-t",
className
)}
/>
);
@ -148,7 +153,7 @@ type MenuItemProps = RACMenuItemProps & {
export function MenuItem({ destructive, ...props }: MenuItemProps) {
const textValue =
props.textValue ||
(typeof props.children === 'string' ? props.children : undefined);
(typeof props.children === "string" ? props.children : undefined);
return (
<RACMenuItem
@ -158,16 +163,16 @@ export function MenuItem({ destructive, ...props }: MenuItemProps) {
props.className,
(className, { isFocused, isDisabled }) => {
return twMerge([
'group rounded-sm outline-hidden',
'flex items-center gap-x-1.5',
'px-2 py-2.5 sm:py-1.5',
'text-base/6 sm:text-sm/6',
isDisabled && 'opacity-50',
isFocused && 'bg-zinc-100 dark:bg-zinc-800',
destructive && 'text-destructive',
"group rounded-sm outline-hidden",
"flex items-center gap-x-1.5",
"px-2 py-2.5 sm:py-1.5",
"text-base/6 sm:text-sm/6",
isDisabled && "opacity-50",
isFocused && "bg-zinc-100 dark:bg-zinc-800",
destructive && "text-destructive",
className,
]);
},
}
)}
>
{composeRenderProps(
@ -176,11 +181,11 @@ export function MenuItem({ destructive, ...props }: MenuItemProps) {
<>
<CheckIcon
className={twMerge(
'flex h-[1lh] w-4 items-center self-start',
selectionMode == 'none'
? 'hidden'
: 'in-data-[check-icon-placement=end]:hidden',
isSelected ? 'visible' : 'invisible',
"flex h-[1lh] w-4 items-center self-start",
selectionMode == "none"
? "hidden"
: "in-data-[check-icon-placement=end]:hidden",
isSelected ? "visible" : "invisible"
)}
/>
<div
@ -191,18 +196,18 @@ export function MenuItem({ destructive, ...props }: MenuItemProps) {
</div>
<CheckIcon
className={twMerge(
'flex h-[1lh] w-4 items-center self-start',
selectionMode == 'none'
? 'hidden'
: 'in-data-[check-icon-placement=start]:hidden',
isSelected ? 'visible' : 'invisible',
"flex h-[1lh] w-4 items-center self-start",
selectionMode == "none"
? "hidden"
: "in-data-[check-icon-placement=start]:hidden",
isSelected ? "visible" : "invisible"
)}
/>
{/* Submenu indicator */}
<ChevronRightIcon className="text-muted hidden size-4 group-data-has-submenu:inline-block" />
</>
),
)
)}
</RACMenuItem>
);
@ -211,12 +216,12 @@ export function MenuItem({ destructive, ...props }: MenuItemProps) {
export function MenuItemLabel({
className,
...props
}: React.JSX.IntrinsicElements['span']) {
}: React.JSX.IntrinsicElements["span"]) {
return (
<span
slot="label"
data-ui="label"
className={twMerge('truncate', className)}
className={twMerge("truncate", className)}
{...props}
/>
);
@ -225,7 +230,7 @@ export function MenuItemLabel({
export function MenuItemDescription({
className,
...props
}: React.JSX.IntrinsicElements['span']) {
}: React.JSX.IntrinsicElements["span"]) {
return (
<Small
slot="description"
@ -248,13 +253,13 @@ export function MenuSection<T extends object>({
<RACMenuSection
{...props}
className={twMerge(
'not-first:mt-1.5',
'not-first:border-t',
'not-first:border-t-border/75',
className,
"not-first:mt-1.5",
"not-first:border-t",
"not-first:border-t-border/75",
className
)}
>
<Header className="text-muted bg-background sticky inset-0 z-10 truncate pt-2 text-xs/6">
<Header className="text-muted bg-background sticky inset-0 z-10 truncate pt-2 text-xs/6 rtl:text-right">
{props.title}
</Header>
<Collection items={props.items}>{props.children}</Collection>