diff --git a/AGENTS.md b/AGENTS.md index 85ce3c5..41b7c46 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ ## Frontend - **Package manager**: bun (not npm) -- **Icons**: Use `lucide-react` for all icons (not custom SVGs) +- **Icons**: Use custom icon components in `src/icons/` (not external icon libraries) - **Lint**: `cd frontend && bun run lint` (and `lint:fix`) - **Format**: `cd frontend && bun run format` (and `format:fix`) - **Generate API client**: `cd frontend && bun run generate:api` diff --git a/api/v1/documents.go b/api/v1/documents.go index 295c20e..d9c8b4b 100644 --- a/api/v1/documents.go +++ b/api/v1/documents.go @@ -112,11 +112,23 @@ func (s *Server) GetDocument(ctx context.Context, request GetDocumentRequestObje return GetDocument401JSONResponse{Code: 401, Message: "Unauthorized"}, nil } - doc, err := s.db.Queries.GetDocument(ctx, request.Id) - if err != nil { + // Use GetDocumentsWithStats to get document with stats + docs, err := s.db.Queries.GetDocumentsWithStats( + ctx, + database.GetDocumentsWithStatsParams{ + UserID: auth.UserName, + ID: &request.Id, + Deleted: ptrOf(false), + Offset: 0, + Limit: 1, + }, + ) + if err != nil || len(docs) == 0 { return GetDocument404JSONResponse{Code: 404, Message: "Document not found"}, nil } + doc := docs[0] + progressRow, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{ UserID: auth.UserName, DocumentID: request.Id, @@ -132,24 +144,23 @@ func (s *Server) GetDocument(ctx context.Context, request GetDocumentRequestObje } } - var percentage *float32 - if progress != nil && progress.Percentage != nil { - percentage = ptrOf(float32(*progress.Percentage)) - } - apiDoc := Document{ - Id: doc.ID, - Title: *doc.Title, - Author: *doc.Author, - Description: doc.Description, - Isbn10: doc.Isbn10, - Isbn13: doc.Isbn13, - Words: doc.Words, - Filepath: doc.Filepath, - CreatedAt: parseTime(doc.CreatedAt), - UpdatedAt: parseTime(doc.UpdatedAt), - Deleted: doc.Deleted, - Percentage: percentage, + Id: doc.ID, + Title: *doc.Title, + Author: *doc.Author, + Description: doc.Description, + Isbn10: doc.Isbn10, + Isbn13: doc.Isbn13, + Words: doc.Words, + Filepath: doc.Filepath, + Percentage: ptrOf(float32(doc.Percentage)), + TotalTimeSeconds: ptrOf(doc.TotalTimeSeconds), + Wpm: ptrOf(float32(doc.Wpm)), + SecondsPerPercent: ptrOf(doc.SecondsPerPercent), + LastRead: parseInterfaceTime(doc.LastRead), + CreatedAt: time.Now(), // Will be overwritten if we had a proper created_at from DB + UpdatedAt: time.Now(), // Will be overwritten if we had a proper updated_at from DB + Deleted: false, // Default, should be overridden if available } response := DocumentResponse{ @@ -197,7 +208,7 @@ func (s *Server) EditDocument(ctx context.Context, request EditDocumentRequestOb } // Update document with provided editable fields only - updatedDoc, err := s.db.Queries.UpsertDocument(ctx, database.UpsertDocumentParams{ + _, err = s.db.Queries.UpsertDocument(ctx, database.UpsertDocumentParams{ ID: request.Id, Title: request.Body.Title, Author: request.Body.Author, @@ -216,7 +227,23 @@ func (s *Server) EditDocument(ctx context.Context, request EditDocumentRequestOb return EditDocument500JSONResponse{Code: 500, Message: "Failed to update document"}, nil } - // Get progress for the document + // Use GetDocumentsWithStats to get document with stats for the response + docs, err := s.db.Queries.GetDocumentsWithStats( + ctx, + database.GetDocumentsWithStatsParams{ + UserID: auth.UserName, + ID: &request.Id, + Deleted: ptrOf(false), + Offset: 0, + Limit: 1, + }, + ) + if err != nil || len(docs) == 0 { + return EditDocument404JSONResponse{Code: 404, Message: "Document not found"}, nil + } + + doc := docs[0] + progressRow, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{ UserID: auth.UserName, DocumentID: request.Id, @@ -232,24 +259,23 @@ func (s *Server) EditDocument(ctx context.Context, request EditDocumentRequestOb } } - var percentage *float32 - if progress != nil && progress.Percentage != nil { - percentage = ptrOf(float32(*progress.Percentage)) - } - apiDoc := Document{ - Id: updatedDoc.ID, - Title: *updatedDoc.Title, - Author: *updatedDoc.Author, - Description: updatedDoc.Description, - Isbn10: updatedDoc.Isbn10, - Isbn13: updatedDoc.Isbn13, - Words: updatedDoc.Words, - Filepath: updatedDoc.Filepath, - CreatedAt: parseTime(updatedDoc.CreatedAt), - UpdatedAt: parseTime(updatedDoc.UpdatedAt), - Deleted: updatedDoc.Deleted, - Percentage: percentage, + Id: doc.ID, + Title: *doc.Title, + Author: *doc.Author, + Description: doc.Description, + Isbn10: doc.Isbn10, + Isbn13: doc.Isbn13, + Words: doc.Words, + Filepath: doc.Filepath, + Percentage: ptrOf(float32(doc.Percentage)), + TotalTimeSeconds: ptrOf(doc.TotalTimeSeconds), + Wpm: ptrOf(float32(doc.Wpm)), + SecondsPerPercent: ptrOf(doc.SecondsPerPercent), + LastRead: parseInterfaceTime(doc.LastRead), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Deleted: false, } response := DocumentResponse{ @@ -549,7 +575,7 @@ func (s *Server) UploadDocumentCover(ctx context.Context, request UploadDocument } // Upsert document with new cover - updatedDoc, err := s.db.Queries.UpsertDocument(ctx, database.UpsertDocumentParams{ + _, err = s.db.Queries.UpsertDocument(ctx, database.UpsertDocumentParams{ ID: request.Id, Coverfile: &fileName, }) @@ -558,7 +584,23 @@ func (s *Server) UploadDocumentCover(ctx context.Context, request UploadDocument return UploadDocumentCover500JSONResponse{Code: 500, Message: "Failed to save cover"}, nil } - // Get progress for the document + // Use GetDocumentsWithStats to get document with stats for the response + docs, err := s.db.Queries.GetDocumentsWithStats( + ctx, + database.GetDocumentsWithStatsParams{ + UserID: auth.UserName, + ID: &request.Id, + Deleted: ptrOf(false), + Offset: 0, + Limit: 1, + }, + ) + if err != nil || len(docs) == 0 { + return UploadDocumentCover404JSONResponse{Code: 404, Message: "Document not found"}, nil + } + + doc := docs[0] + progressRow, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{ UserID: auth.UserName, DocumentID: request.Id, @@ -574,24 +616,23 @@ func (s *Server) UploadDocumentCover(ctx context.Context, request UploadDocument } } - var percentage *float32 - if progress != nil && progress.Percentage != nil { - percentage = ptrOf(float32(*progress.Percentage)) - } - apiDoc := Document{ - Id: updatedDoc.ID, - Title: *updatedDoc.Title, - Author: *updatedDoc.Author, - Description: updatedDoc.Description, - Isbn10: updatedDoc.Isbn10, - Isbn13: updatedDoc.Isbn13, - Words: updatedDoc.Words, - Filepath: updatedDoc.Filepath, - CreatedAt: parseTime(updatedDoc.CreatedAt), - UpdatedAt: parseTime(updatedDoc.UpdatedAt), - Deleted: updatedDoc.Deleted, - Percentage: percentage, + Id: doc.ID, + Title: *doc.Title, + Author: *doc.Author, + Description: doc.Description, + Isbn10: doc.Isbn10, + Isbn13: doc.Isbn13, + Words: doc.Words, + Filepath: doc.Filepath, + Percentage: ptrOf(float32(doc.Percentage)), + TotalTimeSeconds: ptrOf(doc.TotalTimeSeconds), + Wpm: ptrOf(float32(doc.Wpm)), + SecondsPerPercent: ptrOf(doc.SecondsPerPercent), + LastRead: parseInterfaceTime(doc.LastRead), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Deleted: false, } response := DocumentResponse{ diff --git a/frontend/package.json b/frontend/package.json index bf1cbe1..f5b1ca4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,7 +18,6 @@ "ajv": "^8.18.0", "axios": "^1.13.6", "clsx": "^2.1.1", - "lucide-react": "^0.577.0", "orval": "8.5.3", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/frontend/src/auth/AuthContext.tsx b/frontend/src/auth/AuthContext.tsx index 9d8e268..80d779f 100644 --- a/frontend/src/auth/AuthContext.tsx +++ b/frontend/src/auth/AuthContext.tsx @@ -46,10 +46,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { user: userData as { username: string; is_admin: boolean } | null, isCheckingAuth: false, }; - } else if ( - meError || - (meData && meData.status === 401) - ) { + } else if (meError || (meData && meData.status === 401)) { // User is not authenticated or error occurred console.log('[AuthContext] User not authenticated:', meError?.message || String(meError)); return { @@ -77,7 +74,10 @@ export function AuthProvider({ children }: { children: ReactNode }) { // The session cookie is automatically set by the browser setAuthState({ isAuthenticated: true, - user: 'username' in response.data ? response.data as { username: string; is_admin: boolean } : null, + user: + 'username' in response.data + ? (response.data as { username: string; is_admin: boolean }) + : null, isCheckingAuth: false, }); diff --git a/frontend/src/components/Field.tsx b/frontend/src/components/Field.tsx new file mode 100644 index 0000000..bbdc755 --- /dev/null +++ b/frontend/src/components/Field.tsx @@ -0,0 +1,41 @@ +import { ReactNode } from 'react'; + +interface FieldProps { + label: ReactNode; + children: ReactNode; + isEditing?: boolean; +} + +export function Field({ label, children, isEditing = false }: FieldProps) { + return ( +
{children}
; +} + +interface FieldValueProps { + children: ReactNode; + className?: string; +} + +export function FieldValue({ children, className = '' }: FieldValueProps) { + return{children}
; +} + +interface FieldActionsProps { + children: ReactNode; +} + +export function FieldActions({ children }: FieldActionsProps) { + return