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

View file

@ -1,7 +1,7 @@
"use client";
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 { NavSecondary } from "@/components/frontend/blocks/NavSecondary";
@ -104,6 +104,11 @@ const data = {
url: "/locker",
icon: Backpack,
},
{
title: "Account",
url: "/account",
icon: UserCog,
},
],
};
@ -111,6 +116,9 @@ export function AppSidebar({
user,
xpLevel,
adminUrl,
hasIntelligence = true,
hasLogistics = true,
showCallsign = false,
...props
}: {
user: {
@ -119,6 +127,9 @@ export function AppSidebar({
};
xpLevel: ResolvedLevel;
adminUrl?: string;
hasIntelligence?: boolean;
hasLogistics?: boolean;
showCallsign?: boolean;
} & React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar variant="inset" {...props}>
@ -147,12 +158,29 @@ export function AppSidebar({
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<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="Intelligence" items={data.navIntelligence} />
<NavCore label="Infrastructure" items={data.navLogistics} />
{hasIntelligence && (
<NavCore
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
items={
!adminUrl
@ -177,6 +205,7 @@ export function AppSidebar({
username: user?.username ?? "",
avatar: "",
}}
showCallsign={showCallsign}
/>
</SidebarFooter>
</Sidebar>

View file

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