diff --git a/src/components/home/HeroSection.tsx b/src/components/home/HeroSection.tsx index 23d7a86..60bc0c7 100644 --- a/src/components/home/HeroSection.tsx +++ b/src/components/home/HeroSection.tsx @@ -37,6 +37,7 @@ export function HeroSection({ series }: HeroSectionProps) { alt={t("home.hero.coverAlt", { title: series.metadata.title })} quality={25} sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 16.666vw" + showProgressUi={false} /> ))} diff --git a/src/components/home/MediaRow.tsx b/src/components/home/MediaRow.tsx index bc8f148..122660a 100644 --- a/src/components/home/MediaRow.tsx +++ b/src/components/home/MediaRow.tsx @@ -128,19 +128,30 @@ function MediaCard({ item, onClick }: MediaCardProps) { >
{isSeries ? ( - + <> + +
+

{title}

+

+ {item.booksCount} tome{item.booksCount > 1 ? "s" : ""} +

+
+ ) : ( - + <> + + )} - {/* Overlay avec les informations au survol */} -
-

{title}

- {isSeries && ( -

- {item.booksCount} tome{item.booksCount > 1 ? "s" : ""} -

- )} -
); diff --git a/src/components/series/BookGrid.tsx b/src/components/series/BookGrid.tsx index 7086fe8..ed51892 100644 --- a/src/components/series/BookGrid.tsx +++ b/src/components/series/BookGrid.tsx @@ -1,60 +1,19 @@ "use client"; import { KomgaBook } from "@/types/komga"; -import { formatDate } from "@/lib/utils"; import { BookCover } from "@/components/ui/book-cover"; -import { MarkAsReadButton } from "@/components/ui/mark-as-read-button"; -import { MarkAsUnreadButton } from "@/components/ui/mark-as-unread-button"; -import { BookOfflineButton } from "@/components/ui/book-offline-button"; import { useState, useEffect } from "react"; import { useTranslate } from "@/hooks/useTranslate"; -import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service"; interface BookGridProps { books: KomgaBook[]; onBookClick: (book: KomgaBook) => void; } -// Fonction utilitaire pour obtenir les informations de statut de lecture -const getReadingStatusInfo = (book: KomgaBook, t: (key: string, options?: any) => string) => { - if (!book.readProgress) { - return { - label: t("books.status.unread"), - className: "bg-yellow-500/10 text-yellow-500", - }; - } - - if (book.readProgress.completed) { - const readDate = book.readProgress.readDate ? formatDate(book.readProgress.readDate) : null; - return { - label: readDate ? t("books.status.readDate", { date: readDate }) : t("books.status.read"), - className: "bg-green-500/10 text-green-500", - }; - } - - const currentPage = ClientOfflineBookService.getCurrentPage(book); - - if (currentPage > 0) { - return { - label: t("books.status.progress", { - current: currentPage, - total: book.media.pagesCount, - }), - className: "bg-blue-500/10 text-blue-500", - }; - } - - return { - label: t("books.status.unread"), - className: "bg-yellow-500/10 text-yellow-500", - }; -}; - export function BookGrid({ books, onBookClick }: BookGridProps) { const [localBooks, setLocalBooks] = useState(books); const { t } = useTranslate(); - // Synchroniser localBooks avec les props books useEffect(() => { setLocalBooks(books); }, [books]); @@ -66,47 +25,41 @@ export function BookGrid({ books, onBookClick }: BookGridProps) { ); } - - const handleMarkAsRead = (bookId: string) => { - setLocalBooks((prevBooks) => - prevBooks.map((book) => - book.id === bookId - ? { - ...book, - readProgress: { - ...(book.readProgress || {}), - completed: true, - readDate: new Date().toISOString(), - page: book.media.pagesCount, - created: book.readProgress?.created || new Date().toISOString(), - lastModified: new Date().toISOString(), - }, - } - : book - ) - ); - }; - - const handleMarkAsUnread = (bookId: string) => { - setLocalBooks((prevBooks) => - prevBooks.map((book) => - book.id === bookId - ? { - ...book, - readProgress: null, - } - : book - ) - ); + const handleOnSuccess = (book: KomgaBook, action: "read" | "unread") => { + if (action === "read") { + setLocalBooks( + localBooks.map((previousBook) => + previousBook.id === book.id + ? { + ...previousBook, + readProgress: { + completed: true, + page: previousBook.media.pagesCount, + readDate: new Date().toISOString(), + created: new Date().toISOString(), + lastModified: new Date().toISOString(), + }, + } + : previousBook + ) + ); + } else if (action === "unread") { + setLocalBooks( + localBooks.map((previousBook) => + previousBook.id === book.id + ? { + ...previousBook, + readProgress: null, + } + : previousBook + ) + ); + } }; return (
{localBooks.map((book) => { - const statusInfo = getReadingStatusInfo(book, t); - const isRead = book.readProgress?.completed || false; - const hasReadProgress = book.readProgress !== null; - return (
handleOnSuccess(book, action)} /> - - {/* Overlay avec les contrôles */} -
- {/* Boutons en haut à droite avec un petit décalage */} -
- {!isRead && ( - handleMarkAsRead(book.id)} - className="bg-white/90 hover:bg-white text-black shadow-sm" - /> - )} - {hasReadProgress && ( - handleMarkAsUnread(book.id)} - className="bg-white/90 hover:bg-white text-black shadow-sm" - /> - )} - -
- - {/* Informations en bas - visible au survol uniquement */} -
-

- {book.metadata.title || `Tome ${book.metadata.number}`} -

-
- - {statusInfo.label} - -
-
-
); })} diff --git a/src/components/series/SeriesHeader.tsx b/src/components/series/SeriesHeader.tsx index b41a847..1a268c1 100644 --- a/src/components/series/SeriesHeader.tsx +++ b/src/components/series/SeriesHeader.tsx @@ -127,6 +127,7 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => { alt={t("series.header.coverAlt", { title: series.metadata.title })} className="blur-sm scale-105 brightness-50" quality={60} + showProgressUi={false} />
@@ -139,6 +140,7 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => { series={series as KomgaSeries} alt={t("series.header.coverAlt", { title: series.metadata.title })} quality={90} + showProgressUi={false} /> diff --git a/src/components/ui/book-cover.tsx b/src/components/ui/book-cover.tsx index e5760e0..e9f4ac8 100644 --- a/src/components/ui/book-cover.tsx +++ b/src/components/ui/book-cover.tsx @@ -4,6 +4,47 @@ import { CoverClient } from "./cover-client"; import { ProgressBar } from "./progress-bar"; import { BookCoverProps, getImageUrl } from "./cover-utils"; import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service"; +import { MarkAsReadButton } from "./mark-as-read-button"; +import { MarkAsUnreadButton } from "./mark-as-unread-button"; +import { BookOfflineButton } from "./book-offline-button"; +import { useTranslate } from "@/hooks/useTranslate"; +import { KomgaBook } from "@/types/komga"; +import { formatDate } from "@/lib/utils"; + +// Fonction utilitaire pour obtenir les informations de statut de lecture +const getReadingStatusInfo = (book: KomgaBook, t: (key: string, options?: any) => string) => { + if (!book.readProgress) { + return { + label: t("books.status.unread"), + className: "bg-yellow-500/10 text-yellow-500", + }; + } + + if (book.readProgress.completed) { + const readDate = book.readProgress.readDate ? formatDate(book.readProgress.readDate) : null; + return { + label: readDate ? t("books.status.readDate", { date: readDate }) : t("books.status.read"), + className: "bg-green-500/10 text-green-500", + }; + } + + const currentPage = ClientOfflineBookService.getCurrentPage(book); + + if (currentPage > 0) { + return { + label: t("books.status.progress", { + current: currentPage, + total: book.media.pagesCount, + }), + className: "bg-blue-500/10 text-blue-500", + }; + } + + return { + label: t("books.status.unread"), + className: "bg-yellow-500/10 text-yellow-500", + }; +}; export function BookCover({ book, @@ -11,27 +52,99 @@ export function BookCover({ className, quality = 80, sizes = "100vw", + showProgressUi = true, + onSuccess, + showControls = true, + showOverlay = true, + overlayVariant = "default", }: BookCoverProps) { - if (!book) return null; + const { t } = useTranslate(); const imageUrl = getImageUrl("book", book.id); const isCompleted = book.readProgress?.completed || false; const currentPage = ClientOfflineBookService.getCurrentPage(book); const totalPages = book.media.pagesCount; - const showProgress = currentPage && totalPages && currentPage > 0 && !isCompleted; + const showProgress = + showProgressUi && currentPage && totalPages && currentPage > 0 && !isCompleted; + + const statusInfo = getReadingStatusInfo(book, t); + const isRead = book.readProgress?.completed || false; + const hasReadProgress = book.readProgress !== null; + + const handleMarkAsRead = () => { + onSuccess?.(book, "read"); + }; + + const handleMarkAsUnread = () => { + onSuccess?.(book, "unread"); + }; return ( -
- - {showProgress && } -
+ <> +
+ + {showProgress && } +
+ {/* Overlay avec les contrôles */} + {(showControls || showOverlay) && ( +
+ {showControls && ( + // Boutons en haut à droite avec un petit décalage +
+ {!isRead && ( + handleMarkAsRead()} + className="bg-white/90 hover:bg-white text-black shadow-sm" + /> + )} + {hasReadProgress && ( + handleMarkAsUnread()} + className="bg-white/90 hover:bg-white text-black shadow-sm" + /> + )} + +
+ )} + {showOverlay && overlayVariant === "default" && ( +
+

+ {book.metadata.title || `Tome ${book.metadata.number}`} +

+
+ + {statusInfo.label} + +
+
+ )} +
+ )} + {showOverlay && overlayVariant === "home" && ( +
+

+ {book.metadata.title || `Tome ${book.metadata.number}`} +

+

+ {currentPage} / {book.media.pagesCount} +

+
+ )} + ); } diff --git a/src/components/ui/cover-utils.tsx b/src/components/ui/cover-utils.tsx index 7e58bd9..c8615e8 100644 --- a/src/components/ui/cover-utils.tsx +++ b/src/components/ui/cover-utils.tsx @@ -5,10 +5,15 @@ export interface BaseCoverProps { className?: string; quality?: number; sizes?: string; + showProgressUi?: boolean; } export interface BookCoverProps extends BaseCoverProps { - book?: KomgaBook; + book: KomgaBook; + onSuccess?: (book: KomgaBook, action: "read" | "unread") => void; + showControls?: boolean; + showOverlay?: boolean; + overlayVariant?: "default" | "home"; } export interface SeriesCoverProps extends BaseCoverProps { diff --git a/src/components/ui/mark-as-read-button.tsx b/src/components/ui/mark-as-read-button.tsx index 574d887..49333c2 100644 --- a/src/components/ui/mark-as-read-button.tsx +++ b/src/components/ui/mark-as-read-button.tsx @@ -1,9 +1,10 @@ "use client"; -import { BookCheck } from "lucide-react"; +import { BookCheck, Loader2 } from "lucide-react"; import { Button } from "./button"; import { useToast } from "./use-toast"; import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service"; +import { useState } from "react"; interface MarkAsReadButtonProps { bookId: string; @@ -21,9 +22,11 @@ export function MarkAsReadButton({ className, }: MarkAsReadButtonProps) { const { toast } = useToast(); + const [isLoading, setIsLoading] = useState(false); const handleMarkAsRead = async (e: React.MouseEvent) => { e.stopPropagation(); // Empêcher la propagation au parent + setIsLoading(true); try { ClientOfflineBookService.removeCurrentPageById(bookId); const response = await fetch(`/api/komga/books/${bookId}/read-progress`, { @@ -50,6 +53,8 @@ export function MarkAsReadButton({ description: "Impossible de marquer le tome comme lu", variant: "destructive", }); + } finally { + setIsLoading(false); } }; @@ -59,10 +64,10 @@ export function MarkAsReadButton({ size="icon" onClick={handleMarkAsRead} className={`h-8 w-8 p-0 rounded-br-lg rounded-tl-lg ${className}`} - disabled={isRead} + disabled={isRead || isLoading} aria-label="Marquer comme lu" > - + {isLoading ? : } ); } diff --git a/src/components/ui/mark-as-unread-button.tsx b/src/components/ui/mark-as-unread-button.tsx index e5664ca..fc577aa 100644 --- a/src/components/ui/mark-as-unread-button.tsx +++ b/src/components/ui/mark-as-unread-button.tsx @@ -1,9 +1,11 @@ "use client"; -import { BookX } from "lucide-react"; +import { BookX, Loader2 } from "lucide-react"; import { Button } from "./button"; import { useToast } from "./use-toast"; import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service"; +import { useState } from "react"; + interface MarkAsUnreadButtonProps { bookId: string; onSuccess?: () => void; @@ -12,9 +14,11 @@ interface MarkAsUnreadButtonProps { export function MarkAsUnreadButton({ bookId, onSuccess, className }: MarkAsUnreadButtonProps) { const { toast } = useToast(); + const [isLoading, setIsLoading] = useState(false); const handleMarkAsUnread = async (e: React.MouseEvent) => { e.stopPropagation(); // Empêcher la propagation au parent + setIsLoading(true); try { ClientOfflineBookService.removeCurrentPageById(bookId); const response = await fetch(`/api/komga/books/${bookId}/read-progress`, { @@ -37,6 +41,8 @@ export function MarkAsUnreadButton({ bookId, onSuccess, className }: MarkAsUnrea description: "Impossible de marquer le tome comme non lu", variant: "destructive", }); + } finally { + setIsLoading(false); } }; @@ -46,9 +52,10 @@ export function MarkAsUnreadButton({ bookId, onSuccess, className }: MarkAsUnrea size="icon" onClick={handleMarkAsUnread} className={`h-8 w-8 p-0 rounded-br-lg rounded-tl-lg ${className}`} + disabled={isLoading} aria-label="Marquer comme non lu" > - + {isLoading ? : } ); } diff --git a/src/components/ui/series-cover.tsx b/src/components/ui/series-cover.tsx index a7fd910..5cb6b3e 100644 --- a/src/components/ui/series-cover.tsx +++ b/src/components/ui/series-cover.tsx @@ -10,15 +10,14 @@ export function SeriesCover({ className, quality = 80, sizes = "100vw", + showProgressUi = true, }: SeriesCoverProps) { - if (!series) return null; - const imageUrl = getImageUrl("series", series.id); const isCompleted = series.booksCount === series.booksReadCount; const readBooks = series.booksReadCount; const totalBooks = series.booksCount; - const showProgress = readBooks && totalBooks && readBooks > 0 && !isCompleted; + const showProgress = showProgressUi && readBooks && totalBooks && readBooks > 0 && !isCompleted; return (