diff --git a/components/level/grid.tsx b/components/level/grid.tsx index e9e245ecc..29fe3bd10 100644 --- a/components/level/grid.tsx +++ b/components/level/grid.tsx @@ -1,10 +1,10 @@ import TileType from '@root/constants/tileType'; -import { AppContext } from '@root/contexts/appContext'; import { GridContext } from '@root/contexts/gridContext'; import { GameState } from '@root/helpers/gameStateHelpers'; import Position from '@root/models/position'; import classNames from 'classnames'; -import React, { useContext, useEffect, useState } from 'react'; +import { useTheme } from 'next-themes'; +import React, { useEffect, useState } from 'react'; import Theme from '../../constants/theme'; import { teko } from '../../pages/_app'; import Tile from './tile/tile'; @@ -18,7 +18,7 @@ interface GridProps { } export default function Grid({ cellClassName, gameState, id, leastMoves, onCellClick }: GridProps) { - const { theme } = useContext(AppContext); + const { theme } = useTheme(); const classic = theme === Theme.Classic; const height = gameState.board.length; const width = gameState.board[0].length; diff --git a/components/level/tile/block.tsx b/components/level/tile/block.tsx index ae16d65d0..b813e15dd 100644 --- a/components/level/tile/block.tsx +++ b/components/level/tile/block.tsx @@ -1,8 +1,8 @@ import TileType from '@root/constants/tileType'; -import { AppContext } from '@root/contexts/appContext'; import { GridContext } from '@root/contexts/gridContext'; import TileTypeHelper from '@root/helpers/tileTypeHelper'; import classNames from 'classnames'; +import { useTheme } from 'next-themes'; import React, { useContext } from 'react'; import Theme from '../../../constants/theme'; import styles from './Block.module.css'; @@ -14,7 +14,7 @@ interface BlockProps { export default function Block({ inHole, tileType }: BlockProps) { const { borderWidth, innerTileSize } = useContext(GridContext); - const { theme } = useContext(AppContext); + const { theme } = useTheme(); const classic = theme === Theme.Classic; const fillCenter = classic && tileType === TileType.Block; const innerBorderWidth = Math.round(innerTileSize / 4.5); diff --git a/components/level/tile/player.tsx b/components/level/tile/player.tsx index 81741bd66..2acdcba73 100644 --- a/components/level/tile/player.tsx +++ b/components/level/tile/player.tsx @@ -1,6 +1,6 @@ -import { AppContext } from '@root/contexts/appContext'; import { GridContext } from '@root/contexts/gridContext'; import classNames from 'classnames'; +import { useTheme } from 'next-themes'; import React, { useContext } from 'react'; import Theme, { getIconFromTheme } from '../../../constants/theme'; import TileType from '../../../constants/tileType'; @@ -16,7 +16,7 @@ export default function Player({ atEnd, moveCount }: PlayerProps) { const text = String(moveCount); const fontSizeRatio = text.length <= 3 ? 2 : (1 + (text.length - 1) / 2); const fontSize = innerTileSize / fontSizeRatio; - const { theme } = useContext(AppContext); + const { theme } = useTheme(); const classic = theme === Theme.Classic; const icon = getIconFromTheme(theme, TileType.Start); const overstepped = leastMoves !== 0 && moveCount > leastMoves; diff --git a/components/level/tile/square.tsx b/components/level/tile/square.tsx index d1d66c2a1..9ea20736d 100644 --- a/components/level/tile/square.tsx +++ b/components/level/tile/square.tsx @@ -1,6 +1,6 @@ -import { AppContext } from '@root/contexts/appContext'; import { GridContext } from '@root/contexts/gridContext'; import TileTypeHelper from '@root/helpers/tileTypeHelper'; +import { useTheme } from 'next-themes'; import React, { useContext } from 'react'; import Theme, { getIconFromTheme } from '../../../constants/theme'; import TileType from '../../../constants/tileType'; @@ -12,7 +12,7 @@ interface SquareProps { export default function Square({ text, tileType }: SquareProps) { const { borderWidth, innerTileSize, leastMoves, tileSize } = useContext(GridContext); - const { theme } = useContext(AppContext); + const { theme } = useTheme(); const classic = theme === Theme.Classic; const innerBorderWidth = Math.round(innerTileSize / 4.5); const fontSizeRatio = text === undefined || String(text).length <= 3 ? diff --git a/components/level/tile/tile.tsx b/components/level/tile/tile.tsx index eedf4eb0c..469b87b82 100644 --- a/components/level/tile/tile.tsx +++ b/components/level/tile/tile.tsx @@ -1,8 +1,8 @@ import Theme from '@root/constants/theme'; -import { AppContext } from '@root/contexts/appContext'; import { GridContext } from '@root/contexts/gridContext'; import Position from '@root/models/position'; import classNames from 'classnames'; +import { useTheme } from 'next-themes'; import React, { useContext, useMemo, useState } from 'react'; import TileType from '../../../constants/tileType'; import Block from './block'; @@ -31,7 +31,7 @@ export default function Tile({ const { borderWidth, innerTileSize, tileSize } = useContext(GridContext); // initialize the block at the starting position to avoid an animation from the top left const [initPos] = useState(new Position(pos.x, pos.y)); - const { theme } = useContext(AppContext); + const { theme } = useTheme(); const classic = theme === Theme.Classic; function onClick(e: React.MouseEvent) { diff --git a/components/modal/themeModal.tsx b/components/modal/themeModal.tsx index 59fe5d093..f99941dff 100644 --- a/components/modal/themeModal.tsx +++ b/components/modal/themeModal.tsx @@ -1,4 +1,3 @@ -import RadioButton from '@root/components/page/radioButton'; import TileType from '@root/constants/tileType'; import { useTheme } from 'next-themes'; import React, { useContext, useEffect } from 'react'; @@ -13,32 +12,29 @@ interface ThemeModalProps { } export default function ThemeModal({ closeModal, isOpen }: ThemeModalProps) { - const { mutateUser, setTheme, theme } = useContext(AppContext); - const { setTheme: setAppTheme } = useTheme(); + const { mutateUser, userConfig } = useContext(AppContext); + const { setTheme, theme } = useTheme(); + // override theme with userConfig theme useEffect(() => { - for (const className of document.body.classList.values()) { - if (className.startsWith('theme-')) { - setTheme(className); + if (!userConfig?.theme) { + return; + } - return; - } + if (Object.values(Theme).includes(userConfig.theme as Theme) && theme !== userConfig.theme) { + setTheme(userConfig.theme); } - }, [isOpen, setTheme]); + // NB: we only want this to run when the userConfig changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userConfig?.theme]); - // maintain accurate app theme for tailwind dark mode classes useEffect(() => { - setAppTheme(theme === Theme.Light ? 'light' : 'dark'); - }, [setAppTheme, theme]); + document.documentElement.setAttribute('data-theme-dark', theme === Theme.Light ? 'false' : 'true'); + }, [theme]); function onChange(e: React.ChangeEvent) { - const newTheme = e.currentTarget.value; - - if (theme !== undefined) { - document.body.classList.remove(theme); - } + const newTheme = e.currentTarget.value as Theme; - document.body.classList.add(newTheme); setTheme(newTheme); } @@ -68,28 +64,29 @@ export default function ThemeModal({ closeModal, isOpen }: ThemeModalProps) { isOpen={isOpen} title={'Theme'} > -
+
{Object.keys(Theme).map(themeTextStr => { const themeText = themeTextStr as keyof typeof Theme; const icon = getIconFromTheme(Theme[themeText], TileType.Start); + const id = `theme-${Theme[themeText]}`; return ( -
-
- -
- - {icon && icon({ - size: 24 - } as ThemeIconProps)} - +
+ + + {icon && + + {icon({ size: 24 } as ThemeIconProps)} + + }
); })} diff --git a/components/page/radioButton.tsx b/components/page/radioButton.tsx deleted file mode 100644 index c2ce1f6d1..000000000 --- a/components/page/radioButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; - -interface RadioButtonProps { - currentValue: string | undefined; - name: string; - onChange: (e: React.ChangeEvent) => void; - text: string; - value: string; -} - -export default function RadioButton({ currentValue, name, onChange, text, value }: RadioButtonProps) { - return ( - - ); -} diff --git a/contexts/appContext.ts b/contexts/appContext.ts index 117e1d0de..1b294a702 100644 --- a/contexts/appContext.ts +++ b/contexts/appContext.ts @@ -18,11 +18,9 @@ interface AppContextInterface { setNotifications: React.Dispatch>; setShouldAttemptAuth: React.Dispatch>; setTempCollection: React.Dispatch>; - setTheme: React.Dispatch>; shouldAttemptAuth: boolean; sounds: { [key: string]: HTMLAudioElement }; tempCollection?: Collection; - theme: string | undefined; user?: ReqUser; userConfig?: UserConfig; userLoading: boolean; @@ -54,10 +52,8 @@ export const AppContext = createContext({ setNotifications: () => { return; }, setShouldAttemptAuth: () => { return; }, setTempCollection: () => { return; }, - setTheme: () => { return; }, shouldAttemptAuth: true, sounds: {}, tempCollection: undefined, - theme: undefined, userLoading: true, }); diff --git a/pages/_app.tsx b/pages/_app.tsx index 3e9487e94..0e513c746 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -81,7 +81,7 @@ MyApp.getInitialProps = async ({ ctx }: { ctx: NextPageContext }) => { export default function MyApp({ Component, pageProps, userAgent }: AppProps & { userAgent: string }) { const deviceInfo = useDeviceCheck(userAgent); const forceUpdate = useForceUpdate(); - const { user, isLoading, mutateUser } = useUser(); + const { isLoading, mutateUser, user } = useUser(); const [notifications, setNotifications] = useState([]); const [multiplayerSocket, setMultiplayerSocket] = useState({ connectedPlayers: [], @@ -95,7 +95,6 @@ export default function MyApp({ Component, pageProps, userAgent }: AppProps & { const [shouldAttemptAuth, setShouldAttemptAuth] = useState(true); const [sounds, setSounds] = useState<{ [key: string]: HTMLAudioElement }>({}); const [tempCollection, setTempCollection] = useState(); - const [theme, setTheme] = useState(); const { matches, privateAndInvitedMatches } = multiplayerSocket; const mutatePlayLater = useCallback(() => { @@ -251,20 +250,6 @@ export default function MyApp({ Component, pageProps, userAgent }: AppProps & { }; }, [user?._id]); - useEffect(() => { - if (!user?.config) { - return; - } - - if (Object.values(Theme).includes(user.config.theme as Theme) && theme !== user.config.theme) { - // need to remove the default theme so we can add the userConfig theme - document.body.classList.remove(Theme.Modern); - document.body.classList.add(user.config.theme); - setTheme(user.config.theme); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user?.config]); - useEffect(() => { for (const match of matches) { // if match is active and includes user, then redirect to match page /match/[matchId] @@ -372,8 +357,8 @@ export default function MyApp({ Component, pageProps, userAgent }: AppProps & { const isEU = Intl.DateTimeFormat().resolvedOptions().timeZone.startsWith('Europe'); - return ( - + return (<> + @@ -421,11 +406,9 @@ export default function MyApp({ Component, pageProps, userAgent }: AppProps & { setNotifications: setNotifications, setShouldAttemptAuth: setShouldAttemptAuth, setTempCollection: setTempCollection, - setTheme: setTheme, shouldAttemptAuth: shouldAttemptAuth, sounds: sounds, tempCollection, - theme: theme, user: user, userConfig: user?.config, userLoading: isLoading, @@ -450,5 +433,5 @@ export default function MyApp({ Component, pageProps, userAgent }: AppProps & { - ); + ); } diff --git a/pages/_document.tsx b/pages/_document.tsx index aeb43c19f..04da593bf 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,9 +1,10 @@ /* istanbul ignore file */ +import Theme from '@root/constants/theme'; import User from '@root/models/db/user'; import { Types } from 'mongoose'; import Document, { DocumentContext, DocumentInitialProps, Head, Html, Main, NextScript } from 'next/document'; +import Script from 'next/script'; import React from 'react'; -import Theme from '../constants/theme'; import { logger } from '../helpers/logger'; import dbConnect from '../lib/dbConnect'; import isLocal from '../lib/isLocal'; @@ -83,8 +84,28 @@ class MyDocument extends Document { dangerouslySetInnerHTML={{ __html: this.props.browserTimingHeader }} type='text/javascript' /> +