refactor: refine home and library visual hierarchy

This commit is contained in:
2026-02-28 21:11:07 +01:00
parent 83212434f2
commit 6ce8a6e38d
8 changed files with 55 additions and 46 deletions

View File

@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation";
import { RefreshButton } from "@/components/library/RefreshButton";
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
import { useTranslate } from "@/hooks/useTranslate";
interface HomeClientWrapperProps {
children: ReactNode;
@@ -13,7 +12,6 @@ interface HomeClientWrapperProps {
export function HomeClientWrapper({ children }: HomeClientWrapperProps) {
const router = useRouter();
const { t } = useTranslate();
const [isRefreshing, setIsRefreshing] = useState(false);
const handleRefresh = async () => {
@@ -45,12 +43,16 @@ export function HomeClientWrapper({ children }: HomeClientWrapperProps) {
canRefresh={pullToRefresh.canRefresh}
isHiding={pullToRefresh.isHiding}
/>
<main className="container mx-auto px-4 py-8 space-y-12">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">{t("home.title")}</h1>
<RefreshButton libraryId="home" refreshLibrary={handleRefresh} />
<main className="relative isolate overflow-hidden">
<div className="pointer-events-none absolute inset-0 -z-10 bg-[linear-gradient(180deg,hsl(var(--background)/0.99)_0%,hsl(var(--background)/0.95)_28%,hsl(var(--background))_100%)]" />
<div className="pointer-events-none absolute inset-0 -z-10 bg-[linear-gradient(128deg,hsl(var(--primary)/0.14)_0%,transparent_36%),linear-gradient(235deg,hsl(185_82%_54%/0.1)_4%,transparent_34%),linear-gradient(320deg,hsl(332_82%_63%/0.08)_8%,transparent_32%)]" />
<div className="container mx-auto space-y-12 px-4 py-8">
<div className="flex justify-end">
<RefreshButton libraryId="home" refreshLibrary={handleRefresh} />
</div>
{children}
</div>
{children}
</main>
</>
);

View File

@@ -29,7 +29,17 @@ const optimizeBookData = (books: KomgaBook[]) => {
export function HomeContent({ data }: HomeContentProps) {
return (
<div className="space-y-12">
<div className="space-y-10 pb-2">
{data.ongoingBooks && data.ongoingBooks.length > 0 && (
<div className="rounded-2xl border border-primary/20 bg-[linear-gradient(145deg,hsl(var(--primary)/0.12),hsl(var(--background)/0.1)_45%)] p-4 sm:p-5">
<MediaRow
titleKey="home.sections.continue_reading"
items={optimizeBookData(data.ongoingBooks)}
iconName="BookOpen"
/>
</div>
)}
{data.ongoing && data.ongoing.length > 0 && (
<MediaRow
titleKey="home.sections.continue_series"
@@ -38,14 +48,6 @@ export function HomeContent({ data }: HomeContentProps) {
/>
)}
{data.ongoingBooks && data.ongoingBooks.length > 0 && (
<MediaRow
titleKey="home.sections.continue_reading"
items={optimizeBookData(data.ongoingBooks)}
iconName="BookOpen"
/>
)}
{data.onDeck && data.onDeck.length > 0 && (
<MediaRow
titleKey="home.sections.up_next"

View File

@@ -64,7 +64,12 @@ export function MediaRow({ titleKey, items, iconName }: MediaRowProps) {
if (!items.length) return null;
return (
<Section title={t(titleKey)} icon={icon}>
<Section
title={t(titleKey)}
icon={icon}
className="space-y-5"
headerClassName="border-b border-border/50 pb-2"
>
<ScrollContainer
showArrows={true}
scrollAmount={400}
@@ -106,7 +111,7 @@ function MediaCard({ item, onClick }: MediaCardProps) {
<Card
onClick={handleClick}
className={cn(
"flex-shrink-0 w-[200px] relative flex flex-col hover:bg-accent hover:text-accent-foreground transition-colors overflow-hidden",
"relative flex w-[188px] flex-shrink-0 flex-col overflow-hidden rounded-xl border border-border/60 bg-card/85 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-card hover:shadow-md sm:w-[200px]",
!isSeries && !isAccessible ? "cursor-not-allowed" : "cursor-pointer"
)}
>
@@ -114,11 +119,11 @@ function MediaCard({ item, onClick }: MediaCardProps) {
{isSeries ? (
<>
<SeriesCover series={item as KomgaSeries} alt={`Couverture de ${title}`} />
<div className="absolute inset-0 bg-black/60 opacity-0 hover:opacity-100 transition-opacity duration-200 flex flex-col justify-end p-3">
<h3 className="font-medium text-sm text-white line-clamp-2">{title}</h3>
<p className="text-xs text-white/80 mt-1">
{t("series.books", { count: item.booksCount })}
</p>
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black/75 via-black/30 to-transparent p-3 opacity-0 transition-opacity duration-200 hover:opacity-100">
<h3 className="font-medium text-sm text-white line-clamp-2">{title}</h3>
<p className="text-xs text-white/80 mt-1">
{t("series.books", { count: item.booksCount })}
</p>
</div>
</>
) : (

View File

@@ -36,14 +36,14 @@ export function LibraryHeader({
const seriesLabel = `${seriesCount} ${seriesCount > 1 ? "series" : "serie"}`;
return (
<div className="relative min-h-[200px] md:h-[200px] w-screen -ml-[calc((100vw-100%)/2)] overflow-hidden">
<div className="relative min-h-[220px] md:h-[220px] w-screen -ml-[calc((100vw-100%)/2)] overflow-hidden border-y border-border/60">
<div className="absolute inset-0">
<div className="absolute inset-0 bg-black/40" />
<div className="absolute inset-0 bg-gradient-to-r from-background/85 via-background/65 to-background/85" />
{background ? (
<SeriesCover
series={background}
alt=""
className="blur-sm scale-105 brightness-50"
className="scale-105 blur-sm brightness-50"
showProgressUi={false}
/>
) : (
@@ -51,9 +51,9 @@ export function LibraryHeader({
)}
</div>
<div className="relative container mx-auto px-4 py-8 h-full">
<div className="flex flex-col md:flex-row gap-6 items-center md:items-start h-full">
<div className="relative w-[120px] h-[120px] rounded-lg overflow-hidden shadow-lg flex-shrink-0">
<div className="relative container mx-auto h-full px-4 py-8">
<div className="flex h-full flex-col items-center gap-6 md:flex-row md:items-start">
<div className="relative h-[120px] w-[120px] flex-shrink-0 overflow-hidden rounded-xl border border-border/60 shadow-lg">
{featured ? (
<div className="relative w-full h-full">
<SeriesCover
@@ -73,10 +73,10 @@ export function LibraryHeader({
)}
</div>
<div className="flex-1 space-y-3 text-center md:text-left">
<h1 className="text-3xl md:text-4xl font-bold text-foreground">{library.name}</h1>
<div className="flex-1 space-y-4 text-center md:text-left">
<h1 className="text-3xl font-bold text-foreground md:text-4xl">{library.name}</h1>
<div className="flex items-center gap-4 justify-center md:justify-start flex-wrap">
<div className="flex flex-wrap items-center justify-center gap-3 rounded-xl border border-border/60 bg-background/45 p-2 backdrop-blur-sm md:justify-start">
<StatusBadge status="unread" icon={Library}>
{seriesLabel}
</StatusBadge>

View File

@@ -171,19 +171,19 @@ export function PaginatedSeriesGrid({
return (
<div className="space-y-8">
<div className="flex flex-col gap-4">
<p className="text-sm text-muted-foreground text-right">{getShowingText()}</p>
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
<div className="rounded-xl border border-border/60 bg-background/35 p-4 shadow-sm backdrop-blur-sm">
<div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-center">
<div className="w-full">
<SearchInput placeholder={t("series.filters.search")} />
</div>
<div className="flex items-center justify-end gap-2">
<div className="flex items-center justify-end gap-2 rounded-full border border-border/50 bg-background/40 px-2 py-1">
<PageSizeSelect pageSize={effectivePageSize} onSizeChange={handlePageSizeChange} />
<ViewModeButton viewMode={viewMode} onToggle={handleViewModeToggle} />
<CompactModeButton isCompact={isCompact} onToggle={handleCompactModeToggle} />
<UnreadFilterButton showOnlyUnread={showOnlyUnread} onToggle={handleUnreadFilter} />
</div>
</div>
<p className="text-right text-sm text-muted-foreground">{getShowingText()}</p>
</div>
{viewMode === "grid" ? (

View File

@@ -34,12 +34,12 @@ export const SearchInput = ({ placeholder }: SearchInputProps) => {
}, 300);
return (
<div className="relative w-full max-w-md">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<div className="relative w-full max-w-md rounded-full border border-border/60 bg-background/45 shadow-sm backdrop-blur-sm transition-colors focus-within:border-primary/40 focus-within:ring-2 focus-within:ring-ring/30">
<Search className="absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
type={isPending ? "text" : "search"}
placeholder={placeholder}
className="pl-3"
className="h-11 rounded-full border-0 bg-transparent pl-10 pr-10 shadow-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-offset-0"
defaultValue={searchParams.get("search") ?? ""}
onChange={(e) => handleSearch(e.target.value)}
aria-label={placeholder}

View File

@@ -36,7 +36,7 @@ const getReadingStatusInfo = (
read: series.booksReadCount,
total: series.booksCount,
}),
className: "bg-blue-500/10 text-blue-500",
className: "bg-primary/15 text-primary",
};
}
@@ -61,7 +61,7 @@ export function SeriesGrid({ series, isCompact = false }: SeriesGridProps) {
return (
<div
className={cn(
"grid gap-4",
"grid gap-4 md:gap-5",
isCompact
? "grid-cols-3 sm:grid-cols-4 lg:grid-cols-6"
: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5"
@@ -72,7 +72,7 @@ export function SeriesGrid({ series, isCompact = false }: SeriesGridProps) {
key={series.id}
onClick={() => router.push(`/series/${series.id}`)}
className={cn(
"group relative aspect-[2/3] overflow-hidden rounded-lg bg-muted",
"group relative aspect-[2/3] overflow-hidden rounded-xl border border-border/60 bg-card/80 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md",
series.booksCount === series.booksReadCount && "opacity-50",
isCompact && "aspect-[3/4]"
)}
@@ -81,7 +81,7 @@ export function SeriesGrid({ series, isCompact = false }: SeriesGridProps) {
series={series as KomgaSeries}
alt={t("series.coverAlt", { title: series.metadata.title })}
/>
<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">
<div className="absolute inset-x-0 bottom-0 translate-y-full space-y-2 bg-gradient-to-t from-black/75 via-black/25 to-transparent p-4 transition-transform duration-200 group-hover:translate-y-0">
<h3 className="font-medium text-sm text-white line-clamp-2">{series.metadata.title}</h3>
<div className="flex items-center gap-2">
<span

View File

@@ -44,7 +44,7 @@ const getReadingStatusInfo = (
read: series.booksReadCount,
total: series.booksCount,
}),
className: "bg-blue-500/10 text-blue-500",
className: "bg-primary/15 text-primary",
};
}
@@ -72,7 +72,7 @@ function SeriesListItem({ series, isCompact = false }: SeriesListItemProps) {
return (
<div
className={cn(
"group relative flex gap-3 p-2 rounded-lg border bg-card hover:bg-accent/50 transition-colors cursor-pointer",
"group relative flex cursor-pointer gap-3 rounded-lg border border-border/60 bg-background/35 p-2 transition-colors hover:bg-accent/35",
isCompleted && "opacity-75"
)}
onClick={handleClick}
@@ -128,7 +128,7 @@ function SeriesListItem({ series, isCompact = false }: SeriesListItemProps) {
return (
<div
className={cn(
"group relative flex gap-4 p-4 rounded-lg border bg-card hover:bg-accent/50 transition-colors cursor-pointer",
"group relative flex cursor-pointer gap-4 rounded-xl border border-border/60 bg-background/35 p-4 transition-all duration-200 hover:bg-accent/35 hover:shadow-sm",
isCompleted && "opacity-75"
)}
onClick={handleClick}