import { Fragment } from "react"; import Link from "next/link"; import { ExternalLink } from "lucide-react"; import { sourceAnchor, subjectMetaParts, type SourceView } from "yt-dlp-transcript-common/lib/report/views"; import type { SourceArchive } from "yt-dlp-transcript-common/lib/citations/schema"; import { reportIds } from "../../lib/reports"; // Small pieces the report, index and moment pages share. // `2026-10-04T12:00:00Z` → `2026-10-04`; a partial date (`2026-10`) as given. export function dateLabel(v: string | undefined): string | undefined { if (!v) return undefined; return /^\d{4}-\d{2}-\d{2}T/.test(v) ? v.slice(0, 10) : v; } export function Eyebrow({ children }: { children: React.ReactNode }) { return

{children}

; } // A report's name as a heading, on its page, in the index and on the history // page. With a series, the series is its own line in the accent (the tag, // like a wordmark's lead) and the title the next, in regular weight — both // inside the heading, so its name reads " "; without one, the // title alone. export function ReportName({ series, title }: { series?: string; title: string }) { if (!series) return <>{title}</>; return ( <> <span data-report-series="" className="block font-semibold text-brand"> {series} </span>{" "} <span className="block font-normal">{title}</span> </> ); } export const textLink = "text-brand underline decoration-brand/40 underline-offset-2 hover:decoration-brand"; export function ExternalLinkText({ href, children, title }: { href: string; children: React.ReactNode; title?: string }) { return ( <a href={href} target="_blank" rel="noopener noreferrer" title={title} className={`inline-flex items-center gap-1 ${textLink}`}> {children} <ExternalLink className="size-3 shrink-0" aria-hidden /> </a> ); } // A document a report quotes: its title (linked), byline, note, and its // archive links in the context the document gave them — collapsed, since a // long document may carry hundreds. The block is the document's one place on // the page: its anchor is what a source citation's "from <title>" links to. // With an `accent`, its left edge wears it, as its sentences' rails do. export function SourceBlock({ source, label }: { source: SourceView; label: string }) { const byline = [source.publisher, source.author, dateLabel(source.date)].filter(Boolean).join(" · "); const n = source.archives.length; return ( <div id={sourceAnchor(source.id)} data-subject-source={source.id} className={`flex scroll-mt-20 flex-col gap-1.5 rounded-lg border border-border bg-card p-4 text-sm${source.accent ? " border-l-4" : ""}`} style={source.accent ? { borderLeftColor: source.accent } : undefined} > <p className="font-mono text-[10px] uppercase tracking-[0.16em] text-muted-foreground">{label}</p> <p className="font-medium leading-snug text-foreground"> {source.url ? <ExternalLinkText href={source.url}>{source.title}</ExternalLinkText> : source.title} </p> {byline && <p className="font-mono text-xs text-muted-foreground">{byline}</p>} {source.note && <p className="text-xs text-muted-foreground">{source.note}</p>} {n > 0 && ( <details data-source-archives={n} className="mt-1 text-xs"> <summary className="cursor-pointer text-muted-foreground hover:text-foreground"> {`${n} archive link${n === 1 ? "" : "s"} in context`} </summary> <ArchiveList archives={source.archives} className="mt-2" /> </details> )} </div> ); } // The document under review, under the report's heading: its title linked to // the document, then "<author> · <publisher> · <date>", then its archive links // folded away. No label: its left rail, in the document's colour (its // `accent`, else the border colour) as its sentences' rails below are, says // whose it is. It is the document's one place on the page, so it carries the // anchor a citation's "from <title>" links to. export function SubjectCard({ source }: { source: SourceView }) { // Each part stays whole on a narrow screen (a date never splits at its dashes). const meta = subjectMetaParts(source); const n = source.archives.length; return ( <div id={sourceAnchor(source.id)} data-subject-source={source.id} className={`flex min-w-0 scroll-mt-20 flex-col gap-1 rounded-r-md border-l-4 bg-surface py-2.5 pl-3 pr-4 text-sm${source.accent ? "" : " border-border"}`} style={source.accent ? { borderLeftColor: source.accent } : undefined} > <p data-subject-title="" className="font-medium leading-snug text-foreground [overflow-wrap:anywhere]"> {source.url ? ( <a href={source.url} target="_blank" rel="noopener noreferrer" className={`${textLink} after:ml-1 after:inline-block after:text-xs after:content-['↗']`} > {source.title} </a> ) : ( source.title )} </p> {meta.length > 0 && ( <p className="text-xs text-muted-foreground"> {meta.map((part, i) => ( <Fragment key={i}> {i > 0 && " · "} <span className="whitespace-nowrap">{part}</span> </Fragment> ))} </p> )} {n > 0 && ( <details data-source-archives={n} className="text-xs"> <summary className="cursor-pointer text-muted-foreground hover:text-foreground"> {`${n} archive link${n === 1 ? "" : "s"}`} </summary> <ArchiveList archives={source.archives} className="mt-2" /> </details> )} </div> ); } // Archive links, each with the words around it in the document. A document // may carry hundreds, so a link is text with a CSS arrow, not two inline SVG // icons apiece. export function ArchiveList({ archives, className }: { archives: readonly SourceArchive[]; className?: string }) { return ( <ul className={`flex flex-col gap-1 ${className ?? ""}`}> {archives.map((a, i) => ( <li key={`${i}:${a.url}`} className="flex flex-wrap items-baseline gap-x-2"> <a href={a.url} target="_blank" rel="noopener noreferrer" className={`${textLink} after:ml-0.5 after:content-['↗']`}> {a.label} </a> {a.context && <span className="text-muted-foreground">— {a.context}</span>} </li> ))} </ul> ); } export function EmptyState({ title, children }: { title: string; children: React.ReactNode }) { return ( <div data-reports-empty="" className="mx-auto flex max-w-2xl flex-col gap-3"> <h1 className="font-display text-2xl font-semibold text-foreground">{title}</h1> <p className="text-sm text-muted-foreground">{children}</p> </div> ); } // What a corpus-shaped route (search's chat, downloads, duplicates, offline) // renders on a CITED site, which publishes only its reports and their moments. // The route still exists in the static build (every route does); it shows // nothing of the corpus and points at the home page -- the index, or the one // report. export function NotOnCitedSite({ title }: { title: string }) { const one = reportIds().length === 1; return ( <EmptyState title={title}> This site publishes its reports only.{" "} <Link href="/" className={textLink}> {one ? "Read the report" : "Read the reports"} </Link> . </EmptyState> ); }