From d001e29bbc653f905b7095011ec3aed15b2d073c Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Fri, 6 Mar 2026 14:11:23 +0100 Subject: [PATCH] feat(ui): Components refactoring with Tailwind - UI kit, icons, lazy loading images - Created reusable UI components (Card, Button, Badge, Form, Icon) - Added PageIcon and NavIcon components with consistent styling - Refactored all pages to use new UI components - Added non-blocking image loading with skeleton for book covers - Created LibraryActions dropdown for library settings - Added emojis to buttons for better UX - Fixed Client Component issues with getBookCoverUrl --- apps/backoffice/app/books/[id]/page.tsx | 195 ++++++++++-------- apps/backoffice/app/books/page.tsx | 76 ++++--- apps/backoffice/app/components/BookCard.tsx | 112 +++++++--- .../backoffice/app/components/JobProgress.tsx | 48 ++--- apps/backoffice/app/components/JobRow.tsx | 72 ++++--- .../app/components/JobsIndicator.tsx | 105 +++++----- apps/backoffice/app/components/JobsList.tsx | 50 ++--- .../app/components/LibraryActions.tsx | 119 +++++++++++ .../app/components/MonitoringForm.tsx | 26 ++- apps/backoffice/app/components/ui/Badge.tsx | 61 ++++++ apps/backoffice/app/components/ui/Button.tsx | 48 +++++ apps/backoffice/app/components/ui/Card.tsx | 27 +++ apps/backoffice/app/components/ui/Form.tsx | 57 +++++ apps/backoffice/app/components/ui/Icon.tsx | 94 +++++++++ apps/backoffice/app/components/ui/Input.tsx | 30 +++ .../app/components/ui/ProgressBar.tsx | 56 +++++ apps/backoffice/app/components/ui/StatBox.tsx | 33 +++ apps/backoffice/app/components/ui/index.ts | 8 + apps/backoffice/app/globals.css | 18 ++ apps/backoffice/app/jobs/[id]/page.tsx | 184 +++++++---------- apps/backoffice/app/jobs/page.tsx | 62 +++--- apps/backoffice/app/layout.tsx | 23 ++- apps/backoffice/app/page.tsx | 80 ++++++- apps/backoffice/app/tokens/page.tsx | 110 ++++++---- 24 files changed, 1235 insertions(+), 459 deletions(-) create mode 100644 apps/backoffice/app/components/LibraryActions.tsx create mode 100644 apps/backoffice/app/components/ui/Badge.tsx create mode 100644 apps/backoffice/app/components/ui/Button.tsx create mode 100644 apps/backoffice/app/components/ui/Card.tsx create mode 100644 apps/backoffice/app/components/ui/Form.tsx create mode 100644 apps/backoffice/app/components/ui/Icon.tsx create mode 100644 apps/backoffice/app/components/ui/Input.tsx create mode 100644 apps/backoffice/app/components/ui/ProgressBar.tsx create mode 100644 apps/backoffice/app/components/ui/StatBox.tsx create mode 100644 apps/backoffice/app/components/ui/index.ts diff --git a/apps/backoffice/app/books/[id]/page.tsx b/apps/backoffice/app/books/[id]/page.tsx index 0233ec2..c04afe5 100644 --- a/apps/backoffice/app/books/[id]/page.tsx +++ b/apps/backoffice/app/books/[id]/page.tsx @@ -32,113 +32,128 @@ export default async function BookDetailPage({ return ( <> -
- ← Back to books +
+ + ← Back to books +
-
-
- {`Cover +
+
+
+ {`Cover +
-
-

{book.title}

- - {book.author && ( -

by {book.author}

- )} - - {book.series && ( -

- {book.series} - {book.volume && Volume {book.volume}} -

- )} - -
-
- Format: - {book.kind.toUpperCase()} -
+
+
+

{book.title}

- {book.volume && ( -
- Volume: - {book.volume} -
+ {book.author && ( +

by {book.author}

)} - - {book.language && ( -
- Language: - {book.language.toUpperCase()} -
- )} - - {book.page_count && ( -
- Pages: - {book.page_count} -
- )} - -
- Library: - {library?.name || book.library_id} -
{book.series && ( -
- Series: - {book.series} -
+

+ {book.series} + {book.volume && Volume {book.volume}} +

)} - {book.file_format && ( -
- File Format: - {book.file_format.toUpperCase()} +
+
+ Format: + + {book.kind.toUpperCase()} +
- )} + + {book.volume && ( +
+ Volume: + {book.volume} +
+ )} + + {book.language && ( +
+ Language: + {book.language.toUpperCase()} +
+ )} + + {book.page_count && ( +
+ Pages: + {book.page_count} +
+ )} - {book.file_parse_status && ( -
- Parse Status: - {book.file_parse_status} +
+ Library: + {library?.name || book.library_id}
- )} - {book.file_path && ( -
- File Path: - {book.file_path} + {book.series && ( +
+ Series: + {book.series} +
+ )} + + {book.file_format && ( +
+ File Format: + {book.file_format.toUpperCase()} +
+ )} + + {book.file_parse_status && ( +
+ Parse Status: + + {book.file_parse_status} + +
+ )} + + {book.file_path && ( +
+ File Path: + {book.file_path} +
+ )} + +
+ Book ID: + {book.id}
- )} -
- Book ID: - {book.id} +
+ Library ID: + {book.library_id} +
+ + {book.updated_at && ( +
+ Updated: + {new Date(book.updated_at).toLocaleString()} +
+ )}
- -
- Library ID: - {book.library_id} -
- - {book.updated_at && ( -
- Updated: - {new Date(book.updated_at).toLocaleString()} -
- )}
diff --git a/apps/backoffice/app/books/page.tsx b/apps/backoffice/app/books/page.tsx index fdb510c..1bcaa99 100644 --- a/apps/backoffice/app/books/page.tsx +++ b/apps/backoffice/app/books/page.tsx @@ -1,5 +1,6 @@ import { fetchBooks, searchBooks, fetchLibraries, BookDto, LibraryDto, getBookCoverUrl } from "../../lib/api"; import { BooksGrid, EmptyState } from "../components/BookCard"; +import { Card, Button, FormField, FormInput, FormSelect, FormRow } from "../components/ui"; import Link from "next/link"; export const dynamic = "force-dynamic"; @@ -50,39 +51,55 @@ export default async function BooksPage({ nextCursor = booksPage.next_cursor; } - const displayBooks = searchResults || books; + const displayBooks = (searchResults || books).map(book => ({ + ...book, + coverUrl: getBookCoverUrl(book.id) + })); return ( <> -

Books

+

+ + Books +

{/* Filtres et recherche */} -
-
- - - - {searchQuery && ( - Clear - )} + + + + + + + + + + {libraries.map((lib) => ( + + ))} + + + + {searchQuery && ( + + βœ• Clear + + )} + -
+ {/* RΓ©sultats de recherche */} {searchQuery && totalHits !== null && ( -

+

Found {totalHits} result{totalHits !== 1 ? 's' : ''} for "{searchQuery}"

)} @@ -90,15 +107,20 @@ export default async function BooksPage({ {/* Grille de livres */} {displayBooks.length > 0 ? ( <> - + {/* Pagination */} {!searchQuery && nextCursor && ( -
+
- +
)} diff --git a/apps/backoffice/app/components/BookCard.tsx b/apps/backoffice/app/components/BookCard.tsx index 60f1aa9..6b9f467 100644 --- a/apps/backoffice/app/components/BookCard.tsx +++ b/apps/backoffice/app/components/BookCard.tsx @@ -1,42 +1,92 @@ +"use client"; + +import { useState } from "react"; import Image from "next/image"; import Link from "next/link"; import { BookDto } from "../../lib/api"; interface BookCardProps { - book: BookDto; - getBookCoverUrl: (bookId: string) => string; + book: BookDto & { coverUrl?: string }; } -export function BookCard({ book, getBookCoverUrl }: BookCardProps) { +function BookImage({ src, alt }: { src: string; alt: string }) { + const [isLoaded, setIsLoaded] = useState(false); + return ( - -
- {`Cover +
+ {/* Skeleton */} +
+
-
-

+ + {/* Image */} + {alt} setIsLoaded(true)} + unoptimized + /> +

+ ); +} + +export function BookCard({ book }: BookCardProps) { + const coverUrl = book.coverUrl || `/api/books/${book.id}/pages/1?format=webp&width=200`; + + return ( + + + + {/* Book Info */} +
+

{book.title}

+ {book.author && ( -

{book.author}

+

{book.author}

)} + {book.series && ( -

+

{book.series} - {book.volume && ` #${book.volume}`} + {book.volume && #{book.volume}}

)} -
- {book.kind.toUpperCase()} - {book.language && {book.language.toUpperCase()}} + + {/* Meta Tags */} +
+ + {book.kind} + + {book.language && ( + + {book.language} + + )}
@@ -44,15 +94,14 @@ export function BookCard({ book, getBookCoverUrl }: BookCardProps) { } interface BooksGridProps { - books: BookDto[]; - getBookCoverUrl: (bookId: string) => string; + books: (BookDto & { coverUrl?: string })[]; } -export function BooksGrid({ books, getBookCoverUrl }: BooksGridProps) { +export function BooksGrid({ books }: BooksGridProps) { return ( -
+
{books.map((book) => ( - + ))}
); @@ -64,8 +113,13 @@ interface EmptyStateProps { export function EmptyState({ message }: EmptyStateProps) { return ( -
-

{message}

+
+
+ + + +
+

{message}

); } diff --git a/apps/backoffice/app/components/JobProgress.tsx b/apps/backoffice/app/components/JobProgress.tsx index ee55914..0ff2d4c 100644 --- a/apps/backoffice/app/components/JobProgress.tsx +++ b/apps/backoffice/app/components/JobProgress.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { StatusBadge, Badge, ProgressBar } from "./ui"; interface ProgressEvent { job_id: string; @@ -28,7 +29,6 @@ export function JobProgress({ jobId, onComplete }: JobProgressProps) { const [isComplete, setIsComplete] = useState(false); useEffect(() => { - // Use SSE via local proxy const eventSource = new EventSource(`/api/jobs/${jobId}/stream`); eventSource.onmessage = (event) => { @@ -69,11 +69,19 @@ export function JobProgress({ jobId, onComplete }: JobProgressProps) { }, [jobId, onComplete]); if (error) { - return
Error: {error}
; + return ( +
+ Error: {error} +
+ ); } if (!progress) { - return
Loading progress...
; + return ( +
+ Loading progress... +
+ ); } const percent = progress.progress_percent ?? 0; @@ -81,26 +89,20 @@ export function JobProgress({ jobId, onComplete }: JobProgressProps) { const total = progress.total_files ?? 0; return ( -
-
- - {progress.status} - - {isComplete && Complete} +
+
+ + {isComplete && ( + Complete + )}
-
-
- {percent}% -
+ -
+
{processed} / {total} files {progress.current_file && ( - + Current: {progress.current_file.length > 40 ? progress.current_file.substring(0, 40) + "..." : progress.current_file} @@ -109,12 +111,12 @@ export function JobProgress({ jobId, onComplete }: JobProgressProps) {
{progress.stats_json && ( -
- Scanned: {progress.stats_json.scanned_files} - Indexed: {progress.stats_json.indexed_files} - Removed: {progress.stats_json.removed_files} +
+ Scanned: {progress.stats_json.scanned_files} + Indexed: {progress.stats_json.indexed_files} + Removed: {progress.stats_json.removed_files} {progress.stats_json.errors > 0 && ( - Errors: {progress.stats_json.errors} + Errors: {progress.stats_json.errors} )}
)} diff --git a/apps/backoffice/app/components/JobRow.tsx b/apps/backoffice/app/components/JobRow.tsx index f4fea71..d4e82a2 100644 --- a/apps/backoffice/app/components/JobRow.tsx +++ b/apps/backoffice/app/components/JobRow.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import Link from "next/link"; import { JobProgress } from "./JobProgress"; +import { StatusBadge, Button } from "./ui"; interface JobRowProps { job: { @@ -25,52 +26,71 @@ export function JobRow({ job, libraryName, highlighted, onCancel }: JobRowProps) const handleComplete = () => { setShowProgress(false); - // Trigger a page refresh to update the job status window.location.reload(); }; return ( <> - - - + + + {job.id.slice(0, 8)} - {job.library_id ? libraryName || job.library_id.slice(0, 8) : "β€”"} - {job.type} - - {job.status} - {job.error_opt && !} - {(job.status === "running" || job.status === "pending") && ( - - )} + + {job.library_id ? libraryName || job.library_id.slice(0, 8) : "β€”"} - {new Date(job.created_at).toLocaleString()} - -
- + {job.type} + +
+ + {job.error_opt && ( + + ! + + )} + {(job.status === "running" || job.status === "pending") && ( + + )} +
+ + + {new Date(job.created_at).toLocaleString()} + + +
+ View {(job.status === "pending" || job.status === "running") && ( - + )}
{showProgress && (job.status === "running" || job.status === "pending") && ( - - + + ([]); const [isOpen, setIsOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); const dropdownRef = useRef(null); useEffect(() => { @@ -66,7 +65,11 @@ export function JobsIndicator() { if (totalCount === 0) { return ( - + @@ -76,15 +79,19 @@ export function JobsIndicator() { } return ( -
+
+ + {isOpen && ( +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ )} +
+ ); +} diff --git a/apps/backoffice/app/components/MonitoringForm.tsx b/apps/backoffice/app/components/MonitoringForm.tsx index 38365da..f3a25cc 100644 --- a/apps/backoffice/app/components/MonitoringForm.tsx +++ b/apps/backoffice/app/components/MonitoringForm.tsx @@ -34,28 +34,38 @@ export function MonitoringForm({ libraryId, monitorEnabled, scanMode, watcherEna }; return ( -
+ -
-