From 3f0a9854cb7ca935a459d6f5372ef9cfda5d7f27 Mon Sep 17 00:00:00 2001 From: Brad Garropy Date: Fri, 24 Feb 2023 15:22:39 -0600 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=91=20persist=20darkness=20(#349)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Brad Garropy --- package-lock.json | 4 +- package.json | 2 +- public/.gitkeep | 0 public/theme.js | 9 +++ src/components/ColorTheme/ColorTheme.tsx | 3 +- src/context/App.tsx | 2 +- src/hooks/useTheme/useTheme.test.ts | 80 +++++++++++++++++------- src/hooks/useTheme/useTheme.ts | 24 ++++--- src/pages/_document.tsx | 2 + 9 files changed, 91 insertions(+), 35 deletions(-) delete mode 100644 public/.gitkeep create mode 100644 public/theme.js diff --git a/package-lock.json b/package-lock.json index b880e40c..eac69f22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bradgarropy.com", - "version": "6.0.0", + "version": "6.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bradgarropy.com", - "version": "6.0.0", + "version": "6.1.0", "license": "MIT", "dependencies": { "@bradgarropy/captivate-sdk": "^0.4.0", diff --git a/package.json b/package.json index 357bcc3d..c207b9dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bradgarropy.com", - "version": "6.0.0", + "version": "6.1.0", "description": "🏠 my home on the web", "keywords": [ "javascript", diff --git a/public/.gitkeep b/public/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/public/theme.js b/public/theme.js new file mode 100644 index 00000000..ddd3d9d2 --- /dev/null +++ b/public/theme.js @@ -0,0 +1,9 @@ +const localTheme = window.localStorage.getItem("theme") + +if (localTheme === "light") { + document.documentElement.classList.remove("dark") +} + +if (localTheme === "dark") { + document.documentElement.classList.add("dark") +} diff --git a/src/components/ColorTheme/ColorTheme.tsx b/src/components/ColorTheme/ColorTheme.tsx index 1293f732..66b25d6f 100644 --- a/src/components/ColorTheme/ColorTheme.tsx +++ b/src/components/ColorTheme/ColorTheme.tsx @@ -21,7 +21,8 @@ const ColorTheme: FC = () => { onClick={onClick} aria-label={label} > - {theme === "light" ? : } + {theme === "light" ? : null} + {theme === "dark" ? : null} ) } diff --git a/src/context/App.tsx b/src/context/App.tsx index 5f33364e..75f7f888 100644 --- a/src/context/App.tsx +++ b/src/context/App.tsx @@ -10,7 +10,7 @@ type AppProviderProps = { const AppProvider: FC = ({children}) => { const [open, setOpen] = useState(false) - const [theme, setTheme] = useTheme() + const {theme, setTheme} = useTheme() const context = { open, diff --git a/src/hooks/useTheme/useTheme.test.ts b/src/hooks/useTheme/useTheme.test.ts index 84323e09..0f8431db 100644 --- a/src/hooks/useTheme/useTheme.test.ts +++ b/src/hooks/useTheme/useTheme.test.ts @@ -1,42 +1,80 @@ import {act, renderHook} from "@testing-library/react" import {useTheme} from "hooks" -test("returns default theme", () => { +const mockGetItem = jest.fn() +const mockSetItem = jest.fn() + +beforeEach(() => { + global.Storage.prototype.getItem = mockGetItem + global.Storage.prototype.setItem = mockSetItem +}) + +afterEach(() => { + mockGetItem.mockReset() + mockGetItem.mockReset() +}) + +test("defaults to light theme", () => { + mockGetItem.mockReturnValue(null) + const {result} = renderHook(() => useTheme()) - const [theme] = result.current - expect(theme).toEqual("light") + + expect(result.current.theme).toEqual("light") + expect(mockSetItem).toHaveBeenCalledTimes(1) + expect(mockSetItem).toHaveBeenCalledWith("theme", "light") + expect(document.documentElement.classList.contains("dark")).toBeFalsy() }) -test("returns light theme", () => { - const {result} = renderHook(() => useTheme("light")) - const [theme] = result.current - expect(theme).toEqual("light") +test("uses localstorage light theme", () => { + mockGetItem.mockReturnValue("light") + + const {result} = renderHook(() => useTheme()) + + expect(result.current.theme).toEqual("light") + expect(mockSetItem).toHaveBeenCalledTimes(1) + expect(mockSetItem).toHaveBeenCalledWith("theme", "light") + expect(document.documentElement.classList.contains("dark")).toBeFalsy() }) -test("returns dark theme", () => { - const {result} = renderHook(() => useTheme("dark")) - const [theme] = result.current - expect(theme).toEqual("dark") +test("uses localstorage dark theme", () => { + mockGetItem.mockReturnValue("dark") + + const {result} = renderHook(() => useTheme()) + + expect(result.current.theme).toEqual("dark") + expect(mockSetItem).toHaveBeenCalledTimes(1) + expect(mockSetItem).toHaveBeenCalledWith("theme", "dark") + expect(document.documentElement.classList.contains("dark")).toBeTruthy() }) -test("sets dark theme", () => { - const {result} = renderHook(() => useTheme("light")) - expect(result.current[0]).toEqual("light") +test("switches to light theme", () => { + mockGetItem.mockReturnValue("dark") + + const {result} = renderHook(() => useTheme()) + expect(result.current.theme).toEqual("dark") act(() => { - result.current[1]("dark") + result.current.setTheme("light") }) - expect(result.current[0]).toEqual("dark") + expect(result.current.theme).toEqual("light") + expect(mockSetItem).toHaveBeenCalledTimes(2) + expect(mockSetItem).toHaveBeenLastCalledWith("theme", "light") + expect(document.documentElement.classList.contains("dark")).toBeFalsy() }) -test("sets light theme", () => { - const {result} = renderHook(() => useTheme("dark")) - expect(result.current[0]).toEqual("dark") +test("switches to dark theme", () => { + mockGetItem.mockReturnValue("light") + + const {result} = renderHook(() => useTheme()) + expect(result.current.theme).toEqual("light") act(() => { - result.current[1]("light") + result.current.setTheme("dark") }) - expect(result.current[0]).toEqual("light") + expect(result.current.theme).toEqual("dark") + expect(mockSetItem).toHaveBeenCalledTimes(2) + expect(mockSetItem).toHaveBeenLastCalledWith("theme", "dark") + expect(document.documentElement.classList.contains("dark")).toBeTruthy() }) diff --git a/src/hooks/useTheme/useTheme.ts b/src/hooks/useTheme/useTheme.ts index 013ac338..cb72c0e7 100644 --- a/src/hooks/useTheme/useTheme.ts +++ b/src/hooks/useTheme/useTheme.ts @@ -1,27 +1,33 @@ -import {Dispatch, SetStateAction, useEffect, useRef, useState} from "react" +import {Dispatch, SetStateAction, useEffect, useState} from "react" type Theme = "light" | "dark" -const useTheme = ( - defaultTheme: Theme = "light", -): [Theme, Dispatch>] => { - const initialRender = useRef(true) - const [theme, setTheme] = useState(defaultTheme) +const useTheme = (): { + theme: Theme + setTheme: Dispatch> +} => { + const [theme, setTheme] = useState() useEffect(() => { - if (initialRender.current) { - initialRender.current = false + const localTheme = window.localStorage.getItem("theme") as Theme + setTheme(localTheme ?? "light") + }, []) + + useEffect(() => { + if (!theme) { return } if (theme === "dark") { + window.localStorage.setItem("theme", "dark") document.documentElement.classList.add("dark") } else { + window.localStorage.setItem("theme", "light") document.documentElement.classList.remove("dark") } }, [theme]) - return [theme, setTheme] + return {theme, setTheme} } export default useTheme diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index d680736a..149448f5 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -5,6 +5,7 @@ import Document, { Main, NextScript, } from "next/document" +import Script from "next/script" class MyDocument extends Document { static async getInitialProps(ctx: DocumentContext) { @@ -35,6 +36,7 @@ class MyDocument extends Document {
+