Skip to content

Commit

Permalink
feat(due): Add due date for tasks (#24)
Browse files Browse the repository at this point in the history
- Implement with `@mui/x-date-pickers/DateTimePicker`.
  - Can select a date with time. able to select time back before today.
  - Will turn red background if past and incomplete.

- Add new field `due` for `task`, default null.

---------

Co-authored-by: ZL Asica <[email protected]>
  • Loading branch information
chipanyanwu and ZL-Asica authored Nov 7, 2024
1 parent fbe2ca4 commit 5b90ff4
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 29 deletions.
106 changes: 106 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"@fontsource/roboto": "^5.1.0",
"@mui/icons-material": "^6.1.6",
"@mui/material": "^6.1.6",
"@mui/x-date-pickers": "^7.22.1",
"dayjs": "^1.11.13",
"firebase": "^11.0.1",
"react": "^18.3.1",
"react-calendar": "^5.1.0",
Expand Down
40 changes: 27 additions & 13 deletions src/components/Home/AddItem.jsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,47 @@
// @ts-check

import CategoryPicker from '@/components/Home/CategoryPicker'
import { DateTimePicker } from '@/components/Home/DateTimePickers'
import AddIcon from '@mui/icons-material/Add'
import { IconButton, InputAdornment, TextField } from '@mui/material'
import { useState } from 'react'

const AddItem = ({ label, onAdd }) => {
const [inputValue, setInputValue] = useState('')
const [selectedCategory, setSelectedCategory] = useState('#000000')
const [selectedCategory, setSelectedCategory] = useState('#000000') // For 'New Goal'
const [dueDate, setDueDate] = useState(null) // For 'New Task'

const handleAdd = async () => {
if (inputValue.trim()) {
await onAdd(
inputValue.trim(),
label === 'New Goal' ? selectedCategory : null
)
let attributes = null

if (label === 'New Goal') {
attributes = selectedCategory
} else if (label === 'New Task') {
attributes = dueDate
}

await onAdd(inputValue.trim(), attributes ?? null)
setInputValue('')
setSelectedCategory('#000000')
setDueDate(null)
}
}

// Conditionally render the category picker
const startAdornment =
label === 'New Goal' ? (
<CategoryPicker
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
/>
) : null
const getStartAdornment = () => {
if (label === 'New Goal') {
return (
<CategoryPicker
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
/>
)
} else if (label === 'New Task') {
return <DateTimePicker value={dueDate} onChange={setDueDate} />
}
return null
}

return (
<TextField
Expand All @@ -45,7 +59,7 @@ const AddItem = ({ label, onAdd }) => {
fullWidth
slotProps={{
input: {
startAdornment,
startAdornment: getStartAdornment(),
endAdornment: (
<InputAdornment position='end'>
<IconButton
Expand Down
65 changes: 65 additions & 0 deletions src/components/Home/DateTimePickers.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// @ts-check

import CalendarTodayIcon from '@mui/icons-material/CalendarToday'
import { Box, IconButton } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { LocalizationProvider } from '@mui/x-date-pickers'
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import { DateTimePicker as MUIDateTimePicker } from '@mui/x-date-pickers/DateTimePicker'
import { useRef, useState } from 'react'

export function DateTimePicker({
value,
onChange,
label = 'Select date and time',
}) {
const [open, setOpen] = useState(false)
const anchorRef = useRef(null)
const theme = useTheme()

const toggleOpen = () => setOpen((prev) => !prev) // Toggle the open state

return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
backgroundColor: value ? theme.palette.primary.light : '#eeeeee',
borderRadius: 2,
marginRight: 1,
}}
>
<IconButton ref={anchorRef} onClick={toggleOpen} aria-label={label}>
<CalendarTodayIcon
className='h-5 w-5'
style={{
color: value ? theme.palette.primary.dark : undefined,
}}
/>
</IconButton>
<MUIDateTimePicker
open={open}
onClose={toggleOpen}
onOpen={toggleOpen}
value={value}
onChange={onChange}
slotProps={{
actionBar: {
actions: ['clear', 'accept'],
},
textField: {
sx: { display: 'none' },
},
popper: {
anchorEl: anchorRef.current,
placement: 'bottom-start',
sx: { zIndex: 1300 },
},
}}
/>
</Box>
</LocalizationProvider>
)
}
4 changes: 2 additions & 2 deletions src/components/Home/MicroGoal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ const MicroGoal = ({ microGoal, macroGoalIndex, microGoalIndex }) => {
</List>
<AddItem
label='New Task'
onAdd={(taskName) =>
addTask(macroGoalIndex, microGoalIndex, taskName)
onAdd={(taskName, dueDate) =>
addTask(macroGoalIndex, microGoalIndex, taskName, dueDate)
}
/>
</Collapse>
Expand Down
71 changes: 57 additions & 14 deletions src/components/Home/Task.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
// @ts-check

import DeleteItem from '@/components/Home/DeleteItem'
import { Checkbox, ListItem, ListItemText } from '@mui/material'
import AccessTimeIcon from '@mui/icons-material/AccessTime'
import { Box, Checkbox, Chip, ListItem, ListItemText } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import dayjs from 'dayjs'
import calendar from 'dayjs/plugin/calendar'

dayjs.extend(calendar)

const Task = ({
task,
Expand All @@ -8,33 +16,49 @@ const Task = ({
microGoalIndex,
taskIndex,
}) => {
const renderDueDateChip = () => {
const theme = useTheme()
const isOverdue =
dayjs().isAfter(dayjs(task.due.toMillis())) && !task.completed

return (
<Chip
icon={<AccessTimeIcon color='inherit' />}
size='small'
label={`${isOverdue ? 'Past' : 'Due'}: ${dayjs(task.due.toMillis()).calendar()}`}
sx={{
...(isOverdue && {
bgcolor: theme.palette.error.main,
color: theme.palette.error.contrastText,
}),
}}
/>
)
}

return (
<ListItem
dense
sx={{
display: 'flex',
alignItems: 'center',
padding: 1,
bgcolor: task.completed ? 'action.hover' : 'background.paper',
borderRadius: 1,
mb: 1,
'&:hover': { bgcolor: 'action.hover' },
...styles.listItem,
bgcolor: task.completed ? 'action.hover' : 'inherit',
}}
>
<Checkbox
edge='start'
checked={task.completed}
onChange={onToggle}
size='medium'
sx={{ '& .MuiSvgIcon-root': { fontSize: 28 } }} // Enlarge the checkbox icon
/>
<ListItemText
primary={task.name}
primary={
<Box sx={styles.textContainer}>
{task.name}
{task.due ? renderDueDateChip() : null}
</Box>
}
primaryTypographyProps={{
sx: {
textDecoration: task.completed ? 'line-through' : 'none',
color: task.completed ? 'text.disabled' : 'text.primary',
},
sx: task.completed ? styles.primaryTextCompleted : {},
}}
/>
<DeleteItem
Expand All @@ -46,4 +70,23 @@ const Task = ({
)
}

const styles = {
listItem: {
padding: 1,
mb: 1,
'&:hover': { bgcolor: 'action.hover' },
},
primaryTextCompleted: {
textDecoration: 'line-through',
color: 'text.disabled',
},
textContainer: {
marginLeft: 1,
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
alignItems: { xs: 'baseline', sm: 'center' },
gap: '0.5rem',
},
}

export default Task

0 comments on commit 5b90ff4

Please sign in to comment.