From 0483fee8cdf07fec660350c4869ddc1a86c31746 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Wed, 12 Feb 2025 13:09:31 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20Am=C3=A9lioration=20de=20la=20page?= =?UTF-8?q?=20d'accueil=20et=20du=20cache=20-=20Suppression=20du=20cache.s?= =?UTF-8?q?ervice.ts=20redondant=20-=20Mise=20=C3=A0=20jour=20de=20l'ordre?= =?UTF-8?q?=20des=20sections=20(ongoing,=20onDeck,=20recentlyRead)=20-=20C?= =?UTF-8?q?orrection=20des=20types=20et=20interfaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devbook.md | 53 ++-- src/app/api/komga/cache/clear/route.ts | 15 ++ src/app/api/komga/test/route.ts | 7 +- src/app/page.tsx | 9 +- src/app/settings/page.tsx | 305 +++++++++++++---------- src/components/home/HomeContent.tsx | 31 +-- src/lib/services/cache.service.ts | 151 ----------- src/lib/services/home.service.ts | 14 +- src/lib/services/server-cache.service.ts | 23 +- src/lib/services/test.service.ts | 30 +++ src/types/auth.ts | 4 +- 11 files changed, 306 insertions(+), 336 deletions(-) create mode 100644 src/app/api/komga/cache/clear/route.ts delete mode 100644 src/lib/services/cache.service.ts create mode 100644 src/lib/services/test.service.ts diff --git a/devbook.md b/devbook.md index 1ab83cb..42865f9 100644 --- a/devbook.md +++ b/devbook.md @@ -1,8 +1,8 @@ -# Plan de développement - Paniels (Komga Reader) +# 📚 Paniels - Devbook -## 🎯 Objectif +## 🎯 Objectifs -Créer une application web moderne avec Next.js permettant de lire des fichiers CBZ, CBR, EPUB et PDF via un serveur Komga. +Application web moderne pour la lecture de BD/mangas/comics via un serveur Komga. ## 📋 Fonctionnalités principales @@ -169,20 +169,39 @@ Créer une application web moderne avec Next.js permettant de lire des fichiers - [x] Service d'API - [x] Client HTTP avec fetch natif - [x] Gestion des tokens Basic Auth - - [x] Cache des réponses - - [x] Cache en mémoire côté serveur - - [x] TTL configurable (5 minutes par défaut) - - [x] Cache par route et paramètres - - [x] Endpoints - - [x] Authentication - - [x] Bibliothèques - - [x] Séries - - [x] Livres - - [x] Pages -- [x] Gestion des erreurs - - [x] Retry automatique - - [x] Feedback utilisateur - - [x] Messages d'erreur détaillés + - [x] Cache des requêtes + - [x] Gestion des erreurs + - [x] Typage des réponses +- [x] Endpoints + - [x] Authentification + - [x] Collections + - [x] Séries + - [x] Tomes + - [x] Progression de lecture + - [x] Images et miniatures + +## 🚀 Prochaines étapes + +- [ ] Amélioration de l'UX + - [ ] Animations de transition + - [ ] Retour haptique + - [ ] Messages de confirmation + - [ ] Tooltips d'aide +- [ ] Fonctionnalités avancées + - [ ] Recherche globale + - [ ] Filtres avancés + - [ ] Tri personnalisé + - [ ] Vue liste/grille configurable +- [ ] Performance + - [ ] Optimisation des images + - [ ] Lazy loading amélioré + - [ ] Prefetching intelligent + - [ ] Cache optimisé +- [ ] Accessibilité + - [ ] Navigation au clavier + - [ ] Support lecteur d'écran + - [ ] Contraste et lisibilité + - [ ] ARIA labels ## 🎨 UI/UX diff --git a/src/app/api/komga/cache/clear/route.ts b/src/app/api/komga/cache/clear/route.ts new file mode 100644 index 0000000..ebf4239 --- /dev/null +++ b/src/app/api/komga/cache/clear/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; +import { serverCacheService } from "@/lib/services/server-cache.service"; + +export async function POST() { + try { + serverCacheService.clear(); + return NextResponse.json({ message: "Cache serveur supprimé avec succès" }); + } catch (error) { + console.error("Erreur lors de la suppression du cache serveur:", error); + return NextResponse.json( + { error: "Erreur lors de la suppression du cache serveur" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/komga/test/route.ts b/src/app/api/komga/test/route.ts index 13f39a7..c586b4b 100644 --- a/src/app/api/komga/test/route.ts +++ b/src/app/api/komga/test/route.ts @@ -11,8 +11,11 @@ export async function POST(request: Request) { credentials: { username, password }, }; - const result = await TestService.testConnection(config); - return NextResponse.json(result); + const { libraries } = await TestService.testConnection(config); + return NextResponse.json({ + message: "Connexion réussie", + librariesCount: libraries.length, + }); } catch (error) { console.error("Erreur lors du test de connexion:", error); return NextResponse.json( diff --git a/src/app/page.tsx b/src/app/page.tsx index 7276e10..19ff8c3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,9 +6,9 @@ import { useRouter } from "next/navigation"; import { KomgaBook, KomgaSeries } from "@/types/komga"; interface HomeData { - onGoingSeries: KomgaSeries[]; + ongoing: KomgaSeries[]; recentlyRead: KomgaBook[]; - popularSeries: KomgaSeries[]; + onDeck: KomgaBook[]; } export default function HomePage() { @@ -29,9 +29,9 @@ export default function HomePage() { const jsonData = await response.json(); // Transformer les données pour correspondre à l'interface HomeData setData({ - onGoingSeries: jsonData.ongoing || [], + ongoing: jsonData.ongoing || [], recentlyRead: jsonData.recentlyRead || [], - popularSeries: jsonData.popular || [], + onDeck: jsonData.onDeck || [], }); } catch (error) { console.error("Erreur lors de la récupération des données:", error); @@ -75,6 +75,7 @@ export default function HomePage() { } if (!data) return null; + console.log("PAGE", data); return ; } diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index fb2d6bd..cf767a7 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,16 +1,22 @@ "use client"; import { useState, useEffect, useRef } from "react"; +import { Loader2, Network, Trash2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { storageService } from "@/lib/services/storage.service"; import { AuthError } from "@/types/auth"; +interface ErrorMessage { + message: string; +} + export default function SettingsPage() { const router = useRouter(); const formRef = useRef(null); const [isLoading, setIsLoading] = useState(false); + const [isCacheClearing, setIsCacheClearing] = useState(false); const [error, setError] = useState(null); - const [success, setSuccess] = useState(false); + const [success, setSuccess] = useState(null); const [config, setConfig] = useState({ serverUrl: "", username: "", @@ -29,17 +35,38 @@ export default function SettingsPage() { } }, []); - const handleTest = async () => { - if (!formRef.current) return; + const handleClearCache = async () => { + setIsCacheClearing(true); + setError(null); + setSuccess(null); + try { + const response = await fetch("/api/komga/cache/clear", { + method: "POST", + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "Erreur lors de la suppression du cache"); + } + + setSuccess("Cache serveur supprimé avec succès"); + router.refresh(); // Rafraîchir la page pour recharger les données + } catch (error) { + console.error("Erreur:", error); + setError({ + code: "CACHE_CLEAR_ERROR", + message: error instanceof Error ? error.message : "Une erreur est survenue", + }); + } finally { + setIsCacheClearing(false); + } + }; + + const handleTest = async () => { setIsLoading(true); setError(null); - setSuccess(false); - - const formData = new FormData(formRef.current); - const serverUrl = formData.get("serverUrl") as string; - const username = formData.get("username") as string; - const password = formData.get("password") as string; + setSuccess(null); try { const response = await fetch("/api/komga/test", { @@ -48,28 +75,23 @@ export default function SettingsPage() { "Content-Type": "application/json", }, body: JSON.stringify({ - serverUrl: serverUrl.trim(), - username, - password, + serverUrl: config.serverUrl, + username: config.username, + password: config.password, }), }); if (!response.ok) { const data = await response.json(); - throw new Error( - `${data.error}${ - data.details ? `\n\nDétails: ${JSON.stringify(data.details, null, 2)}` : "" - }` - ); + throw new Error(data.error || "Erreur lors du test de connexion"); } - setSuccess(true); + setSuccess("Connexion réussie"); } catch (error) { - console.error("Erreur de test:", error); + console.error("Erreur:", error); setError({ - code: "INVALID_SERVER_URL", - message: - error instanceof Error ? error.message : "Impossible de se connecter au serveur Komga", + code: "TEST_CONNECTION_ERROR", + message: error instanceof Error ? error.message : "Une erreur est survenue", }); } finally { setIsLoading(false); @@ -78,7 +100,7 @@ export default function SettingsPage() { const handleSave = (event: React.FormEvent) => { event.preventDefault(); - setSuccess(false); + setSuccess(null); const formData = new FormData(event.currentTarget); const serverUrl = formData.get("serverUrl") as string; @@ -100,120 +122,151 @@ export default function SettingsPage() { ); setConfig(newConfig); - setSuccess(true); + setSuccess("Configuration sauvegardée avec succès"); }; return ( -
+
+
+

