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 Link from "next/link";
|
||||
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 {
|
||||
ArrowLeftIcon,
|
||||
CalendarDaysIcon,
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
FlagIcon,
|
||||
MapPinIcon,
|
||||
ServerIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react";
|
||||
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||
import { ClientDate } from "@/components/frontend/ClientDate";
|
||||
|
|
@ -94,6 +95,13 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
|||
? (missionWithCoverImage.coverImage as Media)
|
||||
: 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 durationMins = mission.classification.estimatedDuration % 60;
|
||||
|
|
@ -328,6 +336,17 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro
|
|||
</span>
|
||||
</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 && (
|
||||
<Link
|
||||
href={`/intelligence/campaigns/${campaign.id}`}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
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 {
|
||||
operationTypeColors,
|
||||
|
|
@ -22,6 +22,7 @@ import {
|
|||
ClockIcon,
|
||||
CrosshairIcon,
|
||||
MapPinIcon,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
|
|
@ -52,6 +53,7 @@ export interface MissionCardProps {
|
|||
readonly missionRoles?: {
|
||||
readonly maxPlayers: number;
|
||||
} | null;
|
||||
readonly createdBy?: number | User | null;
|
||||
readonly coverImage?: number | Media | null;
|
||||
};
|
||||
readonly variant?: "default" | "image";
|
||||
|
|
@ -77,6 +79,12 @@ export function MissionCard({
|
|||
typeof mission.classification.campaign === "object" && mission.classification.campaign !== null
|
||||
? mission.classification.campaign
|
||||
: 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 dateStr = startDate.toLocaleDateString("en-US", {
|
||||
|
|
@ -122,7 +130,7 @@ export function MissionCard({
|
|||
<ItemMedia variant="icon">
|
||||
<CrosshairIcon />
|
||||
</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>{mission.name}</span>
|
||||
</ItemTitle>
|
||||
|
|
@ -131,7 +139,7 @@ export function MissionCard({
|
|||
<span
|
||||
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
|
||||
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
|
||||
</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>}
|
||||
</div>
|
||||
</ItemContent>
|
||||
|
|
|
|||
Loading…
Reference in a new issue