diff --git a/frontend/package.json b/frontend/package.json index 4da93fe..810257b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,45 +1,50 @@ { - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "bunx --bun vite", - "build": "vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@antfu/ni": "^23.3.1", - "@radix-ui/react-slot": "^1.1.2", - "@tailwindcss/vite": "^4.0.6", - "@tanstack/react-query": "^5.66.5", - "@tanstack/react-router": "^1.106.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.475.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "shadcn": "^2.4.0-canary.6", - "tailwind-merge": "^3.0.1", - "tailwindcss": "^4.0.6", - "tailwindcss-animate": "^1.0.7", - "zustand": "^5.0.3" - }, - "devDependencies": { - "@eslint/js": "^9.19.0", - "@tanstack/router-devtools": "^1.106.0", - "@tanstack/router-plugin": "^1.106.0", - "@types/node": "^22.13.4", - "@types/react": "^19.0.8", - "@types/react-dom": "^19.0.3", - "@vitejs/plugin-react-swc": "^3.5.0", - "eslint": "^9.19.0", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.18", - "globals": "^15.14.0", - "typescript": "~5.7.2", - "typescript-eslint": "^8.22.0", - "vite": "^6.1.0" - } + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "bun --bun vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@antfu/ni": "^23.3.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.8", + "@tailwindcss/vite": "^4.0.6", + "@tanstack/react-query": "^5.66.5", + "@tanstack/react-router": "^1.106.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "js-cookie": "^3.0.5", + "lucide-react": "^0.475.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "shadcn": "^2.4.0-canary.6", + "tailwind-merge": "^3.0.1", + "tailwindcss": "^4.0.6", + "tailwindcss-animate": "^1.0.7", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "@tanstack/router-devtools": "^1.106.0", + "@tanstack/router-plugin": "^1.106.0", + "@types/node": "^22.13.4", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^9.19.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.22.0", + "vite": "^6.1.0" + } } diff --git a/frontend/public/imgs/ExampleOrder.png b/frontend/public/imgs/ExampleOrder.png new file mode 100644 index 0000000..3fc75ba Binary files /dev/null and b/frontend/public/imgs/ExampleOrder.png differ diff --git a/frontend/public/imgs/card_ppo_dark.png b/frontend/public/imgs/card_ppo_dark.png new file mode 100644 index 0000000..c6f0ace Binary files /dev/null and b/frontend/public/imgs/card_ppo_dark.png differ diff --git a/frontend/public/imgs/card_ppo_light.png b/frontend/public/imgs/card_ppo_light.png new file mode 100644 index 0000000..1c0ab22 Binary files /dev/null and b/frontend/public/imgs/card_ppo_light.png differ 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/imgs/exampleforecast.png b/frontend/public/imgs/exampleforecast.png new file mode 100644 index 0000000..dc7a2a1 Binary files /dev/null and b/frontend/public/imgs/exampleforecast.png differ diff --git a/frontend/public/imgs/ltLst.png b/frontend/public/imgs/ltLst.png new file mode 100644 index 0000000..cfd8d4c Binary files /dev/null and b/frontend/public/imgs/ltLst.png differ diff --git a/frontend/public/imgs/web-app-manifest-192x192.png b/frontend/public/imgs/web-app-manifest-192x192.png new file mode 100644 index 0000000..ea9a337 Binary files /dev/null and b/frontend/public/imgs/web-app-manifest-192x192.png differ diff --git a/frontend/public/imgs/web-app-manifest-512x512.png b/frontend/public/imgs/web-app-manifest-512x512.png new file mode 100644 index 0000000..61e9cee Binary files /dev/null and b/frontend/public/imgs/web-app-manifest-512x512.png differ diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx index 51772bd..cbe233f 100644 --- a/frontend/src/components/auth/LoginForm.tsx +++ b/frontend/src/components/auth/LoginForm.tsx @@ -33,7 +33,7 @@ const LoginForm = () => { // Optionally store user info // localStorage.setItem("user", JSON.stringify(user)); - setSession(data.data.token); + setSession(data.data.user, data.data.token); // Refetch the session data to reflect the logged-in state queryClient.invalidateQueries(); diff --git a/frontend/src/components/layout/lst-sidebar.tsx b/frontend/src/components/layout/lst-sidebar.tsx new file mode 100644 index 0000000..495d7f3 --- /dev/null +++ b/frontend/src/components/layout/lst-sidebar.tsx @@ -0,0 +1,27 @@ +import {Sidebar, SidebarContent, SidebarFooter, SidebarTrigger} from "../ui/sidebar"; +import {ProductionSideBar} from "./side-components/production"; +import {Header} from "./side-components/header"; +import {LogisticsSideBar} from "./side-components/logistics"; +import {QualitySideBar} from "./side-components/quality"; +import {AdminSideBar} from "./side-components/Admin"; +import {ForkliftSideBar} from "./side-components/forklift"; +import {EomSideBar} from "./side-components/eom"; + +export function AppSidebar() { + return ( + + +
+ + + + + + + + + + + + ); +} diff --git a/frontend/src/components/layout/mode-toggle.tsx b/frontend/src/components/layout/mode-toggle.tsx new file mode 100644 index 0000000..e30463d --- /dev/null +++ b/frontend/src/components/layout/mode-toggle.tsx @@ -0,0 +1,26 @@ +import {Moon, Sun} from "lucide-react"; + +import {Button} from "../ui/button"; +import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "../ui/dropdown-menu"; +import {useTheme} from "../layout/theme-provider"; + +export function ModeToggle() { + const {setTheme} = useTheme(); + + return ( + + + + + + setTheme("light")}>Light + setTheme("dark")}>Dark + setTheme("system")}>System + + + ); +} diff --git a/frontend/src/components/layout/side-components/admin.tsx b/frontend/src/components/layout/side-components/admin.tsx new file mode 100644 index 0000000..ddce1e8 --- /dev/null +++ b/frontend/src/components/layout/side-components/admin.tsx @@ -0,0 +1,59 @@ +import {Printer} from "lucide-react"; +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "../../ui/sidebar"; + +const items = [ + { + title: "Settings", + url: "#", + icon: Printer, + }, + { + title: "Swagger", + url: "#", + icon: Printer, + }, + { + title: "Logs", + url: "#", + icon: Printer, + }, + { + title: "Users", + url: "#", + icon: Printer, + }, + { + title: "UCD", + url: "#", + icon: Printer, + }, +]; + +export function AdminSideBar() { + return ( + + Admin + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + ); +} diff --git a/frontend/src/components/layout/side-components/eom.tsx b/frontend/src/components/layout/side-components/eom.tsx new file mode 100644 index 0000000..309f3b5 --- /dev/null +++ b/frontend/src/components/layout/side-components/eom.tsx @@ -0,0 +1,39 @@ +import {Printer} from "lucide-react"; +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "../../ui/sidebar"; + +const items = [ + { + title: "Qaulity Request", + url: "#", + icon: Printer, + }, +]; + +export function EomSideBar() { + return ( + + End of month + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + ); +} diff --git a/frontend/src/components/layout/side-components/forklift.tsx b/frontend/src/components/layout/side-components/forklift.tsx new file mode 100644 index 0000000..1b376d1 --- /dev/null +++ b/frontend/src/components/layout/side-components/forklift.tsx @@ -0,0 +1,39 @@ +import {Printer} from "lucide-react"; +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "../../ui/sidebar"; + +const items = [ + { + title: "Qaulity Request", + url: "#", + icon: Printer, + }, +]; + +export function ForkliftSideBar() { + return ( + + Forklift + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + ); +} diff --git a/frontend/src/components/layout/side-components/header.tsx b/frontend/src/components/layout/side-components/header.tsx new file mode 100644 index 0000000..6a5a8c8 --- /dev/null +++ b/frontend/src/components/layout/side-components/header.tsx @@ -0,0 +1,21 @@ +import {SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem} from "../../ui/sidebar"; + +export function Header() { + return ( + + + + +
+ Description +
+ Logistics Support Tool + v1.0.0 +
+
+
+
+
+
+ ); +} diff --git a/frontend/src/components/layout/side-components/logistics.tsx b/frontend/src/components/layout/side-components/logistics.tsx new file mode 100644 index 0000000..d6548b7 --- /dev/null +++ b/frontend/src/components/layout/side-components/logistics.tsx @@ -0,0 +1,54 @@ +import {Printer} from "lucide-react"; +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "../../ui/sidebar"; + +const items = [ + { + title: "Silo Adjustments", + url: "#", + icon: Printer, + }, + { + title: "Bulk orders", + url: "#", + icon: Printer, + }, + { + title: "Forecast", + url: "#", + icon: Printer, + }, + { + title: "Ocme cycle counts", + url: "#", + icon: Printer, + }, +]; + +export function LogisticsSideBar() { + return ( + + Logistics + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + ); +} diff --git a/frontend/src/components/layout/side-components/production.tsx b/frontend/src/components/layout/side-components/production.tsx new file mode 100644 index 0000000..897a79a --- /dev/null +++ b/frontend/src/components/layout/side-components/production.tsx @@ -0,0 +1,39 @@ +import {Printer} from "lucide-react"; +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "../../ui/sidebar"; + +const items = [ + { + title: "One Click Print", + url: "#", + icon: Printer, + }, +]; + +export function ProductionSideBar() { + return ( + + Production + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + ); +} diff --git a/frontend/src/components/layout/side-components/quality.tsx b/frontend/src/components/layout/side-components/quality.tsx new file mode 100644 index 0000000..8143495 --- /dev/null +++ b/frontend/src/components/layout/side-components/quality.tsx @@ -0,0 +1,39 @@ +import {Printer} from "lucide-react"; +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "../../ui/sidebar"; + +const items = [ + { + title: "Qaulity Request", + url: "#", + icon: Printer, + }, +]; + +export function QualitySideBar() { + return ( + + Quality + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + ); +} diff --git a/frontend/src/components/layout/theme-provider.tsx b/frontend/src/components/layout/theme-provider.tsx new file mode 100644 index 0000000..bf8e3b4 --- /dev/null +++ b/frontend/src/components/layout/theme-provider.tsx @@ -0,0 +1,67 @@ +import {createContext, useContext, useEffect, useState} from "react"; + +type Theme = "dark" | "light" | "system"; + +type ThemeProviderProps = { + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +}; + +type ThemeProviderState = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const initialState: ThemeProviderState = { + theme: "system", + setTheme: () => null, +}; + +const ThemeProviderContext = createContext(initialState); + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "vite-ui-theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme); + + useEffect(() => { + const root = window.document.documentElement; + + root.classList.remove("light", "dark"); + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + + root.classList.add(systemTheme); + return; + } + + root.classList.add(theme); + }, [theme]); + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; + + return ( + + {children} + + ); +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext); + + if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider"); + + return context; +}; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 61875fc..972f57f 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -1,58 +1,48 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import {Slot} from "@radix-ui/react-slot"; +import {cva, type VariantProps} from "class-variance-authority"; -import { cn } from "@/lib/utils" +import {cn} from "../../lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", - outline: - "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", + outline: "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); function Button({ - className, - variant, - size, - asChild = false, - ...props + className, + variant, + size, + asChild = false, + ...props }: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean - }) { - const Comp = asChild ? Slot : "button" + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : "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..356457f --- /dev/null +++ b/frontend/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + 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, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + 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..8b652f6 --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,19 @@ +import * 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..7288ae0 --- /dev/null +++ b/frontend/src/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client"; + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +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..6fdd5cb --- /dev/null +++ b/frontend/src/components/ui/sheet.tsx @@ -0,0 +1,101 @@ +import * as React from "react"; +import * as SheetPrimitive from "@radix-ui/react-dialog"; +import {XIcon} from "lucide-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", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left"; +}) { + return ( + + + + {children} + + + 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..97b4dd5 --- /dev/null +++ b/frontend/src/components/ui/sidebar.tsx @@ -0,0 +1,668 @@ +import * as React from "react"; +import {Slot} from "@radix-ui/react-slot"; +import {VariantProps, cva} from "class-variance-authority"; +import {PanelLeftIcon} from "lucide-react"; + +import {useIsMobile} from "../../hooks/use-mobile"; +import {cn} from "../../lib/utils"; +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"; + +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 SidebarContext = { + 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; +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + } +>(({defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props}, ref) => { + 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} +
+
+
+ ); +}); +SidebarProvider.displayName = "SidebarProvider"; + +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 ( + +
+ ); } diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 6074078..12ceb84 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -3,6 +3,7 @@ import LoginForm from "../components/auth/LoginForm"; import {Button} from "../components/ui/button"; import {useLogout} from "../lib/hooks/useLogout"; import {useSession} from "../lib/hooks/useSession"; +import {useSessionStore} from "../lib/store/sessionStore"; export const Route = createFileRoute("/")({ component: Index, @@ -10,20 +11,20 @@ export const Route = createFileRoute("/")({ function Index() { const {session} = useSession(); + const {user} = useSessionStore(); const logout = useLogout(); return (
-

Welcome Home!

-

-

- {session ? ( - <> - {" "} - - ) : ( - - )} -

+ {session ? ( + <> +

Welcome Home {user?.username}

+

Your current role is: {user?.role}

+

+ {" "} + + ) : ( + + )}
); } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 0a1f462..e68c19c 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -30,6 +30,14 @@ --chart-4: hsl(43 74% 66%); --chart-5: hsl(27 87% 67%); --radius: 0.6rem; + --sidebar: hsl(0 0% 98%); + --sidebar-foreground: hsl(240 5.3% 26.1%); + --sidebar-primary: hsl(240 5.9% 10%); + --sidebar-primary-foreground: hsl(0 0% 98%); + --sidebar-accent: hsl(240 4.8% 95.9%); + --sidebar-accent-foreground: hsl(240 5.9% 10%); + --sidebar-border: hsl(220 13% 91%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); } .dark { @@ -57,6 +65,14 @@ --chart-3: hsl(30 80% 55%); --chart-4: hsl(280 65% 60%); --chart-5: hsl(340 75% 55%); + --sidebar: hsl(240 5.9% 10%); + --sidebar-foreground: hsl(240 4.8% 95.9%); + --sidebar-primary: hsl(224.3 76.3% 48%); + --sidebar-primary-foreground: hsl(0 0% 100%); + --sidebar-accent: hsl(240 3.7% 15.9%); + --sidebar-accent-foreground: hsl(240 4.8% 95.9%); + --sidebar-border: hsl(240 3.7% 15.9%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); } @theme inline { @@ -88,6 +104,14 @@ --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); } @layer base { diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index fec8c8e..0ceb734 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,13 +1,10 @@ { - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ], - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] + "files": [], + "references": [{"path": "./tsconfig.app.json"}, {"path": "./tsconfig.node.json"}], + "compilerOptions": { + // "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } } - } } diff --git a/package.json b/package.json index 9e446d0..7ff2a31 100644 --- a/package.json +++ b/package.json @@ -30,17 +30,20 @@ "compression": "^1.8.0", "cookie": "^1.0.2", "date-fns": "^4.1.0", + "drizzle-orm": "^0.39.3", + "drizzle-zod": "^0.7.0", "hono": "^4.7.1", "http-proxy-middleware": "^3.0.3", "jsonwebtoken": "^9.0.2", "zod": "^3.24.2" }, "devDependencies": { + "@types/js-cookie": "^3.0.6", + "concurrently": "^9.1.2", "cz-conventional-changelog": "^3.3.0", "rimraf": "^6.0.1", "standard-version": "^9.5.0", - "typescript": "~5.7.3", - "concurrently": "^9.1.2" + "typescript": "~5.7.3" }, "config": { "commitizen": { diff --git a/server/src/services/auth/controllers/login.ts b/server/src/services/auth/controllers/login.ts index cd594a6..0031f87 100644 --- a/server/src/services/auth/controllers/login.ts +++ b/server/src/services/auth/controllers/login.ts @@ -5,21 +5,24 @@ import {sign, verify} from "jsonwebtoken"; */ const fakeUsers = [ - {id: 1, username: "admin", password: "password123"}, - {id: 2, username: "user", password: "password123"}, - {id: 3, username: "user2", password: "password123"}, + {id: 1, username: "admin", password: "password123", role: "admin"}, + {id: 2, username: "user", password: "pass", role: "user"}, + {id: 3, username: "user2", password: "password123", role: "user"}, ]; -export function login(username: string, password: string): {token: string; user: {id: number; username: string}} { +export function login( + username: string, + password: string +): {token: string; user: {id: number; username: string; role: string}} { const user = fakeUsers.find((u) => u.username === username && u.password === password); if (!user) { throw new Error("Invalid credentials"); } // Create a JWT - const token = sign({userId: user?.id, username: user?.username}, process.env.JWT_SECRET, { + const token = sign({user}, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES, }); - return {token, user: {id: user?.id, username: user.username}}; + return {token, user: {id: user?.id, username: user.username, role: user.role}}; } diff --git a/server/src/services/auth/routes/session.ts b/server/src/services/auth/routes/session.ts index cc7f186..7c2dbfd 100644 --- a/server/src/services/auth/routes/session.ts +++ b/server/src/services/auth/routes/session.ts @@ -40,7 +40,7 @@ session.openapi(route, async (c) => { try { const payload = await verify(token, JWT_SECRET); console.log(payload); - return c.json({token}); + return c.json({data: {token, user: payload.user}}); } catch (err) { return c.json({error: "Invalid or expired token"}, 401); }