Merge branch 'main' into feature/internationalization-support

This commit is contained in:
Mohamad Tarbin
2026-04-05 20:51:29 -04:00
committed by GitHub
24 changed files with 584 additions and 374 deletions

View File

@@ -20,12 +20,9 @@ As an avid for open-source, I was eager to create a solution that could benefit
- Recurring Tasks: Schedule tasks to repeat daily, weekly, monthly, or yearly, with flexible customization options.
- Progress Tracking: Track the completion status of tasks and view historical data.
## Installation
## Development Environment
1. Clone the repository:
2. Navigate to the project directory: `cd frontend`
3. Download dependency `npm install`
4. Run locally `npm start`
Follow the full instructions here: https://github.com/donetick/donetick?tab=readme-ov-file#development-environment
## Contributing
@@ -41,7 +38,7 @@ Contributions are welcome! If you would like to contribute to Donetick, please f
Donetick is a work in progress and has been a fantastic learning experience for me as I've honed my React skills,I'm looking for collaborators to help improve and refine the Donetick. Feel free to open PR or suggest changes.
## Plans :
## Plans:
My goal is to expand Donetick by offering a hosted infrastructure option. This will make it even easier for users to access and utilize Donetick's features without the need for self-hosting.

View File

@@ -12,7 +12,7 @@ import Input from '@mui/joy/Input'
import Option from '@mui/joy/Option'
import Select from '@mui/joy/Select'
import Typography from '@mui/joy/Typography'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useRef } from 'react'
import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors'
import { TIME_UNITS } from '../utils/DurationUtils'
@@ -73,6 +73,9 @@ const NotificationTemplate = ({
[],
)
const notificationsRef = useRef(notifications)
const [draftValues, setDraftValues] = useState({})
const [error, setError] = useState(null)
const [showSaveDefault, setShowSaveDefault] = useState(false)
// Create a map of notification indices for timeline display
@@ -114,7 +117,6 @@ const NotificationTemplate = ({
// Sort notifications and update the index mapping
useEffect(() => {
updateNotificationIndices()
setError(null)
}, [updateNotificationIndices])
// Notify parent component of changes including the template name
@@ -125,8 +127,8 @@ const NotificationTemplate = ({
}, [notifications, onChange])
// Validates if a notification configuration already exists
const isDuplicate = (notification, currentIdx = -1) => {
return notifications.some((n, idx) => {
const isDuplicate = (notification, currentIdx = -1, list = notifications) => {
return list.some((n, idx) => {
if (idx === currentIdx) return false
return (
@@ -136,6 +138,26 @@ const NotificationTemplate = ({
})
}
const getSmartSuggestion = type => {
let suggestions = []
if (type === 'reminder' || type === 'before') {
suggestions = [
{ value: -1, unit: 'd' },
{ value: -3, unit: 'h' },
{ value: -30, unit: 'm' },
]
} else if (type === 'followup' || type === 'after') {
suggestions = [
{ value: 1, unit: 'd' },
{ value: 3, unit: 'd' },
{ value: 7, unit: 'd' },
]
}
return suggestions.find(
suggestion => !isDuplicate(suggestion, -1, notificationsRef.current),
)
}
const handleChange = (idx, field, value) => {
const currentNotification = notifications[idx]
const uiRep = getUIRepresentation(currentNotification)
@@ -149,6 +171,15 @@ const NotificationTemplate = ({
// Reset display value when switching to "On Due"
if (value === 'ondue') {
updatedUIRep.displayValue = 0
} else if (Number(currentNotification.value) === 0) {
const suggestion = getSmartSuggestion(value)
if (suggestion) {
updatedUIRep.displayValue = Math.abs(suggestion.value)
updatedUIRep.unit = suggestion.unit
} else {
updatedUIRep.displayValue = 1
updatedUIRep.unit = 'h'
}
}
} else if (field === 'displayValue') {
updatedUIRep.displayValue = Math.max(0, Number(value))
@@ -168,71 +199,41 @@ const NotificationTemplate = ({
unit: updatedUIRep.unit,
}
// Check if another notification is already "On Due" (value = 0)
if (newInternalValue === 0) {
const existingOnDue = notifications.findIndex(
(n, i) => i !== idx && Number(n.value) === 0,
)
const updated = notifications.map((n, i) =>
i === idx ? updatedNotification : n,
)
setNotifications(updated)
notificationsRef.current = updated
setError(null)
}
if (existingOnDue !== -1) {
setError(
'Only one notification can be set to "On Due". Please choose a different timing.',
)
return
}
}
const handleBlur = idx => {
const currentList = notificationsRef.current
const currentNotification = currentList[idx]
if (isDuplicate(updatedNotification, idx)) {
if (!currentNotification) return
if (isDuplicate(currentNotification, idx, currentList)) {
setError(
'This notification setting already exists. Please use a different timing.',
)
return
}
const updated = notifications.map((n, i) =>
i === idx ? updatedNotification : n,
)
setNotifications(updated)
setError(null)
}
const addSmartNotification = type => {
if (notifications.length >= maxNotifications) return
setShowSaveDefault(true)
let newNotification
let suggestions = []
switch (type) {
case 'reminder':
// Suggest common reminder times that don't exist
suggestions = [
{ value: -1, unit: 'd' }, // 1 day before
{ value: -3, unit: 'h' }, // 3 hours before
{ value: -30, unit: 'm' }, // 3 days before
]
break
case 'due':
if (notifications.some(n => Number(n.value) === 0)) {
setError('Only one "Due Alert" notification is allowed.')
return
}
newNotification = { value: 0, unit: 'm' }
break
case 'followup':
suggestions = [
{ value: 1, unit: 'd' }, // 1 day after
{ value: 3, unit: 'd' }, // 3 days after
{ value: 7, unit: 'd' }, // 1 week after
]
break
}
// For reminder/followup, find first non-duplicate suggestion
if (suggestions.length > 0) {
newNotification = suggestions.find(suggestion => !isDuplicate(suggestion))
if (type === 'due') {
if (notificationsRef.current.some(n => Number(n.value) === 0)) {
setError('Only one "Due Alert" notification is allowed.')
return
}
newNotification = { value: 0, unit: 'm' }
} else {
newNotification = getSmartSuggestion(type)
if (!newNotification) {
setError(`All common ${type} times are already configured.`)
return
@@ -243,15 +244,25 @@ const NotificationTemplate = ({
const updatedNotifications = [...notifications, newNotification]
setNotifications(updatedNotifications)
notificationsRef.current = updatedNotifications
setError(null)
}
const removeNotification = idx => {
const updated = notifications.filter((_, i) => i !== idx)
setNotifications(updated)
notificationsRef.current = updated
setDraftValues(prev => {
const next = { ...prev }
delete next[idx]
return next
})
onChange && onChange(updated)
setShowSaveDefault(true)
}
const renderTimeline = () => {
// Convert notifications to minutes for proper chronological sorting
const convertToMinutes = (value, unit) => {
@@ -459,6 +470,11 @@ const NotificationTemplate = ({
const badgeNumber = notificationIndexMap[idx]
const uiRep = getUIRepresentation(n)
// Check if an "On Due" notification exists anywhere else in the list
const hasOnDueElsewhere = notificationsRef.current.some(
(notif, i) => i !== idx && Number(notif.value) === 0,
)
const getNotificationColors = value => {
if (Number(value) < 0) {
return {
@@ -487,7 +503,7 @@ const NotificationTemplate = ({
const colors = getNotificationColors(n.value)
return (
<>
<Box key={idx} sx={{ position: 'relative' }}>
<Badge
badgeContent={badgeNumber}
size={'sm'}
@@ -495,7 +511,6 @@ const NotificationTemplate = ({
'--Badge-minHeight': '16px',
'--Badge-fontSize': '0.7rem',
'--Badge-paddingX': '5px',
top: 10,
'& .MuiBadge-badge': {
background: colors.bgColor,
@@ -504,7 +519,6 @@ const NotificationTemplate = ({
}}
/>
<Box
key={idx}
sx={{
mb: 1.5,
p: 2,
@@ -548,11 +562,16 @@ const NotificationTemplate = ({
<Select
value={uiRep.timing}
onChange={(_, value) => handleChange(idx, 'timing', value)}
onBlur={() => handleBlur(idx)}
sx={{ minWidth: 80 }}
size={'sm'}
>
{timingOptions.map(opt => (
<Option key={opt.value} value={opt.value}>
<Option
key={opt.value}
value={opt.value}
disabled={opt.value === 'ondue' && hasOnDueElsewhere}
>
{opt.label}
</Option>
))}
@@ -560,11 +579,42 @@ const NotificationTemplate = ({
<Input
type={'number'}
min={0}
value={uiRep.displayValue}
disabled={uiRep.timing === 'ondue'}
onChange={e =>
handleChange(idx, 'displayValue', e.target.value)
value={
draftValues[idx] !== undefined
? draftValues[idx]
: uiRep.displayValue
}
disabled={uiRep.timing === 'ondue'}
onChange={e => {
const val = e.target.value
if (val.includes('-')) return
setDraftValues(prev => ({ ...prev, [idx]: val }))
}}
onKeyDown={e => {
if (['-', 'e', '+', '.'].includes(e.key)) {
e.preventDefault()
}
}}
onBlur={e => {
let val = e.target.value
const numericVal = Number(val)
val =
numericVal <= 0
? hasOnDueElsewhere
? 1
: 0
: numericVal
handleChange(idx, 'displayValue', val)
setDraftValues(prev => {
const next = { ...prev }
delete next[idx]
return next
})
handleBlur(idx)
}}
sx={{
width: 60,
opacity: uiRep.timing === 'ondue' ? 0.6 : 1,
@@ -576,6 +626,7 @@ const NotificationTemplate = ({
value={n.unit}
disabled={uiRep.timing === 'ondue'}
onChange={(_, value) => handleChange(idx, 'unit', value)}
onBlur={() => handleBlur(idx)}
sx={{
minWidth: 70,
opacity: uiRep.timing === 'ondue' ? 0.6 : 1,
@@ -605,7 +656,7 @@ const NotificationTemplate = ({
</IconButton>
</Box>
</Box>
</>
</Box>
)
})}
<Box

View File

@@ -32,6 +32,15 @@ html {
/* Prevent iOS Safari from auto-zooming on input focus (triggered when font-size < 16px) */
@supports (-webkit-touch-callout: none) {
input,
textarea,
select {
font-size: max(16px, 1em);
}
}
/* Ensure smooth transitions for dynamic content */
* {
box-sizing: border-box;

View File

@@ -34,8 +34,9 @@ export const getDueDateChipText = (nextDueDate, chore, timeFormat = 'h:mm A') =>
sameElse: `MMM D ${timeFormat}`,
}
// if seconds and minutes set to 59, treat as no time (date only)
if (dueDate.seconds() === 59 && dueDate.minutes() === 59) {
// if time is 23:59:59, treat as end-of-day (date only, no specific time)
if (dueDate.hours() === 23 && dueDate.minutes() === 59 && dueDate.seconds() === 59) {
if (diff < 0) {
// For overdue dates, show calendar format for recent dates
const absDiff = Math.abs(diff)

View File

@@ -16,6 +16,8 @@ export const ChoreHistoryStatus = Object.freeze({
SKIPPED: 2,
PENDING_APPROVAL: 3,
REJECTED: 4,
MISSED: 5,
RESCHEDULED: 6,
})
export const ChoreStatus = Object.freeze({
INACTIVE: 0,
@@ -324,7 +326,7 @@ export const notInCompletionWindow = chore => {
chore.completionWindow &&
chore.completionWindow > -1 &&
chore.nextDueDate &&
moment() < moment(chore.nextDueDate).add(-chore.completionWindow, 'seconds')
moment() < moment(chore.nextDueDate).add(-chore.completionWindow, 'hours')
)
}
export const ChoreFilters = userId => ({
@@ -332,6 +334,9 @@ export const ChoreFilters = userId => ({
assigned_to_me: chore => {
return chore.assignedTo && chore.assignedTo === userId
},
available_for_me: chore => {
return chore.assignedTo === null || chore.assignedTo === userId
},
assigned_to_others: chore => {
return chore.assignedTo && chore.assignedTo !== userId
},

View File

@@ -771,17 +771,19 @@ const LoginView = () => {
</Button>
)}
<Button
onClick={() => {
Navigate('/signup')
}}
fullWidth
variant='soft'
size='lg'
// sx={{ mt: 3, mb: 2 }}
>
Create new account
</Button>
{!resource?.is_user_creation_disabled && (
<Button
onClick={() => {
Navigate('/signup')
}}
fullWidth
variant='soft'
size='lg'
// sx={{ mt: 3, mb: 2 }}
>
Create new account
</Button>
)}
<Box
sx={{ display: 'flex', justifyContent: 'center', gap: 2, mt: 2 }}

View File

@@ -1,18 +1,23 @@
import { Add, HorizontalRule, Save } from '@mui/icons-material'
import { Add, ArrowDropDown, HorizontalRule, Save } from '@mui/icons-material'
import {
Avatar,
Box,
Button,
ButtonGroup,
Card,
Checkbox,
Chip,
Container,
Divider,
Dropdown,
FormControl,
FormHelperText,
IconButton,
Input,
List,
ListItem,
Menu,
MenuButton,
MenuItem,
Option,
Radio,
@@ -291,7 +296,7 @@ const ChoreEdit = () => {
if (dueDateOnly) {
const combinedDateTime = moment(`${dueDateOnly}T${defaultTime}`).format(
'YYYY-MM-DDTHH:mm:00',
'YYYY-MM-DDTHH:mm:59',
)
setDueDate(combinedDateTime)
@@ -309,7 +314,7 @@ const ChoreEdit = () => {
if (dueDateOnly) {
const endOfDay = moment(dueDateOnly)
.endOf('day')
.format('YYYY-MM-DDTHH:mm:00')
.format('YYYY-MM-DDTHH:mm:ss')
setDueDate(endOfDay)
}
}
@@ -558,7 +563,7 @@ const ChoreEdit = () => {
const today = moment(new Date()).format('YYYY-MM-DD')
setDueDateOnly(today)
// Default to end of day
setDueDate(moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:00'))
setDueDate(moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:59'))
setUseCustomTime(false)
setDueTime(null)
}
@@ -1107,7 +1112,7 @@ const ChoreEdit = () => {
const today = moment(new Date()).format('YYYY-MM-DD')
setDueDateOnly(today)
setDueDate(
moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:00'),
moment(today).endOf('day').format('YYYY-MM-DDTHH:mm:59'),
)
setUseCustomTime(false)
setDueTime(null)
@@ -1189,7 +1194,7 @@ const ChoreEdit = () => {
checked={completionWindow !== -1}
onChange={e => {
if (e.target.checked) {
setCompletionWindow(3600) // default 1 hour in seconds
setCompletionWindow(1) // default 1 hour in seconds
} else {
setCompletionWindow(-1)
}
@@ -1203,29 +1208,38 @@ const ChoreEdit = () => {
</FormControl>
{completionWindow !== -1 && (
<Box
sx={{
mt: 1,
ml: 4,
display: 'flex',
gap: 1,
alignItems: 'center',
}}
>
<DurationInput
value={completionWindow}
onChange={setCompletionWindow}
size='sm'
minValue={0}
/>
<Typography level='body-sm'>before due date</Typography>
</Box>
<Card variant='outlined'>
<Box
sx={{
mt: 0,
ml: 4,
}}
>
<Typography level='body-sm'>Hours:</Typography>
<Input
type='number'
value={completionWindow}
sx={{ maxWidth: 100 }}
slotProps={{
input: {
min: 0,
max: 24 * 7,
},
}}
placeholder='Hours'
onChange={e => {
setCompletionWindow(parseInt(e.target.value))
}}
/>
</Box>
</Card>
)}
{/* Expires After (Deadline) */}
<FormControl sx={{ mt: 2 }}>
{/* <FormControl sx={{ mt: 2 }}>
<Checkbox
checked={deadlineOffset !== -1}
disabled={isRolling}
onChange={e => {
if (e.target.checked) {
setDeadlineOffset(86400) // default 1 day in seconds
@@ -1237,9 +1251,11 @@ const ChoreEdit = () => {
label='Set a deadline'
/>
<FormHelperText>
Task will be considered expired after the due date
{isRolling && !['once', 'no_repeat'].includes(frequencyType)
? 'Deadline is not available when scheduling from completion date'
: 'Task will be considered expired after the due date'}
</FormHelperText>
</FormControl>
</FormControl> */}
{deadlineOffset !== -1 && (
<Box
@@ -1286,7 +1302,10 @@ const ChoreEdit = () => {
<Radio
overlay
checked={isRolling}
onClick={() => setIsRolling(true)}
onClick={() => {
setIsRolling(true)
setDeadlineOffset(-1)
}}
label='Reschedule from completion date'
/>
<FormHelperText>
@@ -1624,39 +1643,38 @@ const ChoreEdit = () => {
}}
>
{choreId > 0 && (
<>
{isActive ? (
<Button
color='danger'
variant='outlined'
onClick={() => {
archiveChore.mutate(choreId)
}}
>
Archive
</Button>
) : (
<Button
color='neutral'
variant='outlined'
onClick={() => {
unarchiveChore.mutate(choreId)
}}
>
Unarchive
</Button>
)}
<Button
color='danger'
variant='solid'
onClick={() => {
// confirm before deleting:
handleDelete()
}}
<Dropdown>
<ButtonGroup
variant='outlined'
color={isActive ? 'danger' : 'neutral'}
>
Delete
</Button>
</>
<Button
onClick={() => {
isActive
? archiveChore.mutate(choreId)
: unarchiveChore.mutate(choreId)
}}
>
{isActive ? 'Archive' : 'Unarchive'}
</Button>
<MenuButton
slots={{ root: IconButton }}
slotProps={{
root: {
variant: 'outlined',
color: isActive ? 'danger' : 'neutral',
},
}}
>
<ArrowDropDown />
</MenuButton>
</ButtonGroup>
<Menu placement='top-end'>
<MenuItem color='danger' onClick={handleDelete}>
Delete
</MenuItem>
</Menu>
</Dropdown>
)}
<Button
color='neutral'

View File

@@ -673,7 +673,6 @@ const ChoreView = () => {
color='neutral'
variant='plain'
fullWidth
disabled={chore.isActive === false}
onClick={() => {
navigate(`/chores/${choreId}/history`)
}}
@@ -692,7 +691,6 @@ const ChoreView = () => {
color='neutral'
variant='plain'
fullWidth
disabled={chore.isActive === false}
sx={{
// top right of the card:
flexDirection: 'column',
@@ -1018,9 +1016,7 @@ const ChoreView = () => {
size='lg'
onClick={handleTaskCompletion}
disabled={
notInCompletionWindow(chore) ||
(chore.lastCompletedDate !== null &&
chore.frequencyType === 'once')
notInCompletionWindow(chore) || chore.isActive === false
}
color='success'
startDecorator={<Check />}
@@ -1050,9 +1046,7 @@ const ChoreView = () => {
})
}}
disabled={
notInCompletionWindow(chore) ||
(chore.lastCompletedDate !== null &&
chore.frequencyType === 'once')
notInCompletionWindow(chore) || chore.isActive === false
}
startDecorator={<SwitchAccessShortcut />}
sx={{
@@ -1071,7 +1065,7 @@ const ChoreView = () => {
>
Available to complete starting{' '}
{moment(chore.nextDueDate)
.subtract(chore.completionWindow, 'seconds')
.subtract(chore.completionWindow, 'hours')
.format('MM/DD/YYYY hh:mm A')}
</Typography>
)}
@@ -1081,8 +1075,7 @@ const ChoreView = () => {
disabled={
(chore.status === ChoreStatus.PAUSED &&
notInCompletionWindow(chore)) ||
(chore.lastCompletedDate !== null &&
chore.frequencyType === 'once')
chore.isActive === false
}
chore={chore}
onAction={action => {
@@ -1108,9 +1101,7 @@ const ChoreView = () => {
variant='soft'
color='success'
disabled={
notInCompletionWindow(chore) ||
(chore.lastCompletedDate !== null &&
chore.frequencyType === 'once')
notInCompletionWindow(chore) || chore.isActive === false
}
startDecorator={<PlayArrow />}
sx={{

View File

@@ -76,9 +76,9 @@ const ActivityItem = ({ activity, members, onViewNote }) => {
icon: <Timelapse />,
}
} else if (activity.status === 1) {
const wasOnTime = moment(activity.performedAt).isSameOrBefore(
moment(activity.dueDate),
)
const wasOnTime =
!activity.dueDate ||
moment(activity.performedAt).isSameOrBefore(moment(activity.dueDate))
if (wasOnTime) {
return {

View File

@@ -80,7 +80,7 @@ const ChoreCard = ({
const getName = name => {
const split = Array.from(chore.name)
// if the first character is emoji then remove it from the name
if (/\p{Emoji}/u.test(split[0])) {
if (isNaN(Number(split[0])) && /\p{Emoji}/u.test(split[0])) {
return split.slice(1).join('').trim()
}
return name

View File

@@ -221,15 +221,12 @@ const MyChores = () => {
let choresToGroup = chores
if (tempFilter || activeFilterId) {
choresToGroup = customFilteredChores
} else if (selectedProject) {
// Otherwise, use project-filtered chores for section grouping
if (selectedProject.id === 'default') {
// Default project: only show tasks without a projectId
choresToGroup = chores.filter(chore => !chore.projectId)
} else {
// Other projects: use the existing filter function
choresToGroup = filterByProject(chores, selectedProject.id)
}
} else if (!selectedProject || selectedProject.id === 'default') {
// No project selected or default project: only show tasks without a projectId
choresToGroup = chores.filter(chore => !chore.projectId)
} else {
// Specific project: use the existing filter function
choresToGroup = filterByProject(chores, selectedProject.id)
}
const sections = ChoresGrouper(
@@ -529,10 +526,11 @@ const MyChores = () => {
onEnableMultiSelectAndSelectAll: () => {
toggleMultiSelectMode()
setTimeout(() => {
selectAllVisibleChores()
selectAllVisibleChores(null, choreSections, openChoreSections)
}, 0)
},
onSelectAll: () => selectAllVisibleChores(),
onSelectAll: () =>
selectAllVisibleChores(null, choreSections, openChoreSections),
onClearSelection: clearSelection,
onBulkComplete: handleBulkComplete,
onBulkSkip: handleBulkSkip,
@@ -1276,6 +1274,9 @@ const MyChores = () => {
setSearchFilter('All')
setSearchTerm('')
clearActiveFilter()
// reset project and filters :
setSelectedProjectWithCache(null)
updateFilterUrl(null, null)
}}
variant='outlined'
color='neutral'

View File

@@ -317,7 +317,7 @@ const SmartInsightsCard = ({
return (
<Button
key={insight.id}
variant={isActive ? 'solid' : 'soft'}
variant={isActive ? 'outlined' : 'soft'}
color='neutral'
onClick={() => handleInsightClick(insight)}
sx={{

View File

@@ -85,7 +85,12 @@ const SortAndGrouping = ({
{ name: 'Labels', value: 'labels' },
]
const filterItems = ['anyone', 'assigned_to_me', 'assigned_to_others']
const filterItems = [
'anyone',
'assigned_to_me',
'available_for_me',
'assigned_to_others',
]
// Total selectable items: 4 (group by) + 3 (filters) + 1 (create custom filter) = 8
const totalItems = groupByItems.length + filterItems.length + 1
@@ -169,6 +174,65 @@ const SortAndGrouping = ({
}
}, [])
const MenuItem_QuickFilter = props => {
return (
<MenuItem
key={props.key}
onClick={() => {
setFilter(props.filterKey)
handleMenuClose()
}}
onMouseEnter={() => setIsKeyboardNavigating(false)}
sx={{
borderRadius: 'var(--joy-radius-sm)',
backgroundColor:
selectedFilter === props.filterKey
? 'var(--joy-palette-primary-softBg)'
: selectedIndex === props.index &&
anchorEl &&
isKeyboardNavigating
? 'var(--joy-palette-neutral-softHoverBg)'
: 'transparent',
'&:hover': {
backgroundColor:
selectedFilter === props.filterKey
? 'var(--joy-palette-primary-softBg)'
: 'var(--joy-palette-neutral-softHoverBg)',
},
}}
>
<ListItemDecorator>
<Radio
checked={selectedFilter === props.filterKey}
variant='outlined'
/>
</ListItemDecorator>
<ListItemContent>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography
level='body-sm'
sx={{
fontWeight: selectedFilter === props.filterKey ? 600 : 400,
color:
selectedFilter === props.filterKey
? 'var(--joy-palette-primary-600)'
: 'var(--joy-palette-text-primary)',
}}
>
{props.label}
</Typography>
</Box>
</ListItemContent>
</MenuItem>
)
}
return (
<>
{!label && (
@@ -359,162 +423,33 @@ const SortAndGrouping = ({
</ListItemContent>
</MenuItem>
<MenuItem
<MenuItem_QuickFilter
key={`${k}-assignee-anyone`}
onClick={() => {
setFilter('anyone')
handleMenuClose()
}}
onMouseEnter={() => setIsKeyboardNavigating(false)}
sx={{
borderRadius: 'var(--joy-radius-sm)',
backgroundColor:
selectedFilter === 'anyone'
? 'var(--joy-palette-primary-softBg)'
: selectedIndex === 4 && anchorEl && isKeyboardNavigating
? 'var(--joy-palette-neutral-softHoverBg)'
: 'transparent',
'&:hover': {
backgroundColor:
selectedFilter === 'anyone'
? 'var(--joy-palette-primary-softBg)'
: 'var(--joy-palette-neutral-softHoverBg)',
},
}}
>
<ListItemDecorator>
<Radio checked={selectedFilter === 'anyone'} variant='outlined' />
</ListItemDecorator>
<ListItemContent>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography
level='body-sm'
sx={{
fontWeight: selectedFilter === 'anyone' ? 600 : 400,
color:
selectedFilter === 'anyone'
? 'var(--joy-palette-primary-600)'
: 'var(--joy-palette-text-primary)',
}}
>
Anyone
</Typography>
</Box>
</ListItemContent>
</MenuItem>
index={4}
filterKey='anyone'
label='Anyone'
/>
<MenuItem
<MenuItem_QuickFilter
key={`${k}-assignee-assigned-to-me`}
onClick={() => {
setFilter('assigned_to_me')
handleMenuClose()
}}
onMouseEnter={() => setIsKeyboardNavigating(false)}
sx={{
borderRadius: 'var(--joy-radius-sm)',
backgroundColor:
selectedFilter === 'assigned_to_me'
? 'var(--joy-palette-primary-softBg)'
: selectedIndex === 5 && anchorEl && isKeyboardNavigating
? 'var(--joy-palette-neutral-softHoverBg)'
: 'transparent',
'&:hover': {
backgroundColor:
selectedFilter === 'assigned_to_me'
? 'var(--joy-palette-primary-softBg)'
: 'var(--joy-palette-neutral-softHoverBg)',
},
}}
>
<ListItemDecorator>
<Radio
checked={selectedFilter === 'assigned_to_me'}
variant='outlined'
/>
</ListItemDecorator>
<ListItemContent>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography
level='body-sm'
sx={{
fontWeight: selectedFilter === 'assigned_to_me' ? 600 : 400,
color:
selectedFilter === 'assigned_to_me'
? 'var(--joy-palette-primary-600)'
: 'var(--joy-palette-text-primary)',
}}
>
Assigned to me
</Typography>
</Box>
</ListItemContent>
</MenuItem>
index={5}
filterKey='assigned_to_me'
label='Assigned to me'
/>
<MenuItem
<MenuItem_QuickFilter
key={`${k}-assignee-available-for-me`}
index={6}
filterKey='available_for_me'
label='Available for me'
/>
<MenuItem_QuickFilter
key={`${k}-assignee-assigned-to-others`}
onClick={() => {
setFilter('assigned_to_others')
handleMenuClose()
}}
onMouseEnter={() => setIsKeyboardNavigating(false)}
sx={{
borderRadius: 'var(--joy-radius-sm)',
backgroundColor:
selectedFilter === 'assigned_to_others'
? 'var(--joy-palette-primary-softBg)'
: selectedIndex === 6 && anchorEl && isKeyboardNavigating
? 'var(--joy-palette-neutral-softHoverBg)'
: 'transparent',
'&:hover': {
backgroundColor:
selectedFilter === 'assigned_to_others'
? 'var(--joy-palette-primary-softBg)'
: 'var(--joy-palette-neutral-softHoverBg)',
},
}}
>
<ListItemDecorator>
<Radio
checked={selectedFilter === 'assigned_to_others'}
variant='outlined'
/>
</ListItemDecorator>
<ListItemContent>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography
level='body-sm'
sx={{
fontWeight:
selectedFilter === 'assigned_to_others' ? 600 : 400,
color:
selectedFilter === 'assigned_to_others'
? 'var(--joy-palette-primary-600)'
: 'var(--joy-palette-text-primary)',
}}
>
Assigned to others
</Typography>
</Box>
</ListItemContent>
</MenuItem>
index={7}
filterKey='assigned_to_others'
label='Assigned to others'
/>
<Divider sx={{ my: 1 }} />
@@ -528,7 +463,7 @@ const SortAndGrouping = ({
sx={{
borderRadius: 'var(--joy-radius-sm)',
backgroundColor:
selectedIndex === 7 && anchorEl && isKeyboardNavigating
selectedIndex === 8 && anchorEl && isKeyboardNavigating
? 'var(--joy-palette-success-softHoverBg)'
: 'transparent',
'&:hover': {

View File

@@ -154,6 +154,18 @@ export const useChoreActions = ({
async (action, chore, extraData = {}) => {
switch (action) {
case 'complete':
// 1. Instantly hide the chore from the UI and Cache
setChores(prev => prev.filter(c => c.id !== chore.id))
setFilteredChores(prev => prev.filter(c => c.id !== chore.id))
queryClient.setQueriesData({ queryKey: ['chores'] }, oldData => {
if (!oldData || !oldData.res) return oldData;
return {
...oldData,
res: oldData.res.filter(c => c.id !== chore.id),
}
});
try {
const response = await MarkChoreComplete(
chore.id,
@@ -162,10 +174,36 @@ export const useChoreActions = ({
null,
)
if (response.ok) {
const data = await response.json()
updateChoreInState(data.res, 'completed')
// 2. Show the success notification with Undo
showSuccess({
message: 'Task completed',
undoAction: async () => {
try {
const undoResponse = await UndoChoreAction(chore.id)
if (undoResponse.ok) {
refetchChores()
showUndo({
title: 'Undo Successful',
message: 'Task completion has been undone.',
})
} else throw new Error('Failed to undo')
} catch (error) {
showError({
title: 'Undo Failed',
message: 'Unable to undo the action. Please try again.',
})
}
},
})
// 3. Fetch the fresh active list from the server silently
// (This brings in the next occurrence if recurring, without showing the completed one)
queryClient.invalidateQueries({ queryKey: ['chores'] })
} else {
refetchChores() // Network failed, revert to truth
}
} catch (error) {
refetchChores() // Network failed, revert to truth
if (error?.queued) {
showError({
title: 'Update Failed',
@@ -259,6 +297,7 @@ export const useChoreActions = ({
c => c.id !== chore.id,
)
setChores(newChores)
updateChoreInState(chore.id, 'deleted')
setFilteredChores(newFilteredChores)
showSuccess({
title: 'Task Deleted',
@@ -472,7 +511,7 @@ export const useChoreActions = ({
)
const handleBulkComplete = useCallback(async () => {
const selectedData = getSelectedChoresData()
const selectedData = getSelectedChoresData(chores)
if (selectedData.length === 0) return
setConfirmModelConfig({
@@ -532,7 +571,7 @@ export const useChoreActions = ({
}, [getSelectedChoresData, impersonatedUser, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
const handleBulkArchive = useCallback(async () => {
const selectedData = getSelectedChoresData()
const selectedData = getSelectedChoresData(chores)
if (selectedData.length === 0) return
setConfirmModelConfig({
@@ -594,7 +633,7 @@ export const useChoreActions = ({
}, [getSelectedChoresData, archiveChore, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
const handleBulkDelete = useCallback(async () => {
const selectedData = getSelectedChoresData()
const selectedData = getSelectedChoresData(chores)
if (selectedData.length === 0) return
setConfirmModelConfig({
@@ -654,7 +693,7 @@ export const useChoreActions = ({
}, [getSelectedChoresData, chores, filteredChores, setChores, setFilteredChores, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
const handleBulkSkip = useCallback(async () => {
const selectedData = getSelectedChoresData()
const selectedData = getSelectedChoresData(chores)
if (selectedData.length === 0) return
setConfirmModelConfig({

View File

@@ -15,9 +15,7 @@ export const useChoreFilters = ({
)
const projectFilteredChores = useMemo(() => {
if (!selectedProject) return chores
if (selectedProject.id === 'default') {
if (!selectedProject || selectedProject.id === 'default') {
return chores.filter(chore => !chore.projectId)
}

View File

@@ -123,7 +123,7 @@ const ChoreHistory = () => {
{
icon: <Checklist />,
text: 'All Completed',
subtext: `${histories.filter(h => h.status === ChoreHistoryStatus.COMPLETED).length} times`,
subtext: `${histories.filter(h => h.status === ChoreHistoryStatus.COMPLETED || h.status === ChoreHistoryStatus.SKIPPED).length} times`,
},
{
icon: <TrendingUp />,

View File

@@ -8,6 +8,7 @@ import {
Person,
Redo,
RunningWithErrors,
Schedule,
ThumbDown,
Timelapse,
Toll,
@@ -18,7 +19,7 @@ import { useLocalization } from '../../contexts/LocalizationContext'
import { TASK_COLOR } from '../../utils/Colors.jsx'
const getCompletedChip = historyEntry => {
if (historyEntry.status === 0 || historyEntry.status === 5) {
if (historyEntry.status === 0 || historyEntry.status === 5 || historyEntry.status === 6) {
return null
}
@@ -128,6 +129,7 @@ const HistoryCard = ({
3: { icon: <HourglassEmpty />, color: 'neutral' }, // Pending Approval
4: { icon: <ThumbDown />, color: 'danger' }, // Rejected
5: { icon: <RunningWithErrors />, color: 'danger' }, // Missed
6: { icon: <Schedule />, color: 'warning' }, // Rescheduled
}
const config = statusMap[historyEntry.status] || statusMap[1]
@@ -194,7 +196,9 @@ const HistoryCard = ({
? 'Rejected'
: historyEntry.status === 5
? 'Missed'
: 'Completed'}
: historyEntry.status === 6
? 'Rescheduled'
: 'Completed'}
</Typography>
<Chip size='sm' startDecorator={<EventNote />}>

View File

@@ -611,7 +611,7 @@ const NotificationSetting = () => {
<Typography level='h3'>Custom Notification</Typography>
<Divider />
<Typography level='body-md'>
Notificaiton through other platform like Telegram or Pushover
Notification through other platform like Telegram or Pushover
</Typography>
<FormControl orientation='horizontal'>

View File

@@ -1,14 +1,16 @@
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'
import ThumbDownIcon from '@mui/icons-material/ThumbDown'
import TimelapseIcon from '@mui/icons-material/Timelapse'
import { Cell, Pie, PieChart, Tooltip } from 'recharts'
import {
Block,
AccessTime,
Check,
EventBusy,
EventNote,
Group,
HourglassEmpty,
Redo,
RunningWithErrors,
Schedule,
ThumbDown,
Timeline,
Toll,
} from '@mui/icons-material'
@@ -34,6 +36,7 @@ import React, { useEffect, useState } from 'react'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
import { ChoresGrouper } from '../../utils/Chores'
import { COLORS, TASK_COLOR } from '../../utils/Colors.jsx'
@@ -44,7 +47,9 @@ const groupByDate = history => {
const aggregated = {}
for (let i = 0; i < history.length; i++) {
const item = history[i]
const date = new Date(item.performedAt).toLocaleDateString()
const date = new Date(
item.performedAt || item.updatedAt,
).toLocaleDateString()
if (!aggregated[date]) {
aggregated[date] = []
}
@@ -53,21 +58,25 @@ const groupByDate = history => {
return aggregated
}
const ChoreHistoryItem = ({ time, name, points, status, performer }) => {
const ChoreHistoryItem = ({ time, name, points, status, performer, notes, onViewNote }) => {
const getStatusIcon = status => {
switch (status) {
case 0:
return <TimelapseIcon color='primary' />
return <AccessTime color='primary' />
case 1:
return <Check color='success' />
case 2:
return <Block color='warning' />
return <Redo color='warning' />
case 3:
return <HourglassEmptyIcon color='action' />
return <HourglassEmpty color='neutral' />
case 4:
return <ThumbDownIcon color='error' />
return <ThumbDown color='error' />
case 5:
return <RunningWithErrors color='error' />
case 6:
return <Schedule color='warning' />
default:
return <CheckCircleIcon color='success' />
return <Check color='success' />
}
}
@@ -114,15 +123,36 @@ const ChoreHistoryItem = ({ time, name, points, status, performer }) => {
{`${points} points`}
</Chip>
)}
{notes && (
<Chip
size='sm'
variant='soft'
color='neutral'
startDecorator={<EventNote />}
sx={{ cursor: 'pointer' }}
onClick={e => {
e.stopPropagation()
onViewNote?.(notes)
}}
>
Note
</Chip>
)}
</Box>
</Stack>
)
}
const ChoreHistoryTimeline = ({ history }) => {
const ChoreHistoryTimeline = ({ history, onViewNote }) => {
const { fmt } = useLocalization()
const groupedHistory = groupByDate(history)
const sortedEntries = Object.entries(groupedHistory).sort(
([a], [b]) => new Date(b) - new Date(a),
)
return (
<Container sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
@@ -143,10 +173,15 @@ const ChoreHistoryTimeline = ({ history }) => {
<>
<ChoreHistoryItem
key={record.id}
time={fmt.time(record.performedAt)}
time={fmt.time(
record.performedAt || record.updatedAt,
)}
name={record.choreName}
points={record.points}
status={record.status}
notes={record.notes}
onViewNote={onViewNote}
/>
</>
))}
@@ -366,6 +401,7 @@ const UserActivites = () => {
const [selectedHistory, setSelectedHistory] = React.useState([])
const [enrichedHistory, setEnrichedHistory] = React.useState([])
const [selectedChart, setSelectedChart] = React.useState('history')
const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false })
const [historyPieChartData, setHistoryPieChartData] = React.useState([])
const [choreDuePieChartData, setChoreDuePieChartData] = React.useState([])
@@ -1066,7 +1102,17 @@ const UserActivites = () => {
>
{/* Left Side - Timeline (Mobile: Full width, Desktop: Flexible) */}
<Box sx={{ flex: 1, minWidth: 0, width: '100%' }}>
<ChoreHistoryTimeline history={selectedHistory} />
<ChoreHistoryTimeline
history={selectedHistory}
onViewNote={notes => {
setNoteViewerConfig({
isOpen: true,
title: 'Note',
content: notes,
onClose: () => setNoteViewerConfig({ isOpen: false }),
})
}}
/>
</Box>
{/* Right Sidebar - Charts (Mobile: Full width, Desktop: Fixed width + sticky) */}
@@ -1216,6 +1262,7 @@ const UserActivites = () => {
</Box>
</>
)}
<NoteViewerModal config={noteViewerConfig} />
</Container>
)
}

View File

@@ -1,5 +1,14 @@
import { Add, EditNotifications } from '@mui/icons-material'
import { Box, Button, Input, Option, Select, Typography } from '@mui/joy'
import {
Box,
Button,
Checkbox,
FormHelperText,
Input,
Option,
Select,
Typography,
} from '@mui/joy'
import { FormControl } from '@mui/material'
import * as chrono from 'chrono-node'
import moment from 'moment'
@@ -20,7 +29,6 @@ import {
} from './CustomParsers'
import SmartTaskTitleInput from './SmartTaskTitleInput'
import DurationInput from '../../components/common/DurationInput'
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
import NotificationTemplate from '../../components/NotificationTemplate'
import LearnMoreButton from './LearnMore'
@@ -93,6 +101,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
const [hasNotifications, setHasNotifications] = useState(false)
const [hasDeadline, setHasDeadline] = useState(false)
const [deadlineOffset, setDeadlineOffset] = useState(-1)
const [dueDateOnly, setDueDateOnly] = useState(null)
const [dueTime, setDueTime] = useState(null)
const [useCustomTime, setUseCustomTime] = useState(false)
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
const [projectId, setProjectId] = useState(getInitialProject())
@@ -136,7 +147,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
!dueDate
) {
// add due date:
setDueDate(moment().add(1, 'day').format('YYYY-MM-DDTHH:00:00'))
const tomorrow = moment().add(1, 'day')
setDueDateOnly(tomorrow.format('YYYY-MM-DD'))
setDueDate(tomorrow.endOf('day').format('YYYY-MM-DDTHH:mm:59'))
setUseCustomTime(false)
setDueTime(null)
setShowKeyboardShortcuts(false)
}
// Enter key to create task
@@ -380,9 +395,24 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
setFrequencyHumanReadable(repeat.name)
}
const syncDueDateStates = parsedDate => {
const m = moment(parsedDate)
const dateOnly = m.format('YYYY-MM-DD')
const timeOnly = m.format('HH:mm')
setDueDateOnly(dateOnly)
setDueDate(m.format('YYYY-MM-DDTHH:mm:ss'))
if (timeOnly !== '23:59') {
setUseCustomTime(true)
setDueTime(timeOnly)
} else {
setUseCustomTime(false)
setDueTime(null)
}
}
let dueDateHighlight = null
if (dueDateParsed.result) {
setDueDate(moment(dueDateParsed.result).format('YYYY-MM-DDTHH:mm:ss'))
syncDueDateStates(dueDateParsed.result)
dueDateHighlight = dueDateParsed.highlight[0]
}
@@ -391,9 +421,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
// we need to reparse the date again to get the correct due date:
const dueDateParsedAgain = parseDueDate(sentence, chrono)
if (dueDateParsedAgain.result) {
setDueDate(
moment(dueDateParsedAgain.result).format('YYYY-MM-DDTHH:mm:ss'),
)
syncDueDateStates(dueDateParsedAgain.result)
}
}
@@ -473,6 +501,47 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
processText,
])
const handleDueDateChange = e => {
const dateValue = e.target.value
setDueDateOnly(dateValue)
if (useCustomTime && dueTime) {
setDueDate(
moment(`${dateValue}T${dueTime}`).format('YYYY-MM-DDTHH:mm:00'),
)
} else {
setDueDate(moment(dateValue).endOf('day').format('YYYY-MM-DDTHH:mm:ss'))
}
}
const handleDueTimeChange = e => {
const timeValue = e.target.value
setDueTime(timeValue)
if (dueDateOnly) {
setDueDate(
moment(`${dueDateOnly}T${timeValue}`).format('YYYY-MM-DDTHH:mm:00'),
)
}
}
const handleUseCustomTimeChange = checked => {
setUseCustomTime(checked)
if (checked) {
const defaultTime = dueTime || '18:00'
setDueTime(defaultTime)
if (dueDateOnly) {
setDueDate(
moment(`${dueDateOnly}T${defaultTime}`).format('YYYY-MM-DDTHH:mm:00'),
)
}
} else {
if (dueDateOnly) {
setDueDate(
moment(dueDateOnly).endOf('day').format('YYYY-MM-DDTHH:mm:ss'),
)
}
}
}
const handleEnterPressed = () => {
createChore()
}
@@ -496,6 +565,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
setProjectId(getInitialProject())
setHasDeadline(false)
setDeadlineOffset(-1)
setDueDateOnly(null)
setDueTime(null)
setUseCustomTime(false)
}
const createChore = () => {
@@ -526,6 +598,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
const chore = {
name: taskTitle,
description: description,
assignees: finalAssignees,
dueDate: dueDate ? new Date(dueDate).toISOString() : null,
assignedTo: finalAssignedTo,
@@ -781,7 +854,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
variant='plain'
size='sm'
onClick={() => {
setDueDate(moment().add(1, 'day').format('YYYY-MM-DDTHH:00:00'))
const tomorrow = moment().add(1, 'day')
setDueDateOnly(tomorrow.format('YYYY-MM-DD'))
setDueDate(tomorrow.endOf('day').format('YYYY-MM-DDTHH:mm:ss'))
setUseCustomTime(false)
setDueTime(null)
}}
endDecorator={
showKeyboardShortcuts && <KeyboardShortcutHint shortcut='B' />
@@ -804,7 +881,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
Edit Notifications
</Button>
)}
{!hasDeadline && dueDate && (
{/* {!hasDeadline && dueDate && (
<Button
startDecorator={<Add />}
variant='plain'
@@ -816,7 +893,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
>
Set Deadline
</Button>
)}
)} */}
</Box>
{hasDescription && (
@@ -871,11 +948,30 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
<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' }}
type='date'
value={dueDateOnly || ''}
onChange={handleDueDateChange}
/>
<Checkbox
size='sm'
checked={useCustomTime}
onChange={e => handleUseCustomTimeChange(e.target.checked)}
label='Set a specific time'
sx={{ mt: 1 }}
/>
<FormHelperText>
{useCustomTime
? 'Task will be due at the specified time'
: 'Task will be due at the end of the day (11:59 PM)'}
</FormHelperText>
{useCustomTime && (
<Input
type='time'
value={dueTime || '18:00'}
onChange={handleDueTimeChange}
sx={{ maxWidth: 200, mt: 1 }}
/>
)}
</FormControl>
)}
</Box>
@@ -978,7 +1074,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
)}
</Box>
</FormControl> */}
{hasDeadline && dueDate && (
{/* {hasDeadline && dueDate && (
<Box
sx={{
flexDirection: 'column',
@@ -998,7 +1094,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
<Typography level='body-sm'>after due date</Typography>
</Box>
</Box>
)}
)} */}
{hasNotifications && dueDate && (
<Box
sx={{
@@ -1013,11 +1109,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
if (
metadata.notifications !== notificationMetadata.templates
) {
const newNotificaitonMetadata = {
const newNotificationMetadata = {
...notificationMetadata,
templates: metadata.notifications,
}
setNotificationMetadata(newNotificaitonMetadata)
setNotificationMetadata(newNotificationMetadata)
}
}}
value={notificationMetadata}

View File

@@ -722,8 +722,16 @@ export const parseDueDate = (inputSentence, chrono) => {
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
.trim()
// If no specific time was mentioned, set to end of day (23:59:59)
// to indicate the date has no specific time tied to it (same convention as ChoreEdit)
let resultDate = dueDateMatch.start.date()
if (!dueDateMatch.start.isCertain('hour')) {
resultDate = new Date(resultDate)
resultDate.setHours(23, 59, 59, 0)
}
return {
result: dueDateMatch.start.date(),
result: resultDate,
highlight: [
{
text: fullHighlightText,

View File

@@ -155,6 +155,7 @@ const NavBar = () => {
'/login',
'/auth/oauth2',
'/forgot-password',
'/password/update',
'/login/settings',
'/welcome',
].includes(location.pathname)

View File

@@ -167,6 +167,13 @@
line-height: 1.5;
}
/* Prevent iOS Safari from auto-zooming on focus (triggered when font-size < 16px) */
@supports (-webkit-touch-callout: none) {
.quill-root .ql-editor {
font-size: 16px;
}
}
/* Custom focus styles */
.quill-root:focus-within .ql-toolbar.ql-snow {
border-color: var(--joy-palette-primary-outlinedBorder, #1976d2);

View File

@@ -213,7 +213,7 @@ const SmartTaskTitleInput = ({
fontSize: 'inherit',
lineHeight: 'inherit',
backgroundColor: 'transparent',
color: mode === 'dark' ? '#cbd5e1' : '#1a202c',
color: 'transparent',
caretColor: mode === 'dark' ? '#fff' : '#000',
}}
/>