Changed how theme is set and stored on client

This commit is contained in:
Paweł Malak 2022-03-23 14:49:35 +01:00
parent e427fbf54c
commit 89bd921875
12 changed files with 92 additions and 191 deletions

View file

@ -10,7 +10,7 @@ import { actionCreators, store } from './store';
import { State } from './store/reducers'; import { State } from './store/reducers';
// Utils // Utils
import { checkVersion, decodeToken } from './utility'; import { checkVersion, decodeToken, parsePABToTheme } from './utility';
// Routes // Routes
import { Home } from './components/Home/Home'; import { Home } from './components/Home/Home';
@ -31,7 +31,7 @@ export const App = (): JSX.Element => {
const { config, loading } = useSelector((state: State) => state.config); const { config, loading } = useSelector((state: State) => state.config);
const dispath = useDispatch(); const dispath = useDispatch();
const { fetchQueries, setTheme, logout, createNotification } = const { fetchQueries, setTheme, logout, createNotification, fetchThemes } =
bindActionCreators(actionCreators, dispath); bindActionCreators(actionCreators, dispath);
useEffect(() => { useEffect(() => {
@ -51,9 +51,12 @@ export const App = (): JSX.Element => {
} }
}, 1000); }, 1000);
// load themes
fetchThemes();
// set user theme if present // set user theme if present
if (localStorage.theme) { if (localStorage.theme) {
setTheme(localStorage.theme); setTheme(parsePABToTheme(localStorage.theme));
} }
// check for updated // check for updated
@ -68,7 +71,7 @@ export const App = (): JSX.Element => {
// If there is no user theme, set the default one // If there is no user theme, set the default one
useEffect(() => { useEffect(() => {
if (!loading && !localStorage.theme) { if (!loading && !localStorage.theme) {
setTheme(config.defaultTheme, false); setTheme(parsePABToTheme(config.defaultTheme), false);
} }
}, [loading]); }, [loading]);

View file

@ -1,8 +1,3 @@
// Redux
import { useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../../store';
// Components // Components
import { ThemePreview } from '../ThemePreview/ThemePreview'; import { ThemePreview } from '../ThemePreview/ThemePreview';
@ -15,14 +10,11 @@ interface Props {
} }
export const ThemeGrid = ({ themes }: Props): JSX.Element => { export const ThemeGrid = ({ themes }: Props): JSX.Element => {
const dispatch = useDispatch();
const { setTheme } = bindActionCreators(actionCreators, dispatch);
return ( return (
<div className={classes.ThemerGrid}> <div className={classes.ThemerGrid}>
{themes.map( {themes.map(
(theme: Theme, idx: number): JSX.Element => ( (theme: Theme, idx: number): JSX.Element => (
<ThemePreview key={idx} theme={theme} applyTheme={setTheme} /> <ThemePreview key={idx} theme={theme} />
) )
)} )}
</div> </div>

View file

@ -1,32 +1,38 @@
// Redux
import { useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../../store';
// Other
import { Theme } from '../../../../interfaces/Theme'; import { Theme } from '../../../../interfaces/Theme';
import classes from './ThemePreview.module.css'; import classes from './ThemePreview.module.css';
interface Props { interface Props {
theme: Theme; theme: Theme;
applyTheme: Function;
} }
export const ThemePreview = (props: Props): JSX.Element => { export const ThemePreview = ({
theme: { colors, name },
}: Props): JSX.Element => {
const { setTheme } = bindActionCreators(actionCreators, useDispatch());
return ( return (
<div <div className={classes.ThemePreview} onClick={() => setTheme(colors)}>
className={classes.ThemePreview}
onClick={() => props.applyTheme(props.theme.name)}
>
<div className={classes.ColorsPreview}> <div className={classes.ColorsPreview}>
<div <div
className={classes.ColorPreview} className={classes.ColorPreview}
style={{ backgroundColor: props.theme.colors.background }} style={{ backgroundColor: colors.background }}
></div> ></div>
<div <div
className={classes.ColorPreview} className={classes.ColorPreview}
style={{ backgroundColor: props.theme.colors.primary }} style={{ backgroundColor: colors.primary }}
></div> ></div>
<div <div
className={classes.ColorPreview} className={classes.ColorPreview}
style={{ backgroundColor: props.theme.colors.accent }} style={{ backgroundColor: colors.accent }}
></div> ></div>
</div> </div>
<p>{props.theme.name}</p> <p>{name}</p>
</div> </div>
); );
}; };

View file

@ -9,12 +9,11 @@ import { actionCreators } from '../../../store';
import { Theme, ThemeSettingsForm } from '../../../interfaces'; import { Theme, ThemeSettingsForm } from '../../../interfaces';
// Components // Components
import { Button, InputGroup, SettingsHeadline } from '../../UI'; import { Button, InputGroup, SettingsHeadline, Spinner } from '../../UI';
import { ThemeBuilder } from './ThemeBuilder/ThemeBuilder'; import { ThemeBuilder } from './ThemeBuilder/ThemeBuilder';
import { ThemeGrid } from './ThemeGrid/ThemeGrid'; import { ThemeGrid } from './ThemeGrid/ThemeGrid';
// Other // Other
import { themes } from './themes.json';
import { State } from '../../../store/reducers'; import { State } from '../../../store/reducers';
import { inputHandler, themeSettingsTemplate } from '../../../utility'; import { inputHandler, themeSettingsTemplate } from '../../../utility';
@ -22,6 +21,7 @@ export const Themer = (): JSX.Element => {
const { const {
auth: { isAuthenticated }, auth: { isAuthenticated },
config: { loading, config }, config: { loading, config },
theme: { themes },
} = useSelector((state: State) => state); } = useSelector((state: State) => state);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -63,10 +63,10 @@ export const Themer = (): JSX.Element => {
return ( return (
<Fragment> <Fragment>
<SettingsHeadline text="App themes" /> <SettingsHeadline text="App themes" />
<ThemeGrid themes={themes} /> {!themes.length ? <Spinner /> : <ThemeGrid themes={themes} />}
<SettingsHeadline text="User themes" /> {/* <SettingsHeadline text="User themes" />
<ThemeBuilder /> <ThemeBuilder /> */}
{isAuthenticated && ( {isAuthenticated && (
<form onSubmit={formSubmitHandler}> <form onSubmit={formSubmitHandler}>

View file

@ -1,124 +0,0 @@
{
"themes": [
{
"name": "blackboard",
"colors": {
"background": "#1a1a1a",
"primary": "#FFFDEA",
"accent": "#5c5c5c"
}
},
{
"name": "gazette",
"colors": {
"background": "#F2F7FF",
"primary": "#000000",
"accent": "#5c5c5c"
}
},
{
"name": "espresso",
"colors": {
"background": "#21211F",
"primary": "#D1B59A",
"accent": "#4E4E4E"
}
},
{
"name": "cab",
"colors": {
"background": "#F6D305",
"primary": "#1F1F1F",
"accent": "#424242"
}
},
{
"name": "cloud",
"colors": {
"background": "#f1f2f0",
"primary": "#35342f",
"accent": "#37bbe4"
}
},
{
"name": "lime",
"colors": {
"background": "#263238",
"primary": "#AABBC3",
"accent": "#aeea00"
}
},
{
"name": "white",
"colors": {
"background": "#ffffff",
"primary": "#222222",
"accent": "#dddddd"
}
},
{
"name": "tron",
"colors": {
"background": "#242B33",
"primary": "#EFFBFF",
"accent": "#6EE2FF"
}
},
{
"name": "blues",
"colors": {
"background": "#2B2C56",
"primary": "#EFF1FC",
"accent": "#6677EB"
}
},
{
"name": "passion",
"colors": {
"background": "#f5f5f5",
"primary": "#12005e",
"accent": "#8e24aa"
}
},
{
"name": "chalk",
"colors": {
"background": "#263238",
"primary": "#AABBC3",
"accent": "#FF869A"
}
},
{
"name": "paper",
"colors": {
"background": "#F8F6F1",
"primary": "#4C432E",
"accent": "#AA9A73"
}
},
{
"name": "neon",
"colors": {
"background": "#091833",
"primary": "#EFFBFF",
"accent": "#ea00d9"
}
},
{
"name": "pumpkin",
"colors": {
"background": "#2d3436",
"primary": "#EFFBFF",
"accent": "#ffa500"
}
},
{
"name": "onedark",
"colors": {
"background": "#282c34",
"primary": "#dfd9d6",
"accent": "#98c379"
}
}
]
}

View file

@ -1,8 +1,10 @@
export interface ThemeColors {
background: string;
primary: string;
accent: string;
}
export interface Theme { export interface Theme {
name: string; name: string;
colors: { colors: ThemeColors;
background: string; }
primary: string;
accent: string;
}
}

View file

@ -1,30 +1,32 @@
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { SetThemeAction } from '../actions/theme'; import { FetchThemesAction, SetThemeAction } from '../actions/theme';
import { ActionType } from '../action-types'; import { ActionType } from '../action-types';
import { Theme } from '../../interfaces/Theme'; import { Theme, ApiResponse, ThemeColors } from '../../interfaces';
import { themes } from '../../components/Settings/Themer/themes.json'; import { parseThemeToPAB } from '../../utility';
import axios from 'axios';
export const setTheme = export const setTheme =
(name: string, remeberTheme: boolean = true) => (colors: ThemeColors, remeberTheme: boolean = true) =>
(dispatch: Dispatch<SetThemeAction>) => { (dispatch: Dispatch<SetThemeAction>) => {
const theme = themes.find((theme) => theme.name === name); if (remeberTheme) {
localStorage.setItem('theme', parseThemeToPAB(colors));
}
if (theme) { for (const [key, value] of Object.entries(colors)) {
if (remeberTheme) { document.body.style.setProperty(`--color-${key}`, value);
localStorage.setItem('theme', name);
}
loadTheme(theme);
dispatch({
type: ActionType.setTheme,
payload: theme,
});
} }
}; };
export const loadTheme = (theme: Theme): void => { export const fetchThemes =
for (const [key, value] of Object.entries(theme.colors)) { () => async (dispatch: Dispatch<FetchThemesAction>) => {
document.body.style.setProperty(`--color-${key}`, value); try {
} const res = await axios.get<ApiResponse<Theme[]>>('/api/themes');
};
dispatch({
type: ActionType.fetchThemes,
payload: res.data.data,
});
} catch (err) {
console.log(err);
}
};

View file

@ -1,6 +1,7 @@
export enum ActionType { export enum ActionType {
// THEME // THEME
setTheme = 'SET_THEME', setTheme = 'SET_THEME',
fetchThemes = 'FETCH_THEMES',
// CONFIG // CONFIG
getConfig = 'GET_CONFIG', getConfig = 'GET_CONFIG',
updateConfig = 'UPDATE_CONFIG', updateConfig = 'UPDATE_CONFIG',

View file

@ -3,5 +3,9 @@ import { Theme } from '../../interfaces';
export interface SetThemeAction { export interface SetThemeAction {
type: ActionType.setTheme; type: ActionType.setTheme;
payload: Theme; }
export interface FetchThemesAction {
type: ActionType.fetchThemes;
payload: Theme[];
} }

View file

@ -3,18 +3,11 @@ import { ActionType } from '../action-types';
import { Theme } from '../../interfaces/Theme'; import { Theme } from '../../interfaces/Theme';
interface ThemeState { interface ThemeState {
theme: Theme; themes: Theme[];
} }
const initialState: ThemeState = { const initialState: ThemeState = {
theme: { themes: [],
name: 'tron',
colors: {
background: '#242B33',
primary: '#EFFBFF',
accent: '#6EE2FF',
},
},
}; };
export const themeReducer = ( export const themeReducer = (
@ -22,8 +15,9 @@ export const themeReducer = (
action: Action action: Action
): ThemeState => { ): ThemeState => {
switch (action.type) { switch (action.type) {
case ActionType.setTheme: case ActionType.fetchThemes: {
return { theme: action.payload }; return { themes: action.payload };
}
default: default:
return state; return state;

View file

@ -12,3 +12,4 @@ export * from './parseTime';
export * from './decodeToken'; export * from './decodeToken';
export * from './applyAuth'; export * from './applyAuth';
export * from './escapeRegex'; export * from './escapeRegex';
export * from './parseTheme';

View file

@ -0,0 +1,20 @@
import { ThemeColors } from '../interfaces';
// parse theme in PAB (primary;accent;background) format to theme colors object
export const parsePABToTheme = (themeStr: string): ThemeColors => {
const [primary, accent, background] = themeStr.split(';');
return {
primary,
accent,
background,
};
};
export const parseThemeToPAB = ({
primary: p,
accent: a,
background: b,
}: ThemeColors): string => {
return `${p};${a};${b}`;
};