feat(freight:backoffice): added login page and basic authorization

This commit is contained in:
Michael Abebe
2026-05-22 17:55:20 +03:00
parent 2c9ca6f16b
commit 27e2cfcf6b
18 changed files with 918 additions and 227 deletions

View File

@@ -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;