This repository has been archived on 2026-07-21. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cloud-reader/frontend/src/pages/Reader.tsx
T
markoandreid f84a593c5d Implement: Refactor: Consolidate Duplicate Backend and Frontend Implementations (#11)
Reviewed and merged by Reid (Hermes Reviewer)

Co-authored-by: crisleo-hermes <hermes@codescripters.org>
Co-committed-by: crisleo-hermes <hermes@codescripters.org>
2026-05-26 03:54:25 +00:00

64 lines
3.8 KiB
TypeScript

import React, { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { booksApi } from "../api/books";
import type { EBookDetail } from "../types/book";
export function ReaderPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [book, setBook] = useState<EBookDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const progressTimer = useRef<ReturnType<typeof setInterval> | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
const bookId = Number(id);
const saveProgress = useCallback(async () => {
if (!scrollRef.current || !book) return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
const position = Math.min(100, Math.round((scrollTop / (scrollHeight - clientHeight)) * 100));
try { await booksApi.updateProgress(bookId, { current_position: position }); } catch { /* silent */ }
}, [bookId, book]);
useEffect(() => {
const loadBook = async () => {
setLoading(true); setError(null);
try {
const data = await booksApi.getEBook(bookId);
setBook(data);
document.title = data.title;
} catch (err) { setError(err instanceof Error ? err.message : "Failed to load book"); }
finally { setLoading(false); }
};
void loadBook();
return () => { document.title = "Cloud Reader"; };
}, [bookId]);
useEffect(() => {
progressTimer.current = setInterval(() => { void saveProgress(); }, 5000);
return () => { if (progressTimer.current) clearInterval(progressTimer.current); };
}, [saveProgress]);
useEffect(() => {
const handleBeforeUnload = () => { void saveProgress(); };
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [saveProgress]);
if (loading) return <div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "100vh", color: "#888" }}><p>Loading book...</p></div>;
if (error) return <div style={{ display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", height: "100vh", gap: 16 }}><p style={{ color: "#e74c3c", fontSize: 16 }}>{error}</p><button onClick={() => navigate("/")} style={{ padding: "8px 16px", borderRadius: 6, border: "1px solid #ddd", background: "#fff", cursor: "pointer" }}> Back to Library</button></div>;
if (!book) return null;
return (
<div style={{ display: "flex", flexDirection: "column", height: "100vh", background: "#fff" }}>
<header style={{ display: "flex", alignItems: "center", padding: "12px 16px", borderBottom: "1px solid #eee", gap: 12, flexShrink: 0 }}>
<button onClick={() => navigate("/")} style={{ padding: "8px 16px", borderRadius: 6, border: "1px solid #ddd", background: "#fff", cursor: "pointer", fontSize: 14 }}> Library</button>
<div style={{ flex: 1 }}><h1 style={{ fontSize: 18, fontWeight: 600, color: "#1a1a2e", margin: 0 }}>{book.title}</h1><p style={{ fontSize: 14, color: "#888", margin: "4px 0 0" }}>{book.author}</p></div>
<button onClick={() => navigate(`/bookmarks-notes/${bookId}`)} style={{ padding: "8px 16px", borderRadius: 6, border: "1px solid #ddd", background: "#fff", cursor: "pointer", fontSize: 14 }}>📑 Bookmarks & Notes</button>
</header>
<div ref={scrollRef} style={{ flex: 1, overflow: "auto", background: "#fafafa" }}>
{book.file_url ? <iframe src={book.file_url} style={{ width: "100%", height: "100%", border: "none" }} title={book.title} /> : <div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "100%", color: "#888" }}><p>No file available.</p></div>}
</div>
</div>
);
}