- /admin opted out of shopper chrome in MyLayout; /admin added to server protectedRoutes; useRequireAdmin() gates to superusers (UserProfile.isSuperuser). - Admin shell (admin.tsx) with sidebar nav (Overview live; other sections stubbed) and a mobile top bar; Overview dashboard (KPI cards + 14-day orders chart) via use-admin-hooks.ts hitting /api/adminpanel/v1/overview/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
145 lines
4.7 KiB
TypeScript
145 lines
4.7 KiB
TypeScript
import { Link, Outlet, useLocation } from "@remix-run/react";
|
||
import {
|
||
LayoutDashboard,
|
||
ClipboardList,
|
||
Store,
|
||
Users,
|
||
Package,
|
||
Landmark,
|
||
ArrowRight,
|
||
ShieldCheck,
|
||
Loader2,
|
||
} from "lucide-react";
|
||
import type { LucideIcon } from "lucide-react";
|
||
import { useRequireAdmin } from "~/hooks/useRequireAdmin";
|
||
|
||
interface NavItem {
|
||
label: string;
|
||
to: string;
|
||
icon: LucideIcon;
|
||
exact?: boolean;
|
||
soon?: boolean; // planned but not built yet
|
||
}
|
||
|
||
const NAV: NavItem[] = [
|
||
{ label: "نمای کلی", to: "/admin", icon: LayoutDashboard, exact: true },
|
||
{ label: "سفارشها", to: "/admin/orders", icon: ClipboardList, soon: true },
|
||
{ label: "فروشگاهها", to: "/admin/sellers", icon: Store, soon: true },
|
||
{ label: "کاربران", to: "/admin/users", icon: Users, soon: true },
|
||
{ label: "محصولات", to: "/admin/products", icon: Package, soon: true },
|
||
{ label: "دفتر مالی", to: "/admin/ledger", icon: Landmark, soon: true },
|
||
];
|
||
|
||
export default function AdminLayout() {
|
||
const location = useLocation();
|
||
const { isLoading, isAdmin } = useRequireAdmin();
|
||
|
||
const isActive = (item: NavItem) =>
|
||
item.exact
|
||
? location.pathname === item.to
|
||
: location.pathname.startsWith(item.to);
|
||
|
||
if (isLoading || !isAdmin) {
|
||
return (
|
||
<div className="flex min-h-[60vh] items-center justify-center">
|
||
<Loader2 className="h-7 w-7 animate-spin text-GRAY" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const NavLinks = ({ onNavigate }: { onNavigate?: () => void }) => (
|
||
<>
|
||
{NAV.map((n) => {
|
||
const active = isActive(n);
|
||
const cls = `flex items-center gap-3 px-3.5 py-3 rounded-xl text-[14.5px] font-semibold transition-colors ${
|
||
active ? "bg-BLACK text-white" : "text-BLACK2 hover:bg-WHITE2"
|
||
} ${n.soon ? "opacity-45 cursor-not-allowed" : ""}`;
|
||
const inner = (
|
||
<>
|
||
<n.icon size={20} />
|
||
{n.label}
|
||
{n.soon && (
|
||
<span className="ms-auto text-[10px] text-GRAY font-normal">
|
||
بهزودی
|
||
</span>
|
||
)}
|
||
</>
|
||
);
|
||
return n.soon ? (
|
||
<div key={n.to} className={cls} aria-disabled>
|
||
{inner}
|
||
</div>
|
||
) : (
|
||
<Link
|
||
key={n.to}
|
||
to={n.to}
|
||
prefetch="intent"
|
||
onClick={onNavigate}
|
||
className={cls}
|
||
>
|
||
{inner}
|
||
</Link>
|
||
);
|
||
})}
|
||
<Link
|
||
to="/"
|
||
className="flex items-center gap-3 px-3.5 py-3 rounded-xl text-[14.5px] font-semibold text-BLACK2 hover:bg-WHITE2 transition-colors"
|
||
>
|
||
<ArrowRight size={20} />
|
||
بازگشت به سایت
|
||
</Link>
|
||
</>
|
||
);
|
||
|
||
return (
|
||
<div className="lg:max-w-[1440px] lg:mx-auto lg:w-full lg:px-8 lg:py-8 lg:grid lg:grid-cols-[248px_1fr] lg:gap-7 lg:items-start">
|
||
{/* Desktop sidebar */}
|
||
<aside className="hidden lg:block lg:sticky lg:top-6 border border-WHITE3 rounded-2xl overflow-hidden">
|
||
<div className="p-4 border-b border-WHITE3 flex items-center gap-3">
|
||
<div className="h-10 w-10 rounded-xl bg-BLACK text-white grid place-items-center shrink-0">
|
||
<ShieldCheck size={20} />
|
||
</div>
|
||
<div className="min-w-0">
|
||
<p className="font-bold text-[15px] truncate">پنل مدیریت</p>
|
||
<p className="text-GRAY text-[12px]">ویترون</p>
|
||
</div>
|
||
</div>
|
||
<nav className="p-2.5 flex flex-col gap-0.5">
|
||
<NavLinks />
|
||
</nav>
|
||
</aside>
|
||
|
||
{/* Mobile top bar + horizontal nav */}
|
||
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)] border-b border-WHITE3">
|
||
<div className="flex items-center gap-2 px-4 h-[56px]">
|
||
<div className="h-8 w-8 rounded-lg bg-BLACK text-white grid place-items-center">
|
||
<ShieldCheck size={16} />
|
||
</div>
|
||
<p className="font-bold text-[15px]">پنل مدیریت</p>
|
||
</div>
|
||
<div className="flex gap-2 overflow-x-auto px-4 pb-2 hide-scrollbar">
|
||
{NAV.map((n) => {
|
||
const active = isActive(n);
|
||
const cls = `whitespace-nowrap px-3 py-1.5 rounded-full text-[13px] font-semibold ${
|
||
active ? "bg-BLACK text-white" : "bg-WHITE2 text-BLACK2"
|
||
} ${n.soon ? "opacity-45" : ""}`;
|
||
return n.soon ? (
|
||
<span key={n.to} className={cls}>
|
||
{n.label}
|
||
</span>
|
||
) : (
|
||
<Link key={n.to} to={n.to} prefetch="intent" className={cls}>
|
||
{n.label}
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="min-w-0 px-4 py-4 lg:p-0">
|
||
<Outlet />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|