Local Build and Deploy / deploy (push) Successful in 51s
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().
547 lines
13 KiB
TypeScript
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"],
|
|
},
|
|
],
|
|
},
|
|
];
|