diff --git a/frontend/app/src/App.tsx b/frontend/app/src/App.tsx index ddfa0a5..20e68bd 100644 --- a/frontend/app/src/App.tsx +++ b/frontend/app/src/App.tsx @@ -2,11 +2,12 @@ import React, { createContext } from "react"; import { ConfigProvider, message } from "antd"; import "./App.css"; import HeaderComponent from "./components/HeaderComponent"; -import { theme } from "./config/style"; -import { Provider } from "react-redux"; -import { store } from "./config/store"; +import { darkTheme, lightTheme, theme } from "./config/style"; +import { Provider, useSelector } from "react-redux"; +import { StorePrototype, store } from "./config/store"; import { MessageInstance } from "antd/es/message/interface"; import AppRoutes from "./pages/AppRoutes"; +import ThemeProviderWrapper from "./config/ThemeProviderWrapper"; export const MessageContext = createContext({} as MessageInstance); @@ -17,7 +18,7 @@ const App = () => { return ( - +
{contextHolder} @@ -26,7 +27,7 @@ const App = () => {
-
+
); }; diff --git a/frontend/app/src/components/HeaderComponent.tsx b/frontend/app/src/components/HeaderComponent.tsx index cc57ff3..45b7b36 100644 --- a/frontend/app/src/components/HeaderComponent.tsx +++ b/frontend/app/src/components/HeaderComponent.tsx @@ -3,15 +3,23 @@ import { GlobalOutlined, LogoutOutlined, MenuOutlined, + MoonOutlined, PicCenterOutlined, SettingOutlined, + SunOutlined, UserOutlined, } from "@ant-design/icons"; -import { Drawer, Layout, Menu, MenuProps } from "antd"; +import { Drawer, Layout, Menu, MenuProps, Switch } from "antd"; import React, { useEffect, useState } from "react"; import AuthModal from "./AuthModal"; import "./styles.css"; -import { StorePrototype, logOut, setLanguage, store } from "../config/store"; +import { + StorePrototype, + logOut, + setLanguage, + setTheme, + store, +} from "../config/store"; import { useSelector } from "react-redux"; import tr from "../config/translation"; import { Link, useNavigate } from "react-router-dom"; @@ -43,6 +51,10 @@ const HeaderComponent = () => { (state: StorePrototype) => state.auth.user ); + const currentTheme: string | undefined = useSelector( + (state: StorePrototype) => state.settings.theme + ); + const [selectedKeys, setSelectedKeys] = useState([]); useEffect(() => { const keys = []; @@ -113,6 +125,18 @@ const HeaderComponent = () => { children: languageSelectItems, style: { background: "#001529" }, }, + { + label: ( + + store.dispatch(setTheme(v ? "light" : "dark")) + } + defaultChecked={currentTheme === "light"} + /> + ), + key: "theme", + icon: currentTheme === "dark" ? : , + }, { label: user ? user.username : tr("Log in"), key: "login", @@ -167,6 +191,18 @@ const HeaderComponent = () => { onClick: () => !user && setAuthModalOpen(true), ...(user ? { children: userMenuItems } : {}), }, + { + label: ( + + store.dispatch(setTheme(v ? "light" : "dark")) + } + defaultChecked={currentTheme === "light"} + /> + ), + key: "theme", + icon: currentTheme === "dark" ? : , + }, ]; return ( diff --git a/frontend/app/src/config/ThemeProviderWrapper.tsx b/frontend/app/src/config/ThemeProviderWrapper.tsx new file mode 100644 index 0000000..6798350 --- /dev/null +++ b/frontend/app/src/config/ThemeProviderWrapper.tsx @@ -0,0 +1,22 @@ +import React, { ReactNode } from "react"; +import { useSelector } from "react-redux"; +import { StorePrototype } from "./store"; +import { ConfigProvider } from "antd"; +import { darkTheme, lightTheme } from "./style"; +import PropTypes from "prop-types"; + +const ThemeProviderWrapper = ({ children }: { children: ReactNode }) => { + const theme = useSelector((state: StorePrototype) => state.settings.theme); + + return ( + + {children} + + ); +}; + +ThemeProviderWrapper.propTypes = { + children: PropTypes.node, +}; + +export default ThemeProviderWrapper; diff --git a/frontend/app/src/config/store.ts b/frontend/app/src/config/store.ts index 516f325..6651ffc 100644 --- a/frontend/app/src/config/store.ts +++ b/frontend/app/src/config/store.ts @@ -23,10 +23,12 @@ const initialAuthDataState: AuthDataType = { export type SettingsType = { language: string | undefined; + theme: string | undefined; }; const initialSettingsState: SettingsType = { language: undefined, + theme: undefined, }; export type StorePrototype = { @@ -44,6 +46,8 @@ export const logOut = createAction("auth/logOut"); export const setLanguage = createAction("settings/setLanguage"); export const loadLanguage = createAction("settings/loadLanguage"); +export const setTheme = createAction("settings/setTheme"); +export const loadTheme = createAction("settings/loadTheme"); export const store = configureStore({ reducer: { @@ -94,6 +98,19 @@ export const store = configureStore({ state.language = "en"; } }); + builder.addCase(setTheme, (state, action) => { + state.theme = action.payload || "dark"; + localStorage.setItem("theme", action.payload || "dark"); + }); + builder.addCase(loadTheme, (state) => { + const theme: string | null = localStorage.getItem("theme"); + if (theme) { + state.theme = theme; + } else { + const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); + state.theme = darkThemeMq.matches ? "dark" : "light"; + } + }); }), }, // Adding the api middleware enables caching, invalidation, polling, diff --git a/frontend/app/src/config/style.ts b/frontend/app/src/config/style.ts index 8f8eb5d..a46e01a 100644 --- a/frontend/app/src/config/style.ts +++ b/frontend/app/src/config/style.ts @@ -1,6 +1,6 @@ import { ThemeConfig } from "antd"; -export const theme: ThemeConfig = { +export const darkTheme: ThemeConfig = { token: { colorText: "white", colorIcon: "white", @@ -18,3 +18,13 @@ export const theme: ThemeConfig = { }, }, }; + +export const lightTheme: ThemeConfig = { + token: { + colorPrimary: "#00d8a4", + colorIconHover: "#00d8a4", + borderRadius: 5, + fontFamily: "Comfortaa", + // colorWarningBg: "#001529", + }, +}; diff --git a/frontend/app/src/pages/AppRoutes.tsx b/frontend/app/src/pages/AppRoutes.tsx index b07c733..48d5cfc 100644 --- a/frontend/app/src/pages/AppRoutes.tsx +++ b/frontend/app/src/pages/AppRoutes.tsx @@ -5,6 +5,7 @@ import { getLocalClient, getLocalToken, loadLanguage, + loadTheme, store, } from "../config/store"; import DashboardPage from "./DashboardPage"; @@ -19,6 +20,7 @@ const AppRoutes = ({ children }: { children: ReactNode }) => { store.dispatch(getLocalToken()); store.dispatch(getLocalClient()); store.dispatch(loadLanguage()); + store.dispatch(loadTheme()); return (