Release v0.1.2
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { Settings, Shield } from "lucide-react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { NavLink, useLocation } from "react-router-dom";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { AuthInfo, PlatformNavItem } from "../types";
|
||||
import { adminReadScopes, hasAnyScope, hasScope } from "../utils/permissions";
|
||||
|
||||
const MODULE_NAV_STORAGE_KEY = "govoplan.lastModuleNav";
|
||||
|
||||
function visibleNavItems(auth: AuthInfo | null | undefined, navItems: PlatformNavItem[]): PlatformNavItem[] {
|
||||
return [...navItems]
|
||||
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100))
|
||||
@@ -22,10 +25,25 @@ export default function IconRail({
|
||||
auth?: AuthInfo | null;
|
||||
navItems?: PlatformNavItem[];
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const visibleItems = visibleNavItems(auth, navItems);
|
||||
const items = hasAnyScope(auth, adminReadScopes)
|
||||
? [...visibleItems, { to: "/admin", label: "Admin", icon: Shield }]
|
||||
: visibleItems;
|
||||
const [rememberedTargets, setRememberedTargets] = useState<Record<string, string>>(() => loadRememberedTargets());
|
||||
const topLevelItems = useMemo(() => items.map((item) => item.to), [items]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentRoot = topLevelItems.find((root) => modulePathActive(location.pathname, root));
|
||||
if (!currentRoot || location.pathname === currentRoot) return;
|
||||
const target = `${location.pathname}${location.search}${location.hash}`;
|
||||
setRememberedTargets((current) => {
|
||||
if (current[currentRoot] === target) return current;
|
||||
const next = { ...current, [currentRoot]: target };
|
||||
saveRememberedTargets(next);
|
||||
return next;
|
||||
});
|
||||
}, [location.hash, location.pathname, location.search, topLevelItems]);
|
||||
|
||||
return (
|
||||
<aside className={`icon-rail ${compact ? "compact" : ""}`}>
|
||||
@@ -34,11 +52,15 @@ export default function IconRail({
|
||||
{!compact && (
|
||||
<>
|
||||
<nav className="icon-nav">
|
||||
{items.map(({ to, label, icon: Icon }) => (
|
||||
<NavLink key={to} to={to} className={({ isActive }) => `icon-nav-item ${isActive ? "active" : ""}`} title={label}>
|
||||
{Icon ? <Icon size={20} /> : label.slice(0, 1)}
|
||||
</NavLink>
|
||||
))}
|
||||
{items.map(({ to, label, icon: Icon }) => {
|
||||
const target = rememberedTargets[to] ?? to;
|
||||
const active = modulePathActive(location.pathname, to);
|
||||
return (
|
||||
<NavLink key={to} to={target} className={`icon-nav-item ${active ? "active" : ""}`} title={label}>
|
||||
{Icon ? <Icon size={20} /> : label.slice(0, 1)}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div className="icon-rail-bottom">
|
||||
<NavLink to="/settings" className={({ isActive }) => `icon-nav-item ${isActive ? "active" : ""}`} title="Settings">
|
||||
@@ -50,3 +72,34 @@ export default function IconRail({
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function modulePathActive(pathname: string, root: string): boolean {
|
||||
if (root === "/") return pathname === "/";
|
||||
return pathname === root || pathname.startsWith(`${root}/`);
|
||||
}
|
||||
|
||||
function loadRememberedTargets(): Record<string, string> {
|
||||
if (typeof window === "undefined") return {};
|
||||
try {
|
||||
const raw = window.localStorage.getItem(MODULE_NAV_STORAGE_KEY);
|
||||
const parsed = raw ? JSON.parse(raw) : {};
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(parsed).filter((entry): entry is [string, string] => {
|
||||
const [root, target] = entry;
|
||||
return typeof root === "string" && typeof target === "string" && root.startsWith("/") && target.startsWith(`${root}/`);
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveRememberedTargets(targets: Record<string, string>): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage.setItem(MODULE_NAV_STORAGE_KEY, JSON.stringify(targets));
|
||||
} catch {
|
||||
// Remembered navigation is a convenience only.
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user