import config from "@payload-config"; import { getPayload } from "payload"; import { notFound } from "next/navigation"; import Link from "next/link"; import type { Assignment, Award, Media, Qualification, Rank, User } from "@/payload-types"; import { RichText } from "@payloadcms/richtext-lexical/react"; import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext"; import { resolveLevel } from "@/utils/xp/resolveLevel"; import { ArrowLeftIcon, AwardIcon, BadgeCheckIcon, CalendarDaysIcon, CrosshairIcon, FileTextIcon, HashIcon, PlaneIcon, ShieldCheck, ShieldIcon, SkullIcon, SparkleIcon, SwordsIcon, TargetIcon, TruckIcon, UserIcon, } from "lucide-react"; import { Item, ItemContent, ItemDescription, ItemHeader, ItemMedia, ItemTitle, } from "@/components/ui/item"; interface ProfilePageProps { params: Promise<{ username: string }>; } export default async function ProfilePage({ params }: ProfilePageProps) { const { username } = await params; const payload = await getPayload({ config }); const profileRes = await payload .find({ collection: "profiles", where: { "user.username": { equals: username } }, depth: 2, select: { user: true, rank: true, dossier: true, progression: { awards: true, qualifications: true, xp: true, scoreboard: true, minigames: true, }, createdAt: true, }, }) .catch(() => null); if (!profileRes || profileRes.docs.length === 0) { notFound(); } const profile = profileRes.docs[0]; const user = profile.user as User; const rank = profile.rank as Rank; const awards = (profile.progression?.awards ?? []).filter( (a): a is Award => typeof a === "object" && a !== null, ); const qualifications = (profile.progression?.qualifications ?? []).filter( (q): q is Qualification => typeof q === "object" && q !== null, ); const scoreboard = profile.progression?.scoreboard ?? {}; const xp = profile.progression?.xp ?? 0; const experienceRes = await payload.find({ collection: "experience", sort: "requiredPoints", limit: 100, select: { name: true, requiredPoints: true }, }); const xpLevel = resolveLevel(xp, experienceRes.docs); const assignmentRes = await payload.find({ collection: "assignments", where: { members: { equals: user.id } }, limit: 10, select: { name: true, type: true, callsign: true }, }); const assignments = assignmentRes.docs as Pick[]; const rankInsignia = typeof rank === "object" && rank.insignia && typeof rank.insignia === "object" ? (rank.insignia as Media) : null; const infantryKills = scoreboard.infantryKills ?? 0; const vehicleKills = scoreboard.vehicleKills ?? 0; const armorKills = scoreboard.armorKills ?? 0; const airKills = scoreboard.airKills ?? 0; const deaths = scoreboard.deaths ?? 0; const totalKills = infantryKills + vehicleKills + armorKills + airKills; const kdRatio = deaths > 0 ? (totalKills / deaths).toFixed(2) : totalKills > 0 ? "∞" : "—"; const enlistDate = new Date(profile.createdAt); const enlistDateStr = enlistDate.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }); const now = new Date(); const serviceMonths = (now.getFullYear() - enlistDate.getFullYear()) * 12 + (now.getMonth() - enlistDate.getMonth()); const serviceYears = Math.floor(serviceMonths / 12); const remainingMonths = serviceMonths % 12; const serviceLength = serviceYears > 0 ? `${serviceYears} yr${serviceYears !== 1 ? "s" : ""} ${remainingMonths} mo` : `${remainingMonths} mo`; return (
Back {/* ── Service Header ─────────────────────────────────── */}
{rankInsignia?.url && ( {rank.name} )}

{user.displayName}

{rank.name} | {xpLevel.levelName} | @{user.username}
{xp.toLocaleString()} XP
{/* ── Service Record ─────────────────────────────────── */}

Service Record

Enlisted {enlistDateStr} Time in Service {serviceLength} Steam ID {user.steamId} Discord {user.discordUsername}
{assignments.length > 0 && (
{assignments.map((a) => ( {a.callsign ? `[${a.callsign}] ` : ""} {a.name} ({a.type}) ))}
)}
{/* ── Dossier ────────────────────────────────────────── */}

Personnel Dossier

Personal Excerpt

{profile.dossier?.personalExcerpt || "No personal excerpt on file."}

Intelligence Record
Verified by the Intelligence Division
{profile.dossier?.intelExcerpt ? ( ) : (

No intelligence record on file.

)}
{/* ── Qualifications ─────────────────────────────────── */}

Qualifications

{qualifications.length > 0 ? (
{qualifications.map((q) => ( {q.name} ))}
) : (

No qualifications on file.

)}
{/* ── Combat Record ──────────────────────────────────── */}

Combat Record

Category Count XP Value
Infantry Kills {infantryKills.toLocaleString()} {(infantryKills * 10).toLocaleString()}
Vehicle Kills {vehicleKills.toLocaleString()} {(vehicleKills * 100).toLocaleString()}
Armor Kills {armorKills.toLocaleString()} {(armorKills * 500).toLocaleString()}
Air Kills {airKills.toLocaleString()} {(airKills * 500).toLocaleString()}
Deaths {deaths.toLocaleString()} —
K/D Ratio {kdRatio}
{/* ── Awards & Decorations ───────────────────────────── */}

Awards & Decorations

{awards.length > 0 ? (
{awards.map((award) => { const awardImage = award.image && typeof award.image === "object" ? (award.image as Media) : null; return (
{awardImage?.url && ( {award.name} )}
{award.name} {award.type ?? "commendation"}
{award.description ? convertLexicalToPlaintext({ data: award.description }).slice(0, 120) : "No description."} {award.experiencePoints != null && ( +{award.experiencePoints} XP )}
); })}
) : (

This soldier has not yet received any awards or decorations.

)}
{/* ── Progression ────────────────────────────────────── */}

Progression

{xpLevel.levelName} {xp.toLocaleString()} XP
{xpLevel.currentLevelXP.toLocaleString()} XP {xpLevel.nextLevelXP !== null ? ( {Math.floor(xpLevel.progress)}% — {xpLevel.nextLevelXP.toLocaleString()} XP to next level ) : ( Max level reached )}
); }