feat: Implement advanced filtering in FilterBar and integrate ActiveFilterChips component

- Refactored FilterBar to utilize ActiveFilterChips for displaying active filters.
- Added selectable chip styles and improved hover effects for better UX.
- Enhanced filter handling with new props for managing active filters and results count.
- Introduced new hooks and state management for custom filters in MyChores and UserActivities.
- Updated ChoreToolbar to support new filter functionalities and improved UI components.
- Added client-side filtering capabilities in UserActivities with dynamic filter definitions.
This commit is contained in:
Mo Tarbin
2026-07-02 20:24:05 -04:00
parent 8e5c31f7f3
commit 165c76bb69
6 changed files with 603 additions and 394 deletions

View File

@@ -1,4 +1,4 @@
import { Check, Close, FilterList, Tune } from '@mui/icons-material'
import { Check, FilterList, Tune } from '@mui/icons-material'
import {
Avatar,
Badge,
@@ -11,6 +11,7 @@ import {
} from '@mui/joy'
import { useState } from 'react'
import BottomSheetModal from './BottomSheetModal'
import ActiveFilterChips from './filter/ActiveFilterChips'
/**
* Reusable filter bar component.
@@ -134,6 +135,47 @@ const FilterBar = ({
const hasActive = activeFilterCount > 0
const selectableChipSx = {
cursor: 'pointer',
transition: 'all 0.15s ease',
userSelect: 'none',
alignItems: 'center',
'& .MuiChip-startDecorator': {
display: 'flex',
alignItems: 'center',
mr: 0.5,
},
'& .MuiChip-label': {
lineHeight: 1.2,
},
'&:hover': { opacity: 0.85 },
}
const sectionBadgeChipSx = {
ml: 'auto',
fontSize: '0.7rem',
minHeight: 22,
py: 0.25,
px: 0.75,
alignItems: 'center',
'& .MuiChip-label': {
lineHeight: 1.2,
px: 0,
},
}
const modalCountChipSx = {
ml: 0.5,
minHeight: 22,
py: 0.25,
px: 0.75,
alignItems: 'center',
'& .MuiChip-label': {
lineHeight: 1.2,
px: 0,
},
}
// ── Chip labels for inline bar ─────────────────────────────────────────────
const getActiveChipLabel = def => {
@@ -223,85 +265,47 @@ const FilterBar = ({
color='primary'
size='sm'
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
sx={{ display: 'flex', alignItems: 'center' }}
>
<Button
size='sm'
size='md'
variant={hasActive ? 'solid' : 'outlined'}
color={hasActive ? 'primary' : 'neutral'}
startDecorator={<FilterList sx={{ fontSize: 16 }} />}
onClick={() => setIsOpen(true)}
sx={{ borderRadius: 'xl', gap: 0.5 }}
sx={{
borderRadius: 'xl',
py: 0.5,
px: 1,
gap: 0.5,
alignItems: 'center',
'& .MuiButton-startDecorator': {
display: 'flex',
alignItems: 'center',
mr: 0.5,
},
}}
>
Filters
</Button>
</Badge>
{(() => {
const activeChips = filterDefs
<ActiveFilterChips
chips={filterDefs
.map(def => ({ def, label: getActiveChipLabel(def) }))
.filter(({ label }) => !!label)
const MAX_VISIBLE = 2
const visible = activeChips.slice(0, MAX_VISIBLE)
const overflow = activeChips.length - MAX_VISIBLE
return (
<>
{visible.map(({ def, label }) => (
<Chip
key={def.id}
size='md'
variant='soft'
color='primary'
endDecorator={
<Close
sx={{ cursor: 'pointer' }}
onClick={e => {
e.stopPropagation()
onSetFilter(def.id, null)
}}
/>
}
onClick={() => setIsOpen(true)}
sx={{
py: 0.64,
cursor: 'pointer', transition: 'all 0.15s ease', '&:hover': { opacity: 0.85 } }}
>
{label}
</Chip>
))}
{overflow > 0 && (
<Chip
size='md'
variant='soft'
color='neutral'
onClick={() => setIsOpen(true)}
sx={{ py: 0.64, cursor: 'pointer', transition: 'all 0.15s ease', '&:hover': { opacity: 0.85 } }}
>
+{overflow} more
</Chip>
)}
</>
)
})()}
{hasActive && (
<Button
size='sm'
variant='plain'
color='neutral'
sx={{ px: 0.5, fontSize: '0.75rem', color: 'text.secondary', minHeight: 0 }}
onClick={onClearAll}
>
Clear all
</Button>
)}
{hasActive && resultCount !== undefined && totalCount !== undefined && (
<Typography level='body-xs' sx={{ color: 'text.tertiary', ml: 'auto', flexShrink: 0 }}>
{resultCount} / {totalCount}
</Typography>
)}
.map(({ def, label }) => ({
key: def.id,
label,
onClear: () => onSetFilter(def.id, null),
}))}
onOpen={() => setIsOpen(true)}
onClearAll={hasActive ? onClearAll : undefined}
resultCount={hasActive ? resultCount : undefined}
totalCount={hasActive ? totalCount : undefined}
maxVisible={2}
chipSize='md'
/>
</Box>
{/* ── Bottom sheet ────────────────────────────────────── */}
@@ -313,7 +317,7 @@ const FilterBar = ({
<Tune sx={{ fontSize: 20 }} />
Filters
{hasActive && (
<Chip size='sm' variant='solid' color='primary' sx={{ ml: 0.5 }}>
<Chip size='sm' variant='solid' color='primary' sx={modalCountChipSx}>
{activeFilterCount}
</Chip>
)}
@@ -356,20 +360,20 @@ const FilterBar = ({
{/* active badge in header */}
{def.type === 'multi-select' && (activeFilters[def.id]?.length ?? 0) > 0 && (
<Chip size='sm' variant='solid' color='primary' sx={{ ml: 'auto', fontSize: '0.7rem', height: 20 }}>
<Chip size='sm' variant='solid' color='primary' sx={sectionBadgeChipSx}>
{activeFilters[def.id].length} selected
</Chip>
)}
{def.type === 'single-select' && activeFilters[def.id] != null && (() => {
const opt = def.options?.find(o => o.value === activeFilters[def.id])
return opt ? (
<Chip size='sm' variant='solid' color='primary' sx={{ ml: 'auto', fontSize: '0.7rem', height: 20 }}>
<Chip size='sm' variant='solid' color='primary' sx={sectionBadgeChipSx}>
{opt.label}
</Chip>
) : null
})()}
{def.type === 'date-range' && getActiveChipLabel(def) && (
<Chip size='sm' variant='solid' color='primary' sx={{ ml: 'auto', fontSize: '0.7rem', height: 20 }}>
<Chip size='sm' variant='solid' color='primary' sx={sectionBadgeChipSx}>
{getActiveChipLabel(def)}
</Chip>
)}
@@ -393,7 +397,7 @@ const FilterBar = ({
) : (opt.icon ?? null)
}
onClick={() => handleMultiToggle(def.id, opt.value)}
sx={{ cursor: 'pointer', transition: 'all 0.15s ease', userSelect: 'none', '&:hover': { opacity: 0.85 } }}
sx={selectableChipSx}
>
{opt.label}
</Chip>
@@ -420,7 +424,7 @@ const FilterBar = ({
) : (opt.icon ?? null)
}
onClick={() => handleSingleToggle(def.id, opt.value)}
sx={{ cursor: 'pointer', transition: 'all 0.15s ease', userSelect: 'none', '&:hover': { opacity: 0.85 } }}
sx={selectableChipSx}
>
{opt.label}
</Chip>
@@ -436,7 +440,7 @@ const FilterBar = ({
color={activeFilters[def.id] ? 'primary' : 'neutral'}
startDecorator={activeFilters[def.id] ? <Check sx={{ fontSize: 14 }} /> : null}
onClick={() => handleBoolToggle(def.id)}
sx={{ cursor: 'pointer', transition: 'all 0.15s ease', userSelect: 'none', '&:hover': { opacity: 0.85 } }}
sx={selectableChipSx}
>
{def.label}
</Chip>
@@ -458,7 +462,7 @@ const FilterBar = ({
color={isSelected ? 'primary' : 'neutral'}
startDecorator={isSelected ? <Check sx={{ fontSize: 14 }} /> : null}
onClick={() => handleDateRangePreset(def.id, preset.value)}
sx={{ cursor: 'pointer', transition: 'all 0.15s ease', userSelect: 'none', '&:hover': { opacity: 0.85 } }}
sx={selectableChipSx}
>
{preset.label}
</Chip>

View File

@@ -0,0 +1,131 @@
import { Close } from '@mui/icons-material'
import { Box, Button, Chip, Typography } from '@mui/joy'
const ActiveFilterChips = ({
chips = [],
onOpen,
onClearAll,
resultCount,
totalCount,
maxVisible = 2,
chipSize = 'md',
clearButtonSize = 'sm',
clearButtonSx,
containerSx,
chipSx,
overflowChipSx,
resultSx,
}) => {
if (!chips.length) {
return null
}
const visible = chips.slice(0, maxVisible)
const overflow = chips.length - maxVisible
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'nowrap',
overflowX: 'auto',
py: 0.5,
'&::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
...containerSx,
}}
>
{visible.map(({ key, label, onClear, color = 'primary' }) => (
<Chip
key={key}
size={chipSize}
variant='soft'
color={color}
endDecorator={
<Close
sx={{ cursor: 'pointer', fontSize: chipSize === 'sm' ? 12 : 16 }}
onClick={e => {
e.stopPropagation()
onClear?.()
}}
/>
}
onClick={onOpen}
sx={{
cursor: 'pointer',
flexShrink: 0,
transition: 'all 0.15s ease',
alignItems: 'center',
'& .MuiChip-endDecorator': {
display: 'flex',
alignItems: 'center',
ml: 0.5,
},
'& .MuiChip-label': {
lineHeight: 1.2,
},
'&:hover': { opacity: 0.85 },
...chipSx,
}}
>
{label}
</Chip>
))}
{overflow > 0 && (
<Chip
size={chipSize}
variant='soft'
color='neutral'
onClick={onOpen}
sx={{
cursor: 'pointer',
flexShrink: 0,
transition: 'all 0.15s ease',
'&:hover': { opacity: 0.85 },
...overflowChipSx,
}}
>
+{overflow} more
</Chip>
)}
{resultCount != null && totalCount != null && (
<Typography
level='body-xs'
sx={{
color: 'text.tertiary',
ml: 'auto',
flexShrink: 0,
...resultSx,
}}
>
{resultCount} / {totalCount}
</Typography>
)}
{onClearAll && (
<Button
size={clearButtonSize}
variant='plain'
color='neutral'
onClick={onClearAll}
sx={{
px: 0.5,
fontSize: chipSize === 'sm' ? '0.72rem' : '0.75rem',
color: 'text.secondary',
minHeight: 0,
flexShrink: 0,
...clearButtonSx,
}}
>
Clear all
</Button>
)}
</Box>
)
}
export default ActiveFilterChips