fix: aligner la page livre sur le layout de la page série
- Cover w-48 → w-40 (cohérent avec la page série) - Titre séparé, auteur + série + statut de lecture regroupés en badges - Métadonnées (format, pages, langue, ISBN, date) en ligne texte au lieu de pills (style série) - Toolbar d'actions groupée en bas (Edit, MarkRead, Convert, Delete) - Tous les boutons d'action (MarkBookRead, Convert, Delete) alignés en py-1.5 au lieu de Button size=sm (h-9) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,13 +94,13 @@ export default async function BookDetailPage({
|
||||
<div className="flex flex-col sm:flex-row gap-6">
|
||||
{/* Cover */}
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-48 aspect-[2/3] relative rounded-xl overflow-hidden shadow-card border border-border">
|
||||
<div className="w-40 aspect-[2/3] relative rounded-xl overflow-hidden shadow-card border border-border">
|
||||
<Image
|
||||
src={getBookCoverUrl(book.id)}
|
||||
alt={t("bookDetail.coverOf", { title: book.title })}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="192px"
|
||||
sizes="160px"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
@@ -108,36 +108,24 @@ export default async function BookDetailPage({
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
{/* Title */}
|
||||
<h1 className="text-3xl font-bold text-foreground">{book.title}</h1>
|
||||
{book.author && (
|
||||
<p className="text-base text-muted-foreground mt-1">{book.author}</p>
|
||||
)}
|
||||
</div>
|
||||
<EditBookForm book={book} />
|
||||
</div>
|
||||
|
||||
{/* Series + Volume link */}
|
||||
{/* Author + Series + Volume + Reading status badges */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{book.author && (
|
||||
<p className="text-base text-muted-foreground">{book.author}</p>
|
||||
)}
|
||||
{book.series && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Link
|
||||
href={`/libraries/${book.library_id}/series/${encodeURIComponent(book.series)}`}
|
||||
className="text-primary hover:text-primary/80 transition-colors font-medium"
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-primary/10 text-primary text-xs border border-primary/30 font-medium"
|
||||
>
|
||||
{book.series}
|
||||
{book.volume != null && <span className="font-semibold">Vol. {book.volume}</span>}
|
||||
</Link>
|
||||
{book.volume != null && (
|
||||
<span className="px-2 py-0.5 bg-primary/10 text-primary rounded-md text-xs font-semibold">
|
||||
Vol. {book.volume}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reading status + actions */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold ${statusClassName}`}>
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusClassName}`}>
|
||||
{statusLabel}
|
||||
{book.reading_status === "reading" && book.reading_current_page != null && ` · p. ${book.reading_current_page}`}
|
||||
</span>
|
||||
@@ -146,35 +134,24 @@ export default async function BookDetailPage({
|
||||
{new Date(book.reading_last_read_at).toLocaleDateString(locale)}
|
||||
</span>
|
||||
)}
|
||||
<MarkBookReadButton bookId={book.id} currentStatus={book.reading_status} />
|
||||
{book.file_format === "cbr" && <ConvertButton bookId={book.id} />}
|
||||
<DeleteBookButton bookId={book.id} libraryId={book.library_id} />
|
||||
</div>
|
||||
|
||||
{/* Metadata pills */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className={`inline-flex px-2.5 py-1 rounded-full text-xs font-semibold border ${formatColor}`}>
|
||||
{/* Metadata stats */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm">
|
||||
<span className={`inline-flex px-2.5 py-0.5 rounded-full text-xs font-semibold border ${formatColor}`}>
|
||||
{formatBadge}
|
||||
</span>
|
||||
{book.page_count && (
|
||||
<span className="inline-flex px-2.5 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">
|
||||
{book.page_count} {t("dashboard.pages").toLowerCase()}
|
||||
</span>
|
||||
<span className="text-muted-foreground"><span className="font-semibold text-foreground">{book.page_count}</span> {t("dashboard.pages").toLowerCase()}</span>
|
||||
)}
|
||||
{book.language && (
|
||||
<span className="inline-flex px-2.5 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">
|
||||
{book.language.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-muted-foreground">{book.language.toUpperCase()}</span>
|
||||
)}
|
||||
{book.isbn && (
|
||||
<span className="inline-flex px-2.5 py-1 rounded-full text-xs font-mono font-medium bg-muted/50 text-muted-foreground border border-border">
|
||||
ISBN {book.isbn}
|
||||
</span>
|
||||
<span className="text-muted-foreground font-mono">ISBN {book.isbn}</span>
|
||||
)}
|
||||
{book.publish_date && (
|
||||
<span className="inline-flex px-2.5 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">
|
||||
{book.publish_date}
|
||||
</span>
|
||||
<span className="text-muted-foreground">{book.publish_date}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -182,6 +159,14 @@ export default async function BookDetailPage({
|
||||
{book.summary && (
|
||||
<SafeHtml html={book.summary} className="text-sm text-muted-foreground leading-relaxed" />
|
||||
)}
|
||||
|
||||
{/* Action buttons toolbar */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<MarkBookReadButton bookId={book.id} currentStatus={book.reading_status} />
|
||||
<EditBookForm book={book} />
|
||||
{book.file_format === "cbr" && <ConvertButton bookId={book.id} />}
|
||||
<DeleteBookButton bookId={book.id} libraryId={book.library_id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -61,13 +61,13 @@ export function ConvertButton({ bookId }: ConvertButtonProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleConvert}
|
||||
disabled={state.type === "loading"}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border bg-card text-sm font-medium text-muted-foreground hover:text-foreground hover:border-primary transition-colors disabled:opacity-50"
|
||||
>
|
||||
{state.type === "loading" ? t("convert.converting") : t("convert.convertToCbz")}
|
||||
</Button>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,15 +26,15 @@ export function DeleteBookButton({ bookId, libraryId }: { bookId: string; librar
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirm(true)}
|
||||
disabled={deleting}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-destructive/30 bg-destructive/10 text-destructive text-sm font-medium hover:bg-destructive/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{deleting ? <Icon name="spinner" size="sm" className="animate-spin" /> : <Icon name="trash" size="sm" />}
|
||||
<span className="ml-1.5">{t("bookDetail.delete")}</span>
|
||||
</Button>
|
||||
{t("bookDetail.delete")}
|
||||
</button>
|
||||
|
||||
<Modal isOpen={showConfirm} onClose={() => setShowConfirm(false)} maxWidth="sm">
|
||||
<div className="p-6">
|
||||
|
||||
@@ -40,11 +40,15 @@ export function MarkBookReadButton({ bookId, currentStatus }: MarkBookReadButton
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={isRead ? "outline" : "primary"}
|
||||
size="sm"
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={loading}
|
||||
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-sm font-medium transition-colors disabled:opacity-50 ${
|
||||
isRead
|
||||
? "border-border bg-card text-muted-foreground hover:text-foreground hover:border-primary"
|
||||
: "border-green-500/30 bg-green-500/10 text-green-600 hover:bg-green-500/20"
|
||||
}`}
|
||||
>
|
||||
{loading ? (
|
||||
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
@@ -52,15 +56,15 @@ export function MarkBookReadButton({ bookId, currentStatus }: MarkBookReadButton
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
) : isRead ? (
|
||||
<svg className="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
|
||||
</svg>
|
||||
)}
|
||||
{!loading && label}
|
||||
</Button>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user