Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants. Co-authored-by: Cursor <cursoragent@cursor.com>
90 lines
3.2 KiB
TypeScript
90 lines
3.2 KiB
TypeScript
"use client";
|
||
|
||
import { ReactNode } from "react";
|
||
import Link from "next/link";
|
||
import { usePathname } from "next/navigation";
|
||
import { clearTokens, loginWithRedirect } from "@/lib/auth";
|
||
|
||
const NAV_ITEMS: { href: string; label: string; icon: string }[] = [
|
||
{ href: "/admin", label: "نمای کلی", icon: "▦" },
|
||
{ href: "/admin/tenants", label: "Tenantها", icon: "🏢" },
|
||
{ href: "/admin/users", label: "کاربران", icon: "👤" },
|
||
{ href: "/admin/plans", label: "پلنها", icon: "📦" },
|
||
{ href: "/admin/features", label: "قابلیتها", icon: "🧩" },
|
||
{ href: "/admin/services", label: "سرویسها", icon: "🛠" },
|
||
{ href: "/admin/settings", label: "حساب کاربری", icon: "⚙" },
|
||
];
|
||
|
||
function isActive(pathname: string, href: string): boolean {
|
||
if (href === "/admin") return pathname === "/admin";
|
||
return pathname === href || pathname.startsWith(`${href}/`);
|
||
}
|
||
|
||
export function AdminShell({ children }: { children: ReactNode }) {
|
||
const pathname = usePathname();
|
||
|
||
const handleLogout = () => {
|
||
clearTokens();
|
||
void loginWithRedirect("/admin");
|
||
};
|
||
|
||
return (
|
||
<div className="flex min-h-[calc(100vh-4rem)]">
|
||
<aside className="hidden w-60 shrink-0 border-l border-gray-100 bg-white/60 p-4 lg:block">
|
||
<nav className="space-y-1">
|
||
{NAV_ITEMS.map((item) => {
|
||
const active = isActive(pathname, item.href);
|
||
return (
|
||
<Link
|
||
key={item.href}
|
||
href={item.href}
|
||
className={`flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-colors ${
|
||
active
|
||
? "bg-primary text-white shadow-sm"
|
||
: "text-gray-600 hover:bg-gray-100 hover:text-secondary"
|
||
}`}
|
||
>
|
||
<span aria-hidden className="text-base">
|
||
{item.icon}
|
||
</span>
|
||
{item.label}
|
||
</Link>
|
||
);
|
||
})}
|
||
</nav>
|
||
<button
|
||
type="button"
|
||
onClick={handleLogout}
|
||
className="mt-6 flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-red-600 transition-colors hover:bg-red-50"
|
||
>
|
||
<span aria-hidden className="text-base">
|
||
⎋
|
||
</span>
|
||
خروج
|
||
</button>
|
||
</aside>
|
||
|
||
{/* ناوبری موبایل */}
|
||
<div className="w-full">
|
||
<div className="flex gap-2 overflow-x-auto border-b border-gray-100 bg-white px-4 py-2 lg:hidden">
|
||
{NAV_ITEMS.map((item) => {
|
||
const active = isActive(pathname, item.href);
|
||
return (
|
||
<Link
|
||
key={item.href}
|
||
href={item.href}
|
||
className={`whitespace-nowrap rounded-lg px-3 py-1.5 text-xs font-medium ${
|
||
active ? "bg-primary text-white" : "text-gray-600 hover:bg-gray-100"
|
||
}`}
|
||
>
|
||
{item.label}
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
<main className="px-4 py-8 sm:px-6">{children}</main>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|