diff --git a/src/app/api/komga/libraries/[libraryId]/series/route.ts b/src/app/api/komga/libraries/[libraryId]/series/route.ts
index 33b47ce..495bfbe 100644
--- a/src/app/api/komga/libraries/[libraryId]/series/route.ts
+++ b/src/app/api/komga/libraries/[libraryId]/series/route.ts
@@ -21,26 +21,32 @@ export async function GET(request: Request, { params }: { params: { libraryId: s
return NextResponse.json({ error: "Credentials Komga manquants" }, { status: 401 });
}
- // Récupérer les paramètres de pagination depuis l'URL
+ // Récupérer les paramètres de pagination et de filtre depuis l'URL
const { searchParams } = new URL(request.url);
const page = searchParams.get("page") || "0";
const size = searchParams.get("size") || "20";
+ const unreadOnly = searchParams.get("unread") === "true";
// Clé de cache unique pour cette page de séries
- const cacheKey = `library-${params.libraryId}-series-${page}-${size}`;
+ const cacheKey = `library-${params.libraryId}-series-${page}-${size}-${unreadOnly}`;
// Fonction pour récupérer les séries
const fetchSeries = async () => {
- const response = await fetch(
- `${config.serverUrl}/api/v1/series?library_id=${params.libraryId}&page=${page}&size=${size}`,
- {
- headers: {
- Authorization: `Basic ${Buffer.from(
- `${config.credentials.username}:${config.credentials.password}`
- ).toString("base64")}`,
- },
- }
- );
+ // Construire l'URL avec les paramètres
+ let url = `${config.serverUrl}/api/v1/series?library_id=${params.libraryId}&page=${page}&size=${size}`;
+
+ // Ajouter le filtre pour les séries non lues et en cours si nécessaire
+ if (unreadOnly) {
+ url += "&read_status=UNREAD&read_status=IN_PROGRESS";
+ }
+
+ const response = await fetch(url, {
+ headers: {
+ Authorization: `Basic ${Buffer.from(
+ `${config.credentials.username}:${config.credentials.password}`
+ ).toString("base64")}`,
+ },
+ });
if (!response.ok) {
const errorData = await response.json().catch(() => null);
diff --git a/src/app/api/komga/series/[seriesId]/books/route.ts b/src/app/api/komga/series/[seriesId]/books/route.ts
new file mode 100644
index 0000000..b0d035c
--- /dev/null
+++ b/src/app/api/komga/series/[seriesId]/books/route.ts
@@ -0,0 +1,78 @@
+import { NextResponse } from "next/server";
+import { cookies } from "next/headers";
+import { serverCacheService } from "@/lib/services/server-cache.service";
+
+export async function GET(request: Request, { params }: { params: { seriesId: string } }) {
+ try {
+ // Récupérer les credentials Komga depuis le cookie
+ const configCookie = cookies().get("komgaCredentials");
+ if (!configCookie) {
+ return NextResponse.json({ error: "Configuration Komga manquante" }, { status: 401 });
+ }
+
+ let config;
+ try {
+ config = JSON.parse(atob(configCookie.value));
+ } catch (error) {
+ return NextResponse.json({ error: "Configuration Komga invalide" }, { status: 401 });
+ }
+
+ if (!config.credentials?.username || !config.credentials?.password) {
+ return NextResponse.json({ error: "Credentials Komga manquants" }, { status: 401 });
+ }
+
+ // Récupérer les paramètres de pagination et de filtre depuis l'URL
+ const { searchParams } = new URL(request.url);
+ const page = searchParams.get("page") || "0";
+ const size = searchParams.get("size") || "24";
+ const unreadOnly = searchParams.get("unread") === "true";
+
+ // Clé de cache unique pour cette page de tomes
+ const cacheKey = `series-${params.seriesId}-books-${page}-${size}-${unreadOnly}`;
+
+ // Fonction pour récupérer les tomes
+ const fetchBooks = async () => {
+ // Construire l'URL avec les paramètres
+ let url = `${config.serverUrl}/api/v1/series/${params.seriesId}/books?page=${page}&size=${size}&sort=metadata.numberSort,asc`;
+
+ // Ajouter le filtre pour les tomes non lus et en cours si nécessaire
+ if (unreadOnly) {
+ url += "&read_status=UNREAD&read_status=IN_PROGRESS";
+ }
+
+ const response = await fetch(url, {
+ headers: {
+ Authorization: `Basic ${Buffer.from(
+ `${config.credentials.username}:${config.credentials.password}`
+ ).toString("base64")}`,
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => null);
+ throw new Error(
+ JSON.stringify({
+ error: "Erreur lors de la récupération des tomes",
+ details: errorData,
+ })
+ );
+ }
+
+ return response.json();
+ };
+
+ // Récupérer les données du cache ou faire l'appel API
+ const data = await serverCacheService.getOrSet(cacheKey, fetchBooks, 5 * 60); // Cache de 5 minutes
+
+ return NextResponse.json(data);
+ } catch (error) {
+ console.error("Erreur lors de la récupération des tomes:", error);
+ return NextResponse.json(
+ {
+ error: "Erreur serveur",
+ details: error instanceof Error ? error.message : "Erreur inconnue",
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/libraries/[libraryId]/page.tsx b/src/app/libraries/[libraryId]/page.tsx
index d53a07f..42c805c 100644
--- a/src/app/libraries/[libraryId]/page.tsx
+++ b/src/app/libraries/[libraryId]/page.tsx
@@ -3,12 +3,12 @@ import { PaginatedSeriesGrid } from "@/components/library/PaginatedSeriesGrid";
interface PageProps {
params: { libraryId: string };
- searchParams: { page?: string };
+ searchParams: { page?: string; unread?: string };
}
const PAGE_SIZE = 20;
-async function getLibrarySeries(libraryId: string, page: number = 1) {
+async function getLibrarySeries(libraryId: string, page: number = 1, unreadOnly: boolean = false) {
const configCookie = cookies().get("komgaCredentials");
if (!configCookie) {
@@ -25,7 +25,13 @@ async function getLibrarySeries(libraryId: string, page: number = 1) {
// 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=${pageIndex}&size=${PAGE_SIZE}`;
+ // Construire l'URL avec les paramètres
+ let url = `${config.serverUrl}/api/v1/series?library_id=${libraryId}&page=${pageIndex}&size=${PAGE_SIZE}`;
+
+ // Ajouter le filtre pour les séries non lues et en cours si nécessaire
+ if (unreadOnly) {
+ url += "&read_status=UNREAD&read_status=IN_PROGRESS";
+ }
const credentials = `${config.credentials.username}:${config.credentials.password}`;
const auth = Buffer.from(credentials).toString("base64");
@@ -51,9 +57,14 @@ async function getLibrarySeries(libraryId: string, page: number = 1) {
export default async function LibraryPage({ params, searchParams }: PageProps) {
const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
+ const unreadOnly = searchParams.unread === "true";
try {
- const { data: series, serverUrl } = await getLibrarySeries(params.libraryId, currentPage);
+ const { data: series, serverUrl } = await getLibrarySeries(
+ params.libraryId,
+ currentPage,
+ unreadOnly
+ );
return (
diff --git a/src/app/series/[seriesId]/page.tsx b/src/app/series/[seriesId]/page.tsx
index 8d1b29d..43b2465 100644
--- a/src/app/series/[seriesId]/page.tsx
+++ b/src/app/series/[seriesId]/page.tsx
@@ -1,265 +1,92 @@
-"use client";
-
-import { useRouter } from "next/navigation";
+import { cookies } from "next/headers";
+import { PaginatedBookGrid } from "@/components/series/PaginatedBookGrid";
+import { SeriesHeader } from "@/components/series/SeriesHeader";
import { KomgaSeries, KomgaBook } from "@/types/komga";
-import { useEffect, useState } from "react";
-import { BookGrid } from "@/components/series/BookGrid";
-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: {
- content: KomgaBook[];
- totalElements: number;
- totalPages: number;
- number: number;
- size: number;
- };
+interface PageProps {
+ params: { seriesId: string };
+ searchParams: { page?: string; unread?: string };
}
-const PAGE_SIZE = 24; // 6 colonnes x 4 lignes pour un affichage optimal
+const PAGE_SIZE = 24;
-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;
+export default async function SeriesPage({ params, searchParams }: PageProps) {
+ const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
+ const unreadOnly = searchParams.unread === "true";
- 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);
+ const configCookie = cookies().get("komgaCredentials");
+ if (!configCookie) {
+ throw new Error("Configuration Komga manquante");
+ }
- useEffect(() => {
- const fetchSeriesData = async () => {
- try {
- 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");
+ try {
+ const config = JSON.parse(atob(configCookie.value));
+ if (!config.serverUrl || !config.credentials?.username || !config.credentials?.password) {
+ throw new Error("Configuration Komga invalide ou incomplète");
+ }
+
+ const credentials = `${config.credentials.username}:${config.credentials.password}`;
+ const auth = Buffer.from(credentials).toString("base64");
+
+ // Paramètres de pagination
+ const pageIndex = currentPage - 1; // L'API Komga utilise un index base 0
+
+ // Appels API parallèles pour les détails de la série et les tomes
+ const [seriesResponse, booksResponse] = await Promise.all([
+ // Détails de la série
+ fetch(`${config.serverUrl}/api/v1/series/${params.seriesId}`, {
+ headers: {
+ Authorization: `Basic ${auth}`,
+ Accept: "application/json",
+ },
+ next: { revalidate: 300 },
+ }),
+ // Liste des tomes avec pagination et filtre
+ fetch(
+ `${config.serverUrl}/api/v1/series/${
+ params.seriesId
+ }/books?page=${pageIndex}&size=${PAGE_SIZE}&sort=metadata.numberSort,asc${
+ unreadOnly ? "&read_status=UNREAD&read_status=IN_PROGRESS" : ""
+ }`,
+ {
+ headers: {
+ Authorization: `Basic ${auth}`,
+ Accept: "application/json",
+ },
+ next: { revalidate: 300 },
}
- const data = await response.json();
- setData(data);
- } catch (error) {
- console.error("Erreur:", error);
- setError(error instanceof Error ? error.message : "Une erreur est survenue");
- } finally {
- setIsLoading(false);
- setIsChangingPage(false);
- }
- };
+ ),
+ ]);
- fetchSeriesData();
- }, [params.seriesId, currentPage]);
+ if (!seriesResponse.ok || !booksResponse.ok) {
+ throw new Error("Erreur lors de la récupération des données");
+ }
- const handleBookClick = (book: KomgaBook) => {
- router.push(`/books/${book.id}`);
- };
+ const [series, books] = await Promise.all([seriesResponse.json(), booksResponse.json()]);
- const handlePageChange = (page: number) => {
- setIsChangingPage(true);
- router.push(`/series/${params.seriesId}?page=${page}`);
- };
-
- const getBookThumbnailUrl = (bookId: string) => {
- return `/api/komga/images/books/${bookId}/thumbnail`;
- };
-
- if (isLoading) {
return (
-
-
-
-
-
- {[...Array(6)].map((_, i) => (
-
- ))}
-
-
+
);
- }
-
- if (error || !data) {
+ } catch (error) {
return (
-
+
+
Série
-
{error || "Données non disponibles"}
+
+ {error instanceof Error ? error.message : "Erreur lors de la récupération de la série"}
+
);
}
-
- const { series, books } = data;
- const startIndex = (currentPage - 1) * PAGE_SIZE + 1;
- const endIndex = Math.min(currentPage * PAGE_SIZE, books.totalElements);
-
- return (
-
- {/* En-tête de la série */}
-
- {/* Couverture */}
-
-
- {!imageError ? (
-
setImageError(true)}
- />
- ) : (
-
-
-
- )}
-
-
-
- {/* Informations */}
-
-
-
{series.metadata.title}
- {series.metadata.status && (
-
- {series.metadata.status === "ENDED"
- ? "Terminé"
- : series.metadata.status === "ONGOING"
- ? "En cours"
- : series.metadata.status === "ABANDONED"
- ? "Abandonné"
- : "En pause"}
-
- )}
-
-
- {series.metadata.summary && (
-
{series.metadata.summary}
- )}
-
-
- {series.metadata.publisher && (
-
- Éditeur : {series.metadata.publisher}
-
- )}
- {series.metadata.genres?.length > 0 && (
-
- Genres : {series.metadata.genres.join(", ")}
-
- )}
- {series.metadata.tags?.length > 0 && (
-
- Tags : {series.metadata.tags.join(", ")}
-
- )}
- {series.metadata.language && (
-
- Langue :{" "}
- {new Intl.DisplayNames([navigator.language], { type: "language" }).of(
- series.metadata.language
- )}
-
- )}
- {series.metadata.ageRating && (
-
- Âge recommandé : {series.metadata.ageRating}+
-
- )}
-
-
-
-
- {/* Grille des tomes */}
-
-
-
- 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
index dab3b0b..389773f 100644
--- a/src/components/library/PaginatedSeriesGrid.tsx
+++ b/src/components/library/PaginatedSeriesGrid.tsx
@@ -4,7 +4,7 @@ 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 { Loader2, Filter } from "lucide-react";
import { cn } from "@/lib/utils";
interface PaginatedSeriesGridProps {
@@ -28,6 +28,7 @@ export function PaginatedSeriesGrid({
const pathname = usePathname();
const searchParams = useSearchParams();
const [isChangingPage, setIsChangingPage] = useState(false);
+ const [showOnlyUnread, setShowOnlyUnread] = useState(searchParams.get("unread") === "true");
// Réinitialiser l'état de chargement quand les séries changent
useEffect(() => {
@@ -39,11 +40,29 @@ export function PaginatedSeriesGrid({
// Créer un nouvel objet URLSearchParams pour manipuler les paramètres
const params = new URLSearchParams(searchParams);
params.set("page", page.toString());
+ if (showOnlyUnread) {
+ params.set("unread", "true");
+ }
// Rediriger vers la nouvelle URL avec les paramètres mis à jour
router.push(`${pathname}?${params.toString()}`);
};
+ const handleUnreadFilter = () => {
+ setIsChangingPage(true);
+ const params = new URLSearchParams(searchParams);
+ params.set("page", "1"); // Retourner à la première page lors du changement de filtre
+
+ if (!showOnlyUnread) {
+ params.set("unread", "true");
+ } else {
+ params.delete("unread");
+ }
+
+ setShowOnlyUnread(!showOnlyUnread);
+ 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);
@@ -62,6 +81,13 @@ export function PaginatedSeriesGrid({
"Aucune série trouvée"
)}
+
diff --git a/src/components/series/SeriesHeader.tsx b/src/components/series/SeriesHeader.tsx
new file mode 100644
index 0000000..a57f95d
--- /dev/null
+++ b/src/components/series/SeriesHeader.tsx
@@ -0,0 +1,91 @@
+"use client";
+
+import Image from "next/image";
+import { ImageOff } from "lucide-react";
+import { KomgaSeries } from "@/types/komga";
+
+interface SeriesHeaderProps {
+ series: KomgaSeries;
+ serverUrl: string;
+}
+
+export function SeriesHeader({ series, serverUrl }: SeriesHeaderProps) {
+ return (
+
+ {/* Couverture */}
+
+
+ {/* Informations */}
+
+
+
{series.metadata.title}
+ {series.metadata.status && (
+
+ {series.metadata.status === "ENDED"
+ ? "Terminé"
+ : series.metadata.status === "ONGOING"
+ ? "En cours"
+ : series.metadata.status === "ABANDONED"
+ ? "Abandonné"
+ : "En pause"}
+
+ )}
+
+
+ {series.metadata.summary && (
+
{series.metadata.summary}
+ )}
+
+
+ {series.metadata.publisher && (
+
+ Éditeur : {series.metadata.publisher}
+
+ )}
+ {series.metadata.genres?.length > 0 && (
+
+ Genres : {series.metadata.genres.join(", ")}
+
+ )}
+ {series.metadata.tags?.length > 0 && (
+
+ Tags : {series.metadata.tags.join(", ")}
+
+ )}
+ {series.metadata.language && (
+
+ Langue :{" "}
+ {new Intl.DisplayNames([navigator.language], { type: "language" }).of(
+ series.metadata.language
+ )}
+
+ )}
+ {series.metadata.ageRating && (
+
+ Âge recommandé : {series.metadata.ageRating}+
+
+ )}
+
+
+
+ );
+}