Add network management features and sync functionality; introduce NetworkBanner component

This commit is contained in:
Mo Tarbin
2025-04-12 01:03:00 -04:00
parent 06288af18d
commit 33d46209ef
15 changed files with 1484 additions and 381 deletions

334
package-lock.json generated
View File

@@ -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"

View File

@@ -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",

View File

@@ -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 (
<div className='min-h-screen'>
<NetworkBanner />
<QueryClientProvider client={queryClient}>
<AuthenticationProvider />
<UserContext.Provider value={{ userProfile, setUserProfile }}>

View File

@@ -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()

View File

@@ -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'])
},
})
}

216
src/utils/LocalStore.jsx Normal file
View File

@@ -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()

33
src/utils/SyncManager.jsx Normal file
View File

@@ -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()

View File

@@ -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,
),
)
}
}

View File

@@ -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 <LoadingComponent />
}
return (
<Container maxWidth='md'>
{/* <Typography level='h3' mb={1.5}>

View File

@@ -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 <LoadingComponent />
}
return (
<Container
maxWidth='sm'
@@ -527,9 +536,7 @@ const ChoreView = () => {
<FormControl size='sm'>
<Checkbox
defaultChecked={note !== null}
checked={note !== null}
value={note !== null}
size='lg'
onChange={e => {
if (e.target.checked) {
@@ -575,9 +582,7 @@ const ChoreView = () => {
<FormControl size='sm'>
<Checkbox
defaultChecked={completedDate !== null}
checked={completedDate !== null}
value={completedDate !== null}
size='lg'
onChange={e => {
if (e.target.checked) {

View File

@@ -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()

View File

@@ -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) => (
</PieChart>
)
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',
}}
>
<Tabs
onChange={(e, tabValue) => {
setTabValue(tabValue)
refetchHistory(tabValue)
}}
defaultValue={7}
sx={{
py: 0.5,
borderRadius: 16,
maxWidth: 400,
mb: 1,
}}
>
<TabList
disableUnderline
<Box>
<Typography mb={2} level='h4'>
Points Overview
</Typography>
<Select
sx={{
width: 150,
}}
variant='soft'
label='User'
value={selectedUser}
onChange={(e, selected) => {
setSelectedUser(selected)
setSelectedHistory(
enrichedHistory.filter(h => USER_FILTER(h, selected)),
)
console.log(
enrichedHistory,
selected,
enrichedHistory.filter(h => USER_FILTER(h, selected)),
)
}}
renderValue={selected => (
<Typography
startDecorator={
<Avatar color='primary' m={0} size='sm'>
{
circleUsers.find(user => user.userId === selectedUser)
?.displayName[0]
}
</Avatar>
}
>
{
circleUsers.find(user => user.userId === selectedUser)
?.displayName
}
</Typography>
)}
>
{circleUsers.map(user => (
<Option key={user.userId} value={user.userId}>
<Typography>{user.displayName}</Typography>
<Chip
color='success'
size='sm'
variant='soft'
startDecorator={<Toll />}
>
{user.points - user.pointsRedeemed}
</Chip>
</Option>
))}
</Select>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
<Tabs
onChange={(e, tabValue) => {
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
key={index}
sx={{
borderRadius: 16,
color: 'text.secondary',
'&.Mui-selected': {
color: 'text.primary',
backgroundColor: 'primary.light',
},
}}
disableIndicator
value={tab.value}
>
{tab.label}
</Tab>
))}
</TabList>
</Tabs>
<TabList
disableUnderline
sx={{
borderRadius: 16,
backgroundColor: 'background.paper',
boxShadow: 1,
justifyContent: 'space-evenly',
}}
>
{[
{ label: '7 Days', value: 7 },
{ label: '30 Days', value: 30 },
{ label: '90 Days', value: 90 },
].map((tab, index) => (
<Tab
key={index}
sx={{
borderRadius: 16,
color: 'text.secondary',
'&.Mui-selected': {
color: 'text.primary',
backgroundColor: 'primary.light',
},
}}
disableIndicator
value={tab.value}
>
{tab.label}
</Tab>
))}
</TabList>
</Tabs>
<Box sx={{ ml: 2 }}>
<IconButtonWithMenu
k={'icon-menu-labels-filter'}
// label={' Labels'}
icon={<Style />}
options={[{ name: 'Anyone' }, { name: 'Me' }]}
selectedItem={'Anyone'}
onItemSelect={selected => {
console.log(selected)
}}
// isActive={searchFilter.startsWith('Label: ')}
// mouseClickHandler={handleMenuOutsideClick}
// useChips
/>
</Box>
</Box>
<Box sx={{ mb: 4 }}>
<Typography level='h4' textAlign='center'>
{chartData[selectedChart].title}

View File

@@ -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 }) => {
)}
<Modal open={openModal} onClose={handleCloseModal}>
<ModalDialog>
{/* <Button
<ModalOverflow>
<ModalDialog>
{/* <Button
size='sm'
onClick={() => navigate(`/chores/create`)}
variant='outlined'
@@ -506,150 +526,173 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
>
Full Editor
</Button> */}
<Typography level='h4'>Create new task</Typography>
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
Experimental Feature
</Chip>
<Box>
<Typography level='body-sm'>Task in a sentence:</Typography>
<Input
autoFocus
ref={textareaRef}
value={taskText}
onChange={handleTextChange}
onKeyUp={handleEnterPressed}
placeholder='Type your full text here...'
sx={{ width: '100%', fontSize: '16px' }}
/>
<LearnMoreButton
content={
<>
<Typography level='body-sm' sx={{ mb: 1 }}>
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.
</Typography>
<Typography
level='body-sm'
sx={{ fontWeight: 'bold', mt: 2 }}
>
Examples:
</Typography>
<Typography
level='body-sm'
component='ul'
sx={{ pl: 2, mt: 1, listStyle: 'disc' }}
>
<li>
<strong>Priority:</strong>For highest priority any of the
following keyword <em>P1</em>, <em>Urgent</em>,{' '}
<em>Important</em>, or <em>ASAP</em>. For lower
priorities, use <em>P2</em>, <em>P3</em>, or <em>P4</em>.
</li>
<li>
<strong>Due date:</strong> Specify dates with phrases like{' '}
<em>tomorrow</em>, <em>next week</em>, <em>Monday</em>, or{' '}
<em>August 1st at 12pm</em>.
</li>
<li>
<strong>Frequency:</strong> Set recurring tasks with terms
like <em>daily</em>, <em>weekly</em>, <em>monthly</em>,{' '}
<em>yearly</em>, or patterns such as{' '}
<em>every Tuesday and Thursday</em>.
</li>
</Typography>
</>
}
/>
</Box>
<Box>
<Typography level='body-sm'>Title:</Typography>
<Input
value={taskTitle}
onChange={e => setTaskTitle(e.target.value)}
sx={{ width: '100%', fontSize: '16px' }}
/>
</Box>
<Box>
<Typography level='body-sm'>Description:</Typography>
<Textarea
minRows={2}
value={description}
onChange={e => setDescription(e.target.value)}
/>
</Box>
<Box
sx={{ marginTop: 2, display: 'flex', flexDirection: 'row', gap: 2 }}
>
<FormControl>
<Typography level='body-sm'>Priority</Typography>
<Select
defaultValue={0}
value={priority}
onChange={(e, value) => setPriority(value)}
>
<Option value='0'>No Priority</Option>
<Option value='1'>P1</Option>
<Option value='2'>P2</Option>
<Option value='3'>P3</Option>
<Option value='4'>P4</Option>
</Select>
</FormControl>
<FormControl>
<Typography level='body-sm'>Due Date</Typography>
<Typography level='h4'>Create new task</Typography>
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
Experimental Feature
</Chip>
<Box>
<Typography level='body-sm'>Task in a sentence:</Typography>
<Input
type='datetime-local'
value={dueDate}
onChange={e => setDueDate(e.target.value)}
autoFocus
ref={textareaRef}
value={taskText}
onChange={handleTextChange}
onKeyUp={handleEnterPressed}
placeholder='Type your full text here...'
sx={{ width: '100%', fontSize: '16px' }}
/>
</FormControl>
</Box>
<Box
sx={{
marginTop: 2,
display: 'flex',
flexDirection: 'row',
justifyContent: 'start',
gap: 2,
}}
>
<FormControl>
<Typography level='body-sm'>Assignee</Typography>
<Select value={'0'} disabled>
<Option value='0'>Me</Option>
{/* <Option value='1'>Other</Option> */}
</Select>
</FormControl>
<FormControl>
<Typography level='body-sm'>Frequency</Typography>
<Input value={frequencyHumanReadable || 'Once'} variant='plain' />
</FormControl>
</Box>
<Box
sx={{
marginTop: 2,
display: 'flex',
flexDirection: 'row',
justifyContent: 'end',
gap: 1,
}}
>
<Button
variant='outlined'
color='neutral'
onClick={handleCloseModal}
<LearnMoreButton
content={
<>
<Typography level='body-sm' sx={{ mb: 1 }}>
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.
</Typography>
<Typography
level='body-sm'
sx={{ fontWeight: 'bold', mt: 2 }}
>
Examples:
</Typography>
<Typography
level='body-sm'
component='ul'
sx={{ pl: 2, mt: 1, listStyle: 'disc' }}
>
<li>
<strong>Priority:</strong>For highest priority any of
the following keyword <em>P1</em>, <em>Urgent</em>,{' '}
<em>Important</em>, or <em>ASAP</em>. For lower
priorities, use <em>P2</em>, <em>P3</em>, or <em>P4</em>
.
</li>
<li>
<strong>Due date:</strong> Specify dates with phrases
like <em>tomorrow</em>, <em>next week</em>,{' '}
<em>Monday</em>, or <em>August 1st at 12pm</em>.
</li>
<li>
<strong>Frequency:</strong> Set recurring tasks with
terms like <em>daily</em>, <em>weekly</em>,{' '}
<em>monthly</em>, <em>yearly</em>, or patterns such as{' '}
<em>every Tuesday and Thursday</em>.
</li>
</Typography>
</>
}
/>
</Box>
<Box>
<Typography level='body-sm'>Title:</Typography>
<Input
value={taskTitle}
onChange={e => setTaskTitle(e.target.value)}
sx={{ width: '100%', fontSize: '16px' }}
/>
</Box>
{hasDescription ? (
<Box>
<Typography level='body-sm'>Description:</Typography>
<Textarea
minRows={2}
value={description}
onChange={e => setDescription(e.target.value)}
/>
</Box>
) : (
<Button
variant='plain'
size='sm'
onClick={() => setHasDescription(true)}
>
Add Description
</Button>
)}
<Typography level='body-sm'>Subtasks:</Typography>
<SubTasks editMode={true} tasks={subTasks} setTasks={setSubTasks} />
<Box
sx={{
marginTop: 2,
display: 'flex',
flexDirection: 'row',
gap: 2,
}}
>
Cancel
</Button>
<Button variant='solid' color='primary' onClick={handleSubmit}>
Create
</Button>
</Box>
</ModalDialog>
<FormControl>
<Typography level='body-sm'>Priority</Typography>
<Select
defaultValue={0}
value={priority}
onChange={(e, value) => setPriority(value)}
>
<Option value='0'>No Priority</Option>
<Option value='1'>P1</Option>
<Option value='2'>P2</Option>
<Option value='3'>P3</Option>
<Option value='4'>P4</Option>
</Select>
</FormControl>
<FormControl>
<Typography level='body-sm'>Due Date</Typography>
<Input
type='datetime-local'
value={dueDate}
onChange={e => setDueDate(e.target.value)}
sx={{ width: '100%', fontSize: '16px' }}
/>
</FormControl>
</Box>
<Box
sx={{
marginTop: 2,
display: 'flex',
flexDirection: 'row',
justifyContent: 'start',
gap: 2,
}}
>
<FormControl>
<Typography level='body-sm'>Assignee</Typography>
<Select value={'0'} disabled>
<Option value='0'>Me</Option>
{/* <Option value='1'>Other</Option> */}
</Select>
</FormControl>
<FormControl>
<Typography level='body-sm'>Frequency</Typography>
<Input
value={frequencyHumanReadable || 'Once'}
variant='plain'
/>
</FormControl>
</Box>
<Box
sx={{
marginTop: 2,
display: 'flex',
flexDirection: 'row',
justifyContent: 'end',
gap: 1,
}}
>
<Button
variant='outlined'
color='neutral'
onClick={handleCloseModal}
>
Cancel
</Button>
<Button variant='solid' color='primary' onClick={handleSubmit}>
Create
</Button>
</Box>
</ModalDialog>
</ModalOverflow>
</Modal>
</>
)

View File

@@ -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 = () => {
>
<CircularProgress
color='success'
determinate={!isOnline}
sx={{ '--CircularProgress-size': '200px' }}
>
<Logo />
@@ -43,6 +59,16 @@ const LoadingComponent = () => {
<Typography level='h2' fontWeight={500} textAlign={'center'}>
{subMessage}
</Typography>
<Button
variant='outlined'
color='primary'
sx={{ mt: 4 }}
onClick={() => {
window.location.href = '/' // navigate back to the home page
}}
>
Navigate Back
</Button>
</Box>
</Container>
)

View File

@@ -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 (
<Box sx={{ position: 'relative' }}>
{!isOnline && (
<Alert
variant='soft'
color='warning'
sx={{
zIndex: 1000,
width: '100%',
justifyContent: 'center',
alignItems: 'center',
padding: '4px',
fontSize: '10px',
fontWeight: 'md',
}}
startDecorator={<WifiOff />}
>
You are currently offline. Some features may not be available.
</Alert>
)}
</Box>
)
}
export default NetworkBanner