209 lines
8.1 KiB
TypeScript
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>
|
|
);
|
|
}
|