From 6834d00b152fd3dbb5dca477c255af3eb23a3b60 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Thu, 26 Mar 2026 23:08:34 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20pagination=20et=20lignes=20compactes=20?= =?UTF-8?q?sur=20la=20page=20t=C3=A9l=C3=A9chargements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remplace les cartes par des lignes compactes (DownloadRow) pour réduire l'espace vertical de chaque téléchargement - Pagination côté client (10 par page) avec navigation prev/next - Reset automatique à la page 1 au changement de filtre Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/(app)/downloads/DownloadsPage.tsx | 201 ++++++++---------- 1 file changed, 92 insertions(+), 109 deletions(-) diff --git a/apps/backoffice/app/(app)/downloads/DownloadsPage.tsx b/apps/backoffice/app/(app)/downloads/DownloadsPage.tsx index 50abc6f..dd6d0f2 100644 --- a/apps/backoffice/app/(app)/downloads/DownloadsPage.tsx +++ b/apps/backoffice/app/(app)/downloads/DownloadsPage.tsx @@ -66,11 +66,14 @@ interface DownloadsPageProps { initialLatestFound: LatestFoundPerLibraryDto[]; } +const PAGE_SIZE = 10; + export function DownloadsPage({ initialDownloads, initialLatestFound }: DownloadsPageProps) { const { t } = useTranslation(); const [downloads, setDownloads] = useState(initialDownloads); const [filter, setFilter] = useState("all"); const [isRefreshing, setIsRefreshing] = useState(false); + const [page, setPage] = useState(1); const refresh = useCallback(async (showSpinner = true) => { if (showSpinner) setIsRefreshing(true); @@ -103,6 +106,12 @@ export function DownloadsPage({ initialDownloads, initialLatestFound }: Download return d.status === filter; }); + const totalPages = Math.ceil(visible.length / PAGE_SIZE); + const paged = visible.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); + + // Reset to page 1 when filter changes + const handleFilterChange = (id: string) => { setFilter(id); setPage(1); }; + return ( <>
@@ -125,7 +134,7 @@ export function DownloadsPage({ initialDownloads, initialLatestFound }: Download {filters.map(f => ( + + {page} / {totalPages} + + +
+ )} + )} {/* Available downloads from latest detection */} @@ -177,7 +201,7 @@ export function DownloadsPage({ initialDownloads, initialLatestFound }: Download ); } -function DownloadCard({ dl, onDeleted }: { dl: TorrentDownloadDto; onDeleted: () => void }) { +function DownloadRow({ dl, onDeleted }: { dl: TorrentDownloadDto; onDeleted: () => void }) { const { t } = useTranslation(); const [deleting, setDeleting] = useState(false); const [showConfirm, setShowConfirm] = useState(false); @@ -194,115 +218,74 @@ function DownloadCard({ dl, onDeleted }: { dl: TorrentDownloadDto; onDeleted: () } } + const statusIcon = dl.status === "importing" ? ( + + ) : dl.status === "imported" ? ( + + ) : dl.status === "error" ? ( + + ) : dl.status === "downloading" ? ( + + ) : ( + + ); + return ( - - -
- {/* Status indicator */} -
- {dl.status === "importing" ? ( - - ) : dl.status === "imported" ? ( - - ) : dl.status === "error" ? ( - - ) : dl.status === "downloading" ? ( - - ) : ( - + <> +
+ {statusIcon} + +
+
+ {dl.series_name} + + {statusLabel(dl.status, t)} + + {dl.expected_volumes.length > 0 && ( + {formatVolumes(dl.expected_volumes)} + )} + {dl.status === "imported" && importedCount > 0 && ( + {importedCount} {t("downloads.filesImported")} )}
- {/* Main info */} -
-
- {dl.series_name} - - {statusLabel(dl.status, t)} - -
- -
- {dl.expected_volumes.length > 0 && ( - {t("downloads.volumes")} : {formatVolumes(dl.expected_volumes)} - )} - {dl.status === "imported" && importedCount > 0 && ( - {importedCount} {t("downloads.filesImported")} - )} - {dl.qb_hash && ( - - {dl.qb_hash.slice(0, 8)}… - - )} -
- - {dl.status === "downloading" && ( -
-
-
-
-
- - {Math.round(dl.progress * 100)}% - -
- {(dl.download_speed > 0 || dl.eta > 0) && ( -
- {dl.download_speed > 0 && {formatSpeed(dl.download_speed)}} - {dl.eta > 0 && dl.eta < 8640000 && ETA {formatEta(dl.eta)}} -
- )} + {dl.status === "downloading" && ( +
+
+
- )} - - {dl.content_path && dl.status !== "imported" && ( -

- {dl.content_path} -

- )} - - {dl.error_message && ( -

{dl.error_message}

- )} - - {dl.status === "imported" && Array.isArray(dl.imported_files) && dl.imported_files.length > 0 && ( -
    - {(dl.imported_files as Array<{ volume: number; destination: string }>).map((f, i) => ( -
  • - T{String(f.volume).padStart(2, "0")} → {f.destination.split("/").pop()} -
  • - ))} -
- )} -
- - {/* Actions */} -
-
-

{formatDate(dl.created_at)}

- {dl.updated_at !== dl.created_at && ( -

maj {formatDate(dl.updated_at)}

+ + {Math.round(dl.progress * 100)}% + + {dl.download_speed > 0 && ( + {formatSpeed(dl.download_speed)} + )} + {dl.eta > 0 && dl.eta < 8640000 && ( + ETA {formatEta(dl.eta)} )}
- -
+ )} + + {dl.error_message && ( +

{dl.error_message}

+ )}
- + + {formatDate(dl.created_at)} + + +
{showConfirm && createPortal( <> @@ -330,7 +313,7 @@ function DownloadCard({ dl, onDeleted }: { dl: TorrentDownloadDto; onDeleted: () , document.body )} - + ); }