refactor: refine home and library visual hierarchy
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" ? (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user