From 8668a59d063fafbd51c947fc803eb408e0b319ee Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Wed, 9 Sep 2026 19:42:50 -0400 Subject: [PATCH] feat(campaigns): split page into official and community sections with tactical official cards --- .../intelligence/campaigns/page.tsx | 60 ++++- .../frontend/intelligence/CampaignCard.tsx | 243 +++++++++++++++--- 2 files changed, 254 insertions(+), 49 deletions(-) diff --git a/src/app/(frontend)/intelligence/campaigns/page.tsx b/src/app/(frontend)/intelligence/campaigns/page.tsx index cc90034..34c8a2f 100644 --- a/src/app/(frontend)/intelligence/campaigns/page.tsx +++ b/src/app/(frontend)/intelligence/campaigns/page.tsx @@ -23,6 +23,7 @@ export default async function CampaignsPage() { name: true, summary: true, status: true, + campaignMode: true, startDate: true, endDate: true, owner: true, @@ -56,6 +57,9 @@ export default async function CampaignsPage() { }); const missionCount = missionsRes.docs.length; + const completedMissions = missionsRes.docs.filter( + (mission) => mission.ownershipAndStatus.status === "Completed", + ).length; const totalPlayers = missionsRes.docs.reduce( (total, mission) => total + mission.missionRoles.maxPlayers, 0, @@ -81,6 +85,7 @@ export default async function CampaignsPage() { ...campaign, coverImageUrl, missionCount, + completedMissions, avgPlayers: missionCount === 0 ? 0 : totalPlayers / missionCount, avgDurationMinutes: missionCount === 0 ? 0 : totalDurationMinutes / missionCount, ownerName, @@ -88,8 +93,15 @@ export default async function CampaignsPage() { }), ); + const officialCampaigns = campaignsWithStats.filter( + (campaign) => campaign.campaignMode === "official", + ); + const communityCampaigns = campaignsWithStats.filter( + (campaign) => campaign.campaignMode !== "official", + ); + return ( -
+
@@ -106,12 +118,46 @@ export default async function CampaignsPage() {
- {sorted.length > 0 ? ( -
- {campaignsWithStats.map((campaign) => ( - - ))} -
+ {campaignsWithStats.length > 0 ? ( + <> + {officialCampaigns.length > 0 && ( +
+
+

+ Official Campaigns +

+

+ Command-sanctioned operations at the heart of the unit's story. +

+
+
+ {officialCampaigns.map((campaign) => ( + + ))} +
+
+ )} + {officialCampaigns.length > 0 && communityCampaigns.length > 0 && ( +
+ )} + {communityCampaigns.length > 0 && ( +
+
+

+ Community Campaigns +

+

+ Player-made side operations and stories, open to everyone. +

+
+
+ {communityCampaigns.map((campaign) => ( + + ))} +
+
+ )} + ) : (

No campaigns found.

diff --git a/src/components/frontend/intelligence/CampaignCard.tsx b/src/components/frontend/intelligence/CampaignCard.tsx index cab9171..24ba1b4 100644 --- a/src/components/frontend/intelligence/CampaignCard.tsx +++ b/src/components/frontend/intelligence/CampaignCard.tsx @@ -11,6 +11,7 @@ interface CampaignCardCampaign extends Pick< > { readonly coverImageUrl: string | null; readonly missionCount: number; + readonly completedMissions: number; readonly avgPlayers: number; readonly avgDurationMinutes: number; } @@ -19,6 +20,7 @@ type CampaignCardInput = CampaignCardCampaign | Campaign; interface CampaignCardProps { readonly campaign: CampaignCardInput; + readonly size?: "lg" | "sm"; } const statusColors: Record = { @@ -33,7 +35,20 @@ const statusLabels: Record = { completed: "Completed", }; -export function CampaignCard({ campaign }: CampaignCardProps) { +const officialStatusLabels: Record = { + concept: "Planning", + inProgress: "Active Deployment", + completed: "Concluded", +}; + +const statusTextColors: Record = { + concept: "text-muted-foreground", + inProgress: "text-blue-400", + completed: "text-emerald-400", +}; + +export function CampaignCard({ campaign, size = "lg" }: CampaignCardProps) { + const isSmall = size === "sm"; const coverImageUrl = "coverImageUrl" in campaign ? campaign.coverImageUrl @@ -41,6 +56,8 @@ export function CampaignCard({ campaign }: CampaignCardProps) { ? (campaign.coverImage.url ?? null) : null; const missionCount = "missionCount" in campaign ? campaign.missionCount : 0; + const completedMissions = + "completedMissions" in campaign ? campaign.completedMissions : 0; const avgPlayers = "avgPlayers" in campaign ? campaign.avgPlayers : 0; const avgDurationMinutes = "avgDurationMinutes" in campaign ? campaign.avgDurationMinutes : 0; @@ -57,21 +74,56 @@ export function CampaignCard({ campaign }: CampaignCardProps) { ? owner.displayName : "Unknown"; + const refCode = String(campaign.id).padStart(3, "0"); + const statusLabel = isSmall ? statusLabels[campaign.status] : officialStatusLabels[campaign.status]; + const readoutCells = [ + { label: "Msns", value: missionCount.toString(), mono: true }, + { label: "Done", value: completedMissions.toString(), mono: true }, + { label: "Avg Squad", value: playersLabel, mono: true }, + { label: "Duration", value: durationLabel, mono: true }, + ]; + return ( -
+
{coverImageUrl !== null ? ( - + <> + + {!isSmall && ( + <> + + + + )} + ) : (
);