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,87 @@
|
||||
import React, { useState } from "react";
|
||||
import { useAnnotations } from "@/context/AnnotationsContext";
|
||||
import type { Bookmark } from "@/types";
|
||||
|
||||
interface BookmarkListProps {
|
||||
bookId?: string;
|
||||
onNavigateToPage?: (bookId: string, page: number) => void;
|
||||
}
|
||||
|
||||
export function BookmarkList({
|
||||
bookId,
|
||||
onNavigateToPage,
|
||||
}: BookmarkListProps): React.ReactElement {
|
||||
const { state, loadBookmarks, removeBookmark } = useAnnotations();
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
loadBookmarks(bookId);
|
||||
}, [loadBookmarks, bookId]);
|
||||
|
||||
const handleDelete = async (id: string): Promise<void> => {
|
||||
setDeletingId(id);
|
||||
try {
|
||||
await removeBookmark(id);
|
||||
} catch {
|
||||
// error handled by context
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (state.bookmarksLoading) {
|
||||
return <div className="annotations-loading">Loading bookmarks...</div>;
|
||||
}
|
||||
|
||||
if (state.bookmarks.length === 0) {
|
||||
return (
|
||||
<div className="annotations-empty">
|
||||
No bookmarks yet. Select a passage and add a bookmark while reading.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="annotations-list">
|
||||
{state.bookmarks.map((bookmark: Bookmark) => (
|
||||
<div key={bookmark.id} className="annotation-card">
|
||||
<div className="annotation-card-header">
|
||||
<span className="annotation-kind-badge bookmark-badge">
|
||||
Bookmark
|
||||
</span>
|
||||
<span className="annotation-page">
|
||||
Page {bookmark.page}
|
||||
</span>
|
||||
<span className="annotation-date">
|
||||
{new Date(bookmark.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
{bookmark.location_text && (
|
||||
<blockquote className="annotation-quote">
|
||||
“{bookmark.location_text}”
|
||||
</blockquote>
|
||||
)}
|
||||
<div className="annotation-actions">
|
||||
{onNavigateToPage && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() =>
|
||||
onNavigateToPage(bookmark.book, bookmark.page)
|
||||
}
|
||||
>
|
||||
Go to page
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => handleDelete(bookmark.id)}
|
||||
disabled={deletingId === bookmark.id}
|
||||
>
|
||||
{deletingId === bookmark.id ? "Deleting..." : "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user