diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index 0802264..58d9a75 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -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; + 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} diff --git a/src/components/frontend/blocks/AppSidebar.tsx b/src/components/frontend/blocks/AppSidebar.tsx index 5f3140b..5a510cb 100644 --- a/src/components/frontend/blocks/AppSidebar.tsx +++ b/src/components/frontend/blocks/AppSidebar.tsx @@ -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({
+ {hasIntelligence && ( )} @@ -162,14 +163,19 @@ export function AppSidebar({ {/**/}

- + + {adminUrl && ( + + + + + Admin + + + + )} +
+
diff --git a/src/components/frontend/blocks/NavCore.tsx b/src/components/frontend/blocks/NavCore.tsx index 184a80d..325a10f 100644 --- a/src/components/frontend/blocks/NavCore.tsx +++ b/src/components/frontend/blocks/NavCore.tsx @@ -32,6 +32,8 @@ export function NavCore({ }[]; }[]; }) { + if (items.length === 0) return null; + return ( {label} diff --git a/src/components/frontend/blocks/NavMinigames.tsx b/src/components/frontend/blocks/NavMinigames.tsx new file mode 100644 index 0000000..36f9aa4 --- /dev/null +++ b/src/components/frontend/blocks/NavMinigames.tsx @@ -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 ( + + + + + + Minigames + + + + + + Core + {coreGames.map((game) => ( + + + + {game.title} + + + ))} + + + + + Cardtable + Earn chips to spend by playing Core minigames! + + {cardtableGames.map((game) => ( + + + + {game.title} + + + ))} + + + + + ); +} diff --git a/src/components/frontend/blocks/NavUser.tsx b/src/components/frontend/blocks/NavUser.tsx index 69b7d8b..f7496f4 100644 --- a/src/components/frontend/blocks/NavUser.tsx +++ b/src/components/frontend/blocks/NavUser.tsx @@ -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 - - - - Exercises - - - - - - Flight Simulator - - - - - - Minefield Clearance - - - - - - Radio Traffic - - - - - - Qualification Course - - - - - - OPORD Recall - - - - - - Poker - - - - - - Blackjack - - - - diff --git a/src/components/frontend/blocks/sidebarData.ts b/src/components/frontend/blocks/sidebarData.ts index 5f0b3ce..6a3b435 100644 --- a/src/components/frontend/blocks/sidebarData.ts +++ b/src/components/frontend/blocks/sidebarData.ts @@ -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: [], }; diff --git a/src/utils/access-control/hasPermission.ts b/src/utils/access-control/hasPermission.ts index 2658d01..d7ca099 100644 --- a/src/utils/access-control/hasPermission.ts +++ b/src/utils/access-control/hasPermission.ts @@ -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::manage` permission (which is what `requireAdminPageAccess` + * requires for any collection). + */ +export async function canAccessAdminPanel( + payload: Payload, + user: UserLike | null | undefined, +): Promise { + 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. *