diff --git a/package-lock.json b/package-lock.json
index daef1e3..3e38f4e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@emotion/cache": "^11.13.1",
"@emotion/styled": "^11.13.0",
+ "@fontsource/roboto": "^5.1.0",
"@mui/material": "^6.1.0",
"@mui/material-nextjs": "^6.0.0",
"@next/env": "^14.2.7",
@@ -2224,6 +2225,12 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@fontsource/roboto": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.1.0.tgz",
+ "integrity": "sha512-cFRRC1s6RqPygeZ8Uw/acwVHqih8Czjt6Q0MwoUoDe9U3m4dH1HmNDRBZyqlMSFwgNAUKgFImncKdmDHyKpwdg==",
+ "license": "Apache-2.0"
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
diff --git a/package.json b/package.json
index 6f5ac71..71c6654 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"dependencies": {
"@emotion/cache": "^11.13.1",
"@emotion/styled": "^11.13.0",
+ "@fontsource/roboto": "^5.1.0",
"@mui/material": "^6.1.0",
"@mui/material-nextjs": "^6.0.0",
"@next/env": "^14.2.7",
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 4bebf97..f1da86e 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,15 +1,24 @@
-import React, { ReactNode } from 'react';
+import '@fontsource/roboto/300.css';
+import '@fontsource/roboto/400.css';
+import '@fontsource/roboto/500.css';
+import '@fontsource/roboto/700.css';
import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter';
+import { CssBaseline } from '@mui/material';
+import React from 'react';
+import { ThemeProvider } from '../theme/ThemeContext';
-interface RootLayoutProps {
- children: ReactNode;
-}
-
-export default function RootLayout({ children }: RootLayoutProps) {
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
return (
- {children}
+
+
+ {children}
+
);
diff --git a/src/theme/ThemeContext.tsx b/src/theme/ThemeContext.tsx
new file mode 100644
index 0000000..028c1b0
--- /dev/null
+++ b/src/theme/ThemeContext.tsx
@@ -0,0 +1,47 @@
+'use client';
+
+import { ThemeProvider as MuiThemeProvider, Theme } from '@mui/material/styles';
+import React, { createContext, useEffect, useMemo, useState } from 'react';
+import { darkTheme, lightTheme } from './theme';
+
+interface ThemeContextType {
+ toggleTheme: () => void;
+ isDarkMode: boolean;
+}
+
+export const ThemeContext = createContext({
+ toggleTheme: () => {},
+ isDarkMode: false,
+});
+
+export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const [isDarkMode, setIsDarkMode] = useState(false);
+
+ useEffect(() => {
+ const savedTheme = localStorage.getItem('theme');
+ if (savedTheme) {
+ setIsDarkMode(savedTheme === 'dark');
+ }
+ }, []);
+
+ const toggleTheme = () => {
+ setIsDarkMode((prev) => {
+ const newMode = !prev;
+ localStorage.setItem('theme', newMode ? 'dark' : 'light');
+ return newMode;
+ });
+ };
+
+ const theme: Theme = useMemo(
+ () => (isDarkMode ? darkTheme : lightTheme),
+ [isDarkMode]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/theme/theme.ts b/src/theme/theme.ts
new file mode 100644
index 0000000..1943e1d
--- /dev/null
+++ b/src/theme/theme.ts
@@ -0,0 +1,114 @@
+import { createTheme } from '@mui/material/styles';
+
+const commonSettings = {
+ typography: {
+ fontFamily: 'Roboto, Arial, sans-serif, ',
+ body1: {
+ color: 'var(--common-white_states-main, #FFF)',
+ fontFamily: 'Roboto',
+ fontSize: '16px',
+ fontStyle: 'normal',
+ fontWeight: 400,
+ lineHeight: '24px',
+ letterSpacing: '0.15px',
+ },
+ body2: {
+ color: '#FFFFFF',
+ fontFamily: 'Roboto',
+ fontSize: '14px',
+ fontWeight: 400,
+ lineHeight: '20px',
+ letterSpacing: '0.15px',
+ },
+ h1: {
+ color: 'var(--common-white_states-main, #FFF)',
+ fontFamily: 'Roboto',
+ fontSize: '30px',
+ fontStyle: 'normal',
+ fontWeight: 500,
+ lineHeight: '24px',
+ letterSpacing: '0.15px',
+ },
+ h2: {
+ color: 'var(--common-white_states-main, #FFF)',
+ fontFamily: 'Roboto',
+ fontSize: '24px',
+ fontWeight: 500,
+ lineHeight: '32px',
+ letterSpacing: '0.1px',
+ },
+ h3: {
+ color: 'var(--common-white_states-main, #FFF)',
+ fontFamily: 'Roboto',
+ fontSize: '20px',
+ fontWeight: 500,
+ lineHeight: '28px',
+ letterSpacing: '0.1px',
+ },
+ subtitle1: {
+ color: '#D9D9D9',
+ fontFamily: 'Roboto',
+ fontSize: '16px',
+ fontWeight: 400,
+ lineHeight: '24px',
+ letterSpacing: '0.15px',
+ },
+ subtitle2: {
+ color: '#8F8C8C',
+ fontFamily: 'Roboto',
+ fontSize: '14px',
+ fontWeight: 400,
+ lineHeight: '20px',
+ letterSpacing: '0.1px',
+ },
+ caption: {
+ color: '#8F8C8C',
+ fontFamily: 'Roboto',
+ fontSize: '12px',
+ fontWeight: 400,
+ lineHeight: '16px',
+ letterSpacing: '0.4px',
+ },
+ overline: {
+ color: '#8F8C8C',
+ fontFamily: 'Roboto',
+ fontSize: '10px',
+ fontWeight: 400,
+ lineHeight: '16px',
+ letterSpacing: '1.5px',
+ textTransform: 'uppercase' as const,
+ },
+ },
+};
+
+export const lightTheme = createTheme({
+ ...commonSettings,
+ palette: {
+ mode: 'light',
+ primary: { main: '#282828' },
+ secondary: { main: '#8F8C8C' },
+ background: { default: '#D9D9D9', paper: '#fff' },
+ text: {
+ primary: '#000000',
+ secondary: '#282828',
+ },
+ },
+});
+
+export const darkTheme = createTheme({
+ ...commonSettings,
+ palette: {
+ mode: 'dark',
+ primary: { main: '#FFFFFF' },
+ secondary: { main: '#8F8C8C' },
+ background: {
+ default: '#000000',
+ paper: '#161616',
+ },
+ text: {
+ primary: '#FFFFFF',
+ secondary: '#D9D9D9',
+ },
+ divider: '#282828',
+ },
+});
diff --git a/tests/theme/theme.test.tsx b/tests/theme/theme.test.tsx
new file mode 100644
index 0000000..b71573a
--- /dev/null
+++ b/tests/theme/theme.test.tsx
@@ -0,0 +1,187 @@
+import '@testing-library/jest-dom';
+import React, { useContext } from 'react';
+import { ThemeContext, ThemeProvider } from '../../src/theme/ThemeContext';
+import { act, fireEvent, render, screen } from '@testing-library/react';
+import { darkTheme, lightTheme } from '../../src/theme/theme';
+import { useTheme } from '@mui/material/styles';
+
+const localStorageMock = (() => {
+ let store: { [key: string]: string } = {};
+ return {
+ getItem: (key: string) => store[key] || null,
+ setItem: (key: string, value: string) => {
+ store[key] = value;
+ },
+ clear: () => {
+ store = {};
+ },
+ };
+})();
+
+Object.defineProperty(window, 'localStorage', {
+ value: localStorageMock,
+});
+
+const TestThemeComponent = () => {
+ const theme = useTheme();
+ const { toggleTheme, isDarkMode } = useContext(ThemeContext);
+
+ return (
+
+
{isDarkMode ? 'Dark Mode' : 'Light Mode'}
+
{theme.palette.primary.main}
+
{theme.palette.background.default}
+
{theme.palette.text.primary}
+
+
+ );
+};
+
+describe('ThemeProvider functionality', () => {
+ beforeEach(() => {
+ window.localStorage.clear();
+ });
+
+ const renderWithThemeProvider = () =>
+ render(
+
+
+
+ );
+
+ describe('Initial Render', () => {
+ it('renders in light mode by default when no theme is stored', () => {
+ renderWithThemeProvider();
+ expect(screen.getByTestId('theme-mode')).toHaveTextContent('Light Mode');
+ expect(localStorage.getItem('theme')).toBeNull();
+ });
+
+ it('loads dark theme from localStorage on initial render', async () => {
+ localStorage.setItem('theme', 'dark');
+ await act(async () => {
+ renderWithThemeProvider();
+ });
+ expect(screen.getByTestId('theme-mode')).toHaveTextContent('Dark Mode');
+ });
+
+ it('loads light theme from localStorage on initial render', async () => {
+ localStorage.setItem('theme', 'light');
+ await act(async () => {
+ renderWithThemeProvider();
+ });
+ expect(screen.getByTestId('theme-mode')).toHaveTextContent('Light Mode');
+ });
+ });
+
+ describe('Theme Toggle Functionality', () => {
+ it('toggles from light to dark mode', async () => {
+ renderWithThemeProvider();
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('Toggle Theme'));
+ });
+
+ expect(screen.getByTestId('theme-mode')).toHaveTextContent('Dark Mode');
+ expect(localStorage.getItem('theme')).toBe('dark');
+ });
+
+ it('toggles from dark to light mode', async () => {
+ localStorage.setItem('theme', 'dark');
+ renderWithThemeProvider();
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('Toggle Theme'));
+ });
+
+ expect(screen.getByTestId('theme-mode')).toHaveTextContent('Light Mode');
+ expect(localStorage.getItem('theme')).toBe('light');
+ });
+
+ it('persists theme choice across multiple toggles', async () => {
+ renderWithThemeProvider();
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('Toggle Theme'));
+ });
+ expect(localStorage.getItem('theme')).toBe('dark');
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('Toggle Theme'));
+ });
+ expect(localStorage.getItem('theme')).toBe('light');
+ });
+ });
+
+ describe('Theme Properties', () => {
+ it('applies correct light theme properties', () => {
+ renderWithThemeProvider();
+
+ expect(screen.getByTestId('primary-color')).toHaveTextContent(
+ lightTheme.palette.primary.main
+ );
+ expect(screen.getByTestId('background-color')).toHaveTextContent(
+ lightTheme.palette.background.default
+ );
+ expect(screen.getByTestId('text-color')).toHaveTextContent(
+ lightTheme.palette.text.primary
+ );
+ });
+
+ it('applies correct dark theme properties', async () => {
+ renderWithThemeProvider();
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('Toggle Theme'));
+ });
+
+ expect(screen.getByTestId('primary-color')).toHaveTextContent(
+ darkTheme.palette.primary.main
+ );
+ expect(screen.getByTestId('background-color')).toHaveTextContent(
+ darkTheme.palette.background.default
+ );
+ expect(screen.getByTestId('text-color')).toHaveTextContent(
+ darkTheme.palette.text.primary
+ );
+ });
+
+ it('maintains theme properties after multiple toggles', async () => {
+ renderWithThemeProvider();
+
+ expect(screen.getByTestId('primary-color')).toHaveTextContent(
+ lightTheme.palette.primary.main
+ );
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('Toggle Theme'));
+ });
+ expect(screen.getByTestId('primary-color')).toHaveTextContent(
+ darkTheme.palette.primary.main
+ );
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('Toggle Theme'));
+ });
+ expect(screen.getByTestId('primary-color')).toHaveTextContent(
+ lightTheme.palette.primary.main
+ );
+ });
+ });
+
+ describe('Theme Context', () => {
+ it('provides isDarkMode value through context', () => {
+ renderWithThemeProvider();
+ expect(screen.getByTestId('theme-mode')).toHaveTextContent('Light Mode');
+ });
+
+ it('updates context value when theme changes', async () => {
+ renderWithThemeProvider();
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('Toggle Theme'));
+ });
+
+ expect(screen.getByTestId('theme-mode')).toHaveTextContent('Dark Mode');
+ });
+ });
+});