diff --git a/package-lock.json b/package-lock.json index f5eb71b..0935c6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/src/App.jsx b/src/App.jsx index 4f66e0f..7310ae1 100644 --- a/src/App.jsx +++ b/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() { - - - - + + + + + + {needRefresh && ( diff --git a/src/hooks/NetworkManager.jsx b/src/hooks/NetworkManager.jsx index 847fbd0..d9c43e4 100644 --- a/src/hooks/NetworkManager.jsx +++ b/src/hooks/NetworkManager.jsx @@ -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() { diff --git a/src/queries/ChoreQueries.jsx b/src/queries/ChoreQueries.jsx index f228a04..bdf9c71 100644 --- a/src/queries/ChoreQueries.jsx +++ b/src/queries/ChoreQueries.jsx @@ -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) diff --git a/src/queries/ResourceQueries.jsx b/src/queries/ResourceQueries.jsx index c310631..5205dcf 100644 --- a/src/queries/ResourceQueries.jsx +++ b/src/queries/ResourceQueries.jsx @@ -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 } } diff --git a/src/queries/SubtaskQueries.jsx b/src/queries/SubtaskQueries.jsx new file mode 100644 index 0000000..616b703 --- /dev/null +++ b/src/queries/SubtaskQueries.jsx @@ -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) => {}, + }) +} diff --git a/src/queries/UserQueries.jsx b/src/queries/UserQueries.jsx index d5f2d56..2992b02 100644 --- a/src/queries/UserQueries.jsx +++ b/src/queries/UserQueries.jsx @@ -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() diff --git a/src/service/ErrorProvider.jsx b/src/service/ErrorProvider.jsx new file mode 100644 index 0000000..5cfc120 --- /dev/null +++ b/src/service/ErrorProvider.jsx @@ -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 ( + + {children} + setError(null)} + startDecorator={} + endDecorator={ + + } + > + {typeof error === 'string' ? ( + + {error} + + ) : ( + + + {error?.title} + + + {error?.message} + + + )} + + + ) +} diff --git a/src/utils/LocalStore.jsx b/src/utils/LocalStore.jsx index bd68b40..0573af7 100644 --- a/src/utils/LocalStore.jsx +++ b/src/utils/LocalStore.jsx @@ -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)) || [] diff --git a/src/utils/TokenManager.jsx b/src/utils/TokenManager.jsx index d6aef13..877ece5 100644 --- a/src/utils/TokenManager.jsx +++ b/src/utils/TokenManager.jsx @@ -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: ' + diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index 92313c8..6eaf88d 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -386,7 +386,7 @@ const ChoreEdit = () => { }, }) } - if (isChoreLoading && choreId) { + if ((isChoreLoading && choreId) || isUserLabelsLoading) { return } 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' /> Add sub tasks to this task @@ -997,7 +994,6 @@ const ChoreEdit = () => { } }} checked={points > -1} - value={points > -1} overlay label='Assign Points' /> diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx index ebd1a0d..f27743f 100644 --- a/src/views/ChoreEdit/ChoreView.jsx +++ b/src/views/ChoreEdit/ChoreView.jsx @@ -243,6 +243,7 @@ const ChoreView = () => { justifyContent: 'center', alignItems: 'center', textAlign: 'center', + mb: 1, }} > { ? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}` : 'N/A'} - {chore?.labelsV2?.map((label, index) => ( - - {label?.name} - - ))} + + {chore?.labelsV2?.map((label, index) => ( + + {label?.name} + + ))} + diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index d269cd0..5134477 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -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 = ({ { - 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, + }) + } + }) }} > diff --git a/src/views/Chores/CompactCard.jsx b/src/views/Chores/CompactCard.jsx new file mode 100644 index 0000000..39ba804 --- /dev/null +++ b/src/views/Chores/CompactCard.jsx @@ -0,0 +1,35 @@ +import { Box, Checkbox, Typography } from '@mui/joy' + +export const CompactCard = ({ + chore, + performers, + onChoreUpdate, + onChoreRemove, + userLabels, + sx, + viewOnly, + onChipClick, +}) => { + return ( + + handleToggle(task.id)} + /> + + {chore.name} + + + ) +} diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index a0bb754..2b942e9 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -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' && ( - - )} + { + setShowSearchFilter(true) + }} + fullWidth + sx={{ + mt: 1, + mb: 1, + borderRadius: 24, + height: 24, + borderColor: 'text.disabled', + padding: 1, + }} + onChange={handleSearchChange} + endDecorator={ + searchTerm && ( + { + setSearchTerm('') + setFilteredChores(chores) + }} + /> + ) + } + /> - {activeTextField == 'search' && ( - 0} - placeholder='Search' - value={searchTerm} - fullWidth - sx={{ - mt: 1, - mb: 1, - borderRadius: 24, - height: 24, - borderColor: 'text.disabled', - padding: 1, - }} - onChange={handleSearchChange} - endDecorator={ - searchTerm && ( - { - setSearchTerm('') - setFilteredChores(chores) - }} - /> - ) - } - /> - )} - {activeTextField != 'task' && ( - { - setActiveTextFieldWithCache('task') - setTaskInputFocus(taskInputFocus + 1) - }} - > - - - )} - {activeTextField != 'search' && ( + {/* {activeTextField != 'search' && ( { > - )} + )} */} { mouseClickHandler={handleMenuOutsideClick} /> - {activeTextField === 'search' && ( + {showSearchFilter && (
{
+ { + setShowSearchFilter(false) + setSearchTerm('') + setFilteredChores(chores) + setSearchFilter('All') + }} + > + +
)} {searchFilter !== 'All' && ( @@ -811,7 +819,7 @@ const MyChores = () => { display: 'flex', justifyContent: 'flex-end', gap: 2, - 'z-index': 1000, + 'z-index': 100, }} > { borderRadius: '50%', width: 50, height: 50, + zIndex: 101, }} onClick={() => { Navigate(`/chores/create`) @@ -828,6 +837,27 @@ const MyChores = () => { > + { + setAddTaskModalOpen(true) + }} + > + + { {snackBarMessage} + {addTaskModalOpen && ( + { + setAddTaskModalOpen(false) + if (forceRefresh) { + refetchChores() + } + }} + /> + )} diff --git a/src/views/ChoresOverview.jsx b/src/views/ChoresOverview.jsx index 14b2040..a4d77da 100644 --- a/src/views/ChoresOverview.jsx +++ b/src/views/ChoresOverview.jsx @@ -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) + }) + } + }, + ) }} /> diff --git a/src/views/Labels/LabelQueries.jsx b/src/views/Labels/LabelQueries.jsx index ebf587e..5fec321 100644 --- a/src/views/Labels/LabelQueries.jsx +++ b/src/views/Labels/LabelQueries.jsx @@ -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, }) } diff --git a/src/views/Labels/LabelView.jsx b/src/views/Labels/LabelView.jsx index 4bc83fb..81c9b47 100644 --- a/src/views/Labels/LabelView.jsx +++ b/src/views/Labels/LabelView.jsx @@ -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 = () => { handleDeleteLabel(label.id)} + onClick={() => handleDeleteClicked(label.id)} color='danger' > diff --git a/src/views/Modals/Inputs/LabelModal.jsx b/src/views/Modals/Inputs/LabelModal.jsx index 2804d1f..81118c4 100644 --- a/src/views/Modals/Inputs/LabelModal.jsx +++ b/src/views/Modals/Inputs/LabelModal.jsx @@ -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 }) { diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx index 1cc14e3..bd5c00f 100644 --- a/src/views/Settings/Settings.jsx +++ b/src/views/Settings/Settings.jsx @@ -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 = () => { + +
+ Experimental Features + + + Clean up some part of the local storage and cache. Only use if you + know are you doing. + + + +
) } diff --git a/src/views/TestView/AutocompleteDropdown.jsx b/src/views/TestView/AutocompleteDropdown.jsx new file mode 100644 index 0000000..afe96b3 --- /dev/null +++ b/src/views/TestView/AutocompleteDropdown.jsx @@ -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 ( + {}} + 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) => ( + 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} + + ))} + + {/* + { + onSelectSuggestion(currentValue) + }} + sx={{ + cursor: 'pointer', + backgroundColor: 'gray.800', + }} + > + + Add new Label + */} + + ) +} + +export default AutocompleteDropdown diff --git a/src/views/TestView/MentionTextfield.css b/src/views/TestView/MentionTextfield.css deleted file mode 100644 index 40c40ad..0000000 --- a/src/views/TestView/MentionTextfield.css +++ /dev/null @@ -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; -} diff --git a/src/views/TestView/MentionTextfield.jsx b/src/views/TestView/MentionTextfield.jsx deleted file mode 100644 index 66c95ba..0000000 --- a/src/views/TestView/MentionTextfield.jsx +++ /dev/null @@ -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( - - {matchedText} - , - ) - - lastIndex = regex.lastIndex - } - - // Add any remaining text after the last match - if (lastIndex < text.length) { - parts.push(text.substring(lastIndex)) - } - - return
{parts}
- } - - return ( -
-
-