feat: pref for default filter showUnread

This commit is contained in:
Julien Froidefond
2025-02-21 23:18:41 +01:00
parent c62cff07d2
commit 72e28ed27b
8 changed files with 188 additions and 44 deletions

View File

@@ -1,5 +1,6 @@
import { PaginatedSeriesGrid } from "@/components/library/PaginatedSeriesGrid";
import { LibraryService } from "@/lib/services/library.service";
import { PreferencesService } from "@/lib/services/preferences.service";
interface PageProps {
params: { libraryId: string };
@@ -28,7 +29,11 @@ async function getLibrarySeries(libraryId: string, page: number = 1, unreadOnly:
export default async function LibraryPage({ params, searchParams }: PageProps) {
const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
const unreadOnly = searchParams.unread === "true";
const preferences = await PreferencesService.getPreferences();
// Utiliser le paramètre d'URL s'il existe, sinon utiliser la préférence utilisateur
const unreadOnly =
searchParams.unread !== undefined ? searchParams.unread === "true" : preferences.showOnlyUnread;
try {
const { data: series, library } = await getLibrarySeries(
@@ -53,6 +58,8 @@ export default async function LibraryPage({ params, searchParams }: PageProps) {
totalPages={series.totalPages}
totalElements={series.totalElements}
pageSize={PAGE_SIZE}
defaultShowOnlyUnread={preferences.showOnlyUnread}
showOnlyUnread={unreadOnly}
/>
</div>
);

View File

@@ -1,6 +1,7 @@
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";
interface PageProps {
params: { seriesId: string };
@@ -9,18 +10,29 @@ interface PageProps {
const PAGE_SIZE = 24;
async function getSeriesBooks(seriesId: string, page: number = 1, unreadOnly: boolean = false) {
try {
const pageIndex = page - 1;
const books = await SeriesService.getSeriesBooks(seriesId, pageIndex, PAGE_SIZE, unreadOnly);
const series = await SeriesService.getSeries(seriesId);
return { data: books, series };
} catch (error) {
throw error instanceof Error ? error : new Error("Erreur lors de la récupération des tomes");
}
}
export default async function SeriesPage({ params, searchParams }: PageProps) {
const currentPage = searchParams.page ? parseInt(searchParams.page) : 1;
const unreadOnly = searchParams.unread === "true";
const preferences = await PreferencesService.getPreferences();
// Utiliser le paramètre d'URL s'il existe, sinon utiliser la préférence utilisateur
const unreadOnly =
searchParams.unread !== undefined ? searchParams.unread === "true" : preferences.showOnlyUnread;
try {
const pageIndex = currentPage - 1;
// Appels API parallèles pour les détails de la série et les tomes
const [series, books] = await Promise.all([
SeriesService.getSeries(params.seriesId),
SeriesService.getSeriesBooks(params.seriesId, pageIndex, PAGE_SIZE, unreadOnly),
]);
const { data: books, series } = await getSeriesBooks(params.seriesId, currentPage, unreadOnly);
return (
<div className="container py-8 space-y-8">
@@ -31,6 +43,8 @@ export default async function SeriesPage({ params, searchParams }: PageProps) {
totalPages={books.totalPages}
totalElements={books.totalElements}
pageSize={PAGE_SIZE}
defaultShowOnlyUnread={preferences.showOnlyUnread}
showOnlyUnread={unreadOnly}
/>
</div>
);

View File

@@ -14,6 +14,8 @@ interface PaginatedSeriesGridProps {
totalPages: number;
totalElements: number;
pageSize: number;
defaultShowOnlyUnread: boolean;
showOnlyUnread: boolean;
}
export function PaginatedSeriesGrid({
@@ -22,26 +24,43 @@ export function PaginatedSeriesGrid({
totalPages,
totalElements,
pageSize,
defaultShowOnlyUnread,
showOnlyUnread: initialShowOnlyUnread,
}: PaginatedSeriesGridProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [isChangingPage, setIsChangingPage] = useState(false);
const [showOnlyUnread, setShowOnlyUnread] = useState(searchParams.get("unread") === "true");
const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread);
// Réinitialiser l'état de chargement quand les séries changent
useEffect(() => {
setIsChangingPage(false);
}, [series]);
// Mettre à jour l'état local quand la prop change
useEffect(() => {
setShowOnlyUnread(initialShowOnlyUnread);
}, [initialShowOnlyUnread]);
// Appliquer le filtre par défaut au chargement initial
useEffect(() => {
if (defaultShowOnlyUnread && !searchParams.has("unread")) {
const params = new URLSearchParams(searchParams.toString());
params.set("page", "1");
params.set("unread", "true");
router.push(`${pathname}?${params.toString()}`);
}
}, [defaultShowOnlyUnread, pathname, router, searchParams]);
const handlePageChange = async (page: number) => {
setIsChangingPage(true);
// Créer un nouvel objet URLSearchParams pour manipuler les paramètres
const params = new URLSearchParams(searchParams.toString());
params.set("page", page.toString());
if (showOnlyUnread) {
params.set("unread", "true");
}
// Conserver l'état du filtre unread
params.set("unread", showOnlyUnread.toString());
// Rediriger vers la nouvelle URL avec les paramètres mis à jour
await router.push(`${pathname}?${params.toString()}`);
@@ -52,13 +71,12 @@ export function PaginatedSeriesGrid({
const params = new URLSearchParams(searchParams.toString());
params.set("page", "1"); // Retourner à la première page lors du changement de filtre
if (!showOnlyUnread) {
params.set("unread", "true");
} else {
params.delete("unread");
}
const newUnreadState = !showOnlyUnread;
setShowOnlyUnread(newUnreadState);
// Toujours définir explicitement le paramètre unread
params.set("unread", newUnreadState.toString());
setShowOnlyUnread(!showOnlyUnread);
await router.push(`${pathname}?${params.toString()}`);
};

View File

@@ -14,6 +14,8 @@ interface PaginatedBookGridProps {
totalPages: number;
totalElements: number;
pageSize: number;
defaultShowOnlyUnread: boolean;
showOnlyUnread: boolean;
}
export function PaginatedBookGrid({
@@ -22,26 +24,43 @@ export function PaginatedBookGrid({
totalPages,
totalElements,
pageSize,
defaultShowOnlyUnread,
showOnlyUnread: initialShowOnlyUnread,
}: PaginatedBookGridProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [isChangingPage, setIsChangingPage] = useState(false);
const [showOnlyUnread, setShowOnlyUnread] = useState(searchParams.get("unread") === "true");
const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread);
// Réinitialiser l'état de chargement quand les tomes changent
useEffect(() => {
setIsChangingPage(false);
}, [books]);
// Mettre à jour l'état local quand la prop change
useEffect(() => {
setShowOnlyUnread(initialShowOnlyUnread);
}, [initialShowOnlyUnread]);
// Appliquer le filtre par défaut au chargement initial
useEffect(() => {
if (defaultShowOnlyUnread && !searchParams.has("unread")) {
const params = new URLSearchParams(searchParams.toString());
params.set("page", "1");
params.set("unread", "true");
router.push(`${pathname}?${params.toString()}`);
}
}, [defaultShowOnlyUnread, pathname, router, searchParams]);
const handlePageChange = async (page: number) => {
setIsChangingPage(true);
// Créer un nouvel objet URLSearchParams pour manipuler les paramètres
const params = new URLSearchParams(searchParams.toString());
params.set("page", page.toString());
if (showOnlyUnread) {
params.set("unread", "true");
}
// Conserver l'état du filtre unread
params.set("unread", showOnlyUnread.toString());
// Rediriger vers la nouvelle URL avec les paramètres mis à jour
await router.push(`${pathname}?${params.toString()}`);
@@ -52,13 +71,12 @@ export function PaginatedBookGrid({
const params = new URLSearchParams(searchParams.toString());
params.set("page", "1"); // Retourner à la première page lors du changement de filtre
if (!showOnlyUnread) {
params.set("unread", "true");
} else {
params.delete("unread");
}
const newUnreadState = !showOnlyUnread;
setShowOnlyUnread(newUnreadState);
// Toujours définir explicitement le paramètre unread
params.set("unread", newUnreadState.toString());
setShowOnlyUnread(!showOnlyUnread);
await router.push(`${pathname}?${params.toString()}`);
};

View File

@@ -306,6 +306,39 @@ export function ClientSettings({ initialConfig, initialTTLConfig }: ClientSettin
onCheckedChange={handleToggleThumbnails}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="unread-filter">Filtre "À lire" par défaut</Label>
<p className="text-sm text-muted-foreground">
Afficher uniquement les séries non lues par défaut
</p>
</div>
<Switch
id="unread-filter"
checked={preferences.showOnlyUnread}
onCheckedChange={async (checked) => {
try {
await updatePreferences({ showOnlyUnread: checked });
toast({
title: "Préférences sauvegardées",
description: `Le filtre "À lire" par défaut est maintenant ${
checked ? "activé" : "désactivé"
}`,
});
} catch (error) {
console.error("Erreur détaillée:", error);
toast({
variant: "destructive",
title: "Erreur",
description:
error instanceof Error
? error.message
: "Une erreur est survenue lors de la mise à jour des préférences",
});
}
}}
/>
</div>
</div>
</div>
</div>

View File

@@ -5,11 +5,13 @@ import React, { createContext, useContext, useEffect, useState } from "react";
export interface UserPreferences {
showThumbnails: boolean;
cacheMode: "memory" | "file";
showOnlyUnread: boolean;
}
const defaultPreferences: UserPreferences = {
showThumbnails: true,
cacheMode: "memory",
showOnlyUnread: false,
};
interface PreferencesContextType {
@@ -30,7 +32,10 @@ export function PreferencesProvider({ children }: { children: React.ReactNode })
const response = await fetch("/api/preferences");
if (!response.ok) throw new Error("Erreur lors de la récupération des préférences");
const data = await response.json();
setPreferences(data);
setPreferences({
...defaultPreferences,
...data,
});
} catch (error) {
console.error("Erreur lors de la récupération des préférences:", error);
// En cas d'erreur, on garde les préférences par défaut
@@ -56,7 +61,15 @@ export function PreferencesProvider({ children }: { children: React.ReactNode })
if (!response.ok) throw new Error("Erreur lors de la mise à jour des préférences");
const updatedPreferences = await response.json();
setPreferences(updatedPreferences);
setPreferences((prev) => {
const newState = {
...prev,
...updatedPreferences,
};
console.log("Nouvel état des préférences:", newState);
return newState;
});
} catch (error) {
console.error("Erreur lors de la mise à jour des préférences:", error);
throw error;

View File

@@ -16,11 +16,47 @@ const preferencesSchema = new mongoose.Schema(
enum: ["memory", "file"],
default: "memory",
},
showOnlyUnread: {
type: Boolean,
default: false,
required: false,
},
},
{
timestamps: true,
strict: true,
toObject: {
transform: function (doc, ret) {
// Assurez-vous que showOnlyUnread est toujours un booléen
ret.showOnlyUnread = ret.showOnlyUnread === true;
return ret;
},
},
}
);
// Middleware pour s'assurer que showOnlyUnread est toujours un booléen
preferencesSchema.pre("save", function (next) {
if (this.showOnlyUnread === undefined) {
this.showOnlyUnread = false;
}
this.showOnlyUnread = this.showOnlyUnread === true;
next();
});
preferencesSchema.pre("findOneAndUpdate", function (next) {
const update = this.getUpdate() as mongoose.UpdateQuery<any>;
if (
update &&
"$set" in update &&
update.$set &&
typeof update.$set === "object" &&
"showOnlyUnread" in update.$set
) {
update.$set.showOnlyUnread = update.$set.showOnlyUnread === true;
}
next();
});
export const PreferencesModel =
mongoose.models.Preferences || mongoose.model("Preferences", preferencesSchema);

View File

@@ -9,8 +9,15 @@ interface User {
export interface UserPreferences {
showThumbnails: boolean;
cacheMode: "memory" | "file";
showOnlyUnread: boolean;
}
const defaultPreferences: UserPreferences = {
showThumbnails: true,
cacheMode: "memory",
showOnlyUnread: false,
};
export class PreferencesService {
static async getCurrentUser(): Promise<User> {
const userCookie = cookies().get("stripUser");
@@ -32,26 +39,21 @@ export class PreferencesService {
const user = await this.getCurrentUser();
const preferences = await PreferencesModel.findOne({ userId: user.id });
if (!preferences) {
return {
showThumbnails: true,
cacheMode: "memory",
};
return defaultPreferences;
}
return {
showThumbnails: preferences.showThumbnails,
cacheMode: preferences.cacheMode || "memory",
...defaultPreferences,
...preferences.toObject(),
};
} catch (error) {
console.error("Error getting preferences:", error);
return {
showThumbnails: true,
cacheMode: "memory",
};
return defaultPreferences;
}
}
static async updatePreferences(preferences: Partial<UserPreferences>): Promise<UserPreferences> {
try {
console.log("Service - Préférences reçues pour mise à jour:", preferences);
const user = await this.getCurrentUser();
const updatedPreferences = await PreferencesModel.findOneAndUpdate(
{ userId: user.id },
@@ -59,10 +61,13 @@ export class PreferencesService {
{ new: true, upsert: true }
);
return {
showThumbnails: updatedPreferences.showThumbnails,
cacheMode: updatedPreferences.cacheMode || "memory",
console.log("Service - Document MongoDB après mise à jour:", updatedPreferences);
const result = {
...defaultPreferences,
...updatedPreferences.toObject(),
};
console.log("Service - Résultat final:", result);
return result;
} catch (error) {
console.error("Error updating preferences:", error);
throw error;