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 && (
+ <>
+

+

+ >
+ )}
+ >
) : (
-
+ )}
+
+ {isSmall && (
+
+ {statusLabel}
+
+ )}
+
+ {!isSmall && (
+
+
+ Official // Unit Command
+
+
+ REF {refCode}
+
+
+ )}
+
+ {!isSmall && (
+
+
+
+
+
+
+ )}
+
+
- {statusLabels[campaign.status]}
-
-
-
-
+
{campaign.name}
+ {!isSmall && (
+ <>
+
+ {campaign.name}
+
+
+ {campaign.name}
+
+
+ {campaign.name}
+
+ >
+ )}
-
- {statusLabels[campaign.status]}
-
+ {campaign.summary && (
+
+ {campaign.summary}
+
+ )}
+ {!isSmall && (
+
+ {officialStatusLabels[campaign.status]}
+
+ )}
-