feat: add chapter reader with reading progress bar and navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
consultoria-as
2026-02-17 07:47:18 +00:00
parent bf6a5f21e1
commit b187369f83
3 changed files with 177 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
import { notFound } from "next/navigation";
import { getAllGames, getGame, getChapter, getAdjacentChapters } from "@/lib/content";
import ChapterReader from "@/components/ChapterReader";
interface PageProps {
params: { game: string; chapter: string };
}
export function generateStaticParams() {
const games = getAllGames();
const paths: { game: string; chapter: string }[] = [];
for (const gameMeta of games) {
const game = getGame(gameMeta.slug);
for (const chapter of game.chapters) {
paths.push({ game: gameMeta.slug, chapter: chapter.slug });
}
}
return paths;
}
export function generateMetadata({ params }: PageProps) {
try {
const game = getGame(params.game);
const chapter = getChapter(params.game, params.chapter);
return {
title: `${chapter.title} - ${game.title} | Cronicas de los Reinos`,
description: `Capitulo ${chapter.number} de ${game.title}`,
};
} catch {
return { title: "No encontrado" };
}
}
export default function ChapterPage({ params }: PageProps) {
try {
const game = getGame(params.game);
const chapter = getChapter(params.game, params.chapter);
const { prev, next } = getAdjacentChapters(params.game, params.chapter);
return (
<ChapterReader
game={game}
chapter={chapter}
totalChapters={game.chapters.length}
prevChapter={prev}
nextChapter={next}
/>
);
} catch {
notFound();
}
}

View File

@@ -0,0 +1,96 @@
import Link from "next/link";
import { MDXRemote } from "next-mdx-remote/rsc";
import { GameMeta, Chapter } from "@/types";
import ReadingProgress from "./ReadingProgress";
interface Props {
game: GameMeta;
chapter: Chapter;
totalChapters: number;
prevChapter: string | null;
nextChapter: string | null;
}
export default function ChapterReader({
game,
chapter,
totalChapters,
prevChapter,
nextChapter,
}: Props) {
return (
<div className="min-h-screen bg-parchment text-stone-800">
<ReadingProgress color={game.accentColor} />
{/* Header */}
<header className="max-w-3xl mx-auto px-6 pt-8 pb-4">
<Link
href={`/${game.slug}`}
className="inline-flex items-center text-stone-500 hover:text-stone-700 font-lora text-sm transition-colors"
>
<span className="mr-2">&larr;</span> {game.title}
</Link>
</header>
{/* Chapter heading */}
<div className="max-w-3xl mx-auto px-6 pt-8 pb-12 text-center">
<p
className="font-playfair text-sm font-semibold tracking-widest uppercase mb-3"
style={{ color: game.color }}
>
Capitulo {chapter.number} de {totalChapters}
</p>
<h1 className="font-playfair text-3xl md:text-4xl font-bold text-stone-800 mb-4">
{chapter.title}
</h1>
<div className="flex items-center justify-center gap-4">
<div className="w-12 h-px bg-stone-300" />
<div
className="w-2 h-2 rotate-45"
style={{ backgroundColor: game.accentColor }}
/>
<div className="w-12 h-px bg-stone-300" />
</div>
</div>
{/* Chapter content */}
<article className="max-w-[700px] mx-auto px-6 pb-20 font-lora text-lg leading-relaxed text-stone-700 prose prose-stone prose-lg prose-headings:font-playfair prose-headings:text-stone-800 prose-p:mb-6 prose-p:indent-8 first:prose-p:indent-0">
<MDXRemote source={chapter.content} />
</article>
{/* Chapter navigation */}
<nav className="max-w-[700px] mx-auto px-6 pb-16">
<div className="border-t border-stone-300 pt-8 flex justify-between items-center">
{prevChapter ? (
<Link
href={`/${game.slug}/${prevChapter}`}
className="font-lora text-sm hover:text-stone-900 transition-colors"
style={{ color: game.color }}
>
&larr; Capitulo anterior
</Link>
) : (
<span />
)}
<Link
href={`/${game.slug}`}
className="font-lora text-sm text-stone-400 hover:text-stone-600 transition-colors"
>
Indice
</Link>
{nextChapter ? (
<Link
href={`/${game.slug}/${nextChapter}`}
className="font-lora text-sm hover:text-stone-900 transition-colors"
style={{ color: game.color }}
>
Siguiente capitulo &rarr;
</Link>
) : (
<span />
)}
</div>
</nav>
</div>
);
}

View File

@@ -0,0 +1,27 @@
"use client";
import { useEffect, useState } from "react";
export default function ReadingProgress({ color }: { color: string }) {
const [progress, setProgress] = useState(0);
useEffect(() => {
function handleScroll() {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
setProgress(docHeight > 0 ? (scrollTop / docHeight) * 100 : 0);
}
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<div className="fixed top-0 left-0 right-0 h-1 z-50 bg-parchment-dark/50">
<div
className="h-full transition-[width] duration-150"
style={{ width: `${progress}%`, backgroundColor: color }}
/>
</div>
);
}