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..c025855 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "donetick", "private": true, - "version": "0.1.78", + "version": "0.1.79", "type": "module", "lint-staged": { "*.{js,jsx,ts,tsx}": [ @@ -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. - - - - )} + + + + + + + {needRefresh && ( + + + A new version is now available.Click on reload button to update. + + + + )} + + ,
) } 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/utils/Priorities.jsx b/src/utils/Priorities.jsx index 0d1f1f1..95b4ca6 100644 --- a/src/utils/Priorities.jsx +++ b/src/utils/Priorities.jsx @@ -10,21 +10,25 @@ const Priorities = [ name: 'P4', value: 4, icon: , + color: '', }, { name: 'P3 ', value: 3, icon: , + color: '', }, { name: 'P2', value: 2, icon: , + color: 'warning', }, { name: 'P1', value: 1, icon: , + color: 'danger', }, ] diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index 3a645ad..0954c81 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', @@ -67,6 +71,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) @@ -83,6 +88,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 = () => { @@ -173,7 +181,8 @@ const ChoreEdit = () => { isRolling: isRolling, isActive: isActive, notification: isNotificable, - labels: labels, + labels: labels.map(l => l.name), + labelsV2: labelsV2, notificationMetadata: notificationMetadata, thingTrigger: thingTrigger, } @@ -227,8 +236,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 @@ -276,6 +286,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) { @@ -330,6 +348,7 @@ const ChoreEdit = () => { }, }) } + return ( {/* @@ -727,8 +746,9 @@ const ChoreEdit = () => { Things to remember about this chore or to tag it - { const newLabels = [] changes.map(change => { @@ -742,7 +762,99 @@ const ChoreEdit = () => { }) setLabels(newLabels) }} - /> + /> */} + + {/* + + {labels?.map((label, index) => ( + + { + setLabels(labels.filter(l => l !== label)) + }} + checked={true} + overlay + variant='soft' + color='primary' + size='lg' + endDecorator={} + > + {label} + + + ))} + + */} {choreId > 0 && ( @@ -823,6 +935,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} + + ))} { {Priorities.map((priority, index) => ( { handleUpdatePriority(priority) }} + color={priority.color} > + {priority.icon} {priority.name} ))} { handleUpdatePriority({ name: 'No Priority', diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index ec5e33d..60af834 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -47,6 +47,7 @@ import { SkipChore, UpdateChoreAssignee, } from '../../utils/Fetcher' +import { getTextColorFromBackgroundColor } from '../../utils/LabelColors' import { Fetch } from '../../utils/TokenManager' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import DateModal from '../Modals/Inputs/DateModal' @@ -58,6 +59,7 @@ const ChoreCard = ({ performers, onChoreUpdate, onChoreRemove, + userLabels, sx, viewOnly, }) => { @@ -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 ( + + + + + + + + + + + {userLabels.map(label => ( + + + + + + ))} + +
LabelColorActions
{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: + + + + {error && ( + + {error} + + )} + + + + + + + + + ) +} + +export default LabelModal diff --git a/src/views/Things/ThingsView.jsx b/src/views/Things/ThingsView.jsx index 69b3a88..7187ebe 100644 --- a/src/views/Things/ThingsView.jsx +++ b/src/views/Things/ThingsView.jsx @@ -70,7 +70,6 @@ const ThingCard = ({ // flexDirection: 'row', // Change to 'row' justifyContent: 'space-between', p: 2, - backgroundColor: 'white', boxShadow: 'sm', borderRadius: 8, mb: 1, 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',