Skip to content

Commit 1d0ef83

Browse files
committed
feat(ui): add Continue/Read button to series detail page
Add a prominent button next to Download on the series detail page that navigates directly to the next book to read, removing the need to scroll through the book list to find it. The button picks the next book by sorting by number and selecting the first in-progress book, or the first unread book if none are in progress. It shows "Continue" for books with existing progress and "Read" for fresh starts. Hidden when all books are completed. The Download button becomes secondary (light variant) when the Continue button is present. Reuses the same query cache as SeriesBookList so no extra API calls.
1 parent 95760b2 commit 1d0ef83

1 file changed

Lines changed: 43 additions & 2 deletions

File tree

web/src/pages/SeriesDetail.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { useDisclosure } from "@mantine/hooks";
2020
import { notifications } from "@mantine/notifications";
2121
import {
2222
IconAnalyze,
23+
IconBook,
2324
IconBookOff,
2425
IconCheck,
2526
IconChevronDown,
@@ -37,7 +38,7 @@ import {
3738
IconWand,
3839
} from "@tabler/icons-react";
3940
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
40-
import { useState } from "react";
41+
import { useMemo, useState } from "react";
4142
import { Link, useNavigate, useParams } from "react-router-dom";
4243
import {
4344
type PluginActionDto,
@@ -156,6 +157,29 @@ export function SeriesDetail() {
156157
enabled: canEditSeries && !!series, // Only fetch if user can edit series and series is loaded
157158
});
158159

160+
// Fetch books for this series to determine the next book to continue reading
161+
const { data: seriesBooks } = useQuery({
162+
queryKey: ["series-books", seriesId, false],
163+
queryFn: () => seriesApi.getBooks(seriesId!),
164+
enabled: !!seriesId,
165+
});
166+
167+
// Find the next book to read: first in-progress book (by number), or first unread book
168+
const nextBook = useMemo(() => {
169+
if (!seriesBooks?.length) return null;
170+
const sorted = [...seriesBooks].sort(
171+
(a, b) => (a.number ?? 0) - (b.number ?? 0),
172+
);
173+
// Prefer the first book that is in-progress (has progress but not completed)
174+
const inProgress = sorted.find(
175+
(b) => b.readProgress && !b.readProgress.completed,
176+
);
177+
if (inProgress) return inProgress;
178+
// Otherwise, the first book with no progress at all
179+
const unread = sorted.find((b) => !b.readProgress);
180+
return unread ?? null;
181+
}, [seriesBooks]);
182+
159183
// Mutation to fetch preprocessed search title before opening modal
160184
const searchTitleMutation = useMutation({
161185
mutationFn: (pluginId: string) => {
@@ -752,9 +776,26 @@ export function SeriesDetail() {
752776

753777
{/* Action buttons */}
754778
<Group gap="sm" mt="xs">
779+
{nextBook && (
780+
<Button
781+
size="xs"
782+
variant="filled"
783+
leftSection={<IconBook size={14} />}
784+
onClick={() => {
785+
if (nextBook.fileFormat === "epub") {
786+
navigate(`/reader/${nextBook.id}`);
787+
} else {
788+
const page = nextBook.readProgress?.currentPage ?? 1;
789+
navigate(`/reader/${nextBook.id}?page=${page}`);
790+
}
791+
}}
792+
>
793+
{nextBook.readProgress ? "Continue" : "Read"}
794+
</Button>
795+
)}
755796
<Button
756797
size="xs"
757-
variant="filled"
798+
variant={nextBook ? "light" : "filled"}
758799
component="a"
759800
href={`/api/v1/series/${series.id}/download`}
760801
leftSection={<IconDownload size={14} />}

0 commit comments

Comments
 (0)