Added custom theme creator
This commit is contained in:
parent
378dd8e36d
commit
9ab6c65d85
10 changed files with 246 additions and 41 deletions
|
@ -1,21 +1,69 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
// Redux
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { State } from '../../../../store/reducers';
|
||||||
|
|
||||||
|
// Other
|
||||||
import { Theme } from '../../../../interfaces';
|
import { Theme } from '../../../../interfaces';
|
||||||
import { Button } from '../../../UI';
|
|
||||||
|
// UI
|
||||||
|
import { Button, Modal } from '../../../UI';
|
||||||
import { ThemeGrid } from '../ThemeGrid/ThemeGrid';
|
import { ThemeGrid } from '../ThemeGrid/ThemeGrid';
|
||||||
import classes from './ThemeBuilder.module.css';
|
import classes from './ThemeBuilder.module.css';
|
||||||
|
import { ThemeCreator } from './ThemeCreator';
|
||||||
|
import { ThemeEditor } from './ThemeEditor';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
themes: Theme[];
|
themes: Theme[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThemeBuilder = ({ themes }: Props): JSX.Element => {
|
export const ThemeBuilder = ({ themes }: Props): JSX.Element => {
|
||||||
|
const {
|
||||||
|
auth: { isAuthenticated },
|
||||||
|
} = useSelector((state: State) => state);
|
||||||
|
|
||||||
|
const [showModal, toggleShowModal] = useState(false);
|
||||||
|
const [isInEdit, toggleIsInEdit] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.ThemeBuilder}>
|
<div className={classes.ThemeBuilder}>
|
||||||
|
{/* MODALS */}
|
||||||
|
<Modal isOpen={showModal} setIsOpen={() => toggleShowModal(!showModal)}>
|
||||||
|
{isInEdit ? (
|
||||||
|
<ThemeEditor modalHandler={() => toggleShowModal(!showModal)} />
|
||||||
|
) : (
|
||||||
|
<ThemeCreator modalHandler={() => toggleShowModal(!showModal)} />
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* USER THEMES */}
|
||||||
<ThemeGrid themes={themes} />
|
<ThemeGrid themes={themes} />
|
||||||
|
|
||||||
<div className={classes.Buttons}>
|
{/* BUTTONS */}
|
||||||
<Button>Create new theme</Button>
|
{isAuthenticated && (
|
||||||
{themes.length && <Button>Edit user themes</Button>}
|
<div className={classes.Buttons}>
|
||||||
</div>
|
<Button
|
||||||
|
click={() => {
|
||||||
|
toggleIsInEdit(false);
|
||||||
|
toggleShowModal(!showModal);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create new theme
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{themes.length && (
|
||||||
|
<Button
|
||||||
|
click={() => {
|
||||||
|
toggleIsInEdit(true);
|
||||||
|
toggleShowModal(!showModal);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit user themes
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
.ColorsContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { ChangeEvent, FormEvent, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { actionCreators } from '../../../../store';
|
||||||
|
import { Theme } from '../../../../interfaces';
|
||||||
|
import { Button, InputGroup, ModalForm } from '../../../UI';
|
||||||
|
|
||||||
|
import classes from './ThemeCreator.module.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modalHandler: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeCreator = ({ modalHandler }: Props): JSX.Element => {
|
||||||
|
const { addTheme } = bindActionCreators(actionCreators, useDispatch());
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<Theme>({
|
||||||
|
name: '',
|
||||||
|
isCustom: true,
|
||||||
|
colors: {
|
||||||
|
primary: '#ffffff',
|
||||||
|
accent: '#ffffff',
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setColor = ({
|
||||||
|
target: { value, name },
|
||||||
|
}: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
colors: {
|
||||||
|
...formData.colors,
|
||||||
|
[name]: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formHandler = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// add new theme
|
||||||
|
addTheme(formData);
|
||||||
|
|
||||||
|
// close modal
|
||||||
|
modalHandler();
|
||||||
|
|
||||||
|
// clear theme name
|
||||||
|
setFormData({ ...formData, name: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalForm formHandler={formHandler} modalHandler={modalHandler}>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="name">Theme name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
placeholder="my_theme"
|
||||||
|
required
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<div className={classes.ColorsContainer}>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="primary">Primary color</label>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
name="primary"
|
||||||
|
id="primary"
|
||||||
|
required
|
||||||
|
value={formData.colors.primary}
|
||||||
|
onChange={(e) => setColor(e)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="accent">Accent color</label>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
name="accent"
|
||||||
|
id="accent"
|
||||||
|
required
|
||||||
|
value={formData.colors.accent}
|
||||||
|
onChange={(e) => setColor(e)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="background">Background color</label>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
name="background"
|
||||||
|
id="background"
|
||||||
|
required
|
||||||
|
value={formData.colors.background}
|
||||||
|
onChange={(e) => setColor(e)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button>Add theme</Button>
|
||||||
|
</ModalForm>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { ModalForm } from '../../../UI';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modalHandler: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeEditor = (props: Props): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<ModalForm formHandler={() => {}} modalHandler={props.modalHandler}>
|
||||||
|
<h1>edit</h1>
|
||||||
|
</ModalForm>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,6 +4,7 @@ import { ChangeEvent, FormEvent, Fragment, useEffect, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { actionCreators } from '../../../store';
|
import { actionCreators } from '../../../store';
|
||||||
|
import { State } from '../../../store/reducers';
|
||||||
|
|
||||||
// Typescript
|
// Typescript
|
||||||
import { Theme, ThemeSettingsForm } from '../../../interfaces';
|
import { Theme, ThemeSettingsForm } from '../../../interfaces';
|
||||||
|
@ -14,7 +15,6 @@ import { ThemeBuilder } from './ThemeBuilder/ThemeBuilder';
|
||||||
import { ThemeGrid } from './ThemeGrid/ThemeGrid';
|
import { ThemeGrid } from './ThemeGrid/ThemeGrid';
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
import { State } from '../../../store/reducers';
|
|
||||||
import {
|
import {
|
||||||
inputHandler,
|
inputHandler,
|
||||||
parseThemeToPAB,
|
parseThemeToPAB,
|
||||||
|
@ -82,7 +82,7 @@ export const Themer = (): JSX.Element => {
|
||||||
<form onSubmit={formSubmitHandler}>
|
<form onSubmit={formSubmitHandler}>
|
||||||
<SettingsHeadline text="Other settings" />
|
<SettingsHeadline text="Other settings" />
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor="defaultTheme">Default theme (for new users)</label>
|
<label htmlFor="defaultTheme">Default theme for new users</label>
|
||||||
<select
|
<select
|
||||||
id="defaultTheme"
|
id="defaultTheme"
|
||||||
name="defaultTheme"
|
name="defaultTheme"
|
||||||
|
|
|
@ -38,3 +38,7 @@
|
||||||
resize: none;
|
resize: none;
|
||||||
height: 50vh;
|
height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.InputGroup input[type='color'] {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { FetchThemesAction, SetThemeAction } from '../actions/theme';
|
import {
|
||||||
|
AddThemeAction,
|
||||||
|
FetchThemesAction,
|
||||||
|
SetThemeAction,
|
||||||
|
} from '../actions/theme';
|
||||||
import { ActionType } from '../action-types';
|
import { ActionType } from '../action-types';
|
||||||
import { Theme, ApiResponse, ThemeColors } from '../../interfaces';
|
import { Theme, ApiResponse, ThemeColors } from '../../interfaces';
|
||||||
import { parseThemeToPAB } from '../../utility';
|
import { applyAuth, parseThemeToPAB } from '../../utility';
|
||||||
import axios from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
|
|
||||||
export const setTheme =
|
export const setTheme =
|
||||||
(colors: ThemeColors, remeberTheme: boolean = true) =>
|
(colors: ThemeColors, remeberTheme: boolean = true) =>
|
||||||
|
@ -36,32 +40,34 @@ export const fetchThemes =
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// export const addTheme = () => async (dispatch: Dispatch<>) => {
|
export const addTheme =
|
||||||
// try {
|
(theme: Theme) => async (dispatch: Dispatch<AddThemeAction>) => {
|
||||||
// // const res = await axios.post<>('/api/themes')
|
try {
|
||||||
// } catch (err) {}
|
const res = await axios.post<ApiResponse<Theme>>('/api/themes', theme, {
|
||||||
// };
|
headers: applyAuth(),
|
||||||
|
});
|
||||||
|
|
||||||
// export const addQuery =
|
dispatch({
|
||||||
// (query: Query) => async (dispatch: Dispatch<AddQueryAction>) => {
|
type: ActionType.addTheme,
|
||||||
// try {
|
payload: res.data.data,
|
||||||
// const res = await axios.post<ApiResponse<Query>>('/api/queries', query, {
|
});
|
||||||
// headers: applyAuth(),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// dispatch({
|
dispatch<any>({
|
||||||
// type: ActionType.addQuery,
|
type: ActionType.createNotification,
|
||||||
// payload: res.data.data,
|
payload: {
|
||||||
// });
|
title: 'Success',
|
||||||
// } catch (err) {
|
message: 'Theme added',
|
||||||
// const error = err as AxiosError<{ error: string }>;
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as AxiosError<{ error: string }>;
|
||||||
|
|
||||||
// dispatch<any>({
|
dispatch<any>({
|
||||||
// type: ActionType.createNotification,
|
type: ActionType.createNotification,
|
||||||
// payload: {
|
payload: {
|
||||||
// title: 'Error',
|
title: 'Error',
|
||||||
// message: error.response?.data.error,
|
message: error.response?.data.error,
|
||||||
// },
|
},
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
// };
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@ export enum ActionType {
|
||||||
// THEME
|
// THEME
|
||||||
setTheme = 'SET_THEME',
|
setTheme = 'SET_THEME',
|
||||||
fetchThemes = 'FETCH_THEMES',
|
fetchThemes = 'FETCH_THEMES',
|
||||||
|
addTheme = 'ADD_THEME',
|
||||||
// CONFIG
|
// CONFIG
|
||||||
getConfig = 'GET_CONFIG',
|
getConfig = 'GET_CONFIG',
|
||||||
updateConfig = 'UPDATE_CONFIG',
|
updateConfig = 'UPDATE_CONFIG',
|
||||||
|
|
|
@ -10,3 +10,8 @@ export interface FetchThemesAction {
|
||||||
type: ActionType.fetchThemes;
|
type: ActionType.fetchThemes;
|
||||||
payload: Theme[];
|
payload: Theme[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AddThemeAction {
|
||||||
|
type: ActionType.addTheme;
|
||||||
|
payload: Theme;
|
||||||
|
}
|
||||||
|
|
|
@ -9,11 +9,9 @@ interface ThemeState {
|
||||||
userThemes: Theme[];
|
userThemes: Theme[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedTheme: ThemeColors = parsePABToTheme(localStorage.theme) || {
|
const savedTheme = localStorage.theme
|
||||||
primary: '#effbff',
|
? parsePABToTheme(localStorage.theme)
|
||||||
accent: '#6ee2ff',
|
: parsePABToTheme('#effbff;#6ee2ff;#242b33');
|
||||||
background: '#242b33',
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialState: ThemeState = {
|
const initialState: ThemeState = {
|
||||||
activeTheme: {
|
activeTheme: {
|
||||||
|
@ -55,6 +53,13 @@ export const themeReducer = (
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ActionType.addTheme: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
userThemes: [...state.userThemes, action.payload],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue