feat(ui): Add pagination to books pages and improve spacing

- Added CursorPagination component with page size selector (20/50/100)
- Updated /books page with pagination support
- Updated /libraries/[id]/books with pagination
- Improved layout margins (added pb-16 and responsive px)
- Series page uses improved layout spacing
This commit is contained in:
2026-03-06 14:50:27 +01:00
parent c421f427b0
commit fa574586ed
9 changed files with 368 additions and 24 deletions

View File

@@ -2,6 +2,7 @@
import { useState, useEffect } from "react";
import { JobRow } from "./JobRow";
import { MiniProgressBar } from "./ui";
interface Job {
id: string;
@@ -9,7 +10,18 @@ interface Job {
type: string;
status: string;
created_at: string;
started_at: string | null;
finished_at: string | null;
error_opt: string | null;
stats_json: {
scanned_files: number;
indexed_files: number;
removed_files: number;
errors: number;
} | null;
progress_percent: number | null;
processed_files: number | null;
total_files: number | null;
}
interface JobsListProps {
@@ -18,6 +30,36 @@ interface JobsListProps {
highlightJobId?: string;
}
function formatDuration(start: string, end: string | null): string {
const startDate = new Date(start);
const endDate = end ? new Date(end) : new Date();
const diff = endDate.getTime() - startDate.getTime();
if (diff < 60000) return `${Math.floor(diff / 1000)}s`;
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ${Math.floor((diff % 60000) / 1000)}s`;
return `${Math.floor(diff / 3600000)}h ${Math.floor((diff % 3600000) / 60000)}m`;
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diff = now.getTime() - date.getTime();
// Less than 1 hour: show relative
if (diff < 3600000) {
const mins = Math.floor(diff / 60000);
if (mins < 1) return "Just now";
return `${mins}m ago`;
}
// Less than 24 hours: show hours
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours}h ago`;
}
// Otherwise: show date
return date.toLocaleDateString();
}
export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListProps) {
const [jobs, setJobs] = useState(initialJobs);
@@ -53,7 +95,6 @@ export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListPro
});
if (response.ok) {
// Update local state to reflect cancellation
setJobs(jobs.map(job =>
job.id === id ? { ...job, status: "cancelled" } : job
));
@@ -73,6 +114,8 @@ export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListPro
<th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">Library</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">Status</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">Files</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">Duration</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">Created</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase tracking-wider">Actions</th>
</tr>
@@ -85,6 +128,8 @@ export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListPro
libraryName={job.library_id ? libraries.get(job.library_id) : undefined}
highlighted={job.id === highlightJobId}
onCancel={handleCancel}
formatDate={formatDate}
formatDuration={formatDuration}
/>
))}
</tbody>