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:
parent
caae76e6b2
commit
d4ff6ba1a1
3 changed files with 63 additions and 19 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue