diff --git a/README.md b/README.md
index b235431..65b9eae 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/src/components/NotificationTemplate.jsx b/src/components/NotificationTemplate.jsx
index 0556e39..734fd0b 100644
--- a/src/components/NotificationTemplate.jsx
+++ b/src/components/NotificationTemplate.jsx
@@ -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 (
- <>
+
handleChange(idx, 'timing', value)}
+ onBlur={() => handleBlur(idx)}
sx={{ minWidth: 80 }}
size={'sm'}
>
{timingOptions.map(opt => (
-
))}
@@ -560,11 +579,42 @@ const NotificationTemplate = ({
- 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 = ({
- >
+
)
})}
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)
diff --git a/src/utils/Chores.jsx b/src/utils/Chores.jsx
index 6813603..cde9fe7 100644
--- a/src/utils/Chores.jsx
+++ b/src/utils/Chores.jsx
@@ -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
},
diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx
index f0a1467..108ff14 100644
--- a/src/views/Authorization/LoginView.jsx
+++ b/src/views/Authorization/LoginView.jsx
@@ -771,17 +771,19 @@ const LoginView = () => {
)}
-
+ {!resource?.is_user_creation_disabled && (
+
+ )}
{
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 = () => {
{completionWindow !== -1 && (
-
-
- before due date
-
+
+
+ Hours:
+ {
+ setCompletionWindow(parseInt(e.target.value))
+ }}
+ />
+
+
)}
{/* Expires After (Deadline) */}
-
+ {/*
{
if (e.target.checked) {
setDeadlineOffset(86400) // default 1 day in seconds
@@ -1237,9 +1251,11 @@ const ChoreEdit = () => {
label='Set a deadline'
/>
- 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'}
-
+ */}
{deadlineOffset !== -1 && (
{
setIsRolling(true)}
+ onClick={() => {
+ setIsRolling(true)
+ setDeadlineOffset(-1)
+ }}
label='Reschedule from completion date'
/>
@@ -1624,39 +1643,38 @@ const ChoreEdit = () => {
}}
>
{choreId > 0 && (
- <>
- {isActive ? (
-
- ) : (
-
- )}
-
- >
+
+
+
+
+
+
+
)}
)
}
-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 (
@@ -143,10 +173,15 @@ const ChoreHistoryTimeline = ({ history }) => {
<>
>
))}
@@ -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) */}
-
+ {
+ setNoteViewerConfig({
+ isOpen: true,
+ title: 'Note',
+ content: notes,
+ onClose: () => setNoteViewerConfig({ isOpen: false }),
+ })
+ }}
+ />
{/* Right Sidebar - Charts (Mobile: Full width, Desktop: Fixed width + sticky) */}
@@ -1216,6 +1262,7 @@ const UserActivites = () => {
>
)}
+
)
}
diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx
index 4e83499..2254cf7 100644
--- a/src/views/components/AddTaskModal.jsx
+++ b/src/views/components/AddTaskModal.jsx
@@ -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 &&
@@ -804,7 +881,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
Edit Notifications
)}
- {!hasDeadline && dueDate && (
+ {/* {!hasDeadline && dueDate && (
}
variant='plain'
@@ -816,7 +893,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
>
Set Deadline
- )}
+ )} */}
{hasDescription && (
@@ -871,11 +948,30 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
Due Date
setDueDate(e.target.value)}
- sx={{ width: '100%', fontSize: '16px' }}
+ type='date'
+ value={dueDateOnly || ''}
+ onChange={handleDueDateChange}
/>
+ handleUseCustomTimeChange(e.target.checked)}
+ label='Set a specific time'
+ sx={{ mt: 1 }}
+ />
+
+ {useCustomTime
+ ? 'Task will be due at the specified time'
+ : 'Task will be due at the end of the day (11:59 PM)'}
+
+ {useCustomTime && (
+
+ )}
)}
@@ -978,7 +1074,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
)}
*/}
- {hasDeadline && dueDate && (
+ {/* {hasDeadline && dueDate && (
{
after due date
- )}
+ )} */}
{hasNotifications && dueDate && (
{
if (
metadata.notifications !== notificationMetadata.templates
) {
- const newNotificaitonMetadata = {
+ const newNotificationMetadata = {
...notificationMetadata,
templates: metadata.notifications,
}
- setNotificationMetadata(newNotificaitonMetadata)
+ setNotificationMetadata(newNotificationMetadata)
}
}}
value={notificationMetadata}
diff --git a/src/views/components/CustomParsers.js b/src/views/components/CustomParsers.js
index 2983ba5..0a45e2d 100644
--- a/src/views/components/CustomParsers.js
+++ b/src/views/components/CustomParsers.js
@@ -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,
diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx
index ec5df1e..98b2483 100644
--- a/src/views/components/NavBar.jsx
+++ b/src/views/components/NavBar.jsx
@@ -155,6 +155,7 @@ const NavBar = () => {
'/login',
'/auth/oauth2',
'/forgot-password',
+ '/password/update',
'/login/settings',
'/welcome',
].includes(location.pathname)
diff --git a/src/views/components/RichTextEditor.css b/src/views/components/RichTextEditor.css
index 5c5cf95..20147ea 100644
--- a/src/views/components/RichTextEditor.css
+++ b/src/views/components/RichTextEditor.css
@@ -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);
diff --git a/src/views/components/SmartTaskTitleInput.jsx b/src/views/components/SmartTaskTitleInput.jsx
index 4d87016..8432c44 100644
--- a/src/views/components/SmartTaskTitleInput.jsx
+++ b/src/views/components/SmartTaskTitleInput.jsx
@@ -213,7 +213,7 @@ const SmartTaskTitleInput = ({
fontSize: 'inherit',
lineHeight: 'inherit',
backgroundColor: 'transparent',
- color: mode === 'dark' ? '#cbd5e1' : '#1a202c',
+ color: 'transparent',
caretColor: mode === 'dark' ? '#fff' : '#000',
}}
/>