diff --git a/client/src/App.tsx b/client/src/App.tsx index 68faaea..e302c57 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,7 +10,7 @@ import { actionCreators, store } from './store'; import { State } from './store/reducers'; // Utils -import { checkVersion, decodeToken } from './utility'; +import { checkVersion, decodeToken, parsePABToTheme } from './utility'; // Routes import { Home } from './components/Home/Home'; @@ -31,7 +31,7 @@ export const App = (): JSX.Element => { const { config, loading } = useSelector((state: State) => state.config); const dispath = useDispatch(); - const { fetchQueries, setTheme, logout, createNotification } = + const { fetchQueries, setTheme, logout, createNotification, fetchThemes } = bindActionCreators(actionCreators, dispath); useEffect(() => { @@ -51,9 +51,12 @@ export const App = (): JSX.Element => { } }, 1000); + // load themes + fetchThemes(); + // set user theme if present if (localStorage.theme) { - setTheme(localStorage.theme); + setTheme(parsePABToTheme(localStorage.theme)); } // check for updated @@ -68,7 +71,7 @@ export const App = (): JSX.Element => { // If there is no user theme, set the default one useEffect(() => { if (!loading && !localStorage.theme) { - setTheme(config.defaultTheme, false); + setTheme(parsePABToTheme(config.defaultTheme), false); } }, [loading]); diff --git a/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx b/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx index bd88961..fbf5dab 100644 --- a/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx +++ b/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx @@ -1,8 +1,3 @@ -// Redux -import { useDispatch } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { actionCreators } from '../../../../store'; - // Components import { ThemePreview } from '../ThemePreview/ThemePreview'; @@ -15,14 +10,11 @@ interface Props { } export const ThemeGrid = ({ themes }: Props): JSX.Element => { - const dispatch = useDispatch(); - const { setTheme } = bindActionCreators(actionCreators, dispatch); - return (
{themes.map( (theme: Theme, idx: number): JSX.Element => ( - + ) )}
diff --git a/client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx b/client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx index 81a48fe..ccbb42e 100644 --- a/client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx +++ b/client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx @@ -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 classes from './ThemePreview.module.css'; interface Props { 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 ( -
props.applyTheme(props.theme.name)} - > +
setTheme(colors)}>
-

{props.theme.name}

+

