feature/IO-1113-Online-Dark-Mode - Toggle / Local storage solution
This commit is contained in:
@@ -2,15 +2,19 @@ import { ApolloProvider } from "@apollo/client";
|
||||
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||
import { ConfigProvider } from "antd";
|
||||
import enLocale from "antd/es/locale/en_US";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { connect, useSelector } from "react-redux";
|
||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||
import client from "../utils/GraphQLClient";
|
||||
import App from "./App";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import getTheme from "./themeProvider";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectCurrentUser } from "../redux/user/user.selectors.js";
|
||||
import { selectDarkMode } from "../redux/application/application.selectors";
|
||||
import { setDarkMode } from "../redux/application/application.actions";
|
||||
|
||||
// Base Split configuration
|
||||
const config = {
|
||||
@@ -32,17 +36,47 @@ function SplitClientProvider({ children }) {
|
||||
return children;
|
||||
}
|
||||
|
||||
function AppContainer() {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkMode, setIsDarkMode] = useState(true); // Manage dark mode state
|
||||
const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]); // Memoize theme
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setDarkMode: (isDarkMode) => dispatch(setDarkMode(isDarkMode))
|
||||
});
|
||||
|
||||
// Set data-theme attribute on document root
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
function AppContainer({ currentUser, setDarkMode }) {
|
||||
const { t } = useTranslation();
|
||||
const isDarkMode = useSelector(selectDarkMode);
|
||||
const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]);
|
||||
|
||||
// Update data-theme attribute when dark mode changes
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", isDarkMode ? "dark" : "light");
|
||||
return () => document.documentElement.removeAttribute("data-theme"); // Cleanup
|
||||
return () => document.documentElement.removeAttribute("data-theme");
|
||||
}, [isDarkMode]);
|
||||
|
||||
// Sync Redux darkMode with localStorage on user change
|
||||
useEffect(() => {
|
||||
if (currentUser?.uid) {
|
||||
const savedMode = localStorage.getItem(`dark-mode-${currentUser.uid}`);
|
||||
if (savedMode !== null) {
|
||||
setDarkMode(JSON.parse(savedMode));
|
||||
} else {
|
||||
setDarkMode(false); // default to light mode
|
||||
}
|
||||
} else {
|
||||
setDarkMode(false);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [currentUser?.uid]);
|
||||
|
||||
// Persist darkMode to localStorage when it or user changes
|
||||
useEffect(() => {
|
||||
if (currentUser?.uid) {
|
||||
localStorage.setItem(`dark-mode-${currentUser.uid}`, JSON.stringify(isDarkMode));
|
||||
}
|
||||
}, [isDarkMode, currentUser?.uid]);
|
||||
|
||||
return (
|
||||
<CookiesProvider>
|
||||
<ApolloProvider client={client}>
|
||||
@@ -60,10 +94,6 @@ function AppContainer() {
|
||||
<SplitFactoryProvider config={config}>
|
||||
<SplitClientProvider>
|
||||
<App />
|
||||
{/* Optional: Button to toggle dark mode for testing */}
|
||||
<button onClick={() => setIsDarkMode(!isDarkMode)}>
|
||||
{t("general.toggleDarkMode")}
|
||||
</button>
|
||||
</SplitClientProvider>
|
||||
</SplitFactoryProvider>
|
||||
</ConfigProvider>
|
||||
@@ -72,4 +102,4 @@ function AppContainer() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Sentry.withProfiler(AppContainer);
|
||||
export default Sentry.withProfiler(connect(mapStateToProps, mapDispatchToProps)(AppContainer));
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
UsergroupAddOutlined,
|
||||
UserOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { FaCalendarAlt, FaCarCrash, FaTasks } from "react-icons/fa";
|
||||
import { FaCalendarAlt, FaCarCrash, FaMoon, FaSun, FaTasks } from "react-icons/fa";
|
||||
import { BsKanban } from "react-icons/bs";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
import { GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||
@@ -33,6 +33,7 @@ import { RiSurveyLine } from "react-icons/ri";
|
||||
import { IoBusinessOutline } from "react-icons/io5";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import LockWrapper from "../../components/lock-wrapper/lock-wrapper.component.jsx";
|
||||
import { Tooltip } from "antd";
|
||||
|
||||
const buildLeftMenuItems = ({
|
||||
t,
|
||||
@@ -41,7 +42,9 @@ const buildLeftMenuItems = ({
|
||||
setTaskUpsertContext,
|
||||
setReportCenterContext,
|
||||
signOutStart,
|
||||
accountingChildren
|
||||
accountingChildren,
|
||||
handleDarkModeToggle,
|
||||
darkMode
|
||||
}) => {
|
||||
return [
|
||||
{
|
||||
@@ -331,6 +334,15 @@ const buildLeftMenuItems = ({
|
||||
label: t("user.actions.signout"),
|
||||
onClick: () => signOutStart()
|
||||
},
|
||||
{
|
||||
key: "darkmode-toggle",
|
||||
id: "header-darkmode-toggle",
|
||||
label: darkMode ? t("user.actions.light_theme") : t("user.actions.dark_theme"),
|
||||
icon: (
|
||||
<Tooltip title={darkMode ? t("Light mode") : t("Dark mode")}>{darkMode ? <FaSun /> : <FaMoon />}</Tooltip>
|
||||
),
|
||||
onClick: handleDarkModeToggle
|
||||
},
|
||||
{
|
||||
key: "help",
|
||||
id: "header-help",
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { TASKS_CENTER_POLL_INTERVAL, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
||||
import { QUERY_MY_TASKS_COUNT } from "../../graphql/tasks.queries.js";
|
||||
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||
import { selectDarkMode, selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
@@ -22,13 +22,15 @@ import NotificationCenterContainer from "../notification-center/notification-cen
|
||||
import TaskCenterContainer from "../task-center/task-center.container.jsx";
|
||||
import buildAccountingChildren from "./buildAccountingChildren.jsx";
|
||||
import buildLeftMenuItems from "./buildLeftMenuItems.jsx";
|
||||
import { toggleDarkMode } from "../../redux/application/application.actions";
|
||||
|
||||
// --- Redux mappings ---
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
recentItems: selectRecentItems,
|
||||
selectedHeader: selectSelectedHeader,
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
darkMode: selectDarkMode
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
@@ -38,7 +40,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
setReportCenterContext: (context) => dispatch(setModalContext({ context, modal: "reportCenter" })),
|
||||
signOutStart: () => dispatch(signOutStart()),
|
||||
setCardPaymentContext: (context) => dispatch(setModalContext({ context, modal: "cardPayment" })),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" })),
|
||||
toggleDarkMode: () => dispatch(toggleDarkMode())
|
||||
});
|
||||
|
||||
// --- Utility Hooks ---
|
||||
@@ -84,22 +87,22 @@ function useIncompleteTaskCount(assignedToId, bodyshopId, isEmployee, isConnecte
|
||||
}
|
||||
|
||||
// --- Main Component ---
|
||||
function Header(props) {
|
||||
const {
|
||||
handleMenuClick,
|
||||
currentUser,
|
||||
bodyshop,
|
||||
selectedHeader,
|
||||
signOutStart,
|
||||
setBillEnterContext,
|
||||
setTimeTicketContext,
|
||||
setPaymentContext,
|
||||
setReportCenterContext,
|
||||
recentItems,
|
||||
setCardPaymentContext,
|
||||
setTaskUpsertContext
|
||||
} = props;
|
||||
|
||||
function Header({
|
||||
handleMenuClick,
|
||||
currentUser,
|
||||
bodyshop,
|
||||
selectedHeader,
|
||||
signOutStart,
|
||||
setBillEnterContext,
|
||||
setTimeTicketContext,
|
||||
setPaymentContext,
|
||||
setReportCenterContext,
|
||||
recentItems,
|
||||
setCardPaymentContext,
|
||||
setTaskUpsertContext,
|
||||
toggleDarkMode,
|
||||
darkMode
|
||||
}) {
|
||||
// Feature flags
|
||||
const {
|
||||
treatments: { ImEXPay, DmsAp, Simple_Inventory }
|
||||
@@ -216,6 +219,10 @@ function Header(props) {
|
||||
[handleMenuClick]
|
||||
);
|
||||
|
||||
const handleDarkModeToggle = useCallback(() => {
|
||||
toggleDarkMode();
|
||||
}, [toggleDarkMode]);
|
||||
|
||||
// --- Menu Items ---
|
||||
|
||||
// built externally to keep the component clean, but on this level to prevent unnecessary re-renders
|
||||
@@ -257,9 +264,21 @@ function Header(props) {
|
||||
setTaskUpsertContext,
|
||||
setReportCenterContext,
|
||||
signOutStart,
|
||||
accountingChildren
|
||||
accountingChildren,
|
||||
darkMode,
|
||||
handleDarkModeToggle
|
||||
}),
|
||||
[t, bodyshop, recentItems, setTaskUpsertContext, setReportCenterContext, signOutStart, accountingChildren]
|
||||
[
|
||||
t,
|
||||
bodyshop,
|
||||
recentItems,
|
||||
setTaskUpsertContext,
|
||||
setReportCenterContext,
|
||||
signOutStart,
|
||||
accountingChildren,
|
||||
darkMode,
|
||||
handleDarkModeToggle
|
||||
]
|
||||
);
|
||||
|
||||
const rightMenuItems = useMemo(() => {
|
||||
@@ -292,6 +311,7 @@ function Header(props) {
|
||||
),
|
||||
onClick: handleTaskCenterClick
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [
|
||||
scenarioNotificationsOn,
|
||||
|
||||
@@ -77,3 +77,11 @@ export const setWssStatus = (status) => ({
|
||||
type: ApplicationActionTypes.SET_WSS_STATUS,
|
||||
payload: status
|
||||
});
|
||||
export const toggleDarkMode = () => ({
|
||||
type: ApplicationActionTypes.TOGGLE_DARK_MODE
|
||||
});
|
||||
|
||||
export const setDarkMode = (value) => ({
|
||||
type: ApplicationActionTypes.SET_DARK_MODE,
|
||||
payload: value
|
||||
});
|
||||
|
||||
@@ -16,7 +16,8 @@ const INITIAL_STATE = {
|
||||
},
|
||||
jobReadOnly: false,
|
||||
partnerVersion: null,
|
||||
alerts: {}
|
||||
alerts: {},
|
||||
darkMode: false
|
||||
};
|
||||
|
||||
const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
@@ -104,6 +105,16 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
alerts: newAlertsMap
|
||||
};
|
||||
}
|
||||
case ApplicationActionTypes.TOGGLE_DARK_MODE:
|
||||
return {
|
||||
...state,
|
||||
darkMode: !state.darkMode
|
||||
};
|
||||
case ApplicationActionTypes.SET_DARK_MODE:
|
||||
return {
|
||||
...state,
|
||||
darkMode: action.payload
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -24,3 +24,4 @@ export const selectProblemJobs = createSelector([selectApplication], (applicatio
|
||||
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
|
||||
export const selectWssStatus = createSelector([selectApplication], (application) => application.wssStatus);
|
||||
export const selectAlerts = createSelector([selectApplication], (application) => application.alerts);
|
||||
export const selectDarkMode = createSelector([selectApplication], (application) => application.darkMode);
|
||||
|
||||
@@ -14,6 +14,8 @@ const ApplicationActionTypes = {
|
||||
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
|
||||
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
|
||||
SET_WSS_STATUS: "SET_WSS_STATUS",
|
||||
ADD_ALERTS: "ADD_ALERTS"
|
||||
ADD_ALERTS: "ADD_ALERTS",
|
||||
TOGGLE_DARK_MODE: "TOGGLE_DARK_MODE",
|
||||
SET_DARK_MODE: "SET_DARK_MODE"
|
||||
};
|
||||
export default ApplicationActionTypes;
|
||||
|
||||
@@ -3781,7 +3781,9 @@
|
||||
"actions": {
|
||||
"changepassword": "Change Password",
|
||||
"signout": "Sign Out",
|
||||
"updateprofile": "Update Profile"
|
||||
"updateprofile": "Update Profile",
|
||||
"light_theme": "Switch to Light Theme",
|
||||
"dark_theme": "Switch to Dark Theme"
|
||||
},
|
||||
"errors": {
|
||||
"updating": "Error updating user or association {{message}}"
|
||||
|
||||
@@ -3781,7 +3781,9 @@
|
||||
"actions": {
|
||||
"changepassword": "",
|
||||
"signout": "desconectar",
|
||||
"updateprofile": "Actualización del perfil"
|
||||
"updateprofile": "Actualización del perfil",
|
||||
"light_theme": "",
|
||||
"dark_theme": ""
|
||||
},
|
||||
"errors": {
|
||||
"updating": ""
|
||||
|
||||
@@ -3781,7 +3781,9 @@
|
||||
"actions": {
|
||||
"changepassword": "",
|
||||
"signout": "Déconnexion",
|
||||
"updateprofile": "Mettre à jour le profil"
|
||||
"updateprofile": "Mettre à jour le profil",
|
||||
"light_theme": "",
|
||||
"dark_theme": ""
|
||||
},
|
||||
"errors": {
|
||||
"updating": ""
|
||||
|
||||
Reference in New Issue
Block a user