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:
54
src/app/[game]/[chapter]/page.tsx
Normal file
54
src/app/[game]/[chapter]/page.tsx
Normal 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();
|
||||
}
|
||||
}
|
||||
96
src/components/ChapterReader.tsx
Normal file
96
src/components/ChapterReader.tsx
Normal 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">←</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 }}
|
||||
>
|
||||
← 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 →
|
||||
</Link>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
src/components/ReadingProgress.tsx
Normal file
27
src/components/ReadingProgress.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user