fix: ui fixes in transcript lusting for books
This commit is contained in:
@@ -38,13 +38,6 @@ export type TranscriptionItem = {
|
|||||||
audioUrl: string | null;
|
audioUrl: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type YearGroup = {
|
|
||||||
year: number;
|
|
||||||
label: string;
|
|
||||||
count: number;
|
|
||||||
transcriptions: TranscriptionItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChapterGroup = {
|
type ChapterGroup = {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -60,48 +53,13 @@ type SectionGroup = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Grouping helpers
|
// Grouping
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function buildYearGroups(transcriptions: TranscriptionItem[]): YearGroup[] {
|
// Group all of a book's transcripts into sections (canto/līlā) → chapters,
|
||||||
const map = new Map<number, TranscriptionItem[]>();
|
|
||||||
|
|
||||||
for (const t of transcriptions) {
|
|
||||||
const y = t.year ?? 0;
|
|
||||||
if (!map.has(y)) map.set(y, []);
|
|
||||||
map.get(y)!.push(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
const groups: YearGroup[] = [];
|
|
||||||
|
|
||||||
const years = Array.from(map.keys()).sort((a, b) => a - b);
|
|
||||||
for (const y of years) {
|
|
||||||
if (y === 0) continue;
|
|
||||||
const items = map.get(y)!;
|
|
||||||
groups.push({
|
|
||||||
year: y,
|
|
||||||
label: String(y),
|
|
||||||
count: items.length,
|
|
||||||
transcriptions: items,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const undated = map.get(0);
|
|
||||||
if (undated && undated.length > 0) {
|
|
||||||
groups.push({
|
|
||||||
year: 0,
|
|
||||||
label: "Undated",
|
|
||||||
count: undated.length,
|
|
||||||
transcriptions: undated,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group a year's transcripts into sections (canto/līlā) → chapter accordions,
|
|
||||||
// ordered by the parsed sort tuple. Books without a canto/līlā level collapse
|
// ordered by the parsed sort tuple. Books without a canto/līlā level collapse
|
||||||
// into a single section with a null label. Only sections/chapters that have
|
// into a single section with a null label. Only sections/chapters that have
|
||||||
// transcripts appear.
|
// transcripts appear. Transcripts arrive already sorted chronologically.
|
||||||
function buildSections(
|
function buildSections(
|
||||||
transcriptions: TranscriptionItem[],
|
transcriptions: TranscriptionItem[],
|
||||||
code: string,
|
code: string,
|
||||||
@@ -152,68 +110,93 @@ function buildSections(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// Flatten sections into a linear chapter list for prev/next navigation and
|
||||||
// Year Sidebar
|
// active-chapter lookup, carrying each chapter's section label for display.
|
||||||
// ---------------------------------------------------------------------------
|
type FlatChapter = { sectionLabel: string | null; chapter: ChapterGroup };
|
||||||
|
|
||||||
function YearSidebar({
|
function flattenChapters(sections: SectionGroup[]): FlatChapter[] {
|
||||||
groups,
|
return sections.flatMap((s) =>
|
||||||
activeYear,
|
s.chapters.map((chapter) => ({ sectionLabel: s.label, chapter })),
|
||||||
onSelect,
|
|
||||||
}: {
|
|
||||||
groups: YearGroup[];
|
|
||||||
activeYear: number;
|
|
||||||
onSelect: (year: number) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
|
|
||||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
|
||||||
Years
|
|
||||||
</h2>
|
|
||||||
<nav className="flex flex-col gap-0.5">
|
|
||||||
{groups.map((g) => {
|
|
||||||
const isActive = g.year === activeYear;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={g.year}
|
|
||||||
onClick={() => onSelect(g.year)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-between gap-2 px-2 py-1.5 rounded-md text-sm transition-colors text-left",
|
|
||||||
isActive
|
|
||||||
? "bg-primary/10 text-primary font-medium"
|
|
||||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="truncate">{g.label}</span>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"text-xs tabular-nums shrink-0",
|
|
||||||
isActive ? "text-primary/70" : "text-muted-foreground/60",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{g.count}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Mobile drawer
|
// Chapter nav list (shared by sidebar + mobile drawer)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function YearMobileNav({
|
function ChapterNavList({
|
||||||
groups,
|
sections,
|
||||||
activeYear,
|
activeKey,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
sections: SectionGroup[];
|
||||||
|
activeKey: string;
|
||||||
|
onSelect: (key: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<nav className="flex flex-col gap-3">
|
||||||
|
{sections.map((section) => (
|
||||||
|
<div key={section.key} className="flex flex-col gap-0.5">
|
||||||
|
{section.label && (
|
||||||
|
<div className="px-2 pt-1 pb-0.5 text-xs font-bold uppercase tracking-wider text-muted-foreground/80">
|
||||||
|
{section.label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{section.chapters.map((ch) => {
|
||||||
|
const isActive = ch.key === activeKey;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={ch.key}
|
||||||
|
onClick={() => onSelect(ch.key)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-between gap-2 px-2 py-1.5 rounded-md text-sm transition-colors text-left",
|
||||||
|
isActive
|
||||||
|
? "bg-primary/10 text-primary font-medium"
|
||||||
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="truncate">{ch.label}</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-xs tabular-nums shrink-0",
|
||||||
|
isActive ? "text-primary/70" : "text-muted-foreground/60",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{ch.transcriptions.length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChapterSidebar(props: {
|
||||||
|
sections: SectionGroup[];
|
||||||
|
activeKey: string;
|
||||||
|
onSelect: (key: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<aside className="hidden lg:block w-60 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
|
||||||
|
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||||
|
Chapters
|
||||||
|
</h2>
|
||||||
|
<ChapterNavList {...props} />
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChapterMobileNav({
|
||||||
|
sections,
|
||||||
|
activeKey,
|
||||||
onSelect,
|
onSelect,
|
||||||
title,
|
title,
|
||||||
}: {
|
}: {
|
||||||
groups: YearGroup[];
|
sections: SectionGroup[];
|
||||||
activeYear: number;
|
activeKey: string;
|
||||||
onSelect: (year: number) => void;
|
onSelect: (key: string) => void;
|
||||||
title: string;
|
title: string;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -227,47 +210,22 @@ function YearMobileNav({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Menu className="size-4" />
|
<Menu className="size-4" />
|
||||||
<span className="hidden sm:inline">Years</span>
|
<span className="hidden sm:inline">Chapters</span>
|
||||||
</DrawerTrigger>
|
</DrawerTrigger>
|
||||||
<DrawerContent direction="left" className="h-full">
|
<DrawerContent direction="left" className="h-full">
|
||||||
<DrawerHeader className="text-left border-b border-border pb-4 mb-2">
|
<DrawerHeader className="text-left border-b border-border pb-4 mb-2">
|
||||||
<DrawerTitle>{title}</DrawerTitle>
|
<DrawerTitle>{title}</DrawerTitle>
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<nav className="overflow-y-auto px-4 pb-6 pt-2">
|
<div className="overflow-y-auto px-4 pb-6 pt-2">
|
||||||
<div className="flex flex-col gap-0.5">
|
<ChapterNavList
|
||||||
{groups.map((g) => {
|
sections={sections}
|
||||||
const isActive = g.year === activeYear;
|
activeKey={activeKey}
|
||||||
return (
|
onSelect={(key) => {
|
||||||
<DrawerClose key={g.year} asChild>
|
onSelect(key);
|
||||||
<button
|
setOpen(false);
|
||||||
onClick={() => {
|
}}
|
||||||
onSelect(g.year);
|
/>
|
||||||
setOpen(false);
|
</div>
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-between gap-2 px-2 py-2 rounded-md text-sm transition-colors text-left",
|
|
||||||
isActive
|
|
||||||
? "bg-primary/10 text-primary font-medium"
|
|
||||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{g.label}</span>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"text-xs tabular-nums",
|
|
||||||
isActive
|
|
||||||
? "text-primary/70"
|
|
||||||
: "text-muted-foreground/60",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{g.count}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</DrawerClose>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
@@ -277,34 +235,34 @@ function YearMobileNav({
|
|||||||
// Prev / Next nav
|
// Prev / Next nav
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function YearNav({
|
function ChapterNav({
|
||||||
prev,
|
prev,
|
||||||
next,
|
next,
|
||||||
groups,
|
sections,
|
||||||
activeYear,
|
activeKey,
|
||||||
onSelect,
|
onSelect,
|
||||||
drawerTitle,
|
drawerTitle,
|
||||||
}: {
|
}: {
|
||||||
prev: YearGroup | null;
|
prev: FlatChapter | null;
|
||||||
next: YearGroup | null;
|
next: FlatChapter | null;
|
||||||
groups: YearGroup[];
|
sections: SectionGroup[];
|
||||||
activeYear: number;
|
activeKey: string;
|
||||||
onSelect: (year: number) => void;
|
onSelect: (key: string) => void;
|
||||||
drawerTitle: string;
|
drawerTitle: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<YearMobileNav
|
<ChapterMobileNav
|
||||||
groups={groups}
|
sections={sections}
|
||||||
activeYear={activeYear}
|
activeKey={activeKey}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
title={drawerTitle}
|
title={drawerTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{prev ? (
|
{prev ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => onSelect(prev.year)}
|
onClick={() => onSelect(prev.chapter.key)}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline", size: "sm" }),
|
buttonVariants({ variant: "outline", size: "sm" }),
|
||||||
"gap-1.5",
|
"gap-1.5",
|
||||||
@@ -312,7 +270,7 @@ function YearNav({
|
|||||||
>
|
>
|
||||||
<ChevronLeft className="size-4" />
|
<ChevronLeft className="size-4" />
|
||||||
<span className="hidden sm:inline truncate max-w-[180px]">
|
<span className="hidden sm:inline truncate max-w-[180px]">
|
||||||
{prev.label}
|
{prev.chapter.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="sm:hidden">Previous</span>
|
<span className="sm:hidden">Previous</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -323,14 +281,14 @@ function YearNav({
|
|||||||
|
|
||||||
{next ? (
|
{next ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => onSelect(next.year)}
|
onClick={() => onSelect(next.chapter.key)}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline", size: "sm" }),
|
buttonVariants({ variant: "outline", size: "sm" }),
|
||||||
"gap-1.5",
|
"gap-1.5",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:inline truncate max-w-[180px]">
|
<span className="hidden sm:inline truncate max-w-[180px]">
|
||||||
{next.label}
|
{next.chapter.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="sm:hidden">Next</span>
|
<span className="sm:hidden">Next</span>
|
||||||
<ChevronRight className="size-4" />
|
<ChevronRight className="size-4" />
|
||||||
@@ -343,101 +301,60 @@ function YearNav({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Chapter section — collapsible group of transcriptions for a single chapter
|
// Lecture row
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function ChapterSection({
|
function LectureRow({ t }: { t: TranscriptionItem }) {
|
||||||
group,
|
|
||||||
defaultOpen,
|
|
||||||
}: {
|
|
||||||
group: ChapterGroup;
|
|
||||||
defaultOpen: boolean;
|
|
||||||
}) {
|
|
||||||
const [open, setOpen] = useState(defaultOpen);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Link
|
||||||
<button
|
href={`/transcriptions/${t.slug}`}
|
||||||
onClick={() => setOpen(!open)}
|
className="flex items-center px-5 py-3.5 gap-3 hover:bg-muted transition-colors"
|
||||||
className="flex items-center gap-3 w-full px-5 py-3 text-left hover:bg-muted/50 transition-colors"
|
>
|
||||||
>
|
<div className="min-w-0 flex-1">
|
||||||
<ChevronRight
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
className={cn(
|
<p className="font-medium text-foreground text-sm">{t.name}</p>
|
||||||
"size-4 text-muted-foreground shrink-0 transition-transform",
|
<Badge
|
||||||
open && "rotate-90",
|
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"
|
||||||
/>
|
/>
|
||||||
<span className="font-semibold text-foreground text-sm">
|
|
||||||
{group.label}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-muted-foreground tabular-nums">
|
|
||||||
{group.transcriptions.length}{" "}
|
|
||||||
{group.transcriptions.length === 1
|
|
||||||
? "transcription"
|
|
||||||
: "transcriptions"}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{open && (
|
|
||||||
<ul className="divide-y divide-border">
|
|
||||||
{group.transcriptions.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>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,45 +373,43 @@ export function TranscriptionBookBrowser({
|
|||||||
title: string;
|
title: string;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}) {
|
}) {
|
||||||
const yearGroups = useMemo(
|
const sections = useMemo(
|
||||||
() => buildYearGroups(transcriptions),
|
() => buildSections(transcriptions, code),
|
||||||
[transcriptions],
|
[transcriptions, code],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [activeYearRaw, setActiveYear] = useState<number>(
|
const flat = useMemo(() => flattenChapters(sections), [sections]);
|
||||||
yearGroups.length > 0 ? yearGroups[0].year : 0,
|
|
||||||
|
const [activeKeyRaw, setActiveKey] = useState<string>(
|
||||||
|
flat.length > 0 ? flat[0].chapter.key : "",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Derive a safe active year
|
// Derive a safe active chapter key (the data could change underneath us).
|
||||||
const activeYear = useMemo(() => {
|
const activeKey = useMemo(() => {
|
||||||
if (yearGroups.length === 0) return activeYearRaw;
|
if (flat.length === 0) return activeKeyRaw;
|
||||||
if (yearGroups.find((g) => g.year === activeYearRaw)) return activeYearRaw;
|
if (flat.find((f) => f.chapter.key === activeKeyRaw)) return activeKeyRaw;
|
||||||
return yearGroups[0].year;
|
return flat[0].chapter.key;
|
||||||
}, [yearGroups, activeYearRaw]);
|
}, [flat, activeKeyRaw]);
|
||||||
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleYearSelect = (year: number) => {
|
const handleSelect = (key: string) => {
|
||||||
setActiveYear(year);
|
setActiveKey(key);
|
||||||
contentRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
contentRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Active group + neighbors
|
const activeIdx = flat.findIndex((f) => f.chapter.key === activeKey);
|
||||||
const activeIdx = yearGroups.findIndex((g) => g.year === activeYear);
|
const active = activeIdx >= 0 ? flat[activeIdx] : null;
|
||||||
const activeGroup = activeIdx >= 0 ? yearGroups[activeIdx] : null;
|
const prev = activeIdx > 0 ? flat[activeIdx - 1] : null;
|
||||||
const prev = activeIdx > 0 ? yearGroups[activeIdx - 1] : null;
|
|
||||||
const next =
|
const next =
|
||||||
activeIdx >= 0 && activeIdx < yearGroups.length - 1
|
activeIdx >= 0 && activeIdx < flat.length - 1 ? flat[activeIdx + 1] : null;
|
||||||
? yearGroups[activeIdx + 1]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Sections (canto/līlā) → chapters within the active year
|
const activeFullLabel = active
|
||||||
const sections = useMemo(
|
? active.sectionLabel
|
||||||
() => buildSections(activeGroup?.transcriptions ?? [], code),
|
? `${active.sectionLabel} · ${active.chapter.label}`
|
||||||
[activeGroup, code],
|
: active.chapter.label
|
||||||
);
|
: "";
|
||||||
|
const activeCount = active?.chapter.transcriptions.length ?? 0;
|
||||||
const activeYearLabel = activeGroup?.label ?? "Unknown";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -508,79 +423,61 @@ export function TranscriptionBookBrowser({
|
|||||||
← Transcriptions by Book
|
← Transcriptions by Book
|
||||||
</Link>
|
</Link>
|
||||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||||
<span className="text-primary">{title}: </span>
|
<span className="text-primary">{title}</span>
|
||||||
{activeYearLabel}
|
{activeFullLabel ? `: ${activeFullLabel}` : ""}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-muted-foreground">
|
<p className="mt-1 text-muted-foreground">
|
||||||
{activeGroup?.count ?? 0}{" "}
|
{activeCount} {activeCount === 1 ? "lecture" : "lectures"} in this
|
||||||
{(activeGroup?.count ?? 0) === 1
|
chapter · {totalCount} total
|
||||||
? "transcription"
|
|
||||||
: "transcriptions"}{" "}
|
|
||||||
in {activeYearLabel} · {totalCount} total
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Sidebar + Main */}
|
{/* Sidebar + Main */}
|
||||||
<div className="max-w-5xl mx-auto flex">
|
<div className="max-w-5xl mx-auto flex">
|
||||||
{/* Left sidebar — years */}
|
{/* Left sidebar — chapters */}
|
||||||
<YearSidebar
|
<ChapterSidebar
|
||||||
groups={yearGroups}
|
sections={sections}
|
||||||
activeYear={activeYear}
|
activeKey={activeKey}
|
||||||
onSelect={handleYearSelect}
|
onSelect={handleSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<main ref={contentRef} className="flex-1 min-w-0 px-6 py-8 space-y-8">
|
<main ref={contentRef} className="flex-1 min-w-0 px-6 py-8 space-y-8">
|
||||||
{/* Top navigation */}
|
{/* Top navigation */}
|
||||||
<YearNav
|
<ChapterNav
|
||||||
prev={prev}
|
prev={prev}
|
||||||
next={next}
|
next={next}
|
||||||
groups={yearGroups}
|
sections={sections}
|
||||||
activeYear={activeYear}
|
activeKey={activeKey}
|
||||||
onSelect={handleYearSelect}
|
onSelect={handleSelect}
|
||||||
drawerTitle={title}
|
drawerTitle={title}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Content — sections (canto/līlā) → chapter accordions */}
|
{/* Content — every lecture of the active chapter (all years/places) */}
|
||||||
{sections.length === 0 ? (
|
{!active || active.chapter.transcriptions.length === 0 ? (
|
||||||
<div className="py-12 text-center text-muted-foreground">
|
<div className="py-12 text-center text-muted-foreground">
|
||||||
<p className="text-lg font-medium">
|
<p className="text-lg font-medium">
|
||||||
No transcriptions for this year
|
No transcriptions for this chapter
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-12">
|
<Card className="overflow-hidden">
|
||||||
{sections.map((section) => (
|
<CardContent className="p-0 divide-y divide-border">
|
||||||
<section key={section.key} className="space-y-3">
|
{active.chapter.transcriptions.map((t) => (
|
||||||
{section.label && (
|
<LectureRow key={t.slug} t={t} />
|
||||||
<h2 className="text-lg font-bold text-foreground border-b border-border pb-2">
|
))}
|
||||||
{section.label}
|
</CardContent>
|
||||||
</h2>
|
</Card>
|
||||||
)}
|
|
||||||
<Card className="overflow-hidden">
|
|
||||||
<CardContent className="p-0 divide-y divide-border">
|
|
||||||
{section.chapters.map((cg, i) => (
|
|
||||||
<ChapterSection
|
|
||||||
key={cg.key}
|
|
||||||
group={cg}
|
|
||||||
defaultOpen={i === 0 || section.chapters.length <= 3}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bottom navigation */}
|
{/* Bottom navigation */}
|
||||||
<YearNav
|
<ChapterNav
|
||||||
prev={prev}
|
prev={prev}
|
||||||
next={next}
|
next={next}
|
||||||
groups={yearGroups}
|
sections={sections}
|
||||||
activeYear={activeYear}
|
activeKey={activeKey}
|
||||||
onSelect={handleYearSelect}
|
onSelect={handleSelect}
|
||||||
drawerTitle={title}
|
drawerTitle={title}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user