import { useCallback, useMemo, useState } from 'react'; import type { ComponentType } from 'react'; import { useTranslation } from 'react-i18next'; import { Drawer, Layout, Menu } from 'antd'; import type { MenuProps } from 'antd'; import { ApiOutlined, ClusterOutlined, CloseOutlined, DashboardOutlined, HeartOutlined, LogoutOutlined, MenuOutlined, SettingOutlined, TeamOutlined, ToolOutlined, UserOutlined, } from '@ant-design/icons'; import { HttpUtil } from '@/utils'; import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme'; import './AppSidebar.css'; const SIDEBAR_COLLAPSED_KEY = 'isSidebarCollapsed'; const DONATE_URL = 'https://donate.sanaei.dev/'; interface AppSidebarProps { basePath?: string; requestUri?: string; } type IconName = 'dashboard' | 'user' | 'team' | 'setting' | 'tool' | 'cluster' | 'logout' | 'apidocs'; const iconByName: Record = { dashboard: DashboardOutlined, user: UserOutlined, team: TeamOutlined, setting: SettingOutlined, tool: ToolOutlined, cluster: ClusterOutlined, logout: LogoutOutlined, apidocs: ApiOutlined, }; function readCollapsed(): boolean { try { return JSON.parse(localStorage.getItem(SIDEBAR_COLLAPSED_KEY) || 'false'); } catch { return false; } } function DonateButton({ ariaLabel }: { ariaLabel: string }) { return ( ); } function ThemeCycleButton({ id, isDark, isUltra, onCycle, ariaLabel }: { id: string; isDark: boolean; isUltra: boolean; onCycle: () => void; ariaLabel: string; }) { return ( ); } export default function AppSidebar({ basePath = '', requestUri = '' }: AppSidebarProps) { const { t } = useTranslation(); const { isDark, isUltra, toggleTheme, toggleUltra } = useTheme(); const [collapsed, setCollapsed] = useState(() => readCollapsed()); const [drawerOpen, setDrawerOpen] = useState(false); const prefix = basePath.startsWith('/') ? basePath : `/${basePath || ''}`; const currentTheme: 'light' | 'dark' = isDark ? 'dark' : 'light'; const panelVersion = window.X_UI_CUR_VER || ''; const tabs = useMemo<{ key: string; icon: IconName; title: string }[]>(() => [ { key: `${prefix}panel/`, icon: 'dashboard', title: t('menu.dashboard') }, { key: `${prefix}panel/inbounds`, icon: 'user', title: t('menu.inbounds') }, { key: `${prefix}panel/clients`, icon: 'team', title: t('menu.clients') }, { key: `${prefix}panel/nodes`, icon: 'cluster', title: t('menu.nodes') }, { key: `${prefix}panel/settings`, icon: 'setting', title: t('menu.settings') }, { key: `${prefix}panel/xray`, icon: 'tool', title: t('menu.xray') }, { key: `${prefix}panel/api-docs`, icon: 'apidocs', title: t('menu.apiDocs') }, { key: 'logout', icon: 'logout', title: t('logout') }, ], [prefix, t]); const navItems = useMemo(() => tabs.filter((tab) => tab.icon !== 'logout'), [tabs]); const utilItems = useMemo(() => tabs.filter((tab) => tab.icon === 'logout'), [tabs]); const toMenuItems = useCallback((items: typeof tabs): MenuProps['items'] => items.map((tab) => { const Icon = iconByName[tab.icon]; return { key: tab.key, icon: , label: tab.title, }; }), []); const openLink = useCallback(async (key: string) => { if (key === 'logout') { await HttpUtil.post('/logout'); window.location.href = basePath || '/'; return; } if (key.startsWith('http')) { window.open(key); } else { window.location.href = key; } }, [basePath]); const onMenuClick = useCallback>(({ key }) => { openLink(String(key)); }, [openLink]); const onSiderCollapse = useCallback((isCollapsed: boolean, type: 'clickTrigger' | 'responsive') => { if (type === 'clickTrigger') { localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(isCollapsed)); setCollapsed(isCollapsed); } }, []); const cycleTheme = useCallback((id: string) => { pauseAnimationsUntilLeave(id); if (!isDark) { toggleTheme(); if (isUltra) toggleUltra(); } else if (!isUltra) { toggleUltra(); } else { toggleUltra(); toggleTheme(); } }, [isDark, isUltra, toggleTheme, toggleUltra]); return (
{collapsed ? '3X' : '3X-UI'} {!collapsed && panelVersion && ( v{panelVersion} )}
{!collapsed && (
cycleTheme('theme-cycle')} ariaLabel={t('menu.theme')} />
)}
setDrawerOpen(false)} >
3X-UI {panelVersion && v{panelVersion}}
cycleTheme('theme-cycle-drawer')} ariaLabel={t('menu.theme')} />
{ onMenuClick(info); setDrawerOpen(false); }} /> { onMenuClick(info); setDrawerOpen(false); }} /> {!drawerOpen && ( )}
); }