From cf5041e14f06f882dc92e135eeecafb4d3eea07b Mon Sep 17 00:00:00 2001 From: Priyam Srivastava Date: Wed, 4 Feb 2026 14:39:09 +0530 Subject: [PATCH 1/9] darkmode/css_animation --- app/components/DarkModeToggle/index.js | 92 ++++++++++++++++++ .../DarkModeToggle/tests/index.test.js | 70 ++++++++++++++ .../tests/__snapshots__/index.test.js.snap | 27 ++++-- app/containers/App/index.js | 68 ++++++++------ .../tests/__snapshots__/index.test.js.snap | 29 ++++++ app/containers/HomeContainer/index.js | 71 ++++++++++++-- .../tests/__snapshots__/index.test.js.snap | 27 ++++-- app/contexts/themeContext.js | 93 +++++++++++++++++++ app/global-styles.js | 65 ++++++++++++- app/themes/colors.js | 30 +++++- app/themes/tests/colors.test.js | 30 +++++- app/utils/testUtils.js | 5 +- yarn.lock | 84 ++++++++--------- 13 files changed, 579 insertions(+), 112 deletions(-) create mode 100644 app/components/DarkModeToggle/index.js create mode 100644 app/components/DarkModeToggle/tests/index.test.js create mode 100644 app/contexts/themeContext.js diff --git a/app/components/DarkModeToggle/index.js b/app/components/DarkModeToggle/index.js new file mode 100644 index 00000000..927c3d22 --- /dev/null +++ b/app/components/DarkModeToggle/index.js @@ -0,0 +1,92 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { IconButton, Tooltip } from '@mui/material'; +import { Brightness4, Brightness7 } from '@mui/icons-material'; +import { useTheme } from '@app/contexts/themeContext'; + +const ToggleButton = styled(IconButton)` + && { + position: fixed; + bottom: 2rem; + right: 2rem; + width: 56px; + height: 56px; + background: ${(props) => props.bgcolor}; + color: ${(props) => props.color}; + box-shadow: 0 4px 20px ${(props) => props.shadowcolor}; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 1000; + + &:hover { + background: ${(props) => props.hoverbg}; + transform: translateY(-4px) rotate(15deg); + box-shadow: 0 8px 30px ${(props) => props.shadowcolor}; + } + + &:active { + transform: translateY(-2px) rotate(0deg); + } + + @media (max-width: 768px) { + width: 48px; + height: 48px; + bottom: 1.5rem; + right: 1.5rem; + } + } +`; + +const IconWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + animation: ${(props) => (props.animate ? 'rotate 0.5s ease-in-out' : 'none')}; + + @keyframes rotate { + from { + transform: rotate(0deg) scale(0.8); + opacity: 0.5; + } + to { + transform: rotate(360deg) scale(1); + opacity: 1; + } + } +`; + +// eslint-disable-next-line complexity +export const DarkModeToggle = () => { + const { isDarkMode, toggleTheme, colors } = useTheme(); + const [isAnimating, setIsAnimating] = React.useState(false); + + const handleToggle = () => { + setIsAnimating(true); + toggleTheme(); + setTimeout(() => setIsAnimating(false), 500); + }; + + const tooltipTitle = isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'; + const bgColor = isDarkMode ? colors.surface : colors.primary; + const textColor = isDarkMode ? colors.accent : colors.text; + const hoverBgColor = isDarkMode ? colors.hover : colors.secondary; + const Icon = isDarkMode ? Brightness7 : Brightness4; + + return ( + + + + + + + + ); +}; + +export default DarkModeToggle; diff --git a/app/components/DarkModeToggle/tests/index.test.js b/app/components/DarkModeToggle/tests/index.test.js new file mode 100644 index 00000000..17db72ca --- /dev/null +++ b/app/components/DarkModeToggle/tests/index.test.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { DarkModeToggle } from '../index'; +import { ThemeProvider } from '@app/contexts/themeContext'; + +describe('DarkModeToggle', () => { + const renderWithTheme = (initialMode = 'light') => { + if (initialMode === 'dark') { + localStorage.setItem('theme', 'dark'); + } else { + localStorage.setItem('theme', 'light'); + } + + return render( + + + + ); + }; + + beforeEach(() => { + localStorage.clear(); + }); + + it('should render the toggle button', () => { + renderWithTheme(); + const button = screen.getByRole('button', { name: /toggle dark mode/i }); + expect(button).toBeInTheDocument(); + }); + + it('should show sun icon in dark mode', () => { + renderWithTheme('dark'); + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + }); + + it('should show moon icon in light mode', () => { + renderWithTheme('light'); + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + }); + + it('should toggle theme when clicked', () => { + renderWithTheme(); + const button = screen.getByRole('button', { name: /toggle dark mode/i }); + + // Click to switch to dark mode + fireEvent.click(button); + expect(localStorage.getItem('theme')).toBe('dark'); + + // Click to switch back to light mode + fireEvent.click(button); + expect(localStorage.getItem('theme')).toBe('light'); + }); + + it('should have fixed positioning', () => { + renderWithTheme(); + const button = screen.getByRole('button'); + const styles = window.getComputedStyle(button); + expect(styles.position).toBe('fixed'); + }); + + it('should show tooltip on hover', () => { + renderWithTheme(); + const button = screen.getByRole('button'); + fireEvent.mouseOver(button); + // Material-UI tooltips appear after a delay + }); +}); diff --git a/app/components/ProtectedRoute/tests/__snapshots__/index.test.js.snap b/app/components/ProtectedRoute/tests/__snapshots__/index.test.js.snap index 8f25242d..77f1be60 100644 --- a/app/components/ProtectedRoute/tests/__snapshots__/index.test.js.snap +++ b/app/components/ProtectedRoute/tests/__snapshots__/index.test.js.snap @@ -10,18 +10,23 @@ exports[` should render and match the snapshot 1`] = ` class="css-1cnh6jp e666d1v2" >

