diff --git a/src/ThemeProvider.tsx b/src/ThemeProvider.tsx index f637c08..8747f85 100644 --- a/src/ThemeProvider.tsx +++ b/src/ThemeProvider.tsx @@ -2,11 +2,14 @@ import React, { createContext, FC, useMemo } from "react" import { ThemeProvider as BaseThemeProvider } from "styled-components" import { darkTheme, lightTheme } from "./design/themes" +import { useIsBrowser } from "./hooks/useIsBrowser" import { useLocalStorage } from "./hooks/useLocalStorage" +import { useMedia } from "./hooks/useMedia" + +type ThemeType = "dark" | "light" export interface IThemeContext { - theme: "dark" | "light" - setTheme: () => void + theme: ThemeType toggleTheme: () => void } @@ -16,28 +19,51 @@ interface IScopedDownChildren { export const ThemeContext = createContext(null) -const ThemeProvider: FC = ({ children }) => { - const [theme, setTheme] = useLocalStorage("theme", "dark") +function useTheme() { + const preferredTheme = useMedia( + ["(prefers-color-scheme: light)", "(prefers-color-scheme: dark)"], + ["light", "dark"], + "dark" + ) + + const [userSelectedTheme, setUserSelectedTheme] = useLocalStorage< + ThemeType | "unset" + >("user-selected-theme", "unset") + + const theme = useMemo( + () => (userSelectedTheme !== "unset" ? userSelectedTheme : preferredTheme), + [preferredTheme, userSelectedTheme] + ) const themeObject = useMemo( () => (theme === "dark" ? darkTheme : lightTheme), [theme] ) + return { + theme, + themeObject, + setTheme: setUserSelectedTheme, + } +} + +const ThemeProvider: FC = ({ children }) => { + const isBrowser = useIsBrowser() + const { theme, themeObject, setTheme } = useTheme() + const contextValue = useMemo( () => ({ theme, - setTheme, - toggleTheme: () => { - setTheme(theme === "light" ? "dark" : "light") - }, + toggleTheme: () => setTheme(theme === "light" ? "dark" : "light"), }), [theme, setTheme] ) return ( - {children} + + {isBrowser ? children : undefined} + ) } diff --git a/src/hooks/useIsBrowser.tsx b/src/hooks/useIsBrowser.tsx new file mode 100644 index 0000000..cd598b6 --- /dev/null +++ b/src/hooks/useIsBrowser.tsx @@ -0,0 +1,11 @@ +import { useEffect, useState } from "react" + +export function useIsBrowser() { + const isBrowserCheck = () => typeof window !== "undefined" + + const [isBrowser, setIsBrowser] = useState(isBrowserCheck) + + useEffect(() => setIsBrowser(isBrowserCheck), []) + + return isBrowser +} diff --git a/src/hooks/useLocalStorage.tsx b/src/hooks/useLocalStorage.tsx index 8cea92a..849eb33 100644 --- a/src/hooks/useLocalStorage.tsx +++ b/src/hooks/useLocalStorage.tsx @@ -1,9 +1,13 @@ /* globals window */ import { useEffect, useState } from "react" -export const useLocalStorage = (name: string, initialValue: string) => { +export const useLocalStorage = ( + name: string, + initialValue: Value +) => { const windowGlobal = typeof window !== "undefined" && window - const [value, setValue] = useState(() => { + + const [value, setValue] = useState(() => { if (windowGlobal) { const currentValue = windowGlobal.localStorage.getItem(name) return currentValue ? JSON.parse(currentValue) : initialValue @@ -17,5 +21,5 @@ export const useLocalStorage = (name: string, initialValue: string) => { } }, [name, value, windowGlobal]) - return [value, setValue] + return [value, setValue] as const } diff --git a/src/hooks/useMedia.tsx b/src/hooks/useMedia.tsx new file mode 100644 index 0000000..d683d1e --- /dev/null +++ b/src/hooks/useMedia.tsx @@ -0,0 +1,36 @@ +/* globals window */ +import { useEffect, useState } from "react" + +export function useMedia( + queries: string[], + values: Value[], + defaultValue: Value +) { + const [currentValue, setCurrentValue] = useState(defaultValue) + + useEffect(() => { + const windowGlobal = typeof window !== "undefined" && window + + if (!windowGlobal) return + + const mediaQueries = queries.map((query) => windowGlobal.matchMedia(query)) + + const getCurrentValue = () => { + const index = mediaQueries.findIndex((value) => value.matches) + return values[index] || defaultValue + } + + setCurrentValue(getCurrentValue) + + const handler = () => setCurrentValue(getCurrentValue) + + mediaQueries.forEach((query) => query.addEventListener("change", handler)) + + return () => + mediaQueries.forEach((query) => + query.removeEventListener("change", handler) + ) + }, [queries, values, defaultValue]) + + return currentValue +}