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 { useTranslate } from "@/hooks/useTranslate";
import { LayoutGrid, LayoutTemplate } from "lucide-react"; import { LayoutGrid, LayoutTemplate } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface CompactModeButtonProps { interface CompactModeButtonProps {
onToggle?: (isCompact: boolean) => void; onToggle?: (isCompact: boolean) => void;
isCompact?: boolean; isCompact?: boolean;
className?: string;
} }
function CompactModeButtonBase({ function CompactModeButtonBase({
isCompact, isCompact,
onToggle, onToggle,
className,
}: { }: {
isCompact: boolean; isCompact: boolean;
onToggle: (isCompact: boolean) => Promise<void> | void; onToggle: (isCompact: boolean) => Promise<void> | void;
className?: string;
}) { }) {
const { t } = useTranslate(); const { t } = useTranslate();
@@ -31,15 +35,21 @@ function CompactModeButtonBase({
size="sm" size="sm"
onClick={handleClick} onClick={handleClick}
title={label} 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" /> <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> </Button>
); );
} }
function CompactModeButtonUncontrolled({ onToggle }: Pick<CompactModeButtonProps, "onToggle">) { function CompactModeButtonUncontrolled({
onToggle,
className,
}: Pick<CompactModeButtonProps, "onToggle" | "className">) {
const { isCompact, handleCompactToggle } = useDisplayPreferences(); const { isCompact, handleCompactToggle } = useDisplayPreferences();
const handleToggle = async (nextCompactMode: boolean) => { const handleToggle = async (nextCompactMode: boolean) => {
@@ -47,15 +57,17 @@ function CompactModeButtonUncontrolled({ onToggle }: Pick<CompactModeButtonProps
onToggle?.(nextCompactMode); 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"; const isControlled = typeof isCompact === "boolean" && typeof onToggle === "function";
if (isControlled) { 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, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { cn } from "@/lib/utils";
interface PageSizeSelectProps { interface PageSizeSelectProps {
onSizeChange?: (size: number) => void; onSizeChange?: (size: number) => void;
pageSize?: number; pageSize?: number;
className?: string;
} }
function PageSizeSelectBase({ function PageSizeSelectBase({
value, value,
onChange, onChange,
className,
}: { }: {
value: number; value: number;
onChange: (size: number) => Promise<void> | void; onChange: (size: number) => Promise<void> | void;
className?: string;
}) { }) {
const handleChange = async (rawValue: string) => { const handleChange = async (rawValue: string) => {
const size = parseInt(rawValue); const size = parseInt(rawValue);
@@ -27,7 +31,12 @@ function PageSizeSelectBase({
return ( return (
<Select value={value.toString()} onValueChange={handleChange}> <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" /> <LayoutList className="h-4 w-4" />
<SelectValue className="ml-2" /> <SelectValue className="ml-2" />
</SelectTrigger> </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 { itemsPerPage, handlePageSizeChange } = useDisplayPreferences();
const onChange = async (size: number) => { const onChange = async (size: number) => {
@@ -48,15 +60,15 @@ function PageSizeSelectUncontrolled({ onSizeChange }: Pick<PageSizeSelectProps,
onSizeChange?.(size); 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"; const isControlled = typeof pageSize === "number" && typeof onSizeChange === "function";
if (isControlled) { 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 { useTranslate } from "@/hooks/useTranslate";
import { Filter } from "lucide-react"; import { Filter } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface UnreadFilterButtonProps { interface UnreadFilterButtonProps {
showOnlyUnread: boolean; showOnlyUnread: boolean;
onToggle: () => void; onToggle: () => void;
className?: string;
} }
export function UnreadFilterButton({ showOnlyUnread, onToggle }: UnreadFilterButtonProps) { export function UnreadFilterButton({
showOnlyUnread,
onToggle,
className,
}: UnreadFilterButtonProps) {
const { t } = useTranslate(); const { t } = useTranslate();
const label = showOnlyUnread ? t("series.filters.showAll") : t("series.filters.unread"); const label = showOnlyUnread ? t("series.filters.showAll") : t("series.filters.unread");
@@ -20,10 +26,16 @@ export function UnreadFilterButton({ showOnlyUnread, onToggle }: UnreadFilterBut
size="sm" size="sm"
onClick={onToggle} onClick={onToggle}
title={label} 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" /> <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> </Button>
); );
} }

View File

@@ -2,18 +2,22 @@ import { useDisplayPreferences } from "@/hooks/useDisplayPreferences";
import { useTranslate } from "@/hooks/useTranslate"; import { useTranslate } from "@/hooks/useTranslate";
import { LayoutGrid, List } from "lucide-react"; import { LayoutGrid, List } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface ViewModeButtonProps { interface ViewModeButtonProps {
onToggle?: (viewMode: "grid" | "list") => void; onToggle?: (viewMode: "grid" | "list") => void;
viewMode?: "grid" | "list"; viewMode?: "grid" | "list";
className?: string;
} }
function ViewModeButtonBase({ function ViewModeButtonBase({
viewMode, viewMode,
onToggle, onToggle,
className,
}: { }: {
viewMode: "grid" | "list"; viewMode: "grid" | "list";
onToggle: (viewMode: "grid" | "list") => Promise<void> | void; onToggle: (viewMode: "grid" | "list") => Promise<void> | void;
className?: string;
}) { }) {
const { t } = useTranslate(); const { t } = useTranslate();
@@ -31,15 +35,21 @@ function ViewModeButtonBase({
size="sm" size="sm"
onClick={handleClick} onClick={handleClick}
title={label} 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" /> <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> </Button>
); );
} }
function ViewModeButtonUncontrolled({ onToggle }: Pick<ViewModeButtonProps, "onToggle">) { function ViewModeButtonUncontrolled({
onToggle,
className,
}: Pick<ViewModeButtonProps, "onToggle" | "className">) {
const { viewMode, handleViewModeToggle } = useDisplayPreferences(); const { viewMode, handleViewModeToggle } = useDisplayPreferences();
const handleToggle = async (nextViewMode: "grid" | "list") => { const handleToggle = async (nextViewMode: "grid" | "list") => {
@@ -47,15 +57,15 @@ function ViewModeButtonUncontrolled({ onToggle }: Pick<ViewModeButtonProps, "onT
onToggle?.(nextViewMode); 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"; const isControlled = typeof viewMode === "string" && typeof onToggle === "function";
if (isControlled) { 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 ( return (
<div className="space-y-8"> <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="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-4 sm:flex-row sm:items-center"> <div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div className="w-full"> <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>
<p className="text-sm text-muted-foreground">{getShowingText()}</p>
</div>
<div className="space-y-3">
<SearchInput placeholder={t("series.filters.search")} /> <SearchInput placeholder={t("series.filters.search")} />
</div>
<div className="flex items-center justify-end gap-2 rounded-full border border-border/50 bg-background/40 px-2 py-1"> <div className="pb-1">
<PageSizeSelect pageSize={effectivePageSize} onSizeChange={handlePageSizeChange} /> <div className="flex flex-wrap items-center gap-2">
<ViewModeButton viewMode={viewMode} onToggle={handleViewModeToggle} /> <UnreadFilterButton
<CompactModeButton isCompact={isCompact} onToggle={handleCompactModeToggle} /> showOnlyUnread={showOnlyUnread}
<UnreadFilterButton showOnlyUnread={showOnlyUnread} onToggle={handleUnreadFilter} /> onToggle={handleUnreadFilter}
/>
<ViewModeButton
viewMode={viewMode}
onToggle={handleViewModeToggle}
/>
<CompactModeButton
isCompact={isCompact}
onToggle={handleCompactModeToggle}
/>
<PageSizeSelect
pageSize={effectivePageSize}
onSizeChange={handlePageSizeChange}
/>
</div>
</div> </div>
</div> </div>
<p className="text-right text-sm text-muted-foreground">{getShowingText()}</p>
</div> </div>
{viewMode === "grid" ? ( {viewMode === "grid" ? (

View File

@@ -34,12 +34,12 @@ export const SearchInput = ({ placeholder }: SearchInputProps) => {
}, 300); }, 300);
return ( 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"> <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-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
type={isPending ? "text" : "search"} type={isPending ? "text" : "search"}
placeholder={placeholder} 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") ?? ""} defaultValue={searchParams.get("search") ?? ""}
onChange={(e) => handleSearch(e.target.value)} onChange={(e) => handleSearch(e.target.value)}
aria-label={placeholder} aria-label={placeholder}