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 { cn, debounce } from "@/lib/utils";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import {
|
import { SearchIcon, XIcon } from "lucide-react";
|
||||||
ArrowDownAZ,
|
|
||||||
ArrowUpAz,
|
|
||||||
CalendarArrowDown,
|
|
||||||
CalendarArrowUp,
|
|
||||||
Eye,
|
|
||||||
FilterIcon,
|
|
||||||
LucideIcon,
|
|
||||||
MessageSquare,
|
|
||||||
SearchIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import CategorySelect from "../category/category-select";
|
import CategorySelect from "../category/category-select";
|
||||||
import { Combobox } from "../combobox";
|
import SortFilterSelect from "../sort-filter-select";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
export type ArticleFilter = {
|
export type ArticleFilter = {
|
||||||
query?: string;
|
query?: string;
|
||||||
@ -22,6 +13,12 @@ export type ArticleFilter = {
|
|||||||
sort?: string;
|
sort?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultFilter: ArticleFilter = {
|
||||||
|
query: "",
|
||||||
|
category: undefined,
|
||||||
|
sort: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
function ArticleFilterBar({
|
function ArticleFilterBar({
|
||||||
className,
|
className,
|
||||||
onFilterUpdate,
|
onFilterUpdate,
|
||||||
@ -29,9 +26,7 @@ function ArticleFilterBar({
|
|||||||
className?: string;
|
className?: string;
|
||||||
onFilterUpdate: (filter: ArticleFilter) => void;
|
onFilterUpdate: (filter: ArticleFilter) => void;
|
||||||
}) {
|
}) {
|
||||||
const [filter, setFilter] = React.useState<ArticleFilter>({
|
const [filter, setFilter] = React.useState<ArticleFilter>(defaultFilter);
|
||||||
query: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const onFilterChange = React.useCallback(
|
const onFilterChange = React.useCallback(
|
||||||
(newFilter: Partial<ArticleFilter>, notify: boolean = true) => {
|
(newFilter: Partial<ArticleFilter>, notify: boolean = true) => {
|
||||||
@ -49,6 +44,8 @@ function ArticleFilterBar({
|
|||||||
}, 300),
|
}, 300),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
const hasFilter =
|
||||||
|
filter.query?.length || filter.sort?.length || filter.category?.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -71,6 +68,21 @@ function ArticleFilterBar({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col gap-2 sm:flex-row">
|
<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
|
<CategorySelect
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onSelect={(category) => {
|
onSelect={(category) => {
|
||||||
@ -82,9 +94,7 @@ function ArticleFilterBar({
|
|||||||
size: "sm",
|
size: "sm",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Combobox
|
<SortFilterSelect
|
||||||
hideSearch
|
|
||||||
data={sortItems}
|
|
||||||
initialValue={filter.sort}
|
initialValue={filter.sort}
|
||||||
onSelect={(currentValue) => {
|
onSelect={(currentValue) => {
|
||||||
onFilterChange({
|
onFilterChange({
|
||||||
@ -92,10 +102,6 @@ function ArticleFilterBar({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="w-full lg:max-w-64"
|
className="w-full lg:max-w-64"
|
||||||
messageUi={{
|
|
||||||
selectIcon: FilterIcon,
|
|
||||||
select: "Sortieren",
|
|
||||||
}}
|
|
||||||
buttonProps={{
|
buttonProps={{
|
||||||
size: "sm",
|
size: "sm",
|
||||||
}}
|
}}
|
||||||
@ -107,14 +113,3 @@ function ArticleFilterBar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default 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 { useInfiniteItemsObserver } from "@/lib/hooks/infinite-items-observer-hook";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { CATEGORY_GRID_CLASS } from "./category-grid";
|
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() {
|
export default function InfiniteCategoryGrid() {
|
||||||
// const [filter, setFilter] = React.useState<ArticleFilter | undefined>(
|
const [filter, setFilter] = React.useState<CategoryFilter>({});
|
||||||
// undefined,
|
|
||||||
// );
|
|
||||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
|
||||||
api.category.getByCursor.useInfiniteQuery(
|
api.category.getByCursor.useInfiniteQuery(
|
||||||
{
|
{
|
||||||
// filter,
|
filter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
@ -39,7 +37,7 @@ export default function InfiniteCategoryGrid() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative space-y-4">
|
<div className="relative space-y-4">
|
||||||
{/* <ArticleFilterBar onFilterUpdate={setFilter} /> */}
|
<CategoryFilterBar onFilterUpdate={setFilter} />
|
||||||
<menu className={`${CATEGORY_GRID_CLASS} overflow-auto`}>
|
<menu className={`${CATEGORY_GRID_CLASS} overflow-auto`}>
|
||||||
{data?.pages?.length
|
{data?.pages?.length
|
||||||
? allItems.map((category, idx) => (
|
? allItems.map((category, idx) => (
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export function Combobox({
|
|||||||
}: ComboboxProps) {
|
}: ComboboxProps) {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [value, setValue] = React.useState(initialValue ?? "");
|
const [value, setValue] = React.useState(initialValue ?? "");
|
||||||
const selectedItem = data.find((item) => item.value === value)!;
|
const selectedItem = data.find((item) => item.value === initialValue)!;
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<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