a.indexOf(v) === i,
-)
-
-const TaskInput = ({ autoFocus, onChoreUpdate }) => {
+const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
+ const { data: allUsers, isLoading: isAllUserLoading } = useAllUsers()
const createChoreMutation = useCreateChore()
const { userProfile } = useContext(UserContext)
@@ -82,24 +38,29 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
const [taskText, setTaskText] = useState('')
const debounceParsing = useDebounce(taskText, 30)
const [taskTitle, setTaskTitle] = useState('')
- const [openModal, setOpenModal] = useState(false)
+ const [renderedParts, setRenderedParts] = useState([])
+
const textareaRef = useRef(null)
const mainInputRef = useRef(null)
const [priority, setPriority] = useState(0)
const [dueDate, setDueDate] = useState(null)
const [description, setDescription] = useState(null)
+ const [assignedTo, setAssignedTo] = useState(userProfile?.id)
+ const [assignees, setAssignees] = useState([])
+ const [labelsV2, setLabelsV2] = useState([])
const [frequency, setFrequency] = useState(null)
const [frequencyHumanReadable, setFrequencyHumanReadable] = useState(null)
- const [subTasks, setSubTasks] = useState([])
+ const [subTasks, setSubTasks] = useState(null)
const [hasDescription, setHasDescription] = useState(false)
+ const [hasSubTasks, setHasSubTasks] = useState(false)
useEffect(() => {
- if (openModal && textareaRef.current) {
+ if (isModalOpen && textareaRef.current) {
textareaRef.current.focus()
textareaRef.current.selectionStart = textareaRef.current.value?.length
textareaRef.current.selectionEnd = textareaRef.current.value?.length
}
- }, [openModal])
+ }, [isModalOpen])
useEffect(() => {
if (autoFocus > 0 && mainInputRef.current) {
@@ -110,21 +71,19 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
}, [autoFocus])
useEffect(() => {
- if (debounceParsing) {
- processText(debounceParsing)
+ if (!isModalOpen || userLabelsLoading || isAllUserLoading) {
+ return
}
- }, [debounceParsing])
- const handleEnterPressed = e => {
- if (e.key === 'Enter') {
- createChore()
- handleCloseModal()
- setTaskText('')
- }
+ processText(taskText)
+ }, [taskText, userLabelsLoading, isAllUserLoading])
+
+ const handleEnterPressed = () => {
+ createChore()
}
- const handleCloseModal = () => {
- setOpenModal(false)
+ const handleCloseModal = forceRefetch => {
+ onClose(forceRefetch)
setTaskText('')
setTaskTitle('')
setDueDate(null)
@@ -133,7 +92,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
setPriority(0)
setHasDescription(false)
setDescription(null)
- setSubTasks([])
+ setSubTasks(null)
+ setHasSubTasks(false)
+ setLabelsV2([])
}
const handleSubmit = () => {
@@ -142,245 +103,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
setTaskText('')
}
- const parsePriority = inputSentence => {
- let sentence = inputSentence.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, ''),
- inputSentence,
- ),
- }
- }
- }
- 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 = {
- 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|everyday)$/i,
- name: 'Every day',
- },
- {
- frequencyType: 'daily:time',
- regex: /every (morning|noon|afternoon|evening|night)$/i,
- name: 'Every {time} daily',
- },
- {
- 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}',
- },
- {
- 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: inputSentence.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: inputSentence.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: inputSentence }
- return {
- result,
- name: pattern.name.replace(
- '{days}',
- result.frequencyMetadata.days.join(', '),
- ),
- cleanedSentence: inputSentence.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: inputSentence.replace(match[0], '').trim(),
- }
- case 'interval:2week':
- result.frequency = 2
- result.frequencyMetadata.unit = 'weeks'
- result.frequencyType = 'interval'
- return {
- result,
- name: pattern.name,
- cleanedSentence: inputSentence.replace(match[0], '').trim(),
- }
- case 'daily:time':
- result.frequency = 1
- result.frequencyMetadata.unit = 'days'
- result.frequencyType = 'daily'
- return {
- result,
- name: pattern.name.replace('{time}', match[1]),
- // replace every x with ''
-
- cleanedSentence: inputSentence.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: 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 }
- }
-
const handleTextChange = e => {
if (!e.target.value) {
setTaskText('')
@@ -394,18 +116,27 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
}
const processText = sentence => {
let cleanedSentence = sentence
- const priority = parsePriority(cleanedSentence)
+ const priority = parsePriority(sentence)
if (priority.result) setPriority(priority.result)
cleanedSentence = priority.cleanedSentence
+ const labels = parseLabels(sentence, userLabels)
+ if (labels.result) {
+ cleanedSentence = labels.cleanedSentence
+ setLabelsV2(labels.result)
+ }
- const repeat = parseRepeatV2(cleanedSentence)
+ const repeat = parseRepeatV2(sentence)
if (repeat.result) {
setFrequency(repeat.result)
setFrequencyHumanReadable(repeat.name)
cleanedSentence = repeat.cleanedSentence
}
-
- const parsedDueDate = chrono.parse(cleanedSentence, new Date(), {
+ // const assignees = parseAssignees(sentence)
+ // if (assignees.result) {
+ // cleanedSentence = assignees.cleanedSentence
+ // set
+ // }
+ const parsedDueDate = chrono.parse(sentence, new Date(), {
forwardDate: true,
})
if (parsedDueDate[0]?.index > -1) {
@@ -428,14 +159,106 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
}
}
- if (priority.result || parsedDueDate[0]?.index > -1 || repeat.result) {
- setOpenModal(true)
- }
-
setTaskText(sentence)
setTaskTitle(cleanedSentence.trim())
- }
+ const rendered = renderText(
+ sentence,
+ repeat.highlight,
+ priority.highlight,
+ labels.highlight,
+ parsedDueDate && parsedDueDate[0]
+ ? {
+ start: parsedDueDate[0].index,
+ end: parsedDueDate[0].index + parsedDueDate[0].text.length,
+ text: parsedDueDate[0].text,
+ }
+ : null,
+ )
+ setRenderedParts(rendered)
+ }
+ const renderText = (
+ sentence,
+ repeatHighlight,
+ priorityHighlight,
+ labelsHighlight,
+ dueDateHighlight,
+ ) => {
+ const parts = []
+ let lastIndex = 0
+
+ // Combine all highlight ranges and sort them by their start index
+ const allHighlights = []
+ if (repeatHighlight) {
+ repeatHighlight.forEach(h => allHighlights.push({ ...h, type: 'repeat' }))
+ }
+ if (priorityHighlight) {
+ priorityHighlight.forEach(h =>
+ allHighlights.push({ ...h, type: 'priority' }),
+ )
+ }
+ if (labelsHighlight) {
+ labelsHighlight.forEach(h => allHighlights.push({ ...h, type: 'label' }))
+ }
+ if (dueDateHighlight) {
+ allHighlights.push({ ...dueDateHighlight, type: 'dueDate' })
+ }
+
+ allHighlights.sort((a, b) => a.start - b.start)
+
+ for (const highlight of allHighlights) {
+ // Add the text before the highlight
+ if (highlight.start > lastIndex) {
+ parts.push(sentence.substring(lastIndex, highlight.start))
+ }
+
+ // Determine the class name based on the highlight type
+ let className = ''
+ switch (highlight.type) {
+ case 'repeat':
+ className = 'highlight-repeat'
+ break
+ case 'priority':
+ className = 'highlight-priority'
+ break
+ case 'label':
+ className = 'highlight-label'
+ break
+ case 'dueDate':
+ className = 'highlight-date'
+ break
+ default:
+ break
+ }
+
+ // Add the highlighted span
+ parts.push(
+
+ {sentence.substring(highlight.start, highlight.end)}
+ ,
+ )
+
+ // Update the last index to the end of the current highlight
+ lastIndex = highlight.end
+ }
+
+ // Add any remaining text after the last highlight
+ if (lastIndex < sentence.length) {
+ parts.push(sentence.substring(lastIndex))
+ }
+
+ return parts
+ }
const createChore = () => {
const chore = {
name: taskTitle,
@@ -446,13 +269,13 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
isRolling: false,
notification: false,
description: description || null,
- labelsV2: [],
+ labelsV2: labelsV2,
priority: priority ? Number(priority) : 0,
status: 0,
frequencyType: 'once',
frequencyMetadata: {},
notificationMetadata: {},
- subTasks: subTasks.length > 0 ? subTasks : null,
+ subTasks: subTasks?.length > 0 ? subTasks : null,
}
if (frequency) {
@@ -469,78 +292,51 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
chore.nextDueDate = new Date(dueDate).toUTCString()
}
- createChoreMutation.mutateAsync(chore).then(resp => {
- resp.json().then(data => {
- if (resp.status !== 200) {
- console.error('Error creating chore:', data)
- return
- } else {
- onChoreUpdate({ ...chore, id: data.res, nextDueDate: chore.dueDate })
+ createChoreMutation
+ .mutateAsync(chore)
+ .then(resp => {
+ resp.json().then(data => {
+ if (resp.status !== 200) {
+ console.error('Error creating chore:', data)
+ return
+ } else {
+ onChoreUpdate({
+ ...chore,
+ id: data.res,
+ nextDueDate: chore.dueDate,
+ })
+
+ handleCloseModal(false)
+ }
+ })
+ })
+ .catch(error => {
+ if (error?.queued) {
+ handleCloseModal(true)
}
})
- })
+ }
+ if (userLabelsLoading || isAllUserLoading) {
+ return <>>
}
return (
- <>
- {!openModal && (
-
- 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={
-
-
-
- }
- />
-
- )}
-
-
-
-
- {/* */}
- Create new task
-
- Experimental Feature
-
-
+
+
+
+ Create new task
+
+ Experimental Feature
+
+
+
Task in a sentence:
-
@@ -585,58 +381,125 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
}
/>
-
+
+ {
+ setTaskText(text)
+ }}
+ customRenderer={renderedParts}
+ onEnterPressed={handleEnterPressed}
+ suggestions={{
+ '#': {
+ value: 'id',
+ display: 'name',
+ options: userLabels ? userLabels : [],
+ },
+ '!': ['P1', 'P2', 'P3', 'P4'],
+ '@': {
+ // value: 'userId',
+ // display: 'displayName',
+ // options: allUsers ? allUsers : [],
+ value: 'id',
+ display: 'name',
+ options: [
+ { id: userProfile.id, name: userProfile.displayName },
+ ],
+ },
+ }}
+ />
+
+ {/*
Title:
setTaskTitle(e.target.value)}
sx={{ width: '100%', fontSize: '16px' }}
/>
-
-
- {hasDescription ? (
-
- Description:
-
- ) : (
+ */}
+
+ {!hasDescription && (
}
variant='plain'
size='sm'
onClick={() => setHasDescription(true)}
>
- Add Description
+ Description
)}
- Subtasks:
-
+ {!hasSubTasks && (
+ }
+ variant='plain'
+ size='sm'
+ onClick={() => setHasSubTasks(true)}
+ >
+ Subtasks
+
+ )}
+ {!dueDate && (
+ }
+ variant='plain'
+ size='sm'
+ onClick={() => {
+ setDueDate(
+ moment().add(1, 'day').format('YYYY-MM-DDTHH:00:00'),
+ )
+ }}
+ >
+ Due Date
+
+ )}
+
-
-
- Priority
-
-
+ {hasDescription && (
+
+ Description:
+
+ )}
+ {hasSubTasks && (
+
+ Subtasks:
+
+
+ )}
+
+
+
+ Priority
+
+
+ {dueDate && (
Due Date
{
sx={{ width: '100%', fontSize: '16px' }}
/>
-
-
+
+
+ Assignee
+
+
+
+ Frequency
+
+
+
+
+
-
-
-
-
-
-
-
- >
+ Cancel
+
+
+
+
+
+
)
}
diff --git a/src/views/components/AutocompleteInput.jsx b/src/views/components/AutocompleteInput.jsx
new file mode 100644
index 0000000..cc889f5
--- /dev/null
+++ b/src/views/components/AutocompleteInput.jsx
@@ -0,0 +1,116 @@
+import { Chip, List, ListItem, ListItemButton, Textarea } from '@mui/joy'
+import React, { useEffect, useRef, useState } from 'react'
+
+const AutocompleteInput = ({ options, ref, value, onChange, ...props }) => {
+ const [filteredOptions, setFilteredOptions] = useState([])
+ const [menuVisible, setMenuVisible] = useState(false)
+ const [highlightedIndex, setHighlightedIndex] = useState(-1)
+ const [triggerKey, setTriggerKey] = useState(null)
+ // const inputRef = useRef(null)
+ const menuRef = useRef(null)
+
+ useEffect(() => {
+ if (!triggerKey || !value.includes(triggerKey)) {
+ setMenuVisible(false)
+ return
+ }
+
+ const query = value.split(triggerKey).pop().toLowerCase()
+ const matchedOptions = (options[triggerKey] || []).filter(option =>
+ option.label.toLowerCase().startsWith(query),
+ )
+
+ setFilteredOptions(matchedOptions)
+ setMenuVisible(matchedOptions.length > 0)
+ setHighlightedIndex(0)
+ }, [value, triggerKey, options])
+
+ const handleKeyDown = e => {
+ if (menuVisible) {
+ if (e.key === 'ArrowDown') {
+ e.preventDefault()
+ setHighlightedIndex(prev => (prev + 1) % filteredOptions.length)
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault()
+ setHighlightedIndex(
+ prev => (prev - 1 + filteredOptions.length) % filteredOptions.length,
+ )
+ } else if (e.key === 'Tab' || e.key === 'Enter') {
+ e.preventDefault()
+ if (filteredOptions[highlightedIndex]) {
+ selectOption(filteredOptions[highlightedIndex])
+ }
+ } else if (e.key === 'Escape') {
+ setMenuVisible(false)
+ }
+ } else if (Object.keys(options).includes(e.key)) {
+ setTriggerKey(e.key)
+ }
+ }
+
+ const selectOption = option => {
+ const parts = value.split(triggerKey)
+ parts.pop()
+ onChange(parts.join(triggerKey) + triggerKey + option.label + ' ')
+ setMenuVisible(false)
+ setTriggerKey(null)
+ }
+
+ const handleClickOutside = event => {
+ if (
+ menuRef.current &&
+ !menuRef.current.contains(event.target) &&
+ ref.current &&
+ !ref.current.contains(event.target)
+ ) {
+ setMenuVisible(false)
+ }
+ }
+
+ useEffect(() => {
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside)
+ }
+ }, [])
+
+ return (
+
+
+ {menuVisible && (
+
+ {filteredOptions.map((option, index) => (
+
+ selectOption(option)}
+ >
+ {option.color && (
+
+ )}
+ {option.label}
+
+
+ ))}
+
+ )}
+
+ )
+}
+
+export default AutocompleteInput
diff --git a/src/views/components/CalendarView.jsx b/src/views/components/CalendarView.jsx
index 9a27811..154b723 100644
--- a/src/views/components/CalendarView.jsx
+++ b/src/views/components/CalendarView.jsx
@@ -21,15 +21,11 @@ const CalendarView = ({ chores }) => {
const tileContent = ({ date, view }) => {
if (view === 'month') {
- const dayChores = chores.filter(chore => {
- // Validate chore.nextDueDate before using it
- if (!chore.nextDueDate) return false
-
- const choreDate = new Date(chore.nextDueDate)
- if (isNaN(choreDate)) return false // Check if the date is invalid
- if (!date) return false
- return choreDate.toLocaleDateString() === date.toLocaleDateString()
- })
+ const dayChores = chores.filter(
+ chore =>
+ new Date(chore.nextDueDate)?.toISOString().split('T')[0] ===
+ date.toISOString().split('T')[0],
+ )
return (
@@ -70,7 +66,7 @@ const CalendarView = ({ chores }) => {
{
- setSeletedDate(new Date(d.toLocaleDateString()))
+ setSeletedDate(new Date(d))
}}
/>
{!selectedDate && (
@@ -122,8 +118,8 @@ const CalendarView = ({ chores }) => {
{chores
.filter(
chore =>
- new Date(chore.nextDueDate)?.toLocaleDateString() ===
- selectedDate.toLocaleDateString(),
+ new Date(chore.nextDueDate)?.toISOString().split('T')[0] ===
+ selectedDate.toISOString().split('T')[0],
)
.map((chore, idx) => (
a.indexOf(v) === i,
+)
+
+export const parsePriority = inputSentence => {
+ let sentence = inputSentence.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,
+ highlight: terms
+ .map(term => {
+ const index = sentence.indexOf(term)
+ return {
+ text: term,
+ start: index,
+ end: index + term.length,
+ }
+ })
+ .filter(term => term.start !== -1),
+
+ cleanedSentence: terms.reduce(
+ (s, t) => s.replace(t, ''),
+ inputSentence,
+ ),
+ }
+ }
+ }
+ return { result: 0, cleanedSentence: inputSentence }
+}
+export const parseLabels = (inputSentence, userLabels) => {
+ 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,
+ highlight: currentLabels.map(label => {
+ const index = inputSentence
+ .toLowerCase()
+ .indexOf(`#${label.name.toLowerCase()}`)
+ return {
+ text: `#${label.name}`,
+ start: index,
+ end: index + label.name.length + 1,
+ }
+ }),
+
+ cleanedSentence: sentence,
+ }
+ }
+ return { result: null, cleanedSentence: sentence }
+}
+
+export const parseRepeatV2 = inputSentence => {
+ const sentence = inputSentence.toLowerCase()
+ 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|everyday)/i,
+ name: 'Every day',
+ },
+ {
+ frequencyType: 'daily:time',
+ regex: /every (morning|noon|afternoon|evening|night)/i,
+ name: 'Every {time} daily',
+ },
+ {
+ 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}',
+ },
+ {
+ 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,
+ highlight: [
+ {
+ text: pattern.name,
+ start: inputSentence.indexOf(match[0]),
+ end: inputSentence.indexOf(match[0]) + match[0].length,
+ },
+ ],
+ cleanedSentence: inputSentence.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),
+ highlight: [
+ {
+ text: pattern.name,
+ start: inputSentence.indexOf(match[0]),
+ end: inputSentence.indexOf(match[0]) + match[0].length,
+ },
+ ],
+ cleanedSentence: inputSentence.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: inputSentence }
+ return {
+ result,
+ name: pattern.name.replace(
+ '{days}',
+ result.frequencyMetadata.days.join(', '),
+ ),
+ highlight: [
+ {
+ text: pattern.name,
+ start: inputSentence.indexOf(match[0]),
+ end: inputSentence.indexOf(match[0]) + match[0].length,
+ },
+ ],
+ cleanedSentence: inputSentence.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(', ')),
+ highlight: [
+ {
+ text: pattern.name,
+ start: inputSentence.indexOf(match[0]),
+ end: inputSentence.indexOf(match[0]) + match[0].length,
+ },
+ ],
+ cleanedSentence: inputSentence.replace(match[0], '').trim(),
+ }
+ case 'interval:2week':
+ result.frequency = 2
+ result.frequencyMetadata.unit = 'weeks'
+ result.frequencyType = 'interval'
+ return {
+ result,
+ name: pattern.name,
+ highlight: [
+ {
+ text: pattern.name,
+ start: inputSentence.indexOf(match[0]),
+ end: inputSentence.indexOf(match[0]) + match[0].length,
+ },
+ ],
+ cleanedSentence: inputSentence.replace(match[0], '').trim(),
+ }
+ case 'daily:time':
+ result.frequency = 1
+ result.frequencyMetadata.unit = 'days'
+ result.frequencyType = 'daily'
+ return {
+ result,
+ name: pattern.name.replace('{time}', match[1]),
+ // replace every x with ''
+ highlight: [
+ {
+ text: pattern.name,
+ start: inputSentence.indexOf(match[0]),
+ end: inputSentence.indexOf(match[0]) + match[0].length,
+ },
+ ],
+ cleanedSentence: inputSentence.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(', ')),
+ highlight: [
+ {
+ text: pattern.name,
+ start: inputSentence.indexOf(match[0]),
+ end: inputSentence.indexOf(match[0]) + match[0].length,
+ },
+ ],
+ 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),
+ highlight: [
+ {
+ text: pattern.name,
+ start: inputSentence.indexOf(match[0]),
+ end: inputSentence.indexOf(match[0]) + match[0].length,
+ },
+ ],
+ cleanedSentence: inputSentence.replace(match[0], '').trim(),
+ }
+ }
+ }
+ return {
+ result: null,
+ name: null,
+ highlight: [],
+ cleanedSentence: inputSentence,
+ }
+}
+
+export const parseAssignees = (inputSentence, users) => {
+ const sentence = inputSentence.toLowerCase()
+ const result = []
+ const highlight = []
+ console.log('users:', users)
+
+ for (const user of users) {
+ if (sentence.includes(`@${user.username.toLowerCase()}`)) {
+ result.push(user)
+ const index = inputSentence
+ .toLowerCase()
+ .indexOf(`@${user.username.toLowerCase()}`)
+ highlight.push({
+ text: `@${user.username}`,
+ start: index,
+ end: index + user.username.length + 1,
+ })
+ }
+ }
+
+ if (result.length > 0) {
+ return {
+ result,
+ highlight,
+ cleanedSentence: sentence.replace(
+ new RegExp(`@(${users.map(u => u.username).join('|')})`, 'g'),
+ '',
+ ),
+ }
+ }
+ return { result: null, cleanedSentence: sentence }
+}
diff --git a/src/views/components/ErrorSnackbar.jsx b/src/views/components/ErrorSnackbar.jsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/views/components/LearnMore.jsx b/src/views/components/LearnMore.jsx
index 9145059..31ecd9a 100644
--- a/src/views/components/LearnMore.jsx
+++ b/src/views/components/LearnMore.jsx
@@ -1,5 +1,5 @@
import { Info } from '@mui/icons-material'
-import { Box, Button, Sheet } from '@mui/joy'
+import { Box, IconButton, Sheet } from '@mui/joy'
import React, { useRef, useState } from 'react'
const LearnMoreButton = ({ content }) => {
@@ -29,16 +29,15 @@ const LearnMoreButton = ({ content }) => {
return (
- }
size='sm'
color='primary'
onClick={handleToggle}
>
- Learn More
-
+
+
{open && (