Files
9router/src/shared/components/Sidebar.js
2026-01-05 09:58:59 +07:00

172 lines
6.3 KiB
JavaScript

"use client";
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/shared/utils/cn";
import { APP_CONFIG } from "@/shared/constants/config";
import Button from "./Button";
import { ConfirmModal } from "./Modal";
const navItems = [
{ href: "/dashboard/endpoint", label: "Endpoint", icon: "api" },
{ href: "/dashboard/providers", label: "Providers", icon: "dns" },
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
];
const systemItems = [
{ href: "/dashboard/profile", label: "Settings", icon: "settings" },
];
export default function Sidebar({ onClose }) {
const pathname = usePathname();
const [showShutdownModal, setShowShutdownModal] = useState(false);
const [isShuttingDown, setIsShuttingDown] = useState(false);
const [isDisconnected, setIsDisconnected] = useState(false);
const isActive = (href) => {
if (href === "/dashboard/endpoint") {
return pathname === "/dashboard" || pathname.startsWith("/dashboard/endpoint");
}
return pathname.startsWith(href);
};
const handleShutdown = async () => {
setIsShuttingDown(true);
try {
await fetch("/api/shutdown", { method: "POST" });
} catch (e) {
// Expected to fail as server shuts down
}
setIsShuttingDown(false);
setShowShutdownModal(false);
setIsDisconnected(true);
};
return (
<>
<aside className="flex w-72 flex-col border-r border-border bg-sidebar transition-colors duration-300">
{/* Logo */}
<div className="p-8">
<Link href="/dashboard" className="flex items-center gap-3">
<div className="flex items-center justify-center size-9 rounded bg-gradient-to-br from-[#f97815] to-[#c2590a]">
<span className="material-symbols-outlined text-white text-[20px]">hub</span>
</div>
<h1 className="text-lg font-semibold tracking-tight text-text-main">
{APP_CONFIG.name}
</h1>
</Link>
</div>
{/* Navigation */}
<nav className="flex-1 px-4 py-2 space-y-1 overflow-y-auto custom-scrollbar">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
onClick={onClose}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-lg transition-all group",
isActive(item.href)
? "bg-surface text-primary shadow-sm border border-border"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
)}
>
<span
className={cn(
"material-symbols-outlined text-[20px]",
isActive(item.href) ? "fill-1" : "group-hover:text-primary transition-colors"
)}
>
{item.icon}
</span>
<span className="text-sm font-medium">{item.label}</span>
</Link>
))}
{/* System section */}
<div className="pt-6 mt-2">
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-3">
System
</p>
{systemItems.map((item) => (
<Link
key={item.href}
href={item.href}
onClick={onClose}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-lg transition-all group",
isActive(item.href)
? "bg-surface text-primary shadow-sm border border-border"
: "text-text-muted hover:bg-surface/50 hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[20px] group-hover:text-primary transition-colors">
{item.icon}
</span>
<span className="text-sm font-medium">{item.label}</span>
</Link>
))}
</div>
</nav>
{/* Footer section */}
<div className="p-4 border-t border-border">
{/* Info message */}
<div className="flex items-start gap-3 p-3 rounded-xl bg-surface border border-border mb-3">
<div className="flex items-center justify-center size-8 rounded-lg bg-blue-500/10 text-blue-500 shrink-0 mt-0.5">
<span className="material-symbols-outlined text-[18px]">info</span>
</div>
<div className="flex flex-col">
<span className="text-xs font-medium text-text-main leading-relaxed">
Service is running in terminal. You can close this web page. Shutdown will stop the service.
</span>
</div>
</div>
{/* Shutdown button */}
<Button
variant="outline"
fullWidth
icon="power_settings_new"
onClick={() => setShowShutdownModal(true)}
className="text-red-500 border-red-200 hover:bg-red-50 hover:border-red-300"
>
Shutdown
</Button>
</div>
</aside>
{/* Shutdown Confirmation Modal */}
<ConfirmModal
isOpen={showShutdownModal}
onClose={() => setShowShutdownModal(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}
/>
{/* Disconnected Overlay */}
{isDisconnected && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="text-center p-8">
<div className="flex items-center justify-center size-16 rounded-full bg-red-500/20 text-red-500 mx-auto mb-4">
<span className="material-symbols-outlined text-[32px]">power_off</span>
</div>
<h2 className="text-xl font-semibold text-white mb-2">Server Disconnected</h2>
<p className="text-text-muted mb-6">The proxy server has been stopped.</p>
<Button variant="secondary" onClick={() => window.location.reload()}>
Reload Page
</Button>
</div>
</div>
)}
</>
);
}