diff --git a/apps/app/components/auth-screens/not-authorized-view.tsx b/apps/app/components/auth-screens/not-authorized-view.tsx index 0ae730b5b..dc1db8a21 100644 --- a/apps/app/components/auth-screens/not-authorized-view.tsx +++ b/apps/app/components/auth-screens/not-authorized-view.tsx @@ -27,7 +27,7 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { description: "You are not authorized to view this page", }} > -
+
= ({ actionButton, type }) => { alt="ProjectSettingImg" />
-

Oops! You are not authorized to view this page

+

+ Oops! You are not authorized to view this page +

{user ? ( diff --git a/apps/app/components/auth-screens/project/join-project.tsx b/apps/app/components/auth-screens/project/join-project.tsx index 098469114..06ae1c240 100644 --- a/apps/app/components/auth-screens/project/join-project.tsx +++ b/apps/app/components/auth-screens/project/join-project.tsx @@ -41,11 +41,11 @@ export const JoinProject: React.FC = () => { }; return ( -
+
JoinProject
-

You are not a member of this project

+

You are not a member of this project

diff --git a/apps/app/components/core/color-picker-input.tsx b/apps/app/components/core/color-picker-input.tsx new file mode 100644 index 000000000..28b6c4e2c --- /dev/null +++ b/apps/app/components/core/color-picker-input.tsx @@ -0,0 +1,121 @@ +import React from "react"; + +// react-form +import { + FieldError, + FieldErrorsImpl, + Merge, + UseFormRegister, + UseFormSetValue, + UseFormWatch, +} from "react-hook-form"; +// react-color +import { ColorResult, SketchPicker } from "react-color"; +// component +import { Popover, Transition } from "@headlessui/react"; +import { Input } from "components/ui"; +// icons +import { ColorPickerIcon } from "components/icons"; + +type Props = { + name: string; + watch: UseFormWatch; + setValue: UseFormSetValue; + error: FieldError | Merge> | undefined; + register: UseFormRegister; +}; + +export const ColorPickerInput: React.FC = ({ name, watch, setValue, error, register }) => { + const handleColorChange = (newColor: ColorResult) => { + const { hex } = newColor; + setValue(name, hex); + }; + + const getColorText = (colorName: string) => { + switch (colorName) { + case "accent": + return "Accent"; + case "bgBase": + return "Background"; + case "bgSurface1": + return "Background surface 1"; + case "bgSurface2": + return "Background surface 2"; + case "border": + return "Border"; + case "sidebar": + return "Sidebar"; + case "textBase": + return "Text primary"; + case "textSecondary": + return "Text secondary"; + default: + return "Color"; + } + }; + + return ( +

+ +
+ + {({ open }) => ( + <> + + {watch(name) && watch(name) !== "" ? ( + + ) : ( + + )} + + + + + + + + + )} + +
+
+ ); +}; diff --git a/apps/app/components/core/custom-theme-form.tsx b/apps/app/components/core/custom-theme-form.tsx deleted file mode 100644 index ed1009468..000000000 --- a/apps/app/components/core/custom-theme-form.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import { useEffect, useState } from "react"; - -// react-hook-form -import { useForm } from "react-hook-form"; -// ui -import { Input, PrimaryButton, SecondaryButton } from "components/ui"; - -const defaultValues = { - palette: "", -}; - -export const ThemeForm: React.FC = ({ handleFormSubmit, handleClose, status, data }) => { - const { - register, - formState: { errors, isSubmitting }, - handleSubmit, - reset, - } = useForm({ - defaultValues, - }); - const [darkPalette, setDarkPalette] = useState(false); - - const handleUpdateTheme = async (formData: any) => { - await handleFormSubmit({ ...formData, darkPalette }); - - reset({ - ...defaultValues, - }); - }; - - useEffect(() => { - reset({ - ...defaultValues, - ...data, - }); - }, [data, reset]); - - // --color-bg-base: 25, 27, 27; - // --color-bg-surface-1: 31, 32, 35; - // --color-bg-surface-2: 39, 42, 45; - - // --color-border: 46, 50, 52; - // --color-bg-sidebar: 19, 20, 22; - // --color-accent: 60, 133, 217; - - // --color-text-base: 255, 255, 255; - // --color-text-secondary: 142, 148, 146; - - return ( -
-
-

Customize your theme

-
-
-
- -
-
- -
-
- -
- -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- -
-
setDarkPalette((prevData) => !prevData)} - > - Dark palette - -
-
-
-
- Cancel - - {status - ? isSubmitting - ? "Updating Theme..." - : "Update Theme" - : isSubmitting - ? "Creating Theme..." - : "Set Theme"} - -
-
- ); -}; diff --git a/apps/app/components/core/custom-theme-modal.tsx b/apps/app/components/core/custom-theme-modal.tsx deleted file mode 100644 index d46d17d28..000000000 --- a/apps/app/components/core/custom-theme-modal.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from "react"; - -// headless ui -import { Dialog, Transition } from "@headlessui/react"; -// components -import { ThemeForm } from "./custom-theme-form"; -// helpers -import { applyTheme } from "helpers/theme.helper"; -// fetch-keys - -type Props = { - isOpen: boolean; - handleClose: () => void; -}; - -export const CustomThemeModal: React.FC = ({ isOpen, handleClose }) => { - const onClose = () => { - handleClose(); - }; - - const handleFormSubmit = async (formData: any) => { - applyTheme(formData.palette, formData.darkPalette); - onClose(); - }; - - return ( - - - -
- - -
-
- - - - - -
-
-
-
- ); -}; diff --git a/apps/app/components/core/custom-theme-selector.tsx b/apps/app/components/core/custom-theme-selector.tsx new file mode 100644 index 000000000..59aed8b2b --- /dev/null +++ b/apps/app/components/core/custom-theme-selector.tsx @@ -0,0 +1,198 @@ +import React, { useEffect, useState } from "react"; + +import { useTheme } from "next-themes"; + +import { useForm } from "react-hook-form"; + +// hooks +import useUser from "hooks/use-user"; +// ui +import { PrimaryButton } from "components/ui"; +import { ColorPickerInput } from "components/core"; +// services +import userService from "services/user.service"; +// helper +import { applyTheme } from "helpers/theme.helper"; +// types +import { ICustomTheme } from "types"; + +type Props = { + preLoadedData?: Partial | null; +}; + +export const CustomThemeSelector: React.FC = ({ preLoadedData }) => { + const [darkPalette, setDarkPalette] = useState(false); + + const defaultValues = { + accent: preLoadedData?.accent ?? "#FE5050", + bgBase: preLoadedData?.bgBase ?? "#FFF7F7", + bgSurface1: preLoadedData?.bgSurface1 ?? "#FFE0E0", + bgSurface2: preLoadedData?.bgSurface2 ?? "#FFF7F7", + border: preLoadedData?.border ?? "#FFC9C9", + darkPalette: preLoadedData?.darkPalette ?? false, + palette: preLoadedData?.palette ?? "", + sidebar: preLoadedData?.sidebar ?? "#FFFFFF", + textBase: preLoadedData?.textBase ?? "#430000", + textSecondary: preLoadedData?.textSecondary ?? "#323232", + }; + + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + watch, + setValue, + reset, + } = useForm({ + defaultValues, + }); + + const { setTheme } = useTheme(); + const { mutateUser } = useUser(); + + const handleFormSubmit = async (formData: any) => { + await userService + .updateUser({ + theme: { + accent: formData.accent, + bgBase: formData.bgBase, + bgSurface1: formData.bgSurface1, + bgSurface2: formData.bgSurface2, + border: formData.border, + darkPalette: darkPalette, + palette: `${formData.bgBase},${formData.bgSurface1},${formData.bgSurface2},${formData.border},${formData.sidebar},${formData.accent},${formData.textBase},${formData.textSecondary}`, + sidebar: formData.sidebar, + textBase: formData.textBase, + textSecondary: formData.textSecondary, + }, + }) + .then((res) => { + mutateUser((prevData) => { + if (!prevData) return prevData; + return { ...prevData, user: res }; + }, false); + applyTheme(formData.palette, darkPalette); + setTheme("custom"); + }) + .catch((err) => console.log(err)); + }; + + const handleUpdateTheme = async (formData: any) => { + await handleFormSubmit({ ...formData, darkPalette }); + + reset({ + ...defaultValues, + }); + }; + + useEffect(() => { + reset({ + ...defaultValues, + ...preLoadedData, + }); + }, [preLoadedData, reset]); + + return ( +
+
+

