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 && (
+
+ )}
+
+ {/* 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 && (
+
+ )}
+
+ {/* 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 (
+
+ );
+}