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 { 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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" ? (
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user