added category filter bar; clear filter button

This commit is contained in:
mr-shortman 2025-03-15 15:43:56 +01:00
parent 4ad0357133
commit 1ccbcddae3
5 changed files with 169 additions and 40 deletions

View File

@ -2,19 +2,10 @@
import { cn, debounce } from "@/lib/utils";
import React from "react";
import { Input } from "../ui/input";
import {
ArrowDownAZ,
ArrowUpAz,
CalendarArrowDown,
CalendarArrowUp,
Eye,
FilterIcon,
LucideIcon,
MessageSquare,
SearchIcon,
} from "lucide-react";
import { SearchIcon, XIcon } from "lucide-react";
import CategorySelect from "../category/category-select";
import { Combobox } from "../combobox";
import SortFilterSelect from "../sort-filter-select";
import { Button } from "../ui/button";
export type ArticleFilter = {
query?: string;
@ -22,6 +13,12 @@ export type ArticleFilter = {
sort?: string;
};
const defaultFilter: ArticleFilter = {
query: "",
category: undefined,
sort: undefined,
};
function ArticleFilterBar({
className,
onFilterUpdate,
@ -29,9 +26,7 @@ function ArticleFilterBar({
className?: string;
onFilterUpdate: (filter: ArticleFilter) => void;
}) {
const [filter, setFilter] = React.useState<ArticleFilter>({
query: "",
});
const [filter, setFilter] = React.useState<ArticleFilter>(defaultFilter);
const onFilterChange = React.useCallback(
(newFilter: Partial<ArticleFilter>, notify: boolean = true) => {
@ -49,6 +44,8 @@ function ArticleFilterBar({
}, 300),
[],
);
const hasFilter =
filter.query?.length || filter.sort?.length || filter.category?.length;
return (
<>
@ -71,6 +68,21 @@ function ArticleFilterBar({
/>
</div>
<div className="flex w-full flex-col gap-2 sm:flex-row">
<Button
disabled={!hasFilter}
variant={"ghost"}
className={cn(
"text-xs text-muted-foreground",
hasFilter
? "flex lg:opacity-100"
: "hidden lg:opacity-0 lg:disabled:opacity-0",
)}
onClick={() => onFilterChange(defaultFilter)}
>
<span>Filter entfernen</span>
<XIcon className="size-4" />
</Button>
<CategorySelect
className="w-full"
onSelect={(category) => {
@ -82,9 +94,7 @@ function ArticleFilterBar({
size: "sm",
}}
/>
<Combobox
hideSearch
data={sortItems}
<SortFilterSelect
initialValue={filter.sort}
onSelect={(currentValue) => {
onFilterChange({
@ -92,10 +102,6 @@ function ArticleFilterBar({
});
}}
className="w-full lg:max-w-64"
messageUi={{
selectIcon: FilterIcon,
select: "Sortieren",
}}
buttonProps={{
size: "sm",
}}
@ -107,14 +113,3 @@ function ArticleFilterBar({
}
export default ArticleFilterBar;
const sortItems: Array<{
Icon: LucideIcon;
value: string;
label: string;
}> = [
{ Icon: CalendarArrowUp, value: "newest", label: "Neueste" },
{ Icon: CalendarArrowDown, value: "oldest", label: "Älteste" },
{ Icon: ArrowDownAZ, value: "abc", label: "Alphabetisch A-Z" },
{ Icon: ArrowUpAz, value: "cba", label: "Alphabetisch Z-A" },
];

View File

@ -0,0 +1,99 @@
"use client";
import { cn, debounce } from "@/lib/utils";
import React from "react";
import { Input } from "../ui/input";
import { SearchIcon, XIcon } from "lucide-react";
import CategorySelect from "../category/category-select";
import SortFilterSelect from "../sort-filter-select";
import { Button } from "../ui/button";
export type CategoryFilter = {
query?: string;
sort?: string;
};
const defaultFilter: CategoryFilter = {
query: "",
sort: undefined,
};
export default function CategoryFilterBar({
className,
onFilterUpdate,
}: {
className?: string;
onFilterUpdate: (filter: CategoryFilter) => void;
}) {
const [filter, setFilter] = React.useState<CategoryFilter>(defaultFilter);
const onFilterChange = React.useCallback(
(newFilter: Partial<CategoryFilter>, notify: boolean = true) => {
const f = { ...filter, ...newFilter };
setFilter(f);
if (notify) onFilterUpdate(f);
},
[filter, onFilterUpdate], // Ensure dependencies are listed
);
const debouncedSearch = React.useMemo(
() =>
debounce((query: string) => {
onFilterChange({ query });
}, 300),
[],
);
const hasFilter = filter.query?.length || filter.sort?.length;
return (
<>
<div
className={cn(
"flex w-full flex-col-reverse items-center justify-center gap-2 rounded-md border bg-background p-2 lg:flex-row lg:gap-4",
className,
)}
>
<div className="relative w-full">
<SearchIcon className="absolute left-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
className="border-0 p-0 pl-8 shadow-none focus-visible:ring-0"
value={filter.query}
onChange={(e) => {
onFilterChange({ query: e.currentTarget.value }, false);
debouncedSearch(e.currentTarget.value);
}}
placeholder="Kategorie Suchen..."
/>
</div>
<div className="flex w-full flex-col items-center justify-end gap-2 sm:flex-row">
<Button
disabled={!hasFilter}
variant={"ghost"}
className={cn(
"text-xs text-muted-foreground",
hasFilter
? "flex lg:opacity-100"
: "hidden lg:opacity-0 lg:disabled:opacity-0",
)}
onClick={() => onFilterChange(defaultFilter)}
>
<span>Filter entfernen</span>
<XIcon className="size-4" />
</Button>
<SortFilterSelect
initialValue={filter.sort}
onSelect={(currentValue) => {
onFilterChange({
sort: currentValue?.length ? currentValue : undefined,
});
}}
className="w-full lg:max-w-64"
buttonProps={{
size: "sm",
}}
/>
</div>
</div>
</>
);
}

View File

@ -7,16 +7,14 @@ import CategoryCard from "../category-card";
import { useInfiniteItemsObserver } from "@/lib/hooks/infinite-items-observer-hook";
import { Skeleton } from "@/components/ui/skeleton";
import { CATEGORY_GRID_CLASS } from "./category-grid";
// import ArticleFilterBar, { ArticleFilter } from "../article-filter-bar";
import CategoryFilterBar, { CategoryFilter } from "../category-filter-bar";
export default function InfiniteCategoryGrid() {
// const [filter, setFilter] = React.useState<ArticleFilter | undefined>(
// undefined,
// );
const [filter, setFilter] = React.useState<CategoryFilter>({});
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
api.category.getByCursor.useInfiniteQuery(
{
// filter,
filter,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
@ -39,7 +37,7 @@ export default function InfiniteCategoryGrid() {
return (
<div className="relative space-y-4">
{/* <ArticleFilterBar onFilterUpdate={setFilter} /> */}
<CategoryFilterBar onFilterUpdate={setFilter} />
<menu className={`${CATEGORY_GRID_CLASS} overflow-auto`}>
{data?.pages?.length
? allItems.map((category, idx) => (

View File

@ -49,7 +49,7 @@ export function Combobox({
}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(initialValue ?? "");
const selectedItem = data.find((item) => item.value === value)!;
const selectedItem = data.find((item) => item.value === initialValue)!;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>

View File

@ -0,0 +1,37 @@
import React from "react";
import {
ArrowDownAZ,
ArrowUpAz,
CalendarArrowDown,
CalendarArrowUp,
FilterIcon,
LucideIcon,
} from "lucide-react";
import { Combobox, ComboboxProps } from "./combobox";
function SortFilterSelect(props: Partial<ComboboxProps>) {
return (
<Combobox
hideSearch
messageUi={{
selectIcon: FilterIcon,
select: "Sortieren",
}}
{...(props as ComboboxProps)}
data={sortItems}
/>
);
}
export default SortFilterSelect;
const sortItems: Array<{
Icon: LucideIcon;
value: string;
label: string;
}> = [
{ Icon: CalendarArrowUp, value: "newest", label: "Neueste" },
{ Icon: CalendarArrowDown, value: "oldest", label: "Älteste" },
{ Icon: ArrowDownAZ, value: "abc", label: "Alphabetisch A-Z" },
{ Icon: ArrowUpAz, value: "cba", label: "Alphabetisch Z-A" },
];