Refactor chore components and enhance scheduling features

- Removed redundant priority color function from CompactChoreCard and imported from Colors utility.
- Replaced FadeModal with ResponsiveModal in MultiSelectHelp for improved modal handling.
- Simplified MyChores component by removing archived chores state and related logic, and added keyboard shortcut for navigating to archived tasks.
- Integrated ResponsiveModal in RedeemPointsModal for consistent modal usage.
- Added quick scheduling options in ChoreActionMenu for better task management, including scheduling for today, tomorrow, weekend, and next week.
- Enhanced CustomParsers to support nth occurrence of days in monthly scheduling.
- Updated NavBar links and navigation logic for improved user experience.
This commit is contained in:
Mo Tarbin
2025-08-22 23:37:30 -04:00
parent 2d008a5cc3
commit 65f84b116f
12 changed files with 1275 additions and 254 deletions

View File

@@ -21,6 +21,7 @@ export const Z_INDEX = {
// Modals and Overlays (2000-8999)
MODAL_BACKDROP: 2000,
MODAL_CONTENT: 2001,
MODAL_CLOSE_BUTTON: 2002,
TOAST: 3000,
// Critical System UI (9000-9999)

View File

@@ -107,3 +107,18 @@ export const getTextColorFromBackgroundColor = bgColor => {
const b = parseInt(hex.substring(4, 6), 16)
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#000000' : '#ffffff'
}
export const getPriorityColor = priority => {
switch (priority) {
case 1:
return TASK_COLOR.PRIORITY_1
case 2:
return TASK_COLOR.PRIORITY_2
case 3:
return TASK_COLOR.PRIORITY_3
case 4:
return TASK_COLOR.PRIORITY_4
default:
return TASK_COLOR.NO_PRIORITY
}
}

View File

@@ -10,7 +10,8 @@ import {
Typography,
} from '@mui/joy'
import { useState } from 'react'
import FadeModal from '../../components/common/FadeModal'
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
import { VerifyMFA } from '../../utils/Fetcher'
const MFAVerificationModal = ({
@@ -24,7 +25,7 @@ const MFAVerificationModal = ({
const [isBackupCode, setIsBackupCode] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const { ResponsiveModal } = useResponsiveModal()
const handleVerify = async () => {
if (!verificationCode.trim()) {
setError('Please enter a verification code')
@@ -69,7 +70,7 @@ const MFAVerificationModal = ({
}
return (
<FadeModal open={open} onClose={handleClose} size='sm'>
<ResponsiveModal open={open} onClose={handleClose} size='sm'>
<ModalClose />
<Box className='mb-4 text-center'>
@@ -150,7 +151,7 @@ const MFAVerificationModal = ({
</Typography>
</Alert>
</Stack>
</FadeModal>
</ResponsiveModal>
)
}

View File

@@ -1,9 +1,4 @@
import {
Add,
ChevronRight,
ExpandMore,
HorizontalRule,
} from '@mui/icons-material'
import { Add, HorizontalRule } from '@mui/icons-material'
import {
Box,
Button,
@@ -107,12 +102,7 @@ const ChoreEdit = () => {
const [errors, setErrors] = useState({})
const [attemptToSave, setAttemptToSave] = useState(false)
const [addLabelModalOpen, setAddLabelModalOpen] = useState(false)
const [expandedSections, setExpandedSections] = useState({
basicInfo: true,
assignment: true,
schedule: true,
taskSettings: true,
})
const { data: userLabelsRaw, isLoading: isUserLabelsLoading } = useLabels()
const updateChoreMutation = useUpdateChore()
const createChoreMutation = useCreateChore()
@@ -135,78 +125,6 @@ const ChoreEdit = () => {
const Navigate = useNavigate()
const toggleSection = sectionKey => {
setExpandedSections(prev => ({
...prev,
[sectionKey]: !prev[sectionKey],
}))
}
const CollapsibleSection = ({ sectionKey, title, subtitle, children }) => {
const isExpanded = expandedSections[sectionKey]
return (
<Box mb={4}>
<Box
onClick={() => toggleSection(sectionKey)}
sx={{
mx: 0,
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
width: '100%',
py: 2,
borderRadius: 'md',
backgroundColor: 'background.level1',
border: '1px solid',
borderColor: 'divider',
mb: isExpanded ? 2 : 0,
transition: 'all 0.2s ease-in-out',
'&:hover': {
backgroundColor: 'background.level2',
borderColor: 'primary.main',
},
}}
>
{isExpanded ? (
<ExpandMore sx={{ mr: 1, color: 'primary.main' }} />
) : (
<ChevronRight sx={{ mr: 1, color: 'text.secondary' }} />
)}
<Box sx={{ flexGrow: 1 }}>
<Typography
level='h3'
sx={{
color: isExpanded ? 'primary.main' : 'text.primary',
fontWeight: 'bold',
mb: subtitle ? 0.5 : 0,
}}
>
{title}
</Typography>
{subtitle && (
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
{subtitle}
</Typography>
)}
</Box>
</Box>
{isExpanded && (
<Box
sx={{
// pl: 2,
// pr: 2,
pb: 2,
}}
>
{children}
</Box>
)}
</Box>
)
}
const HandleValidateChore = () => {
const errors = {}
@@ -493,15 +411,21 @@ const ChoreEdit = () => {
return (
<Container maxWidth='md'>
{/* Section 1: Basic Information */}
<CollapsibleSection
sectionKey='basicInfo'
title='Basic Information'
subtitle='Essential details about your task'
<Box mb={4}>
{/* <Typography
level='h4'
mb={2}
sx={{ borderBottom: '2px solid', borderColor: 'primary.main', pb: 1 }}
>
Basic Information
</Typography> */}
<Box mb={3}>
<FormControl error={errors.name}>
<Typography level='h4'>Name</Typography>
<Typography level='h5'>What is the name of this chore?</Typography>
<Typography level='body-md'>
What is the name of this task?
</Typography>
<Input value={name} onChange={e => setName(e.target.value)} />
<FormHelperText error>{errors.name}</FormHelperText>
</FormControl>
@@ -510,7 +434,7 @@ const ChoreEdit = () => {
<Box mb={3}>
<FormControl error={errors.description}>
<Typography level='h4'>Description</Typography>
<Typography level='h5'>What is this task about?</Typography>
<Typography level='body-md'>What is this task about?</Typography>
<RichTextEditor
value={description}
onChange={setDescription}
@@ -523,7 +447,7 @@ const ChoreEdit = () => {
<Box mb={3}>
<Typography level='h4'>Priority</Typography>
<Typography level='h5'>How important is this task?</Typography>
<Typography level='body-md'>How important is this task?</Typography>
{/* Priority Chip Selection */}
<Box
@@ -576,7 +500,7 @@ const ChoreEdit = () => {
<Box mb={3}>
<Typography level='h4'>Labels</Typography>
<Typography level='h5'>
<Typography level='body-md'>
Things to remember about this task or to tag it
</Typography>
<Select
@@ -675,17 +599,13 @@ const ChoreEdit = () => {
/>
</Card>
</Box>
</CollapsibleSection>
</Box>
{/* Section 2: Assignment & Responsibility */}
<CollapsibleSection
sectionKey='assignment'
title='Assignment & Responsibility'
subtitle='Who will be responsible for this task'
>
<Box mb={4}>
<Box mb={3}>
<Typography level='h4'>Assignees</Typography>
<Typography level='h5'>Who can do this task?</Typography>
<Typography level='body-md'>Who can do this task?</Typography>
<Card>
<List
orientation='horizontal'
@@ -729,7 +649,9 @@ const ChoreEdit = () => {
<>
<Box mb={3}>
<Typography level='h4'>Currently Assigned To</Typography>
<Typography level='h5'>Who is assigned the next due?</Typography>
<Typography level='body-md'>
Who is assigned the next due?
</Typography>
<Select
placeholder={
assignees.length === 0
@@ -757,7 +679,7 @@ const ChoreEdit = () => {
<Box>
<Typography level='h4'>Assignment Strategy</Typography>
<Typography level='h5'>
<Typography level='body-md'>
How to pick the next assignee for the following task?
</Typography>
<Card>
@@ -789,14 +711,10 @@ const ChoreEdit = () => {
</Box>
</>
)}
</CollapsibleSection>
</Box>
{/* Section 3: Schedule & Timing */}
<CollapsibleSection
sectionKey='schedule'
title='Schedule & Timing'
subtitle='When and how often this task should be done'
>
<Box mb={4}>
<RepeatSection
frequency={frequency}
onFrequencyUpdate={setFrequency}
@@ -931,7 +849,7 @@ const ChoreEdit = () => {
{!['once', 'no_repeat'].includes(frequencyType) && (
<Box>
<Typography level='h4'>Scheduling Preferences</Typography>
<Typography level='h5'>
<Typography level='body-md'>
How to reschedule the next due date?
</Typography>
<RadioGroup name='tiers' sx={{ gap: 1, '& > div': { p: 1 } }}>
@@ -1084,14 +1002,21 @@ const ChoreEdit = () => {
</Card>
</Box>
)}
</CollapsibleSection>
</Box>
{/* Section 4: Task Settings */}
<CollapsibleSection
sectionKey='taskSettings'
title='Task Settings'
subtitle='Additional options and configurations'
<Box mb={4}>
<Typography
level='h3'
mb={2}
sx={{
borderColor: 'primary.main',
pb: 1,
}}
>
Task Settings:
</Typography>
<Box mb={3}>
<Typography level='h4'>Points System</Typography>
<FormControl sx={{ mt: 1 }}>
@@ -1160,16 +1085,16 @@ const ChoreEdit = () => {
</Box>
<Box>
<Typography level='h4'>Visibility</Typography>
<Typography level='h5' sx={{ mb: 2 }}>
Choose who can see this task
</Typography>
<Typography level='h4'>Privacy Settings</Typography>
<Typography level='body-md'>Who can see this task?</Typography>
<RadioGroup
name='isPrivate'
value={isPrivate}
onChange={event => setIsPrivate(event.target.value)}
onChange={event => {
setIsPrivate(event.target.value === 'true' ? true : false)
}}
sx={{
'& > div': { p: 1 },
'& > div': { py: 1 },
}}
>
<FormControl>
@@ -1177,14 +1102,14 @@ const ChoreEdit = () => {
<FormHelperText>Everyone in your circle</FormHelperText>
</FormControl>
<FormControl>
<Radio overlay value={true} label='Private' />
<Radio overlay value={true} label='Limited' />
<FormHelperText>
Only you and others that are assigned to the task
You and others that are assigned to the task
</FormHelperText>
</FormControl>
</RadioGroup>
</Box>
</CollapsibleSection>
</Box>
{choreId > 0 && (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, mt: 3 }}>

View File

@@ -0,0 +1,843 @@
import {
Archive,
CheckBox,
CheckBoxOutlineBlank,
Close,
Delete,
SelectAll,
Unarchive,
ViewAgenda,
ViewModule,
} from '@mui/icons-material'
import {
Box,
Button,
Container,
Divider,
IconButton,
Input,
List,
Typography,
} from '@mui/joy'
import Fuse from 'fuse.js'
import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
import { ChoreSorter } from '../../utils/Chores'
import {
DeleteChore,
GetArchivedChores,
UnArchiveChore,
} from '../../utils/Fetcher'
import LoadingComponent from '../components/Loading'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import ChoreCard from './ChoreCard'
import CompactChoreCard from './CompactChoreCard'
import MultiSelectHelp from './MultiSelectHelp'
const ArchivedTasks = () => {
const { data: userProfile, isLoading: isUserProfileLoading } =
useUserProfile()
const { showSuccess, showError } = useNotification()
const { impersonatedUser } = useImpersonateUser()
const [archivedChores, setArchivedChores] = useState([])
const [filteredChores, setFilteredChores] = useState([])
const [searchTerm, setSearchTerm] = useState('')
const [performers, setPerformers] = useState([])
const navigate = useNavigate()
const [viewMode, setViewMode] = useState(
localStorage.getItem('archivedChoreCardViewMode') || 'default',
)
const [isLoading, setIsLoading] = useState(true)
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
const searchInputRef = useRef(null)
// Multi-select state
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
const [selectedChores, setSelectedChores] = useState(new Set())
const [confirmModelConfig, setConfirmModelConfig] = useState({})
const { data: membersData, isLoading: membersLoading } = useCircleMembers()
useEffect(() => {
const loadArchivedChores = async () => {
if (!membersLoading && userProfile) {
setPerformers(membersData.res)
try {
const response = await GetArchivedChores()
const data = await response.json()
const sortedChores = data.res.sort(ChoreSorter)
setArchivedChores(sortedChores)
setFilteredChores(sortedChores)
} catch (error) {
showError({
title: 'Failed to load archived tasks',
message: 'Please try again later.',
})
} finally {
setIsLoading(false)
}
}
}
loadArchivedChores()
}, [membersLoading, userProfile, membersData])
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = event => {
const isHoldingCmdOrCtrl = event.ctrlKey || event.metaKey
if (isHoldingCmdOrCtrl) {
setShowKeyboardShortcuts(true)
}
// Ctrl/Cmd + F to focus search input
if (isHoldingCmdOrCtrl && event.key === 'f') {
event.preventDefault()
searchInputRef.current?.focus()
return
}
// Ctrl/Cmd + S Toggle Multi-select mode
if (isHoldingCmdOrCtrl && event.key === 's') {
event.preventDefault()
toggleMultiSelectMode()
return
}
// Ctrl/Cmd + A to select all
if (
isHoldingCmdOrCtrl &&
event.key === 'a' &&
!['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)
) {
event.preventDefault()
if (!isMultiSelectMode) {
setIsMultiSelectMode(true)
setTimeout(() => {
selectAllVisibleChores()
}, 0)
} else {
selectAllVisibleChores()
}
}
// Multi-select keyboard shortcuts
if (isMultiSelectMode) {
// Escape to clear selection or exit multi-select mode
if (event.key === 'Escape') {
event.preventDefault()
if (selectedChores.size > 0) {
clearSelection()
} else {
setIsMultiSelectMode(false)
}
return
}
// "r" key for bulk restore (unarchive)
if (
isHoldingCmdOrCtrl &&
event.key === 'r' &&
selectedChores.size > 0
) {
event.preventDefault()
handleBulkRestore()
return
}
// "e" key for bulk delete
if (
isHoldingCmdOrCtrl &&
event.key === 'e' &&
selectedChores.size > 0
) {
event.preventDefault()
handleBulkDelete()
return
}
}
}
const handleKeyUp = event => {
if (!event.ctrlKey && !event.metaKey) {
setShowKeyboardShortcuts(false)
}
}
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
}, [isMultiSelectMode, selectedChores.size])
const toggleViewMode = () => {
const modes = ['default', 'compact']
const currentIndex = modes.indexOf(viewMode)
const nextIndex = (currentIndex + 1) % modes.length
const newMode = modes[nextIndex]
setViewMode(newMode)
localStorage.setItem('archivedChoreCardViewMode', newMode)
}
const searchOptions = {
keys: ['name', 'raw_label'],
includeScore: true,
isCaseSensitive: false,
findAllMatches: true,
}
const fuse = new Fuse(
archivedChores.map(c => ({
...c,
raw_label: c.labelsV2?.map(c => c.name).join(' '),
})),
searchOptions,
)
const handleSearchChange = e => {
const search = e.target.value
if (search === '') {
setFilteredChores(archivedChores)
setSearchTerm('')
return
}
const term = search.toLowerCase()
setSearchTerm(term)
setFilteredChores(fuse.search(term).map(result => result.item))
}
const handleSearchClose = () => {
setSearchTerm('')
setFilteredChores(archivedChores)
searchInputRef.current?.blur()
}
const handleChoreUpdated = (updatedChore, event) => {
if (event === 'unarchive') {
// Remove from archived list when unarchived
const newArchivedChores = archivedChores.filter(
chore => chore.id !== updatedChore.id,
)
const newFilteredChores = filteredChores.filter(
chore => chore.id !== updatedChore.id,
)
setArchivedChores(newArchivedChores)
setFilteredChores(newFilteredChores)
showSuccess({
title: 'Task Restored',
message: 'The task has been restored and is now active.',
})
}
}
const handleChoreDeleted = deletedChore => {
const newArchivedChores = archivedChores.filter(
chore => chore.id !== deletedChore.id,
)
const newFilteredChores = filteredChores.filter(
chore => chore.id !== deletedChore.id,
)
setArchivedChores(newArchivedChores)
setFilteredChores(newFilteredChores)
showSuccess({
title: 'Task Deleted',
message: 'The archived task has been permanently deleted.',
})
}
// Multi-select helper functions
const toggleMultiSelectMode = () => {
const newMode = !isMultiSelectMode
setIsMultiSelectMode(newMode)
if (!newMode) {
setSelectedChores(new Set())
}
}
const toggleChoreSelection = choreId => {
const newSelection = new Set(selectedChores)
if (newSelection.has(choreId)) {
newSelection.delete(choreId)
} else {
newSelection.add(choreId)
}
setSelectedChores(newSelection)
}
const selectAllVisibleChores = () => {
const visibleChores =
searchTerm?.length > 0 ? filteredChores : archivedChores
if (visibleChores.length > 0) {
const allIds = new Set(visibleChores.map(chore => chore.id))
setSelectedChores(allIds)
}
}
const clearSelection = () => {
if (selectedChores.size === 0) {
setIsMultiSelectMode(false)
return
}
setSelectedChores(new Set())
}
const getSelectedChoresData = () => {
return Array.from(selectedChores)
.map(id => archivedChores.find(chore => chore.id === id))
.filter(Boolean)
}
// Bulk operations
const handleBulkRestore = async () => {
const selectedData = getSelectedChoresData()
if (selectedData.length === 0) return
setConfirmModelConfig({
isOpen: true,
title: 'Restore Tasks',
confirmText: 'Restore',
cancelText: 'Cancel',
message: `Restore ${selectedData.length} task${selectedData.length > 1 ? 's' : ''} to active list?`,
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
const restoredTasks = []
const failedTasks = []
for (const chore of selectedData) {
try {
await UnArchiveChore(chore.id)
restoredTasks.push(chore)
} catch (error) {
failedTasks.push(chore)
}
}
if (restoredTasks.length > 0) {
showSuccess({
title: '📤 Tasks Restored',
message: `Successfully restored ${restoredTasks.length} task${restoredTasks.length > 1 ? 's' : ''}.`,
})
// Remove restored tasks from archived list
const restoredIds = new Set(restoredTasks.map(c => c.id))
const newArchivedChores = archivedChores.filter(
c => !restoredIds.has(c.id),
)
const newFilteredChores = filteredChores.filter(
c => !restoredIds.has(c.id),
)
setArchivedChores(newArchivedChores)
setFilteredChores(newFilteredChores)
}
if (failedTasks.length > 0) {
showError({
title: 'Some Tasks Failed',
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be restored.`,
})
}
clearSelection()
} catch (error) {
showError({
title: 'Bulk Restore Failed',
message: 'An unexpected error occurred. Please try again.',
})
}
}
setConfirmModelConfig({})
},
})
}
const handleBulkDelete = async () => {
const selectedData = getSelectedChoresData()
if (selectedData.length === 0) return
setConfirmModelConfig({
isOpen: true,
title: 'Delete Archived Tasks',
confirmText: 'Delete',
cancelText: 'Cancel',
message: `Permanently delete ${selectedData.length} archived task${selectedData.length > 1 ? 's' : ''}?\n\nThis action cannot be undone.`,
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
const deletedTasks = []
const failedTasks = []
for (const chore of selectedData) {
try {
await DeleteChore(chore.id)
deletedTasks.push(chore)
} catch (error) {
failedTasks.push(chore)
}
}
if (deletedTasks.length > 0) {
showSuccess({
title: '🗑️ Tasks Deleted',
message: `Successfully deleted ${deletedTasks.length} task${deletedTasks.length > 1 ? 's' : ''}.`,
})
const deletedIds = new Set(deletedTasks.map(c => c.id))
const newArchivedChores = archivedChores.filter(
c => !deletedIds.has(c.id),
)
const newFilteredChores = filteredChores.filter(
c => !deletedIds.has(c.id),
)
setArchivedChores(newArchivedChores)
setFilteredChores(newFilteredChores)
}
if (failedTasks.length > 0) {
showError({
title: 'Some Tasks Failed',
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be deleted.`,
})
}
clearSelection()
} catch (error) {
showError({
title: 'Bulk Delete Failed',
message: 'An unexpected error occurred. Please try again.',
})
}
}
setConfirmModelConfig({})
},
})
}
// Helper function to render the appropriate card component
const renderChoreCard = (chore, key) => {
const CardComponent = viewMode === 'compact' ? CompactChoreCard : ChoreCard
return (
<CardComponent
key={key || chore.id}
chore={chore}
onChoreUpdate={handleChoreUpdated}
onChoreRemove={handleChoreDeleted}
performers={performers}
viewOnly={false}
// Multi-select props
isMultiSelectMode={isMultiSelectMode}
isSelected={selectedChores.has(chore.id)}
onSelectionToggle={() => toggleChoreSelection(chore.id)}
/>
)
}
if (isUserProfileLoading || performers.length === 0 || isLoading) {
return <LoadingComponent />
}
return (
<Container maxWidth='md'>
{/* Header */}
{/* <Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
pt: 2,
}}
>
<Typography
level='h3'
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
>
<Archive />
Archived Tasks
</Typography>
<Button
variant='outlined'
color='neutral'
startDecorator={<Close />}
onClick={() => navigate('/chores')}
sx={{ ml: 'auto' }}
>
Close
</Button>
</Box> */}
{/* Search and Controls */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignContent: 'center',
alignItems: 'center',
gap: 0.5,
mb: 2,
}}
>
<Input
slotProps={{ input: { ref: searchInputRef } }}
placeholder='Search archived tasks'
value={searchTerm}
fullWidth
sx={{
borderRadius: 24,
height: 24,
borderColor: 'text.disabled',
padding: 1,
}}
onChange={handleSearchChange}
startDecorator={
<KeyboardShortcutHint shortcut='F' show={showKeyboardShortcuts} />
}
endDecorator={
searchTerm && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<KeyboardShortcutHint
shortcut='X'
show={showKeyboardShortcuts}
/>
<IconButton
variant='plain'
size='sm'
onClick={handleSearchClose}
sx={{ borderRadius: '50%' }}
>
<Close />
</IconButton>
</Box>
)
}
/>
{/* View Mode Toggle Button */}
<IconButton
variant='outlined'
color='neutral'
size='sm'
sx={{
height: 32,
width: 32,
borderRadius: '50%',
}}
onClick={toggleViewMode}
title={
viewMode === 'default'
? 'Switch to Compact View'
: 'Switch to Card View'
}
>
{viewMode === 'default' ? <ViewAgenda /> : <ViewModule />}
</IconButton>
{/* Multi-select Toggle Button */}
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<IconButton
variant={isMultiSelectMode ? 'solid' : 'outlined'}
color={isMultiSelectMode ? 'primary' : 'neutral'}
size='sm'
sx={{
height: 32,
width: 32,
borderRadius: '50%',
}}
onClick={toggleMultiSelectMode}
title={
isMultiSelectMode
? 'Exit Multi-select Mode (Ctrl+S)'
: 'Enable Multi-select Mode (Ctrl+S)'
}
>
{isMultiSelectMode ? <CheckBox /> : <CheckBoxOutlineBlank />}
</IconButton>
<KeyboardShortcutHint
shortcut='S'
show={showKeyboardShortcuts}
sx={{
position: 'absolute',
top: -8,
right: -8,
zIndex: 1000,
}}
/>
</Box>
</Box>
{/* Multi-select Toolbar */}
{isMultiSelectMode && (
<Box
sx={{
position: 'sticky',
top: 0,
zIndex: 1000,
mb: 2,
}}
>
<Box
sx={{
backgroundColor: 'background.surface',
backdropFilter: 'blur(8px)',
borderRadius: 'lg',
p: 2,
border: '1px solid',
borderColor: 'divider',
boxShadow: 'm',
gap: 2,
display: 'flex',
flexDirection: {
sm: 'column',
md: 'row',
},
alignItems: {
xs: 'stretch',
sm: 'center',
},
justifyContent: {
xs: 'center',
sm: 'space-between',
},
}}
>
{/* Selection Info and Controls */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
flexWrap: {
xs: 'wrap',
sm: 'nowrap',
},
justifyContent: {
xs: 'center',
sm: 'flex-start',
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckBox sx={{ color: 'primary.500' }} />
<Typography level='body-sm' fontWeight='md'>
{selectedChores.size} task
{selectedChores.size !== 1 ? 's' : ''} selected
</Typography>
</Box>
<Divider
orientation='vertical'
sx={{
display: { xs: 'none', sm: 'block' },
}}
/>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
size='sm'
variant='outlined'
onClick={selectAllVisibleChores}
startDecorator={<SelectAll />}
disabled={selectedChores.size === filteredChores.length}
sx={{
minWidth: 'auto',
'--Button-paddingInline': '0.75rem',
position: 'relative',
}}
title='Select all visible tasks (Ctrl+A)'
>
All
{showKeyboardShortcuts && (
<KeyboardShortcutHint
shortcut='A'
sx={{
position: 'absolute',
top: -8,
right: -8,
zIndex: 1000,
}}
/>
)}
</Button>
<Button
size='sm'
variant='outlined'
onClick={clearSelection}
startDecorator={
selectedChores.size === 0 ? (
<Close />
) : (
<CheckBoxOutlineBlank />
)
}
sx={{
minWidth: 'auto',
'--Button-paddingInline': '0.75rem',
position: 'relative',
}}
title={`${selectedChores.size === 0 ? 'Close' : 'Clear'} multi-select (Esc)`}
>
{selectedChores.size === 0 ? 'Close' : 'Clear'}
{showKeyboardShortcuts && (
<KeyboardShortcutHint
withCtrl={false}
shortcut='Esc'
sx={{
position: 'absolute',
top: -8,
right: -8,
zIndex: 1000,
}}
/>
)}
</Button>
</Box>
</Box>
{/* Action Buttons */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: {
xs: 'wrap',
sm: 'nowrap',
},
justifyContent: {
xs: 'center',
sm: 'flex-end',
},
}}
>
<Button
size='sm'
variant='solid'
color='success'
onClick={handleBulkRestore}
startDecorator={<Unarchive />}
disabled={selectedChores.size === 0}
sx={{
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
position: 'relative',
}}
title='Restore selected tasks (R)'
>
Restore
{showKeyboardShortcuts && selectedChores.size > 0 && (
<KeyboardShortcutHint
shortcut='R'
sx={{
position: 'absolute',
top: -8,
right: -8,
zIndex: 1000,
}}
/>
)}
</Button>
<Button
size='sm'
variant='soft'
color='danger'
onClick={handleBulkDelete}
startDecorator={<Delete />}
disabled={selectedChores.size === 0}
sx={{
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
position: 'relative',
}}
title='Delete selected tasks (E)'
>
Delete
{showKeyboardShortcuts && selectedChores.size > 0 && (
<KeyboardShortcutHint
shortcut='E'
sx={{
position: 'absolute',
top: -8,
right: -8,
zIndex: 1000,
}}
/>
)}
</Button>
</Box>
</Box>
</Box>
)}
{/* Content */}
{filteredChores.length === 0 ? (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
height: '50vh',
}}
>
<Archive
sx={{
fontSize: '4rem',
mb: 1,
color: 'text.tertiary',
}}
/>
<Typography level='title-md' gutterBottom>
{searchTerm ? 'No archived tasks found' : 'No archived tasks'}
</Typography>
<Typography level='body-sm' color='text.secondary' sx={{ mb: 2 }}>
{searchTerm
? 'Try adjusting your search terms'
: 'Archived tasks will appear here when you archive them from the main task list'}
</Typography>
{searchTerm && (
<Button
onClick={handleSearchClose}
variant='outlined'
color='neutral'
>
Clear search
</Button>
)}
</Box>
) : (
<Box>
<Typography level='body-sm' color='text.secondary' sx={{ mb: 2 }}>
{filteredChores.length} archived task
{filteredChores.length !== 1 ? 's' : ''}
{searchTerm && ` matching "${searchTerm}"`}
</Typography>
<List sx={{ gap: viewMode === 'compact' ? 0 : 1 }}>
{filteredChores.map(chore =>
renderChoreCard(chore, `archived-${chore.id}`),
)}
</List>
</Box>
)}
{/* Multi-select Help */}
<MultiSelectHelp isVisible={isMultiSelectMode} />
{/* Confirmation Modal */}
{confirmModelConfig?.isOpen && (
<ConfirmationModal config={confirmModelConfig} />
)}
</Container>
)
}
export default ArchivedTasks

View File

@@ -31,8 +31,8 @@ import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
import { useNotification } from '../../service/NotificationProvider'
import { notInCompletionWindow } from '../../utils/Chores.jsx'
import {
getPriorityColor,
getTextColorFromBackgroundColor,
TASK_COLOR,
} from '../../utils/Colors.jsx'
import {
ApproveChore,
@@ -614,20 +614,6 @@ const CompactChoreCard = ({
return parts.join(' • ')
}
const getPriorityColor = priority => {
switch (priority) {
case 1:
return TASK_COLOR.PRIORITY_1
case 2:
return TASK_COLOR.PRIORITY_2
case 3:
return TASK_COLOR.PRIORITY_3
case 4:
return TASK_COLOR.PRIORITY_4
default:
return TASK_COLOR.NO_PRIORITY
}
}
const handleChorePause = () => {
PauseChore(chore.id).then(response => {
if (response.ok) {

View File

@@ -1,9 +1,11 @@
import { Close, HelpOutline, Keyboard } from '@mui/icons-material'
import { Box, Button, Card, Divider, IconButton, Typography } from '@mui/joy'
import { useState } from 'react'
import FadeModal from '../../components/common/FadeModal'
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
const MultiSelectHelp = ({ isVisible = true }) => {
const { ResponsiveModal } = useResponsiveModal()
const [isHelpOpen, setIsHelpOpen] = useState(false)
if (!isVisible) return null
@@ -32,7 +34,7 @@ const MultiSelectHelp = ({ isVisible = true }) => {
</IconButton>
{/* Help Modal */}
<FadeModal open={isHelpOpen} onClose={() => setIsHelpOpen(false)}>
<ResponsiveModal open={isHelpOpen} onClose={() => setIsHelpOpen(false)}>
<Box
sx={{
display: 'flex',
@@ -115,7 +117,7 @@ const MultiSelectHelp = ({ isVisible = true }) => {
Got it!
</Button>
</Box>
</FadeModal>
</ResponsiveModal>
</>
)
}

View File

@@ -17,7 +17,6 @@ import {
SkipNext,
Sort,
Style,
Unarchive,
ViewAgenda,
ViewModule,
} from '@mui/icons-material'
@@ -42,7 +41,7 @@ import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useChores } from '../../queries/ChoreQueries'
import { useNotification } from '../../service/NotificationProvider'
import { ArchiveChore, GetArchivedChores } from '../../utils/Fetcher'
import { ArchiveChore } from '../../utils/Fetcher'
import Priorities from '../../utils/Priorities'
import LoadingComponent from '../components/Loading'
import { useLabels } from '../Labels/LabelQueries'
@@ -76,7 +75,6 @@ const MyChores = () => {
const { showSuccess, showError, showWarning } = useNotification()
const { impersonatedUser } = useImpersonateUser()
const [chores, setChores] = useState([])
const [archivedChores, setArchivedChores] = useState(null)
const [filteredChores, setFilteredChores] = useState([])
const [searchFilter, setSearchFilter] = useState('All')
const [choreSections, setChoreSections] = useState([])
@@ -366,6 +364,13 @@ const MyChores = () => {
}
return
}
// Ctrl/Cmd + O to navigate to archived tasks
if (isHoldingCmdOrCtrl && event.key === 'o') {
event.preventDefault()
Navigate('/archived')
return
}
}
const handleKeyUp = event => {
if (!event.ctrlKey && !event.metaKey) {
@@ -524,16 +529,6 @@ const MyChores = () => {
newFilteredChores = newFilteredChores.filter(
chore => chore.id !== updatedChore.id,
)
if (archivedChores !== null) {
setArchivedChores([...archivedChores, updatedChore])
}
}
if (event === 'unarchive') {
newChores.push(updatedChore)
newFilteredChores.push(updatedChore)
setArchivedChores(
archivedChores.filter(chore => chore.id !== updatedChore.id),
)
}
setChores(newChores)
setFilteredChores(newFilteredChores)
@@ -726,9 +721,8 @@ const MyChores = () => {
}
const getSelectedChoresData = () => {
const allChores = [...chores, ...(archivedChores || [])]
return Array.from(selectedChores)
.map(id => allChores.find(chore => chore.id === id))
.map(id => chores.find(chore => chore.id === id))
.filter(Boolean)
}
@@ -822,14 +816,6 @@ const MyChores = () => {
title: '📦 Tasks Archived',
message: `Successfully archived ${archivedTasks.length} task${archivedTasks.length > 1 ? 's' : ''}.`,
})
// Update archived chores state
setArchivedChores([
...(archivedChores || []),
...archivedTasks.map(c => ({
...c,
archived: true,
})),
])
}
if (failedTasks.length > 0) {
showError({
@@ -1595,7 +1581,7 @@ const MyChores = () => {
Current Filter: {searchFilter}
</Chip>
)}
{filteredChores.length === 0 && archivedChores == null && (
{filteredChores.length === 0 && (
<Box
sx={{
display: 'flex',
@@ -1785,55 +1771,7 @@ const MyChores = () => {
justifyContent: 'center',
mt: 2,
}}
>
{archivedChores === null && (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Button
sx={{}}
onClick={() => {
GetArchivedChores()
.then(response => response.json())
.then(data => {
setArchivedChores(data.res)
})
}}
variant='outlined'
color='neutral'
startDecorator={<Unarchive />}
endDecorator={
<KeyboardShortcutHint
shortcut='O'
show={showKeyboardShortcuts}
/>
}
>
Show Archived
</Button>
</Box>
)}
{archivedChores !== null && (
<>
<Divider orientation='horizontal'>
<Chip
variant='soft'
color='danger'
size='md'
startDecorator={
<>
<Chip color='danger' size='sm' variant='plain'>
{archivedChores?.length}
</Chip>
</>
}
>
Archived
</Chip>
</Divider>
{archivedChores?.map(chore => renderChoreCard(chore))}
</>
)}
</Box>
></Box>
<Box
// variant='outlined'
sx={{

View File

@@ -15,6 +15,7 @@ import {
} from '@mui/joy'
import { useEffect, useState } from 'react'
import { useResponsiveModal } from '../../hooks/useResponsiveModal.js'
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
function RedeemPointsModal({ config }) {

View File

@@ -1,20 +1,25 @@
import {
Archive,
Cancel,
CopyAll,
Delete,
Edit,
ManageSearch,
MoreTime,
MoreVert,
NextWeek,
Nfc,
NoteAdd,
RecordVoiceOver,
SwitchAccessShortcut,
Today,
Unarchive,
Update,
ViewCarousel,
WbSunny,
Weekend,
} from '@mui/icons-material'
import { Divider, IconButton, Menu, MenuItem } from '@mui/joy'
import { Divider, IconButton, Menu, MenuItem, Tooltip } from '@mui/joy'
import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useNotification } from '../../service/NotificationProvider'
@@ -23,6 +28,7 @@ import {
DeleteChore,
SkipChore,
UnArchiveChore,
UpdateDueDate,
} from '../../utils/Fetcher'
const ChoreActionMenu = ({
@@ -158,6 +164,86 @@ const ChoreActionMenu = ({
handleMenuClose()
}
const getQuickScheduleDate = option => {
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
switch (option) {
case 'today': {
// Schedule for today at the next available slot: 9am, 12pm, 5pm, or now if after 5pm
const nowHour = now.getHours()
const scheduled = new Date(today)
if (nowHour < 9) {
scheduled.setHours(9, 0, 0, 0)
} else if (nowHour < 12) {
scheduled.setHours(12, 0, 0, 0)
} else if (nowHour < 17) {
scheduled.setHours(17, 0, 0, 0)
} else {
// After 5pm, use current time
scheduled.setHours(
now.getHours(),
now.getMinutes(),
now.getSeconds(),
now.getMilliseconds(),
)
}
return scheduled
}
case 'tomorrow-morning': {
const tomorrowMorning = new Date(today)
tomorrowMorning.setDate(today.getDate() + 1)
tomorrowMorning.setHours(9, 0, 0, 0)
return tomorrowMorning
}
case 'tomorrow': {
const tomorrow = new Date(today)
tomorrow.setDate(today.getDate() + 1)
tomorrow.setHours(12, 0, 0, 0) // Set to noon
return tomorrow
}
case 'tomorrow-afternoon': {
const tomorrowAfternoon = new Date(today)
tomorrowAfternoon.setDate(today.getDate() + 1)
tomorrowAfternoon.setHours(14, 0, 0, 0)
return tomorrowAfternoon
}
case 'weekend': {
const weekend = new Date(today)
const daysUntilSaturday = (6 - today.getDay() + 7) % 7 || 7
weekend.setDate(today.getDate() + daysUntilSaturday)
return weekend
}
case 'next-week': {
const nextWeek = new Date(today)
const daysUntilMonday = (1 - today.getDay() + 7) % 7 || 7
nextWeek.setDate(today.getDate() + daysUntilMonday)
return nextWeek
}
default:
return today
}
}
const handleQuickSchedule = option => {
const date = option === 'remove' ? null : getQuickScheduleDate(option)
UpdateDueDate(chore.id, date).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = {
...chore,
nextDueDate: date ? date.toISOString() : null,
}
onChoreUpdate(
newChore,
option === 'remove' ? 'due-date-removed' : 'rescheduled',
)
})
}
})
handleMenuClose()
}
return (
<>
<IconButton
@@ -240,6 +326,88 @@ const ChoreActionMenu = ({
History
</MenuItem>
<Divider />
<MenuItem
sx={{
display: 'flex',
justifyContent: 'space-around',
alignItems: 'center',
gap: 1,
cursor: 'default',
'&:hover': {
backgroundColor: 'transparent',
},
}}
onClick={e => e.stopPropagation()}
>
<Tooltip title='Today' placement='top'>
<IconButton
size='sm'
onClick={e => {
e.stopPropagation()
handleQuickSchedule('today')
}}
>
<Today />
</IconButton>
</Tooltip>
<Tooltip title='Tomorrow' placement='top'>
<IconButton
size='sm'
onClick={e => {
e.stopPropagation()
handleQuickSchedule('tomorrow')
}}
>
<WbSunny />
</IconButton>
</Tooltip>
{/* <Tooltip title='Tomorrow afternoon' placement='top'>
<IconButton
size='sm'
onClick={e => {
e.stopPropagation()
handleQuickSchedule('tomorrow-afternoon')
}}
>
<WbTwilight />
</IconButton>
</Tooltip> */}
<Tooltip title='Weekend' placement='top'>
<IconButton
size='sm'
onClick={e => {
e.stopPropagation()
handleQuickSchedule('weekend')
}}
>
<Weekend />
</IconButton>
</Tooltip>
<Tooltip title='Next week' placement='top'>
<IconButton
size='sm'
onClick={e => {
e.stopPropagation()
handleQuickSchedule('next-week')
}}
>
<NextWeek />
</IconButton>
</Tooltip>
<Tooltip title='Remove due date' placement='top'>
<IconButton
size='sm'
color='neutral'
onClick={e => {
e.stopPropagation()
handleQuickSchedule('remove')
}}
>
<Cancel />
</IconButton>
</Tooltip>
</MenuItem>
<Divider />
<MenuItem
onClick={e => {
e.stopPropagation()

View File

@@ -135,6 +135,18 @@ export const parseRepeatV2 = inputSentence => {
regex: /(\d+)(?:th|st|nd|rd)? of every month/i,
name: 'Every {day} of every month',
},
{
frequencyType: 'days_of_the_week:nth_occurrence',
regex:
/(first|second|third|fourth|last|\d+(?:st|nd|rd|th)?) (monday|tuesday|wednesday|thursday|friday|saturday|sunday) of (?:the )?month/i,
name: '{occurrence} {day} of the month',
},
{
frequencyType: 'days_of_the_week:nth_occurrence_multiple',
regex:
/((?:first|second|third|fourth|last|\d+(?:st|nd|rd|th)?)(?:,? (?:and |& )?))+\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)s? of (?:the )?month/i,
name: '{occurrences} {day} of the month',
},
{
frequencyType: 'daily',
regex: /(every day|daily|everyday)/i,
@@ -361,6 +373,116 @@ export const parseRepeatV2 = inputSentence => {
],
cleanedSentence: inputSentence.replace(match[0], '').trim(),
}
case 'days_of_the_week:nth_occurrence':
const occurrenceText = match[1].toLowerCase()
const dayName = match[2].toLowerCase()
// Map occurrence words to numbers
const occurrenceMap = {
first: 1,
'1st': 1,
second: 2,
'2nd': 2,
third: 3,
'3rd': 3,
fourth: 4,
'4th': 4,
last: -1,
}
const occurrence =
occurrenceMap[occurrenceText] ||
parseInt(occurrenceText.replace(/\D/g, ''), 10)
if (!VALID_DAYS[dayName]) {
return { result: null, name: null, cleanedSentence: inputSentence }
}
result.frequencyType = 'days_of_the_week'
result.frequencyMetadata.days = [VALID_DAYS[dayName].toLowerCase()]
result.frequencyMetadata.weekPattern = 'nth_day_of_month'
result.frequencyMetadata.occurrences = [occurrence]
const startIndex = inputSentence
.toLowerCase()
.indexOf(match[0].toLowerCase())
return {
result,
name: pattern.name
.replace('{occurrence}', match[1])
.replace('{day}', VALID_DAYS[dayName]),
highlight: [
{
text: inputSentence.substring(
startIndex,
startIndex + match[0].length,
),
start: startIndex,
end: startIndex + match[0].length,
},
],
cleanedSentence: inputSentence.replace(match[0], '').trim(),
}
case 'days_of_the_week:nth_occurrence_multiple':
const occurrencesText = match[1].toLowerCase()
const dayName2 = match[2].toLowerCase()
if (!VALID_DAYS[dayName2]) {
return { result: null, name: null, cleanedSentence: inputSentence }
}
// Parse multiple occurrences like "first, second and third"
const occurrences = occurrencesText
.replace(/,?\s*(and|&)\s*/g, ' ')
.split(/\s+/)
.filter(word => word.trim())
.map(word => {
const cleanWord = word.replace(',', '').trim()
const occurrenceMap = {
first: 1,
'1st': 1,
second: 2,
'2nd': 2,
third: 3,
'3rd': 3,
fourth: 4,
'4th': 4,
last: -1,
}
return (
occurrenceMap[cleanWord] ||
parseInt(cleanWord.replace(/\D/g, ''), 10)
)
})
.filter(num => !isNaN(num))
result.frequencyType = 'days_of_the_week'
result.frequencyMetadata.days = [VALID_DAYS[dayName2].toLowerCase()]
result.frequencyMetadata.weekPattern = 'nth_day_of_month'
result.frequencyMetadata.occurrences = occurrences
const startIndex2 = inputSentence
.toLowerCase()
.indexOf(match[0].toLowerCase())
return {
result,
name: pattern.name
.replace('{occurrences}', match[1])
.replace('{day}', VALID_DAYS[dayName2]),
highlight: [
{
text: inputSentence.substring(
startIndex2,
startIndex2 + match[0].length,
),
start: startIndex2,
end: startIndex2 + match[0].length,
},
],
cleanedSentence: inputSentence.replace(match[0], '').trim(),
}
}
}
return {
@@ -375,17 +497,19 @@ export const parseAssignees = (inputSentence, users) => {
const sentence = inputSentence.toLowerCase()
const result = []
const highlight = []
for (const user of users) {
if (sentence.includes(`@${user.username.toLowerCase()}`)) {
// sort users by the longest so we remove first the full match:
for (const user of users.sort(
(a, b) => b.displayName.length - a.displayName.length,
)) {
if (sentence.includes(`@${user.displayName.toLowerCase()}`)) {
result.push(user)
const index = inputSentence
.toLowerCase()
.indexOf(`@${user.username.toLowerCase()}`)
.indexOf(`@${user.displayName.toLowerCase()}`)
highlight.push({
text: `@${user.username}`,
text: `@${user.displayName}`,
start: index,
end: index + user.username.length + 1,
end: index + user.displayName.length + 1,
})
}
}
@@ -395,7 +519,10 @@ export const parseAssignees = (inputSentence, users) => {
result,
highlight,
cleanedSentence: sentence.replace(
new RegExp(`@(${users.map(u => u.username).join('|')})`, 'g'),
new RegExp(
`@(${result.map(u => u.displayName.toLowerCase()).join('|')})`,
'g',
),
'',
),
}

View File

@@ -1,14 +1,13 @@
import Logo from '@/assets/logo.svg'
import {
AccountBox,
Archive,
ArrowBack,
History,
HomeOutlined,
Inbox,
ListAlt,
Logout,
MenuRounded,
Message,
SettingsOutlined,
ShareOutlined,
Toll,
Widgets,
} from '@mui/icons-material'
@@ -30,9 +29,14 @@ import ThemeToggleButton from '../Settings/ThemeToggleButton'
import NavBarLink from './NavBarLink'
const links = [
{
to: '/my/chores',
label: 'Home',
icon: <HomeOutlined />,
to: '/chores',
label: 'All Tasks',
icon: <Inbox />,
},
{
to: '/archived',
label: 'Archived',
icon: <Archive />,
},
// {
@@ -60,21 +64,21 @@ const links = [
label: 'Points',
icon: <Toll />,
},
{
to: '/settings#sharing',
label: 'Sharing',
icon: <ShareOutlined />,
},
{
to: '/settings#notifications',
label: 'Notifications',
icon: <Message />,
},
{
to: '/settings#account',
label: 'Account',
icon: <AccountBox />,
},
// {
// to: '/settings#sharing',
// label: 'Sharing',
// icon: <ShareOutlined />,
// },
// {
// to: '/settings#notifications',
// label: 'Notifications',
// icon: <Message />,
// },
// {
// to: '/settings#account',
// label: 'Account',
// icon: <AccountBox />,
// },
{
to: '/settings',
label: 'Settings',
@@ -119,16 +123,26 @@ const NavBar = () => {
backgroundColor: 'var(--joy-palette-background-body)',
}}
>
<IconButton size='md' variant='plain' onClick={() => setDrawerOpen(true)}>
{['/chores', '/'].includes(location.pathname) ? (
<IconButton
size='md'
variant='plain'
onClick={() => setDrawerOpen(true)}
>
<MenuRounded />
</IconButton>
) : (
<IconButton size='md' variant='plain' onClick={() => navigate(-1)}>
<ArrowBack />
</IconButton>
)}
<Box
className='flex items-center gap-2'
onClick={() => {
navigate('/my/chores')
navigate('/chores')
}}
>
<img component='img' src={Logo} width='25' />
<img src={Logo} width='25' alt='Logo' />
<Typography
level='title-lg'
sx={{