Files
EpicNext-Cms/src/lib/admin-nav.ts
T
openhands c6a6653803
Local Build and Deploy / deploy (push) Successful in 51s
Fix build: move store import from admin-nav to layout only
admin-nav.ts was importing from store.ts (server-only), which caused
build errors when admin-topbar.tsx (client component) pulled in admin-nav.
Now layout.tsx directly imports from store and passes enabled state to
allNavGroups().
2026-07-18 14:44:45 +02:00

547 lines
13 KiB
TypeScript

import type { LucideIcon } from "lucide-react";
import {
Ban,
Calendar,
Cog,
Compass,
Gavel,
LayoutDashboard,
Monitor,
Newspaper,
Puzzle,
Radio,
Settings,
Shield,
ShoppingCart,
Store,
Users,
Wrench,
} from "lucide-react";
import { pluginRegistry } from "@/lib/plugins";
import "@/lib/plugins/loader";
export interface AdminHubTab {
href: string;
/** Key under pages.admin.hubs.tabs.* */
labelKey: string;
match?: string[];
/** Optional group id for multi-row tab strips (e.g. Radio primary/tools). */
group?: string;
}
export interface AdminHubDefinition {
id: string;
/** Key under pages.admin.hubs.* for title/subtitle */
titleKey: string;
subtitleKey: string;
icon: LucideIcon;
/** Path prefixes belonging to this hub (for chrome + sidebar active). */
prefixes: string[];
tabs: AdminHubTab[];
}
export interface AdminNavItem {
href: string;
labelKey: string;
icon: LucideIcon;
/** When set, sidebar item is active if pathname matches any prefix. */
matchPrefixes?: string[];
}
export interface AdminNavGroup {
labelKey: string;
icon: LucideIcon;
items: AdminNavItem[];
}
export const ADMIN_HUBS: AdminHubDefinition[] = [
{
id: "news-media",
titleKey: "newsMedia",
subtitleKey: "newsMediaSubtitle",
icon: Newspaper,
prefixes: [
"/admin/articles",
"/admin/photos",
"/admin/banners",
"/admin/ads",
"/admin/media",
],
tabs: [
{
href: "/admin/articles",
labelKey: "articles",
match: ["/admin/articles"],
},
{ href: "/admin/photos", labelKey: "photos" },
{ href: "/admin/banners", labelKey: "banners" },
{ href: "/admin/ads", labelKey: "ads", match: ["/admin/ads"] },
{ href: "/admin/media", labelKey: "media" },
],
},
{
id: "site-content",
titleKey: "siteContent",
subtitleKey: "siteContentSubtitle",
icon: Compass,
prefixes: [
"/admin/navigation",
"/admin/help-questions",
"/admin/writeable-boxes",
"/admin/tags",
"/admin/prefixes",
],
tabs: [
{ href: "/admin/navigation", labelKey: "navigator" },
{
href: "/admin/help-questions",
labelKey: "help",
match: ["/admin/help-questions"],
},
{ href: "/admin/writeable-boxes", labelKey: "boxes" },
{ href: "/admin/tags", labelKey: "tags" },
{ href: "/admin/prefixes", labelKey: "prefixes" },
],
},
{
id: "engagement",
titleKey: "engagement",
subtitleKey: "engagementSubtitle",
icon: Calendar,
prefixes: ["/admin/events", "/admin/polls", "/admin/tickets"],
tabs: [
{ href: "/admin/events", labelKey: "events", match: ["/admin/events"] },
{ href: "/admin/events/types", labelKey: "eventTypes" },
{ href: "/admin/polls", labelKey: "polls", match: ["/admin/polls"] },
{
href: "/admin/tickets",
labelKey: "tickets",
match: ["/admin/tickets"],
},
{ href: "/admin/tickets/templates", labelKey: "templates" },
],
},
{
id: "users",
titleKey: "users",
subtitleKey: "usersSubtitle",
icon: Users,
prefixes: [
"/admin/users",
"/admin/online",
"/admin/applications",
"/admin/teams",
],
tabs: [
{
href: "/admin/users",
labelKey: "directory",
match: ["/admin/users"],
},
{ href: "/admin/users/multi-accounts", labelKey: "multiAccounts" },
{ href: "/admin/online", labelKey: "online" },
{ href: "/admin/applications", labelKey: "applications" },
{ href: "/admin/teams", labelKey: "teams" },
],
},
{
id: "access",
titleKey: "access",
subtitleKey: "accessSubtitle",
icon: Shield,
prefixes: [
"/admin/bans",
"/admin/ip",
"/admin/vpn",
"/admin/permissions",
"/admin/wordfilter",
],
tabs: [
{ href: "/admin/bans", labelKey: "bans" },
{ href: "/admin/ip", labelKey: "ip" },
{ href: "/admin/vpn", labelKey: "vpn" },
{
href: "/admin/permissions",
labelKey: "permissions",
match: ["/admin/permissions"],
},
{ href: "/admin/wordfilter", labelKey: "wordFilter" },
],
},
{
id: "moderation",
titleKey: "moderation",
subtitleKey: "moderationSubtitle",
icon: Gavel,
prefixes: ["/admin/moderation"],
tabs: [
{ href: "/admin/moderation", labelKey: "overview" },
{ href: "/admin/moderation/actions", labelKey: "actions" },
{
href: "/admin/moderation/cfh",
labelKey: "cfh",
match: ["/admin/moderation/cfh"],
},
{ href: "/admin/moderation/team", labelKey: "team" },
],
},
{
id: "catalog",
titleKey: "catalog",
subtitleKey: "catalogSubtitle",
icon: Store,
prefixes: [
"/admin/catalog",
"/admin/rare-values",
"/admin/badges",
"/admin/achievements",
"/admin/sounds",
],
tabs: [
{ href: "/admin/catalog", labelKey: "catalogTab", match: ["/admin/catalog"] },
{ href: "/admin/rare-values", labelKey: "rares" },
{ href: "/admin/badges", labelKey: "badges" },
{ href: "/admin/achievements", labelKey: "achievements" },
{ href: "/admin/sounds", labelKey: "sounds" },
],
},
{
id: "shop",
titleKey: "shop",
subtitleKey: "shopSubtitle",
icon: ShoppingCart,
prefixes: [
"/admin/shop",
"/admin/transactions",
"/admin/vouchers",
"/admin/subscriptions",
"/admin/calendar",
],
tabs: [
{ href: "/admin/shop", labelKey: "shopTab" },
{ href: "/admin/transactions", labelKey: "transactions" },
{ href: "/admin/vouchers", labelKey: "vouchers" },
{ href: "/admin/subscriptions", labelKey: "subscriptions" },
{
href: "/admin/calendar",
labelKey: "calendar",
match: ["/admin/calendar"],
},
],
},
{
id: "radio",
titleKey: "radio",
subtitleKey: "radioSubtitle",
icon: Radio,
prefixes: ["/admin/radio"],
tabs: [
{ href: "/admin/radio", labelKey: "overview", group: "primary" },
{ href: "/admin/radio/history", labelKey: "history", group: "primary" },
{
href: "/admin/radio/moderation",
labelKey: "moderationTab",
group: "primary",
},
{
href: "/admin/radio/monitoring",
labelKey: "monitoring",
group: "primary",
},
{ href: "/admin/radio/settings", labelKey: "settingsTab", group: "primary" },
{ href: "/admin/radio/banners", labelKey: "banners", group: "tools" },
{ href: "/admin/radio/embed", labelKey: "embed", group: "tools" },
{ href: "/admin/radio/api-keys", labelKey: "apiKeys", group: "tools" },
{ href: "/admin/radio/points", labelKey: "points", group: "tools" },
{ href: "/admin/radio/ranks", labelKey: "ranks", group: "tools" },
{ href: "/admin/radio/autodj", labelKey: "autoDj", group: "tools" },
],
},
{
id: "settings",
titleKey: "settings",
subtitleKey: "settingsSubtitle",
icon: Cog,
prefixes: [
"/admin/settings",
"/admin/theme",
"/admin/maintenance",
"/admin/favicon",
"/admin/emulator",
"/admin/email-templates",
"/admin/housekeeping",
"/admin/webhooks",
],
tabs: [
{ href: "/admin/settings", labelKey: "cms" },
{ href: "/admin/theme", labelKey: "theme" },
{ href: "/admin/maintenance", labelKey: "maintenance" },
{ href: "/admin/favicon", labelKey: "favicon" },
{ href: "/admin/emulator", labelKey: "emulator" },
{ href: "/admin/email-templates", labelKey: "email" },
{ href: "/admin/housekeeping", labelKey: "housekeeping" },
{ href: "/admin/webhooks", labelKey: "webhooks" },
],
},
{
id: "tools",
titleKey: "tools",
subtitleKey: "toolsSubtitle",
icon: Wrench,
prefixes: [
"/admin/commandocentrum",
"/admin/rooms",
"/admin/import",
"/admin/translations",
],
tabs: [
{ href: "/admin/commandocentrum", labelKey: "commando" },
{ href: "/admin/rooms", labelKey: "rooms", match: ["/admin/rooms"] },
{ href: "/admin/import", labelKey: "import", match: ["/admin/import"] },
{
href: "/admin/translations",
labelKey: "translations",
match: ["/admin/translations"],
},
],
},
{
id: "observability",
titleKey: "observability",
subtitleKey: "observabilitySubtitle",
icon: Monitor,
prefixes: [
"/admin/logs",
"/admin/analytics",
"/admin/devops",
"/admin/alerts",
],
tabs: [
{ href: "/admin/logs", labelKey: "logs" },
{ href: "/admin/logs/audit", labelKey: "audit" },
{ href: "/admin/logs/chat", labelKey: "chat" },
{ href: "/admin/logs/commands", labelKey: "commands" },
{ href: "/admin/logs/trades", labelKey: "trades" },
{
href: "/admin/analytics",
labelKey: "analytics",
match: ["/admin/analytics"],
},
{ href: "/admin/analytics/activity", labelKey: "activity" },
{ href: "/admin/analytics/economy", labelKey: "economy" },
{ href: "/admin/devops", labelKey: "devops", match: ["/admin/devops"] },
{ href: "/admin/devops/errors", labelKey: "errors" },
{ href: "/admin/alerts", labelKey: "alerts" },
],
},
{
id: "plugins",
titleKey: "plugins",
subtitleKey: "pluginsSubtitle",
icon: Puzzle,
prefixes: ["/admin/plugins"],
tabs: [
{ href: "/admin/plugins", labelKey: "plugins" },
],
},
];
export function allHubs(enabled?: Record<string, boolean>): AdminHubDefinition[] {
const allReg = pluginRegistry.getHubs();
const pluginHubs: AdminHubDefinition[] = [];
for (const h of allReg) {
const hubPlugin = pluginRegistry
.getAll()
.find((p) => (p.hubs ?? []).some((ph) => ph.id === h.id));
if (hubPlugin && enabled && enabled[hubPlugin.id] === false) continue;
pluginHubs.push({
id: h.id,
titleKey: h.titleKey,
subtitleKey: h.subtitleKey,
icon: h.icon,
prefixes: h.prefixes,
tabs: h.tabs.map((t) => ({
href: t.href,
labelKey: t.labelKey,
match: t.match,
group: t.group,
})),
});
}
return [...ADMIN_HUBS, ...pluginHubs];
}
export function findAdminHub(pathname: string, enabled?: Record<string, boolean>): AdminHubDefinition | null {
const hubs = allHubs(enabled);
return (
hubs.find((hub) =>
hub.prefixes.some((p) => pathname === p || pathname.startsWith(`${p}/`)),
) ?? null
);
}
export function allNavGroups(enabled?: Record<string, boolean>): AdminNavGroup[] {
const allReg = pluginRegistry.getNavGroups();
const pluginGroups: AdminNavGroup[] = [];
for (const g of allReg) {
const groupPlugin = pluginRegistry
.getAll()
.find((p) => (p.navGroups ?? []).some((pg) => pg.labelKey === g.labelKey));
if (groupPlugin && enabled && enabled[groupPlugin.id] === false) continue;
pluginGroups.push({
labelKey: g.labelKey,
icon: g.icon,
items: g.items.map((item) => ({
href: item.href,
labelKey: item.labelKey,
icon: item.icon,
matchPrefixes: item.matchPrefixes,
})),
});
}
if (pluginGroups.length === 0) return ADMIN_NAV_GROUPS;
return [
...ADMIN_NAV_GROUPS,
{
labelKey: "plugins",
icon: Puzzle,
items: pluginGroups.flatMap((g) => g.items),
},
];
}
function hubPrefixes(id: string): string[] {
return ADMIN_HUBS.find((h) => h.id === id)?.prefixes ?? [];
}
/** Condensed sidebar — one entry per hub (plus dashboard). */
export const ADMIN_NAV_GROUPS: AdminNavGroup[] = [
{
labelKey: "overview",
icon: LayoutDashboard,
items: [
{
href: "/admin",
labelKey: "dashboard",
icon: LayoutDashboard,
},
],
},
{
labelKey: "content",
icon: Newspaper,
items: [
{
href: "/admin/articles",
labelKey: "hubNewsMedia",
icon: Newspaper,
matchPrefixes: hubPrefixes("news-media"),
},
{
href: "/admin/navigation",
labelKey: "hubSiteContent",
icon: Compass,
matchPrefixes: hubPrefixes("site-content"),
},
{
href: "/admin/events",
labelKey: "hubEngagement",
icon: Calendar,
matchPrefixes: hubPrefixes("engagement"),
},
],
},
{
labelKey: "usersAndAccess",
icon: Shield,
items: [
{
href: "/admin/users",
labelKey: "hubUsers",
icon: Users,
matchPrefixes: hubPrefixes("users"),
},
{
href: "/admin/bans",
labelKey: "hubAccess",
icon: Ban,
matchPrefixes: hubPrefixes("access"),
},
{
href: "/admin/moderation",
labelKey: "moderation",
icon: Gavel,
matchPrefixes: hubPrefixes("moderation"),
},
],
},
{
labelKey: "economy",
icon: ShoppingCart,
items: [
{
href: "/admin/catalog",
labelKey: "hubCatalog",
icon: Store,
matchPrefixes: hubPrefixes("catalog"),
},
{
href: "/admin/shop",
labelKey: "hubShop",
icon: ShoppingCart,
matchPrefixes: hubPrefixes("shop"),
},
],
},
{
labelKey: "radio",
icon: Radio,
items: [
{
href: "/admin/radio",
labelKey: "radio",
icon: Radio,
matchPrefixes: ["/admin/radio"],
},
],
},
{
labelKey: "system",
icon: Settings,
items: [
{
href: "/admin/settings",
labelKey: "settings",
icon: Cog,
matchPrefixes: hubPrefixes("settings"),
},
{
href: "/admin/commandocentrum",
labelKey: "hubTools",
icon: Wrench,
matchPrefixes: hubPrefixes("tools"),
},
{
href: "/admin/logs",
labelKey: "hubObservability",
icon: Monitor,
matchPrefixes: hubPrefixes("observability"),
},
],
},
{
labelKey: "pluginsGroup",
icon: Puzzle,
items: [
{
href: "/admin/plugins",
labelKey: "hubPlugins",
icon: Puzzle,
matchPrefixes: ["/admin/plugins"],
},
],
},
];