feat: amélioration de l'affichage des séries et tomes - Ajout d'un overlay au survol pour les informations - Ajout d'une transparence pour les séries/tomes lus - Amélioration de l'affichage du statut de lecture (X/Y pour les séries en cours)

This commit is contained in:
Julien Froidefond
2025-02-12 08:13:06 +01:00
parent ea1b9b2285
commit 143d9c1bc6
3 changed files with 98 additions and 64 deletions

View File

@@ -119,17 +119,17 @@ function MediaCard({ item, onClick }: MediaCardProps) {
<ImageOff className="w-12 h-12" /> <ImageOff className="w-12 h-12" />
</div> </div>
)} )}
</div>
{/* Contenu */} {/* Overlay avec les informations au survol */}
<div className="flex flex-col p-2"> <div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col justify-end p-3">
<h3 className="font-medium line-clamp-2 text-sm">{title}</h3> <h3 className="font-medium text-sm text-white line-clamp-2">{title}</h3>
{isSeries && ( {isSeries && (
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-white/80 mt-1">
{item.booksCount} tome{item.booksCount > 1 ? "s" : ""} {item.booksCount} tome{item.booksCount > 1 ? "s" : ""}
</p> </p>
)} )}
</div> </div>
</div>
</button> </button>
); );
} }

View File

