diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index 0b4c9577b..e96acbebc 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -24,8 +24,12 @@ import issuesService from "services/issues.service"; import inboxService from "services/inbox.service"; // fetch keys import { INBOX_LIST, ISSUE_DETAILS } from "constants/fetch-keys"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; export const CommandPalette: React.FC = () => { + const store: any = useMobxStore(); + const [isPaletteOpen, setIsPaletteOpen] = useState(false); const [isIssueModalOpen, setIsIssueModalOpen] = useState(false); const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); @@ -96,7 +100,8 @@ export const CommandPalette: React.FC = () => { setIsIssueModalOpen(true); } else if ((ctrlKey || metaKey) && keyPressed === "b") { e.preventDefault(); - toggleCollapsed(); + // toggleCollapsed(); + store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed); } else if (key === "Delete") { e.preventDefault(); setIsBulkDeleteIssuesModalOpen(true); @@ -120,7 +125,7 @@ export const CommandPalette: React.FC = () => { } } }, - [toggleCollapsed, copyIssueUrlToClipboard] + [copyIssueUrlToClipboard] ); useEffect(() => { diff --git a/apps/app/components/core/theme/custom-theme-selector.tsx b/apps/app/components/core/theme/custom-theme-selector.tsx index 668083b59..9db7b1dd8 100644 --- a/apps/app/components/core/theme/custom-theme-selector.tsx +++ b/apps/app/components/core/theme/custom-theme-selector.tsx @@ -15,6 +15,8 @@ import userService from "services/user.service"; import { applyTheme } from "helpers/theme.helper"; // types import { ICustomTheme } from "types"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; type Props = { preLoadedData?: Partial | null; @@ -32,6 +34,8 @@ const defaultValues: ICustomTheme = { }; export const CustomThemeSelector: React.FC = ({ preLoadedData }) => { + const store: any = useMobxStore(); + const [darkPalette, setDarkPalette] = useState(false); const { @@ -60,21 +64,15 @@ export const CustomThemeSelector: React.FC = ({ preLoadedData }) => { theme: "custom", }; - await userService - .updateUser({ - theme: payload, - }) - .then((res) => { - mutateUser((prevData) => { - if (!prevData) return prevData; - - return { ...prevData, ...res }; - }, false); - + store.user + .updateCurrentUserSettings({ theme: payload }) + .then((response: any) => { setTheme("custom"); applyTheme(payload.palette, darkPalette); }) - .catch((err) => console.log(err)); + .catch((error: any) => { + console.log("error", error); + }); }; const handleUpdateTheme = async (formData: any) => { diff --git a/apps/app/components/notifications/notification-popover.tsx b/apps/app/components/notifications/notification-popover.tsx index cd8c50bd2..da9fb5668 100644 --- a/apps/app/components/notifications/notification-popover.tsx +++ b/apps/app/components/notifications/notification-popover.tsx @@ -21,8 +21,12 @@ import { NotificationsOutlined } from "@mui/icons-material"; import emptyNotification from "public/empty-state/notification.svg"; // helpers import { getNumberCount } from "helpers/string.helper"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; export const NotificationPopover = () => { + const store: any = useMobxStore(); + const { notifications, archived, @@ -77,17 +81,17 @@ export const NotificationPopover = () => { tooltipContent="Notifications" position="right" className="ml-2" - disabled={!sidebarCollapse} + disabled={!store?.theme?.sidebarCollapsed} > - {sidebarCollapse ? null : Notifications} + {store?.theme?.sidebarCollapsed ? null : Notifications} {totalNotificationCount && totalNotificationCount > 0 ? ( {getNumberCount(totalNotificationCount)} diff --git a/apps/app/components/project/sidebar-list.tsx b/apps/app/components/project/sidebar-list.tsx index 1bb3711fc..a753b98ba 100644 --- a/apps/app/components/project/sidebar-list.tsx +++ b/apps/app/components/project/sidebar-list.tsx @@ -26,8 +26,12 @@ import { orderArrayBy } from "helpers/array.helper"; import { IProject } from "types"; // fetch-keys import { PROJECTS_LIST } from "constants/fetch-keys"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; export const ProjectSidebarList: FC = () => { + const store: any = useMobxStore(); + const [deleteProjectModal, setDeleteProjectModal] = useState(false); const [projectToDelete, setProjectToDelete] = useState(null); @@ -139,7 +143,7 @@ export const ProjectSidebarList: FC = () => { {({ open }) => ( <> - {!sidebarCollapse && ( + {!store?.theme?.sidebarCollapsed && ( { handleDeleteProject(project)} @@ -194,7 +198,7 @@ export const ProjectSidebarList: FC = () => { {({ open }) => ( <> - {!sidebarCollapse && ( + {!store?.theme?.sidebarCollapsed && ( { handleDeleteProject(project)} @@ -243,7 +247,7 @@ export const ProjectSidebarList: FC = () => { > {({ open }) => ( <> - {!sidebarCollapse && ( + {!store?.theme?.sidebarCollapsed && ( { handleDeleteProject(project)} handleCopyText={() => handleCopyText(project.id)} shortContextMenu @@ -284,7 +288,7 @@ export const ProjectSidebarList: FC = () => { }} > - {!sidebarCollapse && "Add Project"} + {!store?.theme?.sidebarCollapsed && "Add Project"} )} diff --git a/apps/app/components/workspace/help-section.tsx b/apps/app/components/workspace/help-section.tsx index d1ea4f8a3..10548a248 100644 --- a/apps/app/components/workspace/help-section.tsx +++ b/apps/app/components/workspace/help-section.tsx @@ -11,6 +11,8 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { Bolt, HelpOutlineOutlined, WestOutlined } from "@mui/icons-material"; import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline"; import { DocumentIcon, DiscordIcon, GithubIcon } from "components/icons"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; const helpOptions = [ { @@ -41,6 +43,8 @@ export interface WorkspaceHelpSectionProps { } export const WorkspaceHelpSection: React.FC = ({ setSidebarActive }) => { + const store: any = useMobxStore(); + const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); const helpOptionsRef = useRef(null); @@ -53,23 +57,23 @@ export const WorkspaceHelpSection: React.FC = ({ setS <>
- {!sidebarCollapse && ( + {!store?.theme?.sidebarCollapsed && (
Free Plan
)}
@@ -122,7 +126,7 @@ export const WorkspaceHelpSection: React.FC = ({ setS >
diff --git a/apps/app/components/workspace/sidebar-dropdown.tsx b/apps/app/components/workspace/sidebar-dropdown.tsx index 7a43cfb74..50328ecc3 100644 --- a/apps/app/components/workspace/sidebar-dropdown.tsx +++ b/apps/app/components/workspace/sidebar-dropdown.tsx @@ -23,6 +23,8 @@ import { CheckIcon, PlusIcon } from "@heroicons/react/24/outline"; import { truncateText } from "helpers/string.helper"; // types import { IWorkspace } from "types"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // Static Data const userLinks = (workspaceSlug: string, userId: string) => [ @@ -54,6 +56,8 @@ const profileLinks = (workspaceSlug: string, userId: string) => [ ]; export const WorkspaceSidebarDropdown = () => { + const store: any = useMobxStore(); + const router = useRouter(); const { workspaceSlug } = router.query; @@ -108,7 +112,7 @@ export const WorkspaceSidebarDropdown = () => {
@@ -123,7 +127,7 @@ export const WorkspaceSidebarDropdown = () => { )}
- {!sidebarCollapse && ( + {!store?.theme?.sidebarCollapsed && (

{activeWorkspace?.name ? truncateText(activeWorkspace.name, 14) : "Loading..."}

@@ -243,7 +247,7 @@ export const WorkspaceSidebarDropdown = () => { - {!sidebarCollapse && ( + {!store?.theme?.sidebarCollapsed && ( diff --git a/apps/app/components/workspace/sidebar-menu.tsx b/apps/app/components/workspace/sidebar-menu.tsx index 6a2d94cb1..3e4d5472b 100644 --- a/apps/app/components/workspace/sidebar-menu.tsx +++ b/apps/app/components/workspace/sidebar-menu.tsx @@ -16,6 +16,8 @@ import { TaskAltOutlined, WorkOutlineOutlined, } from "@mui/icons-material"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; const workspaceLinks = (workspaceSlug: string) => [ { @@ -41,6 +43,8 @@ const workspaceLinks = (workspaceSlug: string) => [ ]; export const WorkspaceSidebarMenu = () => { + const store: any = useMobxStore(); + const router = useRouter(); const { workspaceSlug } = router.query; @@ -61,17 +65,17 @@ export const WorkspaceSidebarMenu = () => { tooltipContent={link.name} position="right" className="ml-2" - disabled={!sidebarCollapse} + disabled={!store?.theme?.sidebarCollapsed} >
{} - {!sidebarCollapse && link.name} + {!store?.theme?.sidebarCollapsed && link.name}
diff --git a/apps/app/layouts/app-layout/app-sidebar.tsx b/apps/app/layouts/app-layout/app-sidebar.tsx index 8ea9e00a1..60ef645cf 100644 --- a/apps/app/layouts/app-layout/app-sidebar.tsx +++ b/apps/app/layouts/app-layout/app-sidebar.tsx @@ -7,20 +7,25 @@ import { WorkspaceSidebarMenu, } from "components/workspace"; import { ProjectSidebarList } from "components/project"; +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; export interface SidebarProps { toggleSidebar: boolean; setToggleSidebar: React.Dispatch>; } -const Sidebar: React.FC = ({ toggleSidebar, setToggleSidebar }) => { +const Sidebar: React.FC = observer(({ toggleSidebar, setToggleSidebar }) => { + const store: any = useMobxStore(); // theme const { collapsed: sidebarCollapse } = useTheme(); return (
@@ -31,6 +36,6 @@ const Sidebar: React.FC = ({ toggleSidebar, setToggleSidebar }) =>
); -}; +}); export default Sidebar; diff --git a/apps/app/lib/mobx/store-init.tsx b/apps/app/lib/mobx/store-init.tsx new file mode 100644 index 000000000..9eb0d7493 --- /dev/null +++ b/apps/app/lib/mobx/store-init.tsx @@ -0,0 +1,33 @@ +import { useEffect } from "react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; + +const MobxStoreInit = () => { + const store: any = useMobxStore(); + + useEffect(() => { + // sidebar collapsed toggle + if ( + localStorage && + localStorage.getItem("app_sidebar_collapsed") && + store?.theme?.sidebarCollapsed === null + ) + store.theme.setSidebarCollapsed( + localStorage.getItem("app_sidebar_collapsed") + ? localStorage.getItem("app_sidebar_collapsed") === "true" + ? true + : false + : false + ); + + // theme + if (localStorage && localStorage.getItem("theme") && store.theme.theme === null) + store.theme.setTheme( + localStorage.getItem("theme") ? localStorage.getItem("theme") : "system" + ); + }, [store?.theme]); + + return <>; +}; + +export default MobxStoreInit; diff --git a/apps/app/lib/mobx/store-provider.tsx b/apps/app/lib/mobx/store-provider.tsx new file mode 100644 index 000000000..244968028 --- /dev/null +++ b/apps/app/lib/mobx/store-provider.tsx @@ -0,0 +1,30 @@ +import { createContext, useContext } from "react"; +// mobx store +import { RootStore } from "store/root"; + +let rootStore: any = null; + +export const MobxStoreContext = createContext(null); + +const initializeStore = () => { + const _rootStore = rootStore ?? new RootStore(); + + if (typeof window === "undefined") return _rootStore; + + if (!rootStore) rootStore = _rootStore; + + return _rootStore; +}; + +export const MobxStoreProvider = ({ children }: any) => { + const store = initializeStore(); + + return {children}; +}; + +// hook +export const useMobxStore = () => { + const context = useContext(MobxStoreContext); + if (context === undefined) throw new Error("useMobxStore must be used within MobxStoreProvider"); + return context; +}; diff --git a/apps/app/package.json b/apps/app/package.json index abaa30754..6b1d58692 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -36,6 +36,8 @@ "dotenv": "^16.0.3", "js-cookie": "^3.0.1", "lodash.debounce": "^4.0.8", + "mobx": "^6.10.0", + "mobx-react-lite": "^4.0.3", "next": "12.3.2", "next-pwa": "^5.6.0", "next-themes": "^0.2.1", diff --git a/apps/app/pages/_app.tsx b/apps/app/pages/_app.tsx index 6681a7fb9..5ef047ca9 100644 --- a/apps/app/pages/_app.tsx +++ b/apps/app/pages/_app.tsx @@ -33,6 +33,9 @@ import { SITE_KEYWORDS, SITE_TITLE, } from "constants/seo-variables"; +// mobx store provider +import { MobxStoreProvider } from "lib/mobx/store-provider"; +import MobxStoreInit from "lib/mobx/store-init"; const CrispWithNoSSR = dynamic(() => import("constants/crisp"), { ssr: false }); @@ -45,9 +48,10 @@ Router.events.on("routeChangeComplete", NProgress.done); function MyApp({ Component, pageProps }: AppProps) { return ( // - - - + // mobx root provider + + + {SITE_TITLE} @@ -64,10 +68,11 @@ function MyApp({ Component, pageProps }: AppProps) { + - - - + + + // ); } diff --git a/apps/app/store/root.ts b/apps/app/store/root.ts new file mode 100644 index 000000000..43daa32e6 --- /dev/null +++ b/apps/app/store/root.ts @@ -0,0 +1,17 @@ +// mobx lite +import { enableStaticRendering } from "mobx-react-lite"; +// store imports +import UserStore from "./user"; +import ThemeStore from "./theme"; + +enableStaticRendering(typeof window === "undefined"); + +export class RootStore { + user; + theme; + + constructor() { + this.user = new UserStore(this); + this.theme = new ThemeStore(this); + } +} diff --git a/apps/app/store/theme.ts b/apps/app/store/theme.ts new file mode 100644 index 000000000..ba84f3ba4 --- /dev/null +++ b/apps/app/store/theme.ts @@ -0,0 +1,66 @@ +// mobx +import { action, observable, makeObservable } from "mobx"; +// helper +import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper"; +// interfaces +import { ICurrentUserSettings } from "types"; + +class ThemeStore { + sidebarCollapsed: boolean | null = null; + theme: string | null = null; + // root store + rootStore; + + constructor(_rootStore: any | null = null) { + makeObservable(this, { + // observable + sidebarCollapsed: observable, + theme: observable, + // action + setSidebarCollapsed: action, + setTheme: action, + // computed + }); + + this.rootStore = _rootStore; + this.initialLoad(); + } + + setSidebarCollapsed(collapsed: boolean | null = null) { + if (collapsed === null) { + let _sidebarCollapsed: string | boolean | null = + localStorage.getItem("app_sidebar_collapsed"); + _sidebarCollapsed = _sidebarCollapsed ? (_sidebarCollapsed === "true" ? true : false) : false; + this.sidebarCollapsed = _sidebarCollapsed; + } else { + this.sidebarCollapsed = collapsed; + localStorage.setItem("app_sidebar_collapsed", collapsed.toString()); + } + } + + setTheme = async (_theme: ICurrentUserSettings) => { + try { + localStorage.setItem("theme", _theme.theme.toString()); + this.theme = _theme.theme.toString(); + + if (this.theme === "custom") { + let themeSettings = this.rootStore.user.currentUserSettings || null; + if (themeSettings && themeSettings.theme.palette) { + applyTheme( + themeSettings.theme.palette !== ",,,," + ? themeSettings.theme.palette + : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", + themeSettings.theme.darkPalette + ); + } + } else unsetCustomCssVariables(); + } catch (error) { + console.error("setting user theme error", error); + } + }; + + // init load + initialLoad() {} +} + +export default ThemeStore; diff --git a/apps/app/store/user.ts b/apps/app/store/user.ts new file mode 100644 index 000000000..dde652656 --- /dev/null +++ b/apps/app/store/user.ts @@ -0,0 +1,106 @@ +// mobx +import { action, observable, computed, makeObservable } from "mobx"; +// services +import UserService from "services/user.service"; +// interfaces +import { ICurrentUser, ICurrentUserSettings } from "types/users"; + +class UserStore { + currentUser: ICurrentUser | null = null; + currentUserSettings: ICurrentUserSettings | null = null; + // root store + rootStore; + + constructor(_rootStore: any) { + makeObservable(this, { + // observable + currentUser: observable.ref, + currentUserSettings: observable.ref, + // action + setCurrentUser: action, + setCurrentUserSettings: action, + updateCurrentUser: action, + updateCurrentUserSettings: action, + // computed + }); + this.rootStore = _rootStore; + this.initialLoad(); + } + + setCurrentUser = async () => { + try { + let userResponse: ICurrentUser | null = await UserService.currentUser(); + userResponse = userResponse || null; + if (userResponse) { + this.currentUser = { + id: userResponse?.id, + avatar: userResponse?.avatar, + first_name: userResponse?.first_name, + last_name: userResponse?.last_name, + username: userResponse?.username, + email: userResponse?.email, + mobile_number: userResponse?.mobile_number, + is_email_verified: userResponse?.is_email_verified, + is_tour_completed: userResponse?.is_tour_completed, + onboarding_step: userResponse?.onboarding_step, + is_onboarded: userResponse?.is_onboarded, + role: userResponse?.role, + }; + } + } catch (error) { + console.error("Fetching current user error", error); + } + }; + + setCurrentUserSettings = async () => { + try { + let userSettingsResponse: ICurrentUserSettings | null = await UserService.currentUser(); + userSettingsResponse = userSettingsResponse || null; + if (userSettingsResponse) { + this.currentUserSettings = { + theme: userSettingsResponse?.theme, + }; + this.rootStore.theme.setTheme(); + } + } catch (error) { + console.error("Fetching current user error", error); + } + }; + + updateCurrentUser = async (user: ICurrentUser) => { + try { + let userResponse: ICurrentUser = await UserService.updateUser(user); + userResponse = userResponse || null; + if (userResponse) { + this.currentUser = userResponse; + return userResponse; + } + } catch (error) { + console.error("Updating user error", error); + return error; + } + }; + + updateCurrentUserSettings = async (userTheme: ICurrentUserSettings) => { + try { + let userSettingsResponse: ICurrentUserSettings = await UserService.updateUser(userTheme); + userSettingsResponse = userSettingsResponse || null; + if (userSettingsResponse) { + this.currentUserSettings = userSettingsResponse; + this.rootStore.theme.setTheme(userTheme); + return userSettingsResponse; + } + } catch (error) { + console.error("Updating user settings error", error); + return error; + } + }; + + // init load + initialLoad() { + this.setCurrentUser(); + this.setCurrentUserSettings(); + } +} + +export default UserStore; diff --git a/apps/app/types/users.d.ts b/apps/app/types/users.d.ts index ec77df242..5b174cc5a 100644 --- a/apps/app/types/users.d.ts +++ b/apps/app/types/users.d.ts @@ -38,17 +38,6 @@ export interface IUser { [...rest: string]: any; } -export interface ICustomTheme { - background: string; - text: string; - primary: string; - sidebarBackground: string; - sidebarText: string; - darkPalette: boolean; - palette: string; - theme: string; -} - export interface ICurrentUserResponse extends IUser { assigned_issues: number; last_workspace_id: string | null; @@ -158,3 +147,33 @@ export interface IUserProfileProjectSegregation { user_timezone: string; }; } + +export interface ICurrentUser { + id: readonly string; + avatar: string; + first_name: string; + last_name: string; + username: string; + email: string; + mobile_number: string; + is_email_verified: boolean; + is_tour_completed: boolean; + onboarding_step: TOnboardingSteps; + is_onboarded: boolean; + role: string; +} + +export interface ICustomTheme { + background: string; + text: string; + primary: string; + sidebarBackground: string; + sidebarText: string; + darkPalette: boolean; + palette: string; + theme: string; +} + +export interface ICurrentUserSettings { + theme: ICustomTheme; +} diff --git a/yarn.lock b/yarn.lock index a81c5c77d..a11b49918 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6673,6 +6673,18 @@ mkdirp@^0.5.5: dependencies: minimist "^1.2.6" +mobx-react-lite@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.0.3.tgz#f7aa5ac3be558ca19a53b2929d9599679769c2a8" + integrity sha512-wEE1oT5zvDdvplG4HnRrFgPwg5GFVVrEtl42Er85k23zeu3om8H8wbDPgdbQP88zAihVsik6xJfw6VnzUl8fQw== + dependencies: + use-sync-external-store "^1.2.0" + +mobx@^6.10.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.10.0.tgz#3537680fe98d45232cc19cc8f76280bd8bb6b0b7" + integrity sha512-WMbVpCMFtolbB8swQ5E2YRrU+Yu8iLozCVx3CdGjbBKlP7dFiCSuiG06uea3JCFN5DnvtAX7+G5Bp82e2xu0ww== + moo@^0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c"