From 0cd28d73949c4281f0a6c2e1e34c96df34563ec8 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 6 Apr 2025 18:21:25 +0200 Subject: [PATCH] Make more UI improvements --- ui/package.json | 2 + ui/pnpm-lock.yaml | 31 ++ ui/src/App.tsx | 58 +++- ui/src/components/Layout.tsx | 35 +- ui/src/components/NavigationBar.tsx | 480 ++++++++++++++-------------- ui/src/pages/chantiers.tsx | 24 ++ ui/src/pages/devis.tsx | 106 ++++++ ui/src/pages/factures.tsx | 24 ++ ui/src/pages/planning.tsx | 24 ++ ui/src/pages/tablo.tsx | 8 +- ui/src/ui-library/menu.tsx | 185 +++++------ 11 files changed, 640 insertions(+), 337 deletions(-) create mode 100644 ui/src/pages/chantiers.tsx create mode 100644 ui/src/pages/devis.tsx create mode 100644 ui/src/pages/factures.tsx create mode 100644 ui/src/pages/planning.tsx diff --git a/ui/package.json b/ui/package.json index ccc490d..7af6989 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 66c9ae8..c6cf44a 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -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 diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 37485d4..d9b6c22 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -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 = () => { )} > - - - - } - > - } /> + }> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> } /> } /> diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index c14f7e9..0b01f9c 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -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 (
- - -
{children}
+ + +
+ +
+ +
{children}
); } diff --git a/ui/src/components/NavigationBar.tsx b/ui/src/components/NavigationBar.tsx index 1cb1f7b..3dc802d 100644 --- a/ui/src/components/NavigationBar.tsx +++ b/ui/src/components/NavigationBar.tsx @@ -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} + ); } -function AvatarMenuPopover() { - return ( - - - Clear status - - - - - - My profile - - - - - - Settings - - +export function ControlledOpenState({ isCollapsed }: { isCollapsed: boolean }) { + const [isOpen, setIsOpen] = useState(false); + const ref = useRef(null); - - - - - Sign out - - - - ); -} - -export function SideNavigation() { return ( -
-
-
- - + <> + + + +
+
- Acme, Inc - - - - Item 1 - Item 2 - - - -
-
+ src="https://i.imgur.com/xIe7Wlb.png" + alt="Marissa Whitaker" + > + } /> + +
+ Lisa Wilson + Admin +
+
- + -
- - + + + Available + + + + + + Available + + + + Busy + + + + Away + + + + Do not disturb + + + + + +
+ Notifications + +
+
+ Badges + +
+
+ + + + ); +} + +export const SideNavigation = () => { + const [isCollapsed, setIsCollapsed] = useState(false); + + return ( +
+
+ + Logo XTablo +

- + XTablo +

+
+ +
+ - - Marissa Whitaker - - - - +
+
); -} - -export function HamburgerMenu() { - return ( -
- - - - - -
- - - - Acme, Inc - - - - Item 1 - Item 2 - - - -
-
- - - - - -
-
-
- -
- - - - - - - -
-
- ); -} - -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: }, { - path: "/tablo", - label: "Tableaux", - icon: , + path: "/", + label: "Tableau de Bord", + icon: , }, { - path: "/settings", - label: "Paramètres", - icon: , + path: "/devis", + label: "Devis", + icon: , + }, + { + path: "/factures", + label: "Factures", + icon: , + }, + { + path: "/planning", + label: "Planning", + icon: , + }, + { + path: "/chantiers", + label: "Chantiers", + icon: , }, ]; return ( ); - - // return ( - //
- //
- //

Xtablo

- //
- - // - - // {session && ( - //
- //
- //
- // - //
- //
- //

- // {session.user?.email} - //

- //

- // {session.user?.email?.split("@")[0]} - //

- //
- //
- //
- // )} - //
- // ); - // } } diff --git a/ui/src/pages/chantiers.tsx b/ui/src/pages/chantiers.tsx new file mode 100644 index 0000000..b080e63 --- /dev/null +++ b/ui/src/pages/chantiers.tsx @@ -0,0 +1,24 @@ +import { useSession } from "../contexts/SessionContext"; + +export const ChantiersPage = () => { + const { session } = useSession(); + + return ( +
+
+

+ Chantiers +

+
+
+
+
+

+ Gestion des chantiers +

+
+
+
+
+ ); +}; diff --git a/ui/src/pages/devis.tsx b/ui/src/pages/devis.tsx new file mode 100644 index 0000000..03479a7 --- /dev/null +++ b/ui/src/pages/devis.tsx @@ -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([ + { + 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 ( +
+
+
+

+ 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" + /> +
+
+
+
+
+ ); +}; diff --git a/ui/src/pages/factures.tsx b/ui/src/pages/factures.tsx new file mode 100644 index 0000000..f7964af --- /dev/null +++ b/ui/src/pages/factures.tsx @@ -0,0 +1,24 @@ +import { useSession } from "../contexts/SessionContext"; + +export const FacturesPage = () => { + const { session } = useSession(); + + return ( +
+
+

+ Factures +

+
+
+
+
+

+ Gestion des factures +

+
+
+
+
+ ); +}; diff --git a/ui/src/pages/planning.tsx b/ui/src/pages/planning.tsx new file mode 100644 index 0000000..50de73f --- /dev/null +++ b/ui/src/pages/planning.tsx @@ -0,0 +1,24 @@ +import { useSession } from "../contexts/SessionContext"; + +export const PlanningPage = () => { + const { session } = useSession(); + + return ( +
+
+

+ Planning +

+
+
+
+
+

+ Gestion du planning +

+
+
+
+
+ ); +}; diff --git a/ui/src/pages/tablo.tsx b/ui/src/pages/tablo.tsx index 1f62484..9e62386 100644 --- a/ui/src/pages/tablo.tsx +++ b/ui/src/pages/tablo.tsx @@ -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 = () => {

+ ); diff --git a/ui/src/ui-library/menu.tsx b/ui/src/ui-library/menu.tsx index 2e148ff..c3882f2 100644 --- a/ui/src/ui-library/menu.tsx +++ b/ui/src/ui-library/menu.tsx @@ -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 = , - 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 ( - - ); -} +export const MenuPopover = React.forwardRef( + ({ className, ...props }: PopoverProps, ref: React.Ref) => { + return ( + + ); + } +); type MenuProps = RACMenuProps & { - checkIconPlacement?: 'start' | 'end'; + checkIconPlacement?: "start" | "end"; }; export function Menu({ - checkIconPlacement = 'end', + checkIconPlacement = "end", ...props }: MenuProps) { return ( @@ -75,57 +79,58 @@ export function Menu({ 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( - props: MenuProps & { 'aria-label': string }, + props: MenuProps & { "aria-label": string } ) { return ; } @@ -134,8 +139,8 @@ export function MenuSeparator({ className }: { className?: string }) { return ( ); @@ -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 ( { 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) { <>
{/* Submenu indicator */} - ), + ) )} ); @@ -211,12 +216,12 @@ export function MenuItem({ destructive, ...props }: MenuItemProps) { export function MenuItemLabel({ className, ...props -}: React.JSX.IntrinsicElements['span']) { +}: React.JSX.IntrinsicElements["span"]) { return ( ); @@ -225,7 +230,7 @@ export function MenuItemLabel({ export function MenuItemDescription({ className, ...props -}: React.JSX.IntrinsicElements['span']) { +}: React.JSX.IntrinsicElements["span"]) { return ( ({ -
+
{props.title}
{props.children}