21st

ReUI Filters

Component 5

About

ReUI Filters with the first 10 official ReUI examples, preserved from the MIT-licensed source registry.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds ReUI Filters to your project and fits it to your theme.

Code

How ReUI Filters is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

import { UserIcon, MailIcon, BuildingIcon, MapPinIcon, ListFilterIcon, FunnelXIcon, CircleAlertIcon } from "lucide-react"
"use client"
"use no memo"

import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Alert, AlertTitle } from "@/components/ui/reui-filters-utils/alert"
import { Badge } from "@/components/ui/reui-filters-utils/badge"
import {
  DataGrid,
  DataGridContainer,
} from "@/components/ui/reui-filters-utils/data-grid"
import { DataGridColumnHeader } from "@/components/ui/reui-filters-utils/data-grid-column-header"
import { DataGridPagination } from "@/components/ui/reui-filters-utils/data-grid-pagination"
import { DataGridTable } from "@/components/ui/reui-filters-utils/data-grid-table"
import {
  createFilter,
  Filters,
  type Filter,
  type FilterFieldConfig,
} from "@/components/ui/reui-filters"
import {
  ColumnDef,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  PaginationState,
  SortingState,
  useReactTable,
} from "@tanstack/react-table"
import {
  createParser,
  parseAsBoolean,
  parseAsJson,
  useQueryState,
  useQueryStates,
} from "nuqs"
import { NuqsAdapter } from "nuqs/adapters/react"

import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/reui-filters-utils/avatar"
import { Button } from "@/components/ui/reui-filters-utils/button"
import { ScrollArea, ScrollBar } from "@/components/ui/reui-filters-utils/scroll-area"
import { Skeleton } from "@/components/ui/reui-filters-utils/skeleton"

interface IData {
  id: string
  name: string
  availability: "online" | "away" | "busy" | "offline"
  avatar: string
  status: "active" | "inactive"
  flag: string // Emoji flags
  email: string
  company: string
  role: string
  joined: string
  location: string
  balance: number
}

