Skip to content

Commit

Permalink
Next themes (#1061)
Browse files Browse the repository at this point in the history
* load theme without a flash (#1012)

* trying to get custom themes to work

* woww i think i got it working

* some cleanup

* still trying things, almost done

* touching up

* move load-theme script to run before ThemeProvider

* try moving it to document

* next.js script beforeInteractive
  • Loading branch information
sspenst committed Dec 14, 2023
1 parent 2ea43f1 commit 631f36b
Show file tree
Hide file tree
Showing 12 changed files with 78 additions and 102 deletions.
6 changes: 3 additions & 3 deletions components/level/grid.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions components/level/tile/block.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions components/level/tile/player.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions components/level/tile/square.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 ?
Expand Down
4 changes: 2 additions & 2 deletions components/level/tile/tile.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<HTMLDivElement>) {
Expand Down
67 changes: 32 additions & 35 deletions components/modal/themeModal.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<HTMLInputElement>) {
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);
}

Expand Down Expand Up @@ -68,28 +64,29 @@ export default function ThemeModal({ closeModal, isOpen }: ThemeModalProps) {
isOpen={isOpen}
title={'Theme'}
>
<div>
<div className='flex flex-col gap-1'>
{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 (
<div key={`${Theme[themeText]}-parent-div`} className='flex flex-row'>
<div>
<RadioButton
currentValue={theme}
key={`${Theme[themeText]}`}
name={'theme'}
onChange={onChange}
text={themeText}
value={Theme[themeText]}
/>
</div>
<span className='ml-2 w-6 h-6'>
{icon && icon({
size: 24
} as ThemeIconProps)}
</span>
<div className='flex gap-2' key={`${Theme[themeText]}-parent-div`}>
<input
checked={theme === Theme[themeText]}
id={id}
onChange={onChange}
type='radio'
value={Theme[themeText]}
/>
<label htmlFor={id}>
{themeText}
</label>
{icon &&
<span>
{icon({ size: 24 } as ThemeIconProps)}
</span>
}
</div>
);
})}
Expand Down
28 changes: 0 additions & 28 deletions components/page/radioButton.tsx

This file was deleted.

4 changes: 0 additions & 4 deletions contexts/appContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ interface AppContextInterface {
setNotifications: React.Dispatch<React.SetStateAction<Notification[]>>;
setShouldAttemptAuth: React.Dispatch<React.SetStateAction<boolean>>;
setTempCollection: React.Dispatch<React.SetStateAction<Collection | undefined>>;
setTheme: React.Dispatch<React.SetStateAction<string | undefined>>;
shouldAttemptAuth: boolean;
sounds: { [key: string]: HTMLAudioElement };
tempCollection?: Collection;
theme: string | undefined;
user?: ReqUser;
userConfig?: UserConfig;
userLoading: boolean;
Expand Down Expand Up @@ -54,10 +52,8 @@ export const AppContext = createContext<AppContextInterface>({
setNotifications: () => { return; },
setShouldAttemptAuth: () => { return; },
setTempCollection: () => { return; },
setTheme: () => { return; },
shouldAttemptAuth: true,
sounds: {},
tempCollection: undefined,
theme: undefined,
userLoading: true,
});
25 changes: 4 additions & 21 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Notification[]>([]);
const [multiplayerSocket, setMultiplayerSocket] = useState<MultiplayerSocket>({
connectedPlayers: [],
Expand All @@ -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<Collection>();
const [theme, setTheme] = useState<string>();
const { matches, privateAndInvitedMatches } = multiplayerSocket;

const mutatePlayLater = useCallback(() => {
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -372,8 +357,8 @@ export default function MyApp({ Component, pageProps, userAgent }: AppProps & {

const isEU = Intl.DateTimeFormat().resolvedOptions().timeZone.startsWith('Europe');

return (
<ThemeProvider attribute='class'>
return (<>
<ThemeProvider attribute='class' defaultTheme={Theme.Modern} themes={Object.values(Theme)}>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' />
<meta name='apple-itunes-app' content='app-id=1668925562, app-argument=pathology.gg' />
Expand Down Expand Up @@ -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,
Expand All @@ -450,5 +433,5 @@ export default function MyApp({ Component, pageProps, userAgent }: AppProps & {
</AppContext.Provider>
</GrowthBookProvider>
</ThemeProvider>
);
</>);
}
25 changes: 23 additions & 2 deletions pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -83,8 +84,28 @@ class MyDocument extends Document<DocumentProps> {
dangerouslySetInnerHTML={{ __html: this.props.browserTimingHeader }}
type='text/javascript'
/>
<Script
id='load-theme'
strategy='beforeInteractive'
dangerouslySetInnerHTML={{
__html: `
!function() {
const theme = localStorage.getItem('theme');
// set data-theme-dark for Tailwind dark classes
document.documentElement.setAttribute('data-theme-dark', theme === 'theme-light' ? 'false' : 'true');
// check for an invalid theme and default to theme-modern
// ThemeProvider doesn't handle this case with defaultTheme so we have to do it manually here
if (!${JSON.stringify(Object.values(Theme))}.includes(theme)) {
localStorage.setItem('theme', 'theme-modern');
}
}();
`,
}}
/>
</Head>
<body className={Theme.Modern}>
<body>
<Main />
<NextScript />
</body>
Expand Down
4 changes: 4 additions & 0 deletions styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ body {
--level-wall: rgb(38, 38, 38);
}

[data-theme-dark="true"] {
color-scheme: dark;
}

#arrow,
#arrow::before {
position: absolute;
Expand Down
5 changes: 4 additions & 1 deletion tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ module.exports = {
'./components/**/*.{js,jsx,ts,tsx}',
'./pages/**/*.{js,jsx,ts,tsx}',
],
darkMode: 'class',
darkMode: [
'class',
'[data-theme-dark="true"]',
],
plugins: [
require('@headlessui/tailwindcss')
],
Expand Down

0 comments on commit 631f36b

Please sign in to comment.