enhance user input validation and parsing for usernames, points, and labels

This commit is contained in:
Mo Tarbin
2025-10-02 21:45:25 -04:00
parent 68f09125d4
commit 409c5a0ba3
9 changed files with 395 additions and 136 deletions

View File

@@ -731,7 +731,7 @@ const NotificationTemplate = ({
setShowSaveDefault(false)
}}
>
Save Preference
Remember for Future Tasks
</Button>
</Box>
)}

View File

@@ -259,6 +259,7 @@ export const useChoresHistory = (initialLimit, includeMembers) => {
const resp = await GetChoresHistory(limit, includeMembers)
return resp?.res || []
},
staleTime: 0,
})
const handleLimitChange = newLimit => {

View File

@@ -1,5 +1,4 @@
// create boilerplate for ResetPasswordView:
import Logo from '../../Logo'
import {
Box,
Button,
@@ -12,6 +11,7 @@ import {
} from '@mui/joy'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import Logo from '../../Logo'
import { useNotification } from '../../service/NotificationProvider'
import { ResetPassword } from '../../utils/Fetcher'
@@ -77,10 +77,7 @@ const ForgotPasswordView = () => {
}
return (
<Container
component='main'
maxWidth='xs'
>
<Container component='main' maxWidth='xs'>
<Box
sx={{
marginTop: 4,
@@ -110,17 +107,28 @@ const ForgotPasswordView = () => {
</Typography>
{resetStatusOk === null && (
<>
<Typography level='body2' sx={{ textAlign: 'center', mt: 2, mb: 3 }}>
<Typography level='body2' sx={{ mb: 3 }}>
Enter your email, and we'll send you a link to get into your
account.
</Typography>
<FormControl error={emailError !== null} sx={{ width: '100%', mb: 2 }}>
<Typography level='body2' alignSelf={'start'} mb={1}>
Email Address
</Typography>
<FormControl
error={emailError !== null}
sx={{ width: '100%', mb: 2 }}
>
<Input
placeholder='Email'
type='email'
variant='soft'
margin='normal'
required
fullWidth
size='lg'
id='email'
placeholder='Enter your email address'
type='email'
name='email'
autoComplete='email'
autoFocus
value={email}
onChange={handleEmailChange}
error={emailError !== null}
@@ -135,21 +143,30 @@ const ForgotPasswordView = () => {
</FormControl>
<Button
variant='solid'
size='lg'
type='submit'
fullWidth
sx={{ mb: 2 }}
size='lg'
variant='solid'
sx={{
width: '100%',
mt: 3,
mb: 2,
border: 'moccasin',
borderRadius: '8px',
}}
onClick={handleSubmit}
>
Reset Password
</Button>
<Button
type='submit'
fullWidth
size='lg'
variant='plain'
sx={{
width: '100%',
mb: 2,
border: 'moccasin',
borderRadius: '8px',
}}
@@ -164,12 +181,15 @@ const ForgotPasswordView = () => {
)}
{resetStatusOk != null && (
<>
<Typography level='body-md' sx={{ textAlign: 'center', mt: 2, mb: 3 }}>
<Typography
level='body-md'
sx={{ textAlign: 'center', mt: 2, mb: 3 }}
>
If there is an account associated with the email you entered,
you will receive an email with instructions on how to reset
your password.
you will receive an email with instructions on how to reset your
password.
</Typography>
<Button
variant='solid'
size='lg'

View File

@@ -85,10 +85,10 @@ const SignupView = () => {
isValid = false
}
// username should only contain letters , numbers , dot and dash:
if (!/^[a-zA-Z0-9.-]+$/.test(username)) {
// username should only contain lowercase letters, dot and dash:
if (!/^[a-z.-]+$/.test(username)) {
setUsernameError(
'Username can only contain letters, numbers, dot and dash',
'Username can only contain lowercase letters, dot and dash',
)
isValid = false
}

View File

@@ -29,17 +29,14 @@ import {
useArchiveChore,
useChore,
useCreateChore,
useDeleteChores,
useUnArchiveChore,
useUpdateChore,
} from '../../queries/ChoreQueries.jsx'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
import { useNotification } from '../../service/NotificationProvider'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import {
DeleteChore,
GetAllCircleMembers,
GetThings,
} from '../../utils/Fetcher'
import { GetAllCircleMembers, GetThings } from '../../utils/Fetcher'
import { isPlusAccount } from '../../utils/Helpers'
import Priorities from '../../utils/Priorities.jsx'
import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
@@ -117,6 +114,7 @@ const ChoreEdit = () => {
const createChoreMutation = useCreateChore()
const archiveChore = useArchiveChore()
const unarchiveChore = useUnArchiveChore()
const deleteChores = useDeleteChores()
const {
data: choreData,
isLoading: isChoreLoading,
@@ -432,12 +430,16 @@ const ChoreEdit = () => {
message: 'Are you sure you want to delete this chore?',
onClose: isConfirmed => {
if (isConfirmed === true) {
DeleteChore(choreId).then(response => {
if (response.status === 200) {
deleteChores.mutate([choreId], {
onSuccess: () => {
Navigate('/chores')
} else {
alert('Failed to delete chore')
}
},
onError: error => {
showError({
title: 'Delete Failed',
message: `Failed to delete chore: ${error.message}`,
})
},
})
}
setConfirmModelConfig({})
@@ -727,7 +729,7 @@ const ChoreEdit = () => {
setShowSaveAssigneeDefault(false)
}}
>
Save Assignee Preference
Remember for Future Tasks
</Button>
</Box>
)}
@@ -1229,7 +1231,7 @@ const ChoreEdit = () => {
setShowSavePrivacyDefault(false)
}}
>
Save Preference
Remember for Future Tasks
</Button>
</Box>
)}

View File

@@ -6,7 +6,7 @@ import {
Input,
Typography,
} from '@mui/joy'
import React, { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
@@ -30,9 +30,9 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
newErrors.childName = 'Sub account name must be at least 2 characters'
} else if (childName.length > 20) {
newErrors.childName = 'Sub account name must be less than 20 characters'
} else if (!/^[a-zA-Z0-9_]+$/.test(childName)) {
} else if (!/^[a-z.-]+$/.test(childName)) {
newErrors.childName =
'Sub account name can only contain letters, numbers, and underscores'
'Sub account name can only contain lowercase letters, dot and dash'
}
}
@@ -133,9 +133,6 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
{errors.childName && (
<FormHelperText>{errors.childName}</FormHelperText>
)}
<FormHelperText>
This will create a username like: primaryname_subaccountname
</FormHelperText>
</FormControl>
<FormControl error={!!errors.displayName} sx={{ mb: 2 }}>

View File

@@ -882,10 +882,7 @@ const UserActivites = () => {
)
}}
renderValue={() => {
if (
selectedUser === undefined ||
selectedUser === 'all'
) {
if (selectedUser === undefined || selectedUser === 'all') {
return (
<Typography
startDecorator={
@@ -917,9 +914,8 @@ const UserActivites = () => {
}
>
{
circleUsers.find(
user => user.userId === selectedUser,
)?.displayName
circleUsers.find(user => user.userId === selectedUser)
?.displayName
}
</Typography>
)
@@ -1048,7 +1044,7 @@ const UserActivites = () => {
</Box>
{/* Conditional Content Based on Data Availability */}
{(!choresData.res?.length > 0 || !choresHistory?.length > 0) ? (
{!choresData.res?.length > 0 || !choresHistory?.length > 0 ? (
<Container
maxWidth='md'
sx={{
@@ -1167,7 +1163,11 @@ const UserActivites = () => {
<Typography level='h4' textAlign='center' sx={{ mb: 1 }}>
{chartData[selectedChart].title}
</Typography>
<Typography level='body-xs' textAlign='center' sx={{ mb: 2 }}>
<Typography
level='body-xs'
textAlign='center'
sx={{ mb: 2 }}
>
{chartData[selectedChart].description}
</Typography>
<Box

View File

@@ -13,6 +13,7 @@ import {
parseAssignees,
parseDueDate,
parseLabels,
parsePoints,
parsePriority,
parseRepeatV2,
} from './CustomParsers'
@@ -68,6 +69,8 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
})
const [frequencyHumanReadable, setFrequencyHumanReadable] = useState(null)
const [subTasks, setSubTasks] = useState(null)
const [points, setPoints] = useState(-1)
const [isAnyoneTask, setIsAnyoneTask] = useState(false)
const [hasDescription, setHasDescription] = useState(false)
const [hasSubTasks, setHasSubTasks] = useState(false)
const [hasNotifications, setHasNotifications] = useState(false)
@@ -170,6 +173,8 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
priorityHighlight,
labelsHighlight,
dueDateHighlight,
pointsHighlight,
assigneesHighlight,
) => {
const parts = []
let lastIndex = 0
@@ -179,24 +184,34 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
const allHighlights = []
if (repeatHighlight) {
repeatHighlight.forEach(h =>
allHighlights.push({ ...h, type: 'repeat', priority: 40 }),
allHighlights.push({ ...h, type: 'repeat', priority: 60 }),
)
}
if (priorityHighlight) {
priorityHighlight.forEach(h =>
allHighlights.push({ ...h, type: 'priority', priority: 30 }),
allHighlights.push({ ...h, type: 'priority', priority: 50 }),
)
}
if (pointsHighlight) {
pointsHighlight.forEach(h =>
allHighlights.push({ ...h, type: 'points', priority: 45 }),
)
}
if (assigneesHighlight) {
assigneesHighlight.forEach(h =>
allHighlights.push({ ...h, type: 'assignee', priority: 40 }),
)
}
if (labelsHighlight) {
labelsHighlight.forEach(h =>
allHighlights.push({ ...h, type: 'label', priority: 20 }),
allHighlights.push({ ...h, type: 'label', priority: 30 }),
)
}
if (dueDateHighlight) {
allHighlights.push({
...dueDateHighlight,
type: 'dueDate',
priority: 10,
priority: 20,
})
}
@@ -234,6 +249,12 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
case 'priority':
className = 'highlight-priority'
break
case 'points':
className = 'highlight-points'
break
case 'assignee':
className = 'highlight-assignee'
break
case 'label':
className = 'highlight-label'
break
@@ -286,22 +307,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
const processText = useCallback(
sentence => {
let cleanedSentence = sentence
// Parse everything from the original sentence to get correct highlight positions
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 pointsParsed = parsePoints(sentence)
const labels = parseLabels(sentence, userLabels || [])
const repeat = parseRepeatV2(sentence)
if (repeat.result) {
setFrequency(repeat.result)
setFrequencyHumanReadable(repeat.name)
cleanedSentence = repeat.cleanedSentence
}
// Parse assignees using circle members
const circleMembersList = circleMembers?.res || []
const assigneesForParsing = circleMembersList.map(member => ({
@@ -315,28 +325,44 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
}))
const assigneesResult = parseAssignees(sentence, assigneesForParsing)
if (assigneesResult.result) {
cleanedSentence = assigneesResult.cleanedSentence
console.log('CLEANED', cleanedSentence)
setAssignees(
assigneesResult.result.map(assignee => ({
userId: assignee.userId,
})),
)
} else {
setAssignees([
{
userId: userProfile.id,
},
])
}
// Parse due date
const repeat = parseRepeatV2(sentence)
const dueDateParsed = parseDueDate(sentence, chrono)
// Set all the parsed values
if (priority.result) setPriority(priority.result)
if (pointsParsed.result) setPoints(pointsParsed.result)
if (labels.result) setLabelsV2(labels.result)
if (assigneesResult.isAnyone) {
// @Anyone was used - set empty assignees (anyone can do the task)
setIsAnyoneTask(true)
setAssignees([])
} else if (assigneesResult.result && assigneesResult.result.length > 0) {
setIsAnyoneTask(false)
const parsedAssignees = assigneesResult.result.map(assignee => ({
userId: assignee.userId,
}))
setAssignees(parsedAssignees)
} else {
// Only assign to current user if no @ mentions found and userProfile exists
setIsAnyoneTask(false)
if (userProfile?.id) {
setAssignees([
{
userId: userProfile.id,
},
])
}
}
if (repeat.result) {
setFrequency(repeat.result)
setFrequencyHumanReadable(repeat.name)
}
let dueDateHighlight = null
if (dueDateParsed.result) {
setDueDate(moment(dueDateParsed.result).format('YYYY-MM-DDTHH:mm:ss'))
cleanedSentence = dueDateParsed.cleanedSentence
dueDateHighlight = dueDateParsed.highlight[0]
}
@@ -351,24 +377,69 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
}
}
// Create the cleaned sentence by sequentially applying all cleanups
let cleanedSentence = sentence
if (priority.result) cleanedSentence = priority.cleanedSentence
if (pointsParsed.result) {
// Apply points cleaning to the current cleaned sentence
const pointsReparse = parsePoints(cleanedSentence)
if (pointsReparse.result)
cleanedSentence = pointsReparse.cleanedSentence
}
if (labels.result) {
// Apply labels cleaning to the current cleaned sentence
const labelsReparse = parseLabels(cleanedSentence, userLabels || [])
if (labelsReparse.result)
cleanedSentence = labelsReparse.cleanedSentence
}
if (assigneesResult.result) {
// Apply assignees cleaning to the current cleaned sentence
const assigneesReparse = parseAssignees(
cleanedSentence,
assigneesForParsing,
)
if (assigneesReparse.result)
cleanedSentence = assigneesReparse.cleanedSentence
}
if (repeat.result) {
// Apply repeat cleaning to the current cleaned sentence
const repeatReparse = parseRepeatV2(cleanedSentence)
if (repeatReparse.result)
cleanedSentence = repeatReparse.cleanedSentence
}
if (dueDateParsed.result) {
// Apply date cleaning to the current cleaned sentence
const dueDateReparse = parseDueDate(cleanedSentence, chrono)
if (dueDateReparse.result)
cleanedSentence = dueDateReparse.cleanedSentence
}
setTaskText(sentence)
setTaskTitle(cleanedSentence.trim())
const { parts, plainText } = renderHighlightedSentence(
// Generate highlights for rendering using original sentence positions
const { parts } = renderHighlightedSentence(
sentence,
repeat.highlight,
priority.highlight,
labels.highlight,
dueDateHighlight,
pointsParsed.highlight,
assigneesResult.highlight,
)
setRenderedParts(parts)
setTaskTitle(plainText)
},
[userLabels, renderHighlightedSentence],
[userLabels, renderHighlightedSentence, circleMembers, userProfile],
)
useEffect(() => {
if (!isModalOpen || userLabelsLoading || isCircleMembersLoading) {
if (
!isModalOpen ||
userLabelsLoading ||
isCircleMembersLoading ||
!userProfile
) {
return
}
@@ -378,6 +449,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
userLabelsLoading,
isCircleMembersLoading,
isModalOpen,
userProfile,
processText,
])
@@ -393,6 +465,8 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
setFrequency(null)
setFrequencyHumanReadable(null)
setPriority(0)
setPoints(-1)
setIsAnyoneTask(false)
setHasDescription(false)
setDescription(null)
setSubTasks(null)
@@ -402,17 +476,41 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
}
const createChore = () => {
// Handle different assignee scenarios
let finalAssignees = assignees
let finalAssignedTo = null
let finalAssignStrategy = 'random'
if (isAnyoneTask) {
// @Anyone was explicitly used - anyone can do the task
finalAssignees = []
finalAssignedTo = null
finalAssignStrategy = 'no_assignee'
} else if (assignees.length === 0) {
// No assignees and no @Anyone - fallback to current user
finalAssignees = [{ userId: userProfile?.id }]
finalAssignedTo = userProfile?.id
finalAssignStrategy = 'keep_last_assigned'
} else if (assignees.length === 1) {
// Single assignee
finalAssignedTo = assignees[0].userId
finalAssignStrategy = 'keep_last_assigned'
} else {
// Multiple assignees
finalAssignedTo = null
finalAssignStrategy = 'random'
}
const chore = {
name: taskTitle,
assignees:
assignees.length > 0 ? assignees : [{ userId: userProfile.id }],
assignees: finalAssignees,
dueDate: dueDate ? new Date(dueDate).toISOString() : null,
assignedTo: assignees.length > 0 ? assignees[0].userId : userProfile.id,
assignStrategy: 'random',
assignedTo: finalAssignedTo,
assignStrategy: finalAssignStrategy,
isRolling: false,
labelsV2: labelsV2,
priority: priority ? Number(priority) : 0,
points: points > -1 ? points : null,
status: 0,
frequencyType: 'once',
frequencyMetadata: {},
@@ -552,7 +650,22 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
'@': {
value: 'userId',
display: 'displayName',
options: circleMembers?.res || [],
options: [
{ userId: 'anyone', displayName: 'Anyone' },
...(circleMembers?.res || []),
],
},
'*': {
value: 'id',
display: 'name',
options: [
{ id: '1', name: '1 point' },
{ id: '5', name: '5 points' },
{ id: '10', name: '10 points' },
{ id: '25', name: '25 points' },
{ id: '50', name: '50 points' },
{ id: '100', name: '100 points' },
],
},
}}
/>

View File

@@ -85,35 +85,63 @@ export const parsePriority = inputSentence => {
export const parseLabels = (inputSentence, userLabels) => {
let sentence = inputSentence.toLowerCase()
const currentLabels = []
// label will always be prefixed #:
const newLabels = []
const allHighlights = []
for (const label of userLabels) {
if (sentence.includes(`#${label.name.toLowerCase()}`)) {
currentLabels.push(label)
sentence = sentence.replace(`#${label.name.toLowerCase()}`, '')
// Find all #label patterns in the sentence
const labelPattern = /#(\w+)/gi
const matches = [...inputSentence.matchAll(labelPattern)]
for (const match of matches) {
const labelName = match[1]
const fullMatch = match[0]
const startIndex = match.index
// Check if this label already exists
const existingLabel = userLabels.find(
label => label.name.toLowerCase() === labelName.toLowerCase(),
)
if (existingLabel) {
currentLabels.push(existingLabel)
} else {
// Create a new label object for new labels
newLabels.push({
name: labelName,
color: '#3b82f6', // Default blue color
isNew: true,
})
}
allHighlights.push({
text: fullMatch,
start: startIndex,
end: startIndex + fullMatch.length,
})
// Remove the label from the sentence
sentence = sentence.replace(fullMatch.toLowerCase(), '')
}
if (currentLabels.length > 0) {
const allLabels = [...currentLabels, ...newLabels]
if (allLabels.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.replace(
new RegExp(`#(${userLabels.map(l => l.name).join('|')})`, 'g'),
'',
),
result: allLabels,
newLabels: newLabels,
highlight: allHighlights,
cleanedSentence: inputSentence
.replace(labelPattern, '')
.replace(/\s+/g, ' ')
.trim(),
}
}
return { result: null, cleanedSentence: sentence }
return {
result: null,
newLabels: [],
cleanedSentence: inputSentence,
}
}
export const parseRepeatV2 = inputSentence => {
@@ -498,37 +526,135 @@ export const parseAssignees = (inputSentence, users) => {
const sentence = inputSentence.toLowerCase()
const result = []
const highlight = []
// sort users by the longest so we remove first the full match:
for (const user of users.sort(
(a, b) => b.displayName.length - a.displayName.length,
)) {
if (sentence.includes(`@${user.displayName.toLowerCase()}`)) {
const matchedTexts = []
// Check for @Anyone first (special case)
const anyoneRegex = /@anyone(?=\s|$)/i
const anyoneMatch = inputSentence.match(anyoneRegex)
if (anyoneMatch) {
const index = inputSentence.search(anyoneRegex)
highlight.push({
text: anyoneMatch[0],
start: index,
end: index + anyoneMatch[0].length,
})
matchedTexts.push({ pattern: '@anyone', original: anyoneMatch[0] })
// For @Anyone, return empty result (no specific assignees)
let cleanedSentence = inputSentence
for (const matchedText of matchedTexts) {
cleanedSentence = cleanedSentence.replace(
new RegExp(
matchedText.original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
'gi',
),
'',
)
}
return {
result: [], // Empty assignees for @Anyone
isAnyone: true, // Flag to indicate @Anyone was used
highlight,
cleanedSentence: cleanedSentence.replace(/\s+/g, ' ').trim(),
}
}
// Sort users by longest displayName first to avoid partial matches
const sortedUsers = users.sort(
(a, b) => (b.displayName?.length || 0) - (a.displayName?.length || 0),
)
for (const user of sortedUsers) {
if (!user.displayName) continue
// Only match on display name - use word boundaries for exact matching
const displayNamePattern = `@${user.displayName.toLowerCase()}`
// Use word boundary or space/end to ensure exact match, not partial
const exactMatchRegex = new RegExp(
`@${user.displayName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=\\s|$)`,
'i',
)
const exactMatch = inputSentence.match(exactMatchRegex)
if (exactMatch && !result.some(r => r.userId === user.userId)) {
result.push(user)
const index = inputSentence
.toLowerCase()
.indexOf(`@${user.displayName.toLowerCase()}`)
const index = inputSentence.search(exactMatchRegex)
highlight.push({
text: `@${user.displayName}`,
text: exactMatch[0],
start: index,
end: index + user.displayName.length + 1,
end: index + exactMatch[0].length,
})
matchedTexts.push({
pattern: displayNamePattern,
original: exactMatch[0],
})
}
}
if (result.length > 0) {
return {
result,
highlight,
cleanedSentence: sentence.replace(
let cleanedSentence = inputSentence
// Remove all matched assignee patterns using the original matched text
for (const matchedText of matchedTexts) {
cleanedSentence = cleanedSentence.replace(
new RegExp(
`@(${result.map(u => u.displayName.toLowerCase()).join('|')})`,
'g',
matchedText.original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
'gi',
),
'',
),
)
}
return {
result,
isAnyone: false,
highlight,
cleanedSentence: cleanedSentence.replace(/\s+/g, ' ').trim(),
}
}
return { result: null, cleanedSentence: sentence }
return { result: null, isAnyone: false, cleanedSentence: inputSentence }
}
export const parsePoints = inputSentence => {
let sentence = inputSentence.toLowerCase()
const pointsPattern = /\*(\d+)\s*(?:points?)?/gi
const match = sentence.match(pointsPattern)
if (!match) {
return {
result: null,
highlight: [],
cleanedSentence: inputSentence,
}
}
// Extract the first points match
const pointsMatch = match[0]
const pointsValue = parseInt(pointsMatch.replace(/\D/g, ''), 10)
const startIndex = inputSentence
.toLowerCase()
.indexOf(pointsMatch.toLowerCase())
return {
result: pointsValue,
highlight: [
{
text: pointsMatch,
start: startIndex,
end: startIndex + pointsMatch.length,
},
],
cleanedSentence: inputSentence
.replace(
new RegExp(pointsMatch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'),
'',
)
.replace(/\s+/g, ' ')
.trim(),
}
}
export const parseDueDate = (inputSentence, chrono) => {