From 2846b9cb0d288ada74573c2b90652eb19bd5850f Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Mon, 16 Mar 2026 20:59:05 -0500 Subject: [PATCH] logs route behind protected route and menu --- backend/auth/register.route.ts | 6 +- brunoApi/auth/Register.bru | 3 +- brunoApi/environments/lstv3.bru | 2 +- frontend/package-lock.json | 69 +- frontend/package.json | 5 +- frontend/public/imgs/dkLst.png | Bin 0 -> 23064 bytes frontend/public/lst.ico | Bin 0 -> 22454 bytes frontend/src/components/Header.tsx | 38 + frontend/src/components/Sidebar/AdminBar.tsx | 69 ++ frontend/src/components/Sidebar/sidebar.tsx | 30 + frontend/src/components/mode-toggle.tsx | 36 + frontend/src/components/ui/button.tsx | 113 ++- frontend/src/components/ui/dropdown-menu.tsx | 267 +++++++ frontend/src/components/ui/input.tsx | 21 + frontend/src/components/ui/separator.tsx | 26 + frontend/src/components/ui/sheet.tsx | 143 ++++ frontend/src/components/ui/sidebar.tsx | 725 +++++++++++++++++++ frontend/src/components/ui/skeleton.tsx | 13 + frontend/src/components/ui/sonner.tsx | 43 ++ frontend/src/components/ui/tooltip.tsx | 55 ++ frontend/src/hooks/use-mobile.ts | 19 + frontend/src/lib/auth-client.ts | 13 +- frontend/src/lib/theme-provider.tsx | 73 ++ frontend/src/routeTree.gen.ts | 24 +- frontend/src/routes/__root.tsx | 43 +- frontend/src/routes/admin/logs.tsx | 144 ++++ frontend/src/routes/index.tsx | 144 +--- 27 files changed, 1898 insertions(+), 226 deletions(-) create mode 100644 frontend/public/imgs/dkLst.png create mode 100644 frontend/public/lst.ico create mode 100644 frontend/src/components/Header.tsx create mode 100644 frontend/src/components/Sidebar/AdminBar.tsx create mode 100644 frontend/src/components/Sidebar/sidebar.tsx create mode 100644 frontend/src/components/mode-toggle.tsx create mode 100644 frontend/src/components/ui/dropdown-menu.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/separator.tsx create mode 100644 frontend/src/components/ui/sheet.tsx create mode 100644 frontend/src/components/ui/sidebar.tsx create mode 100644 frontend/src/components/ui/skeleton.tsx create mode 100644 frontend/src/components/ui/sonner.tsx create mode 100644 frontend/src/components/ui/tooltip.tsx create mode 100644 frontend/src/hooks/use-mobile.ts create mode 100644 frontend/src/lib/theme-provider.tsx create mode 100644 frontend/src/routes/admin/logs.tsx diff --git a/backend/auth/register.route.ts b/backend/auth/register.route.ts index 00cec0b..6d1cf13 100644 --- a/backend/auth/register.route.ts +++ b/backend/auth/register.route.ts @@ -81,7 +81,7 @@ r.post("/", async (req, res) => { // details: flattened, // }); - apiReturn(res, { + return apiReturn(res, { success: false, level: "error", //connect.success ? "info" : "error", module: "routes", @@ -93,7 +93,7 @@ r.post("/", async (req, res) => { } if (err instanceof APIError) { - apiReturn(res, { + return apiReturn(res, { success: false, level: "error", //connect.success ? "info" : "error", module: "routes", @@ -104,7 +104,7 @@ r.post("/", async (req, res) => { }); } - apiReturn(res, { + return apiReturn(res, { success: false, level: "error", //connect.success ? "info" : "error", module: "routes", diff --git a/brunoApi/auth/Register.bru b/brunoApi/auth/Register.bru index 19d34dd..133ae2d 100644 --- a/brunoApi/auth/Register.bru +++ b/brunoApi/auth/Register.bru @@ -5,13 +5,14 @@ meta { } post { - url: {{url}}/authentication/register + url: {{url}}/api/authentication/register body: json auth: inherit } body:json { { + "name":"Blake", // option when in the frontend as we will pass over as username if not added "username": "matthes01", "email": "blake.matthes@alpla.com", "password": "nova0511" diff --git a/brunoApi/environments/lstv3.bru b/brunoApi/environments/lstv3.bru index ea6d279..b2e9db9 100644 --- a/brunoApi/environments/lstv3.bru +++ b/brunoApi/environments/lstv3.bru @@ -1,3 +1,3 @@ vars { - url: http://localhost:3000/lst + url: http://uslim1vms006:3100/lst } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a724e79..c72188f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,14 +18,17 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", + "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "^19.1.1", "react-dom": "^19.1.1", "shadcn": "^4.0.8", "socket.io-client": "^4.8.3", + "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -4173,6 +4176,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/router-generator/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@tanstack/router-plugin": { "version": "1.166.12", "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.166.12.tgz", @@ -4226,6 +4239,16 @@ } } }, + "node_modules/@tanstack/router-plugin/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@tanstack/router-utils": { "version": "1.161.6", "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.161.6.tgz", @@ -5182,15 +5205,6 @@ } } }, - "node_modules/better-auth/node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -8203,6 +8217,16 @@ "node": ">= 0.6" } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -9352,6 +9376,15 @@ "shadcn": "dist/index.js" } }, + "node_modules/shadcn/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9491,6 +9524,16 @@ "node": ">=10.0.0" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -10490,9 +10533,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/frontend/package.json b/frontend/package.json index 5b401bc..a783ef0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,14 +20,17 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", + "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "^19.1.1", "react-dom": "^19.1.1", "shadcn": "^4.0.8", "socket.io-client": "^4.8.3", + "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^9.36.0", diff --git a/frontend/public/imgs/dkLst.png b/frontend/public/imgs/dkLst.png new file mode 100644 index 0000000000000000000000000000000000000000..22ffb87c99ff02fe176ed00599f71d2bf4af07de GIT binary patch literal 23064 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 b|I7=XeTmfSoPP~?)H8#ptDnm{r-UW|oQjZx literal 0 HcmV?d00001 diff --git a/frontend/public/lst.ico b/frontend/public/lst.ico new file mode 100644 index 0000000000000000000000000000000000000000..cfd8d4c1abeb1ada4b979ce845703f0f54fb5b89 GIT binary patch literal 22454 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
    Mqcyf2kT8<4K7Gkl`%&juN>lGN&CURqII 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 +
    +
    + + Home + +
    + +
    +
    + Logistics Support Tool +
    +
    + +
    + + ); +} diff --git a/frontend/src/components/Sidebar/AdminBar.tsx b/frontend/src/components/Sidebar/AdminBar.tsx new file mode 100644 index 0000000..0d9f3d5 --- /dev/null +++ b/frontend/src/components/Sidebar/AdminBar.tsx @@ -0,0 +1,69 @@ +import { Link } from "@tanstack/react-router"; +import { Logs } from "lucide-react"; + +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "../ui/sidebar"; + +export default function AdminSidebar() { + const { setOpen } = useSidebar(); + const items = [ + // { + // title: "Users", + // url: "/admin/users", + // icon: User, + // role: ["systemAdmin", "admin"], + // module: "admin", + // active: true, + // }, + { + title: "Logs", + url: "/admin/logs", + icon: Logs, + role: ["systemAdmin", "admin"], + module: "admin", + active: true, + }, + // { + // title: "Modules", + // url: "/admin/modules", + // icon: Settings, + // role: ["systemAdmin", "admin"], + // module: "admin", + // active: true, + // }, + // { + // title: "Servers", + // url: "/admin/servers", + // icon: Server, + // role: ["systemAdmin", "admin"], + // module: "admin", + // active: true, + // }, + ]; + return ( + + Admin + + + {items.map((item) => ( + + + setOpen(false)}> + + {item.title} + + + + ))} + + + + ); +} diff --git a/frontend/src/components/Sidebar/sidebar.tsx b/frontend/src/components/Sidebar/sidebar.tsx new file mode 100644 index 0000000..584e5c1 --- /dev/null +++ b/frontend/src/components/Sidebar/sidebar.tsx @@ -0,0 +1,30 @@ +import { + Sidebar, + SidebarContent, + SidebarHeader, + SidebarMenu, + SidebarMenuItem, +} from "@/components/ui/sidebar"; +import { useSession } from "@/lib/auth-client"; +import AdminSidebar from "./AdminBar"; + +export function AppSidebar() { + const { data: session } = useSession(); + return ( + + + + + + {session && session.user.role === "admin" && } + + + + + + ); +} diff --git a/frontend/src/components/mode-toggle.tsx b/frontend/src/components/mode-toggle.tsx new file mode 100644 index 0000000..14216f0 --- /dev/null +++ b/frontend/src/components/mode-toggle.tsx @@ -0,0 +1,36 @@ +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "@/lib/theme-provider"; +import { Button } from "./ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index c88ffd6..aab525f 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -1,67 +1,64 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" +import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; +import type * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", - outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", - destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", - icon: "size-8", - "icon-xs": - "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", - "icon-sm": - "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", - "icon-lg": "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); function Button({ - className, - variant = "default", - size = "default", - asChild = false, - ...props + className, + variant = "default", + size = "default", + asChild = false, + ...props }: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean - }) { - const Comp = asChild ? Slot.Root : "button" + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot.Root : "button"; - return ( - - ) + return ( + + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/frontend/src/components/ui/dropdown-menu.tsx b/frontend/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..a4695ec --- /dev/null +++ b/frontend/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,267 @@ +import * as React from "react" +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { CheckIcon, ChevronRightIcon } from "lucide-react" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + align = "start", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..c99a522 --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/frontend/src/components/ui/separator.tsx b/frontend/src/components/ui/separator.tsx new file mode 100644 index 0000000..4d9fe7a --- /dev/null +++ b/frontend/src/components/ui/separator.tsx @@ -0,0 +1,26 @@ +import { Separator as SeparatorPrimitive } from "radix-ui"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Separator }; diff --git a/frontend/src/components/ui/sheet.tsx b/frontend/src/components/ui/sheet.tsx new file mode 100644 index 0000000..0d5f5f1 --- /dev/null +++ b/frontend/src/components/ui/sheet.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { XIcon } from "lucide-react"; +import { Dialog as SheetPrimitive } from "radix-ui"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Sheet({ ...props }: React.ComponentProps) { + return ; +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SheetContent({ + className, + children, + side = "right", + showCloseButton = true, + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left"; + showCloseButton?: boolean; +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ); +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ); +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +}; diff --git a/frontend/src/components/ui/sidebar.tsx b/frontend/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..02c3144 --- /dev/null +++ b/frontend/src/components/ui/sidebar.tsx @@ -0,0 +1,725 @@ +"use client"; + +import { cva, type VariantProps } from "class-variance-authority"; +import { PanelLeftIcon } from "lucide-react"; +import { Slot } from "radix-ui"; +import * as React from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { cn } from "@/lib/utils"; + +const SIDEBAR_COOKIE_NAME = "sidebar_state"; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = "16rem"; +const SIDEBAR_WIDTH_MOBILE = "18rem"; +const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_KEYBOARD_SHORTCUT = "b"; + +type SidebarContextProps = { + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; + +const SidebarContext = React.createContext(null); + +function useSidebar() { + const context = React.useContext(SidebarContext); + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider."); + } + + return context; +} + +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<"div"> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}) { + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value; + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, + [setOpenProp, open], + ); + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [toggleSidebar]); + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed"; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], + ); + + return ( + + +
    + {children} +
    +
    +
    + ); +} + +function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props +}: React.ComponentProps<"div"> & { + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + + if (collapsible === "none") { + return ( +
    + {children} +
    + ); + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
    {children}
    +
    +
    + ); + } + + return ( +
    + {/* This is what handles the sidebar gap on desktop */} +
    + +
    + ); +} + +function SidebarTrigger({ + className, + onClick, + ...props +}: React.ComponentProps) { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +} + +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar(); + + return ( + - return (

    Welcome Home!

    @@ -86,80 +56,6 @@ function Index() {
    )} - - {/* Log Table */} -
    - - - - - - - - - - - - - - - {logs.length === 0 ? ( - - - - ) : ( - logs.map((log, i) => ( - - - - - - - - - - - )) - )} - -
    TimeLevelModuleHostPIDMessagelogIdremove
    - No logs yet — join the room to start receiving -
    - {new Date(log.createdAt).toLocaleTimeString()} - - - {log.module}{log.hostname}{log.pid}{log.message}{log.id} - -
    -
    ); } - -function LevelBadge({ level }: { level: number }) { - const config: Record = { - 10: { label: "TRACE", className: "bg-gray-100 text-gray-600" }, - 20: { label: "DEBUG", className: "bg-blue-100 text-blue-700" }, - 30: { label: "INFO", className: "bg-green-100 text-green-700" }, - 40: { label: "WARN", className: "bg-yellow-100 text-yellow-700" }, - 50: { label: "ERROR", className: "bg-red-100 text-red-700" }, - 60: { label: "FATAL", className: "bg-purple-100 text-purple-700" }, - }; - - const { label, className } = config[level] ?? { - label: String(level), - className: "bg-gray-100", - }; - - return ( - - {label} - - ); -}