feat(frontend): finished login form with validation

This commit is contained in:
2025-02-21 09:16:07 -06:00
parent d939332499
commit 9719451580
25 changed files with 551 additions and 230 deletions

View File

@@ -0,0 +1,18 @@
import {ReactNode} from "react";
import {Card} from "../ui/card";
interface LstCardProps {
children?: ReactNode;
className?: string;
style?: React.CSSProperties;
}
export function LstCard({children, className = "", style = {}}: LstCardProps) {
return (
<div className="m-auto">
<Card className={`border-solid border-2 border-[#00659c] ${className}`} style={style}>
{children}
</Card>
</div>
);
}

View File

@@ -6,91 +6,17 @@ import {QualitySideBar} from "./side-components/quality";
import {ForkliftSideBar} from "./side-components/forklift";
import {EomSideBar} from "./side-components/eom";
import {AdminSideBar} from "./side-components/admin";
type Feature = string;
interface Module {
id: number;
name: string;
features: Feature[];
active: boolean;
}
interface RolePermissions {
[moduleName: string]: Feature[];
}
interface Roles {
[roleName: string]: RolePermissions;
}
interface User {
id: number;
username: string;
role: keyof Roles;
}
const modules: Module[] = [
{id: 1, name: "production", active: true, features: ["view", "edit", "approve"]},
{id: 2, name: "logistics", active: true, features: ["view", "assign", "track"]},
{id: 3, name: "quality", active: false, features: ["view", "audit", "approve"]},
{id: 4, name: "forklift", active: true, features: ["view", "request", "operate"]},
{id: 5, name: "admin", active: true, features: ["view", "manage_users", "view_logs", "settings"]},
];
const rolePermissions: Roles = {
admin: {
production: ["view", "edit", "approve"],
logistics: ["view", "assign", "track"],
quality: ["view", "audit", "approve"],
forklift: ["view", "request", "operate"],
admin: ["view", "manage_users", "view_logs", "settings"],
},
manager: {
production: ["view", "edit"],
logistics: ["view", "assign"],
quality: ["view", "audit"],
forklift: ["view", "request"],
admin: ["view_logs"],
},
supervisor: {
production: ["view"],
logistics: ["view"],
quality: ["view"],
forklift: ["view", "request"],
admin: [],
},
user: {
production: ["view"],
logistics: [],
quality: [],
forklift: [],
admin: [],
},
};
// const users: User[] = [
// {id: 1, username: "admin", role: "admin"},
// {id: 2, username: "manager", role: "manager"},
// {id: 3, username: "supervisor", role: "supervisor"},
// {id: 4, username: "user", role: "user"},
// ];
function hasAccess(user: User, moduleName: string, feature: Feature): boolean {
return rolePermissions[user.role]?.[moduleName]?.includes(feature) || false;
}
function moduleActive(moduleName: string): boolean {
const module = modules.find((m) => m.name === moduleName);
return module ? module.active : false;
}
import {useSessionStore} from "../../lib/store/sessionStore";
import {hasAccess} from "../../utils/userAccess";
import {moduleActive} from "../../utils/moduleActive";
export function AppSidebar() {
const user = {id: 4, username: "admin", role: "admin"};
const {user} = useSessionStore();
return (
<Sidebar collapsible="icon">
<SidebarContent>
<Header />
{hasAccess(user, "production", "view") && moduleActive("production") && <ProductionSideBar />}
{moduleActive("production") && <ProductionSideBar />}
{hasAccess(user, "logistics", "view") && moduleActive("logistics") && <LogisticsSideBar />}
{hasAccess(user, "forklift", "view") && moduleActive("forklift") && <ForkliftSideBar />}
{hasAccess(user, "eom", "view") && moduleActive("admin") && <EomSideBar />}

View File

@@ -13,7 +13,7 @@ export function Header() {
<div className="flex flex-col gap-0.5 leading-none">
<span className="font-semibold">Logistics Support Tool</span>
<span className="">v1.0.0</span>
<span className="">v2.0.0</span>
</div>
</div>
</SidebarMenuButton>

View File

@@ -1,51 +1,36 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils"
import {cn} from "../../lib/utils";
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
function Avatar({className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
function AvatarImage({className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
function AvatarFallback({className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback }
export {Avatar, AvatarImage, AvatarFallback};

View File

@@ -0,0 +1,68 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn("flex flex-col gap-1.5 px-6 pt-6", className)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 pb-6", className)}
{...props}
/>
)
}
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,30 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@@ -1,19 +1,21 @@
import * as React from "react";
import * as React from "react"
import {cn} from "../../lib/utils";
import { cn } from "@/lib/utils"
function Input({className, type, ...props}: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground aria-invalid:outline-destructive/60 aria-invalid:ring-destructive/20 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:outline-destructive/60 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:ring-destructive/20 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4",
className
)}
{...props}
/>
);
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export {Input};
export { Input }

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@@ -0,0 +1,27 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground font-medium",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground font-medium",
},
}}
{...props}
/>
)
}
export { Toaster }