Search code examples
javascriptcssreactjstailwind-cssantd

I am trying to implement dark mode functionality in my header component , so i created context api to handle it but its not working


first i have root component which handles the component tree ,

RootApp.jsx

import { Suspense } from 'react';
import { HashRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import store from '@/redux/store';
import PageLoader from '@/components/PageLoader';

import '@/style/app.css';
import '@/style/index.css';
import '@/style/tailwind.css'

import ERP_SODEOs from '@/apps/IdurarOs';
import { ThemeProvider } from '@/context/ThemeContext/ThemeContext'; // Import ThemeProvider

export default function RoutApp() {
  return (
    <Router>
      <Provider store={store}>
        <ThemeProvider>
          <Suspense fallback={<PageLoader />}>
            <ERP_SODEOs />
          </Suspense>
        </ThemeProvider>
      </Provider>
    </Router>
  );
}

in <ERP_SODEOs> there are multiple component , the main component handling the the child component is ErpApp.jsx :


  return (
    <Layout hasSider>
      <Navigation onPathChange={handlePathChange} />

      {isMobile ? (
        <Layout style={{ marginLeft: 0 }}>
          <HeaderContent />
          <Content
            style={{
              margin: '40px auto 30px',
              overflow: 'initial',
              width: '100%',
              padding: '0 25px',
              maxWidth: 'none',
            }}
          >
            <AppRouter />
          </Content>
        </Layout>
      ) : (
        <Layout style={{ marginLeft: isNavMenuClose ? 100 : 220 }}>
          <HeaderContent currentPath={currentPath} />
          <Content
            style={{
              margin: '30px auto 30px',
              overflow: 'initial',
              width: '100%',
              padding: '0px 10px 0px 0px',
              maxWidth: isNavMenuClose ? 1700 : 1600,
            }}
          >
            <AppRouter />
          </Content>
        </Layout>
      )}
    </Layout>
  );
}

i am trying to implement dark mode inside my with my context api component which is not working HeaderContent.jsx :


