feat: Refactor Filter Section and Add Filter View

- Removed unused props and components from FilterSection.
- Introduced FilterView component to manage and display saved filters.
- Implemented swipe functionality for filter actions (edit, delete, pin).
- Enhanced AdvancedFilterBuilder with description and color selection.
- Deleted obsolete SaveFilterModal component.
- Updated NavBar to include a link to the new Filters page.
- Adjusted chore filter hooks for improved functionality and organization.
This commit is contained in:
Mo Tarbin
2026-01-26 17:27:27 -05:00
parent 2b8482cb06
commit ad45bafd62
13 changed files with 1103 additions and 434 deletions

View File

@@ -9,10 +9,12 @@ import {
ListItem,
Option,
Select,
Textarea,
Typography,
} from '@mui/joy'
import { useEffect, useMemo, useState } from 'react'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { FILTER_COLORS } from '../../../utils/Colors'
import { filterNameExists } from '../../../utils/CustomFilterStorage'
import { applyFilter } from '../../../utils/FilterEngine'
import Priorities from '../../../utils/Priorities'
@@ -30,19 +32,36 @@ const AdvancedFilterBuilder = ({
}) => {
const { ResponsiveModal } = useResponsiveModal()
const [filterName, setFilterName] = useState('')
const [filterDescription, setFilterDescription] = useState('')
const [filterColor, setFilterColor] = useState(FILTER_COLORS[0].value)
const [conditions, setConditions] = useState([
{ type: 'assignee', operator: 'is', value: [] },
])
const [error, setError] = useState('')
const [existedFilters] = useState(() => {
const storedFilters = localStorage.getItem('customFilters')
return storedFilters ? JSON.parse(storedFilters) : []
})
// Initialize state when editing a filter
useEffect(() => {
if (editingFilter) {
setFilterName(editingFilter.name)
setFilterDescription(editingFilter.description || '')
setFilterColor(editingFilter.color || FILTER_COLORS[0].value)
setConditions(editingFilter.conditions || [])
setError('')
} else {
setFilterName('')
setFilterDescription('')
// find color no filter has it :
const potentialColor = FILTER_COLORS.find(
color => !existedFilters.some(filter => filter.color === color.value),
)
setFilterColor(
potentialColor ? potentialColor.value : FILTER_COLORS[0].value,
)
setConditions([{ type: 'assignee', operator: 'is', value: [] }])
setError('')
}
@@ -127,6 +146,8 @@ const AdvancedFilterBuilder = ({
const filterData = {
name: filterName.trim(),
description: filterDescription.trim(),
color: filterColor,
conditions: validConditions,
operator: 'AND',
}
@@ -375,11 +396,27 @@ const AdvancedFilterBuilder = ({
}
return (
<ResponsiveModal open={isOpen} onClose={onClose} size='md'>
<Typography level='h4' sx={{ mb: 2 }}>
{editingFilter ? 'Edit Filter' : 'Create Advanced Filter'}
</Typography>
<ResponsiveModal
open={isOpen}
onClose={onClose}
size='lg'
title={editingFilter ? 'Edit Filter' : 'Create Advanced Filter'}
footer={
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
<Button variant='outlined' color='neutral' onClick={onClose}>
Cancel
</Button>
<Button
variant='solid'
color='primary'
onClick={handleSave}
startDecorator={<Save />}
>
{editingFilter ? 'Update Filter' : 'Save Filter'}
</Button>
</Box>
}
>
<Box
sx={{
display: 'flex',
@@ -409,6 +446,61 @@ const AdvancedFilterBuilder = ({
)}
</Box>
<Box>
<Typography level='body-sm' sx={{ mb: 1 }}>
Description (Optional)
</Typography>
<Textarea
placeholder='Optional description for this filter...'
value={filterDescription}
onChange={e => setFilterDescription(e.target.value)}
minRows={2}
maxRows={3}
/>
</Box>
<Box>
<Typography level='body-sm' sx={{ mb: 1 }}>
Color
</Typography>
<Select
value={filterColor}
onChange={(_, value) => value && setFilterColor(value)}
renderValue={selected => (
<Typography
startDecorator={
<Box
sx={{
width: 16,
height: 16,
borderRadius: '50%',
background: selected.value,
}}
/>
}
>
{selected.label}
</Typography>
)}
>
{FILTER_COLORS.map(color => (
<Option key={color.value} value={color.value}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box
sx={{
width: 20,
height: 20,
borderRadius: '50%',
background: color.value,
}}
/>
<Typography>{color.name}</Typography>
</Box>
</Option>
))}
</Select>
</Box>
<Box
sx={{
flex: 1,
@@ -572,20 +664,6 @@ const AdvancedFilterBuilder = ({
)}
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
<Button variant='outlined' color='neutral' onClick={onClose}>
Cancel
</Button>
<Button
variant='solid'
color='primary'
onClick={handleSave}
startDecorator={<Save />}
>
{editingFilter ? 'Update Filter' : 'Save Filter'}
</Button>
</Box>
</Box>
</ResponsiveModal>
)

View File

@@ -1,263 +0,0 @@
import { Save, Star, StarBorder } from '@mui/icons-material'
import {
Box,
Button,
Chip,
Input,
Modal,
ModalClose,
ModalDialog,
Typography,
} from '@mui/joy'
import { useState } from 'react'
import { filterNameExists } from '../../../utils/CustomFilterStorage'
import CompactChoreCard from '../../Chores/CompactChoreCard'
const SaveFilterModal = ({
isOpen,
onClose,
onSave,
filterData,
previewChores = [],
previewCount = 0,
previewOverdueCount = 0,
}) => {
const [filterName, setFilterName] = useState('')
const [isPinned, setIsPinned] = useState(false)
const [error, setError] = useState('')
const handleSave = () => {
if (!filterName.trim()) {
setError('Please enter a filter name')
return
}
if (filterNameExists(filterName.trim())) {
setError('A filter with this name already exists')
return
}
const newFilter = {
...filterData,
name: filterName.trim(),
isPinned,
}
onSave(newFilter)
onClose()
}
const getConditionLabel = condition => {
switch (condition.type) {
case 'assignee':
if (condition.value === 'me') return 'Assigned to me'
if (condition.value === 'others') return 'Assigned to others'
return 'Specific assignee'
case 'createdBy':
if (condition.value === 'me') return 'Created by me'
return 'Created by specific user'
case 'priority':
return `Priority ${condition.value}`
case 'status':
return condition.value === 3 ? 'Pending approval' : `Status ${condition.value}`
case 'dueDate':
if (condition.operator === 'isOverdue') return 'Overdue'
if (condition.operator === 'isDueToday') return 'Due today'
if (condition.operator === 'isDueThisWeek') return 'Due this week'
if (condition.operator === 'hasNoDueDate') return 'No due date'
return 'Due date condition'
case 'label':
return 'Has label'
case 'project':
if (condition.value === 'default') return 'Default project'
return 'Specific project'
default:
return condition.type
}
}
return (
<Modal open={isOpen} onClose={onClose}>
<ModalDialog
sx={{
maxWidth: 500,
width: '90%',
maxHeight: '90vh',
overflow: 'auto',
}}
>
<ModalClose />
<Typography level='h4' sx={{ mb: 2 }}>
Save Filter
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box>
<Typography level='body-sm' sx={{ mb: 1 }}>
Filter Name
</Typography>
<Input
placeholder='e.g., My High Priority Tasks'
value={filterName}
onChange={e => {
setFilterName(e.target.value)
setError('')
}}
error={!!error}
autoFocus
/>
{error && (
<Typography level='body-sm' color='danger' sx={{ mt: 0.5 }}>
{error}
</Typography>
)}
</Box>
<Box>
<Typography level='body-sm' sx={{ mb: 1 }}>
Filter Conditions
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{filterData.conditions.length === 0 ? (
<Typography level='body-sm' color='neutral'>
No filters applied
</Typography>
) : (
filterData.conditions.map((condition, index) => (
<Chip
key={index}
variant='soft'
color='neutral'
size='sm'
>
{getConditionLabel(condition)}
</Chip>
))
)}
</Box>
</Box>
<Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 1,
}}
>
<Typography level='body-sm'>Preview</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Chip size='sm' variant='soft' color='neutral'>
{previewCount} tasks
</Chip>
{previewOverdueCount > 0 && (
<Chip size='sm' variant='solid' color='danger'>
{previewOverdueCount} overdue
</Chip>
)}
</Box>
</Box>
<Box
sx={{
maxHeight: 200,
overflowY: 'auto',
bgcolor: 'background.level1',
p: 1,
borderRadius: 'sm',
}}
>
{previewCount === 0 ? (
<Typography
level='body-sm'
color='neutral'
sx={{ textAlign: 'center', py: 2 }}
>
No tasks match these filters
</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{previewChores.slice(0, 3).map(chore => (
<Box
key={chore.id}
sx={{
bgcolor: 'background.surface',
p: 1,
borderRadius: 'sm',
}}
>
<Typography level='body-sm'>{chore.name}</Typography>
</Box>
))}
{previewCount > 3 && (
<Typography
level='body-xs'
color='neutral'
sx={{ textAlign: 'center', mt: 0.5 }}
>
...and {previewCount - 3} more
</Typography>
)}
</Box>
)}
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
cursor: 'pointer',
p: 1,
borderRadius: 'sm',
'&:hover': {
bgcolor: 'background.level1',
},
}}
onClick={() => setIsPinned(!isPinned)}
>
{isPinned ? (
<Star color='warning' />
) : (
<StarBorder color='neutral' />
)}
<Box>
<Typography level='body-sm' fontWeight='md'>
Pin this filter
</Typography>
<Typography level='body-xs' color='neutral'>
Pinned filters appear first in the list
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
<Button variant='outlined' color='neutral' onClick={onClose}>
Cancel
</Button>
<Button
variant='solid'
color='primary'
onClick={handleSave}
startDecorator={<Save />}
disabled={!filterName.trim() || filterData.conditions.length === 0}
>
Save Filter
</Button>
</Box>
</Box>
</ModalDialog>
</Modal>
)
}
export default SaveFilterModal