feat: add ssr

This commit is contained in:
gosangam
2026-04-17 22:51:58 +05:30
parent d88ababd76
commit 0aab1d23ec
97 changed files with 29891 additions and 68 deletions
+283
View File
@@ -0,0 +1,283 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getPqpaContent,
getPqpaSidebarItems,
type PqpaChapter,
type PqpaSidebarItem,
} from "@/lib/pqpa";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function PqpaChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getPqpaContent(slug);
if (!content) notFound();
const sidebarItems = await getPqpaSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as PqpaChapter).chapter === "number" &&
(content as PqpaChapter).chapter !== null;
const chapterNum = isChapter ? (content as PqpaChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/pqpa"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Perfect Questions Perfect Answers
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<PqpaSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<PqpaNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<PqpaNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function PqpaSidebar({
items,
currentSlug,
}: {
items: PqpaSidebarItem[];
currentSlug: string;
}) {
// Split items into groups for visual sections
// Front matter: intro-segment items before chapters
// Chapters: chapter items
// Conclusion: conclusion items
// Back matter: intro-segment items after conclusion (about-the-author)
const frontMatterItems: PqpaSidebarItem[] = [];
const chapterItems: PqpaSidebarItem[] = [];
const conclusionItems: PqpaSidebarItem[] = [];
const backMatterItems: PqpaSidebarItem[] = [];
let pastChapters = false;
for (const item of items) {
if (item.type === "chapter") {
pastChapters = true;
chapterItems.push(item);
} else if (item.type === "conclusion") {
pastChapters = true;
conclusionItems.push(item);
} else if (!pastChapters) {
frontMatterItems.push(item);
} else {
backMatterItems.push(item);
}
}
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 */}
{frontMatterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{frontMatterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* 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 mb-4">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Concluding Words */}
{conclusionItems.length > 0 && (
<nav className="flex flex-col gap-0.5 mb-4">
{conclusionItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
)}
{/* About the Author */}
{backMatterItems.length > 0 && (
<nav className="flex flex-col gap-0.5">
{backMatterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
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;
}
return (
<Link
href={`/books/pqpa/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={label}
>
{label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function PqpaNav({
prev,
next,
}: {
prev: PqpaSidebarItem | null;
next: PqpaSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/pqpa/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/pqpa/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+161
View File
@@ -0,0 +1,161 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getPqpaMeta, getPqpaIntroSegments } from "@/lib/pqpa";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function PqpaPage() {
const meta = await getPqpaMeta();
if (!meta) notFound();
const introSegments = await getPqpaIntroSegments();
// Split intro segments into front matter (order < 99) and back matter (order >= 99)
const frontMatter = introSegments.filter((seg) => seg.order < 99);
const backMatter = introSegments.filter((seg) => seg.order >= 99);
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{frontMatter.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{frontMatter.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/pqpa/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/pqpa/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
{/* Concluding Words */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Conclusion
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
<li>
<Link
href="/books/pqpa/concluding-words"
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
Concluding Words
</p>
</div>
</Link>
</li>
</ul>
</CardContent>
</Card>
</div>
{/* About the Author */}
{backMatter.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Back Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{backMatter.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/pqpa/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
</main>
</div>
);
}