diff --git a/devbook.md b/devbook.md index 14dc8f5..d17575f 100644 --- a/devbook.md +++ b/devbook.md @@ -13,7 +13,7 @@ Créer une application web moderne avec Next.js permettant de lire des fichiers - [x] Affichage des séries par bibliothèque - [x] Couvertures et informations des séries - [ ] Filtres et recherche - - [ ] Pagination + - [x] Pagination - [x] Lecteur de fichiers (CBZ, CBR) - [x] Navigation entre les pages - [x] Mode plein écran @@ -113,7 +113,7 @@ Créer une application web moderne avec Next.js permettant de lire des fichiers - [x] Page de détails de la série - [x] Couverture et informations - [x] Liste des tomes - - [ ] Progression de lecture + - [x] Progression de lecture - [x] Bouton de lecture contextuel - [x] Page de détails du tome - [x] Couverture et informations diff --git a/src/app/api/komga/series/[seriesId]/route.ts b/src/app/api/komga/series/[seriesId]/route.ts index 7492e1f..b7251c3 100644 --- a/src/app/api/komga/series/[seriesId]/route.ts +++ b/src/app/api/komga/series/[seriesId]/route.ts @@ -4,7 +4,7 @@ import { cookies } from "next/headers"; export async function GET(request: Request, { params }: { params: { seriesId: string } }) { try { // Récupérer les credentials Komga depuis le cookie - const configCookie = cookies().get("komga_credentials"); + const configCookie = cookies().get("komgaCredentials"); if (!configCookie) { return NextResponse.json({ error: "Configuration Komga manquante" }, { status: 401 }); } @@ -24,6 +24,11 @@ export async function GET(request: Request, { params }: { params: { seriesId: st `${config.credentials.username}:${config.credentials.password}` ).toString("base64"); + // Récupérer les paramètres de pagination depuis l'URL + const { searchParams } = new URL(request.url); + const page = searchParams.get("page") || "0"; + const size = searchParams.get("size") || "24"; + // Appel à l'API Komga pour récupérer les détails de la série const [seriesResponse, booksResponse] = await Promise.all([ // Détails de la série @@ -32,9 +37,9 @@ export async function GET(request: Request, { params }: { params: { seriesId: st Authorization: `Basic ${auth}`, }, }), - // Liste des tomes (on récupère tous les tomes avec size=1000) + // Liste des tomes avec pagination fetch( - `${config.serverUrl}/api/v1/series/${params.seriesId}/books?page=0&size=1000&unpaged=true&sort=metadata.numberSort,asc`, + `${config.serverUrl}/api/v1/series/${params.seriesId}/books?page=${page}&size=${size}&sort=metadata.numberSort,asc`, { headers: { Authorization: `Basic ${auth}`, @@ -55,10 +60,7 @@ export async function GET(request: Request, { params }: { params: { seriesId: st ); } - const [series, booksData] = await Promise.all([seriesResponse.json(), booksResponse.json()]); - - // On extrait la liste des tomes de la réponse paginée - const books = booksData.content; + const [series, books] = await Promise.all([seriesResponse.json(), booksResponse.json()]); return NextResponse.json({ series, books }); } catch (error) { diff --git a/src/app/libraries/[libraryId]/page.tsx b/src/app/libraries/[libraryId]/page.tsx index 7f40fe1..d53a07f 100644 --- a/src/app/libraries/[libraryId]/page.tsx +++ b/src/app/libraries/[libraryId]/page.tsx @@ -1,8 +1,14 @@ import { cookies } from "next/headers"; -import { SeriesGrid } from "@/components/library/SeriesGrid"; -import { KomgaSeries } from "@/types/komga"; +import { PaginatedSeriesGrid } from "@/components/library/PaginatedSeriesGrid"; -async function getLibrarySeries(libraryId: string) { +interface PageProps { + params: { libraryId: string }; + searchParams: { page?: string }; +} + +const PAGE_SIZE = 20; + +async function getLibrarySeries(libraryId: string, page: number = 1) { const configCookie = cookies().get("komgaCredentials"); if (!configCookie) { @@ -16,14 +22,10 @@ async function getLibrarySeries(libraryId: string) { throw new Error("Configuration Komga invalide ou incomplète"); } - console.log("Config:", { - serverUrl: config.serverUrl, - hasCredentials: !!config.credentials, - username: config.credentials.username, - }); + // Paramètres de pagination + const pageIndex = page - 1; // L'API Komga utilise un index base 0 - const url = `${config.serverUrl}/api/v1/series?library_id=${libraryId}&page=0&size=100`; - console.log("URL de l'API:", url); + const url = `${config.serverUrl}/api/v1/series?library_id=${libraryId}&page=${pageIndex}&size=${PAGE_SIZE}`; const credentials = `${config.credentials.username}:${config.credentials.password}`; const auth = Buffer.from(credentials).toString("base64"); @@ -33,46 +35,44 @@ async function getLibrarySeries(libraryId: string) { Authorization: `Basic ${auth}`, Accept: "application/json", }, - cache: "no-store", // Désactiver le cache pour le debug + next: { revalidate: 300 }, // Cache de 5 minutes }); if (!response.ok) { - const errorText = await response.text(); - console.error("Réponse de l'API non valide:", { - status: response.status, - statusText: response.statusText, - headers: Object.fromEntries(response.headers.entries()), - body: errorText, - }); throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`); } const data = await response.json(); - console.log("Données reçues:", { - totalElements: data.totalElements, - totalPages: data.totalPages, - numberOfElements: data.numberOfElements, - }); - return { data, serverUrl: config.serverUrl }; } catch (error) { - console.error("Erreur détaillée:", { - message: error instanceof Error ? error.message : "Erreur inconnue", - stack: error instanceof Error ? error.stack : undefined, - error, - }); throw error instanceof Error ? error : new Error("Erreur lors de la récupération des séries"); } } -export default async function LibraryPage({ params }: { params: { libraryId: string } }) { +export default async function LibraryPage({ params, searchParams }: PageProps) { + const currentPage = searchParams.page ? parseInt(searchParams.page) : 1; + try { - const { data: series, serverUrl } = await getLibrarySeries(params.libraryId); + const { data: series, serverUrl } = await getLibrarySeries(params.libraryId, currentPage); return (
-

Séries

- +
+

Séries

+ {series.totalElements > 0 && ( +

+ {series.totalElements} série{series.totalElements > 1 ? "s" : ""} +

+ )} +
+
); } catch (error) { diff --git a/src/app/series/[seriesId]/page.tsx b/src/app/series/[seriesId]/page.tsx index a5c1711..8d1b29d 100644 --- a/src/app/series/[seriesId]/page.tsx +++ b/src/app/series/[seriesId]/page.tsx @@ -4,25 +4,44 @@ import { useRouter } from "next/navigation"; import { KomgaSeries, KomgaBook } from "@/types/komga"; import { useEffect, useState } from "react"; import { BookGrid } from "@/components/series/BookGrid"; -import { ImageOff } from "lucide-react"; +import { ImageOff, Loader2 } from "lucide-react"; import Image from "next/image"; +import { Pagination } from "@/components/ui/Pagination"; +import { useSearchParams, usePathname } from "next/navigation"; +import { cn } from "@/lib/utils"; interface SeriesData { series: KomgaSeries; - books: KomgaBook[]; + books: { + content: KomgaBook[]; + totalElements: number; + totalPages: number; + number: number; + size: number; + }; } +const PAGE_SIZE = 24; // 6 colonnes x 4 lignes pour un affichage optimal + export default function SeriesPage({ params }: { params: { seriesId: string } }) { const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const currentPage = searchParams.get("page") ? parseInt(searchParams.get("page")!) : 1; + const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [isChangingPage, setIsChangingPage] = useState(false); const [error, setError] = useState(null); const [imageError, setImageError] = useState(false); useEffect(() => { const fetchSeriesData = async () => { try { - const response = await fetch(`/api/komga/series/${params.seriesId}`); + setIsChangingPage(true); + const response = await fetch( + `/api/komga/series/${params.seriesId}?page=${currentPage - 1}&size=${PAGE_SIZE}` + ); if (!response.ok) { const data = await response.json(); throw new Error(data.error || "Erreur lors de la récupération de la série"); @@ -34,16 +53,22 @@ export default function SeriesPage({ params }: { params: { seriesId: string } }) setError(error instanceof Error ? error.message : "Une erreur est survenue"); } finally { setIsLoading(false); + setIsChangingPage(false); } }; fetchSeriesData(); - }, [params.seriesId]); + }, [params.seriesId, currentPage]); const handleBookClick = (book: KomgaBook) => { router.push(`/books/${book.id}`); }; + const handlePageChange = (page: number) => { + setIsChangingPage(true); + router.push(`/series/${params.seriesId}?page=${page}`); + }; + const getBookThumbnailUrl = (bookId: string) => { return `/api/komga/images/books/${bookId}/thumbnail`; }; @@ -88,6 +113,8 @@ export default function SeriesPage({ params }: { params: { seriesId: string } }) } const { series, books } = data; + const startIndex = (currentPage - 1) * PAGE_SIZE + 1; + const endIndex = Math.min(currentPage * PAGE_SIZE, books.totalElements); return (
@@ -178,14 +205,60 @@ export default function SeriesPage({ params }: { params: { seriesId: string } }) {/* Grille des tomes */}
-

- Tomes ({books.length}) -

- +
+

+ Tomes ({books.totalElements}) +

+

+ {books.totalElements > 0 ? ( + <> + Affichage des tomes {startIndex} à{" "} + {endIndex} sur{" "} + {books.totalElements} + + ) : ( + "Aucun tome disponible" + )} +

+
+ +
+ {/* Indicateur de chargement */} + {isChangingPage && ( +
+
+ + Chargement... +
+
+ )} + + {/* Grille avec animation de transition */} +
+ +
+
+ +
+

+ Page {currentPage} sur {books.totalPages} +

+ +
); diff --git a/src/components/library/PaginatedSeriesGrid.tsx b/src/components/library/PaginatedSeriesGrid.tsx new file mode 100644 index 0000000..dab3b0b --- /dev/null +++ b/src/components/library/PaginatedSeriesGrid.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { SeriesGrid } from "./SeriesGrid"; +import { Pagination } from "@/components/ui/Pagination"; +import { useRouter, usePathname, useSearchParams } from "next/navigation"; +import { useState, useEffect } from "react"; +import { Loader2 } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface PaginatedSeriesGridProps { + series: any[]; + serverUrl: string; + currentPage: number; + totalPages: number; + totalElements: number; + pageSize: number; +} + +export function PaginatedSeriesGrid({ + series, + serverUrl, + currentPage, + totalPages, + totalElements, + pageSize, +}: PaginatedSeriesGridProps) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const [isChangingPage, setIsChangingPage] = useState(false); + + // Réinitialiser l'état de chargement quand les séries changent + useEffect(() => { + setIsChangingPage(false); + }, [series]); + + const handlePageChange = (page: number) => { + setIsChangingPage(true); + // Créer un nouvel objet URLSearchParams pour manipuler les paramètres + const params = new URLSearchParams(searchParams); + params.set("page", page.toString()); + + // Rediriger vers la nouvelle URL avec les paramètres mis à jour + router.push(`${pathname}?${params.toString()}`); + }; + + // Calcul des indices de début et de fin pour l'affichage + const startIndex = (currentPage - 1) * pageSize + 1; + const endIndex = Math.min(currentPage * pageSize, totalElements); + + return ( +
+
+