const demoData: IData[] = [
  {
    id: "1",
    name: "Alex Johnson",
    availability: "online",
    avatar:
      "https://cdn.21st.dev/assets/mirror/7a/7a441860d9b014f1d071006b567655c4fa0d3f45a693dcaa6a765ccd5953f7f0.jpg",
    status: "active",
    flag: "us",
    email: "alex@apple.com",
    company: "Apple",
    role: "CEO",
    joined: "Jan, 2024",
    location: "San Francisco, USA",
    balance: 5143.03,
  },
  {
    id: "2",
    name: "Sarah Chen",
    availability: "away",
    avatar:
      "https://cdn.21st.dev/assets/mirror/30/3046299adaf36760e400c8c71d6f738d8dbec09de899c2b15eba88fcaa8d4ef0.jpg",
    status: "inactive",
    flag: "gb",
    email: "sarah@openai.com",
    company: "OpenAI",
    role: "CTO",
    joined: "Mar, 2023",
    location: "London, UK",
    balance: 4321.87,
  },
  {
    id: "3",
    name: "Michael Rodriguez",
    availability: "busy",
    avatar:
      "https://cdn.21st.dev/assets/mirror/80/80037eb615485428bea8d3554ac30b5c9cad6f234b5c6a6df293e727429bb29e.jpg",
    status: "active",
    flag: "ca",
    email: "michael@meta.com",
    company: "Meta",
    role: "Designer",
    joined: "Jun, 2022",
    location: "Toronto, Canada",
    balance: 7654.98,
  },
  {
    id: "4",
    name: "Emma Wilson",
    availability: "offline",
    avatar:
      "https://cdn.21st.dev/assets/mirror/45/4577549deb2040d7057c6b8baf60439cd843c81a71e20a6e48ad380db196b8fb.jpg",
    status: "inactive",
    flag: "au",
    email: "emma@tesla.com",
    company: "Tesla",
    role: "Developer",
    joined: "Sep, 2024",
    location: "Sydney, Australia",
    balance: 3456.45,
  },
  {
    id: "5",
    name: "David Kim",
    availability: "online",
    avatar:
      "https://cdn.21st.dev/assets/mirror/d7/d7e11fabd9762b998c690c7152c7c482f0ae443df7bb17497dcd6cbe83cca8b5.jpg",
    status: "active",
    flag: "de",
    email: "david@sap.com",
    company: "SAP",
    role: "Lawyer",
    joined: "Nov, 2023",
    location: "Berlin, Germany",
    balance: 9876.54,
  },
  {
    id: "6",
    name: "Aron Thompson",
    availability: "away",
    avatar:
      "https://cdn.21st.dev/assets/mirror/a3/a399b0de331de2cb08a1da5a7f91e54ef63dbebef8af4393a1036212f4de2fa9.jpg",
    status: "active",
    flag: "my",
    email: "aron@keenthemes.com",
    company: "Keenthemes",
    role: "Director",
    joined: "Feb, 2022",
    location: "Kuala Lumpur, MY",
    balance: 6214.22,
  },
  {
    id: "7",
    name: "James Brown",
    availability: "busy",
    avatar:
      "https://cdn.21st.dev/assets/mirror/83/83085f000e6eed1ba3e84d4bd37e0cc795d37f58b792ffa122bef94d137f3716.jpg",
    status: "inactive",
    flag: "es",
    email: "james@bbva.es",
    company: "BBVA",
    role: "Product Manager",
    joined: "Aug, 2024",
    location: "Barcelona, Spain",
    balance: 5321.77,
  },
  {
    id: "8",
    name: "Maria Garcia",
    availability: "offline",
    avatar:
      "https://cdn.21st.dev/assets/mirror/fb/fb7eb5cc927e3c339864a3e6a897c0bffe58c08404411fbe5b05d4a3e1a9ffee.jpg",
    status: "active",
    flag: "jp",
    email: "maria@sony.jp",
    company: "Sony",
    role: "Marketing Lead",
    joined: "Dec, 2023",
    location: "Tokyo, Japan",
    balance: 8452.39,
  },
  {
    id: "9",
    name: "Nick Johnson",
    availability: "online",
    avatar:
      "https://cdn.21st.dev/assets/mirror/13/13ac4fd1dacbd55debb2b0161597b171af94c2d9564d7a8f6433ca0d1145e26c.jpg",
    status: "active",
    flag: "fr",
    email: "nick@lvmh.fr",
    company: "LVMH",
    role: "Data Scientist",
    joined: "Apr, 2022",
    location: "Paris, France",
    balance: 7345.1,
  },
  {
    id: "10",
    name: "Liam Thompson",
    availability: "away",
    avatar:
      "https://cdn.21st.dev/assets/mirror/1d/1d69f426bcc1aea1fb5c6d40dd4ba3b9a5e51c7e12a386cbd8682d0efdde8559.jpg",
    status: "inactive",
    flag: "it",
    email: "liam@eni.it",
    company: "ENI",
    role: "Engineer",
    joined: "Jul, 2024",
    location: "Milan, Italy",
    balance: 5214.88,
  },
  {
    id: "11",
    name: "Alex Johnson",
    availability: "busy",
    avatar:
      "https://cdn.21st.dev/assets/mirror/7a/7a441860d9b014f1d071006b567655c4fa0d3f45a693dcaa6a765ccd5953f7f0.jpg",
    status: "active",
    flag: "br",
    email: "alex@vale.br",
    company: "Vale",
    role: "Software Engineer",
    joined: "May, 2023",
    location: "Rio de Janeiro, Brazil",
    balance: 9421.5,
  },
  {
    id: "12",
    name: "Sarah Chen",
    availability: "offline",
    avatar:
      "https://cdn.21st.dev/assets/mirror/30/3046299adaf36760e400c8c71d6f738d8dbec09de899c2b15eba88fcaa8d4ef0.jpg",
    status: "active",
    flag: "in",
    email: "sarah@tata.in",
    company: "Tata",
    role: "Sales Manager",
    joined: "Oct, 2024",
    location: "Mumbai, India",
    balance: 4521.67,
  },
]