Préférences

+
+
-
-

Préférences

-

Configurez votre connexion au serveur Komga

-
- -
-

Configuration du serveur Komga

-

- Ces identifiants sont différents de ceux utilisés pour vous connecter à l'application. - Il s'agit des identifiants de votre serveur Komga. -

- -
+ {/* Section Configuration Komga */} +
+
+

Configuration Komga

+

+ Configurez les informations de connexion à votre serveur Komga. Ces informations sont + nécessaires pour accéder à votre bibliothèque. +

+
+
+ {/* Formulaire de configuration */}
-
-
- -
- - -

- L'identifiant de votre compte sur le serveur Komga -

-
- -
- - -

- Le mot de passe de votre compte sur le serveur Komga -

-
+ Sauvegarder + +
- {error && ( -
- {error.message} + {/* Actions */} +
+
+

Actions

+

+ Outils de gestion de la connexion et du cache +

+
+
+ +

+ Vérifie que la connexion au serveur est fonctionnelle avec les paramètres + actuels +

+
+
+ +

+ Vide le cache du serveur pour forcer le rechargement des données. Utile en cas + de problème d'affichage. +

+
+
- )} - - {success && ( -
- {isLoading ? "Test de connexion réussi" : "Configuration sauvegardée"} -
- )} - -
- - -
- +
+ + {/* Messages de succès/erreur */} + {error && ( +
+

{error.message}

+
+ )} + {success && ( +
+

{success}

+
+ )}
); diff --git a/src/components/home/HomeContent.tsx b/src/components/home/HomeContent.tsx index 044b4fd..4e05318 100644 --- a/src/components/home/HomeContent.tsx +++ b/src/components/home/HomeContent.tsx @@ -7,15 +7,14 @@ import { useRouter } from "next/navigation"; interface HomeContentProps { data: { - onGoingSeries: KomgaSeries[]; + ongoing: KomgaSeries[]; recentlyRead: KomgaBook[]; - popularSeries: KomgaSeries[]; + onDeck: KomgaBook[]; }; } 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) { @@ -28,28 +27,30 @@ export function HomeContent({ data }: HomeContentProps) { // Vérification des données pour le debug console.log("HomeContent - Données reçues:", { - onGoingCount: data.onGoingSeries?.length || 0, + ongoingCount: data.ongoing?.length || 0, recentlyReadCount: data.recentlyRead?.length || 0, - popularCount: data.popularSeries?.length || 0, + onDeckCount: data.onDeck?.length || 0, }); return (
- {/* Hero Section - Afficher uniquement si nous avons des séries populaires */} - {data.popularSeries && data.popularSeries.length > 0 && ( - - )} + {/* Hero Section - Afficher uniquement si nous avons des séries en cours */} + {data.ongoing && data.ongoing.length > 0 && } {/* Sections de contenu */}
- {data.onGoingSeries && data.onGoingSeries.length > 0 && ( + {data.ongoing && data.ongoing.length > 0 && ( )} + {data.onDeck && data.onDeck.length > 0 && ( + + )} + {data.recentlyRead && data.recentlyRead.length > 0 && ( )} - - {data.popularSeries && data.popularSeries.length > 0 && ( - - )}
); diff --git a/src/lib/services/cache.service.ts b/src/lib/services/cache.service.ts deleted file mode 100644 index 789fafc..0000000 --- a/src/lib/services/cache.service.ts +++ /dev/null @@ -1,151 +0,0 @@ -class CacheService { - private static instance: CacheService; - private cacheName = "komga-cache-v1"; - - private static readonly fiveMinutes = 5 * 60; - private static readonly tenMinutes = 10 * 60; - private static readonly twentyFourHours = 24 * 60 * 60; - private static readonly oneMinute = 1 * 60; - private static readonly noCache = 0; - - // Configuration des temps de cache en secondes - private static readonly TTL = { - DEFAULT: CacheService.fiveMinutes, // 5 minutes - HOME: CacheService.fiveMinutes, // 5 minutes - LIBRARIES: CacheService.tenMinutes, // 10 minutes - SERIES: CacheService.fiveMinutes, // 5 minutes - BOOKS: CacheService.fiveMinutes, // 5 minutes - IMAGES: CacheService.twentyFourHours, // 24 heures - READ_PROGRESS: CacheService.oneMinute, // 1 minute - }; - // private static readonly TTL = { - // DEFAULT: CacheService.noCache, // 5 minutes - // HOME: CacheService.noCache, // 5 minutes - // LIBRARIES: CacheService.noCache, // 10 minutes - // SERIES: CacheService.noCache, // 5 minutes - // BOOKS: CacheService.noCache, // 5 minutes - // IMAGES: CacheService.noCache, // 24 heures - // READ_PROGRESS: CacheService.noCache, // 1 minute - // }; - - private constructor() {} - - public static getInstance(): CacheService { - if (!CacheService.instance) { - CacheService.instance = new CacheService(); - } - return CacheService.instance; - } - - /** - * Retourne le TTL pour un type de données spécifique - */ - public getTTL(type: keyof typeof CacheService.TTL): number { - return CacheService.TTL[type]; - } - - /** - * Met en cache une réponse avec une durée de vie - */ - async set( - key: string, - response: Response, - ttl: number = CacheService.TTL.DEFAULT - ): Promise { - if (typeof window === "undefined") return; - - try { - const cache = await caches.open(this.cacheName); - const headers = new Headers(response.headers); - headers.append("x-cache-timestamp", Date.now().toString()); - headers.append("x-cache-ttl", ttl.toString()); - - const cachedResponse = new Response(await response.clone().blob(), { - status: response.status, - statusText: response.statusText, - headers, - }); - - await cache.put(key, cachedResponse); - } catch (error) { - console.error("Erreur lors de la mise en cache:", error); - } - } - - /** - * Récupère une réponse du cache si elle est valide - */ - async get(key: string): Promise { - if (typeof window === "undefined") return null; - - try { - const cache = await caches.open(this.cacheName); - const response = await cache.match(key); - - if (!response) return null; - - // Vérifier si la réponse est expirée - const timestamp = parseInt(response.headers.get("x-cache-timestamp") || "0"); - const ttl = parseInt(response.headers.get("x-cache-ttl") || "0"); - const now = Date.now(); - - if (now - timestamp > ttl * 1000) { - await cache.delete(key); - return null; - } - - return response; - } catch (error) { - console.error("Erreur lors de la lecture du cache:", error); - return null; - } - } - - /** - * Supprime une entrée du cache - */ - async delete(key: string): Promise { - if (typeof window === "undefined") return; - - try { - const cache = await caches.open(this.cacheName); - await cache.delete(key); - } catch (error) { - console.error("Erreur lors de la suppression du cache:", error); - } - } - - /** - * Vide le cache - */ - async clear(): Promise { - if (typeof window === "undefined") return; - - try { - await caches.delete(this.cacheName); - } catch (error) { - console.error("Erreur lors du nettoyage du cache:", error); - } - } - - /** - * Récupère une réponse du cache ou fait l'appel API si nécessaire - */ - async getOrFetch( - key: string, - fetcher: () => Promise, - type: keyof typeof CacheService.TTL = "DEFAULT" - ): Promise { - const cachedResponse = await this.get(key); - if (cachedResponse) { - return cachedResponse; - } - - const response = await fetcher(); - const clonedResponse = response.clone(); - await this.set(key, clonedResponse, CacheService.TTL[type]); - return response; - } -} - -export const cacheService = CacheService.getInstance(); diff --git a/src/lib/services/home.service.ts b/src/lib/services/home.service.ts index 5c108e6..9fe384e 100644 --- a/src/lib/services/home.service.ts +++ b/src/lib/services/home.service.ts @@ -5,7 +5,7 @@ import { LibraryResponse } from "@/types/library"; interface HomeData { ongoing: KomgaSeries[]; recentlyRead: KomgaBook[]; - popular: KomgaSeries[]; + onDeck: KomgaBook[]; } export class HomeService extends BaseApiService { @@ -34,27 +34,25 @@ export class HomeService extends BaseApiService { media_status: "READY", }); - const popularUrl = this.buildUrl(config, "series", { + const onDeckUrl = this.buildUrl(config, "books/ondeck", { page: "0", size: "20", - sort: "metadata.titleSort,asc", - media_status: "READY", }); // Appels API parallèles avec fetchFromApi - const [ongoing, recentlyRead, popular] = await Promise.all([ + const [ongoing, recentlyRead, onDeck] = await Promise.all([ this.fetchFromApi>(ongoingUrl, headers), this.fetchFromApi>(recentlyReadUrl, headers), - this.fetchFromApi>(popularUrl, headers), + this.fetchFromApi>(onDeckUrl, headers), ]); return { ongoing: ongoing.content || [], recentlyRead: recentlyRead.content || [], - popular: popular.content || [], + onDeck: onDeck.content || [], }; }, - "HOME" // Type de cache + "HOME" ); } catch (error) { return this.handleError(error, "Impossible de récupérer les données de la page d'accueil"); diff --git a/src/lib/services/server-cache.service.ts b/src/lib/services/server-cache.service.ts index a35fb52..6e7d218 100644 --- a/src/lib/services/server-cache.service.ts +++ b/src/lib/services/server-cache.service.ts @@ -8,15 +8,21 @@ class ServerCacheService { private static instance: ServerCacheService; private cache: Map = new Map(); - // Configuration des temps de cache en secondes (identique à CacheService) + private static readonly fiveMinutes = 5 * 60; + private static readonly tenMinutes = 10 * 60; + private static readonly twentyFourHours = 24 * 60 * 60; + private static readonly oneMinute = 1 * 60; + private static readonly noCache = 0; + + // Configuration des temps de cache en secondes private static readonly TTL = { - DEFAULT: 5 * 60, // 5 minutes - HOME: 5 * 60, // 5 minutes - LIBRARIES: 10 * 60, // 10 minutes - SERIES: 5 * 60, // 5 minutes - BOOKS: 5 * 60, // 5 minutes - IMAGES: 24 * 60 * 60, // 24 heures - READ_PROGRESS: 1 * 60, // 1 minute + DEFAULT: ServerCacheService.fiveMinutes, // 5 minutes + HOME: ServerCacheService.oneMinute, // 1 minute + LIBRARIES: ServerCacheService.tenMinutes, // 10 minutes + SERIES: ServerCacheService.fiveMinutes, // 5 minutes + BOOKS: ServerCacheService.fiveMinutes, // 5 minutes + IMAGES: ServerCacheService.twentyFourHours, // 24 heures + READ_PROGRESS: ServerCacheService.oneMinute, // 1 minute }; private constructor() { @@ -89,6 +95,7 @@ class ServerCacheService { const cached = this.cache.get(key); if (cached && cached.expiry > now) { + console.log("Cache hit for key:", key); return cached.data as T; } diff --git a/src/lib/services/test.service.ts b/src/lib/services/test.service.ts new file mode 100644 index 0000000..3aabf1a --- /dev/null +++ b/src/lib/services/test.service.ts @@ -0,0 +1,30 @@ +import { BaseApiService } from "./base-api.service"; +import { AuthConfig } from "@/types/auth"; +import { KomgaLibrary } from "@/types/komga"; + +export class TestService extends BaseApiService { + static async testConnection(config: AuthConfig): Promise<{ libraries: KomgaLibrary[] }> { + try { + const url = this.buildUrl(config, "libraries"); + const headers = this.getAuthHeaders(config); + + const response = await fetch(url, { headers }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || "Erreur lors du test de connexion"); + } + + const libraries = await response.json(); + return { libraries }; + } catch (error) { + console.error("Erreur lors du test de connexion:", error); + if (error instanceof Error && error.message.includes("fetch")) { + throw new Error( + "Impossible de se connecter au serveur. Vérifiez l'URL et que le serveur est accessible." + ); + } + throw error instanceof Error ? error : new Error("Erreur lors du test de connexion"); + } + } +} diff --git a/src/types/auth.ts b/src/types/auth.ts index 12df4f7..f332763 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -24,4 +24,6 @@ export type AuthErrorCode = | "INVALID_SERVER_URL" | "SERVER_UNREACHABLE" | "NETWORK_ERROR" - | "UNKNOWN_ERROR"; + | "UNKNOWN_ERROR" + | "CACHE_CLEAR_ERROR" + | "TEST_CONNECTION_ERROR";