Skip to content

Commit

Permalink
Replace ThemeToggleButton with ThemeSelector
Browse files Browse the repository at this point in the history
  • Loading branch information
Genne23v authored and manekenpix committed Nov 20, 2022
1 parent 25c2ddd commit 450ac6f
Show file tree
Hide file tree
Showing 14 changed files with 328 additions and 79 deletions.
Binary file added src/web/app/public/colorThemes/dark-default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/web/app/public/colorThemes/dark-dim.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/web/app/public/colorThemes/light-default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/web/app/src/components/NavBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import useAuth from '../../hooks/use-auth';
* This ensures that the version displayed to user is the client view which ties to the client's preference theme.
* This is only an issue on DesktopHeader since on MobileHeader there is a listener triggering rerendering.
* */
const DynamicThemeToggleButton = dynamic(() => import('../ThemeToggleButton'), {

const DynamicThemeSelector = dynamic(() => import('../ThemeSelector'), {
ssr: false,
});

Expand Down Expand Up @@ -159,7 +160,7 @@ export default function NavBar({ disabled }: NavBarProps) {
<NavBarButton {...props} key={props.title} />
))}
{!user && <Login />}
<DynamicThemeToggleButton />
<DynamicThemeSelector />
{user && (
<ButtonTooltip title="Logout" arrow placement="top" TransitionComponent={Zoom}>
<button type="button" className={classes.avatar} onClick={() => logout()}>
Expand Down
11 changes: 5 additions & 6 deletions src/web/app/src/components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { createContext, useContext } from 'react';
import { Theme } from '@material-ui/core/styles';

import { ThemeName } from '../interfaces';
import { lightTheme } from '../theme';
import { ThemeName, LIGHT_DEFAULT } from '../interfaces/index';

type ThemeContextType = {
theme: Theme;
themeName: ThemeName;
toggleTheme: () => void;
preferredTheme?: ThemeName;
changeTheme: (themeId: ThemeName) => void;
};

export const ThemeContext = createContext<ThemeContextType>({
theme: lightTheme,
themeName: 'light',
toggleTheme: () => console.warn('missing theme provider'),
preferredTheme: LIGHT_DEFAULT,
changeTheme: () => console.warn('missing change theme provider'),
});

export const useTheme = () => useContext(ThemeContext);
151 changes: 151 additions & 0 deletions src/web/app/src/components/ThemeSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// eslint-disable-next-line no-use-before-define
import React, { useState } from 'react';
import {
IconButton,
Popover,
Typography,
Paper,
Tooltip,
Zoom,
Divider,
MenuList,
MenuItem,
ListItemText,
} from '@material-ui/core';
import { List, ListSubheader } from '@mui/material';
import PaletteIcon from '@mui/icons-material/Palette';
import { makeStyles, withStyles } from '@material-ui/core/styles';
import { useTheme } from './ThemeProvider';
import {
Theme,
ThemeName,
LIGHT_DEFAULT,
LIGHT_HIGH_CONTRAST,
DARK_DEFAULT,
DARK_DIM,
} from '../interfaces/index';

const lightDefaultLogoUrl = '/colorThemes/light-default.png';
const darkDefaultLogoUrl = '/colorThemes/dark-default.png';
const lightContrastLogoUrl = '/colorThemes/light-high-contrast.png';
const darkDimLogoUrl = '/colorThemes/dark-dim.png';
const themes: Theme[] = [
{ title: 'Light Default', id: LIGHT_DEFAULT, image: lightDefaultLogoUrl },
{ title: 'Light High Contrast', id: LIGHT_HIGH_CONTRAST, image: lightContrastLogoUrl },
{ title: 'Dark Default', id: DARK_DEFAULT, image: darkDefaultLogoUrl },
{ title: 'Dark Dim', id: DARK_DIM, image: darkDimLogoUrl },
];

const useStyles = makeStyles((theme) => ({
menuSubheader: {
color: theme.palette.text.primary,
backgroundColor: 'transparent',
},
popoverPaper: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.paper,
},
themeSelectButton: {
'&:hover': {
color: theme.palette.action.active,
},
},
themeSelectorButton: {
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: 'transparent',
color: theme.palette.text.primary,
},
},
selected: {
color: 'black',
backgroundColor: '#A9A9A9 !important',
},
}));

