NODEDC_TASKMANAGER/plane-src/apps/admin/components/common/header/index.tsx

183 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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>
);
});