refactor(nav): sidebar minigames group, inline admin link, hidden map entry
Move minigame links from the user dropdown into a NavMinigames sidebar group, inline the admin link in AppSidebar via a new canAccessAdminPanel helper, guard NavCore against empty groups, drop the feedback secondary link, and add an empty navMap slot with a restore comment so the World Map ships hidden while /map stays live. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
fcf220b961
commit
ad2fbb167a
7 changed files with 131 additions and 78 deletions
|
|
@ -11,6 +11,7 @@ import { Metadata } from "next";
|
|||
import { type ResolvedLevel, resolveLevel } from "@/utils/xp/resolveLevel";
|
||||
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
||||
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
|
||||
import { canAccessAdminPanel } from "@/utils/access-control/hasPermission";
|
||||
import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts";
|
||||
import { MissionReminderToasts } from "@/components/frontend/intelligence/MissionReminderToasts";
|
||||
import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime";
|
||||
|
|
@ -77,6 +78,8 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
|||
const shimsGlobal = await payload.findGlobal({ slug: "shims" });
|
||||
const shimEntries = (shimsGlobal.shims ?? []) as NonNullable<Shim["shims"]>;
|
||||
|
||||
let isAdmin = false;
|
||||
|
||||
if (user) {
|
||||
const [intelligenceQual, logisticsQual, profileRes, experienceRes, personalAccountRes] =
|
||||
await Promise.all([
|
||||
|
|
@ -121,6 +124,8 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
|||
xpLevel = resolveLevel(userXp, experienceRes.docs);
|
||||
|
||||
currentRank = (profileRes.docs[0]?.rank as Rank | null | undefined)?.abbreviation ?? "";
|
||||
|
||||
isAdmin = await canAccessAdminPanel(payload, user);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -141,7 +146,7 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
|||
rank: currentRank,
|
||||
}}
|
||||
xpLevel={xpLevel}
|
||||
adminUrl={payloadConfig.routes.admin}
|
||||
adminUrl={isAdmin ? payloadConfig.routes.admin : undefined}
|
||||
hasIntelligence={isIntelligence}
|
||||
hasLogistics={isLogistics}
|
||||
showCallsign={user.preferences?.display?.showCallsign ?? false}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from "lucide-react";
|
||||
|
||||
import { NavCore } from "@/components/frontend/blocks/NavCore";
|
||||
import { NavSecondary } from "@/components/frontend/blocks/NavSecondary";
|
||||
import { NavMinigames } from "@/components/frontend/blocks/NavMinigames";
|
||||
import { NavUser } from "@/components/frontend/blocks/NavUser";
|
||||
import {
|
||||
Sidebar,
|
||||
|
|
@ -149,6 +149,7 @@ export function AppSidebar({
|
|||
<SidebarContent className="overflow-hidden">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto group-data-[collapsible=icon]:overflow-hidden">
|
||||
<NavCore label="Platform" items={data.navMain} />
|
||||
<NavCore label="Map" items={data.navMap} />
|
||||
{hasIntelligence && (
|
||||
<NavCore label="Intelligence" items={data.navIntelligence} />
|
||||
)}
|
||||
|
|
@ -162,14 +163,19 @@ export function AppSidebar({
|
|||
{/*<NavProjects projects={data.projects} />*/}
|
||||
</div>
|
||||
<hr />
|
||||
<NavSecondary
|
||||
items={
|
||||
adminUrl
|
||||
? [...data.navSecondary, { title: "Admin", url: adminUrl, icon: Shield }]
|
||||
: data.navSecondary
|
||||
}
|
||||
className="mt-auto"
|
||||
/>
|
||||
<SidebarMenu className="px-1">
|
||||
{adminUrl && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="sm" asChild>
|
||||
<Link href={adminUrl}>
|
||||
<Shield />
|
||||
<span>Admin</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)}
|
||||
<div className="pb-1"><NavMinigames /></div>
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<div className="flex flex-col">
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ export function NavCore({
|
|||
}[];
|
||||
}[];
|
||||
}) {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{label}</SidebarGroupLabel>
|
||||
|
|
|
|||
83
src/components/frontend/blocks/NavMinigames.tsx
Normal file
83
src/components/frontend/blocks/NavMinigames.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"use client";
|
||||
|
||||
import { Bird, Bomb, ChevronRight, Club, Crosshair, FileTextIcon, Gamepad2, Radio, Spade } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
const coreGames = [
|
||||
{ title: "Flight Simulator", url: "/minigames/flappy", icon: Bird },
|
||||
{ title: "Minefield Clearance", url: "/minigames/eod", icon: Bomb },
|
||||
{ title: "Radio Traffic", url: "/minigames/radio", icon: Radio },
|
||||
{ title: "Qualification Course", url: "/minigames/qualification", icon: Crosshair },
|
||||
{ title: "OPORD Recall", url: "/minigames/opord", icon: FileTextIcon },
|
||||
];
|
||||
|
||||
const cardtableGames = [
|
||||
{ title: "Poker", url: "/minigames/poker", icon: Spade },
|
||||
{ title: "Blackjack", url: "/minigames/blackjack", icon: Club },
|
||||
];
|
||||
|
||||
export function NavMinigames() {
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton size="sm">
|
||||
<Gamepad2 />
|
||||
<span>Minigames</span>
|
||||
<ChevronRight className="ml-auto size-4 text-sidebar-foreground/50" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Core</DropdownMenuLabel>
|
||||
{coreGames.map((game) => (
|
||||
<DropdownMenuItem key={game.url} asChild>
|
||||
<Link href={game.url}>
|
||||
<game.icon />
|
||||
{game.title}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="flex flex-col">
|
||||
<span>Cardtable</span>
|
||||
<span className="text-xs font-normal text-muted-foreground">Earn chips to spend by playing Core minigames!</span>
|
||||
</DropdownMenuLabel>
|
||||
{cardtableGames.map((game) => (
|
||||
<DropdownMenuItem key={game.url} asChild>
|
||||
<Link href={game.url}>
|
||||
<game.icon />
|
||||
{game.title}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,17 +2,10 @@
|
|||
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bird,
|
||||
Bomb,
|
||||
ChevronsUpDown,
|
||||
Crosshair,
|
||||
FileTextIcon,
|
||||
Gamepad2,
|
||||
LogOut,
|
||||
Mic,
|
||||
Radio,
|
||||
Spade,
|
||||
Club,
|
||||
Users2,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
|
@ -27,9 +20,6 @@ import {
|
|||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
|
|
@ -139,56 +129,6 @@ export function NavUser({
|
|||
Service Record
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Gamepad2 />
|
||||
Exercises
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/minigames/flappy">
|
||||
<Bird />
|
||||
Flight Simulator
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/minigames/eod">
|
||||
<Bomb />
|
||||
Minefield Clearance
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/minigames/radio">
|
||||
<Radio />
|
||||
Radio Traffic
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/minigames/qualification">
|
||||
<Crosshair />
|
||||
Qualification Course
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/minigames/opord">
|
||||
<FileTextIcon />
|
||||
OPORD Recall
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/minigames/poker">
|
||||
<Spade />
|
||||
Poker
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/minigames/blackjack">
|
||||
<Club />
|
||||
Blackjack
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
Package,
|
||||
ScrollText,
|
||||
Search,
|
||||
Send,
|
||||
ShoppingBag,
|
||||
Swords,
|
||||
TrendingUp,
|
||||
|
|
@ -24,6 +23,10 @@ import {
|
|||
} from "lucide-react";
|
||||
|
||||
export const sidebarData = {
|
||||
// World Map hidden until the feature ships; the /map route and all of its
|
||||
// functionality stay live for direct-URL access. Restore with:
|
||||
// [{ title: "World Map", url: "/map", icon: MapIcon }] (and the MapIcon import).
|
||||
navMap: [],
|
||||
navHelpdesk: [
|
||||
{
|
||||
title: "Helpdesk",
|
||||
|
|
@ -157,11 +160,5 @@ export const sidebarData = {
|
|||
icon: ScrollText,
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Feedback",
|
||||
url: "https://onyxsimple.youtrack.cloud",
|
||||
icon: Send,
|
||||
},
|
||||
],
|
||||
navSecondary: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -227,6 +227,26 @@ export function requireCampaignOwnership(permission: Permission) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a user can access the Payload admin panel.
|
||||
*
|
||||
* A user has admin panel access if they are a superuser OR hold any
|
||||
* `admin:<slug>:manage` permission (which is what `requireAdminPageAccess`
|
||||
* requires for any collection).
|
||||
*/
|
||||
export async function canAccessAdminPanel(
|
||||
payload: Payload,
|
||||
user: UserLike | null | undefined,
|
||||
): Promise<boolean> {
|
||||
if (!user) return false;
|
||||
const { permissions, isSuperuser } = await loadUserPermissions(payload, user);
|
||||
if (isSuperuser) return true;
|
||||
for (const p of permissions) {
|
||||
if (p.startsWith("admin:") && p.endsWith(":manage")) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the ids of every user holding a superuser role.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue