refacto: cover split and simplify
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { SeriesCover } from "@/components/ui/series-cover";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { KomgaSeries } from "@/types/komga";
|
||||
|
||||
interface OptimizedHeroSeries {
|
||||
id: string;
|
||||
@@ -31,9 +32,8 @@ export function HeroSection({ series }: HeroSectionProps) {
|
||||
key={series.id}
|
||||
className="relative aspect-[2/3] bg-muted rounded-lg overflow-hidden"
|
||||
>
|
||||
<Cover
|
||||
type="series"
|
||||
id={series.id}
|
||||
<SeriesCover
|
||||
series={series as KomgaSeries}
|
||||
alt={t("home.hero.coverAlt", { title: series.metadata.title })}
|
||||
quality={25}
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 16.666vw"
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
|
||||
import { KomgaBook } from "@/types/komga";
|
||||
import { KomgaBook, KomgaSeries } from "@/types/komga";
|
||||
import { BookCover } from "../ui/book-cover";
|
||||
import { SeriesCover } from "../ui/series-cover";
|
||||
|
||||
interface BaseItem {
|
||||
id: string;
|
||||
@@ -128,24 +128,9 @@ function MediaCard({ item, onClick }: MediaCardProps) {
|
||||
>
|
||||
<div className="relative aspect-[2/3] bg-muted">
|
||||
{isSeries ? (
|
||||
<Cover
|
||||
type={isSeries ? "series" : "book"}
|
||||
id={item.id}
|
||||
alt={`Couverture de ${title}`}
|
||||
quality={100}
|
||||
readBooks={item.booksReadCount}
|
||||
totalBooks={item.booksCount}
|
||||
isCompleted={item.booksCount === item.booksReadCount}
|
||||
/>
|
||||
<SeriesCover series={item as KomgaSeries} alt={`Couverture de ${title}`} quality={100} />
|
||||
) : (
|
||||
<Cover
|
||||
type="book"
|
||||
id={item.id}
|
||||
alt={`Couverture de ${title}`}
|
||||
quality={100}
|
||||
currentPage={ClientOfflineBookService.getCurrentPage(item as KomgaBook)}
|
||||
totalPages={item.media?.pagesCount}
|
||||
/>
|
||||
<BookCover book={item as KomgaBook} alt={`Couverture de ${title}`} quality={100} />
|
||||
)}
|
||||
{/* Overlay avec les informations au survol */}
|
||||
<div className="absolute inset-0 bg-black/60 opacity-0 hover:opacity-100 transition-opacity duration-200 flex flex-col justify-end p-3">
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Book } from "lucide-react";
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { KomgaLibrary } from "@/types/komga";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
interface LibraryGridProps {
|
||||
libraries: KomgaLibrary[];
|
||||
onLibraryClick?: (library: KomgaLibrary) => void;
|
||||
}
|
||||
|
||||
// Utility function to format date safely
|
||||
const formatDate = (dateString: string, locale: string): string => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) {
|
||||
return "Date unavailable";
|
||||
}
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
} catch (error) {
|
||||
console.error("Error formatting date:", error);
|
||||
return "Date unavailable";
|
||||
}
|
||||
};
|
||||
|
||||
export function LibraryGrid({ libraries, onLibraryClick }: LibraryGridProps) {
|
||||
const { t } = useTranslate();
|
||||
|
||||
if (!libraries.length) {
|
||||
return (
|
||||
<div className="text-center p-8">
|
||||
<p className="text-muted-foreground">{t("library.empty")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{libraries.map((library) => (
|
||||
<LibraryCard key={library.id} library={library} onClick={() => onLibraryClick?.(library)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface LibraryCardProps {
|
||||
library: KomgaLibrary;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
function LibraryCard({ library, onClick }: LibraryCardProps) {
|
||||
const { t, i18n } = useTranslate();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="group relative flex flex-col h-48 rounded-lg border bg-card text-card-foreground shadow-sm hover:bg-accent hover:text-accent-foreground transition-colors overflow-hidden"
|
||||
>
|
||||
{/* Cover image */}
|
||||
<div className="absolute inset-0 bg-muted">
|
||||
<div className="w-full h-full opacity-20 group-hover:opacity-30 transition-opacity">
|
||||
<Cover
|
||||
type="series"
|
||||
id={library.id}
|
||||
alt={t("library.coverAlt", { name: library.name })}
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
quality={25}
|
||||
readBooks={library.booksReadCount}
|
||||
totalBooks={library.booksCount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative h-full flex flex-col p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Book className="h-6 w-6 shrink-0" />
|
||||
<h3 className="text-lg font-semibold line-clamp-1">{library.name}</h3>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded-full text-xs ${
|
||||
library.unavailable
|
||||
? "bg-destructive/10 text-destructive"
|
||||
: "bg-green-500/10 text-green-500"
|
||||
}`}
|
||||
>
|
||||
{t(library.unavailable ? "library.status.unavailable" : "library.status.available")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("library.lastUpdated", {
|
||||
date: formatDate(library.lastModified, i18n.language === "fr" ? "fr-FR" : "en-US"),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { KomgaSeries } from "@/types/komga";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { SeriesCover } from "@/components/ui/series-cover";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
interface SeriesGridProps {
|
||||
@@ -65,13 +65,11 @@ export function SeriesGrid({ series }: SeriesGridProps) {
|
||||
series.booksCount === series.booksReadCount && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<Cover
|
||||
type="series"
|
||||
id={series.id}
|
||||
<SeriesCover
|
||||
series={series as KomgaSeries}
|
||||
alt={t("series.coverAlt", { title: series.metadata.title })}
|
||||
isCompleted={series.booksCount === series.booksReadCount}
|
||||
readBooks={series.booksReadCount}
|
||||
totalBooks={series.booksCount}
|
||||
quality={25}
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
/>
|
||||
<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 text-sm text-white line-clamp-2">{series.metadata.title}</h3>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { KomgaBook } from "@/types/komga";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
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";
|
||||
@@ -106,7 +106,6 @@ export function BookGrid({ books, onBookClick }: BookGridProps) {
|
||||
const statusInfo = getReadingStatusInfo(book, t);
|
||||
const isRead = book.readProgress?.completed || false;
|
||||
const hasReadProgress = book.readProgress !== null;
|
||||
const currentPage = ClientOfflineBookService.getCurrentPage(book);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -117,15 +116,11 @@ export function BookGrid({ books, onBookClick }: BookGridProps) {
|
||||
onClick={() => onBookClick(book)}
|
||||
className="w-full h-full hover:opacity-100 transition-all"
|
||||
>
|
||||
<Cover
|
||||
type="book"
|
||||
id={book.id}
|
||||
<BookCover
|
||||
book={book}
|
||||
alt={t("books.coverAlt", {
|
||||
title: book.metadata.title || `Tome ${book.metadata.number}`,
|
||||
})}
|
||||
isCompleted={isRead}
|
||||
currentPage={currentPage}
|
||||
totalPages={book.media.pagesCount}
|
||||
/>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import { KomgaSeries } from "@/types/komga";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { Cover } from "@/components/ui/cover";
|
||||
import { RefreshButton } from "@/components/library/RefreshButton";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import { getErrorMessage } from "@/utils/errors";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { SeriesCover } from "@/components/ui/series-cover";
|
||||
|
||||
interface SeriesHeaderProps {
|
||||
series: KomgaSeries;
|
||||
@@ -122,9 +122,8 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
|
||||
<div className="relative min-h-[300px] md:h-[300px] w-screen -ml-[calc((100vw-100%)/2)] overflow-hidden">
|
||||
{/* Image de fond */}
|
||||
<div className="absolute inset-0">
|
||||
<Cover
|
||||
type="series"
|
||||
id={series.id}
|
||||
<SeriesCover
|
||||
series={series as KomgaSeries}
|
||||
alt={t("series.header.coverAlt", { title: series.metadata.title })}
|
||||
className="blur-sm scale-105 brightness-50"
|
||||
quality={60}
|
||||
@@ -136,9 +135,8 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
|
||||
<div className="flex flex-col md:flex-row gap-6 items-center md:items-start w-full">
|
||||
{/* Image principale */}
|
||||
<div className="relative w-[180px] aspect-[2/3] rounded-lg overflow-hidden shadow-lg bg-muted flex-shrink-0">
|
||||
<Cover
|
||||
type="series"
|
||||
id={series.id}
|
||||
<SeriesCover
|
||||
series={series as KomgaSeries}
|
||||
alt={t("series.header.coverAlt", { title: series.metadata.title })}
|
||||
quality={90}
|
||||
/>
|
||||
|
||||
37
src/components/ui/book-cover.tsx
Normal file
37
src/components/ui/book-cover.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { CoverClient } from "./cover-client";
|
||||
import { ProgressBar } from "./progress-bar";
|
||||
import { BookCoverProps, getImageUrl } from "./cover-utils";
|
||||
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
|
||||
|
||||
export function BookCover({
|
||||
book,
|
||||
alt = "Image de couverture",
|
||||
className,
|
||||
quality = 80,
|
||||
sizes = "100vw",
|
||||
}: BookCoverProps) {
|
||||
if (!book) return null;
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<CoverClient
|
||||
imageUrl={imageUrl}
|
||||
alt={alt}
|
||||
className={className}
|
||||
quality={quality}
|
||||
sizes={sizes}
|
||||
isCompleted={isCompleted}
|
||||
/>
|
||||
{showProgress && <ProgressBar progress={currentPage} total={totalPages} type="book" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
src/components/ui/cover-utils.tsx
Normal file
23
src/components/ui/cover-utils.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { KomgaBook, KomgaSeries } from "@/types/komga";
|
||||
|
||||
export interface BaseCoverProps {
|
||||
alt?: string;
|
||||
className?: string;
|
||||
quality?: number;
|
||||
sizes?: string;
|
||||
}
|
||||
|
||||
export interface BookCoverProps extends BaseCoverProps {
|
||||
book?: KomgaBook;
|
||||
}
|
||||
|
||||
export interface SeriesCoverProps extends BaseCoverProps {
|
||||
series: KomgaSeries;
|
||||
}
|
||||
|
||||
export function getImageUrl(type: "series" | "book", id: string) {
|
||||
if (type === "series") {
|
||||
return `/api/komga/images/series/${id}/thumbnail`;
|
||||
}
|
||||
return `/api/komga/images/books/${id}/thumbnail`;
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { CoverClient } from "./cover-client";
|
||||
import { ProgressBar } from "./progress-bar";
|
||||
|
||||
interface BaseCoverProps {
|
||||
type: "series" | "book";
|
||||
id: string;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
quality?: number;
|
||||
sizes?: string;
|
||||
isCompleted?: boolean;
|
||||
}
|
||||
|
||||
interface BookCoverProps extends BaseCoverProps {
|
||||
type: "book";
|
||||
currentPage?: number;
|
||||
totalPages?: number;
|
||||
}
|
||||
|
||||
interface SeriesCoverProps extends BaseCoverProps {
|
||||
type: "series";
|
||||
readBooks?: number;
|
||||
totalBooks?: number;
|
||||
}
|
||||
|
||||
type CoverProps = BookCoverProps | SeriesCoverProps;
|
||||
|
||||
function getImageUrl(type: "series" | "book", id: string) {
|
||||
if (type === "series") {
|
||||
return `/api/komga/images/series/${id}/thumbnail`;
|
||||
}
|
||||
return `/api/komga/images/books/${id}/thumbnail`;
|
||||
}
|
||||
|
||||
export function Cover(props: CoverProps) {
|
||||
const {
|
||||
type,
|
||||
id,
|
||||
alt = "Image de couverture",
|
||||
className,
|
||||
quality = 80,
|
||||
sizes = "100vw",
|
||||
isCompleted = false,
|
||||
} = props;
|
||||
|
||||
const imageUrl = getImageUrl(type, id);
|
||||
|
||||
const showProgress = () => {
|
||||
if (type === "book") {
|
||||
const { currentPage, totalPages } = props;
|
||||
return currentPage && totalPages && currentPage > 0 && !isCompleted ? (
|
||||
<ProgressBar progress={currentPage} total={totalPages} type="book" />
|
||||
) : null;
|
||||
}
|
||||
|
||||
if (type === "series") {
|
||||
const { readBooks, totalBooks } = props;
|
||||
return readBooks && totalBooks && readBooks > 0 && !isCompleted ? (
|
||||
<ProgressBar progress={readBooks} total={totalBooks} type="series" />
|
||||
) : null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<CoverClient
|
||||
imageUrl={imageUrl}
|
||||
alt={alt}
|
||||
className={className}
|
||||
quality={quality}
|
||||
sizes={sizes}
|
||||
isCompleted={isCompleted}
|
||||
/>
|
||||
{showProgress()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
src/components/ui/series-cover.tsx
Normal file
36
src/components/ui/series-cover.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { CoverClient } from "./cover-client";
|
||||
import { ProgressBar } from "./progress-bar";
|
||||
import { SeriesCoverProps, getImageUrl } from "./cover-utils";
|
||||
|
||||
export function SeriesCover({
|
||||
series,
|
||||
alt = "Image de couverture",
|
||||
className,
|
||||
quality = 80,
|
||||
sizes = "100vw",
|
||||
}: 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;
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<CoverClient
|
||||
imageUrl={imageUrl}
|
||||
alt={alt}
|
||||
className={className}
|
||||
quality={quality}
|
||||
sizes={sizes}
|
||||
isCompleted={isCompleted}
|
||||
/>
|
||||
{showProgress && <ProgressBar progress={readBooks} total={totalBooks} type="series" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user