diff --git a/src/app/(frontend)/intelligence/campaigns/[id]/page.tsx b/src/app/(frontend)/intelligence/campaigns/[id]/page.tsx index 98ee66f..754d083 100644 --- a/src/app/(frontend)/intelligence/campaigns/[id]/page.tsx +++ b/src/app/(frontend)/intelligence/campaigns/[id]/page.tsx @@ -4,6 +4,7 @@ import { notFound } from "next/navigation"; import Link from "next/link"; import type { Faction, Media, Mission } from "@/payload-types"; import { RichText } from "@payloadcms/richtext-lexical/react"; +import { ClientDate } from "@/components/frontend/ClientDate"; import { ArrowLeftIcon, CalendarDaysIcon, @@ -104,21 +105,6 @@ export default async function CampaignDetailPage({ params }: CampaignDetailPageP const missions = missionsRes.docs as Mission[]; const factions = factionsRes.docs as Faction[]; - const startDate = campaign.startDate - ? new Date(campaign.startDate).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }) - : null; - const endDate = campaign.endDate - ? new Date(campaign.endDate).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }) - : null; - // Mission status breakdown const statusCounts: Record = {}; for (const m of missions) { @@ -169,12 +155,28 @@ export default async function CampaignDetailPage({ params }: CampaignDetailPageP {campaign.name}

{campaign.summary}

- {(startDate || endDate) && ( + {(campaign.startDate || campaign.endDate) && (
- {startDate && {startDate}} - {startDate && endDate && —} - {endDate ? {endDate} : startDate ? Present : null} + {campaign.startDate && ( + + + + )} + {campaign.startDate && campaign.endDate && —} + {campaign.endDate ? ( + + + + ) : campaign.startDate ? ( + Present + ) : null}
)} @@ -257,8 +259,23 @@ export default async function CampaignDetailPage({ params }: CampaignDetailPageP
Duration - {startDate} - {endDate ? ` — ${endDate}` : " — Present"} + {campaign.startDate && ( + + )} + {campaign.endDate ? ( + <> + {" — "} + + + ) : ( + " — Present" + )}
diff --git a/src/app/(frontend)/intelligence/missions/[id]/page.tsx b/src/app/(frontend)/intelligence/missions/[id]/page.tsx index 7f5fbb3..fc51f6e 100644 --- a/src/app/(frontend)/intelligence/missions/[id]/page.tsx +++ b/src/app/(frontend)/intelligence/missions/[id]/page.tsx @@ -15,6 +15,7 @@ import { ServerIcon, } from "lucide-react"; import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item"; +import { ClientDate } from "@/components/frontend/ClientDate"; import { MissionBriefingSection } from "@/components/frontend/intelligence/MissionBriefingSection"; import { MissionObjectives } from "@/components/frontend/intelligence/MissionObjectives"; import { hasPermission } from "@/utils/access-control/hasPermission"; @@ -91,17 +92,6 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro : null; const coverImageUrl = coverImage?.url ?? null; - const startDate = new Date(mission.classification.startDateTime); - const dateStr = startDate.toLocaleDateString("en-US", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - }); - const timeStr = startDate.toLocaleTimeString("en-US", { - hour: "2-digit", - minute: "2-digit", - }); const durationHours = Math.floor(mission.classification.estimatedDuration / 60); const durationMins = mission.classification.estimatedDuration % 60; const durationStr = durationMins > 0 ? `${durationHours}h ${durationMins}m` : `${durationHours}h`; @@ -241,7 +231,12 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro Date - {dateStr} + + +
@@ -251,7 +246,11 @@ export default async function MissionDetailPage({ params }: MissionDetailPagePro Time - {timeStr} · {durationStr} + {" "} + · {durationStr}
diff --git a/src/app/(frontend)/logistics/shipments/[id]/page.tsx b/src/app/(frontend)/logistics/shipments/[id]/page.tsx index 6877b1a..a9d5cc2 100644 --- a/src/app/(frontend)/logistics/shipments/[id]/page.tsx +++ b/src/app/(frontend)/logistics/shipments/[id]/page.tsx @@ -12,6 +12,7 @@ import { TruckIcon, } from "lucide-react"; import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item"; +import { ClientDate } from "@/components/frontend/ClientDate"; import { ShipmentActions } from "@/components/frontend/logistics/ShipmentActions"; import { ShipmentProgressBar } from "@/components/frontend/logistics/ShipmentProgressBar"; @@ -308,11 +309,11 @@ export default async function ShipmentDetailPage({ params }: ShipmentDetailPageP
Dispatched - {dispatchedAt.toLocaleDateString()}{" "} - {dispatchedAt.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} + {" "} +
)} @@ -322,11 +323,14 @@ export default async function ShipmentDetailPage({ params }: ShipmentDetailPageP {shipment.status === "completed" ? "Arrived" : "Est. Arrival"} - {estimatedArrival.toLocaleDateString()}{" "} - {estimatedArrival.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} + {" "} + )} @@ -334,11 +338,11 @@ export default async function ShipmentDetailPage({ params }: ShipmentDetailPageP
Completed - {completedAt.toLocaleDateString()}{" "} - {completedAt.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} + {" "} +
)} diff --git a/src/components/frontend/ClientDate.tsx b/src/components/frontend/ClientDate.tsx new file mode 100644 index 0000000..d39edd0 --- /dev/null +++ b/src/components/frontend/ClientDate.tsx @@ -0,0 +1,32 @@ +"use client"; + +/** + * Renders a date/time in the browser's local timezone. + * + * Server components cannot know the visitor's timezone — `Intl` methods there + * format using the server process's timezone (typically UTC in Docker/Node), + * producing a misleading multi-hour offset. This client component guarantees + * the value is formatted in the browser's local timezone instead, matching + * client-rendered lists (e.g. `MissionCard`). + * + * Usage: + * + * + * + * Falls back to an em dash when `date` is missing/invalid. + */ +export function ClientDate({ + date, + options, + locale, +}: { + date: string | null | undefined; + options?: Intl.DateTimeFormatOptions; + locale?: string; +}) { + if (!date) return <>—; + const parsed = new Date(date); + if (Number.isNaN(parsed.getTime())) return <>—; + const formatted = new Intl.DateTimeFormat(locale ?? undefined, options).format(parsed); + return <>{formatted}; +}