Archived
feat: uv config other feats
- add uv configuration for the backend - update frontend to make auth work - add new auth endpoints - add bookmars feat - add reader feat
This commit is contained in:
+61
-94
@@ -1,7 +1,9 @@
|
||||
/**
|
||||
* API client for the reader module — reading settings, chapters, and progress.
|
||||
* Uses the shared axios client so JWT auth is attached automatically.
|
||||
*/
|
||||
|
||||
import api from "./client";
|
||||
import type {
|
||||
ChapterDetail,
|
||||
ChapterSummary,
|
||||
@@ -9,140 +11,105 @@ import type {
|
||||
ReadingSettings,
|
||||
} from "../types/reader";
|
||||
|
||||
const API_BASE = "/api";
|
||||
interface TocChapter {
|
||||
id: number;
|
||||
title: string;
|
||||
index: number;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current user's reading settings.
|
||||
* Auto-creates defaults on the server if none exist.
|
||||
*/
|
||||
export async function getReadingSettings(): Promise<ReadingSettings> {
|
||||
const response = await fetch(`${API_BASE}/reader/settings/`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch reading settings: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
return response.json() as Promise<ReadingSettings>;
|
||||
const { data } = await api.get<ReadingSettings>("/reader/settings/");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update (full or partial) the user's reading settings.
|
||||
*/
|
||||
export async function updateReadingSettings(
|
||||
settings: Partial<ReadingSettings>
|
||||
settings: Partial<ReadingSettings>,
|
||||
): Promise<ReadingSettings> {
|
||||
const method = settings.theme !== undefined ? "PUT" : "PATCH";
|
||||
const response = await fetch(`${API_BASE}/reader/settings/`, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to update reading settings: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
return response.json() as Promise<ReadingSettings>;
|
||||
const { data } = await api.patch<ReadingSettings>("/reader/settings/", settings);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the table of contents (chapter list) for a book (ebook).
|
||||
* Maps to EBookViewSet.toc → GET /api/books/ebooks/{id}/toc/
|
||||
*/
|
||||
export async function getChapters(bookId: number): Promise<ChapterSummary[]> {
|
||||
const response = await fetch(`${API_BASE}/books/ebooks/${bookId}/toc/`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch chapters: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
const data = await response.json();
|
||||
// Main backend wraps chapters under a "chapters" key
|
||||
return (data.chapters ?? data) as ChapterSummary[];
|
||||
const { data } = await api.get<{ chapters: TocChapter[] }>(
|
||||
`/books/ebooks/${bookId}/toc/`,
|
||||
);
|
||||
const chapters = data.chapters ?? [];
|
||||
return chapters.map((ch) => ({
|
||||
id: ch.id,
|
||||
book: bookId,
|
||||
title: ch.title,
|
||||
number: ch.index + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a specific chapter with full content for reading.
|
||||
* Maps to EBookViewSet.content → GET /api/books/ebooks/{id}/content/?page={number}
|
||||
*/
|
||||
export async function getChapterContent(
|
||||
bookId: number,
|
||||
chapterNumber: number
|
||||
chapterNumber: number,
|
||||
): Promise<ChapterDetail> {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/books/ebooks/${bookId}/content/?page=${chapterNumber}`
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch chapter ${chapterNumber}: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
const data = await response.json();
|
||||
// Main backend returns: { page, total_pages, content, chapter_title, format }
|
||||
const { data } = await api.get<{
|
||||
page: number;
|
||||
chapter_title: string;
|
||||
content: string;
|
||||
}>(`/books/ebooks/${bookId}/content/`, { params: { page: chapterNumber } });
|
||||
return {
|
||||
id: chapterNumber,
|
||||
book: bookId,
|
||||
title: data.chapter_title ?? "",
|
||||
number: data.page,
|
||||
number: data.page ?? chapterNumber,
|
||||
content: data.content ?? "",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
} as ChapterDetail;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch reading progress for a book (ebook).
|
||||
* Maps to EBookViewSet.progress → GET /api/books/ebooks/{id}/progress/
|
||||
*/
|
||||
export async function getReadingProgress(
|
||||
bookId: number
|
||||
bookId: number,
|
||||
): Promise<ReadingProgress> {
|
||||
const response = await fetch(`${API_BASE}/books/ebooks/${bookId}/progress/`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch reading progress: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
const data = await response.json();
|
||||
// Main backend returns: { current_position, last_page, version, updated_at }
|
||||
const { data } = await api.get<{
|
||||
current_position: number;
|
||||
last_page: number;
|
||||
epub_location?: string;
|
||||
updated_at?: string;
|
||||
}>(`/books/ebooks/${bookId}/progress/`);
|
||||
return {
|
||||
id: bookId,
|
||||
book: bookId,
|
||||
current_chapter: Math.floor((data.current_position ?? 0) / 10) + 1,
|
||||
current_chapter: data.last_page || 1,
|
||||
current_position: data.current_position ?? 0,
|
||||
percentage: data.current_position ?? 0,
|
||||
epub_location: data.epub_location ?? "",
|
||||
updated_at: data.updated_at ?? "",
|
||||
} as ReadingProgress;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update reading progress for a book (ebook).
|
||||
* Maps to EBookViewSet.progress → PATCH /api/books/ebooks/{id}/progress/
|
||||
*/
|
||||
export async function updateReadingProgress(
|
||||
bookId: number,
|
||||
progress: Partial<ReadingProgress>
|
||||
progress: Partial<ReadingProgress>,
|
||||
): Promise<ReadingProgress> {
|
||||
const response = await fetch(`${API_BASE}/books/ebooks/${bookId}/progress/`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
current_position: progress.percentage ?? progress.current_position ?? 0,
|
||||
last_page: progress.current_chapter ?? 0,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to update reading progress: ${response.status} ${response.statusText}`
|
||||
);
|
||||
const body: Record<string, string | number> = {};
|
||||
if (progress.percentage !== undefined || progress.current_position !== undefined) {
|
||||
body.current_position = progress.percentage ?? progress.current_position ?? 0;
|
||||
}
|
||||
const data = await response.json();
|
||||
if (progress.current_chapter !== undefined) {
|
||||
body.last_page = progress.current_chapter;
|
||||
}
|
||||
if (progress.epub_location !== undefined) {
|
||||
body.epub_location = progress.epub_location;
|
||||
}
|
||||
const { data } = await api.patch<{
|
||||
current_position: number;
|
||||
last_page: number;
|
||||
epub_location?: string;
|
||||
updated_at?: string;
|
||||
}>(`/books/ebooks/${bookId}/progress/`, body);
|
||||
return {
|
||||
id: bookId,
|
||||
book: bookId,
|
||||
current_chapter: Math.floor((data.current_position ?? 0) / 10) + 1,
|
||||
current_chapter: data.last_page || 1,
|
||||
current_position: data.current_position ?? 0,
|
||||
percentage: data.current_position ?? 0,
|
||||
epub_location: data.epub_location ?? "",
|
||||
updated_at: data.updated_at ?? "",
|
||||
} as ReadingProgress;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user