Add SmartTaskInput with Custom Renderer.
Change to use `@tanstack/react-query` Add Global ErrorProvider Add limited Support for offline version Fix issue with calendar display +1 day
This commit is contained in:
165
package-lock.json
generated
165
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "donetick",
|
||||
"version": "0.1.98",
|
||||
"version": "0.1.99",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "donetick",
|
||||
"version": "0.1.98",
|
||||
"version": "0.1.99",
|
||||
"dependencies": {
|
||||
"@capacitor-community/sqlite": "^6.0.2",
|
||||
"@capacitor/android": "^6.1.1",
|
||||
@@ -42,7 +42,6 @@
|
||||
"react": "^18.2.0",
|
||||
"react-calendar": "^5.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-query": "^3.39.3",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"reactjs-social-login": "^2.6.3",
|
||||
@@ -5228,81 +5227,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/broadcast-channel": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
|
||||
"integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"detect-node": "^2.1.0",
|
||||
"js-sha3": "0.8.0",
|
||||
"microseconds": "0.2.0",
|
||||
"nano-time": "1.0.0",
|
||||
"oblivious-set": "1.0.0",
|
||||
"rimraf": "3.0.2",
|
||||
"unload": "2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/broadcast-channel/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/broadcast-channel/node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/broadcast-channel/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/broadcast-channel/node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/browser-fs-access": {
|
||||
"version": "0.35.0",
|
||||
"resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz",
|
||||
@@ -6566,12 +6490,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
@@ -9341,12 +9259,6 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-sha3": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
|
||||
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -9807,16 +9719,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/match-sorter": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz",
|
||||
"integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.8",
|
||||
"remove-accents": "0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -10054,12 +9956,6 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/microseconds": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
|
||||
"integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
||||
@@ -10226,15 +10122,6 @@
|
||||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nano-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
|
||||
"integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"big-integer": "^1.6.16"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
@@ -10542,12 +10429,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/oblivious-set": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
|
||||
"integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -11409,32 +11290,6 @@
|
||||
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-query": {
|
||||
"version": "3.39.3",
|
||||
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
|
||||
"integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"broadcast-channel": "^3.4.1",
|
||||
"match-sorter": "^6.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.28.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.1.tgz",
|
||||
@@ -11882,12 +11737,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/remove-accents": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
|
||||
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/replace": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/replace/-/replace-1.2.2.tgz",
|
||||
@@ -13855,16 +13704,6 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unload": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
|
||||
"integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.6.2",
|
||||
"detect-node": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/untildify": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
||||
|
||||
13
src/App.jsx
13
src/App.jsx
@@ -1,13 +1,14 @@
|
||||
import NavBar from '@/views/components/NavBar'
|
||||
import { Button, Snackbar, Typography, useColorScheme } from '@mui/joy'
|
||||
import Tracker from '@openreplay/tracker'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
import { registerCapacitorListeners } from './CapacitorListener'
|
||||
import { UserContext } from './contexts/UserContext'
|
||||
import { AuthenticationProvider } from './service/AuthenticationService'
|
||||
import { ErrorProvider } from './service/ErrorProvider'
|
||||
import { GetUserProfile } from './utils/Fetcher'
|
||||
import { apiManager, isTokenValid } from './utils/TokenManager'
|
||||
import NetworkBanner from './views/components/NetworkBanner'
|
||||
@@ -91,10 +92,12 @@ function App() {
|
||||
<NetworkBanner />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthenticationProvider />
|
||||
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
||||
<NavBar />
|
||||
<Outlet />
|
||||
</UserContext.Provider>
|
||||
<ErrorProvider>
|
||||
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
||||
<NavBar />
|
||||
<Outlet />
|
||||
</UserContext.Provider>
|
||||
</ErrorProvider>
|
||||
{needRefresh && (
|
||||
<Snackbar open={showUpdateSnackbar}>
|
||||
<Typography level='body-md'>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Network } from '@capacitor/network'
|
||||
// import { localStore } from '../utils/LocalStore'
|
||||
import { localStore } from '../utils/LocalStore'
|
||||
import { syncManager } from '../utils/SyncManager.jsx' // Ensure you import syncManager if needed for syncing
|
||||
class NetworkManager {
|
||||
constructor() {
|
||||
@@ -23,29 +23,33 @@ class NetworkManager {
|
||||
this.isOnline = status.connected
|
||||
}
|
||||
})
|
||||
const syncQueue = () => {
|
||||
localStore
|
||||
.syncQueuedRequests()
|
||||
.then(hasMessages => {
|
||||
console.log(
|
||||
'Queued requests synced successfully. Queue has messaage is: ',
|
||||
hasMessages,
|
||||
)
|
||||
if (hasMessages) {
|
||||
this.notifyBackendSync()
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error syncing queued requests:', error)
|
||||
})
|
||||
}
|
||||
this.registerNetworkListener(async isOnline => {
|
||||
if (isOnline && this.isNetworkOn) {
|
||||
// TODO: Delete when Sync manager Implemented
|
||||
// localStore
|
||||
// .syncQueuedRequests()
|
||||
// .then(hasMessages => {
|
||||
// console.log(
|
||||
// 'Queued requests synced successfully. Queue has messaage is: ',
|
||||
// hasMessages,
|
||||
// )
|
||||
// if (hasMessages) {
|
||||
// this.notifyBackendSync()
|
||||
// }
|
||||
// })
|
||||
// .catch(error => {
|
||||
// console.error('Error syncing queued requests:', error)
|
||||
// })
|
||||
syncQueue()
|
||||
console.log('NetworkManager: Network is back online. with SYNCMANAGER')
|
||||
|
||||
await syncManager.syncTasks()
|
||||
console.log('Finished syncing queued requests.')
|
||||
}
|
||||
})
|
||||
syncQueue()
|
||||
}
|
||||
|
||||
setOffline() {
|
||||
|
||||
@@ -67,19 +67,22 @@ export const useCreateChore = () => {
|
||||
{ ...newTask, id: tempId, tempId }, // Use the tempId for offline tracking
|
||||
]
|
||||
await localStore.saveToCache('offlineTasks', updateOfflineTasks) // Save to local storage
|
||||
|
||||
// force useChores to refetch:
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
// Force the chores query to refetch
|
||||
queryClient.refetchQueries(['chores'])
|
||||
// Update the chores query cache immediately
|
||||
queryClient.setQueryData(['chores'], oldData => {
|
||||
console.log('ATTEMPT TO SAVE OFFLINE TASKS:', updateOfflineTasks)
|
||||
// queryClient.setQueryData(['chores'], oldData => {
|
||||
// console.log('ATTEMPT TO SAVE OFFLINE TASKS:', updateOfflineTasks)
|
||||
|
||||
if (!oldData)
|
||||
return {
|
||||
res: [{ ...newTask, id: tempId, tempId }],
|
||||
} // If no data, return offline tasks
|
||||
return {
|
||||
res: [...oldData.res, { ...newTask, id: tempId, tempId }],
|
||||
}
|
||||
})
|
||||
// if (!oldData)
|
||||
// return {
|
||||
// res: [{ ...newTask, id: tempId, tempId }],
|
||||
// } // If no data, return offline tasks
|
||||
// return {
|
||||
// res: [...oldData.res, { ...newTask, id: tempId, tempId }],
|
||||
// }
|
||||
// })
|
||||
return { tempId }
|
||||
}
|
||||
return { tempId: null }
|
||||
@@ -135,11 +138,11 @@ export const useUpdateChore = () => {
|
||||
throw new Error('Failed to save chore')
|
||||
}
|
||||
const updatedChoreRes = await resp.json()
|
||||
if (!updatedChoreRes || !updatedChoreRes.res) {
|
||||
if (!updatedChoreRes) {
|
||||
throw new Error('Failed to get updated chore data')
|
||||
}
|
||||
// Successfully updated the chore on the server, return the updated chore
|
||||
return updatedChoreRes.res
|
||||
return updatedChoreRes?.res || updatedChoreRes
|
||||
}
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
@@ -158,9 +161,13 @@ export const useUpdateChore = () => {
|
||||
export const useChoresHistory = (initialLimit, includeMembers) => {
|
||||
const [limit, setLimit] = useState(initialLimit) // Initially, no limit is selected
|
||||
|
||||
const { data, error, isLoading } = useQuery(['choresHistory', limit], () =>
|
||||
GetChoresHistory(limit, includeMembers),
|
||||
)
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ['choresHistory', limit],
|
||||
queryFn: async () => {
|
||||
const resp = await GetChoresHistory(limit, includeMembers)
|
||||
return resp?.res || []
|
||||
},
|
||||
})
|
||||
|
||||
const handleLimitChange = newLimit => {
|
||||
setLimit(newLimit)
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { useQuery } from 'react-query'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { GetResource } from '../utils/Fetcher'
|
||||
|
||||
export const useResource = () => {
|
||||
const { data, isLoading, error } = useQuery('resource', GetResource, {
|
||||
cacheTime: 1000 * 60 * 10, // 1 minute
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['resource'],
|
||||
queryFn: async () => {
|
||||
const response = await GetResource()
|
||||
return response.json()
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
})
|
||||
return { data, isLoading, error }
|
||||
}
|
||||
|
||||
94
src/queries/SubtaskQueries.jsx
Normal file
94
src/queries/SubtaskQueries.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { networkManager } from '../hooks/NetworkManager'
|
||||
import { CompleteSubTask, SaveChore } from '../utils/Fetcher'
|
||||
import { localStore } from '../utils/LocalStore'
|
||||
|
||||
export const useUpdate = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async updatedChore => {
|
||||
if (!networkManager.isOnline) {
|
||||
updatedChore['updatedAt'] = new Date().toISOString()
|
||||
if (!updatedChore['nextDueDate']) {
|
||||
updatedChore['nextDueDate'] = updatedChore['dueDate']
|
||||
}
|
||||
const offlineTasks =
|
||||
(await localStore.getFromCache('offlineTasks')) || []
|
||||
|
||||
for (const task of offlineTasks) {
|
||||
// Find the task with the same id or tempId and update it
|
||||
if (task.id === updatedChore.id || task.tempId === updatedChore.id) {
|
||||
// Update the task in local storage
|
||||
const updatedTask = { ...task, ...updatedChore }
|
||||
const updatedOfflineTasks = offlineTasks.map(t =>
|
||||
t.id === task.id ? updatedTask : t,
|
||||
)
|
||||
await localStore.saveToCache('offlineTasks', updatedOfflineTasks)
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(updatedTask)
|
||||
})
|
||||
}
|
||||
}
|
||||
const newTaskId = crypto.randomUUID()
|
||||
const updatedChoreWithNewId = {
|
||||
...updatedChore,
|
||||
tempId: newTaskId,
|
||||
}
|
||||
|
||||
await localStore.saveToCache('offlineTasks', [
|
||||
...offlineTasks,
|
||||
updatedChoreWithNewId,
|
||||
])
|
||||
return new Promise((resolve, reject) => {
|
||||
// Resolve with the updated task
|
||||
resolve(updatedChoreWithNewId)
|
||||
})
|
||||
} else {
|
||||
// Call the API to update the chore
|
||||
const resp = await SaveChore(updatedChore)
|
||||
if (!resp || !resp.ok) {
|
||||
throw new Error('Failed to save chore')
|
||||
}
|
||||
const updatedChoreRes = await resp.json()
|
||||
if (!updatedChoreRes) {
|
||||
throw new Error('Failed to get updated chore data')
|
||||
}
|
||||
// Successfully updated the chore on the server, return the updated chore
|
||||
return updatedChoreRes?.res || updatedChoreRes
|
||||
}
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate the chores query to refresh the data
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
},
|
||||
onMutate: async updatedChore => {
|
||||
if (!networkManager.isOnline) {
|
||||
// Handle offline case here if needed
|
||||
return
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useCompleteSubTask = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (subTaskId, choreId, completedAt) => {
|
||||
if (!networkManager.isOnline) {
|
||||
throw new Error('Cannot complete subtask while offline')
|
||||
}
|
||||
const resp = await CompleteSubTask(subTaskId, choreId, completedAt)
|
||||
if (!resp || !resp.ok) {
|
||||
throw new Error('Failed to complete subtask')
|
||||
}
|
||||
const result = await resp.json()
|
||||
if (!result || !result.res) {
|
||||
throw new Error('Failed to get completed subtask data')
|
||||
}
|
||||
return result.res
|
||||
},
|
||||
onSuccess: (data, variables) => {},
|
||||
})
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { GetAllCircleMembers, GetAllUsers } from '../utils/Fetcher'
|
||||
|
||||
export const useAllUsers = () => {
|
||||
return useQuery('allUsers', GetAllUsers)
|
||||
return useQuery({
|
||||
queryKey: ['allUsers'],
|
||||
queryFn: GetAllUsers,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCircleMembers = () => {
|
||||
const [refetchKey, setRefetchKey] = useState(0)
|
||||
|
||||
const { data, error, isLoading, refetch } = useQuery(
|
||||
['allCircleMembers', refetchKey],
|
||||
GetAllCircleMembers,
|
||||
)
|
||||
const { data, error, isLoading, refetch } = useQuery({
|
||||
queryKey: ['allCircleMembers', refetchKey],
|
||||
queryFn: GetAllCircleMembers,
|
||||
})
|
||||
const handleRefetch = () => {
|
||||
setRefetchKey(prevKey => prevKey + 1)
|
||||
refetch()
|
||||
|
||||
51
src/service/ErrorProvider.jsx
Normal file
51
src/service/ErrorProvider.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Error } from '@mui/icons-material'
|
||||
import { Box, Button, Snackbar, Typography } from '@mui/joy'
|
||||
import React, { createContext, useContext, useState } from 'react'
|
||||
|
||||
const ErrorContext = createContext()
|
||||
|
||||
export const useError = () => useContext(ErrorContext)
|
||||
|
||||
export const ErrorProvider = ({ children }) => {
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const showError = error => {
|
||||
setError(error)
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorContext.Provider value={{ showError }}>
|
||||
{children}
|
||||
<Snackbar
|
||||
open={Boolean(error)}
|
||||
autoHideDuration={6000}
|
||||
onClose={() => setError(null)}
|
||||
startDecorator={<Error color='danger' />}
|
||||
endDecorator={
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='danger'
|
||||
onClick={() => setError(null)}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{typeof error === 'string' ? (
|
||||
<Typography color='danger' level='body-md'>
|
||||
{error}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box>
|
||||
<Typography color='danger' level='title-sm'>
|
||||
{error?.title}
|
||||
</Typography>
|
||||
<Typography color='danger' level='body-sm'>
|
||||
{error?.message}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Snackbar>
|
||||
</ErrorContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CapacitorSQLite } from '@capacitor-community/sqlite'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
|
||||
const CACHE_TABLE = 'offline_cache'
|
||||
const QUEUE_TABLE = 'offline_request_queue'
|
||||
@@ -8,7 +7,8 @@ const OFFLINE_TASK = 'offlineTasks' // For storing offline tasks
|
||||
class LocalStore {
|
||||
constructor() {
|
||||
this.db = null
|
||||
this.useLocalStorage = !Capacitor.isNativePlatform()
|
||||
// this.useLocalStorage = !Capacitor.isNativePlatform()
|
||||
this.useLocalStorage = true // default to localStorage for now.
|
||||
}
|
||||
|
||||
async initDatabase() {
|
||||
@@ -153,10 +153,15 @@ class LocalStore {
|
||||
)
|
||||
}
|
||||
|
||||
async queueRequest(requestId, requestBody) {
|
||||
async queueRequest(requestId, requestPayload) {
|
||||
if (this.useLocalStorage) {
|
||||
const queue = JSON.parse(localStorage.getItem(QUEUE_TABLE)) || []
|
||||
queue.push({ requestId, requestBody })
|
||||
console.log('requestPayload', requestPayload)
|
||||
|
||||
if (typeof requestPayload?.options?.body['id'] === 'string') {
|
||||
requestPayload['id'] = null
|
||||
}
|
||||
queue.push({ requestId, requestBody: requestPayload })
|
||||
localStorage.setItem(QUEUE_TABLE, JSON.stringify(queue))
|
||||
return
|
||||
}
|
||||
@@ -167,11 +172,13 @@ class LocalStore {
|
||||
INSERT INTO ${QUEUE_TABLE} (url, requestBody)
|
||||
VALUES (?, ?);
|
||||
`,
|
||||
[requestId, JSON.stringify(requestBody)],
|
||||
[requestId, JSON.stringify(requestPayload)],
|
||||
)
|
||||
}
|
||||
|
||||
async syncQueuedRequests() {
|
||||
console.log('Syncing queued requests...')
|
||||
|
||||
var queueSize = 0
|
||||
if (this.useLocalStorage) {
|
||||
const queue = JSON.parse(localStorage.getItem(QUEUE_TABLE)) || []
|
||||
|
||||
@@ -137,7 +137,11 @@ async function handleOfflineRequest(url, options) {
|
||||
const requestId = murmurhash.v3(JSON.stringify({ url, options }))
|
||||
await localStore.queueRequest(requestId, { url, options })
|
||||
console.log('Request queued for later processing:', requestId)
|
||||
return Promise.reject(new Error('Offline and request queued: ' + requestId))
|
||||
return Promise.reject({
|
||||
error: 'Offline and request queued',
|
||||
requestId,
|
||||
queued: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
async function attemptFetchFromCache(url, options) {
|
||||
@@ -152,6 +156,7 @@ async function attemptFetchFromCache(url, options) {
|
||||
json: async () => cachedData,
|
||||
})
|
||||
} else {
|
||||
// TODO: change this to throw error instead of returning promise
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
'No cached data found for URL: ' +
|
||||
|
||||
@@ -386,7 +386,7 @@ const ChoreEdit = () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
if (isChoreLoading && choreId) {
|
||||
if ((isChoreLoading && choreId) || isUserLabelsLoading) {
|
||||
return <LoadingComponent />
|
||||
}
|
||||
return (
|
||||
@@ -573,7 +573,6 @@ const ChoreEdit = () => {
|
||||
}}
|
||||
defaultChecked={dueDate !== null}
|
||||
checked={dueDate !== null}
|
||||
value={dueDate !== null}
|
||||
overlay
|
||||
label='Give this task a due date'
|
||||
/>
|
||||
@@ -711,7 +710,6 @@ const ChoreEdit = () => {
|
||||
}}
|
||||
defaultChecked={isNotificable}
|
||||
checked={isNotificable}
|
||||
value={isNotificable}
|
||||
disabled={!isPlusAccount(userProfile)}
|
||||
overlay
|
||||
label='Notify for this task'
|
||||
@@ -967,7 +965,6 @@ const ChoreEdit = () => {
|
||||
}}
|
||||
overlay
|
||||
checked={subTasks != null}
|
||||
value={subTasks != null}
|
||||
label='Sub Tasks'
|
||||
/>
|
||||
<FormHelperText>Add sub tasks to this task</FormHelperText>
|
||||
@@ -997,7 +994,6 @@ const ChoreEdit = () => {
|
||||
}
|
||||
}}
|
||||
checked={points > -1}
|
||||
value={points > -1}
|
||||
overlay
|
||||
label='Assign Points'
|
||||
/>
|
||||
|
||||
@@ -243,6 +243,7 @@ const ChoreView = () => {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
@@ -260,21 +261,31 @@ const ChoreView = () => {
|
||||
? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}`
|
||||
: 'N/A'}
|
||||
</Chip>
|
||||
{chore?.labelsV2?.map((label, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
ml: index === 0 ? 0 : 0.5,
|
||||
top: 2,
|
||||
zIndex: 1,
|
||||
backgroundColor: label?.color,
|
||||
color: getTextColorFromBackgroundColor(label?.color),
|
||||
}}
|
||||
>
|
||||
{label?.name}
|
||||
</Chip>
|
||||
))}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{chore?.labelsV2?.map((label, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
ml: index === 0 ? 0 : 0.5,
|
||||
top: 2,
|
||||
zIndex: 1,
|
||||
backgroundColor: label?.color,
|
||||
color: getTextColorFromBackgroundColor(label?.color),
|
||||
}}
|
||||
>
|
||||
{label?.name}
|
||||
</Chip>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
|
||||
@@ -44,6 +44,7 @@ import moment from 'moment'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { useError } from '../../service/ErrorProvider'
|
||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
import {
|
||||
@@ -91,6 +92,7 @@ const ChoreCard = ({
|
||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
||||
const [timeoutId, setTimeoutId] = React.useState(null)
|
||||
const { userProfile } = React.useContext(UserContext)
|
||||
const { showError } = useError()
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleMenuOutsideClick)
|
||||
return () => {
|
||||
@@ -176,6 +178,7 @@ const ChoreCard = ({
|
||||
|
||||
if (seconds <= 0) {
|
||||
clearInterval(countdownInterval) // Stop the countdown when it reaches 0
|
||||
setIsPendingCompletion(false) // Reset the state
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
@@ -195,6 +198,25 @@ const ChoreCard = ({
|
||||
setTimeoutId(null)
|
||||
setSecondsLeftToCancel(null)
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.queued) {
|
||||
showError({
|
||||
title: 'Update Failed',
|
||||
message: 'Request will be reattempt when you are online',
|
||||
})
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to update',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
|
||||
setIsPendingCompletion(false)
|
||||
clearTimeout(id)
|
||||
clearInterval(countdownInterval) // Ensure to clear this interval as well
|
||||
setTimeoutId(null)
|
||||
setSecondsLeftToCancel(null)
|
||||
})
|
||||
}, 2000)
|
||||
|
||||
setTimeoutId(id)
|
||||
@@ -676,15 +698,30 @@ const ChoreCard = ({
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
SkipChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'skipped')
|
||||
handleMenuClose()
|
||||
})
|
||||
}
|
||||
})
|
||||
SkipChore(chore.id)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'skipped')
|
||||
handleMenuClose()
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.queued) {
|
||||
showError({
|
||||
title: 'Failed to update',
|
||||
message:
|
||||
'Request will be processed when you are online',
|
||||
})
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to update',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SwitchAccessShortcut />
|
||||
|
||||
35
src/views/Chores/CompactCard.jsx
Normal file
35
src/views/Chores/CompactCard.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Box, Checkbox, Typography } from '@mui/joy'
|
||||
|
||||
export const CompactCard = ({
|
||||
chore,
|
||||
performers,
|
||||
onChoreUpdate,
|
||||
onChoreRemove,
|
||||
userLabels,
|
||||
sx,
|
||||
viewOnly,
|
||||
onChipClick,
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
// checked={!!task.completedAt}
|
||||
// onChange={() => handleToggle(task.id)}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
textDecoration: chore.completedAt ? 'line-through' : 'none',
|
||||
}}
|
||||
>
|
||||
{chore.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
Add,
|
||||
Bolt,
|
||||
CancelRounded,
|
||||
EditCalendar,
|
||||
ExpandCircleDown,
|
||||
Grain,
|
||||
PriorityHigh,
|
||||
Search,
|
||||
Sort,
|
||||
Style,
|
||||
Unarchive,
|
||||
@@ -63,11 +63,11 @@ const MyChores = () => {
|
||||
const [filteredChores, setFilteredChores] = useState([])
|
||||
const [searchFilter, setSearchFilter] = useState('All')
|
||||
const [choreSections, setChoreSections] = useState([])
|
||||
const [activeTextField, setActiveTextField] = useState(
|
||||
localStorage.getItem('activeTextField') || 'task',
|
||||
)
|
||||
|
||||
const [showSearchFilter, setShowSearchFilter] = useState(false)
|
||||
const [addTaskModalOpen, setAddTaskModalOpen] = useState(false)
|
||||
const [taskInputFocus, setTaskInputFocus] = useState(0)
|
||||
const searchInputRef = useRef()
|
||||
const searchInputRef = useRef(null)
|
||||
const [searchInputFocus, setSearchInputFocus] = useState(0)
|
||||
const [selectedChoreSection, setSelectedChoreSection] = useState(
|
||||
localStorage.getItem('selectedChoreSection') || 'due_date',
|
||||
@@ -84,7 +84,11 @@ const MyChores = () => {
|
||||
const menuRef = useRef(null)
|
||||
const Navigate = useNavigate()
|
||||
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
||||
const { data: choresData, isLoading: choresLoading } = useChores()
|
||||
const {
|
||||
data: choresData,
|
||||
isLoading: choresLoading,
|
||||
refetch: refetchChores,
|
||||
} = useChores()
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([GetChores(), GetAllUsers(), GetUserProfile()]).then(
|
||||
@@ -173,6 +177,21 @@ const MyChores = () => {
|
||||
searchInputRef.current.selectionEnd = searchInputRef.current.value?.length
|
||||
}
|
||||
}, [searchInputFocus])
|
||||
|
||||
// add listern to Control/Command + K to focus on search input
|
||||
useEffect(() => {
|
||||
const handleKeyDown = event => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
|
||||
event.preventDefault()
|
||||
setAddTaskModalOpen(true)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [])
|
||||
const setSelectedChoreSectionWithCache = value => {
|
||||
setSelectedChoreSection(value)
|
||||
localStorage.setItem('selectedChoreSection', value)
|
||||
@@ -185,10 +204,6 @@ const MyChores = () => {
|
||||
setSelectedChoreFilter(value)
|
||||
localStorage.setItem('selectedChoreFilter', value)
|
||||
}
|
||||
const setActiveTextFieldWithCache = value => {
|
||||
setActiveTextField(value)
|
||||
localStorage.setItem('activeTextField', value)
|
||||
}
|
||||
|
||||
const updateChores = newChore => {
|
||||
const newChores = chores
|
||||
@@ -381,60 +396,36 @@ const MyChores = () => {
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
{activeTextField == 'task' && (
|
||||
<TaskInput
|
||||
autoFocus={taskInputFocus}
|
||||
onChoreUpdate={updateChores}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
placeholder='Search'
|
||||
value={searchTerm}
|
||||
onFocus={() => {
|
||||
setShowSearchFilter(true)
|
||||
}}
|
||||
fullWidth
|
||||
sx={{
|
||||
mt: 1,
|
||||
mb: 1,
|
||||
borderRadius: 24,
|
||||
height: 24,
|
||||
borderColor: 'text.disabled',
|
||||
padding: 1,
|
||||
}}
|
||||
onChange={handleSearchChange}
|
||||
endDecorator={
|
||||
searchTerm && (
|
||||
<CancelRounded
|
||||
onClick={() => {
|
||||
setSearchTerm('')
|
||||
setFilteredChores(chores)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{activeTextField == 'search' && (
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
autoFocus={searchInputFocus > 0}
|
||||
placeholder='Search'
|
||||
value={searchTerm}
|
||||
fullWidth
|
||||
sx={{
|
||||
mt: 1,
|
||||
mb: 1,
|
||||
borderRadius: 24,
|
||||
height: 24,
|
||||
borderColor: 'text.disabled',
|
||||
padding: 1,
|
||||
}}
|
||||
onChange={handleSearchChange}
|
||||
endDecorator={
|
||||
searchTerm && (
|
||||
<CancelRounded
|
||||
onClick={() => {
|
||||
setSearchTerm('')
|
||||
setFilteredChores(chores)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{activeTextField != 'task' && (
|
||||
<IconButton
|
||||
variant='outlined'
|
||||
size='sm'
|
||||
color='neutral'
|
||||
sx={{
|
||||
height: 24,
|
||||
borderRadius: 24,
|
||||
// minWidth: 100,
|
||||
}}
|
||||
onClick={() => {
|
||||
setActiveTextFieldWithCache('task')
|
||||
setTaskInputFocus(taskInputFocus + 1)
|
||||
}}
|
||||
>
|
||||
<EditCalendar />
|
||||
</IconButton>
|
||||
)}
|
||||
{activeTextField != 'search' && (
|
||||
{/* {activeTextField != 'search' && (
|
||||
<IconButton
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
@@ -452,7 +443,7 @@ const MyChores = () => {
|
||||
>
|
||||
<Search />
|
||||
</IconButton>
|
||||
)}
|
||||
)} */}
|
||||
<SortAndGrouping
|
||||
title='Group by'
|
||||
k={'icon-menu-group-by'}
|
||||
@@ -496,7 +487,7 @@ const MyChores = () => {
|
||||
mouseClickHandler={handleMenuOutsideClick}
|
||||
/>
|
||||
</Box>
|
||||
{activeTextField === 'search' && (
|
||||
{showSearchFilter && (
|
||||
<div className='flex gap-4'>
|
||||
<div className='grid flex-1 grid-cols-3 gap-4'>
|
||||
<IconButtonWithMenu
|
||||
@@ -597,6 +588,23 @@ const MyChores = () => {
|
||||
</Menu>
|
||||
</List>
|
||||
</div>
|
||||
<IconButton
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
sx={{
|
||||
height: 24,
|
||||
borderRadius: 24,
|
||||
}}
|
||||
onClick={() => {
|
||||
setShowSearchFilter(false)
|
||||
setSearchTerm('')
|
||||
setFilteredChores(chores)
|
||||
setSearchFilter('All')
|
||||
}}
|
||||
>
|
||||
<CancelRounded />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
{searchFilter !== 'All' && (
|
||||
@@ -811,7 +819,7 @@ const MyChores = () => {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 2,
|
||||
'z-index': 1000,
|
||||
'z-index': 100,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
@@ -821,6 +829,7 @@ const MyChores = () => {
|
||||
borderRadius: '50%',
|
||||
width: 50,
|
||||
height: 50,
|
||||
zIndex: 101,
|
||||
}}
|
||||
onClick={() => {
|
||||
Navigate(`/chores/create`)
|
||||
@@ -828,6 +837,27 @@ const MyChores = () => {
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color='primary'
|
||||
variant='soft'
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
width: 25,
|
||||
height: 25,
|
||||
position: 'relative',
|
||||
left: -25,
|
||||
top: 22,
|
||||
}}
|
||||
onClick={() => {
|
||||
setAddTaskModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<Bolt
|
||||
style={{
|
||||
rotate: '20deg',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Snackbar
|
||||
open={isSnackbarOpen}
|
||||
@@ -843,6 +873,19 @@ const MyChores = () => {
|
||||
<Typography level='title-md'>{snackBarMessage}</Typography>
|
||||
</Snackbar>
|
||||
<NotificationAccessSnackbar />
|
||||
{addTaskModalOpen && (
|
||||
<TaskInput
|
||||
autoFocus={taskInputFocus}
|
||||
onChoreUpdate={updateChores}
|
||||
isModalOpen={addTaskModalOpen}
|
||||
onClose={forceRefresh => {
|
||||
setAddTaskModalOpen(false)
|
||||
if (forceRefresh) {
|
||||
refetchChores()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
<Sidepanel chores={chores} performers={performers} />
|
||||
|
||||
@@ -26,9 +26,7 @@ import {
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { API_URL } from '../Config'
|
||||
import { GetAllUsers, GetChores, MarkChoreComplete } from '../utils/Fetcher'
|
||||
import { Fetch } from '../utils/TokenManager'
|
||||
import DateModal from './Modals/Inputs/DateModal'
|
||||
// import moment from 'moment'
|
||||
|
||||
@@ -263,21 +261,22 @@ const ChoresOverview = () => {
|
||||
size='sm'
|
||||
// sx={{ borderRadius: '50%' }}
|
||||
onClick={() => {
|
||||
MarkChoreComplete(chore.id,null,null,null)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
const newChores = [...chores]
|
||||
const index = newChores.findIndex(
|
||||
c => c.id === chore.id,
|
||||
)
|
||||
newChores[index] = newChore
|
||||
setChores(newChores)
|
||||
setFilteredChores(newChores)
|
||||
})
|
||||
}
|
||||
})
|
||||
MarkChoreComplete(chore.id, null, null, null).then(
|
||||
response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
const newChores = [...chores]
|
||||
const index = newChores.findIndex(
|
||||
c => c.id === chore.id,
|
||||
)
|
||||
newChores[index] = newChore
|
||||
setChores(newChores)
|
||||
setFilteredChores(newChores)
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
}}
|
||||
aria-setsize={2}
|
||||
>
|
||||
@@ -325,19 +324,20 @@ const ChoresOverview = () => {
|
||||
alert('Please select a performer')
|
||||
return
|
||||
}
|
||||
MarkChoreComplete(choreId, null, date, activeUserId)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
const newChores = [...chores]
|
||||
const index = newChores.findIndex(c => c.id === chore.id)
|
||||
newChores[index] = newChore
|
||||
setChores(newChores)
|
||||
setFilteredChores(newChores)
|
||||
})
|
||||
}
|
||||
})
|
||||
MarkChoreComplete(choreId, null, date, activeUserId).then(
|
||||
response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
const newChores = [...chores]
|
||||
const index = newChores.findIndex(c => c.id === chore.id)
|
||||
newChores[index] = newChore
|
||||
setChores(newChores)
|
||||
setFilteredChores(newChores)
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { useQuery } from 'react-query'
|
||||
import { GetLabels } from '../../utils/Fetcher'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { CreateLabel, GetLabels } from '../../utils/Fetcher'
|
||||
|
||||
export const useLabels = () => {
|
||||
return useQuery('labels', GetLabels, {
|
||||
initialData: [],
|
||||
return useQuery({
|
||||
queryKey: ['labels'],
|
||||
queryFn: GetLabels,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateLabel = () => {
|
||||
return useQuery({
|
||||
queryKey: ['createLabel'],
|
||||
queryFn: CreateLabel,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,26 +11,25 @@ import {
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import LabelModal from '../Modals/Inputs/LabelModal'
|
||||
import { useLabels } from './LabelQueries'
|
||||
|
||||
// import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Add } from '@mui/icons-material'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors'
|
||||
import { DeleteLabel } from '../../utils/Fetcher'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import { useLabels } from './LabelQueries'
|
||||
|
||||
const LabelView = () => {
|
||||
const { data: labels, isLabelsLoading, isError } = useLabels()
|
||||
|
||||
const [userLabels, setUserLabels] = useState([labels])
|
||||
const [userLabels, setUserLabels] = useState([])
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
|
||||
const [currentLabel, setCurrentLabel] = useState(null)
|
||||
const queryClient = useQueryClient()
|
||||
const [confirmationModel, setConfirmationModel] = useState({})
|
||||
const Navigate = useNavigate()
|
||||
|
||||
const handleAddLabel = () => {
|
||||
setCurrentLabel(null)
|
||||
setModalOpen(true)
|
||||
@@ -53,7 +52,7 @@ const LabelView = () => {
|
||||
color: 'danger',
|
||||
cancelText: 'Cancel',
|
||||
onClose: confirmed => {
|
||||
if (confirmed) {
|
||||
if (confirmed === true) {
|
||||
handleDeleteLabel(id)
|
||||
}
|
||||
setConfirmationModel({})
|
||||
@@ -140,7 +139,7 @@ const LabelView = () => {
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='soft'
|
||||
onClick={() => handleDeleteLabel(label.id)}
|
||||
onClick={() => handleDeleteClicked(label.id)}
|
||||
color='danger'
|
||||
>
|
||||
<DeleteIcon />
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useError } from '../../../service/ErrorProvider.jsx'
|
||||
import LABEL_COLORS from '../../../utils/Colors.jsx'
|
||||
import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher'
|
||||
import { useLabels } from '../../Labels/LabelQueries'
|
||||
@@ -22,6 +23,7 @@ function LabelModal({ isOpen, onClose, label }) {
|
||||
const [error, setError] = useState('')
|
||||
const { data: userLabels = [] } = useLabels()
|
||||
const queryClient = useQueryClient()
|
||||
const { showError } = useError()
|
||||
|
||||
// Populate the form fields when editing
|
||||
useEffect(() => {
|
||||
@@ -57,26 +59,54 @@ function LabelModal({ isOpen, onClose, label }) {
|
||||
}
|
||||
|
||||
// Mutation for saving labels
|
||||
const saveLabelMutation = useMutation(
|
||||
newLabel =>
|
||||
label
|
||||
? UpdateLabel({ id: label.id, ...newLabel })
|
||||
: CreateLabel(newLabel),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries('labels')
|
||||
onClose()
|
||||
},
|
||||
onError: () => {
|
||||
setError('Failed to save label. Please try again.')
|
||||
},
|
||||
},
|
||||
)
|
||||
// const saveLabelMutation = useMutation(
|
||||
// newLabel =>
|
||||
// label
|
||||
// ? UpdateLabel({ id: label.id, ...newLabel })
|
||||
// : CreateLabel(newLabel),
|
||||
// {
|
||||
// onSuccess: () => {
|
||||
// queryClient.invalidateQueries('labels')
|
||||
// onClose()
|
||||
// },
|
||||
// onError: () => {
|
||||
// setError('Failed to save label. Please try again.')
|
||||
// },
|
||||
// },
|
||||
// )
|
||||
|
||||
const handleSave = () => {
|
||||
if (!validateLabel()) return
|
||||
|
||||
saveLabelMutation.mutate({ name: labelName, color })
|
||||
const saveLabel = label?.id && label.id !== -1 ? UpdateLabel : CreateLabel
|
||||
// ? { id: label.id, name: labelName, color }
|
||||
// : { name: labelName, color }
|
||||
// saveLabelMutation.mutate({ name: labelName, color })
|
||||
saveLabel({
|
||||
id: label?.id,
|
||||
name: labelName,
|
||||
color,
|
||||
})
|
||||
.then(res => {
|
||||
if (res?.error) {
|
||||
setError(res.error)
|
||||
} else {
|
||||
queryClient.invalidateQueries('labels')
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.queued) {
|
||||
showError({
|
||||
title: 'Failed to save label',
|
||||
message: 'Unable to save label. Please try again.',
|
||||
})
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to save label',
|
||||
message: 'Unable to save label. Please try again.',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -109,7 +139,7 @@ function LabelModal({ isOpen, onClose, label }) {
|
||||
<Typography
|
||||
startDecorator={
|
||||
<Box
|
||||
className='h-4 w-4'
|
||||
className='size-4'
|
||||
borderRadius={10}
|
||||
sx={{ background: selected.value }}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import Logo from '../../Logo'
|
||||
import {
|
||||
@@ -498,6 +499,46 @@ const Settings = () => {
|
||||
</Typography>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 py-4'>
|
||||
<Typography level='h3'>Experimental Features </Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
Clean up some part of the local storage and cache. Only use if you
|
||||
know are you doing.
|
||||
</Typography>
|
||||
<Button
|
||||
variant='soft'
|
||||
color='danger'
|
||||
onClick={() => {
|
||||
const confirmed = confirm(
|
||||
`Are you sure you want to clear your local storage and cache? This will remove all your data. on device and require login`,
|
||||
)
|
||||
if (confirmed) {
|
||||
localStorage.clear()
|
||||
Navigate('/login')
|
||||
}
|
||||
}}
|
||||
>
|
||||
Clear Local Storage and Cache
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='danger'
|
||||
onClick={() => {
|
||||
const confirmed = confirm(
|
||||
`Are you sure you want to clear your local storage and cache? This will remove all your data.`,
|
||||
)
|
||||
if (confirmed) {
|
||||
localStorage.removeItem('offline_cache')
|
||||
localStorage.removeItem('offline_request_queue')
|
||||
localStorage.removeItem('offlineTasks')
|
||||
}
|
||||
}}
|
||||
>
|
||||
Clear Offline Cache and Offline tasks
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
88
src/views/TestView/AutocompleteDropdown.jsx
Normal file
88
src/views/TestView/AutocompleteDropdown.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// AutocompleteDropdown.jsx
|
||||
import { Divider, Menu, MenuItem } from '@mui/joy'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
const AutocompleteDropdown = ({
|
||||
currentValue,
|
||||
suggestions,
|
||||
selectedIndex,
|
||||
onSelectSuggestion,
|
||||
onMouseEnterSuggestion, // Added for hover selection
|
||||
parentRefer, // Ref to the dropdown element
|
||||
}) => {
|
||||
// Scroll selected item into view
|
||||
const dropdownMenuRef = React.useRef(null)
|
||||
useEffect(() => {
|
||||
const selectedElement = dropdownMenuRef.current?.querySelector('.selected')
|
||||
if (selectedElement) {
|
||||
selectedElement.scrollIntoView({
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
})
|
||||
}
|
||||
}, [selectedIndex, dropdownMenuRef])
|
||||
|
||||
if (!suggestions || suggestions.options === 0) {
|
||||
return null // Don't render if no suggestions
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={Boolean(suggestions?.options?.length)}
|
||||
ref={dropdownMenuRef}
|
||||
anchorEl={parentRefer.current}
|
||||
onClose={() => {}}
|
||||
sx={{
|
||||
width: '300px',
|
||||
maxHeight: '160px',
|
||||
overflow: 'auto',
|
||||
position: 'relative',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
zIndex: 1300,
|
||||
}}
|
||||
>
|
||||
{suggestions?.options
|
||||
.filter(option => {
|
||||
if (typeof option === 'string') {
|
||||
return option.toLowerCase().includes(currentValue.toLowerCase())
|
||||
}
|
||||
return option[suggestions.display]
|
||||
.toLowerCase()
|
||||
.includes(currentValue.toLowerCase())
|
||||
})
|
||||
.map((option, index) => (
|
||||
<MenuItem
|
||||
key={suggestions.display ? option[suggestions.value] : option}
|
||||
selected={selectedIndex === index}
|
||||
onClick={() => onSelectSuggestion(option)}
|
||||
onMouseEnter={() => onMouseEnterSuggestion(index)} // Update selected index on hover
|
||||
className={selectedIndex === index ? 'selected' : ''} // Add class for selected item
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: selectedIndex === index ? 'gray.800' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{suggestions.display ? option[suggestions.display] : option}
|
||||
</MenuItem>
|
||||
))}
|
||||
<Divider orientation='horizontal' />
|
||||
{/*
|
||||
<MenuItem
|
||||
selected={selectedIndex === suggestions?.options?.length}
|
||||
onClick={() => {
|
||||
onSelectSuggestion(currentValue)
|
||||
}}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: 'gray.800',
|
||||
}}
|
||||
>
|
||||
<Add />
|
||||
Add new Label
|
||||
</MenuItem> */}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutocompleteDropdown
|
||||
@@ -1,152 +0,0 @@
|
||||
.task-editor-container {
|
||||
background-color: #2d2d2d;
|
||||
color: #ffffff;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
max-width: 500px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.smart-task-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: #ffffff;
|
||||
font-size: 1.2em;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.task-description-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: #ffffff;
|
||||
font-size: 1em;
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.task-attributes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.attribute-tag {
|
||||
background-color: #444;
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 0.9em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.remove-tag {
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.project-select select {
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
background-color: #444;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-buttons button {
|
||||
padding: 8px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: #666;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.add-task-button {
|
||||
background-color: #d9534f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.smart-task-input {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
/* color: transparent;
|
||||
caret-color: white;
|
||||
|
||||
|
||||
padding: 0px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.smart-task-display {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
/* top: 0;
|
||||
left: 0; */
|
||||
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
|
||||
padding: 0px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
|
||||
overflow-y: auto;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.smart-task-common {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.2em;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.highlight-date {
|
||||
color: #f08080;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.highlight-label {
|
||||
color: #add8e6;
|
||||
}
|
||||
|
||||
.highlight-priority {
|
||||
color: #ffb6c1;
|
||||
}
|
||||
|
||||
.task-input {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #2d2d2d;
|
||||
color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import './MentionTextfield.css' // We'll assume a CSS file for basic styling
|
||||
|
||||
// Helper function to render text with highlighting
|
||||
|
||||
const MentionTextfield = ({ initialTask = {}, onCancel, onAddTask }) => {
|
||||
// State to hold the task details
|
||||
const [taskTitle, setTaskTitle] = useState(initialTask.title || '')
|
||||
const [taskDescription, setTaskDescription] = useState(
|
||||
initialTask.description || '',
|
||||
)
|
||||
const [selectedDate, setSelectedDate] = useState(initialTask.date || null)
|
||||
const [selectedPriority, setSelectedPriority] = useState(
|
||||
initialTask.priority || null,
|
||||
)
|
||||
const [selectedLabels, setSelectedLabels] = useState(initialTask.labels || [])
|
||||
const [reminders, setReminders] = useState(initialTask.reminders || [])
|
||||
const [selectedProject, setSelectedProject] = useState(
|
||||
initialTask.project || 'Inbox',
|
||||
)
|
||||
|
||||
const [cursorPosition, setCursorPosition] = useState(0) // State to track cursor position
|
||||
const [showCursor, setShowCursor] = useState(false) // State to toggle cursor visibility
|
||||
|
||||
// Ref for the input field (if needed for more advanced handling)
|
||||
const titleInputRef = useRef(null)
|
||||
|
||||
// Effect to update bottom tags based on title parsing
|
||||
useEffect(() => {
|
||||
// Basic parsing logic
|
||||
const lowerCaseTitle = taskTitle.toLowerCase()
|
||||
const extractedDate = lowerCaseTitle.includes('tomorrow')
|
||||
? 'Tomorrow'
|
||||
: null
|
||||
|
||||
const labelMatches = taskTitle.match(/@(\w+)/g)
|
||||
const extractedLabels = labelMatches
|
||||
? labelMatches.map(match => match.substring(1))
|
||||
: []
|
||||
|
||||
const priorityMatch = taskTitle.match(/P(\d)/)
|
||||
const extractedPriority = priorityMatch ? `P${priorityMatch[1]}` : null
|
||||
|
||||
setSelectedDate(extractedDate)
|
||||
setSelectedLabels(extractedLabels)
|
||||
setSelectedPriority(extractedPriority)
|
||||
}, [taskTitle]) // Re-run effect when taskTitle changes
|
||||
|
||||
// Handlers for removing attributes (same as before)
|
||||
const removeDate = () => setSelectedDate(null) // Note: This won't remove from the input field text
|
||||
const removePriority = () => setSelectedPriority(null) // Note: This won't remove from the input field text
|
||||
const removeLabel = labelToRemove => {
|
||||
// Note: This won't remove from the input field text
|
||||
setSelectedLabels(selectedLabels.filter(label => label !== labelToRemove))
|
||||
}
|
||||
const removeReminder = reminderToRemove => {
|
||||
setReminders(reminders.filter(reminder => reminder !== reminderToRemove))
|
||||
}
|
||||
|
||||
const handleAddTask = () => {
|
||||
const newTask = {
|
||||
title: taskTitle,
|
||||
description: taskDescription,
|
||||
date: selectedDate, // These come from the parsing effect
|
||||
priority: selectedPriority, // These come from the parsing effect
|
||||
labels: selectedLabels, // These come from the parsing effect
|
||||
reminders: reminders,
|
||||
project: selectedProject,
|
||||
}
|
||||
onAddTask(newTask) // Call the provided add task function
|
||||
}
|
||||
const renderHighlightedText = (text, position) => {
|
||||
const parts = []
|
||||
let lastIndex = 0
|
||||
|
||||
// Regex to find patterns: Tomorrow, @label, P followed by a digit
|
||||
const regex = /(Tomorrow)|(@\w+)|(P\d)/gi
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const matchedText = match[0]
|
||||
const matchIndex = match.index
|
||||
|
||||
// Add text before the match
|
||||
if (matchIndex > lastIndex) {
|
||||
parts.push(text.substring(lastIndex, matchIndex))
|
||||
}
|
||||
|
||||
// Add the highlighted part
|
||||
let className = ''
|
||||
if (matchedText.toLowerCase() === 'tomorrow') {
|
||||
className = 'highlight-date'
|
||||
} else if (matchedText.startsWith('@')) {
|
||||
className = 'highlight-label'
|
||||
} else if (matchedText.startsWith('P')) {
|
||||
className = 'highlight-priority'
|
||||
}
|
||||
|
||||
parts.push(
|
||||
<span key={matchIndex} className={className}>
|
||||
{matchedText}
|
||||
</span>,
|
||||
)
|
||||
|
||||
lastIndex = regex.lastIndex
|
||||
}
|
||||
|
||||
// Add any remaining text after the last match
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(text.substring(lastIndex))
|
||||
}
|
||||
|
||||
return <div>{parts}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='task-editor-container'>
|
||||
<div className='task-input h-20 overflow-auto rounded border'>
|
||||
<textarea
|
||||
type='text'
|
||||
className='smart-task-input smart-task-common'
|
||||
rows={100}
|
||||
placeholder='Tomorrow do some task with @test_label and P3'
|
||||
value={taskTitle}
|
||||
onChange={e => setTaskTitle(e.target.value)}
|
||||
// onKeyUp={e => {
|
||||
// setCursorPosition(e.target.selectionStart)
|
||||
// }}
|
||||
// onKeyDown={e => {
|
||||
// setCursorPosition(e.target.selectionStart)
|
||||
// }}
|
||||
onBlur={() => setShowCursor(false)}
|
||||
onFocus={() => setShowCursor(true)}
|
||||
ref={titleInputRef}
|
||||
// style={{ opacity: 0, position: 'absolute', zIndex: -1 }}
|
||||
/>
|
||||
{/* Display Area with Highlighting */}
|
||||
<div
|
||||
className='smart-task-display smart-task-common'
|
||||
onClick={e => {
|
||||
const range = document.caretRangeFromPoint(e.clientX, e.clientY)
|
||||
if (range) {
|
||||
setCursorPosition(range.startOffset)
|
||||
|
||||
const offset = range.startOffset
|
||||
|
||||
titleInputRef.current.focus()
|
||||
titleInputRef.current.setSelectionRange(offset, offset)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{renderHighlightedText(taskTitle, cursorPosition)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Description Area */}
|
||||
<textarea
|
||||
className='task-description-input'
|
||||
placeholder='Description'
|
||||
value={taskDescription}
|
||||
onChange={e => setTaskDescription(e.target.value)}
|
||||
/>
|
||||
{/* Attributes/Tags Display (same as before, updated by useEffect) */}
|
||||
<div className='task-attributes'>
|
||||
{selectedDate && (
|
||||
<div className='attribute-tag'>
|
||||
🗓️ {selectedDate}{' '}
|
||||
<span className='remove-tag' onClick={removeDate}>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{selectedPriority && (
|
||||
<div className='attribute-tag'>
|
||||
🚩 {selectedPriority}{' '}
|
||||
<span className='remove-tag' onClick={removePriority}>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{reminders.map((reminder, index) => (
|
||||
<div key={index} className='attribute-tag'>
|
||||
⏰ {reminder}{' '}
|
||||
<span
|
||||
className='remove-tag'
|
||||
onClick={() => removeReminder(reminder)}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{selectedLabels.map((label, index) => (
|
||||
<div key={index} className='attribute-tag'>
|
||||
🏷️ {label}{' '}
|
||||
<span className='remove-tag' onClick={() => removeLabel(label)}>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{/* More options button - static for this example */}
|
||||
<div className='attribute-tag'>...</div>
|
||||
</div>
|
||||
{/* Project Selection and Action Buttons (same as before) */}
|
||||
<div className='task-actions'>
|
||||
<div className='project-select'>
|
||||
{/* In a real app, this would be a more robust dropdown */}
|
||||
<select
|
||||
value={selectedProject}
|
||||
onChange={e => setSelectedProject(e.target.value)}
|
||||
>
|
||||
<option value='Inbox'>📥 Inbox</option>
|
||||
{/* Add other project options here */}
|
||||
</select>
|
||||
</div>
|
||||
<div className='action-buttons'>
|
||||
<button className='cancel-button' onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className='add-task-button' onClick={handleAddTask}>
|
||||
Add task
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MentionTextfield
|
||||
42
src/views/TestView/SmartTaskTitleInput.css
Normal file
42
src/views/TestView/SmartTaskTitleInput.css
Normal file
@@ -0,0 +1,42 @@
|
||||
.smart-task-display {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
padding: 0px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
overflow-y: auto;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.smart-task-common {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.2em;
|
||||
font-family: inherit;
|
||||
caret-color: #f08080;
|
||||
}
|
||||
|
||||
.highlight-date {
|
||||
color: #f08080;
|
||||
}
|
||||
|
||||
.highlight-repeat {
|
||||
color: #90ee90;
|
||||
}
|
||||
|
||||
.highlight-label {
|
||||
color: #add8e6;
|
||||
}
|
||||
|
||||
.highlight-priority {
|
||||
color: #ffb6c1;
|
||||
}
|
||||
|
||||
.task-input {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
273
src/views/TestView/SmartTaskTitleInput.jsx
Normal file
273
src/views/TestView/SmartTaskTitleInput.jsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import { useColorScheme } from '@mui/joy'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import AutocompleteDropdown from './AutocompleteDropdown'
|
||||
import './SmartTaskTitleInput.css'
|
||||
const renderHighlightedText = (text, cursorPosition) => {
|
||||
const parts = []
|
||||
let lastIndex = 0
|
||||
|
||||
const regex = /(Tomorrow)|(#\w+)|(P\d)/gi
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const matchedText = match[0]
|
||||
const matchIndex = match.index
|
||||
|
||||
if (matchIndex > lastIndex) {
|
||||
parts.push(text.substring(lastIndex, matchIndex))
|
||||
}
|
||||
|
||||
let className = ''
|
||||
if (matchedText.toLowerCase() === 'tomorrow') {
|
||||
className = 'highlight-date'
|
||||
} else if (matchedText.startsWith('#')) {
|
||||
className = 'highlight-label'
|
||||
} else if (matchedText.startsWith('P')) {
|
||||
className = 'highlight-priority'
|
||||
}
|
||||
|
||||
parts.push(
|
||||
<span key={matchIndex} className={className}>
|
||||
{matchedText}
|
||||
</span>,
|
||||
)
|
||||
|
||||
lastIndex = regex.lastIndex
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(text.substring(lastIndex))
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
const SmartTaskTitleInput = ({
|
||||
value,
|
||||
placeholder,
|
||||
autoFocus,
|
||||
onChange,
|
||||
suggestions,
|
||||
onEnterPressed,
|
||||
customRenderer,
|
||||
}) => {
|
||||
const { mode, setMode } = useColorScheme()
|
||||
const titleInputRef = useRef(null)
|
||||
const [cursorPosition, setCursorPosition] = useState(value?.length)
|
||||
const dropdownRef = useRef(null)
|
||||
const [lastWord, setLastWord] = useState('')
|
||||
const [suggestionTrigger, setSuggestionTrigger] = useState('P')
|
||||
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setCursorPosition(prevPos => Math.min(prevPos, value.length))
|
||||
|
||||
if (
|
||||
titleInputRef.current &&
|
||||
document.activeElement === titleInputRef.current
|
||||
) {
|
||||
requestAnimationFrame(() => {
|
||||
titleInputRef.current.setSelectionRange(cursorPosition, cursorPosition)
|
||||
})
|
||||
}
|
||||
}, [value])
|
||||
useEffect(() => {
|
||||
// set focus on the input when the component is mounted:
|
||||
if (titleInputRef.current) {
|
||||
titleInputRef.current.focus()
|
||||
titleInputRef.current.setSelectionRange(cursorPosition, cursorPosition)
|
||||
}
|
||||
}, [])
|
||||
const handleSuggestionChange = text => {
|
||||
// if the last word start with '@' or '#' or 'P':
|
||||
const lastWord = text.split(' ').pop()
|
||||
if (
|
||||
lastWord.startsWith('@') ||
|
||||
lastWord.startsWith('#') ||
|
||||
lastWord.startsWith('P')
|
||||
) {
|
||||
setSuggestionTrigger(lastWord[0])
|
||||
// last word without the first character:
|
||||
setLastWord(lastWord.slice(1))
|
||||
|
||||
setShowSuggestions(true)
|
||||
} else {
|
||||
setShowSuggestions(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTextareaChange = e => {
|
||||
handleSuggestionChange(e.target.value)
|
||||
onChange(e.target.value)
|
||||
setCursorPosition(e.target.selectionStart)
|
||||
}
|
||||
|
||||
const handleTextareaKeyDown = e => {
|
||||
if (showSuggestions) {
|
||||
const currentSuggestions = suggestions[suggestionTrigger].options.filter(
|
||||
option => {
|
||||
if (typeof option === 'string') {
|
||||
return option.toLowerCase().includes(lastWord.toLowerCase())
|
||||
}
|
||||
return option[suggestions[suggestionTrigger].display]
|
||||
.toLowerCase()
|
||||
.includes(lastWord.toLowerCase())
|
||||
},
|
||||
)
|
||||
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
const newIndex =
|
||||
e.key === 'ArrowDown'
|
||||
? (selectedSuggestionIndex + 1) % currentSuggestions.length
|
||||
: (selectedSuggestionIndex - 1 + currentSuggestions.length) %
|
||||
currentSuggestions.length
|
||||
setSelectedSuggestionIndex(newIndex)
|
||||
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
const selectedSuggestion = currentSuggestions[selectedSuggestionIndex]
|
||||
const suggestionValue = suggestions[suggestionTrigger].display
|
||||
? selectedSuggestion[suggestions[suggestionTrigger].display]
|
||||
: selectedSuggestion
|
||||
|
||||
if (suggestionValue) {
|
||||
const newValue = `${value.slice(0, cursorPosition - lastWord.length)}${suggestionValue} ${value.slice(cursorPosition)}`
|
||||
onChange(newValue)
|
||||
titleInputRef.current.value = newValue
|
||||
|
||||
setShowSuggestions(false)
|
||||
|
||||
const newCursorPosition = cursorPosition + suggestionValue.length + 1
|
||||
titleInputRef.current.setSelectionRange(
|
||||
newCursorPosition,
|
||||
newCursorPosition,
|
||||
)
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
setShowSuggestions(false)
|
||||
}
|
||||
} else {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
if (onEnterPressed) {
|
||||
onEnterPressed(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentPos = e.target.selectionStart
|
||||
setCursorPosition(currentPos)
|
||||
}
|
||||
|
||||
const handleTextareaClick = e => {
|
||||
const currentPos = e.target.selectionStart
|
||||
setCursorPosition(currentPos)
|
||||
}
|
||||
|
||||
const handleDisplayClick = e => {
|
||||
const range = document.caretRangeFromPoint(e.clientX, e.clientY)
|
||||
if (range) {
|
||||
const offset = range.startOffset
|
||||
setCursorPosition(offset)
|
||||
|
||||
if (titleInputRef.current) {
|
||||
titleInputRef.current.focus()
|
||||
titleInputRef.current.setSelectionRange(offset, offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='task-input h-20 overflow-auto rounded border'>
|
||||
<textarea
|
||||
ref={titleInputRef}
|
||||
autoFocus={autoFocus}
|
||||
rows={1}
|
||||
value={value}
|
||||
onChange={handleTextareaChange}
|
||||
onKeyDown={handleTextareaKeyDown}
|
||||
onClick={handleTextareaClick}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
// opacity: 100,
|
||||
zIndex: 1,
|
||||
resize: 'none',
|
||||
overflow: 'hidden',
|
||||
padding: '0.5rem',
|
||||
boxSizing: 'border-box',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
backgroundColor: 'transparent',
|
||||
color: mode === 'dark' ? '#cbd5e1' : '#1a202c',
|
||||
caretColor: mode === 'dark' ? '#fff' : '#000',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='smart-task-display smart-task-common'
|
||||
ref={dropdownRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
minHeight: '100%',
|
||||
padding: '0.5rem',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
}}
|
||||
onClick={handleDisplayClick}
|
||||
>
|
||||
{placeholder && !value && (
|
||||
<span className='pointer-events-none text-gray-400'>
|
||||
{placeholder}
|
||||
</span>
|
||||
)}
|
||||
{customRenderer
|
||||
? customRenderer
|
||||
: renderHighlightedText(value, cursorPosition)}
|
||||
</div>
|
||||
</div>
|
||||
{showSuggestions && (
|
||||
<AutocompleteDropdown
|
||||
currentValue={lastWord}
|
||||
suggestions={suggestions[suggestionTrigger]}
|
||||
selectedIndex={selectedSuggestionIndex}
|
||||
onMouseEnterSuggestion={index => {
|
||||
setSelectedSuggestionIndex(index)
|
||||
}}
|
||||
onSelectSuggestion={suggestion => {
|
||||
const suggestionValue = suggestions[suggestionTrigger].display
|
||||
? suggestion[suggestions[suggestionTrigger].display]
|
||||
: suggestion
|
||||
const newValue = `${value.slice(0, cursorPosition)}${suggestionValue}${value.slice(cursorPosition)}`
|
||||
|
||||
onChange(newValue)
|
||||
titleInputRef?.current?.focus()
|
||||
|
||||
setCursorPosition(cursorPosition + suggestion.length)
|
||||
titleInputRef.current.value = newValue
|
||||
titleInputRef.current.setSelectionRange(
|
||||
cursorPosition + suggestionValue.length,
|
||||
cursorPosition + suggestionValue.length,
|
||||
)
|
||||
setShowSuggestions(false)
|
||||
}}
|
||||
parentRefer={dropdownRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SmartTaskTitleInput
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useError } from '../../service/ErrorProvider'
|
||||
import {
|
||||
CreateThing,
|
||||
DeleteThing,
|
||||
@@ -172,6 +173,7 @@ const ThingsView = () => {
|
||||
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||
const [snackbarColor, setSnackbarColor] = useState('success')
|
||||
const { showError } = useError()
|
||||
|
||||
useEffect(() => {
|
||||
// fetch things
|
||||
@@ -187,25 +189,39 @@ const ThingsView = () => {
|
||||
if (thing?.id) {
|
||||
saveFunc = SaveThing
|
||||
}
|
||||
saveFunc(thing).then(result => {
|
||||
result.json().then(data => {
|
||||
if (thing?.id) {
|
||||
const currentThings = [...things]
|
||||
const thingIndex = currentThings.findIndex(
|
||||
currentThing => currentThing.id === thing.id,
|
||||
)
|
||||
currentThings[thingIndex] = data.res
|
||||
setThings(currentThings)
|
||||
saveFunc(thing)
|
||||
.then(result => {
|
||||
result.json().then(data => {
|
||||
if (thing?.id) {
|
||||
const currentThings = [...things]
|
||||
const thingIndex = currentThings.findIndex(
|
||||
currentThing => currentThing.id === thing.id,
|
||||
)
|
||||
currentThings[thingIndex] = data.res
|
||||
setThings(currentThings)
|
||||
} else {
|
||||
const currentThings = [...things]
|
||||
currentThings.push(data.res)
|
||||
setThings(currentThings)
|
||||
}
|
||||
setSnackbarMessage('Thing saved successfully')
|
||||
setSnackbarColor('success')
|
||||
setIsSnackbarOpen(true)
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.queued) {
|
||||
showError({
|
||||
title: 'Unable to save thing',
|
||||
message: 'You are offline and the request has been queued',
|
||||
})
|
||||
} else {
|
||||
const currentThings = [...things]
|
||||
currentThings.push(data.res)
|
||||
setThings(currentThings)
|
||||
showError({
|
||||
title: 'Unable to save thing',
|
||||
message: 'An error occurred while saving the thing',
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
setSnackbarMessage('Thing saved successfully')
|
||||
setSnackbarColor('success')
|
||||
setIsSnackbarOpen(true)
|
||||
}
|
||||
const handleEditClick = thing => {
|
||||
setIsShowEditStateModal(true)
|
||||
@@ -220,21 +236,37 @@ const ThingsView = () => {
|
||||
message: 'Are you sure you want to delete this Thing?',
|
||||
onClose: isConfirmed => {
|
||||
if (isConfirmed === true) {
|
||||
DeleteThing(thing.id).then(response => {
|
||||
if (response.ok) {
|
||||
const currentThings = [...things]
|
||||
const thingIndex = currentThings.findIndex(
|
||||
currentThing => currentThing.id === thing.id,
|
||||
)
|
||||
currentThings.splice(thingIndex, 1)
|
||||
setThings(currentThings)
|
||||
} else if (response.status === 405) {
|
||||
setSnackbarMessage('Unable to delete thing with associated tasks')
|
||||
setSnackbarColor('danger')
|
||||
setIsSnackbarOpen(true)
|
||||
}
|
||||
// if method not allwo show snackbar:
|
||||
})
|
||||
DeleteThing(thing.id)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
const currentThings = [...things]
|
||||
const thingIndex = currentThings.findIndex(
|
||||
currentThing => currentThing.id === thing.id,
|
||||
)
|
||||
currentThings.splice(thingIndex, 1)
|
||||
setThings(currentThings)
|
||||
} else if (response.status === 405) {
|
||||
setSnackbarMessage(
|
||||
'Unable to delete thing with associated tasks',
|
||||
)
|
||||
setSnackbarColor('danger')
|
||||
setIsSnackbarOpen(true)
|
||||
}
|
||||
// if method not allwo show snackbar:
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.queued) {
|
||||
showError({
|
||||
title: 'Unable to delete thing',
|
||||
message: 'You are offline and the request has been queued',
|
||||
})
|
||||
} else {
|
||||
showError({
|
||||
title: 'Unable to delete thing',
|
||||
message: 'An error occurred while deleting the thing',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
setConfirmModelConfig({})
|
||||
},
|
||||
@@ -252,19 +284,32 @@ const ThingsView = () => {
|
||||
}
|
||||
}
|
||||
|
||||
UpdateThingState(thing).then(result => {
|
||||
result.json().then(data => {
|
||||
const currentThings = [...things]
|
||||
const thingIndex = currentThings.findIndex(
|
||||
currentThing => currentThing.id === thing.id,
|
||||
)
|
||||
currentThings[thingIndex] = data.res
|
||||
setThings(currentThings)
|
||||
UpdateThingState(thing)
|
||||
.then(result => {
|
||||
result.json().then(data => {
|
||||
const currentThings = [...things]
|
||||
const thingIndex = currentThings.findIndex(
|
||||
currentThing => currentThing.id === thing.id,
|
||||
)
|
||||
currentThings[thingIndex] = data.res
|
||||
setThings(currentThings)
|
||||
setSnackbarMessage('Thing state updated successfully')
|
||||
setIsSnackbarOpen(true)
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.queued) {
|
||||
showError({
|
||||
title: 'Unable to update thing state',
|
||||
message: 'You are offline and the request has been queued',
|
||||
})
|
||||
} else {
|
||||
showError({
|
||||
title: 'Unable to update thing state',
|
||||
message: 'An error occurred while updating the thing state',
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
setSnackbarMessage('Thing state updated successfully')
|
||||
setIsSnackbarOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||
import CircleIcon from '@mui/icons-material/Circle'
|
||||
import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts'
|
||||
|
||||
import { EventBusy, Style, Toll } from '@mui/icons-material'
|
||||
import { EventBusy, Toll } from '@mui/icons-material'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@@ -28,7 +28,6 @@ import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
|
||||
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
||||
import { ChoresGrouper } from '../../utils/Chores'
|
||||
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
||||
import IconButtonWithMenu from '../Chores/IconButtonWithMenu'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
|
||||
const groupByDate = history => {
|
||||
@@ -204,7 +203,7 @@ const UserActivites = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isChoresHistoryLoading && !isChoresLoading && choresHistory) {
|
||||
const enrichedHistory = choresHistory.res.map(item => {
|
||||
const enrichedHistory = choresHistory.map(item => {
|
||||
const chore = choresData.res.find(chore => chore.id === item.choreId)
|
||||
return {
|
||||
...item,
|
||||
@@ -235,7 +234,7 @@ const UserActivites = () => {
|
||||
var assignedToMe = 0
|
||||
var assignedToOthers = 0
|
||||
chores.forEach(chore => {
|
||||
if (chore.assignedTo === userProfile.id) {
|
||||
if (chore.assignedTo === userProfile?.id) {
|
||||
assignedToMe++
|
||||
} else assignedToOthers++
|
||||
})
|
||||
@@ -342,7 +341,7 @@ const UserActivites = () => {
|
||||
},
|
||||
}
|
||||
|
||||
if (!choresData.res?.length > 0 || !choresHistory?.res?.length > 0) {
|
||||
if (!choresData.res?.length > 0 || !choresHistory?.length > 0) {
|
||||
return (
|
||||
<Container
|
||||
maxWidth='md'
|
||||
@@ -386,7 +385,7 @@ const UserActivites = () => {
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Box mb={1}>
|
||||
<Typography mb={2} level='h4'>
|
||||
Points Overview
|
||||
</Typography>
|
||||
@@ -487,21 +486,6 @@ const UserActivites = () => {
|
||||
))}
|
||||
</TabList>
|
||||
</Tabs>
|
||||
<Box sx={{ ml: 2 }}>
|
||||
<IconButtonWithMenu
|
||||
k={'icon-menu-labels-filter'}
|
||||
// label={' Labels'}
|
||||
icon={<Style />}
|
||||
options={[{ name: 'Anyone' }, { name: 'Me' }]}
|
||||
selectedItem={'Anyone'}
|
||||
onItemSelect={selected => {
|
||||
console.log(selected)
|
||||
}}
|
||||
// isActive={searchFilter.startsWith('Label: ')}
|
||||
// mouseClickHandler={handleMenuOutsideClick}
|
||||
// useChips
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography level='h4' textAlign='center'>
|
||||
|
||||
@@ -65,13 +65,13 @@ const UserPoints = () => {
|
||||
if (choresHistoryData) {
|
||||
var history
|
||||
if (tabValue === 7) {
|
||||
history = generateWeeklySummary(choresHistoryData.res, selectedUser)
|
||||
history = generateWeeklySummary(choresHistoryData, selectedUser)
|
||||
} else if (tabValue === 30) {
|
||||
history = generateMonthSummary(choresHistoryData.res, selectedUser)
|
||||
history = generateMonthSummary(choresHistoryData, selectedUser)
|
||||
} else if (tabValue === 6 * 30) {
|
||||
history = generateMonthlySummary(choresHistoryData.res, selectedUser)
|
||||
history = generateMonthlySummary(choresHistoryData, selectedUser)
|
||||
} else {
|
||||
history = generateYearlySummary(choresHistoryData.res, selectedUser)
|
||||
history = generateYearlySummary(choresHistoryData, selectedUser)
|
||||
}
|
||||
setSelectedHistory(history)
|
||||
}
|
||||
@@ -394,7 +394,7 @@ const UserPoints = () => {
|
||||
{ label: '7 Days', value: 7 },
|
||||
// { label: '3 Month', value: 30 },
|
||||
{ label: '6 Months', value: 6 * 30 },
|
||||
{ label: 'All Time', value: 90 },
|
||||
{ label: 'All Time', value: 24 * 30 },
|
||||
].map((tab, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { KeyboardReturnOutlined } from '@mui/icons-material'
|
||||
import { Add } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalDialog,
|
||||
@@ -18,63 +17,20 @@ import * as chrono from 'chrono-node'
|
||||
import moment from 'moment'
|
||||
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 { useCreateChore } from '../../queries/ChoreQueries'
|
||||
import { useAllUsers } from '../../queries/UserQueries'
|
||||
import useDebounce from '../../utils/Debounce'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
import { useLabels } from '../Labels/LabelQueries'
|
||||
import SmartTaskTitleInput from '../TestView/SmartTaskTitleInput'
|
||||
import { parseLabels, parsePriority, parseRepeatV2 } from './CustomParsers'
|
||||
import LearnMoreButton from './LearnMore'
|
||||
import SubTasks from './SubTask'
|
||||
const VALID_DAYS = {
|
||||
monday: 'Monday',
|
||||
mon: 'Monday',
|
||||
tuesday: 'Tuesday',
|
||||
tue: 'Tuesday',
|
||||
wednesday: 'Wednesday',
|
||||
wed: 'Wednesday',
|
||||
thursday: 'Thursday',
|
||||
thu: 'Thursday',
|
||||
friday: 'Friday',
|
||||
fri: 'Friday',
|
||||
saturday: 'Saturday',
|
||||
sat: 'Saturday',
|
||||
sunday: 'Sunday',
|
||||
sun: 'Sunday',
|
||||
}
|
||||
|
||||
const VALID_MONTHS = {
|
||||
january: 'January',
|
||||
jan: 'January',
|
||||
february: 'February',
|
||||
feb: 'February',
|
||||
march: 'March',
|
||||
mar: 'March',
|
||||
april: 'April',
|
||||
apr: 'April',
|
||||
may: 'May',
|
||||
june: 'June',
|
||||
jun: 'June',
|
||||
july: 'July',
|
||||
jul: 'July',
|
||||
august: 'August',
|
||||
aug: 'August',
|
||||
september: 'September',
|
||||
sep: 'September',
|
||||
october: 'October',
|
||||
oct: 'October',
|
||||
november: 'November',
|
||||
nov: 'November',
|
||||
december: 'December',
|
||||
dec: 'December',
|
||||
}
|
||||
|
||||
const ALL_MONTHS = Object.values(VALID_MONTHS).filter(
|
||||
(v, i, a) => 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(
|
||||
<span
|
||||
key={highlight.start}
|
||||
className={className}
|
||||
style={{
|
||||
// text underline:
|
||||
textDecoration: 'underline',
|
||||
// textDecorationColor: 'red',
|
||||
textDecorationThickness: '2px',
|
||||
textDecorationStyle: 'dashed',
|
||||
}}
|
||||
>
|
||||
{sentence.substring(highlight.start, highlight.end)}
|
||||
</span>,
|
||||
)
|
||||
|
||||
// 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 && (
|
||||
<CSSTransition in={!openModal} timeout={300} classNames='fade'>
|
||||
<Input
|
||||
autoFocus={autoFocus > 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={
|
||||
<IconButton
|
||||
variant='outlined'
|
||||
sx={{ borderRadius: 24, marginRight: -0.5 }}
|
||||
>
|
||||
<KeyboardReturnOutlined />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
</CSSTransition>
|
||||
)}
|
||||
|
||||
<Modal open={openModal} onClose={handleCloseModal}>
|
||||
<ModalOverflow>
|
||||
<ModalDialog>
|
||||
{/* <Button
|
||||
size='sm'
|
||||
onClick={() => navigate(`/chores/create`)}
|
||||
variant='outlined'
|
||||
sx={{ position: 'absolute', right: 20 }}
|
||||
startDecorator={<OpenInFull />}
|
||||
>
|
||||
Full Editor
|
||||
</Button> */}
|
||||
<Typography level='h4'>Create new task</Typography>
|
||||
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
|
||||
Experimental Feature
|
||||
</Chip>
|
||||
<Box>
|
||||
<Modal open={isModalOpen} onClose={handleCloseModal}>
|
||||
<ModalOverflow>
|
||||
<ModalDialog size='lg' sx={{ minWidth: '80%' }}>
|
||||
<Typography level='h4'>Create new task</Typography>
|
||||
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
|
||||
Experimental Feature
|
||||
</Chip>
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography level='body-sm'>Task in a sentence:</Typography>
|
||||
<Input
|
||||
autoFocus
|
||||
ref={textareaRef}
|
||||
value={taskText}
|
||||
onChange={handleTextChange}
|
||||
onKeyUp={handleEnterPressed}
|
||||
placeholder='Type your full text here...'
|
||||
sx={{ width: '100%', fontSize: '16px' }}
|
||||
/>
|
||||
<LearnMoreButton
|
||||
content={
|
||||
<>
|
||||
@@ -585,58 +381,125 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
|
||||
<SmartTaskTitleInput
|
||||
autoFocus
|
||||
value={taskText}
|
||||
placeholder='Type your full text here...'
|
||||
onChange={text => {
|
||||
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 },
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* <Box>
|
||||
<Typography level='body-sm'>Title:</Typography>
|
||||
<Input
|
||||
value={taskTitle}
|
||||
onChange={e => setTaskTitle(e.target.value)}
|
||||
sx={{ width: '100%', fontSize: '16px' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{hasDescription ? (
|
||||
<Box>
|
||||
<Typography level='body-sm'>Description:</Typography>
|
||||
<Textarea
|
||||
minRows={2}
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
</Box> */}
|
||||
<Box>
|
||||
{!hasDescription && (
|
||||
<Button
|
||||
startDecorator={<Add />}
|
||||
variant='plain'
|
||||
size='sm'
|
||||
onClick={() => setHasDescription(true)}
|
||||
>
|
||||
Add Description
|
||||
Description
|
||||
</Button>
|
||||
)}
|
||||
<Typography level='body-sm'>Subtasks:</Typography>
|
||||
<SubTasks editMode={true} tasks={subTasks} setTasks={setSubTasks} />
|
||||
{!hasSubTasks && (
|
||||
<Button
|
||||
startDecorator={<Add />}
|
||||
variant='plain'
|
||||
size='sm'
|
||||
onClick={() => setHasSubTasks(true)}
|
||||
>
|
||||
Subtasks
|
||||
</Button>
|
||||
)}
|
||||
{!dueDate && (
|
||||
<Button
|
||||
startDecorator={<Add />}
|
||||
variant='plain'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
setDueDate(
|
||||
moment().add(1, 'day').format('YYYY-MM-DDTHH:00:00'),
|
||||
)
|
||||
}}
|
||||
>
|
||||
Due Date
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<Typography level='body-sm'>Priority</Typography>
|
||||
<Select
|
||||
defaultValue={0}
|
||||
value={priority}
|
||||
onChange={(e, value) => setPriority(value)}
|
||||
>
|
||||
<Option value='0'>No Priority</Option>
|
||||
<Option value='1'>P1</Option>
|
||||
<Option value='2'>P2</Option>
|
||||
<Option value='3'>P3</Option>
|
||||
<Option value='4'>P4</Option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{hasDescription && (
|
||||
<Box>
|
||||
<Typography level='body-sm'>Description:</Typography>
|
||||
<Textarea
|
||||
minRows={2}
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{hasSubTasks && (
|
||||
<Box>
|
||||
<Typography level='body-sm'>Subtasks:</Typography>
|
||||
<SubTasks
|
||||
editMode={true}
|
||||
tasks={subTasks ? subTasks : []}
|
||||
setTasks={setSubTasks}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<Typography level='body-sm'>Priority</Typography>
|
||||
<Select
|
||||
defaultValue={0}
|
||||
value={priority}
|
||||
onChange={(e, value) => setPriority(value)}
|
||||
>
|
||||
<Option value='0'>No Priority</Option>
|
||||
<Option value='1'>P1</Option>
|
||||
<Option value='2'>P2</Option>
|
||||
<Option value='3'>P3</Option>
|
||||
<Option value='4'>P4</Option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{dueDate && (
|
||||
<FormControl>
|
||||
<Typography level='body-sm'>Due Date</Typography>
|
||||
<Input
|
||||
@@ -646,55 +509,52 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
||||
sx={{ width: '100%', fontSize: '16px' }}
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'start',
|
||||
gap: 2,
|
||||
}}
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'start',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<Typography level='body-sm'>Assignee</Typography>
|
||||
<Select value={'0'} disabled>
|
||||
<Option value='0'>Me</Option>
|
||||
{/* <Option value='1'>Other</Option> */}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Typography level='body-sm'>Frequency</Typography>
|
||||
<Input value={frequencyHumanReadable || 'Once'} variant='plain' />
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'end',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
onClick={handleCloseModal}
|
||||
>
|
||||
<FormControl>
|
||||
<Typography level='body-sm'>Assignee</Typography>
|
||||
<Select value={'0'} disabled>
|
||||
<Option value='0'>Me</Option>
|
||||
{/* <Option value='1'>Other</Option> */}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Typography level='body-sm'>Frequency</Typography>
|
||||
<Input
|
||||
value={frequencyHumanReadable || 'Once'}
|
||||
variant='plain'
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'end',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
onClick={handleCloseModal}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='solid' color='primary' onClick={handleSubmit}>
|
||||
Create
|
||||
</Button>
|
||||
</Box>
|
||||
</ModalDialog>
|
||||
</ModalOverflow>
|
||||
</Modal>
|
||||
</>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='solid' color='primary' onClick={handleSubmit}>
|
||||
Create
|
||||
</Button>
|
||||
</Box>
|
||||
</ModalDialog>
|
||||
</ModalOverflow>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
116
src/views/components/AutocompleteInput.jsx
Normal file
116
src/views/components/AutocompleteInput.jsx
Normal file
@@ -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 (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Textarea
|
||||
{...props}
|
||||
ref={ref}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder='Type here...'
|
||||
/>
|
||||
{menuVisible && (
|
||||
<List ref={menuRef} style={{ position: 'absolute', zIndex: 1000 }}>
|
||||
{filteredOptions.map((option, index) => (
|
||||
<ListItem key={option.value || option.label}>
|
||||
<ListItemButton
|
||||
selected={index === highlightedIndex}
|
||||
onClick={() => selectOption(option)}
|
||||
>
|
||||
{option.color && (
|
||||
<Chip
|
||||
style={{
|
||||
backgroundColor: option.color,
|
||||
marginRight: '0.5rem',
|
||||
color: '#fff',
|
||||
}}
|
||||
size='sm'
|
||||
variant='soft'
|
||||
/>
|
||||
)}
|
||||
{option.label}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutocompleteInput
|
||||
@@ -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 (
|
||||
<div className='dot-container'>
|
||||
@@ -70,7 +66,7 @@ const CalendarView = ({ chores }) => {
|
||||
<Calendar
|
||||
tileContent={tileContent}
|
||||
onChange={d => {
|
||||
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) => (
|
||||
<Card
|
||||
|
||||
398
src/views/components/CustomParsers.js
Normal file
398
src/views/components/CustomParsers.js
Normal file
@@ -0,0 +1,398 @@
|
||||
const VALID_DAYS = {
|
||||
monday: 'Monday',
|
||||
mon: 'Monday',
|
||||
tuesday: 'Tuesday',
|
||||
tue: 'Tuesday',
|
||||
wednesday: 'Wednesday',
|
||||
wed: 'Wednesday',
|
||||
thursday: 'Thursday',
|
||||
thu: 'Thursday',
|
||||
friday: 'Friday',
|
||||
fri: 'Friday',
|
||||
saturday: 'Saturday',
|
||||
sat: 'Saturday',
|
||||
sunday: 'Sunday',
|
||||
sun: 'Sunday',
|
||||
}
|
||||
|
||||
const VALID_MONTHS = {
|
||||
january: 'January',
|
||||
jan: 'January',
|
||||
february: 'February',
|
||||
feb: 'February',
|
||||
march: 'March',
|
||||
mar: 'March',
|
||||
april: 'April',
|
||||
apr: 'April',
|
||||
may: 'May',
|
||||
june: 'June',
|
||||
jun: 'June',
|
||||
july: 'July',
|
||||
jul: 'July',
|
||||
august: 'August',
|
||||
aug: 'August',
|
||||
september: 'September',
|
||||
sep: 'September',
|
||||
october: 'October',
|
||||
oct: 'October',
|
||||
november: 'November',
|
||||
nov: 'November',
|
||||
december: 'December',
|
||||
dec: 'December',
|
||||
}
|
||||
|
||||
const ALL_MONTHS = Object.values(VALID_MONTHS).filter(
|
||||
(v, i, a) => 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 }
|
||||
}
|
||||
0
src/views/components/ErrorSnackbar.jsx
Normal file
0
src/views/components/ErrorSnackbar.jsx
Normal file
@@ -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 (
|
||||
<Box sx={{ position: 'relative', display: 'inline-block' }}>
|
||||
<Button
|
||||
<IconButton
|
||||
ref={anchorRef}
|
||||
variant='plain'
|
||||
startDecorator={<Info />}
|
||||
size='sm'
|
||||
color='primary'
|
||||
onClick={handleToggle}
|
||||
>
|
||||
Learn More
|
||||
</Button>
|
||||
<Info />
|
||||
</IconButton>
|
||||
{open && (
|
||||
<Sheet
|
||||
variant='outlined'
|
||||
|
||||
Reference in New Issue
Block a user