From 5fcc3706114c3ad944ed985c84964545e1c0306f Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Fri, 12 Jun 2026 15:08:43 +0330 Subject: [PATCH] feat(admin): add collapsible sidebar navigation --- src/views/AdminLayout.tsx | 147 ++++++++++++++++++++++++++++++-------- 1 file changed, 117 insertions(+), 30 deletions(-) diff --git a/src/views/AdminLayout.tsx b/src/views/AdminLayout.tsx index 2667c46..d7ab169 100644 --- a/src/views/AdminLayout.tsx +++ b/src/views/AdminLayout.tsx @@ -1,24 +1,53 @@ "use client"; import type { ReactNode } from "react"; -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { + CalendarDays, + FileText, + FolderTree, + PanelRightClose, + PanelRightOpen, + ShieldCheck, + Tags, + UsersRound, +} from "lucide-react"; import { Navigate, NavLink, useLocation } from "@/lib/router"; import { useAuth } from "@/contexts/AuthContext"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; const navItems = [ - { to: "/admin/users", label: "مدیریت کاربران", requiresStaff: true }, - { to: "/admin/events", label: "مدیریت رویدادها", requiresStaff: true }, - { to: "/admin/blog", label: "مدیریت بلاگ", requiresStaff: false }, + { to: "/admin/users", label: "کاربران", icon: UsersRound, visibility: "staff" }, + { to: "/admin/events", label: "رویدادها", icon: CalendarDays, visibility: "staff" }, + { to: "/admin/blog", label: "نوشته‌های بلاگ", icon: FileText, visibility: "blog" }, + { to: "/admin/blog/categories", label: "دسته‌بندی‌ها", icon: FolderTree, visibility: "taxonomy" }, + { to: "/admin/blog/tags", label: "برچسب‌ها", icon: Tags, visibility: "taxonomy" }, + { to: "/admin/authorizations", label: "دسترسی‌ها", icon: ShieldCheck, visibility: "superuser" }, ] as const; export default function AdminLayout({ children }: { children: ReactNode }) { const location = useLocation(); const { user, isAuthenticated, loading } = useAuth(); + const [collapsed, setCollapsed] = useState(false); const canAccessAdmin = useMemo( () => isAuthenticated && Boolean(user?.is_staff || user?.is_superuser || user?.can_access_blog_admin), [isAuthenticated, user?.can_access_blog_admin, user?.is_staff, user?.is_superuser], ); + useEffect(() => { + const saved = window.localStorage.getItem("admin-sidebar-collapsed"); + if (saved) setCollapsed(saved === "true"); + }, []); + + const toggleCollapsed = () => { + setCollapsed((current) => { + const next = !current; + window.localStorage.setItem("admin-sidebar-collapsed", String(next)); + return next; + }); + }; + if (loading) { return (
@@ -32,40 +61,98 @@ export default function AdminLayout({ children }: { children: ReactNode }) { } const visibleNavItems = navItems.filter((item) => { - if (item.requiresStaff) { - return Boolean(user?.is_staff || user?.is_superuser); - } + if (item.visibility === "staff") return Boolean(user?.is_staff || user?.is_superuser); + if (item.visibility === "taxonomy") return Boolean(user?.is_staff || user?.is_superuser || user?.can_review_blog_posts); + if (item.visibility === "superuser") return Boolean(user?.is_superuser); return Boolean(user?.is_staff || user?.is_superuser || user?.can_access_blog_admin); }); + const isItemActive = (to: string) => { + if (location.pathname === to) return true; + if (to === "/admin/blog") { + return /^\/admin\/blog\/(new|\d+)/.test(location.pathname ?? ""); + } + return Boolean(location.pathname?.startsWith(`${to}/`)); + }; + return ( -
-
-
-

پنل مدیریت

-
- {visibleNavItems.map((item) => ( - - [ - "rounded-full px-4 py-2 text-sm transition", - (isActive || location.pathname?.startsWith(item.to)) +
+
+ + +
+
+
+

پنل مدیریت

+
+ {visibleNavItems.map((item) => { + const Icon = item.icon; + return ( + + cn( + "flex shrink-0 items-center gap-1 rounded-full px-3 py-2 text-xs", + isActive || isItemActive(item.to) + ? "bg-primary text-primary-foreground" + : "bg-muted text-muted-foreground", + ) + } + > + + {item.label} + + ); + })} +
+
+
+
+ {children}
-
- {children} -
); }