232 lines
7.0 KiB
TypeScript
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"
|
|
>
|
|
← 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>
|
|
</>
|
|
);
|
|
}
|