Experiment: Add Sidepanel with calendar view
Support redeem points Experiment: task in sentences Fix small bugs in the chip when loading the chore card
This commit is contained in:
commit
3706c66f5f
17 changed files with 1665 additions and 478 deletions
23
package-lock.json
generated
23
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "donetick",
|
"name": "donetick",
|
||||||
"version": "0.1.83",
|
"version": "0.1.91",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "donetick",
|
"name": "donetick",
|
||||||
"version": "0.1.83",
|
"version": "0.1.91",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^6.1.1",
|
"@capacitor/android": "^6.1.1",
|
||||||
"@capacitor/app": "^6.0.0",
|
"@capacitor/app": "^6.0.0",
|
||||||
|
@ -26,6 +26,7 @@
|
||||||
"@openreplay/tracker": "^14.0.4",
|
"@openreplay/tracker": "^14.0.4",
|
||||||
"@tanstack/react-query": "^5.17.0",
|
"@tanstack/react-query": "^5.17.0",
|
||||||
"aos": "^2.3.4",
|
"aos": "^2.3.4",
|
||||||
|
"chrono-node": "^2.7.7",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
|
@ -5362,6 +5363,18 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chrono-node": {
|
||||||
|
"version": "2.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.7.7.tgz",
|
||||||
|
"integrity": "sha512-p3S7gotuTPu5oqhRL2p1fLwQXGgdQaRTtWR3e8Di9P1Pa9mzkK5DWR5AWBieMUh2ZdOnPgrK+zCrbbtyuA+D/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dayjs": "^1.10.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/classlist-polyfill": {
|
"node_modules/classlist-polyfill": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz",
|
||||||
|
@ -6060,6 +6073,12 @@
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
"@openreplay/tracker": "^14.0.4",
|
"@openreplay/tracker": "^14.0.4",
|
||||||
"@tanstack/react-query": "^5.17.0",
|
"@tanstack/react-query": "^5.17.0",
|
||||||
"aos": "^2.3.4",
|
"aos": "^2.3.4",
|
||||||
|
"chrono-node": "^2.7.7",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -137,5 +134,5 @@ const startOpenReplay = () => {
|
||||||
export default App
|
export default App
|
||||||
|
|
||||||
const startApiManager = () => {
|
const startApiManager = () => {
|
||||||
apiManager.init();
|
apiManager.init()
|
||||||
}
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useState } from 'react'
|
||||||
import { useQuery } from 'react-query'
|
import { useQuery } from 'react-query'
|
||||||
import { GetAllCircleMembers, GetAllUsers } from '../utils/Fetcher'
|
import { GetAllCircleMembers, GetAllUsers } from '../utils/Fetcher'
|
||||||
|
|
||||||
|
@ -6,5 +7,16 @@ export const useAllUsers = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCircleMembers = () => {
|
export const useCircleMembers = () => {
|
||||||
return useQuery('allCircleMembers', GetAllCircleMembers)
|
const [refetchKey, setRefetchKey] = useState(0)
|
||||||
|
|
||||||
|
const { data, error, isLoading, refetch } = useQuery(
|
||||||
|
['allCircleMembers', refetchKey],
|
||||||
|
GetAllCircleMembers,
|
||||||
|
)
|
||||||
|
const handleRefetch = () => {
|
||||||
|
setRefetchKey(prevKey => prevKey + 1)
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, error, isLoading, handleRefetch }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth'
|
||||||
|
import { Settings } from '@mui/icons-material'
|
||||||
import GoogleIcon from '@mui/icons-material/Google'
|
import GoogleIcon from '@mui/icons-material/Google'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
@ -15,15 +18,11 @@ import Cookies from 'js-cookie'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { LoginSocialGoogle } from 'reactjs-social-login'
|
import { LoginSocialGoogle } from 'reactjs-social-login'
|
||||||
import { API_URL, GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config'
|
import { GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config'
|
||||||
import { UserContext } from '../../contexts/UserContext'
|
import { UserContext } from '../../contexts/UserContext'
|
||||||
import Logo from '../../Logo'
|
import Logo from '../../Logo'
|
||||||
import { GetUserProfile, login } from '../../utils/Fetcher'
|
import { GetUserProfile, login } from '../../utils/Fetcher'
|
||||||
import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth';
|
import { apiManager } from '../../utils/TokenManager'
|
||||||
import { Capacitor } from '@capacitor/core'
|
|
||||||
import { Settings } from '@mui/icons-material'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const LoginView = () => {
|
const LoginView = () => {
|
||||||
const { userProfile, setUserProfile } = React.useContext(UserContext)
|
const { userProfile, setUserProfile } = React.useContext(UserContext)
|
||||||
|
@ -34,7 +33,6 @@ const LoginView = () => {
|
||||||
const handleSubmit = async e => {
|
const handleSubmit = async e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
login(username, password)
|
login(username, password)
|
||||||
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json().then(data => {
|
return response.json().then(data => {
|
||||||
|
@ -63,7 +61,8 @@ const LoginView = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const loggedWithProvider = function (provider, data) {
|
const loggedWithProvider = function (provider, data) {
|
||||||
return fetch(API_URL + `/auth/${provider}/callback`, {
|
const baseURL = apiManager.getApiURL()
|
||||||
|
return fetch(`${baseURL}/auth/${provider}/callback`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
@ -151,10 +150,11 @@ const LoginView = () => {
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Navigate('/login/settings')
|
Navigate('/login/settings')
|
||||||
|
}}
|
||||||
}
|
>
|
||||||
}
|
{' '}
|
||||||
> <Settings /></IconButton>
|
<Settings />
|
||||||
|
</IconButton>
|
||||||
{/* <img
|
{/* <img
|
||||||
src='/src/assets/logo.svg'
|
src='/src/assets/logo.svg'
|
||||||
alt='logo'
|
alt='logo'
|
||||||
|
@ -335,24 +335,29 @@ const LoginView = () => {
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</LoginSocialGoogle>
|
</LoginSocialGoogle>
|
||||||
</Box> )}
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{Capacitor.isNativePlatform() && (
|
{Capacitor.isNativePlatform() && (
|
||||||
<Box sx={{ width: '100%' }}>
|
<Box sx={{ width: '100%' }}>
|
||||||
<Button fullWidth variant='soft' size='lg' sx={{ mt: 3, mb: 2 }}
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant='soft'
|
||||||
|
size='lg'
|
||||||
|
sx={{ mt: 3, mb: 2 }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
GoogleAuth.initialize({
|
GoogleAuth.initialize({
|
||||||
clientId: import.meta.env.VITE_APP_GOOGLE_CLIENT_ID,
|
clientId: import.meta.env.VITE_APP_GOOGLE_CLIENT_ID,
|
||||||
scopes: ['profile', 'email', 'openid'],
|
scopes: ['profile', 'email', 'openid'],
|
||||||
grantOfflineAccess: true,
|
grantOfflineAccess: true,
|
||||||
});
|
})
|
||||||
GoogleAuth.signIn().then((user) => {
|
GoogleAuth.signIn().then(user => {
|
||||||
console.log("Google user", user);
|
console.log('Google user', user)
|
||||||
|
|
||||||
loggedWithProvider("google", user.authentication)
|
loggedWithProvider('google', user.authentication)
|
||||||
});
|
})
|
||||||
|
}}
|
||||||
}}>
|
>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<GoogleIcon />
|
<GoogleIcon />
|
||||||
Continue with Google
|
Continue with Google
|
||||||
|
@ -361,7 +366,6 @@ const LoginView = () => {
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Navigate('/signup')
|
Navigate('/signup')
|
||||||
|
|
|
@ -378,7 +378,7 @@ const ChoreEdit = () => {
|
||||||
</Typography> */}
|
</Typography> */}
|
||||||
<Box>
|
<Box>
|
||||||
<FormControl error={errors.name}>
|
<FormControl error={errors.name}>
|
||||||
<Typography level='h4'>Description :</Typography>
|
<Typography level='h4'>Title :</Typography>
|
||||||
<Typography level='h5'>What is this chore about?</Typography>
|
<Typography level='h5'>What is this chore about?</Typography>
|
||||||
<Input value={name} onChange={e => setName(e.target.value)} />
|
<Input value={name} onChange={e => setName(e.target.value)} />
|
||||||
<FormHelperText error>{errors.name}</FormHelperText>
|
<FormHelperText error>{errors.name}</FormHelperText>
|
||||||
|
|
|
@ -377,9 +377,13 @@ const ChoreCard = ({
|
||||||
const notSelectedShortMonths = notSelectedMonth.map(m =>
|
const notSelectedShortMonths = notSelectedMonth.map(m =>
|
||||||
moment().month(m).format('MMM'),
|
moment().month(m).format('MMM'),
|
||||||
)
|
)
|
||||||
return `${chore.frequency}${dayOfMonthSuffix(
|
let result = `Monthly ${chore.frequency}${dayOfMonthSuffix(
|
||||||
chore.frequency,
|
chore.frequency,
|
||||||
)} except ${notSelectedShortMonths.join(', ')}`
|
)}`
|
||||||
|
if (notSelectedShortMonths.length > 0)
|
||||||
|
result += `
|
||||||
|
except ${notSelectedShortMonths.join(', ')}`
|
||||||
|
return result
|
||||||
} else {
|
} else {
|
||||||
let freqData = JSON.parse(chore.frequencyMetadata)
|
let freqData = JSON.parse(chore.frequencyMetadata)
|
||||||
const months = freqData.months.map(m => moment().month(m).format('MMM'))
|
const months = freqData.months.map(m => moment().month(m).format('MMM'))
|
||||||
|
|
|
@ -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,6 +338,12 @@ const MyChores = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Container maxWidth='md'>
|
<Container maxWidth='md'>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -325,7 +354,17 @@ const MyChores = () => {
|
||||||
gap: 0.5,
|
gap: 0.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{activeTextField == 'task' && (
|
||||||
|
<TaskInput
|
||||||
|
autoFocus={taskInputFocus}
|
||||||
|
onChoreUpdate={updateChores}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTextField == 'search' && (
|
||||||
<Input
|
<Input
|
||||||
|
ref={searchInputRef}
|
||||||
|
autoFocus={searchInputFocus > 0}
|
||||||
placeholder='Search'
|
placeholder='Search'
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
@ -349,6 +388,51 @@ const MyChores = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{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
|
<IconButtonWithMenu
|
||||||
icon={<PriorityHigh />}
|
icon={<PriorityHigh />}
|
||||||
title='Filter by Priority'
|
title='Filter by Priority'
|
||||||
|
@ -388,12 +472,13 @@ const MyChores = () => {
|
||||||
mouseClickHandler={handleMenuOutsideClick}
|
mouseClickHandler={handleMenuOutsideClick}
|
||||||
useChips
|
useChips
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleFilterMenuOpen}
|
onClick={handleFilterMenuOpen}
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
color={
|
color={
|
||||||
selectedFilter && FILTERS[selectedFilter] && selectedFilter != 'All'
|
selectedFilter &&
|
||||||
|
FILTERS[selectedFilter] &&
|
||||||
|
selectedFilter != 'All'
|
||||||
? 'primary'
|
? 'primary'
|
||||||
: 'neutral'
|
: 'neutral'
|
||||||
}
|
}
|
||||||
|
@ -433,7 +518,9 @@ const MyChores = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{filter}
|
{filter}
|
||||||
<Chip color={selectedFilter === filter ? 'primary' : 'neutral'}>
|
<Chip
|
||||||
|
color={selectedFilter === filter ? 'primary' : 'neutral'}
|
||||||
|
>
|
||||||
{FILTERS[filter].length === 2
|
{FILTERS[filter].length === 2
|
||||||
? FILTERS[filter](chores, userProfile.id).length
|
? FILTERS[filter](chores, userProfile.id).length
|
||||||
: FILTERS[filter](chores).length}
|
: FILTERS[filter](chores).length}
|
||||||
|
@ -561,7 +648,9 @@ const MyChores = () => {
|
||||||
size='md'
|
size='md'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (openChoreSections[index]) {
|
if (openChoreSections[index]) {
|
||||||
const newOpenChoreSections = { ...openChoreSections }
|
const newOpenChoreSections = {
|
||||||
|
...openChoreSections,
|
||||||
|
}
|
||||||
delete newOpenChoreSections[index]
|
delete newOpenChoreSections[index]
|
||||||
setOpenChoreSections(newOpenChoreSections)
|
setOpenChoreSections(newOpenChoreSections)
|
||||||
} else {
|
} else {
|
||||||
|
@ -717,6 +806,9 @@ const MyChores = () => {
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
<NotificationAccessSnackbar />
|
<NotificationAccessSnackbar />
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
<Sidepanel chores={chores} performers={performers} />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
91
src/views/Chores/Sidepanel.jsx
Normal file
91
src/views/Chores/Sidepanel.jsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { Box, Sheet } from '@mui/joy'
|
||||||
|
import { useMediaQuery } from '@mui/material'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { ChoresGrouper } from '../../utils/Chores'
|
||||||
|
import CalendarView from '../components/CalendarView'
|
||||||
|
|
||||||
|
const Sidepanel = ({ chores }) => {
|
||||||
|
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
|
||||||
|
const [dueDatePieChartData, setDueDatePieChartData] = useState([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDueDatePieChartData(generateChoreDuePieChartData(chores))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const generateChoreDuePieChartData = chores => {
|
||||||
|
const groups = ChoresGrouper('due_date', chores)
|
||||||
|
return groups
|
||||||
|
.map(group => {
|
||||||
|
return {
|
||||||
|
label: group.name,
|
||||||
|
value: group.content.length,
|
||||||
|
color: group.color,
|
||||||
|
id: group.name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(item => item.value > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLargeScreen) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
variant='outlined'
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
// borderRadius: 'sm',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
mr: 10,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
boxShadow: 'sm',
|
||||||
|
borderRadius: 20,
|
||||||
|
|
||||||
|
// minimum height to fit the content:
|
||||||
|
height: '80vh',
|
||||||
|
width: '290px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PieChart width={200} height={200}>
|
||||||
|
<Pie
|
||||||
|
data={dueDatePieChartData}
|
||||||
|
dataKey='value'
|
||||||
|
nameKey='label'
|
||||||
|
innerRadius={30}
|
||||||
|
paddingAngle={5}
|
||||||
|
cornerRadius={5}
|
||||||
|
>
|
||||||
|
{dueDatePieChartData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
|
||||||
|
<Legend
|
||||||
|
layout='horizontal'
|
||||||
|
align='center'
|
||||||
|
iconType='circle'
|
||||||
|
iconSize={8}
|
||||||
|
fontSize={12}
|
||||||
|
formatter={(label, value) => `${label}: ${value.payload.value}`}
|
||||||
|
wrapperStyle={{ paddingTop: 0, marginTop: 0 }} // Adjust padding and margin
|
||||||
|
/>
|
||||||
|
<Tooltip />
|
||||||
|
</PieChart>
|
||||||
|
</Box> */}
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
|
<CalendarView chores={chores} />
|
||||||
|
</Box>
|
||||||
|
</Sheet>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sidepanel
|
89
src/views/Modals/RedeemPointsModal.jsx
Normal file
89
src/views/Modals/RedeemPointsModal.jsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
function RedeemPointsModal({ config }) {
|
||||||
|
useEffect(() => {
|
||||||
|
setPoints(0)
|
||||||
|
}, [config])
|
||||||
|
|
||||||
|
const [points, setPoints] = useState(0)
|
||||||
|
|
||||||
|
const predefinedPoints = [1, 5, 10, 25]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={config?.isOpen} onClose={config?.onClose}>
|
||||||
|
<ModalDialog>
|
||||||
|
<Typography level='h4' mb={1}>
|
||||||
|
Redeem Points
|
||||||
|
</Typography>
|
||||||
|
<FormLabel>
|
||||||
|
Points to Redeem ({config.available ? config.available : 0} points
|
||||||
|
available)
|
||||||
|
</FormLabel>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
value={points}
|
||||||
|
slotProps={{
|
||||||
|
input: { min: 0, max: config.available ? config.available : 0 },
|
||||||
|
}}
|
||||||
|
onChange={e => {
|
||||||
|
if (e.target.value > config.available) {
|
||||||
|
setPoints(config.available)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPoints(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormLabel>Or select from predefined points:</FormLabel>
|
||||||
|
<Box display='flex' justifyContent='space-evenly' mb={1}>
|
||||||
|
{predefinedPoints.map(point => (
|
||||||
|
<IconButton
|
||||||
|
variant='outlined'
|
||||||
|
sx={{ borderRadius: '50%' }}
|
||||||
|
key={point}
|
||||||
|
onClick={() => {
|
||||||
|
const newPoints = points + point
|
||||||
|
if (newPoints > config.available) {
|
||||||
|
setPoints(config.available)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPoints(newPoints)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{point}
|
||||||
|
</IconButton>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 3 button save , cancel and delete */}
|
||||||
|
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
config.onSave({
|
||||||
|
points,
|
||||||
|
userId: config.user.userId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
>
|
||||||
|
Redeem
|
||||||
|
</Button>
|
||||||
|
<Button onClick={config.onClose} variant='outlined'>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default RedeemPointsModal
|
|
@ -259,26 +259,31 @@ const UserActivites = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateHistoryPieChartData = history => {
|
const generateHistoryPieChartData = history => {
|
||||||
const totalCompleted = history.filter(
|
const totalCompleted =
|
||||||
item => item.dueDate > item.completedAt,
|
history.filter(item => item.dueDate > item.completedAt).length || 0
|
||||||
).length
|
const totalLate =
|
||||||
const totalLate = history.filter(
|
history.filter(item => item.dueDate < item.completedAt).length || 0
|
||||||
item => item.dueDate < item.completedAt,
|
const totalNoDueDate = history.filter(item => !item.dueDate).length || 0
|
||||||
).length
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: `On time`,
|
label: `On time`,
|
||||||
value: totalCompleted,
|
value: totalCompleted,
|
||||||
color: '#4ec1a2',
|
color: TASK_COLOR.COMPLETED,
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `Late`,
|
label: `Late`,
|
||||||
value: totalLate,
|
value: totalLate,
|
||||||
color: '#f6ad55',
|
color: TASK_COLOR.LATE,
|
||||||
id: 2,
|
id: 2,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: `Completed`,
|
||||||
|
value: totalNoDueDate,
|
||||||
|
color: TASK_COLOR.ANYTIME,
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
if (isChoresHistoryLoading || isChoresLoading) {
|
if (isChoresHistoryLoading || isChoresLoading) {
|
||||||
|
|
|
@ -7,10 +7,11 @@ import {
|
||||||
YAxis,
|
YAxis,
|
||||||
} from 'recharts'
|
} from 'recharts'
|
||||||
|
|
||||||
import { Toll } from '@mui/icons-material'
|
import { CreditCard, Toll } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Chip,
|
Chip,
|
||||||
Container,
|
Container,
|
||||||
|
@ -27,11 +28,17 @@ import LoadingComponent from '../components/Loading.jsx'
|
||||||
|
|
||||||
import { useChoresHistory } from '../../queries/ChoreQueries.jsx'
|
import { useChoresHistory } from '../../queries/ChoreQueries.jsx'
|
||||||
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
||||||
|
import { RedeemPoints } from '../../utils/Fetcher.jsx'
|
||||||
|
import RedeemPointsModal from '../Modals/RedeemPointsModal'
|
||||||
const UserPoints = () => {
|
const UserPoints = () => {
|
||||||
const [tabValue, setTabValue] = useState(7)
|
const [tabValue, setTabValue] = useState(7)
|
||||||
|
const [isRedeemModalOpen, setIsRedeemModalOpen] = useState(false)
|
||||||
|
|
||||||
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
|
const {
|
||||||
useCircleMembers()
|
data: circleMembersData,
|
||||||
|
isLoading: isCircleMembersLoading,
|
||||||
|
handleRefetch: handleCircleMembersRefetch,
|
||||||
|
} = useCircleMembers()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: choresHistoryData,
|
data: choresHistoryData,
|
||||||
|
@ -208,7 +215,7 @@ const UserPoints = () => {
|
||||||
return yearlyAggregated
|
return yearlyAggregated
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChoresHistoryLoading || isCircleMembersLoading) {
|
if (isChoresHistoryLoading || isCircleMembersLoading || !userProfile) {
|
||||||
return <LoadingComponent />
|
return <LoadingComponent />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,6 +240,8 @@ const UserPoints = () => {
|
||||||
sx={{
|
sx={{
|
||||||
gap: 1,
|
gap: 1,
|
||||||
my: 2,
|
my: 2,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'start',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
@ -278,6 +287,19 @@ const UserPoints = () => {
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
{circleUsers.find(user => user.userId === userProfile.id)?.role ===
|
||||||
|
'admin' && (
|
||||||
|
<Button
|
||||||
|
variant='soft'
|
||||||
|
size='md'
|
||||||
|
startDecorator={<CreditCard />}
|
||||||
|
onClick={() => {
|
||||||
|
setIsRedeemModalOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Redeem Points
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
|
@ -458,6 +480,27 @@ const UserPoints = () => {
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
<RedeemPointsModal
|
||||||
|
config={{
|
||||||
|
onClose: () => {
|
||||||
|
setIsRedeemModalOpen(false)
|
||||||
|
},
|
||||||
|
isOpen: isRedeemModalOpen,
|
||||||
|
available: circleUsers.find(user => user.userId === selectedUser)
|
||||||
|
?.points,
|
||||||
|
user: circleUsers.find(user => user.userId === selectedUser),
|
||||||
|
onSave: ({ userId, points }) => {
|
||||||
|
RedeemPoints(userId, points, userProfile.circleID)
|
||||||
|
.then(res => {
|
||||||
|
setIsRedeemModalOpen(false)
|
||||||
|
handleCircleMembersRefetch()
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
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
|
70
src/views/components/Calendar.css
Normal file
70
src/views/components/Calendar.css
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
.react-calendar {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile {
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 45px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile:enabled:hover {
|
||||||
|
background-color: #cbcbcb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile--active {
|
||||||
|
background-color: #007bff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile--now {
|
||||||
|
border: 1px dotted #4ec1a2e3 !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile--now:enabled:hover {
|
||||||
|
background-color: rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 0.3em;
|
||||||
|
height: 0.3em;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chore-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
visibility 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__month-view__days__day--weekend {
|
||||||
|
color: inherit;
|
||||||
|
}
|
176
src/views/components/CalendarView.jsx
Normal file
176
src/views/components/CalendarView.jsx
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
import { Box, Card, CardContent, Chip, Grid, Typography } from '@mui/joy'
|
||||||
|
import moment from 'moment'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import Calendar from 'react-calendar'
|
||||||
|
import 'react-calendar/dist/Calendar.css'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { UserContext } from '../../contexts/UserContext'
|
||||||
|
import { getTextColorFromBackgroundColor, TASK_COLOR } from '../../utils/Colors'
|
||||||
|
import './Calendar.css'
|
||||||
|
|
||||||
|
const getAssigneeColor = (assignee, userProfile) => {
|
||||||
|
return assignee === userProfile.id
|
||||||
|
? TASK_COLOR.ASSIGNED_TO_ME
|
||||||
|
: TASK_COLOR.ASSIGNED_TO_OTHER
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalendarView = ({ chores }) => {
|
||||||
|
const { userProfile } = React.useContext(UserContext)
|
||||||
|
const [selectedDate, setSeletedDate] = useState(null)
|
||||||
|
const Navigate = useNavigate()
|
||||||
|
|
||||||
|
const tileContent = ({ date, view }) => {
|
||||||
|
if (view === 'month') {
|
||||||
|
const dayChores = chores.filter(
|
||||||
|
chore =>
|
||||||
|
new Date(chore.nextDueDate)?.toISOString().split('T')[0] ===
|
||||||
|
date.toISOString().split('T')[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='dot-container'>
|
||||||
|
{dayChores.map((chore, index) => {
|
||||||
|
if (index > 6) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className='dot'
|
||||||
|
style={{
|
||||||
|
backgroundColor: getAssigneeColor(
|
||||||
|
chore.assignedTo,
|
||||||
|
userProfile,
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Calendar
|
||||||
|
tileContent={tileContent}
|
||||||
|
onChange={d => {
|
||||||
|
setSeletedDate(new Date(d))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{!selectedDate && (
|
||||||
|
<Grid container ml={-3}>
|
||||||
|
{[
|
||||||
|
{ name: 'Assigned to me', color: TASK_COLOR.ASSIGNED_TO_ME },
|
||||||
|
{ name: 'Assigned to other', color: TASK_COLOR.ASSIGNED_TO_OTHER },
|
||||||
|
].map((item, index) => (
|
||||||
|
<Grid
|
||||||
|
key={index}
|
||||||
|
item
|
||||||
|
xs={12}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: 5,
|
||||||
|
height: 5,
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: item.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography level='body-xs' ml={0.3}>
|
||||||
|
{item.name}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{selectedDate && (
|
||||||
|
<Box
|
||||||
|
variant='outlined'
|
||||||
|
sx={{
|
||||||
|
// p: 2,
|
||||||
|
// borderRadius: 20,
|
||||||
|
// if exceed the height, scroll:
|
||||||
|
maxHeight: '160px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
// minimum height to fit the content:
|
||||||
|
height: '50vh',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chores
|
||||||
|
.filter(
|
||||||
|
chore =>
|
||||||
|
new Date(chore.nextDueDate)?.toISOString().split('T')[0] ===
|
||||||
|
selectedDate.toISOString().split('T')[0],
|
||||||
|
)
|
||||||
|
.map((chore, idx) => (
|
||||||
|
<Card
|
||||||
|
key={idx}
|
||||||
|
variant='soft'
|
||||||
|
onClick={() => {
|
||||||
|
Navigate('/chores/' + chore.id)
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
mb: 0.4,
|
||||||
|
py: 1,
|
||||||
|
px: 1,
|
||||||
|
|
||||||
|
// backgroundColor: getAssigneeColor(
|
||||||
|
// chore.assignedTo,
|
||||||
|
// userProfile,
|
||||||
|
// ),
|
||||||
|
// everything show in one row:
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography
|
||||||
|
key={chore.id}
|
||||||
|
className='truncate'
|
||||||
|
maxWidth='100%'
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
variant='plain'
|
||||||
|
size='sm'
|
||||||
|
sx={{
|
||||||
|
backgroundColor: getAssigneeColor(
|
||||||
|
chore.assignedTo,
|
||||||
|
userProfile,
|
||||||
|
),
|
||||||
|
mr: 0.5,
|
||||||
|
color: getTextColorFromBackgroundColor(
|
||||||
|
getAssigneeColor(chore.assignedTo, userProfile),
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{moment(chore.nextDueDate).format('hh:mm A')}
|
||||||
|
</Chip>
|
||||||
|
{chore.name}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarView
|
34
src/views/components/IconButtonTouchable.jsx
Normal file
34
src/views/components/IconButtonTouchable.jsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import IconButton from '@mui/joy/IconButton'
|
||||||
|
import React, { useRef, useState } from 'react'
|
||||||
|
|
||||||
|
const IconButtonTouchable = ({ onHold, onClick, ...props }) => {
|
||||||
|
const [holdTimeout, setHoldTimeout] = useState(null)
|
||||||
|
const holdRef = useRef(false)
|
||||||
|
|
||||||
|
const handleMouseDown = () => {
|
||||||
|
holdRef.current = false
|
||||||
|
setHoldTimeout(
|
||||||
|
setTimeout(() => {
|
||||||
|
holdRef.current = true
|
||||||
|
onHold && onHold()
|
||||||
|
}, 1000),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
clearTimeout(holdTimeout)
|
||||||
|
if (!holdRef.current) {
|
||||||
|
onClick && onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
{...props}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IconButtonTouchable
|
Loading…
Add table
Reference in a new issue