From d4c36e2057545e88ff4dbcb068888073abd98e4b Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sat, 28 Dec 2024 18:52:06 -0500 Subject: [PATCH] Initial Activity View Add New Colors.jsx Allow Dark model Toggle from the Navbar --- package-lock.json | 398 ++++++++++++++++++++++ package.json | 4 +- src/contexts/RouterContext.jsx | 7 +- src/queries/ChoreQueries.jsx | 23 +- src/utils/Chores.jsx | 160 +++++++++ src/utils/{LabelColors.jsx => Colors.jsx} | 54 +++ src/utils/Fetcher.jsx | 90 ++--- src/views/ChoreEdit/ChoreEdit.jsx | 128 ++++--- src/views/ChoreEdit/ChoreView.jsx | 2 +- src/views/Chores/ChoreCard.jsx | 23 +- src/views/Chores/IconButtonWithMenu.jsx | 2 +- src/views/Chores/MyChores.jsx | 197 +++-------- src/views/History/HistoryCard.jsx | 86 ++--- src/views/Modals/Inputs/LabelModal.jsx | 2 +- src/views/Settings/ThemeToggleButton.jsx | 51 +++ src/views/User/UserActivities.jsx | 395 +++++++++++++++++++++ src/views/components/NavBar.jsx | 14 +- 17 files changed, 1326 insertions(+), 310 deletions(-) create mode 100644 src/utils/Chores.jsx rename src/utils/{LabelColors.jsx => Colors.jsx} (56%) create mode 100644 src/views/Settings/ThemeToggleButton.jsx create mode 100644 src/views/User/UserActivities.jsx diff --git a/package-lock.json b/package-lock.json index 1530bf4..7e89160 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,11 +33,13 @@ "moment": "^2.30.1", "prop-types": "^15.8.1", "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", + "recharts": "^2.15.0", "reusify": "^1.0.4", "vite-plugin-pwa": "^0.20.0" }, @@ -4048,6 +4050,69 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -4303,6 +4368,15 @@ "vite": "^4 || ^5 || ^6" } }, + "node_modules/@wojtekmaj/date-utils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.1.tgz", + "integrity": "sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" + } + }, "node_modules/@xml-tools/parser": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@xml-tools/parser/-/parser-1.0.11.tgz", @@ -5794,6 +5868,127 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/dargs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", @@ -5919,6 +6114,12 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/decode-bmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/decode-bmp/-/decode-bmp-0.2.1.tgz", @@ -7112,6 +7313,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -7135,6 +7342,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -7719,6 +7935,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-user-locale": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.3.2.tgz", + "integrity": "sha512-O2GWvQkhnbDoWFUJfaBlDIKUEdND8ATpBXD6KXcbhxlfktyD/d8w6mkzM/IlQEqGZAMz/PW6j6Hv53BiigKLUQ==", + "license": "MIT", + "dependencies": { + "mem": "^8.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" + } + }, "node_modules/git-raw-commits": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", @@ -8248,6 +8476,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9204,6 +9441,18 @@ "dev": true, "license": "ISC" }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "license": "MIT", + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -9236,6 +9485,22 @@ "node": ">= 0.4" } }, + "node_modules/mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "license": "MIT", + "dependencies": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/mem?sponsor=1" + } + }, "node_modules/meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", @@ -9454,6 +9719,15 @@ "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", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -9976,6 +10250,15 @@ "node": ">= 0.8.0" } }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10743,6 +11026,31 @@ "node": ">=0.10.0" } }, + "node_modules/react-calendar": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-5.1.0.tgz", + "integrity": "sha512-09o/rQHPZGEi658IXAJtWfra1N69D1eFnuJ3FQm9qUVzlzNnos1+GWgGiUeSs22QOpNm32aoVFOimq0p3Ug9Eg==", + "license": "MIT", + "dependencies": { + "@wojtekmaj/date-utils": "^1.1.3", + "clsx": "^2.0.0", + "get-user-locale": "^2.2.1", + "warning": "^4.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -10820,6 +11128,21 @@ "react-dom": ">=16.8" } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -11041,6 +11364,44 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz", + "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -12716,6 +13077,12 @@ "readable-stream": "3" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", @@ -13243,6 +13610,28 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "5.4.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", @@ -13355,6 +13744,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 6897839..04db653 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "donetick", "private": true, - "version": "0.1.83", + "version": "0.1.85", "type": "module", "lint-staged": { "*.{js,jsx,ts,tsx}": [ @@ -47,11 +47,13 @@ "moment": "^2.30.1", "prop-types": "^15.8.1", "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", + "recharts": "^2.15.0", "reusify": "^1.0.4", "vite-plugin-pwa": "^0.20.0" }, diff --git a/src/contexts/RouterContext.jsx b/src/contexts/RouterContext.jsx index fa0bd4a..18081c0 100644 --- a/src/contexts/RouterContext.jsx +++ b/src/contexts/RouterContext.jsx @@ -5,6 +5,7 @@ import Error from '@/views/Error' import Settings from '@/views/Settings/Settings' import { RouterProvider, createBrowserRouter } from 'react-router-dom' import ForgotPasswordView from '../views/Authorization/ForgotPasswordView' +import LoginSettings from '../views/Authorization/LoginSettings' import LoginView from '../views/Authorization/LoginView' import SignupView from '../views/Authorization/Signup' import UpdatePasswordView from '../views/Authorization/UpdatePasswordView' @@ -21,7 +22,7 @@ import TermsView from '../views/Terms/TermsView' import TestView from '../views/TestView/Test' import ThingsHistory from '../views/Things/ThingsHistory' import ThingsView from '../views/Things/ThingsView' -import LoginSettings from '../views/Authorization/LoginSettings' +import UserActivities from '../views/User/UserActivities' const getMainRoute = () => { if (import.meta.env.VITE_IS_LANDING_DEFAULT === 'true') { return @@ -66,6 +67,10 @@ const Router = createBrowserRouter([ path: '/my/chores', element: , }, + { + path: '/activities', + element: , + }, { path: '/login', element: , diff --git a/src/queries/ChoreQueries.jsx b/src/queries/ChoreQueries.jsx index ad9369e..8b63ae1 100644 --- a/src/queries/ChoreQueries.jsx +++ b/src/queries/ChoreQueries.jsx @@ -1,9 +1,12 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useState } from 'react' import { useQuery } from 'react-query' -import { CreateChore, GetChoresNew } from '../utils/Fetcher' +import { CreateChore, GetChoresHistory, GetChoresNew } from '../utils/Fetcher' -export const useChores = () => { - return useQuery('chores', GetChoresNew) +export const useChores = includeArchive => { + return useQuery(['chores', includeArchive], () => + GetChoresNew(includeArchive), + ) } export const useCreateChore = () => { @@ -15,3 +18,17 @@ export const useCreateChore = () => { }, }) } + +export const useChoresHistory = initialLimit => { + const [limit, setLimit] = useState(initialLimit) // Initially, no limit is selected + + const { data, error, isLoading } = useQuery(['choresHistory', limit], () => + GetChoresHistory(limit), + ) + + const handleLimitChange = newLimit => { + setLimit(newLimit) + } + + return { data, error, isLoading, handleLimitChange } +} diff --git a/src/utils/Chores.jsx b/src/utils/Chores.jsx new file mode 100644 index 0000000..a295fb0 --- /dev/null +++ b/src/utils/Chores.jsx @@ -0,0 +1,160 @@ +import { TASK_COLOR } from './Colors.jsx' + +export const ChoresGrouper = (groupBy, chores) => { + // sort by priority then due date: + chores.sort((a, b) => { + // no priority is lowest priority: + if (a.priority === 0) { + return 1 + } + if (a.priority !== b.priority) { + return a.priority - b.priority + } + if (a.nextDueDate === null) { + return 1 + } + if (b.nextDueDate === null) { + return -1 + } + return new Date(a.nextDueDate) - new Date(b.nextDueDate) + }) + + var groups = [] + switch (groupBy) { + case 'due_date': + var groupRaw = { + Today: [], + 'In a week': [], + 'This month': [], + Later: [], + Overdue: [], + Anytime: [], + } + chores.forEach(chore => { + if (chore.nextDueDate === null) { + groupRaw['Anytime'].push(chore) + } else if (new Date(chore.nextDueDate) < new Date()) { + groupRaw['Overdue'].push(chore) + } else if ( + new Date(chore.nextDueDate).toDateString() === + new Date().toDateString() + ) { + groupRaw['Today'].push(chore) + } else if ( + new Date(chore.nextDueDate) < + new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) && + new Date(chore.nextDueDate) > new Date() + ) { + groupRaw['In a week'].push(chore) + } else if ( + new Date(chore.nextDueDate).getMonth() === new Date().getMonth() + ) { + groupRaw['This month'].push(chore) + } else { + groupRaw['Later'].push(chore) + } + }) + groups = [ + { + name: 'Overdue', + content: groupRaw['Overdue'], + color: TASK_COLOR.OVERDUE, + }, + { name: 'Today', content: groupRaw['Today'], color: TASK_COLOR.TODAY }, + { + name: 'In a week', + content: groupRaw['In a week'], + color: TASK_COLOR.IN_A_WEEK, + }, + { + name: 'This month', + content: groupRaw['This month'], + color: TASK_COLOR.THIS_MONTH, + }, + { name: 'Later', content: groupRaw['Later'], color: TASK_COLOR.LATER }, + { + name: 'Anytime', + content: groupRaw['Anytime'], + color: TASK_COLOR.ANYTIME, + }, + ] + break + case 'priority': + groupRaw = { + p1: [], + p2: [], + p3: [], + p4: [], + no_priority: [], + } + chores.forEach(chore => { + switch (chore.priority) { + case 1: + groupRaw['p1'].push(chore) + break + case 2: + groupRaw['p2'].push(chore) + break + case 3: + groupRaw['p3'].push(chore) + break + case 4: + groupRaw['p4'].push(chore) + break + default: + groupRaw['no_priority'].push(chore) + break + } + }) + groups = [ + { + name: 'Priority 1', + content: groupRaw['p1'], + color: TASK_COLOR.PRIORITY_1, + }, + { + name: 'Priority 2', + content: groupRaw['p2'], + color: TASK_COLOR.PRIORITY_2, + }, + { + name: 'Priority 3', + content: groupRaw['p3'], + color: TASK_COLOR.PRIORITY_3, + }, + { + name: 'Priority 4', + content: groupRaw['p4'], + color: TASK_COLOR.PRIORITY_4, + }, + { + name: 'No Priority', + content: groupRaw['no_priority'], + color: TASK_COLOR.NO_PRIORITY, + }, + ] + break + case 'labels': + groupRaw = {} + var labels = {} + chores.forEach(chore => { + chore.labelsV2.forEach(label => { + labels[label.id] = label + if (groupRaw[label.id] === undefined) { + groupRaw[label.id] = [] + } + groupRaw[label.id].push(chore) + }) + }) + groups = Object.keys(groupRaw).map(key => { + return { + name: labels[key].name, + content: groupRaw[key], + } + }) + groups.sort((a, b) => { + a.name < b.name ? 1 : -1 + }) + } + return groups +} diff --git a/src/utils/LabelColors.jsx b/src/utils/Colors.jsx similarity index 56% rename from src/utils/LabelColors.jsx rename to src/utils/Colors.jsx index d585d1b..b7f31e4 100644 --- a/src/utils/LabelColors.jsx +++ b/src/utils/Colors.jsx @@ -26,6 +26,60 @@ const LABEL_COLORS = [ { name: 'Sand', value: '#d7ccc8' }, ] +export const COLORS = { + white: '#FFFFFF', + salmon: '#ff7961', + teal: '#26a69a', + skyBlue: '#80d8ff', + grape: '#7e57c2', + sunshine: '#ffee58', + coral: '#ff7043', + lavender: '#ce93d8', + rose: '#f48fb1', + charcoal: '#616161', + sienna: '#8d6e63', + mint: '#a7ffeb', + amber: '#ffc107', + cobalt: '#3f51b5', + emerald: '#4caf50', + peach: '#ffab91', + ocean: '#0288d1', + mustard: '#ffca28', + ruby: '#d32f2f', + periwinkle: '#b39ddb', + turquoise: '#00bcd4', + lime: '#cddc39', + blush: '#f8bbd0', + ash: '#90a4ae', + sand: '#d7ccc8', +} + +export const TASK_COLOR = { + COMPLETED: '#4ec1a2', + LATE: '#f6ad55', + MISSED: '#F03A47', + UPCOMING: '#AF5B5B', + SKIPPED: '#E2C2FF', + + // For the calendar + OVERDUE: '#F03A47', + TODAY: '#ffc107', + IN_A_WEEK: '#4ec1a2', + THIS_MONTH: '#00bcd4', + LATER: '#d7ccc8', + ANYTIME: '#90a4ae', + + // FOR ASSIGNEE: + ASSIGNED_TO_ME: '#4ec1a2', + ASSIGNED_TO_OTHER: '#b39ddb', + + // FOR PRIORITY: + PRIORITY_1: '#F03A47', + PRIORITY_2: '#ffc107', + PRIORITY_3: '#00bcd4', + PRIORITY_4: '#7e57c2', + NO_PRIORITY: '#90a4ae', +} export default LABEL_COLORS export const getTextColorFromBackgroundColor = bgColor => { diff --git a/src/utils/Fetcher.jsx b/src/utils/Fetcher.jsx index 91489ff..655a15b 100644 --- a/src/utils/Fetcher.jsx +++ b/src/utils/Fetcher.jsx @@ -1,9 +1,5 @@ -import { API_URL } from '../Config' import { Fetch, HEADERS, apiManager } from './TokenManager' - - - const createChore = userID => { return Fetch(`/chores/`, { method: 'POST', @@ -33,7 +29,7 @@ const UpdatePassword = newPassword => { } const login = (username, password) => { - const baseURL = apiManager.getApiURL(); + const baseURL = apiManager.getApiURL() return fetch(`${baseURL}/auth/login`, { headers: { 'Content-Type': 'application/json', @@ -49,8 +45,13 @@ const GetAllUsers = () => { headers: HEADERS(), }) } -const GetChoresNew = async () => { - const resp = await Fetch(`/chores/`, { +const GetChoresNew = async includeArchived => { + var url = `/chores/` + if (includeArchived) { + url += `?includeArchived=true` + } + + const resp = await Fetch(url, { method: 'GET', headers: HEADERS(), }) @@ -95,7 +96,7 @@ const GetChoreDetailById = id => { }) } const MarkChoreComplete = (id, note, completedDate, performer) => { - var markChoreURL =`/chores/${id}/do` + var markChoreURL = `/chores/${id}/do` const body = { note, @@ -109,15 +110,13 @@ const MarkChoreComplete = (id, note, completedDate, performer) => { } if (performer) { body.performer = Number(performer) - if(completedDateFormated === ''){ - markChoreURL += `&performer=${performer}` - } - else{ + if (completedDateFormated === '') { + markChoreURL += `&performer=${performer}` + } else { markChoreURL += `?performer=${performer}` } } - return Fetch(markChoreURL, { method: 'POST', headers: HEADERS(), @@ -244,13 +243,10 @@ const LeaveCircle = id => { } const DeleteCircleMember = (circleID, memberID) => { - return Fetch( - `/circles/${circleID}/members/delete?member_id=${memberID}`, - { - method: 'DELETE', - headers: HEADERS(), - }, - ) + return Fetch(`/circles/${circleID}/members/delete?member_id=${memberID}`, { + method: 'DELETE', + headers: HEADERS(), + }) } const UpdateUserDetails = userDetails => { @@ -345,13 +341,12 @@ const GetLongLiveTokens = () => { headers: HEADERS(), }) } -const PutNotificationTarget = ( platform, deviceToken) => { +const PutNotificationTarget = (platform, deviceToken) => { return Fetch(`/users/targets`, { method: 'PUT', headers: HEADERS(), - body: JSON.stringify({ platform,deviceToken }), - } - ) + body: JSON.stringify({ platform, deviceToken }), + }) } const CreateLabel = label => { return Fetch(`/labels`, { @@ -383,22 +378,19 @@ const DeleteLabel = id => { }) } -const ChangePassword = (verifiticationCode, password) => { - const baseURL = apiManager.getApiURL(); - return fetch( - `${baseURL}/auth/password?c=${verifiticationCode}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ password: password }), +const ChangePassword = (verifiticationCode, password) => { + const baseURL = apiManager.getApiURL() + return fetch(`${baseURL}/auth/password?c=${verifiticationCode}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, - ) + body: JSON.stringify({ password: password }), + }) } const ResetPassword = email => { - const basedURL = apiManager.getApiURL(); + const basedURL = apiManager.getApiURL() return fetch(`${basedURL}/auth/reset`, { method: 'POST', headers: { @@ -422,24 +414,34 @@ const UpdateDueDate = (id, dueDate) => { } const RefreshToken = () => { - const basedURL = apiManager.getApiURL(); + const basedURL = apiManager.getApiURL() return fetch(basedURL + '/auth/refresh', { method: 'GET', headers: HEADERS(), }) } +const GetChoresHistory = async limit => { + var url = `/chores/history` + if (limit) { + url += `?limit=${limit}` + } + const resp = await Fetch(url, { + method: 'GET', + headers: HEADERS(), + }) + return resp.json() +} export { AcceptCircleMemberRequest, ArchiveChore, CancelSubscription, - createChore, + ChangePassword, CreateChore, CreateLabel, CreateLongLiveToken, CreateThing, DeleteChore, DeleteChoreHistory, - ChangePassword, DeleteCircleMember, DeleteLabel, DeleteLongLiveToken, @@ -451,6 +453,7 @@ export { GetChoreDetailById, GetChoreHistory, GetChores, + GetChoresHistory, GetChoresNew, GetCircleMemberRequests, GetLabels, @@ -462,14 +465,12 @@ export { GetUserProfile, JoinCircle, LeaveCircle, - login, MarkChoreComplete, - RefreshToken, - ResetPassword, PutNotificationTarget, + RefreshToken, + ResetPassword, SaveChore, SaveThing, - signUp, SkipChore, UnArchiveChore, UpdateChoreAssignee, @@ -481,4 +482,7 @@ export { UpdatePassword, UpdateThingState, UpdateUserDetails, + createChore, + login, + signUp, } diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index 6bfefa5..d6f5196 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -9,6 +9,7 @@ import { Divider, FormControl, FormHelperText, + FormLabel, Input, List, ListItem, @@ -20,12 +21,14 @@ import { Sheet, Snackbar, Stack, + Switch, Typography, } from '@mui/joy' import moment from 'moment' import { useContext, useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { UserContext } from '../../contexts/UserContext' +import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { CreateChore, DeleteChore, @@ -36,7 +39,6 @@ import { SaveChore, } from '../../utils/Fetcher' import { isPlusAccount } from '../../utils/Helpers' -import { getTextColorFromBackgroundColor } from '../../utils/LabelColors' import { useLabels } from '../Labels/LabelQueries' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import LabelModal from '../Modals/Inputs/LabelModal' @@ -72,6 +74,7 @@ const ChoreEdit = () => { const [frequencyMetadata, setFrequencyMetadata] = useState({}) const [labels, setLabels] = useState([]) const [labelsV2, setLabelsV2] = useState([]) + const [points, setPoints] = useState(-1) const [allUserThings, setAllUserThings] = useState([]) const [thingTrigger, setThingTrigger] = useState(null) const [isThingValid, setIsThingValid] = useState(false) @@ -196,6 +199,7 @@ const ChoreEdit = () => { labelsV2: labelsV2, notificationMetadata: notificationMetadata, thingTrigger: thingTrigger, + points: points < 0 ? null : points, } let SaveFunction = CreateChore if (choreId > 0) { @@ -247,6 +251,9 @@ const ChoreEdit = () => { setFrequency(data.res.frequency) setNotificationMetadata(JSON.parse(data.res.notificationMetadata)) + setPoints( + data.res.points && data.res.points > -1 ? data.res.points : -1, + ) // setLabels(data.res.labels ? data.res.labels.split(',') : []) setLabelsV2(data.res.labelsV2) @@ -758,23 +765,6 @@ const ChoreEdit = () => { Things to remember about this chore or to tag it - {/* { - const newLabels = [] - changes.map(change => { - // if type is string : - if (typeof change === 'string') { - // add the change to the labels array: - newLabels.push(change) - } else { - newLabels.push(change.inputValue) - } - }) - setLabels(newLabels) - }} - /> */} - {/* - - {labels?.map((label, index) => ( - - { - setLabels(labels.filter(l => l !== label)) - }} - checked={true} - overlay - variant='soft' - color='primary' - size='lg' - endDecorator={} - > - {label} - - - ))} - - */} + + + + Others : + + + +
+ Assign Points + + Assign points to this task and user will earn points when they + completed it + +
+ -1} + onClick={event => { + event.preventDefault() + if (points > -1) { + setPoints(-1) + } else { + setPoints(1) + } + }} + color={points !== -1 ? 'success' : 'neutral'} + variant={points !== -1 ? 'solid' : 'outlined'} + // endDecorator={points !== -1 ? 'On' : 'Off'} + slotProps={{ + endDecorator: { + sx: { + minWidth: 24, + }, + }, + }} + /> +
+ + {points != -1 && ( + + + Points: + + { + setPoints(parseInt(e.target.value)) + }} + /> + + + )} +
+ {choreId > 0 && ( - + { )} + {/* diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx index f40dab3..35f2efa 100644 --- a/src/views/ChoreEdit/ChoreView.jsx +++ b/src/views/ChoreEdit/ChoreView.jsx @@ -36,6 +36,7 @@ import { Divider } from '@mui/material' import moment from 'moment' import { useEffect, useState } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' +import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { GetAllUsers, GetChoreDetailById, @@ -43,7 +44,6 @@ import { SkipChore, UpdateChorePriority, } from '../../utils/Fetcher' -import { getTextColorFromBackgroundColor } from '../../utils/LabelColors' import Priorities from '../../utils/Priorities' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' const IconCard = styled('div')({ diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index c8b37e7..7eb33ea 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -42,8 +42,8 @@ import { import moment from 'moment' import React, { useEffect } from 'react' import { useNavigate } from 'react-router-dom' -import { API_URL } from '../../Config' import { UserContext } from '../../contexts/UserContext' +import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { ArchiveChore, DeleteChore, @@ -53,9 +53,7 @@ import { UpdateChoreAssignee, UpdateDueDate, } from '../../utils/Fetcher' -import { getTextColorFromBackgroundColor } from '../../utils/LabelColors' import Priorities from '../../utils/Priorities' -import { Fetch } from '../../utils/TokenManager' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import DateModal from '../Modals/Inputs/DateModal' import SelectModal from '../Modals/Inputs/SelectModal' @@ -130,8 +128,7 @@ const ChoreCard = ({ message: 'Are you sure you want to delete this chore?', onClose: isConfirmed => { if (isConfirmed === true) { - DeleteChore(chore.id) - .then(response => { + DeleteChore(chore.id).then(response => { if (response.ok) { onChoreRemove(chore) } @@ -181,7 +178,7 @@ const ChoreCard = ({ }, 1000) const id = setTimeout(() => { - MarkChoreComplete(chore.id, null, null,null) + MarkChoreComplete(chore.id, null, null, null) .then(resp => { if (resp.ok) { return resp.json().then(data => { @@ -221,9 +218,13 @@ const ChoreCard = ({ alert('Please select a performer') return } - - MarkChoreComplete(chore.id, null, new Date(newDate).toISOString(), null) - .then(response => { + + MarkChoreComplete( + chore.id, + null, + new Date(newDate).toISOString(), + null, + ).then(response => { if (response.ok) { response.json().then(data => { const newChore = data.res @@ -243,9 +244,7 @@ const ChoreCard = ({ }) } const handleCompleteWithNote = note => { - - MarkChoreComplete(chore.id, note, null, null) - .then(response => { + MarkChoreComplete(chore.id, note, null, null).then(response => { if (response.ok) { response.json().then(data => { const newChore = data.res diff --git a/src/views/Chores/IconButtonWithMenu.jsx b/src/views/Chores/IconButtonWithMenu.jsx index 0adb89c..bd7db7c 100644 --- a/src/views/Chores/IconButtonWithMenu.jsx +++ b/src/views/Chores/IconButtonWithMenu.jsx @@ -1,7 +1,7 @@ import { Chip, Menu, MenuItem, Typography } from '@mui/joy' import IconButton from '@mui/joy/IconButton' import React, { useEffect, useRef, useState } from 'react' -import { getTextColorFromBackgroundColor } from '../../utils/LabelColors' +import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' const IconButtonWithMenu = ({ key, diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index ca549f8..e5ced06 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -43,7 +43,11 @@ import { useLabels } from '../Labels/LabelQueries' import ChoreCard from './ChoreCard' import IconButtonWithMenu from './IconButtonWithMenu' -import { canScheduleNotification, scheduleChoreNotification } from './LocalNotificationScheduler' +import { ChoresGrouper } from '../../utils/Chores' +import { + canScheduleNotification, + scheduleChoreNotification, +} from './LocalNotificationScheduler' import NotificationAccessSnackbar from './NotificationAccessSnackbar' const MyChores = () => { @@ -92,159 +96,40 @@ const MyChores = () => { return aDueDate - bDueDate // Sort ascending by due date } - const sectionSorter = (t, chores) => { - // sort by priority then due date: - chores.sort((a, b) => { - // no priority is lowest priority: - if (a.priority === 0) { - return 1 - } - if (a.priority !== b.priority) { - return a.priority - b.priority - } - if (a.nextDueDate === null) { - return 1 - } - if (b.nextDueDate === null) { - return -1 - } - return new Date(a.nextDueDate) - new Date(b.nextDueDate) - }) - - var groups = [] - switch (t) { - case 'due_date': - var groupRaw = { - Today: [], - 'In a week': [], - 'This month': [], - Later: [], - Overdue: [], - Anytime: [], - } - chores.forEach(chore => { - if (chore.nextDueDate === null) { - groupRaw['Anytime'].push(chore) - } else if (new Date(chore.nextDueDate) < new Date()) { - groupRaw['Overdue'].push(chore) - } else if ( - new Date(chore.nextDueDate).toDateString() === - new Date().toDateString() - ) { - groupRaw['Today'].push(chore) - } else if ( - new Date(chore.nextDueDate) < - new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) && - new Date(chore.nextDueDate) > new Date() - ) { - groupRaw['In a week'].push(chore) - } else if ( - new Date(chore.nextDueDate).getMonth() === new Date().getMonth() - ) { - groupRaw['This month'].push(chore) - } else { - groupRaw['Later'].push(chore) - } - }) - groups = [ - { name: 'Overdue', content: groupRaw['Overdue'] }, - { name: 'Today', content: groupRaw['Today'] }, - { name: 'In a week', content: groupRaw['In a week'] }, - { name: 'This month', content: groupRaw['This month'] }, - { name: 'Later', content: groupRaw['Later'] }, - { name: 'Anytime', content: groupRaw['Anytime'] }, - ] - break - case 'priority': - groupRaw = { - p1: [], - p2: [], - p3: [], - p4: [], - no_priority: [], - } - chores.forEach(chore => { - switch (chore.priority) { - case 1: - groupRaw['p1'].push(chore) - break - case 2: - groupRaw['p2'].push(chore) - break - case 3: - groupRaw['p3'].push(chore) - break - case 4: - groupRaw['p4'].push(chore) - break - default: - groupRaw['no_priority'].push(chore) - break - } - }) - groups = [ - { name: 'Priority 1', content: groupRaw['p1'] }, - { name: 'Priority 2', content: groupRaw['p2'] }, - { name: 'Priority 3', content: groupRaw['p3'] }, - { name: 'Priority 4', content: groupRaw['p4'] }, - { name: 'No Priority', content: groupRaw['no_priority'] }, - ] - break - case 'labels': - groupRaw = {} - var labels = {} - chores.forEach(chore => { - chore.labelsV2.forEach(label => { - labels[label.id] = label - if (groupRaw[label.id] === undefined) { - groupRaw[label.id] = [] - } - groupRaw[label.id].push(chore) - }) - }) - groups = Object.keys(groupRaw).map(key => { - return { - name: labels[key].name, - content: groupRaw[key], - } - }) - groups.sort((a, b) => { - a.name < b.name ? 1 : -1 - }) - } - return groups - } useEffect(() => { - - - - Promise.all([GetChores(), GetAllUsers(),GetUserProfile()]).then(responses => { - const [choresResponse, usersResponse, userProfileResponse] = responses; - if (!choresResponse.ok) { - throw new Error(choresResponse.statusText); - } - if (!usersResponse.ok) { - throw new Error(usersResponse.statusText); - } - if (!userProfileResponse.ok) { - throw new Error(userProfileResponse.statusText); + Promise.all([GetChores(), GetAllUsers(), GetUserProfile()]).then( + responses => { + const [choresResponse, usersResponse, userProfileResponse] = responses + if (!choresResponse.ok) { + throw new Error(choresResponse.statusText) } - Promise.all([choresResponse.json(), usersResponse.json(), userProfileResponse.json()]).then(data => { - const [choresData, usersData, userProfileData] = data; - setUserProfile(userProfileData.res); - choresData.res.sort(choreSorter); - setChores(choresData.res); - setFilteredChores(choresData.res); - setPerformers(usersData.res); + if (!usersResponse.ok) { + throw new Error(usersResponse.statusText) + } + if (!userProfileResponse.ok) { + throw new Error(userProfileResponse.statusText) + } + Promise.all([ + choresResponse.json(), + usersResponse.json(), + userProfileResponse.json(), + ]).then(data => { + const [choresData, usersData, userProfileData] = data + setUserProfile(userProfileData.res) + choresData.res.sort(choreSorter) + setChores(choresData.res) + setFilteredChores(choresData.res) + setPerformers(usersData.res) if (canScheduleNotification()) { - scheduleChoreNotification(choresData.res, userProfileData.res, usersData.res); + scheduleChoreNotification( + choresData.res, + userProfileData.res, + usersData.res, + ) } - }); - - - }) - - + }) + }, + ) // GetAllUsers() // .then(response => response.json()) @@ -266,7 +151,7 @@ const MyChores = () => { const sortedChores = choresData.res.sort(choreSorter) setChores(sortedChores) setFilteredChores(sortedChores) - const sections = sectionSorter('due_date', sortedChores) + const sections = ChoresGrouper('due_date', sortedChores) setChoreSections(sections) setOpenChoreSections( Object.keys(sections).reduce((acc, key) => { @@ -354,7 +239,7 @@ const MyChores = () => { } setChores(newChores) setFilteredChores(newFilteredChores) - setChoreSections(sectionSorter('due_date', newChores)) + setChoreSections(ChoresGrouper('due_date', newChores)) switch (event) { case 'completed': @@ -385,7 +270,7 @@ const MyChores = () => { ) setChores(newChores) setFilteredChores(newFilteredChores) - setChoreSections(sectionSorter('due_date', newChores)) + setChoreSections(ChoresGrouper('due_date', newChores)) } const searchOptions = { @@ -419,13 +304,13 @@ const MyChores = () => { setSearchTerm(term) setFilteredChores(fuse.search(term).map(result => result.item)) } - + if ( userProfile === null || userLabelsLoading || performers.length === 0 || choresLoading - ) { + ) { return } @@ -580,7 +465,7 @@ const MyChores = () => { ]} selectedItem={selectedChoreSection} onItemSelect={selected => { - const section = sectionSorter(selected.value, chores) + const section = ChoresGrouper(selected.value, chores) setChoreSections(section) setSelectedChoreSection(selected.value) setFilteredChores(chores) diff --git a/src/views/History/HistoryCard.jsx b/src/views/History/HistoryCard.jsx index 34a0b33..04e999c 100644 --- a/src/views/History/HistoryCard.jsx +++ b/src/views/History/HistoryCard.jsx @@ -11,6 +11,49 @@ import { } from '@mui/joy' import moment from 'moment' +export const getCompletedChip = historyEntry => { + var text = 'No Due Date' + var color = 'info' + var icon = + // if completed few hours +-6 hours + if ( + historyEntry.dueDate && + historyEntry.completedAt > historyEntry.dueDate - 1000 * 60 * 60 * 6 && + historyEntry.completedAt < historyEntry.dueDate + 1000 * 60 * 60 * 6 + ) { + text = 'On Time' + color = 'success' + icon = + } else if ( + historyEntry.dueDate && + historyEntry.completedAt < historyEntry.dueDate + ) { + text = 'On Time' + color = 'success' + icon = + } + + // if completed after due date then it's late + else if ( + historyEntry.dueDate && + historyEntry.completedAt > historyEntry.dueDate + ) { + text = 'Late' + color = 'warning' + icon = + } else { + text = 'No Due Date' + color = 'neutral' + icon = + } + + return ( + + {text} + + ) +} + const HistoryCard = ({ allHistory, performers, @@ -38,49 +81,6 @@ const HistoryCard = ({ return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}` } - const getCompletedChip = historyEntry => { - var text = 'No Due Date' - var color = 'info' - var icon = - // if completed few hours +-6 hours - if ( - historyEntry.dueDate && - historyEntry.completedAt > historyEntry.dueDate - 1000 * 60 * 60 * 6 && - historyEntry.completedAt < historyEntry.dueDate + 1000 * 60 * 60 * 6 - ) { - text = 'On Time' - color = 'success' - icon = - } else if ( - historyEntry.dueDate && - historyEntry.completedAt < historyEntry.dueDate - ) { - text = 'On Time' - color = 'success' - icon = - } - - // if completed after due date then it's late - else if ( - historyEntry.dueDate && - historyEntry.completedAt > historyEntry.dueDate - ) { - text = 'Late' - color = 'warning' - icon = - } else { - text = 'No Due Date' - color = 'neutral' - icon = - } - - return ( - - {text} - - ) - } - return ( <> diff --git a/src/views/Modals/Inputs/LabelModal.jsx b/src/views/Modals/Inputs/LabelModal.jsx index 3021611..6f7599c 100644 --- a/src/views/Modals/Inputs/LabelModal.jsx +++ b/src/views/Modals/Inputs/LabelModal.jsx @@ -12,8 +12,8 @@ import { import React, { useEffect } from 'react' import { useQueryClient } from 'react-query' +import LABEL_COLORS from '../../../utils/Colors.jsx' import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher' -import LABEL_COLORS from '../../../utils/LabelColors' import { useLabels } from '../../Labels/LabelQueries' function LabelModal({ isOpen, onClose, onSave, label }) { diff --git a/src/views/Settings/ThemeToggleButton.jsx b/src/views/Settings/ThemeToggleButton.jsx new file mode 100644 index 0000000..055efe4 --- /dev/null +++ b/src/views/Settings/ThemeToggleButton.jsx @@ -0,0 +1,51 @@ +import useStickyState from '@/hooks/useStickyState' +import { + BrightnessAuto, + DarkModeOutlined, + LightModeOutlined, +} from '@mui/icons-material' +import { FormControl, IconButton, useColorScheme } from '@mui/joy' + +const ELEMENTID = 'select-theme-mode' + +const ThemeToggleButton = ({ sx }) => { + const { mode, setMode } = useColorScheme() + const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode') + + const handleThemeModeChange = e => { + e.preventDefault() + e.stopPropagation() + + let newThemeMode + switch (themeMode) { + case 'light': + newThemeMode = 'dark' + break + case 'dark': + newThemeMode = 'system' + break + case 'system': + default: + newThemeMode = 'light' + break + } + setThemeMode(newThemeMode) + setMode(newThemeMode) + } + + return ( + + + {themeMode === 'light' ? ( + + ) : themeMode === 'dark' ? ( + + ) : ( + + )} + + + ) +} + +export default ThemeToggleButton diff --git a/src/views/User/UserActivities.jsx b/src/views/User/UserActivities.jsx new file mode 100644 index 0000000..af9f5e5 --- /dev/null +++ b/src/views/User/UserActivities.jsx @@ -0,0 +1,395 @@ +import CancelIcon from '@mui/icons-material/Cancel' +import CheckCircleIcon from '@mui/icons-material/CheckCircle' +import CircleIcon from '@mui/icons-material/Circle' +import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts' + +import { Toll } from '@mui/icons-material' +import { + Box, + Card, + Chip, + Container, + Divider, + Grid, + Stack, + Tab, + TabList, + Tabs, + Typography, +} from '@mui/joy' +import React, { useEffect } from 'react' +import { UserContext } from '../../contexts/UserContext' +import { useChores, useChoresHistory } from '../../queries/ChoreQueries' +import { ChoresGrouper } from '../../utils/Chores' +import { TASK_COLOR } from '../../utils/Colors.jsx' +import LoadingComponent from '../components/Loading' + +const groupByDate = history => { + const aggregated = {} + for (let i = 0; i < history.length; i++) { + const item = history[i] + const date = new Date(item.completedAt).toLocaleDateString() + if (!aggregated[date]) { + aggregated[date] = [] + } + aggregated[date].push(item) + } + return aggregated +} + +const ChoreHistoryItem = ({ time, name, points, status }) => { + const statusIcon = { + completed: , + missed: , + pending: , + } + + return ( + + + {time} + + + {statusIcon[status] ? statusIcon[status] : statusIcon['completed']} + + + + {name} + + {points && ( + }> + {`${points} points`} + + )} + + + ) +} + +const ChoreHistoryTimeline = ({ history }) => { + const groupedHistory = groupByDate(history) + + return ( + + + Activities Timeline + + + {Object.entries(groupedHistory).map(([date, items]) => ( + + + {new Date(date).toLocaleDateString([], { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + })} + + + + {items.map(record => ( + <> + + + ))} + + + ))} + + ) +} + +const renderPieChart = (data, size, isPrimary) => ( + + + {data.map((entry, index) => ( + + ))} + + {isPrimary && } + {isPrimary && ( + `${label}: ${value.payload.value}`} + /> + )} + +) + +const UserActivites = () => { + const { userProfile } = React.useContext(UserContext) + const [tabValue, setTabValue] = React.useState(30) + const [selectedHistory, setSelectedHistory] = React.useState([]) + const [selectedChart, setSelectedChart] = React.useState('history') + + const [historyPieChartData, setHistoryPieChartData] = React.useState([]) + const [choreDuePieChartData, setChoreDuePieChartData] = React.useState([]) + const [choresAssignedChartData, setChoresAssignedChartData] = React.useState( + [], + ) + const [choresPriorityChartData, setChoresPriorityChartData] = React.useState( + [], + ) + const { data: choresData, isLoading: isChoresLoading } = useChores(true) + const { + data: choresHistory, + isChoresHistoryLoading, + handleLimitChange: refetchHistory, + } = useChoresHistory(tabValue ? tabValue : 30) + useEffect(() => { + if (!isChoresHistoryLoading && !isChoresLoading && choresHistory) { + const enrichedHistory = choresHistory.res.map(item => { + const chore = choresData.res.find(chore => chore.id === item.choreId) + return { + ...item, + choreName: chore?.name, + } + }) + + setSelectedHistory(enrichedHistory) + setHistoryPieChartData(generateHistoryPieChartData(enrichedHistory)) + } + }, [isChoresHistoryLoading, isChoresLoading, choresHistory]) + + useEffect(() => { + if (!isChoresLoading && choresData) { + const choreDuePieChartData = generateChoreDuePieChartData(choresData.res) + setChoreDuePieChartData(choreDuePieChartData) + setChoresAssignedChartData(generateChoreAssignedChartData(choresData.res)) + setChoresPriorityChartData( + generateChorePriorityPieChartData(choresData.res), + ) + } + }, [isChoresLoading, choresData]) + + const generateChoreAssignedChartData = chores => { + var assignedToMe = 0 + var assignedToOthers = 0 + chores.forEach(chore => { + if (chore.assignedTo === userProfile.id) { + assignedToMe++ + } else assignedToOthers++ + }) + + const group = [] + if (assignedToMe > 0) { + group.push({ + label: `Assigned to me`, + value: assignedToMe, + color: TASK_COLOR.ASSIGNED_TO_ME, + id: 1, + }) + } + if (assignedToOthers > 0) { + group.push({ + label: `Assigned to others`, + value: assignedToOthers, + color: TASK_COLOR.ASSIGNED_TO_OTHERS, + id: 2, + }) + } + return group + } + + const generateChoreDuePieChartData = chores => { + const groups = ChoresGrouper('due_date', chores) + return groups + .map(group => { + return { + label: group.name, + value: group.content.length, + color: group.color, + id: group.name, + } + }) + .filter(item => item.value > 0) + } + const generateChorePriorityPieChartData = chores => { + const groups = ChoresGrouper('priority', chores) + return groups + .map(group => { + return { + label: group.name, + value: group.content.length, + color: group.color, + id: group.name, + } + }) + .filter(item => item.value > 0) + } + + const generateHistoryPieChartData = history => { + const totalCompleted = history.filter( + item => item.dueDate > item.completedAt, + ).length + const totalLate = history.filter( + item => item.dueDate < item.completedAt, + ).length + + return [ + { + label: `On time`, + value: totalCompleted, + color: '#4ec1a2', + id: 1, + }, + { + label: `Late`, + value: totalLate, + color: '#f6ad55', + id: 2, + }, + ] + } + if (isChoresHistoryLoading || isChoresLoading) { + return + } + const COLORS = historyPieChartData.map(item => item.color) + const chartData = { + history: { + data: historyPieChartData, + title: 'Status', + description: 'Completed tasks status', + }, + due: { + data: choreDuePieChartData, + title: 'Due Date', + description: 'Current tasks due date', + }, + assigned: { + data: choresAssignedChartData, + title: 'Assignee', + description: 'Tasks assigned to you vs others', + }, + priority: { + data: choresPriorityChartData, + title: 'Priority', + description: 'Tasks by priority', + }, + } + + return ( + + { + setTabValue(tabValue) + refetchHistory(tabValue) + }} + defaultValue={7} + sx={{ + py: 0.5, + borderRadius: 16, + maxWidth: 400, + mb: 1, + }} + > + + {[ + { label: '7 Days', value: 7 }, + { label: '30 Days', value: 30 }, + { label: '90 Days', value: 90 }, + ].map((tab, index) => ( + + {tab.label} + + ))} + + + + + {chartData[selectedChart].title} + + + {chartData[selectedChart].description} + + {renderPieChart(chartData[selectedChart].data, 250, true)} + + + {Object.entries(chartData) + .filter(([key]) => key !== selectedChart) + .map(([key, { data, title }]) => ( + + setSelectedChart(key)} + sx={{ cursor: 'pointer', p: 1 }} + > + + {title} + + {renderPieChart(data, 75, false)} + + + ))} + + + + ) +} + +export default UserActivites diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx index 742da5d..46af5ce 100644 --- a/src/views/components/NavBar.jsx +++ b/src/views/components/NavBar.jsx @@ -1,6 +1,7 @@ import Logo from '@/assets/logo.svg' import { AccountBox, + History, HomeOutlined, ListAlt, Logout, @@ -23,6 +24,7 @@ import { import { useState } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import { version } from '../../../package.json' +import ThemeToggleButton from '../Settings/ThemeToggleButton' import NavBarLink from './NavBarLink' const links = [ { @@ -41,6 +43,11 @@ const links = [ label: 'Things', icon: , }, + { + to: 'activities', + label: 'Activities', + icon: , + }, { to: 'labels', label: 'Labels', @@ -120,7 +127,12 @@ const NavBar = () => { tick✓ - +