This repository has been archived on 2026-07-21. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cloud-reader/web/src/components/BookDetail.tsx
T
Marko (Hermes Implementer) 3b5b301e42 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
2026-05-26 00:50:06 +00:00

91 lines
2.6 KiB
TypeScript

/**
* 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>
);
}