chore: profile themning

This commit is contained in:
gurusainath 2024-05-06 14:00:19 +05:30
parent 48ad9d5fea
commit e95cd1e385
7 changed files with 108 additions and 110 deletions

View File

@ -27,7 +27,7 @@ export interface IUser {
user_timezone: string; user_timezone: string;
username: string; username: string;
last_login_medium: TLoginMediums; last_login_medium: TLoginMediums;
// theme: IUserTheme; theme: IUserTheme;
} }
export interface IUserAccount { export interface IUserAccount {
@ -48,7 +48,7 @@ export type TUserProfile = {
palette: string | undefined; palette: string | undefined;
primary: string | undefined; primary: string | undefined;
background: string | undefined; background: string | undefined;
darkPalette: string | undefined; darkPalette: boolean | undefined;
sidebarText: string | undefined; sidebarText: string | undefined;
sidebarBackground: string | undefined; sidebarBackground: string | undefined;
}; };
@ -80,14 +80,14 @@ export interface IUserSettings {
} }
export interface IUserTheme { export interface IUserTheme {
background: string; text: string | undefined;
text: string; theme: string | undefined;
primary: string; palette: string | undefined;
sidebarBackground: string; primary: string | undefined;
sidebarText: string; background: string | undefined;
darkPalette: boolean; darkPalette: boolean | undefined;
palette: string; sidebarText: string | undefined;
theme: string; sidebarBackground: string | undefined;
} }
export interface IUserLite { export interface IUserLite {

View File

@ -4,9 +4,9 @@ import { Controller, useForm } from "react-hook-form";
// types // types
import { IUserTheme } from "@plane/types"; import { IUserTheme } from "@plane/types";
// ui // ui
import { Button, InputColorPicker } from "@plane/ui"; import { Button, InputColorPicker, setPromiseToast } from "@plane/ui";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUserProfile } from "@/hooks/store";
const inputRules = { const inputRules = {
required: "Background color is required", required: "Background color is required",
@ -25,13 +25,9 @@ const inputRules = {
}; };
export const CustomThemeSelector: React.FC = observer(() => { export const CustomThemeSelector: React.FC = observer(() => {
const {
userProfile: { data: userProfile },
} = useUser();
const userTheme: any = userProfile?.theme;
// hooks
const { setTheme } = useTheme(); const { setTheme } = useTheme();
// hooks
const { data: userProfile, updateUserTheme } = useUserProfile();
const { const {
control, control,
@ -40,17 +36,18 @@ export const CustomThemeSelector: React.FC = observer(() => {
watch, watch,
} = useForm<IUserTheme>({ } = useForm<IUserTheme>({
defaultValues: { defaultValues: {
background: userTheme?.background !== "" ? userTheme?.background : "#0d101b", background: userProfile?.theme?.background !== "" ? userProfile?.theme?.background : "#0d101b",
text: userTheme?.text !== "" ? userTheme?.text : "#c5c5c5", text: userProfile?.theme?.text !== "" ? userProfile?.theme?.text : "#c5c5c5",
primary: userTheme?.primary !== "" ? userTheme?.primary : "#3f76ff", primary: userProfile?.theme?.primary !== "" ? userProfile?.theme?.primary : "#3f76ff",
sidebarBackground: userTheme?.sidebarBackground !== "" ? userTheme?.sidebarBackground : "#0d101b", sidebarBackground:
sidebarText: userTheme?.sidebarText !== "" ? userTheme?.sidebarText : "#c5c5c5", userProfile?.theme?.sidebarBackground !== "" ? userProfile?.theme?.sidebarBackground : "#0d101b",
darkPalette: userTheme?.darkPalette || false, sidebarText: userProfile?.theme?.sidebarText !== "" ? userProfile?.theme?.sidebarText : "#c5c5c5",
palette: userTheme?.palette !== "" ? userTheme?.palette : "", darkPalette: userProfile?.theme?.darkPalette || false,
palette: userProfile?.theme?.palette !== "" ? userProfile?.theme?.palette : "",
}, },
}); });
const handleUpdateTheme = async (formData: any) => { const handleUpdateTheme = async (formData: Partial<IUserTheme>) => {
const payload: IUserTheme = { const payload: IUserTheme = {
background: formData.background, background: formData.background,
text: formData.text, text: formData.text,
@ -61,12 +58,22 @@ export const CustomThemeSelector: React.FC = observer(() => {
palette: `${formData.background},${formData.text},${formData.primary},${formData.sidebarBackground},${formData.sidebarText}`, palette: `${formData.background},${formData.text},${formData.primary},${formData.sidebarBackground},${formData.sidebarText}`,
theme: "custom", theme: "custom",
}; };
setTheme("custom"); setTheme("custom");
console.log(payload); const updateCurrentUserThemePromise = updateUserTheme(payload);
setPromiseToast(updateCurrentUserThemePromise, {
loading: "Updating theme...",
success: {
title: "Success!",
message: () => "Theme updated successfully!",
},
error: {
title: "Error!",
message: () => "Failed to Update the theme",
},
});
// return updateUserProfile({ theme: payload }); return;
}; };
const handleValueChange = (val: string | undefined, onChange: any) => { const handleValueChange = (val: string | undefined, onChange: any) => {

View File

@ -5,7 +5,7 @@ import { useTheme } from "next-themes";
// helpers // helpers
import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper"; import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper";
// hooks // hooks
import { useAppRouter, useAppTheme, useUser } from "@/hooks/store"; import { useAppRouter, useAppTheme, useUserProfile } from "@/hooks/store";
type TStoreWrapper = { type TStoreWrapper = {
children: ReactNode; children: ReactNode;
@ -20,9 +20,7 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
// store hooks // store hooks
const { setQuery } = useAppRouter(); const { setQuery } = useAppRouter();
const { sidebarCollapsed, toggleSidebar } = useAppTheme(); const { sidebarCollapsed, toggleSidebar } = useAppTheme();
const { const { data: userProfile } = useUserProfile();
userProfile: { data: userProfile },
} = useUser();
// states // states
const [dom, setDom] = useState<undefined | HTMLElement>(); const [dom, setDom] = useState<undefined | HTMLElement>();
@ -40,14 +38,19 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
* Setting up the theme of the user by fetching it from local storage * Setting up the theme of the user by fetching it from local storage
*/ */
useEffect(() => { useEffect(() => {
if (!userProfile) return; if (!userProfile?.theme?.theme) return;
if (window) setDom(window.document?.querySelector<HTMLElement>("[data-theme='custom']") || undefined); if (window) setDom(() => window.document?.querySelector<HTMLElement>("[data-theme='custom']") || undefined);
setTheme(userProfile?.theme?.theme || "system"); setTheme(userProfile?.theme?.theme || "system");
if (userProfile?.theme?.theme === "custom" && userProfile?.theme?.palette && dom) if (userProfile?.theme?.theme === "custom" && userProfile?.theme?.palette && dom)
applyTheme(userProfile?.theme?.palette, false); applyTheme(
userProfile?.theme?.palette !== ",,,,"
? userProfile?.theme?.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
false
);
else unsetCustomCssVariables(); else unsetCustomCssVariables();
}, [userProfile, setTheme, dom]); }, [userProfile, userProfile?.theme?.theme, userProfile?.theme?.palette, setTheme, dom]);
useEffect(() => { useEffect(() => {
if (!router.query) return; if (!router.query) return;

View File

@ -2,64 +2,55 @@ import { useEffect, useState, ReactElement } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// ui // ui
import { import { Spinner, setPromiseToast } from "@plane/ui";
Spinner,
// setPromiseToast
} from "@plane/ui";
// components // components
import { CustomThemeSelector, ThemeSwitch, PageHead } from "@/components/core"; import { CustomThemeSelector, ThemeSwitch, PageHead } from "@/components/core";
// constants // constants
import { I_THEME_OPTION, THEME_OPTIONS } from "@/constants/themes"; import { I_THEME_OPTION, THEME_OPTIONS } from "@/constants/themes";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUserProfile } from "@/hooks/store";
// layouts // layouts
import { ProfilePreferenceSettingsLayout } from "@/layouts/settings-layout/profile/preferences"; import { ProfilePreferenceSettingsLayout } from "@/layouts/settings-layout/profile/preferences";
// type // type
import { NextPageWithLayout } from "@/lib/types"; import { NextPageWithLayout } from "@/lib/types";
const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
const { setTheme } = useTheme();
// states // states
const [currentTheme, setCurrentTheme] = useState<I_THEME_OPTION | null>(null); const [currentTheme, setCurrentTheme] = useState<I_THEME_OPTION | null>(null);
// store hooks
const {
data: currentUser,
userProfile: { data: userProfile },
} = useUser();
// computed
const userTheme = userProfile?.theme;
// hooks // hooks
const { setTheme } = useTheme(); const { data: userProfile, updateUserTheme } = useUserProfile();
useEffect(() => { useEffect(() => {
if (userTheme) { if (userProfile?.theme?.theme) {
const userThemeOption = THEME_OPTIONS.find((t) => t.value === userTheme?.theme); const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme);
if (userThemeOption) { if (userThemeOption) {
setCurrentTheme(userThemeOption); setCurrentTheme(userThemeOption);
} }
} }
}, [userTheme]); }, [userProfile?.theme?.theme]);
const handleThemeChange = (themeOption: I_THEME_OPTION) => { const handleThemeChange = (themeOption: I_THEME_OPTION) => {
setTheme(themeOption.value); setTheme(themeOption.value);
// const updateCurrentUserThemePromise = updateCurrentUserTheme(themeOption.value); const updateCurrentUserThemePromise = updateUserTheme({ theme: themeOption.value });
// setPromiseToast(updateCurrentUserThemePromise, { setPromiseToast(updateCurrentUserThemePromise, {
// loading: "Updating theme...", loading: "Updating theme...",
// success: { success: {
// title: "Success!", title: "Success!",
// message: () => "Theme updated successfully!", message: () => "Theme updated successfully!",
// }, },
// error: { error: {
// title: "Error!", title: "Error!",
// message: () => "Failed to Update the theme", message: () => "Failed to Update the theme",
// }, },
// }); });
}; };
return ( return (
<> <>
<PageHead title="Profile - Theme Prefrence" /> <PageHead title="Profile - Theme Prefrence" />
{currentUser ? ( {userProfile ? (
<div className="mx-auto mt-10 h-full w-full overflow-y-auto md:px-6 px-4 pb-8 md:mt-14 lg:px-20 vertical-scrollbar scrollbar-md"> <div className="mx-auto mt-10 h-full w-full overflow-y-auto md:px-6 px-4 pb-8 md:mt-14 lg:px-20 vertical-scrollbar scrollbar-md">
<div className="flex items-center border-b border-custom-border-100 pb-3.5"> <div className="flex items-center border-b border-custom-border-100 pb-3.5">
<h3 className="text-xl font-medium">Preferences</h3> <h3 className="text-xl font-medium">Preferences</h3>
@ -73,7 +64,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
<ThemeSwitch value={currentTheme} onChange={handleThemeChange} /> <ThemeSwitch value={currentTheme} onChange={handleThemeChange} />
</div> </div>
</div> </div>
{userTheme?.theme === "custom" && <CustomThemeSelector />} {userProfile?.theme?.theme === "custom" && <CustomThemeSelector />}
</div> </div>
) : ( ) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0"> <div className="grid h-full w-full place-items-center px-4 sm:px-0">

View File

@ -1,18 +1,15 @@
// mobx
import { action, observable, makeObservable } from "mobx"; import { action, observable, makeObservable } from "mobx";
// helper // store types
import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper"; import { RootStore } from "@/store/root.store";
export interface IThemeStore { export interface IThemeStore {
// observables // observables
theme: string | null;
sidebarCollapsed: boolean | undefined; sidebarCollapsed: boolean | undefined;
profileSidebarCollapsed: boolean | undefined; profileSidebarCollapsed: boolean | undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined; workspaceAnalyticsSidebarCollapsed: boolean | undefined;
issueDetailSidebarCollapsed: boolean | undefined; issueDetailSidebarCollapsed: boolean | undefined;
// actions // actions
toggleSidebar: (collapsed?: boolean) => void; toggleSidebar: (collapsed?: boolean) => void;
setTheme: (theme: any) => void;
toggleProfileSidebar: (collapsed?: boolean) => void; toggleProfileSidebar: (collapsed?: boolean) => void;
toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void; toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void;
toggleIssueDetailSidebar: (collapsed?: boolean) => void; toggleIssueDetailSidebar: (collapsed?: boolean) => void;
@ -21,31 +18,23 @@ export interface IThemeStore {
export class ThemeStore implements IThemeStore { export class ThemeStore implements IThemeStore {
// observables // observables
sidebarCollapsed: boolean | undefined = undefined; sidebarCollapsed: boolean | undefined = undefined;
theme: string | null = null;
profileSidebarCollapsed: boolean | undefined = undefined; profileSidebarCollapsed: boolean | undefined = undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined; workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined;
issueDetailSidebarCollapsed: boolean | undefined = undefined; issueDetailSidebarCollapsed: boolean | undefined = undefined;
// root store
rootStore;
constructor(_rootStore: any | null = null) { constructor(private store: RootStore) {
makeObservable(this, { makeObservable(this, {
// observable // observable
sidebarCollapsed: observable.ref, sidebarCollapsed: observable.ref,
theme: observable.ref,
profileSidebarCollapsed: observable.ref, profileSidebarCollapsed: observable.ref,
workspaceAnalyticsSidebarCollapsed: observable.ref, workspaceAnalyticsSidebarCollapsed: observable.ref,
issueDetailSidebarCollapsed: observable.ref, issueDetailSidebarCollapsed: observable.ref,
// action // action
toggleSidebar: action, toggleSidebar: action,
setTheme: action,
toggleProfileSidebar: action, toggleProfileSidebar: action,
toggleWorkspaceAnalyticsSidebar: action, toggleWorkspaceAnalyticsSidebar: action,
toggleIssueDetailSidebar: action, toggleIssueDetailSidebar: action,
// computed
}); });
// root store
this.rootStore = _rootStore;
} }
/** /**
@ -95,30 +84,4 @@ export class ThemeStore implements IThemeStore {
} }
localStorage.setItem("issue_detail_sidebar_collapsed", this.issueDetailSidebarCollapsed.toString()); localStorage.setItem("issue_detail_sidebar_collapsed", this.issueDetailSidebarCollapsed.toString());
}; };
/**
* Sets the user theme and applies it to the platform
* @param _theme
*/
setTheme = async (_theme: { theme: any }) => {
try {
const currentTheme: string = _theme?.theme?.theme?.toString();
// updating the local storage theme value
localStorage.setItem("theme", currentTheme);
// updating the mobx theme value
this.theme = currentTheme;
// applying the theme to platform if the selected theme is custom
if (currentTheme === "custom") {
const themeSettings = this.rootStore.user.currentUserSettings || null;
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);
}
};
} }

View File

@ -1,9 +1,11 @@
import cloneDeep from "lodash/cloneDeep";
import set from "lodash/set"; import set from "lodash/set";
import { action, makeObservable, observable, runInAction } from "mobx"; import { action, makeObservable, observable, runInAction } from "mobx";
// services // services
import { UserService } from "services/user.service"; import { UserService } from "services/user.service";
// types // types
import { TUserProfile } from "@plane/types"; import { IUserTheme, TUserProfile } from "@plane/types";
import { RootStore } from "@/store/root.store";
type TError = { type TError = {
status: string; status: string;
@ -20,6 +22,7 @@ export interface IUserProfileStore {
updateUserProfile: (data: Partial<TUserProfile>) => Promise<TUserProfile | undefined>; updateUserProfile: (data: Partial<TUserProfile>) => Promise<TUserProfile | undefined>;
updateUserOnBoard: () => Promise<TUserProfile | undefined>; updateUserOnBoard: () => Promise<TUserProfile | undefined>;
updateTourCompleted: () => Promise<TUserProfile | undefined>; updateTourCompleted: () => Promise<TUserProfile | undefined>;
updateUserTheme: (data: Partial<IUserTheme>) => Promise<TUserProfile | undefined>;
} }
export class ProfileStore implements IUserProfileStore { export class ProfileStore implements IUserProfileStore {
@ -59,7 +62,7 @@ export class ProfileStore implements IUserProfileStore {
// services // services
userService: UserService; userService: UserService;
constructor() { constructor(public store: RootStore) {
makeObservable(this, { makeObservable(this, {
// observables // observables
isLoading: observable.ref, isLoading: observable.ref,
@ -70,6 +73,7 @@ export class ProfileStore implements IUserProfileStore {
updateUserProfile: action, updateUserProfile: action,
updateUserOnBoard: action, updateUserOnBoard: action,
updateTourCompleted: action, updateTourCompleted: action,
updateUserTheme: action,
}); });
// services // services
this.userService = new UserService(); this.userService = new UserService();
@ -179,4 +183,34 @@ export class ProfileStore implements IUserProfileStore {
throw error; throw error;
} }
}; };
/**
* @description updates the user theme
* @returns @returns {Promise<TUserProfile | undefined>}
*/
updateUserTheme = async (data: Partial<IUserTheme>) => {
const currentProfileTheme = cloneDeep(this.data.theme);
try {
runInAction(() => {
Object.keys(data).forEach((key: string) => {
const userKey: keyof IUserTheme = key as keyof IUserTheme;
if (this.data.theme) set(this.data.theme, userKey, data[userKey]);
});
});
const userProfile = await this.userService.updateCurrentUserProfile({ theme: this.data.theme });
return userProfile;
} catch (error) {
runInAction(() => {
Object.keys(data).forEach((key: string) => {
const userKey: keyof IUserTheme = key as keyof IUserTheme;
if (currentProfileTheme) set(this.data.theme, userKey, currentProfileTheme[userKey]);
});
this.error = {
status: "user-profile-theme-update-error",
message: "Failed to update user profile theme",
};
});
throw error;
}
};
} }

View File

@ -2759,7 +2759,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@^18.2.42", "@types/react@^18.2.48": "@types/react@*", "@types/react@18.2.48", "@types/react@^18.2.42", "@types/react@^18.2.48":
version "18.2.48" version "18.2.48"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1"
integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w== integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==