diff --git a/package-lock.json b/package-lock.json
index efde8e5..16efd44 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "donetick",
- "version": "0.1.72",
+ "version": "0.1.78",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "donetick",
- "version": "0.1.72",
+ "version": "0.1.78",
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
@@ -23,7 +23,9 @@
"prop-types": "^15.8.1",
"react": "^18.2.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",
"vite-plugin-pwa": "^0.20.0"
},
@@ -4142,6 +4144,15 @@
}
]
},
+ "node_modules/big-integer": {
+ "version": "1.6.52",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+ "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -4182,6 +4193,22 @@
"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/browserslist": {
"version": "4.23.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz",
@@ -4664,6 +4691,12 @@
"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/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -6360,6 +6393,12 @@
"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",
@@ -6574,6 +6613,16 @@
"sourcemap-codec": "^1.4.8"
}
},
+ "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/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -6594,6 +6643,12 @@
"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-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
@@ -6665,6 +6720,15 @@
"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.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -6864,6 +6928,12 @@
"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",
@@ -7481,6 +7551,32 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "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.21.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.1.tgz",
@@ -7515,6 +7611,7 @@
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
@@ -7673,6 +7770,12 @@
"jsesc": "bin/jsesc"
}
},
+ "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/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -7728,7 +7831,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -8857,6 +8959,16 @@
"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/upath": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
diff --git a/package.json b/package.json
index f6c6ca8..26b0d97 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,9 @@
"prop-types": "^15.8.1",
"react": "^18.2.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",
"vite-plugin-pwa": "^0.20.0"
},
diff --git a/src/App.jsx b/src/App.jsx
index de8a3dd..296a227 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -2,6 +2,7 @@ import NavBar from '@/views/components/NavBar'
import { Button, Snackbar, Typography, useColorScheme } from '@mui/joy'
import Tracker from '@openreplay/tracker'
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 { UserContext } from './contexts/UserContext'
@@ -20,6 +21,8 @@ const remove = className => {
const intervalMS = 5 * 60 * 1000 // 5 minutes
function App() {
+ const queryClient = new QueryClient()
+
startOpenReplay()
const { mode, systemMode } = useColorScheme()
@@ -92,28 +95,31 @@ function App() {
return (
-
-
-
-
-
- {needRefresh && (
-
-
- A new version is now available.Click on reload button to update.
-
- {
- updateServiceWorker(true)
- setShowUpdateSnackbar(false)
- }}
- >
- Refresh
-
-
- )}
+
+
+
+
+
+
+ {needRefresh && (
+
+
+ A new version is now available.Click on reload button to update.
+
+ {
+ updateServiceWorker(true)
+ setShowUpdateSnackbar(false)
+ }}
+ >
+ Refresh
+
+
+ )}
+
+ ,
)
}
diff --git a/src/contexts/RouterContext.jsx b/src/contexts/RouterContext.jsx
index 4fea83d..96ad9cd 100644
--- a/src/contexts/RouterContext.jsx
+++ b/src/contexts/RouterContext.jsx
@@ -12,6 +12,7 @@ import ChoreView from '../views/ChoreEdit/ChoreView'
import MyChores from '../views/Chores/MyChores'
import JoinCircleView from '../views/Circles/JoinCircle'
import ChoreHistory from '../views/History/ChoreHistory'
+import LabelView from '../views/Labels/LabelView'
import Landing from '../views/Landing/Landing'
import PaymentCancelledView from '../views/Payments/PaymentFailView'
import PaymentSuccessView from '../views/Payments/PaymentSuccessView'
@@ -116,6 +117,10 @@ const Router = createBrowserRouter([
path: 'things/:id',
element: ,
},
+ {
+ path: 'labels/',
+ element: ,
+ },
],
},
])
diff --git a/src/queries/ChoreQueries.jsx b/src/queries/ChoreQueries.jsx
new file mode 100644
index 0000000..ad9369e
--- /dev/null
+++ b/src/queries/ChoreQueries.jsx
@@ -0,0 +1,17 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { useQuery } from 'react-query'
+import { CreateChore, GetChoresNew } from '../utils/Fetcher'
+
+export const useChores = () => {
+ return useQuery('chores', GetChoresNew)
+}
+
+export const useCreateChore = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation(CreateChore, {
+ onSuccess: () => {
+ queryClient.invalidateQueries('chores')
+ },
+ })
+}
diff --git a/src/queries/UserQueries.jsx b/src/queries/UserQueries.jsx
new file mode 100644
index 0000000..8cb1548
--- /dev/null
+++ b/src/queries/UserQueries.jsx
@@ -0,0 +1,6 @@
+import { useQuery } from 'react-query'
+import { GetAllUsers } from '../utils/Fetcher'
+
+export const useAllUsers = () => {
+ return useQuery('allUsers', GetAllUsers)
+}
diff --git a/src/utils/Fetcher.jsx b/src/utils/Fetcher.jsx
index 3310d87..836431e 100644
--- a/src/utils/Fetcher.jsx
+++ b/src/utils/Fetcher.jsx
@@ -45,6 +45,13 @@ const GetAllUsers = () => {
headers: HEADERS(),
})
}
+const GetChoresNew = async () => {
+ const resp = await Fetch(`${API_URL}/chores/`, {
+ method: 'GET',
+ headers: HEADERS(),
+ })
+ return resp.json()
+}
const GetChores = () => {
return Fetch(`${API_URL}/chores/`, {
@@ -294,16 +301,49 @@ const GetLongLiveTokens = () => {
headers: HEADERS(),
})
}
+
+const CreateLabel = label => {
+ return Fetch(`${API_URL}/labels`, {
+ method: 'POST',
+ headers: HEADERS(),
+ body: JSON.stringify(label),
+ })
+}
+
+const GetLabels = async () => {
+ const resp = await Fetch(`${API_URL}/labels`, {
+ method: 'GET',
+ headers: HEADERS(),
+ })
+ return resp.json()
+}
+
+const UpdateLabel = label => {
+ return Fetch(`${API_URL}/labels`, {
+ method: 'PUT',
+ headers: HEADERS(),
+ body: JSON.stringify(label),
+ })
+}
+const DeleteLabel = id => {
+ return Fetch(`${API_URL}/labels/${id}`, {
+ method: 'DELETE',
+ headers: HEADERS(),
+ })
+}
+
export {
AcceptCircleMemberRequest,
CancelSubscription,
createChore,
CreateChore,
+ CreateLabel,
CreateLongLiveToken,
CreateThing,
DeleteChore,
DeleteChoreHistory,
DeleteCircleMember,
+ DeleteLabel,
DeleteLongLiveToken,
DeleteThing,
GetAllCircleMembers,
@@ -312,7 +352,9 @@ export {
GetChoreDetailById,
GetChoreHistory,
GetChores,
+ GetChoresNew,
GetCircleMemberRequests,
+ GetLabels,
GetLongLiveTokens,
GetSubscriptionSession,
GetThingHistory,
@@ -330,6 +372,7 @@ export {
UpdateChoreAssignee,
UpdateChoreHistory,
UpdateChorePriority,
+ UpdateLabel,
UpdatePassword,
UpdateThingState,
UpdateUserDetails,
diff --git a/src/utils/LabelColors.jsx b/src/utils/LabelColors.jsx
new file mode 100644
index 0000000..d585d1b
--- /dev/null
+++ b/src/utils/LabelColors.jsx
@@ -0,0 +1,38 @@
+const LABEL_COLORS = [
+ { name: 'Default', value: '#FFFFFF' },
+ { name: 'Salmon', value: '#ff7961' },
+ { name: 'Teal', value: '#26a69a' },
+ { name: 'Sky Blue', value: '#80d8ff' },
+ { name: 'Grape', value: '#7e57c2' },
+ { name: 'Sunshine', value: '#ffee58' },
+ { name: 'Coral', value: '#ff7043' },
+ { name: 'Lavender', value: '#ce93d8' },
+ { name: 'Rose', value: '#f48fb1' },
+ { name: 'Charcoal', value: '#616161' },
+ { name: 'Sienna', value: '#8d6e63' },
+ { name: 'Mint', value: '#a7ffeb' },
+ { name: 'Amber', value: '#ffc107' },
+ { name: 'Cobalt', value: '#3f51b5' },
+ { name: 'Emerald', value: '#4caf50' },
+ { name: 'Peach', value: '#ffab91' },
+ { name: 'Ocean', value: '#0288d1' },
+ { name: 'Mustard', value: '#ffca28' },
+ { name: 'Ruby', value: '#d32f2f' },
+ { name: 'Periwinkle', value: '#b39ddb' },
+ { name: 'Turquoise', value: '#00bcd4' },
+ { name: 'Lime', value: '#cddc39' },
+ { name: 'Blush', value: '#f8bbd0' },
+ { name: 'Ash', value: '#90a4ae' },
+ { name: 'Sand', value: '#d7ccc8' },
+]
+
+export default LABEL_COLORS
+
+export const getTextColorFromBackgroundColor = bgColor => {
+ if (!bgColor) return ''
+ const hex = bgColor.replace('#', '')
+ const r = parseInt(hex.substring(0, 2), 16)
+ const g = parseInt(hex.substring(2, 4), 16)
+ const b = parseInt(hex.substring(4, 6), 16)
+ return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#000000' : '#ffffff'
+}
diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx
index 01a947d..61fa863 100644
--- a/src/views/ChoreEdit/ChoreEdit.jsx
+++ b/src/views/ChoreEdit/ChoreEdit.jsx
@@ -1,3 +1,4 @@
+import { Add } from '@mui/icons-material'
import {
Box,
Button,
@@ -11,6 +12,7 @@ import {
Input,
List,
ListItem,
+ MenuItem,
Option,
Radio,
RadioGroup,
@@ -34,8 +36,10 @@ import {
SaveChore,
} from '../../utils/Fetcher'
import { isPlusAccount } from '../../utils/Helpers'
-import FreeSoloCreateOption from '../components/AutocompleteSelect'
+import { getTextColorFromBackgroundColor } from '../../utils/LabelColors'
+import { useLabels } from '../Labels/LabelQueries'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
+import LabelModal from '../Modals/Inputs/LabelModal'
import RepeatSection from './RepeatSection'
const ASSIGN_STRATEGIES = [
'random',
@@ -66,6 +70,7 @@ const ChoreEdit = () => {
const [frequency, setFrequency] = useState(1)
const [frequencyMetadata, setFrequencyMetadata] = useState({})
const [labels, setLabels] = useState([])
+ const [labelsV2, setLabelsV2] = useState([])
const [allUserThings, setAllUserThings] = useState([])
const [thingTrigger, setThingTrigger] = useState(null)
const [isThingValid, setIsThingValid] = useState(false)
@@ -82,6 +87,9 @@ const ChoreEdit = () => {
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
const [snackbarMessage, setSnackbarMessage] = useState('')
const [snackbarColor, setSnackbarColor] = useState('warning')
+ const [addLabelModalOpen, setAddLabelModalOpen] = useState(false)
+ const { data: userLabels, isLoading: isUserLabelsLoading } = useLabels()
+
const Navigate = useNavigate()
const HandleValidateChore = () => {
@@ -172,7 +180,8 @@ const ChoreEdit = () => {
isRolling: isRolling,
isActive: isActive,
notification: isNotificable,
- labels: labels,
+ labels: labels.map(l => l.name),
+ labelsV2: labelsV2,
notificationMetadata: notificationMetadata,
thingTrigger: thingTrigger,
}
@@ -226,8 +235,9 @@ const ChoreEdit = () => {
setFrequency(data.res.frequency)
setNotificationMetadata(JSON.parse(data.res.notificationMetadata))
- setLabels(data.res.labels ? data.res.labels.split(',') : [])
+ // setLabels(data.res.labels ? data.res.labels.split(',') : [])
+ setLabelsV2(data.res.labelsV2)
setAssignStrategy(
data.res.assignStrategy
? data.res.assignStrategy
@@ -275,6 +285,14 @@ const ChoreEdit = () => {
}
}, [])
+ // useEffect(() => {
+ // if (userLabels && userLabels.length == 0 && labelsV2.length == 0) {
+ // return
+ // }
+ // const labelIds = labelsV2.map(l => l.id)
+ // setLabelsV2(userLabels.filter(l => labelIds.indexOf(l.id) > -1))
+ // }, [userLabels, labelsV2])
+
useEffect(() => {
// if frequancy type change to somthing need a due date then set it to the current date:
if (!NO_DUE_DATE_REQUIRED_TYPE.includes(frequencyType) && !dueDate) {
@@ -329,6 +347,7 @@ const ChoreEdit = () => {
},
})
}
+
return (
{/*
@@ -726,8 +745,9 @@ const ChoreEdit = () => {
Things to remember about this chore or to tag it
- {
const newLabels = []
changes.map(change => {
@@ -741,7 +761,99 @@ const ChoreEdit = () => {
})
setLabels(newLabels)
}}
- />
+ /> */}
+ {
+ setLabelsV2(userLabels.filter(l => newValue.indexOf(l.name) > -1))
+ }}
+ value={labelsV2.map(l => l.name)}
+ renderValue={selected => (
+
+ {labelsV2.map(selectedOption => {
+ return (
+
+ {selectedOption.name}
+
+ )
+ })}
+
+ )}
+ sx={{ minWidth: '15rem' }}
+ slotProps={{
+ listbox: {
+ sx: {
+ width: '100%',
+ },
+ },
+ }}
+ >
+ {userLabels &&
+ userLabels
+ // .map(l => l.name)
+ .map(label => (
+
+
+ {label.name}
+
+ ))}
+ {
+ setAddLabelModalOpen(true)
+ }}
+ >
+
+ Add New Label
+
+
+ {/*
+
+ {labels?.map((label, index) => (
+
+ {
+ setLabels(labels.filter(l => l !== label))
+ }}
+ checked={true}
+ overlay
+ variant='soft'
+ color='primary'
+ size='lg'
+ endDecorator={ }
+ >
+ {label}
+
+
+ ))}
+
+ */}
{choreId > 0 && (
@@ -822,6 +934,16 @@ const ChoreEdit = () => {
+ {addLabelModalOpen && (
+ {
+ setLabels([...labels, label])
+ setAddLabelModalOpen(false)
+ }}
+ onClose={() => setAddLabelModalOpen(false)}
+ />
+ )}
{/* */}
{
? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}`
: 'N/A'}
+ {/* show each label : */}
+ {chore?.labelsV2?.map((label, index) => (
+
+ {label?.name}
+
+ ))}
{
@@ -407,7 +409,7 @@ const ChoreCard = ({
}
return (
- <>
+
)}
-
+
{chore.priority > 0 && (
)}
- {chore.labels?.split(',').map((label, index) => (
-
- {label}
-
- ))}
+ {chore.labelsV2?.map((l, index) => {
+ return (
+
+ {l?.name}
+
+ )
+ })}
@@ -757,7 +765,7 @@ const ChoreCard = ({
- >
+
)
}
diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx
index d71dd2a..cc7ab58 100644
--- a/src/views/Chores/MyChores.jsx
+++ b/src/views/Chores/MyChores.jsx
@@ -21,8 +21,10 @@ import Fuse from 'fuse.js'
import { useContext, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { UserContext } from '../../contexts/UserContext'
-import { GetAllUsers, GetChores, GetUserProfile } from '../../utils/Fetcher'
+import { useChores } from '../../queries/ChoreQueries'
+import { GetAllUsers, GetUserProfile } from '../../utils/Fetcher'
import LoadingComponent from '../components/Loading'
+import { useLabels } from '../Labels/LabelQueries'
import ChoreCard from './ChoreCard'
const MyChores = () => {
@@ -38,6 +40,8 @@ const MyChores = () => {
const [anchorEl, setAnchorEl] = useState(null)
const menuRef = useRef(null)
const Navigate = useNavigate()
+ const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
+ const { data: choresData, isLoading: choresLoading } = useChores()
const choreSorter = (a, b) => {
// 1. Handle null due dates (always last):
if (!a.nextDueDate && !b.nextDueDate) return 0 // Both null, no order
@@ -74,14 +78,6 @@ const MyChores = () => {
setUserProfile(data.res)
})
}
- GetChores()
- .then(response => response.json())
- .then(data => {
- data.res.sort(choreSorter)
- setChores(data.res)
-
- setFilteredChores(data.res)
- })
GetAllUsers()
.then(response => response.json())
@@ -94,6 +90,15 @@ const MyChores = () => {
setActiveUserId(currentUser.id)
}
}, [])
+
+ useEffect(() => {
+ if (choresData) {
+ const sortedChores = choresData.res.sort(choreSorter)
+ setChores(sortedChores)
+ setFilteredChores(sortedChores)
+ }
+ }, [choresData, choresLoading])
+
useEffect(() => {
document.addEventListener('mousedown', handleMenuOutsideClick)
return () => {
@@ -160,12 +165,21 @@ const MyChores = () => {
const searchOptions = {
// keys to search in
- keys: ['name', 'labels'],
+ keys: ['name', 'raw_label'],
includeScore: true, // Optional: if you want to see how well each result matched the search term
isCaseSensitive: false,
findAllMatches: true,
}
- const fuse = new Fuse(chores, searchOptions)
+
+ const fuse = new Fuse(
+ chores.map(c => ({
+ ...c,
+ raw_label: c.labelsV2
+ .map(l => userLabels.find(x => (x.id = l.id)).name)
+ .join(' '),
+ })),
+ searchOptions,
+ )
const handleSearchChange = e => {
const search = e.target.value
@@ -180,7 +194,12 @@ const MyChores = () => {
setFilteredChores(fuse.search(term).map(result => result.item))
}
- if (userProfile === null) {
+ if (
+ userProfile === null ||
+ userLabelsLoading ||
+ performers.length === 0 ||
+ choresLoading
+ ) {
return
}
@@ -326,6 +345,7 @@ const MyChores = () => {
onChoreUpdate={handleChoreUpdated}
onChoreRemove={handleChoreDeleted}
performers={performers}
+ userLabels={userLabels}
/>
))}
diff --git a/src/views/Labels/LabelQueries.jsx b/src/views/Labels/LabelQueries.jsx
new file mode 100644
index 0000000..ebf587e
--- /dev/null
+++ b/src/views/Labels/LabelQueries.jsx
@@ -0,0 +1,8 @@
+import { useQuery } from 'react-query'
+import { GetLabels } from '../../utils/Fetcher'
+
+export const useLabels = () => {
+ return useQuery('labels', GetLabels, {
+ initialData: [],
+ })
+}
diff --git a/src/views/Labels/LabelView.jsx b/src/views/Labels/LabelView.jsx
new file mode 100644
index 0000000..9dd442a
--- /dev/null
+++ b/src/views/Labels/LabelView.jsx
@@ -0,0 +1,178 @@
+import DeleteIcon from '@mui/icons-material/Delete'
+import EditIcon from '@mui/icons-material/Edit'
+import {
+ Box,
+ CircularProgress,
+ Container,
+ IconButton,
+ Table,
+ Typography,
+} from '@mui/joy'
+import React, { 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 { DeleteLabel } from '../../utils/Fetcher'
+
+const LabelView = () => {
+ const { data: labels, isLabelsLoading, isError } = useLabels()
+ const [userLabels, setUserLabels] = useState([labels])
+ const [modalOpen, setModalOpen] = useState(false)
+ const [currentLabel, setCurrentLabel] = useState(null) // Label being edited or null for new label
+ const queryClient = useQueryClient()
+ const handleAddLabel = () => {
+ setCurrentLabel(null) // Adding a new label
+ setModalOpen(true)
+ }
+
+ const handleEditLabel = label => {
+ setCurrentLabel(label) // Editing an existing label
+ setModalOpen(true)
+ }
+
+ const handleDeleteLabel = id => {
+ DeleteLabel(id).then(res => {
+ // Invalidate and refetch labels after deleting a label
+ const updatedLabels = userLabels.filter(label => label.id !== id)
+ setUserLabels(updatedLabels)
+
+ queryClient.invalidateQueries('labels')
+ })
+ // Implement deletion logic here
+ }
+
+ const handleSaveLabel = newOrUpdatedLabel => {
+ queryClient.invalidateQueries('labels')
+ setModalOpen(false)
+ const updatedLabels = userLabels.map(label =>
+ label.id === newOrUpdatedLabel.id ? newOrUpdatedLabel : label,
+ )
+ setUserLabels(updatedLabels)
+ }
+ useEffect(() => {
+ if (labels) {
+ setUserLabels(labels)
+ }
+ }, [labels])
+
+ if (isLabelsLoading) {
+ return (
+
+
+
+ )
+ }
+
+ if (isError) {
+ return (
+
+ Failed to load labels. Please try again.
+
+ )
+ }
+
+ return (
+
+
+
+
+ Label
+ Color
+ Actions
+
+
+
+ {userLabels.map(label => (
+
+ {label.name}
+
+
+
+
+ handleEditLabel(label)}>
+
+
+ handleDeleteLabel(label.id)}
+ color='danger'
+ >
+
+
+
+
+ ))}
+
+
+
+ {userLabels.length === 0 && (
+
+ No labels available. Add a new label to get started.
+
+ )}
+
+ {modalOpen && (
+ setModalOpen(false)}
+ onSave={handleSaveLabel}
+ label={currentLabel}
+ />
+ )}
+
+
+
+
+
+
+ )
+}
+
+export default LabelView
diff --git a/src/views/Labels/Labels.jsx b/src/views/Labels/Labels.jsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/views/Modals/Inputs/LabelModal.jsx b/src/views/Modals/Inputs/LabelModal.jsx
new file mode 100644
index 0000000..2fc03fb
--- /dev/null
+++ b/src/views/Modals/Inputs/LabelModal.jsx
@@ -0,0 +1,175 @@
+import {
+ Box,
+ Button,
+ FormControl,
+ Input,
+ Modal,
+ ModalDialog,
+ Option,
+ Select,
+ Typography,
+} from '@mui/joy'
+
+import React, { useEffect } from 'react'
+import { useQueryClient } from 'react-query'
+import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher'
+import LABEL_COLORS from '../../../utils/LabelColors'
+import { useLabels } from '../../Labels/LabelQueries'
+
+function LabelModal({ isOpen, onClose, onSave, label }) {
+ const [labelName, setLabelName] = React.useState('')
+ const [color, setColor] = React.useState('')
+ const [error, setError] = React.useState('')
+ const { data: userLabels, isLoadingLabels } = useLabels()
+ const queryClient = useQueryClient()
+
+ // Populate the form fields when editing
+ useEffect(() => {
+ if (label) {
+ setLabelName(label.name)
+ setColor(label.color)
+ } else {
+ setLabelName('')
+ setColor('')
+ }
+ setError('')
+ }, [label])
+
+ const validateLabel = () => {
+ if (!labelName || labelName.trim() === '') {
+ setError('Name cannot be empty')
+ return false
+ } else if (
+ !label ||
+ userLabels.some(
+ userLabel => userLabel.name === labelName && userLabel.id !== label.id,
+ )
+ ) {
+ setError('Label with this name already exists')
+ return false
+ } else if (color === '') {
+ setError('Please select a color')
+ return false
+ }
+ return true
+ }
+
+ const handleSave = () => {
+ if (!validateLabel()) {
+ return
+ }
+
+ const saveAction = label
+ ? UpdateLabel({ id: label.id, name: labelName, color })
+ : CreateLabel({ name: labelName, color })
+
+ saveAction.then(res => {
+ if (res.error) {
+ console.log(res.error)
+ setError('Failed to save label. Please try again.')
+ return
+ }
+ queryClient.invalidateQueries('labels').then(() => {
+ onSave({ id: label?.id, name: labelName, color })
+ onClose()
+ })
+ })
+ }
+
+ return (
+
+
+
+ {label ? 'Edit Label' : 'Add Label'}
+
+
+
+ Name
+
+ setLabelName(e.target.value)}
+ />
+
+
+ {/* Color Selection */}
+
+
+ Color:
+
+
+ (
+
+ }
+ >
+ {selected.label}
+
+ )}
+ onChange={(e, value) => {
+ value && setColor(value)
+ }}
+ >
+ {LABEL_COLORS.map(val => (
+
+
+
+
+ {val.name}
+
+
+
+ ))}
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ {label ? 'Save Changes' : 'Add Label'}
+
+
+ Cancel
+
+
+
+
+ )
+}
+
+export default LabelModal
diff --git a/src/views/components/AutocompleteSelect.jsx b/src/views/components/AutocompleteSelect.jsx
index 7708214..d7746f4 100644
--- a/src/views/components/AutocompleteSelect.jsx
+++ b/src/views/components/AutocompleteSelect.jsx
@@ -7,14 +7,18 @@ import * as React from 'react'
const filter = createFilterOptions()
-export default function FreeSoloCreateOption({ options, onSelectChange }) {
+export default function FreeSoloCreateOption({
+ options,
+ onSelectChange,
+ selected,
+}) {
React.useEffect(() => {
setValue(options)
}, [options])
- const [value, setValue] = React.useState([])
+ const [value, setValue] = React.useState([selected])
const [selectOptions, setSelectOptions] = React.useState(
- options ? options : [],
+ selected ? selected : [],
)
return (
@@ -38,26 +42,27 @@ export default function FreeSoloCreateOption({ options, onSelectChange }) {
}
onSelectChange(newValue)
}}
- filterOptions={(options, params) => {
- const filtered = filter(options, params)
+ filterOptions={(selected, params) => {
+ const filtered = filter(selected, params)
const { inputValue } = params
// Suggest the creation of a new value
- const isExisting = options.some(option => inputValue === option.title)
+ const isExisting = selected.some(
+ option => inputValue === option.title,
+ )
if (inputValue !== '' && !isExisting) {
filtered.push({
inputValue,
title: `Add "${inputValue}"`,
})
}
-
return filtered
}}
selectOnFocus
clearOnBlur
handleHomeEndKeys
// freeSolo
- options={selectOptions}
+ options={options}
getOptionLabel={option => {
// Value selected with enter, right from the input
if (typeof option === 'string') {
diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx
index 5537246..6545ac7 100644
--- a/src/views/components/NavBar.jsx
+++ b/src/views/components/NavBar.jsx
@@ -2,7 +2,7 @@ import Logo from '@/assets/logo.svg'
import {
AccountBox,
HomeOutlined,
- ListAltRounded,
+ ListAlt,
Logout,
MenuRounded,
Message,
@@ -30,6 +30,7 @@ const links = [
label: 'Home',
icon: ,
},
+
// {
// to: '/chores',
// label: 'Desktop View',
@@ -40,6 +41,11 @@ const links = [
label: 'Things',
icon: ,
},
+ {
+ to: 'labels',
+ label: 'Labels',
+ icon: ,
+ },
{
to: '/settings#sharing',
label: 'Sharing',