diff --git a/public/locales/en/labels.json b/public/locales/en/labels.json
index f02b90f..47a296c 100644
--- a/public/locales/en/labels.json
+++ b/public/locales/en/labels.json
@@ -9,7 +9,9 @@
"placeholder": "Search labels",
"noResultsTitle": "No labels match",
"noResultsDescription": "No label matches \"{{searchTerm}}\".",
- "clear": "Clear search"
+ "noFilterResultsDescription": "No label matches the current filter.",
+ "clear": "Clear search",
+ "showAll": "Show all labels"
},
"detail": {
"taskCount_one": "{{count}} task",
diff --git a/public/locales/en/projects.json b/public/locales/en/projects.json
index 0e7eed4..19c9c9e 100644
--- a/public/locales/en/projects.json
+++ b/public/locales/en/projects.json
@@ -12,6 +12,14 @@
"message": "Are you sure you want to delete \"{{name}}\"? This will remove the project but keep all tasks (they'll move to the Default Project)."
},
"loadError": "Failed to load projects. Please try again.",
+ "search": {
+ "placeholder": "Search projects",
+ "noResultsTitle": "No projects match",
+ "noResultsDescription": "No project matches \"{{searchTerm}}\".",
+ "noFilterResultsDescription": "No project matches the current filter.",
+ "clear": "Clear search",
+ "showAll": "Show all projects"
+ },
"blurb": "Organize your tasks into projects. Create custom workspaces to keep your tasks organized and easily accessible.",
"defaultDescription": "All tasks without a specific project",
"selector": {
diff --git a/src/components/common/SortAndFilterMenu.jsx b/src/components/common/SortAndFilterMenu.jsx
new file mode 100644
index 0000000..b6235c8
--- /dev/null
+++ b/src/components/common/SortAndFilterMenu.jsx
@@ -0,0 +1,256 @@
+import { ArrowDownward, ArrowUpward, Check, Sort } from '@mui/icons-material'
+import {
+ Box,
+ Divider,
+ IconButton,
+ ListItemContent,
+ ListItemDecorator,
+ Menu,
+ MenuItem,
+ Radio,
+ Typography,
+} from '@mui/joy'
+import { useEffect, useRef, useState } from 'react'
+
+/**
+ * Compact sort + filter menu, meant to sit next to a search input.
+ *
+ * Props:
+ * sortOptions - [{ name, value }] shown under the sort header
+ * selectedSort - currently selected sort value
+ * onSortChange - (value) => void
+ * sortDirection - 'asc' | 'desc'
+ * onSortDirectionChange - (direction) => void
+ * filterTitle - optional header for the filter section
+ * filterOptions - optional [{ name, value }] rendered as radios
+ * selectedFilter - currently selected filter value
+ * onFilterChange - (value) => void
+ * isActive - highlights the trigger button when a non-default choice is on
+ */
+const SortAndFilterMenu = ({
+ filterOptions,
+ filterTitle,
+ icon = ,
+ isActive,
+ onFilterChange,
+ onSortChange,
+ onSortDirectionChange,
+ selectedFilter,
+ selectedSort,
+ sortDirection = 'asc',
+ sortOptions = [],
+ title = 'Sort by',
+}) => {
+ const [anchorEl, setAnchorEl] = useState(null)
+ const menuRef = useRef(null)
+ const buttonRef = useRef(null)
+
+ const handleMenuClose = () => setAnchorEl(null)
+
+ useEffect(() => {
+ const handleMenuOutsideClick = event => {
+ if (
+ menuRef.current &&
+ !menuRef.current.contains(event.target) &&
+ !buttonRef.current?.contains(event.target)
+ ) {
+ handleMenuClose()
+ }
+ }
+
+ document.addEventListener('mousedown', handleMenuOutsideClick)
+ return () => {
+ document.removeEventListener('mousedown', handleMenuOutsideClick)
+ }
+ }, [])
+
+ const SectionHeader = ({ children }) => (
+
+ )
+
+ return (
+ <>
+ setAnchorEl(anchorEl ? null : event.currentTarget)}
+ variant='outlined'
+ color={isActive ? 'primary' : 'neutral'}
+ size='sm'
+ sx={{ height: 32, width: 32, borderRadius: '50%', flexShrink: 0 }}
+ aria-label='Sort and filter options'
+ title='Sort & Filter'
+ >
+ {icon}
+
+
+
+ >
+ )
+}
+
+export default SortAndFilterMenu
diff --git a/src/views/Chores/ArchivedTasks.jsx b/src/views/Chores/ArchivedTasks.jsx
index 7e5a9f4..547f1e7 100644
--- a/src/views/Chores/ArchivedTasks.jsx
+++ b/src/views/Chores/ArchivedTasks.jsx
@@ -32,6 +32,7 @@ import { useNavigate } from 'react-router-dom'
import EmptyState from '../../components/common/EmptyState'
import FilterBar from '../../components/common/FilterBar'
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
+import SortAndFilterMenu from '../../components/common/SortAndFilterMenu'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useFilter } from '../../hooks/useFilter'
import { useUnArchiveChore } from '../../queries/ChoreQueries'
@@ -203,11 +204,46 @@ const ArchivedTasks = () => {
const {
activeFilters,
clearAll,
- filteredData: finalChores,
+ filteredData: filteredByBar,
hasActiveFilters,
setFilter,
} = useFilter(filteredChores, filterDefs)
+ const [sortBy, setSortBy] = useState(
+ () => localStorage.getItem('archivedChoresSortBy') || 'archivedAt',
+ )
+ const [sortDirection, setSortDirection] = useState(
+ () => localStorage.getItem('archivedChoresSortDirection') || 'desc',
+ )
+
+ useEffect(() => {
+ localStorage.setItem('archivedChoresSortBy', sortBy)
+ localStorage.setItem('archivedChoresSortDirection', sortDirection)
+ }, [sortBy, sortDirection])
+
+ const finalChores = useMemo(() => {
+ const direction = sortDirection === 'desc' ? -1 : 1
+ return [...filteredByBar].sort((a, b) => {
+ switch (sortBy) {
+ case 'name':
+ return direction * (a.name || '').localeCompare(b.name || '')
+ case 'priority':
+ return direction * ((a.priority ?? 0) - (b.priority ?? 0))
+ case 'dueDate': {
+ const aDue = new Date(a.nextDueDate || 0).getTime()
+ const bDue = new Date(b.nextDueDate || 0).getTime()
+ return direction * (aDue - bDue)
+ }
+ case 'archivedAt':
+ default: {
+ const aDate = new Date(a.updatedAt || 0).getTime()
+ const bDate = new Date(b.updatedAt || 0).getTime()
+ return direction * (aDate - bDate)
+ }
+ }
+ })
+ }, [filteredByBar, sortBy, sortDirection])
+
useEffect(() => {
const loadArchivedChores = async () => {
if (!membersLoading && userProfile) {
@@ -734,6 +770,21 @@ const ArchivedTasks = () => {
}
/>
+ {/* Sort Menu */}
+
+
{/* View Mode Toggle Button */}
{
const [editingFilter, setEditingFilter] = useState(null)
const [confirmationModel, setConfirmationModel] = useState({})
const [showMoreInfoId, setShowMoreInfoId] = useState(null)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [sortBy, setSortBy] = useState(
+ () => localStorage.getItem('filtersSortBy') || 'smart',
+ )
+ const [sortDirection, setSortDirection] = useState(
+ () => localStorage.getItem('filtersSortDirection') || 'asc',
+ )
+ const [pinnedFilter, setPinnedFilter] = useState('all')
+ const searchInputRef = useRef(null)
+
+ useEffect(() => {
+ localStorage.setItem('filtersSortBy', sortBy)
+ localStorage.setItem('filtersSortDirection', sortDirection)
+ }, [sortBy, sortDirection])
+
// Sort filters: pinned first, then by usage count, then by last used
const savedFilters = useMemo(() => {
return [...filtersData].sort((a, b) => {
@@ -282,6 +303,71 @@ const FilterView = () => {
})
}, [filtersData])
+ const visibleFilters = useMemo(() => {
+ if (pinnedFilter === 'pinned') {
+ return savedFilters.filter(filter => filter.isPinned)
+ }
+ if (pinnedFilter === 'unpinned') {
+ return savedFilters.filter(filter => !filter.isPinned)
+ }
+ return savedFilters
+ }, [pinnedFilter, savedFilters])
+
+ const fuse = useMemo(
+ () =>
+ new Fuse(visibleFilters, {
+ keys: ['name', 'description'],
+ includeScore: true,
+ isCaseSensitive: false,
+ findAllMatches: true,
+ }),
+ [visibleFilters],
+ )
+
+ const filteredFilters = useMemo(() => {
+ const matched = searchTerm
+ ? fuse.search(searchTerm).map(result => result.item)
+ : visibleFilters
+
+ const direction = sortDirection === 'desc' ? -1 : 1
+
+ // "Smart" keeps the pinned-then-usage order the list already arrives in.
+ if (sortBy === 'smart') {
+ return direction === -1 ? [...matched].reverse() : matched
+ }
+
+ return [...matched].sort((a, b) => {
+ switch (sortBy) {
+ case 'name':
+ return direction * (a.name || '').localeCompare(b.name || '')
+ case 'usage':
+ return direction * ((a.usageCount || 0) - (b.usageCount || 0))
+ case 'lastUsed': {
+ const aUsed = new Date(a.lastUsedAt || 0).getTime()
+ const bUsed = new Date(b.lastUsedAt || 0).getTime()
+ return direction * (aUsed - bUsed)
+ }
+ case 'created': {
+ const aCreated = new Date(a.createdAt || 0).getTime()
+ const bCreated = new Date(b.createdAt || 0).getTime()
+ return direction * (aCreated - bCreated)
+ }
+ default:
+ return 0
+ }
+ })
+ }, [fuse, searchTerm, visibleFilters, sortBy, sortDirection])
+
+ const handleSearchChange = e => {
+ setSearchTerm(e.target.value)
+ setShowMoreInfoId(null)
+ }
+
+ const handleSearchClose = () => {
+ setSearchTerm('')
+ searchInputRef.current?.blur()
+ }
+
// Calculate task counts for each filter
useEffect(() => {
if (chores && chores.res && savedFilters.length > 0) {
@@ -428,6 +514,68 @@ const FilterView = () => {
+ {savedFilters.length > 0 && (
+
+ }
+ endDecorator={
+ searchTerm && (
+
+
+
+ )
+ }
+ />
+ {
+ setPinnedFilter(value)
+ setShowMoreInfoId(null)
+ }}
+ isActive={
+ pinnedFilter !== 'all' ||
+ sortBy !== 'smart' ||
+ sortDirection !== 'asc'
+ }
+ />
+
+ )}
+
{
onClick: handleAddFilter,
}}
/>
+ ) : filteredFilters.length === 0 ? (
+ }
+ title='No filters match'
+ description={
+ searchTerm
+ ? `No saved filter matches "${searchTerm}".`
+ : 'No saved filter matches the current filter.'
+ }
+ primaryAction={{
+ label: searchTerm ? 'Clear search' : 'Show all filters',
+ onClick: () => {
+ handleSearchClose()
+ setPinnedFilter('all')
+ },
+ }}
+ />
) : (
- {savedFilters.map(filter => {
+ {filteredFilters.map(filter => {
return (
{
const [confirmationModel, setConfirmationModel] = useState({})
const [showMoreInfoId, setShowMoreInfoId] = useState(null)
const [searchTerm, setSearchTerm] = useState('')
+ const [sortBy, setSortBy] = useState(
+ () => localStorage.getItem('labelsSortBy') || 'name',
+ )
+ const [sortDirection, setSortDirection] = useState(
+ () => localStorage.getItem('labelsSortDirection') || 'asc',
+ )
+ const [ownershipFilter, setOwnershipFilter] = useState('all')
const searchInputRef = useRef(null)
+ useEffect(() => {
+ localStorage.setItem('labelsSortBy', sortBy)
+ localStorage.setItem('labelsSortDirection', sortDirection)
+ }, [sortBy, sortDirection])
+
+ const visibleLabels = useMemo(() => {
+ if (ownershipFilter === 'mine') {
+ return userLabels.filter(label => label.created_by === userProfile?.id)
+ }
+ if (ownershipFilter === 'shared') {
+ return userLabels.filter(label => label.created_by !== userProfile?.id)
+ }
+ return userLabels
+ }, [ownershipFilter, userLabels, userProfile?.id])
+
const fuse = useMemo(
() =>
- new Fuse(userLabels, {
+ new Fuse(visibleLabels, {
keys: ['name'],
includeScore: true,
isCaseSensitive: false,
findAllMatches: true,
}),
- [userLabels],
+ [visibleLabels],
)
const filteredLabels = useMemo(() => {
- if (!searchTerm) {
- return userLabels
- }
- return fuse.search(searchTerm).map(result => result.item)
- }, [fuse, searchTerm, userLabels])
+ const matched = searchTerm
+ ? fuse.search(searchTerm).map(result => result.item)
+ : visibleLabels
+
+ const direction = sortDirection === 'desc' ? -1 : 1
+ return [...matched].sort((a, b) => {
+ switch (sortBy) {
+ case 'color':
+ return direction * (a.color || '').localeCompare(b.color || '')
+ case 'created':
+ return direction * ((a.id || 0) - (b.id || 0))
+ case 'name':
+ default:
+ return direction * (a.name || '').localeCompare(b.name || '')
+ }
+ })
+ }, [fuse, searchTerm, visibleLabels, sortBy, sortDirection])
const handleSearchChange = e => {
setSearchTerm(e.target.value.toLowerCase())
@@ -310,7 +345,9 @@ const LabelView = () => {
{userLabels.length > 0 && (
-
+
{
)
}
/>
+ {
+ setOwnershipFilter(value)
+ setShowMoreInfoId(null)
+ }}
+ isActive={
+ ownershipFilter !== 'all' ||
+ sortBy !== 'name' ||
+ sortDirection !== 'asc'
+ }
+ />
)}
{
fullHeight
icon={}
title={t('search.noResultsTitle')}
- description={t('search.noResultsDescription', {
- searchTerm,
- })}
+ description={
+ searchTerm
+ ? t('search.noResultsDescription', { searchTerm })
+ : t('search.noFilterResultsDescription')
+ }
primaryAction={{
- label: t('search.clear'),
- onClick: handleSearchClose,
+ label: searchTerm ? t('search.clear') : t('search.showAll'),
+ onClick: () => {
+ handleSearchClose()
+ setOwnershipFilter('all')
+ },
}}
/>
)}
diff --git a/src/views/Projects/ProjectView.jsx b/src/views/Projects/ProjectView.jsx
index 644e3d4..1baffde 100644
--- a/src/views/Projects/ProjectView.jsx
+++ b/src/views/Projects/ProjectView.jsx
@@ -7,7 +7,14 @@ import {
TrailingActions,
Type as ListType,
} from '@meauxt/react-swipeable-list'
-import { Add, MoreVert, Task } from '@mui/icons-material'
+import {
+ Add,
+ Close,
+ MoreVert,
+ Search,
+ SearchOff,
+ Task,
+} from '@mui/icons-material'
import DeleteIcon from '@mui/icons-material/Delete'
import EditIcon from '@mui/icons-material/Edit'
import {
@@ -17,14 +24,18 @@ import {
CircularProgress,
Container,
IconButton,
+ Input,
Stack,
Typography,
} from '@mui/joy'
import { useQueryClient } from '@tanstack/react-query'
-import { useEffect, useState } from 'react'
+import Fuse from 'fuse.js'
+import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate, useSearchParams } from 'react-router-dom'
+import EmptyState from '../../components/common/EmptyState'
+import SortAndFilterMenu from '../../components/common/SortAndFilterMenu'
import { useChores } from '../../queries/ChoreQueries'
import { useUserProfile } from '../../queries/UserQueries'
import { getTextColorFromBackgroundColor } from '../../utils/Colors'
@@ -240,6 +251,83 @@ const ProjectView = () => {
const queryClient = useQueryClient()
const [confirmationModel, setConfirmationModel] = useState({})
const [showMoreInfoId, setShowMoreInfoId] = useState(null)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [sortBy, setSortBy] = useState(
+ () => localStorage.getItem('projectsSortBy') || 'name',
+ )
+ const [sortDirection, setSortDirection] = useState(
+ () => localStorage.getItem('projectsSortDirection') || 'asc',
+ )
+ const [ownershipFilter, setOwnershipFilter] = useState('all')
+ const searchInputRef = useRef(null)
+
+ useEffect(() => {
+ localStorage.setItem('projectsSortBy', sortBy)
+ localStorage.setItem('projectsSortDirection', sortDirection)
+ }, [sortBy, sortDirection])
+
+ const visibleProjects = useMemo(() => {
+ if (ownershipFilter === 'mine') {
+ return userProjects.filter(
+ project => project.created_by === userProfile?.id,
+ )
+ }
+ if (ownershipFilter === 'shared') {
+ return userProjects.filter(
+ project => project.created_by !== userProfile?.id,
+ )
+ }
+ return userProjects
+ }, [ownershipFilter, userProjects, userProfile?.id])
+
+ const fuse = useMemo(
+ () =>
+ new Fuse(visibleProjects, {
+ keys: ['name', 'description'],
+ includeScore: true,
+ isCaseSensitive: false,
+ findAllMatches: true,
+ }),
+ [visibleProjects],
+ )
+
+ const filteredProjects = useMemo(() => {
+ const matched = searchTerm
+ ? fuse.search(searchTerm).map(result => result.item)
+ : visibleProjects
+
+ const direction = sortDirection === 'desc' ? -1 : 1
+ return [...matched].sort((a, b) => {
+ switch (sortBy) {
+ case 'tasks':
+ return direction * ((taskCounts[a.id] || 0) - (taskCounts[b.id] || 0))
+ case 'created':
+ return direction * ((a.id || 0) - (b.id || 0))
+ case 'name':
+ default:
+ return direction * (a.name || '').localeCompare(b.name || '')
+ }
+ })
+ }, [fuse, searchTerm, visibleProjects, sortBy, sortDirection, taskCounts])
+
+ // The default project is pinned above the list, so it is matched separately.
+ const showDefaultProject = useMemo(() => {
+ if (ownershipFilter === 'shared') return false
+ if (!searchTerm) return true
+ return t('chores:toolbar.defaultProject')
+ .toLowerCase()
+ .includes(searchTerm.toLowerCase())
+ }, [ownershipFilter, searchTerm, t])
+
+ const handleSearchChange = e => {
+ setSearchTerm(e.target.value)
+ setShowMoreInfoId(null)
+ }
+
+ const handleSearchClose = () => {
+ setSearchTerm('')
+ searchInputRef.current?.blur()
+ }
const handleAddProject = () => {
setCurrentProject(null)
@@ -388,36 +476,114 @@ const ProjectView = () => {
+
+ }
+ endDecorator={
+ searchTerm && (
+
+
+
+ )
+ }
+ />
+ {
+ setOwnershipFilter(value)
+ setShowMoreInfoId(null)
+ }}
+ isActive={
+ ownershipFilter !== 'all' ||
+ sortBy !== 'name' ||
+ sortDirection !== 'asc'
+ }
+ />
+
+
+ {!showDefaultProject && filteredProjects.length === 0 && (
+ }
+ title={t('search.noResultsTitle')}
+ description={
+ searchTerm
+ ? t('search.noResultsDescription', { searchTerm })
+ : t('search.noFilterResultsDescription')
+ }
+ primaryAction={{
+ label: searchTerm ? t('search.clear') : t('search.showAll'),
+ onClick: () => {
+ handleSearchClose()
+ setOwnershipFilter('all')
+ },
+ }}
+ />
+ )}
{/* Default project - not swipeable */}
-
- handleCardClick({
+ {showDefaultProject && (
+
+ created_by: userProfile?.id,
+ }}
+ currentUserId={userProfile?.id}
+ taskCounts={{ default: taskCounts.default || 0 }}
+ onCardClick={() =>
+ handleCardClick({
+ id: 'default',
+ name: t('chores:toolbar.defaultProject'),
+ icon: 'FolderOpen',
+ color: '#1976d2',
+ })
+ }
+ />
+ )}
{/* User projects - swipeable */}
- {userProjects.map(project => (
+ {filteredProjects.map(project => (
handleCardClick(project)}
key={project.id}
diff --git a/src/views/Things/ThingsView.jsx b/src/views/Things/ThingsView.jsx
index 2136bbc..68d13aa 100644
--- a/src/views/Things/ThingsView.jsx
+++ b/src/views/Things/ThingsView.jsx
@@ -9,11 +9,14 @@ import {
} from '@meauxt/react-swipeable-list'
import {
Add,
+ Close,
Delete,
Edit,
Flip,
MoreVert,
PlusOne,
+ Search,
+ SearchOff,
ToggleOff,
ToggleOn,
Widgets,
@@ -24,14 +27,17 @@ import {
Chip,
Container,
IconButton,
+ Input,
Stack,
Typography,
} from '@mui/joy'
-import { useEffect, useState } from 'react'
+import Fuse from 'fuse.js'
+import { useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { track } from '../../analytics'
import EmptyState from '../../components/common/EmptyState'
+import SortAndFilterMenu from '../../components/common/SortAndFilterMenu'
import { useNotification } from '../../service/NotificationProvider'
import {
CreateThing,
@@ -215,8 +221,78 @@ const ThingsView = () => {
const [createModalThing, setCreateModalThing] = useState(null)
const [confirmModelConfig, setConfirmModelConfig] = useState({})
const [showMoreInfoId, setShowMoreInfoId] = useState(null)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [sortBy, setSortBy] = useState(
+ () => localStorage.getItem('thingsSortBy') || 'name',
+ )
+ const [sortDirection, setSortDirection] = useState(
+ () => localStorage.getItem('thingsSortDirection') || 'asc',
+ )
+ const [typeFilter, setTypeFilter] = useState('all')
+ const searchInputRef = useRef(null)
const { showError, showNotification } = useNotification()
+ useEffect(() => {
+ localStorage.setItem('thingsSortBy', sortBy)
+ localStorage.setItem('thingsSortDirection', sortDirection)
+ }, [sortBy, sortDirection])
+
+ const visibleThings = useMemo(
+ () =>
+ typeFilter === 'all'
+ ? things
+ : things.filter(thing => thing?.type === typeFilter),
+ [things, typeFilter],
+ )
+
+ const fuse = useMemo(
+ () =>
+ new Fuse(visibleThings, {
+ keys: ['name', 'state'],
+ includeScore: true,
+ isCaseSensitive: false,
+ findAllMatches: true,
+ }),
+ [visibleThings],
+ )
+
+ const filteredThings = useMemo(() => {
+ const matched = searchTerm
+ ? fuse.search(searchTerm).map(result => result.item)
+ : visibleThings
+
+ const direction = sortDirection === 'desc' ? -1 : 1
+ return [...matched].sort((a, b) => {
+ switch (sortBy) {
+ case 'type':
+ return direction * (a.type || '').localeCompare(b.type || '')
+ case 'state':
+ return (
+ direction *
+ String(a.state ?? '').localeCompare(String(b.state ?? ''))
+ )
+ case 'updated': {
+ const aDate = new Date(a.updatedAt || a.updated_at || 0).getTime()
+ const bDate = new Date(b.updatedAt || b.updated_at || 0).getTime()
+ return direction * (aDate - bDate)
+ }
+ case 'name':
+ default:
+ return direction * (a.name || '').localeCompare(b.name || '')
+ }
+ })
+ }, [fuse, searchTerm, visibleThings, sortBy, sortDirection])
+
+ const handleSearchChange = e => {
+ setSearchTerm(e.target.value)
+ setShowMoreInfoId(null)
+ }
+
+ const handleSearchClose = () => {
+ setSearchTerm('')
+ searchInputRef.current?.blur()
+ }
+
useEffect(() => {
// fetch things
GetThings().then(result => {
@@ -404,6 +480,67 @@ const ThingsView = () => {
+ {things.length > 0 && (
+
+ }
+ endDecorator={
+ searchTerm && (
+
+
+
+ )
+ }
+ />
+ {
+ setTypeFilter(value)
+ setShowMoreInfoId(null)
+ }}
+ isActive={
+ typeFilter !== 'all' ||
+ sortBy !== 'name' ||
+ sortDirection !== 'asc'
+ }
+ />
+
+ )}
{
}}
/>
)}
+ {things.length > 0 && filteredThings.length === 0 && (
+ }
+ title='No things match'
+ description={
+ searchTerm
+ ? `No thing matches "${searchTerm}".`
+ : 'No thing matches the current filter.'
+ }
+ primaryAction={{
+ label: searchTerm ? 'Clear search' : 'Show all things',
+ onClick: () => {
+ handleSearchClose()
+ setTypeFilter('all')
+ },
+ }}
+ />
+ )}
- {things.map(thing => (
+ {filteredThings.map(thing => (
navigate(`/things/${thing?.id}`)}
key={thing.id}