feat(missions): show op host on side op cards and mission pages
Side ops now credit their creator: mission cards get a 'Hosted by X' meta entry and the mission detail page gets a Host chip. Display name preferred over username; only shown for side ops with a populated createdBy.
This commit is contained in:
parent
3af17928e8
commit
ffb2c2c5d9
2 changed files with 37 additions and 4 deletions
|
|
@ -3,7 +3,7 @@ import { getPayload } from "payload";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { headers as nextHeaders } from "next/headers";
|
import { headers as nextHeaders } from "next/headers";
|
||||||
import type { Campaign, Map, Media, Mission } from "@/payload-types";
|
import type { Campaign, Map, Media, Mission, User } from "@/payload-types";
|
||||||
import {
|
import {
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
FlagIcon,
|
FlagIcon,
|
||||||
MapPinIcon,
|
MapPinIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
|
UserIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||||
import { ClientDate } from "@/components/frontend/ClientDate";
|
import { ClientDate } from "@/components/frontend/ClientDate";
|
||||||
|
|
@ -94,6 +95,13 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
||||||
? (missionWithCoverImage.coverImage as Media)
|
? (missionWithCoverImage.coverImage as Media)
|
||||||
: null;
|
: null;
|
||||||
const coverImageUrl = coverImage?.url ?? null;
|
const coverImageUrl = coverImage?.url ?? null;
|
||||||
|
const host =
|
||||||
|
mission.operationType === "side" &&
|
||||||
|
typeof mission.createdBy === "object" &&
|
||||||
|
mission.createdBy !== null
|
||||||
|
? (mission.createdBy as User)
|
||||||
|
: null;
|
||||||
|
const hostLabel = host ? (host.displayName || host.username) : null;
|
||||||
|
|
||||||
const durationHours = Math.floor(mission.classification.estimatedDuration / 60);
|
const durationHours = Math.floor(mission.classification.estimatedDuration / 60);
|
||||||
const durationMins = mission.classification.estimatedDuration % 60;
|
const durationMins = mission.classification.estimatedDuration % 60;
|
||||||
|
|
@ -328,6 +336,17 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{hostLabel && (
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2">
|
||||||
|
<UserIcon className="size-4 text-white/60" />
|
||||||
|
<span className="flex flex-col">
|
||||||
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-white/50">
|
||||||
|
Host
|
||||||
|
</span>
|
||||||
|
<span className="max-w-56 truncate text-sm text-white">{hostLabel}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{campaign && (
|
{campaign && (
|
||||||
<Link
|
<Link
|
||||||
href={`/intelligence/campaigns/${campaign.id}`}
|
href={`/intelligence/campaigns/${campaign.id}`}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { Campaign, Map, Media } from "@/payload-types";
|
import type { Campaign, Map, Media, User } from "@/payload-types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
operationTypeColors,
|
operationTypeColors,
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
CrosshairIcon,
|
CrosshairIcon,
|
||||||
MapPinIcon,
|
MapPinIcon,
|
||||||
|
UserIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
|
@ -52,6 +53,7 @@ export interface MissionCardProps {
|
||||||
readonly missionRoles?: {
|
readonly missionRoles?: {
|
||||||
readonly maxPlayers: number;
|
readonly maxPlayers: number;
|
||||||
} | null;
|
} | null;
|
||||||
|
readonly createdBy?: number | User | null;
|
||||||
readonly coverImage?: number | Media | null;
|
readonly coverImage?: number | Media | null;
|
||||||
};
|
};
|
||||||
readonly variant?: "default" | "image";
|
readonly variant?: "default" | "image";
|
||||||
|
|
@ -77,6 +79,12 @@ export function MissionCard({
|
||||||
typeof mission.classification.campaign === "object" && mission.classification.campaign !== null
|
typeof mission.classification.campaign === "object" && mission.classification.campaign !== null
|
||||||
? mission.classification.campaign
|
? mission.classification.campaign
|
||||||
: null;
|
: null;
|
||||||
|
const createdByUser =
|
||||||
|
typeof mission.createdBy === "object" && mission.createdBy !== null
|
||||||
|
? mission.createdBy
|
||||||
|
: null;
|
||||||
|
const hostLabel = createdByUser ? (createdByUser.displayName || createdByUser.username) : null;
|
||||||
|
const showHost = mission.operationType === "side" && hostLabel !== null;
|
||||||
|
|
||||||
const startDate = new Date(mission.classification.startDateTime);
|
const startDate = new Date(mission.classification.startDateTime);
|
||||||
const dateStr = startDate.toLocaleDateString("en-US", {
|
const dateStr = startDate.toLocaleDateString("en-US", {
|
||||||
|
|
@ -122,7 +130,7 @@ export function MissionCard({
|
||||||
<ItemMedia variant="icon">
|
<ItemMedia variant="icon">
|
||||||
<CrosshairIcon />
|
<CrosshairIcon />
|
||||||
</ItemMedia>
|
</ItemMedia>
|
||||||
<ItemTitle className="flex flex-col">
|
<ItemTitle className="flex flex-col items-start text-start">
|
||||||
<span className="font-mono text-xs text-muted-foreground">{mission.codeName}</span>
|
<span className="font-mono text-xs text-muted-foreground">{mission.codeName}</span>
|
||||||
<span>{mission.name}</span>
|
<span>{mission.name}</span>
|
||||||
</ItemTitle>
|
</ItemTitle>
|
||||||
|
|
@ -131,7 +139,7 @@ export function MissionCard({
|
||||||
<span
|
<span
|
||||||
className={`text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded ${operationTypeColors[mission.operationType] ?? "bg-muted text-muted-foreground"}`}
|
className={`text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded ${operationTypeColors[mission.operationType] ?? "bg-muted text-muted-foreground"}`}
|
||||||
>
|
>
|
||||||
{operationTypeLabels[mission.operationType] ?? mission.operationType}
|
{operationTypeLabels[mission.operationType] ?? mission.operationType} Operation
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded ${statusColors[mission.ownershipAndStatus.status] ?? "bg-muted text-muted-foreground"}`}
|
className={`text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded ${statusColors[mission.ownershipAndStatus.status] ?? "bg-muted text-muted-foreground"}`}
|
||||||
|
|
@ -167,6 +175,12 @@ export function MissionCard({
|
||||||
{mission.missionRoles.maxPlayers} slots
|
{mission.missionRoles.maxPlayers} slots
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{showHost && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<UserIcon className="size-3" />
|
||||||
|
Hosted by {hostLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{campaign && <span className="text-primary">{campaign.name}</span>}
|
{campaign && <span className="text-primary">{campaign.name}</span>}
|
||||||
</div>
|
</div>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue