183 lines
8.3 KiB
TypeScript
183 lines
8.3 KiB
TypeScript
/**
|
||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||
* SPDX-License-Identifier: AGPL-3.0-only
|
||
* See the LICENSE file for details.
|
||
*/
|
||
|
||
import { Fragment, useEffect, useState } from "react";
|
||
import { observer } from "mobx-react";
|
||
import { useTheme as useNextTheme } from "next-themes";
|
||
import Link from "next/link";
|
||
import { usePathname } from "next/navigation";
|
||
import { BrainCog, ChevronDown, ExternalLink, Image, LogOut, Mail, Palette, Settings, UserCog2 } from "lucide-react";
|
||
import { Menu, Transition } from "@headlessui/react";
|
||
// plane imports
|
||
import { API_BASE_URL } from "@plane/constants";
|
||
import { LockIcon, WorkspaceIcon } from "@plane/propel/icons";
|
||
import { AuthService } from "@plane/services";
|
||
import { Avatar } from "@plane/ui";
|
||
import { cn, getFileURL } from "@plane/utils";
|
||
// assets
|
||
import NodeDcLogo from "@/app/assets/logos/nodedc-logo.svg?url";
|
||
// hooks
|
||
import { useUser } from "@/hooks/store";
|
||
// local imports
|
||
|
||
const authService = new AuthService();
|
||
|
||
const PRIMARY_NAVIGATION = [
|
||
{ label: "Основное", href: "/general/", Icon: Settings },
|
||
{ label: "Почта", href: "/email/", Icon: Mail },
|
||
{ label: "Аутентификация", href: "/authentication/", Icon: LockIcon },
|
||
{ label: "Воркспейсы", href: "/workspace/", Icon: WorkspaceIcon },
|
||
];
|
||
|
||
const FEATURE_NAVIGATION = [
|
||
{ label: "ИИ", href: "/ai/", Icon: BrainCog, description: "OpenAI модель и API-ключ" },
|
||
{ label: "Изображения", href: "/image/", Icon: Image, description: "Внешние библиотеки изображений" },
|
||
];
|
||
|
||
export const AdminHeader = observer(function AdminHeader() {
|
||
const pathName = usePathname();
|
||
const { currentUser, signOut } = useUser();
|
||
const { resolvedTheme, setTheme } = useNextTheme();
|
||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||
|
||
const isFeatureRoute = FEATURE_NAVIGATION.some((item) => pathName?.startsWith(item.href));
|
||
const adminName = currentUser?.display_name || currentUser?.email || "Глобальный админ";
|
||
const avatarName = currentUser?.display_name || currentUser?.email || "DC";
|
||
|
||
const handleThemeSwitch = () => {
|
||
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||
setTheme(newTheme);
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (csrfToken === undefined)
|
||
void authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||
}, [csrfToken]);
|
||
|
||
return (
|
||
<header className="nodedc-admin-header relative z-30 flex w-full flex-shrink-0 flex-col gap-4">
|
||
<div className="nodedc-admin-header-top grid w-full items-center gap-4">
|
||
<a href="/" className="nodedc-admin-logo-link inline-flex w-fit items-center" aria-label="NODE.DC">
|
||
<img src={NodeDcLogo} alt="NODE.DC" className="nodedc-admin-logo" />
|
||
</a>
|
||
|
||
<nav className="nodedc-admin-top-nav justify-self-center" aria-label="Основная навигация God Mode">
|
||
{PRIMARY_NAVIGATION.map((item) => {
|
||
const isActive = item.href === pathName || Boolean(pathName?.startsWith(item.href));
|
||
return (
|
||
<Link key={item.href} href={item.href} className="nodedc-admin-top-nav-item" data-active={isActive}>
|
||
<item.Icon className="size-3.5 stroke-[1.7]" />
|
||
<span>{item.label}</span>
|
||
</Link>
|
||
);
|
||
})}
|
||
|
||
<Menu as="div" className="relative">
|
||
<Menu.Button className="nodedc-admin-top-nav-item" data-active={isFeatureRoute}>
|
||
<BrainCog className="size-3.5 stroke-[1.7]" />
|
||
<span>Возможности</span>
|
||
<ChevronDown className="size-3 stroke-[2]" />
|
||
</Menu.Button>
|
||
<Transition
|
||
as={Fragment}
|
||
enter="transition ease-out duration-100"
|
||
enterFrom="transform opacity-0 scale-95"
|
||
enterTo="transform opacity-100 scale-100"
|
||
leave="transition ease-in duration-75"
|
||
leaveFrom="transform opacity-100 scale-100"
|
||
leaveTo="transform opacity-0 scale-95"
|
||
>
|
||
<Menu.Items className="nodedc-glass-popup-surface absolute top-full left-1/2 z-[120] mt-3 flex w-64 -translate-x-1/2 flex-col gap-1 p-2 outline-none">
|
||
{FEATURE_NAVIGATION.map((item) => {
|
||
const isActive = item.href === pathName || Boolean(pathName?.startsWith(item.href));
|
||
return (
|
||
<Menu.Item key={item.href}>
|
||
{({ active }) => (
|
||
<Link
|
||
href={item.href}
|
||
className={cn("nodedc-admin-feature-menu-item", {
|
||
"is-active": isActive,
|
||
"is-hovered": active,
|
||
})}
|
||
>
|
||
<span className="grid size-9 place-items-center rounded-full bg-white/6">
|
||
<item.Icon className="size-4 stroke-[1.7]" />
|
||
</span>
|
||
<span className="min-w-0">
|
||
<span className="block truncate text-13 font-medium text-primary">{item.label}</span>
|
||
<span className="block truncate text-11 text-tertiary">{item.description}</span>
|
||
</span>
|
||
</Link>
|
||
)}
|
||
</Menu.Item>
|
||
);
|
||
})}
|
||
</Menu.Items>
|
||
</Transition>
|
||
</Menu>
|
||
|
||
<a href="/" className="nodedc-admin-top-nav-item">
|
||
<ExternalLink className="size-3.5 stroke-[1.7]" />
|
||
<span>В приложение</span>
|
||
</a>
|
||
</nav>
|
||
|
||
<Menu as="div" className="relative justify-self-end">
|
||
<Menu.Button className="nodedc-admin-user-button">
|
||
<span className="min-w-0 text-right">
|
||
<span className="block max-w-40 truncate text-14 font-medium text-primary">{adminName}</span>
|
||
</span>
|
||
<span className="grid size-10 place-items-center rounded-full bg-white/7">
|
||
{currentUser ? (
|
||
<Avatar
|
||
name={avatarName}
|
||
src={getFileURL(currentUser.avatar_url)}
|
||
size={32}
|
||
shape="circle"
|
||
className="!text-body-sm-medium"
|
||
/>
|
||
) : (
|
||
<UserCog2 className="size-5 text-[rgb(var(--nodedc-card-active-rgb))]" />
|
||
)}
|
||
</span>
|
||
</Menu.Button>
|
||
<Transition
|
||
as={Fragment}
|
||
enter="transition ease-out duration-100"
|
||
enterFrom="transform opacity-0 scale-95"
|
||
enterTo="transform opacity-100 scale-100"
|
||
leave="transition ease-in duration-75"
|
||
leaveFrom="transform opacity-100 scale-100"
|
||
leaveTo="transform opacity-0 scale-95"
|
||
>
|
||
<Menu.Items className="nodedc-glass-popup-surface absolute top-full right-0 z-[120] mt-3 flex w-60 flex-col divide-y divide-white/6 p-2 text-12 outline-none">
|
||
<div className="flex flex-col gap-1 px-2 pb-2">
|
||
<span className="truncate text-13 font-medium text-primary">{adminName}</span>
|
||
<span className="truncate text-11 text-tertiary">{currentUser?.email}</span>
|
||
</div>
|
||
<div className="py-2">
|
||
<Menu.Item as="button" type="button" className="nodedc-admin-menu-action" onClick={handleThemeSwitch}>
|
||
<Palette className="h-4 w-4 stroke-[1.5]" />
|
||
{resolvedTheme === "dark" ? "Светлая тема" : "Темная тема"}
|
||
</Menu.Item>
|
||
</div>
|
||
<div className="py-2">
|
||
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`} onSubmit={signOut}>
|
||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||
<Menu.Item as="button" type="submit" className="nodedc-admin-menu-action">
|
||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||
Выйти
|
||
</Menu.Item>
|
||
</form>
|
||
</div>
|
||
</Menu.Items>
|
||
</Transition>
|
||
</Menu>
|
||
</div>
|
||
</header>
|
||
);
|
||
});
|