feat(i18n): series page

This commit is contained in:
Julien Froidefond
2025-02-27 12:33:58 +01:00
parent 52a212ef07
commit 0d1d969e53
5 changed files with 111 additions and 38 deletions

View File

@@ -7,6 +7,7 @@ import { MarkAsReadButton } from "@/components/ui/mark-as-read-button";
import { MarkAsUnreadButton } from "@/components/ui/mark-as-unread-button"; import { MarkAsUnreadButton } from "@/components/ui/mark-as-unread-button";
import { BookOfflineButton } from "@/components/ui/book-offline-button"; import { BookOfflineButton } from "@/components/ui/book-offline-button";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useTranslate } from "@/hooks/useTranslate";
interface BookGridProps { interface BookGridProps {
books: KomgaBook[]; books: KomgaBook[];
@@ -14,10 +15,10 @@ interface BookGridProps {
} }
// Fonction utilitaire pour obtenir les informations de statut de lecture // Fonction utilitaire pour obtenir les informations de statut de lecture
const getReadingStatusInfo = (book: KomgaBook) => { const getReadingStatusInfo = (book: KomgaBook, t: (key: string, options?: any) => string) => {
if (!book.readProgress) { if (!book.readProgress) {
return { return {
label: "Non lu", label: t("books.status.unread"),
className: "bg-yellow-500/10 text-yellow-500", className: "bg-yellow-500/10 text-yellow-500",
}; };
} }
@@ -25,26 +26,30 @@ const getReadingStatusInfo = (book: KomgaBook) => {
if (book.readProgress.completed) { if (book.readProgress.completed) {
const readDate = book.readProgress.readDate ? formatDate(book.readProgress.readDate) : null; const readDate = book.readProgress.readDate ? formatDate(book.readProgress.readDate) : null;
return { return {
label: readDate ? `Lu le ${readDate}` : "Lu", label: readDate ? t("books.status.readDate", { date: readDate }) : t("books.status.read"),
className: "bg-green-500/10 text-green-500", className: "bg-green-500/10 text-green-500",
}; };
} }
if (book.readProgress.page > 0) { if (book.readProgress.page > 0) {
return { return {
label: `Page ${book.readProgress.page}/${book.media.pagesCount}`, label: t("books.status.progress", {
current: book.readProgress.page,
total: book.media.pagesCount,
}),
className: "bg-blue-500/10 text-blue-500", className: "bg-blue-500/10 text-blue-500",
}; };
} }
return { return {
label: "Non lu", label: t("books.status.unread"),
className: "bg-yellow-500/10 text-yellow-500", className: "bg-yellow-500/10 text-yellow-500",
}; };
}; };
export function BookGrid({ books, onBookClick }: BookGridProps) { export function BookGrid({ books, onBookClick }: BookGridProps) {
const [localBooks, setLocalBooks] = useState(books); const [localBooks, setLocalBooks] = useState(books);
const { t } = useTranslate();
// Synchroniser localBooks avec les props books // Synchroniser localBooks avec les props books
useEffect(() => { useEffect(() => {
@@ -54,7 +59,7 @@ export function BookGrid({ books, onBookClick }: BookGridProps) {
if (!localBooks.length) { if (!localBooks.length) {
return ( return (
<div className="text-center p-8"> <div className="text-center p-8">
<p className="text-muted-foreground">Aucun tome disponible</p> <p className="text-muted-foreground">{t("books.empty")}</p>
</div> </div>
); );
} }
@@ -95,7 +100,7 @@ export function BookGrid({ books, onBookClick }: BookGridProps) {
return ( return (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6"> <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
{localBooks.map((book) => { {localBooks.map((book) => {
const statusInfo = getReadingStatusInfo(book); const statusInfo = getReadingStatusInfo(book, t);
const isRead = book.readProgress?.completed || false; const isRead = book.readProgress?.completed || false;
const currentPage = book.readProgress?.page || 0; const currentPage = book.readProgress?.page || 0;
@@ -111,7 +116,9 @@ export function BookGrid({ books, onBookClick }: BookGridProps) {
<Cover <Cover
type="book" type="book"
id={book.id} id={book.id}
alt={`Couverture de ${book.metadata.title || `Tome ${book.metadata.number}`}`} alt={t("books.coverAlt", {
title: book.metadata.title || `Tome ${book.metadata.number}`,
})}
isCompleted={isRead} isCompleted={isRead}
currentPage={currentPage} currentPage={currentPage}
totalPages={book.media.pagesCount} totalPages={book.media.pagesCount}

View File

@@ -7,6 +7,7 @@ import { useState, useEffect } from "react";
import { Loader2, Filter } from "lucide-react"; import { Loader2, Filter } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { KomgaBook } from "@/types/komga"; import { KomgaBook } from "@/types/komga";
import { useTranslate } from "@/hooks/useTranslate";
interface PaginatedBookGridProps { interface PaginatedBookGridProps {
books: KomgaBook[]; books: KomgaBook[];
@@ -32,6 +33,7 @@ export function PaginatedBookGrid({
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [isChangingPage, setIsChangingPage] = useState(false); const [isChangingPage, setIsChangingPage] = useState(false);
const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread); const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread);
const { t } = useTranslate();
// Réinitialiser l'état de chargement quand les tomes changent // Réinitialiser l'état de chargement quand les tomes changent
useEffect(() => { useEffect(() => {
@@ -55,26 +57,19 @@ export function PaginatedBookGrid({
const handlePageChange = async (page: number) => { const handlePageChange = async (page: number) => {
setIsChangingPage(true); setIsChangingPage(true);
// Créer un nouvel objet URLSearchParams pour manipuler les paramètres
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
params.set("page", page.toString()); params.set("page", page.toString());
// Conserver l'état du filtre unread
params.set("unread", showOnlyUnread.toString()); params.set("unread", showOnlyUnread.toString());
// Rediriger vers la nouvelle URL avec les paramètres mis à jour
await router.push(`${pathname}?${params.toString()}`); await router.push(`${pathname}?${params.toString()}`);
}; };
const handleUnreadFilter = async () => { const handleUnreadFilter = async () => {
setIsChangingPage(true); setIsChangingPage(true);
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
params.set("page", "1"); // Retourner à la première page lors du changement de filtre params.set("page", "1");
const newUnreadState = !showOnlyUnread; const newUnreadState = !showOnlyUnread;
setShowOnlyUnread(newUnreadState); setShowOnlyUnread(newUnreadState);
// Toujours définir explicitement le paramètre unread
params.set("unread", newUnreadState.toString()); params.set("unread", newUnreadState.toString());
await router.push(`${pathname}?${params.toString()}`); await router.push(`${pathname}?${params.toString()}`);
@@ -88,26 +83,26 @@ export function PaginatedBookGrid({
const startIndex = (currentPage - 1) * pageSize + 1; const startIndex = (currentPage - 1) * pageSize + 1;
const endIndex = Math.min(currentPage * pageSize, totalElements); const endIndex = Math.min(currentPage * pageSize, totalElements);
const getShowingText = () => {
if (!totalElements) return t("books.empty");
return t("books.display.showing", {
start: startIndex,
end: endIndex,
total: totalElements,
});
};
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<div className="flex items-center justify-between flex-wrap gap-4"> <div className="flex items-center justify-between flex-wrap gap-4">
<p className="text-sm text-muted-foreground flex-1 min-w-[200px]"> <p className="text-sm text-muted-foreground flex-1 min-w-[200px]">{getShowingText()}</p>
{totalElements > 0 ? (
<>
Affichage des tomes <span className="font-medium">{startIndex}</span> à{" "}
<span className="font-medium">{endIndex}</span> sur{" "}
<span className="font-medium">{totalElements}</span>
</>
) : (
"Aucun tome trouvé"
)}
</p>
<button <button
onClick={handleUnreadFilter} onClick={handleUnreadFilter}
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg hover:bg-accent hover:text-accent-foreground whitespace-nowrap ml-auto" className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg hover:bg-accent hover:text-accent-foreground whitespace-nowrap ml-auto"
> >
<Filter className="h-4 w-4" /> <Filter className="h-4 w-4" />
{showOnlyUnread ? "Afficher tout" : "À lire"} {showOnlyUnread ? t("books.filters.showAll") : t("books.filters.unread")}
</button> </button>
</div> </div>
@@ -117,7 +112,7 @@ export function PaginatedBookGrid({
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm z-10"> <div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm z-10">
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-background border shadow-sm"> <div className="flex items-center gap-2 px-4 py-2 rounded-full bg-background border shadow-sm">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm">Chargement...</span> <span className="text-sm">{t("books.loading")}</span>
</div> </div>
</div> </div>
)} )}
@@ -135,7 +130,7 @@ export function PaginatedBookGrid({
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-between"> <div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-between">
<p className="text-sm text-muted-foreground order-2 sm:order-1"> <p className="text-sm text-muted-foreground order-2 sm:order-1">
Page {currentPage} sur {totalPages} {t("books.display.page", { current: currentPage, total: totalPages })}
</p> </p>
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}

View File

@@ -10,6 +10,7 @@ import { RefreshButton } from "@/components/library/RefreshButton";
import { AppError } from "@/utils/errors"; import { AppError } from "@/utils/errors";
import { ERROR_CODES } from "@/constants/errorCodes"; import { ERROR_CODES } from "@/constants/errorCodes";
import { ERROR_MESSAGES } from "@/constants/errorMessages"; import { ERROR_MESSAGES } from "@/constants/errorMessages";
import { useTranslate } from "@/hooks/useTranslate";
interface SeriesHeaderProps { interface SeriesHeaderProps {
series: KomgaSeries; series: KomgaSeries;
@@ -19,6 +20,7 @@ interface SeriesHeaderProps {
export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => { export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
const { toast } = useToast(); const { toast } = useToast();
const [isFavorite, setIsFavorite] = useState(false); const [isFavorite, setIsFavorite] = useState(false);
const { t } = useTranslate();
useEffect(() => { useEffect(() => {
const checkFavorite = async () => { const checkFavorite = async () => {
@@ -59,7 +61,7 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
setIsFavorite(!isFavorite); setIsFavorite(!isFavorite);
window.dispatchEvent(new Event("favoritesChanged")); window.dispatchEvent(new Event("favoritesChanged"));
toast({ toast({
title: !isFavorite ? "Ajouté aux favoris" : "Retiré des favoris", title: t(isFavorite ? "series.header.favorite.remove" : "series.header.favorite.add"),
description: series.metadata.title, description: series.metadata.title,
}); });
} else if (response.status === 500) { } else if (response.status === 500) {
@@ -90,7 +92,7 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
if (booksReadCount === booksCount) { if (booksReadCount === booksCount) {
return { return {
label: "Lu", label: t("series.header.status.read"),
className: "bg-green-500/10 text-green-500", className: "bg-green-500/10 text-green-500",
icon: BookMarked, icon: BookMarked,
}; };
@@ -98,14 +100,17 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
if (booksInProgressCount > 0 || (booksReadCount > 0 && booksReadCount < booksCount)) { if (booksInProgressCount > 0 || (booksReadCount > 0 && booksReadCount < booksCount)) {
return { return {
label: `${booksReadCount}/${booksCount}`, label: t("series.header.status.progress", {
read: booksReadCount,
total: booksCount,
}),
className: "bg-blue-500/10 text-blue-500", className: "bg-blue-500/10 text-blue-500",
icon: BookOpen, icon: BookOpen,
}; };
} }
return { return {
label: "Non lu", label: t("series.header.status.unread"),
className: "bg-yellow-500/10 text-yellow-500", className: "bg-yellow-500/10 text-yellow-500",
icon: Book, icon: Book,
}; };
@@ -120,7 +125,7 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
<Cover <Cover
type="series" type="series"
id={series.id} id={series.id}
alt={`Couverture de ${series.metadata.title}`} alt={t("series.header.coverAlt", { title: series.metadata.title })}
className="blur-sm scale-105 brightness-50" className="blur-sm scale-105 brightness-50"
quality={60} quality={60}
/> />
@@ -134,7 +139,7 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
<Cover <Cover
type="series" type="series"
id={series.id} id={series.id}
alt={`Couverture de ${series.metadata.title}`} alt={t("series.header.coverAlt", { title: series.metadata.title })}
quality={90} quality={90}
/> />
</div> </div>
@@ -155,7 +160,7 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
{statusInfo.label} {statusInfo.label}
</span> </span>
<span className="text-sm text-white/80"> <span className="text-sm text-white/80">
{series.booksCount} tome{series.booksCount > 1 ? "s" : ""} {t("series.header.books", { count: series.booksCount })}
</span> </span>
<Button <Button
variant="ghost" variant="ghost"

View File

@@ -130,7 +130,7 @@
"unread": "Unread", "unread": "Unread",
"progress": "{read}/{total}" "progress": "{read}/{total}"
}, },
"books": "{count} book | {count} books", "books": "{count} books",
"filters": { "filters": {
"title": "Filters", "title": "Filters",
"showAll": "Show all", "showAll": "Show all",
@@ -140,6 +140,39 @@
"display": { "display": {
"showing": "Showing series {start} to {end} of {total}", "showing": "Showing series {start} to {end} of {total}",
"page": "Page {current} of {total}" "page": "Page {current} of {total}"
},
"header": {
"coverAlt": "Cover of {{title}}",
"books": "{{count}} book",
"books_plural": "{{count}} books",
"status": {
"read": "Read",
"unread": "Unread",
"progress": "{{read}}/{{total}}"
},
"favorite": {
"add": "Added to favorites",
"remove": "Removed from favorites"
} }
} }
},
"books": {
"empty": "No books available",
"coverAlt": "Cover of {{title}}",
"status": {
"unread": "Unread",
"read": "Read",
"readDate": "Read on {{date}}",
"progress": "Page {{current}}/{{total}}"
},
"display": {
"showing": "Showing books {start} to {end} of {total}",
"page": "Page {current} of {total}"
},
"filters": {
"showAll": "Show all",
"unread": "Unread"
},
"loading": "Loading..."
}
} }

View File

@@ -140,6 +140,39 @@
"display": { "display": {
"showing": "Affichage des séries {start} à {end} sur {total}", "showing": "Affichage des séries {start} à {end} sur {total}",
"page": "Page {current} sur {total}" "page": "Page {current} sur {total}"
},
"header": {
"coverAlt": "Couverture de {{title}}",
"books": "{{count}} tome",
"books_plural": "{{count}} tomes",
"status": {
"read": "Lu",
"unread": "Non lu",
"progress": "{{read}}/{{total}}"
},
"favorite": {
"add": "Ajouté aux favoris",
"remove": "Retiré des favoris"
} }
} }
},
"books": {
"empty": "Aucun tome disponible",
"coverAlt": "Couverture de {{title}}",
"status": {
"unread": "Non lu",
"read": "Lu",
"readDate": "Lu le {{date}}",
"progress": "Page {{current}}/{{total}}"
},
"display": {
"showing": "Affichage des tomes {start} à {end} sur {total}",
"page": "Page {current} sur {total}"
},
"filters": {
"showAll": "Afficher tout",
"unread": "À lire"
},
"loading": "Chargement..."
}
} }