diff --git a/package-lock.json b/package-lock.json index fb72e97..f5eb71b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,14 @@ { "name": "donetick", - "version": "0.1.94", + "version": "0.1.98", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "donetick", - "version": "0.1.94", + "version": "0.1.98", "dependencies": { + "@capacitor-community/sqlite": "^6.0.2", "@capacitor/android": "^6.1.1", "@capacitor/app": "^6.0.0", "@capacitor/cli": "^6.1.1", @@ -15,6 +16,7 @@ "@capacitor/device": "^6.0.2", "@capacitor/ios": "6.1.1", "@capacitor/local-notifications": "^6.0.0", + "@capacitor/network": "^6.0.3", "@capacitor/preferences": "^6.0.1", "@capacitor/push-notifications": "^6.0.1", "@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4", @@ -31,9 +33,11 @@ "chrono-node": "^2.7.7", "dotenv": "^16.4.5", "esm": "^3.2.25", + "farmhash": "^4.0.1", "fuse.js": "^7.0.0", "js-cookie": "^3.0.5", "moment": "^2.30.1", + "murmurhash": "^2.0.1", "prop-types": "^15.8.1", "react": "^18.2.0", "react-calendar": "^5.1.0", @@ -1590,6 +1594,21 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@capacitor-community/sqlite": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz", + "integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==", + "license": "MIT", + "dependencies": { + "jeep-sqlite": "^2.7.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@capacitor/android": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-6.2.0.tgz", @@ -1755,6 +1774,15 @@ "@capacitor/core": "^6.0.0" } }, + "node_modules/@capacitor/network": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/network/-/network-6.0.3.tgz", + "integrity": "sha512-ODRgVY8AMIDXPoND6OvaaeMwRhtnq+K/IK7IEKV1pSg8CqJgqoD6je0omZnDqM+S1BKI2JOm8pe3gVvtcoehdw==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@capacitor/preferences": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-6.0.3.tgz", @@ -3727,6 +3755,133 @@ "win32" ] }, + "node_modules/@stencil/core": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.28.2.tgz", + "integrity": "sha512-sHwBNionxzHfmjQ93ffaQOBNvd5HrSn8e5KSsiBBaODgC/g9squZoARYHHJ0VFfUpOXkEoZQ+ASixpUBafG9bA==", + "license": "MIT", + "bin": { + "stencil": "bin/stencil" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.10.0" + }, + "optionalDependencies": { + "@rollup/rollup-darwin-arm64": "4.34.9", + "@rollup/rollup-darwin-x64": "4.34.9", + "@rollup/rollup-linux-arm64-gnu": "4.34.9", + "@rollup/rollup-linux-arm64-musl": "4.34.9", + "@rollup/rollup-linux-x64-gnu": "4.34.9", + "@rollup/rollup-linux-x64-musl": "4.34.9", + "@rollup/rollup-win32-arm64-msvc": "4.34.9", + "@rollup/rollup-win32-x64-msvc": "4.34.9" + } + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", + "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", + "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", + "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", + "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", + "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", + "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", + "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@stencil/core/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", + "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -5015,7 +5170,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "devOptional": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -5149,6 +5303,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/browser-fs-access": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz", + "integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw==", + "license": "Apache-2.0" + }, "node_modules/browserslist": { "version": "4.24.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", @@ -5185,7 +5345,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "devOptional": true, "funding": [ { "type": "github", @@ -5838,7 +5997,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, "node_modules/cosmiconfig": { @@ -6227,7 +6385,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "devOptional": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -6243,7 +6400,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=4.0.0" @@ -6405,7 +6561,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -6636,7 +6791,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "devOptional": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -7397,12 +7551,34 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "devOptional": true, "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" } }, + "node_modules/farmhash": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/farmhash/-/farmhash-4.0.1.tgz", + "integrity": "sha512-7OqIJ+tvlABfz+oNyZA6C5YAnhFQWzqyNC7C1R3jAYfEjB2ghuiX6WSCzmy01Sy+yJppMXD3hvpn8FXrZbbGEg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "node-addon-api": "^8.1.0", + "prebuild-install": "^7.1.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/farmhash/node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7733,7 +7909,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "devOptional": true, "license": "MIT" }, "node_modules/fs-extra": { @@ -8103,7 +8278,6 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "devOptional": true, "license": "MIT" }, "node_modules/glob": { @@ -8446,7 +8620,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "devOptional": true, "funding": [ { "type": "github", @@ -8473,6 +8646,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -9130,6 +9309,19 @@ "node": "*" } }, + "node_modules/jeep-sqlite": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jeep-sqlite/-/jeep-sqlite-2.8.0.tgz", + "integrity": "sha512-FWNUP6OAmrUHwiW7H1xH5YUQ8tN2O4l4psT1sLd7DQtHd5PfrA1nvNdeKPNj+wQBtu7elJa8WoUibTytNTaaCg==", + "license": "MIT", + "dependencies": { + "@stencil/core": "^4.20.0", + "browser-fs-access": "^0.35.0", + "jszip": "^3.10.1", + "localforage": "^1.10.0", + "sql.js": "^1.11.0" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -9309,6 +9501,54 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9361,6 +9601,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -9420,6 +9669,24 @@ "node": ">=4" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/localforage/node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9806,7 +10073,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -9845,7 +10111,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9916,7 +10181,6 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "devOptional": true, "license": "MIT" }, "node_modules/modify-values": { @@ -9944,6 +10208,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "license": "MIT" }, + "node_modules/murmurhash": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/murmurhash/-/murmurhash-2.0.1.tgz", + "integrity": "sha512-5vQEh3y+DG/lMPM0mCGPDnyV8chYg/g7rl6v3Gd8WMF9S429ox3Xk8qrk174kWhG767KQMqqxLD1WnGd77hiew==", + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -9987,7 +10257,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "devOptional": true, "license": "MIT" }, "node_modules/native-run": { @@ -10058,7 +10327,6 @@ "version": "3.71.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", - "devOptional": true, "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -10398,6 +10666,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10744,7 +11018,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", - "devOptional": true, "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -10771,14 +11044,12 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "devOptional": true, "license": "ISC" }, "node_modules/prebuild-install/node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "devOptional": true, "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -10791,7 +11062,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "devOptional": true, "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -10934,7 +11204,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, "node_modules/prompts": { @@ -10980,7 +11249,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "devOptional": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -11059,7 +11327,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "devOptional": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -11075,14 +11342,12 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "devOptional": true, "license": "ISC" }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12193,6 +12458,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/sharp": { "version": "0.32.6", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", @@ -12332,7 +12603,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "devOptional": true, "funding": [ { "type": "github", @@ -12353,7 +12623,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "devOptional": true, "funding": [ { "type": "github", @@ -12559,6 +12828,12 @@ "readable-stream": "^3.0.0" } }, + "node_modules/sql.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz", + "integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==", + "license": "MIT" + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -13343,7 +13618,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" diff --git a/package.json b/package.json index 011567c..9cb7919 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "donetick", "private": true, - "version": "0.1.97", + "version": "0.1.99", "type": "module", "lint-staged": { "*.{js,jsx,ts,tsx}": [ @@ -22,6 +22,7 @@ "ionic:serve": "npm run start" }, "dependencies": { + "@capacitor-community/sqlite": "^6.0.2", "@capacitor/android": "^6.1.1", "@capacitor/app": "^6.0.0", "@capacitor/cli": "^6.1.1", @@ -29,6 +30,7 @@ "@capacitor/device": "^6.0.2", "@capacitor/ios": "6.1.1", "@capacitor/local-notifications": "^6.0.0", + "@capacitor/network": "^6.0.3", "@capacitor/preferences": "^6.0.1", "@capacitor/push-notifications": "^6.0.1", "@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4", @@ -45,14 +47,15 @@ "chrono-node": "^2.7.7", "dotenv": "^16.4.5", "esm": "^3.2.25", + "farmhash": "^4.0.1", "fuse.js": "^7.0.0", "js-cookie": "^3.0.5", "moment": "^2.30.1", + "murmurhash": "^2.0.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", diff --git a/src/App.jsx b/src/App.jsx index 12f848a..4f66e0f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,7 +10,7 @@ import { UserContext } from './contexts/UserContext' import { AuthenticationProvider } from './service/AuthenticationService' import { GetUserProfile } from './utils/Fetcher' import { apiManager, isTokenValid } from './utils/TokenManager' - +import NetworkBanner from './views/components/NetworkBanner' const add = className => { document.getElementById('root').classList.add(className) } @@ -51,14 +51,6 @@ function App() { setNeedRefresh(false) } - // const updateServiceWorker = useRegisterSW({ - // onRegistered(r) { - // r && - // setInterval(() => { - // r.update() - // }, intervalMS) - // }, - // }) const setThemeClass = () => { const value = JSON.parse(localStorage.getItem('themeMode')) || mode @@ -96,6 +88,7 @@ function App() { return (
+ diff --git a/src/hooks/NetworkManager.jsx b/src/hooks/NetworkManager.jsx new file mode 100644 index 0000000..847fbd0 --- /dev/null +++ b/src/hooks/NetworkManager.jsx @@ -0,0 +1,88 @@ +import { Network } from '@capacitor/network' +// import { localStore } from '../utils/LocalStore' +import { syncManager } from '../utils/SyncManager.jsx' // Ensure you import syncManager if needed for syncing +class NetworkManager { + constructor() { + this.isOnline = true + this.isNetworkOn = null + this.init() + this.connectionStatusListeners = [] + this.queueSyncListeners = [] + this.lastChecked = null + this.offlineSince = null + } + async init() { + const status = await Network.getStatus() + this.isNetworkOn = status.connected + this.lastChecked = Date.now() + + Network.addListener('networkStatusChange', status => { + if (this.isNetworkOn !== status.connected) { + this.isNetworkOn = status.connected + this.lastChecked = Date.now() + this.isOnline = status.connected + } + }) + this.registerNetworkListener(async isOnline => { + if (isOnline && this.isNetworkOn) { + // TODO: Delete when Sync manager Implemented + // localStore + // .syncQueuedRequests() + // .then(hasMessages => { + // console.log( + // 'Queued requests synced successfully. Queue has messaage is: ', + // hasMessages, + // ) + // if (hasMessages) { + // this.notifyBackendSync() + // } + // }) + // .catch(error => { + // console.error('Error syncing queued requests:', error) + // }) + console.log('NetworkManager: Network is back online. with SYNCMANAGER') + + await syncManager.syncTasks() + console.log('Finished syncing queued requests.') + } + }) + } + + setOffline() { + if (this.isOnline === true) { + this.isOnline = false + this.notifyConnectionStatus() + this.offlineSince = Date.now() // Record the time when we went offline + } + } + setOnline() { + if (this.isOnline === false) { + this.isOnline = true + this.notifyConnectionStatus() + } + } + + notifyConnectionStatus() { + this.connectionStatusListeners.forEach(callback => { + callback(this.isOnline) + }) + } + + notifyBackendSync() { + this.queueSyncListeners.forEach(callback => { + callback() + }) + } + + registerNetworkListener(callback) { + this.connectionStatusListeners.push(callback) + } + registerBackendSyncListener(callback) { + // if callback is not in the list already, add it + if (!this.queueSyncListeners.includes(callback)) { + this.queueSyncListeners.push(callback) + } + } +} + +export const networkManager = new NetworkManager() diff --git a/src/queries/ChoreQueries.jsx b/src/queries/ChoreQueries.jsx index 1b7b4c6..f228a04 100644 --- a/src/queries/ChoreQueries.jsx +++ b/src/queries/ChoreQueries.jsx @@ -1,20 +1,156 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' -import { useQuery } from 'react-query' -import { CreateChore, GetChoresHistory, GetChoresNew } from '../utils/Fetcher' +import { networkManager } from '../hooks/NetworkManager' +import { + CreateChore, + GetChoreByID, + GetChoreDetailById, + GetChoresHistory, + GetChoresNew, + SaveChore, +} from '../utils/Fetcher' +import { localStore } from '../utils/LocalStore' export const useChores = includeArchive => { - return useQuery(['chores', includeArchive], () => - GetChoresNew(includeArchive), - ) + return useQuery({ + queryKey: ['chores'], + queryFn: async () => { + const onlineChores = await GetChoresNew(includeArchive) + + const offlineTasks = (await localStore.getFromCache('offlineTasks')) || [] + // go throught each and if there is two chores with same id in offline and online, prefer the offline one: + var finalChores = [] + if (onlineChores && onlineChores.res) { + finalChores = onlineChores.res.filter( + onlineChore => + !offlineTasks.some(offlineTask => { + // Match by id or tempId + return ( + String(onlineChore.id) === String(offlineTask.id) || + (offlineTask.tempId && + String(onlineChore.id) === String(offlineTask.tempId)) + ) + }), + ) + } + // Combine online chores with offline tasks + if (offlineTasks.length > 0) { + // Merge the offline tasks with the online chores + finalChores = [ + ...finalChores, + ...offlineTasks.map(task => ({ + ...task, + id: task.id || task.tempId, // Ensure we have an id for consistency + })), + ] + } + + return { res: finalChores } + + // return { res: [...onlineChores.res, ...offlineTasks] } + }, + }) } export const useCreateChore = () => { const queryClient = useQueryClient() - return useMutation(CreateChore, { - onSuccess: () => { - queryClient.invalidateQueries('chores') + return useMutation({ + mutationFn: CreateChore, + onMutate: async newTask => { + if (!networkManager.isOnline) { + const tempId = crypto.randomUUID() // Generate temp ID + const offlineTasks = + (await localStore.getFromCache('offlineTasks')) || [] + const updateOfflineTasks = [ + ...offlineTasks, + { ...newTask, id: tempId, tempId }, // Use the tempId for offline tracking + ] + await localStore.saveToCache('offlineTasks', updateOfflineTasks) // Save to local storage + + // Update the chores query cache immediately + queryClient.setQueryData(['chores'], oldData => { + console.log('ATTEMPT TO SAVE OFFLINE TASKS:', updateOfflineTasks) + + if (!oldData) + return { + res: [{ ...newTask, id: tempId, tempId }], + } // If no data, return offline tasks + return { + res: [...oldData.res, { ...newTask, id: tempId, tempId }], + } + }) + return { tempId } + } + return { tempId: null } + }, + }) +} + +export const useUpdateChore = () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async updatedChore => { + if (!networkManager.isOnline) { + updatedChore['updatedAt'] = new Date().toISOString() + if (!updatedChore['nextDueDate']) { + updatedChore['nextDueDate'] = updatedChore['dueDate'] + } + const offlineTasks = + (await localStore.getFromCache('offlineTasks')) || [] + + for (const task of offlineTasks) { + // Find the task with the same id or tempId and update it + if (task.id === updatedChore.id || task.tempId === updatedChore.id) { + // Update the task in local storage + const updatedTask = { ...task, ...updatedChore } + const updatedOfflineTasks = offlineTasks.map(t => + t.id === task.id ? updatedTask : t, + ) + await localStore.saveToCache('offlineTasks', updatedOfflineTasks) + return new Promise((resolve, reject) => { + resolve(updatedTask) + }) + } + } + const newTaskId = crypto.randomUUID() + const updatedChoreWithNewId = { + ...updatedChore, + tempId: newTaskId, + } + + await localStore.saveToCache('offlineTasks', [ + ...offlineTasks, + updatedChoreWithNewId, + ]) + return new Promise((resolve, reject) => { + // Resolve with the updated task + resolve(updatedChoreWithNewId) + }) + } else { + // Call the API to update the chore + const resp = await SaveChore(updatedChore) + if (!resp || !resp.ok) { + throw new Error('Failed to save chore') + } + const updatedChoreRes = await resp.json() + if (!updatedChoreRes || !updatedChoreRes.res) { + throw new Error('Failed to get updated chore data') + } + // Successfully updated the chore on the server, return the updated chore + return updatedChoreRes.res + } + }, + onSuccess: (data, variables) => { + // Invalidate the chores query to refresh the data + queryClient.invalidateQueries(['chores']) + }, + onMutate: async updatedChore => { + if (!networkManager.isOnline) { + // Handle offline case here if needed + return + } }, }) } @@ -32,3 +168,67 @@ export const useChoresHistory = (initialLimit, includeMembers) => { return { data, error, isLoading, handleLimitChange } } + +export const useChoreDetails = choreId => { + return useQuery({ + queryKey: ['chore', choreId], + queryFn: async () => { + var onlineChore = null + + try { + const response = await GetChoreDetailById(choreId) + + if (response && response.ok) { + onlineChore = await response.json() + } + } catch (error) { + console.error('Error fetching chore detail:', error) + } + + const offlineTasks = (await localStore.getFromCache('offlineTasks')) || [] + const offline = offlineTasks.find(task => { + // Match by tempId or id if it was created offline + return task.id === choreId || (task.tempId && task.tempId === choreId) + }) + + return { res: offline ? { ...offline } : onlineChore.res } + }, + }) +} + +export const useChore = choreId => { + const queryClient = useQueryClient() + + return useQuery({ + queryKey: ['chore', choreId], + queryFn: async () => { + if (!choreId) { + throw new Error('Chore ID is required to fetch chore details') + } + var onlineChore = null + + try { + const response = await GetChoreByID(choreId) + + if (response && response.ok) { + onlineChore = await response.json() + } + } catch (error) { + console.error('Error fetching chore detail:', error) + } + + const offlineTasks = (await localStore.getFromCache('offlineTasks')) || [] + const offline = offlineTasks.find(task => { + return ( + String(task.id) === choreId || + (task.tempId && task.tempId === choreId) + ) + }) + + return { res: offline ? { ...offline } : onlineChore.res } + }, + onSuccess: () => { + queryClient.invalidateQueries(['chores']) + }, + }) +} diff --git a/src/utils/LocalStore.jsx b/src/utils/LocalStore.jsx new file mode 100644 index 0000000..bd68b40 --- /dev/null +++ b/src/utils/LocalStore.jsx @@ -0,0 +1,216 @@ +import { CapacitorSQLite } from '@capacitor-community/sqlite' +import { Capacitor } from '@capacitor/core' + +const CACHE_TABLE = 'offline_cache' +const QUEUE_TABLE = 'offline_request_queue' +const OFFLINE_TASK = 'offlineTasks' // For storing offline tasks + +class LocalStore { + constructor() { + this.db = null + this.useLocalStorage = !Capacitor.isNativePlatform() + } + + async initDatabase() { + if (this.useLocalStorage) return null + if (this.db) return this.db + + const db = await CapacitorSQLite.createConnection({ + database: 'offline_data', + version: 1, + }) + await db.open() + + // Create tables if they don't exist + await db.execute(` + CREATE TABLE IF NOT EXISTS ${CACHE_TABLE} ( + key TEXT PRIMARY KEY, + value TEXT, + timestamp INTEGER + ); + CREATE TABLE IF NOT EXISTS ${QUEUE_TABLE} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + url TEXT, + requestBody TEXT + ); + `) + + this.db = db + return db + } + + async saveToCache(key, data) { + const timestamp = Date.now() // Current timestamp in milliseconds + + if (this.useLocalStorage) { + localStorage.setItem(key, JSON.stringify({ value: data, timestamp })) + + return + } + + const db = await this.initDatabase() + await db.run( + ` + INSERT OR REPLACE INTO ${CACHE_TABLE} (key, value, timestamp) + VALUES (?, ?, ?); + `, + [key, JSON.stringify(data), timestamp], + ) + } + + // async saveTemporaryTask(task) { + // const baseURL = apiManager.getApiURL() + // const fullURL = `${baseURL}/chores/${task.tempId}` + // const options = { + // method: 'GET', + // headers: HEADERS(), + // url: fullURL, + // } + // const respond = { res: task } + // const requestId = murmurhash.v3(JSON.stringify({ fullURL, options })) + + // if (this.useLocalStorage) { + // this.saveToCache(requestId, respond) + // return + // } + // const db = await this.initDatabase() + + // await db.run( + // ` + // INSERT INTO ${CACHE_TABLE} (url, requestBody) + // VALUES (?, ?); + // `, + // [ + // requestId, + // JSON.stringify({ + // url: fullURL, + // options: { method: 'GET', headers: HEADERS() }, + // }), + // ], + // ) + // console.log('Saved temporary task to queue:', task) + // return + // } + + async getFromCache(key, ttl = 0) { + const now = Date.now() + + if (this.useLocalStorage) { + const cachedItem = localStorage.getItem(key) + if (!cachedItem) return null + + const { value, timestamp } = JSON.parse(cachedItem) + if (ttl > 0 && now - timestamp > ttl) { + localStorage.removeItem(key) // Remove expired item + return null + } + return value + } + + const db = await this.initDatabase() + const result = await db.query( + ` + SELECT value, timestamp FROM ${CACHE_TABLE} WHERE key = ?; + `, + [key], + ) + + if (result.values.length === 0) return null + + const { value, timestamp } = result.values[0] + if (ttl > 0 && now - timestamp > ttl) { + // Remove expired item + await db.run(`DELETE FROM ${CACHE_TABLE} WHERE key = ?;`, [key]) + return null + } + + return JSON.parse(value) + } + + async cleanExpiredCache(ttl) { + const now = Date.now() + + if (this.useLocalStorage) { + const keys = Object.keys(localStorage) + for (const key of keys) { + const cachedItem = localStorage.getItem(key) + if (!cachedItem) continue + + const { timestamp } = JSON.parse(cachedItem) + if (now - timestamp > ttl) { + localStorage.removeItem(key) // Remove expired item + } + } + return + } + + const db = await this.initDatabase() + await db.run( + ` + DELETE FROM ${CACHE_TABLE} WHERE ? - timestamp > ?; + `, + [now, ttl], + ) + } + + async queueRequest(requestId, requestBody) { + if (this.useLocalStorage) { + const queue = JSON.parse(localStorage.getItem(QUEUE_TABLE)) || [] + queue.push({ requestId, requestBody }) + localStorage.setItem(QUEUE_TABLE, JSON.stringify(queue)) + return + } + + const db = await this.initDatabase() + await db.run( + ` + INSERT INTO ${QUEUE_TABLE} (url, requestBody) + VALUES (?, ?); + `, + [requestId, JSON.stringify(requestBody)], + ) + } + + async syncQueuedRequests() { + var queueSize = 0 + if (this.useLocalStorage) { + const queue = JSON.parse(localStorage.getItem(QUEUE_TABLE)) || [] + console.log('LocalStore: queue: ', queue) + queueSize = queue.length + for (const request of queue) { + try { + await fetch(request.requestBody.url, request.requestBody.options) + console.log('LocalStore: Synced request:', request) + } catch (error) { + console.error('LocalStore: Failed to sync request:', request, error) + } + } + + // Clear the queue after syncing + localStorage.removeItem(QUEUE_TABLE) + localStorage.removeItem(OFFLINE_TASK) + return queueSize > 0 + } + + const db = await this.initDatabase() + const result = await db.query(`SELECT * FROM ${QUEUE_TABLE};`) + queueSize = result.values.length + for (const request of result.values) { + try { + await fetch( + request.requestBody.url, + JSON.parse(request.requestBody.options), + ) + console.log('Synced request:', request) + } catch (error) { + console.error('Failed to sync request:', request, error) + } + } + + // Clear the queue after syncing + await db.run(`DELETE FROM ${QUEUE_TABLE};`) + return queueSize > 0 + } +} + +export const localStore = new LocalStore() diff --git a/src/utils/SyncManager.jsx b/src/utils/SyncManager.jsx new file mode 100644 index 0000000..679019a --- /dev/null +++ b/src/utils/SyncManager.jsx @@ -0,0 +1,33 @@ +import { CreateChore, SaveChore } from './Fetcher' +import { localStore } from './LocalStore' + +class SyncManager { + async syncTasks() { + console.log('SYNCMANAGER: Starting sync process for offline tasks.') + const offlineTasks = (await localStore.getFromCache('offlineTasks')) || [] + for (const task of offlineTasks) { + // if task.needSync then it's need to be created: + var resp + if (task.needSync) { + resp = await CreateChore(task) + } else { + resp = await SaveChore(task) + } + if (!resp.ok) { + console.log( + `SYNCMANAGER: Failed to sync task with id: ${task.id}. Error: ${resp.statusText}`, + ) + } else { + console.log( + `SYNCMANAGER: Successfully synced task with id: ${task.id}.`, + ) + console.log(`SYNCMANAGER: Response:`, resp) + } + } + // Clear the offline tasks cache after syncing + await localStore.saveToCache('offlineTasks', []) + return true + } +} + +export const syncManager = new SyncManager() diff --git a/src/utils/TokenManager.jsx b/src/utils/TokenManager.jsx index 41751d1..d6aef13 100644 --- a/src/utils/TokenManager.jsx +++ b/src/utils/TokenManager.jsx @@ -1,28 +1,36 @@ -import Cookies from 'js-cookie' -import { API_URL } from '../Config' -import { RefreshToken } from './Fetcher' - +import { Network } from '@capacitor/network' import { Preferences } from '@capacitor/preferences' +import Cookies from 'js-cookie' +import murmurhash from 'murmurhash' +import { API_URL } from '../Config' +import { networkManager } from '../hooks/NetworkManager' +import { RefreshToken } from './Fetcher' +import { localStore } from './LocalStore' class ApiManager { constructor() { this.customServerURL = `${API_URL}/api/v1` this.initialized = false } + async init() { if (this.initialized) { return } + const { value: serverURL } = await Preferences.get({ key: 'customServerUrl', }) this.customServerURL = `${serverURL || API_URL}/api/v1` + await localStore.initDatabase() this.initialized = true } + getApiURL() { return this.customServerURL } + updateApiURL(url) { this.customServerURL = url this.init() @@ -31,23 +39,51 @@ class ApiManager { export const apiManager = new ApiManager() -export function Fetch(url, options) { +export async function Fetch(url, options) { if (!isTokenValid()) { - // store current location in cookie Cookies.set('ca_redirect', window.location.pathname) - // Assuming you have a function isTokenValid() that checks token validity - window.location.href = '/login' // Redirect to login page - // return Promise.reject("Token is not valid"); + window.location.href = '/login' } + if (!options) { options = {} } options.headers = { ...options.headers, ...HEADERS() } const baseURL = apiManager.getApiURL() - const fullURL = `${baseURL}${url}` - return fetch(fullURL, options) + + const networkStatus = await Network.getStatus() + + if (!networkStatus.connected) { + return handleOfflineRequest(fullURL, options) + } + + // Online: Perform the fetch + try { + const response = await fetch(fullURL, options) + + if (response.ok) { + const data = await response.clone().json() + const optionsHash = murmurhash.v3(JSON.stringify(options)) + await localStore.saveToCache(fullURL + optionsHash, data) + networkManager.setOnline() + } else if ( + response.status === 503 || + response.type === 'opaque' || + response.status === 0 + ) { + networkManager.setOffline() + return handleOfflineRequest(fullURL, options) + } + // return promise that resolves to response object: + return Promise.resolve(response) + } catch (error) { + networkManager.setOffline() + console.error('Fetch error:', error) + // throw error + return handleOfflineRequest(fullURL, options) + } } export const HEADERS = () => { @@ -61,14 +97,13 @@ export const isTokenValid = () => { const expiration = localStorage.getItem('ca_expiration') const token = localStorage.getItem('ca_token') - if (localStorage.getItem('ca_token')) { + if (token) { const now = new Date() const expire = new Date(expiration) if (now < expire) { if (now.getTime() + 24 * 60 * 60 * 1000 > expire.getTime()) { refreshAccessToken() } - return true } else { localStorage.removeItem('ca_token') @@ -92,3 +127,38 @@ export const refreshAccessToken = () => { } }) } + +async function handleOfflineRequest(url, options) { + // if get request then attempt to fetch from cache otherewise queue it : + if (options.method === 'GET') { + return attemptFetchFromCache(url, options) + } else { + // Queue the request for later processing + const requestId = murmurhash.v3(JSON.stringify({ url, options })) + await localStore.queueRequest(requestId, { url, options }) + console.log('Request queued for later processing:', requestId) + return Promise.reject(new Error('Offline and request queued: ' + requestId)) + } +} +async function attemptFetchFromCache(url, options) { + const optionsHash = murmurhash.v3(JSON.stringify(options)) + const cachedData = await localStore.getFromCache(url + optionsHash) + networkManager.setOffline() + + if (cachedData) { + return Promise.resolve({ + ok: true, + status: 200, + json: async () => cachedData, + }) + } else { + return Promise.reject( + new Error( + 'No cached data found for URL: ' + + url + + ' with options hash: ' + + optionsHash, + ), + ) + } +} diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index 76d65d9..92313c8 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -28,18 +28,20 @@ import moment from 'moment' import { useContext, useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { UserContext } from '../../contexts/UserContext' +import { + useChore, + useCreateChore, + useUpdateChore, +} from '../../queries/ChoreQueries.jsx' import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { - CreateChore, DeleteChore, GetAllCircleMembers, - GetChoreByID, - GetChoreHistory, GetThings, - SaveChore, } from '../../utils/Fetcher' import { isPlusAccount } from '../../utils/Helpers' import Priorities from '../../utils/Priorities.jsx' +import LoadingComponent from '../components/Loading.jsx' import SubTasks from '../components/SubTask.jsx' import { useLabels } from '../Labels/LabelQueries' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' @@ -99,6 +101,13 @@ const ChoreEdit = () => { const [snackbarColor, setSnackbarColor] = useState('warning') const [addLabelModalOpen, setAddLabelModalOpen] = useState(false) const { data: userLabelsRaw, isLoading: isUserLabelsLoading } = useLabels() + const updateChoreMutation = useUpdateChore() + const createChoreMutation = useCreateChore() + const { + data: choreData, + isLoading: isChoreLoading, + refetch: refetchChore, + } = useChore(choreId) const [userLabels, setUserLabels] = useState([]) @@ -143,6 +152,8 @@ const ChoreEdit = () => { !NO_DUE_DATE_ALLOWED_TYPE.includes(frequencyType) ) { if (REPEAT_ON_TYPE.includes(frequencyType)) { + console.log('VALIDATION:', dueDate, frequencyType) + errors.dueDate = 'Start date is required' } else { errors.dueDate = 'Due date is required' @@ -213,18 +224,44 @@ const ChoreEdit = () => { completionWindow < 0 || dueDate === null ? null : completionWindow, priority: priority, } - let SaveFunction = CreateChore + let SaveFunction = createChoreMutation.mutateAsync if (choreId > 0) { - SaveFunction = SaveChore + SaveFunction = updateChoreMutation.mutateAsync } - SaveFunction(chore).then(response => { - if (response.status === 200) { - Navigate(`/my/chores`) - } else { - alert('Failed to save chore') - } - }) + SaveFunction(chore) + .then(() => { + setSnackbarColor('success') + setSnackbarMessage('Chore saved successfully!') + setIsSnackbarOpen(true) + Navigate('/my/chores/') + }) + .catch(error => { + console.error('Failed to save chore:', error) + setSnackbarColor('danger') + setSnackbarMessage('Failed to save chore, please try again.') + setIsSnackbarOpen(true) + }) + + // handle if mutateAsync fails, if successful then redirect to the chore view page:: + // .then(data => { + // data.json().then(res => { + // if (res && res.res && res.res.id) { + // // chore saved successfully: + // setSnackbarMessage('Chore saved successfully!') + // setSnackbarColor('success') + // setIsSnackbarOpen(true) + // refetchChore() // refetch the chore to get the latest data + // Navigate('/my/chores/') // redirect to the chore view page + // } + // }) + // }) + // .catch(error => { + // console.error('Failed! to save chore:', error) + // setSnackbarMessage('Failed! to save chore, please try again.') + // setSnackbarColor('danger') + // setIsSnackbarOpen(true) + // }) } useEffect(() => { //fetch performers: @@ -236,90 +273,56 @@ const ChoreEdit = () => { setAllUserThings(data.res) }) }) - // fetch chores: - if (choreId > 0) { - GetChoreByID(choreId) - .then(response => { - if (response.status !== 200) { - alert('You are not authorized to view this chore.') - Navigate('/my/chores') - return null - } else { - return response.json() - } - }) - .then(data => { - setChore(data.res) - setName(data.res.name ? data.res.name : '') - setDescription(data.res.description ? data.res.description : '') - setAssignees(data.res.assignees ? data.res.assignees : []) - setAssignedTo(data.res.assignedTo) - setFrequencyType( - data.res.frequencyType ? data.res.frequencyType : 'once', - ) - - setFrequencyMetadata(JSON.parse(data.res.frequencyMetadata)) - setFrequency(data.res.frequency) - - setNotificationMetadata(JSON.parse(data.res.notificationMetadata)) - setPoints( - data.res.points && data.res.points > -1 ? data.res.points : -1, - ) - setCompletionWindow( - data.res.completionWindow && data.res.completionWindow > -1 - ? data.res.completionWindow - : -1, - ) - - setLabelsV2(data.res.labelsV2) - setSubTasks(data.res.subTasks) - setPriority(data.res.priority) - setAssignStrategy( - data.res.assignStrategy - ? data.res.assignStrategy - : ASSIGN_STRATEGIES[2], - ) - setIsRolling(data.res.isRolling) - setIsActive(data.res.isActive) - // parse the due date to a string from this format "2021-10-10T00:00:00.000Z" - // use moment.js or date-fns to format the date for to be usable in the input field: - setDueDate( - data.res.nextDueDate - ? moment(data.res.nextDueDate).format('YYYY-MM-DDTHH:mm:ss') - : null, - ) - setUpdatedBy(data.res.updatedBy) - setCreatedBy(data.res.createdBy) - setIsNotificable(data.res.notification) - setThingTrigger(data.res.thingChore) - // setDueDate(data.res.dueDate) - // setCompleted(data.res.completed) - // setCompletedDate(data.res.completedDate) - }) - - // fetch chores history: - GetChoreHistory(choreId) - .then(response => response.json()) - .then(data => { - setChoresHistory(data.res) - const newUserChoreHistory = {} - data.res.forEach(choreHistory => { - if (newUserChoreHistory[choreHistory.completedBy]) { - newUserChoreHistory[choreHistory.completedBy] += 1 - } else { - newUserChoreHistory[choreHistory.completedBy] = 1 - } - }) - - setUserHistory(newUserChoreHistory) - }) - } - // set focus on the first input field: - else { - // new task/ chore set focus on the first input field: - document.querySelector('input').focus() - } }, []) + useEffect(() => { + if (isChoreLoading === false && choreData && choreId) { + const data = choreData + + setChore(data.res) + setName(data.res.name ? data.res.name : '') + setDescription(data.res.description ? data.res.description : '') + setAssignees(data.res.assignees ? data.res.assignees : []) + setAssignedTo(data.res.assignedTo) + setFrequencyType(data.res.frequencyType ? data.res.frequencyType : 'once') + + setFrequencyMetadata(data.res.frequencyMetadata) + setFrequency(data.res.frequency) + + setNotificationMetadata(data.res.notificationMetadata) + setPoints(data.res.points && data.res.points > -1 ? data.res.points : -1) + setCompletionWindow( + data.res.completionWindow && data.res.completionWindow > -1 + ? data.res.completionWindow + : -1, + ) + + setLabelsV2(data.res.labelsV2) + setSubTasks(data.res.subTasks) + setPriority(data.res.priority) + setAssignStrategy( + data.res.assignStrategy + ? data.res.assignStrategy + : ASSIGN_STRATEGIES[2], + ) + setIsRolling(data.res.isRolling) + setIsActive(data.res.isActive) + // parse the due date to a string from this format "2021-10-10T00:00:00.000Z" + // use moment.js or date-fns to format the date for to be usable in the input field: + setDueDate( + data.res.nextDueDate + ? moment(data.res.nextDueDate).format('YYYY-MM-DDTHH:mm:ss') + : null, + ) + + setUpdatedBy(data.res.updatedBy) + setCreatedBy(data.res.createdBy) + setIsNotificable(data.res.notification) + setThingTrigger(data.res.thingChore) + // setDueDate(data.res.dueDate) + // setCompleted(data.res.completed) + // setCompletedDate(data.res.completedDate) + } + }, [choreData, isChoreLoading]) // useEffect(() => { // if (userLabels && userLabels.length == 0 && labelsV2.length == 0) { @@ -383,7 +386,9 @@ const ChoreEdit = () => { }, }) } - + if (isChoreLoading && choreId) { + return + } return ( {/* diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx index db9e678..ebd1a0d 100644 --- a/src/views/ChoreEdit/ChoreView.jsx +++ b/src/views/ChoreEdit/ChoreView.jsx @@ -37,10 +37,11 @@ import { Divider } from '@mui/material' import moment from 'moment' import { useEffect, useState } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' +import { useChore } from '../../queries/ChoreQueries.jsx' +import { useCircleMembers } from '../../queries/UserQueries.jsx' import { notInCompletionWindow } from '../../utils/Chores.jsx' import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { - GetAllUsers, GetChoreDetailById, MarkChoreComplete, SkipChore, @@ -48,6 +49,7 @@ import { } from '../../utils/Fetcher' import Priorities from '../../utils/Priorities' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' +import LoadingComponent from '../components/Loading.jsx' import SubTasks from '../components/SubTask.jsx' const IconCard = styled('div')({ display: 'flex', @@ -77,32 +79,35 @@ const ChoreView = () => { const [confirmModelConfig, setConfirmModelConfig] = useState({}) const [chorePriority, setChorePriority] = useState(null) const [isDescriptionOpen, setIsDescriptionOpen] = useState(false) + const { + data: circleMembersData, + isLoading: isCircleMembersLoading, + handleRefetch: handleCircleMembersRefetch, + } = useCircleMembers() + + const { + data: choreData, + isLoading: isChoreLoading, + refetch: refetchChore, + } = useChore(choreId) + useEffect(() => { - Promise.all([ - GetChoreDetailById(choreId).then(resp => { - if (resp.ok) { - return resp.json().then(data => { - setChore(data.res) - setChorePriority( - Priorities.find(p => p.value === data.res.priority), - ) - document.title = 'Donetick: ' + data.res.name - }) - } - }), - GetAllUsers() - .then(response => response.json()) - .then(data => { - setPerformers(data.res) - }), - ]) + if (!choreData || !choreData.res || !circleMembersData) { + return + } + setChore(choreData.res) + setChorePriority(Priorities.find(p => p.value === choreData.res.priority)) + document.title = 'Donetick: ' + choreData.res.name + + setPerformers(circleMembersData.res) const auto_complete = searchParams.get('auto_complete') if (auto_complete === 'true') { handleTaskCompletion() } - }, []) + }, [choreData, circleMembersData]) + useEffect(() => { - if (chore && performers.length > 0) { + if (chore && performers?.length > 0) { generateInfoCards(chore) } }, [chore, performers]) @@ -215,6 +220,10 @@ const ChoreView = () => { } }) } + if (isChoreLoading || isCircleMembersLoading) { + // while loading the chore or circle members, return a loading state + return + } return ( { { if (e.target.checked) { @@ -575,9 +582,7 @@ const ChoreView = () => { { if (e.target.checked) { diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index 5c257f4..a0bb754 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -63,7 +63,9 @@ const MyChores = () => { const [filteredChores, setFilteredChores] = useState([]) const [searchFilter, setSearchFilter] = useState('All') const [choreSections, setChoreSections] = useState([]) - const [activeTextField, setActiveTextField] = useState('task') + const [activeTextField, setActiveTextField] = useState( + localStorage.getItem('activeTextField') || 'task', + ) const [taskInputFocus, setTaskInputFocus] = useState(0) const searchInputRef = useRef() const [searchInputFocus, setSearchInputFocus] = useState(0) @@ -183,6 +185,10 @@ const MyChores = () => { setSelectedChoreFilter(value) localStorage.setItem('selectedChoreFilter', value) } + const setActiveTextFieldWithCache = value => { + setActiveTextField(value) + localStorage.setItem('activeTextField', value) + } const updateChores = newChore => { const newChores = chores @@ -421,7 +427,7 @@ const MyChores = () => { // minWidth: 100, }} onClick={() => { - setActiveTextField('task') + setActiveTextFieldWithCache('task') setTaskInputFocus(taskInputFocus + 1) }} > @@ -438,7 +444,7 @@ const MyChores = () => { borderRadius: 24, }} onClick={() => { - setActiveTextField('search') + setActiveTextFieldWithCache('search') setSearchInputFocus(searchInputFocus + 1) searchInputRef?.current?.focus() diff --git a/src/views/User/UserActivities.jsx b/src/views/User/UserActivities.jsx index 1738950..b34af1e 100644 --- a/src/views/User/UserActivities.jsx +++ b/src/views/User/UserActivities.jsx @@ -3,8 +3,9 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle' import CircleIcon from '@mui/icons-material/Circle' import { Cell, Legend, Pie, PieChart, Tooltip } from 'recharts' -import { EventBusy, Toll } from '@mui/icons-material' +import { EventBusy, Style, Toll } from '@mui/icons-material' import { + Avatar, Box, Button, Card, @@ -13,17 +14,21 @@ import { Divider, Grid, Link, + Option, + Select, Stack, Tab, TabList, Tabs, Typography, } from '@mui/joy' -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { UserContext } from '../../contexts/UserContext' import { useChores, useChoresHistory } from '../../queries/ChoreQueries' +import { useCircleMembers } from '../../queries/UserQueries.jsx' import { ChoresGrouper } from '../../utils/Chores' import { TASK_COLOR } from '../../utils/Colors.jsx' +import IconButtonWithMenu from '../Chores/IconButtonWithMenu' import LoadingComponent from '../components/Loading' const groupByDate = history => { @@ -156,10 +161,17 @@ const renderPieChart = (data, size, isPrimary) => ( ) +const USER_FILTER = (history, userId) => { + if (userId === undefined) return true + return history.completedBy === userId +} + const UserActivites = () => { const { userProfile } = React.useContext(UserContext) const [tabValue, setTabValue] = React.useState(30) const [selectedHistory, setSelectedHistory] = React.useState([]) + const [enrichedHistory, setEnrichedHistory] = React.useState([]) + const [selectedFilter, setSelectedFilter] = React.useState('Anyone') const [selectedChart, setSelectedChart] = React.useState('history') const [historyPieChartData, setHistoryPieChartData] = React.useState([]) @@ -175,7 +187,21 @@ const UserActivites = () => { data: choresHistory, isChoresHistoryLoading, handleLimitChange: refetchHistory, - } = useChoresHistory(tabValue ? tabValue : 30, false) + } = useChoresHistory(tabValue ? tabValue : 30, true) + const { + data: circleMembersData, + isLoading: isCircleMembersLoading, + handleRefetch: handleCircleMembersRefetch, + } = useCircleMembers() + const [selectedUser, setSelectedUser] = React.useState(userProfile?.id) + const [circleUsers, setCircleUsers] = useState([]) + + useEffect(() => { + if (circleMembersData) { + setCircleUsers(circleMembersData.res) + } + }, [circleMembersData]) + useEffect(() => { if (!isChoresHistoryLoading && !isChoresLoading && choresHistory) { const enrichedHistory = choresHistory.res.map(item => { @@ -185,8 +211,11 @@ const UserActivites = () => { choreName: chore?.name, } }) + setEnrichedHistory(enrichedHistory) - setSelectedHistory(enrichedHistory) + setSelectedHistory( + enrichedHistory.filter(h => USER_FILTER(h, selectedUser)), + ) setHistoryPieChartData(generateHistoryPieChartData(enrichedHistory)) } }, [isChoresHistoryLoading, isChoresLoading, choresHistory]) @@ -357,51 +386,123 @@ const UserActivites = () => { justifyContent: 'center', }} > - { - setTabValue(tabValue) - refetchHistory(tabValue) - }} - defaultValue={7} - sx={{ - py: 0.5, - borderRadius: 16, - maxWidth: 400, - mb: 1, - }} - > - + + Points Overview + + + + + { + setTabValue(tabValue) + refetchHistory(tabValue) + }} + defaultValue={7} + sx={{ + py: 0.5, borderRadius: 16, - backgroundColor: 'background.paper', - boxShadow: 1, - justifyContent: 'space-evenly', + maxWidth: 400, + mb: 1, }} > - {[ - { label: '7 Days', value: 7 }, - { label: '30 Days', value: 30 }, - { label: '90 Days', value: 90 }, - ].map((tab, index) => ( - - {tab.label} - - ))} - - + + {[ + { label: '7 Days', value: 7 }, + { label: '30 Days', value: 30 }, + { label: '90 Days', value: 90 }, + ].map((tab, index) => ( + + {tab.label} + + ))} + + + + } + options={[{ name: 'Anyone' }, { name: 'Me' }]} + selectedItem={'Anyone'} + onItemSelect={selected => { + console.log(selected) + }} + // isActive={searchFilter.startsWith('Label: ')} + // mouseClickHandler={handleMenuOutsideClick} + // useChips + /> + + {chartData[selectedChart].title} diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index 92c2ffc..530a06c 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -7,6 +7,7 @@ import { Input, Modal, ModalDialog, + ModalOverflow, Option, Select, Textarea, @@ -19,11 +20,12 @@ import { useContext, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { CSSTransition } from 'react-transition-group' import { UserContext } from '../../contexts/UserContext' +import { useCreateChore } from '../../queries/ChoreQueries' import useDebounce from '../../utils/Debounce' -import { CreateChore } from '../../utils/Fetcher' import { isPlusAccount } from '../../utils/Helpers' import { useLabels } from '../Labels/LabelQueries' import LearnMoreButton from './LearnMore' +import SubTasks from './SubTask' const VALID_DAYS = { monday: 'Monday', mon: 'Monday', @@ -73,6 +75,8 @@ const ALL_MONTHS = Object.values(VALID_MONTHS).filter( const TaskInput = ({ autoFocus, onChoreUpdate }) => { const { data: userLabels, isLoading: userLabelsLoading } = useLabels() + const createChoreMutation = useCreateChore() + const { userProfile } = useContext(UserContext) const navigate = useNavigate() const [taskText, setTaskText] = useState('') @@ -86,6 +90,8 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { const [description, setDescription] = useState(null) const [frequency, setFrequency] = useState(null) const [frequencyHumanReadable, setFrequencyHumanReadable] = useState(null) + const [subTasks, setSubTasks] = useState([]) + const [hasDescription, setHasDescription] = useState(false) useEffect(() => { if (openModal && textareaRef.current) { @@ -120,6 +126,14 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { const handleCloseModal = () => { setOpenModal(false) setTaskText('') + setTaskTitle('') + setDueDate(null) + setFrequency(null) + setFrequencyHumanReadable(null) + setPriority(0) + setHasDescription(false) + setDescription(null) + setSubTasks([]) } const handleSubmit = () => { @@ -438,6 +452,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { frequencyType: 'once', frequencyMetadata: {}, notificationMetadata: {}, + subTasks: subTasks.length > 0 ? subTasks : null, } if (frequency) { @@ -449,8 +464,12 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { chore.notificationMetadata = { dueDate: true } } } + if (!frequency && dueDate) { + // use dueDate converted to UTC: + chore.nextDueDate = new Date(dueDate).toUTCString() + } - CreateChore(chore).then(resp => { + createChoreMutation.mutateAsync(chore).then(resp => { resp.json().then(data => { if (resp.status !== 200) { console.error('Error creating chore:', data) @@ -496,8 +515,9 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => { )} - - {/* */} - Create new task - - Experimental Feature - - - Task in a sentence: - - - - This feature lets you create a task simply by typing a - sentence. It attempt parses the sentence to identify the - task's due date, priority, and frequency. - - - - Examples: - - - -
  • - Priority:For highest priority any of the - following keyword P1, Urgent,{' '} - Important, or ASAP. For lower - priorities, use P2, P3, or P4. -
  • -
  • - Due date: Specify dates with phrases like{' '} - tomorrow, next week, Monday, or{' '} - August 1st at 12pm. -
  • -
  • - Frequency: Set recurring tasks with terms - like daily, weekly, monthly,{' '} - yearly, or patterns such as{' '} - every Tuesday and Thursday. -
  • -
    - - } - /> -
    - - Title: - setTaskTitle(e.target.value)} - sx={{ width: '100%', fontSize: '16px' }} - /> - - - Description: -