feat: review series search panel

This commit is contained in:
2026-02-28 21:37:39 +01:00
parent 25ede2532e
commit 41faa30453
6 changed files with 104 additions and 36 deletions

View File

@@ -2,18 +2,22 @@ import { useDisplayPreferences } from "@/hooks/useDisplayPreferences";
import { useTranslate } from "@/hooks/useTranslate";
import { LayoutGrid, LayoutTemplate } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface CompactModeButtonProps {
onToggle?: (isCompact: boolean) => void;
isCompact?: boolean;
className?: string;
}
function CompactModeButtonBase({
isCompact,
onToggle,
className,
}: {
isCompact: boolean;
onToggle: (isCompact: boolean) => Promise<void> | void;
className?: string;
}) {
const { t } = useTranslate();
@@ -31,15 +35,21 @@ function CompactModeButtonBase({
size="sm"
onClick={handleClick}
title={label}
className="whitespace-nowrap"
className={cn(
"h-9 rounded-full border border-border/60 bg-background/40 px-3 text-xs font-medium backdrop-blur-sm hover:bg-accent/40 sm:text-sm",
className
)}
>
<Icon className="h-4 w-4" />
<span className="hidden sm:inline ml-2">{label}</span>
<span className="ml-2 hidden whitespace-nowrap min-[420px]:inline">{label}</span>
</Button>
);
}
function CompactModeButtonUncontrolled({ onToggle }: Pick<CompactModeButtonProps, "onToggle">) {
function CompactModeButtonUncontrolled({
onToggle,
className,
}: Pick<CompactModeButtonProps, "onToggle" | "className">) {
const { isCompact, handleCompactToggle } = useDisplayPreferences();
const handleToggle = async (nextCompactMode: boolean) => {
@@ -47,15 +57,17 @@ function CompactModeButtonUncontrolled({ onToggle }: Pick<CompactModeButtonProps
onToggle?.(nextCompactMode);
};
return <CompactModeButtonBase isCompact={isCompact} onToggle={handleToggle} />;
return (
<CompactModeButtonBase isCompact={isCompact} onToggle={handleToggle} className={className} />
);
}
export function CompactModeButton({ onToggle, isCompact }: CompactModeButtonProps) {
export function CompactModeButton({ onToggle, isCompact, className }: CompactModeButtonProps) {
const isControlled = typeof isCompact === "boolean" && typeof onToggle === "function";
if (isControlled) {
return <CompactModeButtonBase isCompact={isCompact} onToggle={onToggle} />;
return <CompactModeButtonBase isCompact={isCompact} onToggle={onToggle} className={className} />;
}
return <CompactModeButtonUncontrolled onToggle={onToggle} />;
return <CompactModeButtonUncontrolled onToggle={onToggle} className={className} />;
}

View File

@@ -7,18 +7,22 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
interface PageSizeSelectProps {
onSizeChange?: (size: number) => void;
pageSize?: number;
className?: string;
}
function PageSizeSelectBase({
value,
onChange,
className,
}: {
value: number;
onChange: (size: number) => Promise<void> | void;
className?: string;
}) {
const handleChange = async (rawValue: string) => {
const size = parseInt(rawValue);
@@ -27,7 +31,12 @@ function PageSizeSelectBase({
return (
<Select value={value.toString()} onValueChange={handleChange}>
<SelectTrigger className="w-[80px]">
<SelectTrigger
className={cn(
"h-9 w-[96px] rounded-full border border-border/60 bg-background/40 text-xs font-medium backdrop-blur-sm sm:text-sm",
className
)}
>
<LayoutList className="h-4 w-4" />
<SelectValue className="ml-2" />
</SelectTrigger>
@@ -40,7 +49,10 @@ function PageSizeSelectBase({
);
}
function PageSizeSelectUncontrolled({ onSizeChange }: Pick<PageSizeSelectProps, "onSizeChange">) {
function PageSizeSelectUncontrolled({
onSizeChange,
className,
}: Pick<PageSizeSelectProps, "onSizeChange" | "className">) {
const { itemsPerPage, handlePageSizeChange } = useDisplayPreferences();
const onChange = async (size: number) => {
@@ -48,15 +60,15 @@ function PageSizeSelectUncontrolled({ onSizeChange }: Pick<PageSizeSelectProps,
onSizeChange?.(size);
};
return <PageSizeSelectBase value={itemsPerPage} onChange={onChange} />;
return <PageSizeSelectBase value={itemsPerPage} onChange={onChange} className={className} />;
}
export function PageSizeSelect({ onSizeChange, pageSize }: PageSizeSelectProps) {
export function PageSizeSelect({ onSizeChange, pageSize, className }: PageSizeSelectProps) {
const isControlled = typeof pageSize === "number" && typeof onSizeChange === "function";
if (isControlled) {
return <PageSizeSelectBase value={pageSize} onChange={onSizeChange} />;
return <PageSizeSelectBase value={pageSize} onChange={onSizeChange} className={className} />;
}
return <PageSizeSelectUncontrolled onSizeChange={onSizeChange} />;
return <PageSizeSelectUncontrolled onSizeChange={onSizeChange} className={className} />;
}

View File

@@ -3,13 +3,19 @@
import { useTranslate } from "@/hooks/useTranslate";
import { Filter } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface UnreadFilterButtonProps {
showOnlyUnread: boolean;
onToggle: () => void;
className?: string;
}
export function UnreadFilterButton({ showOnlyUnread, onToggle }: UnreadFilterButtonProps) {
export function UnreadFilterButton({
showOnlyUnread,
onToggle,
className,
}: UnreadFilterButtonProps) {
const { t } = useTranslate();
const label = showOnlyUnread ? t("series.filters.showAll") : t("series.filters.unread");
@@ -20,10 +26,16 @@ export function UnreadFilterButton({ showOnlyUnread, onToggle }: UnreadFilterBut
size="sm"
onClick={onToggle}
title={label}
className="whitespace-nowrap"
className={cn(
"h-9 rounded-full border px-3 text-xs font-medium backdrop-blur-sm sm:text-sm",
showOnlyUnread
? "border-primary/40 bg-primary/15 text-primary hover:bg-primary/20"
: "border-border/60 bg-background/40 hover:bg-accent/40",
className
)}
>
<Filter className="h-4 w-4" />
<span className="hidden sm:inline ml-2">{label}</span>
<span className="ml-2 hidden whitespace-nowrap min-[420px]:inline">{label}</span>
</Button>
);
}

View File

@@ -2,18 +2,22 @@ import { useDisplayPreferences } from "@/hooks/useDisplayPreferences";
import { useTranslate } from "@/hooks/useTranslate";
import { LayoutGrid, List } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface ViewModeButtonProps {
onToggle?: (viewMode: "grid" | "list") => void;
viewMode?: "grid" | "list";
className?: string;
}
function ViewModeButtonBase({
viewMode,
onToggle,
className,
}: {
viewMode: "grid" | "list";
onToggle: (viewMode: "grid" | "list") => Promise<void> | void;
className?: string;
}) {
const { t } = useTranslate();
@@ -31,15 +35,21 @@ function ViewModeButtonBase({
size="sm"
onClick={handleClick}
title={label}
className="whitespace-nowrap"
className={cn(
"h-9 rounded-full border border-border/60 bg-background/40 px-3 text-xs font-medium backdrop-blur-sm hover:bg-accent/40 sm:text-sm",
className
)}
>
<Icon className="h-4 w-4" />
<span className="hidden sm:inline ml-2">{label}</span>
<span className="ml-2 hidden whitespace-nowrap min-[420px]:inline">{label}</span>
</Button>
);
}
function ViewModeButtonUncontrolled({ onToggle }: Pick<ViewModeButtonProps, "onToggle">) {
function ViewModeButtonUncontrolled({
onToggle,
className,
}: Pick<ViewModeButtonProps, "onToggle" | "className">) {
const { viewMode, handleViewModeToggle } = useDisplayPreferences();
const handleToggle = async (nextViewMode: "grid" | "list") => {
@@ -47,15 +57,15 @@ function ViewModeButtonUncontrolled({ onToggle }: Pick<ViewModeButtonProps, "onT
onToggle?.(nextViewMode);
};
return <ViewModeButtonBase viewMode={viewMode} onToggle={handleToggle} />;
return <ViewModeButtonBase viewMode={viewMode} onToggle={handleToggle} className={className} />;
}
export function ViewModeButton({ onToggle, viewMode }: ViewModeButtonProps) {
export function ViewModeButton({ onToggle, viewMode, className }: ViewModeButtonProps) {
const isControlled = typeof viewMode === "string" && typeof onToggle === "function";
if (isControlled) {
return <ViewModeButtonBase viewMode={viewMode} onToggle={onToggle} />;
return <ViewModeButtonBase viewMode={viewMode} onToggle={onToggle} className={className} />;
}
return <ViewModeButtonUncontrolled onToggle={onToggle} />;
return <ViewModeButtonUncontrolled onToggle={onToggle} className={className} />;
}

View File

@@ -171,19 +171,41 @@ export function PaginatedSeriesGrid({
return (
<div className="space-y-8">
<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 className="rounded-2xl border border-border/60 bg-[linear-gradient(140deg,hsl(var(--background)/0.6),hsl(var(--background)/0.38))] p-4 shadow-sm backdrop-blur-sm sm:p-5">
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div className="space-y-1">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
Explorer
</p>
<h2 className="text-xl font-semibold tracking-tight sm:text-2xl">Séries</h2>
</div>
<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} />
<p className="text-sm text-muted-foreground">{getShowingText()}</p>
</div>
<div className="space-y-3">
<SearchInput placeholder={t("series.filters.search")} />
<div className="pb-1">
<div className="flex flex-wrap items-center gap-2">
<UnreadFilterButton
showOnlyUnread={showOnlyUnread}
onToggle={handleUnreadFilter}
/>
<ViewModeButton
viewMode={viewMode}
onToggle={handleViewModeToggle}
/>
<CompactModeButton
isCompact={isCompact}
onToggle={handleCompactModeToggle}
/>
<PageSizeSelect
pageSize={effectivePageSize}
onSizeChange={handlePageSizeChange}
/>
</div>
</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 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" />
<div className="relative w-full rounded-2xl border border-border/60 bg-[linear-gradient(140deg,hsl(var(--background)/0.72),hsl(var(--background)/0.5))] px-1 shadow-sm backdrop-blur-sm transition-colors focus-within:border-primary/40 focus-within:ring-2 focus-within:ring-ring/30 sm:max-w-2xl">
<Search className="absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
type={isPending ? "text" : "search"}
placeholder={placeholder}
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"
className="h-12 rounded-xl border-0 bg-transparent pl-10 pr-10 text-sm 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}