added category filter bar; clear filter button
This commit is contained in:
parent
4ad0357133
commit
1ccbcddae3
@ -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" },
|
||||
];
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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) => (
|
||||
|
||||
@ -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>
|
||||
|
||||
37
src/components/sort-filter-select.tsx
Normal file
37
src/components/sort-filter-select.tsx
Normal 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" },
|
||||
];
|
||||
Loading…
x
Reference in New Issue
Block a user