1
0
Fork 0

feat(frontend): add account navigation, qualification-gated sidebar sections, and callsign display

- Add Account entry to the sidebar and user menu
- Hide Intelligence and Infrastructure sections unless the user is qualified
- Make the sidebar content area scrollable
- Support showing the callsign in the user menu via preferences
This commit is contained in:
Jason Fraley 2026-08-11 23:25:33 -04:00
parent caae76e6b2
commit d4ff6ba1a1
3 changed files with 63 additions and 19 deletions

View file

@ -8,6 +8,7 @@ import { headers as nextHeaders } from "next/headers";
import { SiteHeader } from "@/components/frontend/SiteHeader"; import { SiteHeader } from "@/components/frontend/SiteHeader";
import { Metadata } from "next"; import { Metadata } from "next";
import { resolveLevel, type ResolvedLevel } from "@/utils/xp/resolveLevel"; import { resolveLevel, type ResolvedLevel } from "@/utils/xp/resolveLevel";
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification"; import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts"; import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts";
import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime"; import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime";
@ -36,10 +37,14 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
xp: 0, xp: 0,
}; };
let isIntelligence = false;
let isLogistics = false; let isLogistics = false;
if (user) { if (user) {
isLogistics = await hasLogisticsQualification(payload, user).catch(() => false); [isIntelligence, isLogistics] = await Promise.all([
hasIntelligenceQualification(payload, user).catch(() => false),
hasLogisticsQualification(payload, user).catch(() => false),
]);
const [profileRes, experienceRes] = await Promise.all([ const [profileRes, experienceRes] = await Promise.all([
payload.find({ payload.find({
collection: "profiles", collection: "profiles",
@ -80,6 +85,9 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
}} }}
xpLevel={xpLevel} xpLevel={xpLevel}
adminUrl={payloadConfig.routes.admin} adminUrl={payloadConfig.routes.admin}
hasIntelligence={isIntelligence}
hasLogistics={isLogistics}
showCallsign={user.preferences?.display?.showCallsign ?? false}
className="border-r" className="border-r"
/> />
<main className="min-h-full bg-background w-full"> <main className="min-h-full bg-background w-full">

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { Bolt, Send, Shield, Users, Warehouse, Crosshair, Backpack } from "lucide-react"; import { Bolt, Send, Shield, Users, UserCog, Warehouse, Crosshair, Backpack } from "lucide-react";
import { NavCore } from "@/components/frontend/blocks/NavCore"; import { NavCore } from "@/components/frontend/blocks/NavCore";
import { NavSecondary } from "@/components/frontend/blocks/NavSecondary"; import { NavSecondary } from "@/components/frontend/blocks/NavSecondary";
@ -104,6 +104,11 @@ const data = {
url: "/locker", url: "/locker",
icon: Backpack, icon: Backpack,
}, },
{
title: "Account",
url: "/account",
icon: UserCog,
},
], ],
}; };
@ -111,6 +116,9 @@ export function AppSidebar({
user, user,
xpLevel, xpLevel,
adminUrl, adminUrl,
hasIntelligence = true,
hasLogistics = true,
showCallsign = false,
...props ...props
}: { }: {
user: { user: {
@ -119,6 +127,9 @@ export function AppSidebar({
}; };
xpLevel: ResolvedLevel; xpLevel: ResolvedLevel;
adminUrl?: string; adminUrl?: string;
hasIntelligence?: boolean;
hasLogistics?: boolean;
showCallsign?: boolean;
} & React.ComponentProps<typeof Sidebar>) { } & React.ComponentProps<typeof Sidebar>) {
return ( return (
<Sidebar variant="inset" {...props}> <Sidebar variant="inset" {...props}>
@ -147,12 +158,29 @@ export function AppSidebar({
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent className="overflow-hidden">
<NavCore label="Platform" items={data.navMain} /> <div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto group-data-[collapsible=icon]:overflow-hidden">
<NavCore label="Intelligence" items={data.navIntelligence} /> <NavCore label="Platform" items={data.navMain} />
<NavCore label="Infrastructure" items={data.navLogistics} /> {hasIntelligence && (
<NavCore label="Personal" items={data.navPersonal} /> <NavCore
{/*<NavProjects projects={data.projects} />*/} label="Intelligence"
items={data.navIntelligence.map((i) =>
typeof i.items === "object" ? { ...i, items: [...i.items] } : { ...i },
)}
/>
)}
{hasLogistics && (
<NavCore
label="Infrastructure"
items={data.navLogistics.map((i) =>
typeof i.items === "object" ? { ...i, items: [...i.items] } : { ...i },
)}
/>
)}
<NavCore label="Personal" items={data.navPersonal} />
{/*<NavProjects projects={data.projects} />*/}
</div>
<hr />
<NavSecondary <NavSecondary
items={ items={
!adminUrl !adminUrl
@ -177,6 +205,7 @@ export function AppSidebar({
username: user?.username ?? "", username: user?.username ?? "",
avatar: "", avatar: "",
}} }}
showCallsign={showCallsign}
/> />
</SidebarFooter> </SidebarFooter>
</Sidebar> </Sidebar>

View file

@ -33,17 +33,22 @@ import {
export function NavUser({ export function NavUser({
user, user,
showCallsign = false,
}: { }: {
user: { user: {
displayName: string; displayName: string;
username: string; username: string;
avatar: string; avatar: string;
}; };
showCallsign?: boolean;
}) { }) {
const { isMobile } = useSidebar(); const { isMobile } = useSidebar();
const router = useRouter(); const router = useRouter();
const [loggingOut, setLoggingOut] = useState(false); const [loggingOut, setLoggingOut] = useState(false);
const primaryLine = showCallsign ? user.username : user.displayName;
const secondaryLine = showCallsign ? user.displayName : user.username;
const handleLogout = async () => { const handleLogout = async () => {
setLoggingOut(true); setLoggingOut(true);
try { try {
@ -71,8 +76,8 @@ export function NavUser({
<AvatarFallback className="rounded-lg">CN</AvatarFallback> <AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar> </Avatar>
<div className="grid flex-1 text-left text-sm leading-tight"> <div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.displayName}</span> <span className="truncate font-medium">{primaryLine}</span>
<span className="truncate text-xs text-muted-foreground">{user.username}</span> <span className="truncate text-xs text-muted-foreground">{secondaryLine}</span>
</div> </div>
<ChevronsUpDown className="ml-auto size-4" /> <ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton> </SidebarMenuButton>
@ -92,8 +97,8 @@ export function NavUser({
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="grid flex-1 text-left text-sm leading-tight"> <div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.displayName}</span> <span className="truncate font-medium">{primaryLine}</span>
<span className="truncate text-xs">{user.username}</span> <span className="truncate text-xs">{secondaryLine}</span>
</div> </div>
</div> </div>
</DropdownMenuLabel> </DropdownMenuLabel>
@ -114,14 +119,16 @@ export function NavUser({
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem> <DropdownMenuItem asChild>
<BadgeCheck /> <Link href="/account">
Account <BadgeCheck />
</DropdownMenuItem> Account
<DropdownMenuItem> </Link>
<Bell />
Notifications
</DropdownMenuItem> </DropdownMenuItem>
{/* <DropdownMenuItem> */}
{/* <Bell /> */}
{/* Notifications */}
{/* </DropdownMenuItem> */}
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => handleLogout()}> <DropdownMenuItem onSelect={() => handleLogout()}>