+ {totalElements > 0 ? ( + <> + Affichage des séries {startIndex} à{" "} + {endIndex} sur{" "} + {totalElements} + + ) : ( + "Aucune série trouvée" + )} +

+
+ +
+ {/* Indicateur de chargement */} + {isChangingPage && ( +
+
+ + Chargement... +
+
+ )} + + {/* Grille avec animation de transition */} +
+ +
+
+ +
+

+ Page {currentPage} sur {totalPages} +

+ +
+
+ ); +} diff --git a/src/components/ui/Pagination.tsx b/src/components/ui/Pagination.tsx new file mode 100644 index 0000000..3e8c1e0 --- /dev/null +++ b/src/components/ui/Pagination.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface PaginationProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + className?: string; +} + +export function Pagination({ currentPage, totalPages, onPageChange, className }: PaginationProps) { + // Ne pas afficher la pagination s'il n'y a qu'une seule page + if (totalPages <= 1) return null; + + // Fonction pour générer la liste des pages à afficher + const getPageNumbers = () => { + const pages: (number | "...")[] = []; + + if (totalPages <= 7) { + // Si moins de 7 pages, afficher toutes les pages + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + // Toujours afficher la première page + pages.push(1); + + if (currentPage > 3) { + pages.push("..."); + } + + // Pages autour de la page courante + for ( + let i = Math.max(2, currentPage - 1); + i <= Math.min(totalPages - 1, currentPage + 1); + i++ + ) { + pages.push(i); + } + + if (currentPage < totalPages - 2) { + pages.push("..."); + } + + // Toujours afficher la dernière page + pages.push(totalPages); + } + + return pages; + }; + + return ( + + ); +}