diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index d49da16..c35dddf 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -2,8 +2,8 @@ android { compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 } } @@ -14,7 +14,9 @@ dependencies { implementation project(':capacitor-local-notifications') implementation project(':capacitor-preferences') implementation project(':capacitor-push-notifications') - implementation project(':codetrix-studio-capacitor-google-auth') + implementation project(':capacitor-status-bar') + implementation project(':capgo-capacitor-social-login') + implementation project(':capacitor-plugin-safe-area') } diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 1a19eac..f4f93ae 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -17,5 +17,11 @@ project(':capacitor-preferences').projectDir = new File('../node_modules/@capaci include ':capacitor-push-notifications' project(':capacitor-push-notifications').projectDir = new File('../node_modules/@capacitor/push-notifications/android') -include ':codetrix-studio-capacitor-google-auth' -project(':codetrix-studio-capacitor-google-auth').projectDir = new File('../node_modules/@codetrix-studio/capacitor-google-auth/android') +include ':capacitor-status-bar' +project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android') + +include ':capgo-capacitor-social-login' +project(':capgo-capacitor-social-login').projectDir = new File('../node_modules/@capgo/capacitor-social-login/android') + +include ':capacitor-plugin-safe-area' +project(':capacitor-plugin-safe-area').projectDir = new File('../node_modules/capacitor-plugin-safe-area/android') diff --git a/ios/App/Podfile b/ios/App/Podfile index 76d75af..67db58d 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -16,7 +16,9 @@ def capacitor_pods pod 'CapacitorLocalNotifications', :path => '../../node_modules/@capacitor/local-notifications' pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences' pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications' - pod 'CodetrixStudioCapacitorGoogleAuth', :path => '../../node_modules/@codetrix-studio/capacitor-google-auth' + pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' + pod 'CapgoCapacitorSocialLogin', :path => '../../node_modules/@capgo/capacitor-social-login' + pod 'CapacitorPluginSafeArea', :path => '../../node_modules/capacitor-plugin-safe-area' end target 'App' do diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 4290288..5ddce35 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -1,34 +1,77 @@ PODS: - - AppAuth (1.7.5): - - AppAuth/Core (= 1.7.5) - - AppAuth/ExternalUserAgent (= 1.7.5) - - AppAuth/Core (1.7.5) - - AppAuth/ExternalUserAgent (1.7.5): + - Alamofire (5.10.2) + - AppAuth (1.7.6): + - AppAuth/Core (= 1.7.6) + - AppAuth/ExternalUserAgent (= 1.7.6) + - AppAuth/Core (1.7.6) + - AppAuth/ExternalUserAgent (1.7.6): - AppAuth/Core - - Capacitor (6.1.1): + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - Capacitor (7.2.0): - CapacitorCordova - - CapacitorApp (6.0.0): + - CapacitorApp (7.0.1): - Capacitor - - CapacitorCordova (6.1.1) - - CapacitorDevice (6.0.2): + - CapacitorCommunitySqlite (7.0.0): - Capacitor - - CapacitorLocalNotifications (6.0.0): + - SQLCipher + - ZIPFoundation + - CapacitorCordova (7.2.0) + - CapacitorDevice (7.0.1): - Capacitor - - CapacitorPreferences (6.0.1): + - CapacitorLocalNotifications (7.0.1): - Capacitor - - CapacitorPushNotifications (6.0.1): + - CapacitorNetwork (7.0.1): - Capacitor - - CodetrixStudioCapacitorGoogleAuth (0.0.1): + - CapacitorPluginSafeArea (4.0.0): - Capacitor - - GoogleSignIn (~> 6.2.4) - - GoogleSignIn (6.2.4): - - AppAuth (~> 1.5) - - GTMAppAuth (~> 1.3) - - GTMSessionFetcher/Core (< 3.0, >= 1.1) - - GTMAppAuth (1.3.1): - - AppAuth/Core (~> 1.6) - - GTMSessionFetcher/Core (< 3.0, >= 1.5) - - GTMSessionFetcher/Core (2.3.0) + - CapacitorPreferences (7.0.1): + - Capacitor + - CapacitorPushNotifications (7.0.1): + - Capacitor + - CapacitorStatusBar (7.0.1): + - Capacitor + - CapgoCapacitorSocialLogin (7.5.3): + - Alamofire (~> 5.10.2) + - Capacitor + - FBSDKCoreKit (= 17.4.0) + - FBSDKLoginKit (= 17.4.0) + - GoogleSignIn (~> 8.0.0) + - FBAEMKit (17.4.0): + - FBSDKCoreKit_Basics (= 17.4.0) + - FBSDKCoreKit (17.4.0): + - FBAEMKit (= 17.4.0) + - FBSDKCoreKit_Basics (= 17.4.0) + - FBSDKCoreKit_Basics (17.4.0) + - FBSDKLoginKit (17.4.0): + - FBSDKCoreKit (= 17.4.0) + - GoogleSignIn (8.0.0): + - AppAuth (< 2.0, >= 1.7.3) + - AppCheckCore (~> 11.0) + - GTMAppAuth (< 5.0, >= 4.1.1) + - GTMSessionFetcher/Core (~> 3.3) + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/UserDefaults (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMAppAuth (4.1.1): + - AppAuth/Core (~> 1.7) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher/Core (3.5.0) + - PromisesObjC (2.4.0) + - SQLCipher (4.7.0): + - SQLCipher/standard (= 4.7.0) + - SQLCipher/common (4.7.0) + - SQLCipher/standard (4.7.0): + - SQLCipher/common + - ZIPFoundation (0.9.19) DEPENDENCIES: - "Capacitor (from `../../node_modules/@capacitor/ios`)" @@ -36,16 +79,29 @@ DEPENDENCIES: - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" - "CapacitorDevice (from `../../node_modules/@capacitor/device`)" - "CapacitorLocalNotifications (from `../../node_modules/@capacitor/local-notifications`)" + - "CapacitorNetwork (from `../../node_modules/@capacitor/network`)" + - CapacitorPluginSafeArea (from `../../node_modules/capacitor-plugin-safe-area`) - "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)" - "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)" - - "CodetrixStudioCapacitorGoogleAuth (from `../../node_modules/@codetrix-studio/capacitor-google-auth`)" + - "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)" + - "CapgoCapacitorSocialLogin (from `../../node_modules/@capgo/capacitor-social-login`)" SPEC REPOS: trunk: + - Alamofire - AppAuth + - AppCheckCore + - FBAEMKit + - FBSDKCoreKit + - FBSDKCoreKit_Basics + - FBSDKLoginKit - GoogleSignIn + - GoogleUtilities - GTMAppAuth - GTMSessionFetcher + - PromisesObjC + - SQLCipher + - ZIPFoundation EXTERNAL SOURCES: Capacitor: @@ -58,27 +114,47 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/device" CapacitorLocalNotifications: :path: "../../node_modules/@capacitor/local-notifications" + CapacitorNetwork: + :path: "../../node_modules/@capacitor/network" + CapacitorPluginSafeArea: + :path: "../../node_modules/capacitor-plugin-safe-area" CapacitorPreferences: :path: "../../node_modules/@capacitor/preferences" CapacitorPushNotifications: :path: "../../node_modules/@capacitor/push-notifications" - CodetrixStudioCapacitorGoogleAuth: - :path: "../../node_modules/@codetrix-studio/capacitor-google-auth" + CapacitorStatusBar: + :path: "../../node_modules/@capacitor/status-bar" + CapgoCapacitorSocialLogin: + :path: "../../node_modules/@capgo/capacitor-social-login" SPEC CHECKSUMS: - AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa - Capacitor: 8941aba4364ba9d1b22188569001f2ce45cc2b00 - CapacitorApp: 9d53aec7101f7b030a950c5bdc4df8612576b279 - CapacitorCordova: 8f2cc8d8d3619c566e9418fe8772064a94266106 - CapacitorDevice: 1a215717f0b5061503b21a03508b0ec458a57d78 - CapacitorLocalNotifications: 4ab68f0be5f697a579558fadd307d823a9ec1c26 - CapacitorPreferences: 72909b165bc7807103778ddbb86d5d8ce06abf71 - CapacitorPushNotifications: 293fabe42c7229756078e81f2d3a60394ba4e5d8 - CodetrixStudioCapacitorGoogleAuth: fcce058390347c1ce5d8ac4764bdf1f5c1ee233b - GoogleSignIn: 5651ce3a61e56ca864160e79b484cd9ed3f49b7a - GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd - GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 + Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 + AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f + Capacitor: 03bc7cbdde6a629a8b910a9d7d78c3cc7ed09ea7 + CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9 + CapacitorCommunitySqlite: b11e556cf5d149f9e0b6fbbb47959a6cd718c3d0 + CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f + CapacitorDevice: c6f6d587dd310527f8a48bf09c4e7b4a4cf14329 + CapacitorLocalNotifications: c2212755b33d2513b8bb325096ce3a64d4039ae3 + CapacitorNetwork: 15cb4385f0913a8ceb5e9a4d7af1ec554bdb8de8 + CapacitorPluginSafeArea: 22031c3436269ca80fac90ec2c94bc7c1e59a81d + CapacitorPreferences: 6c98117d4d7508034a4af9db64d6b26fc75d7b94 + CapacitorPushNotifications: 6a2794788c583dd89215f1805ca4bced1b13dbdf + CapacitorStatusBar: 6e7af040d8fc4dd655999819625cae9c2d74c36f + CapgoCapacitorSocialLogin: 0daa087f32775c656fbfba09b8eae7c089b85ce2 + FBAEMKit: 58cb5f302cdd715a56d4c1d0dfdd2e423ac1421a + FBSDKCoreKit: 94d7461d0cecf441b1ba7c41acfff41daa8ccd41 + FBSDKCoreKit_Basics: 151b43db8b834d3f0e02f95d36a44ffd36265e45 + FBSDKLoginKit: 5c1cd53c91a2282b3a4fe6e6d3dcf2b8b0d33d55 + GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + SQLCipher: ba9d0076041ed767c5bd3d3f77098318d04a403c + ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c -PODFILE CHECKSUM: 8801d4a4bb2e0bae0cca04ae313886af2d1ef072 +PODFILE CHECKSUM: 54ec5ec72c1c5e3e8d03bd96cc1ec0deaf577d9e COCOAPODS: 1.15.2 diff --git a/package-lock.json b/package-lock.json index f5eb71b..0935c6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "donetick", - "version": "0.1.98", + "version": "0.1.99", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "donetick", - "version": "0.1.98", + "version": "0.1.99", "dependencies": { "@capacitor-community/sqlite": "^6.0.2", "@capacitor/android": "^6.1.1", @@ -42,7 +42,6 @@ "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", @@ -5228,81 +5227,6 @@ "node": ">=8" } }, - "node_modules/broadcast-channel": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", - "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "detect-node": "^2.1.0", - "js-sha3": "0.8.0", - "microseconds": "0.2.0", - "nano-time": "1.0.0", - "oblivious-set": "1.0.0", - "rimraf": "3.0.2", - "unload": "2.2.0" - } - }, - "node_modules/broadcast-channel/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/broadcast-channel/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/broadcast-channel/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/broadcast-channel/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "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", @@ -6566,12 +6490,6 @@ "node": ">=8" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT" - }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -9341,12 +9259,6 @@ "node": ">=14" } }, - "node_modules/js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "license": "MIT" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9807,16 +9719,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/match-sorter": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", - "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.8", - "remove-accents": "0.5.0" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10054,12 +9956,6 @@ "node": ">=8.6" } }, - "node_modules/microseconds": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", - "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==", - "license": "MIT" - }, "node_modules/mimic-fn": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", @@ -10226,15 +10122,6 @@ "thenify-all": "^1.0.0" } }, - "node_modules/nano-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", - "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", - "license": "ISC", - "dependencies": { - "big-integer": "^1.6.16" - } - }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -10542,12 +10429,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/oblivious-set": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", - "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==", - "license": "MIT" - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -11409,32 +11290,6 @@ "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==", "license": "MIT" }, - "node_modules/react-query": { - "version": "3.39.3", - "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", - "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.5.5", - "broadcast-channel": "^3.4.1", - "match-sorter": "^6.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/react-router": { "version": "6.28.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.1.tgz", @@ -11882,12 +11737,6 @@ "node": ">=6" } }, - "node_modules/remove-accents": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", - "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", - "license": "MIT" - }, "node_modules/replace": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/replace/-/replace-1.2.2.tgz", @@ -13855,16 +13704,6 @@ "node": ">= 10.0.0" } }, - "node_modules/unload": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", - "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.6.2", - "detect-node": "^2.0.4" - } - }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", diff --git a/src/App.jsx b/src/App.jsx index bd3f0e8..7310ae1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,6 +8,7 @@ import { useRegisterSW } from 'virtual:pwa-register/react' import { registerCapacitorListeners } from './CapacitorListener' import { UserContext } from './contexts/UserContext' import { AuthenticationProvider } from './service/AuthenticationService' +import { ErrorProvider } from './service/ErrorProvider' import { GetUserProfile } from './utils/Fetcher' import { apiManager, isTokenValid } from './utils/TokenManager' import NetworkBanner from './views/components/NetworkBanner' @@ -91,10 +92,12 @@ function App() { - - - - + + + + + + {needRefresh && ( diff --git a/src/hooks/NetworkManager.jsx b/src/hooks/NetworkManager.jsx index 847fbd0..d9c43e4 100644 --- a/src/hooks/NetworkManager.jsx +++ b/src/hooks/NetworkManager.jsx @@ -1,5 +1,5 @@ import { Network } from '@capacitor/network' -// import { localStore } from '../utils/LocalStore' +import { localStore } from '../utils/LocalStore' import { syncManager } from '../utils/SyncManager.jsx' // Ensure you import syncManager if needed for syncing class NetworkManager { constructor() { @@ -23,29 +23,33 @@ class NetworkManager { this.isOnline = status.connected } }) + const syncQueue = () => { + 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) + }) + } 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) - // }) + syncQueue() console.log('NetworkManager: Network is back online. with SYNCMANAGER') await syncManager.syncTasks() console.log('Finished syncing queued requests.') } }) + syncQueue() } setOffline() { diff --git a/src/queries/ChoreQueries.jsx b/src/queries/ChoreQueries.jsx index f228a04..bdf9c71 100644 --- a/src/queries/ChoreQueries.jsx +++ b/src/queries/ChoreQueries.jsx @@ -67,19 +67,22 @@ export const useCreateChore = () => { { ...newTask, id: tempId, tempId }, // Use the tempId for offline tracking ] await localStore.saveToCache('offlineTasks', updateOfflineTasks) // Save to local storage - + // force useChores to refetch: + queryClient.invalidateQueries(['chores']) + // Force the chores query to refetch + queryClient.refetchQueries(['chores']) // Update the chores query cache immediately - queryClient.setQueryData(['chores'], oldData => { - console.log('ATTEMPT TO SAVE OFFLINE TASKS:', updateOfflineTasks) + // 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 }], - } - }) + // 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 } @@ -135,11 +138,11 @@ export const useUpdateChore = () => { throw new Error('Failed to save chore') } const updatedChoreRes = await resp.json() - if (!updatedChoreRes || !updatedChoreRes.res) { + if (!updatedChoreRes) { throw new Error('Failed to get updated chore data') } // Successfully updated the chore on the server, return the updated chore - return updatedChoreRes.res + return updatedChoreRes?.res || updatedChoreRes } }, onSuccess: (data, variables) => { @@ -158,9 +161,13 @@ export const useUpdateChore = () => { export const useChoresHistory = (initialLimit, includeMembers) => { const [limit, setLimit] = useState(initialLimit) // Initially, no limit is selected - const { data, error, isLoading } = useQuery(['choresHistory', limit], () => - GetChoresHistory(limit, includeMembers), - ) + const { data, error, isLoading } = useQuery({ + queryKey: ['choresHistory', limit], + queryFn: async () => { + const resp = await GetChoresHistory(limit, includeMembers) + return resp?.res || [] + }, + }) const handleLimitChange = newLimit => { setLimit(newLimit) diff --git a/src/queries/ResourceQueries.jsx b/src/queries/ResourceQueries.jsx index e09e61e..5205dcf 100644 --- a/src/queries/ResourceQueries.jsx +++ b/src/queries/ResourceQueries.jsx @@ -4,8 +4,12 @@ import { GetResource } from '../utils/Fetcher' export const useResource = () => { const { data, isLoading, error } = useQuery({ queryKey: ['resource'], - queryFn: GetResource, - cacheTime: 1000 * 60 * 10, // 10 minutes + queryFn: async () => { + const response = await GetResource() + return response.json() + }, + refetchOnWindowFocus: false, + refetchOnReconnect: false, }) return { data, isLoading, error } } diff --git a/src/queries/SubtaskQueries.jsx b/src/queries/SubtaskQueries.jsx new file mode 100644 index 0000000..616b703 --- /dev/null +++ b/src/queries/SubtaskQueries.jsx @@ -0,0 +1,94 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { networkManager } from '../hooks/NetworkManager' +import { CompleteSubTask, SaveChore } from '../utils/Fetcher' +import { localStore } from '../utils/LocalStore' + +export const useUpdate = () => { + 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) { + throw new Error('Failed to get updated chore data') + } + // Successfully updated the chore on the server, return the updated chore + return updatedChoreRes?.res || updatedChoreRes + } + }, + 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 + } + }, + }) +} + +export const useCompleteSubTask = () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (subTaskId, choreId, completedAt) => { + if (!networkManager.isOnline) { + throw new Error('Cannot complete subtask while offline') + } + const resp = await CompleteSubTask(subTaskId, choreId, completedAt) + if (!resp || !resp.ok) { + throw new Error('Failed to complete subtask') + } + const result = await resp.json() + if (!result || !result.res) { + throw new Error('Failed to get completed subtask data') + } + return result.res + }, + onSuccess: (data, variables) => {}, + }) +} diff --git a/src/queries/UserQueries.jsx b/src/queries/UserQueries.jsx index c37428b..a97db03 100644 --- a/src/queries/UserQueries.jsx +++ b/src/queries/UserQueries.jsx @@ -11,7 +11,7 @@ export const useAllUsers = () => { export const useCircleMembers = () => { const queryClient = useQueryClient() - const { data, error, isLoading, refetch } = useQuery({ + const { data, error, isLoading } = useQuery({ queryKey: ['allCircleMembers'], queryFn: GetAllCircleMembers, }) diff --git a/src/service/ErrorProvider.jsx b/src/service/ErrorProvider.jsx new file mode 100644 index 0000000..5cfc120 --- /dev/null +++ b/src/service/ErrorProvider.jsx @@ -0,0 +1,51 @@ +import { Error } from '@mui/icons-material' +import { Box, Button, Snackbar, Typography } from '@mui/joy' +import React, { createContext, useContext, useState } from 'react' + +const ErrorContext = createContext() + +export const useError = () => useContext(ErrorContext) + +export const ErrorProvider = ({ children }) => { + const [error, setError] = useState(null) + + const showError = error => { + setError(error) + } + + return ( + + {children} + setError(null)} + startDecorator={} + endDecorator={ + + } + > + {typeof error === 'string' ? ( + + {error} + + ) : ( + + + {error?.title} + + + {error?.message} + + + )} + + + ) +} diff --git a/src/utils/LocalStore.jsx b/src/utils/LocalStore.jsx index bd68b40..0573af7 100644 --- a/src/utils/LocalStore.jsx +++ b/src/utils/LocalStore.jsx @@ -1,5 +1,4 @@ import { CapacitorSQLite } from '@capacitor-community/sqlite' -import { Capacitor } from '@capacitor/core' const CACHE_TABLE = 'offline_cache' const QUEUE_TABLE = 'offline_request_queue' @@ -8,7 +7,8 @@ const OFFLINE_TASK = 'offlineTasks' // For storing offline tasks class LocalStore { constructor() { this.db = null - this.useLocalStorage = !Capacitor.isNativePlatform() + // this.useLocalStorage = !Capacitor.isNativePlatform() + this.useLocalStorage = true // default to localStorage for now. } async initDatabase() { @@ -153,10 +153,15 @@ class LocalStore { ) } - async queueRequest(requestId, requestBody) { + async queueRequest(requestId, requestPayload) { if (this.useLocalStorage) { const queue = JSON.parse(localStorage.getItem(QUEUE_TABLE)) || [] - queue.push({ requestId, requestBody }) + console.log('requestPayload', requestPayload) + + if (typeof requestPayload?.options?.body['id'] === 'string') { + requestPayload['id'] = null + } + queue.push({ requestId, requestBody: requestPayload }) localStorage.setItem(QUEUE_TABLE, JSON.stringify(queue)) return } @@ -167,11 +172,13 @@ class LocalStore { INSERT INTO ${QUEUE_TABLE} (url, requestBody) VALUES (?, ?); `, - [requestId, JSON.stringify(requestBody)], + [requestId, JSON.stringify(requestPayload)], ) } async syncQueuedRequests() { + console.log('Syncing queued requests...') + var queueSize = 0 if (this.useLocalStorage) { const queue = JSON.parse(localStorage.getItem(QUEUE_TABLE)) || [] diff --git a/src/utils/TokenManager.jsx b/src/utils/TokenManager.jsx index d6aef13..877ece5 100644 --- a/src/utils/TokenManager.jsx +++ b/src/utils/TokenManager.jsx @@ -137,7 +137,11 @@ async function handleOfflineRequest(url, options) { 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)) + return Promise.reject({ + error: 'Offline and request queued', + requestId, + queued: true, + }) } } async function attemptFetchFromCache(url, options) { @@ -152,6 +156,7 @@ async function attemptFetchFromCache(url, options) { json: async () => cachedData, }) } else { + // TODO: change this to throw error instead of returning promise return Promise.reject( new Error( 'No cached data found for URL: ' + diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx index bd93055..86ea151 100644 --- a/src/views/Authorization/LoginView.jsx +++ b/src/views/Authorization/LoginView.jsx @@ -1,5 +1,6 @@ import { Capacitor } from '@capacitor/core' -import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth' +// import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth' +import { SocialLogin } from '@capgo/capacitor-social-login' import { Settings } from '@mui/icons-material' import GoogleIcon from '@mui/icons-material/Google' import { @@ -15,7 +16,7 @@ import { Typography, } from '@mui/joy' import Cookies from 'js-cookie' -import React from 'react' +import React, { useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { LoginSocialGoogle } from 'reactjs-social-login' import { GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config' @@ -32,6 +33,19 @@ const LoginView = () => { const [error, setError] = React.useState(null) const { data: resource } = useResource() const Navigate = useNavigate() + useEffect(() => { + const initializeSocialLogin = async () => { + await SocialLogin.initialize({ + google: { + webClientId: import.meta.env.VITE_APP_GOOGLE_CLIENT_ID, + iOSClientId: import.meta.env.VITE_APP_IOS_CLIENT_ID, + mode: 'online', // replaces grantOfflineAccess + }, + }) + } + initializeSocialLogin() + }, []) + const handleSubmit = async e => { e.preventDefault() login(username, password) @@ -64,16 +78,23 @@ const LoginView = () => { const loggedWithProvider = function (provider, data) { const baseURL = apiManager.getApiURL() + + const getAccessToken = data => { + if (data['access_token']) { + // data["access_token"] is for Google + return data['access_token'] + } else if (data['accessToken']) { + // data["accessToken"] is for Google Capacitor + return data['accessToken']['token'] + } + } + return fetch(`${baseURL}/auth/${provider}/callback`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider: provider, - token: - data['access_token'] || // data["access_token"] is for Google - data['accessToken'], // data["accessToken"] is for Google Capacitor + token: getAccessToken(data), data: data, }), }).then(response => { @@ -167,12 +188,7 @@ const LoginView = () => { {Capacitor.isNativePlatform() && ( { Navigate('/login/settings') }} @@ -185,13 +201,7 @@ const LoginView = () => { Done - - tick - + tick {userProfile && ( @@ -200,12 +210,7 @@ const LoginView = () => { src={userProfile?.image} alt={userProfile?.username} size='lg' - sx={{ - mt: 2, - width: '96px', - height: '96px', - mb: 1, - }} + sx={{ mt: 2, width: '96px', height: '96px', mb: 1 }} /> Welcome back,{' '} @@ -367,15 +372,22 @@ const LoginView = () => { size='lg' sx={{ mt: 3, mb: 2 }} onClick={() => { - GoogleAuth.initialize({ - clientId: import.meta.env.VITE_APP_GOOGLE_CLIENT_ID, - scopes: ['profile', 'email', 'openid'], - grantOfflineAccess: true, - }) - GoogleAuth.signIn().then(user => { - console.log('Google user', user) + // GoogleAuth.initialize({ + // clientId: import.meta.env.VITE_APP_GOOGLE_CLIENT_ID, + // scopes: ['profile', 'email', 'openid'], + // grantOfflineAccess: true, + // }) + // GoogleAuth.signIn().then(user => { + // console.log('Google user', user) + // loggedWithProvider('google', user.authentication) + // }) - loggedWithProvider('google', user.authentication) + SocialLogin.login({ + provider: 'google', + options: { scopes: ['profile', 'email', 'openid'] }, + }).then(user => { + console.log('Google user', user) + loggedWithProvider('google', user.result) }) }} > diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index 92313c8..6eaf88d 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -386,7 +386,7 @@ const ChoreEdit = () => { }, }) } - if (isChoreLoading && choreId) { + if ((isChoreLoading && choreId) || isUserLabelsLoading) { return } return ( @@ -573,7 +573,6 @@ const ChoreEdit = () => { }} defaultChecked={dueDate !== null} checked={dueDate !== null} - value={dueDate !== null} overlay label='Give this task a due date' /> @@ -711,7 +710,6 @@ const ChoreEdit = () => { }} defaultChecked={isNotificable} checked={isNotificable} - value={isNotificable} disabled={!isPlusAccount(userProfile)} overlay label='Notify for this task' @@ -967,7 +965,6 @@ const ChoreEdit = () => { }} overlay checked={subTasks != null} - value={subTasks != null} label='Sub Tasks' /> Add sub tasks to this task @@ -997,7 +994,6 @@ const ChoreEdit = () => { } }} checked={points > -1} - value={points > -1} overlay label='Assign Points' /> diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx index ebd1a0d..f27743f 100644 --- a/src/views/ChoreEdit/ChoreView.jsx +++ b/src/views/ChoreEdit/ChoreView.jsx @@ -243,6 +243,7 @@ const ChoreView = () => { justifyContent: 'center', alignItems: 'center', textAlign: 'center', + mb: 1, }} > { ? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}` : 'N/A'} - {chore?.labelsV2?.map((label, index) => ( - - {label?.name} - - ))} + + {chore?.labelsV2?.map((label, index) => ( + + {label?.name} + + ))} + diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index d269cd0..5134477 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -44,6 +44,7 @@ import moment from 'moment' import React, { useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { UserContext } from '../../contexts/UserContext' +import { useError } from '../../service/ErrorProvider' import { notInCompletionWindow } from '../../utils/Chores.jsx' import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { @@ -91,6 +92,7 @@ const ChoreCard = ({ const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null) const [timeoutId, setTimeoutId] = React.useState(null) const { userProfile } = React.useContext(UserContext) + const { showError } = useError() useEffect(() => { document.addEventListener('mousedown', handleMenuOutsideClick) return () => { @@ -176,6 +178,7 @@ const ChoreCard = ({ if (seconds <= 0) { clearInterval(countdownInterval) // Stop the countdown when it reaches 0 + setIsPendingCompletion(false) // Reset the state } }, 1000) @@ -195,6 +198,25 @@ const ChoreCard = ({ setTimeoutId(null) setSecondsLeftToCancel(null) }) + .catch(error => { + if (error?.queued) { + showError({ + title: 'Update Failed', + message: 'Request will be reattempt when you are online', + }) + } else { + showError({ + title: 'Failed to update', + message: error, + }) + } + + setIsPendingCompletion(false) + clearTimeout(id) + clearInterval(countdownInterval) // Ensure to clear this interval as well + setTimeoutId(null) + setSecondsLeftToCancel(null) + }) }, 2000) setTimeoutId(id) @@ -676,15 +698,30 @@ const ChoreCard = ({ { - SkipChore(chore.id).then(response => { - if (response.ok) { - response.json().then(data => { - const newChore = data.res - onChoreUpdate(newChore, 'skipped') - handleMenuClose() - }) - } - }) + SkipChore(chore.id) + .then(response => { + if (response.ok) { + response.json().then(data => { + const newChore = data.res + onChoreUpdate(newChore, 'skipped') + handleMenuClose() + }) + } + }) + .catch(error => { + if (error?.queued) { + showError({ + title: 'Failed to update', + message: + 'Request will be processed when you are online', + }) + } else { + showError({ + title: 'Failed to update', + message: error, + }) + } + }) }} > diff --git a/src/views/Chores/CompactCard.jsx b/src/views/Chores/CompactCard.jsx new file mode 100644 index 0000000..39ba804 --- /dev/null +++ b/src/views/Chores/CompactCard.jsx @@ -0,0 +1,35 @@ +import { Box, Checkbox, Typography } from '@mui/joy' + +export const CompactCard = ({ + chore, + performers, + onChoreUpdate, + onChoreRemove, + userLabels, + sx, + viewOnly, + onChipClick, +}) => { + return ( + + handleToggle(task.id)} + /> + + {chore.name} + + + ) +} diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index a0bb754..2b942e9 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -1,11 +1,11 @@ import { Add, + Bolt, CancelRounded, EditCalendar, ExpandCircleDown, Grain, PriorityHigh, - Search, Sort, Style, Unarchive, @@ -63,11 +63,11 @@ const MyChores = () => { const [filteredChores, setFilteredChores] = useState([]) const [searchFilter, setSearchFilter] = useState('All') const [choreSections, setChoreSections] = useState([]) - const [activeTextField, setActiveTextField] = useState( - localStorage.getItem('activeTextField') || 'task', - ) + + const [showSearchFilter, setShowSearchFilter] = useState(false) + const [addTaskModalOpen, setAddTaskModalOpen] = useState(false) const [taskInputFocus, setTaskInputFocus] = useState(0) - const searchInputRef = useRef() + const searchInputRef = useRef(null) const [searchInputFocus, setSearchInputFocus] = useState(0) const [selectedChoreSection, setSelectedChoreSection] = useState( localStorage.getItem('selectedChoreSection') || 'due_date', @@ -84,7 +84,11 @@ const MyChores = () => { const menuRef = useRef(null) const Navigate = useNavigate() const { data: userLabels, isLoading: userLabelsLoading } = useLabels() - const { data: choresData, isLoading: choresLoading } = useChores() + const { + data: choresData, + isLoading: choresLoading, + refetch: refetchChores, + } = useChores() useEffect(() => { Promise.all([GetChores(), GetAllUsers(), GetUserProfile()]).then( @@ -173,6 +177,21 @@ const MyChores = () => { searchInputRef.current.selectionEnd = searchInputRef.current.value?.length } }, [searchInputFocus]) + + // add listern to Control/Command + K to focus on search input + useEffect(() => { + const handleKeyDown = event => { + if ((event.ctrlKey || event.metaKey) && event.key === 'k') { + event.preventDefault() + setAddTaskModalOpen(true) + } + } + document.addEventListener('keydown', handleKeyDown) + + return () => { + document.removeEventListener('keydown', handleKeyDown) + } + }, []) const setSelectedChoreSectionWithCache = value => { setSelectedChoreSection(value) localStorage.setItem('selectedChoreSection', value) @@ -185,10 +204,6 @@ const MyChores = () => { setSelectedChoreFilter(value) localStorage.setItem('selectedChoreFilter', value) } - const setActiveTextFieldWithCache = value => { - setActiveTextField(value) - localStorage.setItem('activeTextField', value) - } const updateChores = newChore => { const newChores = chores @@ -381,60 +396,36 @@ const MyChores = () => { gap: 0.5, }} > - {activeTextField == 'task' && ( - - )} + { + setShowSearchFilter(true) + }} + fullWidth + sx={{ + mt: 1, + mb: 1, + borderRadius: 24, + height: 24, + borderColor: 'text.disabled', + padding: 1, + }} + onChange={handleSearchChange} + endDecorator={ + searchTerm && ( + { + setSearchTerm('') + setFilteredChores(chores) + }} + /> + ) + } + /> - {activeTextField == 'search' && ( - 0} - placeholder='Search' - value={searchTerm} - fullWidth - sx={{ - mt: 1, - mb: 1, - borderRadius: 24, - height: 24, - borderColor: 'text.disabled', - padding: 1, - }} - onChange={handleSearchChange} - endDecorator={ - searchTerm && ( - { - setSearchTerm('') - setFilteredChores(chores) - }} - /> - ) - } - /> - )} - {activeTextField != 'task' && ( - { - setActiveTextFieldWithCache('task') - setTaskInputFocus(taskInputFocus + 1) - }} - > - - - )} - {activeTextField != 'search' && ( + {/* {activeTextField != 'search' && ( { > - )} + )} */} { mouseClickHandler={handleMenuOutsideClick} /> - {activeTextField === 'search' && ( + {showSearchFilter && (
{
+ { + setShowSearchFilter(false) + setSearchTerm('') + setFilteredChores(chores) + setSearchFilter('All') + }} + > + +
)} {searchFilter !== 'All' && ( @@ -811,7 +819,7 @@ const MyChores = () => { display: 'flex', justifyContent: 'flex-end', gap: 2, - 'z-index': 1000, + 'z-index': 100, }} > { borderRadius: '50%', width: 50, height: 50, + zIndex: 101, }} onClick={() => { Navigate(`/chores/create`) @@ -828,6 +837,27 @@ const MyChores = () => { > + { + setAddTaskModalOpen(true) + }} + > + + { {snackBarMessage} + {addTaskModalOpen && ( + { + setAddTaskModalOpen(false) + if (forceRefresh) { + refetchChores() + } + }} + /> + )} diff --git a/src/views/ChoresOverview.jsx b/src/views/ChoresOverview.jsx index 14b2040..a4d77da 100644 --- a/src/views/ChoresOverview.jsx +++ b/src/views/ChoresOverview.jsx @@ -26,9 +26,7 @@ import { import moment from 'moment' import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { API_URL } from '../Config' import { GetAllUsers, GetChores, MarkChoreComplete } from '../utils/Fetcher' -import { Fetch } from '../utils/TokenManager' import DateModal from './Modals/Inputs/DateModal' // import moment from 'moment' @@ -263,21 +261,22 @@ const ChoresOverview = () => { size='sm' // sx={{ borderRadius: '50%' }} onClick={() => { - MarkChoreComplete(chore.id,null,null,null) - .then(response => { - if (response.ok) { - response.json().then(data => { - const newChore = data.res - const newChores = [...chores] - const index = newChores.findIndex( - c => c.id === chore.id, - ) - newChores[index] = newChore - setChores(newChores) - setFilteredChores(newChores) - }) - } - }) + MarkChoreComplete(chore.id, null, null, null).then( + response => { + if (response.ok) { + response.json().then(data => { + const newChore = data.res + const newChores = [...chores] + const index = newChores.findIndex( + c => c.id === chore.id, + ) + newChores[index] = newChore + setChores(newChores) + setFilteredChores(newChores) + }) + } + }, + ) }} aria-setsize={2} > @@ -325,19 +324,20 @@ const ChoresOverview = () => { alert('Please select a performer') return } - MarkChoreComplete(choreId, null, date, activeUserId) - .then(response => { - if (response.ok) { - response.json().then(data => { - const newChore = data.res - const newChores = [...chores] - const index = newChores.findIndex(c => c.id === chore.id) - newChores[index] = newChore - setChores(newChores) - setFilteredChores(newChores) - }) - } - }) + MarkChoreComplete(choreId, null, date, activeUserId).then( + response => { + if (response.ok) { + response.json().then(data => { + const newChore = data.res + const newChores = [...chores] + const index = newChores.findIndex(c => c.id === chore.id) + newChores[index] = newChore + setChores(newChores) + setFilteredChores(newChores) + }) + } + }, + ) }} /> diff --git a/src/views/Labels/LabelQueries.jsx b/src/views/Labels/LabelQueries.jsx index 5328f63..5fec321 100644 --- a/src/views/Labels/LabelQueries.jsx +++ b/src/views/Labels/LabelQueries.jsx @@ -1,9 +1,16 @@ import { useQuery } from '@tanstack/react-query' -import { GetLabels } from '../../utils/Fetcher' +import { CreateLabel, GetLabels } from '../../utils/Fetcher' + export const useLabels = () => { return useQuery({ queryKey: ['labels'], queryFn: GetLabels, - initialData: [], + }) +} + +export const useCreateLabel = () => { + return useQuery({ + queryKey: ['createLabel'], + queryFn: CreateLabel, }) } diff --git a/src/views/Labels/LabelView.jsx b/src/views/Labels/LabelView.jsx index 4291133..81c9b47 100644 --- a/src/views/Labels/LabelView.jsx +++ b/src/views/Labels/LabelView.jsx @@ -11,26 +11,25 @@ import { } from '@mui/joy' import { useEffect, useState } from 'react' import LabelModal from '../Modals/Inputs/LabelModal' -import { useLabels } from './LabelQueries' // import { useMutation, useQueryClient } from '@tanstack/react-query' import { Add } from '@mui/icons-material' import { useQueryClient } from '@tanstack/react-query' -import { useNavigate } from 'react-router-dom' import { getTextColorFromBackgroundColor } from '../../utils/Colors' import { DeleteLabel } from '../../utils/Fetcher' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' +import { useLabels } from './LabelQueries' const LabelView = () => { const { data: labels, isLabelsLoading, isError } = useLabels() - const [userLabels, setUserLabels] = useState([labels]) + const [userLabels, setUserLabels] = useState([]) const [modalOpen, setModalOpen] = useState(false) const [currentLabel, setCurrentLabel] = useState(null) const queryClient = useQueryClient() const [confirmationModel, setConfirmationModel] = useState({}) - const Navigate = useNavigate() + const handleAddLabel = () => { setCurrentLabel(null) setModalOpen(true) @@ -53,7 +52,7 @@ const LabelView = () => { color: 'danger', cancelText: 'Cancel', onClose: confirmed => { - if (confirmed) { + if (confirmed === true) { handleDeleteLabel(id) } setConfirmationModel({}) @@ -140,7 +139,7 @@ const LabelView = () => { handleDeleteLabel(label.id)} + onClick={() => handleDeleteClicked(label.id)} color='danger' > diff --git a/src/views/Modals/Inputs/LabelModal.jsx b/src/views/Modals/Inputs/LabelModal.jsx index 2cf3b04..81118c4 100644 --- a/src/views/Modals/Inputs/LabelModal.jsx +++ b/src/views/Modals/Inputs/LabelModal.jsx @@ -11,7 +11,8 @@ import { } from '@mui/joy' import { useEffect, useState } from 'react' -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useQueryClient } from '@tanstack/react-query' +import { useError } from '../../../service/ErrorProvider.jsx' import LABEL_COLORS from '../../../utils/Colors.jsx' import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher' import { useLabels } from '../../Labels/LabelQueries' @@ -22,6 +23,7 @@ function LabelModal({ isOpen, onClose, label }) { const [error, setError] = useState('') const { data: userLabels = [] } = useLabels() const queryClient = useQueryClient() + const { showError } = useError() // Populate the form fields when editing useEffect(() => { @@ -57,26 +59,54 @@ function LabelModal({ isOpen, onClose, label }) { } // Mutation for saving labels - const saveLabelMutation = useMutation( - newLabel => - label - ? UpdateLabel({ id: label.id, ...newLabel }) - : CreateLabel(newLabel), - { - onSuccess: () => { - queryClient.invalidateQueries('labels') - onClose() - }, - onError: () => { - setError('Failed to save label. Please try again.') - }, - }, - ) + // const saveLabelMutation = useMutation( + // newLabel => + // label + // ? UpdateLabel({ id: label.id, ...newLabel }) + // : CreateLabel(newLabel), + // { + // onSuccess: () => { + // queryClient.invalidateQueries('labels') + // onClose() + // }, + // onError: () => { + // setError('Failed to save label. Please try again.') + // }, + // }, + // ) const handleSave = () => { if (!validateLabel()) return - - saveLabelMutation.mutate({ name: labelName, color }) + const saveLabel = label?.id && label.id !== -1 ? UpdateLabel : CreateLabel + // ? { id: label.id, name: labelName, color } + // : { name: labelName, color } + // saveLabelMutation.mutate({ name: labelName, color }) + saveLabel({ + id: label?.id, + name: labelName, + color, + }) + .then(res => { + if (res?.error) { + setError(res.error) + } else { + queryClient.invalidateQueries('labels') + onClose() + } + }) + .catch(err => { + if (err.queued) { + showError({ + title: 'Failed to save label', + message: 'Unable to save label. Please try again.', + }) + } else { + showError({ + title: 'Failed to save label', + message: 'Unable to save label. Please try again.', + }) + } + }) } return ( diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx index 1cc14e3..bd5c00f 100644 --- a/src/views/Settings/Settings.jsx +++ b/src/views/Settings/Settings.jsx @@ -14,6 +14,7 @@ import { } from '@mui/joy' import moment from 'moment' import { useContext, useEffect, useState } from 'react' +import { Navigate } from 'react-router-dom' import { UserContext } from '../../contexts/UserContext' import Logo from '../../Logo' import { @@ -498,6 +499,46 @@ const Settings = () => {
+ +
+ Experimental Features + + + Clean up some part of the local storage and cache. Only use if you + know are you doing. + + + +
) } diff --git a/src/views/TestView/AutocompleteDropdown.jsx b/src/views/TestView/AutocompleteDropdown.jsx new file mode 100644 index 0000000..afe96b3 --- /dev/null +++ b/src/views/TestView/AutocompleteDropdown.jsx @@ -0,0 +1,88 @@ +// AutocompleteDropdown.jsx +import { Divider, Menu, MenuItem } from '@mui/joy' +import React, { useEffect } from 'react' + +const AutocompleteDropdown = ({ + currentValue, + suggestions, + selectedIndex, + onSelectSuggestion, + onMouseEnterSuggestion, // Added for hover selection + parentRefer, // Ref to the dropdown element +}) => { + // Scroll selected item into view + const dropdownMenuRef = React.useRef(null) + useEffect(() => { + const selectedElement = dropdownMenuRef.current?.querySelector('.selected') + if (selectedElement) { + selectedElement.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }) + } + }, [selectedIndex, dropdownMenuRef]) + + if (!suggestions || suggestions.options === 0) { + return null // Don't render if no suggestions + } + + return ( + {}} + sx={{ + width: '300px', + maxHeight: '160px', + overflow: 'auto', + position: 'relative', + bottom: 0, + left: 0, + zIndex: 1300, + }} + > + {suggestions?.options + .filter(option => { + if (typeof option === 'string') { + return option.toLowerCase().includes(currentValue.toLowerCase()) + } + return option[suggestions.display] + .toLowerCase() + .includes(currentValue.toLowerCase()) + }) + .map((option, index) => ( + onSelectSuggestion(option)} + onMouseEnter={() => onMouseEnterSuggestion(index)} // Update selected index on hover + className={selectedIndex === index ? 'selected' : ''} // Add class for selected item + sx={{ + cursor: 'pointer', + backgroundColor: selectedIndex === index ? 'gray.800' : 'inherit', + }} + > + {suggestions.display ? option[suggestions.display] : option} + + ))} + + {/* + { + onSelectSuggestion(currentValue) + }} + sx={{ + cursor: 'pointer', + backgroundColor: 'gray.800', + }} + > + + Add new Label + */} + + ) +} + +export default AutocompleteDropdown diff --git a/src/views/TestView/SmartTaskTitleInput.css b/src/views/TestView/SmartTaskTitleInput.css new file mode 100644 index 0000000..34ebe4a --- /dev/null +++ b/src/views/TestView/SmartTaskTitleInput.css @@ -0,0 +1,42 @@ +.smart-task-display { + position: absolute; + width: 100%; + z-index: 10; + pointer-events: none; + padding: 0px; + margin: 0; + border: none; + overflow-y: auto; + word-wrap: break-word; + white-space: pre-wrap; + box-sizing: border-box; +} +.smart-task-common { + font-size: 1.2em; + line-height: 1.2em; + font-family: inherit; + caret-color: #f08080; +} + +.highlight-date { + color: #f08080; +} + +.highlight-repeat { + color: #90ee90; +} + +.highlight-label { + color: #add8e6; +} + +.highlight-priority { + color: #ffb6c1; +} + +.task-input { + position: relative; + width: 100%; + border-radius: 8px; + box-sizing: border-box; +} diff --git a/src/views/TestView/SmartTaskTitleInput.jsx b/src/views/TestView/SmartTaskTitleInput.jsx new file mode 100644 index 0000000..320cd6d --- /dev/null +++ b/src/views/TestView/SmartTaskTitleInput.jsx @@ -0,0 +1,273 @@ +import { useColorScheme } from '@mui/joy' +import { useEffect, useRef, useState } from 'react' +import AutocompleteDropdown from './AutocompleteDropdown' +import './SmartTaskTitleInput.css' +const renderHighlightedText = (text, cursorPosition) => { + const parts = [] + let lastIndex = 0 + + const regex = /(Tomorrow)|(#\w+)|(P\d)/gi + let match + + while ((match = regex.exec(text)) !== null) { + const matchedText = match[0] + const matchIndex = match.index + + if (matchIndex > lastIndex) { + parts.push(text.substring(lastIndex, matchIndex)) + } + + let className = '' + if (matchedText.toLowerCase() === 'tomorrow') { + className = 'highlight-date' + } else if (matchedText.startsWith('#')) { + className = 'highlight-label' + } else if (matchedText.startsWith('P')) { + className = 'highlight-priority' + } + + parts.push( + + {matchedText} + , + ) + + lastIndex = regex.lastIndex + } + + if (lastIndex < text.length) { + parts.push(text.substring(lastIndex)) + } + + return parts +} + +const SmartTaskTitleInput = ({ + value, + placeholder, + autoFocus, + onChange, + suggestions, + onEnterPressed, + customRenderer, +}) => { + const { mode, setMode } = useColorScheme() + const titleInputRef = useRef(null) + const [cursorPosition, setCursorPosition] = useState(value?.length) + const dropdownRef = useRef(null) + const [lastWord, setLastWord] = useState('') + const [suggestionTrigger, setSuggestionTrigger] = useState('P') + const [showSuggestions, setShowSuggestions] = useState(false) + const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(0) + + useEffect(() => { + setCursorPosition(prevPos => Math.min(prevPos, value.length)) + + if ( + titleInputRef.current && + document.activeElement === titleInputRef.current + ) { + requestAnimationFrame(() => { + titleInputRef.current.setSelectionRange(cursorPosition, cursorPosition) + }) + } + }, [value]) + useEffect(() => { + // set focus on the input when the component is mounted: + if (titleInputRef.current) { + titleInputRef.current.focus() + titleInputRef.current.setSelectionRange(cursorPosition, cursorPosition) + } + }, []) + const handleSuggestionChange = text => { + // if the last word start with '@' or '#' or 'P': + const lastWord = text.split(' ').pop() + if ( + lastWord.startsWith('@') || + lastWord.startsWith('#') || + lastWord.startsWith('P') + ) { + setSuggestionTrigger(lastWord[0]) + // last word without the first character: + setLastWord(lastWord.slice(1)) + + setShowSuggestions(true) + } else { + setShowSuggestions(false) + } + } + + const handleTextareaChange = e => { + handleSuggestionChange(e.target.value) + onChange(e.target.value) + setCursorPosition(e.target.selectionStart) + } + + const handleTextareaKeyDown = e => { + if (showSuggestions) { + const currentSuggestions = suggestions[suggestionTrigger].options.filter( + option => { + if (typeof option === 'string') { + return option.toLowerCase().includes(lastWord.toLowerCase()) + } + return option[suggestions[suggestionTrigger].display] + .toLowerCase() + .includes(lastWord.toLowerCase()) + }, + ) + + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + e.preventDefault() + const newIndex = + e.key === 'ArrowDown' + ? (selectedSuggestionIndex + 1) % currentSuggestions.length + : (selectedSuggestionIndex - 1 + currentSuggestions.length) % + currentSuggestions.length + setSelectedSuggestionIndex(newIndex) + } else if (e.key === 'Enter' || e.key === 'Tab') { + e.preventDefault() + const selectedSuggestion = currentSuggestions[selectedSuggestionIndex] + const suggestionValue = suggestions[suggestionTrigger].display + ? selectedSuggestion[suggestions[suggestionTrigger].display] + : selectedSuggestion + + if (suggestionValue) { + const newValue = `${value.slice(0, cursorPosition - lastWord.length)}${suggestionValue} ${value.slice(cursorPosition)}` + onChange(newValue) + titleInputRef.current.value = newValue + + setShowSuggestions(false) + + const newCursorPosition = cursorPosition + suggestionValue.length + 1 + titleInputRef.current.setSelectionRange( + newCursorPosition, + newCursorPosition, + ) + } + } else if (e.key === 'Escape') { + e.preventDefault() + setShowSuggestions(false) + } + } else { + if (e.key === 'Enter') { + e.preventDefault() + if (onEnterPressed) { + onEnterPressed(value) + } + } + } + + const currentPos = e.target.selectionStart + setCursorPosition(currentPos) + } + + const handleTextareaClick = e => { + const currentPos = e.target.selectionStart + setCursorPosition(currentPos) + } + + const handleDisplayClick = e => { + const range = document.caretRangeFromPoint(e.clientX, e.clientY) + if (range) { + const offset = range.startOffset + setCursorPosition(offset) + + if (titleInputRef.current) { + titleInputRef.current.focus() + titleInputRef.current.setSelectionRange(offset, offset) + } + } + } + + return ( +
+
+