export default function HeaderContent() {
  const currentAdmin = useSelector(selectCurrentAdmin);
  const translate = useLanguage();
  const [hasPhotoprofile, setHasPhotoprofile] = useState(false);
  const [activeKey, setActiveKey] = useState(null);
  const [isScrolled, setIsScrolled] = useState(false);
  const isAdmin = currentAdmin?.role === 'admin';
  const { theme, toggleTheme } = useContext(ThemeContext);

  useEffect(() => {
    function handleScroll() {
      const scrollPosition = window.scrollY;
      setIsScrolled(scrollPosition > 0);
    }

    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  useEffect(() => {
    async function fetchData() {
      try {
        const result = await checkImage(BASE_URL + currentAdmin?.photo);
        setHasPhotoprofile(result);
      } catch (error) {
        console.error('Error checking image:', error);
      }
    }

    fetchData();
  }, [currentAdmin]);

  const srcImgProfile = hasPhotoprofile ? BASE_URL + currentAdmin?.photo : null;

  const ProfileDropdown = () => {
    const navigate = useNavigate();
    return (
      <div className="profileDropdown" onClick={() => navigate('/profile')}>
        <Avatar
          size="large"
          className="last"
          src={srcImgProfile}
          style={{ color: '#f56a00', backgroundColor: !hasPhotoprofile ? '#fde3cf' : '#f9fafc' }}
        >
          {currentAdmin?.fullname && currentAdmin?.fullname.charAt(0).toUpperCase()}
        </Avatar>
        <div className="profileDropdownInfo">
          <p className='capitalize font-thin text-sm text-blue-600'>
            {currentAdmin?.fullname}
          </p>
          <p className='font-mono text-[11px] text-sm text-red-600 font-thin'>{currentAdmin?.username}</p>
        </div>
      </div>
    );
  };

  const DropdownMenu = ({ text }) => <span>{text}</span>;

  const items = isAdmin
    ? [
      {
        label: <ProfileDropdown className="headerDropDownMenu" />,
        key: 'ProfileDropdown',
      },
      { type: 'divider' },
      {
        icon: <SettingOutlined />,
        key: 'settingProfile',
        label: (
          <Link to={'/profile'}>
            <DropdownMenu text={translate('profile_settings')} />
          </Link>
        ),
      },
      {
        icon: <SettingOutlined />,
        key: 'settingApp',
        label: <Link to={'/settings'}>{translate('app_settings')}</Link>,
      },
      { type: 'divider' },
      {
        icon: <LogoutOutlined />,
        key: 'logout',
        label: <Link to={'/logout'}>{translate('logout')}</Link>,
      }
    ]
    : [
      {
        icon: <SettingOutlined />,
        key: 'settingProfile',
        label: (
          <Link to={'/profile'}>
            <DropdownMenu text={translate('update_password')} />
          </Link>
        ),
      },
      {
        icon: <LogoutOutlined />,
        key: 'logout',
        label: <Link to={'/logout'}>{translate('logout')}</Link>,
      }
    ];

  const darkModeIcons = [
    {
      icon: <BsMoonStars />,
      key: 'dark',
      label: 'Dark',
      onClick: () => toggleTheme('dark'),
    },
    {
      icon: <GrSun />,
      key: 'light',
      label: 'Light',
      onClick: () => toggleTheme('light'),
    },
    {
      icon: <MdOutlineComputer />,
      key: 'system',
      label: 'System',
      onClick: () => toggleTheme('system'),
    },
  ];

  const darkModeDropdown = (
    <Menu className='w-36' selectedKeys={[activeKey]}>
      {darkModeIcons.map((item) => (
        <Menu.Item
          key={item.key}
          onClick={() => handleItemClick(item.key)}
          style={{ display: 'flex', alignItems: 'center' }}
        >
          <div className='flex items-center gap-2.5'>
            {item.icon} <span>{item.label}</span>
          </div>
        </Menu.Item>
      ))}
    </Menu>
  );

  const handleItemClick = (key) => {
    setActiveKey(key);
  };

  return (
    <Header
      className={`sticky top-0 z-50 ${isScrolled ? 'scrolled' : 'bg-white w-[100%] scroll-smooth'} ${theme === 'dark' ? 'bg-gray-900' : 'bg-white'}`}
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
      }}
    >
      <div className={`text-[15px] font-thin ${theme === 'dark' ? 'text-white' : 'text-black'}`}>
        <span className='uppercase'>Dashboard</span>
      </div>
      <div className='flex flex-row-reverse gap-3 items-center'>
        <Dropdown
          overlay={<Menu items={items} />}
          trigger={['click']}
          placement="bottomRight"
          style={{ width: '280px', float: 'right' }}
        >
          <Avatar
            className="last"
            src={srcImgProfile}
            style={{
              color: '#f56a00',
              backgroundColor: !hasPhotoprofile ? '#fde3cf' : '#f9fafc',
              float: 'right',
            }}
            size="large"
          >
            {currentAdmin?.fullname && currentAdmin?.fullname.charAt(0).toUpperCase()}
          </Avatar>
        </Dropdown>
        <Dropdown overlay={darkModeDropdown} trigger={['click']} placement='bottomRight'>
          <div>
            <BsMoonStars className={`text-[20px] cursor-pointer ${theme === 'dark' ? 'text-white' : 'text-black'}`} />
          </div>
        </Dropdown>
        <SelectLanguage />
      </div>
    </Header>
  );
}

as global css files are imported in the root file i also tried to add custom css in that global css file but its not working.

there is dropdown button in the headercontent file in which there are 3 options to change the theme of the application , when user click the button dark it should change the color of the header i also want to use context created by me used in other components as well, i also want to add that i have already changed the tsconfig file with darkMode: 'class'


