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 { 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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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()}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue