diff --git a/.env.local.example b/.env.local.example deleted file mode 100644 index 88d321e..0000000 --- a/.env.local.example +++ /dev/null @@ -1,8 +0,0 @@ -# URL de l'application -NEXT_PUBLIC_APP_URL=http://localhost:3000 - -# URL par défaut du serveur Komga (optionnel) -NEXT_PUBLIC_DEFAULT_KOMGA_URL=http://localhost:8080 - -# Version de l'application (depuis package.json) -NEXT_PUBLIC_APP_VERSION=$npm_package_version \ No newline at end of file diff --git a/src/app/api/komga/home/route.ts b/src/app/api/komga/home/route.ts new file mode 100644 index 0000000..338eca2 --- /dev/null +++ b/src/app/api/komga/home/route.ts @@ -0,0 +1,122 @@ +import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { config } from "@/lib/config"; +import { cacheService } from "@/lib/services/cache.service"; + +export async function GET() { + try { + // Récupérer les credentials Komga depuis le cookie + const cookieStore = cookies(); + const configCookie = cookieStore.get("komgaCredentials"); + console.log("API Home - Cookie komgaCredentials:", configCookie?.value); + + if (!configCookie) { + console.log("API Home - Cookie komgaCredentials manquant"); + return NextResponse.json({ error: "Configuration Komga manquante" }, { status: 401 }); + } + + let komgaConfig; + try { + komgaConfig = JSON.parse(atob(configCookie.value)); + console.log("API Home - Config décodée:", { + serverUrl: komgaConfig.serverUrl, + hasCredentials: !!komgaConfig.credentials, + }); + } catch (error) { + console.error("API Home - Erreur de décodage du cookie:", error); + return NextResponse.json({ error: "Configuration Komga invalide" }, { status: 401 }); + } + + if (!komgaConfig.credentials?.username || !komgaConfig.credentials?.password) { + console.log("API Home - Credentials manquants dans la config"); + return NextResponse.json({ error: "Credentials Komga manquants" }, { status: 401 }); + } + + const auth = Buffer.from( + `${komgaConfig.credentials.username}:${komgaConfig.credentials.password}` + ).toString("base64"); + + console.log("API Home - Début des appels API"); + + try { + // Appels API parallèles + const [ongoingResponse, recentlyReadResponse, popularResponse] = await Promise.all([ + // Séries en cours + fetch( + `${komgaConfig.serverUrl}/api/v1/series?read_status=IN_PROGRESS&sort=readDate,desc&page=0&size=20&media_status=READY`, + { + headers: { + Authorization: `Basic ${auth}`, + }, + cache: "no-store", // Désactiver le cache + } + ).catch((error) => { + console.error("API Home - Erreur fetch ongoing:", error); + throw error; + }), + // Derniers livres lus + fetch( + `${komgaConfig.serverUrl}/api/v1/books?read_status=READ&sort=readDate,desc&page=0&size=20`, + { + headers: { + Authorization: `Basic ${auth}`, + }, + cache: "no-store", // Désactiver le cache + } + ).catch((error) => { + console.error("API Home - Erreur fetch recently read:", error); + throw error; + }), + // Séries populaires + fetch( + `${komgaConfig.serverUrl}/api/v1/series?page=0&size=20&sort=metadata.titleSort,asc&media_status=READY`, + { + headers: { + Authorization: `Basic ${auth}`, + }, + cache: "no-store", // Désactiver le cache + } + ).catch((error) => { + console.error("API Home - Erreur fetch popular:", error); + throw error; + }), + ]); + + console.log("API Home - Status des réponses:", { + ongoing: ongoingResponse.status, + recentlyRead: recentlyReadResponse.status, + popular: popularResponse.status, + }); + + // Vérifier les réponses et récupérer les données + const [ongoing, recentlyRead, popular] = await Promise.all([ + ongoingResponse.json(), + recentlyReadResponse.json(), + popularResponse.json(), + ]); + + console.log("API Home - Données récupérées:", { + ongoingCount: ongoing.content?.length || 0, + recentlyReadCount: recentlyRead.content?.length || 0, + popularCount: popular.content?.length || 0, + }); + + // Retourner les données + return NextResponse.json({ + ongoing: ongoing.content || [], + recentlyRead: recentlyRead.content || [], + popular: popular.content || [], + }); + } catch (error) { + console.error("API Home - Erreur lors de la récupération des données:", error); + throw error; + } + } catch (error) { + console.error("API Home - Erreur générale:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des données" }, + { status: 500 } + ); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 063ac6a..7276e10 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,37 +1,80 @@ "use client"; -export default function Home() { - return ( -
-
-

Bienvenue sur Paniels

-

- Votre lecteur Komga moderne pour lire vos BD, mangas et comics préférés. -

-
+import { HomeContent } from "@/components/home/HomeContent"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { KomgaBook, KomgaSeries } from "@/types/komga"; -
-
-

Bibliothèques

-

- Accédez à vos bibliothèques Komga et parcourez vos collections. -

-
- -
-

Collections

-

- Organisez vos lectures en collections thématiques. -

-
- -
-

Lecture

-

- Profitez d'une expérience de lecture fluide et confortable. -

-
-
-
- ); +interface HomeData { + onGoingSeries: KomgaSeries[]; + recentlyRead: KomgaBook[]; + popularSeries: KomgaSeries[]; +} + +export default function HomePage() { + const router = useRouter(); + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchHomeData = async () => { + try { + const response = await fetch("/api/komga/home"); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `Erreur ${response.status}`); + } + + const jsonData = await response.json(); + // Transformer les données pour correspondre à l'interface HomeData + setData({ + onGoingSeries: jsonData.ongoing || [], + recentlyRead: jsonData.recentlyRead || [], + popularSeries: jsonData.popular || [], + }); + } catch (error) { + console.error("Erreur lors de la récupération des données:", error); + setError(error instanceof Error ? error.message : "Une erreur est survenue"); + } finally { + setIsLoading(false); + } + }; + + fetchHomeData(); + }, []); + + if (isLoading) { + return ( +
+
+
+ {[...Array(3)].map((_, i) => ( +
+
+
+ {[...Array(6)].map((_, j) => ( +
+ ))} +
+
+ ))} +
+
+ ); + } + + if (error) { + return ( +
+
+

{error}

+
+
+ ); + } + + if (!data) return null; + + return ; } diff --git a/src/components/home/HeroSection.tsx b/src/components/home/HeroSection.tsx new file mode 100644 index 0000000..bc5aaa6 --- /dev/null +++ b/src/components/home/HeroSection.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { KomgaSeries } from "@/types/komga"; +import Image from "next/image"; +import { useState } from "react"; +import { ImageOff } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface HeroSectionProps { + series: KomgaSeries[]; +} + +export function HeroSection({ series }: HeroSectionProps) { + console.log("HeroSection - Séries reçues:", { + count: series?.length || 0, + firstSeries: series?.[0], + }); + + return ( +
+ {/* Grille de couvertures en arrière-plan */} +
+ {series?.map((series) => ( + + ))} +
+ + {/* Overlay gradient */} +
+ + {/* Contenu */} +
+

+ Bienvenue sur Paniels +

+

+ Votre bibliothèque numérique pour lire vos BD, mangas et comics préférés. +

+
+
+ ); +} + +interface CoverImageProps { + series: KomgaSeries; +} + +function CoverImage({ series }: CoverImageProps) { + const [imageError, setImageError] = useState(false); + + return ( +
+ {!imageError ? ( + {`Couverture setImageError(true)} + /> + ) : ( +
+ +
+ )} +
+ ); +} diff --git a/src/components/home/HomeContent.tsx b/src/components/home/HomeContent.tsx new file mode 100644 index 0000000..044b4fd --- /dev/null +++ b/src/components/home/HomeContent.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { HeroSection } from "./HeroSection"; +import { MediaRow } from "./MediaRow"; +import { KomgaBook, KomgaSeries } from "@/types/komga"; +import { useRouter } from "next/navigation"; + +interface HomeContentProps { + data: { + onGoingSeries: KomgaSeries[]; + recentlyRead: KomgaBook[]; + popularSeries: KomgaSeries[]; + }; +} + +export function HomeContent({ data }: HomeContentProps) { + const router = useRouter(); + + const handleItemClick = (item: KomgaSeries | KomgaBook) => { + // Si c'est une série (a la propriété booksCount), on va vers la page de la série + if ("booksCount" in item) { + router.push(`/series/${item.id}`); + } else { + // Si c'est un livre, on va directement vers la page de lecture + router.push(`/books/${item.id}`); + } + }; + + // Vérification des données pour le debug + console.log("HomeContent - Données reçues:", { + onGoingCount: data.onGoingSeries?.length || 0, + recentlyReadCount: data.recentlyRead?.length || 0, + popularCount: data.popularSeries?.length || 0, + }); + + return ( +
+ {/* Hero Section - Afficher uniquement si nous avons des séries populaires */} + {data.popularSeries && data.popularSeries.length > 0 && ( + + )} + + {/* Sections de contenu */} +
+ {data.onGoingSeries && data.onGoingSeries.length > 0 && ( + + )} + + {data.recentlyRead && data.recentlyRead.length > 0 && ( + + )} + + {data.popularSeries && data.popularSeries.length > 0 && ( + + )} +
+
+ ); +} diff --git a/src/components/home/MediaRow.tsx b/src/components/home/MediaRow.tsx new file mode 100644 index 0000000..eb89c82 --- /dev/null +++ b/src/components/home/MediaRow.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { KomgaBook, KomgaSeries } from "@/types/komga"; +import { ChevronLeft, ChevronRight, ImageOff } from "lucide-react"; +import Image from "next/image"; +import { useRef, useState } from "react"; +import { cn } from "@/lib/utils"; + +interface MediaRowProps { + title: string; + items: (KomgaSeries | KomgaBook)[]; + onItemClick?: (item: KomgaSeries | KomgaBook) => void; +} + +export function MediaRow({ title, items, onItemClick }: MediaRowProps) { + const scrollContainerRef = useRef(null); + const [showLeftArrow, setShowLeftArrow] = useState(false); + const [showRightArrow, setShowRightArrow] = useState(true); + + const handleScroll = () => { + if (!scrollContainerRef.current) return; + + const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current; + setShowLeftArrow(scrollLeft > 0); + setShowRightArrow(scrollLeft < scrollWidth - clientWidth - 10); + }; + + const scroll = (direction: "left" | "right") => { + if (!scrollContainerRef.current) return; + + const scrollAmount = direction === "left" ? -400 : 400; + scrollContainerRef.current.scrollBy({ left: scrollAmount, behavior: "smooth" }); + }; + + if (!items.length) return null; + + return ( +
+

{title}

+
+ {/* Bouton de défilement gauche */} + {showLeftArrow && ( + + )} + + {/* Conteneur défilant */} +
+ {items.map((item) => ( + onItemClick?.(item)} /> + ))} +
+ + {/* Bouton de défilement droit */} + {showRightArrow && ( + + )} +
+
+ ); +} + +interface MediaCardProps { + item: KomgaSeries | KomgaBook; + onClick?: () => void; +} + +function MediaCard({ item, onClick }: MediaCardProps) { + const [imageError, setImageError] = useState(false); + + // Déterminer si c'est une série ou un livre + const isSeries = "booksCount" in item; + const title = isSeries + ? item.metadata.title + : item.metadata.title || `Tome ${item.metadata.number}`; + + return ( + + ); +} diff --git a/src/lib/config.ts b/src/lib/config.ts index 8357982..c4470d2 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,3 +1 @@ -export const config = { - serverUrl: process.env.NEXT_PUBLIC_KOMGA_URL || "http://localhost:8080", -}; +export const config = {};