// Availability status component
const AvailabilityStatus = ({ availability }: { availability: string }) => {
  const getStatusColor = (status: string) => {
    switch (status) {
      case "online":
        return "bg-green-500"
      case "away":
        return "bg-yellow-500"
      case "busy":
        return "bg-red-500"
      case "offline":
        return "bg-gray-400"
      default:
        return "bg-gray-400"
    }
  }

  const getStatusLabel = (status: string) => {
    switch (status) {
      case "online":
        return "Online"
      case "away":
        return "Away"
      case "busy":
        return "Busy"
      case "offline":
        return "Offline"
      default:
        return "Unknown"
    }
  }

  return (
    <div className="flex items-center gap-1.5">
      <div className={`size-2 rounded-full ${getStatusColor(availability)}`} />
      <span className="text-muted-foreground text-sm">
        {getStatusLabel(availability)}
      </span>
    </div>
  )
}

// Helper to check if a filter has meaningful values
const getActiveFilters = (filters: Filter[]) => {
  return filters.filter((filter) => {
    const { values } = filter

    // Check if filter has meaningful values
    if (!values || values.length === 0) return false

    // For text/string values, check if they're not empty strings
    if (
      values.every((value) => typeof value === "string" && value.trim() === "")
    )
      return false

    // For number values, check if they're not null/undefined
    if (values.every((value) => value === null || value === undefined))
      return false

    // For arrays, check if they're not empty
    if (values.every((value) => Array.isArray(value) && value.length === 0))
      return false

    return true
  })
}

type FilterState = { operator: string; values: string[] }

// Custom parser for "operator:value1|value2" format
const parseAsFilterValue = createParser<FilterState>({
  parse: (queryValue: string) => {
    if (!queryValue) return null
    const separatorIndex = queryValue.indexOf(":")
    if (separatorIndex === -1) {
      return { operator: "is", values: queryValue.split("|").filter(Boolean) }
    }
    const operator = queryValue.slice(0, separatorIndex)
    const values = queryValue
      .slice(separatorIndex + 1)
      .split("|")
      .filter(Boolean)
    return { operator, values }
  },
  serialize: (filter: FilterState) => {
    if (!filter.values?.length) return ""
    return `${filter.operator}:${filter.values.join("|")}`
  },
})