const ButtonTooltip = withStyles({
tooltip: {
fontSize: '1.5rem',
margin: 0,
},
})(Tooltip);

const ThemeSelector = () => {
const classes = useStyles();
const { preferredTheme, changeTheme } = useTheme();
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);

const handleClick = (e: React.MouseEvent<HTMLElement>) => {
setAnchorEl(e.currentTarget);
};

const handleClose = () => {
setAnchorEl(null);
};

const handleChange = (id: ThemeName): void => {
changeTheme(id);
};

const open = Boolean(anchorEl);
const id = open ? 'simple-popover' : undefined;

return (
<div>
<ButtonTooltip
title="Change Colour Theme"
arrow
placement="top"
TransitionComponent={Zoom}
onClick={handleClick}
>
<IconButton className={classes.themeSelectorButton}>
<PaletteIcon fontSize="large" />
</IconButton>
</ButtonTooltip>

<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
PaperProps={{
style: { width: '240px' },
}}
>
<Paper className={classes.popoverPaper}>
<List>
<ListSubheader className={classes.menuSubheader}>
<Typography variant="h6" gutterBottom>
Change Colour Theme
</Typography>
</ListSubheader>
<Divider />
<MenuList>
{themes.map((theme) => {
return (
<MenuItem
key={theme.id}
onClick={() => handleChange(theme.id)}
selected={preferredTheme === theme.id}
className={classes.themeSelectButton}
classes={{ selected: classes.selected }}
>
<ListItemText>{theme.title}</ListItemText>
<img src={theme.image} className="palette-preview" alt={theme.title} />
</MenuItem>
);
})}
</MenuList>
</List>
</Paper>
</Popover>
</div>
);
};

export default ThemeSelector;
42 changes: 0 additions & 42 deletions src/web/app/src/components/ThemeToggleButton.tsx

This file was deleted.

18 changes: 12 additions & 6 deletions src/web/app/src/hooks/use-preferred-theme.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import { useEffect } from 'react';
import { useLocalStorage, useMedia } from 'react-use';
import { useLocalStorage } from 'react-use';
import {
ThemeName,
LIGHT_DEFAULT,
LIGHT_HIGH_CONTRAST,
DARK_DEFAULT,
DARK_DIM,
} from '../interfaces/index';

