From e396503ddb22ca597aa8e52af8687ad330c13013 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 17 Oct 2025 08:46:19 +0200 Subject: [PATCH] refactor: simplify HomePage and LibraryPage components by integrating ClientHomePage and ClientLibraryPage, enhancing data fetching and error handling --- src/app/api/komga/home/route.ts | 38 +++++ .../libraries/[libraryId]/series/route.ts | 56 +++++++ .../komga/series/[seriesId]/books/route.ts | 55 +++++++ .../[libraryId]/ClientLibraryPage.tsx | 120 ++++++++++++-- src/app/libraries/[libraryId]/page.tsx | 107 ++----------- src/app/page.tsx | 51 +----- .../series/[seriesId]/ClientSeriesPage.tsx | 148 ++++++++++++++++++ src/app/series/[seriesId]/page.tsx | 96 ++---------- src/components/home/ClientHomePage.tsx | 94 +++++++++++ 9 files changed, 523 insertions(+), 242 deletions(-) create mode 100644 src/app/api/komga/home/route.ts create mode 100644 src/app/api/komga/libraries/[libraryId]/series/route.ts create mode 100644 src/app/api/komga/series/[seriesId]/books/route.ts create mode 100644 src/app/series/[seriesId]/ClientSeriesPage.tsx create mode 100644 src/components/home/ClientHomePage.tsx diff --git a/src/app/api/komga/home/route.ts b/src/app/api/komga/home/route.ts new file mode 100644 index 0000000..718714e --- /dev/null +++ b/src/app/api/komga/home/route.ts @@ -0,0 +1,38 @@ +import { NextResponse } from "next/server"; +import { HomeService } from "@/lib/services/home.service"; +import { ERROR_CODES } from "@/constants/errorCodes"; +import { AppError } from "@/utils/errors"; +import { getErrorMessage } from "@/utils/errors"; +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + const data = await HomeService.getHomeData(); + return NextResponse.json(data); + } catch (error) { + console.error("API Home - Erreur:", error); + if (error instanceof AppError) { + return NextResponse.json( + { + error: { + code: error.code, + name: "Home data fetch error", + message: getErrorMessage(error.code), + }, + }, + { status: error.code === ERROR_CODES.KOMGA.MISSING_CONFIG ? 404 : 500 } + ); + } + return NextResponse.json( + { + error: { + code: ERROR_CODES.KOMGA.SERVER_UNREACHABLE, + name: "Home data fetch error", + message: getErrorMessage(ERROR_CODES.KOMGA.SERVER_UNREACHABLE), + }, + }, + { status: 500 } + ); + } +} + diff --git a/src/app/api/komga/libraries/[libraryId]/series/route.ts b/src/app/api/komga/libraries/[libraryId]/series/route.ts new file mode 100644 index 0000000..768c68a --- /dev/null +++ b/src/app/api/komga/libraries/[libraryId]/series/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from "next/server"; +import { LibraryService } from "@/lib/services/library.service"; +import { ERROR_CODES } from "@/constants/errorCodes"; +import { AppError } from "@/utils/errors"; +import { getErrorMessage } from "@/utils/errors"; +import type { NextRequest } from "next/server"; +export const dynamic = "force-dynamic"; + +const DEFAULT_PAGE_SIZE = 20; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ libraryId: string }> } +) { + try { + const libraryId: string = (await params).libraryId; + const searchParams = request.nextUrl.searchParams; + + const page = parseInt(searchParams.get("page") || "0"); + const size = parseInt(searchParams.get("size") || String(DEFAULT_PAGE_SIZE)); + const unreadOnly = searchParams.get("unread") === "true"; + const search = searchParams.get("search") || undefined; + + const [series, library] = await Promise.all([ + LibraryService.getLibrarySeries(libraryId, page, size, unreadOnly, search), + LibraryService.getLibrary(libraryId) + ]); + + return NextResponse.json({ series, library }); + } catch (error) { + console.error("API Library Series - Erreur:", error); + if (error instanceof AppError) { + return NextResponse.json( + { + error: { + code: error.code, + name: "Library series fetch error", + message: getErrorMessage(error.code), + }, + }, + { status: 500 } + ); + } + return NextResponse.json( + { + error: { + code: ERROR_CODES.SERIES.FETCH_ERROR, + name: "Library series fetch error", + message: getErrorMessage(ERROR_CODES.SERIES.FETCH_ERROR), + }, + }, + { status: 500 } + ); + } +} + 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..2ec6cb7 --- /dev/null +++ b/src/app/api/komga/series/[seriesId]/books/route.ts @@ -0,0 +1,55 @@ +import { NextResponse } from "next/server"; +import { SeriesService } from "@/lib/services/series.service"; +import { ERROR_CODES } from "@/constants/errorCodes"; +import { AppError } from "@/utils/errors"; +import { getErrorMessage } from "@/utils/errors"; +import type { NextRequest } from "next/server"; +export const dynamic = "force-dynamic"; + +const DEFAULT_PAGE_SIZE = 20; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ seriesId: string }> } +) { + try { + const seriesId: string = (await params).seriesId; + const searchParams = request.nextUrl.searchParams; + + const page = parseInt(searchParams.get("page") || "0"); + const size = parseInt(searchParams.get("size") || String(DEFAULT_PAGE_SIZE)); + const unreadOnly = searchParams.get("unread") === "true"; + + const [books, series] = await Promise.all([ + SeriesService.getSeriesBooks(seriesId, page, size, unreadOnly), + SeriesService.getSeries(seriesId) + ]); + + return NextResponse.json({ books, series }); + } catch (error) { + console.error("API Series Books - Erreur:", error); + if (error instanceof AppError) { + return NextResponse.json( + { + error: { + code: error.code, + name: "Series books fetch error", + message: getErrorMessage(error.code), + }, + }, + { status: 500 } + ); + } + return NextResponse.json( + { + error: { + code: ERROR_CODES.BOOK.PAGES_FETCH_ERROR, + name: "Series books fetch error", + message: getErrorMessage(ERROR_CODES.BOOK.PAGES_FETCH_ERROR), + }, + }, + { status: 500 } + ); + } +} + diff --git a/src/app/libraries/[libraryId]/ClientLibraryPage.tsx b/src/app/libraries/[libraryId]/ClientLibraryPage.tsx index 55f8db7..5320713 100644 --- a/src/app/libraries/[libraryId]/ClientLibraryPage.tsx +++ b/src/app/libraries/[libraryId]/ClientLibraryPage.tsx @@ -1,46 +1,138 @@ "use client"; +import { useEffect, useState } from "react"; import { PaginatedSeriesGrid } from "@/components/library/PaginatedSeriesGrid"; import { RefreshButton } from "@/components/library/RefreshButton"; import { ErrorMessage } from "@/components/ui/ErrorMessage"; import { useTranslate } from "@/hooks/useTranslate"; +import { OptimizedSkeleton } from "@/components/skeletons/OptimizedSkeletons"; +import { LibraryService } from "@/lib/services/library.service"; import type { LibraryResponse } from "@/types/library"; import type { KomgaSeries, KomgaLibrary } from "@/types/komga"; import type { UserPreferences } from "@/types/preferences"; interface ClientLibraryPageProps { - library: KomgaLibrary | null; - series: LibraryResponse | null; currentPage: number; libraryId: string; - refreshLibrary: (libraryId: string) => Promise<{ success: boolean; error?: string }>; preferences: UserPreferences; unreadOnly: boolean; - errorCode?: string; + search?: string; + pageSize?: number; } +const DEFAULT_PAGE_SIZE = 20; + export function ClientLibraryPage({ - library, - series, currentPage, libraryId, - refreshLibrary, preferences, unreadOnly, - errorCode, + search, + pageSize, }: ClientLibraryPageProps) { const { t } = useTranslate(); + const [library, setLibrary] = useState(null); + const [series, setSeries] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - if (errorCode) { + const effectivePageSize = pageSize || preferences.displayMode?.itemsPerPage || DEFAULT_PAGE_SIZE; + + useEffect(() => { + const fetchData = async () => { + setLoading(true); + setError(null); + + try { + const params = new URLSearchParams({ + page: String(currentPage - 1), + size: String(effectivePageSize), + unread: String(unreadOnly), + }); + + if (search) { + params.append("search", search); + } + + const response = await fetch(`/api/komga/libraries/${libraryId}/series?${params}`); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error?.code || "SERIES_FETCH_ERROR"); + } + + const data = await response.json(); + setLibrary(data.library); + setSeries(data.series); + } catch (err) { + console.error("Error fetching library series:", err); + setError(err instanceof Error ? err.message : "SERIES_FETCH_ERROR"); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [libraryId, currentPage, unreadOnly, search, effectivePageSize]); + + const handleRefresh = async (libraryId: string) => { + try { + await LibraryService.invalidateLibrarySeriesCache(libraryId); + + // Recharger les données + const params = new URLSearchParams({ + page: String(currentPage - 1), + size: String(effectivePageSize), + unread: String(unreadOnly), + }); + + if (search) { + params.append("search", search); + } + + const response = await fetch(`/api/komga/libraries/${libraryId}/series?${params}`); + + if (!response.ok) { + throw new Error("Error refreshing library"); + } + + const data = await response.json(); + setLibrary(data.library); + setSeries(data.series); + + return { success: true }; + } catch (error) { + console.error("Error during refresh:", error); + return { success: false, error: "Error refreshing library" }; + } + }; + + if (loading) { + return ( +
+
+ + +
+
+ {Array.from({ length: effectivePageSize }).map((_, i) => ( + + ))} +
+
+ ); + } + + if (error) { return (

{library?.name || t("series.empty")}

- +
- +
); } @@ -61,13 +153,13 @@ export function ClientLibraryPage({ {series.totalElements > 0 && (

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

)} - + ; library: KomgaLibrary } = - await getLibrarySeries(libraryId, currentPage, unreadOnly, search, pageSize); - - return ( - - ); - } catch (error) { - if (error instanceof AppError) { - return ( - - ); - } - return ( - - ); - } + return ( + + ); } - -export default withPageTiming("LibraryPage", LibraryPage); diff --git a/src/app/page.tsx b/src/app/page.tsx index 1adceea..7472c46 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,50 +1,5 @@ -import { HomeContent } from "@/components/home/HomeContent"; -import { HomeService } from "@/lib/services/home.service"; -import { redirect } from "next/navigation"; -import { revalidatePath } from "next/cache"; -import { withPageTiming } from "@/lib/hoc/withPageTiming"; -import { ErrorMessage } from "@/components/ui/ErrorMessage"; -import { ERROR_CODES } from "@/constants/errorCodes"; -import type { HomeData } from "@/lib/services/home.service"; -import { AppError } from "@/utils/errors"; +import { ClientHomePage } from "@/components/home/ClientHomePage"; -async function refreshHome() { - "use server"; - - try { - await HomeService.invalidateHomeCache(); - revalidatePath("/"); - return { success: true }; - } catch (error) { - console.error("Erreur lors du rafraîchissement:", error); - return { success: false, error: "Erreur lors du rafraîchissement de la page d'accueil" }; - } +export default function HomePage() { + return ; } - -async function HomePage() { - try { - const data: HomeData = await HomeService.getHomeData(); - return ; - } catch (error) { - // Si l'erreur indique une configuration manquante, rediriger vers les préférences - - if (error instanceof AppError) { - if (error.code === ERROR_CODES.KOMGA.MISSING_CONFIG) { - redirect("/settings"); - } - return ( -
- -
- ); - } - - return ( -
- -
- ); - } -} - -export default withPageTiming("HomePage", HomePage); diff --git a/src/app/series/[seriesId]/ClientSeriesPage.tsx b/src/app/series/[seriesId]/ClientSeriesPage.tsx new file mode 100644 index 0000000..f10d6e7 --- /dev/null +++ b/src/app/series/[seriesId]/ClientSeriesPage.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { PaginatedBookGrid } from "@/components/series/PaginatedBookGrid"; +import { SeriesHeader } from "@/components/series/SeriesHeader"; +import { SeriesService } from "@/lib/services/series.service"; +import { ErrorMessage } from "@/components/ui/ErrorMessage"; +import { OptimizedSkeleton } from "@/components/skeletons/OptimizedSkeletons"; +import type { LibraryResponse } from "@/types/library"; +import type { KomgaBook, KomgaSeries } from "@/types/komga"; +import type { UserPreferences } from "@/types/preferences"; +import { ERROR_CODES } from "@/constants/errorCodes"; + +interface ClientSeriesPageProps { + seriesId: string; + currentPage: number; + preferences: UserPreferences; + unreadOnly: boolean; + pageSize?: number; +} + +const DEFAULT_PAGE_SIZE = 20; + +export function ClientSeriesPage({ + seriesId, + currentPage, + preferences, + unreadOnly, + pageSize, +}: ClientSeriesPageProps) { + const [series, setSeries] = useState(null); + const [books, setBooks] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const effectivePageSize = pageSize || preferences.displayMode?.itemsPerPage || DEFAULT_PAGE_SIZE; + + useEffect(() => { + const fetchData = async () => { + setLoading(true); + setError(null); + + try { + const params = new URLSearchParams({ + page: String(currentPage - 1), + size: String(effectivePageSize), + unread: String(unreadOnly), + }); + + const response = await fetch(`/api/komga/series/${seriesId}/books?${params}`); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error?.code || ERROR_CODES.BOOK.PAGES_FETCH_ERROR); + } + + const data = await response.json(); + setSeries(data.series); + setBooks(data.books); + } catch (err) { + console.error("Error fetching series books:", err); + setError(err instanceof Error ? err.message : ERROR_CODES.BOOK.PAGES_FETCH_ERROR); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [seriesId, currentPage, unreadOnly, effectivePageSize]); + + const handleRefresh = async (seriesId: string) => { + try { + await SeriesService.invalidateSeriesBooksCache(seriesId); + await SeriesService.invalidateSeriesCache(seriesId); + + // Recharger les données + const params = new URLSearchParams({ + page: String(currentPage - 1), + size: String(effectivePageSize), + unread: String(unreadOnly), + }); + + const response = await fetch(`/api/komga/series/${seriesId}/books?${params}`); + + if (!response.ok) { + throw new Error("Erreur lors du rafraîchissement de la série"); + } + + const data = await response.json(); + setSeries(data.series); + setBooks(data.books); + + return { success: true }; + } catch (error) { + console.error("Erreur lors du rafraîchissement:", error); + return { success: false, error: "Erreur lors du rafraîchissement de la série" }; + } + }; + + if (loading) { + return ( +
+
+ + +
+
+ {Array.from({ length: effectivePageSize }).map((_, i) => ( + + ))} +
+
+ ); + } + + if (error) { + return ( +
+

Série

+ +
+ ); + } + + if (!series || !books) { + return ( +
+

Série

+ +
+ ); + } + + return ( +
+ + +
+ ); +} + diff --git a/src/app/series/[seriesId]/page.tsx b/src/app/series/[seriesId]/page.tsx index e517818..5c66245 100644 --- a/src/app/series/[seriesId]/page.tsx +++ b/src/app/series/[seriesId]/page.tsx @@ -1,58 +1,13 @@ -import { PaginatedBookGrid } from "@/components/series/PaginatedBookGrid"; -import { SeriesHeader } from "@/components/series/SeriesHeader"; -import { SeriesService } from "@/lib/services/series.service"; import { PreferencesService } from "@/lib/services/preferences.service"; -import { revalidatePath } from "next/cache"; -import { withPageTiming } from "@/lib/hoc/withPageTiming"; -import { ErrorMessage } from "@/components/ui/ErrorMessage"; -import type { LibraryResponse } from "@/types/library"; -import type { KomgaBook, KomgaSeries } from "@/types/komga"; +import { ClientSeriesPage } from "./ClientSeriesPage"; import type { UserPreferences } from "@/types/preferences"; -import { ERROR_CODES } from "@/constants/errorCodes"; -import { AppError } from "@/utils/errors"; interface PageProps { params: { seriesId: string }; searchParams: { page?: string; unread?: string; size?: string }; } -const DEFAULT_PAGE_SIZE = 20; - -async function getSeriesBooks( - seriesId: string, - page: number = 1, - unreadOnly: boolean = false, - size: number = DEFAULT_PAGE_SIZE -) { - try { - const pageIndex = page - 1; - - const [books, series] = await Promise.all([ - SeriesService.getSeriesBooks(seriesId, pageIndex, size, unreadOnly), - SeriesService.getSeries(seriesId) - ]); - - return { data: books, series }; - } catch (error) { - throw error instanceof AppError ? error : new AppError(ERROR_CODES.BOOK.PAGES_FETCH_ERROR); - } -} - -async function refreshSeries(seriesId: string) { - "use server"; - - try { - await SeriesService.invalidateSeriesBooksCache(seriesId); - await SeriesService.invalidateSeriesCache(seriesId); - revalidatePath(`/series/${seriesId}`); - return { success: true }; - } catch (error) { - console.error("Erreur lors du rafraîchissement:", error); - return { success: false, error: "Erreur lors du rafraîchissement de la série" }; - } -} - -async function SeriesPage({ params, searchParams }: PageProps) { +export default async function SeriesPage({ params, searchParams }: PageProps) { const seriesId = (await params).seriesId; const page = (await searchParams).page; const unread = (await searchParams).unread; @@ -63,43 +18,14 @@ async function SeriesPage({ params, searchParams }: PageProps) { // Utiliser le paramètre d'URL s'il existe, sinon utiliser la préférence utilisateur const unreadOnly = unread !== undefined ? unread === "true" : preferences.showOnlyUnread; - // Utiliser le paramètre de pageSize s'il existe, sinon utiliser la préférence utilisateur - const pageSize = size - ? parseInt(size) - : preferences.displayMode?.itemsPerPage || DEFAULT_PAGE_SIZE; - try { - const { data: books, series }: { data: LibraryResponse; series: KomgaSeries } = - await getSeriesBooks(seriesId, currentPage, unreadOnly, pageSize); - - return ( -
- - -
- ); - } catch (error) { - if (error instanceof AppError) { - return ( -
- -
- ); - } - return ( -
-

Série

- -
- ); - } + return ( + + ); } - -export default withPageTiming("SeriesPage", SeriesPage); diff --git a/src/components/home/ClientHomePage.tsx b/src/components/home/ClientHomePage.tsx new file mode 100644 index 0000000..c018c20 --- /dev/null +++ b/src/components/home/ClientHomePage.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { HomeContent } from "./HomeContent"; +import { HomeService } from "@/lib/services/home.service"; +import { ErrorMessage } from "@/components/ui/ErrorMessage"; +import { HomePageSkeleton } from "@/components/skeletons/OptimizedSkeletons"; +import { ERROR_CODES } from "@/constants/errorCodes"; +import type { HomeData } from "@/lib/services/home.service"; + +export function ClientHomePage() { + const router = useRouter(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + setLoading(true); + setError(null); + + try { + const response = await fetch("/api/komga/home"); + + if (!response.ok) { + const errorData = await response.json(); + const errorCode = errorData.error?.code || ERROR_CODES.KOMGA.SERVER_UNREACHABLE; + + // Si la config Komga est manquante, rediriger vers les settings + if (errorCode === ERROR_CODES.KOMGA.MISSING_CONFIG) { + router.push("/settings"); + return; + } + + throw new Error(errorCode); + } + + const homeData = await response.json(); + setData(homeData); + } catch (err) { + console.error("Error fetching home data:", err); + setError(err instanceof Error ? err.message : ERROR_CODES.KOMGA.SERVER_UNREACHABLE); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [router]); + + const handleRefresh = async () => { + try { + await HomeService.invalidateHomeCache(); + + const response = await fetch("/api/komga/home"); + + if (!response.ok) { + throw new Error("Erreur lors du rafraîchissement de la page d'accueil"); + } + + const homeData = await response.json(); + setData(homeData); + + return { success: true }; + } catch (error) { + console.error("Erreur lors du rafraîchissement:", error); + return { success: false, error: "Erreur lors du rafraîchissement de la page d'accueil" }; + } + }; + + if (loading) { + return ; + } + + if (error) { + return ( +
+ +
+ ); + } + + if (!data) { + return ( +
+ +
+ ); + } + + return ; +} +