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