Skip to content

Commit 7269b3c

Browse files
authored
Bring back dark mode support with simpler logic (#107)
* merge storage context into themecontext * TokenManager -> api util * reduce themecontext * lowercase util files * dead code * remove Contexts * remove theme from app * JoyUI to save in custom storage key * remove custom ThemeContext * remove custom ThemeMode * simplify theme toggles * apply theme
1 parent 55ed6cb commit 7269b3c

File tree

8 files changed

+105
-99
lines changed

8 files changed

+105
-99
lines changed

src/App.tsx

+6-32
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { NavBar } from './views/Navigation/NavBar'
2-
import { useColorScheme } from '@mui/joy'
32
import { Outlet } from 'react-router-dom'
43
import { UserContext } from './contexts/UserContext'
54
import { isTokenValid } from './utils/api'
65
import React from 'react'
7-
import { ThemeMode } from './constants/theme'
86
import { GetUserProfile } from './api/users'
97
import { User } from './models/user'
10-
import { useRoot } from './utils/dom'
118
import { WithNavigate } from './utils/navigation'
9+
import { CssBaseline, CssVarsProvider } from '@mui/joy'
1210

1311
type AppProps = WithNavigate
1412

@@ -32,37 +30,10 @@ export class App extends React.Component<AppProps, AppState> {
3230
})
3331
}
3432

35-
private applyTheme = (className: string) => {
36-
useRoot().classList.add(className)
37-
}
38-
3933
private setUserProfile = (userProfile: User | null) => {
4034
this.setState({ userProfile })
4135
}
4236

