Resolve merge conflicts for PR #25: customizable mobile reading experience

- Backend: Replace PR's Book-only models with main's full models
  (EBook, BookChapter, DownloadRecord, ReadingProgress, ReadingSettings)
- Backend: Add reader app (ReadingSettings model, serializer, view, URL)
- Frontend: Port reader feature files from web/ to frontend/ (api, hooks,
  components/reader/, pages/ReadingPage, types/reader, reader.css)
- Frontend: Add /read/:id route to App.tsx for chapter-based reading view
- All imports adjusted for frontend/src directory structure
This commit is contained in:
Marko (Hermes Implementer)
2026-05-29 05:53:14 +00:00
parent b29ea8211c
commit 74fdae61db
14 changed files with 2420 additions and 72 deletions
@@ -0,0 +1,80 @@
/**
* TableOfContents — slide-in drawer listing all chapters.
* Tap a chapter to navigate. Current chapter is highlighted.
*/
import type { ChapterSummary } from "../../types/reader";
interface TableOfContentsProps {
chapters: ChapterSummary[];
currentChapterNumber: number;
isOpen: boolean;
onClose: () => void;
onNavigate: (number: number) => void;
}
export default function TableOfContents({
chapters,
currentChapterNumber,
isOpen,
onClose,
onNavigate,
}: TableOfContentsProps) {
const handleChapterClick = (number: number) => {
onNavigate(number);
onClose();
};
return (
<>
{/* Overlay */}
{isOpen && (
<div
className="toc-overlay"
onClick={onClose}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === "Escape") onClose();
}}
role="presentation"
/>
)}
{/* Drawer */}
<aside className={`toc-drawer ${isOpen ? "toc-drawer--open" : ""}`}>
<div className="toc-header">
<h2 className="toc-title">Contents</h2>
<button
type="button"
className="toc-close-btn"
onClick={onClose}
aria-label="Close table of contents"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
<nav className="toc-list">
{chapters.length === 0 && (
<p className="toc-empty">No chapters available.</p>
)}
{chapters.map((chapter) => (
<button
key={chapter.number}
type="button"
className={`toc-item ${
chapter.number === currentChapterNumber ? "toc-item--active" : ""
}`}
onClick={() => handleChapterClick(chapter.number)}
>
<span className="toc-item-number">{chapter.number}</span>
<span className="toc-item-title">{chapter.title}</span>
</button>
))}
</nav>
</aside>
</>
);
}