mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(freight:backoffice): added login page and basic authorization
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { ReactNode } from "react";
|
||||
import { type MouseEvent, type ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
export interface SidebarItem {
|
||||
label: string;
|
||||
href: string;
|
||||
icon?: ReactNode;
|
||||
children?: SidebarItem[];
|
||||
}
|
||||
|
||||
export interface SidebarProps {
|
||||
@@ -27,62 +29,153 @@ const Sidebar = ({
|
||||
activeHref,
|
||||
onNavigate,
|
||||
headerExtra,
|
||||
}: SidebarProps) => (
|
||||
<aside className="flex w-64 flex-col gap-1 border-r border-sidebar-border bg-sidebar px-3 py-5 ">
|
||||
{title ? (
|
||||
<div className="flex items-center justify-between gap-2 px-3 pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-sidebar-primary text-sm font-bold text-white">
|
||||
{title.charAt(0)}
|
||||
</div>
|
||||
<div className="text-base font-semibold text-slate-800 dark:text-slate-100">
|
||||
{title}
|
||||
}: SidebarProps) => {
|
||||
const activePath = activeHref?.toLowerCase() ?? "";
|
||||
const defaultExpanded = useMemo(
|
||||
() =>
|
||||
items.reduce<Record<string, boolean>>((acc, item) => {
|
||||
if (item.children?.length) {
|
||||
acc[item.href] =
|
||||
activePath === item.href.toLowerCase() ||
|
||||
activePath.startsWith(`${item.href.toLowerCase()}/`) ||
|
||||
item.children.some((child) =>
|
||||
activePath.startsWith(child.href.toLowerCase()),
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
[activePath, items],
|
||||
);
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>(defaultExpanded);
|
||||
|
||||
useEffect(() => {
|
||||
setExpanded((current) => ({ ...defaultExpanded, ...current }));
|
||||
}, [defaultExpanded]);
|
||||
|
||||
const navigateTo = (event: MouseEvent<HTMLAnchorElement>, href: string) => {
|
||||
if (onNavigate) {
|
||||
event.preventDefault();
|
||||
onNavigate(href);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="flex w-64 flex-col gap-1 border-r border-sidebar-border bg-sidebar px-3 py-5 ">
|
||||
{title ? (
|
||||
<div className="flex items-center justify-between gap-2 px-3 pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-sidebar-primary text-sm font-bold text-white">
|
||||
{title.charAt(0)}
|
||||
</div>
|
||||
<div className="text-base font-semibold text-slate-800 dark:text-slate-100">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
{headerExtra}
|
||||
</div>
|
||||
{headerExtra}
|
||||
</div>
|
||||
) : null}
|
||||
) : null}
|
||||
|
||||
<nav className="flex flex-col gap-1">
|
||||
{items.map((item) => {
|
||||
const isActive = activeHref?.toLowerCase() === item.href.toLowerCase();
|
||||
<nav className="flex flex-col gap-1">
|
||||
{items.map((item) => {
|
||||
const hasChildren = Boolean(item.children?.length);
|
||||
const itemHref = item.href.toLowerCase();
|
||||
const childActive =
|
||||
item.children?.some((child) =>
|
||||
activePath.startsWith(child.href.toLowerCase()),
|
||||
) ?? false;
|
||||
const isActive =
|
||||
activePath === itemHref ||
|
||||
activePath.startsWith(`${itemHref}/`) ||
|
||||
childActive;
|
||||
|
||||
return (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={(event) => {
|
||||
if (onNavigate) {
|
||||
event.preventDefault();
|
||||
onNavigate(item.href);
|
||||
}
|
||||
}}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={clsx(
|
||||
"group flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition",
|
||||
isActive
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground shadow-sm "
|
||||
: "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
)}
|
||||
>
|
||||
{item.icon ? (
|
||||
<span
|
||||
return (
|
||||
<div key={item.href} className="flex flex-col gap-1">
|
||||
<div
|
||||
className={clsx(
|
||||
"flex h-5 w-5 items-center justify-center [&_svg]:h-5 [&_svg]:w-5",
|
||||
"group flex items-center gap-2 rounded-md transition",
|
||||
isActive
|
||||
? "text-white"
|
||||
: "text-slate-500 group-hover:text-[#10B981] dark:text-slate-400 dark:group-hover:text-white",
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground shadow-sm"
|
||||
: "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
) : null}
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
<a
|
||||
href={item.href}
|
||||
onClick={(event) => navigateTo(event, item.href)}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className="flex min-w-0 flex-1 items-center gap-3 px-3 py-2.5 text-sm font-medium"
|
||||
>
|
||||
{item.icon ? (
|
||||
<span
|
||||
className={clsx(
|
||||
"flex h-5 w-5 items-center justify-center [&_svg]:h-5 [&_svg]:w-5",
|
||||
isActive
|
||||
? "text-white"
|
||||
: "text-slate-500 group-hover:text-[#10B981] dark:text-slate-400 dark:group-hover:text-white",
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="truncate">{item.label}</span>
|
||||
</a>
|
||||
{hasChildren ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Toggle ${item.label}`}
|
||||
aria-expanded={expanded[item.href] ?? false}
|
||||
onClick={() =>
|
||||
setExpanded((current) => ({
|
||||
...current,
|
||||
[item.href]: !current[item.href],
|
||||
}))
|
||||
}
|
||||
className={clsx(
|
||||
"mr-2 inline-flex h-8 w-8 items-center justify-center rounded-md transition",
|
||||
isActive
|
||||
? "text-white/90 hover:bg-white/10"
|
||||
: "text-slate-500 hover:bg-sidebar-accent dark:text-slate-400",
|
||||
)}
|
||||
>
|
||||
<ChevronDown
|
||||
className={clsx(
|
||||
"h-4 w-4 transition-transform",
|
||||
expanded[item.href] ? "rotate-180" : "rotate-0",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasChildren && expanded[item.href] ? (
|
||||
<div className="ml-4 flex flex-col gap-1 border-l border-sidebar-border/60 pl-3">
|
||||
{item.children!.map((child) => {
|
||||
const childActiveHref = activePath === child.href.toLowerCase();
|
||||
|
||||
return (
|
||||
<a
|
||||
key={child.href}
|
||||
href={child.href}
|
||||
onClick={(event) => navigateTo(event, child.href)}
|
||||
aria-current={childActiveHref ? "page" : undefined}
|
||||
className={clsx(
|
||||
"rounded-md px-3 py-2 text-sm transition",
|
||||
childActiveHref
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground shadow-sm"
|
||||
: "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
)}
|
||||
>
|
||||
{child.label}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
||||
Reference in New Issue
Block a user