function PatternContent() {
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 5,
  })

  // Sorting state synced with URL
  const [sorting, setSorting] = useQueryState(
    "sort",
    parseAsJson<SortingState>((v) => v as SortingState).withDefault([
      { id: "name", desc: false },
    ])
  )

  // Individual filter states synced with URL
  const [filterStates, setFilterStates] = useQueryStates(
    {
      filters: parseAsBoolean.withDefault(false),
      name: parseAsFilterValue,
      email: parseAsFilterValue,
      company: parseAsFilterValue,
      role: parseAsFilterValue,
      status: parseAsFilterValue,
      availability: parseAsFilterValue,
      location: parseAsFilterValue,
    },
    { history: "replace", shallow: true }
  )

  // Derived filters array for the Filters component
  const filters = useMemo(() => {
    return Object.entries(filterStates)
      .filter(
        ([key, state]) =>
          key !== "filters" && state !== null && typeof state !== "boolean"
      )
      .map(([key, state]) => {
        const filterState = state as FilterState
        return createFilter(key, filterState.operator, filterState.values)
      })
  }, [filterStates])

  // Async state management
  const [isLoading, setIsLoading] = useState(false)
  const [filteredData, setFilteredData] = useState<IData[]>(demoData)
  const isInitialLoad = useRef(true)
  const filterTimeoutRef = useRef<NodeJS.Timeout | null>(null)

  // Filter field configurations
  const fields: FilterFieldConfig[] = [
    {
      key: "name",
      label: "Name",
      icon: (
        <UserIcon className="size-3.5" />
      ),
      type: "text",
      className: "w-40",
      placeholder: "Search names...",
    },
    {
      key: "email",
      label: "Email",
      icon: (
        <MailIcon className="size-3.5" />
      ),
      type: "text",
      className: "w-48",
      placeholder: "user@example.com",
    },
    {
      key: "company",
      label: "Company",
      icon: (
        <BuildingIcon className="size-3.5" />
      ),
      type: "select",
      searchable: true,
      className: "w-[180px]",
      options: [
        { value: "Apple", label: "Apple" },
        { value: "OpenAI", label: "OpenAI" },
        { value: "Meta", label: "Meta" },
        { value: "Tesla", label: "Tesla" },
        { value: "SAP", label: "SAP" },
        { value: "Keenthemes", label: "Keenthemes" },
        { value: "BBVA", label: "BBVA" },
        { value: "Sony", label: "Sony" },
        { value: "LVMH", label: "LVMH" },
        { value: "ENI", label: "ENI" },
        { value: "Vale", label: "Vale" },
        { value: "Tata", label: "Tata" },
      ],
    },
    {
      key: "role",
      label: "Role",
      icon: (
        <UserIcon className="size-3.5" />
      ),
      type: "select",
      searchable: true,
      className: "w-[160px]",
      options: [
        { value: "CEO", label: "CEO" },
        { value: "CTO", label: "CTO" },
        { value: "Designer", label: "Designer" },
        { value: "Developer", label: "Developer" },
        { value: "Lawyer", label: "Lawyer" },
        { value: "Director", label: "Director" },
        { value: "Product Manager", label: "Product Manager" },
        { value: "Marketing Lead", label: "Marketing Lead" },
        { value: "Data Scientist", label: "Data Scientist" },
        { value: "Engineer", label: "Engineer" },
        { value: "Software Engineer", label: "Software Engineer" },
        { value: "Sales Manager", label: "Sales Manager" },
      ],
    },
    {
      key: "status",
      label: "Status",
      icon: (
        <UserIcon className="size-3.5" />
      ),
      type: "multiselect",
      searchable: false,
      className: "w-[140px]",
      options: [
        {
          value: "active",
          label: "Active",
          icon: <div className="size-2 rounded-full bg-green-500"></div>,
        },
        {
          value: "inactive",
          label: "Inactive",
          icon: <div className="bg-destructive size-2 rounded-full"></div>,
        },
        {
          value: "archived",
          label: "Archived",
          icon: <div className="size-2 rounded-full bg-zinc-400"></div>,
        },
      ],
    },
    {
      key: "availability",
      label: "Availability",
      icon: (
        <UserIcon className="size-3.5" />
      ),
      type: "select",
      searchable: false,
      className: "w-[160px]",
      options: [
        {
          value: "online",
          label: "Online",
          icon: (
            <div className="flex items-center gap-2">
              <div className="size-2 rounded-full bg-green-500" />
              <span>Online</span>
            </div>
          ),
        },
        {
          value: "away",
          label: "Away",
          icon: (
            <div className="flex items-center gap-2">
              <div className="size-2 rounded-full bg-yellow-500" />
              <span>Away</span>
            </div>
          ),
        },
        {
          value: "busy",
          label: "Busy",
          icon: (
            <div className="flex items-center gap-2">
              <div className="size-2 rounded-full bg-red-500" />
              <span>Busy</span>
            </div>
          ),
        },
        {
          value: "offline",
          label: "Offline",
          icon: (
            <div className="flex items-center gap-2">
              <div className="size-2 rounded-full bg-gray-400" />
              <span>Offline</span>
            </div>
          ),
        },
      ],
    },
    {
      key: "location",
      label: "Location",
      icon: (
        <MapPinIcon className="size-3.5" />
      ),
      type: "text",
      className: "w-40",
      placeholder: "Search locations...",
    },
  ]

  // Apply filters to data (shared function)
  const applyFiltersToData = useCallback((newFilters: Filter[]) => {
    let filtered = [...demoData]

    // Filter out empty filters before applying
    const activeFilters = getActiveFilters(newFilters)

    activeFilters.forEach((filter) => {
      const { field, operator, values } = filter

      filtered = filtered.filter((item) => {
        const fieldValue = item[field as keyof IData]

        switch (operator) {
          case "is":
            return values.includes(fieldValue)
          case "is_not":
            return !values.includes(fieldValue)
          case "contains":
            return values.some((value) =>
              String(fieldValue)
                .toLowerCase()
                .includes(String(value).toLowerCase())
            )
          case "not_contains":
            return !values.some((value) =>
              String(fieldValue)
                .toLowerCase()
                .includes(String(value).toLowerCase())
            )
          case "equals":
            return fieldValue === values[0]
          case "not_equals":
            return fieldValue !== values[0]
          case "greater_than":
            return Number(fieldValue) > Number(values[0])
          case "less_than":
            return Number(fieldValue) < Number(values[0])
          case "greater_than_or_equal":
            return Number(fieldValue) >= Number(values[0])
          case "less_than_or_equal":
            return Number(fieldValue) <= Number(values[0])
          case "between":
            if (values.length >= 2) {
              const min = Number(values[0])
              const max = Number(values[1])
              return Number(fieldValue) >= min && Number(fieldValue) <= max
            }
            return true
          case "not_between":
            if (values.length >= 2) {
              const min = Number(values[0])
              const max = Number(values[1])
              return Number(fieldValue) < min || Number(fieldValue) > max
            }
            return true
          case "before":
            return new Date(String(fieldValue)) < new Date(String(values[0]))
          case "after":
            return new Date(String(fieldValue)) > new Date(String(values[0]))
          default:
            return true
        }
      })
    })

    return filtered
  }, [])

  // Simulate async data filtering
  const simulateAsyncFiltering = useCallback(
    async (newFilters: Filter[]) => {
      setIsLoading(true) // Show loading on current data

      // Simulate API call delay
      await new Promise((resolve) =>
        setTimeout(resolve, 800 + Math.random() * 1200)
      )

      // Apply filters and update data after timeout
      const filtered = applyFiltersToData(newFilters)
      setFilteredData(filtered)
      setIsLoading(false)
    },
    [applyFiltersToData]
  )

  const handleFiltersChange = useCallback(
    (newFilters: Filter[]) => {
      const oldActive = getActiveFilters(filters)
      const newActive = getActiveFilters(newFilters)

      // Convert Filter[] back to individual query states
      const nextStates: Record<string, FilterState | boolean | null> = {}

      // Reset all tracked fields first
      Object.keys(filterStates).forEach((key) => {
        nextStates[key] = null
      })

      // Set only active ones
      newFilters.forEach((f) => {
        if (f.values.length > 0) {
          nextStates[f.field] = {
            operator: f.operator,
            values: f.values as string[],
          }
        }
      })

      // Set the filters marker if any filters exist
      nextStates.filters = newFilters.length > 0 ? true : null

      setFilterStates(nextStates)

      if (JSON.stringify(oldActive) === JSON.stringify(newActive)) {
        return
      }

      setPagination((prev) => ({ ...prev, pageIndex: 0 }))

      // Clear any pending timeout
      if (filterTimeoutRef.current) {
        clearTimeout(filterTimeoutRef.current)
      }

      // Add a small debounce before starting the async simulation
      filterTimeoutRef.current = setTimeout(() => {
        simulateAsyncFiltering(newFilters)
      }, 300)
    },
    [filters, filterStates, setFilterStates, simulateAsyncFiltering]
  )

  // Cleanup timeout on unmount
  useEffect(() => {
    return () => {
      if (filterTimeoutRef.current) {
        clearTimeout(filterTimeoutRef.current)
      }
    }
  }, [])

  // Initial data load - only run once on mount
  useEffect(() => {
    if (isInitialLoad.current) {
      // Apply initial filter without loading state
      const initialFiltered = applyFiltersToData(filters || [])
      setFilteredData(initialFiltered)
      isInitialLoad.current = false
    }
  }, [filters, applyFiltersToData])

  const columns = useMemo<ColumnDef<IData>[]>(
    () => [
      {
        accessorKey: "name",
        id: "name",
        header: ({ column }) => (
          <DataGridColumnHeader title="Staff" column={column} />
        ),
        cell: ({ row }) => {
          return (
            <div className="flex items-center gap-3">
              <Avatar className="size-8">
                <AvatarImage
                  src={row.original.avatar}
                  alt={row.original.name}
                />
                <AvatarFallback>
                  {row.original.name
                    .split(" ")
                    .map((n) => n[0])
                    .join("")}
                </AvatarFallback>
              </Avatar>
              <div className="space-y-px">
                <div className="text-foreground font-medium">
                  {row.original.name}
                </div>
                <div className="text-muted-foreground truncate text-xs">
                  {row.original.email}
                </div>
              </div>
            </div>
          )
        },
        size: 200,
        enableSorting: true,
        enableHiding: false,
        meta: {
          skeleton: (
            <div className="flex items-center gap-3">
              <Skeleton className="size-8 rounded-full" />
              <div className="space-y-1">
                <Skeleton className="h-4 w-24" />
                <Skeleton className="h-4 w-16" />
              </div>
            </div>
          ),
        },
      },
      {
        accessorKey: "company",
        id: "company",
        header: ({ column }) => (
          <DataGridColumnHeader title="Company" column={column} />
        ),
        cell: (info) => <span>{info.getValue() as string}</span>,
        size: 150,
        enableSorting: true,
        enableHiding: false,
        meta: {
          skeleton: <Skeleton className="h-4 w-20" />,
        },
      },
      {
        accessorKey: "role",
        id: "role",
        header: ({ column }) => (
          <DataGridColumnHeader title="Occupation" column={column} />
        ),
        cell: (info) => <span>{info.getValue() as string}</span>,
        size: 125,
        enableSorting: true,
        enableHiding: false,
        meta: {
          skeleton: <Skeleton className="h-4 w-16" />,
        },
      },
      {
        accessorKey: "status",
        id: "status",
        header: "Status",
        cell: ({ row }) => {
          const status = row.original.status

          if (status == "active") {
            return <Badge variant="success-outline">Active</Badge>
          } else if (status == "inactive") {
            return <Badge variant="destructive-outline">Inactive</Badge>
          } else if (status == "archived") {
            return <Badge variant="warning-outline">Archived</Badge>
          }
        },
        size: 100,
        meta: {
          skeleton: <Skeleton className="h-4 w-16 rounded-full" />,
        },
      },
      {
        accessorKey: "availability",
        id: "availability",
        header: "Availability",
        cell: ({ row }) => (
          <AvailabilityStatus availability={row.original.availability} />
        ),
        size: 120,
        enableSorting: true,
        meta: {
          skeleton: (
            <div className="flex items-center gap-1.5">
              <Skeleton className="size-4 rounded-full" />
              <Skeleton className="h-3.5 w-12" />
            </div>
          ),
        },
      },
      {
        accessorKey: "location",
        id: "location",
        header: ({ column }) => (
          <DataGridColumnHeader title="Location" column={column} />
        ),
        cell: ({ row }) => (
          <div className="flex items-center gap-2">
            <img
              src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
              alt={row.original.flag}
              className="size-4 rounded-full object-cover"
            />
            <span>{row.original.location}</span>
          </div>
        ),
        size: 180,
        enableSorting: true,
        meta: {
          skeleton: (
            <div className="flex items-center gap-2">
              <Skeleton className="size-4 rounded" />
              <Skeleton className="h-3.5 w-24" />
            </div>
          ),
        },
      },
      {
        accessorKey: "balance",
        id: "balance",
        header: ({ column }) => (
          <DataGridColumnHeader title="Balance" column={column} />
        ),
        cell: ({ row }) => (
          <span className="font-medium">
            ${row.original.balance.toLocaleString()}
          </span>
        ),
        size: 120,
        enableSorting: true,
        meta: {
          skeleton: <Skeleton className="h-4 w-16" />,
        },
      },
    ],
    []
  )

  const [columnOrder, setColumnOrder] = useState<string[]>(
    columns.map((column) => column.id as string)
  )

  const table = useReactTable({
    columns,
    data: filteredData,
    pageCount: Math.ceil((filteredData?.length || 0) / pagination.pageSize),
    getRowId: (row: IData) => row.id,
    state: {
      pagination,
      sorting: sorting || [],
      columnOrder,
    },
    onColumnOrderChange: setColumnOrder,
    onPaginationChange: setPagination,
    onSortingChange: (updater) => {
      const nextSorting =
        typeof updater === "function" ? updater(sorting || []) : updater
      setSorting(nextSorting, { history: "replace", shallow: true })
    },
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
  })

  return (
    <div className="w-full max-w-[850px]">
      {/* Filters Section */}
      <div className="mb-3.5 flex items-start gap-2.5">
        <div className="flex-1">
          <Filters
            filters={filters}
            fields={fields}
            onChange={handleFiltersChange}
            size="sm"
            trigger={
              <Button variant="outline" size="icon-sm">
                <ListFilterIcon />
              </Button>
            }
          />
        </div>
        {filters.length > 0 && (
          <Button
            variant="outline"
            size="sm"
            onClick={() => {
              if (filterTimeoutRef.current) {
                clearTimeout(filterTimeoutRef.current)
              }
              // Clear all filters in the URL
              const clearedStates: Record<string, null> = {}
              Object.keys(filterStates).forEach(
                (key) => (clearedStates[key] = null)
              )
              clearedStates.filters = null
              setFilterStates(clearedStates)
              simulateAsyncFiltering([])
            }}
            disabled={isLoading}
          >
            <FunnelXIcon />
            Clear
          </Button>
        )}
      </div>

      {/* Data Grid */}
      <DataGrid
        table={table}
        isLoading={isLoading}
        loadingMode="skeleton"
        recordCount={filteredData?.length || 0}
        tableLayout={{
          dense: true,
          columnsMovable: true,
        }}
      >
        <div className="w-full space-y-2.5">
          <DataGridContainer>
            <ScrollArea>
              <DataGridTable />
              <ScrollBar orientation="horizontal" />
            </ScrollArea>
          </DataGridContainer>
          <DataGridPagination />
        </div>
      </DataGrid>

      {/* Async Info Alert */}
      <Alert variant="success" className="mt-5">
        <CircleAlertIcon />
        <AlertTitle>
          Async Mode: Simulated API Delay of <strong>800-2000ms</strong>
        </AlertTitle>
      </Alert>
    </div>
  )
}

export default function Pattern() {
  return (
    <NuqsAdapter>
      <PatternContent />
    </NuqsAdapter>
  )
}

Dependencies

More by 21st

View all
  • Futuristic Hero

    Futuristic Hero

    2.7K
  • Hero ASCII one

    Hero ASCII one

    1.4K
  • Hero ASCII

    Hero ASCII

    1.3K
  • Admit One Ticket

    Admit One Ticket

    961
  • Navigation Menu

    Navigation Menu

    916
  • Border Beam

    Border Beam

    890
  • Border Beam Search

    Border Beam Search

    545
  • Thinking Orbs

    Thinking Orbs

    438