Skip to content

Instantly share code, notes, and snippets.

@adnanalbeda
Last active January 23, 2024 14:31
Show Gist options
  • Save adnanalbeda/166c60f3e64a515ed674ea6083e0fa77 to your computer and use it in GitHub Desktop.
Save adnanalbeda/166c60f3e64a515ed674ea6083e0fa77 to your computer and use it in GitHub Desktop.
Vite_React_Tailwind
// *******************************************************
// * for tailwind, in order for this to work, *
// * must have this config: (darkMode: "class"). *
// *******************************************************
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
//******************
//* types
// Why not React.ReactNode? Because react team changed their types api recently, so I'll use my own JIC.
export type Node = JSX.Element | string | number | boolean | null;
export type Nodes = Node | Node[];
export type NodesBuilder<T> = (props: T) => Nodes;
export type Children<T> = Nodes | NodesBuilder<T>;
export type ThemeValues =
| typeof AUTO_LIGHT
| typeof AUTO_DARK
| typeof LIGHT
| typeof DARK;
interface ThemeManagerValues {
theme: ThemeValues;
setTheme: (theme: ThemeValues) => void;
toggleTheme: () => void;
isDarkMode: boolean;
toggleDarkMode: () => void;
isAutoMode: boolean;
}
//******************
//* constants
const _ROOT_ = document.body;
export const _LOCAL_STORAGE_THEME_KEY_ = "tw-theme";
export const _DARK_MODE_MEDIA_QUERY_ = "(prefers-color-scheme: dark)";
const AUTO_LIGHT = "auto-light";
const AUTO_DARK = "auto-dark";
const DARK = "dark";
const LIGHT = "light";
//******************
//* utils
export const mediaMatchesDarkModeQuery = () =>
window.matchMedia(_DARK_MODE_MEDIA_QUERY_).matches;
export const getTheme = () =>
window.localStorage.getItem(_LOCAL_STORAGE_THEME_KEY_) as ThemeValues;
const setThemeInStorage = (value: ThemeValues) =>
window.localStorage.setItem(_LOCAL_STORAGE_THEME_KEY_, value);
const applyDarkStyles = () => {
_ROOT_.classList.add(DARK);
};
const removeDarkStyles = () => {
_ROOT_.classList.remove(DARK);
};
export const getCurrentAutoMode = () =>
mediaMatchesDarkModeQuery() ? AUTO_DARK : AUTO_LIGHT;
//******************
//* hooks
const useEffects = (theme: ThemeValues, setTheme: (v: ThemeValues) => void) => {
// track dark mode media changes.
const [mediaIsDarkMode, setMediaIsDarkMode] = useState(
mediaMatchesDarkModeQuery()
);
// subscribe to dark mode media query change events, only when using auto mode.
useEffect(() => {
if (theme !== AUTO_DARK && theme !== AUTO_LIGHT) return;
const callback = (e: MediaQueryListEvent) => setMediaIsDarkMode(e.matches);
window
.matchMedia(_DARK_MODE_MEDIA_QUERY_)
.addEventListener("change", callback);
return () =>
window
.matchMedia(_DARK_MODE_MEDIA_QUERY_)
.removeEventListener("change", callback);
}, [theme, mediaIsDarkMode]);
// auto update theme for auto mode.
useEffect(() => {
if (theme === DARK || theme === LIGHT) return;
setTheme(mediaIsDarkMode ? AUTO_DARK : AUTO_LIGHT);
}, [mediaIsDarkMode]);
// store theme value in local storage to use later on refresh.
useEffect(() => {
setThemeInStorage(theme);
}, [theme]);
// update root element to add or remove "dark" class.
useEffect(() => {
if (theme === DARK || theme == AUTO_DARK) applyDarkStyles();
else removeDarkStyles();
}, [theme]);
return null;
};
//****************************
//* Context
const context = createContext<ThemeManagerValues>(
{} as unknown as ThemeManagerValues
);
export const useThemeManager = () => useContext(context);
const ThemeManagerConsumer = (props: {
children?: Children<ThemeManagerValues>;
}) => {
const store = useThemeManager();
return (
<>
{typeof props.children === "function"
? props.children(store)
: props.children}
</>
);
};
const ThemeManagerProvider = (props: {
children: Nodes;
defaultTheme?: ThemeValues;
}) => {
// create theme state
const [theme, setTheme] = useState<ThemeValues>(
getTheme() === AUTO_DARK || getTheme() === AUTO_LIGHT // if stored was auto
? getCurrentAutoMode()
: getTheme() ?? props.defaultTheme ?? getCurrentAutoMode() // keep the original or the default if no original or auto if no default.
);
// prepare effects
useEffects(theme, setTheme);
// extended feature - switch to next style (light -> dark -> auto -> ...repeat)
const toggleTheme = useCallback(() => {
let value: ThemeValues = getCurrentAutoMode();
switch (theme) {
case AUTO_DARK:
case AUTO_LIGHT:
value = LIGHT;
break;
case LIGHT:
value = DARK;
break;
}
setTheme(value);
}, [theme]);
// extended feature - switch between light and dark modes. P.S. only works when not using auto (dark - light)
const toggleDarkMode = useCallback(() => {
if (theme === AUTO_DARK || theme === AUTO_LIGHT) return;
setTheme(theme === DARK ? LIGHT : DARK);
}, [theme]);
// Prepare Theme Manager Values.
const manager = useMemo<ThemeManagerValues>(
() => ({
theme,
isDarkMode: theme === DARK || theme === AUTO_DARK,
isAutoMode: theme === AUTO_DARK || theme === AUTO_LIGHT,
setTheme,
toggleTheme,
toggleDarkMode,
}),
[theme]
);
return <context.Provider value={manager}>{props.children}</context.Provider>;
};
ThemeManagerConsumer.Provider = ThemeManagerProvider;
//******************
//* EXPORTS
/**
* @example
* <ThemeManager.Provider defaultTheme="light">
* <ThemeManager>
* {
* ({toggleTheme,theme}) => (
* <button onClick={toggleTheme}>
* {theme}
* </button>
* )
* }
* </ThemeManager>
* </ThemeManager.Provider>
*/
export const ThemeManager = ThemeManagerConsumer;
export const WhenDarkMode = (props: { children?: Nodes }) => {
const { isDarkMode } = useThemeManager();
if (!isDarkMode) return null;
return <>{props.children}</>;
};
export const WhenLightMode = (props: { children?: Nodes }) => {
const { isDarkMode } = useThemeManager();
if (isDarkMode) return null;
return <>{props.children}</>;
};
export const WhenAutoMode = (props: { children?: Nodes }) => {
const { isAutoMode } = useThemeManager();
if (!isAutoMode) return null;
return <>{props.children}</>;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment