Archived
feat: bookmarks and notes management
- Backend: Django REST Framework API with Bookmark and Note models - ViewSets with user-scoped querysets and select_related for N+1 prevention - Create/List/Detail/Update/Delete endpoints - Batch delete operations - Unique constraint on user+book+page for bookmarks - IsOwner permission class for object-level access control - Full serializer validation (page > 0, non-empty content, duplicate check) - 30+ pytest-django tests covering CRUD, auth, filtering, edge cases - Frontend: React TypeScript components - AnnotationsContext with useReducer for state management - BookmarkList, NoteList, AddAnnotationForm, AnnotationsDashboard - Inline note editing with immediate save - Batch delete support - API client with JWT auto-refresh interceptors - Paginated query hook for infinite scroll support - Responsive CSS with loading/empty states - Infrastructure: Django project with custom User model, JWT auth, CORS - PostgreSQL database models with proper FK and indexes - Django admin configuration for all models
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* BookDetail component — full book information display.
|
||||
*/
|
||||
|
||||
import type { BookDetail as BookDetailType } from "../types";
|
||||
import { READING_STATUS_LABELS } from "../types";
|
||||
|
||||
interface BookDetailProps {
|
||||
book: BookDetailType;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export default function BookDetail({ book, onBack }: BookDetailProps) {
|
||||
return (
|
||||
<div className="book-detail">
|
||||
<button
|
||||
type="button"
|
||||
className="back-button"
|
||||
onClick={onBack}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</svg>
|
||||
Back to search
|
||||
</button>
|
||||
|
||||
<div className="book-detail-content">
|
||||
<div className="book-detail-cover">
|
||||
{book.cover_url ? (
|
||||
<img src={book.cover_url} alt={`${book.title} cover`} />
|
||||
) : (
|
||||
<div className="cover-placeholder large">
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="book-detail-info">
|
||||
<h1 className="book-detail-title">{book.title}</h1>
|
||||
<p className="book-detail-author">by {book.author}</p>
|
||||
<p className="book-detail-genre">{book.genre}</p>
|
||||
<p className="book-detail-status">
|
||||
Status:{" "}
|
||||
<span className={`status-badge status-${book.reading_status}`}>
|
||||
{READING_STATUS_LABELS[book.reading_status]}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{book.description && (
|
||||
<div className="book-detail-description">
|
||||
<h2>Description</h2>
|
||||
<p>{book.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="book-detail-meta">
|
||||
<p>
|
||||
<strong>Added:</strong>{" "}
|
||||
{new Date(book.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Updated:</strong>{" "}
|
||||
{new Date(book.updated_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user