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 ( +
+
{label}
+ {children} +
+ ); +} + +interface FieldLabelProps { + children: ReactNode; +} + +export function FieldLabel({ children }: FieldLabelProps) { + 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
{children}
; +} diff --git a/frontend/src/components/HamburgerMenu.tsx b/frontend/src/components/HamburgerMenu.tsx index 94e4657..d625708 100644 --- a/frontend/src/components/HamburgerMenu.tsx +++ b/frontend/src/components/HamburgerMenu.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { HomeIcon, DocumentsIcon, ActivityIcon, SearchIcon, SettingsIcon } from '../icons'; +import { HomeIcon, DocumentsIcon, ActivityIcon, SearchIcon, SettingsIcon, GitIcon } from '../icons'; import { useAuth } from '../auth/AuthContext'; import { useGetInfo } from '../generated/anthoLumeAPIV1'; @@ -184,47 +184,7 @@ export default function HamburgerMenu() { href="https://gitea.va.reichard.io/evan/AnthoLume" rel="noreferrer" > - - - - - - - - - - - - - - - - - - - - - - - + {version} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 4157521..a1299d2 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -2,8 +2,7 @@ import { useState, useEffect, useRef } from 'react'; import { Link, useLocation, Outlet, Navigate } from 'react-router-dom'; import { useGetMe } from '../generated/anthoLumeAPIV1'; import { useAuth } from '../auth/AuthContext'; -import { UserIcon } from '../icons'; -import { ChevronDown } from 'lucide-react'; +import { UserIcon, DropdownIcon } from '../icons'; import HamburgerMenu from './HamburgerMenu'; export default function Layout() { @@ -119,7 +118,7 @@ export default function Layout() { className="text-gray-800 transition-transform duration-200 dark:text-gray-200" style={{ transform: isUserDropdownOpen ? 'rotate(180deg)' : 'rotate(0deg)' }} > - + diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx index 6f193c3..0e94fca 100644 --- a/frontend/src/components/Toast.tsx +++ b/frontend/src/components/Toast.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Info, AlertTriangle, XCircle, X } from 'lucide-react'; +import { InfoIcon, WarningIcon, ErrorIcon, CloseIcon } from '../icons'; export type ToastType = 'info' | 'warning' | 'error'; @@ -62,9 +62,9 @@ export function Toast({ id, type, message, duration = 5000, onClose }: ToastProp } const icons = { - info: , - warning: , - error: , + info: , + warning: , + error: , }; return ( @@ -80,7 +80,7 @@ export function Toast({ id, type, message, duration = 5000, onClose }: ToastProp className={`ml-2 opacity-70 transition-opacity hover:opacity-100 ${textStyles[type]}`} aria-label="Close" > - + ); diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index fba8417..b3f8b00 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -17,3 +17,6 @@ export { PageLoader, InlineLoader, } from './Skeleton'; + +// Field components +export { Field, FieldLabel, FieldValue, FieldActions } from './Field'; diff --git a/frontend/src/hooks/useDebounce.ts b/frontend/src/hooks/useDebounce.ts new file mode 100644 index 0000000..98d0abd --- /dev/null +++ b/frontend/src/hooks/useDebounce.ts @@ -0,0 +1,23 @@ +import { useState, useEffect } from 'react'; + +/** + * Debounces a value by delaying updates until after a specified delay + * @param value The value to debounce + * @param delay The delay in milliseconds + * @returns The debounced value + */ +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/frontend/src/icons/BookIcon.tsx b/frontend/src/icons/BookIcon.tsx new file mode 100644 index 0000000..415c56e --- /dev/null +++ b/frontend/src/icons/BookIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface BookIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function BookIcon({ size = 24, className = '', disabled = false }: BookIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/CheckIcon.tsx b/frontend/src/icons/CheckIcon.tsx new file mode 100644 index 0000000..6449135 --- /dev/null +++ b/frontend/src/icons/CheckIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface CheckIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function CheckIcon({ size = 24, className = '', disabled = false }: CheckIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/CloseIcon.tsx b/frontend/src/icons/CloseIcon.tsx new file mode 100644 index 0000000..587fa7e --- /dev/null +++ b/frontend/src/icons/CloseIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface CloseIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function CloseIcon({ size = 24, className = '', disabled = false }: CloseIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/ErrorIcon.tsx b/frontend/src/icons/ErrorIcon.tsx new file mode 100644 index 0000000..48ee784 --- /dev/null +++ b/frontend/src/icons/ErrorIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface ErrorIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function ErrorIcon({ size = 24, className = '', disabled = false }: ErrorIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/FolderOpenIcon.tsx b/frontend/src/icons/FolderOpenIcon.tsx new file mode 100644 index 0000000..f5237f7 --- /dev/null +++ b/frontend/src/icons/FolderOpenIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface FolderOpenIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function FolderOpenIcon({ size = 24, className = '', disabled = false }: FolderOpenIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/GitIcon.tsx b/frontend/src/icons/GitIcon.tsx new file mode 100644 index 0000000..a5f6cbb --- /dev/null +++ b/frontend/src/icons/GitIcon.tsx @@ -0,0 +1,45 @@ +export function GitIcon() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/icons/WarningIcon.tsx b/frontend/src/icons/WarningIcon.tsx new file mode 100644 index 0000000..bfb4663 --- /dev/null +++ b/frontend/src/icons/WarningIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface WarningIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function WarningIcon({ size = 24, className = '', disabled = false }: WarningIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/index.ts b/frontend/src/icons/index.ts index a559e95..af340ab 100644 --- a/frontend/src/icons/index.ts +++ b/frontend/src/icons/index.ts @@ -17,3 +17,10 @@ export { DropdownIcon } from './DropdownIcon'; export { ClockIcon } from './ClockIcon'; export { PasswordIcon } from './PasswordIcon'; export { LoadingIcon } from './LoadingIcon'; +export { GitIcon } from './GitIcon'; +export { WarningIcon } from './WarningIcon'; +export { ErrorIcon } from './ErrorIcon'; +export { CloseIcon } from './CloseIcon'; +export { CheckIcon } from './CheckIcon'; +export { FolderOpenIcon } from './FolderOpenIcon'; +export { BookIcon } from './BookIcon'; diff --git a/frontend/src/pages/AdminImportPage.tsx b/frontend/src/pages/AdminImportPage.tsx index 23319d4..9a7b60e 100644 --- a/frontend/src/pages/AdminImportPage.tsx +++ b/frontend/src/pages/AdminImportPage.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useGetImportDirectory, usePostImport } from '../generated/anthoLumeAPIV1'; import { Button } from '../components/Button'; -import { FolderOpen } from 'lucide-react'; +import { FolderOpenIcon } from '../icons'; import { useToasts } from '../components/ToastContext'; export default function AdminImportPage() { @@ -73,7 +73,7 @@ export default function AdminImportPage() {
- +

{selectedDirectory}

@@ -151,7 +151,7 @@ export default function AdminImportPage() { diff --git a/frontend/src/pages/DocumentPage.tsx b/frontend/src/pages/DocumentPage.tsx index 00c4dde..d02a43a 100644 --- a/frontend/src/pages/DocumentPage.tsx +++ b/frontend/src/pages/DocumentPage.tsx @@ -1,43 +1,30 @@ import { useParams } from 'react-router-dom'; -import { useGetDocument, useGetProgress, useEditDocument } from '../generated/anthoLumeAPIV1'; +import { + useGetDocument, + useEditDocument, + getGetDocumentQueryKey, +} from '../generated/anthoLumeAPIV1'; +import { Document } from '../generated/model/document'; +import { Progress } from '../generated/model/progress'; +import { useQueryClient } from '@tanstack/react-query'; import { formatDuration } from '../utils/formatters'; -import { DeleteIcon, ActivityIcon, SearchIcon, DownloadIcon, EditIcon, InfoIcon } from '../icons'; -import { X, Check } from 'lucide-react'; +import { + DeleteIcon, + ActivityIcon, + SearchIcon, + DownloadIcon, + EditIcon, + InfoIcon, + CloseIcon, + CheckIcon, +} from '../icons'; import { useState } from 'react'; - -interface Document { - id: string; - title: string; - author: string; - description?: string; - isbn10?: string; - isbn13?: string; - words?: number; - filepath?: string; - created_at: string; - updated_at: string; - deleted: boolean; - percentage?: number; - total_time_seconds?: number; - wpm?: number; - seconds_per_percent?: number; - last_read?: string; -} - -interface Progress { - document_id?: string; - percentage?: number; - created_at?: string; - user_id?: string; - device_name?: string; - title?: string; - author?: string; -} +import { Field, FieldLabel, FieldValue, FieldActions } from '../components'; export default function DocumentPage() { const { id } = useParams<{ id: string }>(); + const queryClient = useQueryClient(); const { data: docData, isLoading: docLoading } = useGetDocument(id || ''); - const { data: progressData, isLoading: progressLoading } = useGetProgress(id || ''); const editMutation = useEditDocument(); const [showEditCover, setShowEditCover] = useState(false); @@ -53,7 +40,7 @@ export default function DocumentPage() { const [editAuthor, setEditAuthor] = useState(''); const [editDescription, setEditDescription] = useState(''); - if (docLoading || progressLoading) { + if (docLoading) { return
Loading...
; } @@ -64,14 +51,14 @@ export default function DocumentPage() { const document = docData.data.document as Document; const progress = - progressData?.status === 200 ? (progressData.data.progress as Progress | undefined) : undefined; + docData?.status === 200 ? (docData.data.progress as Progress | undefined) : undefined; if (!document) { return
Document not found
; } - // Calculate total time left (mirroring legacy template logic) - const percentage = progress?.percentage || document.percentage || 0; + const percentage = + document.percentage ?? (progress?.percentage ? progress.percentage * 100 : 0) ?? 0; const secondsPerPercent = document.seconds_per_percent || 0; const totalTimeLeftSeconds = Math.round((100 - percentage) * secondsPerPercent); @@ -90,7 +77,11 @@ export default function DocumentPage() { data: { title: editTitle }, }, { - onSuccess: () => setIsEditingTitle(false), + onSuccess: response => { + setIsEditingTitle(false); + // Update cache with the response data (no refetch needed) + queryClient.setQueryData(getGetDocumentQueryKey(document.id), response); + }, onError: () => setIsEditingTitle(false), } ); @@ -103,7 +94,11 @@ export default function DocumentPage() { data: { author: editAuthor }, }, { - onSuccess: () => setIsEditingAuthor(false), + onSuccess: response => { + setIsEditingAuthor(false); + // Update cache with the response data (no refetch needed) + queryClient.setQueryData(getGetDocumentQueryKey(document.id), response); + }, onError: () => setIsEditingAuthor(false), } ); @@ -116,21 +111,25 @@ export default function DocumentPage() { data: { description: editDescription }, }, { - onSuccess: () => setIsEditingDescription(false), + onSuccess: response => { + setIsEditingDescription(false); + // Update cache with the response data (no refetch needed) + queryClient.setQueryData(getGetDocumentQueryKey(document.id), response); + }, onError: () => setIsEditingDescription(false), } ); }; return ( -
-
+
+
{/* Document Info - Left Column */}
{/* Cover Image with Edit Label */}
{/* Icons Container */} -
+
{/* Edit Cover Dropdown */}
setShowEditCover(e.target.checked)} />
- + -
+ @@ -219,14 +218,14 @@ export default function DocumentPage() {
- + @@ -254,18 +253,18 @@ export default function DocumentPage() {
- + @@ -314,221 +313,230 @@ export default function DocumentPage() { {/* Document Details Grid */}
{/* Title - Editable */} -
+ Title + + {isEditingTitle ? ( + <> + + + + ) : ( + + )} + + + } > -
-

Title

- {isEditingTitle ? ( -
- - -
- ) : ( - - )} -
{isEditingTitle ? ( -
+
setEditTitle(e.target.value)} - className="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white rounded font-medium text-lg flex-grow" + className="grow rounded border border-blue-200 bg-blue-50 p-2 text-lg font-medium text-black focus:outline-none focus:ring-2 focus:ring-blue-400 dark:border-blue-700 dark:bg-blue-900/20 dark:text-white dark:focus:ring-blue-500" />
) : ( -

{document.title}

+ {document.title} )} -
+ {/* Author - Editable */} -
+ Author + + {isEditingAuthor ? ( + <> + + + + ) : ( + + )} + + + } > -
-

Author

- {isEditingAuthor ? ( -
- - -
- ) : ( - - )} -
{isEditingAuthor ? ( -
+
setEditAuthor(e.target.value)} - className="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white rounded font-medium text-lg flex-grow" + className="grow rounded border border-blue-200 bg-blue-50 p-2 text-lg font-medium text-black focus:outline-none focus:ring-2 focus:ring-blue-400 dark:border-blue-700 dark:bg-blue-900/20 dark:text-white dark:focus:ring-blue-500" />
) : ( -

{document.author}

+ {document.author} )} -
+ {/* Time Read with Info Dropdown */} -
-
-

Time Read

- -
-
-

Seconds / Percent

-

- {secondsPerPercent !== 0 ? secondsPerPercent : 'N/A'} -

+ + Time Read + +
+
+

Seconds / Percent

+

+ {secondsPerPercent !== 0 ? secondsPerPercent : 'N/A'} +

+
+
+

Words / Minute

+

+ {document.wpm && document.wpm > 0 ? document.wpm : 'N/A'} +

+
+
+

Est. Time Left

+

+ {totalTimeLeftSeconds > 0 ? formatDuration(totalTimeLeftSeconds) : 'N/A'} +

+
-
-

Words / Minute

-

- {document.wpm && document.wpm > 0 ? document.wpm : 'N/A'} -

-
-
-

Est. Time Left

-

- {totalTimeLeftSeconds > 0 ? formatDuration(totalTimeLeftSeconds) : 'N/A'} -

-
-
-
-

+ + } + > + {document.total_time_seconds && document.total_time_seconds > 0 ? formatDuration(document.total_time_seconds) : 'N/A'} -

-
+ + {/* Progress */} -
-

Progress

-

- {percentage ? `${Math.round(percentage)}%` : '0%'} -

-
+ Progress}> + {`${percentage.toFixed(2)}%`} +
{/* Description - Editable */} -
+ Description + + {isEditingDescription ? ( + <> + + + + ) : ( + + )} + + + } > -
-

Description

- {isEditingDescription ? ( -
- - -
- ) : ( - - )} -
{isEditingDescription ? ( -
+