All components

Task Filters

Task Filters is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.

HextaUI MIT other

What it is

Task Filters is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

No packages beyond the project baseline

Licence

This component comes from HextaUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Task Filters

registry/new-york/blocks/tasks/task-filters.tsx
"use client";

import { Search, X } from "lucide-react";
import { useMemo, useState } from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/registry/new-york/ui/badge";
import { Button } from "@/registry/new-york/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/registry/new-york/ui/card";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/registry/new-york/ui/input-group";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/registry/new-york/ui/select";
import { Separator } from "@/registry/new-york/ui/separator";
import type { TaskPriority, TaskStatus } from "./task-list";

export interface TaskFilter {
  status?: TaskStatus[];
  priority?: TaskPriority[];
  assigneeIds?: string[];
  dueDateRange?: { start?: Date; end?: Date };
  tags?: string[];
  projectIds?: string[];
  search?: string;
}

export interface SavedFilter {
  id: string;
  name: string;
  filter: TaskFilter;
}

export interface TaskFiltersProps {
  onFilterChange?: (filter: TaskFilter) => void;
  savedFilters?: SavedFilter[];
  onSaveFilter?: (name: string, filter: TaskFilter) => Promise<void>;
  onLoadFilter?: (filterId: string) => void;
  onDeleteFilter?: (filterId: string) => Promise<void>;
  availableAssignees?: Array<{ id: string; name: string }>;
  availableProjects?: Array<{ id: string; name: string }>;
  availableTags?: string[];
  className?: string;
  showSavedFilters?: boolean;
}

