9router/src/shared/components/Header.js

280 lines
8.8 KiB
JavaScript
Raw Normal View History

2026-01-04 21:58:59 -05:00
"use client";
import { usePathname, useRouter } from "next/navigation";
import { useMemo } from "react";
2026-01-04 21:58:59 -05:00
import Link from "next/link";
import PropTypes from "prop-types";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { ThemeToggle, LanguageSwitcher } from "@/shared/components";
import NineRemoteButton from "@/shared/components/NineRemoteButton";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config";
2026-04-09 23:17:53 -04:00
import { MEDIA_PROVIDER_KINDS, AI_PROVIDERS } from "@/shared/constants/providers";
import { translate } from "@/i18n/runtime";
2026-01-04 21:58:59 -05:00
const getPageInfo = (pathname) => {
if (!pathname) return { title: "", description: "", breadcrumbs: [] };
2026-04-09 23:17:53 -04:00
// Media provider detail: /dashboard/media-providers/[kind]/[id]
const mediaDetailMatch = pathname.match(/\/media-providers\/([^/]+)\/([^/]+)$/);
if (mediaDetailMatch) {
const kindId = mediaDetailMatch[1];
const providerId = mediaDetailMatch[2];
const kindConfig = MEDIA_PROVIDER_KINDS.find((k) => k.id === kindId);
const provider = AI_PROVIDERS[providerId];
return {
title: provider?.name || providerId,
description: "",
breadcrumbs: [
{ label: "Media Providers", href: `/dashboard/media-providers/${kindId}` },
{ label: kindConfig?.label || kindId, href: `/dashboard/media-providers/${kindId}` },
{ label: provider?.name || providerId, image: `/providers/${providerId}.png` },
],
};
}
// Media provider kind: /dashboard/media-providers/[kind]
const mediaKindMatch = pathname.match(/\/media-providers\/([^/]+)$/);
if (mediaKindMatch) {
const kindId = mediaKindMatch[1];
const kindConfig = MEDIA_PROVIDER_KINDS.find((k) => k.id === kindId);
return {
title: kindConfig?.label || kindId,
description: `Manage your ${kindConfig?.label || kindId} providers`,
icon: kindConfig?.icon || "perm_media",
breadcrumbs: [],
};
}
2026-01-04 21:58:59 -05:00
// Provider detail page: /dashboard/providers/[id]
const providerMatch = pathname.match(/\/providers\/([^/]+)$/);
if (providerMatch) {
const providerId = providerMatch[1];
const providerInfo =
OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId];
2026-01-04 21:58:59 -05:00
if (providerInfo) {
return {
title: providerInfo.name,
description: "",
breadcrumbs: [
{ label: "Providers", href: "/dashboard/providers" },
{
label: providerInfo.name,
image: `/providers/${providerInfo.id}.png`,
},
],
2026-01-04 21:58:59 -05:00
};
}
}
2026-04-09 23:17:53 -04:00
if (pathname.includes("/providers") && !pathname.includes("/media-providers"))
return {
title: "Providers",
description: "Manage your AI provider connections",
2026-03-31 04:41:52 -04:00
icon: "dns",
breadcrumbs: [],
};
if (pathname.includes("/combos"))
return {
title: "Combos",
description: "Model combos with fallback",
2026-03-31 04:41:52 -04:00
icon: "layers",
breadcrumbs: [],
};
if (pathname.includes("/usage"))
return {
title: "Usage & Analytics",
description:
"Monitor your API usage, token consumption, and request logs",
2026-03-31 04:41:52 -04:00
icon: "bar_chart",
breadcrumbs: [],
};
if (pathname.includes("/quota"))
return {
title: "Quota Tracker",
description: "Track and manage your API quota limits",
icon: "data_usage",
breadcrumbs: [],
};
if (pathname.includes("/mitm"))
return {
title: "MITM Proxy",
description: "Intercept CLI tool traffic and route through 9Router",
2026-03-31 04:41:52 -04:00
icon: "security",
breadcrumbs: [],
};
if (pathname.includes("/cli-tools"))
return {
title: "CLI Tools",
description: "Configure CLI tools",
2026-03-31 04:41:52 -04:00
icon: "terminal",
breadcrumbs: [],
};
if (pathname.includes("/proxy-pools"))
return {
title: "Proxy Pools",
description: "Manage your proxy pool configurations",
icon: "lan",
breadcrumbs: [],
};
if (pathname.includes("/endpoint"))
return {
title: "Endpoint",
description: "API endpoint configuration",
2026-03-31 04:41:52 -04:00
icon: "api",
breadcrumbs: [],
};
if (pathname.includes("/profile"))
return {
title: "Settings",
description: "Manage your preferences",
2026-03-31 04:41:52 -04:00
icon: "settings",
breadcrumbs: [],
};
if (pathname.includes("/translator"))
return {
title: "Translator",
description: "Debug translation flow between formats",
2026-03-31 04:41:52 -04:00
icon: "translate",
breadcrumbs: [],
};
if (pathname.includes("/console-log"))
return {
title: "Console Log",
description: "Live server console output",
2026-03-31 04:41:52 -04:00
icon: "monitor",
breadcrumbs: [],
};
if (pathname === "/dashboard")
return {
title: "Endpoint",
description: "API endpoint configuration",
2026-03-31 04:41:52 -04:00
icon: "api",
breadcrumbs: [],
};
2026-01-04 21:58:59 -05:00
return { title: "", description: "", breadcrumbs: [] };
};
export default function Header({ onMenuClick, showMenuButton = true }) {
const pathname = usePathname();
const router = useRouter();
// Memoize page info to prevent unnecessary recalculations
const pageInfo = useMemo(() => getPageInfo(pathname), [pathname]);
2026-03-31 04:41:52 -04:00
const { title, description, icon, breadcrumbs } = pageInfo;
2026-01-04 21:58:59 -05:00
const handleLogout = async () => {
try {
const res = await fetch("/api/auth/logout", { method: "POST" });
if (res.ok) {
router.push("/login");
router.refresh();
}
} catch (err) {
console.error("Failed to logout:", err);
}
};
2026-01-04 21:58:59 -05:00
return (
2026-01-31 00:58:04 -05:00
<header className="flex items-center justify-between px-8 py-5 border-b border-black/5 dark:border-white/5 bg-bg/80 backdrop-blur-xl z-10 sticky top-0">
2026-01-04 21:58:59 -05:00
{/* Mobile menu button */}
<div className="flex items-center gap-3 lg:hidden">
{showMenuButton && (
<button
onClick={onMenuClick}
className="text-text-main hover:text-primary transition-colors"
>
<span className="material-symbols-outlined">menu</span>
</button>
)}
</div>
{/* Page title with breadcrumbs - desktop */}
<div className="hidden lg:flex flex-col">
{breadcrumbs.length > 0 ? (
<div className="flex items-center gap-2">
{breadcrumbs.map((crumb, index) => (
<div
key={`${crumb.label}-${crumb.href || "current"}`}
className="flex items-center gap-2"
>
2026-01-04 21:58:59 -05:00
{index > 0 && (
<span className="material-symbols-outlined text-text-muted text-base">
chevron_right
</span>
)}
{crumb.href ? (
<Link
href={crumb.href}
className="text-text-muted hover:text-primary transition-colors"
>
{crumb.label}
</Link>
) : (
<div className="flex items-center gap-2">
{crumb.image && (
<ProviderIcon
2026-01-04 21:58:59 -05:00
src={crumb.image}
alt={crumb.label}
size={28}
2026-01-26 23:35:28 -05:00
className="object-contain rounded max-w-[28px] max-h-[28px]"
fallbackText={crumb.label.slice(0, 2).toUpperCase()}
2026-01-04 21:58:59 -05:00
/>
)}
<h1 className="text-2xl font-semibold text-text-main tracking-tight">
{translate(crumb.label)}
2026-01-04 21:58:59 -05:00
</h1>
</div>
)}
</div>
))}
</div>
) : title ? (
<div>
2026-03-31 04:41:52 -04:00
<div className="flex items-center gap-2">
{icon && (
<span className="material-symbols-outlined text-primary text-2xl">
{icon}
</span>
)}
<h1 className="text-2xl font-semibold tracking-tight">
{translate(title)}
</h1>
</div>
2026-01-04 21:58:59 -05:00
{description && (
<p className="text-sm text-text-muted">
{translate(description)}
</p>
2026-01-04 21:58:59 -05:00
)}
</div>
) : null}
</div>
{/* Right actions */}
<div className="flex items-center gap-3 ml-auto">
{/* 9Remote button */}
<NineRemoteButton />
{/* Language switcher */}
<LanguageSwitcher />
2026-01-04 21:58:59 -05:00
{/* Theme toggle */}
<ThemeToggle />
{/* Logout button */}
<button
onClick={handleLogout}
className="flex items-center justify-center p-2 rounded-lg text-text-muted hover:text-red-500 hover:bg-red-500/10 transition-all"
title="Logout"
>
<span className="material-symbols-outlined">logout</span>
</button>
2026-01-04 21:58:59 -05:00
</div>
</header>
);
}
Header.propTypes = {
onMenuClick: PropTypes.func,
showMenuButton: PropTypes.bool,
};