Customize your theme

+
+
+
+

Background

+ +
+ +
+

Background surface 1

+ +
+ +
+

Background surface 2

+ +
+ +
+

Border

+ +
+ +
+

Sidebar

+ +
+ +
+

Accent

+ +
+ +
+

Text primary

+ +
+ +
+

Text secondary

+ +
+
+
+
+
+ + {isSubmitting ? "Creating Theme..." : "Set Theme"} + +
+
+ ); +}; diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts index f9c0cebdf..01d87495f 100644 --- a/apps/app/components/core/index.ts +++ b/apps/app/components/core/index.ts @@ -13,3 +13,5 @@ export * from "./link-modal"; export * from "./image-picker-popover"; export * from "./feeds"; export * from "./theme-switch"; +export * from "./custom-theme-selector"; +export * from "./color-picker-input"; diff --git a/apps/app/components/core/theme-switch.tsx b/apps/app/components/core/theme-switch.tsx index f93b1998e..e38375c2e 100644 --- a/apps/app/components/core/theme-switch.tsx +++ b/apps/app/components/core/theme-switch.tsx @@ -1,12 +1,30 @@ -import { useState, useEffect, ChangeEvent } from "react"; -import { useTheme } from "next-themes"; -import { THEMES_OBJ } from "constants/themes"; -import { CustomSelect } from "components/ui"; -import { CustomThemeModal } from "./custom-theme-modal"; +import { useState, useEffect, Dispatch, SetStateAction } from "react"; -export const ThemeSwitch = () => { +import { useTheme } from "next-themes"; + +// constants +import { THEMES_OBJ } from "constants/themes"; +// ui +import { CustomSelect } from "components/ui"; +// helper +import { applyTheme } from "helpers/theme.helper"; +// types +import { ICustomTheme, IUser } from "types"; + +type Props = { + user: IUser | undefined; + setPreLoadedData: Dispatch>; + customThemeSelectorOptions: boolean; + setCustomThemeSelectorOptions: Dispatch>; +}; + +export const ThemeSwitch: React.FC = ({ + user, + setPreLoadedData, + customThemeSelectorOptions, + setCustomThemeSelectorOptions, +}) => { const [mounted, setMounted] = useState(false); - const [customThemeModal, setCustomThemeModal] = useState(false); const { theme, setTheme } = useTheme(); // useEffect only runs on the client, so now we can safely show the UI @@ -18,15 +36,49 @@ export const ThemeSwitch = () => { return null; } + const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme); + return ( <> t.value === theme)?.label : "Select your theme"} + label={ + currentThemeObj ? ( +
+
+
+
+
+ {currentThemeObj.label} +
+ ) : ( + "Select your theme" + ) + } onChange={({ value, type }: { value: string; type: string }) => { if (value === "custom") { - if (!customThemeModal) setCustomThemeModal(true); + if (user?.theme.palette) { + setPreLoadedData(user.theme); + } + if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true); } else { + if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false); const cssVars = [ "--color-bg-base", "--color-bg-surface-1", @@ -41,20 +93,41 @@ export const ThemeSwitch = () => { ]; cssVars.forEach((cssVar) => document.documentElement.style.removeProperty(cssVar)); } - document.documentElement.style.setProperty("color-scheme", type); setTheme(value); + document.documentElement.style.setProperty("color-scheme", type); }} input width="w-full" position="right" > - {THEMES_OBJ.map(({ value, label, type }) => ( + {THEMES_OBJ.map(({ value, label, type, icon }) => ( - {label} +
+
+
+
+
+ {label} +
))} - {/* setCustomThemeModal(false)} /> */} ); }; diff --git a/apps/app/components/icons/color-picker-icon.tsx b/apps/app/components/icons/color-picker-icon.tsx new file mode 100644 index 000000000..f6ec3ad27 --- /dev/null +++ b/apps/app/components/icons/color-picker-icon.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const ColorPickerIcon: React.FC = ({ width = 14, height = 14, className }) => ( + + + +); diff --git a/apps/app/components/icons/index.ts b/apps/app/components/icons/index.ts index 7722c4948..b802121d5 100644 --- a/apps/app/components/icons/index.ts +++ b/apps/app/components/icons/index.ts @@ -74,3 +74,4 @@ export * from "./default-file-icon"; export * from "./video-file-icon"; export * from "./audio-file-icon"; export * from "./command-icon"; +export * from "./color-picker-icon"; diff --git a/apps/app/constants/themes.ts b/apps/app/constants/themes.ts index 8ac745b82..edc517b78 100644 --- a/apps/app/constants/themes.ts +++ b/apps/app/constants/themes.ts @@ -1,35 +1,54 @@ -export const THEMES = [ - "light", - "dark", - "light-contrast", - "dark-contrast", - // "custom" -]; +export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"]; export const THEMES_OBJ = [ { value: "light", label: "Light", type: "light", + icon: { + border: "#DEE2E6", + color1: "#FAFAFA", + color2: "#3F76FF", + }, }, { value: "dark", label: "Dark", type: "dark", + icon: { + border: "#2E3234", + color1: "#191B1B", + color2: "#3C85D9", + }, }, { value: "light-contrast", label: "Light High Contrast", type: "light", + icon: { + border: "#000000", + color1: "#FFFFFF", + color2: "#3F76FF", + }, }, { value: "dark-contrast", label: "Dark High Contrast", type: "dark", + icon: { + border: "#FFFFFF", + color1: "#030303", + color2: "#3A8BE9", + }, + }, + { + value: "custom", + label: "Custom", + type: "light", + icon: { + border: "#FFC9C9", + color1: "#FFF7F7", + color2: "#FF5151", + }, }, - // { - // value: "custom", - // label: "Custom", - // type: "light", - // }, ]; diff --git a/apps/app/contexts/theme.context.tsx b/apps/app/contexts/theme.context.tsx index c51cdca1a..318f04095 100644 --- a/apps/app/contexts/theme.context.tsx +++ b/apps/app/contexts/theme.context.tsx @@ -4,10 +4,14 @@ import { useRouter } from "next/router"; import useSWR from "swr"; // components import ToastAlert from "components/toast-alert"; +// hooks +import useUser from "hooks/use-user"; // services import projectService from "services/project.service"; // fetch-keys import { USER_PROJECT_VIEW } from "constants/fetch-keys"; +// helper +import { applyTheme } from "helpers/theme.helper"; // constants export const themeContext = createContext({} as ContextType); @@ -61,6 +65,7 @@ export const reducer: ReducerFunctionType = (state, action) => { export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); + const { user } = useUser(); const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -85,6 +90,15 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ }); }, [myViewProps]); + useEffect(() => { + const theme = localStorage.getItem("theme"); + if (theme && theme === "custom") { + if (user && user.theme.palette) { + applyTheme(user.theme.palette, user.theme.darkPalette); + } + } + }, [user]); + return ( = ({ profilePage = false }) => { label: "Activity", href: `/${workspaceSlug}/me/profile/activity`, }, + { + label: "Preferences", + href: `/${workspaceSlug}/me/profile/preferences`, + }, ]; return ( diff --git a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx index 5fbf3ef2a..b6c3d36b9 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/index.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/index.tsx @@ -13,7 +13,7 @@ import useToast from "hooks/use-toast"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; // components -import { ImageUploadModal, ThemeSwitch } from "components/core"; +import { ImageUploadModal } from "components/core"; // ui import { CustomSelect, DangerButton, Input, SecondaryButton, Spinner } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; @@ -280,17 +280,6 @@ const Profile: NextPage = () => { />
-
-
-

Theme

-

- Select or customize your interface color scheme. -

-
-
- -
-
{isSubmitting ? "Updating..." : "Update profile"} diff --git a/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx b/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx new file mode 100644 index 000000000..5ea79014d --- /dev/null +++ b/apps/app/pages/[workspaceSlug]/me/profile/preferences.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState } from "react"; +import { useTheme } from "next-themes"; + +// hooks +import useUser from "hooks/use-user"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +import SettingsNavbar from "layouts/settings-navbar"; +// components +import { CustomThemeSelector, ThemeSwitch } from "components/core"; +// ui +import { Spinner } from "components/ui"; +import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; +// types +import { ICustomTheme } from "types"; + +const ProfilePreferences = () => { + const { user: myProfile } = useUser(); + const { theme } = useTheme(); + const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false); + const [preLoadedData, setPreLoadedData] = useState(null); + + useEffect(() => { + if (theme === "custom") { + if (myProfile?.theme.palette) setPreLoadedData(myProfile.theme); + if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true); + } + }, [myProfile, theme]); + + return ( + + + + } + > + {myProfile ? ( +
+
+
+

Profile Settings

+

+ This information will be visible to only you. +

+
+ +
+
+
+
+

Theme

+

+ Select or customize your interface color scheme. +

+
+
+ +
+
+ {customThemeSelectorOptions && } +
+
+ ) : ( +
+ +
+ )} +
+ ); +}; + +export default ProfilePreferences; diff --git a/apps/app/types/users.d.ts b/apps/app/types/users.d.ts index 2d90c8b57..eb7f5c742 100644 --- a/apps/app/types/users.d.ts +++ b/apps/app/types/users.d.ts @@ -18,6 +18,7 @@ export interface IUser { is_onboarded: boolean; token: string; role: string; + theme: ICustomTheme; my_issues_prop?: { properties: Properties; @@ -27,6 +28,19 @@ export interface IUser { [...rest: string]: any; } +export interface ICustomTheme { + accent: string; + bgBase: string; + bgSurface1: string; + bgSurface2: string; + border: string; + darkPalette: boolean; + palette: string; + sidebar: string; + textBase: string; + textSecondary: string; +} + export interface ICurrentUserResponse { assigned_issues: number; user: IUser;