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 0000000..22ffb87 Binary files /dev/null and b/frontend/public/imgs/dkLst.png differ diff --git a/frontend/public/lst.ico b/frontend/public/lst.ico new file mode 100644 index 0000000..cfd8d4c Binary files /dev/null and b/frontend/public/lst.ico differ diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..4350364 --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,38 @@ +import { Link } from "@tanstack/react-router"; +import { SidebarIcon } from "lucide-react"; +import { ModeToggle } from "./mode-toggle"; +import { Button } from "./ui/button"; +import { useSidebar } from "./ui/sidebar"; + +export default function Header() { + const { toggleSidebar } = useSidebar(); + return ( +
+
+
+ + 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} - - ); -}