From 43decb21f459380f3a59c4b6a318292e8d71abbe Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Thu, 11 Sep 2025 01:17:27 -0400 Subject: [PATCH] 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. --- android/app/build.gradle | 4 +- ios/App/App.xcodeproj/project.pbxproj | 8 +- ios/App/Podfile.lock | 146 ++--- src/App.jsx | 31 +- src/components/NotificationTemplate.jsx | 12 +- src/contexts/RouterContext.jsx | 69 ++- src/hooks/useAcknowledgmentModal.js | 39 ++ src/utils/TokenManager.jsx | 20 +- src/views/Authorization/LoginView.jsx | 1 + src/views/ChoreEdit/ChoreEdit.jsx | 54 +- src/views/Chores/ActivitesCard.jsx | 10 +- src/views/Chores/MyChores.jsx | 223 +++++++- src/views/Chores/Sidepanel.jsx | 4 +- src/views/Circles/JoinCircle.jsx | 29 +- src/views/Labels/LabelView.jsx | 2 +- src/views/Landing/HomeHero.jsx | 2 +- src/views/Landing/PricingSection.jsx | 103 +--- .../Modals/Inputs/AcknowledgmentModal.jsx | 108 ++++ src/views/Settings/APITokenSettings.jsx | 265 ++++----- src/views/Settings/AccountSettings.jsx | 308 +++++++++++ src/views/Settings/AdvancedSettings.jsx | 163 ++++++ src/views/Settings/CircleSettings.jsx | 489 +++++++++++++++++ src/views/Settings/MFASettings.jsx | 515 +++++++++--------- src/views/Settings/NotificationSettings.jsx | 12 + src/views/Settings/ProfileSettings.jsx | 308 +++++------ src/views/Settings/Settings.jsx | 73 ++- src/views/Settings/SettingsLayout.jsx | 21 + src/views/Settings/SettingsOverview.jsx | 335 ++++++------ src/views/Settings/SettingsRoutes.jsx | 7 + src/views/Settings/SidepanelSettings.jsx | 251 +++++---- src/views/Settings/StorageSettings.jsx | 220 ++++---- src/views/Settings/ThemeSettings.jsx | 19 + src/views/Terms/TermsView.jsx | 362 ++++++------ src/views/Things/ThingsView.jsx | 2 +- src/views/components/AddTaskModal.jsx | 5 +- .../{CalendarView.jsx => CalendarCard.jsx} | 33 +- src/views/components/CustomParsers.js | 33 +- src/views/components/NavBar.jsx | 36 +- 38 files changed, 2934 insertions(+), 1388 deletions(-) create mode 100644 src/hooks/useAcknowledgmentModal.js create mode 100644 src/views/Modals/Inputs/AcknowledgmentModal.jsx create mode 100644 src/views/Settings/AccountSettings.jsx create mode 100644 src/views/Settings/AdvancedSettings.jsx create mode 100644 src/views/Settings/CircleSettings.jsx create mode 100644 src/views/Settings/NotificationSettings.jsx create mode 100644 src/views/Settings/SettingsLayout.jsx create mode 100644 src/views/Settings/SettingsRoutes.jsx create mode 100644 src/views/Settings/ThemeSettings.jsx rename src/views/components/{CalendarView.jsx => CalendarCard.jsx} (91%) diff --git a/android/app/build.gradle b/android/app/build.gradle index dfce19f..2832749 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.donetick.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName "1.0" + versionCode 10 + versionName "1.0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index e23c984..26d52b8 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -355,12 +355,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 6UJJ78R3BS; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -377,12 +377,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 6UJJ78R3BS; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index fa46640..bf7dcbe 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -1,58 +1,58 @@ PODS: - Alamofire (5.10.2) - - AppAuth (1.7.6): - - AppAuth/Core (= 1.7.6) - - AppAuth/ExternalUserAgent (= 1.7.6) - - AppAuth/Core (1.7.6) - - AppAuth/ExternalUserAgent (1.7.6): + - AppAuth (2.0.0): + - AppAuth/Core (= 2.0.0) + - AppAuth/ExternalUserAgent (= 2.0.0) + - AppAuth/Core (2.0.0) + - AppAuth/ExternalUserAgent (2.0.0): - AppAuth/Core - AppCheckCore (11.2.0): - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - PromisesObjC (~> 2.4) - - Capacitor (7.4.0): + - Capacitor (7.4.3): - CapacitorCordova - - CapacitorApp (7.0.1): + - CapacitorApp (7.0.2): - Capacitor - CapacitorBrowser (7.0.2): - Capacitor - - CapacitorCommunitySqlite (7.0.0): + - CapacitorCommunitySqlite (7.0.1): - Capacitor - SQLCipher - ZIPFoundation - - CapacitorCordova (7.4.0) - - CapacitorDevice (7.0.1): + - CapacitorCordova (7.4.3) + - CapacitorDevice (7.0.2): - Capacitor - - CapacitorLocalNotifications (7.0.1): + - CapacitorLocalNotifications (7.0.2): - Capacitor - - CapacitorNetwork (7.0.1): + - CapacitorNetwork (7.0.2): - Capacitor - CapacitorPluginSafeArea (4.0.0): - Capacitor - - CapacitorPreferences (7.0.1): + - CapacitorPreferences (7.0.2): - Capacitor - - CapacitorPushNotifications (7.0.1): + - CapacitorPushNotifications (7.0.2): - Capacitor - - CapacitorStatusBar (7.0.1): + - CapacitorStatusBar (7.0.2): - Capacitor - - CapgoCapacitorSocialLogin (7.5.3): + - CapgoCapacitorSocialLogin (7.11.2): - Alamofire (~> 5.10.2) - Capacitor - - FBSDKCoreKit (= 17.4.0) - - FBSDKLoginKit (= 17.4.0) - - GoogleSignIn (~> 8.0.0) - - FBAEMKit (17.4.0): - - FBSDKCoreKit_Basics (= 17.4.0) - - FBSDKCoreKit (17.4.0): - - FBAEMKit (= 17.4.0) - - FBSDKCoreKit_Basics (= 17.4.0) - - FBSDKCoreKit_Basics (17.4.0) - - FBSDKLoginKit (17.4.0): - - FBSDKCoreKit (= 17.4.0) - - GoogleSignIn (8.0.0): - - AppAuth (< 2.0, >= 1.7.3) + - FBSDKCoreKit (= 18.0.0) + - FBSDKLoginKit (= 18.0.0) + - GoogleSignIn (~> 9.0.0) + - FBAEMKit (18.0.0): + - FBSDKCoreKit_Basics (= 18.0.0) + - FBSDKCoreKit (18.0.0): + - FBAEMKit (= 18.0.0) + - FBSDKCoreKit_Basics (= 18.0.0) + - FBSDKCoreKit_Basics (18.0.0) + - FBSDKLoginKit (18.0.0): + - FBSDKCoreKit (= 18.0.0) + - GoogleSignIn (9.0.0): + - AppAuth (~> 2.0) - AppCheckCore (~> 11.0) - - GTMAppAuth (< 5.0, >= 4.1.1) + - GTMAppAuth (~> 5.0) - GTMSessionFetcher/Core (~> 3.3) - GoogleUtilities/Environment (8.1.0): - GoogleUtilities/Privacy @@ -63,29 +63,29 @@ PODS: - GoogleUtilities/UserDefaults (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GTMAppAuth (4.1.1): - - AppAuth/Core (~> 1.7) + - GTMAppAuth (5.0.0): + - AppAuth/Core (~> 2.0) - GTMSessionFetcher/Core (< 4.0, >= 3.3) - GTMSessionFetcher/Core (3.5.0) - PromisesObjC (2.4.0) - - PurchasesHybridCommon (16.2.0): - - RevenueCat (= 5.34.0) - - PurchasesHybridCommonUI (16.2.0): - - PurchasesHybridCommon (= 16.2.0) - - RevenueCatUI (= 5.34.0) - - RevenueCat (5.34.0) - - RevenuecatPurchasesCapacitor (11.1.1): + - PurchasesHybridCommon (17.0.0): + - RevenueCat (= 5.35.1) + - PurchasesHybridCommonUI (17.0.0): + - PurchasesHybridCommon (= 17.0.0) + - RevenueCatUI (= 5.35.1) + - RevenueCat (5.35.1) + - RevenuecatPurchasesCapacitor (11.1.2): - Capacitor - - PurchasesHybridCommon (= 16.2.0) - - RevenuecatPurchasesCapacitorUi (11.1.1): + - PurchasesHybridCommon (= 17.0.0) + - RevenuecatPurchasesCapacitorUi (11.1.2): - Capacitor - - PurchasesHybridCommonUI (= 16.2.0) - - RevenueCatUI (5.34.0): - - RevenueCat (= 5.34.0) - - SQLCipher (4.7.0): - - SQLCipher/standard (= 4.7.0) - - SQLCipher/common (4.7.0) - - SQLCipher/standard (4.7.0): + - PurchasesHybridCommonUI (= 17.0.0) + - RevenueCatUI (5.35.1): + - RevenueCat (= 5.35.1) + - SQLCipher (4.10.0): + - SQLCipher/standard (= 4.10.0) + - SQLCipher/common (4.10.0) + - SQLCipher/standard (4.10.0): - SQLCipher/common - ZIPFoundation (0.9.19) @@ -161,37 +161,37 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 - AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 + AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063 AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f - Capacitor: 10feab36396883c55373328124a677f118cd4b5f - CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9 + Capacitor: 28d6c01026a9a3f7156529498ec1f389a2a28dbc + CapacitorApp: 1f6922c9c5c8b1c538d7fbe92ebe44a81b34bed3 CapacitorBrowser: 22541e48442de44dc629c214388290d6eecc6ae9 - CapacitorCommunitySqlite: b11e556cf5d149f9e0b6fbbb47959a6cd718c3d0 - CapacitorCordova: 723e017d8a80ab2ede9959fbe37556c35ac860d5 - CapacitorDevice: c6f6d587dd310527f8a48bf09c4e7b4a4cf14329 - CapacitorLocalNotifications: c2212755b33d2513b8bb325096ce3a64d4039ae3 - CapacitorNetwork: 15cb4385f0913a8ceb5e9a4d7af1ec554bdb8de8 + CapacitorCommunitySqlite: 8b2c6bab33e3519280811d481f8bd0fa90343e1b + CapacitorCordova: 435121e81a2df4d0034f0fb11fcefab5104cfdb5 + CapacitorDevice: 81ae78d5d1942707caad79276badd458bf6ec603 + CapacitorLocalNotifications: 665188ae8accd40806129073896fb2b39322d858 + CapacitorNetwork: 695069886b3c5ed514db69aa3d026b8dc3c03a6b CapacitorPluginSafeArea: 22031c3436269ca80fac90ec2c94bc7c1e59a81d - CapacitorPreferences: 6c98117d4d7508034a4af9db64d6b26fc75d7b94 - CapacitorPushNotifications: 6a2794788c583dd89215f1805ca4bced1b13dbdf - CapacitorStatusBar: 6e7af040d8fc4dd655999819625cae9c2d74c36f - CapgoCapacitorSocialLogin: 001d85f081a5167bde4ec4456353dbe4c03d4cd2 - FBAEMKit: 58cb5f302cdd715a56d4c1d0dfdd2e423ac1421a - FBSDKCoreKit: 94d7461d0cecf441b1ba7c41acfff41daa8ccd41 - FBSDKCoreKit_Basics: 151b43db8b834d3f0e02f95d36a44ffd36265e45 - FBSDKLoginKit: 5c1cd53c91a2282b3a4fe6e6d3dcf2b8b0d33d55 - GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4 + CapacitorPreferences: 65107ed7437d96ee72583df5763985e3c0ff2bc2 + CapacitorPushNotifications: 7c0659b349b149ee3936a682da31a9d269de9582 + CapacitorStatusBar: e04d05e121d5a5979c29eb4249186a4e4a84cacb + CapgoCapacitorSocialLogin: a320106e1d032da88576cf8b31a336e971f5acad + FBAEMKit: e34530df538b8eb8aeb53c35867715ba6c63ef0c + FBSDKCoreKit: d3f479a69127acebb1c6aad91c1a33907bcf6c2f + FBSDKCoreKit_Basics: 017b6dc2a1862024815a8229e75661e627ac1e29 + FBSDKLoginKit: 5875762d1fe09ddcb05d03365d4f5dc34413843d + GoogleSignIn: c7f09cfbc85a1abf69187be091997c317cc33b77 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de + GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238 GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - PurchasesHybridCommon: 55e97b4e090015ed138988e6559def2bd4c50ebe - PurchasesHybridCommonUI: 25c66f6ddcf8856ecbcffee897b2525eb272a5e7 - RevenueCat: eb2aa042789d9c99ad5172bd96e28b96286d6ada - RevenuecatPurchasesCapacitor: 420b762736c2842bfb64436b7e0b88d2f672c943 - RevenuecatPurchasesCapacitorUi: 0cf343ae4e1c8b30b294c971a50827316b210a63 - RevenueCatUI: 03025a80b64ce8ff8e43f47a7eb9dbbb6b4f486e - SQLCipher: ba9d0076041ed767c5bd3d3f77098318d04a403c + PurchasesHybridCommon: 34d2f73712f4c02f2963fcfb62d43757a9136860 + PurchasesHybridCommonUI: 16e162423c0010f9a6f2c1d6505dcc075f49dcbe + RevenueCat: 47d52620419dcf3dce4e724eade9d390e16ab4b0 + RevenuecatPurchasesCapacitor: 51cd81cec7db693a2552b45c36261d849e1cf574 + RevenuecatPurchasesCapacitorUi: 267758d61aba42a0a3f035e6ba3c4630112357e4 + RevenueCatUI: 3c72ef61b54c3f4787afab6a3b0f00cbec192063 + SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c PODFILE CHECKSUM: 289063dee9cd9d5cc8abd18e13b4153b38864021 diff --git a/src/App.jsx b/src/App.jsx index a47bc51..bbae0a9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -23,6 +23,21 @@ const remove = className => { // TODO: Update the interval to at 60 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 { 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 diff --git a/src/components/NotificationTemplate.jsx b/src/components/NotificationTemplate.jsx index 5ff23cc..0eeec00 100644 --- a/src/components/NotificationTemplate.jsx +++ b/src/components/NotificationTemplate.jsx @@ -702,7 +702,15 @@ const NotificationTemplate = ({ {showSaveDefault && ( - + )} diff --git a/src/contexts/RouterContext.jsx b/src/contexts/RouterContext.jsx index 5f9f76b..2c27da5 100644 --- a/src/contexts/RouterContext.jsx +++ b/src/contexts/RouterContext.jsx @@ -1,8 +1,14 @@ import App from '@/App' import ChoreEdit from '@/views/ChoreEdit/ChoreEdit' 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 SettingsOverview from '@/views/Settings/SettingsOverview' +import SettingsRoutes from '@/views/Settings/SettingsRoutes' +import ThemeSettings from '@/views/Settings/ThemeSettings' import { Capacitor } from '@capacitor/core' import { RouterProvider, createBrowserRouter } from 'react-router-dom' import AuthenticationLoading from '../views/Authorization/Authenticating' @@ -15,12 +21,18 @@ import ChoreView from '../views/ChoreEdit/ChoreView' import ArchivedTasks from '../views/Chores/ArchivedTasks' import MyChores from '../views/Chores/MyChores' import JoinCircleView from '../views/Circles/JoinCircle' +import NotFound from '../views/components/NotFound' import ChoreHistory from '../views/History/ChoreHistory' import LabelView from '../views/Labels/LabelView' import Landing from '../views/Landing/Landing' import PaymentCancelledView from '../views/Payments/PaymentFailView' import PaymentSuccessView from '../views/Payments/PaymentSuccessView' 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 TestView from '../views/TestView/Test' import ThingsHistory from '../views/Things/ThingsHistory' @@ -28,7 +40,6 @@ import ThingsView from '../views/Things/ThingsView' import TimerDetails from '../views/Timer/TimerDetails' import UserActivities from '../views/User/UserActivities' import UserPoints from '../views/User/UserPoints' -import NotFound from '../views/components/NotFound' const getMainRoute = () => { if ( import.meta.env.VITE_IS_LANDING_DEFAULT === 'true' && @@ -50,11 +61,57 @@ const Router = createBrowserRouter([ }, { path: '/settings', - element: , - }, - { - path: '/settings/detailed', - element: , + element: , + children: [ + { + index: true, + element: , + }, + { + path: 'detailed', + element: , + }, + { + path: 'profile', + element: , + }, + { + path: 'circle', + element: , + }, + { + path: 'account', + element: , + }, + { + path: 'notifications', + element: , + }, + { + path: 'mfa', + element: , + }, + { + path: 'apitokens', + element: , + }, + { + path: 'storage', + element: , + }, + { + path: 'sidepanel', + element: , + }, + { + path: 'theme', + element: , + }, + { + path: 'advanced', + element: , + }, + ], }, { path: '/chores', diff --git a/src/hooks/useAcknowledgmentModal.js b/src/hooks/useAcknowledgmentModal.js new file mode 100644 index 0000000..5482c9f --- /dev/null +++ b/src/hooks/useAcknowledgmentModal.js @@ -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 \ No newline at end of file diff --git a/src/utils/TokenManager.jsx b/src/utils/TokenManager.jsx index c0449de..01d2ecb 100644 --- a/src/utils/TokenManager.jsx +++ b/src/utils/TokenManager.jsx @@ -10,21 +10,31 @@ class ApiManager { constructor() { this.customServerURL = `${API_URL}/api/v1` this.initialized = false + this.initPromise = null this.navigateToLogin = () => {} } async init() { - if (this.initialized) { - return + if (this.initPromise) { + return this.initPromise } + if (this.initialized) { + return Promise.resolve() + } + + this.initPromise = this._doInit() + return this.initPromise + } + + async _doInit() { const { value: serverURL } = await Preferences.get({ key: 'customServerUrl', }) this.customServerURL = `${serverURL || API_URL}/api/v1` - await localStore.initDatabase() this.initialized = true + await localStore.initDatabase() } getApiURL() { @@ -47,6 +57,8 @@ export const getAssetURL = path => { return `${baseURL}/assets/${path}` } export async function UploadFile(url, options) { + await apiManager.init() + if (!isTokenValid()) { Cookies.set('ca_redirect', window.location.pathname) window.location.href = '/login' @@ -65,6 +77,7 @@ export async function UploadFile(url, options) { } export async function Fetch(url, options) { + await apiManager.init() if (!isTokenValid()) { Cookies.set('ca_redirect', window.location.pathname) if (window.location.pathname !== '/login') { @@ -148,6 +161,7 @@ export const isTokenValid = () => { } return false } + return false } export const refreshAccessToken = () => { diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx index 5a5a6ba..b2ee9f8 100644 --- a/src/views/Authorization/LoginView.jsx +++ b/src/views/Authorization/LoginView.jsx @@ -425,6 +425,7 @@ const LoginView = () => { label='Password' type='password' id='password' + autoComplete='password' value={password} onChange={e => { setPassword(e.target.value) diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index 20988f2..34c8f54 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -1,4 +1,4 @@ -import { Add, HorizontalRule } from '@mui/icons-material' +import { Add, HorizontalRule, Save } from '@mui/icons-material' import { Box, Button, @@ -102,6 +102,10 @@ const ChoreEdit = () => { const [errors, setErrors] = useState({}) const [attemptToSave, setAttemptToSave] = 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 updateChoreMutation = useUpdateChore() @@ -272,6 +276,23 @@ const ChoreEdit = () => { 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(() => { if (isChoreLoading === false && choreData && choreId) { @@ -1091,7 +1112,9 @@ const ChoreEdit = () => { name='isPrivate' value={isPrivate} onChange={event => { - setIsPrivate(event.target.value === 'true' ? true : false) + const newValue = event.target.value === 'true' ? true : false + setIsPrivate(newValue) + setShowSavePrivacyDefault(true) }} sx={{ '& > div': { py: 1 }, @@ -1108,6 +1131,33 @@ const ChoreEdit = () => { + + {showSavePrivacyDefault && ( + + + + )} diff --git a/src/views/Chores/ActivitesCard.jsx b/src/views/Chores/ActivitesCard.jsx index 156f5b7..92d6f19 100644 --- a/src/views/Chores/ActivitesCard.jsx +++ b/src/views/Chores/ActivitesCard.jsx @@ -254,12 +254,11 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => { p: 2, display: 'flex', flexDirection: 'column', - alignItems: 'center', - justifyContent: 'space-between', boxShadow: 'sm', borderRadius: 20, + width: '310px', minHeight: 300, - width: '315px', + maxHeight: 400, mb: 1, }} > @@ -311,12 +310,11 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => { p: 2, display: 'flex', flexDirection: 'column', - alignItems: 'center', - justifyContent: 'space-between', boxShadow: 'sm', borderRadius: 20, - // width: '290px', + width: '310px', minHeight: 300, + maxHeight: 400, mb: 1, }} > diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index 4373800..206b9cb 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -38,9 +38,10 @@ import { } from '@mui/joy' import Fuse from 'fuse.js' 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 { useNotification } from '../../service/NotificationProvider' +import { TASK_COLOR } from '../../utils/Colors' import { ArchiveChore } from '../../utils/Fetcher' import Priorities from '../../utils/Priorities' import LoadingComponent from '../components/Loading' @@ -102,6 +103,7 @@ const MyChores = () => { const [selectedCalendarDate, setSelectedCalendarDate] = useState(new Date()) const menuRef = useRef(null) const Navigate = useNavigate() + const [searchParams] = useSearchParams() const { data: userLabels, isLoading: userLabelsLoading } = useLabels() const { data: choresData, @@ -126,14 +128,14 @@ const MyChores = () => { ) { setPerformers(membersData.res) let sortedChores = choresData.res.sort(ChoreSorter) - + // Filter chores based on impersonated user if (impersonatedUser) { - sortedChores = sortedChores.filter(chore => - chore.assignedTo === impersonatedUser.userId + sortedChores = sortedChores.filter( + chore => chore.assignedTo === impersonatedUser.userId, ) } - + setChores(sortedChores) setFilteredChores(sortedChores) const sections = ChoresGrouper( @@ -400,6 +402,72 @@ const MyChores = () => { document.removeEventListener('keyup', handleKeyUp) } }, [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 => { setSelectedChoreSection(value) localStorage.setItem('selectedChoreSection', value) @@ -454,16 +522,16 @@ const MyChores = () => { if (searchTerm?.length > 0 || searchFilter !== 'All') { return filteredChores } - + let choresToFilter = chores - + // Filter by impersonated user first if set if (impersonatedUser) { - choresToFilter = choresToFilter.filter(chore => - chore.assignedTo === impersonatedUser.userId + choresToFilter = choresToFilter.filter( + chore => chore.assignedTo === impersonatedUser.userId, ) } - + return choresToFilter.filter(ChoreFilters(userProfile)[selectedChoreFilter]) } @@ -480,14 +548,14 @@ const MyChores = () => { const updateChores = newChore => { let newChores = [...chores, newChore] - + // Filter chores based on impersonated user if (impersonatedUser) { - newChores = newChores.filter(chore => - chore.assignedTo === impersonatedUser.userId + newChores = newChores.filter( + chore => chore.assignedTo === impersonatedUser.userId, ) } - + setChores(newChores) setFilteredChores(newChores) setChoreSections( @@ -1658,6 +1726,128 @@ const MyChores = () => { )} {viewMode === 'calendar' && ( <> + {/* Summary Chips when no date selected */} + + {FILTERS['Overdue'](getFilteredChores()).length > 0 && ( + { + // 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={ + + {FILTERS['Overdue'](getFilteredChores()).length} + + } + > + Overdue + + )} + + {FILTERS['No Due Date'](getFilteredChores()).length > 0 && ( + { + // 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={ + + {FILTERS['No Due Date'](getFilteredChores()).length} + + } + > + Unplanned + + )} + + {FILTERS['Pending Approval'](getFilteredChores()).length > 0 && ( + { + // 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={ + + {FILTERS['Pending Approval'](getFilteredChores()).length} + + } + > + Pending Approval + + )} + {/* Calendar Monthly View */} {isLargeScreen ? ( @@ -1951,6 +2141,11 @@ const FILTERS = { return chore.nextDueDate === null }) }, + 'Pending Approval': function (chores) { + return chores.filter(chore => { + return chore.status === 3 + }) + }, } export default MyChores diff --git a/src/views/Chores/Sidepanel.jsx b/src/views/Chores/Sidepanel.jsx index d4cd990..b551f1b 100644 --- a/src/views/Chores/Sidepanel.jsx +++ b/src/views/Chores/Sidepanel.jsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react' import { useChoresHistory } from '../../queries/ChoreQueries' import { ChoresGrouper } from '../../utils/Chores' import { getSidepanelConfig } from '../../utils/SidepanelConfig' -import CalendarView from '../components/CalendarView' +import CalendarCard from '../components/CalendarCard' import ActivitiesCard from './ActivitesCard' import TasksByAssigneeCard from './TasksByAssigneeCard' import UserSwitcher from './UserSwitcher' @@ -78,7 +78,7 @@ const Sidepanel = ({ chores }) => { - + ) diff --git a/src/views/Circles/JoinCircle.jsx b/src/views/Circles/JoinCircle.jsx index 76f5101..9026b02 100644 --- a/src/views/Circles/JoinCircle.jsx +++ b/src/views/Circles/JoinCircle.jsx @@ -2,11 +2,19 @@ import { Box, Container, Input, Sheet, Typography } from '@mui/joy' import Logo from '../../Logo' import { Button } from '@mui/joy' +import { useState } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' +import useAcknowledgmentModal from '../../hooks/useAcknowledgmentModal' import { useUserProfile } from '../../queries/UserQueries' +import { useNotification } from '../../service/NotificationProvider' import { JoinCircle } from '../../utils/Fetcher' +import AcknowledgmentModal from '../Modals/Inputs/AcknowledgmentModal' + const JoinCircleView = () => { const { data: userProfile } = useUserProfile() + const { showError } = useNotification() + const { ackModalConfig, showAcknowledgment } = useAcknowledgmentModal() + const [isJoining, setIsJoining] = useState(false) let [searchParams, setSearchParams] = useSearchParams() const navigate = useNavigate() @@ -79,25 +87,31 @@ const JoinCircleView = () => { fullWidth size='lg' sx={{ mt: 3, mb: 2 }} + disabled={isJoining} onClick={() => { + setIsJoining(true) JoinCircle(code).then(resp => { if (resp.ok) { - alert( - 'Joined circle successfully, wait for the circle owner to accept your request.', + showAcknowledgment( + '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 { + setIsJoining(false) if (resp.status === 409) { - alert('You are already a member of this circle') + showError('You are already a member of this circle') } else { - alert('Failed to join circle') + showError('Failed to join circle') } - navigate('/chores') + navigate('/') } }) }} > - Join Circle + {isJoining ? 'Joining...' : 'Join Circle'} - - - - - ))} - - - {/* Here start the test */} ) } diff --git a/src/views/Modals/Inputs/AcknowledgmentModal.jsx b/src/views/Modals/Inputs/AcknowledgmentModal.jsx new file mode 100644 index 0000000..234483e --- /dev/null +++ b/src/views/Modals/Inputs/AcknowledgmentModal.jsx @@ -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 ( + + + + {config?.title} + + + + {config?.message} + + + + + + + + ) +} + +export default AcknowledgmentModal diff --git a/src/views/Settings/APITokenSettings.jsx b/src/views/Settings/APITokenSettings.jsx index 899cc6b..603d9ce 100644 --- a/src/views/Settings/APITokenSettings.jsx +++ b/src/views/Settings/APITokenSettings.jsx @@ -22,6 +22,7 @@ import { import { isPlusAccount } from '../../utils/Helpers' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import TextModal from '../Modals/Inputs/TextModal' +import SettingsLayout from './SettingsLayout' const APITokenSettings = () => { const { data: userProfile } = useUserProfile() @@ -77,142 +78,144 @@ const APITokenSettings = () => { } return ( -
- Access Token - - - Create token to use with the API to update things that trigger task or - chores - - {!isPlusAccount(userProfile) && ( - <> - - Plus Feature - - - API tokens are not available in the Basic plan. Upgrade to Plus to - generate API tokens for integrating with external systems and - automating your tasks. - - - )} + +
+ Access Token + + + Create token to use with the API to update things that trigger task or + chores + + {!isPlusAccount(userProfile) && ( + <> + + Plus Feature + + + API tokens are not available in the Basic plan. Upgrade to Plus to + generate API tokens for integrating with external systems and + automating your tasks. + + + )} - {tokens.map(token => ( - - - - {token.name} - - {moment(token.createdAt).fromNow()}( - {moment(token.createdAt).format('lll')}) - - - - - - - - - {showTokenId === token?.id && ( - - { - navigator.clipboard.writeText(token.token) - showNotification({ - type: 'success', - message: 'Token copied to clipboard', - }) + {tokens.map(token => ( + + + + {token.name} + + {moment(token.createdAt).fromNow()}( + {moment(token.createdAt).format('lll')}) + + + + + + + - )} - - ))} + {showTokenId === token?.id && ( + + { + navigator.clipboard.writeText(token.token) + showNotification({ + type: 'success', + message: 'Token copied to clipboard', + }) + setShowTokenId(null) + }} + > + + + } + /> + + )} + + ))} - - { - setIsGetTokenNameModalOpen(false) - }} - okText={'Generate Token'} - onSave={handleSaveToken} - /> + + { + setIsGetTokenNameModalOpen(false) + }} + okText={'Generate Token'} + onSave={handleSaveToken} + /> - {/* Modals */} - {confirmModalConfig?.isOpen && ( - - )} -
+ {/* Modals */} + {confirmModalConfig?.isOpen && ( + + )} +
+ ) } diff --git a/src/views/Settings/AccountSettings.jsx b/src/views/Settings/AccountSettings.jsx new file mode 100644 index 0000000..ffaa904 --- /dev/null +++ b/src/views/Settings/AccountSettings.jsx @@ -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 ( + +
Loading...
+
+ ) + } + + return ( + +
+ + Change your account settings, type or update your password + + + Account Type : {getSubscriptionStatus()} + + {getSubscriptionDetails()} + + + + {userProfile?.subscription === 'active' && ( + + )} + + {import.meta.env.VITE_IS_SELF_HOSTED === 'true' && ( + + + Password : + + + + {changePasswordModal ? ( + { + 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} + + )} + + + + Danger Zone + + + Once you delete your account, there is no going back. Please be + certain. + + + +
+ + setSubscriptionModal(false)} + /> + + { + setUserDeletionModal(false) + if (success) { + showNotification({ + type: 'success', + message: 'Account deleted successfully', + }) + } + }} + userProfile={userProfile} + /> + + { + 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', + }) + } + }) + } + }} + /> +
+ ) +} + +export default AccountSettings \ No newline at end of file diff --git a/src/views/Settings/AdvancedSettings.jsx b/src/views/Settings/AdvancedSettings.jsx new file mode 100644 index 0000000..7200844 --- /dev/null +++ b/src/views/Settings/AdvancedSettings.jsx @@ -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 ( + +
Loading...
+
+ ) + } + + return ( + +
+ + Configure advanced features like webhooks and real-time updates for enhanced productivity. + + + {/* Webhook Settings - Only show for admins */} + {isAdmin && ( + <> + + Webhook Integration + + + 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. + + {!isPlusAccount(userProfile) && ( + + Webhook notifications are not available in the Basic plan. + Upgrade to Plus to receive real-time updates via webhooks. + + )} + + { + if (webhookURL === null) { + setWebhookURL('') + } else { + setWebhookURL(null) + } + }} + variant='soft' + label='Enable Webhook' + disabled={!isPlusAccount(userProfile)} + overlay + /> + + Enable webhook notifications for tasks and things updates.{' '} + {userProfile && !isPlusAccount(userProfile) && ( + + Plus Feature + + )} + + + + {webhookURL !== null && ( + + Webhook URL + setWebhookURL(e.target.value)} + size='lg' + sx={{ + width: '220px', + mb: 1, + }} + /> + {webhookError && ( + + {webhookError} + + )} + + + )} + + )} + + {/* Real-time Settings */} + + Real-time Updates + + + Configure how you receive live updates when tasks and activities change in your circle. + + +
+
+ ) +} + +export default AdvancedSettings \ No newline at end of file diff --git a/src/views/Settings/CircleSettings.jsx b/src/views/Settings/CircleSettings.jsx new file mode 100644 index 0000000..8819ff0 --- /dev/null +++ b/src/views/Settings/CircleSettings.jsx @@ -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 ( + + + + ) + } + + return ( + +
+ + 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. + + + {userCircles[0]?.userRole === 'member' + ? `You part of ${userCircles[0]?.name} ` + : `You circle code is:`} + + + + + {userCircles.length > 0 && userCircles[0]?.userRole === 'member' && ( + + )} + + + Circle Members + {circleMembers.map(member => ( + + + + + {member.displayName.charAt(0).toUpperCase() + + member.displayName.slice(1)} + {member.userId === userProfile.id ? '(You)' : ''}{' '} + + {' '} + {member.isActive ? member.role : 'Pending Approval'} + + + {member.isActive ? ( + + Joined on {moment(member.createdAt).format('MMM DD, YYYY')} + + ) : ( + + Request to join{' '} + {moment(member.updatedAt).format('MMM DD, YYYY')} + + )} + + + + {member.userId !== userProfile.id && isAdmin && ( + + )} + {isAdmin && + member.userId !== userProfile.id && + member.isActive && ( + + )} + + + + ))} + + + Circle Member Requests + + {lastRefresh && ( + + Last updated: {moment(lastRefresh).format('MMM DD, HH:mm')} + + )} + + + + + {circleMemberRequests.map(request => ( + + + {request.displayName} wants to join your circle. + + + + ))} + or + + + 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. + + + + Enter Circle code: + setCircleInviteCode(e.target.value)} + size='lg' + sx={{ + width: '220px', + mb: 1, + }} + /> + + +
+ + {confirmModalConfig?.isOpen && ( + + )} +
+ ) +} + +export default CircleSettings \ No newline at end of file diff --git a/src/views/Settings/MFASettings.jsx b/src/views/Settings/MFASettings.jsx index 355e596..d913c2a 100644 --- a/src/views/Settings/MFASettings.jsx +++ b/src/views/Settings/MFASettings.jsx @@ -4,8 +4,6 @@ import { Box, Button, Card, - CircularProgress, - Divider, Input, Modal, ModalClose, @@ -22,6 +20,8 @@ import { RegenerateBackupCodes, SetupMFA, } from '../../utils/Fetcher' +import LoadingComponent from '../components/Loading' +import SettingsLayout from './SettingsLayout' const MFASettings = () => { const [mfaEnabled, setMfaEnabled] = useState(false) @@ -202,69 +202,68 @@ const MFASettings = () => { } if (loading) { - return ( - - - - ) + return } return ( -
- Multi-Factor Authentication - - - Add an extra layer of security to your account with multi-factor - authentication (MFA). When enabled, you'll need to provide a - verification code from your authenticator app in addition to your - password when signing in. - + +
+ + Add an extra layer of security to your account with multi-factor + authentication (MFA). When enabled, you'll need to provide a + verification code from your authenticator app in addition to your + password when signing in. + - {success && ( - setSuccess('')}> - {success} - - )} + {success && ( + setSuccess('')}> + {success} + + )} - {error && ( - setError('')}> - {error} - - )} + {error && ( + setError('')}> + {error} + + )} - - - - - - - Two-Factor Authentication - - - {mfaEnabled - ? 'Your account is protected with 2FA' - : 'Secure your account with an authenticator app'} - + + + + + + + Two-Factor Authentication + + + {mfaEnabled + ? 'Your account is protected with 2FA' + : 'Secure your account with an authenticator app'} + + + + + {mfaEnabled ? ( + + ) : ( + + )} - - {mfaEnabled ? ( - - ) : ( - - )} - - - - {/* + + {/* {mfaEnabled && ( @@ -290,87 +289,194 @@ const MFASettings = () => { )} */} - {/* Setup MFA Modal */} - - - - - Set up Multi-Factor Authentication - + {/* Setup MFA Modal */} + + + + + Set up Multi-Factor Authentication + - {setupStep === 1 && setupData && ( - - - Step 1: Scan the QR code below with your - authenticator app (Google Authenticator, Authy, etc.) - - - - {qrCodeDataUrl || setupData.qrCode ? ( - MFA QR Code - ) : ( - - QR code could not be generated. Please try again or use the - manual entry key below. - - )} - - - - - Manual entry key: + {setupStep === 1 && setupData && ( + + + Step 1: Scan the QR code below with your + authenticator app (Google Authenticator, Authy, etc.) - + {qrCodeDataUrl || setupData.qrCode ? ( + MFA QR Code + ) : ( + + QR code could not be generated. Please try again or use + the manual entry key below. + + )} + + + - {setupData.secret} + + Manual entry key: + + + {setupData.secret} + + + + + + )} + + {setupStep === 2 && ( + + + Step 2: Enter the 6-digit verification code + from your authenticator app + + + { + 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 && {error}} + + + + + + + )} + + {setupStep === 3 && ( + + + + + MFA Successfully Enabled! + + + + + + Save these backup codes in a safe place + + + You can use these codes to access your account if you lose + your authenticator device. Each code can only be used once. + + + + + + {backupCodes?.map((code, index) => ( + + {code} + + ))} + + + + + + )} + + + + {/* Disable MFA Modal */} + + + + + Disable Multi-Factor Authentication + + + + + + Disabling MFA will make your account less secure. Are you sure + you want to continue? - - - )} - - {setupStep === 2 && ( - - Step 2: Enter the 6-digit verification code - from your authenticator app + Enter a verification code from your authenticator app to + confirm: { - if (e.key === 'Enter' && verificationCode.length === 6) { - handleConfirmMFA() + if (e.key === 'Enter' && disableCode.length === 6) { + handleDisableMFA() } }} - onChange={e => setVerificationCode(e.target.value)} + onChange={e => setDisableCode(e.target.value)} sx={{ textAlign: 'center', fontSize: '1.2em', @@ -389,39 +495,40 @@ const MFASettings = () => { - )} + + + + {/* Backup Codes Modal */} + setBackupCodesModalOpen(false)} + > + + + + New Backup Codes + - {setupStep === 3 && ( - - - - MFA Successfully Enabled! - - - - - Save these backup codes in a safe place - - You can use these codes to access your account if you lose - your authenticator device. Each code can only be used once. + Your previous backup codes are now invalid. Save these new + codes in a safe place. Each code can only be used once. @@ -439,123 +546,17 @@ const MFASettings = () => { - - )} - - - - {/* Disable MFA Modal */} - - - - - Disable Multi-Factor Authentication - - - - - - Disabling MFA will make your account less secure. Are you sure - you want to continue? - - - - - Enter a verification code from your authenticator app to confirm: - - - { - 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 && {error}} - - - - - - - - - - {/* Backup Codes Modal */} - setBackupCodesModalOpen(false)} - > - - - - New Backup Codes - - - - - - Your previous backup codes are now invalid. Save these new codes - in a safe place. Each code can only be used once. - - - - - - {backupCodes?.map((code, index) => ( - - {code} - - ))} - - - - - - - -
+ + +
+ ) } diff --git a/src/views/Settings/NotificationSettings.jsx b/src/views/Settings/NotificationSettings.jsx new file mode 100644 index 0000000..a94ebd2 --- /dev/null +++ b/src/views/Settings/NotificationSettings.jsx @@ -0,0 +1,12 @@ +import NotificationSetting from './NotificationSetting' +import SettingsLayout from './SettingsLayout' + +const NotificationSettings = () => { + return ( + + + + ) +} + +export default NotificationSettings \ No newline at end of file diff --git a/src/views/Settings/ProfileSettings.jsx b/src/views/Settings/ProfileSettings.jsx index 3975fe5..7f0eb1d 100644 --- a/src/views/Settings/ProfileSettings.jsx +++ b/src/views/Settings/ProfileSettings.jsx @@ -4,7 +4,6 @@ import { Box, Button, Card, - Divider, Input, Typography, } from '@mui/joy' @@ -19,6 +18,7 @@ import { UpdateUserDetails } from '../../utils/Fetcher' import { resolvePhotoURL } from '../../utils/Helpers' import { getCroppedImg } from '../../utils/imageCropUtils' import { UploadFile } from '../../utils/TokenManager' +import SettingsLayout from './SettingsLayout' const ProfileSettings = () => { const { data: userProfile } = useUserProfile() @@ -136,165 +136,165 @@ const ProfileSettings = () => { // Helper to resolve photoURL with baseURL if needed return ( -
- Profile Settings - - - Update your display name and profile photo. - - - - + +
+ + Update your display name and profile photo. + + + + + + + + + { + setShowCropper(false) + setSelectedFile(null) + }} + > + + + + + + + + + + + + + Display Name + + setDisplayName(e.target.value)} + placeholder='Enter your display name' + sx={{ mb: 2 }} + /> + + + Timezone + + 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 }} + /> + - - - { - setShowCropper(false) - setSelectedFile(null) - }} - > - - - - - - - - - - - - - Display Name - - setDisplayName(e.target.value)} - placeholder='Enter your display name' - sx={{ mb: 2 }} - /> - - - Timezone - - 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 }} - /> - - - -
+
+ ) } diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx index ad1ce66..fa55d36 100644 --- a/src/views/Settings/Settings.jsx +++ b/src/views/Settings/Settings.jsx @@ -1,5 +1,5 @@ import { Capacitor } from '@capacitor/core' -import { Refresh } from '@mui/icons-material' +import { Delete, Refresh } from '@mui/icons-material' import { Box, Button, @@ -424,14 +424,10 @@ const Settings = () => { ))} )} - {userProfile.role === 'admin' && + {isAdmin && member.userId !== userProfile.id && member.isActive && ( )} @@ -514,8 +521,18 @@ const Settings = () => { type: 'success', message: 'Accepted request successfully', }) - // reload the page - window.location.reload() + // Invalidate and refetch circle-related queries + 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 = () => { - - - - - - )} - - - {settingsCards.map(setting => ( - - handleCardClick(setting.id)} - > - - + + + + + + Upgrade to Plus + + + Unlock powerful features to enhance your productivity + + + • Rich text descriptions + • Task notifications + • API integrations + • Advanced automation + + + + + + + + + )} + + + + {settingsCards.map((setting, index) => ( + + handleCardClick(setting.id)} + sx={{ + '&:hover': { + backgroundColor: 'background.level1', + }, + py: 2.5, + px: 3, + borderRadius: 'lg', + mb: 1, + }} + > + {setting.icon} - + + + {setting.title} {setting.description} - - - - - ))} - + + + + + ))} + + ) } diff --git a/src/views/Settings/SettingsRoutes.jsx b/src/views/Settings/SettingsRoutes.jsx new file mode 100644 index 0000000..1cb6320 --- /dev/null +++ b/src/views/Settings/SettingsRoutes.jsx @@ -0,0 +1,7 @@ +import { Outlet } from 'react-router-dom' + +const SettingsRoutes = () => { + return +} + +export default SettingsRoutes \ No newline at end of file diff --git a/src/views/Settings/SidepanelSettings.jsx b/src/views/Settings/SidepanelSettings.jsx index 80b72ba..0734da7 100644 --- a/src/views/Settings/SidepanelSettings.jsx +++ b/src/views/Settings/SidepanelSettings.jsx @@ -23,6 +23,7 @@ import { Typography, } from '@mui/joy' import { useEffect, useState } from 'react' +import SettingsLayout from './SettingsLayout' const DEFAULT_SIDEPANEL_CONFIG = [ { @@ -122,136 +123,148 @@ const SidepanelSettings = () => { } return ( - - - Sidepanel Settings - - - Customize which cards appear in the sidepanel and their order. Drag and - drop to reorder, or toggle visibility for each card. - + +
+ + + Sidepanel Settings + + + Customize which cards appear in the sidepanel and their order. Drag + and drop to reorder, or toggle visibility for each card. + - - - {provided => ( - - {config.map((item, index) => ( - - {(provided, snapshot) => ( - + + {provided => ( + + {config.map((item, index) => ( + - - - - - - - - ( + - {getIcon(item.iconName)} - - - - - - {item.name} - - - - {item.description} - - - + + + + + - - - handleToggleEnabled(item.id, e.target.checked) - } - overlay - variant='plain' - size='lg' - checkedIcon={} - uncheckedIcon={} - /> - - - - )} - - ))} - {provided.placeholder} - - )} - - + + {getIcon(item.iconName)} + - - - - This will restore all cards to their default visibility and order. - - - + + + + {item.name} + + + - {item.description} + + + + + + + handleToggleEnabled(item.id, e.target.checked) + } + overlay + variant='plain' + size='lg' + checkedIcon={} + uncheckedIcon={} + /> + + + + )} + + ))} + {provided.placeholder} + + )} + + + + + + + This will restore all cards to their default visibility and order. + + + +
+
) } diff --git a/src/views/Settings/StorageSettings.jsx b/src/views/Settings/StorageSettings.jsx index 75d8e01..6039321 100644 --- a/src/views/Settings/StorageSettings.jsx +++ b/src/views/Settings/StorageSettings.jsx @@ -1,18 +1,12 @@ import { Capacitor } from '@capacitor/core' -import { - Button, - Card, - Chip, - Divider, - LinearProgress, - Typography, -} from '@mui/joy' +import { Button, Card, Chip, LinearProgress, Typography } from '@mui/joy' import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useUserProfile } from '../../queries/UserQueries' import { GetStorageUsage } from '../../utils/Fetcher' import { isPlusAccount } from '../../utils/Helpers' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' +import SettingsLayout from './SettingsLayout' const StorageSettings = () => { const Navigate = useNavigate() @@ -62,114 +56,114 @@ const StorageSettings = () => { const totalMB = (usage.total / (1024 * 1024)).toFixed(2) return ( -
- Storage Settings - - - - Server Storage Usage - {!isPlusAccount(userProfile) && ( - - Plus Feature - + +
+ + + Server Storage Usage + {!isPlusAccount(userProfile) && ( + + Plus Feature + + )} + + + This is the storage used by your account on our servers (e.g. files, + images, and data you have uploaded). + + {!isPlusAccount(userProfile) ? ( + <> + + + -- MB used / -- MB total (--) + + + Server storage is not available in the Basic plan. Upgrade to + Plus to track your server storage usage. + + + ) : loading ? ( + <> + + Loading... + + ) : ( + <> + + + {usedMB} MB used / {totalMB} MB total ({percent}%) + + )} - - - This is the storage used by your account on our servers (e.g. files, - images, and data you have uploaded). - - {!isPlusAccount(userProfile) ? ( - <> - + + + {Capacitor.isNativePlatform() ? 'App' : 'Browser'} Local Storage & + Cache + + + 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. + + - - + 'Clear All', + 'Cancel', + 'danger', + ) + }} + > + Clear All Local Storage and Cache + + + - {/* Modals */} - {confirmModalConfig?.isOpen && ( - - )} -
+ {/* Modals */} + {confirmModalConfig?.isOpen && ( + + )} +
+ ) } diff --git a/src/views/Settings/ThemeSettings.jsx b/src/views/Settings/ThemeSettings.jsx new file mode 100644 index 0000000..06f1d7b --- /dev/null +++ b/src/views/Settings/ThemeSettings.jsx @@ -0,0 +1,19 @@ +import { Typography } from '@mui/joy' +import SettingsLayout from './SettingsLayout' +import ThemeToggle from './ThemeToggle' + +const ThemeSettings = () => { + return ( + +
+ + Choose how the site looks to you. Select a single theme, or sync with + your system and automatically switch between day and night themes. + + +
+
+ ) +} + +export default ThemeSettings \ No newline at end of file diff --git a/src/views/Terms/TermsView.jsx b/src/views/Terms/TermsView.jsx index f0a0d9c..b55f632 100644 --- a/src/views/Terms/TermsView.jsx +++ b/src/views/Terms/TermsView.jsx @@ -2,190 +2,244 @@ import React from 'react' const TermsView = () => { return ( -
+

Terms of Service

+

Effective Date: January 1, 2024

These Terms of Service ("Terms") govern your access to and use of the - services provided by Favoro LLC, doing business as donetick.com - ("Favoro", "we", "us", or "our"). By accessing or using our website and - services, you agree to be bound by these Terms. If you do not agree to + Donetick task management platform provided by Favoro LLC ("Donetick", "we", "us", or "our"). + By accessing or using our 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.

-

Use of Services

+
+

Important: Cloud vs. Self-Hosted Services

+

+ Donetick is available in two deployment models with different terms and responsibilities: +

+
    +
  • Cloud-Hosted Service: Hosted at donetick.com where we provide infrastructure and data management
  • +
  • Self-Hosted Service: Open-source software you deploy on your own infrastructure
  • +
+

Please review the applicable sections carefully as they define different rights and obligations.

+
+

1. Definitions

    -
  • - You must be at least 18 years old or have the legal capacity to enter - into contracts in your jurisdiction to use our services. -
  • -
  • - You are responsible for maintaining the confidentiality of your - account credentials and for any activity that occurs under your - account. -
  • -
  • - You may not use our services for any illegal or unauthorized purpose, - or in any way that violates these Terms. -
  • -
-

Subscriptions

- -
    -
  • - 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. -
  • -
  • - 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. -
  • -
  • - 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. -
  • -
  • - 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. -
  • +
  • "Cloud Service" refers to the Donetick platform hosted at donetick.com and managed by Favoro LLC
  • +
  • "Self-Hosted Service" refers to the open-source Donetick software deployed on user-controlled infrastructure
  • +
  • "Content" means all data, information, tasks, files, and other materials you create, upload, or store using our services
  • +
  • "Circle" means a collaborative workspace shared between users for task management
-

Fee Changes

+

2. Eligibility and Account Registration

    -
  • - {' '} - Favoro, in its sole discretion and at any time, may modify the - Subscription fees for the Subscriptions. Any Subscription fee change - will become effective at the end of the then-current Billing Cycle. -
  • -
  • - 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. -
  • +
  • You must be at least 13 years old to use our services
  • +
  • Users under 18 must have parental consent
  • +
  • You are responsible for maintaining the confidentiality of your account credentials
  • +
  • You must provide accurate and complete information when creating an account
  • +
  • One person or legal entity may maintain only one account
-

Refunds

+

3. Cloud-Hosted Service Terms

+

This section applies only to users of the Cloud Service at donetick.com

+ +

3.1 Service Availability and Uptime

    -
  • - Certain refund requests for Subscriptions may be considered by Favoro - on a case-by-case basis and granted at the sole discretion of Favoro. -
  • +
  • We strive to maintain 99.5% uptime for the Cloud Service on a monthly basis
  • +
  • Scheduled maintenance will be announced at least 24 hours in advance when possible
  • +
  • We reserve the right to temporarily suspend service for emergency maintenance
  • +
  • No SLA credits or compensation are provided for downtime unless otherwise specified in a separate agreement
-

Content

+

3.2 Data Ownership and Responsibility

    -
  • - Our services allow you to post, link, store, share, and otherwise make - available certain information, text, graphics, videos, or other - material ("Content"). -
  • -
  • - You are responsible for the Content that you post to our services, - including its legality, reliability, and appropriateness. -
  • -
  • - You may not post Content that is defamatory, obscene, abusive, - offensive, or otherwise objectionable. -
  • -
  • - You may not post Content that violates any party's intellectual - property rights. -
  • -
  • You may not post Content that violates any law or regulation.
  • +
  • Your Data: You retain ownership of all Content you create or upload
  • +
  • Our Responsibility: We provide secure hosting, backup, and infrastructure management
  • +
  • Data Portability: You can export your data at any time through our export features
  • +
  • Data Retention: We retain your data for 90 days after account deletion to allow recovery
-

Feedback Requests

+

3.3 Subscriptions and Billing

+
    +
  • Cloud Service subscriptions are billed monthly or annually in advance
  • +
  • Subscription fees will automatically renew unless cancelled before the next billing cycle
  • +
  • You may cancel your subscription at any time through your account settings
  • +
  • Upon cancellation, you retain access until the end of your current billing period
  • +
  • We may modify subscription prices with 30 days advance notice
  • +
  • Refunds are considered on a case-by-case basis at our discretion
  • +
+ +

3.4 Cloud Service Limitations

+
    +
  • Storage limits apply based on your subscription tier
  • +
  • API rate limits may be enforced to ensure service stability
  • +
  • We may suspend accounts that exceed reasonable usage limits
  • +
+ +

4. Self-Hosted Service Terms

+

This section applies to users deploying Donetick on their own infrastructure

+ +

4.1 Open Source License

+
    +
  • The Self-Hosted Service is provided under the MIT License
  • +
  • You may modify, distribute, and use the software for any purpose
  • +
  • Attribution to Donetick must be maintained in derivative works
  • +
  • No warranty or support is provided for self-hosted deployments
  • +
+ +

4.2 User Responsibility

+
    +
  • Infrastructure: You are solely responsible for hosting, maintenance, security, and backups
  • +
  • Data Protection: You are the data controller and responsible for compliance with applicable laws
  • +
  • Updates: You are responsible for applying security updates and patches
  • +
  • Support: No technical support is provided for self-hosted installations
  • +
+ +

4.3 Limitation of Our Responsibility

+
    +
  • We provide no guarantees about the performance or security of self-hosted deployments
  • +
  • We are not responsible for any data loss, security breaches, or service interruptions in self-hosted environments
  • +
  • Technical support is limited to community forums and documentation
  • +
+ +

5. Acceptable Use Policy

+

Applies to both Cloud and Self-Hosted Services

+ +

You may not use our services to:

+
    +
  • Violate any applicable laws or regulations
  • +
  • Infringe on intellectual property rights
  • +
  • Transmit malicious code or conduct security attacks
  • +
  • Harass, abuse, or harm other users
  • +
  • Distribute spam or unwanted communications
  • +
  • Attempt to reverse engineer our proprietary systems (Cloud Service)
  • +
  • Resell or redistribute the service without permission
  • +
+ +

6. Content and Data

+ +

6.1 Your Content Rights

+
    +
  • You retain ownership of all Content you create
  • +
  • You grant us the necessary rights to operate the service (Cloud Service only)
  • +
  • You are responsible for ensuring you have rights to any Content you upload
  • +
+ +

6.2 Content Restrictions

+
    +
  • Content must not violate any laws or third-party rights
  • +
  • Content must not contain malicious code or harmful materials
  • +
  • We may remove Content that violates these Terms (Cloud Service only)
  • +
+ +

7. Privacy and Security

+
    +
  • Your privacy is important to us - please review our Privacy Policy
  • +
  • We implement industry-standard security measures (Cloud Service)
  • +
  • You are responsible for security in self-hosted deployments
  • +
  • Report security vulnerabilities to security@donetick.com
  • +
+ +

8. Intellectual Property

+
    +
  • The Donetick name, logo, and proprietary features are our intellectual property
  • +
  • The open-source codebase is licensed under MIT License
  • +
  • You may not use our trademarks without written permission
  • +
+ +

9. Third-Party Integrations

+
    +
  • Donetick integrates with third-party services (Telegram, Discord, webhooks, etc.)
  • +
  • Your use of these integrations is subject to their respective terms
  • +
  • We are not responsible for third-party service availability or functionality
  • +
+ +

10. Liability and Warranties

+ +

10.1 Disclaimer of Warranties

- Our platform allows users to send feedback requests to others. You are - solely responsible for the content of any feedback requests you send - using our services. + Our services are provided "as is" and "as available" without any warranty of any kind, + express or implied, including but not limited to merchantability, fitness for a particular purpose, + or non-infringement.

+

10.2 Cloud Service Liability

+
    +
  • Our total liability for the Cloud Service is limited to the amount you paid in the 12 months preceding the claim
  • +
  • We are not liable for indirect, incidental, special, or consequential damages
  • +
  • We maintain appropriate insurance and implement security best practices
  • +
+ +

10.3 Self-Hosted Service Liability

+
    +
  • We provide no warranties or guarantees for self-hosted deployments
  • +
  • Our liability is limited to the maximum extent permitted by law
  • +
  • You assume all risks associated with self-hosting
  • +
+ +

11. Indemnification

- You may not use our services to send spam, harass others, or engage in - any abusive behavior. + You agree to indemnify and hold us harmless from any claims, damages, or expenses + arising from your use of our services, violation of these Terms, or infringement of any rights.

-

Credits

+

12. Termination

+

12.1 Termination by You

+
    +
  • You may terminate your account at any time
  • +
  • Cloud Service: Access continues until the end of your billing period
  • +
  • Self-Hosted Service: You may stop using the software at any time
  • +
+ +

12.2 Termination by Us

+
    +
  • We may terminate accounts that violate these Terms
  • +
  • We may discontinue the Cloud Service with 90 days notice
  • +
  • We will provide data export capabilities before termination when possible
  • +
+ +

13. Changes to These Terms

+
    +
  • We may update these Terms from time to time
  • +
  • Material changes will be announced via email or in-app notification
  • +
  • Continued use after changes constitutes acceptance of new Terms
  • +
  • For significant changes, we may require explicit acceptance
  • +
+ +

14. Dispute Resolution

+
    +
  • We encourage resolving disputes informally by contacting us first
  • +
  • These Terms are governed by the laws of Delaware, United States
  • +
  • Any disputes will be resolved in the courts of Delaware
  • +
  • You may pursue small claims court for eligible disputes
  • +
+ +

15. Miscellaneous

+
    +
  • If any provision is found unenforceable, the remainder remains in effect
  • +
  • Our failure to enforce any right does not waive that right
  • +
  • These Terms constitute the entire agreement between us
  • +
  • We may assign these Terms; you may not without our consent
  • +
+ +

16. Contact Information

- Certain actions on our platform may require credits. You can purchase - credits through our website. + If you have questions about these Terms, please contact us:

+
    +
  • Email: legal@donetick.com
  • +
  • Support: support@donetick.com
  • +
  • Address: Favoro LLC, [Address to be provided]
  • +
-

Credits are non-refundable and non-transferable.

- -

Intellectual Property

- -

- 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. -

- -

- You may not reproduce, modify, or distribute any content from our - website or services without our prior written consent. -

- -

Disclaimer of Warranties

- -

- Our services are provided "as is" and "as available" without any - warranty of any kind, express or implied. -

- -

- We do not warrant that our services will be uninterrupted, secure, or - error-free, or that any defects will be corrected. -

- -

Limitation of Liability

- -

- 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. -

- -

Governing Law

- -

- 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. -

- -

Changes to These Terms

- -

- 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. -

- -

Contact Us

- -

- If you have any questions or concerns about these Terms, please contact - us at support@donetick.com +


+

+ Last Updated: January 1, 2024
+ These Terms of Service are effective immediately for new users and will become effective + for existing users 30 days after posting.

) diff --git a/src/views/Things/ThingsView.jsx b/src/views/Things/ThingsView.jsx index 56e1ed9..73ecba5 100644 --- a/src/views/Things/ThingsView.jsx +++ b/src/views/Things/ThingsView.jsx @@ -668,7 +668,7 @@ const ThingsView = () => { return ( - + {/* */} { fullWidth={true} title='Create new task' > - - Experimental Feature - { ? TASK_COLOR.ASSIGNED_TO_ME : TASK_COLOR.ASSIGNED_TO_OTHER } -const CalendarView = ({ chores }) => { +const CalendarCard = ({ chores }) => { const { data: userProfile } = useUserProfile() const [selectedDate, setSeletedDate] = useState(null) @@ -23,6 +23,33 @@ const CalendarView = ({ chores }) => { const { data: circleMembersData } = useCircleMembers() 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 const getAssigneeName = assignedTo => { if (assignedTo === userProfile.id) { @@ -307,4 +334,4 @@ const CalendarView = ({ chores }) => { ) } -export default CalendarView +export default CalendarCard diff --git a/src/views/components/CustomParsers.js b/src/views/components/CustomParsers.js index 5e12fda..22667a9 100644 --- a/src/views/components/CustomParsers.js +++ b/src/views/components/CustomParsers.js @@ -118,6 +118,7 @@ export const parseLabels = (inputSentence, userLabels) => { export const parseRepeatV2 = inputSentence => { const sentence = inputSentence.toLowerCase() + const lowerInputSentence = inputSentence.toLowerCase() const result = { frequency: 1, frequencyType: null, @@ -232,8 +233,8 @@ export const parseRepeatV2 = inputSentence => { highlight: [ { text: pattern.name, - start: inputSentence.indexOf(match[0]), - end: inputSentence.indexOf(match[0]) + match[0].length, + start: lowerInputSentence.indexOf(match[0]), + end: lowerInputSentence.indexOf(match[0]) + match[0].length, }, ], cleanedSentence: inputSentence.replace(match[0], '').trim(), @@ -250,8 +251,8 @@ export const parseRepeatV2 = inputSentence => { highlight: [ { text: pattern.name, - start: inputSentence.indexOf(match[0]), - end: inputSentence.indexOf(match[0]) + match[0].length, + start: lowerInputSentence.indexOf(match[0]), + end: lowerInputSentence.indexOf(match[0]) + match[0].length, }, ], cleanedSentence: inputSentence.replace(match[0], '').trim(), @@ -275,8 +276,8 @@ export const parseRepeatV2 = inputSentence => { highlight: [ { text: pattern.name, - start: inputSentence.indexOf(match[0]), - end: inputSentence.indexOf(match[0]) + match[0].length, + start: lowerInputSentence.indexOf(match[0]), + end: lowerInputSentence.indexOf(match[0]) + match[0].length, }, ], cleanedSentence: inputSentence.replace(match[0], '').trim(), @@ -299,8 +300,8 @@ export const parseRepeatV2 = inputSentence => { highlight: [ { text: pattern.name, - start: inputSentence.indexOf(match[0]), - end: inputSentence.indexOf(match[0]) + match[0].length, + start: lowerInputSentence.indexOf(match[0]), + end: lowerInputSentence.indexOf(match[0]) + match[0].length, }, ], cleanedSentence: inputSentence.replace(match[0], '').trim(), @@ -315,8 +316,8 @@ export const parseRepeatV2 = inputSentence => { highlight: [ { text: pattern.name, - start: inputSentence.indexOf(match[0]), - end: inputSentence.indexOf(match[0]) + match[0].length, + start: lowerInputSentence.indexOf(match[0]), + end: lowerInputSentence.indexOf(match[0]) + match[0].length, }, ], cleanedSentence: inputSentence.replace(match[0], '').trim(), @@ -332,8 +333,8 @@ export const parseRepeatV2 = inputSentence => { highlight: [ { text: pattern.name, - start: inputSentence.indexOf(match[0]), - end: inputSentence.indexOf(match[0]) + match[0].length, + start: lowerInputSentence.indexOf(match[0]), + end: lowerInputSentence.indexOf(match[0]) + match[0].length, }, ], cleanedSentence: inputSentence.replace(match[0], '').trim(), @@ -351,8 +352,8 @@ export const parseRepeatV2 = inputSentence => { highlight: [ { text: pattern.name, - start: inputSentence.indexOf(match[0]), - end: inputSentence.indexOf(match[0]) + match[0].length, + start: lowerInputSentence.indexOf(match[0]), + end: lowerInputSentence.indexOf(match[0]) + match[0].length, }, ], cleanedSentence: inputSentence.replace(match[0], '').trim(), @@ -367,8 +368,8 @@ export const parseRepeatV2 = inputSentence => { highlight: [ { text: pattern.name, - start: inputSentence.indexOf(match[0]), - end: inputSentence.indexOf(match[0]) + match[0].length, + start: lowerInputSentence.indexOf(match[0]), + end: lowerInputSentence.indexOf(match[0]) + match[0].length, }, ], cleanedSentence: inputSentence.replace(match[0], '').trim(), diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx index 4189287..389965b 100644 --- a/src/views/components/NavBar.jsx +++ b/src/views/components/NavBar.jsx @@ -23,7 +23,7 @@ import { } from '@mui/joy' 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 UserProfileAvatar from '../../components/UserProfileAvatar' import NavBarLink from './NavBarLink' @@ -97,6 +97,7 @@ const NavBar = () => { () => setDrawerOpen(false), ] const location = useLocation() + const [searchParams] = useSearchParams() useEffect(() => { SafeArea.getSafeAreaInsets().then(data => { const { insets } = data @@ -150,7 +151,8 @@ const NavBar = () => { backgroundColor: 'var(--joy-palette-background-body)', }} > - {['/chores', '/'].includes(location.pathname) ? ( + {['/chores', '/'].includes(location.pathname) && + !searchParams.get('filter') ? ( { ) : ( - navigate(-1)}> + { + 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' + } + > )} @@ -192,9 +211,18 @@ const NavBar = () => {
*/} {links.map((link, index) => (