9router / src /shared /components /HeaderMenu.js
2api
feat: configurable stream stall timeout + per-provider UI
88c4c60
Raw
History Blame Contribute Delete
4.03 kB
"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) {
// Expected to fail as server shuts down; ignore error
}
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,
};