Added custom theme creator

This commit is contained in:
Paweł Malak 2022-03-24 16:07:14 +01:00
parent 378dd8e36d
commit 9ab6c65d85
10 changed files with 246 additions and 41 deletions

View file

@ -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>
); );
}; };

View file

@ -0,0 +1,6 @@
.ColorsContainer {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
margin-bottom: 20px;
}

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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"

View file

@ -38,3 +38,7 @@
resize: none; resize: none;
height: 50vh; height: 50vh;
} }
.InputGroup input[type='color'] {
all: unset;
}

View file

@ -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,
// }, },
// }); });
// } }
// }; };

View file

@ -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',

View file

@ -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;
}

View file

@ -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;
} }