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:
2026-06-03 22:06:01 -05:00
parent 730c748f5f
commit 6b4c0c43f8
137 changed files with 20319 additions and 2340 deletions
+61 -94
View File
@@ -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;
}
};
}