export default function TaskFilters({
  onFilterChange,
  savedFilters = [],
  onSaveFilter,
  onLoadFilter,
  onDeleteFilter,
  availableAssignees = [],
  availableProjects = [],
  availableTags = [],
  className,
  showSavedFilters = true,
}: TaskFiltersProps) {
  const [search, setSearch] = useState("");
  const [status, setStatus] = useState<string[]>([]);
  const [priority, setPriority] = useState<string[]>([]);
  const [assignees, setAssignees] = useState<string[]>([]);
  const [projects, setProjects] = useState<string[]>([]);
  const [tags, setTags] = useState<string[]>([]);
  const [dueDateStart, setDueDateStart] = useState("");
  const [dueDateEnd, setDueDateEnd] = useState("");

  const activeFilters = useMemo(() => {
    const count =
      (status.length > 0 ? 1 : 0) +
      (priority.length > 0 ? 1 : 0) +
      (assignees.length > 0 ? 1 : 0) +
      (projects.length > 0 ? 1 : 0) +
      (tags.length > 0 ? 1 : 0) +
      (dueDateStart || dueDateEnd ? 1 : 0) +
      (search.trim() ? 1 : 0);
    return count;
  }, [
    status,
    priority,
    assignees,
    projects,
    tags,
    dueDateStart,
    dueDateEnd,
    search,
  ]);

  const currentFilter: TaskFilter = useMemo(
    () => ({
      status: status.length > 0 ? (status as TaskStatus[]) : undefined,
      priority: priority.length > 0 ? (priority as TaskPriority[]) : undefined,
      assigneeIds: assignees.length > 0 ? assignees : undefined,
      projectIds: projects.length > 0 ? projects : undefined,
      tags: tags.length > 0 ? tags : undefined,
      dueDateRange:
        dueDateStart || dueDateEnd
          ? {
              start: dueDateStart ? new Date(dueDateStart) : undefined,
              end: dueDateEnd ? new Date(dueDateEnd) : undefined,
            }
          : undefined,
      search: search.trim() || undefined,
    }),
    [
      status,
      priority,
      assignees,
      projects,
      tags,
      dueDateStart,
      dueDateEnd,
      search,
    ]
  );

  const handleFilterChange = () => {
    onFilterChange?.(currentFilter);
  };

  const handleClearAll = () => {
    setSearch("");
    setStatus([]);
    setPriority([]);
    setAssignees([]);
    setProjects([]);
    setTags([]);
    setDueDateStart("");
    setDueDateEnd("");
    onFilterChange?.({});
  };

  const toggleArrayItem = <T,>(
    array: T[],
    item: T,
    setter: (value: T[]) => void
  ) => {
    if (array.includes(item)) {
      setter(array.filter((i) => i !== item));
    } else {
      setter([...array, item]);
    }
  };

  return (
    <Card className={cn("w-full shadow-xs", className)}>
      <CardHeader>
        <div className="flex flex-col gap-1">
          <CardTitle>Filters</CardTitle>
          <CardDescription>
            {activeFilters > 0
              ? `${activeFilters} active filter${activeFilters !== 1 ? "s" : ""}`
              : "No active filters"}
          </CardDescription>
        </div>
      </CardHeader>
      <CardContent>
        <div className="flex flex-col gap-4">
          <InputGroup>
            <InputGroupAddon>
              <Search className="size-4" />
            </InputGroupAddon>
            <InputGroupInput
              onChange={(e) => {
                setSearch(e.target.value);
                handleFilterChange();
              }}
              onKeyDown={(e) => {
                if (e.key === "Enter") {
                  handleFilterChange();
                }
              }}
              placeholder="Search tasks…"
              type="search"
              value={search}
            />
            {search && (
              <Button
                aria-label="Clear search"
                className="absolute top-1/2 right-2 -translate-y-1/2"
                onClick={() => {
                  setSearch("");
                  handleFilterChange();
                }}
                size="icon"
                type="button"
                variant="ghost"
              >
                <X className="size-4" />
              </Button>
            )}
          </InputGroup>

          <div className="grid gap-4 sm:grid-cols-2">
            <div className="flex flex-col gap-2">
              <label className="text-muted-foreground text-sm">Status</label>
              <div className="flex flex-wrap gap-2">
                {(
                  ["todo", "in_progress", "done", "cancelled"] as TaskStatus[]
                ).map((s) => (
                  <Button
                    key={s}
                    onClick={() => {
                      toggleArrayItem(status, s, setStatus);
                      handleFilterChange();
                    }}
                    size="sm"
                    type="button"
                    variant={status.includes(s) ? "default" : "outline"}
                  >
                    {s === "in_progress"
                      ? "In Progress"
                      : s.charAt(0).toUpperCase() + s.slice(1)}
                  </Button>
                ))}
              </div>
            </div>

            <div className="flex flex-col gap-2">
              <label className="text-muted-foreground text-sm">Priority</label>
              <div className="flex flex-wrap gap-2">
                {(["low", "medium", "high", "urgent"] as TaskPriority[]).map(
                  (p) => (
                    <Button
                      key={p}
                      onClick={() => {
                        toggleArrayItem(priority, p, setPriority);
                        handleFilterChange();
                      }}
                      size="sm"
                      type="button"
                      variant={priority.includes(p) ? "default" : "outline"}
                    >
                      {p.charAt(0).toUpperCase() + p.slice(1)}
                    </Button>
                  )
                )}
              </div>
            </div>
          </div>

          {availableAssignees.length > 0 && (
            <div className="flex flex-col gap-2">
              <label className="text-muted-foreground text-sm">Assignees</label>
              <Select
                onValueChange={(value) => {
                  toggleArrayItem(assignees, value, setAssignees);
                  handleFilterChange();
                }}
                value=""
              >
                <SelectTrigger>
                  <SelectValue placeholder="Select assignees" />
                </SelectTrigger>
                <SelectContent>
                  {availableAssignees.map((assignee) => (
                    <SelectItem key={assignee.id} value={assignee.id}>
                      {assignee.name}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
              {assignees.length > 0 && (
                <div className="flex flex-wrap gap-1">
                  {assignees.map((id) => {
                    const assignee = availableAssignees.find(
                      (a) => a.id === id
                    );
                    return assignee ? (
                      <Badge
                        className="flex items-center gap-1"
                        key={id}
                        variant="secondary"
                      >
                        {assignee.name}
                        <button
                          aria-label={`Remove ${assignee.name}`}
                          onClick={() => {
                            setAssignees(assignees.filter((a) => a !== id));
                            handleFilterChange();
                          }}
                          type="button"
                        >
                          <X className="size-3" />
                        </button>
                      </Badge>
                    ) : null;
                  })}
                </div>
              )}
            </div>
          )}

          {availableProjects.length > 0 && (
            <div className="flex flex-col gap-2">
              <label className="text-muted-foreground text-sm">Projects</label>
              <Select
                onValueChange={(value) => {
                  toggleArrayItem(projects, value, setProjects);
                  handleFilterChange();
                }}
                value=""
              >
                <SelectTrigger>
                  <SelectValue placeholder="Select projects" />
                </SelectTrigger>
                <SelectContent>
                  {availableProjects.map((project) => (
                    <SelectItem key={project.id} value={project.id}>
                      {project.name}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
              {projects.length > 0 && (
                <div className="flex flex-wrap gap-1">
                  {projects.map((id) => {
                    const project = availableProjects.find((p) => p.id === id);
                    return project ? (
                      <Badge
                        className="flex items-center gap-1"
                        key={id}
                        variant="secondary"
                      >
                        {project.name}
                        <button
                          aria-label={`Remove ${project.name}`}
                          onClick={() => {
                            setProjects(projects.filter((p) => p !== id));
                            handleFilterChange();
                          }}
                          type="button"
                        >
                          <X className="size-3" />
                        </button>
                      </Badge>
                    ) : null;
                  })}
                </div>
              )}
            </div>
          )}

          <div className="grid gap-4 sm:grid-cols-2">
            <div className="flex flex-col gap-2">
              <label className="text-muted-foreground text-sm">
                Due Date From
              </label>
              <InputGroup>
                <InputGroupInput
                  onChange={(e) => {
                    setDueDateStart(e.target.value);
                    handleFilterChange();
                  }}
                  type="date"
                  value={dueDateStart}
                />
              </InputGroup>
            </div>
            <div className="flex flex-col gap-2">
              <label className="text-muted-foreground text-sm">
                Due Date To
              </label>
              <InputGroup>
                <InputGroupInput
                  onChange={(e) => {
                    setDueDateEnd(e.target.value);
                    handleFilterChange();
                  }}
                  type="date"
                  value={dueDateEnd}
                />
              </InputGroup>
            </div>
          </div>

          {activeFilters > 0 && (
            <>
              <Separator />
              <div className="flex items-center justify-between">
                <span className="text-muted-foreground text-sm">
                  {activeFilters} active filter{activeFilters !== 1 ? "s" : ""}
                </span>
                <Button
                  onClick={handleClearAll}
                  size="sm"
                  type="button"
                  variant="outline"
                >
                  <X className="size-4" />
                  Clear All
                </Button>
              </div>
            </>
          )}
        </div>
      </CardContent>
    </Card>
  );
}

Similar components