Add network management features and sync functionality; introduce NetworkBanner component
This commit is contained in:
334
package-lock.json
generated
334
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
11
src/App.jsx
11
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 (
|
||||
<div className='min-h-screen'>
|
||||
<NetworkBanner />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthenticationProvider />
|
||||
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
||||
|
||||
88
src/hooks/NetworkManager.jsx
Normal file
88
src/hooks/NetworkManager.jsx
Normal 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()
|
||||
@@ -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
216
src/utils/LocalStore.jsx
Normal 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
33
src/utils/SyncManager.jsx
Normal 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()
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
40
src/views/components/NetworkBanner.jsx
Normal file
40
src/views/components/NetworkBanner.jsx
Normal 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
|
||||
Reference in New Issue
Block a user