Compare commits

..
3 changed files with 60 additions and 3 deletions
+10 -1
View File
@@ -69,7 +69,7 @@ Enable users to search books within the library and discover new books via filte
- **Search bar** at top: text input with debounced `onChange` → calls API with `q` param - **Search bar** at top: text input with debounced `onChange` → calls API with `q` param
- **Filter row**: genre dropdown, author dropdown, reading status dropdown - **Filter row**: genre dropdown, author dropdown, reading status dropdown
- Genre/Author dropdowns populated from `/api/books/genres/` and `/api/books/authors/` - Genre/Author dropdowns populated from `/api/books/genres/` and `/api/books/authors/`
- Reading status uses static enum values - Reading status uses static enum values (`READING_STATUS_OPTIONS`)
- **Results grid**: card layout showing cover, title, author, reading status badge - **Results grid**: card layout showing cover, title, author, reading status badge
- **Empty state**: "No books found" with clear message when results are empty - **Empty state**: "No books found" with clear message when results are empty
- **Loading state**: spinner/skeleton while fetching - **Loading state**: spinner/skeleton while fetching
@@ -80,6 +80,15 @@ Enable users to search books within the library and discover new books via filte
- Back button to return to library - Back button to return to library
- Clean, mobile-responsive layout - Clean, mobile-responsive layout
### API Client — `frontend/src/api/books.ts`
| Method | Endpoint | Returns |
|--------|----------|---------|
| `searchBooks(params)` | `GET /api/books/` | `{count, results: BookListItem[]}` |
| `getBook(id)` | `GET /api/books/{id}/` | `BookDetail` |
| `getGenres()` | `GET /api/books/genres/` | `string[]` |
| `getAuthors()` | `GET /api/books/authors/` | `string[]` |
## Routes (Frontend) ## Routes (Frontend)
| Path | Component | Auth | | Path | Component | Auth |
|------|-----------|------| |------|-----------|------|
+31 -1
View File
@@ -1,5 +1,15 @@
import api from "./client"; import api from "./client";
import type { ContentResponse, EBookDetail, EBookListItem, ReadingProgress, ReadingSettings, TocResponse } from "../types/book"; import type {
BookDetail,
BookListItem,
BookSearchParams,
ContentResponse,
EBookDetail,
EBookListItem,
ReadingProgress,
ReadingSettings,
TocResponse,
} from "../types/book";
export const booksApi = { export const booksApi = {
async getEBooks(): Promise<EBookListItem[]> { async getEBooks(): Promise<EBookListItem[]> {
@@ -12,6 +22,26 @@ export const booksApi = {
return data; return data;
}, },
async searchBooks(params: BookSearchParams = {}): Promise<{ count: number; results: BookListItem[] }> {
const { data } = await api.get<{ count: number; results: BookListItem[] }>("/books/", { params });
return data;
},
async getBook(id: number): Promise<BookDetail> {
const { data } = await api.get<BookDetail>(`/books/${id}/`);
return data;
},
async getGenres(): Promise<string[]> {
const { data } = await api.get<string[]>("/books/genres/");
return data;
},
async getAuthors(): Promise<string[]> {
const { data } = await api.get<string[]>("/books/authors/");
return data;
},
async uploadEBook( async uploadEBook(
file: File, file: File,
title: string, title: string,
+19 -1
View File
@@ -81,4 +81,22 @@ export interface ContentResponse {
content: string; content: string;
chapter_title: string; chapter_title: string;
format: string; format: string;
} }
export interface BookSearchParams {
q?: string;
genre?: string;
author?: string;
reading_status?: string;
ordering?: string;
page?: number;
page_size?: number;
}
export const READING_STATUS_OPTIONS: { value: string; label: string }[] = [
{ value: "", label: "All Statuses" },
{ value: "want_to_read", label: "Want to Read" },
{ value: "reading", label: "Reading" },
{ value: "finished", label: "Finished" },
{ value: "dnf", label: "Did Not Finish" },
];