/**
* Combination of localStorage for remembering the user's preference between
* loads of the app, and initial logic to get the browser's preferred colour.
*/
export default function usePreferredTheme() {
const isDarkThemePreferred = useMedia('(prefers-color-scheme: dark)', false);
const [preferredTheme, setPreferredTheme] = useLocalStorage(
const [preferredTheme, setPreferredTheme] = useLocalStorage<ThemeName>(
'preference:theme',
isDarkThemePreferred ? 'dark' : 'light'
LIGHT_DEFAULT
);
useEffect(() => {
const lightStyleSheet = (document.querySelector('#light-stylesheet') as HTMLStyleElement).sheet;

if (lightStyleSheet !== null) {
lightStyleSheet.disabled = preferredTheme === 'dark';
lightStyleSheet.disabled = preferredTheme === (DARK_DEFAULT || DARK_DIM);
}

const darkStyleSheet = (document.querySelector('#dark-stylesheet') as HTMLStyleElement).sheet;

if (darkStyleSheet !== null) {
darkStyleSheet.disabled = preferredTheme === 'light';
darkStyleSheet.disabled = preferredTheme === (LIGHT_DEFAULT || LIGHT_HIGH_CONTRAST);
}
}, [preferredTheme]);

Expand Down
15 changes: 14 additions & 1 deletion src/web/app/src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
const LIGHT_DEFAULT = 'light-default';
const LIGHT_HIGH_CONTRAST = 'light-high-contrast';
const DARK_DEFAULT = 'dark-default';
const DARK_DIM = 'dark-dim';

export type Feed = {
id: string;
author: string;
Expand Down Expand Up @@ -45,4 +50,12 @@ export type SignUpForm = {
channelOwnership: boolean;
};

export type ThemeName = 'light' | 'dark';
export type ThemeName = 'light-default' | 'light-high-contrast' | 'dark-default' | 'dark-dim';

export type Theme = {
title: string;
id: ThemeName;
image: string;
};

export { LIGHT_DEFAULT, LIGHT_HIGH_CONTRAST, DARK_DEFAULT, DARK_DIM };
62 changes: 50 additions & 12 deletions src/web/app/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import { useEffect } from 'react';
import { AppProps } from 'next/app';
import { ThemeProvider } from '@material-ui/core/styles';
import { ThemeProvider, Theme } from '@material-ui/core/styles';
import { SWRConfig } from 'swr';

import { ThemeContext } from '../components/ThemeProvider';
import {
ThemeName,
LIGHT_DEFAULT,
LIGHT_HIGH_CONTRAST,
DARK_DEFAULT,
DARK_DIM,
} from '../interfaces/index';
import AuthProvider from '../components/AuthProvider';

import { darkTheme, lightTheme } from '../theme';
import usePreferredTheme from '../hooks/use-preferred-theme';
import { ThemeContext } from '../components/ThemeProvider';
import { darkTheme, lightTheme, darkDimTheme, lightHighContrastTheme } from '../theme';

import '../styles/globals.css';
import '@fontsource/spartan';
import '@fontsource/pt-serif';

// Reference: https://github.com/mui-org/material-ui/blob/master/examples/nextjs/pages/_app.js
const App = ({ Component, pageProps }: AppProps) => {
// Use the preferred theme for this user and the browser (one of 'dark' or 'light').
let theme: Theme;
// Use the preferred theme for this user and the browser
const [preferredTheme, setPreferredTheme] = usePreferredTheme();
// Set our initial theme to be the preferred system theme, or light theme be default,
const theme = preferredTheme === 'dark' ? darkTheme : lightTheme;
switch (preferredTheme) {
case LIGHT_DEFAULT:
theme = lightTheme;
break;
case LIGHT_HIGH_CONTRAST:
theme = lightHighContrastTheme;
break;
case DARK_DEFAULT:
theme = darkTheme;
break;
case DARK_DIM:
theme = darkDimTheme;
break;
default:
setPreferredTheme(LIGHT_DEFAULT);
theme = lightTheme;
break;
}

// This hook is for ensuring the styling is in sync between client and server
useEffect(() => {
Expand All @@ -29,16 +51,32 @@ const App = ({ Component, pageProps }: AppProps) => {
}
}, []);

// Switch the active theme, and also store it for next load
const toggleTheme = () => {
setPreferredTheme(preferredTheme === 'dark' ? 'light' : 'dark');
const changeTheme = (themeId: ThemeName) => {
switch (themeId) {
case LIGHT_DEFAULT:
theme = lightTheme;
break;
case LIGHT_HIGH_CONTRAST:
theme = lightHighContrastTheme;
break;
case DARK_DEFAULT:
theme = darkTheme;
break;
case DARK_DIM:
theme = darkDimTheme;
break;
default:
console.warn('no theme is selected');
break;
}
setPreferredTheme(themeId);
};

return (
<SWRConfig
value={{ fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()) }}
>
<ThemeContext.Provider value={{ theme, themeName: theme.palette.type, toggleTheme }}>
<ThemeContext.Provider value={{ theme, preferredTheme, changeTheme }}>
<ThemeProvider theme={theme}>
<AuthProvider>
<Component {...pageProps} />
Expand Down
Loading

0 comments on commit 450ac6f

Please sign in to comment.