Solution

  • To implement dark mode functionality using Context API and ensure it works across your application, including in the HeaderContent component, you need to follow a few steps to correctly set up and use your ThemeContext. Here’s a detailed approach to help you troubleshoot and implement the functionality:

    Step 1: Create the Theme Context First, create a ThemeContext with a provider and a custom hook for easier usage.

    ThemeContext.js

    import React, { createContext, useState, useContext, useEffect } from 'react';
    
    const ThemeContext = createContext();
    
    export const ThemeProvider = ({ children }) => {
      const [theme, setTheme] = useState(() => {
        // Retrieve the stored theme or default to 'light'
        const savedTheme = localStorage.getItem('theme');
        return savedTheme ? savedTheme : 'light';
      });
    
      const toggleTheme = (newTheme) => {
        setTheme(newTheme);
        localStorage.setItem('theme', newTheme);
      };
    
      useEffect(() => {
        // Apply the theme to the body class
        document.body.className = theme;
      }, [theme]);
    
      return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
          {children}
        </ThemeContext.Provider>
      );
    };
    
    export const useTheme = () => useContext(ThemeContext);
    

    Step 2: Wrap your application with the Theme Provider Ensure that your ThemeProvider wraps your application so that the context is available to all components.

    RootApp.jsx

    import { Suspense } from 'react';
    import { HashRouter as Router } from 'react-router-dom';
    import { Provider } from 'react-redux';
    import store from '@/redux/store';
    import PageLoader from '@/components/PageLoader';
    
    import '@/style/app.css';
    import '@/style/index.css';
    import '@/style/tailwind.css';
    
    import ERP_SODEOs from '@/apps/IdurarOs';
    import { ThemeProvider } from '@/context/ThemeContext/ThemeContext'; // Import ThemeProvider
    
    export default function RootApp() {
      return (
        <Router>
          <Provider store={store}>
            <ThemeProvider>
              <Suspense fallback={<PageLoader />}>
                <ERP_SODEOs />
              </Suspense>
            </ThemeProvider>
          </Provider>
        </Router>
      );
    }
    
    

    Step 3: Use the Theme Context in HeaderContent Now you can use the useTheme hook to access and modify the theme within your HeaderContent component.

    HeaderContent.jsx

    import React, { useEffect, useState } from 'react';
    import { useSelector } from 'react-redux';
    import { Link, useNavigate } from 'react-router-dom';
    import { Avatar, Dropdown, Menu, Layout } from 'antd';
    import { SettingOutlined, LogoutOutlined } from '@ant-design/icons';
    import { BsMoonStars } from 'react-icons/bs';
    import { GrSun } from 'react-icons/gr';
    import { MdOutlineComputer } from 'react-icons/md';
    import { useTheme } from '@/context/ThemeContext/ThemeContext'; // Import useTheme
    import { selectCurrentAdmin } from '@/redux/selectors';
    import SelectLanguage from '@/components/SelectLanguage';
    import checkImage from '@/utils/checkImage';
    
    const { Header, Content } = Layout;
    
    export default function HeaderContent() {
      const currentAdmin = useSelector(selectCurrentAdmin);
      const [hasPhotoprofile, setHasPhotoprofile] = useState(false);
      const [activeKey, setActiveKey] = useState(null);
      const [isScrolled, setIsScrolled] = useState(false);
      const { theme, toggleTheme } = useTheme(); // Use the custom hook to access theme context
    
      useEffect(() => {
        function handleScroll() {
          const scrollPosition = window.scrollY;
          setIsScrolled(scrollPosition > 0);
        }
    
        window.addEventListener('scroll', handleScroll);
    
        return () => {
          window.removeEventListener('scroll', handleScroll);
        };
      }, []);
    
      useEffect(() => {
        async function fetchData() {
          try {
            const result = await checkImage(BASE_URL + currentAdmin?.photo);
            setHasPhotoprofile(result);
          } catch (error) {
            console.error('Error checking image:', error);
          }
        }
    
        fetchData();
      }, [currentAdmin]);
    
      const srcImgProfile = hasPhotoprofile ? BASE_URL + currentAdmin?.photo : null;
    
      const ProfileDropdown = () => {
        const navigate = useNavigate();
        return (
          <div className="profileDropdown" onClick={() => navigate('/profile')}>
            <Avatar
              size="large"
              className="last"
              src={srcImgProfile}
              style={{ color: '#f56a00', backgroundColor: !hasPhotoprofile ? '#fde3cf' : '#f9fafc' }}
            >
              {currentAdmin?.fullname && currentAdmin?.fullname.charAt(0).toUpperCase()}
            </Avatar>
            <div className="profileDropdownInfo">
              <p className='capitalize font-thin text-sm text-blue-600'>
                {currentAdmin?.fullname}
              </p>
              <p className='font-mono text-[11px] text-sm text-red-600 font-thin'>{currentAdmin?.username}</p>
            </div>
          </div>
        );
      };
    
      const DropdownMenu = ({ text }) => <span>{text}</span>;
    
      const items = currentAdmin?.role === 'admin'
        ? [
          {
            label: <ProfileDropdown className="headerDropDownMenu" />,
            key: 'ProfileDropdown',
          },
          { type: 'divider' },
          {
            icon: <SettingOutlined />,
            key: 'settingProfile',
            label: (
              <Link to={'/profile'}>
                <DropdownMenu text={translate('profile_settings')} />
              </Link>
            ),
          },
          {
            icon: <SettingOutlined />,
            key: 'settingApp',
            label: <Link to={'/settings'}>{translate('app_settings')}</Link>,
          },
          { type: 'divider' },
          {
            icon: <LogoutOutlined />,
            key: 'logout',
            label: <Link to={'/logout'}>{translate('logout')}</Link>,
          }
        ]
        : [
          {
            icon: <SettingOutlined />,
            key: 'settingProfile',
            label: (
              <Link to={'/profile'}>
                <DropdownMenu text={translate('update_password')} />
              </Link>
            ),
          },
          {
            icon: <LogoutOutlined />,
            key: 'logout',
            label: <Link to={'/logout'}>{translate('logout')}</Link>,
          }
        ];
    
      const darkModeIcons = [
        {
          icon: <BsMoonStars />,
          key: 'dark',
          label: 'Dark',
          onClick: () => toggleTheme('dark'),
        },
        {
          icon: <GrSun />,
          key: 'light',
          label: 'Light',
          onClick: () => toggleTheme('light'),
        },
        {
          icon: <MdOutlineComputer />,
          key: 'system',
          label: 'System',
          onClick: () => toggleTheme('system'),
        },
      ];
    
      const darkModeDropdown = (
        <Menu className='w-36' selectedKeys={[activeKey]}>
          {darkModeIcons.map((item) => (
            <Menu.Item
              key={item.key}
              onClick={() => handleItemClick(item.key)}
              style={{ display: 'flex', alignItems: 'center' }}
            >
              <div className='flex items-center gap-2.5'>
                {item.icon} <span>{item.label}</span>
              </div>
            </Menu.Item>
          ))}
        </Menu>
      );
    
      const handleItemClick = (key) => {
        setActiveKey(key);
      };
    
      return (
        <Header
          className={`sticky top-0 z-50 ${isScrolled ? 'scrolled' : 'bg-white w-[100%] scroll-smooth'} ${theme === 'dark' ? 'bg-gray-900' : 'bg-white'}`}
          style={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
          }}
        >
          <div className={`text-[15px] font-thin ${theme === 'dark' ? 'text-white' : 'text-black'}`}>
            <span className='uppercase'>Dashboard</span>
          </div>
          <div className='flex flex-row-reverse gap-3 items-center'>
            <Dropdown
              overlay={<Menu items={items} />}
              trigger={['click']}
              placement="bottomRight"
              style={{ width: '280px', float: 'right' }}
            >
              <Avatar
                className="last"
                src={srcImgProfile}
                style={{
                  color: '#f56a00',
                  backgroundColor: !hasPhotoprofile ? '#fde3cf' : '#f9fafc',
                  float: 'right',
                }}
                size="large"
              >
                {currentAdmin?.fullname && currentAdmin?.fullname.charAt(0).toUpperCase()}
              </Avatar>
            </Dropdown>
            <Dropdown overlay={darkModeDropdown} trigger={['click']} placement='bottomRight'>
              <div>
                <BsMoonStars className={`text-[20px] cursor-pointer ${theme === 'dark' ? 'text-white' : 'text-black'}`} />
              </div>
            </Dropdown>
            <SelectLanguage />
          </div>
        </Header>
      );
    }
    

    Step 4: Add Styles for Dark Mode Finally, ensure that your CSS has the necessary styles for the dark mode and light mode. You can use a CSS class on the body element to handle this.

    body.light {
      background-color: #ffffff;
      color: #000000;
    }
    
    body.dark {
      background-color: #1a1a1a;
      color: #ffffff;
    }
    
    

    In this approach, the toggleTheme function sets the theme state and updates the local storage. The useEffect hook applies the theme by changing the body class. The HeaderContent component uses the useTheme hook to access the current theme and apply styles accordingly.

    This should ensure that your dark mode functionality works as expected across your application.