refacto(db): favorites on db

This commit is contained in:
Julien Froidefond
2025-02-14 15:50:06 +01:00
parent b71ccd6b0e
commit 313cd60e74
9 changed files with 224 additions and 44 deletions

2
.env
View File

@@ -1,7 +1,7 @@
# MongoDB # MongoDB
MONGO_USER=admin MONGO_USER=admin
MONGO_PASSWORD=password MONGO_PASSWORD=password
MONGODB_URI=mongodb://admin:password@localhost:27017/stripstream?authSource=admin MONGODB_URI=mongodb://admin:password@mongodb.paniels.orb.local:27017/stripstream?authSource=admin
# Komga # Komga
NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com

View File

@@ -0,0 +1,37 @@
import { NextResponse } from "next/server";
import { FavoriteService } from "@/lib/services/favorite.service";
export async function GET() {
try {
const favoriteIds = await FavoriteService.getAllFavoriteIds();
return NextResponse.json(favoriteIds);
} catch (error) {
console.error("Erreur lors de la récupération des favoris:", error);
return NextResponse.json(
{ error: "Erreur lors de la récupération des favoris" },
{ status: 500 }
);
}
}
export async function POST(request: Request) {
try {
const { seriesId } = await request.json();
await FavoriteService.addToFavorites(seriesId);
return NextResponse.json({ message: "Favori ajouté avec succès" });
} catch (error) {
console.error("Erreur lors de l'ajout du favori:", error);
return NextResponse.json({ error: "Erreur lors de l'ajout du favori" }, { status: 500 });
}
}
export async function DELETE(request: Request) {
try {
const { seriesId } = await request.json();
await FavoriteService.removeFromFavorites(seriesId);
return NextResponse.json({ message: "Favori supprimé avec succès" });
} catch (error) {
console.error("Erreur lors de la suppression du favori:", error);
return NextResponse.json({ error: "Erreur lors de la suppression du favori" }, { status: 500 });
}
}

View File

