| "use client"; |
|
|
| import { useState, useEffect, useRef } from "react"; |
| import PropTypes from "prop-types"; |
| import { useTheme } from "@/shared/hooks/useTheme"; |
| import ChangelogModal from "./ChangelogModal"; |
| import { ConfirmModal } from "./Modal"; |
|
|
| function MenuItem({ icon, label, onClick, trailing, danger }) { |
| return ( |
| <button |
| onClick={onClick} |
| className={`flex items-center gap-3 w-full px-4 py-2.5 text-sm transition-colors ${ |
| danger |
| ? "text-red-500 hover:bg-red-500/10" |
| : "text-text-main hover:bg-black/5 dark:hover:bg-white/5" |
| }`} |
| > |
| <span className={`material-symbols-outlined text-[20px] ${danger ? "" : "text-text-muted"}`}> |
| {icon} |
| </span> |
| <span className="flex-1 text-left">{label}</span> |
| {trailing && <span className="text-base">{trailing}</span>} |
| </button> |
| ); |
| } |
|
|
| MenuItem.propTypes = { |
| icon: PropTypes.string.isRequired, |
| label: PropTypes.string.isRequired, |
| onClick: PropTypes.func.isRequired, |
| trailing: PropTypes.node, |
| danger: PropTypes.bool, |
| }; |
|
|
| export default function HeaderMenu({ onLogout }) { |
| const [isOpen, setIsOpen] = useState(false); |
| const [changelogOpen, setChangelogOpen] = useState(false); |
| const [shutdownOpen, setShutdownOpen] = useState(false); |
| const [isShuttingDown, setIsShuttingDown] = useState(false); |
| const { toggleTheme, isDark } = useTheme(); |
| const menuRef = useRef(null); |
|
|
| const handleShutdown = async () => { |
| setIsShuttingDown(true); |
| try { |
| await fetch("/api/version/shutdown", { method: "POST" }); |
| } catch (e) { |
| |
| } |
| setIsShuttingDown(false); |
| setShutdownOpen(false); |
| }; |
|
|
| useEffect(() => { |
| const handleClickOutside = (e) => { |
| if (menuRef.current && !menuRef.current.contains(e.target)) { |
| setIsOpen(false); |
| } |
| }; |
| if (isOpen) { |
| document.addEventListener("mousedown", handleClickOutside); |
| return () => document.removeEventListener("mousedown", handleClickOutside); |
| } |
| }, [isOpen]); |
|
|
| const close = () => setIsOpen(false); |
|
|
| return ( |
| <> |
| <div className="relative" ref={menuRef}> |
| <button |
| onClick={() => setIsOpen((v) => !v)} |
| className="flex items-center justify-center p-2 rounded-lg text-text-muted hover:text-text-main hover:bg-black/5 dark:hover:bg-white/5 transition-all" |
| title="Menu" |
| > |
| <span className="material-symbols-outlined">grid_view</span> |
| </button> |
| |
| {isOpen && ( |
| <div className="absolute right-0 top-full mt-2 w-60 bg-surface border border-black/10 dark:border-white/10 rounded-xl shadow-2xl z-50 animate-in fade-in zoom-in-95 duration-150 overflow-hidden py-1"> |
| <MenuItem |
| icon="history" |
| label="Change Log" |
| onClick={() => { close(); setChangelogOpen(true); }} |
| /> |
| <MenuItem |
| icon={isDark ? "light_mode" : "dark_mode"} |
| label="Theme" |
| onClick={() => { toggleTheme(); close(); }} |
| /> |
| <MenuItem |
| icon="power_settings_new" |
| label="Shutdown" |
| danger |
| onClick={() => { close(); setShutdownOpen(true); }} |
| /> |
| <MenuItem |
| icon="logout" |
| label="Logout" |
| danger |
| onClick={() => { close(); onLogout(); }} |
| /> |
| </div> |
| )} |
| </div> |
| |
| <ChangelogModal isOpen={changelogOpen} onClose={() => setChangelogOpen(false)} /> |
| <ConfirmModal |
| isOpen={shutdownOpen} |
| onClose={() => setShutdownOpen(false)} |
| onConfirm={handleShutdown} |
| title="Close Proxy" |
| message="Are you sure you want to close the proxy server?" |
| confirmText="Close" |
| cancelText="Cancel" |
| variant="danger" |
| loading={isShuttingDown} |
| /> |
| </> |
| ); |
| } |
|
|
| HeaderMenu.propTypes = { |
| onLogout: PropTypes.func.isRequired, |
| }; |
|
|