Files
spsays-training/app/(app)/(reader)/transcriptions/year/[year]/year-browser.tsx
T

232 lines
7.0 KiB
TypeScript

"use client";
import { useState, useMemo } from "react";
import Link from "next/link";
import { PageHeader } from "@/components/layout/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { Calendar, ChevronRight, Clock, MapPin } from "lucide-react";
import { TranscriptQueue } from "@/components/audio-player/transcript-queue";
import { PlayAudioButton } from "@/app/(app)/(reader)/transcriptions/[slug]/play-audio-button";
type TranscriptionItem = {
slug: string;
name: string;
type: string;
date: string | null;
year: number | null;
month: number | null;
day: number | null;
place: string | null;
context: string | null;
reference: string | null;
durationMinutes: number | null;
participants: string[];
audioUrl: string | null;
};
const MONTH_NAMES = [
"",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
function MonthSection({
label,
items,
defaultOpen,
}: {
month: number;
label: string;
items: TranscriptionItem[];
defaultOpen: boolean;
}) {
const [open, setOpen] = useState(defaultOpen);
return (
<div>
<button
onClick={() => setOpen(!open)}
className="flex items-center gap-3 w-full px-5 py-3 text-left hover:bg-muted/50 transition-colors"
>
<ChevronRight
className={cn(
"size-4 text-muted-foreground shrink-0 transition-transform",
open && "rotate-90",
)}
/>
<span className="font-semibold text-foreground text-sm">{label}</span>
<span className="text-xs text-muted-foreground tabular-nums">
{items.length}{" "}
{items.length === 1 ? "transcription" : "transcriptions"}
</span>
</button>
{open && (
<ul className="divide-y divide-border">
{items.map((t) => (
<li key={t.slug}>
<Link
href={`/transcriptions/${t.slug}`}
className="flex items-center px-5 pl-12 py-3.5 gap-3 hover:bg-muted transition-colors"
>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<p className="font-medium text-foreground text-sm">
{t.name}
</p>
<Badge
variant={t.type === "lecture" ? "default" : "secondary"}
className="text-[10px] px-1.5 py-0"
>
{t.type === "lecture" ? "Lecture" : "Conversation"}
</Badge>
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1 text-xs text-muted-foreground">
{t.date && (
<span className="flex items-center gap-1">
<Calendar className="size-3" />
{t.date}
</span>
)}
{t.place && (
<span className="flex items-center gap-1">
<MapPin className="size-3" />
{t.place}
</span>
)}
{t.reference && (
<span className="text-primary/70">{t.reference}</span>
)}
{t.durationMinutes && t.durationMinutes > 0 && (
<span className="flex items-center gap-1">
<Clock className="size-3" />
{t.durationMinutes} min
</span>
)}
</div>
</div>
{t.audioUrl && (
<PlayAudioButton
audioUrl={t.audioUrl}
title={t.name}
subtitle={[t.date, t.place].filter(Boolean).join(" · ")}
sourceUrl={`/transcriptions/${t.slug}`}
type={t.type}
variant="compact"
/>
)}
</Link>
</li>
))}
</ul>
)}
</div>
);
}
export function TranscriptionYearBrowser({
transcriptions,
year,
totalCount,
}: {
transcriptions: TranscriptionItem[];
year: number;
totalCount: number;
}) {
const monthGroups = useMemo(() => {
const map = new Map<number, TranscriptionItem[]>();
for (const t of transcriptions) {
const m = t.month && t.month >= 1 && t.month <= 12 ? t.month : 0;
if (!map.has(m)) map.set(m, []);
map.get(m)!.push(t);
}
const groups: {
month: number;
label: string;
items: TranscriptionItem[];
}[] = [];
for (let m = 1; m <= 12; m++) {
const items = map.get(m);
if (items && items.length > 0) {
groups.push({ month: m, label: MONTH_NAMES[m], items });
}
}
const unknown = map.get(0);
if (unknown && unknown.length > 0) {
groups.push({ month: 0, label: "Unknown Month", items: unknown });
}
return groups;
}, [transcriptions]);
// Display order — month by month — is the order Next / Previous play in.
const queueItems = useMemo(
() => monthGroups.flatMap((g) => g.items),
[monthGroups],
);
const lectureCount = transcriptions.filter(
(t) => t.type === "lecture",
).length;
const conversationCount = transcriptions.filter(
(t) => t.type === "conversation",
).length;
return (
<>
<PageHeader sticky>
<Link
href="/transcriptions"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Transcriptions
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
Transcriptions from {year}
</h1>
<p className="mt-1 text-sm text-muted-foreground">
{totalCount} transcriptions · {lectureCount} lectures ·{" "}
{conversationCount} conversations
</p>
</PageHeader>
<main className="max-w-5xl mx-auto px-6 py-8 space-y-6">
{monthGroups.length === 0 ? (
<div className="py-12 text-center text-muted-foreground">
<p className="text-lg font-medium">
No transcriptions for this year
</p>
</div>
) : (
<TranscriptQueue items={queueItems}>
<Card className="overflow-hidden">
<CardContent className="p-0 divide-y divide-border">
{monthGroups.map((mg, i) => (
<MonthSection
key={mg.month}
month={mg.month}
label={mg.label}
items={mg.items}
defaultOpen={i === 0 || monthGroups.length <= 3}
/>
))}
</CardContent>
</Card>
</TranscriptQueue>
)}
</main>
</>
);
}