From 9024a789389c89883d65d2bf5562f9ef7143eee2 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sat, 18 Oct 2025 21:59:13 +0200 Subject: [PATCH] feat: enhance ClientLibraryPage with LibraryHeader component and improve loading skeleton structure for better user experience --- .../[libraryId]/ClientLibraryPage.tsx | 114 +++++++++--------- src/components/library/LibraryHeader.tsx | 102 ++++++++++++++++ .../library/PaginatedSeriesGrid.tsx | 27 ++++- src/components/library/SearchInput.tsx | 2 +- src/i18n/messages/en/common.json | 8 ++ src/i18n/messages/fr/common.json | 8 ++ 6 files changed, 200 insertions(+), 61 deletions(-) create mode 100644 src/components/library/LibraryHeader.tsx diff --git a/src/app/libraries/[libraryId]/ClientLibraryPage.tsx b/src/app/libraries/[libraryId]/ClientLibraryPage.tsx index 240afb6..9086dc2 100644 --- a/src/app/libraries/[libraryId]/ClientLibraryPage.tsx +++ b/src/app/libraries/[libraryId]/ClientLibraryPage.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { PaginatedSeriesGrid } from "@/components/library/PaginatedSeriesGrid"; import { RefreshButton } from "@/components/library/RefreshButton"; +import { LibraryHeader } from "@/components/library/LibraryHeader"; import { ErrorMessage } from "@/components/ui/ErrorMessage"; import { useTranslate } from "@/hooks/useTranslate"; import { OptimizedSkeleton } from "@/components/skeletons/OptimizedSkeletons"; @@ -156,45 +157,54 @@ export function ClientLibraryPage({ if (loading) { return ( - - {/* Section header avec titre + actions */} -
-
- -
- - + <> + {/* Header skeleton */} +
+
+
+
+ +
+ +
+ + + +
+
- {/* Filters */} -
-
-
- -
-
- - - + + {/* Filters */} +
+
+
+ +
+
+ + + +
-
- {/* Grid */} -
- {Array.from({ length: effectivePageSize }).map((_, i) => ( - - ))} -
+ {/* Grid */} +
+ {Array.from({ length: effectivePageSize }).map((_, i) => ( + + ))} +
- {/* Pagination */} -
- - -
- + {/* Pagination */} +
+ + +
+ + ); } @@ -219,32 +229,24 @@ export function ClientLibraryPage({ } return ( - -
- {series.totalElements > 0 && ( -

- {t("series.display.showing", { - start: ((currentPage - 1) * effectivePageSize) + 1, - end: Math.min(currentPage * effectivePageSize, series.totalElements), - total: series.totalElements, - })} -

- )} - -
- } - /> - + - + + + + ); } diff --git a/src/components/library/LibraryHeader.tsx b/src/components/library/LibraryHeader.tsx new file mode 100644 index 0000000..f40fdf4 --- /dev/null +++ b/src/components/library/LibraryHeader.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { Library, BookOpen } from "lucide-react"; +import type { KomgaLibrary, KomgaSeries } from "@/types/komga"; +import { RefreshButton } from "./RefreshButton"; +import { useTranslate } from "@/hooks/useTranslate"; +import { StatusBadge } from "@/components/ui/status-badge"; +import { SeriesCover } from "@/components/ui/series-cover"; + +interface LibraryHeaderProps { + library: KomgaLibrary; + seriesCount: number; + series: KomgaSeries[]; + refreshLibrary: (libraryId: string) => Promise<{ success: boolean; error?: string }>; +} + +export const LibraryHeader = ({ library, seriesCount, series, refreshLibrary }: LibraryHeaderProps) => { + const { t } = useTranslate(); + + // Sélectionner une série aléatoire pour l'image centrale + const randomSeries = series.length > 0 ? series[Math.floor(Math.random() * series.length)] : null; + + // Sélectionner une autre série aléatoire pour le fond (différente de celle du centre) + const backgroundSeries = series.length > 1 + ? series.filter(s => s.id !== randomSeries?.id)[Math.floor(Math.random() * (series.length - 1))] + : randomSeries; + + return ( +
+ {/* Image de fond avec une série aléatoire */} +
+
+ {backgroundSeries ? ( + + ) : ( +
+ )} +
+ + {/* Contenu */} +
+
+ {/* Cover centrale avec icône overlay */} +
+ {randomSeries ? ( +
+ +
+ +
+
+ ) : ( +
+ +
+ )} +
+ + {/* Informations */} +
+

{library.name}

+ +
+ + {seriesCount === 1 + ? t("library.header.series", { count: seriesCount }) + : t("library.header.series_plural", { count: seriesCount }) + } + + + + {library.booksCount === 1 + ? t("library.header.books", { count: library.booksCount }) + : t("library.header.books_plural", { count: library.booksCount }) + } + + + +
+ + {library.unavailable && ( +

+ {t("library.header.unavailable")} +

+ )} +
+
+
+
+ ); +}; + diff --git a/src/components/library/PaginatedSeriesGrid.tsx b/src/components/library/PaginatedSeriesGrid.tsx index d1aa0cc..39078e0 100644 --- a/src/components/library/PaginatedSeriesGrid.tsx +++ b/src/components/library/PaginatedSeriesGrid.tsx @@ -11,7 +11,6 @@ import { useDisplayPreferences } from "@/hooks/useDisplayPreferences"; import { PageSizeSelect } from "@/components/common/PageSizeSelect"; import { CompactModeButton } from "@/components/common/CompactModeButton"; import { UnreadFilterButton } from "@/components/common/UnreadFilterButton"; -import { Container } from "@/components/ui/container"; interface PaginatedSeriesGridProps { series: KomgaSeries[]; @@ -20,6 +19,7 @@ interface PaginatedSeriesGridProps { totalElements: number; defaultShowOnlyUnread: boolean; showOnlyUnread: boolean; + pageSize?: number; } export function PaginatedSeriesGrid({ @@ -29,12 +29,16 @@ export function PaginatedSeriesGrid({ totalElements: _totalElements, defaultShowOnlyUnread, showOnlyUnread: initialShowOnlyUnread, + pageSize, }: PaginatedSeriesGridProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread); - const { isCompact, itemsPerPage: _itemsPerPage } = useDisplayPreferences(); + const { isCompact, itemsPerPage: displayItemsPerPage } = useDisplayPreferences(); + + // Utiliser la taille de page effective (depuis l'URL ou les préférences) + const effectivePageSize = pageSize || displayItemsPerPage; const { t } = useTranslate(); const updateUrlParams = useCallback(async ( @@ -97,9 +101,24 @@ export function PaginatedSeriesGrid({ }); }; + // Calculate start and end indices for display + const startIndex = (currentPage - 1) * effectivePageSize + 1; + const endIndex = Math.min(currentPage * effectivePageSize, _totalElements); + + const getShowingText = () => { + if (!_totalElements) return t("series.empty"); + + return t("books.display.showing", { + start: startIndex, + end: endIndex, + total: _totalElements, + }); + }; + return ( - +
+

{getShowingText()}

@@ -125,6 +144,6 @@ export function PaginatedSeriesGrid({ className="order-1 sm:order-2" />
- +
); } diff --git a/src/components/library/SearchInput.tsx b/src/components/library/SearchInput.tsx index 8d5081f..f21559a 100644 --- a/src/components/library/SearchInput.tsx +++ b/src/components/library/SearchInput.tsx @@ -39,7 +39,7 @@ export const SearchInput = ({ placeholder }: SearchInputProps) => { handleSearch(e.target.value)} aria-label={placeholder} diff --git a/src/i18n/messages/en/common.json b/src/i18n/messages/en/common.json index 17e7c34..15ee1b4 100644 --- a/src/i18n/messages/en/common.json +++ b/src/i18n/messages/en/common.json @@ -197,6 +197,14 @@ "title": "Error", "description": "An error occurred" } + }, + "header": { + "series": "{{count}} series", + "series_plural": "{{count}} series", + "books": "{{count}} book", + "books_plural": "{{count}} books", + "unavailable": "This library is currently unavailable", + "coverAlt": "Cover of {{name}}" } }, "series": { diff --git a/src/i18n/messages/fr/common.json b/src/i18n/messages/fr/common.json index 05fb6af..b983574 100644 --- a/src/i18n/messages/fr/common.json +++ b/src/i18n/messages/fr/common.json @@ -197,6 +197,14 @@ "title": "Erreur", "description": "Une erreur est survenue" } + }, + "header": { + "series": "{{count}} série", + "series_plural": "{{count}} séries", + "books": "{{count}} livre", + "books_plural": "{{count}} livres", + "unavailable": "Cette bibliothèque est actuellement indisponible", + "coverAlt": "Couverture de {{name}}" } }, "series": {