feat: implement bff layer

This commit is contained in:
gosangam
2026-05-10 00:35:14 +05:30
parent 5951eb5e18
commit 32da38b24f
91 changed files with 2434 additions and 11784 deletions
+4 -4
View File
@@ -1,12 +1,12 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { getBookMeta } from "@/lib/api";
export async function GET(
_req: Request,
{ params }: { params: Promise<{ book: string }> }
{ params }: { params: Promise<{ book: string }> },
) {
const { book } = await params;
const meta = await prisma.bookMeta.findUnique({ where: { book } });
const meta = await getBookMeta(book);
if (!meta) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json({ book: meta.book, ...meta.data as Record<string, unknown> });
return NextResponse.json(meta);
}
+21 -37
View File
@@ -1,5 +1,9 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import {
getSingleVerse,
getChapterVerseIndex,
getChapterVerses,
} from "@/lib/api";
export async function GET(
req: Request,
@@ -7,7 +11,7 @@ export async function GET(
) {
const { book } = await params;
const url = new URL(req.url);
const division = url.searchParams.get("division");
const division = url.searchParams.get("division") || null;
const chapterStr = url.searchParams.get("chapter");
const verseStr = url.searchParams.get("verse");
const indexOnly = url.searchParams.get("indexOnly") === "true";
@@ -22,20 +26,18 @@ export async function GET(
}
const chapter = parseInt(chapterStr);
const where: Record<string, unknown> = { book, chapter };
if (book === "bg") {
where.division = null;
} else if (division) {
where.division = division;
}
const effectiveDivision = book === "bg" ? null : division;
// --- Single verse mode: ?verse=N ---
if (verseStr) {
const verseNum = parseInt(verseStr);
const verse = await prisma.verse.findFirst({
where: { ...where, verse: verseNum },
});
const verse = await getSingleVerse(
book,
effectiveDivision,
chapter,
verseNum,
);
if (!verse) {
return NextResponse.json({ error: "Verse not found" }, { status: 404 });
@@ -46,15 +48,7 @@ export async function GET(
// --- Index-only mode: return lightweight verse list for the sidebar ---
if (indexOnly) {
const verses = await prisma.verse.findMany({
where,
orderBy: { verse: "asc" },
select: {
verse: true,
verseEnd: true,
reference: true,
},
});
const verses = await getChapterVerseIndex(book, effectiveDivision, chapter);
return NextResponse.json({ verses, total: verses.length });
}
@@ -62,24 +56,14 @@ export async function GET(
// --- Paginated mode: return full verse data in pages ---
const page = Math.max(1, parseInt(pageStr || "1"));
const limit = Math.min(100, Math.max(1, parseInt(limitStr || "20")));
const skip = (page - 1) * limit;
const [verses, total] = await Promise.all([
prisma.verse.findMany({
where,
orderBy: { verse: "asc" },
skip,
take: limit,
}),
prisma.verse.count({ where }),
]);
return NextResponse.json({
verses,
total,
const result = await getChapterVerses(
book,
effectiveDivision,
chapter,
page,
limit,
totalPages: Math.ceil(total / limit),
hasMore: skip + verses.length < total,
});
);
return NextResponse.json(result);
}
+3 -3
View File
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { getAllBooks } from "@/lib/api";
export async function GET() {
const metas = await prisma.bookMeta.findMany();
return NextResponse.json(metas.map((m) => ({ book: m.book, ...m.data as Record<string, unknown> })));
const books = await getAllBooks();
return NextResponse.json(books);
}
+2 -4
View File
@@ -1,9 +1,7 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { getAllGlossaryEntries } from "@/lib/api";
export async function GET() {
const entries = await prisma.glossaryEntry.findMany({
orderBy: { term: "asc" },
});
const entries = await getAllGlossaryEntries();
return NextResponse.json(entries);
}
-1
View File
@@ -60,7 +60,6 @@ export default async function BbdPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
+1 -8
View File
@@ -162,14 +162,7 @@ function SidebarLink({
item: BkSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
const label = item.label;
return (
<Link
-2
View File
@@ -118,7 +118,6 @@ export default async function BsPage() {
{introduction.title}
</p>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{introduction.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -133,7 +132,6 @@ export default async function BsPage() {
{foreword.title}
</p>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{foreword.paragraphs.length} paragraphs
</span>
</Link>
</li>
+1 -8
View File
@@ -162,14 +162,7 @@ function SidebarLink({
item: CbhcSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
const label = item.label;
return (
<Link
-1
View File
@@ -63,7 +63,6 @@ export default async function DsPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -63,7 +63,6 @@ export default async function EjPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -60,7 +60,6 @@ export default async function EkPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
+1 -8
View File
@@ -217,14 +217,7 @@ function SidebarLink({
item: GgSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
const label = item.label;
return (
<Link
-2
View File
@@ -64,7 +64,6 @@ export default async function GgPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -126,7 +125,6 @@ export default async function GgPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -60,7 +60,6 @@ export default async function HkhPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
+7 -4
View File
@@ -50,8 +50,11 @@ export async function generateMetadata({
const isVerse_ = content && "mantra" in content && "devanagari" in content;
const paragraphs = content
? isVerse_
? [(content as IsoVerse).translation, ...(content as IsoVerse).purport]
: (content as IsoIntroSegment).paragraphs
? [
(content as unknown as IsoVerse).translation,
...(content as unknown as IsoVerse).purport,
]
: (content as unknown as IsoIntroSegment).paragraphs
: undefined;
const quote = extractQuoteFromHl(sp, paragraphs);
@@ -64,7 +67,7 @@ export async function generateMetadata({
}
const title = isVerse_
? `${(content as IsoVerse).reference}: ${(content as IsoVerse).title}`
? `${(content as unknown as IsoVerse).reference}`
: content && "title" in content
? (content as IsoIntroSegment).title
: `Mantra ${slug}`;
@@ -110,7 +113,7 @@ export default async function IsoMantraPage({
{verse && (
<>
<span className="text-primary">{verse.reference}: </span>
{verse.title}
{verse.bookTitle}
</>
)}
{intro && intro.title}
-3
View File
@@ -68,7 +68,6 @@ export default async function IsoPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{frontPages.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -85,7 +84,6 @@ export default async function IsoPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{introduction.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -164,7 +162,6 @@ export default async function IsoPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{aboutAuthor.paragraphs.length} paragraphs
</span>
</Link>
</li>
-2
View File
@@ -72,7 +72,6 @@ export default async function KbVolumePage({
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -106,7 +105,6 @@ export default async function KbVolumePage({
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{ch.paragraphs.length} paragraphs
</span>
</Link>
</li>
+1 -8
View File
@@ -221,14 +221,7 @@ function SidebarLink({
item: LclSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
const label = item.label;
return (
<Link
-2
View File
@@ -64,7 +64,6 @@ export default async function LclPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -126,7 +125,6 @@ export default async function LclPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -60,7 +60,6 @@ export default async function LobPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -60,7 +60,6 @@ export default async function MgPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -116,7 +116,6 @@ export default async function MmPage() {
{introduction.title}
</p>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{introduction.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -60,7 +60,6 @@ export default async function MogPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -116,7 +116,6 @@ export default async function NbsPage() {
{introduction.title}
</p>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{introduction.paragraphs.length} paragraphs
</span>
</Link>
</li>
+1 -8
View File
@@ -203,14 +203,7 @@ function SidebarLink({
item: NodSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
const label = item.label;
return (
<Link
-1
View File
@@ -70,7 +70,6 @@ export default async function NodPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-3
View File
@@ -65,7 +65,6 @@ export default async function NoiPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{frontPages.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -82,7 +81,6 @@ export default async function NoiPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{preface.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -141,7 +139,6 @@ export default async function NoiPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{appendixes.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -60,7 +60,6 @@ export default async function OchkmPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -60,7 +60,6 @@ export default async function OwkPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -60,7 +60,6 @@ export default async function PoyPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
+1 -8
View File
@@ -239,14 +239,7 @@ function SidebarLink({
item: PqpaSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
const label = item.label;
return (
<Link
-2
View File
@@ -64,7 +64,6 @@ export default async function PqpaPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -151,7 +150,6 @@ export default async function PqpaPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-1
View File
@@ -60,7 +60,6 @@ export default async function RvPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-2
View File
@@ -64,7 +64,6 @@ export default async function SsrPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -123,7 +122,6 @@ export default async function SsrPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
-2
View File
@@ -64,7 +64,6 @@ export default async function SvaPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -123,7 +122,6 @@ export default async function SvaPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
+60 -25
View File
@@ -141,6 +141,31 @@ function TlcSidebar({
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
// Group chapters by part (if parts exist)
const hasParts = chapterItems.some((item) => item.partNumber != null);
const partGroups: {
partNumber: number;
partTitle: string;
chapters: TlcSidebarItem[];
}[] = [];
if (hasParts) {
const partMap = new Map<
number,
{ partTitle: string; chapters: TlcSidebarItem[] }
>();
for (const item of chapterItems) {
const pn = item.partNumber ?? 0;
const pt = item.partTitle ?? `Part ${pn}`;
if (!partMap.has(pn)) partMap.set(pn, { partTitle: pt, chapters: [] });
partMap.get(pn)!.chapters.push(item);
}
for (const [partNumber, { partTitle, chapters }] of partMap) {
partGroups.push({ partNumber, partTitle, chapters });
}
partGroups.sort((a, b) => a.partNumber - b.partNumber);
}
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">
{/* Front Matter */}
@@ -161,23 +186,40 @@ function TlcSidebar({
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters — grouped by part if available */}
{hasParts
? partGroups.map((group) => (
<div key={group.partNumber} className="mb-4">
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
{group.partTitle}
</h2>
<nav className="flex flex-col gap-0.5">
{group.chapters.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</div>
))
: chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
@@ -189,14 +231,7 @@ function SidebarLink({
item: TlcSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
const label = item.label;
return (
<Link
-1
View File
@@ -68,7 +68,6 @@ export default async function TlcPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
+1 -8
View File
@@ -226,14 +226,7 @@ function SidebarLink({
item: TlkSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
const label = item.label;
return (
<Link
-2
View File
@@ -64,7 +64,6 @@ export default async function TlkPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -126,7 +125,6 @@ export default async function TlkPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
+1 -8
View File
@@ -226,14 +226,7 @@ function SidebarLink({
item: TqkSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
const label = item.label;
return (
<Link
-2
View File
@@ -64,7 +64,6 @@ export default async function TqkPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
@@ -126,7 +125,6 @@ export default async function TqkPage() {
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
+11 -27
View File
@@ -10,8 +10,7 @@ import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { MobileNav, type MobileNavGroup } from "@/components/mobile-nav";
import { prisma } from "@/lib/db";
import type { JsonValue } from "@prisma/client/runtime/library";
import { bff, bffList } from "@/lib/bff";
// ---------------------------------------------------------------------------
// HTML sanitisation (same pattern used across all readers)
@@ -31,25 +30,6 @@ function sanitizeHtml(html: string): string {
return clean;
}
function normalizeParagraphs(paragraphs: string[] | JsonValue): string[] {
let result: string[];
if (typeof paragraphs === "string") {
try {
const parsed = JSON.parse(paragraphs);
result = Array.isArray(parsed) ? parsed : [paragraphs];
} catch {
result = [paragraphs];
}
} else if (Array.isArray(paragraphs)) {
result = paragraphs as string[];
} else {
result = [];
}
return result.map(sanitizeHtml);
}
// ---------------------------------------------------------------------------
// Data fetching
// ---------------------------------------------------------------------------
@@ -69,7 +49,14 @@ type SidebarItem = {
};
async function getEssay(slug: string): Promise<EssayData | null> {
const doc = await prisma.essay.findUnique({ where: { slug } });
const doc = await bff<{
slug: string;
title: string;
author: string;
year: number | null;
group: string | null;
paragraphs: string[];
}>(`/essays/${slug}`);
if (!doc) return null;
return {
slug: doc.slug,
@@ -77,15 +64,12 @@ async function getEssay(slug: string): Promise<EssayData | null> {
author: doc.author,
year: doc.year,
group: doc.group,
paragraphs: normalizeParagraphs(doc.paragraphs),
paragraphs: (doc.paragraphs ?? []).map(sanitizeHtml),
};
}
async function getEssaySidebar(): Promise<SidebarItem[]> {
return prisma.essay.findMany({
orderBy: [{ group: "asc" }, { order: "asc" }],
select: { slug: true, title: true },
});
return bffList<SidebarItem>("/essays");
}
// ---------------------------------------------------------------------------
+7 -5
View File
@@ -4,7 +4,7 @@ import type { Metadata } from "next";
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import { buildOgMeta } from "@/lib/og";
import { prisma } from "@/lib/db";
import { bffList } from "@/lib/bff";
import { EssaysGrid } from "./essays-grid";
export function generateMetadata(): Metadata {
@@ -17,10 +17,12 @@ export function generateMetadata(): Metadata {
}
async function getAllEssays() {
return prisma.essay.findMany({
orderBy: [{ group: "asc" }, { order: "asc" }],
select: { slug: true, title: true, year: true, group: true },
});
return bffList<{
slug: string;
title: string;
year: number | null;
group: string | null;
}>("/essays");
}
export default async function EssaysPage() {
@@ -67,7 +67,11 @@ export function PlayAudioButton({
aria-label={isThisPlaying ? "Pause" : "Play audio"}
>
{isThisPlaying ? (
<Pause className="size-3.5" />
<AudioWaveAnimation
paused={false}
colorClassName="bg-primary-foreground"
size="sm"
/>
) : (
<Play className="size-3.5 ml-0.5" />
)}
+13 -2
View File
@@ -8,7 +8,8 @@ import { buildOgMeta } from "@/lib/og";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Clock } from "lucide-react";
import { Clock, MapPin } from "lucide-react";
import { PlayAudioButton } from "@/app/transcriptions/[slug]/play-audio-button";
export async function generateMetadata({
params,
@@ -60,7 +61,7 @@ export default async function TranscriptionsByPlacePage({
<Link
key={t.slug}
href={`/transcriptions/${t.slug}`}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 py-3.5 gap-1 sm:gap-4 hover:bg-muted transition-colors"
className="flex items-center px-5 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">
@@ -87,6 +88,16 @@ export default async function TranscriptionsByPlacePage({
)}
</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>
))}
</CardContent>
@@ -8,6 +8,7 @@ import { buttonVariants } from "@/components/ui/button";
import { ReadingSettings } from "@/components/reading-settings";
import { cn } from "@/lib/utils";
import { ChevronLeft, ChevronRight, Menu, Clock, MapPin } from "lucide-react";
import { PlayAudioButton } from "@/app/transcriptions/[slug]/play-audio-button";
import {
Drawer,
DrawerClose,
@@ -34,6 +35,7 @@ export type TranscriptionItem = {
reference: string | null;
durationMinutes: number | null;
participants: string[];
audioUrl: string | null;
};
type YearGroup = {
@@ -364,7 +366,7 @@ function MonthSection({
<li key={t.slug}>
<Link
href={`/transcriptions/${t.slug}`}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 pl-12 py-3.5 gap-1 sm:gap-4 hover:bg-muted transition-colors"
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">
@@ -396,6 +398,16 @@ function MonthSection({
)}
</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>
))}