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
-
-
-
+
+ Priority
+
+
+
+ Due Date
+ setDueDate(e.target.value)}
+ sx={{ width: '100%', fontSize: '16px' }}
+ />
+
+
+
+
+ Assignee
+
+
+
+ Frequency
+
+
+
+
+
+ Cancel
+
+
+ Create
+
+
+
+
>
)
diff --git a/src/views/components/Loading.jsx b/src/views/components/Loading.jsx
index a31cf20..bb70e0e 100644
--- a/src/views/components/Loading.jsx
+++ b/src/views/components/Loading.jsx
@@ -1,17 +1,32 @@
-import { Box, CircularProgress, Container } from '@mui/joy'
+import { Box, Button, CircularProgress, Container } from '@mui/joy'
import { Typography } from '@mui/material'
import { useEffect, useState } from 'react'
import Logo from '../../Logo'
+import { networkManager } from '../../hooks/NetworkManager'
const LoadingComponent = () => {
const [message, setMessage] = useState('Loading...')
const [subMessage, setSubMessage] = useState('')
+ const [isOnline, setIsOnline] = useState(networkManager.isOnline)
+
useEffect(() => {
+ if (!isOnline) {
+ setMessage('You are offline')
+ setSubMessage(
+ 'This not available while offline. Please check your internet connection and try again.',
+ )
+ }
+ }, [isOnline])
+ useEffect(() => {
+ networkManager.registerNetworkListener(isOnline => setIsOnline(isOnline))
+
// if loading took more than 5 seconds update submessage to mention there might be an error:
const timeout = setTimeout(() => {
- setSubMessage(
- 'This is taking longer than usual. There might be an issue.',
- )
+ if (networkManager.isOnline) {
+ setSubMessage(
+ 'This is taking longer than usual. There might be an issue.',
+ )
+ }
}, 5000)
return () => clearTimeout(timeout)
}, [])
@@ -26,6 +41,7 @@ const LoadingComponent = () => {
>
@@ -43,6 +59,16 @@ const LoadingComponent = () => {
{subMessage}
+ {
+ window.location.href = '/' // navigate back to the home page
+ }}
+ >
+ Navigate Back
+
)
diff --git a/src/views/components/NetworkBanner.jsx b/src/views/components/NetworkBanner.jsx
new file mode 100644
index 0000000..9b0dc7c
--- /dev/null
+++ b/src/views/components/NetworkBanner.jsx
@@ -0,0 +1,40 @@
+import { WifiOff } from '@mui/icons-material'
+import { Alert, Box } from '@mui/joy'
+import { useEffect, useState } from 'react'
+import { networkManager } from '../../hooks/NetworkManager'
+
+const NetworkBanner = () => {
+ const [isOnline, setIsOnline] = useState(networkManager.isOnline)
+ useEffect(() => {
+ const handleNetworkChange = isOnline => {
+ setIsOnline(isOnline)
+ }
+
+ networkManager.registerNetworkListener(handleNetworkChange)
+ }, [])
+
+ return (
+
+ {!isOnline && (
+ }
+ >
+ You are currently offline. Some features may not be available.
+
+ )}
+
+ )
+}
+
+export default NetworkBanner