1
0
Fork 0
polaris-task-force/src/components/frontend/story/StoryViewer.tsx

116 lines
4 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { BookOpen, X } from "lucide-react";
import { RichText } from "@payloadcms/richtext-lexical/react";
import { Button } from "@/components/ui/button";
import { BeatReadMarker } from "@/components/frontend/story/BeatReadMarker";
import type { StoryBeat } from "@/payload-types";
import { cn } from "@/lib/utils";
import { READER_BODY_CLASSES } from "@/lib/story/richtextClasses";
export type StoryViewerBeat = {
readonly id: number;
readonly title: string;
readonly chapter: number;
readonly dateLabel: string | null;
readonly body: StoryBeat["body"];
readonly initialRead: boolean;
};
export function StoryViewer({ beats }: { readonly beats: readonly StoryViewerBeat[] }) {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!open) return;
const previousOverflow = document.body.style.overflow;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setOpen(false);
};
document.body.style.overflow = "hidden";
document.addEventListener("keydown", handleKeyDown);
closeButtonRef.current?.focus();
return () => {
document.body.style.overflow = previousOverflow;
document.removeEventListener("keydown", handleKeyDown);
triggerRef.current?.focus();
};
}, [open]);
if (beats.length === 0) return null;
return (
<>
<Button
ref={triggerRef}
type="button"
variant="outline"
aria-label="Open immersive story reader"
onClick={() => setOpen(true)}
className="gap-2"
>
<BookOpen className="size-4" />
Immersive view
</Button>
{open && (
<div
role="dialog"
aria-modal="true"
aria-labelledby="story-reader-title"
className="fixed inset-0 z-50 overflow-y-auto overscroll-contain bg-black text-white"
>
<div className="pointer-events-none fixed inset-x-0 top-0 z-10 flex justify-end p-4 sm:p-6">
<Button
ref={closeButtonRef}
type="button"
variant="ghost"
size="icon"
aria-label="Exit reader mode"
onClick={() => setOpen(false)}
className="pointer-events-auto text-white hover:bg-white/10 hover:text-white"
>
<X className="size-5" />
</Button>
</div>
<div className="min-h-[100dvh] px-6 pb-24 pt-24 sm:px-8 sm:pt-28">
<div className="mx-auto max-w-3xl">
<h1 id="story-reader-title" className="sr-only">
The Unit Chronicle
</h1>
<ol className="flex flex-col gap-24">
{beats.map((beat) => (
<li key={beat.id} className="scroll-mt-24">
<div className="flex items-center justify-between gap-4">
<p className="text-xs uppercase tracking-[0.3em] text-neutral-500">
Chapter {beat.chapter}
</p>
<div className="[&_.text-muted-foreground]:text-neutral-500">
<BeatReadMarker beatId={beat.id} initialRead={beat.initialRead} />
</div>
</div>
<h2 className="mt-6 font-serif text-3xl font-semibold leading-tight text-white sm:text-4xl">
{beat.title}
</h2>
{beat.dateLabel && (
<p className="mt-3 text-sm text-neutral-500">{beat.dateLabel}</p>
)}
<div className="mt-8">
<RichText data={beat.body} className={cn(READER_BODY_CLASSES)} />
</div>
</li>
))}
</ol>
</div>
</div>
</div>
)}
</>
);
}