@@ -12,7 +12,7 @@ interface SeriesGridProps {
} }
// Fonction utilitaire pour obtenir les informations de lecture d'une série // Fonction utilitaire pour obtenir les informations de lecture d'une série
const getReadingStatusInfo = (series: KomgaSeries): { label: string; className: string } => { const getReadingStatusInfo = (series: KomgaSeries) => {
const { booksCount, booksReadCount, booksUnreadCount } = series; const { booksCount, booksReadCount, booksUnreadCount } = series;
const booksInProgressCount = booksCount - (booksReadCount + booksUnreadCount); const booksInProgressCount = booksCount - (booksReadCount + booksUnreadCount);
@@ -25,7 +25,7 @@ const getReadingStatusInfo = (series: KomgaSeries): { label: string; className:
if (booksInProgressCount > 0 || (booksReadCount > 0 && booksReadCount < booksCount)) { if (booksInProgressCount > 0 || (booksReadCount > 0 && booksReadCount < booksCount)) {
return { return {
label: "En cours", label: `${booksReadCount}/${booksCount}`,
className: "bg-blue-500/10 text-blue-500", className: "bg-blue-500/10 text-blue-500",
}; };
} }
@@ -74,16 +74,16 @@ function SeriesCard({ series, onClick, serverUrl }: SeriesCardProps) {
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className="group relative flex flex-col rounded-lg border bg-card text-card-foreground shadow-sm hover:bg-accent hover:text-accent-foreground transition-colors overflow-hidden" className="group relative aspect-[2/3] overflow-hidden rounded-lg bg-muted"
> >
{/* Image de couverture */}
<div className="relative aspect-[2/3] bg-muted">
{!imageError ? ( {!imageError ? (
<Image <Image
src={`/api/komga/images/series/${series.id}/thumbnail`} src={`/api/komga/images/series/${series.id}/thumbnail`}
alt={`Couverture de ${series.metadata.title}`} alt={`Couverture de ${series.metadata.title}`}
fill fill
className="object-cover" className={`object-cover ${
series.booksCount === series.booksReadCount ? "opacity-50" : ""
}`}
sizes="(max-width: 640px) 33vw, (max-width: 1024px) 20vw, 20vw" sizes="(max-width: 640px) 33vw, (max-width: 1024px) 20vw, 20vw"
onError={() => setImageError(true)} onError={() => setImageError(true)}
/> />
@@ -92,23 +92,17 @@ function SeriesCard({ series, onClick, serverUrl }: SeriesCardProps) {
<ImageOff className="w-12 h-12" /> <ImageOff className="w-12 h-12" />
</div> </div>
)} )}
</div>
{/* Contenu */} {/* Overlay avec les informations au survol */}
<div className="flex flex-col p-2"> <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-4 space-y-2 translate-y-full group-hover:translate-y-0 transition-transform duration-200">
<h3 className="font-medium line-clamp-2 text-sm">{series.metadata.title}</h3> <h3 className="font-medium text-sm text-white line-clamp-2">{series.metadata.title}</h3>
<div className="mt-1 text-xs text-muted-foreground space-y-1"> <div className="flex items-center gap-2">
<div className="flex items-center gap-1"> <span className={`px-2 py-0.5 rounded-full text-xs ${statusInfo.className}`}>
<Book className="h-3 w-3" />
<span>
{series.booksCount} tome{series.booksCount > 1 ? "s" : ""}
</span>
</div>
<div className="flex items-center">
<span className={`px-1.5 py-0.5 rounded-full text-[10px] ${statusInfo.className}`}>
{statusInfo.label} {statusInfo.label}
</span> </span>
</div> <span className="text-xs text-white/80">
{series.booksCount} tome{series.booksCount > 1 ? "s" : ""}
</span>
</div> </div>
</div> </div>
</button> </button>

View File

@@ -11,6 +11,38 @@ interface BookGridProps {
getBookThumbnailUrl: (bookId: string) => string; getBookThumbnailUrl: (bookId: string) => string;
} }
// Fonction utilitaire pour obtenir les informations de statut de lecture
const getReadingStatusInfo = (book: KomgaBook) => {
if (!book.readProgress) {
return {
label: "Non lu",
className: "bg-yellow-500/10 text-yellow-500",
};
}
if (book.readProgress.completed) {
const readDate = book.readProgress.readDate
? new Date(book.readProgress.readDate).toLocaleDateString()
: null;
return {
label: readDate ? `Lu le ${readDate}` : "Lu",
className: "bg-green-500/10 text-green-500",
};
}
if (book.readProgress.page > 0) {
return {
label: `Page ${book.readProgress.page}/${book.media.pagesCount}`,
className: "bg-blue-500/10 text-blue-500",
};
}
return {
label: "Non lu",
className: "bg-yellow-500/10 text-yellow-500",
};
};
export function BookGrid({ books, onBookClick, getBookThumbnailUrl }: BookGridProps) { export function BookGrid({ books, onBookClick, getBookThumbnailUrl }: BookGridProps) {
if (!books.length) { if (!books.length) {
return ( return (
@@ -22,26 +54,34 @@ export function BookGrid({ books, onBookClick, getBookThumbnailUrl }: BookGridPr
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">
{books.map((book) => ( {books.map((book) => {
const statusInfo = getReadingStatusInfo(book);
return (
<button <button
key={book.id} key={book.id}
onClick={() => onBookClick(book)} onClick={() => onBookClick(book)}
className="group relative aspect-[2/3] overflow-hidden rounded-lg bg-muted hover:opacity-80 transition-opacity" className="group relative aspect-[2/3] overflow-hidden rounded-lg bg-muted hover:opacity-100 transition-all"
> >
<Image <Image
src={getBookThumbnailUrl(book.id)} src={getBookThumbnailUrl(book.id)}
alt={book.metadata.title} alt={book.metadata.title}
fill fill
className="object-cover" className={`object-cover ${book.readProgress?.completed ? "opacity-50" : ""}`}
sizes="(min-width: 1024px) 16.66vw, (min-width: 768px) 25vw, (min-width: 640px) 33.33vw, 50vw" sizes="(min-width: 1024px) 16.66vw, (min-width: 768px) 25vw, (min-width: 640px) 33.33vw, 50vw"
/> />
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-4"> <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-4 space-y-2 translate-y-full group-hover:translate-y-0 transition-transform duration-200">
<p className="text-sm font-medium text-white text-left line-clamp-2"> <p className="text-sm font-medium text-white text-left line-clamp-2">
{book.metadata.title} {book.metadata.title || `Tome ${book.metadata.number}`}
</p> </p>
<div className="flex items-center gap-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${statusInfo.className}`}>
{statusInfo.label}
</span>
</div>
</div> </div>
</button> </button>
))} );
})}
</div> </div>
); );
} }