Add Task Modal for Task in a sentence
This commit is contained in:
parent
27979ce869
commit
62696002e6
4 changed files with 1027 additions and 388 deletions
11
src/App.jsx
11
src/App.jsx
|
@ -5,12 +5,11 @@ import { useEffect, useState } from 'react'
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||||
import { Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||||
|
import { registerCapacitorListeners } from './CapacitorListener'
|
||||||
import { UserContext } from './contexts/UserContext'
|
import { UserContext } from './contexts/UserContext'
|
||||||
import { AuthenticationProvider } from './service/AuthenticationService'
|
import { AuthenticationProvider } from './service/AuthenticationService'
|
||||||
import { GetUserProfile } from './utils/Fetcher'
|
import { GetUserProfile } from './utils/Fetcher'
|
||||||
import { isTokenValid } from './utils/TokenManager'
|
import { apiManager, isTokenValid } from './utils/TokenManager'
|
||||||
import { registerCapacitorListeners } from './CapacitorListener'
|
|
||||||
import {apiManager} from './utils/TokenManager'
|
|
||||||
|
|
||||||
const add = className => {
|
const add = className => {
|
||||||
document.getElementById('root').classList.add(className)
|
document.getElementById('root').classList.add(className)
|
||||||
|
@ -22,9 +21,7 @@ const remove = className => {
|
||||||
// TODO: Update the interval to at 60 minutes
|
// TODO: Update the interval to at 60 minutes
|
||||||
const intervalMS = 5 * 60 * 1000 // 5 minutes
|
const intervalMS = 5 * 60 * 1000 // 5 minutes
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
startApiManager()
|
startApiManager()
|
||||||
startOpenReplay()
|
startOpenReplay()
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
@ -138,5 +135,5 @@ const startOpenReplay = () => {
|
||||||
export default App
|
export default App
|
||||||
|
|
||||||
const startApiManager = () => {
|
const startApiManager = () => {
|
||||||
apiManager.init();
|
apiManager.init()
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,6 +195,14 @@ const UpdateChoreHistory = (choreId, id, choreHistory) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UpdateChoreStatus = (choreId, status) => {
|
||||||
|
return Fetch(`/chores/${choreId}/status`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: HEADERS(),
|
||||||
|
body: JSON.stringify({ status }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const GetAllCircleMembers = async () => {
|
const GetAllCircleMembers = async () => {
|
||||||
const resp = await Fetch(`/circles/members`, {
|
const resp = await Fetch(`/circles/members`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
@ -415,6 +423,14 @@ const UpdateDueDate = (id, dueDate) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RedeemPoints = (userId, points, circleID) => {
|
||||||
|
return Fetch(`/circles/${circleID}/members/points/redeem`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: HEADERS(),
|
||||||
|
body: JSON.stringify({ points, userId }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const RefreshToken = () => {
|
const RefreshToken = () => {
|
||||||
const basedURL = apiManager.getApiURL()
|
const basedURL = apiManager.getApiURL()
|
||||||
return fetch(`${basedURL}/auth/refresh`, {
|
return fetch(`${basedURL}/auth/refresh`, {
|
||||||
|
@ -474,6 +490,7 @@ export {
|
||||||
LeaveCircle,
|
LeaveCircle,
|
||||||
MarkChoreComplete,
|
MarkChoreComplete,
|
||||||
PutNotificationTarget,
|
PutNotificationTarget,
|
||||||
|
RedeemPoints,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
ResetPassword,
|
ResetPassword,
|
||||||
SaveChore,
|
SaveChore,
|
||||||
|
@ -483,6 +500,7 @@ export {
|
||||||
UpdateChoreAssignee,
|
UpdateChoreAssignee,
|
||||||
UpdateChoreHistory,
|
UpdateChoreHistory,
|
||||||
UpdateChorePriority,
|
UpdateChorePriority,
|
||||||
|
UpdateChoreStatus,
|
||||||
UpdateDueDate,
|
UpdateDueDate,
|
||||||
UpdateLabel,
|
UpdateLabel,
|
||||||
UpdateNotificationTarget,
|
UpdateNotificationTarget,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
ExpandCircleDown,
|
ExpandCircleDown,
|
||||||
FilterAlt,
|
FilterAlt,
|
||||||
PriorityHigh,
|
PriorityHigh,
|
||||||
|
Search,
|
||||||
Sort,
|
Sort,
|
||||||
Style,
|
Style,
|
||||||
Unarchive,
|
Unarchive,
|
||||||
|
@ -44,11 +45,13 @@ import ChoreCard from './ChoreCard'
|
||||||
import IconButtonWithMenu from './IconButtonWithMenu'
|
import IconButtonWithMenu from './IconButtonWithMenu'
|
||||||
|
|
||||||
import { ChoresGrouper } from '../../utils/Chores'
|
import { ChoresGrouper } from '../../utils/Chores'
|
||||||
|
import TaskInput from '../components/AddTaskModal'
|
||||||
import {
|
import {
|
||||||
canScheduleNotification,
|
canScheduleNotification,
|
||||||
scheduleChoreNotification,
|
scheduleChoreNotification,
|
||||||
} from './LocalNotificationScheduler'
|
} from './LocalNotificationScheduler'
|
||||||
import NotificationAccessSnackbar from './NotificationAccessSnackbar'
|
import NotificationAccessSnackbar from './NotificationAccessSnackbar'
|
||||||
|
import Sidepanel from './Sidepanel'
|
||||||
|
|
||||||
const MyChores = () => {
|
const MyChores = () => {
|
||||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||||
|
@ -59,6 +62,10 @@ const MyChores = () => {
|
||||||
const [filteredChores, setFilteredChores] = useState([])
|
const [filteredChores, setFilteredChores] = useState([])
|
||||||
const [selectedFilter, setSelectedFilter] = useState('All')
|
const [selectedFilter, setSelectedFilter] = useState('All')
|
||||||
const [choreSections, setChoreSections] = useState([])
|
const [choreSections, setChoreSections] = useState([])
|
||||||
|
const [activeTextField, setActiveTextField] = useState('task')
|
||||||
|
const [taskInputFocus, setTaskInputFocus] = useState(0)
|
||||||
|
const searchInputRef = useRef()
|
||||||
|
const [searchInputFocus, setSearchInputFocus] = useState(0)
|
||||||
const [selectedChoreSection, setSelectedChoreSection] = useState('due_date')
|
const [selectedChoreSection, setSelectedChoreSection] = useState('due_date')
|
||||||
const [openChoreSections, setOpenChoreSections] = useState({})
|
const [openChoreSections, setOpenChoreSections] = useState({})
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
@ -169,6 +176,22 @@ const MyChores = () => {
|
||||||
}
|
}
|
||||||
}, [anchorEl])
|
}, [anchorEl])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchInputFocus > 0 && searchInputRef.current) {
|
||||||
|
searchInputRef.current.focus()
|
||||||
|
searchInputRef.current.selectionStart =
|
||||||
|
searchInputRef.current.value?.length
|
||||||
|
searchInputRef.current.selectionEnd = searchInputRef.current.value?.length
|
||||||
|
}
|
||||||
|
}, [searchInputFocus])
|
||||||
|
const updateChores = newChore => {
|
||||||
|
const newChores = chores
|
||||||
|
newChores.push(newChore)
|
||||||
|
setChores(newChores)
|
||||||
|
setFilteredChores(newChores)
|
||||||
|
setChoreSections(ChoresGrouper('due_date', newChores))
|
||||||
|
setSelectedFilter('All')
|
||||||
|
}
|
||||||
const handleMenuOutsideClick = event => {
|
const handleMenuOutsideClick = event => {
|
||||||
if (
|
if (
|
||||||
anchorEl &&
|
anchorEl &&
|
||||||
|
@ -315,408 +338,477 @@ const MyChores = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth='md'>
|
<div
|
||||||
<Box
|
style={{
|
||||||
sx={{
|
display: 'flex',
|
||||||
display: 'flex',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
}}
|
||||||
alignContent: 'center',
|
>
|
||||||
alignItems: 'center',
|
<Container maxWidth='md'>
|
||||||
gap: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder='Search'
|
|
||||||
value={searchTerm}
|
|
||||||
fullWidth
|
|
||||||
sx={{
|
|
||||||
mt: 1,
|
|
||||||
mb: 1,
|
|
||||||
borderRadius: 24,
|
|
||||||
height: 24,
|
|
||||||
borderColor: 'text.disabled',
|
|
||||||
padding: 1,
|
|
||||||
}}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
endDecorator={
|
|
||||||
searchTerm && (
|
|
||||||
<CancelRounded
|
|
||||||
onClick={() => {
|
|
||||||
setSearchTerm('')
|
|
||||||
setFilteredChores(chores)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<IconButtonWithMenu
|
|
||||||
icon={<PriorityHigh />}
|
|
||||||
title='Filter by Priority'
|
|
||||||
options={Priorities}
|
|
||||||
selectedItem={selectedFilter}
|
|
||||||
onItemSelect={selected => {
|
|
||||||
handleLabelFiltering({ priority: selected.value })
|
|
||||||
}}
|
|
||||||
mouseClickHandler={handleMenuOutsideClick}
|
|
||||||
isActive={selectedFilter.startsWith('Priority: ')}
|
|
||||||
/>
|
|
||||||
<IconButtonWithMenu
|
|
||||||
icon={<Style />}
|
|
||||||
// TODO : this need simplification we want to display both user labels and chore labels
|
|
||||||
// that why we are merging them here.
|
|
||||||
// we also filter out the labels that user created as those will be part of user labels
|
|
||||||
title='Filter by Label'
|
|
||||||
options={[
|
|
||||||
...userLabels,
|
|
||||||
...chores
|
|
||||||
.map(c => c.labelsV2)
|
|
||||||
.flat()
|
|
||||||
.filter(l => l.created_by !== userProfile.id)
|
|
||||||
.map(l => {
|
|
||||||
// if user created it don't show it:
|
|
||||||
return {
|
|
||||||
...l,
|
|
||||||
name: l.name + ' (Shared Label)',
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
]}
|
|
||||||
selectedItem={selectedFilter}
|
|
||||||
onItemSelect={selected => {
|
|
||||||
handleLabelFiltering({ label: selected })
|
|
||||||
}}
|
|
||||||
isActive={selectedFilter.startsWith('Label: ')}
|
|
||||||
mouseClickHandler={handleMenuOutsideClick}
|
|
||||||
useChips
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
onClick={handleFilterMenuOpen}
|
|
||||||
variant='outlined'
|
|
||||||
color={
|
|
||||||
selectedFilter && FILTERS[selectedFilter] && selectedFilter != 'All'
|
|
||||||
? 'primary'
|
|
||||||
: 'neutral'
|
|
||||||
}
|
|
||||||
size='sm'
|
|
||||||
sx={{
|
|
||||||
height: 24,
|
|
||||||
borderRadius: 24,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FilterAlt />
|
|
||||||
</IconButton>
|
|
||||||
<List
|
|
||||||
orientation='horizontal'
|
|
||||||
wrap
|
|
||||||
sx={{
|
|
||||||
mt: 0.2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Menu
|
|
||||||
ref={menuRef}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
open={Boolean(anchorEl)}
|
|
||||||
onClose={handleFilterMenuClose}
|
|
||||||
>
|
|
||||||
{Object.keys(FILTERS).map((filter, index) => (
|
|
||||||
<MenuItem
|
|
||||||
key={`filter-list-${filter}-${index}`}
|
|
||||||
onClick={() => {
|
|
||||||
const filterFunction = FILTERS[filter]
|
|
||||||
const filteredChores =
|
|
||||||
filterFunction.length === 2
|
|
||||||
? filterFunction(chores, userProfile.id)
|
|
||||||
: filterFunction(chores)
|
|
||||||
setFilteredChores(filteredChores)
|
|
||||||
setSelectedFilter(filter)
|
|
||||||
handleFilterMenuClose()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filter}
|
|
||||||
<Chip color={selectedFilter === filter ? 'primary' : 'neutral'}>
|
|
||||||
{FILTERS[filter].length === 2
|
|
||||||
? FILTERS[filter](chores, userProfile.id).length
|
|
||||||
: FILTERS[filter](chores).length}
|
|
||||||
</Chip>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
{selectedFilter.startsWith('Label: ') ||
|
|
||||||
(selectedFilter.startsWith('Priority: ') && (
|
|
||||||
<MenuItem
|
|
||||||
key={`filter-list-cancel-all-filters`}
|
|
||||||
onClick={() => {
|
|
||||||
setFilteredChores(chores)
|
|
||||||
setSelectedFilter('All')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel All Filters
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
</List>
|
|
||||||
<Divider orientation='vertical' />
|
|
||||||
<IconButtonWithMenu
|
|
||||||
title='Group by'
|
|
||||||
icon={<Sort />}
|
|
||||||
options={[
|
|
||||||
{ name: 'Due Date', value: 'due_date' },
|
|
||||||
{ name: 'Priority', value: 'priority' },
|
|
||||||
{ name: 'Labels', value: 'labels' },
|
|
||||||
]}
|
|
||||||
selectedItem={selectedChoreSection}
|
|
||||||
onItemSelect={selected => {
|
|
||||||
const section = ChoresGrouper(selected.value, chores)
|
|
||||||
setChoreSections(section)
|
|
||||||
setSelectedChoreSection(selected.value)
|
|
||||||
setFilteredChores(chores)
|
|
||||||
setSelectedFilter('All')
|
|
||||||
}}
|
|
||||||
mouseClickHandler={handleMenuOutsideClick}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
{selectedFilter !== 'All' && (
|
|
||||||
<Chip
|
|
||||||
level='title-md'
|
|
||||||
gutterBottom
|
|
||||||
color='warning'
|
|
||||||
label={selectedFilter}
|
|
||||||
onDelete={() => {
|
|
||||||
setFilteredChores(chores)
|
|
||||||
setSelectedFilter('All')
|
|
||||||
}}
|
|
||||||
endDecorator={<CancelRounded />}
|
|
||||||
onClick={() => {
|
|
||||||
setFilteredChores(chores)
|
|
||||||
setSelectedFilter('All')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Current Filter: {selectedFilter}
|
|
||||||
</Chip>
|
|
||||||
)}
|
|
||||||
{filteredChores.length === 0 && (
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-between',
|
||||||
|
alignContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexDirection: 'column',
|
gap: 0.5,
|
||||||
height: '50vh',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditCalendar
|
{activeTextField == 'task' && (
|
||||||
sx={{
|
<TaskInput
|
||||||
fontSize: '4rem',
|
autoFocus={taskInputFocus}
|
||||||
// color: 'text.disabled',
|
onChoreUpdate={updateChores}
|
||||||
mb: 1,
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTextField == 'search' && (
|
||||||
|
<Input
|
||||||
|
ref={searchInputRef}
|
||||||
|
autoFocus={searchInputFocus > 0}
|
||||||
|
placeholder='Search'
|
||||||
|
value={searchTerm}
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
mb: 1,
|
||||||
|
borderRadius: 24,
|
||||||
|
height: 24,
|
||||||
|
borderColor: 'text.disabled',
|
||||||
|
padding: 1,
|
||||||
|
}}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
endDecorator={
|
||||||
|
searchTerm && (
|
||||||
|
<CancelRounded
|
||||||
|
onClick={() => {
|
||||||
|
setSearchTerm('')
|
||||||
|
setFilteredChores(chores)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTextField != 'task' && (
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
size='sm'
|
||||||
|
color='neutral'
|
||||||
|
sx={{
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 24,
|
||||||
|
minWidth: 100,
|
||||||
|
}}
|
||||||
|
startDecorator={<EditCalendar />}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTextField('task')
|
||||||
|
setTaskInputFocus(taskInputFocus + 1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Task
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{activeTextField != 'search' && (
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
color='neutral'
|
||||||
|
size='sm'
|
||||||
|
sx={{
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 24,
|
||||||
|
}}
|
||||||
|
startDecorator={<Search />}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTextField('search')
|
||||||
|
setSearchInputFocus(searchInputFocus + 1)
|
||||||
|
|
||||||
|
searchInputRef.current.focus()
|
||||||
|
searchInputRef.current.selectionStart =
|
||||||
|
searchInputRef.current.value?.length
|
||||||
|
searchInputRef.current.selectionEnd =
|
||||||
|
searchInputRef.current.value?.length
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Divider orientation='vertical' />
|
||||||
|
<IconButtonWithMenu
|
||||||
|
icon={<PriorityHigh />}
|
||||||
|
title='Filter by Priority'
|
||||||
|
options={Priorities}
|
||||||
|
selectedItem={selectedFilter}
|
||||||
|
onItemSelect={selected => {
|
||||||
|
handleLabelFiltering({ priority: selected.value })
|
||||||
}}
|
}}
|
||||||
|
mouseClickHandler={handleMenuOutsideClick}
|
||||||
|
isActive={selectedFilter.startsWith('Priority: ')}
|
||||||
/>
|
/>
|
||||||
<Typography level='title-md' gutterBottom>
|
<IconButtonWithMenu
|
||||||
Nothing scheduled
|
icon={<Style />}
|
||||||
</Typography>
|
// TODO : this need simplification we want to display both user labels and chore labels
|
||||||
{chores.length > 0 && (
|
// that why we are merging them here.
|
||||||
<>
|
// we also filter out the labels that user created as those will be part of user labels
|
||||||
|
title='Filter by Label'
|
||||||
|
options={[
|
||||||
|
...userLabels,
|
||||||
|
...chores
|
||||||
|
.map(c => c.labelsV2)
|
||||||
|
.flat()
|
||||||
|
.filter(l => l.created_by !== userProfile.id)
|
||||||
|
.map(l => {
|
||||||
|
// if user created it don't show it:
|
||||||
|
return {
|
||||||
|
...l,
|
||||||
|
name: l.name + ' (Shared Label)',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
selectedItem={selectedFilter}
|
||||||
|
onItemSelect={selected => {
|
||||||
|
handleLabelFiltering({ label: selected })
|
||||||
|
}}
|
||||||
|
isActive={selectedFilter.startsWith('Label: ')}
|
||||||
|
mouseClickHandler={handleMenuOutsideClick}
|
||||||
|
useChips
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleFilterMenuOpen}
|
||||||
|
variant='outlined'
|
||||||
|
color={
|
||||||
|
selectedFilter &&
|
||||||
|
FILTERS[selectedFilter] &&
|
||||||
|
selectedFilter != 'All'
|
||||||
|
? 'primary'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
size='sm'
|
||||||
|
sx={{
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FilterAlt />
|
||||||
|
</IconButton>
|
||||||
|
<List
|
||||||
|
orientation='horizontal'
|
||||||
|
wrap
|
||||||
|
sx={{
|
||||||
|
mt: 0.2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
ref={menuRef}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleFilterMenuClose}
|
||||||
|
>
|
||||||
|
{Object.keys(FILTERS).map((filter, index) => (
|
||||||
|
<MenuItem
|
||||||
|
key={`filter-list-${filter}-${index}`}
|
||||||
|
onClick={() => {
|
||||||
|
const filterFunction = FILTERS[filter]
|
||||||
|
const filteredChores =
|
||||||
|
filterFunction.length === 2
|
||||||
|
? filterFunction(chores, userProfile.id)
|
||||||
|
: filterFunction(chores)
|
||||||
|
setFilteredChores(filteredChores)
|
||||||
|
setSelectedFilter(filter)
|
||||||
|
handleFilterMenuClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filter}
|
||||||
|
<Chip
|
||||||
|
color={selectedFilter === filter ? 'primary' : 'neutral'}
|
||||||
|
>
|
||||||
|
{FILTERS[filter].length === 2
|
||||||
|
? FILTERS[filter](chores, userProfile.id).length
|
||||||
|
: FILTERS[filter](chores).length}
|
||||||
|
</Chip>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
{selectedFilter.startsWith('Label: ') ||
|
||||||
|
(selectedFilter.startsWith('Priority: ') && (
|
||||||
|
<MenuItem
|
||||||
|
key={`filter-list-cancel-all-filters`}
|
||||||
|
onClick={() => {
|
||||||
|
setFilteredChores(chores)
|
||||||
|
setSelectedFilter('All')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel All Filters
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</List>
|
||||||
|
<Divider orientation='vertical' />
|
||||||
|
<IconButtonWithMenu
|
||||||
|
title='Group by'
|
||||||
|
icon={<Sort />}
|
||||||
|
options={[
|
||||||
|
{ name: 'Due Date', value: 'due_date' },
|
||||||
|
{ name: 'Priority', value: 'priority' },
|
||||||
|
{ name: 'Labels', value: 'labels' },
|
||||||
|
]}
|
||||||
|
selectedItem={selectedChoreSection}
|
||||||
|
onItemSelect={selected => {
|
||||||
|
const section = ChoresGrouper(selected.value, chores)
|
||||||
|
setChoreSections(section)
|
||||||
|
setSelectedChoreSection(selected.value)
|
||||||
|
setFilteredChores(chores)
|
||||||
|
setSelectedFilter('All')
|
||||||
|
}}
|
||||||
|
mouseClickHandler={handleMenuOutsideClick}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{selectedFilter !== 'All' && (
|
||||||
|
<Chip
|
||||||
|
level='title-md'
|
||||||
|
gutterBottom
|
||||||
|
color='warning'
|
||||||
|
label={selectedFilter}
|
||||||
|
onDelete={() => {
|
||||||
|
setFilteredChores(chores)
|
||||||
|
setSelectedFilter('All')
|
||||||
|
}}
|
||||||
|
endDecorator={<CancelRounded />}
|
||||||
|
onClick={() => {
|
||||||
|
setFilteredChores(chores)
|
||||||
|
setSelectedFilter('All')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Current Filter: {selectedFilter}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
{filteredChores.length === 0 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '50vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditCalendar
|
||||||
|
sx={{
|
||||||
|
fontSize: '4rem',
|
||||||
|
// color: 'text.disabled',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography level='title-md' gutterBottom>
|
||||||
|
Nothing scheduled
|
||||||
|
</Typography>
|
||||||
|
{chores.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setFilteredChores(chores)
|
||||||
|
setSearchTerm('')
|
||||||
|
}}
|
||||||
|
variant='outlined'
|
||||||
|
color='neutral'
|
||||||
|
>
|
||||||
|
Reset filters
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{(searchTerm?.length > 0 || selectedFilter !== 'All') &&
|
||||||
|
filteredChores.map(chore => (
|
||||||
|
<ChoreCard
|
||||||
|
key={`filtered-${chore.id} `}
|
||||||
|
chore={chore}
|
||||||
|
onChoreUpdate={handleChoreUpdated}
|
||||||
|
onChoreRemove={handleChoreDeleted}
|
||||||
|
performers={performers}
|
||||||
|
userLabels={userLabels}
|
||||||
|
onChipClick={handleLabelFiltering}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{searchTerm.length === 0 && selectedFilter === 'All' && (
|
||||||
|
<AccordionGroup transition='0.2s ease' disableDivider>
|
||||||
|
{choreSections.map((section, index) => {
|
||||||
|
if (section.content.length === 0) return null
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
title={section.name}
|
||||||
|
key={section.name + index}
|
||||||
|
sx={{
|
||||||
|
my: 0,
|
||||||
|
}}
|
||||||
|
expanded={Boolean(openChoreSections[index])}
|
||||||
|
>
|
||||||
|
<Divider orientation='horizontal'>
|
||||||
|
<Chip
|
||||||
|
variant='soft'
|
||||||
|
color='neutral'
|
||||||
|
size='md'
|
||||||
|
onClick={() => {
|
||||||
|
if (openChoreSections[index]) {
|
||||||
|
const newOpenChoreSections = {
|
||||||
|
...openChoreSections,
|
||||||
|
}
|
||||||
|
delete newOpenChoreSections[index]
|
||||||
|
setOpenChoreSections(newOpenChoreSections)
|
||||||
|
} else {
|
||||||
|
setOpenChoreSections({
|
||||||
|
...openChoreSections,
|
||||||
|
[index]: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
endDecorator={
|
||||||
|
openChoreSections[index] ? (
|
||||||
|
<ExpandCircleDown
|
||||||
|
color='primary'
|
||||||
|
sx={{ transform: 'rotate(180deg)' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ExpandCircleDown color='primary' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
startDecorator={
|
||||||
|
<>
|
||||||
|
<Chip color='primary' size='sm' variant='soft'>
|
||||||
|
{section?.content?.length}
|
||||||
|
</Chip>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{section.name}
|
||||||
|
</Chip>
|
||||||
|
</Divider>
|
||||||
|
<AccordionDetails
|
||||||
|
sx={{
|
||||||
|
flexDirection: 'column',
|
||||||
|
my: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{section.content?.map(chore => (
|
||||||
|
<ChoreCard
|
||||||
|
key={chore.id}
|
||||||
|
chore={chore}
|
||||||
|
onChoreUpdate={handleChoreUpdated}
|
||||||
|
onChoreRemove={handleChoreDeleted}
|
||||||
|
performers={performers}
|
||||||
|
userLabels={userLabels}
|
||||||
|
onChipClick={handleLabelFiltering}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</AccordionGroup>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
// center the button
|
||||||
|
justifyContent: 'center',
|
||||||
|
mt: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{archivedChores === null && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
|
sx={{}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFilteredChores(chores)
|
GetArchivedChores()
|
||||||
setSearchTerm('')
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
setArchivedChores(data.res)
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
color='neutral'
|
color='neutral'
|
||||||
|
startDecorator={<Unarchive />}
|
||||||
>
|
>
|
||||||
Reset filters
|
Show Archived
|
||||||
</Button>
|
</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 => (
|
||||||
|
<ChoreCard
|
||||||
|
key={chore.id}
|
||||||
|
chore={chore}
|
||||||
|
onChoreUpdate={handleChoreUpdated}
|
||||||
|
onChoreRemove={handleChoreDeleted}
|
||||||
|
performers={performers}
|
||||||
|
userLabels={userLabels}
|
||||||
|
onChipClick={handleLabelFiltering}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
<Box
|
||||||
{(searchTerm?.length > 0 || selectedFilter !== 'All') &&
|
// variant='outlined'
|
||||||
filteredChores.map(chore => (
|
|
||||||
<ChoreCard
|
|
||||||
key={`filtered-${chore.id} `}
|
|
||||||
chore={chore}
|
|
||||||
onChoreUpdate={handleChoreUpdated}
|
|
||||||
onChoreRemove={handleChoreDeleted}
|
|
||||||
performers={performers}
|
|
||||||
userLabels={userLabels}
|
|
||||||
onChipClick={handleLabelFiltering}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{searchTerm.length === 0 && selectedFilter === 'All' && (
|
|
||||||
<AccordionGroup transition='0.2s ease' disableDivider>
|
|
||||||
{choreSections.map((section, index) => {
|
|
||||||
if (section.content.length === 0) return null
|
|
||||||
return (
|
|
||||||
<Accordion
|
|
||||||
title={section.name}
|
|
||||||
key={section.name + index}
|
|
||||||
sx={{
|
|
||||||
my: 0,
|
|
||||||
}}
|
|
||||||
expanded={Boolean(openChoreSections[index])}
|
|
||||||
>
|
|
||||||
<Divider orientation='horizontal'>
|
|
||||||
<Chip
|
|
||||||
variant='soft'
|
|
||||||
color='neutral'
|
|
||||||
size='md'
|
|
||||||
onClick={() => {
|
|
||||||
if (openChoreSections[index]) {
|
|
||||||
const newOpenChoreSections = { ...openChoreSections }
|
|
||||||
delete newOpenChoreSections[index]
|
|
||||||
setOpenChoreSections(newOpenChoreSections)
|
|
||||||
} else {
|
|
||||||
setOpenChoreSections({
|
|
||||||
...openChoreSections,
|
|
||||||
[index]: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
endDecorator={
|
|
||||||
openChoreSections[index] ? (
|
|
||||||
<ExpandCircleDown
|
|
||||||
color='primary'
|
|
||||||
sx={{ transform: 'rotate(180deg)' }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ExpandCircleDown color='primary' />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
startDecorator={
|
|
||||||
<>
|
|
||||||
<Chip color='primary' size='sm' variant='soft'>
|
|
||||||
{section?.content?.length}
|
|
||||||
</Chip>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{section.name}
|
|
||||||
</Chip>
|
|
||||||
</Divider>
|
|
||||||
<AccordionDetails
|
|
||||||
sx={{
|
|
||||||
flexDirection: 'column',
|
|
||||||
my: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{section.content?.map(chore => (
|
|
||||||
<ChoreCard
|
|
||||||
key={chore.id}
|
|
||||||
chore={chore}
|
|
||||||
onChoreUpdate={handleChoreUpdated}
|
|
||||||
onChoreRemove={handleChoreDeleted}
|
|
||||||
performers={performers}
|
|
||||||
userLabels={userLabels}
|
|
||||||
onChipClick={handleLabelFiltering}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</AccordionGroup>
|
|
||||||
)}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
// center the button
|
|
||||||
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 />}
|
|
||||||
>
|
|
||||||
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 => (
|
|
||||||
<ChoreCard
|
|
||||||
key={chore.id}
|
|
||||||
chore={chore}
|
|
||||||
onChoreUpdate={handleChoreUpdated}
|
|
||||||
onChoreRemove={handleChoreDeleted}
|
|
||||||
performers={performers}
|
|
||||||
userLabels={userLabels}
|
|
||||||
onChipClick={handleLabelFiltering}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
// variant='outlined'
|
|
||||||
sx={{
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: 0,
|
|
||||||
left: 10,
|
|
||||||
p: 2, // padding
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
gap: 2,
|
|
||||||
'z-index': 1000,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
color='primary'
|
|
||||||
variant='solid'
|
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: '50%',
|
position: 'fixed',
|
||||||
width: 50,
|
bottom: 0,
|
||||||
height: 50,
|
left: 10,
|
||||||
}}
|
p: 2, // padding
|
||||||
onClick={() => {
|
display: 'flex',
|
||||||
Navigate(`/chores/create`)
|
justifyContent: 'flex-end',
|
||||||
|
gap: 2,
|
||||||
|
'z-index': 1000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Add />
|
<IconButton
|
||||||
</IconButton>
|
color='primary'
|
||||||
</Box>
|
variant='solid'
|
||||||
<Snackbar
|
sx={{
|
||||||
open={isSnackbarOpen}
|
borderRadius: '50%',
|
||||||
onClose={() => {
|
width: 50,
|
||||||
setIsSnackbarOpen(false)
|
height: 50,
|
||||||
}}
|
}}
|
||||||
autoHideDuration={3000}
|
onClick={() => {
|
||||||
variant='soft'
|
Navigate(`/chores/create`)
|
||||||
color='success'
|
}}
|
||||||
size='lg'
|
>
|
||||||
invertedColors
|
<Add />
|
||||||
>
|
</IconButton>
|
||||||
<Typography level='title-md'>{snackBarMessage}</Typography>
|
</Box>
|
||||||
</Snackbar>
|
<Snackbar
|
||||||
<NotificationAccessSnackbar />
|
open={isSnackbarOpen}
|
||||||
</Container>
|
onClose={() => {
|
||||||
|
setIsSnackbarOpen(false)
|
||||||
|
}}
|
||||||
|
autoHideDuration={3000}
|
||||||
|
variant='soft'
|
||||||
|
color='success'
|
||||||
|
size='lg'
|
||||||
|
invertedColors
|
||||||
|
>
|
||||||
|
<Typography level='title-md'>{snackBarMessage}</Typography>
|
||||||
|
</Snackbar>
|
||||||
|
<NotificationAccessSnackbar />
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Sidepanel chores={chores} performers={performers} />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
532
src/views/components/AddTaskModal.jsx
Normal file
532
src/views/components/AddTaskModal.jsx
Normal file
|
@ -0,0 +1,532 @@
|
||||||
|
import { KeyboardReturnOutlined, OpenInFull } from '@mui/icons-material'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
Textarea,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
|
import { FormControl } from '@mui/material'
|
||||||
|
import * as chrono from 'chrono-node'
|
||||||
|
import moment from 'moment'
|
||||||
|
import { useContext, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { CSSTransition } from 'react-transition-group'
|
||||||
|
import { UserContext } from '../../contexts/UserContext'
|
||||||
|
import { CreateChore } from '../../utils/Fetcher'
|
||||||
|
const VALID_DAYS = {
|
||||||
|
monday: 'Monday',
|
||||||
|
mon: 'Monday',
|
||||||
|
tuesday: 'Tuesday',
|
||||||
|
tue: 'Tuesday',
|
||||||
|
wednesday: 'Wednesday',
|
||||||
|
wed: 'Wednesday',
|
||||||
|
thursday: 'Thursday',
|
||||||
|
thu: 'Thursday',
|
||||||
|
friday: 'Friday',
|
||||||
|
fri: 'Friday',
|
||||||
|
saturday: 'Saturday',
|
||||||
|
sat: 'Saturday',
|
||||||
|
sunday: 'Sunday',
|
||||||
|
sun: 'Sunday',
|
||||||
|
}
|
||||||
|
|
||||||
|
const VALID_MONTHS = {
|
||||||
|
january: 'January',
|
||||||
|
jan: 'January',
|
||||||
|
february: 'February',
|
||||||
|
feb: 'February',
|
||||||
|
march: 'March',
|
||||||
|
mar: 'March',
|
||||||
|
april: 'April',
|
||||||
|
apr: 'April',
|
||||||
|
may: 'May',
|
||||||
|
june: 'June',
|
||||||
|
jun: 'June',
|
||||||
|
july: 'July',
|
||||||
|
jul: 'July',
|
||||||
|
august: 'August',
|
||||||
|
aug: 'August',
|
||||||
|
september: 'September',
|
||||||
|
sep: 'September',
|
||||||
|
october: 'October',
|
||||||
|
oct: 'October',
|
||||||
|
november: 'November',
|
||||||
|
nov: 'November',
|
||||||
|
december: 'December',
|
||||||
|
dec: 'December',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL_MONTHS = Object.values(VALID_MONTHS).filter(
|
||||||
|
(v, i, a) => a.indexOf(v) === i,
|
||||||
|
)
|
||||||
|
|
||||||
|
const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
||||||
|
const { userProfile } = useContext(UserContext)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [taskText, setTaskText] = useState('')
|
||||||
|
const [taskTitle, setTaskTitle] = useState('')
|
||||||
|
const [openModal, setOpenModal] = useState(false)
|
||||||
|
const textareaRef = useRef(null)
|
||||||
|
const mainInputRef = useRef(null)
|
||||||
|
const [priority, setPriority] = useState('0')
|
||||||
|
const [dueDate, setDueDate] = useState(null)
|
||||||
|
const [description, setDescription] = useState(null)
|
||||||
|
const [frequency, setFrequency] = useState(null)
|
||||||
|
const [frequencyHumanReadable, setFrequencyHumanReadable] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (openModal && textareaRef.current) {
|
||||||
|
textareaRef.current.focus()
|
||||||
|
textareaRef.current.selectionStart = textareaRef.current.value?.length
|
||||||
|
textareaRef.current.selectionEnd = textareaRef.current.value?.length
|
||||||
|
}
|
||||||
|
}, [openModal])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFocus > 0 && mainInputRef.current) {
|
||||||
|
mainInputRef.current.focus()
|
||||||
|
mainInputRef.current.selectionStart = mainInputRef.current.value?.length
|
||||||
|
mainInputRef.current.selectionEnd = mainInputRef.current.value?.length
|
||||||
|
}
|
||||||
|
}, [autoFocus])
|
||||||
|
|
||||||
|
const handleEnterPressed = e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
createChore()
|
||||||
|
handleCloseModal()
|
||||||
|
setTaskText('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setOpenModal(false)
|
||||||
|
setTaskText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
createChore()
|
||||||
|
handleCloseModal()
|
||||||
|
setTaskText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsePriority = sentence => {
|
||||||
|
sentence = sentence.toLowerCase()
|
||||||
|
const priorityMap = {
|
||||||
|
1: ['p1', 'priority 1', 'high priority', 'urgent', 'asap', 'important'],
|
||||||
|
2: ['p2', 'priority 2', 'medium priority'],
|
||||||
|
3: ['p3', 'priority 3', 'low priority'],
|
||||||
|
4: ['p4', 'priority 4'],
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [priority, terms] of Object.entries(priorityMap)) {
|
||||||
|
if (terms.some(term => sentence.includes(term))) {
|
||||||
|
return {
|
||||||
|
result: priority,
|
||||||
|
cleanedSentence: terms.reduce((s, t) => s.replace(t, ''), sentence),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { result: 0, cleanedSentence: sentence }
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseRepeatV2 = sentence => {
|
||||||
|
const result = {
|
||||||
|
frequency: 1,
|
||||||
|
frequencyType: null,
|
||||||
|
frequencyMetadata: {
|
||||||
|
days: [],
|
||||||
|
months: [],
|
||||||
|
unit: null,
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const patterns = [
|
||||||
|
{
|
||||||
|
frequencyType: 'day_of_the_month:every',
|
||||||
|
regex: /(\d+)(?:th|st|nd|rd)? of every month$/i,
|
||||||
|
name: 'Every {day} of every month',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'daily',
|
||||||
|
regex: /(every day|daily)$/i,
|
||||||
|
name: 'Every day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'weekly',
|
||||||
|
regex: /(every week|weekly)$/i,
|
||||||
|
name: 'Every week',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'monthly',
|
||||||
|
regex: /(every month|monthly)$/i,
|
||||||
|
name: 'Every month',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'yearly',
|
||||||
|
regex: /every year$/i,
|
||||||
|
name: 'Every year',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'monthly',
|
||||||
|
regex: /every (?:other )?month$/i,
|
||||||
|
name: 'Bi Monthly',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'interval:2week',
|
||||||
|
regex: /(bi-?weekly|every other week)/i,
|
||||||
|
value: 2,
|
||||||
|
name: 'Bi Weekly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'interval',
|
||||||
|
regex: /every (\d+) (days?|weeks?|months?|years?).*$/i,
|
||||||
|
name: 'Every {frequency} {unit}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'interval:every_other',
|
||||||
|
regex: /every other (days?|weeks?|months?|years?)$/i,
|
||||||
|
name: 'Every other {unit}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'days_of_the_week',
|
||||||
|
regex: /every ([\w, ]+(?:day)?(?:, [\w, ]+(?:day)?)*)$/i,
|
||||||
|
name: 'Every {days} of the week',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
frequencyType: 'day_of_the_month',
|
||||||
|
regex: /(\d+)(?:st|nd|rd|th)? of ([\w ]+(?:(?:,| and |\s)[\w ]+)*)/i,
|
||||||
|
name: 'Every {day} days of {months}',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = sentence.match(pattern.regex)
|
||||||
|
if (!match) continue
|
||||||
|
|
||||||
|
result.frequencyType = pattern.frequencyType
|
||||||
|
const unitMap = {
|
||||||
|
daily: 'days',
|
||||||
|
weekly: 'weeks',
|
||||||
|
monthly: 'months',
|
||||||
|
yearly: 'years',
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (pattern.frequencyType) {
|
||||||
|
case 'daily':
|
||||||
|
case 'weekly':
|
||||||
|
case 'monthly':
|
||||||
|
case 'yearly':
|
||||||
|
result.frequencyType = 'interval'
|
||||||
|
result.frequency = pattern.value || 1
|
||||||
|
result.frequencyMetadata.unit = unitMap[pattern.frequencyType]
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name,
|
||||||
|
cleanedSentence: sentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'interval':
|
||||||
|
result.frequency = parseInt(match[1], 10)
|
||||||
|
result.frequencyMetadata.unit = match[2]
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name
|
||||||
|
.replace('{frequency}', result.frequency)
|
||||||
|
.replace('{unit}', result.frequencyMetadata.unit),
|
||||||
|
cleanedSentence: sentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'days_of_the_week':
|
||||||
|
result.frequencyMetadata.days = match[1]
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/ and |,|\s/)
|
||||||
|
.map(day => day.trim())
|
||||||
|
.filter(day => VALID_DAYS[day])
|
||||||
|
.map(day => VALID_DAYS[day])
|
||||||
|
if (!result.frequencyMetadata.days.length)
|
||||||
|
return { result: null, name: null, cleanedSentence: sentence }
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name.replace(
|
||||||
|
'{days}',
|
||||||
|
result.frequencyMetadata.days.join(', '),
|
||||||
|
),
|
||||||
|
cleanedSentence: sentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'day_of_the_month':
|
||||||
|
result.frequency = parseInt(match[1], 10)
|
||||||
|
result.frequencyMetadata.months = match[2]
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/ and |,|\s/)
|
||||||
|
.map(month => month.trim())
|
||||||
|
.filter(month => VALID_MONTHS[month])
|
||||||
|
.map(month => VALID_MONTHS[month])
|
||||||
|
result.frequencyMetadata.unit = 'days'
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name
|
||||||
|
.replace('{day}', result.frequency)
|
||||||
|
.replace('{months}', result.frequencyMetadata.months.join(', ')),
|
||||||
|
cleanedSentence: sentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'interval:every_other':
|
||||||
|
case 'interval:2week':
|
||||||
|
result.frequency = 2
|
||||||
|
result.frequencyMetadata.unit = 'weeks'
|
||||||
|
result.frequencyType = 'interval'
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name,
|
||||||
|
cleanedSentence: sentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'day_of_the_month:every':
|
||||||
|
result.frequency = parseInt(match[1], 10)
|
||||||
|
result.frequencyMetadata.months = ALL_MONTHS
|
||||||
|
result.frequencyMetadata.unit = 'days'
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
name: pattern.name
|
||||||
|
.replace('{day}', result.frequency)
|
||||||
|
.replace('{months}', result.frequencyMetadata.months.join(', ')),
|
||||||
|
cleanedSentence: sentence.replace(match[0], '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { result: null, name: null, cleanedSentence: sentence }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTextChange = e => {
|
||||||
|
if (!e.target.value) {
|
||||||
|
setTaskText('')
|
||||||
|
setOpenModal(false)
|
||||||
|
setDueDate(null)
|
||||||
|
setFrequency(null)
|
||||||
|
setFrequencyHumanReadable(null)
|
||||||
|
setPriority(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let cleanedSentence = e.target.value
|
||||||
|
const priority = parsePriority(cleanedSentence)
|
||||||
|
if (priority.result) setPriority(priority.result)
|
||||||
|
cleanedSentence = priority.cleanedSentence
|
||||||
|
|
||||||
|
const parsedDueDate = chrono.parse(cleanedSentence, new Date(), {
|
||||||
|
forwardDate: true,
|
||||||
|
})
|
||||||
|
if (parsedDueDate[0]?.index > -1) {
|
||||||
|
setDueDate(
|
||||||
|
moment(parsedDueDate[0].start.date()).format('YYYY-MM-DDTHH:mm:ss'),
|
||||||
|
)
|
||||||
|
cleanedSentence = cleanedSentence.replace(parsedDueDate[0].text, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const repeat = parseRepeatV2(cleanedSentence)
|
||||||
|
if (repeat.result) {
|
||||||
|
setFrequency(repeat.result)
|
||||||
|
setFrequencyHumanReadable(repeat.name)
|
||||||
|
cleanedSentence = repeat.cleanedSentence
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priority.result || parsedDueDate[0]?.index > -1 || repeat.result) {
|
||||||
|
setOpenModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTaskText(e.target.value)
|
||||||
|
setTaskTitle(cleanedSentence.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChore = () => {
|
||||||
|
const chore = {
|
||||||
|
name: taskTitle,
|
||||||
|
assignees: [{ userId: userProfile.id }],
|
||||||
|
dueDate: dueDate ? new Date(dueDate).toISOString() : null,
|
||||||
|
assignedTo: userProfile.id,
|
||||||
|
assignStrategy: 'random',
|
||||||
|
isRolling: false,
|
||||||
|
description: description || null,
|
||||||
|
labelsV2: [],
|
||||||
|
priority: priority || 0,
|
||||||
|
status: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frequency) {
|
||||||
|
chore.frequencyType = frequency.frequencyType
|
||||||
|
chore.frequencyMetadata = frequency.frequencyMetadata
|
||||||
|
chore.frequency = frequency.frequency
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateChore(chore).then(resp => {
|
||||||
|
resp.json().then(data => {
|
||||||
|
onChoreUpdate({ ...chore, id: data.res, nextDueDate: chore.dueDate })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!openModal && (
|
||||||
|
<CSSTransition in={!openModal} timeout={300} classNames='fade'>
|
||||||
|
<Input
|
||||||
|
autoFocus={autoFocus > 0}
|
||||||
|
ref={mainInputRef}
|
||||||
|
placeholder='Add a task...'
|
||||||
|
value={taskText}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
sx={{
|
||||||
|
fontSize: '16px',
|
||||||
|
mt: 1,
|
||||||
|
mb: 1,
|
||||||
|
borderRadius: 24,
|
||||||
|
height: 24,
|
||||||
|
borderColor: 'text.disabled',
|
||||||
|
padding: 1,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
onKeyUp={handleEnterPressed}
|
||||||
|
endDecorator={
|
||||||
|
<IconButton
|
||||||
|
variant='outlined'
|
||||||
|
sx={{ borderRadius: 24, marginRight: -0.5 }}
|
||||||
|
>
|
||||||
|
<KeyboardReturnOutlined />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CSSTransition>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Modal open={openModal} onClose={handleCloseModal}>
|
||||||
|
<ModalDialog>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
onClick={() => navigate(`/chores/create`)}
|
||||||
|
variant='outlined'
|
||||||
|
sx={{ position: 'absolute', right: 20 }}
|
||||||
|
startDecorator={<OpenInFull />}
|
||||||
|
>
|
||||||
|
Advance Mode
|
||||||
|
</Button>
|
||||||
|
<Typography level='h4'>
|
||||||
|
Create new task
|
||||||
|
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
|
||||||
|
Experimental
|
||||||
|
</Chip>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography level='body-sm'>Task in a sentence:</Typography>
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
ref={textareaRef}
|
||||||
|
value={taskText}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
onKeyUp={handleEnterPressed}
|
||||||
|
placeholder='Type your full text here...'
|
||||||
|
sx={{ width: '100%', fontSize: '16px' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography level='body-sm'>Title:</Typography>
|
||||||
|
<Input
|
||||||
|
value={taskTitle}
|
||||||
|
onChange={e => setTaskTitle(e.target.value)}
|
||||||
|
placeholder='Type your full text here...'
|
||||||
|
sx={{ width: '100%', fontSize: '16px' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography level='body-sm'>Description:</Typography>
|
||||||
|
<Textarea
|
||||||
|
minRows={2}
|
||||||
|
value={description}
|
||||||
|
onChange={e => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{ marginTop: 2, display: 'flex', flexDirection: 'row', gap: 2 }}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<Typography level='body-sm'>Priority</Typography>
|
||||||
|
<Select
|
||||||
|
value={priority}
|
||||||
|
onChange={(e, value) => setPriority(value)}
|
||||||
|
>
|
||||||
|
<Option value='0'>No Priority</Option>
|
||||||
|
<Option value='1'>P1</Option>
|
||||||
|
<Option value='2'>P2</Option>
|
||||||
|
<Option value='3'>P3</Option>
|
||||||
|
<Option value='4'>P4</Option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Typography level='body-sm'>Due Date</Typography>
|
||||||
|
<Input
|
||||||
|
type='datetime-local'
|
||||||
|
value={dueDate}
|
||||||
|
onChange={e => setDueDate(e.target.value)}
|
||||||
|
sx={{ width: '100%', fontSize: '16px' }}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'start',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<Typography level='body-sm'>Assignee</Typography>
|
||||||
|
<Select value={'0'}>
|
||||||
|
<Option value='0'>Me</Option>
|
||||||
|
<Option value='1'>Other</Option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Typography level='body-sm'>Frequency</Typography>
|
||||||
|
<Input value={frequencyHumanReadable || 'Once'} variant='plain' />
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'end',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
color='neutral'
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant='solid' color='primary' onClick={handleSubmit}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TaskInput
|
Loading…
Add table
Reference in a new issue