@@ -11,10 +11,10 @@ interface HeroSectionProps {
} }
export function HeroSection({ series }: HeroSectionProps) { export function HeroSection({ series }: HeroSectionProps) {
console.log("HeroSection - Séries reçues:", { // console.log("HeroSection - Séries reçues:", {
count: series?.length || 0, // count: series?.length || 0,
firstSeries: series?.[0], // firstSeries: series?.[0],
}); // });
return ( return (
<div className="relative h-[500px] -mx-4 sm:-mx-8 lg:-mx-14 overflow-hidden"> <div className="relative h-[500px] -mx-4 sm:-mx-8 lg:-mx-14 overflow-hidden">

View File

@@ -26,11 +26,11 @@ export function HomeContent({ data }: HomeContentProps) {
}; };
// Vérification des données pour le debug // Vérification des données pour le debug
console.log("HomeContent - Données reçues:", { // console.log("HomeContent - Données reçues:", {
ongoingCount: data.ongoing?.length || 0, // ongoingCount: data.ongoing?.length || 0,
recentlyReadCount: data.recentlyRead?.length || 0, // recentlyReadCount: data.recentlyRead?.length || 0,
onDeckCount: data.onDeck?.length || 0, // onDeckCount: data.onDeck?.length || 0,
}); // });
return ( return (
<main className="container mx-auto px-4 py-8 space-y-12"> <main className="container mx-auto px-4 py-8 space-y-12">

View File

@@ -1,3 +1,5 @@
"use client";
import { BookOpen, Home, Library, Settings, LogOut, RefreshCw, Star } from "lucide-react"; import { BookOpen, Home, Library, Settings, LogOut, RefreshCw, Star } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
@@ -6,7 +8,6 @@ import { authService } from "@/lib/services/auth.service";
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback } from "react";
import { KomgaLibrary, KomgaSeries } from "@/types/komga"; import { KomgaLibrary, KomgaSeries } from "@/types/komga";
import { storageService } from "@/lib/services/storage.service"; import { storageService } from "@/lib/services/storage.service";
import { FavoriteService } from "@/lib/services/favorite.service";
interface SidebarProps { interface SidebarProps {
isOpen: boolean; isOpen: boolean;
@@ -43,13 +44,20 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
const fetchFavorites = useCallback(async () => { const fetchFavorites = useCallback(async () => {
setIsLoadingFavorites(true); setIsLoadingFavorites(true);
try { try {
const favoriteIds = FavoriteService.getAllFavoriteIds(); // Récupérer les IDs des favoris depuis l'API
const favoritesResponse = await fetch("/api/komga/favorites");
if (!favoritesResponse.ok) {
throw new Error("Erreur lors de la récupération des favoris");
}
const favoriteIds = await favoritesResponse.json();
if (favoriteIds.length === 0) { if (favoriteIds.length === 0) {
setFavorites([]); setFavorites([]);
return; return;
} }
const promises = favoriteIds.map(async (id) => { // Récupérer les détails des séries pour chaque ID
const promises = favoriteIds.map(async (id: string) => {
const response = await fetch(`/api/komga/series/${id}`); const response = await fetch(`/api/komga/series/${id}`);
if (!response.ok) return null; if (!response.ok) return null;
return response.json(); return response.json();
@@ -69,7 +77,7 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
useEffect(() => { useEffect(() => {
fetchLibraries(); fetchLibraries();
fetchFavorites(); fetchFavorites();
}, []); // Suppression de la dépendance pathname }, [fetchLibraries, fetchFavorites]);
// Mettre à jour les favoris quand ils changent // Mettre à jour les favoris quand ils changent
useEffect(() => { useEffect(() => {
@@ -77,18 +85,10 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
fetchFavorites(); fetchFavorites();
}; };
// Écouter les changements de favoris dans la même fenêtre
window.addEventListener("favoritesChanged", handleFavoritesChange); window.addEventListener("favoritesChanged", handleFavoritesChange);
// Écouter les changements de favoris dans d'autres fenêtres
window.addEventListener("storage", (e) => {
if (e.key === "stripstream_favorites") {
fetchFavorites();
}
});
return () => { return () => {
window.removeEventListener("favoritesChanged", handleFavoritesChange); window.removeEventListener("favoritesChanged", handleFavoritesChange);
window.removeEventListener("storage", handleFavoritesChange);
}; };
}, [fetchFavorites]); }, [fetchFavorites]);

View File

@@ -0,0 +1,26 @@
import { FavoriteService } from "@/lib/services/favorite.service";
import { LibraryService } from "@/lib/services/library.service";
import { ClientSidebar } from "./ClientSidebar";
export async function SidebarWrapper() {
// Récupérer les favoris depuis le serveur
const favoriteIds = await FavoriteService.getAllFavoriteIds();
// Récupérer les détails des séries favorites
const favoritesPromises = favoriteIds.map(async (id) => {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/series/${id}`, {
headers: {
Accept: "application/json",
},
});
if (!response.ok) return null;
return response.json();
});
// Récupérer les bibliothèques
const libraries = await LibraryService.getLibraries();
const favorites = (await Promise.all(favoritesPromises)).filter(Boolean);
return { favorites, libraries };
}

View File

@@ -1,11 +1,9 @@
"use client"; "use client";
import Image from "next/image"; import Image from "next/image";
import { ImageOff, Book, BookOpen, BookMarked } from "lucide-react"; import { ImageOff, Book, BookOpen, BookMarked, Star, StarOff } from "lucide-react";
import { KomgaSeries } from "@/types/komga"; import { KomgaSeries } from "@/types/komga";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { FavoriteService } from "@/lib/services/favorite.service";
import { Star, StarOff } from "lucide-react";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -56,13 +54,27 @@ export const SeriesHeader = ({ series, onSeriesUpdate }: SeriesHeaderProps) => {
const [readingStatus, setReadingStatus] = useState<ReadingStatusInfo>( const [readingStatus, setReadingStatus] = useState<ReadingStatusInfo>(
getReadingStatusInfo(series) getReadingStatusInfo(series)
); );
const [isFavorite, setIsFavorite] = useState(FavoriteService.isFavorite(series.id)); const [isFavorite, setIsFavorite] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
// Vérifier si la série est dans les favoris au chargement
useEffect(() => { useEffect(() => {
const checkFavorite = async () => {
try {
const response = await fetch("/api/komga/favorites");
if (response.ok) {
const favoriteIds = await response.json();
setIsFavorite(favoriteIds.includes(series.id));
}
} catch (error) {
console.error("Erreur lors de la vérification des favoris:", error);
}
};
checkFavorite();
setMounted(true); setMounted(true);
}, []); }, [series.id]);
useEffect(() => { useEffect(() => {
setReadingStatus(getReadingStatusInfo(series)); setReadingStatus(getReadingStatusInfo(series));
@@ -82,18 +94,29 @@ export const SeriesHeader = ({ series, onSeriesUpdate }: SeriesHeaderProps) => {
} }
}, [series.metadata.language]); }, [series.metadata.language]);
const handleToggleFavorite = () => { const handleToggleFavorite = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
if (isFavorite) { const response = await fetch("/api/komga/favorites", {
FavoriteService.removeFromFavorites(series.id); method: isFavorite ? "DELETE" : "POST",
} else { headers: {
FavoriteService.addToFavorites(series.id); "Content-Type": "application/json",
},
body: JSON.stringify({ seriesId: series.id }),
});
if (!response.ok) {
throw new Error("Erreur lors de la modification des favoris");
} }
setIsFavorite(!isFavorite); setIsFavorite(!isFavorite);
if (onSeriesUpdate) { if (onSeriesUpdate) {
onSeriesUpdate({ ...series, favorite: !isFavorite }); onSeriesUpdate({ ...series, favorite: !isFavorite });
} }
// Dispatch l'événement pour notifier les autres composants
window.dispatchEvent(new Event("favoritesChanged"));
toast({ toast({
title: isFavorite ? "Retiré des favoris" : "Ajouté aux favoris", title: isFavorite ? "Retiré des favoris" : "Ajouté aux favoris",
variant: "default", variant: "default",

View File

@@ -0,0 +1,23 @@
import mongoose from "mongoose";
const favoriteSchema = new mongoose.Schema(
{
userId: {
type: String,
required: true,
index: true,
},
seriesId: {
type: String,
required: true,
},
},
{
timestamps: true,
}
);
// Index composé pour s'assurer qu'un utilisateur ne peut pas avoir deux fois le même favori
favoriteSchema.index({ userId: 1, seriesId: 1 }, { unique: true });
export const FavoriteModel = mongoose.models.Favorite || mongoose.model("Favorite", favoriteSchema);

View File

@@ -1,40 +1,111 @@
import { storageService } from "./storage.service"; import { cookies } from "next/headers";
import connectDB from "@/lib/mongodb";
import { FavoriteModel } from "@/lib/models/favorite.model";
interface User {
id: string;
email: string;
}
export class FavoriteService { export class FavoriteService {
private static readonly FAVORITES_CHANGE_EVENT = "favoritesChanged"; private static readonly FAVORITES_CHANGE_EVENT = "favoritesChanged";
private static dispatchFavoritesChanged() { private static dispatchFavoritesChanged() {
// Dispatch l'événement pour notifier les changements // Dispatch l'événement pour notifier les changements
if (typeof window !== "undefined") {
window.dispatchEvent(new Event(FavoriteService.FAVORITES_CHANGE_EVENT)); window.dispatchEvent(new Event(FavoriteService.FAVORITES_CHANGE_EVENT));
} }
}
private static async getCurrentUser(): Promise<User> {
const userCookie = cookies().get("stripUser");
if (!userCookie) {
throw new Error("Utilisateur non authentifié");
}
try {
return JSON.parse(atob(userCookie.value));
} catch (error) {
console.error("Erreur lors de la récupération de l'utilisateur depuis le cookie:", error);
throw new Error("Utilisateur non authentifié");
}
}
/** /**
* Vérifie si une série est dans les favoris * Vérifie si une série est dans les favoris
*/ */
static isFavorite(seriesId: string): boolean { static async isFavorite(seriesId: string): Promise<boolean> {
return storageService.isFavorite(seriesId); try {
const user = await this.getCurrentUser();
await connectDB();
const favorite = await FavoriteModel.findOne({
userId: user.id,
seriesId: seriesId,
});
return !!favorite;
} catch (error) {
console.error("Erreur lors de la vérification du favori:", error);
return false;
}
} }
/** /**
* Ajoute une série aux favoris * Ajoute une série aux favoris
*/ */
static addToFavorites(seriesId: string): void { static async addToFavorites(seriesId: string): Promise<void> {
storageService.addFavorite(seriesId); try {
const user = await this.getCurrentUser();
await connectDB();
await FavoriteModel.findOneAndUpdate(
{ userId: user.id, seriesId },
{ userId: user.id, seriesId },
{ upsert: true }
);
this.dispatchFavoritesChanged(); this.dispatchFavoritesChanged();
} catch (error) {
console.error("Erreur lors de l'ajout aux favoris:", error);
throw new Error("Erreur lors de l'ajout aux favoris");
}
} }
/** /**
* Retire une série des favoris * Retire une série des favoris
*/ */
static removeFromFavorites(seriesId: string): void { static async removeFromFavorites(seriesId: string): Promise<void> {
storageService.removeFavorite(seriesId); try {
const user = await this.getCurrentUser();
await connectDB();
await FavoriteModel.findOneAndDelete({
userId: user.id,
seriesId,
});
this.dispatchFavoritesChanged(); this.dispatchFavoritesChanged();
} catch (error) {
console.error("Erreur lors de la suppression des favoris:", error);
throw new Error("Erreur lors de la suppression des favoris");
}
} }
/** /**
* Récupère tous les IDs des séries favorites * Récupère tous les IDs des séries favorites
*/ */
static getAllFavoriteIds(): string[] { static async getAllFavoriteIds(): Promise<string[]> {
return storageService.getFavorites(); try {
const user = await this.getCurrentUser();
await connectDB();
const favorites = await FavoriteModel.find({ userId: user.id });
return favorites.map((favorite) => favorite.seriesId);
} catch (error) {
console.error("Erreur lors de la récupération des favoris:", error);
return [];
}
} }
} }