43-
private loadTheme = () => {
44-
const { mode, systemMode } = useColorScheme()
45-
const value: ThemeMode =
46-
JSON.parse(localStorage.getItem('themeMode') ?? '') || mode
47-
48-
switch (value) {
49-
default:
50-
case 'system':
51-
if (systemMode === 'dark') {
52-
this.applyTheme('dark')
53-
}
54-
break
55-
56-
case 'light':
57-
this.applyTheme('light')
58-
break
59-
60-
case 'dark':
61-
this.applyTheme('dark')
62-
break
63-
}
64-
}
65-
6637
componentDidMount(): void {
6738
if (isTokenValid()) {
6839
this.loadUserProfile()
@@ -77,8 +48,11 @@ export class App extends React.Component<AppProps, AppState> {
7748
return (
7849
<div style={{ minHeight: '100vh' }}>
7950
<UserContext.Provider value={{ userProfile, setUserProfile }}>
80-
<NavBar navigate={navigate} />
81-
<Outlet />
51+
<CssBaseline />
52+
<CssVarsProvider modeStorageKey='themeMode' attribute='data-theme' defaultMode='system' colorSchemeNode={document.body}>
53+
<NavBar navigate={navigate} />
54+
<Outlet />
55+
</CssVarsProvider>
8256
</UserContext.Provider>
8357
</div>
8458
)

src/constants/theme.ts

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1-
export type ThemeMode = 'light' | 'dark' | 'system'
1+
import { applyTheme } from '@/utils/dom'
2+
import { Mode } from '@mui/system/cssVars/useCurrentColorScheme'
23

3-
export const getNextThemeMode = (currentThemeMode: ThemeMode): ThemeMode => {
4+
export type Theme = 'light' | 'dark'
5+
6+
export const getCurrentThemeMode = (): Mode => {
7+
const mode = localStorage.getItem('themeMode')
8+
if (mode === 'light' || mode === 'dark' || mode === 'system') {
9+
return mode
10+
}
11+
12+
return 'system'
13+
}
14+
15+
export const getNextThemeMode = (currentThemeMode: Mode): Mode => {
416
switch (currentThemeMode) {
517
case 'light':
618
return 'dark'
@@ -11,3 +23,24 @@ export const getNextThemeMode = (currentThemeMode: ThemeMode): ThemeMode => {
1123
return 'light'
1224
}
1325
}
26+
27+
function prefersDarkMode() {
28+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
29+
}
30+
31+
function themeForMode(mode: Mode): Theme {
32+
switch (mode) {
33+
case 'light':
34+
case 'dark':
35+
return mode
36+
37+
case 'system':
38+
default:
39+
return prefersDarkMode() ? 'dark' : 'light'
40+
}
41+
}
42+
43+
export const setThemeMode = (mode: Mode): void => {
44+
localStorage.setItem('themeMode', mode)
45+
applyTheme(themeForMode(mode))
46+
}

src/contexts/ThemeContext.tsx

-17
This file was deleted.

src/utils/dom.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Theme } from '@/constants/theme';
2+
13
let root: HTMLDivElement | null = null
24

35
export const useRoot = (): HTMLDivElement => {
@@ -17,3 +19,7 @@ export const setTitle = (title: string): void => {
1719
export const isMobile = (): boolean => {
1820
return window.innerWidth <= 768
1921
}
22+
23+
export const applyTheme = (theme: Theme): void => {
24+
document.body.dataset.theme = theme
25+
}

src/views/Navigation/NavBar.tsx

+6-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { getNextThemeMode } from '@/constants/theme'
21
import {
32
MenuRounded,
43
HomeOutlined,
@@ -21,7 +20,6 @@ import { ThemeToggleButton } from '../Settings/ThemeToggleButton'
2120
import { NavBarLink } from './NavBarLink'
2221
import { getPathName, NavigationPaths, WithNavigate } from '@/utils/navigation'
2322
import { Logo } from '@/Logo'
24-
import { ThemeContext, ThemeContextState } from '@/contexts/ThemeContext'
2523

2624
type NavBarProps = WithNavigate
2725

@@ -91,18 +89,12 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> {
9189
fontSize: 24,
9290
}}
9391
/>
94-
<ThemeContext.Consumer>
95-
{({themeMode, setThemeMode}: ThemeContextState) => (
96-
<ThemeToggleButton
97-
themeMode={themeMode}
98-
onThemeModeToggle={() => setThemeMode(getNextThemeMode(themeMode))}
99-
sx={{
100-
position: 'absolute',
101-
right: 10,
102-
}}
103-
/>
104-
)}
105-
</ThemeContext.Consumer>
92+
<ThemeToggleButton
93+
style={{
94+
position: 'absolute',
95+
right: 10,
96+
}}
97+
/>
10698
</Box>
10799
<Drawer
108100
open={this.state.drawerOpen}

src/views/Settings/Settings.tsx

+1-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { PassowrdChangeModal } from '../Modals/Inputs/PasswordChangeModal'
55
import { APITokenSettings } from './APITokenSettings'
66
import { NotificationSetting } from '../Notifications/NotificationSettings'
77
import { ThemeToggle } from './ThemeToggle'
8-
import { ThemeContext } from '@/contexts/ThemeContext'
98

109
export class Settings extends React.Component {
1110
private changePasswordModal = React.createRef<PassowrdChangeModal>()
@@ -60,14 +59,7 @@ export class Settings extends React.Component {
6059
<Typography level='h3'>Theme preferences</Typography>
6160
<Divider />
6261

63-
<ThemeContext.Consumer>
64-
{storedState => (
65-
<ThemeToggle
66-
themeMode={storedState.themeMode}
67-
onThemeModeToggle={storedState.setThemeMode}
68-
/>
69-
)}
70-
</ThemeContext.Consumer>
62+
<ThemeToggle />
7163
</Box>
7264
</Container>
7365
)

src/views/Settings/ThemeToggle.tsx

+22-9
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
1-
import { ThemeMode } from '@/constants/theme'
1+
import { Mode } from '@mui/system/cssVars/useCurrentColorScheme'
22
import {
33
LightModeOutlined,
44
DarkModeOutlined,
55
LaptopOutlined,
66
} from '@mui/icons-material'
77
import { ToggleButtonGroup, Button, Box } from '@mui/joy'
88
import React from 'react'
9+
import { getCurrentThemeMode, setThemeMode } from '@/constants/theme'
910

10-
interface ThemeToggleProps {
11-
themeMode: ThemeMode
12-
onThemeModeToggle: (newTheme: ThemeMode) => void
11+
type ThemeToggleProps = object
12+
interface ThemeToggleState {
13+
mode: Mode
1314
}
1415

15-
export class ThemeToggle extends React.Component<ThemeToggleProps> {
16-
private onChange = (_: React.MouseEvent, newThemeMode: ThemeMode | null) => {
16+
export class ThemeToggle extends React.Component<ThemeToggleProps, ThemeToggleState> {
17+
constructor(props: ThemeToggleProps) {
18+
super(props)
19+
20+
this.state = {
21+
mode: getCurrentThemeMode(),
22+
}
23+
}
24+
25+
private onChange = (_: React.MouseEvent, newThemeMode: Mode | null) => {
1726
if (!newThemeMode) {
1827
return
1928
}
2029

21-
this.props.onThemeModeToggle(newThemeMode)
30+
setThemeMode(newThemeMode)
31+
32+
this.setState({
33+
mode: newThemeMode,
34+
})
2235
}
2336

2437
render(): React.ReactNode {
2538
const ELEMENTID = 'select-theme-mode'
26-
const { themeMode } = this.props
39+
const { mode } = this.state
2740

2841
return (
2942
<Box sx={{
@@ -32,7 +45,7 @@ export class ThemeToggle extends React.Component<ThemeToggleProps> {
3245
<ToggleButtonGroup
3346
id={ELEMENTID}
3447
variant='outlined'
35-
value={themeMode}
48+
value={mode}
3649
onChange={this.onChange}
3750
>
3851
<Button
+29-16
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,55 @@
1-
import { ThemeMode } from '@/constants/theme'
1+
import { Mode } from '@mui/system/cssVars/useCurrentColorScheme'
22
import {
33
DarkModeOutlined,
44
BrightnessAuto,
55
LightModeOutlined,
66
} from '@mui/icons-material'
7-
import { FormControl, IconButton } from '@mui/joy'
8-
import { SxProps } from '@mui/material'
7+
import { IconButton } from '@mui/joy'
98
import React from 'react'
9+
import { getCurrentThemeMode, getNextThemeMode, setThemeMode } from '@/constants/theme'
1010

1111
interface ThemeToggleButtonProps {
12-
sx: SxProps
13-
themeMode: ThemeMode
14-
onThemeModeToggle: () => void
12+
style: React.CSSProperties
1513
}
1614

17-
export class ThemeToggleButton extends React.Component<ThemeToggleButtonProps> {
18-
private onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
19-
e.preventDefault()
20-
e.stopPropagation()
15+
interface ThemeToggleButtonState {
16+
mode: Mode
17+
}
18+
19+
export class ThemeToggleButton extends React.Component<ThemeToggleButtonProps, ThemeToggleButtonState> {
20+
constructor(props: ThemeToggleButtonProps) {
21+
super(props)
22+
23+
this.state = {
24+
mode: getCurrentThemeMode(),
25+
}
26+
}
27+
28+
private onClick = () => {
29+
const nextMode = getNextThemeMode(this.state.mode)
30+
setThemeMode(nextMode)
2131

22-
this.props.onThemeModeToggle()
32+
this.setState({
33+
mode: nextMode,
34+
})
2335
}
2436

2537
render(): React.ReactNode {
26-
const { themeMode } = this.props
38+
const { style } = this.props
39+
const { mode } = this.state
2740

2841
return (
29-
<FormControl sx={this.props.sx}>
42+
<div style={style}>
3043
<IconButton onClick={this.onClick}>
31-
{themeMode === 'light' ? (
44+
{mode === 'light' ? (
3245
<DarkModeOutlined />
33-
) : themeMode === 'dark' ? (
46+
) : mode === 'dark' ? (
3447
<BrightnessAuto />
3548
) : (
3649
<LightModeOutlined />
3750
)}
3851
</IconButton>
39-
</FormControl>
52+
</div>
4053
)
4154
}
4255
}

0 commit comments

Comments
 (0)