From 9de1c79da6110df752711b1ee8188066b56ba978 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Wed, 15 Jan 2025 02:03:28 -0500 Subject: [PATCH 1/5] Update IconButtonWithMenu to use shorter prop name to not conflict with the react key --- src/views/Chores/IconButtonWithMenu.jsx | 8 ++++---- src/views/Chores/MyChores.jsx | 9 +++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/views/Chores/IconButtonWithMenu.jsx b/src/views/Chores/IconButtonWithMenu.jsx index 2ad1706..8d627b7 100644 --- a/src/views/Chores/IconButtonWithMenu.jsx +++ b/src/views/Chores/IconButtonWithMenu.jsx @@ -5,7 +5,7 @@ import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' const IconButtonWithMenu = ({ label, - key, + k, icon, options, onItemSelect, @@ -72,14 +72,14 @@ const IconButtonWithMenu = ({ )} {title && ( - + {title} @@ -87,7 +87,7 @@ const IconButtonWithMenu = ({ )} {options?.map(item => ( { onItemSelect(item) setSelectedItem?.selectedItem(item.name) diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index 16e0055..21f5e79 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -420,11 +420,7 @@ const MyChores = () => { setActiveTextField('search') setSearchInputFocus(searchInputFocus + 1) - searchInputRef.current.focus() - searchInputRef.current.selectionStart = - searchInputRef.current.value?.length - searchInputRef.current.selectionEnd = - searchInputRef.current.value?.length + searchInputRef?.current?.focus() }} > @@ -434,6 +430,7 @@ const MyChores = () => { } options={[ { name: 'Due Date', value: 'due_date' }, @@ -456,7 +453,7 @@ const MyChores = () => {
} options={Priorities} selectedItem={selectedFilter} From aa7f412f144dfc317573781e04e58bf1ae658479 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Wed, 15 Jan 2025 02:03:59 -0500 Subject: [PATCH 2/5] Update prop names in IconButtonWithMenu to avoid conflicts --- src/views/Chores/MyChores.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index 21f5e79..6cc5d87 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -430,7 +430,7 @@ const MyChores = () => { } options={[ { name: 'Due Date', value: 'due_date' }, @@ -453,7 +453,7 @@ const MyChores = () => {
} options={Priorities} selectedItem={selectedFilter} @@ -465,7 +465,7 @@ const MyChores = () => { /> } options={userLabels} From 56f66e9e8ff9a3353a5018fca0dba2ce6ba38c9e Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Thu, 16 Jan 2025 00:41:04 -0500 Subject: [PATCH 3/5] Add useDebounce hook for debouncing input values Fix parsing --- src/utils/Debounce.jsx | 19 ++++++++ src/views/Chores/ChoreCard.jsx | 16 +++--- src/views/components/AddTaskModal.jsx | 70 ++++++++++++++++++++++++--- src/views/components/CalendarView.jsx | 7 +-- 4 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 src/utils/Debounce.jsx diff --git a/src/utils/Debounce.jsx b/src/utils/Debounce.jsx new file mode 100644 index 0000000..3ef84ab --- /dev/null +++ b/src/utils/Debounce.jsx @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react' + +function useDebounce(value, delay) { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => { + clearTimeout(handler) + } + }, [value, delay]) + + return debouncedValue +} + +export default useDebounce diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index a9df0a1..002b396 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -300,6 +300,12 @@ const ChoreCard = ({ } } const getRecurrentChipText = chore => { + // if chore.frequencyMetadata is type string then parse it otherwise assigned to the metadata: + const metadata = + typeof chore.frequencyMetadata === 'string' + ? JSON.parse(chore.frequencyMetadata) + : chore.frequencyMetadata + const dayOfMonthSuffix = n => { if (n >= 11 && n <= 13) { return 'th' @@ -330,7 +336,7 @@ const ChoreCard = ({ } else if (chore.frequencyType === 'yearly') { return 'Yearly' } else if (chore.frequencyType === 'days_of_the_week') { - let days = JSON.parse(chore.frequencyMetadata).days + let days = metadata.days if (days.length > 4) { const allDays = [ 'Sunday', @@ -354,7 +360,7 @@ const ChoreCard = ({ return days.join(', ') } } else if (chore.frequencyType === 'day_of_the_month') { - let months = JSON.parse(chore.frequencyMetadata).months + let months = metadata.months if (months.length > 6) { const allMonths = [ 'January', @@ -385,16 +391,14 @@ const ChoreCard = ({ except ${notSelectedShortMonths.join(', ')}` return result } else { - let freqData = JSON.parse(chore.frequencyMetadata) + let freqData = metadata const months = freqData.months.map(m => moment().month(m).format('MMM')) return `${chore.frequency}${dayOfMonthSuffix( chore.frequency, )} of ${months.join(', ')}` } } else if (chore.frequencyType === 'interval') { - return `Every ${chore.frequency} ${ - JSON.parse(chore.frequencyMetadata).unit - }` + return `Every ${chore.frequency} ${metadata.unit}` } else { return chore.frequencyType } diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index 3e2b4b2..fcc1bf5 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -19,7 +19,9 @@ 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 useDebounce from '../../utils/Debounce' import { CreateChore } from '../../utils/Fetcher' +import { useLabels } from '../Labels/LabelQueries' import LearnMoreButton from './LearnMore' const VALID_DAYS = { monday: 'Monday', @@ -69,9 +71,11 @@ const ALL_MONTHS = Object.values(VALID_MONTHS).filter( ) const TaskInput = ({ autoFocus, onChoreUpdate }) => { + const { data: userLabels, isLoading: userLabelsLoading } = useLabels() const { userProfile } = useContext(UserContext) const navigate = useNavigate() const [taskText, setTaskText] = useState('') + const debounceParsing = useDebounce(taskText, 300) const [taskTitle, setTaskTitle] = useState('') const [openModal, setOpenModal] = useState(false) const textareaRef = useRef(null) @@ -98,6 +102,12 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { } }, [autoFocus]) + useEffect(() => { + if (debounceParsing) { + processText(debounceParsing) + } + }, [debounceParsing]) + const handleEnterPressed = e => { if (e.key === 'Enter') { createChore() @@ -139,7 +149,28 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { } return { result: 0, cleanedSentence: inputSentence } } - + const parseLabels = inputSentence => { + let sentence = inputSentence.toLowerCase() + const currentLabels = [] + // label will always be prefixed #: + for (const label of userLabels) { + if (sentence.includes(`#${label.name.toLowerCase()}`)) { + currentLabels.push(label) + sentence = sentence.replace(`#${label.name.toLowerCase()}`, '') + } + } + if (currentLabels.length > 0) { + return { + result: currentLabels, + cleanedSentence: sentence, + } + } + return { result: null, cleanedSentence: sentence } + } + const parseAssignee = inputSentence => { + let sentence = inputSentence.toLowerCase() + const assigneeMap = {} + } const parseRepeatV2 = inputSentence => { const sentence = inputSentence.toLowerCase() const result = { @@ -284,8 +315,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { .replace('{months}', result.frequencyMetadata.months.join(', ')), cleanedSentence: inputSentence.replace(match[0], '').trim(), } - - case 'interval:every_other': case 'interval:2week': result.frequency = 2 result.frequencyMetadata.unit = 'weeks' @@ -307,6 +336,15 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { .replace('{months}', result.frequencyMetadata.months.join(', ')), cleanedSentence: inputSentence.replace(match[0], '').trim(), } + case 'interval:every_other': + result.frequency = 2 + result.frequencyMetadata.unit = match[1] + result.frequencyType = 'interval' + return { + result, + name: pattern.name.replace('{unit}', result.frequencyMetadata.unit), + cleanedSentence: inputSentence.replace(match[0], '').trim(), + } } } return { result: null, name: null, cleanedSentence: inputSentence } @@ -321,8 +359,10 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { setPriority(0) return } - - let cleanedSentence = e.target.value + setTaskText(e.target.value) + } + const processText = sentence => { + let cleanedSentence = sentence const priority = parsePriority(cleanedSentence) if (priority.result) setPriority(priority.result) cleanedSentence = priority.cleanedSentence @@ -344,11 +384,24 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { cleanedSentence = cleanedSentence.replace(parsedDueDate[0].text, '') } + if (repeat.result) { + // if repeat has result the cleaned sentence will remove the date related info which mean + // we need to reparse the date again to get the correct due date: + const parsedDueDate = chrono.parse(sentence, new Date(), { + forwardDate: true, + }) + if (parsedDueDate[0]?.index > -1) { + setDueDate( + moment(parsedDueDate[0].start.date()).format('YYYY-MM-DDTHH:mm:ss'), + ) + } + } + if (priority.result || parsedDueDate[0]?.index > -1 || repeat.result) { setOpenModal(true) } - setTaskText(e.target.value) + setTaskText(sentence) setTaskTitle(cleanedSentence.trim()) } @@ -504,6 +557,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { Priority + diff --git a/src/views/components/CalendarView.jsx b/src/views/components/CalendarView.jsx index 43e44b8..33ed5d4 100644 --- a/src/views/components/CalendarView.jsx +++ b/src/views/components/CalendarView.jsx @@ -132,12 +132,7 @@ const CalendarView = ({ chores }) => { mb: 0.4, py: 1, px: 1, - - // backgroundColor: getAssigneeColor( - // chore.assignedTo, - // userProfile, - // ), - // everything show in one row: + cursor: 'pointer', }} > From 2d4fb2b16317e114d65a8f5d26dd5a1b3b06d123 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Fri, 17 Jan 2025 00:49:54 -0500 Subject: [PATCH 4/5] Add description field to ChoreEdit form and Choreview --- src/views/ChoreEdit/ChoreEdit.jsx | 17 +++++++++- src/views/ChoreEdit/ChoreView.jsx | 45 +++++++++++++++++++++++++++ src/views/components/AddTaskModal.jsx | 16 ++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index e3eec30..62c39fe 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -22,6 +22,7 @@ import { Snackbar, Stack, Switch, + Textarea, Typography, } from '@mui/joy' import moment from 'moment' @@ -61,6 +62,7 @@ const ChoreEdit = () => { const [userHistory, setUserHistory] = useState({}) const { choreId } = useParams() const [name, setName] = useState('') + const [description, setDescription] = useState('') const [confirmModelConfig, setConfirmModelConfig] = useState({}) const [assignees, setAssignees] = useState([]) const [performers, setPerformers] = useState([]) @@ -186,6 +188,7 @@ const ChoreEdit = () => { const chore = { id: Number(choreId), name: name, + description: description, assignees: assignees, dueDate: dueDate ? new Date(dueDate).toISOString() : null, frequencyType: frequencyType, @@ -241,6 +244,7 @@ const ChoreEdit = () => { .then(data => { setChore(data.res) setName(data.res.name ? data.res.name : '') + setDescription(data.res.description ? data.res.description : '') setAssignees(data.res.assignees ? data.res.assignees : []) setAssignedTo(data.res.assignedTo) setFrequencyType( @@ -379,11 +383,22 @@ const ChoreEdit = () => { Title : - What is this chore about? + What is the name of this chore? setName(e.target.value)} /> {errors.name} + + + Details: + What is this chore about? +