src\utils\context-utils.ts
import type { Context } from 'react';
import { useContext } from 'react';
export function useContextOrThrow<T>(context: Context<T | undefined>): T {
const theContext = useContext(context);
if (!theContext) {
throw new Error(`The hook is not used within the context.`);
}
return theContext;
}
src\utils\darkmode-utils.ts
import { createContext } from 'react';
import { useContextOrThrow } from './context-utils.ts';
export const Darkmode = {
Light: 'light',
Dark: 'dark',
} as const;
export type Darkmode = (typeof Darkmode)[keyof typeof Darkmode];
type DarkmodeContextType = {
theme: Darkmode;
toggleTheme(): void;
};
export const DarkmodeContext = createContext<DarkmodeContextType | undefined>(
undefined
);
export const useDarkmodeContext = (): DarkmodeContextType =>
useContextOrThrow(DarkmodeContext);
src\utils\DarkmodeProvider.tsx
import type { FC, PropsWithChildren } from 'react';
import { useEffect, useState } from 'react';
import { Darkmode, DarkmodeContext } from './darkmode-utils.ts';
export const DarkmodeProvider: FC<PropsWithChildren> = ({ children }) => {
const localStorageTheme = localStorage.getItem('theme') as Darkmode;
const defaultTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? Darkmode.Dark
: Darkmode.Light;
const [theme, setTheme] = useState(localStorageTheme || defaultTheme);
useEffect(() => {
if (defaultTheme) {
setTheme(theme);
document.documentElement.setAttribute('data-theme', theme);
}
}, [defaultTheme, theme]);
const toggleTheme = () => {
const newTheme = theme === Darkmode.Light ? Darkmode.Dark : Darkmode.Light;
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
};
return (
<DarkmodeContext.Provider value={{ theme, toggleTheme }}>
{children}
</DarkmodeContext.Provider>
);
};
src\components\DarkmodeToggle.tsx
import type { FC } from 'react';
import { useState } from 'react';
import MoonIcon from '../../assets/MoonIcon';
import SunIcon from '../../assets/SunIcon';
import { Darkmode, useDarkmodeContext } from '../utils/darkmode-utils.ts';
import styles from './DarkmodeToggle.module.less';
interface DarkmodeToggleProps {
className?: string;
}
const DarkmodeToggle: FC<DarkmodeToggleProps> = ({ className }) => {
const { theme, toggleTheme } = useDarkmodeContext();
const [isToggled, setIsToggled] = useState(false);
const handleClick = () => {
toggleTheme();
setIsToggled(!isToggled);
};
return (
<button
onClick={handleClick}
aria-label="Toggle dark mode"
className={`${styles.darkmodeToggle} ${className}`}
>
{theme === Darkmode.Dark ? <SunIcon /> : <MoonIcon />}
</button>
);
};
export default DarkmodeToggle;
src\components\DarkmodeToggle.module.less
.darkmodeToggle {
background-color: unset;
min-width: 40px;
height: 40px;
display: flex;
& svg {
width: 20px;
height: auto;
margin: auto;
}
}
src\App.tsx
import MainPage from './page/MainPage.tsx';
import { DarkmodeProvider } from './utils/DarkmodeProvider';
import './App.less';
function App() {
return (
<DarkmodeProvider>
<MainPage />;
</DarkmodeProvider>
);
}
export default App;
src\pages\MainPage.tsx
import { useState } from 'react';
import List from '../components/List.tsx';
import ToggleView from '../components/ToggleView.tsx';
import DarkmodeToggle from '../components/DarkmodeToggle';
import styles from './MainPage.module.less';
const MainPage = () => {
const [view, setView] = useState<'characters' | 'episodes' | 'locations'>(
'characters'
);
return (
<main>
<div className={styles.buttonContainer}>
<DarkmodeToggle />
</div>
<div className={styles.starBackground}></div>
<div className={styles.pageContainer}>
<h1>WUBBA LUBBA FETCH FETCH</h1>
<p>Morty, we’re fetching data, not feelings</p>
<ToggleView view={view} onChange={setView} />
<List view={view} />
</div>
</main>
);
};
export default MainPage;