import config from "@payload-config"; import { getPayload } from "payload"; import { headers as nextHeaders } from "next/headers"; import { RichText } from "@payloadcms/richtext-lexical/react"; import type { StoryBeat, StoryBeatState } from "@/payload-types"; import { isPayloadUser } from "@/utils/access-control/isPayloadUser"; import { cn } from "@/lib/utils"; import { BEAT_BODY_CLASSES } from "@/lib/story/richtextClasses"; import { BeatReactions } from "@/components/frontend/story/BeatReactions"; import { BeatReadMarker } from "@/components/frontend/story/BeatReadMarker"; import { StoryViewer, type StoryViewerBeat } from "@/components/frontend/story/StoryViewer"; import type { StoryReaction } from "@/lib/story/reactions"; export const metadata = { title: "Chronicle", }; type ReactionCounts = Record; const DATE_FORMAT = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "long", day: "numeric", }); function emptyCounts(): ReactionCounts { return { like: 0, heart: 0, salute: 0 }; } function sortBeats(beats: StoryBeat[]): StoryBeat[] { return [...beats].sort((a, b) => { const timeA = a.occurredAt ? Date.parse(a.occurredAt) : Number.POSITIVE_INFINITY; const timeB = b.occurredAt ? Date.parse(b.occurredAt) : Number.POSITIVE_INFINITY; if (timeA !== timeB) return timeA - timeB; const orderA = a.order ?? Number.POSITIVE_INFINITY; const orderB = b.order ?? Number.POSITIVE_INFINITY; if (orderA !== orderB) return orderA - orderB; return Date.parse(a.createdAt) - Date.parse(b.createdAt); }); } export default async function StoryPage() { const payloadConfig = await config; const payload = await getPayload({ config: payloadConfig }); const headers = await nextHeaders(); const { user } = await payload.auth({ headers, canSetHeaders: false }); if (!isPayloadUser(user)) return null; const beatsResult = await payload.find({ collection: "story-beats", where: { status: { equals: "published" } }, limit: 200, depth: 1, overrideAccess: false, user, }); const sorted = sortBeats(beatsResult.docs as StoryBeat[]); const statesResult = await payload.find({ collection: "story-beat-states", where: { user: { equals: user.id } }, limit: 500, depth: 0, overrideAccess: false, user, }); const stateByBeat = new Map(); for (const state of statesResult.docs as StoryBeatState[]) { const beatId = typeof state.beat === "number" ? state.beat : state.beat.id; stateByBeat.set(beatId, state); } const beatIds = sorted.map((beat) => beat.id); const counts = new Map(); for (const id of beatIds) counts.set(id, emptyCounts()); if (beatIds.length > 0) { const reactionDocs = await payload.find({ collection: "story-beat-states", where: { and: [{ beat: { in: beatIds } }, { reaction: { not_equals: null } }], }, limit: 1000, depth: 0, overrideAccess: true, }); for (const doc of reactionDocs.docs as StoryBeatState[]) { const beatId = typeof doc.beat === "number" ? doc.beat : doc.beat.id; const beatCounts = counts.get(beatId); if (doc.reaction && beatCounts) beatCounts[doc.reaction] += 1; } } const readerBeats: StoryViewerBeat[] = sorted.map((beat, index) => ({ id: beat.id, title: beat.title, chapter: index + 1, dateLabel: beat.occurredAt ? DATE_FORMAT.format(new Date(beat.occurredAt)) : null, body: beat.body, initialRead: Boolean(stateByBeat.get(beat.id)?.readAt), })); return (

The Unit Chronicle

Story Chronicle

Every operation leaves a mark. These are the beats of our story, told in order.

{sorted.length > 0 && (
)}
{sorted.length === 0 ? (

No published chapters yet. Check back soon.

) : (
    {sorted.map((beat, index) => { const state = stateByBeat.get(beat.id); const mission = beat.mission && typeof beat.mission !== "number" ? beat.mission : null; const missionLabel = mission ? mission.codeName ? `${mission.codeName} ยท ${mission.name}` : mission.name : null; return (
  1. Chapter {index + 1}

    {beat.title}

    {beat.occurredAt && {DATE_FORMAT.format(new Date(beat.occurredAt))}} {missionLabel && ( {missionLabel} )}
  2. ); })}
)}
); }