1
0
Fork 0
polaris-task-force/src/components/frontend/blocks/AppSidebar.tsx
Z8MB1E 0f7d3a57e3 feat(personnel): add npc contacts directory and statistics pages
Adds a Personnel nav group with Contacts (searchable NPC directory with profile pages showing listings and employment) and Statistics pages; moves Wiki under Intelligence and checks off the corresponding TODO item.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-06 01:24:14 -04:00

217 lines
7.2 KiB
TypeScript

"use client";
import * as React from "react";
import {
ArrowLeftRight,
Bolt,
Shield,
Users,
Maximize,
Minimize,
SparkleIcon,
} from "lucide-react";
import { NavCore } from "@/components/frontend/blocks/NavCore";
import { NavSecondary } from "@/components/frontend/blocks/NavSecondary";
import { NavUser } from "@/components/frontend/blocks/NavUser";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import Image from "next/image";
import Link from "next/link";
import XPDisplay from "@/components/frontend/blocks/XPDisplay";
import type { ResolvedLevel } from "@/utils/xp/resolveLevel";
import type { User } from "@/payload-types";
import { Progress } from "@/components/ui/progress";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { updatePreferences } from "@/app/(frontend)/account/actions";
import { sidebarData } from "@/components/frontend/blocks/sidebarData";
const data = {
navMain: [
{
title: "Dashboard",
url: "/",
icon: Bolt,
},
{
title: "Roster",
url: "/roster",
icon: Users,
},
{
title: "Transfers",
url: "/transfers",
icon: ArrowLeftRight,
},
],
...sidebarData,
};
export function AppSidebar({
user,
xpLevel,
adminUrl,
hasIntelligence = true,
hasLogistics = true,
showCallsign = false,
canImpersonate = false,
preferences,
...props
}: {
user: {
displayName: string;
username: string;
rank: string;
};
xpLevel: ResolvedLevel;
adminUrl?: string;
hasIntelligence?: boolean;
hasLogistics?: boolean;
showCallsign?: boolean;
canImpersonate?: boolean;
preferences?: NonNullable<User["preferences"]>;
} & React.ComponentProps<typeof Sidebar>) {
const [isMiniMode, setIsMiniMode] = React.useState<boolean>(
preferences?.display?.xpDisplayMini ?? false,
);
// Re-sync when the saved preference changes (e.g. account page save + router.refresh()).
const savedXpDisplayMini = preferences?.display?.xpDisplayMini ?? false;
React.useEffect(() => {
setIsMiniMode(savedXpDisplayMini);
}, [savedXpDisplayMini]);
const handleToggleXPMode = React.useCallback(async () => {
try {
await updatePreferences({
mutedAll: preferences?.notifications?.mutedAll ?? false,
mutedTypes: preferences?.notifications?.mutedTypes ?? [],
showCallsign: preferences?.display?.showCallsign ?? false,
showVersionOverlay: preferences?.display?.showVersionOverlay ?? true,
showBackgroundTexture: preferences?.display?.showBackgroundTexture ?? true,
leadOrActual: preferences?.display?.leadOrActual ?? "lead",
discordEnabled: preferences?.discord?.enabled ?? false,
discordMutedTypes: preferences?.discord?.mutedTypes ?? [],
xpDisplayMini: !isMiniMode,
});
setIsMiniMode(!isMiniMode);
} catch (error) {
console.error("Failed to update XP display preference:", error);
}
}, [isMiniMode, preferences]);
return (
<Sidebar variant="inset" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<Link href="/">
{/*<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<Command className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">Polaris Task Force</span>
<span className="truncate text-xs">Enterprise</span>
</div>*/}
<div>
<Image
src="/images/logo.webp"
alt="Polaris Task Force"
width={1372}
height={164}
/>
</div>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<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} />
{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="Helpdesk" items={data.navHelpdesk} />
<NavCore label="Commerce" items={data.navCommerce} />
<NavCore label="Personnel" items={data.navPersonnel} />
<NavCore label="Personal" items={data.navPersonal} />
{/*<NavProjects projects={data.projects} />*/}
</div>
<hr />
<NavSecondary
items={
adminUrl
? [...data.navSecondary, { title: "Admin", url: adminUrl, icon: Shield }]
: data.navSecondary
}
className="mt-auto"
/>
</SidebarContent>
<SidebarFooter>
<div className="flex flex-col">
<div className="flex items-center justify-between">
{isMiniMode ? (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger>
<div className="text-xs text-muted-foreground uppercase">
{xpLevel.levelName}
</div>
</TooltipTrigger>
<TooltipContent>
<p>{xpLevel.levelDesc}</p>
</TooltipContent>
</Tooltip>
<SparkleIcon className="w-4 h-4" />
<span className="text-xs font-bold">{xpLevel.xp.toLocaleString()}</span>
<Progress value={xpLevel.progress} className="w-20 h-1" />
</div>
) : (
<XPDisplay xpLevel={xpLevel} />
)}
<button
onClick={handleToggleXPMode}
className="p-1 rounded-md hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors border border-transparent hover:border-sidebar-border"
aria-label={isMiniMode ? "Expand XP Display" : "Minimize XP Display"}
>
{isMiniMode ? <Maximize size={16} /> : <Minimize size={16} />}
</button>
</div>
</div>
<NavUser
user={{
displayName: user?.displayName
? [user?.rank ? `(${user?.rank}.)` : null, user?.displayName].join(" ")
: "Not logged in",
username: user?.username ?? "",
avatar: "",
}}
showCallsign={showCallsign}
canImpersonate={canImpersonate}
/>
</SidebarFooter>
</Sidebar>
);
}