{name}

); }; diff --git a/client/src/components/Settings/Themer/Themer.tsx b/client/src/components/Settings/Themer/Themer.tsx index 4274a34..f8de5ec 100644 --- a/client/src/components/Settings/Themer/Themer.tsx +++ b/client/src/components/Settings/Themer/Themer.tsx @@ -9,12 +9,11 @@ import { actionCreators } from '../../../store'; import { Theme, ThemeSettingsForm } from '../../../interfaces'; // Components -import { Button, InputGroup, SettingsHeadline } from '../../UI'; +import { Button, InputGroup, SettingsHeadline, Spinner } from '../../UI'; import { ThemeBuilder } from './ThemeBuilder/ThemeBuilder'; import { ThemeGrid } from './ThemeGrid/ThemeGrid'; // Other -import { themes } from './themes.json'; import { State } from '../../../store/reducers'; import { inputHandler, themeSettingsTemplate } from '../../../utility'; @@ -22,6 +21,7 @@ export const Themer = (): JSX.Element => { const { auth: { isAuthenticated }, config: { loading, config }, + theme: { themes }, } = useSelector((state: State) => state); const dispatch = useDispatch(); @@ -63,10 +63,10 @@ export const Themer = (): JSX.Element => { return ( - + {!themes.length ? : } - - + {/* + */} {isAuthenticated && (
diff --git a/client/src/components/Settings/Themer/themes.json b/client/src/components/Settings/Themer/themes.json deleted file mode 100644 index f3b12bd..0000000 --- a/client/src/components/Settings/Themer/themes.json +++ /dev/null @@ -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" - } - } - ] -} diff --git a/client/src/interfaces/Theme.ts b/client/src/interfaces/Theme.ts index 9753427..4bf5bfd 100644 --- a/client/src/interfaces/Theme.ts +++ b/client/src/interfaces/Theme.ts @@ -1,8 +1,10 @@ +export interface ThemeColors { + background: string; + primary: string; + accent: string; +} + export interface Theme { name: string; - colors: { - background: string; - primary: string; - accent: string; - } -} \ No newline at end of file + colors: ThemeColors; +} diff --git a/client/src/store/action-creators/theme.ts b/client/src/store/action-creators/theme.ts index 8eb6fef..87bf184 100644 --- a/client/src/store/action-creators/theme.ts +++ b/client/src/store/action-creators/theme.ts @@ -1,30 +1,32 @@ import { Dispatch } from 'redux'; -import { SetThemeAction } from '../actions/theme'; +import { FetchThemesAction, SetThemeAction } from '../actions/theme'; import { ActionType } from '../action-types'; -import { Theme } from '../../interfaces/Theme'; -import { themes } from '../../components/Settings/Themer/themes.json'; +import { Theme, ApiResponse, ThemeColors } from '../../interfaces'; +import { parseThemeToPAB } from '../../utility'; +import axios from 'axios'; export const setTheme = - (name: string, remeberTheme: boolean = true) => + (colors: ThemeColors, remeberTheme: boolean = true) => (dispatch: Dispatch) => { - const theme = themes.find((theme) => theme.name === name); + if (remeberTheme) { + localStorage.setItem('theme', parseThemeToPAB(colors)); + } - if (theme) { - if (remeberTheme) { - localStorage.setItem('theme', name); - } - - loadTheme(theme); - - dispatch({ - type: ActionType.setTheme, - payload: theme, - }); + for (const [key, value] of Object.entries(colors)) { + document.body.style.setProperty(`--color-${key}`, value); } }; -export const loadTheme = (theme: Theme): void => { - for (const [key, value] of Object.entries(theme.colors)) { - document.body.style.setProperty(`--color-${key}`, value); - } -}; +export const fetchThemes = + () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/themes'); + + dispatch({ + type: ActionType.fetchThemes, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; diff --git a/client/src/store/action-types/index.ts b/client/src/store/action-types/index.ts index 4be159f..601308b 100644 --- a/client/src/store/action-types/index.ts +++ b/client/src/store/action-types/index.ts @@ -1,6 +1,7 @@ export enum ActionType { // THEME setTheme = 'SET_THEME', + fetchThemes = 'FETCH_THEMES', // CONFIG getConfig = 'GET_CONFIG', updateConfig = 'UPDATE_CONFIG', diff --git a/client/src/store/actions/theme.ts b/client/src/store/actions/theme.ts index 036b1a3..6d40d21 100644 --- a/client/src/store/actions/theme.ts +++ b/client/src/store/actions/theme.ts @@ -3,5 +3,9 @@ import { Theme } from '../../interfaces'; export interface SetThemeAction { type: ActionType.setTheme; - payload: Theme; +} + +export interface FetchThemesAction { + type: ActionType.fetchThemes; + payload: Theme[]; } diff --git a/client/src/store/reducers/theme.ts b/client/src/store/reducers/theme.ts index 6db29fe..c204e20 100644 --- a/client/src/store/reducers/theme.ts +++ b/client/src/store/reducers/theme.ts @@ -3,18 +3,11 @@ import { ActionType } from '../action-types'; import { Theme } from '../../interfaces/Theme'; interface ThemeState { - theme: Theme; + themes: Theme[]; } const initialState: ThemeState = { - theme: { - name: 'tron', - colors: { - background: '#242B33', - primary: '#EFFBFF', - accent: '#6EE2FF', - }, - }, + themes: [], }; export const themeReducer = ( @@ -22,8 +15,9 @@ export const themeReducer = ( action: Action ): ThemeState => { switch (action.type) { - case ActionType.setTheme: - return { theme: action.payload }; + case ActionType.fetchThemes: { + return { themes: action.payload }; + } default: return state; diff --git a/client/src/utility/index.ts b/client/src/utility/index.ts index 7358da4..990db06 100644 --- a/client/src/utility/index.ts +++ b/client/src/utility/index.ts @@ -12,3 +12,4 @@ export * from './parseTime'; export * from './decodeToken'; export * from './applyAuth'; export * from './escapeRegex'; +export * from './parseTheme'; diff --git a/client/src/utility/parseTheme.ts b/client/src/utility/parseTheme.ts new file mode 100644 index 0000000..eaa800f --- /dev/null +++ b/client/src/utility/parseTheme.ts @@ -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}`; +};