import config from "@payload-config"; import { getPayload } from "payload"; import { notFound } from "next/navigation"; import Link from "next/link"; import type { BankAccount, Campaign, Faction, Media, Mission, Resource } from "@/payload-types"; import { RichText } from "@payloadcms/richtext-lexical/react"; import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext"; import { ArrowLeftIcon, ChevronRightIcon, FlagIcon, LandmarkIcon, ScaleIcon, ShieldIcon, } from "lucide-react"; import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item"; import { formatAmount } from "@/lib/banking/format"; interface FactionDetailPageProps { params: Promise<{ id: string }>; } const typeColors: Record = { playerFaction: "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30", majorFaction: "bg-blue-500/20 text-blue-400", minorFaction: "bg-muted text-muted-foreground", corporation: "bg-amber-500/20 text-amber-400", special: "bg-purple-500/20 text-purple-400", }; const typeLabels: Record = { playerFaction: "Player Faction", majorFaction: "Major Faction", minorFaction: "Minor Faction", corporation: "Corporation", special: "Special", }; const missionStatusColors: Record = { Concept: "bg-muted text-muted-foreground", Planning: "bg-muted text-muted-foreground", Ready: "bg-blue-500/20 text-blue-400", Scheduled: "bg-amber-500/20 text-amber-400", Active: "bg-green-500/20 text-green-400", Completed: "bg-emerald-500/20 text-emerald-400", Cancelled: "bg-red-500/20 text-red-400", }; const campaignStatusColors: Record = { concept: "bg-muted text-muted-foreground", inProgress: "bg-blue-500/20 text-blue-400", completed: "bg-emerald-500/20 text-emerald-400", }; function reputationTone(rep: number): string { if (rep >= 50) return "bg-emerald-500/20 text-emerald-400"; if (rep >= 20) return "bg-green-500/20 text-green-400"; if (rep > -20) return "bg-muted text-muted-foreground"; if (rep > -50) return "bg-amber-500/20 text-amber-400"; return "bg-red-500/20 text-red-400"; } function reputationLabel(rep: number): string { if (rep >= 50) return "Revered"; if (rep >= 20) return "Respected"; if (rep > -20) return "Neutral"; if (rep > -50) return "Disliked"; return "Reviled"; } function relationshipTone(value: number): { bar: string; text: string; label: string } { // 50..100 allied (green), 20..49 friendly (green-soft), -19..20 neutral, -20..-49 tense, -50..-100 hostile if (value >= 50) return { bar: "bg-emerald-500", text: "text-emerald-400", label: "Allied" }; if (value >= 20) return { bar: "bg-green-500", text: "text-green-400", label: "Friendly" }; if (value > -20) return { bar: "bg-muted-foreground", text: "text-muted-foreground", label: "Neutral" }; if (value > -50) return { bar: "bg-amber-500", text: "text-amber-400", label: "Tense" }; return { bar: "bg-red-500", text: "text-red-400", label: "Hostile" }; } export default async function FactionDetailPage({ params }: FactionDetailPageProps) { const { id } = await params; const payload = await getPayload({ config }); const faction = await payload .findByID({ collection: "factions", id: Number(id), depth: 2, }) .catch(() => null); if (!faction) { notFound(); } const coverImage = typeof faction.coverImage === "object" && faction.coverImage !== null ? (faction.coverImage as Media) : null; const coverImageUrl = coverImage?.url ?? null; const descriptionText = faction.description ? convertLexicalToPlaintext({ data: faction.description }) : null; const heroSubtitle = descriptionText && descriptionText.length > 240 ? descriptionText.slice(0, 240) + "…" : (descriptionText ?? faction.name); const reputation = faction.reputation ?? 0; // Bank account for this faction const bankRes = await payload .find({ collection: "bank-accounts", where: { and: [{ ownerFaction: { equals: faction.id } }, { accountType: { equals: "faction" } }], }, depth: 1, limit: 1, }) .catch(() => ({ docs: [] })); const bankAccount = (bankRes.docs[0] as BankAccount | undefined) ?? null; const bankCurrency = bankAccount && typeof bankAccount.currency === "object" ? (bankAccount.currency as Resource) : null; // Missions involving this faction const missionsRes = await payload .find({ collection: "missions", where: { "factionsInvolved.faction": { equals: faction.id } }, sort: "-classification.startDateTime", limit: 100, depth: 0, select: { name: true, codeName: true, summary: true, ownershipAndStatus: true, classification: true, }, }) .catch(() => ({ docs: [] as Mission[] })); const missions = missionsRes.docs as Mission[]; // Associated campaigns (faction.associatedCampaigns is populated at depth 2) const campaigns = faction.associatedCampaigns?.filter( (c): c is Campaign => typeof c === "object" && c !== null, ) ?? []; // Relationships (targetFaction populated at depth 2) const relationships = (faction.relationships ?? []).filter( (r) => typeof r.targetFaction === "object" && r.targetFaction !== null, ); return (
Back to Factions
{coverImageUrl ? ( ) : (
)}
{typeLabels[faction.type] ?? faction.type} {reputationLabel(reputation)} · {reputation >= 0 ? `+${reputation}` : reputation}

{faction.name}

{heroSubtitle}

{typeLabels[faction.type] ?? faction.type} {faction.playableFaction && ( <> · Playable )} {faction.startingFaction && ( <> · Starting )}
{/* ── Main Content (2/3) ──────────────────────────── */}
{faction.description && ( Description )} {relationships.length > 0 && (
Relationships
{relationships.map((r) => { const target = r.targetFaction as Faction; const value = r.value; const tone = relationshipTone(value); const barWidth = Math.min(Math.abs(value), 100); return (
{target.name} {value >= 0 ? `+${value}` : value} · {tone.label}
{r.notes && (

{r.notes}

)} ); })}
)} {missions.length > 0 && (

Missions

{missions.length} mission{missions.length !== 1 ? "s" : ""}
{missions.map((mission) => (
{mission.codeName} {mission.name}
{mission.ownershipAndStatus.status}

{mission.summary}

))}
)}
{/* ── Right Sidebar (1/3) ─────────────────────────── */}
Standing
Reputation {reputation >= 0 ? `+${reputation}` : reputation}

{reputationLabel(reputation)} standing in the world.

{bankAccount && (
Bank Balance
View
{formatAmount(bankAccount.balance ?? 0, bankCurrency)} {bankAccount.name} {bankAccount.status !== "open" && ( {bankAccount.status} )}
)} {campaigns.length > 0 && (
Associated Campaigns
{campaigns.map((c) => { const campaign = c as { id: number; name: string; status?: string; summary?: string; }; return (
{campaign.name} {campaign.summary && ( {campaign.summary} )}
{campaign.status && ( {campaign.status === "inProgress" ? "In Progress" : campaign.status.charAt(0).toUpperCase() + campaign.status.slice(1)} )} ); })}
)}
); }