1
0
Fork 0
polaris-task-force/src/app/(frontend)/layout.tsx

209 lines
8.1 KiB
TypeScript

import React from "react";
import "./styles.css";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/frontend/blocks/AppSidebar";
import { getPayload } from "payload";
import config from "@payload-config";
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
import { headers as nextHeaders } from "next/headers";
import { SiteHeader } from "@/components/frontend/SiteHeader";
import { Metadata } from "next";
import { type ResolvedLevel, resolveLevel } from "@/utils/xp/resolveLevel";
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
import { canAccessAdminPanel } from "@/utils/access-control/divisionAccess";
import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts";
import { MissionReminderToasts } from "@/components/frontend/intelligence/MissionReminderToasts";
import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime";
import { SessionWatchdog } from "@/components/frontend/session/SessionWatchdog";
import { getSessionExpMs } from "@/lib/session/token";
import VersionOverlay from "@/components/static/VersionOverlay";
import { LandingPage } from "@/components/frontend/LandingPage";
import { Toaster } from "@/components/ui/sonner";
import { CommandPalette } from "@/components/command-palette/CommandPalette";
import { CommandPaletteProvider } from "@/components/command-palette/CommandPaletteContext";
import { KeyboardShortcutsProvider } from "@/components/command-palette/KeyboardShortcutsProvider";
import { isSuperuser } from "@/utils/access-control/hasPermission";
import { Rank, Shim } from "@/payload-types";
import { cookies } from "next/headers";
import { IMPERSONATION_ACTIVE_COOKIE } from "@/lib/impersonation";
import { ImpersonationBanner } from "@/components/frontend/impersonation/ImpersonationBanner";
import { ShimLoader } from "@/components/frontend/shims/ShimLoader";
import { DevDashboardGate } from "@/components/frontend/dev/DevDashboardGate";
// Skip static prerendering of this layout (and all pages under (frontend)/).
// The layout calls `getPayload()` at render time to resolve the current user,
// which connects to PostgreSQL. With force-dynamic, Next.js skips the
// build-time prerender pass (which would try to init Payload and hit the DB)
// and instead server-renders at request time. Without this, `next build`
// inside a container with no reachable DATABASE_URI fails.
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
description: "A gamified platform for Polaris Task Force, an Arma 3 unit.",
title: "Polaris Task Force",
icons: [
{
url: "/favicon.webp",
type: "image/webp",
rel: "icon",
},
],
};
export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props;
const headers = await nextHeaders();
const payloadConfig = await config;
const payload = await getPayload({ config });
const { user: authUser } = await payload.auth({ headers, canSetHeaders: false });
const user = isPayloadUser(authUser) ? authUser : null;
const sessionExp = getSessionExpMs(headers);
const impersonating = (await cookies()).has(IMPERSONATION_ACTIVE_COOKIE);
let xpLevel: ResolvedLevel = {
levelName: "Recruit",
levelDesc: "",
progress: 0,
currentLevelXP: 0,
nextLevelXP: null,
xp: 0,
};
let isIntelligence = false;
let isLogistics = false;
let currentRank = "";
let hasPersonalBankAccount = false;
const shimsGlobal = await payload.findGlobal({ slug: "shims" });
const shimEntries = (shimsGlobal.shims ?? []) as NonNullable<Shim["shims"]>;
let isAdmin = false;
let statisticsEnabled = true;
let helpdeskEnabled = true;
if (user) {
const [
intelligenceQual,
logisticsQual,
profileRes,
experienceRes,
personalAccountRes,
gameRulesRes,
] = await Promise.all([
hasIntelligenceQualification(payload, user).catch(() => false),
hasLogisticsQualification(payload, user).catch(() => false),
payload.find({
collection: "profiles",
where: { user: { equals: user.id } },
select: {
rank: true,
progression: {
awards: true,
scoreboard: true,
xp: true,
minigames: true,
},
},
}),
payload.find({
collection: "experience",
sort: "requiredPoints",
limit: 100,
select: {
name: true,
description: true,
requiredPoints: true,
},
}),
payload.find({
collection: "bank-accounts",
where: {
and: [{ accountType: { equals: "personal" } }, { ownerUser: { equals: user.id } }],
},
limit: 1,
}),
payload.findGlobal({ slug: "game-rules" }),
]);
isIntelligence = intelligenceQual;
isLogistics = logisticsQual;
hasPersonalBankAccount = personalAccountRes.docs.length > 0;
statisticsEnabled = gameRulesRes.statisticsEnabled ?? true;
helpdeskEnabled = gameRulesRes.helpdeskEnabled ?? true;
const userXp = profileRes.docs[0]?.progression?.xp ?? 0;
xpLevel = resolveLevel(userXp, experienceRes.docs);
currentRank = (profileRes.docs[0]?.rank as Rank | null | undefined)?.abbreviation ?? "";
isAdmin = await canAccessAdminPanel(payload, user);
}
return (
<html lang="en" className="bg-black leading-8 antialiased h-full dark">
<body className="h-full font-sans m-0 text-white">
{user ? (
<SidebarProvider>
<KeyboardShortcutsProvider
userRoles={user.roles ?? []}
hasIntelligence={isIntelligence}
hasLogistics={isLogistics}
>
<CommandPaletteProvider>
<AppSidebar
user={{
displayName: user.payloadDisplayName ?? "Guest",
username: user.username ?? "Not logged in",
rank: currentRank,
}}
xpLevel={xpLevel}
adminUrl={isAdmin ? payloadConfig.routes.admin : undefined}
hasIntelligence={isIntelligence}
hasLogistics={isLogistics}
statisticsEnabled={statisticsEnabled}
helpdeskEnabled={helpdeskEnabled}
showCallsign={user.preferences?.display?.showCallsign ?? false}
canImpersonate={await isSuperuser(payload, user)}
preferences={user.preferences}
className="border-r"
/>
<SidebarInset
className={
user.preferences?.display?.showBackgroundTexture !== false
? "app-background-texture"
: undefined
}
>
<SiteHeader />
{impersonating && <ImpersonationBanner />}
{children}
</SidebarInset>
<CommandPalette
userRoles={user.roles ?? []}
hasIntelligence={isIntelligence}
hasLogistics={isLogistics}
username={user.username}
preferences={user.preferences}
hasPersonalBankAccount={hasPersonalBankAccount}
/>
</CommandPaletteProvider>
</KeyboardShortcutsProvider>
</SidebarProvider>
) : (
<LandingPage />
)}
{(!user || user.preferences?.display?.showVersionOverlay !== false) && (
<VersionOverlay canSeeCommit={await isSuperuser(payload, user)} />
)}
{user && <GameTickRealtime />}
{user && <SessionWatchdog sessionExp={sessionExp} />}
{process.env.NODE_ENV === "development" && user && <DevDashboardGate />}
{user && <MissionReminderToasts />}
{isLogistics && <ShipmentToasts />}
<Toaster />
<ShimLoader shims={shimEntries} userId={user?.id ?? null} userRoles={user?.roles ?? []} />
</body>
</html>
);
}