Refactor StorageSettings to use SettingsLayout and improve layout structure; add ThemeSettings component; update TermsView with new terms and structure; enhance ThingsView styling; clean up AddTaskModal by removing experimental feature chip; create CalendarCard component for calendar overview; optimize CustomParsers for repeat parsing; update NavBar for improved navigation and search parameter handling.
This commit is contained in:
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "com.donetick.app"
|
applicationId "com.donetick.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 10
|
||||||
versionName "1.0"
|
versionName "1.0.1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -355,12 +355,12 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 6UJJ78R3BS;
|
DEVELOPMENT_TEAM = 6UJJ78R3BS;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.1;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -377,12 +377,12 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 6UJJ78R3BS;
|
DEVELOPMENT_TEAM = 6UJJ78R3BS;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
|
|||||||
@@ -1,58 +1,58 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.10.2)
|
- Alamofire (5.10.2)
|
||||||
- AppAuth (1.7.6):
|
- AppAuth (2.0.0):
|
||||||
- AppAuth/Core (= 1.7.6)
|
- AppAuth/Core (= 2.0.0)
|
||||||
- AppAuth/ExternalUserAgent (= 1.7.6)
|
- AppAuth/ExternalUserAgent (= 2.0.0)
|
||||||
- AppAuth/Core (1.7.6)
|
- AppAuth/Core (2.0.0)
|
||||||
- AppAuth/ExternalUserAgent (1.7.6):
|
- AppAuth/ExternalUserAgent (2.0.0):
|
||||||
- AppAuth/Core
|
- AppAuth/Core
|
||||||
- AppCheckCore (11.2.0):
|
- AppCheckCore (11.2.0):
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- Capacitor (7.4.0):
|
- Capacitor (7.4.3):
|
||||||
- CapacitorCordova
|
- CapacitorCordova
|
||||||
- CapacitorApp (7.0.1):
|
- CapacitorApp (7.0.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorBrowser (7.0.2):
|
- CapacitorBrowser (7.0.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorCommunitySqlite (7.0.0):
|
- CapacitorCommunitySqlite (7.0.1):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- SQLCipher
|
- SQLCipher
|
||||||
- ZIPFoundation
|
- ZIPFoundation
|
||||||
- CapacitorCordova (7.4.0)
|
- CapacitorCordova (7.4.3)
|
||||||
- CapacitorDevice (7.0.1):
|
- CapacitorDevice (7.0.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorLocalNotifications (7.0.1):
|
- CapacitorLocalNotifications (7.0.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorNetwork (7.0.1):
|
- CapacitorNetwork (7.0.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorPluginSafeArea (4.0.0):
|
- CapacitorPluginSafeArea (4.0.0):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorPreferences (7.0.1):
|
- CapacitorPreferences (7.0.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorPushNotifications (7.0.1):
|
- CapacitorPushNotifications (7.0.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorStatusBar (7.0.1):
|
- CapacitorStatusBar (7.0.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapgoCapacitorSocialLogin (7.5.3):
|
- CapgoCapacitorSocialLogin (7.11.2):
|
||||||
- Alamofire (~> 5.10.2)
|
- Alamofire (~> 5.10.2)
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- FBSDKCoreKit (= 17.4.0)
|
- FBSDKCoreKit (= 18.0.0)
|
||||||
- FBSDKLoginKit (= 17.4.0)
|
- FBSDKLoginKit (= 18.0.0)
|
||||||
- GoogleSignIn (~> 8.0.0)
|
- GoogleSignIn (~> 9.0.0)
|
||||||
- FBAEMKit (17.4.0):
|
- FBAEMKit (18.0.0):
|
||||||
- FBSDKCoreKit_Basics (= 17.4.0)
|
- FBSDKCoreKit_Basics (= 18.0.0)
|
||||||
- FBSDKCoreKit (17.4.0):
|
- FBSDKCoreKit (18.0.0):
|
||||||
- FBAEMKit (= 17.4.0)
|
- FBAEMKit (= 18.0.0)
|
||||||
- FBSDKCoreKit_Basics (= 17.4.0)
|
- FBSDKCoreKit_Basics (= 18.0.0)
|
||||||
- FBSDKCoreKit_Basics (17.4.0)
|
- FBSDKCoreKit_Basics (18.0.0)
|
||||||
- FBSDKLoginKit (17.4.0):
|
- FBSDKLoginKit (18.0.0):
|
||||||
- FBSDKCoreKit (= 17.4.0)
|
- FBSDKCoreKit (= 18.0.0)
|
||||||
- GoogleSignIn (8.0.0):
|
- GoogleSignIn (9.0.0):
|
||||||
- AppAuth (< 2.0, >= 1.7.3)
|
- AppAuth (~> 2.0)
|
||||||
- AppCheckCore (~> 11.0)
|
- AppCheckCore (~> 11.0)
|
||||||
- GTMAppAuth (< 5.0, >= 4.1.1)
|
- GTMAppAuth (~> 5.0)
|
||||||
- GTMSessionFetcher/Core (~> 3.3)
|
- GTMSessionFetcher/Core (~> 3.3)
|
||||||
- GoogleUtilities/Environment (8.1.0):
|
- GoogleUtilities/Environment (8.1.0):
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
@@ -63,29 +63,29 @@ PODS:
|
|||||||
- GoogleUtilities/UserDefaults (8.1.0):
|
- GoogleUtilities/UserDefaults (8.1.0):
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GTMAppAuth (4.1.1):
|
- GTMAppAuth (5.0.0):
|
||||||
- AppAuth/Core (~> 1.7)
|
- AppAuth/Core (~> 2.0)
|
||||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
|
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
|
||||||
- GTMSessionFetcher/Core (3.5.0)
|
- GTMSessionFetcher/Core (3.5.0)
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- PurchasesHybridCommon (16.2.0):
|
- PurchasesHybridCommon (17.0.0):
|
||||||
- RevenueCat (= 5.34.0)
|
- RevenueCat (= 5.35.1)
|
||||||
- PurchasesHybridCommonUI (16.2.0):
|
- PurchasesHybridCommonUI (17.0.0):
|
||||||
- PurchasesHybridCommon (= 16.2.0)
|
- PurchasesHybridCommon (= 17.0.0)
|
||||||
- RevenueCatUI (= 5.34.0)
|
- RevenueCatUI (= 5.35.1)
|
||||||
- RevenueCat (5.34.0)
|
- RevenueCat (5.35.1)
|
||||||
- RevenuecatPurchasesCapacitor (11.1.1):
|
- RevenuecatPurchasesCapacitor (11.1.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- PurchasesHybridCommon (= 16.2.0)
|
- PurchasesHybridCommon (= 17.0.0)
|
||||||
- RevenuecatPurchasesCapacitorUi (11.1.1):
|
- RevenuecatPurchasesCapacitorUi (11.1.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- PurchasesHybridCommonUI (= 16.2.0)
|
- PurchasesHybridCommonUI (= 17.0.0)
|
||||||
- RevenueCatUI (5.34.0):
|
- RevenueCatUI (5.35.1):
|
||||||
- RevenueCat (= 5.34.0)
|
- RevenueCat (= 5.35.1)
|
||||||
- SQLCipher (4.7.0):
|
- SQLCipher (4.10.0):
|
||||||
- SQLCipher/standard (= 4.7.0)
|
- SQLCipher/standard (= 4.10.0)
|
||||||
- SQLCipher/common (4.7.0)
|
- SQLCipher/common (4.10.0)
|
||||||
- SQLCipher/standard (4.7.0):
|
- SQLCipher/standard (4.10.0):
|
||||||
- SQLCipher/common
|
- SQLCipher/common
|
||||||
- ZIPFoundation (0.9.19)
|
- ZIPFoundation (0.9.19)
|
||||||
|
|
||||||
@@ -161,37 +161,37 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||||
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
|
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
|
||||||
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
|
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
|
||||||
Capacitor: 10feab36396883c55373328124a677f118cd4b5f
|
Capacitor: 28d6c01026a9a3f7156529498ec1f389a2a28dbc
|
||||||
CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9
|
CapacitorApp: 1f6922c9c5c8b1c538d7fbe92ebe44a81b34bed3
|
||||||
CapacitorBrowser: 22541e48442de44dc629c214388290d6eecc6ae9
|
CapacitorBrowser: 22541e48442de44dc629c214388290d6eecc6ae9
|
||||||
CapacitorCommunitySqlite: b11e556cf5d149f9e0b6fbbb47959a6cd718c3d0
|
CapacitorCommunitySqlite: 8b2c6bab33e3519280811d481f8bd0fa90343e1b
|
||||||
CapacitorCordova: 723e017d8a80ab2ede9959fbe37556c35ac860d5
|
CapacitorCordova: 435121e81a2df4d0034f0fb11fcefab5104cfdb5
|
||||||
CapacitorDevice: c6f6d587dd310527f8a48bf09c4e7b4a4cf14329
|
CapacitorDevice: 81ae78d5d1942707caad79276badd458bf6ec603
|
||||||
CapacitorLocalNotifications: c2212755b33d2513b8bb325096ce3a64d4039ae3
|
CapacitorLocalNotifications: 665188ae8accd40806129073896fb2b39322d858
|
||||||
CapacitorNetwork: 15cb4385f0913a8ceb5e9a4d7af1ec554bdb8de8
|
CapacitorNetwork: 695069886b3c5ed514db69aa3d026b8dc3c03a6b
|
||||||
CapacitorPluginSafeArea: 22031c3436269ca80fac90ec2c94bc7c1e59a81d
|
CapacitorPluginSafeArea: 22031c3436269ca80fac90ec2c94bc7c1e59a81d
|
||||||
CapacitorPreferences: 6c98117d4d7508034a4af9db64d6b26fc75d7b94
|
CapacitorPreferences: 65107ed7437d96ee72583df5763985e3c0ff2bc2
|
||||||
CapacitorPushNotifications: 6a2794788c583dd89215f1805ca4bced1b13dbdf
|
CapacitorPushNotifications: 7c0659b349b149ee3936a682da31a9d269de9582
|
||||||
CapacitorStatusBar: 6e7af040d8fc4dd655999819625cae9c2d74c36f
|
CapacitorStatusBar: e04d05e121d5a5979c29eb4249186a4e4a84cacb
|
||||||
CapgoCapacitorSocialLogin: 001d85f081a5167bde4ec4456353dbe4c03d4cd2
|
CapgoCapacitorSocialLogin: a320106e1d032da88576cf8b31a336e971f5acad
|
||||||
FBAEMKit: 58cb5f302cdd715a56d4c1d0dfdd2e423ac1421a
|
FBAEMKit: e34530df538b8eb8aeb53c35867715ba6c63ef0c
|
||||||
FBSDKCoreKit: 94d7461d0cecf441b1ba7c41acfff41daa8ccd41
|
FBSDKCoreKit: d3f479a69127acebb1c6aad91c1a33907bcf6c2f
|
||||||
FBSDKCoreKit_Basics: 151b43db8b834d3f0e02f95d36a44ffd36265e45
|
FBSDKCoreKit_Basics: 017b6dc2a1862024815a8229e75661e627ac1e29
|
||||||
FBSDKLoginKit: 5c1cd53c91a2282b3a4fe6e6d3dcf2b8b0d33d55
|
FBSDKLoginKit: 5875762d1fe09ddcb05d03365d4f5dc34413843d
|
||||||
GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
|
GoogleSignIn: c7f09cfbc85a1abf69187be091997c317cc33b77
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
|
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
|
||||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PurchasesHybridCommon: 55e97b4e090015ed138988e6559def2bd4c50ebe
|
PurchasesHybridCommon: 34d2f73712f4c02f2963fcfb62d43757a9136860
|
||||||
PurchasesHybridCommonUI: 25c66f6ddcf8856ecbcffee897b2525eb272a5e7
|
PurchasesHybridCommonUI: 16e162423c0010f9a6f2c1d6505dcc075f49dcbe
|
||||||
RevenueCat: eb2aa042789d9c99ad5172bd96e28b96286d6ada
|
RevenueCat: 47d52620419dcf3dce4e724eade9d390e16ab4b0
|
||||||
RevenuecatPurchasesCapacitor: 420b762736c2842bfb64436b7e0b88d2f672c943
|
RevenuecatPurchasesCapacitor: 51cd81cec7db693a2552b45c36261d849e1cf574
|
||||||
RevenuecatPurchasesCapacitorUi: 0cf343ae4e1c8b30b294c971a50827316b210a63
|
RevenuecatPurchasesCapacitorUi: 267758d61aba42a0a3f035e6ba3c4630112357e4
|
||||||
RevenueCatUI: 03025a80b64ce8ff8e43f47a7eb9dbbb6b4f486e
|
RevenueCatUI: 3c72ef61b54c3f4787afab6a3b0f00cbec192063
|
||||||
SQLCipher: ba9d0076041ed767c5bd3d3f77098318d04a403c
|
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
|
||||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||||
|
|
||||||
PODFILE CHECKSUM: 289063dee9cd9d5cc8abd18e13b4153b38864021
|
PODFILE CHECKSUM: 289063dee9cd9d5cc8abd18e13b4153b38864021
|
||||||
|
|||||||
31
src/App.jsx
31
src/App.jsx
@@ -23,6 +23,21 @@ const remove = className => {
|
|||||||
// TODO: Update the interval to at 60 minutes
|
// TODO: Update the interval to at 60 minutes
|
||||||
const intervalMS = 5 * 60 * 1000 // 5 minutes
|
const intervalMS = 5 * 60 * 1000 // 5 minutes
|
||||||
|
|
||||||
|
const startOpenReplay = () => {
|
||||||
|
if (!import.meta.env.VITE_OPENREPLAY_PROJECT_KEY) return
|
||||||
|
const tracker = new Tracker({
|
||||||
|
projectKey: import.meta.env.VITE_OPENREPLAY_PROJECT_KEY,
|
||||||
|
})
|
||||||
|
tracker.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
const startApiManager = async navigate => {
|
||||||
|
await apiManager.init()
|
||||||
|
apiManager.setNavigateToLogin(() => {
|
||||||
|
navigate('/login')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const AppContent = () => {
|
const AppContent = () => {
|
||||||
const { showNotification } = useNotification()
|
const { showNotification } = useNotification()
|
||||||
|
|
||||||
@@ -126,20 +141,4 @@ function App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const startOpenReplay = () => {
|
|
||||||
if (!import.meta.env.VITE_OPENREPLAY_PROJECT_KEY) return
|
|
||||||
const tracker = new Tracker({
|
|
||||||
projectKey: import.meta.env.VITE_OPENREPLAY_PROJECT_KEY,
|
|
||||||
})
|
|
||||||
|
|
||||||
tracker.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
const startApiManager = navigate => {
|
|
||||||
apiManager.init()
|
|
||||||
apiManager.setNavigateToLogin(() => {
|
|
||||||
navigate('/login')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
@@ -702,7 +702,15 @@ const NotificationTemplate = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
{showSaveDefault && (
|
{showSaveDefault && (
|
||||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'start',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
size='sm'
|
size='sm'
|
||||||
@@ -723,7 +731,7 @@ const NotificationTemplate = ({
|
|||||||
setShowSaveDefault(false)
|
setShowSaveDefault(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save as Default
|
Save Preference
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import App from '@/App'
|
import App from '@/App'
|
||||||
import ChoreEdit from '@/views/ChoreEdit/ChoreEdit'
|
import ChoreEdit from '@/views/ChoreEdit/ChoreEdit'
|
||||||
import Error from '@/views/Error'
|
import Error from '@/views/Error'
|
||||||
|
import AccountSettings from '@/views/Settings/AccountSettings'
|
||||||
|
import AdvancedSettings from '@/views/Settings/AdvancedSettings'
|
||||||
|
import CircleSettings from '@/views/Settings/CircleSettings'
|
||||||
|
import NotificationSettings from '@/views/Settings/NotificationSettings'
|
||||||
import Settings from '@/views/Settings/Settings'
|
import Settings from '@/views/Settings/Settings'
|
||||||
import SettingsOverview from '@/views/Settings/SettingsOverview'
|
import SettingsOverview from '@/views/Settings/SettingsOverview'
|
||||||
|
import SettingsRoutes from '@/views/Settings/SettingsRoutes'
|
||||||
|
import ThemeSettings from '@/views/Settings/ThemeSettings'
|
||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
||||||
import AuthenticationLoading from '../views/Authorization/Authenticating'
|
import AuthenticationLoading from '../views/Authorization/Authenticating'
|
||||||
@@ -15,12 +21,18 @@ import ChoreView from '../views/ChoreEdit/ChoreView'
|
|||||||
import ArchivedTasks from '../views/Chores/ArchivedTasks'
|
import ArchivedTasks from '../views/Chores/ArchivedTasks'
|
||||||
import MyChores from '../views/Chores/MyChores'
|
import MyChores from '../views/Chores/MyChores'
|
||||||
import JoinCircleView from '../views/Circles/JoinCircle'
|
import JoinCircleView from '../views/Circles/JoinCircle'
|
||||||
|
import NotFound from '../views/components/NotFound'
|
||||||
import ChoreHistory from '../views/History/ChoreHistory'
|
import ChoreHistory from '../views/History/ChoreHistory'
|
||||||
import LabelView from '../views/Labels/LabelView'
|
import LabelView from '../views/Labels/LabelView'
|
||||||
import Landing from '../views/Landing/Landing'
|
import Landing from '../views/Landing/Landing'
|
||||||
import PaymentCancelledView from '../views/Payments/PaymentFailView'
|
import PaymentCancelledView from '../views/Payments/PaymentFailView'
|
||||||
import PaymentSuccessView from '../views/Payments/PaymentSuccessView'
|
import PaymentSuccessView from '../views/Payments/PaymentSuccessView'
|
||||||
import PrivacyPolicyView from '../views/PrivacyPolicy/PrivacyPolicyView'
|
import PrivacyPolicyView from '../views/PrivacyPolicy/PrivacyPolicyView'
|
||||||
|
import APITokenSettings from '../views/Settings/APITokenSettings'
|
||||||
|
import MFASettings from '../views/Settings/MFASettings'
|
||||||
|
import ProfileSettings from '../views/Settings/ProfileSettings'
|
||||||
|
import SidepanelSettings from '../views/Settings/SidepanelSettings'
|
||||||
|
import StorageSettings from '../views/Settings/StorageSettings'
|
||||||
import TermsView from '../views/Terms/TermsView'
|
import TermsView from '../views/Terms/TermsView'
|
||||||
import TestView from '../views/TestView/Test'
|
import TestView from '../views/TestView/Test'
|
||||||
import ThingsHistory from '../views/Things/ThingsHistory'
|
import ThingsHistory from '../views/Things/ThingsHistory'
|
||||||
@@ -28,7 +40,6 @@ import ThingsView from '../views/Things/ThingsView'
|
|||||||
import TimerDetails from '../views/Timer/TimerDetails'
|
import TimerDetails from '../views/Timer/TimerDetails'
|
||||||
import UserActivities from '../views/User/UserActivities'
|
import UserActivities from '../views/User/UserActivities'
|
||||||
import UserPoints from '../views/User/UserPoints'
|
import UserPoints from '../views/User/UserPoints'
|
||||||
import NotFound from '../views/components/NotFound'
|
|
||||||
const getMainRoute = () => {
|
const getMainRoute = () => {
|
||||||
if (
|
if (
|
||||||
import.meta.env.VITE_IS_LANDING_DEFAULT === 'true' &&
|
import.meta.env.VITE_IS_LANDING_DEFAULT === 'true' &&
|
||||||
@@ -50,11 +61,57 @@ const Router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
element: <SettingsOverview />,
|
element: <SettingsRoutes />,
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: '/settings/detailed',
|
index: true,
|
||||||
element: <Settings />,
|
element: <SettingsOverview />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'detailed',
|
||||||
|
element: <Settings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'profile',
|
||||||
|
element: <ProfileSettings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'circle',
|
||||||
|
element: <CircleSettings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'account',
|
||||||
|
element: <AccountSettings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'notifications',
|
||||||
|
element: <NotificationSettings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'mfa',
|
||||||
|
element: <MFASettings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'apitokens',
|
||||||
|
element: <APITokenSettings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'storage',
|
||||||
|
element: <StorageSettings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sidepanel',
|
||||||
|
element: <SidepanelSettings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'theme',
|
||||||
|
element: <ThemeSettings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'advanced',
|
||||||
|
element: <AdvancedSettings />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/chores',
|
path: '/chores',
|
||||||
|
|||||||
39
src/hooks/useAcknowledgmentModal.js
Normal file
39
src/hooks/useAcknowledgmentModal.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const useAcknowledgmentModal = () => {
|
||||||
|
const [ackModalConfig, setAckModalConfig] = useState({})
|
||||||
|
|
||||||
|
const showAcknowledgment = (
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
onAcknowledge,
|
||||||
|
acknowledgeText = 'Got it',
|
||||||
|
color = 'primary',
|
||||||
|
) => {
|
||||||
|
setAckModalConfig({
|
||||||
|
isOpen: true,
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
acknowledgeText,
|
||||||
|
color,
|
||||||
|
onClose: () => {
|
||||||
|
if (onAcknowledge) {
|
||||||
|
onAcknowledge()
|
||||||
|
}
|
||||||
|
setAckModalConfig({})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideAcknowledgment = () => {
|
||||||
|
setAckModalConfig({})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ackModalConfig,
|
||||||
|
showAcknowledgment,
|
||||||
|
hideAcknowledgment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAcknowledgmentModal
|
||||||
@@ -10,21 +10,31 @@ class ApiManager {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.customServerURL = `${API_URL}/api/v1`
|
this.customServerURL = `${API_URL}/api/v1`
|
||||||
this.initialized = false
|
this.initialized = false
|
||||||
|
this.initPromise = null
|
||||||
this.navigateToLogin = () => {}
|
this.navigateToLogin = () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if (this.initialized) {
|
if (this.initPromise) {
|
||||||
return
|
return this.initPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.initialized) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initPromise = this._doInit()
|
||||||
|
return this.initPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
async _doInit() {
|
||||||
const { value: serverURL } = await Preferences.get({
|
const { value: serverURL } = await Preferences.get({
|
||||||
key: 'customServerUrl',
|
key: 'customServerUrl',
|
||||||
})
|
})
|
||||||
|
|
||||||
this.customServerURL = `${serverURL || API_URL}/api/v1`
|
this.customServerURL = `${serverURL || API_URL}/api/v1`
|
||||||
await localStore.initDatabase()
|
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
|
await localStore.initDatabase()
|
||||||
}
|
}
|
||||||
|
|
||||||
getApiURL() {
|
getApiURL() {
|
||||||
@@ -47,6 +57,8 @@ export const getAssetURL = path => {
|
|||||||
return `${baseURL}/assets/${path}`
|
return `${baseURL}/assets/${path}`
|
||||||
}
|
}
|
||||||
export async function UploadFile(url, options) {
|
export async function UploadFile(url, options) {
|
||||||
|
await apiManager.init()
|
||||||
|
|
||||||
if (!isTokenValid()) {
|
if (!isTokenValid()) {
|
||||||
Cookies.set('ca_redirect', window.location.pathname)
|
Cookies.set('ca_redirect', window.location.pathname)
|
||||||
window.location.href = '/login'
|
window.location.href = '/login'
|
||||||
@@ -65,6 +77,7 @@ export async function UploadFile(url, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function Fetch(url, options) {
|
export async function Fetch(url, options) {
|
||||||
|
await apiManager.init()
|
||||||
if (!isTokenValid()) {
|
if (!isTokenValid()) {
|
||||||
Cookies.set('ca_redirect', window.location.pathname)
|
Cookies.set('ca_redirect', window.location.pathname)
|
||||||
if (window.location.pathname !== '/login') {
|
if (window.location.pathname !== '/login') {
|
||||||
@@ -148,6 +161,7 @@ export const isTokenValid = () => {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const refreshAccessToken = () => {
|
export const refreshAccessToken = () => {
|
||||||
|
|||||||
@@ -425,6 +425,7 @@ const LoginView = () => {
|
|||||||
label='Password'
|
label='Password'
|
||||||
type='password'
|
type='password'
|
||||||
id='password'
|
id='password'
|
||||||
|
autoComplete='password'
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setPassword(e.target.value)
|
setPassword(e.target.value)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Add, HorizontalRule } from '@mui/icons-material'
|
import { Add, HorizontalRule, Save } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -102,6 +102,10 @@ const ChoreEdit = () => {
|
|||||||
const [errors, setErrors] = useState({})
|
const [errors, setErrors] = useState({})
|
||||||
const [attemptToSave, setAttemptToSave] = useState(false)
|
const [attemptToSave, setAttemptToSave] = useState(false)
|
||||||
const [addLabelModalOpen, setAddLabelModalOpen] = useState(false)
|
const [addLabelModalOpen, setAddLabelModalOpen] = useState(false)
|
||||||
|
const [showSavePrivacyDefault, setShowSavePrivacyDefault] = useState(false)
|
||||||
|
const [privacySaved, setPrivacySaved] = useState(false)
|
||||||
|
const [showSaveNotificationDefault, setShowSaveNotificationDefault] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
const { data: userLabelsRaw, isLoading: isUserLabelsLoading } = useLabels()
|
const { data: userLabelsRaw, isLoading: isUserLabelsLoading } = useLabels()
|
||||||
const updateChoreMutation = useUpdateChore()
|
const updateChoreMutation = useUpdateChore()
|
||||||
@@ -272,6 +276,23 @@ const ChoreEdit = () => {
|
|||||||
setAllUserThings(data.res)
|
setAllUserThings(data.res)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Load default privacy setting for new chores
|
||||||
|
if (!choreId) {
|
||||||
|
const defaultPrivacySetting = localStorage.getItem(
|
||||||
|
'defaultPrivacySetting',
|
||||||
|
)
|
||||||
|
if (defaultPrivacySetting !== null) {
|
||||||
|
setIsPrivate(JSON.parse(defaultPrivacySetting))
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultNotificationSetting = localStorage.getItem(
|
||||||
|
'defaultNotificationSetting',
|
||||||
|
)
|
||||||
|
if (defaultNotificationSetting !== null) {
|
||||||
|
setIsNotificable(JSON.parse(defaultNotificationSetting))
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isChoreLoading === false && choreData && choreId) {
|
if (isChoreLoading === false && choreData && choreId) {
|
||||||
@@ -1091,7 +1112,9 @@ const ChoreEdit = () => {
|
|||||||
name='isPrivate'
|
name='isPrivate'
|
||||||
value={isPrivate}
|
value={isPrivate}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setIsPrivate(event.target.value === 'true' ? true : false)
|
const newValue = event.target.value === 'true' ? true : false
|
||||||
|
setIsPrivate(newValue)
|
||||||
|
setShowSavePrivacyDefault(true)
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
'& > div': { py: 1 },
|
'& > div': { py: 1 },
|
||||||
@@ -1108,6 +1131,33 @@ const ChoreEdit = () => {
|
|||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
|
{showSavePrivacyDefault && (
|
||||||
|
<Box sx={{ mt: 0, display: 'flex', justifyContent: 'start' }}>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
size='sm'
|
||||||
|
color='neutral'
|
||||||
|
startDecorator={<Save />}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 6,
|
||||||
|
fontWeight: 500,
|
||||||
|
'&:hover': {
|
||||||
|
background: 'neutral.softHoverBg',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'defaultPrivacySetting',
|
||||||
|
JSON.stringify(isPrivate),
|
||||||
|
)
|
||||||
|
setShowSavePrivacyDefault(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save Preference
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -254,12 +254,11 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
|
|||||||
p: 2,
|
p: 2,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
boxShadow: 'sm',
|
boxShadow: 'sm',
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
|
width: '310px',
|
||||||
minHeight: 300,
|
minHeight: 300,
|
||||||
width: '315px',
|
maxHeight: 400,
|
||||||
mb: 1,
|
mb: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -311,12 +310,11 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
|
|||||||
p: 2,
|
p: 2,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
boxShadow: 'sm',
|
boxShadow: 'sm',
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
// width: '290px',
|
width: '310px',
|
||||||
minHeight: 300,
|
minHeight: 300,
|
||||||
|
maxHeight: 400,
|
||||||
mb: 1,
|
mb: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -38,9 +38,10 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { useChores } from '../../queries/ChoreQueries'
|
import { useChores } from '../../queries/ChoreQueries'
|
||||||
import { useNotification } from '../../service/NotificationProvider'
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
|
import { TASK_COLOR } from '../../utils/Colors'
|
||||||
import { ArchiveChore } from '../../utils/Fetcher'
|
import { ArchiveChore } from '../../utils/Fetcher'
|
||||||
import Priorities from '../../utils/Priorities'
|
import Priorities from '../../utils/Priorities'
|
||||||
import LoadingComponent from '../components/Loading'
|
import LoadingComponent from '../components/Loading'
|
||||||
@@ -102,6 +103,7 @@ const MyChores = () => {
|
|||||||
const [selectedCalendarDate, setSelectedCalendarDate] = useState(new Date())
|
const [selectedCalendarDate, setSelectedCalendarDate] = useState(new Date())
|
||||||
const menuRef = useRef(null)
|
const menuRef = useRef(null)
|
||||||
const Navigate = useNavigate()
|
const Navigate = useNavigate()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
|
||||||
const {
|
const {
|
||||||
data: choresData,
|
data: choresData,
|
||||||
@@ -126,14 +128,14 @@ const MyChores = () => {
|
|||||||
) {
|
) {
|
||||||
setPerformers(membersData.res)
|
setPerformers(membersData.res)
|
||||||
let sortedChores = choresData.res.sort(ChoreSorter)
|
let sortedChores = choresData.res.sort(ChoreSorter)
|
||||||
|
|
||||||
// Filter chores based on impersonated user
|
// Filter chores based on impersonated user
|
||||||
if (impersonatedUser) {
|
if (impersonatedUser) {
|
||||||
sortedChores = sortedChores.filter(chore =>
|
sortedChores = sortedChores.filter(
|
||||||
chore.assignedTo === impersonatedUser.userId
|
chore => chore.assignedTo === impersonatedUser.userId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setChores(sortedChores)
|
setChores(sortedChores)
|
||||||
setFilteredChores(sortedChores)
|
setFilteredChores(sortedChores)
|
||||||
const sections = ChoresGrouper(
|
const sections = ChoresGrouper(
|
||||||
@@ -400,6 +402,72 @@ const MyChores = () => {
|
|||||||
document.removeEventListener('keyup', handleKeyUp)
|
document.removeEventListener('keyup', handleKeyUp)
|
||||||
}
|
}
|
||||||
}, [isMultiSelectMode, selectedChores.size, addTaskModalOpen])
|
}, [isMultiSelectMode, selectedChores.size, addTaskModalOpen])
|
||||||
|
|
||||||
|
// Auto-update selected calendar date when day changes (large screen only)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLargeScreen || viewMode !== 'calendar' || !selectedCalendarDate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const tomorrow = new Date(now)
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||||
|
tomorrow.setHours(0, 0, 0, 0) // Set to start of tomorrow
|
||||||
|
|
||||||
|
const msUntilTomorrow = tomorrow.getTime() - now.getTime()
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
const today = new Date()
|
||||||
|
const yesterday = new Date(today)
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1)
|
||||||
|
|
||||||
|
// Check if selected date is now yesterday and update to today
|
||||||
|
if (selectedCalendarDate.toDateString() === yesterday.toDateString()) {
|
||||||
|
setSelectedCalendarDate(today)
|
||||||
|
}
|
||||||
|
}, msUntilTomorrow)
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [isLargeScreen, viewMode, selectedCalendarDate])
|
||||||
|
|
||||||
|
// Handle URL parameters for navigation from calendar
|
||||||
|
useEffect(() => {
|
||||||
|
const filter = searchParams.get('filter')
|
||||||
|
const view = searchParams.get('view')
|
||||||
|
|
||||||
|
if (view === 'calendar') {
|
||||||
|
setViewMode('calendar')
|
||||||
|
setSearchFilter('All')
|
||||||
|
setSelectedCalendarDate(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter && chores.length > 0) {
|
||||||
|
let filterKey = ''
|
||||||
|
let filteredChores = []
|
||||||
|
|
||||||
|
switch (filter) {
|
||||||
|
case 'overdue':
|
||||||
|
filterKey = 'Overdue'
|
||||||
|
filteredChores = FILTERS['Overdue'](chores)
|
||||||
|
break
|
||||||
|
case 'unplanned':
|
||||||
|
filterKey = 'No Due Date'
|
||||||
|
filteredChores = FILTERS['No Due Date'](chores)
|
||||||
|
break
|
||||||
|
case 'pending-approval':
|
||||||
|
filterKey = 'Pending Approval'
|
||||||
|
filteredChores = FILTERS['Pending Approval'](chores)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredChores(filteredChores)
|
||||||
|
setSearchFilter(filterKey)
|
||||||
|
setViewMode('default')
|
||||||
|
}
|
||||||
|
}, [searchParams, chores])
|
||||||
const setSelectedChoreSectionWithCache = value => {
|
const setSelectedChoreSectionWithCache = value => {
|
||||||
setSelectedChoreSection(value)
|
setSelectedChoreSection(value)
|
||||||
localStorage.setItem('selectedChoreSection', value)
|
localStorage.setItem('selectedChoreSection', value)
|
||||||
@@ -454,16 +522,16 @@ const MyChores = () => {
|
|||||||
if (searchTerm?.length > 0 || searchFilter !== 'All') {
|
if (searchTerm?.length > 0 || searchFilter !== 'All') {
|
||||||
return filteredChores
|
return filteredChores
|
||||||
}
|
}
|
||||||
|
|
||||||
let choresToFilter = chores
|
let choresToFilter = chores
|
||||||
|
|
||||||
// Filter by impersonated user first if set
|
// Filter by impersonated user first if set
|
||||||
if (impersonatedUser) {
|
if (impersonatedUser) {
|
||||||
choresToFilter = choresToFilter.filter(chore =>
|
choresToFilter = choresToFilter.filter(
|
||||||
chore.assignedTo === impersonatedUser.userId
|
chore => chore.assignedTo === impersonatedUser.userId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return choresToFilter.filter(ChoreFilters(userProfile)[selectedChoreFilter])
|
return choresToFilter.filter(ChoreFilters(userProfile)[selectedChoreFilter])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,14 +548,14 @@ const MyChores = () => {
|
|||||||
|
|
||||||
const updateChores = newChore => {
|
const updateChores = newChore => {
|
||||||
let newChores = [...chores, newChore]
|
let newChores = [...chores, newChore]
|
||||||
|
|
||||||
// Filter chores based on impersonated user
|
// Filter chores based on impersonated user
|
||||||
if (impersonatedUser) {
|
if (impersonatedUser) {
|
||||||
newChores = newChores.filter(chore =>
|
newChores = newChores.filter(
|
||||||
chore.assignedTo === impersonatedUser.userId
|
chore => chore.assignedTo === impersonatedUser.userId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setChores(newChores)
|
setChores(newChores)
|
||||||
setFilteredChores(newChores)
|
setFilteredChores(newChores)
|
||||||
setChoreSections(
|
setChoreSections(
|
||||||
@@ -1658,6 +1726,128 @@ const MyChores = () => {
|
|||||||
)}
|
)}
|
||||||
{viewMode === 'calendar' && (
|
{viewMode === 'calendar' && (
|
||||||
<>
|
<>
|
||||||
|
{/* Summary Chips when no date selected */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
mb: 1,
|
||||||
|
display: 'flex',
|
||||||
|
gap: 1.5,
|
||||||
|
justifyContent: 'start',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{FILTERS['Overdue'](getFilteredChores()).length > 0 && (
|
||||||
|
<Chip
|
||||||
|
variant='soft'
|
||||||
|
color='danger'
|
||||||
|
size='lg'
|
||||||
|
onClick={() => {
|
||||||
|
// Update URL for navigation context
|
||||||
|
Navigate('/chores?filter=overdue', {
|
||||||
|
replace: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Also update state directly for immediate smooth transition
|
||||||
|
const overdueChores =
|
||||||
|
FILTERS['Overdue'](getFilteredChores())
|
||||||
|
setFilteredChores(overdueChores)
|
||||||
|
setSearchFilter('Overdue')
|
||||||
|
setViewMode('default')
|
||||||
|
setSelectedCalendarDate(null)
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
px: 1,
|
||||||
|
py: 0.5,
|
||||||
|
}}
|
||||||
|
startDecorator={
|
||||||
|
<Chip size='md' variant='solid' color='danger'>
|
||||||
|
{FILTERS['Overdue'](getFilteredChores()).length}
|
||||||
|
</Chip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Overdue
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{FILTERS['No Due Date'](getFilteredChores()).length > 0 && (
|
||||||
|
<Chip
|
||||||
|
variant='soft'
|
||||||
|
color='neutral'
|
||||||
|
size='lg'
|
||||||
|
onClick={() => {
|
||||||
|
// Update URL for navigation context
|
||||||
|
Navigate('/chores?filter=unplanned', {
|
||||||
|
replace: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Also update state directly for immediate smooth transition
|
||||||
|
const unplannedChores =
|
||||||
|
FILTERS['No Due Date'](getFilteredChores())
|
||||||
|
setFilteredChores(unplannedChores)
|
||||||
|
setSearchFilter('No Due Date')
|
||||||
|
setViewMode('default')
|
||||||
|
setSelectedCalendarDate(null)
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
px: 1,
|
||||||
|
py: 0.5,
|
||||||
|
}}
|
||||||
|
startDecorator={
|
||||||
|
<Chip size='md' variant='solid' color='neutral'>
|
||||||
|
{FILTERS['No Due Date'](getFilteredChores()).length}
|
||||||
|
</Chip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Unplanned
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{FILTERS['Pending Approval'](getFilteredChores()).length > 0 && (
|
||||||
|
<Chip
|
||||||
|
variant='soft'
|
||||||
|
size='lg'
|
||||||
|
onClick={() => {
|
||||||
|
// Update URL for navigation context
|
||||||
|
Navigate('/chores?filter=pending-approval', {
|
||||||
|
replace: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Also update state directly for immediate smooth transition
|
||||||
|
const pendingApprovalChores =
|
||||||
|
FILTERS['Pending Approval'](getFilteredChores())
|
||||||
|
setFilteredChores(pendingApprovalChores)
|
||||||
|
setSearchFilter('Pending Approval')
|
||||||
|
setViewMode('default')
|
||||||
|
setSelectedCalendarDate(null)
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
px: 1,
|
||||||
|
py: 0.5,
|
||||||
|
}}
|
||||||
|
startDecorator={
|
||||||
|
<Chip
|
||||||
|
size='md'
|
||||||
|
variant='solid'
|
||||||
|
sx={{
|
||||||
|
bgcolor: TASK_COLOR.PENDING_REVIEW,
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{FILTERS['Pending Approval'](getFilteredChores()).length}
|
||||||
|
</Chip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Pending Approval
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
{/* Calendar Monthly View */}
|
{/* Calendar Monthly View */}
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
{isLargeScreen ? (
|
{isLargeScreen ? (
|
||||||
@@ -1951,6 +2141,11 @@ const FILTERS = {
|
|||||||
return chore.nextDueDate === null
|
return chore.nextDueDate === null
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
'Pending Approval': function (chores) {
|
||||||
|
return chores.filter(chore => {
|
||||||
|
return chore.status === 3
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MyChores
|
export default MyChores
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { useChoresHistory } from '../../queries/ChoreQueries'
|
import { useChoresHistory } from '../../queries/ChoreQueries'
|
||||||
import { ChoresGrouper } from '../../utils/Chores'
|
import { ChoresGrouper } from '../../utils/Chores'
|
||||||
import { getSidepanelConfig } from '../../utils/SidepanelConfig'
|
import { getSidepanelConfig } from '../../utils/SidepanelConfig'
|
||||||
import CalendarView from '../components/CalendarView'
|
import CalendarCard from '../components/CalendarCard'
|
||||||
import ActivitiesCard from './ActivitesCard'
|
import ActivitiesCard from './ActivitesCard'
|
||||||
import TasksByAssigneeCard from './TasksByAssigneeCard'
|
import TasksByAssigneeCard from './TasksByAssigneeCard'
|
||||||
import UserSwitcher from './UserSwitcher'
|
import UserSwitcher from './UserSwitcher'
|
||||||
@@ -78,7 +78,7 @@ const Sidepanel = ({ chores }) => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{ width: '100%', overflowY: 'hidden', overflowX: 'hidden' }}
|
sx={{ width: '100%', overflowY: 'hidden', overflowX: 'hidden' }}
|
||||||
>
|
>
|
||||||
<CalendarView chores={chores} />
|
<CalendarCard chores={chores} />
|
||||||
</Box>
|
</Box>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,11 +2,19 @@ import { Box, Container, Input, Sheet, Typography } from '@mui/joy'
|
|||||||
import Logo from '../../Logo'
|
import Logo from '../../Logo'
|
||||||
|
|
||||||
import { Button } from '@mui/joy'
|
import { Button } from '@mui/joy'
|
||||||
|
import { useState } from 'react'
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
|
import useAcknowledgmentModal from '../../hooks/useAcknowledgmentModal'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
import { JoinCircle } from '../../utils/Fetcher'
|
import { JoinCircle } from '../../utils/Fetcher'
|
||||||
|
import AcknowledgmentModal from '../Modals/Inputs/AcknowledgmentModal'
|
||||||
|
|
||||||
const JoinCircleView = () => {
|
const JoinCircleView = () => {
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
|
const { showError } = useNotification()
|
||||||
|
const { ackModalConfig, showAcknowledgment } = useAcknowledgmentModal()
|
||||||
|
const [isJoining, setIsJoining] = useState(false)
|
||||||
|
|
||||||
let [searchParams, setSearchParams] = useSearchParams()
|
let [searchParams, setSearchParams] = useSearchParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -79,25 +87,31 @@ const JoinCircleView = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
size='lg'
|
size='lg'
|
||||||
sx={{ mt: 3, mb: 2 }}
|
sx={{ mt: 3, mb: 2 }}
|
||||||
|
disabled={isJoining}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
setIsJoining(true)
|
||||||
JoinCircle(code).then(resp => {
|
JoinCircle(code).then(resp => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
alert(
|
showAcknowledgment(
|
||||||
'Joined circle successfully, wait for the circle owner to accept your request.',
|
'Your join request has been sent successfully! The circle admin will need to approve your request before you can access the circle and its chores. You will receive a notification once your request is approved.',
|
||||||
|
'Join Request Sent!',
|
||||||
|
() => navigate('/'),
|
||||||
|
'Got it',
|
||||||
|
'success',
|
||||||
)
|
)
|
||||||
navigate('/chores')
|
|
||||||
} else {
|
} else {
|
||||||
|
setIsJoining(false)
|
||||||
if (resp.status === 409) {
|
if (resp.status === 409) {
|
||||||
alert('You are already a member of this circle')
|
showError('You are already a member of this circle')
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to join circle')
|
showError('Failed to join circle')
|
||||||
}
|
}
|
||||||
navigate('/chores')
|
navigate('/')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Join Circle
|
{isJoining ? 'Joining...' : 'Join Circle'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -141,6 +155,7 @@ const JoinCircleView = () => {
|
|||||||
))}
|
))}
|
||||||
</Sheet>
|
</Sheet>
|
||||||
</Box>
|
</Box>
|
||||||
|
<AcknowledgmentModal config={ackModalConfig} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -553,7 +553,7 @@ const LabelView = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth='md' sx={{ px: 0 }}>
|
<Container maxWidth='md' sx={{ px: 0 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2, p: 2 }}>
|
||||||
{/* <EmojiEvents sx={{ fontSize: '2rem', color: '#FFD700' }} /> */}
|
{/* <EmojiEvents sx={{ fontSize: '2rem', color: '#FFD700' }} /> */}
|
||||||
<Stack sx={{ flex: 1 }}>
|
<Stack sx={{ flex: 1 }}>
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const HomeHero = () => {
|
|||||||
const windowThreshold = 600
|
const windowThreshold = 600
|
||||||
const { mode, setMode } = useColorScheme()
|
const { mode, setMode } = useColorScheme()
|
||||||
const HERO_TEXT_THAT = [
|
const HERO_TEXT_THAT = [
|
||||||
'An open-source, user-friendly app for managing tasks and chores, featuring customizable options to help you and others stay organized',
|
'The task manager that helps familiesand individuals stay on top of chores with smart scheduling, fair assignments, and powerful automation.',
|
||||||
]
|
]
|
||||||
|
|
||||||
const [heroTextIndex, setHeroTextIndex] = React.useState(0)
|
const [heroTextIndex, setHeroTextIndex] = React.useState(0)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable react/jsx-key */
|
/* eslint-disable react/jsx-key */
|
||||||
import { CheckRounded } from '@mui/icons-material'
|
import { CheckRounded } from '@mui/icons-material'
|
||||||
import { Box, Button, Card, Container, Typography } from '@mui/joy'
|
import { Container, Typography } from '@mui/joy'
|
||||||
import React from 'react'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
const PricingSection = () => {
|
const PricingSection = () => {
|
||||||
@@ -67,111 +66,11 @@ const PricingSection = () => {
|
|||||||
<Typography level='h4' mt={2} mb={2}>
|
<Typography level='h4' mt={2} mb={2}>
|
||||||
Pricing
|
Pricing
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Container maxWidth={'sm'} sx={{ mb: 8 }}>
|
<Container maxWidth={'sm'} sx={{ mb: 8 }}>
|
||||||
<Typography level='body-md' color='neutral'>
|
<Typography level='body-md' color='neutral'>
|
||||||
Choose the plan that works best for you.
|
Choose the plan that works best for you.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<div
|
|
||||||
className='mt-8 grid grid-cols-1 gap-2 sm:grid-cols-1 lg:grid-cols-2'
|
|
||||||
data-aos-id-pricing
|
|
||||||
>
|
|
||||||
{PRICEITEMS.map((pi, index) => (
|
|
||||||
<Card
|
|
||||||
key={index}
|
|
||||||
data-aos-delay={50 * (1 + index)}
|
|
||||||
data-aos-anchor='[data-aos-id-pricing]'
|
|
||||||
data-aos='fade-up'
|
|
||||||
className='hover:bg-white dark:hover:bg-teal-900'
|
|
||||||
sx={{
|
|
||||||
textAlign: 'center',
|
|
||||||
p: 5,
|
|
||||||
minHeight: 400,
|
|
||||||
// maxWidth: 400,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
// when top reach the top change the background color:
|
|
||||||
'&:hover': {
|
|
||||||
// backgroundColor: '#FFFFFF',
|
|
||||||
boxShadow: '0px 0px 20px rgba(0, 0, 0, 0.1)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
display='flex'
|
|
||||||
flexDirection='column'
|
|
||||||
justifyContent='flex-start' // Updated property
|
|
||||||
alignItems='center'
|
|
||||||
>
|
|
||||||
<Typography level='h2'>{pi.title}</Typography>
|
|
||||||
<Typography level='body-md'>{pi.description}</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
display='flex'
|
|
||||||
flexDirection='column'
|
|
||||||
justifyContent='center'
|
|
||||||
alignItems='center'
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
display='flex'
|
|
||||||
flexDirection='row'
|
|
||||||
alignItems='baseline'
|
|
||||||
sx={{ my: 4 }}
|
|
||||||
>
|
|
||||||
{pi.discount && (
|
|
||||||
<Typography
|
|
||||||
level='h3'
|
|
||||||
component='span'
|
|
||||||
sx={{ textDecoration: 'line-through', opacity: 0.5 }}
|
|
||||||
>
|
|
||||||
${pi.previousPrice}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<Typography level='h2' component='span'>
|
|
||||||
${pi.price}
|
|
||||||
</Typography>
|
|
||||||
<Typography level='body-md' component='span'>
|
|
||||||
/ {pi.interval}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography level='title-md'>Features</Typography>
|
|
||||||
{pi.features.map(feature => (
|
|
||||||
<Typography
|
|
||||||
startDecorator={feature[1]}
|
|
||||||
level='body-md'
|
|
||||||
color='neutral'
|
|
||||||
lineHeight={1.6}
|
|
||||||
>
|
|
||||||
{feature[0]}
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Here start the test */}
|
|
||||||
<div style={{ marginTop: 'auto' }}>
|
|
||||||
<Button
|
|
||||||
sx={{ mt: 5 }}
|
|
||||||
onClick={() => {
|
|
||||||
navigate('/settings#account')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Get Started
|
|
||||||
</Button>
|
|
||||||
<Typography
|
|
||||||
level='body-md'
|
|
||||||
color='neutral'
|
|
||||||
lineHeight={1.6}
|
|
||||||
></Typography>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Here start the test */}
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
108
src/views/Modals/Inputs/AcknowledgmentModal.jsx
Normal file
108
src/views/Modals/Inputs/AcknowledgmentModal.jsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { Box, Button, Typography } from '@mui/joy'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
|
||||||
|
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||||
|
|
||||||
|
function AcknowledgmentModal({ config }) {
|
||||||
|
const { ResponsiveModal } = useResponsiveModal()
|
||||||
|
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
||||||
|
|
||||||
|
const handleAction = useCallback(() => {
|
||||||
|
config.onClose()
|
||||||
|
}, [config])
|
||||||
|
|
||||||
|
// Keyboard shortcuts for acknowledgment modal
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = event => {
|
||||||
|
if (!config?.isOpen) return
|
||||||
|
|
||||||
|
// Show keyboard shortcuts when Ctrl/Cmd is pressed
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
|
setShowKeyboardShortcuts(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl/Cmd + Y for acknowledge
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === 'y') {
|
||||||
|
event.preventDefault()
|
||||||
|
handleAction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape key for acknowledge
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
|
handleAction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter key for acknowledge
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
handleAction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyUp = event => {
|
||||||
|
if (!event.ctrlKey && !event.metaKey) {
|
||||||
|
setShowKeyboardShortcuts(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config?.isOpen) {
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
document.addEventListener('keyup', handleKeyUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
document.removeEventListener('keyup', handleKeyUp)
|
||||||
|
}
|
||||||
|
}, [config?.isOpen, handleAction])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveModal
|
||||||
|
open={config?.isOpen}
|
||||||
|
onClose={config?.onClose}
|
||||||
|
size='md'
|
||||||
|
unmountDelay={250}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{ p: 2, minWidth: { xs: '100%', sm: '400px' }, maxWidth: '500px' }}
|
||||||
|
>
|
||||||
|
<Typography level='h4' mb={2} textAlign='center'>
|
||||||
|
{config?.title}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
level='body-md'
|
||||||
|
mb={3}
|
||||||
|
sx={{
|
||||||
|
lineHeight: 1.6,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{config?.message}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box display={'flex'} justifyContent={'center'} mt={2}>
|
||||||
|
<Button
|
||||||
|
size='lg'
|
||||||
|
onClick={handleAction}
|
||||||
|
color={config?.color || 'primary'}
|
||||||
|
fullWidth
|
||||||
|
endDecorator={
|
||||||
|
<KeyboardShortcutHint shortcut='Y' show={showKeyboardShortcuts} />
|
||||||
|
}
|
||||||
|
sx={{ minWidth: '120px' }}
|
||||||
|
>
|
||||||
|
{config?.acknowledgeText}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</ResponsiveModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AcknowledgmentModal
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import { isPlusAccount } from '../../utils/Helpers'
|
import { isPlusAccount } from '../../utils/Helpers'
|
||||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||||
import TextModal from '../Modals/Inputs/TextModal'
|
import TextModal from '../Modals/Inputs/TextModal'
|
||||||
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
|
||||||
const APITokenSettings = () => {
|
const APITokenSettings = () => {
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
@@ -77,142 +78,144 @@ const APITokenSettings = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='grid gap-4 py-4' id='apitokens'>
|
<SettingsLayout title='API Tokens'>
|
||||||
<Typography level='h3'>Access Token</Typography>
|
<div className='grid gap-4 py-4' id='apitokens'>
|
||||||
<Divider />
|
<Typography level='h3'>Access Token</Typography>
|
||||||
<Typography level='body-sm'>
|
<Divider />
|
||||||
Create token to use with the API to update things that trigger task or
|
<Typography level='body-sm'>
|
||||||
chores
|
Create token to use with the API to update things that trigger task or
|
||||||
</Typography>
|
chores
|
||||||
{!isPlusAccount(userProfile) && (
|
</Typography>
|
||||||
<>
|
{!isPlusAccount(userProfile) && (
|
||||||
<Chip variant='soft' color='warning'>
|
<>
|
||||||
Plus Feature
|
<Chip variant='soft' color='warning'>
|
||||||
</Chip>
|
Plus Feature
|
||||||
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
|
</Chip>
|
||||||
API tokens are not available in the Basic plan. Upgrade to Plus to
|
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
|
||||||
generate API tokens for integrating with external systems and
|
API tokens are not available in the Basic plan. Upgrade to Plus to
|
||||||
automating your tasks.
|
generate API tokens for integrating with external systems and
|
||||||
</Typography>
|
automating your tasks.
|
||||||
</>
|
</Typography>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{tokens.map(token => (
|
{tokens.map(token => (
|
||||||
<Card key={token.token} className='p-4'>
|
<Card key={token.token} className='p-4'>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography level='body-md'>{token.name}</Typography>
|
<Typography level='body-md'>{token.name}</Typography>
|
||||||
<Typography level='body-xs'>
|
<Typography level='body-xs'>
|
||||||
{moment(token.createdAt).fromNow()}(
|
{moment(token.createdAt).fromNow()}(
|
||||||
{moment(token.createdAt).format('lll')})
|
{moment(token.createdAt).format('lll')})
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Button
|
<Button
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
color='primary'
|
color='primary'
|
||||||
sx={{ mr: 1 }}
|
sx={{ mr: 1 }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (showTokenId === token.id) {
|
if (showTokenId === token.id) {
|
||||||
setShowTokenId(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowTokenId(token.id)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showTokenId === token?.id ? 'Hide' : 'Show'} Token
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='outlined'
|
|
||||||
color='danger'
|
|
||||||
onClick={() => {
|
|
||||||
showConfirmation(
|
|
||||||
`Are you sure you want to remove ${token.name}?`,
|
|
||||||
'Remove Token',
|
|
||||||
() => {
|
|
||||||
DeleteLongLiveToken(token.id).then(resp => {
|
|
||||||
if (resp.ok) {
|
|
||||||
showNotification({
|
|
||||||
type: 'success',
|
|
||||||
title: 'Removed',
|
|
||||||
message: 'API token has been removed',
|
|
||||||
})
|
|
||||||
const newTokens = tokens.filter(
|
|
||||||
t => t.id !== token.id,
|
|
||||||
)
|
|
||||||
setTokens(newTokens)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
'Remove',
|
|
||||||
'Cancel',
|
|
||||||
'danger',
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{showTokenId === token?.id && (
|
|
||||||
<Box>
|
|
||||||
<Input
|
|
||||||
value={token.token}
|
|
||||||
sx={{ width: '100%', mt: 2 }}
|
|
||||||
readOnly
|
|
||||||
endDecorator={
|
|
||||||
<IconButton
|
|
||||||
variant='outlined'
|
|
||||||
color='primary'
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(token.token)
|
|
||||||
showNotification({
|
|
||||||
type: 'success',
|
|
||||||
message: 'Token copied to clipboard',
|
|
||||||
})
|
|
||||||
setShowTokenId(null)
|
setShowTokenId(null)
|
||||||
}}
|
return
|
||||||
>
|
}
|
||||||
<CopyAll />
|
|
||||||
</IconButton>
|
setShowTokenId(token.id)
|
||||||
}
|
}}
|
||||||
/>
|
>
|
||||||
|
{showTokenId === token?.id ? 'Hide' : 'Show'} Token
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
color='danger'
|
||||||
|
onClick={() => {
|
||||||
|
showConfirmation(
|
||||||
|
`Are you sure you want to remove ${token.name}?`,
|
||||||
|
'Remove Token',
|
||||||
|
() => {
|
||||||
|
DeleteLongLiveToken(token.id).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Removed',
|
||||||
|
message: 'API token has been removed',
|
||||||
|
})
|
||||||
|
const newTokens = tokens.filter(
|
||||||
|
t => t.id !== token.id,
|
||||||
|
)
|
||||||
|
setTokens(newTokens)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'Remove',
|
||||||
|
'Cancel',
|
||||||
|
'danger',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
{showTokenId === token?.id && (
|
||||||
</Card>
|
<Box>
|
||||||
))}
|
<Input
|
||||||
|
value={token.token}
|
||||||
|
sx={{ width: '100%', mt: 2 }}
|
||||||
|
readOnly
|
||||||
|
endDecorator={
|
||||||
|
<IconButton
|
||||||
|
variant='outlined'
|
||||||
|
color='primary'
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(token.token)
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Token copied to clipboard',
|
||||||
|
})
|
||||||
|
setShowTokenId(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyAll />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color='primary'
|
color='primary'
|
||||||
disabled={!isPlusAccount(userProfile)}
|
disabled={!isPlusAccount(userProfile)}
|
||||||
sx={{
|
sx={{
|
||||||
width: '210px',
|
width: '210px',
|
||||||
mb: 1,
|
mb: 1,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsGetTokenNameModalOpen(true)
|
setIsGetTokenNameModalOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Generate New Token
|
Generate New Token
|
||||||
</Button>
|
</Button>
|
||||||
<TextModal
|
<TextModal
|
||||||
isOpen={isGetTokenNameModalOpen}
|
isOpen={isGetTokenNameModalOpen}
|
||||||
title='Give a name for your new token, something to remember it by.'
|
title='Give a name for your new token, something to remember it by.'
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsGetTokenNameModalOpen(false)
|
setIsGetTokenNameModalOpen(false)
|
||||||
}}
|
}}
|
||||||
okText={'Generate Token'}
|
okText={'Generate Token'}
|
||||||
onSave={handleSaveToken}
|
onSave={handleSaveToken}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
{confirmModalConfig?.isOpen && (
|
{confirmModalConfig?.isOpen && (
|
||||||
<ConfirmationModal config={confirmModalConfig} />
|
<ConfirmationModal config={confirmModalConfig} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</SettingsLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
308
src/views/Settings/AccountSettings.jsx
Normal file
308
src/views/Settings/AccountSettings.jsx
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
import { Box, Button, Container, Divider, Typography } from '@mui/joy'
|
||||||
|
import { Purchases } from '@revenuecat/purchases-capacitor'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import moment from 'moment'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import SubscriptionModal from '../../components/SubscriptionModal'
|
||||||
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
|
import { CancelSubscription, UpdatePassword } from '../../utils/Fetcher'
|
||||||
|
import NativeCancelSubscriptionModal from '../Modals/Inputs/NativeCancelSubscriptionModal'
|
||||||
|
import PassowrdChangeModal from '../Modals/Inputs/PasswordChangeModal'
|
||||||
|
import UserDeletionModal from '../Modals/Inputs/UserDeletionModal'
|
||||||
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
|
||||||
|
const AccountSettings = () => {
|
||||||
|
const { data: userProfile } = useUserProfile()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { showNotification } = useNotification()
|
||||||
|
|
||||||
|
const [changePasswordModal, setChangePasswordModal] = useState(false)
|
||||||
|
const [subscriptionModal, setSubscriptionModal] = useState(false)
|
||||||
|
const [userDeletionModal, setUserDeletionModal] = useState(false)
|
||||||
|
const [nativeCancelModal, setNativeCancelModal] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function configurePurchases() {
|
||||||
|
if (Capacitor.isNativePlatform() && userProfile) {
|
||||||
|
await Purchases.configure({
|
||||||
|
apiKey: import.meta.env.VITE_REACT_APP_REVENUECAT_API_KEY,
|
||||||
|
appUserID: String(userProfile?.id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configurePurchases()
|
||||||
|
}, [userProfile])
|
||||||
|
|
||||||
|
const getSubscriptionDetails = () => {
|
||||||
|
if (userProfile?.subscription === 'active') {
|
||||||
|
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${moment(
|
||||||
|
userProfile?.expiration,
|
||||||
|
).format('MMM DD, YYYY')}.`
|
||||||
|
} else if (userProfile?.subscription === 'cancelled') {
|
||||||
|
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${moment(
|
||||||
|
userProfile?.expiration,
|
||||||
|
).format('MMM DD, YYYY')}.`
|
||||||
|
} else {
|
||||||
|
return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSubscriptionStatus = () => {
|
||||||
|
if (userProfile?.subscription === 'active') {
|
||||||
|
return `Plus`
|
||||||
|
} else if (userProfile?.subscription === 'cancelled') {
|
||||||
|
if (moment().isBefore(userProfile?.expiration)) {
|
||||||
|
return `Plus(until ${moment(userProfile?.expiration).format(
|
||||||
|
'MMM DD, YYYY',
|
||||||
|
)})`
|
||||||
|
}
|
||||||
|
return `Free`
|
||||||
|
} else {
|
||||||
|
return `Free`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userProfile) {
|
||||||
|
return (
|
||||||
|
<SettingsLayout title="Account Settings">
|
||||||
|
<div>Loading...</div>
|
||||||
|
</SettingsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsLayout title="Account Settings">
|
||||||
|
<div className='grid gap-4'>
|
||||||
|
<Typography level='body-md'>
|
||||||
|
Change your account settings, type or update your password
|
||||||
|
</Typography>
|
||||||
|
<Typography level='title-md' mb={-1}>
|
||||||
|
Account Type : {getSubscriptionStatus()}
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body-sm'>{getSubscriptionDetails()}</Typography>
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
width: '110px',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
userProfile?.subscription === 'active' ||
|
||||||
|
(moment(userProfile?.expiration).isAfter(moment()) &&
|
||||||
|
userProfile?.subscription !== 'cancelled')
|
||||||
|
}
|
||||||
|
onClick={async () => {
|
||||||
|
if (Capacitor.isNativePlatform()) {
|
||||||
|
try {
|
||||||
|
const { RevenueCatUI } = await import(
|
||||||
|
'@revenuecat/purchases-capacitor-ui'
|
||||||
|
)
|
||||||
|
|
||||||
|
const offering = await Purchases.getOfferings()
|
||||||
|
await RevenueCatUI.presentPaywall({
|
||||||
|
offering: offering.current,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { customerInfo } = await Purchases.getCustomerInfo()
|
||||||
|
if (customerInfo.entitlements.active['Donetick Plus']) {
|
||||||
|
queryClient.invalidateQueries(['userProfile'])
|
||||||
|
queryClient.refetchQueries(['userProfile'])
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message:
|
||||||
|
'Purchase successful! Please restart the app to access Plus features.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Purchase error:', error)
|
||||||
|
|
||||||
|
if (error.code === '1') {
|
||||||
|
return
|
||||||
|
} else if (error.code === '2') {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
'Store connection issue. Please check your network and try again.',
|
||||||
|
})
|
||||||
|
} else if (error.code === '3') {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
'Purchases are not allowed on this device. Please check your device restrictions.',
|
||||||
|
})
|
||||||
|
} else if (error.code === '4') {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
'This subscription is not available. Please try again later.',
|
||||||
|
})
|
||||||
|
} else if (error.code === '5') {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
'This purchase has already been processed. If you believe this is an error, please contact support.',
|
||||||
|
})
|
||||||
|
} else if (error.code === '6') {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
'Purchase receipt missing. Please try purchasing again.',
|
||||||
|
})
|
||||||
|
} else if (error.code === '7') {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
'Network error. Please check your connection and try again.',
|
||||||
|
})
|
||||||
|
} else if (error.code === '8') {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
'Invalid purchase receipt. Please contact support if this persists.',
|
||||||
|
})
|
||||||
|
} else if (error.code === '9') {
|
||||||
|
showNotification({
|
||||||
|
type: 'warning',
|
||||||
|
message:
|
||||||
|
'Payment is pending approval. You will receive access once approved.',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.error('Unexpected purchase error:', error)
|
||||||
|
console.error('Error occurred in purchase flow')
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: `Purchase failed: ${error.message || 'Unknown error'}. Please try again or contact support.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSubscriptionModal(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{userProfile?.subscription === 'active' && (
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
width: '110px',
|
||||||
|
mb: 1,
|
||||||
|
ml: 1,
|
||||||
|
}}
|
||||||
|
variant='outlined'
|
||||||
|
color='danger'
|
||||||
|
onClick={() => {
|
||||||
|
setNativeCancelModal(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{import.meta.env.VITE_IS_SELF_HOSTED === 'true' && (
|
||||||
|
<Box>
|
||||||
|
<Typography level='title-md' mb={1}>
|
||||||
|
Password :
|
||||||
|
</Typography>
|
||||||
|
<Typography mb={1} level='body-sm'></Typography>
|
||||||
|
<Button
|
||||||
|
variant='soft'
|
||||||
|
onClick={() => {
|
||||||
|
setChangePasswordModal(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Change Password
|
||||||
|
</Button>
|
||||||
|
{changePasswordModal ? (
|
||||||
|
<PassowrdChangeModal
|
||||||
|
isOpen={changePasswordModal}
|
||||||
|
onClose={password => {
|
||||||
|
if (password) {
|
||||||
|
UpdatePassword(password).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Password changed successfully',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Password change failed',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setChangePasswordModal(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography level='title-md' mb={1} color='danger'>
|
||||||
|
Danger Zone
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body-sm' mb={2} color='neutral'>
|
||||||
|
Once you delete your account, there is no going back. Please be
|
||||||
|
certain.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
color='danger'
|
||||||
|
onClick={() => setUserDeletionModal(true)}
|
||||||
|
>
|
||||||
|
Delete Account
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SubscriptionModal
|
||||||
|
open={subscriptionModal}
|
||||||
|
onClose={() => setSubscriptionModal(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UserDeletionModal
|
||||||
|
isOpen={userDeletionModal}
|
||||||
|
onClose={success => {
|
||||||
|
setUserDeletionModal(false)
|
||||||
|
if (success) {
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Account deleted successfully',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
userProfile={userProfile}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NativeCancelSubscriptionModal
|
||||||
|
isOpen={nativeCancelModal}
|
||||||
|
onClose={action => {
|
||||||
|
setNativeCancelModal(false)
|
||||||
|
if (action === 'desktop') {
|
||||||
|
CancelSubscription().then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Subscription cancelled',
|
||||||
|
})
|
||||||
|
window.location.reload()
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to cancel subscription',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccountSettings
|
||||||
163
src/views/Settings/AdvancedSettings.jsx
Normal file
163
src/views/Settings/AdvancedSettings.jsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
Chip,
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
Input,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import RealTimeSettings from '../../components/RealTimeSettings'
|
||||||
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
|
import { GetUserCircle, PutWebhookURL } from '../../utils/Fetcher'
|
||||||
|
import { isPlusAccount } from '../../utils/Helpers'
|
||||||
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
|
||||||
|
const AdvancedSettings = () => {
|
||||||
|
const { data: userProfile } = useUserProfile()
|
||||||
|
const { showNotification } = useNotification()
|
||||||
|
|
||||||
|
const [userCircles, setUserCircles] = useState([])
|
||||||
|
const [webhookURL, setWebhookURL] = useState(null)
|
||||||
|
const [webhookError, setWebhookError] = useState(null)
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
GetUserCircle().then(resp => {
|
||||||
|
resp.json().then(data => {
|
||||||
|
setUserCircles(data.res ? data.res : [])
|
||||||
|
setWebhookURL(data.res ? data.res[0].webhook_url : null)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Check if user is admin based on userRole from the circle data
|
||||||
|
useEffect(() => {
|
||||||
|
if (userCircles.length > 0) {
|
||||||
|
setIsAdmin(userCircles[0]?.userRole === 'admin')
|
||||||
|
}
|
||||||
|
}, [userCircles])
|
||||||
|
|
||||||
|
if (!userProfile) {
|
||||||
|
return (
|
||||||
|
<SettingsLayout title="Advanced Settings">
|
||||||
|
<div>Loading...</div>
|
||||||
|
</SettingsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsLayout title="Advanced Settings">
|
||||||
|
<div className='grid gap-4'>
|
||||||
|
<Typography level='body-md'>
|
||||||
|
Configure advanced features like webhooks and real-time updates for enhanced productivity.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Webhook Settings - Only show for admins */}
|
||||||
|
{isAdmin && (
|
||||||
|
<>
|
||||||
|
<Typography level='title-lg' mt={2}>
|
||||||
|
Webhook Integration
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body-md' mt={-1}>
|
||||||
|
Webhooks allow you to send real-time notifications to other
|
||||||
|
services when events happen in your Circle. Configure a webhook
|
||||||
|
URL to receive real-time updates.
|
||||||
|
</Typography>
|
||||||
|
{!isPlusAccount(userProfile) && (
|
||||||
|
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
|
||||||
|
Webhook notifications are not available in the Basic plan.
|
||||||
|
Upgrade to Plus to receive real-time updates via webhooks.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<FormControl sx={{ mt: 1 }}>
|
||||||
|
<Checkbox
|
||||||
|
checked={webhookURL !== null}
|
||||||
|
onClick={() => {
|
||||||
|
if (webhookURL === null) {
|
||||||
|
setWebhookURL('')
|
||||||
|
} else {
|
||||||
|
setWebhookURL(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant='soft'
|
||||||
|
label='Enable Webhook'
|
||||||
|
disabled={!isPlusAccount(userProfile)}
|
||||||
|
overlay
|
||||||
|
/>
|
||||||
|
<FormHelperText
|
||||||
|
sx={{
|
||||||
|
opacity: !isPlusAccount(userProfile) ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Enable webhook notifications for tasks and things updates.{' '}
|
||||||
|
{userProfile && !isPlusAccount(userProfile) && (
|
||||||
|
<Chip variant='soft' color='warning'>
|
||||||
|
Plus Feature
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{webhookURL !== null && (
|
||||||
|
<Box>
|
||||||
|
<Typography level='title-sm'>Webhook URL</Typography>
|
||||||
|
<Input
|
||||||
|
value={webhookURL ? webhookURL : ''}
|
||||||
|
onChange={e => setWebhookURL(e.target.value)}
|
||||||
|
size='lg'
|
||||||
|
sx={{
|
||||||
|
width: '220px',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{webhookError && (
|
||||||
|
<Typography level='body-sm' color='danger'>
|
||||||
|
{webhookError}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant='soft'
|
||||||
|
sx={{ width: '110px', mt: 1 }}
|
||||||
|
onClick={() => {
|
||||||
|
PutWebhookURL(webhookURL).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Webhook URL updated successfully',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to update webhook URL',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
disabled={!isPlusAccount(userProfile)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Real-time Settings */}
|
||||||
|
<Typography level='title-lg' mt={2}>
|
||||||
|
Real-time Updates
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body-md' mt={-1}>
|
||||||
|
Configure how you receive live updates when tasks and activities change in your circle.
|
||||||
|
</Typography>
|
||||||
|
<RealTimeSettings />
|
||||||
|
</div>
|
||||||
|
</SettingsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdvancedSettings
|
||||||
489
src/views/Settings/CircleSettings.jsx
Normal file
489
src/views/Settings/CircleSettings.jsx
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
import { Delete, Refresh } from '@mui/icons-material'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Chip,
|
||||||
|
CircularProgress,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import moment from 'moment'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
|
import {
|
||||||
|
AcceptCircleMemberRequest,
|
||||||
|
DeleteCircleMember,
|
||||||
|
GetAllCircleMembers,
|
||||||
|
GetCircleMemberRequests,
|
||||||
|
GetUserCircle,
|
||||||
|
JoinCircle,
|
||||||
|
LeaveCircle,
|
||||||
|
UpdateMemberRole,
|
||||||
|
} from '../../utils/Fetcher'
|
||||||
|
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||||
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
|
||||||
|
const CircleSettings = () => {
|
||||||
|
const { data: userProfile } = useUserProfile()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { showNotification } = useNotification()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const [userCircles, setUserCircles] = useState([])
|
||||||
|
const [circleMemberRequests, setCircleMemberRequests] = useState([])
|
||||||
|
const [circleInviteCode, setCircleInviteCode] = useState('')
|
||||||
|
const [circleMembers, setCircleMembers] = useState([])
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false)
|
||||||
|
const [lastRefresh, setLastRefresh] = useState(null)
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||||
|
const [confirmModalConfig, setConfirmModalConfig] = useState({})
|
||||||
|
|
||||||
|
const showConfirmation = (
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
onConfirm,
|
||||||
|
confirmText = 'Confirm',
|
||||||
|
cancelText = 'Cancel',
|
||||||
|
color = 'primary',
|
||||||
|
) => {
|
||||||
|
setConfirmModalConfig({
|
||||||
|
isOpen: true,
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
confirmText,
|
||||||
|
cancelText,
|
||||||
|
color,
|
||||||
|
onClose: isConfirmed => {
|
||||||
|
if (isConfirmed) {
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
setConfirmModalConfig({})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshMemberRequests = async () => {
|
||||||
|
setIsRefreshing(true)
|
||||||
|
try {
|
||||||
|
const resp = await GetCircleMemberRequests()
|
||||||
|
const data = await resp.json()
|
||||||
|
setCircleMemberRequests(data.res ? data.res : [])
|
||||||
|
setLastRefresh(new Date())
|
||||||
|
} catch (error) {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to refresh member requests',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
GetUserCircle().then(resp => {
|
||||||
|
resp.json().then(data => {
|
||||||
|
setUserCircles(data.res ? data.res : [])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
GetCircleMemberRequests().then(resp => {
|
||||||
|
resp.json().then(data => {
|
||||||
|
setCircleMemberRequests(data.res ? data.res : [])
|
||||||
|
setLastRefresh(new Date())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
GetAllCircleMembers().then(data => {
|
||||||
|
setCircleMembers(data.res ? data.res : [])
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userProfile && userProfile.id) {
|
||||||
|
const isUserAdmin = circleMembers.some(
|
||||||
|
member => member.userId === userProfile.id && member.role === 'admin',
|
||||||
|
)
|
||||||
|
setIsAdmin(isUserAdmin)
|
||||||
|
}
|
||||||
|
}, [circleMembers, userProfile])
|
||||||
|
|
||||||
|
if (!userProfile) {
|
||||||
|
return (
|
||||||
|
<SettingsLayout title="Circle Settings">
|
||||||
|
<CircularProgress />
|
||||||
|
</SettingsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsLayout title="Circle Settings">
|
||||||
|
<div className='grid gap-4'>
|
||||||
|
<Typography level='body-md'>
|
||||||
|
Your account is automatically connected to a Circle when you create or
|
||||||
|
join one. Easily invite friends by sharing the unique Circle code or
|
||||||
|
link below. You'll receive a notification below when someone requests
|
||||||
|
to join your Circle.
|
||||||
|
</Typography>
|
||||||
|
<Typography level='title-sm' mb={-1}>
|
||||||
|
{userCircles[0]?.userRole === 'member'
|
||||||
|
? `You part of ${userCircles[0]?.name} `
|
||||||
|
: `You circle code is:`}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
value={userCircles[0]?.invite_code}
|
||||||
|
disabled
|
||||||
|
size='lg'
|
||||||
|
sx={{
|
||||||
|
width: '220px',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant='soft'
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(userCircles[0]?.invite_code)
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Code copied to clipboard',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Code
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='soft'
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
window.location.protocol +
|
||||||
|
'//' +
|
||||||
|
window.location.host +
|
||||||
|
`/circle/join?code=${userCircles[0]?.invite_code}`,
|
||||||
|
)
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Link copied to clipboard',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Link
|
||||||
|
</Button>
|
||||||
|
{userCircles.length > 0 && userCircles[0]?.userRole === 'member' && (
|
||||||
|
<Button
|
||||||
|
color='danger'
|
||||||
|
variant='outlined'
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
onClick={() => {
|
||||||
|
showConfirmation(
|
||||||
|
'Are you sure you want to leave your circle?',
|
||||||
|
'Leave Circle',
|
||||||
|
() => {
|
||||||
|
LeaveCircle(userCircles[0]?.id).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Left circle successfully',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to leave circle',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'Leave',
|
||||||
|
'Cancel',
|
||||||
|
'danger',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Leave Circle
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography level='title-md'>Circle Members</Typography>
|
||||||
|
{circleMembers.map(member => (
|
||||||
|
<Card key={member.id} className='p-4'>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Box>
|
||||||
|
<Typography level='body-md'>
|
||||||
|
{member.displayName.charAt(0).toUpperCase() +
|
||||||
|
member.displayName.slice(1)}
|
||||||
|
{member.userId === userProfile.id ? '(You)' : ''}{' '}
|
||||||
|
<Chip>
|
||||||
|
{' '}
|
||||||
|
{member.isActive ? member.role : 'Pending Approval'}
|
||||||
|
</Chip>
|
||||||
|
</Typography>
|
||||||
|
{member.isActive ? (
|
||||||
|
<Typography level='body-sm'>
|
||||||
|
Joined on {moment(member.createdAt).format('MMM DD, YYYY')}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography level='body-sm' color='danger'>
|
||||||
|
Request to join{' '}
|
||||||
|
{moment(member.updatedAt).format('MMM DD, YYYY')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
{member.userId !== userProfile.id && isAdmin && (
|
||||||
|
<Select
|
||||||
|
size='sm'
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
value={member.role}
|
||||||
|
renderValue={() => (
|
||||||
|
<Typography>
|
||||||
|
{member.role.charAt(0).toUpperCase() +
|
||||||
|
member.role.slice(1)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
onChange={(e, value) => {
|
||||||
|
UpdateMemberRole(member.userId, value).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
const newCircleMembers = circleMembers.map(m => {
|
||||||
|
if (m.userId === member.userId) {
|
||||||
|
m.role = value
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
})
|
||||||
|
setCircleMembers(newCircleMembers)
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to update role',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
value: 'member',
|
||||||
|
description: 'Just a regular member of the circle',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'manager',
|
||||||
|
description:
|
||||||
|
'Can impersonate users and perform actions on their behalf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'admin',
|
||||||
|
description: 'Full access to the circle',
|
||||||
|
},
|
||||||
|
].map((option, index) => (
|
||||||
|
<Option value={option.value} key={index}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'start',
|
||||||
|
alignItems: 'start',
|
||||||
|
width: '100%',
|
||||||
|
gap: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
level='title-sm'
|
||||||
|
sx={{ mb: 0, mt: 0, lineHeight: 1.1 }}
|
||||||
|
>
|
||||||
|
{option.value.charAt(0).toUpperCase() +
|
||||||
|
option.value.slice(1)}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
level='body-sm'
|
||||||
|
sx={{ mt: 0, mb: 0, lineHeight: 1.1 }}
|
||||||
|
>
|
||||||
|
{option.description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
{isAdmin &&
|
||||||
|
member.userId !== userProfile.id &&
|
||||||
|
member.isActive && (
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
color='danger'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => {
|
||||||
|
showConfirmation(
|
||||||
|
`Are you sure you want to remove ${member.displayName} from your circle?`,
|
||||||
|
'Remove Member',
|
||||||
|
() => {
|
||||||
|
DeleteCircleMember(
|
||||||
|
member.circleId,
|
||||||
|
member.userId,
|
||||||
|
).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Removed member successfully',
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries(['circleMembers'])
|
||||||
|
queryClient.invalidateQueries(['userCircle'])
|
||||||
|
queryClient.refetchQueries(['circleMembers'])
|
||||||
|
queryClient.refetchQueries(['userCircle'])
|
||||||
|
setCircleMembers(prevMembers =>
|
||||||
|
prevMembers.filter(
|
||||||
|
m => m.userId !== member.userId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'Remove',
|
||||||
|
'Cancel',
|
||||||
|
'danger',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography level='title-md'>Circle Member Requests</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
{lastRefresh && (
|
||||||
|
<Typography level='body-sm' color='neutral'>
|
||||||
|
Last updated: {moment(lastRefresh).format('MMM DD, HH:mm')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='soft'
|
||||||
|
onClick={refreshMemberRequests}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
startDecorator={
|
||||||
|
isRefreshing ? <CircularProgress size='sm' /> : <Refresh />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isRefreshing ? 'Refreshing...' : 'Refresh'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{circleMemberRequests.map(request => (
|
||||||
|
<Card key={request.id} className='p-4'>
|
||||||
|
<Typography level='body-md'>
|
||||||
|
{request.displayName} wants to join your circle.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant='soft'
|
||||||
|
color='success'
|
||||||
|
onClick={() => {
|
||||||
|
showConfirmation(
|
||||||
|
`Are you sure you want to accept ${request.displayName} (username: ${request.username}) to join your circle?`,
|
||||||
|
'Accept Member Request',
|
||||||
|
() => {
|
||||||
|
AcceptCircleMemberRequest(request.id).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Accepted request successfully',
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries(['circleMembers'])
|
||||||
|
queryClient.invalidateQueries(['circleMemberRequests'])
|
||||||
|
queryClient.invalidateQueries(['userCircle'])
|
||||||
|
queryClient.refetchQueries(['circleMembers'])
|
||||||
|
queryClient.refetchQueries(['circleMemberRequests'])
|
||||||
|
queryClient.refetchQueries(['userCircle'])
|
||||||
|
refreshMemberRequests()
|
||||||
|
GetAllCircleMembers().then(data => {
|
||||||
|
setCircleMembers(data.res ? data.res : [])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'Accept',
|
||||||
|
'Cancel',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<Divider> or </Divider>
|
||||||
|
|
||||||
|
<Typography level='body-md'>
|
||||||
|
if want to join someone else's Circle? Ask them for their unique
|
||||||
|
Circle code or join link. Enter the code below to join their Circle.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography level='title-sm' mb={-1}>
|
||||||
|
Enter Circle code:
|
||||||
|
<Input
|
||||||
|
placeholder='Enter code'
|
||||||
|
value={circleInviteCode}
|
||||||
|
onChange={e => setCircleInviteCode(e.target.value)}
|
||||||
|
size='lg'
|
||||||
|
sx={{
|
||||||
|
width: '220px',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant='soft'
|
||||||
|
onClick={() => {
|
||||||
|
JoinCircle(circleInviteCode).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message:
|
||||||
|
'Joined circle successfully, wait for the circle owner to accept your request.',
|
||||||
|
})
|
||||||
|
setTimeout(() => navigate('/'), 3000)
|
||||||
|
} else {
|
||||||
|
if (resp.status === 409) {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: 'You are already a member of this circle',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to join circle',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setTimeout(() => navigate('/'), 3000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Join Circle
|
||||||
|
</Button>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{confirmModalConfig?.isOpen && (
|
||||||
|
<ConfirmationModal config={confirmModalConfig} />
|
||||||
|
)}
|
||||||
|
</SettingsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CircleSettings
|
||||||
@@ -4,8 +4,6 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CircularProgress,
|
|
||||||
Divider,
|
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalClose,
|
ModalClose,
|
||||||
@@ -22,6 +20,8 @@ import {
|
|||||||
RegenerateBackupCodes,
|
RegenerateBackupCodes,
|
||||||
SetupMFA,
|
SetupMFA,
|
||||||
} from '../../utils/Fetcher'
|
} from '../../utils/Fetcher'
|
||||||
|
import LoadingComponent from '../components/Loading'
|
||||||
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
|
||||||
const MFASettings = () => {
|
const MFASettings = () => {
|
||||||
const [mfaEnabled, setMfaEnabled] = useState(false)
|
const [mfaEnabled, setMfaEnabled] = useState(false)
|
||||||
@@ -202,69 +202,68 @@ const MFASettings = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <LoadingComponent />
|
||||||
<Box className='flex justify-center py-4'>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='grid gap-4 py-4' id='mfa'>
|
<SettingsLayout title='Multi-Factor Authentication'>
|
||||||
<Typography level='h3'>Multi-Factor Authentication</Typography>
|
<div className='grid gap-4 py-4' id='mfa'>
|
||||||
<Divider />
|
<Typography level='body-md'>
|
||||||
<Typography level='body-md'>
|
Add an extra layer of security to your account with multi-factor
|
||||||
Add an extra layer of security to your account with multi-factor
|
authentication (MFA). When enabled, you'll need to provide a
|
||||||
authentication (MFA). When enabled, you'll need to provide a
|
verification code from your authenticator app in addition to your
|
||||||
verification code from your authenticator app in addition to your
|
password when signing in.
|
||||||
password when signing in.
|
</Typography>
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{success && (
|
{success && (
|
||||||
<Alert color='success' onClose={() => setSuccess('')}>
|
<Alert color='success' onClose={() => setSuccess('')}>
|
||||||
{success}
|
{success}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Alert color='danger' onClose={() => setError('')}>
|
<Alert color='danger' onClose={() => setError('')}>
|
||||||
{error}
|
{error}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card variant='outlined'>
|
<Card variant='outlined'>
|
||||||
<Box className='flex items-center justify-between'>
|
<Box className='flex items-center justify-between'>
|
||||||
<Box className='flex items-center gap-3'>
|
<Box className='flex items-center gap-3'>
|
||||||
<Security color='primary' />
|
<Security color='primary' />
|
||||||
<Box>
|
<Box>
|
||||||
<Typography level='title-md'>
|
<Typography level='title-md'>
|
||||||
Two-Factor Authentication
|
Two-Factor Authentication
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||||
{mfaEnabled
|
{mfaEnabled
|
||||||
? 'Your account is protected with 2FA'
|
? 'Your account is protected with 2FA'
|
||||||
: 'Secure your account with an authenticator app'}
|
: 'Secure your account with an authenticator app'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box className='flex items-center gap-2'>
|
||||||
|
{mfaEnabled ? (
|
||||||
|
<Button
|
||||||
|
color='danger'
|
||||||
|
variant='outlined'
|
||||||
|
onClick={() => setDisableModalOpen(true)}
|
||||||
|
>
|
||||||
|
Disable
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
variant='solid'
|
||||||
|
onClick={handleSetupMFA}
|
||||||
|
>
|
||||||
|
Enable
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box className='flex items-center gap-2'>
|
</Card>
|
||||||
{mfaEnabled ? (
|
{/*
|
||||||
<Button
|
|
||||||
color='danger'
|
|
||||||
variant='outlined'
|
|
||||||
onClick={() => setDisableModalOpen(true)}
|
|
||||||
>
|
|
||||||
Disable
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button color='primary' variant='solid' onClick={handleSetupMFA}>
|
|
||||||
Enable
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
{/*
|
|
||||||
{mfaEnabled && (
|
{mfaEnabled && (
|
||||||
<Card variant='outlined'>
|
<Card variant='outlined'>
|
||||||
<Box className='flex items-center justify-between'>
|
<Box className='flex items-center justify-between'>
|
||||||
@@ -290,87 +289,194 @@ const MFASettings = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
{/* Setup MFA Modal */}
|
{/* Setup MFA Modal */}
|
||||||
<Modal open={setupModalOpen} onClose={closeSetupModal}>
|
<Modal open={setupModalOpen} onClose={closeSetupModal}>
|
||||||
<ModalDialog size='md' sx={{ maxWidth: 500 }}>
|
<ModalDialog size='md' sx={{ maxWidth: 500 }}>
|
||||||
<ModalClose />
|
<ModalClose />
|
||||||
<Typography level='h4' sx={{ mb: 2 }}>
|
<Typography level='h4' sx={{ mb: 2 }}>
|
||||||
Set up Multi-Factor Authentication
|
Set up Multi-Factor Authentication
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{setupStep === 1 && setupData && (
|
{setupStep === 1 && setupData && (
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Typography level='body-md'>
|
<Typography level='body-md'>
|
||||||
<strong>Step 1:</strong> Scan the QR code below with your
|
<strong>Step 1:</strong> Scan the QR code below with your
|
||||||
authenticator app (Google Authenticator, Authy, etc.)
|
authenticator app (Google Authenticator, Authy, etc.)
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box className='flex justify-center rounded bg-white p-4'>
|
|
||||||
{qrCodeDataUrl || setupData.qrCode ? (
|
|
||||||
<img
|
|
||||||
src={
|
|
||||||
qrCodeDataUrl ||
|
|
||||||
`data:image/png;base64,${setupData.qrCode}`
|
|
||||||
}
|
|
||||||
alt='MFA QR Code'
|
|
||||||
style={{ maxWidth: '200px', maxHeight: '200px' }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Alert color='danger'>
|
|
||||||
QR code could not be generated. Please try again or use the
|
|
||||||
manual entry key below.
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Alert
|
|
||||||
color='neutral'
|
|
||||||
variant='soft'
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography level='title-sm'>
|
|
||||||
<strong>Manual entry key:</strong>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
|
||||||
level='body-sm'
|
<Box className='flex justify-center rounded bg-white p-4'>
|
||||||
sx={{ wordBreak: 'break-all', whiteSpace: 'pre-wrap' }}
|
{qrCodeDataUrl || setupData.qrCode ? (
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
qrCodeDataUrl ||
|
||||||
|
`data:image/png;base64,${setupData.qrCode}`
|
||||||
|
}
|
||||||
|
alt='MFA QR Code'
|
||||||
|
style={{ maxWidth: '200px', maxHeight: '200px' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Alert color='danger'>
|
||||||
|
QR code could not be generated. Please try again or use
|
||||||
|
the manual entry key below.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
color='neutral'
|
||||||
|
variant='soft'
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{setupData.secret}
|
<Typography level='title-sm'>
|
||||||
|
<strong>Manual entry key:</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
level='body-sm'
|
||||||
|
sx={{ wordBreak: 'break-all', whiteSpace: 'pre-wrap' }}
|
||||||
|
>
|
||||||
|
{setupData.secret}
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
onClick={() => setSetupStep(2)}
|
||||||
|
startDecorator={<Smartphone />}
|
||||||
|
>
|
||||||
|
I've added the account to my app
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{setupStep === 2 && (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Typography level='body-md'>
|
||||||
|
<strong>Step 2:</strong> Enter the 6-digit verification code
|
||||||
|
from your authenticator app
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder='Enter 6-digit code'
|
||||||
|
value={verificationCode}
|
||||||
|
size='lg'
|
||||||
|
// send on enter:
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter' && verificationCode.length === 6) {
|
||||||
|
handleConfirmMFA()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={e => setVerificationCode(e.target.value)}
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '1.2em',
|
||||||
|
letterSpacing: verificationCode.length === 0 ? '' : '0.4em',
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
maxLength: 6,
|
||||||
|
pattern: '[0-9]*',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <Alert color='danger'>{error}</Alert>}
|
||||||
|
|
||||||
|
<Box className='flex gap-2'>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
onClick={() => setSetupStep(1)}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
onClick={handleConfirmMFA}
|
||||||
|
disabled={verificationCode.length !== 6}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
Verify & Enable
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{setupStep === 3 && (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Box className='text-center'>
|
||||||
|
<CheckCircle color='success' sx={{ fontSize: 48, mb: 2 }} />
|
||||||
|
<Typography level='h4' color='success'>
|
||||||
|
MFA Successfully Enabled!
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Alert color='warning'>
|
||||||
|
<Typography level='title-sm' sx={{ mb: 1 }}>
|
||||||
|
Save these backup codes in a safe place
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body-sm'>
|
||||||
|
You can use these codes to access your account if you lose
|
||||||
|
your authenticator device. Each code can only be used once.
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Card variant='outlined' sx={{ p: 2 }}>
|
||||||
|
<Box className='grid grid-cols-2 gap-2 font-mono text-sm'>
|
||||||
|
{backupCodes?.map((code, index) => (
|
||||||
|
<Typography
|
||||||
|
key={index}
|
||||||
|
level='body-sm'
|
||||||
|
sx={{ fontFamily: 'monospace' }}
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Button color='primary' onClick={closeSetupModal}>
|
||||||
|
I've saved my backup codes
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Disable MFA Modal */}
|
||||||
|
<Modal open={disableModalOpen} onClose={closeDisableModal}>
|
||||||
|
<ModalDialog size='sm'>
|
||||||
|
<ModalClose />
|
||||||
|
<Typography level='h4' sx={{ mb: 2 }}>
|
||||||
|
Disable Multi-Factor Authentication
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Alert color='warning'>
|
||||||
|
<Typography level='body-sm'>
|
||||||
|
Disabling MFA will make your account less secure. Are you sure
|
||||||
|
you want to continue?
|
||||||
</Typography>
|
</Typography>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Button
|
|
||||||
color='primary'
|
|
||||||
onClick={() => setSetupStep(2)}
|
|
||||||
startDecorator={<Smartphone />}
|
|
||||||
>
|
|
||||||
I've added the account to my app
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{setupStep === 2 && (
|
|
||||||
<Stack spacing={3}>
|
|
||||||
<Typography level='body-md'>
|
<Typography level='body-md'>
|
||||||
<strong>Step 2:</strong> Enter the 6-digit verification code
|
Enter a verification code from your authenticator app to
|
||||||
from your authenticator app
|
confirm:
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
placeholder='Enter 6-digit code'
|
placeholder='Enter 6-digit code'
|
||||||
value={verificationCode}
|
value={disableCode}
|
||||||
size='lg'
|
size='lg'
|
||||||
// send on enter:
|
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'Enter' && verificationCode.length === 6) {
|
if (e.key === 'Enter' && disableCode.length === 6) {
|
||||||
handleConfirmMFA()
|
handleDisableMFA()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={e => setVerificationCode(e.target.value)}
|
onChange={e => setDisableCode(e.target.value)}
|
||||||
sx={{
|
sx={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: '1.2em',
|
fontSize: '1.2em',
|
||||||
@@ -389,39 +495,40 @@ const MFASettings = () => {
|
|||||||
<Box className='flex gap-2'>
|
<Box className='flex gap-2'>
|
||||||
<Button
|
<Button
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
onClick={() => setSetupStep(1)}
|
onClick={closeDisableModal}
|
||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1 }}
|
||||||
>
|
>
|
||||||
Back
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color='primary'
|
color='danger'
|
||||||
onClick={handleConfirmMFA}
|
onClick={handleDisableMFA}
|
||||||
disabled={verificationCode.length !== 6}
|
disabled={disableCode.length !== 6}
|
||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1 }}
|
||||||
>
|
>
|
||||||
Verify & Enable
|
Disable MFA
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Backup Codes Modal */}
|
||||||
|
<Modal
|
||||||
|
open={backupCodesModalOpen}
|
||||||
|
onClose={() => setBackupCodesModalOpen(false)}
|
||||||
|
>
|
||||||
|
<ModalDialog size='sm'>
|
||||||
|
<ModalClose />
|
||||||
|
<Typography level='h4' sx={{ mb: 2 }}>
|
||||||
|
New Backup Codes
|
||||||
|
</Typography>
|
||||||
|
|
||||||
{setupStep === 3 && (
|
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Box className='text-center'>
|
|
||||||
<CheckCircle color='success' sx={{ fontSize: 48, mb: 2 }} />
|
|
||||||
<Typography level='h4' color='success'>
|
|
||||||
MFA Successfully Enabled!
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Alert color='warning'>
|
<Alert color='warning'>
|
||||||
<Typography level='title-sm' sx={{ mb: 1 }}>
|
|
||||||
Save these backup codes in a safe place
|
|
||||||
</Typography>
|
|
||||||
<Typography level='body-sm'>
|
<Typography level='body-sm'>
|
||||||
You can use these codes to access your account if you lose
|
Your previous backup codes are now invalid. Save these new
|
||||||
your authenticator device. Each code can only be used once.
|
codes in a safe place. Each code can only be used once.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
@@ -439,123 +546,17 @@ const MFASettings = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Button color='primary' onClick={closeSetupModal}>
|
<Button
|
||||||
|
color='primary'
|
||||||
|
onClick={() => setBackupCodesModalOpen(false)}
|
||||||
|
>
|
||||||
I've saved my backup codes
|
I've saved my backup codes
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
</ModalDialog>
|
||||||
</ModalDialog>
|
</Modal>
|
||||||
</Modal>
|
</div>
|
||||||
|
</SettingsLayout>
|
||||||
{/* Disable MFA Modal */}
|
|
||||||
<Modal open={disableModalOpen} onClose={closeDisableModal}>
|
|
||||||
<ModalDialog size='sm'>
|
|
||||||
<ModalClose />
|
|
||||||
<Typography level='h4' sx={{ mb: 2 }}>
|
|
||||||
Disable Multi-Factor Authentication
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Stack spacing={3}>
|
|
||||||
<Alert color='warning'>
|
|
||||||
<Typography level='body-sm'>
|
|
||||||
Disabling MFA will make your account less secure. Are you sure
|
|
||||||
you want to continue?
|
|
||||||
</Typography>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Typography level='body-md'>
|
|
||||||
Enter a verification code from your authenticator app to confirm:
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
placeholder='Enter 6-digit code'
|
|
||||||
value={disableCode}
|
|
||||||
size='lg'
|
|
||||||
onKeyDown={e => {
|
|
||||||
if (e.key === 'Enter' && disableCode.length === 6) {
|
|
||||||
handleDisableMFA()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={e => setDisableCode(e.target.value)}
|
|
||||||
sx={{
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: '1.2em',
|
|
||||||
letterSpacing: verificationCode.length === 0 ? '' : '0.4em',
|
|
||||||
}}
|
|
||||||
slotProps={{
|
|
||||||
input: {
|
|
||||||
maxLength: 6,
|
|
||||||
pattern: '[0-9]*',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{error && <Alert color='danger'>{error}</Alert>}
|
|
||||||
|
|
||||||
<Box className='flex gap-2'>
|
|
||||||
<Button
|
|
||||||
variant='outlined'
|
|
||||||
onClick={closeDisableModal}
|
|
||||||
sx={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color='danger'
|
|
||||||
onClick={handleDisableMFA}
|
|
||||||
disabled={disableCode.length !== 6}
|
|
||||||
sx={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
Disable MFA
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</ModalDialog>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Backup Codes Modal */}
|
|
||||||
<Modal
|
|
||||||
open={backupCodesModalOpen}
|
|
||||||
onClose={() => setBackupCodesModalOpen(false)}
|
|
||||||
>
|
|
||||||
<ModalDialog size='sm'>
|
|
||||||
<ModalClose />
|
|
||||||
<Typography level='h4' sx={{ mb: 2 }}>
|
|
||||||
New Backup Codes
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Stack spacing={3}>
|
|
||||||
<Alert color='warning'>
|
|
||||||
<Typography level='body-sm'>
|
|
||||||
Your previous backup codes are now invalid. Save these new codes
|
|
||||||
in a safe place. Each code can only be used once.
|
|
||||||
</Typography>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Card variant='outlined' sx={{ p: 2 }}>
|
|
||||||
<Box className='grid grid-cols-2 gap-2 font-mono text-sm'>
|
|
||||||
{backupCodes?.map((code, index) => (
|
|
||||||
<Typography
|
|
||||||
key={index}
|
|
||||||
level='body-sm'
|
|
||||||
sx={{ fontFamily: 'monospace' }}
|
|
||||||
>
|
|
||||||
{code}
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
color='primary'
|
|
||||||
onClick={() => setBackupCodesModalOpen(false)}
|
|
||||||
>
|
|
||||||
I've saved my backup codes
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</ModalDialog>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/views/Settings/NotificationSettings.jsx
Normal file
12
src/views/Settings/NotificationSettings.jsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import NotificationSetting from './NotificationSetting'
|
||||||
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
|
||||||
|
const NotificationSettings = () => {
|
||||||
|
return (
|
||||||
|
<SettingsLayout title="Notification Settings">
|
||||||
|
<NotificationSetting />
|
||||||
|
</SettingsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotificationSettings
|
||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Divider,
|
|
||||||
Input,
|
Input,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
@@ -19,6 +18,7 @@ import { UpdateUserDetails } from '../../utils/Fetcher'
|
|||||||
import { resolvePhotoURL } from '../../utils/Helpers'
|
import { resolvePhotoURL } from '../../utils/Helpers'
|
||||||
import { getCroppedImg } from '../../utils/imageCropUtils'
|
import { getCroppedImg } from '../../utils/imageCropUtils'
|
||||||
import { UploadFile } from '../../utils/TokenManager'
|
import { UploadFile } from '../../utils/TokenManager'
|
||||||
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
|
||||||
const ProfileSettings = () => {
|
const ProfileSettings = () => {
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
@@ -136,165 +136,165 @@ const ProfileSettings = () => {
|
|||||||
// Helper to resolve photoURL with baseURL if needed
|
// Helper to resolve photoURL with baseURL if needed
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='grid gap-4 py-4' id='profile'>
|
<SettingsLayout title='Profile Settings'>
|
||||||
<Typography level='h3'>Profile Settings</Typography>
|
<div className='grid gap-4 py-4' id='profile'>
|
||||||
<Divider />
|
<Typography level='body-md'>
|
||||||
<Typography level='body-md'>
|
Update your display name and profile photo.
|
||||||
Update your display name and profile photo.
|
</Typography>
|
||||||
</Typography>
|
<Card
|
||||||
<Card
|
sx={{
|
||||||
sx={{
|
display: 'flex',
|
||||||
display: 'flex',
|
alignItems: 'center',
|
||||||
alignItems: 'center',
|
gap: 2,
|
||||||
gap: 2,
|
p: 2,
|
||||||
p: 2,
|
maxWidth: 400,
|
||||||
maxWidth: 400,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Avatar src={photoURL} sx={{ width: 64, height: 64 }} />
|
||||||
<Avatar src={photoURL} sx={{ width: 64, height: 64 }} />
|
<Box sx={{ flex: 1 }}>
|
||||||
<Box sx={{ flex: 1 }}>
|
<Button
|
||||||
|
variant='soft'
|
||||||
|
color='primary'
|
||||||
|
onClick={() => fileInputRef.current.click()}
|
||||||
|
loading={isUploading}
|
||||||
|
sx={{ mb: 1 }}
|
||||||
|
>
|
||||||
|
Change Photo
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type='file'
|
||||||
|
accept='image/*'
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handlePhotoChange}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
<Modal
|
||||||
|
open={showCropper}
|
||||||
|
onClose={() => {
|
||||||
|
setShowCropper(false)
|
||||||
|
setSelectedFile(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalDialog
|
||||||
|
layout='center'
|
||||||
|
sx={{
|
||||||
|
width: 360,
|
||||||
|
maxWidth: '90vw',
|
||||||
|
bgcolor: '#fff',
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 0,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minHeight: 420,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ width: 320, height: 320, position: 'relative', mt: 2 }}>
|
||||||
|
<Cropper
|
||||||
|
image={selectedFile}
|
||||||
|
crop={crop}
|
||||||
|
zoom={zoom}
|
||||||
|
aspect={1}
|
||||||
|
cropShape='round'
|
||||||
|
showGrid={false}
|
||||||
|
onCropChange={setCrop}
|
||||||
|
onZoomChange={setZoom}
|
||||||
|
onCropComplete={onCropComplete}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
width: '100%',
|
||||||
|
p: 2,
|
||||||
|
mt: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={handleCropSave}
|
||||||
|
loading={isUploading}
|
||||||
|
variant='solid'
|
||||||
|
color='primary'
|
||||||
|
size='md'
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowCropper(false)
|
||||||
|
setSelectedFile(null)
|
||||||
|
}}
|
||||||
|
variant='soft'
|
||||||
|
color='neutral'
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
<Box sx={{ maxWidth: 400, mt: 3 }}>
|
||||||
|
<Typography level='body-sm' sx={{ mb: 0.5 }}>
|
||||||
|
Display Name
|
||||||
|
</Typography>
|
||||||
|
<Input
|
||||||
|
value={displayName}
|
||||||
|
onChange={e => setDisplayName(e.target.value)}
|
||||||
|
placeholder='Enter your display name'
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography level='body-sm' sx={{ mb: 0.5 }}>
|
||||||
|
Timezone
|
||||||
|
</Typography>
|
||||||
|
<Autocomplete
|
||||||
|
value={timezone}
|
||||||
|
onChange={(e, newValue) => setTimezone(newValue)}
|
||||||
|
options={timezones}
|
||||||
|
getOptionLabel={tz => {
|
||||||
|
const formattedTimezone = tz.replace(/_/g, ' ')
|
||||||
|
const currentTime = new Date().toLocaleString('en-US', {
|
||||||
|
timeZone: tz,
|
||||||
|
timeStyle: 'short',
|
||||||
|
})
|
||||||
|
return `${formattedTimezone} (${currentTime})`
|
||||||
|
}}
|
||||||
|
filterOptions={(options, { inputValue }) => {
|
||||||
|
if (!inputValue) return options
|
||||||
|
|
||||||
|
const searchTerms = inputValue.toLowerCase().split(/\s+/)
|
||||||
|
return options.filter(tz => {
|
||||||
|
const timezoneLower = tz.toLowerCase()
|
||||||
|
const timezoneParts = tz.toLowerCase().split(/[/_]/)
|
||||||
|
|
||||||
|
return searchTerms.every(
|
||||||
|
term =>
|
||||||
|
timezoneLower.includes(term) ||
|
||||||
|
timezoneParts.some(part => part.includes(term)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
placeholder='Select your timezone'
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color='primary'
|
color='primary'
|
||||||
onClick={() => fileInputRef.current.click()}
|
onClick={handleSave}
|
||||||
loading={isUploading}
|
loading={isSaving}
|
||||||
sx={{ mb: 1 }}
|
sx={{ width: 120 }}
|
||||||
>
|
>
|
||||||
Change Photo
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type='file'
|
|
||||||
accept='image/*'
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
onChange={handlePhotoChange}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</div>
|
||||||
<Modal
|
</SettingsLayout>
|
||||||
open={showCropper}
|
|
||||||
onClose={() => {
|
|
||||||
setShowCropper(false)
|
|
||||||
setSelectedFile(null)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalDialog
|
|
||||||
layout='center'
|
|
||||||
sx={{
|
|
||||||
width: 360,
|
|
||||||
maxWidth: '90vw',
|
|
||||||
bgcolor: '#fff',
|
|
||||||
borderRadius: 2,
|
|
||||||
boxShadow: 24,
|
|
||||||
p: 0,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
minHeight: 420,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ width: 320, height: 320, position: 'relative', mt: 2 }}>
|
|
||||||
<Cropper
|
|
||||||
image={selectedFile}
|
|
||||||
crop={crop}
|
|
||||||
zoom={zoom}
|
|
||||||
aspect={1}
|
|
||||||
cropShape='round'
|
|
||||||
showGrid={false}
|
|
||||||
onCropChange={setCrop}
|
|
||||||
onZoomChange={setZoom}
|
|
||||||
onCropComplete={onCropComplete}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
width: '100%',
|
|
||||||
p: 2,
|
|
||||||
mt: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={handleCropSave}
|
|
||||||
loading={isUploading}
|
|
||||||
variant='solid'
|
|
||||||
color='primary'
|
|
||||||
size='md'
|
|
||||||
sx={{ mr: 1 }}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setShowCropper(false)
|
|
||||||
setSelectedFile(null)
|
|
||||||
}}
|
|
||||||
variant='soft'
|
|
||||||
color='neutral'
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</ModalDialog>
|
|
||||||
</Modal>
|
|
||||||
<Box sx={{ maxWidth: 400, mt: 3 }}>
|
|
||||||
<Typography level='body-sm' sx={{ mb: 0.5 }}>
|
|
||||||
Display Name
|
|
||||||
</Typography>
|
|
||||||
<Input
|
|
||||||
value={displayName}
|
|
||||||
onChange={e => setDisplayName(e.target.value)}
|
|
||||||
placeholder='Enter your display name'
|
|
||||||
sx={{ mb: 2 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Typography level='body-sm' sx={{ mb: 0.5 }}>
|
|
||||||
Timezone
|
|
||||||
</Typography>
|
|
||||||
<Autocomplete
|
|
||||||
value={timezone}
|
|
||||||
onChange={(e, newValue) => setTimezone(newValue)}
|
|
||||||
options={timezones}
|
|
||||||
getOptionLabel={tz => {
|
|
||||||
const formattedTimezone = tz.replace(/_/g, ' ')
|
|
||||||
const currentTime = new Date().toLocaleString('en-US', {
|
|
||||||
timeZone: tz,
|
|
||||||
timeStyle: 'short',
|
|
||||||
})
|
|
||||||
return `${formattedTimezone} (${currentTime})`
|
|
||||||
}}
|
|
||||||
filterOptions={(options, { inputValue }) => {
|
|
||||||
if (!inputValue) return options
|
|
||||||
|
|
||||||
const searchTerms = inputValue.toLowerCase().split(/\s+/)
|
|
||||||
return options.filter(tz => {
|
|
||||||
const timezoneLower = tz.toLowerCase()
|
|
||||||
const timezoneParts = tz.toLowerCase().split(/[/_]/)
|
|
||||||
|
|
||||||
return searchTerms.every(
|
|
||||||
term =>
|
|
||||||
timezoneLower.includes(term) ||
|
|
||||||
timezoneParts.some(part => part.includes(term)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
placeholder='Select your timezone'
|
|
||||||
sx={{ mb: 2 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='soft'
|
|
||||||
color='primary'
|
|
||||||
onClick={handleSave}
|
|
||||||
loading={isSaving}
|
|
||||||
sx={{ width: 120 }}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
import { Refresh } from '@mui/icons-material'
|
import { Delete, Refresh } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -424,14 +424,10 @@ const Settings = () => {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
{userProfile.role === 'admin' &&
|
{isAdmin &&
|
||||||
member.userId !== userProfile.id &&
|
member.userId !== userProfile.id &&
|
||||||
member.isActive && (
|
member.isActive && (
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
|
||||||
circleMembers.find(m => userProfile.id == m.userId)
|
|
||||||
.role !== 'admin'
|
|
||||||
}
|
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
color='danger'
|
color='danger'
|
||||||
size='sm'
|
size='sm'
|
||||||
@@ -449,6 +445,17 @@ const Settings = () => {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
message: 'Removed member successfully',
|
message: 'Removed member successfully',
|
||||||
})
|
})
|
||||||
|
// Invalidate and refetch circle-related queries
|
||||||
|
queryClient.invalidateQueries(['circleMembers'])
|
||||||
|
queryClient.invalidateQueries(['userCircle'])
|
||||||
|
queryClient.refetchQueries(['circleMembers'])
|
||||||
|
queryClient.refetchQueries(['userCircle'])
|
||||||
|
// Update local state immediately
|
||||||
|
setCircleMembers(prevMembers =>
|
||||||
|
prevMembers.filter(
|
||||||
|
m => m.userId !== member.userId,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -458,7 +465,7 @@ const Settings = () => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
<Delete />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -514,8 +521,18 @@ const Settings = () => {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
message: 'Accepted request successfully',
|
message: 'Accepted request successfully',
|
||||||
})
|
})
|
||||||
// reload the page
|
// Invalidate and refetch circle-related queries
|
||||||
window.location.reload()
|
queryClient.invalidateQueries(['circleMembers'])
|
||||||
|
queryClient.invalidateQueries(['circleMemberRequests'])
|
||||||
|
queryClient.invalidateQueries(['userCircle'])
|
||||||
|
queryClient.refetchQueries(['circleMembers'])
|
||||||
|
queryClient.refetchQueries(['circleMemberRequests'])
|
||||||
|
queryClient.refetchQueries(['userCircle'])
|
||||||
|
// Refresh local state
|
||||||
|
refreshMemberRequests()
|
||||||
|
GetAllCircleMembers().then(data => {
|
||||||
|
setCircleMembers(data.res ? data.res : [])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -550,23 +567,29 @@ const Settings = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant='soft'
|
variant='soft'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showConfirmation(
|
JoinCircle(circleInviteCode).then(resp => {
|
||||||
`Are you sure you want to leave your circle and join '${circleInviteCode}'?`,
|
if (resp.ok) {
|
||||||
'Join Circle',
|
showNotification({
|
||||||
() => {
|
type: 'success',
|
||||||
JoinCircle(circleInviteCode).then(resp => {
|
message:
|
||||||
if (resp.ok) {
|
'Joined circle successfully, wait for the circle owner to accept your request.',
|
||||||
showNotification({
|
|
||||||
type: 'success',
|
|
||||||
message:
|
|
||||||
'Joined circle successfully, wait for the circle owner to accept your request.',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
setTimeout(() => navigate('/'), 3000)
|
||||||
'Join',
|
} else {
|
||||||
'Cancel',
|
if (resp.status === 409) {
|
||||||
)
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: 'You are already a member of this circle',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to join circle',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setTimeout(() => navigate('/'), 3000)
|
||||||
|
}
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Join Circle
|
Join Circle
|
||||||
|
|||||||
21
src/views/Settings/SettingsLayout.jsx
Normal file
21
src/views/Settings/SettingsLayout.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Box, Container, Typography } from '@mui/joy'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
const SettingsLayout = ({ title, children }) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Box sx={{ py: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||||
|
<Typography level='h2' sx={{ flexGrow: 1 }}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsLayout
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
AccountCircle,
|
AccountCircle,
|
||||||
Api,
|
Api,
|
||||||
|
ChevronRight,
|
||||||
Circle,
|
Circle,
|
||||||
Notifications,
|
Notifications,
|
||||||
Palette,
|
Palette,
|
||||||
Person,
|
Person,
|
||||||
Security,
|
Security,
|
||||||
|
Settings,
|
||||||
Star,
|
Star,
|
||||||
Storage,
|
Storage,
|
||||||
ViewSidebar,
|
ViewSidebar,
|
||||||
@@ -17,7 +19,11 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
Container,
|
Container,
|
||||||
Grid,
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemContent,
|
||||||
|
ListItemDecorator,
|
||||||
Stack,
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
@@ -36,15 +42,13 @@ const SettingsOverview = () => {
|
|||||||
description:
|
description:
|
||||||
'Update your profile information, photo, display name, and timezone preferences.',
|
'Update your profile information, photo, display name, and timezone preferences.',
|
||||||
icon: <Person />,
|
icon: <Person />,
|
||||||
color: 'primary',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'circle',
|
id: 'circle',
|
||||||
title: 'Circle Settings',
|
title: 'Circle Settings',
|
||||||
description:
|
description:
|
||||||
'Manage your circle, invite members, handle join requests, and configure webhooks.',
|
'Manage your circle, invite members, and handle join requests.',
|
||||||
icon: <Circle />,
|
icon: <Circle />,
|
||||||
color: 'success',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'account',
|
id: 'account',
|
||||||
@@ -52,7 +56,6 @@ const SettingsOverview = () => {
|
|||||||
description:
|
description:
|
||||||
'Manage your subscription, change password, and account deletion options.',
|
'Manage your subscription, change password, and account deletion options.',
|
||||||
icon: <AccountCircle />,
|
icon: <AccountCircle />,
|
||||||
color: 'warning',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'notifications',
|
id: 'notifications',
|
||||||
@@ -60,7 +63,6 @@ const SettingsOverview = () => {
|
|||||||
description:
|
description:
|
||||||
'Configure push notifications, email alerts, and notification targets for tasks.',
|
'Configure push notifications, email alerts, and notification targets for tasks.',
|
||||||
icon: <Notifications />,
|
icon: <Notifications />,
|
||||||
color: 'info',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'mfa',
|
id: 'mfa',
|
||||||
@@ -68,7 +70,6 @@ const SettingsOverview = () => {
|
|||||||
description:
|
description:
|
||||||
'Add an extra layer of security with MFA using authenticator apps.',
|
'Add an extra layer of security with MFA using authenticator apps.',
|
||||||
icon: <Security />,
|
icon: <Security />,
|
||||||
color: 'danger',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'apitokens',
|
id: 'apitokens',
|
||||||
@@ -76,7 +77,6 @@ const SettingsOverview = () => {
|
|||||||
description:
|
description:
|
||||||
'Generate and manage access tokens for third-party integrations and API access.',
|
'Generate and manage access tokens for third-party integrations and API access.',
|
||||||
icon: <Api />,
|
icon: <Api />,
|
||||||
color: 'neutral',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'storage',
|
id: 'storage',
|
||||||
@@ -84,7 +84,6 @@ const SettingsOverview = () => {
|
|||||||
description:
|
description:
|
||||||
'Backup and restore your data, manage local storage and sync preferences.',
|
'Backup and restore your data, manage local storage and sync preferences.',
|
||||||
icon: <Storage />,
|
icon: <Storage />,
|
||||||
color: 'primary',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sidepanel',
|
id: 'sidepanel',
|
||||||
@@ -92,7 +91,6 @@ const SettingsOverview = () => {
|
|||||||
description:
|
description:
|
||||||
'Customize the layout and visibility of cards in the sidepanel interface.',
|
'Customize the layout and visibility of cards in the sidepanel interface.',
|
||||||
icon: <ViewSidebar />,
|
icon: <ViewSidebar />,
|
||||||
color: 'success',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'theme',
|
id: 'theme',
|
||||||
@@ -100,12 +98,18 @@ const SettingsOverview = () => {
|
|||||||
description:
|
description:
|
||||||
'Choose your preferred theme and configure dark/light mode settings.',
|
'Choose your preferred theme and configure dark/light mode settings.',
|
||||||
icon: <Palette />,
|
icon: <Palette />,
|
||||||
color: 'warning',
|
},
|
||||||
|
{
|
||||||
|
id: 'advanced',
|
||||||
|
title: 'Advanced Settings',
|
||||||
|
description:
|
||||||
|
'Configure webhooks, real-time updates, and other advanced features for enhanced productivity.',
|
||||||
|
icon: <Settings />,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const handleCardClick = settingId => {
|
const handleCardClick = settingId => {
|
||||||
navigate(`/settings/detailed#${settingId}`)
|
navigate(`/settings/${settingId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -127,179 +131,178 @@ const SettingsOverview = () => {
|
|||||||
|
|
||||||
{/* Upgrade Card - Only show if user is not a Plus member */}
|
{/* Upgrade Card - Only show if user is not a Plus member */}
|
||||||
{userProfile && !isPlusAccount(userProfile) && (
|
{userProfile && !isPlusAccount(userProfile) && (
|
||||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
<Grid xs={12}>
|
<Card
|
||||||
<Card
|
variant='outlined'
|
||||||
variant='outlined'
|
sx={{
|
||||||
sx={{
|
cursor: 'pointer',
|
||||||
cursor: 'pointer',
|
transition: 'all 0.2s ease-in-out',
|
||||||
transition: 'all 0.2s ease-in-out',
|
background: 'linear-gradient(135deg, #0891b2 0%, #0e7490 100%)',
|
||||||
background: 'linear-gradient(135deg, #0891b2 0%, #0e7490 100%)',
|
border: 'none',
|
||||||
border: 'none',
|
color: 'white',
|
||||||
color: 'white',
|
'&:hover': {
|
||||||
'&:hover': {
|
boxShadow: 'xl',
|
||||||
boxShadow: 'xl',
|
transform: 'translateY(-3px)',
|
||||||
transform: 'translateY(-3px)',
|
},
|
||||||
},
|
}}
|
||||||
}}
|
onClick={() => navigate('/settings/account')}
|
||||||
onClick={() => navigate('/settings/detailed#account')}
|
>
|
||||||
>
|
<CardContent sx={{ p: { xs: 1.5, md: 3 } }}>
|
||||||
<CardContent sx={{ p: { xs: 1.5, md: 3 } }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
flexDirection: { xs: 'column', md: 'row' },
|
||||||
|
gap: { xs: 1.5, md: 2 },
|
||||||
|
textAlign: { xs: 'center', md: 'left' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
|
||||||
flexDirection: { xs: 'column', md: 'row' },
|
flexDirection: { xs: 'column', md: 'row' },
|
||||||
gap: { xs: 1.5, md: 2 },
|
gap: { xs: 1, md: 3 },
|
||||||
textAlign: { xs: 'center', md: 'left' },
|
width: { xs: '100%', md: 'auto' },
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
flexDirection: { xs: 'column', md: 'row' },
|
|
||||||
gap: { xs: 1, md: 3 },
|
|
||||||
width: { xs: '100%', md: 'auto' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
sx={{
|
|
||||||
'--Avatar-size': { xs: '36px', md: '60px' },
|
|
||||||
background: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Star
|
|
||||||
sx={{ fontSize: { xs: 18, md: 30 }, color: 'white' }}
|
|
||||||
/>
|
|
||||||
</Avatar>
|
|
||||||
<Box>
|
|
||||||
<Typography
|
|
||||||
level='title-lg'
|
|
||||||
sx={{
|
|
||||||
color: 'white',
|
|
||||||
mb: { xs: 0.25, md: 0.5 },
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: { xs: '0.95rem', md: '1.25rem' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Upgrade to Plus
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
level='body-md'
|
|
||||||
sx={{
|
|
||||||
color: 'rgba(255, 255, 255, 0.9)',
|
|
||||||
mb: { xs: 0.5, md: 1 },
|
|
||||||
fontSize: { xs: '0.75rem', md: '1rem' },
|
|
||||||
lineHeight: { xs: 1.3, md: 1.5 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Unlock powerful features to enhance your productivity
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: { xs: 'none', sm: 'flex' },
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 1,
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
color: 'rgba(255, 255, 255, 0.8)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>• Rich text descriptions</span>
|
|
||||||
<span>• Task notifications</span>
|
|
||||||
<span>• API integrations</span>
|
|
||||||
<span>• Advanced automation</span>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Button
|
|
||||||
variant='solid'
|
|
||||||
size='sm'
|
|
||||||
sx={{
|
|
||||||
bgcolor: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
color: 'white',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
px: { xs: 1.5, md: 3 },
|
|
||||||
py: { xs: 0.5, md: 1.5 },
|
|
||||||
fontWeight: 'bold',
|
|
||||||
minWidth: { xs: '80px', md: '120px' },
|
|
||||||
width: { xs: '100%', md: 'auto' },
|
|
||||||
fontSize: { xs: '0.75rem', md: '0.875rem' },
|
|
||||||
'&:hover': {
|
|
||||||
bgcolor: 'rgba(255, 255, 255, 0.3)',
|
|
||||||
transform: 'scale(1.05)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation()
|
|
||||||
navigate('/settings/detailed#account')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Upgrade Now
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
{settingsCards.map(setting => (
|
|
||||||
<Grid key={setting.id} xs={4} sm={4} md={4}>
|
|
||||||
<Card
|
|
||||||
variant='outlined'
|
|
||||||
sx={{
|
|
||||||
height: '100%',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s ease-in-out',
|
|
||||||
'&:hover': {
|
|
||||||
boxShadow: 'md',
|
|
||||||
transform: 'translateY(-2px)',
|
|
||||||
borderColor: `${setting.color}.500`,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onClick={() => handleCardClick(setting.id)}
|
|
||||||
>
|
|
||||||
<CardContent sx={{ p: 2 }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
textAlign: 'center',
|
|
||||||
mb: 1.5,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
'--Avatar-size': { xs: '36px', md: '60px' },
|
||||||
|
background: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Star
|
||||||
|
sx={{ fontSize: { xs: 18, md: 30 }, color: 'white' }}
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
level='title-lg'
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
mb: { xs: 0.25, md: 0.5 },
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: { xs: '0.95rem', md: '1.25rem' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upgrade to Plus
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
level='body-md'
|
||||||
|
sx={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
mb: { xs: 0.5, md: 1 },
|
||||||
|
fontSize: { xs: '0.75rem', md: '1rem' },
|
||||||
|
lineHeight: { xs: 1.3, md: 1.5 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Unlock powerful features to enhance your productivity
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'none', sm: 'flex' },
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 1,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>• Rich text descriptions</span>
|
||||||
|
<span>• Task notifications</span>
|
||||||
|
<span>• API integrations</span>
|
||||||
|
<span>• Advanced automation</span>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant='solid'
|
||||||
|
size='sm'
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
px: { xs: 1.5, md: 3 },
|
||||||
|
py: { xs: 0.5, md: 1.5 },
|
||||||
|
fontWeight: 'bold',
|
||||||
|
minWidth: { xs: '80px', md: '120px' },
|
||||||
|
width: { xs: '100%', md: 'auto' },
|
||||||
|
fontSize: { xs: '0.75rem', md: '0.875rem' },
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
transform: 'scale(1.05)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
navigate('/settings/account')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upgrade Now
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ mx: 'auto' }}>
|
||||||
|
<List
|
||||||
|
sx={{
|
||||||
|
'--List-gap': '0px',
|
||||||
|
'--ListItem-paddingY': '16px',
|
||||||
|
'--ListItem-paddingX': '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{settingsCards.map((setting, index) => (
|
||||||
|
<ListItem key={setting.id} sx={{ p: 0 }}>
|
||||||
|
<ListItemButton
|
||||||
|
onClick={() => handleCardClick(setting.id)}
|
||||||
|
sx={{
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'background.level1',
|
||||||
|
},
|
||||||
|
py: 2.5,
|
||||||
|
px: 3,
|
||||||
|
borderRadius: 'lg',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemDecorator>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color={setting.color}
|
color='neutral'
|
||||||
sx={{ mb: 1, '--Avatar-size': '40px' }}
|
sx={{ '--Avatar-size': '48px' }}
|
||||||
>
|
>
|
||||||
{setting.icon}
|
{setting.icon}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Typography level='title-sm' component='h3' sx={{ mb: 1 }}>
|
</ListItemDecorator>
|
||||||
|
<ListItemContent sx={{ ml: 2 }}>
|
||||||
|
<Typography
|
||||||
|
level='title-md'
|
||||||
|
sx={{ mb: 0.5, fontWeight: 'lg' }}
|
||||||
|
>
|
||||||
{setting.title}
|
{setting.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
level='body-xs'
|
level='body-sm'
|
||||||
color='neutral'
|
color='neutral'
|
||||||
sx={{
|
sx={{ lineHeight: 1.4 }}
|
||||||
lineHeight: 1.4,
|
|
||||||
display: { xs: 'none', sm: 'block' },
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{setting.description}
|
{setting.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</ListItemContent>
|
||||||
</CardContent>
|
<ChevronRight
|
||||||
</Card>
|
sx={{ color: 'text.tertiary', fontSize: '20px' }}
|
||||||
</Grid>
|
/>
|
||||||
))}
|
</ListItemButton>
|
||||||
</Grid>
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/views/Settings/SettingsRoutes.jsx
Normal file
7
src/views/Settings/SettingsRoutes.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Outlet } from 'react-router-dom'
|
||||||
|
|
||||||
|
const SettingsRoutes = () => {
|
||||||
|
return <Outlet />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsRoutes
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
|
||||||
const DEFAULT_SIDEPANEL_CONFIG = [
|
const DEFAULT_SIDEPANEL_CONFIG = [
|
||||||
{
|
{
|
||||||
@@ -122,136 +123,148 @@ const SidepanelSettings = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<SettingsLayout title='Sidepanel Customization'>
|
||||||
<Typography level='h4' sx={{ mb: 2 }}>
|
<div className='grid gap-4'>
|
||||||
Sidepanel Settings
|
<Box>
|
||||||
</Typography>
|
<Typography level='h4' sx={{ mb: 2 }}>
|
||||||
<Typography level='body-md' sx={{ mb: 3 }}>
|
Sidepanel Settings
|
||||||
Customize which cards appear in the sidepanel and their order. Drag and
|
</Typography>
|
||||||
drop to reorder, or toggle visibility for each card.
|
<Typography level='body-md' sx={{ mb: 3 }}>
|
||||||
</Typography>
|
Customize which cards appear in the sidepanel and their order. Drag
|
||||||
|
and drop to reorder, or toggle visibility for each card.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<DragDropContext onDragEnd={handleDragEnd}>
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
<Droppable droppableId='sidepanel-cards'>
|
<Droppable droppableId='sidepanel-cards'>
|
||||||
{provided => (
|
{provided => (
|
||||||
<List
|
<List
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
sx={{ gap: 1 }}
|
sx={{ gap: 1 }}
|
||||||
>
|
>
|
||||||
{config.map((item, index) => (
|
{config.map((item, index) => (
|
||||||
<Draggable key={item.id} draggableId={item.id} index={index}>
|
<Draggable
|
||||||
{(provided, snapshot) => (
|
key={item.id}
|
||||||
<ListItem
|
draggableId={item.id}
|
||||||
ref={provided.innerRef}
|
index={index}
|
||||||
{...provided.draggableProps}
|
|
||||||
sx={{
|
|
||||||
p: 0,
|
|
||||||
backgroundColor: snapshot.isDragging
|
|
||||||
? 'var(--joy-palette-neutral-softBg)'
|
|
||||||
: 'transparent',
|
|
||||||
borderRadius: 'var(--joy-radius-md)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Card
|
{(provided, snapshot) => (
|
||||||
sx={{
|
<ListItem
|
||||||
width: '100%',
|
ref={provided.innerRef}
|
||||||
display: 'flex',
|
{...provided.draggableProps}
|
||||||
alignItems: 'center',
|
sx={{
|
||||||
flexDirection: 'row',
|
p: 0,
|
||||||
gap: 2,
|
backgroundColor: snapshot.isDragging
|
||||||
p: 2,
|
? 'var(--joy-palette-neutral-softBg)'
|
||||||
opacity: item.enabled ? 1 : 0.6,
|
: 'transparent',
|
||||||
border: snapshot.isDragging
|
borderRadius: 'var(--joy-radius-md)',
|
||||||
? '2px solid var(--joy-palette-primary-400)'
|
}}
|
||||||
: '1px solid var(--joy-palette-divider)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemDecorator>
|
|
||||||
<IconButton
|
|
||||||
{...provided.dragHandleProps}
|
|
||||||
variant='plain'
|
|
||||||
size='sm'
|
|
||||||
sx={{
|
|
||||||
cursor: 'grab',
|
|
||||||
'&:active': { cursor: 'grabbing' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DragIndicator />
|
|
||||||
</IconButton>
|
|
||||||
</ListItemDecorator>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
sx={{ color: 'var(--joy-palette-primary-500)' }}
|
|
||||||
>
|
>
|
||||||
{getIcon(item.iconName)}
|
<Card
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<ListItemContent sx={{ flex: 1 }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
sx={{
|
||||||
|
width: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
|
p: 2,
|
||||||
|
opacity: item.enabled ? 1 : 0.6,
|
||||||
|
border: snapshot.isDragging
|
||||||
|
? '2px solid var(--joy-palette-primary-400)'
|
||||||
|
: '1px solid var(--joy-palette-divider)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<ListItemDecorator>
|
||||||
level='title-sm'
|
<IconButton
|
||||||
sx={{ fontWeight: 600 }}
|
{...provided.dragHandleProps}
|
||||||
>
|
variant='plain'
|
||||||
{item.name}
|
size='sm'
|
||||||
</Typography>
|
sx={{
|
||||||
<Typography
|
cursor: 'grab',
|
||||||
level='body-xs'
|
'&:active': { cursor: 'grabbing' },
|
||||||
sx={{
|
}}
|
||||||
color: 'var(--joy-palette-text-tertiary)',
|
>
|
||||||
}}
|
<DragIndicator />
|
||||||
>
|
</IconButton>
|
||||||
- {item.description}
|
</ListItemDecorator>
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</ListItemContent>
|
|
||||||
|
|
||||||
<FormControl>
|
<IconButton
|
||||||
<Checkbox
|
sx={{ color: 'var(--joy-palette-primary-500)' }}
|
||||||
checked={item.enabled}
|
>
|
||||||
onChange={e =>
|
{getIcon(item.iconName)}
|
||||||
handleToggleEnabled(item.id, e.target.checked)
|
</IconButton>
|
||||||
}
|
|
||||||
overlay
|
|
||||||
variant='plain'
|
|
||||||
size='lg'
|
|
||||||
checkedIcon={<Visibility />}
|
|
||||||
uncheckedIcon={<VisibilityOff />}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</Card>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
))}
|
|
||||||
{provided.placeholder}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
|
|
||||||
<Box
|
<ListItemContent sx={{ flex: 1 }}>
|
||||||
sx={{ mt: 3, pt: 2, borderTop: '1px solid var(--joy-palette-divider)' }}
|
<Box
|
||||||
>
|
sx={{
|
||||||
<Button
|
display: 'flex',
|
||||||
variant='outlined'
|
alignItems: 'center',
|
||||||
color='neutral'
|
gap: 2,
|
||||||
onClick={resetToDefaults}
|
}}
|
||||||
size='sm'
|
>
|
||||||
>
|
<Typography
|
||||||
Reset to Defaults
|
level='title-sm'
|
||||||
</Button>
|
sx={{ fontWeight: 600 }}
|
||||||
<FormHelperText sx={{ mt: 1 }}>
|
>
|
||||||
This will restore all cards to their default visibility and order.
|
{item.name}
|
||||||
</FormHelperText>
|
</Typography>
|
||||||
</Box>
|
<Typography
|
||||||
</Box>
|
level='body-xs'
|
||||||
|
sx={{
|
||||||
|
color: 'var(--joy-palette-text-tertiary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
- {item.description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</ListItemContent>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={item.enabled}
|
||||||
|
onChange={e =>
|
||||||
|
handleToggleEnabled(item.id, e.target.checked)
|
||||||
|
}
|
||||||
|
overlay
|
||||||
|
variant='plain'
|
||||||
|
size='lg'
|
||||||
|
checkedIcon={<Visibility />}
|
||||||
|
uncheckedIcon={<VisibilityOff />}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Card>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 3,
|
||||||
|
pt: 2,
|
||||||
|
borderTop: '1px solid var(--joy-palette-divider)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
color='neutral'
|
||||||
|
onClick={resetToDefaults}
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
|
Reset to Defaults
|
||||||
|
</Button>
|
||||||
|
<FormHelperText sx={{ mt: 1 }}>
|
||||||
|
This will restore all cards to their default visibility and order.
|
||||||
|
</FormHelperText>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</SettingsLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
import {
|
import { Button, Card, Chip, LinearProgress, Typography } from '@mui/joy'
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Chip,
|
|
||||||
Divider,
|
|
||||||
LinearProgress,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/joy'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
import { GetStorageUsage } from '../../utils/Fetcher'
|
import { GetStorageUsage } from '../../utils/Fetcher'
|
||||||
import { isPlusAccount } from '../../utils/Helpers'
|
import { isPlusAccount } from '../../utils/Helpers'
|
||||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||||
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
|
||||||
const StorageSettings = () => {
|
const StorageSettings = () => {
|
||||||
const Navigate = useNavigate()
|
const Navigate = useNavigate()
|
||||||
@@ -62,114 +56,114 @@ const StorageSettings = () => {
|
|||||||
const totalMB = (usage.total / (1024 * 1024)).toFixed(2)
|
const totalMB = (usage.total / (1024 * 1024)).toFixed(2)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='grid gap-4 py-4' id='storage'>
|
<SettingsLayout title='Storage Settings'>
|
||||||
<Typography level='h3'>Storage Settings</Typography>
|
<div className='grid gap-4 py-4' id='storage'>
|
||||||
<Divider />
|
<Card className='p-4' sx={{ maxWidth: 500, mb: 2 }}>
|
||||||
<Card className='p-4' sx={{ maxWidth: 500, mb: 2 }}>
|
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
Server Storage Usage
|
||||||
Server Storage Usage
|
{!isPlusAccount(userProfile) && (
|
||||||
{!isPlusAccount(userProfile) && (
|
<Chip variant='soft' color='warning' sx={{ ml: 1 }}>
|
||||||
<Chip variant='soft' color='warning' sx={{ ml: 1 }}>
|
Plus Feature
|
||||||
Plus Feature
|
</Chip>
|
||||||
</Chip>
|
)}
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body-sm' sx={{ mb: 1 }}>
|
||||||
|
This is the storage used by your account on our servers (e.g. files,
|
||||||
|
images, and data you have uploaded).
|
||||||
|
</Typography>
|
||||||
|
{!isPlusAccount(userProfile) ? (
|
||||||
|
<>
|
||||||
|
<LinearProgress
|
||||||
|
determinate
|
||||||
|
value={0}
|
||||||
|
sx={{
|
||||||
|
mb: 1,
|
||||||
|
opacity: 0.4,
|
||||||
|
'& .MuiLinearProgress-bar': {
|
||||||
|
backgroundColor: 'var(--joy-palette-neutral-400)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography level='body-xs' sx={{ opacity: 0.6, mb: 1 }}>
|
||||||
|
-- MB used / -- MB total (--)
|
||||||
|
</Typography>
|
||||||
|
<Typography level='body-sm' color='warning'>
|
||||||
|
Server storage is not available in the Basic plan. Upgrade to
|
||||||
|
Plus to track your server storage usage.
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
) : loading ? (
|
||||||
|
<>
|
||||||
|
<LinearProgress sx={{ mb: 1 }} />
|
||||||
|
<Typography level='body-xs'>Loading...</Typography>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<LinearProgress determinate value={percent} sx={{ mb: 1 }} />
|
||||||
|
<Typography level='body-xs'>
|
||||||
|
{usedMB} MB used / {totalMB} MB total ({percent}%)
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Card>
|
||||||
<Typography level='body-sm' sx={{ mb: 1 }}>
|
<Card className='p-4' sx={{ maxWidth: 500, mb: 2 }}>
|
||||||
This is the storage used by your account on our servers (e.g. files,
|
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||||
images, and data you have uploaded).
|
{Capacitor.isNativePlatform() ? 'App' : 'Browser'} Local Storage &
|
||||||
</Typography>
|
Cache
|
||||||
{!isPlusAccount(userProfile) ? (
|
</Typography>
|
||||||
<>
|
<Typography level='body-sm' sx={{ mb: 1 }}>
|
||||||
<LinearProgress
|
This is data stored locally in your browser for faster access and
|
||||||
determinate
|
offline use. Clearing this will not affect your server data, but may
|
||||||
value={0}
|
log you out or remove offline tasks.
|
||||||
sx={{
|
</Typography>
|
||||||
mb: 1,
|
<Button
|
||||||
opacity: 0.4,
|
variant='soft'
|
||||||
'& .MuiLinearProgress-bar': {
|
color='danger'
|
||||||
backgroundColor: 'var(--joy-palette-neutral-400)',
|
onClick={() => {
|
||||||
|
showConfirmation(
|
||||||
|
'Are you sure you want to clear your local storage and cache? This will remove all your data from this browser and require login.',
|
||||||
|
'Clear All Local Storage',
|
||||||
|
() => {
|
||||||
|
localStorage.clear()
|
||||||
|
Navigate('/login')
|
||||||
},
|
},
|
||||||
}}
|
'Clear All',
|
||||||
/>
|
'Cancel',
|
||||||
<Typography level='body-xs' sx={{ opacity: 0.6, mb: 1 }}>
|
'danger',
|
||||||
-- MB used / -- MB total (--)
|
)
|
||||||
</Typography>
|
}}
|
||||||
<Typography level='body-sm' color='warning'>
|
>
|
||||||
Server storage monitoring is not available in the Basic plan.
|
Clear All Local Storage and Cache
|
||||||
Upgrade to Plus to track your server storage usage.
|
</Button>
|
||||||
</Typography>
|
<Button
|
||||||
</>
|
variant='outlined'
|
||||||
) : loading ? (
|
color='danger'
|
||||||
<>
|
onClick={() => {
|
||||||
<LinearProgress sx={{ mb: 1 }} />
|
showConfirmation(
|
||||||
<Typography level='body-xs'>Loading...</Typography>
|
'Are you sure you want to clear only the offline cache and tasks?',
|
||||||
</>
|
'Clear Offline Cache',
|
||||||
) : (
|
() => {
|
||||||
<>
|
localStorage.removeItem('offline_cache')
|
||||||
<LinearProgress determinate value={percent} sx={{ mb: 1 }} />
|
localStorage.removeItem('offline_request_queue')
|
||||||
<Typography level='body-xs'>
|
localStorage.removeItem('offlineTasks')
|
||||||
{usedMB} MB used / {totalMB} MB total ({percent}%)
|
},
|
||||||
</Typography>
|
'Clear Cache',
|
||||||
</>
|
'Cancel',
|
||||||
)}
|
'danger',
|
||||||
</Card>
|
)
|
||||||
<Card className='p-4' sx={{ maxWidth: 500, mb: 2 }}>
|
}}
|
||||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
sx={{ mt: 1 }}
|
||||||
{Capacitor.isNativePlatform() ? 'App' : 'Browser'} Local Storage &
|
>
|
||||||
Cache
|
Clear Offline Cache and Offline Tasks
|
||||||
</Typography>
|
</Button>
|
||||||
<Typography level='body-sm' sx={{ mb: 1 }}>
|
</Card>
|
||||||
This is data stored locally in your browser for faster access and
|
|
||||||
offline use. Clearing this will not affect your server data, but may
|
|
||||||
log you out or remove offline tasks.
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
variant='soft'
|
|
||||||
color='danger'
|
|
||||||
onClick={() => {
|
|
||||||
showConfirmation(
|
|
||||||
'Are you sure you want to clear your local storage and cache? This will remove all your data from this browser and require login.',
|
|
||||||
'Clear All Local Storage',
|
|
||||||
() => {
|
|
||||||
localStorage.clear()
|
|
||||||
Navigate('/login')
|
|
||||||
},
|
|
||||||
'Clear All',
|
|
||||||
'Cancel',
|
|
||||||
'danger',
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear All Local Storage and Cache
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='outlined'
|
|
||||||
color='danger'
|
|
||||||
onClick={() => {
|
|
||||||
showConfirmation(
|
|
||||||
'Are you sure you want to clear only the offline cache and tasks?',
|
|
||||||
'Clear Offline Cache',
|
|
||||||
() => {
|
|
||||||
localStorage.removeItem('offline_cache')
|
|
||||||
localStorage.removeItem('offline_request_queue')
|
|
||||||
localStorage.removeItem('offlineTasks')
|
|
||||||
},
|
|
||||||
'Clear Cache',
|
|
||||||
'Cancel',
|
|
||||||
'danger',
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
sx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
Clear Offline Cache and Offline Tasks
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
{confirmModalConfig?.isOpen && (
|
{confirmModalConfig?.isOpen && (
|
||||||
<ConfirmationModal config={confirmModalConfig} />
|
<ConfirmationModal config={confirmModalConfig} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</SettingsLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
src/views/Settings/ThemeSettings.jsx
Normal file
19
src/views/Settings/ThemeSettings.jsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Typography } from '@mui/joy'
|
||||||
|
import SettingsLayout from './SettingsLayout'
|
||||||
|
import ThemeToggle from './ThemeToggle'
|
||||||
|
|
||||||
|
const ThemeSettings = () => {
|
||||||
|
return (
|
||||||
|
<SettingsLayout title="Theme Preferences">
|
||||||
|
<div className='grid gap-4'>
|
||||||
|
<Typography level='body-md'>
|
||||||
|
Choose how the site looks to you. Select a single theme, or sync with
|
||||||
|
your system and automatically switch between day and night themes.
|
||||||
|
</Typography>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</SettingsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemeSettings
|
||||||
@@ -2,190 +2,244 @@ import React from 'react'
|
|||||||
|
|
||||||
const TermsView = () => {
|
const TermsView = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px', lineHeight: '1.6' }}>
|
||||||
<h1>Terms of Service</h1>
|
<h1>Terms of Service</h1>
|
||||||
|
<p><em>Effective Date: January 1, 2024</em></p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
These Terms of Service ("Terms") govern your access to and use of the
|
These Terms of Service ("Terms") govern your access to and use of the
|
||||||
services provided by Favoro LLC, doing business as donetick.com
|
Donetick task management platform provided by Favoro LLC ("Donetick", "we", "us", or "our").
|
||||||
("Favoro", "we", "us", or "our"). By accessing or using our website and
|
By accessing or using our services, you agree to be bound by these Terms. If you do not agree to
|
||||||
services, you agree to be bound by these Terms. If you do not agree to
|
|
||||||
these Terms, you may not access or use our services.
|
these Terms, you may not access or use our services.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Use of Services</h2>
|
<div style={{ backgroundColor: '#f5f5f5', padding: '15px', borderRadius: '5px', margin: '20px 0' }}>
|
||||||
|
<h3>Important: Cloud vs. Self-Hosted Services</h3>
|
||||||
|
<p>
|
||||||
|
Donetick is available in two deployment models with different terms and responsibilities:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Cloud-Hosted Service:</strong> Hosted at donetick.com where we provide infrastructure and data management</li>
|
||||||
|
<li><strong>Self-Hosted Service:</strong> Open-source software you deploy on your own infrastructure</li>
|
||||||
|
</ul>
|
||||||
|
<p>Please review the applicable sections carefully as they define different rights and obligations.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>1. Definitions</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li><strong>"Cloud Service"</strong> refers to the Donetick platform hosted at donetick.com and managed by Favoro LLC</li>
|
||||||
You must be at least 18 years old or have the legal capacity to enter
|
<li><strong>"Self-Hosted Service"</strong> refers to the open-source Donetick software deployed on user-controlled infrastructure</li>
|
||||||
into contracts in your jurisdiction to use our services.
|
<li><strong>"Content"</strong> means all data, information, tasks, files, and other materials you create, upload, or store using our services</li>
|
||||||
</li>
|
<li><strong>"Circle"</strong> means a collaborative workspace shared between users for task management</li>
|
||||||
<li>
|
|
||||||
You are responsible for maintaining the confidentiality of your
|
|
||||||
account credentials and for any activity that occurs under your
|
|
||||||
account.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
You may not use our services for any illegal or unauthorized purpose,
|
|
||||||
or in any way that violates these Terms.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h2>Subscriptions</h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Some parts of the Service are billed on a subscription basis
|
|
||||||
("Subscription(s)"). You will be billed in advance on a recurring and
|
|
||||||
periodic basis ("Billing Cycle"). Billing cycles are set either on a
|
|
||||||
monthly or annual basis, depending on the type of subscription plan
|
|
||||||
you select when purchasing a Subscription.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
At the end of each Billing Cycle, your Subscription will automatically
|
|
||||||
renew under the exact same conditions unless you cancel it or Favoro
|
|
||||||
cancels it. You may cancel your Subscription renewal either through
|
|
||||||
your online account management page or by contacting Donetickcustomer
|
|
||||||
support team.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
A valid payment method, including credit or debit card, is required to
|
|
||||||
process the payment for your Subscription. You shall provide Favoro
|
|
||||||
with accurate and complete billing information including full name,
|
|
||||||
address, state, zip code, telephone number, and a valid payment method
|
|
||||||
information. By submitting such payment information, you automatically
|
|
||||||
authorize Donetickto charge all Subscription fees incurred through
|
|
||||||
your account to any such payment instruments.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Should automatic billing fail to occur for any reason, Donetickwill
|
|
||||||
issue an electronic invoice indicating that you must proceed manually,
|
|
||||||
within a certain deadline date, with the full payment corresponding to
|
|
||||||
the billing period as indicated on the invoice.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Fee Changes</h2>
|
<h2>2. Eligibility and Account Registration</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>You must be at least 13 years old to use our services</li>
|
||||||
{' '}
|
<li>Users under 18 must have parental consent</li>
|
||||||
Favoro, in its sole discretion and at any time, may modify the
|
<li>You are responsible for maintaining the confidentiality of your account credentials</li>
|
||||||
Subscription fees for the Subscriptions. Any Subscription fee change
|
<li>You must provide accurate and complete information when creating an account</li>
|
||||||
will become effective at the end of the then-current Billing Cycle.
|
<li>One person or legal entity may maintain only one account</li>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Donetickwill provide you with reasonable prior notice of any change in
|
|
||||||
Subscription fees to give you an opportunity to terminate your
|
|
||||||
Subscription before such change becomes effective.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Refunds</h2>
|
<h2>3. Cloud-Hosted Service Terms</h2>
|
||||||
|
<p><em>This section applies only to users of the Cloud Service at donetick.com</em></p>
|
||||||
|
|
||||||
|
<h3>3.1 Service Availability and Uptime</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>We strive to maintain 99.5% uptime for the Cloud Service on a monthly basis</li>
|
||||||
Certain refund requests for Subscriptions may be considered by Favoro
|
<li>Scheduled maintenance will be announced at least 24 hours in advance when possible</li>
|
||||||
on a case-by-case basis and granted at the sole discretion of Favoro.
|
<li>We reserve the right to temporarily suspend service for emergency maintenance</li>
|
||||||
</li>
|
<li>No SLA credits or compensation are provided for downtime unless otherwise specified in a separate agreement</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Content</h2>
|
<h3>3.2 Data Ownership and Responsibility</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li><strong>Your Data:</strong> You retain ownership of all Content you create or upload</li>
|
||||||
Our services allow you to post, link, store, share, and otherwise make
|
<li><strong>Our Responsibility:</strong> We provide secure hosting, backup, and infrastructure management</li>
|
||||||
available certain information, text, graphics, videos, or other
|
<li><strong>Data Portability:</strong> You can export your data at any time through our export features</li>
|
||||||
material ("Content").
|
<li><strong>Data Retention:</strong> We retain your data for 90 days after account deletion to allow recovery</li>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
You are responsible for the Content that you post to our services,
|
|
||||||
including its legality, reliability, and appropriateness.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
You may not post Content that is defamatory, obscene, abusive,
|
|
||||||
offensive, or otherwise objectionable.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
You may not post Content that violates any party's intellectual
|
|
||||||
property rights.
|
|
||||||
</li>
|
|
||||||
<li> You may not post Content that violates any law or regulation.</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Feedback Requests</h2>
|
|
||||||
|
|
||||||
|
<h3>3.3 Subscriptions and Billing</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Cloud Service subscriptions are billed monthly or annually in advance</li>
|
||||||
|
<li>Subscription fees will automatically renew unless cancelled before the next billing cycle</li>
|
||||||
|
<li>You may cancel your subscription at any time through your account settings</li>
|
||||||
|
<li>Upon cancellation, you retain access until the end of your current billing period</li>
|
||||||
|
<li>We may modify subscription prices with 30 days advance notice</li>
|
||||||
|
<li>Refunds are considered on a case-by-case basis at our discretion</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>3.4 Cloud Service Limitations</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Storage limits apply based on your subscription tier</li>
|
||||||
|
<li>API rate limits may be enforced to ensure service stability</li>
|
||||||
|
<li>We may suspend accounts that exceed reasonable usage limits</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>4. Self-Hosted Service Terms</h2>
|
||||||
|
<p><em>This section applies to users deploying Donetick on their own infrastructure</em></p>
|
||||||
|
|
||||||
|
<h3>4.1 Open Source License</h3>
|
||||||
|
<ul>
|
||||||
|
<li>The Self-Hosted Service is provided under the MIT License</li>
|
||||||
|
<li>You may modify, distribute, and use the software for any purpose</li>
|
||||||
|
<li>Attribution to Donetick must be maintained in derivative works</li>
|
||||||
|
<li>No warranty or support is provided for self-hosted deployments</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>4.2 User Responsibility</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Infrastructure:</strong> You are solely responsible for hosting, maintenance, security, and backups</li>
|
||||||
|
<li><strong>Data Protection:</strong> You are the data controller and responsible for compliance with applicable laws</li>
|
||||||
|
<li><strong>Updates:</strong> You are responsible for applying security updates and patches</li>
|
||||||
|
<li><strong>Support:</strong> No technical support is provided for self-hosted installations</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>4.3 Limitation of Our Responsibility</h3>
|
||||||
|
<ul>
|
||||||
|
<li>We provide no guarantees about the performance or security of self-hosted deployments</li>
|
||||||
|
<li>We are not responsible for any data loss, security breaches, or service interruptions in self-hosted environments</li>
|
||||||
|
<li>Technical support is limited to community forums and documentation</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>5. Acceptable Use Policy</h2>
|
||||||
|
<p><em>Applies to both Cloud and Self-Hosted Services</em></p>
|
||||||
|
|
||||||
|
<h3>You may not use our services to:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Violate any applicable laws or regulations</li>
|
||||||
|
<li>Infringe on intellectual property rights</li>
|
||||||
|
<li>Transmit malicious code or conduct security attacks</li>
|
||||||
|
<li>Harass, abuse, or harm other users</li>
|
||||||
|
<li>Distribute spam or unwanted communications</li>
|
||||||
|
<li>Attempt to reverse engineer our proprietary systems (Cloud Service)</li>
|
||||||
|
<li>Resell or redistribute the service without permission</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>6. Content and Data</h2>
|
||||||
|
|
||||||
|
<h3>6.1 Your Content Rights</h3>
|
||||||
|
<ul>
|
||||||
|
<li>You retain ownership of all Content you create</li>
|
||||||
|
<li>You grant us the necessary rights to operate the service (Cloud Service only)</li>
|
||||||
|
<li>You are responsible for ensuring you have rights to any Content you upload</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>6.2 Content Restrictions</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Content must not violate any laws or third-party rights</li>
|
||||||
|
<li>Content must not contain malicious code or harmful materials</li>
|
||||||
|
<li>We may remove Content that violates these Terms (Cloud Service only)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>7. Privacy and Security</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Your privacy is important to us - please review our Privacy Policy</li>
|
||||||
|
<li>We implement industry-standard security measures (Cloud Service)</li>
|
||||||
|
<li>You are responsible for security in self-hosted deployments</li>
|
||||||
|
<li>Report security vulnerabilities to security@donetick.com</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>8. Intellectual Property</h2>
|
||||||
|
<ul>
|
||||||
|
<li>The Donetick name, logo, and proprietary features are our intellectual property</li>
|
||||||
|
<li>The open-source codebase is licensed under MIT License</li>
|
||||||
|
<li>You may not use our trademarks without written permission</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>9. Third-Party Integrations</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Donetick integrates with third-party services (Telegram, Discord, webhooks, etc.)</li>
|
||||||
|
<li>Your use of these integrations is subject to their respective terms</li>
|
||||||
|
<li>We are not responsible for third-party service availability or functionality</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>10. Liability and Warranties</h2>
|
||||||
|
|
||||||
|
<h3>10.1 Disclaimer of Warranties</h3>
|
||||||
<p>
|
<p>
|
||||||
Our platform allows users to send feedback requests to others. You are
|
Our services are provided "as is" and "as available" without any warranty of any kind,
|
||||||
solely responsible for the content of any feedback requests you send
|
express or implied, including but not limited to merchantability, fitness for a particular purpose,
|
||||||
using our services.
|
or non-infringement.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h3>10.2 Cloud Service Liability</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Our total liability for the Cloud Service is limited to the amount you paid in the 12 months preceding the claim</li>
|
||||||
|
<li>We are not liable for indirect, incidental, special, or consequential damages</li>
|
||||||
|
<li>We maintain appropriate insurance and implement security best practices</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>10.3 Self-Hosted Service Liability</h3>
|
||||||
|
<ul>
|
||||||
|
<li>We provide no warranties or guarantees for self-hosted deployments</li>
|
||||||
|
<li>Our liability is limited to the maximum extent permitted by law</li>
|
||||||
|
<li>You assume all risks associated with self-hosting</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>11. Indemnification</h2>
|
||||||
<p>
|
<p>
|
||||||
You may not use our services to send spam, harass others, or engage in
|
You agree to indemnify and hold us harmless from any claims, damages, or expenses
|
||||||
any abusive behavior.
|
arising from your use of our services, violation of these Terms, or infringement of any rights.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Credits</h2>
|
<h2>12. Termination</h2>
|
||||||
|
|
||||||
|
<h3>12.1 Termination by You</h3>
|
||||||
|
<ul>
|
||||||
|
<li>You may terminate your account at any time</li>
|
||||||
|
<li>Cloud Service: Access continues until the end of your billing period</li>
|
||||||
|
<li>Self-Hosted Service: You may stop using the software at any time</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>12.2 Termination by Us</h3>
|
||||||
|
<ul>
|
||||||
|
<li>We may terminate accounts that violate these Terms</li>
|
||||||
|
<li>We may discontinue the Cloud Service with 90 days notice</li>
|
||||||
|
<li>We will provide data export capabilities before termination when possible</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>13. Changes to These Terms</h2>
|
||||||
|
<ul>
|
||||||
|
<li>We may update these Terms from time to time</li>
|
||||||
|
<li>Material changes will be announced via email or in-app notification</li>
|
||||||
|
<li>Continued use after changes constitutes acceptance of new Terms</li>
|
||||||
|
<li>For significant changes, we may require explicit acceptance</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>14. Dispute Resolution</h2>
|
||||||
|
<ul>
|
||||||
|
<li>We encourage resolving disputes informally by contacting us first</li>
|
||||||
|
<li>These Terms are governed by the laws of Delaware, United States</li>
|
||||||
|
<li>Any disputes will be resolved in the courts of Delaware</li>
|
||||||
|
<li>You may pursue small claims court for eligible disputes</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>15. Miscellaneous</h2>
|
||||||
|
<ul>
|
||||||
|
<li>If any provision is found unenforceable, the remainder remains in effect</li>
|
||||||
|
<li>Our failure to enforce any right does not waive that right</li>
|
||||||
|
<li>These Terms constitute the entire agreement between us</li>
|
||||||
|
<li>We may assign these Terms; you may not without our consent</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>16. Contact Information</h2>
|
||||||
<p>
|
<p>
|
||||||
Certain actions on our platform may require credits. You can purchase
|
If you have questions about these Terms, please contact us:
|
||||||
credits through our website.
|
|
||||||
</p>
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Email:</strong> legal@donetick.com</li>
|
||||||
|
<li><strong>Support:</strong> support@donetick.com</li>
|
||||||
|
<li><strong>Address:</strong> Favoro LLC, [Address to be provided]</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<p>Credits are non-refundable and non-transferable.</p>
|
<hr style={{ margin: '40px 0' }} />
|
||||||
|
<p style={{ fontSize: '14px', color: '#666' }}>
|
||||||
<h2>Intellectual Property</h2>
|
<strong>Last Updated:</strong> January 1, 2024<br />
|
||||||
|
These Terms of Service are effective immediately for new users and will become effective
|
||||||
<p>
|
for existing users 30 days after posting.
|
||||||
All content on our website and services, including text, graphics,
|
|
||||||
logos, and images, is the property of Donetickor its licensors and is
|
|
||||||
protected by copyright and other intellectual property laws.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
You may not reproduce, modify, or distribute any content from our
|
|
||||||
website or services without our prior written consent.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Disclaimer of Warranties</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Our services are provided "as is" and "as available" without any
|
|
||||||
warranty of any kind, express or implied.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
We do not warrant that our services will be uninterrupted, secure, or
|
|
||||||
error-free, or that any defects will be corrected.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Limitation of Liability</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
In no event shall Donetickbe liable for any indirect, incidental,
|
|
||||||
special, consequential, or punitive damages, including but not limited
|
|
||||||
to lost profits, arising out of or in connection with your use of our
|
|
||||||
services.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Governing Law</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
These Terms shall be governed by and construed in accordance with the
|
|
||||||
laws of the state of [Your State/Country], without regard to its
|
|
||||||
conflict of law principles.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Changes to These Terms</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
We may update these Terms from time to time. Any changes will be posted
|
|
||||||
on this page, and the revised date will be indicated at the top of the
|
|
||||||
page. Your continued use of our services after any such changes
|
|
||||||
constitutes your acceptance of the new Terms.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Contact Us</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
If you have any questions or concerns about these Terms, please contact
|
|
||||||
us at support@donetick.com
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -668,7 +668,7 @@ const ThingsView = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth='md' sx={{ px: 0 }}>
|
<Container maxWidth='md' sx={{ px: 0 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2, p: 2 }}>
|
||||||
{/* <EmojiEvents sx={{ fontSize: '2rem', color: '#FFD700' }} /> */}
|
{/* <EmojiEvents sx={{ fontSize: '2rem', color: '#FFD700' }} /> */}
|
||||||
<Stack sx={{ flex: 1 }}>
|
<Stack sx={{ flex: 1 }}>
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Add, EditNotifications } from '@mui/icons-material'
|
import { Add, EditNotifications } from '@mui/icons-material'
|
||||||
import { Box, Button, Chip, Input, Option, Select, Typography } from '@mui/joy'
|
import { Box, Button, Input, Option, Select, Typography } from '@mui/joy'
|
||||||
import { FormControl } from '@mui/material'
|
import { FormControl } from '@mui/material'
|
||||||
import * as chrono from 'chrono-node'
|
import * as chrono from 'chrono-node'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
@@ -473,9 +473,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
title='Create new task'
|
title='Create new task'
|
||||||
>
|
>
|
||||||
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
|
|
||||||
Experimental Feature
|
|
||||||
</Chip>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CalendarMonth } from '@mui/icons-material'
|
import { CalendarMonth } from '@mui/icons-material'
|
||||||
import { Avatar, Box, Chip, Grid, Typography } from '@mui/joy'
|
import { Avatar, Box, Chip, Grid, Typography } from '@mui/joy'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||||
import { getPriorityColor, TASK_COLOR } from '../../utils/Colors'
|
import { getPriorityColor, TASK_COLOR } from '../../utils/Colors'
|
||||||
@@ -13,7 +13,7 @@ const getAssigneeColor = (assignee, userProfile) => {
|
|||||||
? TASK_COLOR.ASSIGNED_TO_ME
|
? TASK_COLOR.ASSIGNED_TO_ME
|
||||||
: TASK_COLOR.ASSIGNED_TO_OTHER
|
: TASK_COLOR.ASSIGNED_TO_OTHER
|
||||||
}
|
}
|
||||||
const CalendarView = ({ chores }) => {
|
const CalendarCard = ({ chores }) => {
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
|
|
||||||
const [selectedDate, setSeletedDate] = useState(null)
|
const [selectedDate, setSeletedDate] = useState(null)
|
||||||
@@ -23,6 +23,33 @@ const CalendarView = ({ chores }) => {
|
|||||||
const { data: circleMembersData } = useCircleMembers()
|
const { data: circleMembersData } = useCircleMembers()
|
||||||
const circleMembers = circleMembersData?.res || []
|
const circleMembers = circleMembersData?.res || []
|
||||||
|
|
||||||
|
// Auto-update selected calendar date when day changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedDate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const tomorrow = new Date(now)
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||||
|
tomorrow.setHours(0, 0, 0, 0) // Set to start of tomorrow
|
||||||
|
|
||||||
|
const msUntilTomorrow = tomorrow.getTime() - now.getTime()
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
const today = new Date()
|
||||||
|
const yesterday = new Date(today)
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1)
|
||||||
|
|
||||||
|
// Check if selected date is now yesterday and update to today
|
||||||
|
if (selectedDate.toDateString() === yesterday.toDateString()) {
|
||||||
|
setSeletedDate(today)
|
||||||
|
}
|
||||||
|
}, msUntilTomorrow)
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [selectedDate])
|
||||||
|
|
||||||
// Helper function to get assignee display name
|
// Helper function to get assignee display name
|
||||||
const getAssigneeName = assignedTo => {
|
const getAssigneeName = assignedTo => {
|
||||||
if (assignedTo === userProfile.id) {
|
if (assignedTo === userProfile.id) {
|
||||||
@@ -307,4 +334,4 @@ const CalendarView = ({ chores }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CalendarView
|
export default CalendarCard
|
||||||
@@ -118,6 +118,7 @@ export const parseLabels = (inputSentence, userLabels) => {
|
|||||||
|
|
||||||
export const parseRepeatV2 = inputSentence => {
|
export const parseRepeatV2 = inputSentence => {
|
||||||
const sentence = inputSentence.toLowerCase()
|
const sentence = inputSentence.toLowerCase()
|
||||||
|
const lowerInputSentence = inputSentence.toLowerCase()
|
||||||
const result = {
|
const result = {
|
||||||
frequency: 1,
|
frequency: 1,
|
||||||
frequencyType: null,
|
frequencyType: null,
|
||||||
@@ -232,8 +233,8 @@ export const parseRepeatV2 = inputSentence => {
|
|||||||
highlight: [
|
highlight: [
|
||||||
{
|
{
|
||||||
text: pattern.name,
|
text: pattern.name,
|
||||||
start: inputSentence.indexOf(match[0]),
|
start: lowerInputSentence.indexOf(match[0]),
|
||||||
end: inputSentence.indexOf(match[0]) + match[0].length,
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
@@ -250,8 +251,8 @@ export const parseRepeatV2 = inputSentence => {
|
|||||||
highlight: [
|
highlight: [
|
||||||
{
|
{
|
||||||
text: pattern.name,
|
text: pattern.name,
|
||||||
start: inputSentence.indexOf(match[0]),
|
start: lowerInputSentence.indexOf(match[0]),
|
||||||
end: inputSentence.indexOf(match[0]) + match[0].length,
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
@@ -275,8 +276,8 @@ export const parseRepeatV2 = inputSentence => {
|
|||||||
highlight: [
|
highlight: [
|
||||||
{
|
{
|
||||||
text: pattern.name,
|
text: pattern.name,
|
||||||
start: inputSentence.indexOf(match[0]),
|
start: lowerInputSentence.indexOf(match[0]),
|
||||||
end: inputSentence.indexOf(match[0]) + match[0].length,
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
@@ -299,8 +300,8 @@ export const parseRepeatV2 = inputSentence => {
|
|||||||
highlight: [
|
highlight: [
|
||||||
{
|
{
|
||||||
text: pattern.name,
|
text: pattern.name,
|
||||||
start: inputSentence.indexOf(match[0]),
|
start: lowerInputSentence.indexOf(match[0]),
|
||||||
end: inputSentence.indexOf(match[0]) + match[0].length,
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
@@ -315,8 +316,8 @@ export const parseRepeatV2 = inputSentence => {
|
|||||||
highlight: [
|
highlight: [
|
||||||
{
|
{
|
||||||
text: pattern.name,
|
text: pattern.name,
|
||||||
start: inputSentence.indexOf(match[0]),
|
start: lowerInputSentence.indexOf(match[0]),
|
||||||
end: inputSentence.indexOf(match[0]) + match[0].length,
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
@@ -332,8 +333,8 @@ export const parseRepeatV2 = inputSentence => {
|
|||||||
highlight: [
|
highlight: [
|
||||||
{
|
{
|
||||||
text: pattern.name,
|
text: pattern.name,
|
||||||
start: inputSentence.indexOf(match[0]),
|
start: lowerInputSentence.indexOf(match[0]),
|
||||||
end: inputSentence.indexOf(match[0]) + match[0].length,
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
@@ -351,8 +352,8 @@ export const parseRepeatV2 = inputSentence => {
|
|||||||
highlight: [
|
highlight: [
|
||||||
{
|
{
|
||||||
text: pattern.name,
|
text: pattern.name,
|
||||||
start: inputSentence.indexOf(match[0]),
|
start: lowerInputSentence.indexOf(match[0]),
|
||||||
end: inputSentence.indexOf(match[0]) + match[0].length,
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
@@ -367,8 +368,8 @@ export const parseRepeatV2 = inputSentence => {
|
|||||||
highlight: [
|
highlight: [
|
||||||
{
|
{
|
||||||
text: pattern.name,
|
text: pattern.name,
|
||||||
start: inputSentence.indexOf(match[0]),
|
start: lowerInputSentence.indexOf(match[0]),
|
||||||
end: inputSentence.indexOf(match[0]) + match[0].length,
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { version } from '../../../package.json'
|
import { version } from '../../../package.json'
|
||||||
import UserProfileAvatar from '../../components/UserProfileAvatar'
|
import UserProfileAvatar from '../../components/UserProfileAvatar'
|
||||||
import NavBarLink from './NavBarLink'
|
import NavBarLink from './NavBarLink'
|
||||||
@@ -97,6 +97,7 @@ const NavBar = () => {
|
|||||||
() => setDrawerOpen(false),
|
() => setDrawerOpen(false),
|
||||||
]
|
]
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
SafeArea.getSafeAreaInsets().then(data => {
|
SafeArea.getSafeAreaInsets().then(data => {
|
||||||
const { insets } = data
|
const { insets } = data
|
||||||
@@ -150,7 +151,8 @@ const NavBar = () => {
|
|||||||
backgroundColor: 'var(--joy-palette-background-body)',
|
backgroundColor: 'var(--joy-palette-background-body)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{['/chores', '/'].includes(location.pathname) ? (
|
{['/chores', '/'].includes(location.pathname) &&
|
||||||
|
!searchParams.get('filter') ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
size='md'
|
size='md'
|
||||||
variant='plain'
|
variant='plain'
|
||||||
@@ -159,7 +161,24 @@ const NavBar = () => {
|
|||||||
<MenuRounded />
|
<MenuRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
) : (
|
) : (
|
||||||
<IconButton size='md' variant='plain' onClick={() => navigate(-1)}>
|
<IconButton
|
||||||
|
size='md'
|
||||||
|
variant='plain'
|
||||||
|
onClick={() => {
|
||||||
|
if (location.pathname === '/chores') {
|
||||||
|
// Navigate back to calendar view
|
||||||
|
navigate('/')
|
||||||
|
} else {
|
||||||
|
// Default back navigation
|
||||||
|
navigate(-1)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
searchParams.get('from') === 'calendar'
|
||||||
|
? 'Back to Calendar'
|
||||||
|
: 'Back'
|
||||||
|
}
|
||||||
|
>
|
||||||
<ArrowBack />
|
<ArrowBack />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
@@ -192,9 +211,18 @@ const NavBar = () => {
|
|||||||
</div> */}
|
</div> */}
|
||||||
<List
|
<List
|
||||||
// sx={{ p: 2, height: 'min-content' }}
|
// sx={{ p: 2, height: 'min-content' }}
|
||||||
|
|
||||||
size='md'
|
size='md'
|
||||||
onClick={openDrawer}
|
onClick={openDrawer}
|
||||||
sx={{ borderRadius: 4, width: '100%', padding: 1 }}
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
width: '100%',
|
||||||
|
padding: 1,
|
||||||
|
paddingTop:
|
||||||
|
Capacitor.getPlatform() === 'android'
|
||||||
|
? `calc(var(--safe-area-inset-top, 0px))`
|
||||||
|
: '',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{links.map((link, index) => (
|
{links.map((link, index) => (
|
||||||
<NavBarLink key={index} link={link} />
|
<NavBarLink key={index} link={link} />
|
||||||
|
|||||||
Reference in New Issue
Block a user