Go to Storybook

should render and match the snapshot 1`] = `

should render and match the snapshot 1`] = ` Get details of repositories

should render and match the snapshot 1`] = ` >
- - - - -
- - } - of={map(Object.keys(routeConfig))} - renderItem={(routeKey, index) => { - const Component = routeConfig[routeKey].component; - return ( - { - const updatedProps = { - ...props, - ...routeConfig[routeKey].props - }; - return ; - }} - /> - ); - }} - /> - - - + + + + + +
+ + } + of={map(Object.keys(routeConfig))} + renderItem={(routeKey, index) => { + // eslint-disable-next-line security/detect-object-injection + const Component = routeConfig[routeKey].component; + return ( + { + const updatedProps = { + ...props, + // eslint-disable-next-line security/detect-object-injection + ...routeConfig[routeKey].props + }; + return ; + }} + /> + ); + }} + /> + + + + + diff --git a/app/containers/App/tests/__snapshots__/index.test.js.snap b/app/containers/App/tests/__snapshots__/index.test.js.snap index 1c4ed5f1..3aaac036 100644 --- a/app/containers/App/tests/__snapshots__/index.test.js.snap +++ b/app/containers/App/tests/__snapshots__/index.test.js.snap @@ -33,5 +33,34 @@ exports[` container tests should render and match the snapshot 1`] = `
+
`; diff --git a/app/containers/HomeContainer/index.js b/app/containers/HomeContainer/index.js index 5be72f2c..614c0a90 100644 --- a/app/containers/HomeContainer/index.js +++ b/app/containers/HomeContainer/index.js @@ -15,7 +15,7 @@ import T from '@components/T'; import { If } from '@components/If'; import { For } from '@components/For'; import { RepoCard } from '@components/RepoCard'; -import colors from '@app/themes/colors'; +import { useTheme } from '@app/contexts/themeContext'; import { selectLoading, selectReposData, selectReposError, selectRepoName } from './selectors'; import { homeContainerCreators } from './reducer'; import homeContainerSaga from './saga'; @@ -26,13 +26,26 @@ const CustomCard = styled(Card)` margin: 1.25rem 0; padding: 1rem; max-width: ${(props) => props.maxwidth}; + background: ${(props) => props.bgcolor}; color: ${(props) => props.color}; - ${(props) => props.color && `color: ${props.color}`}; + border: 1px solid ${(props) => props.bordercolor}; + box-shadow: 0 4px 12px ${(props) => props.shadowcolor}; + transition: all 0.3s ease; + + &:hover { + box-shadow: 0 6px 20px ${(props) => props.shadowcolor}; + transform: translateY(-2px); + } } `; const CustomCardHeader = styled(CardHeader)` && { padding: 0; + + .MuiCardHeader-title { + color: ${(props) => props.titlecolor}; + font-weight: 600; + } } `; const Container = styled.div` @@ -52,11 +65,40 @@ const RightContent = styled.div` const StyledT = styled(T)` && { - color: ${colors.gotoStories}; + color: ${(props) => props.accentcolor}; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + transform: scale(1.05); + } } `; const StyledOutlinedInput = styled(OutlinedInput)` + && { + background: ${(props) => props.inputbg}; + color: ${(props) => props.inputcolor}; + border-radius: 8px; + transition: all 0.3s ease; + + .MuiOutlinedInput-notchedOutline { + border-color: ${(props) => props.bordercolor}; + } + + &:hover .MuiOutlinedInput-notchedOutline { + border-color: ${(props) => props.hovercolor}; + } + + &.Mui-focused .MuiOutlinedInput-notchedOutline { + border-color: ${(props) => props.accentcolor}; + } + + input::placeholder { + color: ${(props) => props.placeholdercolor}; + } + } + legend { display: none; } @@ -92,6 +134,8 @@ export function HomeContainer({ loading }) { const history = useHistory(); + const { colors: themeColors } = useTheme(); + useEffect(() => { if (repoName && !reposData?.items?.length) { dispatchGithubRepos(repoName); @@ -119,11 +163,17 @@ export function HomeContainer({ return ( - + - - - + + + searchRepos(repoName)} + sx={{ color: themeColors.text }} > diff --git a/app/containers/HomeContainer/tests/__snapshots__/index.test.js.snap b/app/containers/HomeContainer/tests/__snapshots__/index.test.js.snap index 84c3392a..8de2a81c 100644 --- a/app/containers/HomeContainer/tests/__snapshots__/index.test.js.snap +++ b/app/containers/HomeContainer/tests/__snapshots__/index.test.js.snap @@ -10,18 +10,23 @@ exports[` tests should render and match the snapshot 1`] = ` class="css-1cnh6jp e666d1v2" >

Go to Storybook

tests should render and match the snapshot 1`] = `

tests should render and match the snapshot 1`] = ` Get details of repositories

tests should render and match the snapshot 1`] = ` >
{}, + theme: {} +}); + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; + +export const ThemeProvider = ({ children }) => { + const [isDarkMode, setIsDarkMode] = useState(() => { + // Check localStorage for saved preference + const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { + return savedTheme === 'dark'; + } + // Check system preference + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + }); + + useEffect(() => { + // Save theme preference + localStorage.setItem('theme', isDarkMode ? 'dark' : 'light'); + + // Update document class for global styles + if (isDarkMode) { + document.documentElement.classList.add('dark-mode'); + document.documentElement.classList.remove('light-mode'); + } else { + document.documentElement.classList.add('light-mode'); + document.documentElement.classList.remove('dark-mode'); + } + }, [isDarkMode]); + + const toggleTheme = () => { + setIsDarkMode((prev) => !prev); + }; + + const theme = useMemo( + () => ({ + isDarkMode, + toggleTheme, + colors: isDarkMode + ? { + primary: '#2d3748', + secondary: '#4a5568', + background: '#1a202c', + surface: '#2d3748', + text: '#f7fafc', + textSecondary: '#cbd5e0', + border: '#4a5568', + shadow: 'rgba(0, 0, 0, 0.3)', + hover: '#374151', + accent: '#60a5fa', + success: '#48bb78', + error: '#f56565', + warning: '#ed8936' + } + : { + primary: '#fcedda', + secondary: '#f8c49c', + background: '#ffffff', + surface: '#f8f9fa', + text: '#212529', + textSecondary: '#6c757d', + border: '#e0e0e0', + shadow: 'rgba(0, 0, 0, 0.1)', + hover: '#f1f3f5', + accent: '#1890ff', + success: '#28a745', + error: '#dc3545', + warning: '#ffc107' + } + }), + [isDarkMode] + ); + + return {children}; +}; + +ThemeProvider.propTypes = { + children: PropTypes.node.isRequired +}; + +export default ThemeContext; diff --git a/app/global-styles.js b/app/global-styles.js index 389139b0..4ac13076 100644 --- a/app/global-styles.js +++ b/app/global-styles.js @@ -1,12 +1,17 @@ import { css } from '@emotion/react'; const globalStyles = css` + :root { + --transition-speed: 0.3s; + } + html, body { height: 100vh; width: 100vw; margin: 0; padding: 0; + transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease; } body { @@ -17,10 +22,50 @@ const globalStyles = css` font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; } - #app { - background-color: #fafafa; - min-height: 100%; - min-width: 100%; + /* Light Mode Styles */ + html.light-mode { + #app { + background-color: #ffffff; + color: #212529; + min-height: 100%; + min-width: 100%; + transition: all var(--transition-speed) ease; + } + } + + /* Dark Mode Styles */ + html.dark-mode { + #app { + background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%); + color: #f7fafc; + min-height: 100%; + min-width: 100%; + transition: all var(--transition-speed) ease; + } + + /* Smooth transitions for all interactive elements */ + * { + transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease, + border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease; + } + + /* Scrollbar styling for dark mode */ + ::-webkit-scrollbar { + width: 12px; + } + + ::-webkit-scrollbar-track { + background: #1a202c; + } + + ::-webkit-scrollbar-thumb { + background: #4a5568; + border-radius: 6px; + + &:hover { + background: #60a5fa; + } + } } p, @@ -33,6 +78,18 @@ const globalStyles = css` line-height: 1.5em; margin-bottom: 0; } + + /* Smooth animations for theme switch */ + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } `; export default globalStyles; diff --git a/app/themes/colors.js b/app/themes/colors.js index 43ef7995..67fc9ee1 100644 --- a/app/themes/colors.js +++ b/app/themes/colors.js @@ -23,12 +23,34 @@ const colors = { gotoStories, theme: { lightMode: { - primary, - secondary + primary: '#fcedda', + secondary: '#f8c49c', + background: '#ffffff', + surface: '#f8f9fa', + text: '#212529', + textSecondary: '#6c757d', + border: '#e0e0e0', + shadow: 'rgba(0, 0, 0, 0.1)', + hover: '#f1f3f5', + accent: '#1890ff', + success: '#28a745', + error: '#dc3545', + warning: '#ffc107' }, darkMode: { - primary: secondary, - secondary: primary + primary: '#2d3748', + secondary: '#4a5568', + background: '#1a202c', + surface: '#2d3748', + text: '#f7fafc', + textSecondary: '#cbd5e0', + border: '#4a5568', + shadow: 'rgba(0, 0, 0, 0.3)', + hover: '#374151', + accent: '#60a5fa', + success: '#48bb78', + error: '#f56565', + warning: '#ed8936' } } }; diff --git a/app/themes/tests/colors.test.js b/app/themes/tests/colors.test.js index 4310e549..414e55ff 100644 --- a/app/themes/tests/colors.test.js +++ b/app/themes/tests/colors.test.js @@ -3,12 +3,34 @@ import colors from '../colors'; describe('colors', () => { it('should have the correct font-size', () => { expect(colors.theme.lightMode).toEqual({ - primary: colors.primary, - secondary: colors.secondary + primary: '#fcedda', + secondary: '#f8c49c', + background: '#ffffff', + surface: '#f8f9fa', + text: '#212529', + textSecondary: '#6c757d', + border: '#e0e0e0', + shadow: 'rgba(0, 0, 0, 0.1)', + hover: '#f1f3f5', + accent: '#1890ff', + success: '#28a745', + error: '#dc3545', + warning: '#ffc107' }); expect(colors.theme.darkMode).toEqual({ - primary: colors.secondary, - secondary: colors.primary + primary: '#2d3748', + secondary: '#4a5568', + background: '#1a202c', + surface: '#2d3748', + text: '#f7fafc', + textSecondary: '#cbd5e0', + border: '#4a5568', + shadow: 'rgba(0, 0, 0, 0.3)', + hover: '#374151', + accent: '#60a5fa', + success: '#48bb78', + error: '#f56565', + warning: '#ed8936' }); }); }); diff --git a/app/utils/testUtils.js b/app/utils/testUtils.js index bd74410e..4efe3cb2 100644 --- a/app/utils/testUtils.js +++ b/app/utils/testUtils.js @@ -9,6 +9,7 @@ import configureStore from '@app/configureStore'; import { DEFAULT_LOCALE, translationMessages } from '@app/i18n'; import ConnectedLanguageProvider from '@containers/LanguageProvider'; import { theme } from '@containers/App'; +import { ThemeProvider as CustomThemeProvider } from '@app/contexts/themeContext'; export const renderWithIntl = (children) => { i18n.loadLocaleData(DEFAULT_LOCALE, { plurals: DEFAULT_LOCALE }); @@ -23,7 +24,9 @@ export const renderProvider = (children, history) => { - {history ? {children} : {children}} + + {history ? {children} : {children}} + diff --git a/yarn.lock b/yarn.lock index d977bcc4..a2e0dbea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1880,7 +1880,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.1", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.4.3", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.1", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== @@ -4906,7 +4906,7 @@ dependencies: defer-to-connect "^2.0.0" -"@testing-library/dom@^8.3.0", "@testing-library/dom@^8.5.0": +"@testing-library/dom@^8.3.0": version "8.20.1" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f" integrity sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g== @@ -4935,14 +4935,12 @@ lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react@^13.3.0": - version "13.4.0" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.4.0.tgz#6a31e3bf5951615593ad984e96b9e5e2d9380966" - integrity sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw== +"@testing-library/react@^16.3.0": + version "16.3.2" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.3.2.tgz#672883b7acb8e775fc0492d9e9d25e06e89786d0" + integrity sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g== dependencies: "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^8.5.0" - "@types/react-dom" "^18.0.0" "@testing-library/user-event@^13.2.1": version "13.5.0" @@ -5252,13 +5250,6 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.12.tgz#afa96b383a3a6fdc859453a1892d41b607fc7756" integrity sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg== -"@types/react-dom@^18.0.0": - version "18.2.19" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.19.tgz#b84b7c30c635a6c26c6a6dfbb599b2da9788be58" - integrity sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA== - dependencies: - "@types/react" "*" - "@types/react-transition-group@^4.4.10": version "4.4.10" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" @@ -5338,6 +5329,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== +"@types/use-sync-external-store@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" + integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== + "@types/vfile-message@*": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-2.0.0.tgz#690e46af0fdfc1f9faae00cd049cc888957927d5" @@ -12366,7 +12362,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -15628,7 +15624,7 @@ lookup-closest-locale@6.0.4: resolved "https://registry.yarnpkg.com/lookup-closest-locale/-/lookup-closest-locale-6.0.4.tgz#1279fed7546a601647bbc980f64423ee990a8590" integrity sha512-bWoFbSGe6f1GvMGzj17LrwMX4FhDXDwZyH04ySVCPbtOJADcSRguZNKewoJ3Ful/MOxD/wRHvFPadk/kYZUbuQ== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -18552,13 +18548,12 @@ react-docgen@^5.0.0: node-dir "^0.1.10" strip-indent "^3.0.0" -react-dom@18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" - integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== +react-dom@^19.1.1: + version "19.2.4" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.4.tgz#6fac6bd96f7db477d966c7ec17c1a2b1ad8e6591" + integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ== dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.0" + scheduler "^0.27.0" react-element-to-jsx-string@^14.3.4: version "14.3.4" @@ -18615,7 +18610,7 @@ react-is@17.0.2, react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.6: +react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -18625,17 +18620,13 @@ react-is@^18.0.0, react-is@^18.2.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-redux@7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.0.2.tgz#34b280a3482aaf60e7d4a504b1295165cbe6b86a" - integrity sha512-uKRuMgQt8dWbcz0U75oFK5tDo3boyAKrqvf/j94vpqRFFZfyDDy4kofUgloFIGyuKTq2Zz51zgK9RzOTFXk5ew== +react-redux@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.2.0.tgz#96c3ab23fb9a3af2cb4654be4b51c989e32366f5" + integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g== dependencies: - "@babel/runtime" "^7.4.3" - hoist-non-react-statics "^3.3.0" - invariant "^2.2.4" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^16.8.6" + "@types/use-sync-external-store" "^0.0.6" + use-sync-external-store "^1.4.0" react-refresh@^0.11.0: version "0.11.0" @@ -18690,12 +18681,10 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react@18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" - integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== - dependencies: - loose-envify "^1.1.0" +react@^19.1.1: + version "19.2.4" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a" + integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ== read-pkg-up@^1.0.1: version "1.0.1" @@ -19606,12 +19595,10 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -scheduler@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" - integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== - dependencies: - loose-envify "^1.1.0" +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== schema-utils@2.7.0: version "2.7.0" @@ -21946,6 +21933,11 @@ url@^0.11.0: punycode "^1.4.1" qs "^6.11.2" +use-sync-external-store@^1.4.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" From 244b14b82ea2b71bf6f737eee4c35c19464dc651 Mon Sep 17 00:00:00 2001 From: Priyam Srivastava Date: Wed, 4 Feb 2026 15:35:36 +0530 Subject: [PATCH 2/9] Git: Stashing changes... [started] --- app/components/DarkModeToggle/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/DarkModeToggle/index.js b/app/components/DarkModeToggle/index.js index 927c3d22..1233e248 100644 --- a/app/components/DarkModeToggle/index.js +++ b/app/components/DarkModeToggle/index.js @@ -54,7 +54,7 @@ const IconWrapper = styled.div` } `; -// eslint-disable-next-line complexity +// dark mode added export const DarkModeToggle = () => { const { isDarkMode, toggleTheme, colors } = useTheme(); const [isAnimating, setIsAnimating] = React.useState(false); From f274dd2ff3635816854392d4a5b5c0f26448e25e Mon Sep 17 00:00:00 2001 From: Priyam Srivastava Date: Fri, 6 Feb 2026 14:10:44 +0530 Subject: [PATCH 3/9] ci error fix --- README.md | 29 ++++++++++++++++++++++++++ app/components/DarkModeToggle/index.js | 1 + app/components/ProtectedRoute/index.js | 7 ++++++- app/components/T/index.js | 9 +++++++- app/containers/App/index.js | 15 +++++++------ app/i18n.js | 8 ++++++- 6 files changed, 60 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 05f0998f..146274e3 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,35 @@ An enterprise react template application showcasing - Testing strategies, Global - Go through the other scripts in `package.json` +## Dark Mode Support 🌙 + +- Dark mode toggle with theme persistence using localStorage + + The app supports light and dark themes that can be toggled by the user. The selected theme preference is persisted in localStorage and automatically applied on subsequent visits. + + **Key Features:** + - Toggle between light and dark themes + - Theme preference saved in localStorage + - Smooth theme transitions + - Automatic theme restoration on app reload + + Take a look at the following files: + + - [app/components/DarkModeToggle/index.js](app/components/DarkModeToggle/index.js) - Toggle button component + - [app/themes/index.js](app/themes/index.js) - Theme configuration with light and dark palettes + - [app/containers/App/index.js](app/containers/App/index.js) - Theme provider implementation + + **Usage:** + ```jsx + import DarkModeToggle from '@components/DarkModeToggle'; + + // Place the toggle button anywhere in your app + + ``` + + The theme state is managed using React's `useState` and Material-UI's `ThemeProvider`. Theme changes are automatically applied across all components using the theme configuration. + + ## Global state management using reduxSauce - Global state management using [Redux Sauce](https://github.com/infinitered/reduxsauce) diff --git a/app/components/DarkModeToggle/index.js b/app/components/DarkModeToggle/index.js index 1233e248..fd25a00f 100644 --- a/app/components/DarkModeToggle/index.js +++ b/app/components/DarkModeToggle/index.js @@ -55,6 +55,7 @@ const IconWrapper = styled.div` `; // dark mode added +// eslint-disable-next-line complexity export const DarkModeToggle = () => { const { isDarkMode, toggleTheme, colors } = useTheme(); const [isAnimating, setIsAnimating] = React.useState(false); diff --git a/app/components/ProtectedRoute/index.js b/app/components/ProtectedRoute/index.js index 529bf950..b61de9be 100644 --- a/app/components/ProtectedRoute/index.js +++ b/app/components/ProtectedRoute/index.js @@ -12,7 +12,12 @@ import routeConstants from '@utils/routeConstants'; const ProtectedRoute = ({ render: C, isLoggedIn, handleLogout, ...rest }) => { const isUnprotectedRoute = Object.keys(routeConstants) - .filter((key) => !routeConstants[key].isProtected) + .filter((key) => { + // Safe property access with validation + // eslint-disable-next-line security/detect-object-injection + return Object.prototype.hasOwnProperty.call(routeConstants, key) && !routeConstants[key].isProtected; + }) + // eslint-disable-next-line security/detect-object-injection .map((key) => routeConstants[key].route) .includes(rest.path) && rest.exact; diff --git a/app/components/T/index.js b/app/components/T/index.js index c745f5ae..3ca7b6a9 100644 --- a/app/components/T/index.js +++ b/app/components/T/index.js @@ -17,7 +17,14 @@ const StyledText = styled.p` ${(props) => props.font()}; } `; -const getFontStyle = (type) => fonts.style[type] || (() => ''); +const getFontStyle = (type) => { + // Safe property access with validation + if (type && Object.prototype.hasOwnProperty.call(fonts.style, type)) { + // eslint-disable-next-line security/detect-object-injection + return fonts.style[type]; + } + return () => ''; +}; const T = ({ type, text, id, marginBottom, values, ...otherProps }) => ( diff --git a/app/containers/App/index.js b/app/containers/App/index.js index 1f531677..f066bdf3 100644 --- a/app/containers/App/index.js +++ b/app/containers/App/index.js @@ -88,19 +88,22 @@ export function App() { ParentComponent={(props) => } of={map(Object.keys(routeConfig))} renderItem={(routeKey, index) => { + // Safe property access with validation + if (!Object.prototype.hasOwnProperty.call(routeConfig, routeKey)) { + return null; + } // eslint-disable-next-line security/detect-object-injection - const Component = routeConfig[routeKey].component; + const routeConfigItem = routeConfig[routeKey]; + const Component = routeConfigItem.component; return ( { const updatedProps = { ...props, - // eslint-disable-next-line security/detect-object-injection - ...routeConfig[routeKey].props + ...routeConfigItem.props }; return ; }} diff --git a/app/i18n.js b/app/i18n.js index 515e2256..ccd714dd 100644 --- a/app/i18n.js +++ b/app/i18n.js @@ -21,8 +21,14 @@ export const formatTranslationMessages = (locale, messages) => { const defaultFormattedMessages = locale !== DEFAULT_LOCALE ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) : {}; const flattenFormattedMessages = (formattedMessages, key) => { + // Safe property access with hasOwnProperty check + const hasMessage = Object.prototype.hasOwnProperty.call(messages, key); + const hasDefaultMessage = Object.prototype.hasOwnProperty.call(defaultFormattedMessages, key); + // eslint-disable-next-line security/detect-object-injection + const messageValue = hasMessage ? messages[key] : undefined; const formattedMessage = - !messages[key] && locale !== DEFAULT_LOCALE ? defaultFormattedMessages[key] : messages[key]; + // eslint-disable-next-line security/detect-object-injection + !messageValue && locale !== DEFAULT_LOCALE && hasDefaultMessage ? defaultFormattedMessages[key] : messageValue; return Object.assign(formattedMessages, { [key]: formattedMessage }); }; return Object.keys(messages).reduce(flattenFormattedMessages, {}); From 32c80020bc66325e6ac5e30522bda29e972353d7 Mon Sep 17 00:00:00 2001 From: Priyam Srivastava Date: Fri, 6 Feb 2026 15:20:17 +0530 Subject: [PATCH 4/9] fix: add React 18/19 polyfills for Storybook 6.x compatibility - Add ReactDOM.render polyfill using createRoot API - Add ReactDOM.unmountComponentAtNode polyfill - Fixes 'react_dom.render is not a function' error - Fixes 'react_dom.unmountComponentAtNode is not a function' error - All 3 Storybook tests now passing --- .storybook/preview.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.storybook/preview.js b/.storybook/preview.js index decf3ec3..7baeb2d8 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,4 +1,42 @@ +import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import enMessages from '../app/translations/en.json'; + +// Polyfill for React 18/19 compatibility with Storybook 6.x +// Storybook 6.x uses the deprecated render and unmountComponentAtNode APIs + +// Store roots for cleanup +const rootsMap = new WeakMap(); + +if (!ReactDOM.render) { + ReactDOM.render = (element, container, callback) => { + let root = rootsMap.get(container); + if (!root) { + root = createRoot(container); + rootsMap.set(container, root); + } + root.render(element); + if (callback) { + callback(); + } + return { + unmount: () => root.unmount() + }; + }; +} + +if (!ReactDOM.unmountComponentAtNode) { + ReactDOM.unmountComponentAtNode = (container) => { + const root = rootsMap.get(container); + if (root) { + root.unmount(); + rootsMap.delete(container); + return true; + } + return false; + }; +} + export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, controls: { From 4afcf7e071ab2e0d72a9fc737e7c1c090afb52c6 Mon Sep 17 00:00:00 2001 From: Priyam Srivastava Date: Fri, 6 Feb 2026 15:26:28 +0530 Subject: [PATCH 5/9] fix: reactive MUI theme and lint fixes --- README.md | 15 +- app/components/ProtectedRoute/index.js | 3 +- app/containers/App/index.js | 137 ++++++++++-------- .../tests/__snapshots__/index.test.js.snap | 2 +- app/i18n.js | 7 +- app/utils/testUtils.js | 25 +++- 6 files changed, 111 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 146274e3..6a1385a0 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ An enterprise react template application showcasing - Testing strategies, Global The app supports light and dark themes that can be toggled by the user. The selected theme preference is persisted in localStorage and automatically applied on subsequent visits. **Key Features:** + - Toggle between light and dark themes - Theme preference saved in localStorage - Smooth theme transitions @@ -72,19 +73,19 @@ An enterprise react template application showcasing - Testing strategies, Global Take a look at the following files: - [app/components/DarkModeToggle/index.js](app/components/DarkModeToggle/index.js) - Toggle button component - - [app/themes/index.js](app/themes/index.js) - Theme configuration with light and dark palettes - - [app/containers/App/index.js](app/containers/App/index.js) - Theme provider implementation + - [app/contexts/themeContext.js](app/contexts/themeContext.js) - Theme context with light and dark palettes + - [app/containers/App/index.js](app/containers/App/index.js) - Theme provider implementation and MUI theme integration **Usage:** + ```jsx - import DarkModeToggle from '@components/DarkModeToggle'; - + import { DarkModeToggle } from '@components/DarkModeToggle'; + // Place the toggle button anywhere in your app - + ; ``` - The theme state is managed using React's `useState` and Material-UI's `ThemeProvider`. Theme changes are automatically applied across all components using the theme configuration. - + The theme state is managed using React Context (ThemeContext) with localStorage persistence. Material-UI's ThemeProvider receives a dynamically generated theme object that updates when dark mode is toggled, ensuring all MUI components respond to theme changes. ## Global state management using reduxSauce diff --git a/app/components/ProtectedRoute/index.js b/app/components/ProtectedRoute/index.js index b61de9be..4b1dcc8e 100644 --- a/app/components/ProtectedRoute/index.js +++ b/app/components/ProtectedRoute/index.js @@ -13,9 +13,8 @@ const ProtectedRoute = ({ render: C, isLoggedIn, handleLogout, ...rest }) => { const isUnprotectedRoute = Object.keys(routeConstants) .filter((key) => { - // Safe property access with validation // eslint-disable-next-line security/detect-object-injection - return Object.prototype.hasOwnProperty.call(routeConstants, key) && !routeConstants[key].isProtected; + return !routeConstants[key].isProtected; }) // eslint-disable-next-line security/detect-object-injection .map((key) => routeConstants[key].route) diff --git a/app/containers/App/index.js b/app/containers/App/index.js index f066bdf3..edfebddf 100644 --- a/app/containers/App/index.js +++ b/app/containers/App/index.js @@ -7,11 +7,10 @@ * */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { Switch, Route } from 'react-router-dom'; import PropTypes from 'prop-types'; import { Router } from 'react-router'; -import map from 'lodash/map'; import { PersistGate } from 'redux-persist/integration/react'; import { Provider } from 'react-redux'; import { CssBaseline, Container } from '@mui/material'; @@ -29,23 +28,9 @@ import { translationMessages } from '@app/i18n'; import history from '@utils/history'; import { SCREEN_BREAK_POINTS } from '@utils/constants'; import configureStore from '@app/configureStore'; -import { colors } from '@themes'; -import { ThemeProvider } from '@app/contexts/themeContext'; +import { ThemeProvider, useTheme } from '@app/contexts/themeContext'; import { DarkModeToggle } from '@app/components/DarkModeToggle'; -export const theme = createTheme({ - palette: { - primary: { - main: colors.primary - }, - secondary: { - main: colors.secondary - } - }, - breakpoints: { - values: SCREEN_BREAK_POINTS - } -}); /** * App component that sets up the application with routing, theme, and language support. * It also handles redirect logic based on the query parameters in the URL. @@ -54,9 +39,37 @@ export const theme = createTheme({ * * @returns {JSX.Element} The App component with the application setup. */ -export function App() { +function AppContent() { const [store, setStore] = useState(null); const [persistor, setPersistor] = useState(null); + const { colors: themeColors } = useTheme(); + + // Create MUI theme that responds to dark mode + const muiTheme = useMemo( + () => + createTheme({ + palette: { + primary: { + main: themeColors.primary + }, + secondary: { + main: themeColors.secondary + }, + background: { + default: themeColors.background, + paper: themeColors.surface + }, + text: { + primary: themeColors.text, + secondary: themeColors.textSecondary + } + }, + breakpoints: { + values: SCREEN_BREAK_POINTS + } + }), + [themeColors] + ); const { location } = history; useEffect(() => { @@ -77,45 +90,39 @@ export function App() { - - - - - -
- - } - of={map(Object.keys(routeConfig))} - renderItem={(routeKey, index) => { - // Safe property access with validation - if (!Object.prototype.hasOwnProperty.call(routeConfig, routeKey)) { - return null; - } - // eslint-disable-next-line security/detect-object-injection - const routeConfigItem = routeConfig[routeKey]; - const Component = routeConfigItem.component; - return ( - { - const updatedProps = { - ...props, - ...routeConfigItem.props - }; - return ; - }} - /> - ); - }} - /> - - - - - + + + + +
+ + } + of={Object.keys(routeConfig)} + renderItem={(routeKey) => { + // eslint-disable-next-line security/detect-object-injection + const routeConfigItem = routeConfig[routeKey]; + const Component = routeConfigItem.component; + return ( + { + const updatedProps = { + ...props, + ...routeConfigItem.props + }; + return ; + }} + /> + ); + }} + /> + + + + @@ -125,8 +132,22 @@ export function App() { ); } -App.propTypes = { + +AppContent.propTypes = { location: PropTypes.object, history: PropTypes.object }; + +/** + * App wrapper component that provides theme context + * @returns {JSX.Element} The App component wrapped in ThemeProvider + */ +export function App() { + return ( + + + + ); +} + export default App; diff --git a/app/containers/App/tests/__snapshots__/index.test.js.snap b/app/containers/App/tests/__snapshots__/index.test.js.snap index 3aaac036..4bdd5321 100644 --- a/app/containers/App/tests/__snapshots__/index.test.js.snap +++ b/app/containers/App/tests/__snapshots__/index.test.js.snap @@ -3,7 +3,7 @@ exports[` container tests should render and match the snapshot 1`] = `
diff --git a/app/i18n.js b/app/i18n.js index ccd714dd..77220744 100644 --- a/app/i18n.js +++ b/app/i18n.js @@ -21,11 +21,10 @@ export const formatTranslationMessages = (locale, messages) => { const defaultFormattedMessages = locale !== DEFAULT_LOCALE ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) : {}; const flattenFormattedMessages = (formattedMessages, key) => { - // Safe property access with hasOwnProperty check - const hasMessage = Object.prototype.hasOwnProperty.call(messages, key); - const hasDefaultMessage = Object.prototype.hasOwnProperty.call(defaultFormattedMessages, key); + // key comes from Object.keys(messages), so it's always an own property // eslint-disable-next-line security/detect-object-injection - const messageValue = hasMessage ? messages[key] : undefined; + const messageValue = messages[key]; + const hasDefaultMessage = Object.prototype.hasOwnProperty.call(defaultFormattedMessages, key); const formattedMessage = // eslint-disable-next-line security/detect-object-injection !messageValue && locale !== DEFAULT_LOCALE && hasDefaultMessage ? defaultFormattedMessages[key] : messageValue; diff --git a/app/utils/testUtils.js b/app/utils/testUtils.js index 4efe3cb2..4b29edba 100644 --- a/app/utils/testUtils.js +++ b/app/utils/testUtils.js @@ -4,15 +4,28 @@ import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; import { BrowserRouter, Router } from 'react-router-dom'; import { i18n } from '@lingui/core'; -import { ThemeProvider } from '@mui/material/styles'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; import configureStore from '@app/configureStore'; import { DEFAULT_LOCALE, translationMessages } from '@app/i18n'; import ConnectedLanguageProvider from '@containers/LanguageProvider'; -import { theme } from '@containers/App'; +import { colors } from '@themes'; import { ThemeProvider as CustomThemeProvider } from '@app/contexts/themeContext'; +// Create a basic theme for testing +const testTheme = createTheme({ + palette: { + primary: { + main: colors.primary + }, + secondary: { + main: colors.secondary + } + } +}); + export const renderWithIntl = (children) => { i18n.loadLocaleData(DEFAULT_LOCALE, { plurals: DEFAULT_LOCALE }); + // eslint-disable-next-line security/detect-object-injection i18n.load(DEFAULT_LOCALE, translationMessages[DEFAULT_LOCALE]); i18n.activate(DEFAULT_LOCALE); return render({children}); @@ -23,11 +36,11 @@ export const renderProvider = (children, history) => { return render( - - + + {history ? {children} : {children}} - - + + ); From 2eef1f77977921c686cf6919a041d77153436921 Mon Sep 17 00:00:00 2001 From: Priyam Srivastava Date: Fri, 6 Feb 2026 15:33:08 +0530 Subject: [PATCH 6/9] fix: clear Storybook cache in CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a31a8fc..cab75037 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,9 @@ jobs: - name: Test run: yarn test + - name: Clear Storybook cache + run: rm -rf node_modules/.cache/storybook + - name: Accessibility tests using Storybook run: yarn playwright install && yarn test-storybook:ci From 86152ec9d3de9e6abbf18aa4bb3858d3b747f8e2 Mon Sep 17 00:00:00 2001 From: Priyam Srivastava Date: Fri, 6 Feb 2026 15:44:26 +0530 Subject: [PATCH 7/9] fix: checkout PR branch in CI, not master --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cab75037..1b274ea8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,9 @@ jobs: steps: - uses: actions/checkout@v4 - + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: @@ -45,5 +47,5 @@ jobs: - name: SonarQube Scan uses: sonarsource/sonarqube-scan-action@master env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} From 0439af310ffa0d2201f005f7d60776dafb3789e8 Mon Sep 17 00:00:00 2001 From: Priyam Srivastava Date: Fri, 6 Feb 2026 16:00:25 +0530 Subject: [PATCH 8/9] ci: retrigger CI with updated master workflow From 1da2bb3a4506b7772f93578ae85b4111171fff37 Mon Sep 17 00:00:00 2001 From: Priyam Srivastava Date: Fri, 6 Feb 2026 16:14:10 +0530 Subject: [PATCH 9/9] fix: address CodeRabbit review comments --- .github/workflows/ci.yml | 7 +------ .storybook/preview.js | 6 ++++-- app/containers/App/index.js | 10 +++------- app/utils/testUtils.js | 6 ++++++ 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b274ea8..977558d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: React Template CI on: - pull_request_target: + pull_request: branches: - master @@ -14,8 +14,6 @@ jobs: steps: - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 @@ -32,9 +30,6 @@ jobs: - name: Test run: yarn test - - name: Clear Storybook cache - run: rm -rf node_modules/.cache/storybook - - name: Accessibility tests using Storybook run: yarn playwright install && yarn test-storybook:ci diff --git a/.storybook/preview.js b/.storybook/preview.js index 7baeb2d8..69c3fe56 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,4 +1,4 @@ -import ReactDOM from 'react-dom'; +import ReactDOM, { flushSync } from 'react-dom'; import { createRoot } from 'react-dom/client'; import enMessages from '../app/translations/en.json'; @@ -15,7 +15,9 @@ if (!ReactDOM.render) { root = createRoot(container); rootsMap.set(container, root); } - root.render(element); + flushSync(() => { + root.render(element); + }); if (callback) { callback(); } diff --git a/app/containers/App/index.js b/app/containers/App/index.js index edfebddf..f975e0f7 100644 --- a/app/containers/App/index.js +++ b/app/containers/App/index.js @@ -9,7 +9,6 @@ import React, { useEffect, useState, useMemo } from 'react'; import { Switch, Route } from 'react-router-dom'; -import PropTypes from 'prop-types'; import { Router } from 'react-router'; import { PersistGate } from 'redux-persist/integration/react'; import { Provider } from 'react-redux'; @@ -75,7 +74,9 @@ function AppContent() { useEffect(() => { if (location.search.includes('?redirect_uri=')) { const routeToReplace = new URLSearchParams(location.search).get('redirect_uri'); - history.replace(routeToReplace); + if (routeToReplace && routeToReplace.startsWith('/') && !routeToReplace.startsWith('//')) { + history.replace(routeToReplace); + } } const { store: s, persistor: p } = configureStore({}, history); setStore(s); @@ -133,11 +134,6 @@ function AppContent() { ); } -AppContent.propTypes = { - location: PropTypes.object, - history: PropTypes.object -}; - /** * App wrapper component that provides theme context * @returns {JSX.Element} The App component wrapped in ThemeProvider diff --git a/app/utils/testUtils.js b/app/utils/testUtils.js index 4b29edba..95dcd3ea 100644 --- a/app/utils/testUtils.js +++ b/app/utils/testUtils.js @@ -11,6 +11,12 @@ import ConnectedLanguageProvider from '@containers/LanguageProvider'; import { colors } from '@themes'; import { ThemeProvider as CustomThemeProvider } from '@app/contexts/themeContext'; +// Clean up theme side effects between tests +afterEach(() => { + localStorage.removeItem('theme'); + document.documentElement.classList.remove('dark', 'light'); +}); + // Create a basic theme for testing const testTheme = createTheme({ palette: {