init
This commit is contained in:
28
worklenz-frontend/src/features/theme/ThemeSelector.tsx
Normal file
28
worklenz-frontend/src/features/theme/ThemeSelector.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// ThemeSelector.tsx
|
||||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { toggleTheme } from './themeSlice';
|
||||
import { MoonOutlined, SunOutlined } from '@ant-design/icons';
|
||||
|
||||
const ThemeSelector = () => {
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleDarkModeToggle = () => {
|
||||
dispatch(toggleTheme());
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type={themeMode === 'dark' ? 'primary' : 'default'}
|
||||
icon={themeMode === 'dark' ? <SunOutlined /> : <MoonOutlined />}
|
||||
shape="circle"
|
||||
onClick={handleDarkModeToggle}
|
||||
className="transition-all duration-300" // Optional: add smooth transition
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSelector;
|
||||
77
worklenz-frontend/src/features/theme/ThemeWrapper.tsx
Normal file
77
worklenz-frontend/src/features/theme/ThemeWrapper.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { ConfigProvider, theme } from 'antd';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { initializeTheme } from './themeSlice';
|
||||
import { colors } from '../../styles/colors';
|
||||
|
||||
type ChildrenProp = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const ThemeWrapper = ({ children }: ChildrenProp) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const isInitialized = useAppSelector(state => state.themeReducer.isInitialized);
|
||||
const configRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Initialize theme after mount
|
||||
useEffect(() => {
|
||||
if (!isInitialized) {
|
||||
dispatch(initializeTheme());
|
||||
}
|
||||
}, [dispatch, isInitialized]);
|
||||
|
||||
// Listen for system theme changes
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
if (!localStorage.getItem('theme')) {
|
||||
dispatch(initializeTheme());
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, [dispatch]);
|
||||
|
||||
// Add CSS transition classes to prevent flash
|
||||
useEffect(() => {
|
||||
if (configRef.current) {
|
||||
configRef.current.style.transition = 'background-color 0.3s ease';
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={configRef} className={`theme-${themeMode}`}>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: themeMode === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||
components: {
|
||||
Layout: {
|
||||
colorBgLayout: themeMode === 'dark' ? colors.darkGray : colors.white,
|
||||
headerBg: themeMode === 'dark' ? colors.darkGray : colors.white,
|
||||
},
|
||||
Menu: {
|
||||
colorBgContainer: colors.transparent,
|
||||
},
|
||||
Table: {
|
||||
rowHoverBg: themeMode === 'dark' ? '#000' : '#edebf0',
|
||||
},
|
||||
Select: {
|
||||
controlHeight: 32,
|
||||
},
|
||||
},
|
||||
token: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeWrapper;
|
||||
88
worklenz-frontend/src/features/theme/themeSlice.ts
Normal file
88
worklenz-frontend/src/features/theme/themeSlice.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
type ThemeType = 'light' | 'dark';
|
||||
|
||||
interface ThemeState {
|
||||
mode: ThemeType;
|
||||
isInitialized: boolean;
|
||||
}
|
||||
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
const getPreloadedTheme = (): ThemeType =>
|
||||
!isBrowser ? 'light' : (window as any).__THEME_STATE__ || 'light';
|
||||
|
||||
const getSystemTheme = (): ThemeType =>
|
||||
!isBrowser
|
||||
? 'light'
|
||||
: window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
|
||||
const getThemeModeFromLocalStorage = (): ThemeType => {
|
||||
if (!isBrowser) return 'light';
|
||||
try {
|
||||
return (localStorage.getItem('theme') as ThemeType) || getSystemTheme();
|
||||
} catch {
|
||||
return 'light';
|
||||
}
|
||||
};
|
||||
|
||||
const updateDocumentTheme = (themeMode: ThemeType): void => {
|
||||
if (!isBrowser) return;
|
||||
|
||||
const root = document.documentElement;
|
||||
const oppositeTheme = themeMode === 'dark' ? 'light' : 'dark';
|
||||
const themeColor = themeMode === 'dark' ? '#181818' : '#ffffff';
|
||||
|
||||
[root, document.body].forEach(element => {
|
||||
element.classList.remove(oppositeTheme);
|
||||
element.classList.add(themeMode);
|
||||
});
|
||||
|
||||
root.style.colorScheme = themeMode;
|
||||
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', themeColor);
|
||||
|
||||
(window as any).__THEME_STATE__ = themeMode;
|
||||
};
|
||||
|
||||
const saveThemeModeToLocalStorage = (themeMode: ThemeType): void => {
|
||||
if (!isBrowser) return;
|
||||
try {
|
||||
localStorage.setItem('theme', themeMode);
|
||||
updateDocumentTheme(themeMode);
|
||||
} catch (error) {
|
||||
console.error('Failed to save theme to localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const initialState: ThemeState = {
|
||||
mode: getPreloadedTheme(),
|
||||
isInitialized: false,
|
||||
};
|
||||
|
||||
const themeSlice = createSlice({
|
||||
name: 'themeReducer',
|
||||
initialState,
|
||||
reducers: {
|
||||
toggleTheme: (state: ThemeState) => {
|
||||
state.mode = state.mode === 'light' ? 'dark' : 'light';
|
||||
saveThemeModeToLocalStorage(state.mode);
|
||||
},
|
||||
setTheme: (state: ThemeState, action: PayloadAction<ThemeType>) => {
|
||||
state.mode = action.payload;
|
||||
saveThemeModeToLocalStorage(state.mode);
|
||||
},
|
||||
initializeTheme: (state: ThemeState) => {
|
||||
if (!state.isInitialized) {
|
||||
state.mode = getThemeModeFromLocalStorage();
|
||||
state.isInitialized = true;
|
||||
updateDocumentTheme(state.mode);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { toggleTheme, setTheme, initializeTheme } = themeSlice.actions;
|
||||
export default themeSlice.reducer;
|
||||
Reference in New Issue
Block a user