The reading progress router tracks how far users have read in each book and their reading status.
Save progress
Update reading progress for a book.
await client.readingProgress.saveProgress({
bookUuid: "book-uuid",
ttuBookId: 123,
exploredCharCount: 5000,
bookCharCount: 100000,
readingTimeSeconds: 1800,
status: "reading",
});
Internal TTU book ID (for epub.js integration)
Number of characters read so far
Total characters in the book
Total reading time in seconds
Reading status: unread, reading, or completed
Response
Reading progress record ID
ISO timestamp of last read session
ISO timestamp when record was created
Get progress
Retrieve reading progress for a specific book.
const progress = await client.readingProgress.getProgress({
bookUuid: "book-uuid",
});
Response
Reading progress object, or null if no progress exists for this book.
List in-progress books
Get all books the user is currently reading.
const books = await client.readingProgress.listInProgress({
limit: 20,
});
Maximum number of books to return (1-50)
Response
Array of objects containing:
Complete book object (same structure as books.getBookWithMetadata)
Reading progress object for this book
Reading statuses
The reading status enum is defined in packages/api/src/constants.ts:
export const READING_STATUSES = {
UNREAD: "unread",
READING: "reading",
COMPLETED: "completed",
} as const;
The progress input schema is defined in packages/api/src/routers/reading-progress/reading-progress.model.ts:
export const SaveProgressInput = z.object({
bookUuid: z.string(),
ttuBookId: z.number().int().optional(),
exploredCharCount: z.number().int().optional(),
bookCharCount: z.number().int().optional(),
readingTimeSeconds: z.number().int().optional(),
status: z
.enum([
READING_STATUSES.UNREAD,
READING_STATUSES.READING,
READING_STATUSES.COMPLETED,
])
.optional(),
});
Example: Reading progress tracker
import { useEffect } from "react";
import { orpc } from "@/utils/orpc";
function BookReader({ bookUuid }: { bookUuid: string }) {
const { data: progress } = orpc.readingProgress.getProgress.useQuery({
bookUuid,
});
const saveProgress = orpc.readingProgress.saveProgress.useMutation();
// Auto-save progress every 30 seconds
useEffect(() => {
const interval = setInterval(() => {
const exploredChars = getExploredCharCount(); // Your implementation
const totalChars = getTotalCharCount();
const readingTime = getReadingTimeSeconds();
saveProgress.mutate({
bookUuid,
exploredCharCount: exploredChars,
bookCharCount: totalChars,
readingTimeSeconds: readingTime,
status: "reading",
});
}, 30000);
return () => clearInterval(interval);
}, [bookUuid]);
const percentComplete = progress
? (progress.exploredCharCount / progress.bookCharCount) * 100
: 0;
return (
<div>
<div>Progress: {percentComplete.toFixed(1)}%</div>
<div>Reading time: {Math.floor(progress?.readingTimeSeconds / 60)} minutes</div>
{/* Your reader component */}
</div>
);
}
Example: Continue reading section
import { orpc } from "@/utils/orpc";
function ContinueReading() {
const { data: books } = orpc.readingProgress.listInProgress.useQuery({
limit: 10,
});
return (
<div>
<h2>Continue reading</h2>
{books?.map(({ book, progress }) => {
const percent = (progress.exploredCharCount / progress.bookCharCount) * 100;
return (
<div key={book.uuid}>
<img src={book.cover} alt={book.title} />
<h3>{book.title}</h3>
<div>
<progress value={percent} max={100} />
<span>{percent.toFixed(0)}% complete</span>
</div>
</div>
);
})}
</div>
);
}