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 { SessionExpiryBanner } from "@/components/frontend/session/SessionExpiryBanner"; 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 { AnnouncementHost } from "@/components/frontend/announcements/AnnouncementHost"; import type { AnnouncementEntry } from "@/lib/announcements/evaluate"; 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; let announcementEntries: AnnouncementEntry[] = []; let userRoleDocIds: number[] = []; let isAdmin = false; let statisticsEnabled = true; let helpdeskEnabled = true; if (user) { const [ intelligenceQual, logisticsQual, profileRes, experienceRes, personalAccountRes, gameRulesRes, announcementsRes, ] = 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" }), payload.find({ collection: "announcements", where: { enabled: { equals: true } }, sort: "-createdAt", limit: 50, }), ]); 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); announcementEntries = (announcementsRes.docs ?? []) as unknown as AnnouncementEntry[]; userRoleDocIds = (user.roleDocs ?? []).map((roleDoc) => typeof roleDoc === "object" && roleDoc !== null ? roleDoc.id : Number(roleDoc), ); } return ( {user ? ( {user && } {impersonating && } {user && ( )} {children} ) : ( )} {(!user || user.preferences?.display?.showVersionOverlay !== false) && ( )} {user && } {user && } {process.env.NODE_ENV === "development" && user && } {user && } {isLogistics && } ); }