feat: review series search panel
This commit is contained in:
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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" ? (
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user