From d257fbf9a391aabb726e3ef2feafaca9e478a664 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 13 Jun 2021 00:16:57 +0200 Subject: [PATCH] Created config global state. Reworked WeatherSettings and WeatherWidget to use new config state. --- client/src/App.tsx | 7 +- .../WeatherSettings/WeatherSettings.tsx | 92 ++++++++----------- .../Widgets/WeatherWidget/WeatherWidget.tsx | 51 +++++----- client/src/interfaces/Forms.ts | 6 ++ client/src/interfaces/GlobalState.ts | 2 + client/src/interfaces/index.ts | 3 +- client/src/store/actions/actionTypes.ts | 15 ++- client/src/store/actions/config.ts | 48 ++++++++++ client/src/store/actions/index.ts | 3 +- client/src/store/reducers/config.ts | 36 ++++++++ client/src/store/reducers/index.ts | 4 +- client/src/store/store.ts | 4 +- client/src/utility/index.ts | 3 +- client/src/utility/searchConfig.ts | 24 +++++ controllers/config.js | 4 +- 15 files changed, 214 insertions(+), 88 deletions(-) create mode 100644 client/src/interfaces/Forms.ts create mode 100644 client/src/store/actions/config.ts create mode 100644 client/src/store/reducers/config.ts create mode 100644 client/src/utility/searchConfig.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index ad7b366..efdf4c8 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,8 +1,8 @@ import { BrowserRouter, Route, Switch } from 'react-router-dom'; -import { setTheme } from './store/actions'; +import { getConfig, setTheme } from './store/actions'; // Redux -import store from './store/store'; +import { store } from './store/store'; import { Provider } from 'react-redux'; import classes from './App.module.css'; @@ -13,6 +13,9 @@ import Settings from './components/Settings/Settings'; import Bookmarks from './components/Bookmarks/Bookmarks'; import NotificationCenter from './components/NotificationCenter/NotificationCenter'; +// Get config pairs from database +store.dispatch(getConfig()); + if (localStorage.theme) { store.dispatch(setTheme(localStorage.theme)); } diff --git a/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx b/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx index 3294fe4..6f14cfc 100644 --- a/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx +++ b/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx @@ -1,25 +1,28 @@ import { useState, ChangeEvent, useEffect, FormEvent } from 'react'; -import { connect } from 'react-redux'; import axios from 'axios'; -import { ApiResponse, Config, NewNotification, Weather } from '../../../interfaces'; +// Redux +import { connect } from 'react-redux'; +import { createNotification, updateConfig } from '../../../store/actions'; + +// Typescript +import { ApiResponse, GlobalState, NewNotification, Weather, WeatherForm } from '../../../interfaces'; + +// UI import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; -import { createNotification } from '../../../store/actions'; -interface FormState { - WEATHER_API_KEY: string; - lat: number; - long: number; - isCelsius: number; -} +// Utils +import { searchConfig } from '../../../utility'; interface ComponentProps { createNotification: (notification: NewNotification) => void; + updateConfig: (formData: WeatherForm) => void; + loading: boolean; } const WeatherSettings = (props: ComponentProps): JSX.Element => { - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ WEATHER_API_KEY: '', lat: 0, long: 0, @@ -40,28 +43,15 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => { } useEffect(() => { - axios.get>('/api/config?keys=WEATHER_API_KEY,lat,long,isCelsius') - .then(data => { - let tmpFormData = { ...formData }; + setFormData({ + WEATHER_API_KEY: searchConfig('WEATHER_API_KEY', ''), + lat: searchConfig('lat', 0), + long: searchConfig('long', 0), + isCelsius: searchConfig('isCelsius', 1) + }) + }, [props.loading]); - data.data.data.forEach((config: Config) => { - let value: string | number = config.value; - if (config.valueType === 'number') { - value = parseFloat(value); - } - - tmpFormData = { - ...tmpFormData, - [config.key]: value - } - }) - - setFormData(tmpFormData); - }) - .catch(err => console.log(err)); - }, []); - - const formSubmitHandler = (e: FormEvent) => { + const formSubmitHandler = async (e: FormEvent) => { e.preventDefault(); // Check for api key input @@ -73,32 +63,22 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => { } // Save settings - axios.put>('/api/config', formData) + await props.updateConfig(formData); + + // Update weather + axios.get>('/api/weather/update') .then(() => { props.createNotification({ title: 'Success', - message: 'Settings updated' + message: 'Weather updated' }) - - // Update weather with new settings - axios.get>('/api/weather/update') - .then(() => { - props.createNotification({ - title: 'Success', - message: 'Weather updated' - }) - }) - .catch((err) => { - props.createNotification({ - title: 'Error', - message: err.response.data.error - }) - }); }) - .catch(err => console.log(err)); - - // set localStorage - localStorage.setItem('isCelsius', JSON.stringify(parseInt(`${formData.isCelsius}`) === 1)) + .catch((err) => { + props.createNotification({ + title: 'Error', + message: err.response.data.error + }) + }); } return ( @@ -170,4 +150,10 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => { ) } -export default connect(null, { createNotification })(WeatherSettings); \ No newline at end of file +const mapStateToProps = (state: GlobalState) => { + return { + loading: state.config.loading + } +} + +export default connect(mapStateToProps, { createNotification, updateConfig })(WeatherSettings); \ No newline at end of file diff --git a/client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx b/client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx index ab758d5..8a0e142 100644 --- a/client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx +++ b/client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx @@ -1,12 +1,27 @@ import { useState, useEffect, Fragment } from 'react'; -import { Weather, ApiResponse, Config } from '../../../interfaces'; import axios from 'axios'; -import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon'; +// Redux +import { connect } from 'react-redux'; +// Typescript +import { Weather, ApiResponse, Config, GlobalState } from '../../../interfaces'; + +// CSS import classes from './WeatherWidget.module.css'; -const WeatherWidget = (): JSX.Element => { +// UI +import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon'; + +// Utils +import { searchConfig } from '../../../utility'; + +interface ComponentProps { + configLoading: boolean; + config: Config[]; +} + +const WeatherWidget = (props: ComponentProps): JSX.Element => { const [weather, setWeather] = useState({ externalLastUpdate: '', tempC: 0, @@ -20,11 +35,9 @@ const WeatherWidget = (): JSX.Element => { updatedAt: new Date() }); const [isLoading, setIsLoading] = useState(true); - const [isCelsius, setIsCelsius] = useState(true); // Initial request to get data useEffect(() => { - // get weather axios.get>('/api/weather') .then(data => { const weatherData = data.data.data[0]; @@ -34,18 +47,6 @@ const WeatherWidget = (): JSX.Element => { setIsLoading(false); }) .catch(err => console.log(err)); - - // get config - if (!localStorage.isCelsius) { - axios.get>('/api/config/isCelsius') - .then((data) => { - setIsCelsius(parseInt(data.data.data.value) === 1); - localStorage.setItem('isCelsius', JSON.stringify(isCelsius)); - }) - .catch((err) => console.log(err)); - } else { - setIsCelsius(JSON.parse(localStorage.isCelsius)); - } }, []); // Open socket for data updates @@ -67,9 +68,8 @@ const WeatherWidget = (): JSX.Element => { return (
- {isLoading - ? 'loading' - : (weather.id > 0 && + {isLoading || props.configLoading || searchConfig('WEATHER_API_KEY', '') && + (weather.id > 0 && (
{ />
- {isCelsius + {searchConfig('isCelsius', true) ? {weather.tempC}°C : {weather.tempF}°F } @@ -91,4 +91,11 @@ const WeatherWidget = (): JSX.Element => { ) } -export default WeatherWidget; \ No newline at end of file +const mapStateToProps = (state: GlobalState) => { + return { + configLoading: state.config.loading, + config: state.config.config + } +} + +export default connect(mapStateToProps)(WeatherWidget); \ No newline at end of file diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts new file mode 100644 index 0000000..6ce2d42 --- /dev/null +++ b/client/src/interfaces/Forms.ts @@ -0,0 +1,6 @@ +export interface WeatherForm { + WEATHER_API_KEY: string; + lat: number; + long: number; + isCelsius: number; +} \ No newline at end of file diff --git a/client/src/interfaces/GlobalState.ts b/client/src/interfaces/GlobalState.ts index 1ef7acb..a88f218 100644 --- a/client/src/interfaces/GlobalState.ts +++ b/client/src/interfaces/GlobalState.ts @@ -2,10 +2,12 @@ import { State as AppState } from '../store/reducers/app'; import { State as ThemeState } from '../store/reducers/theme'; import { State as BookmarkState } from '../store/reducers/bookmark'; import { State as NotificationState } from '../store/reducers/notification'; +import { State as ConfigState } from '../store/reducers/config'; export interface GlobalState { theme: ThemeState; app: AppState; bookmark: BookmarkState; notification: NotificationState; + config: ConfigState; } \ No newline at end of file diff --git a/client/src/interfaces/index.ts b/client/src/interfaces/index.ts index 14aece8..2f333d3 100644 --- a/client/src/interfaces/index.ts +++ b/client/src/interfaces/index.ts @@ -6,4 +6,5 @@ export * from './Weather'; export * from './Bookmark'; export * from './Category'; export * from './Notification'; -export * from './Config'; \ No newline at end of file +export * from './Config'; +export * from './Forms'; \ No newline at end of file diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts index 4ff088c..d2cc17e 100644 --- a/client/src/store/actions/actionTypes.ts +++ b/client/src/store/actions/actionTypes.ts @@ -19,7 +19,10 @@ import { UpdateBookmarkAction, // Notifications CreateNotificationAction, - ClearNotificationAction + ClearNotificationAction, + // Config + GetConfigAction, + UpdateConfigAction } from './'; export enum ActionTypes { @@ -48,7 +51,10 @@ export enum ActionTypes { updateBookmark = 'UPDATE_BOOKMARK', // Notifications createNotification = 'CREATE_NOTIFICATION', - clearNotification = 'CLEAR_NOTIFICATION' + clearNotification = 'CLEAR_NOTIFICATION', + // Config + getConfig = 'GET_CONFIG', + updateConfig = 'UPDATE_CONFIG' } export type Action = @@ -72,4 +78,7 @@ export type Action = UpdateBookmarkAction | // Notifications CreateNotificationAction | - ClearNotificationAction; \ No newline at end of file + ClearNotificationAction | + // Config + GetConfigAction | + UpdateConfigAction; \ No newline at end of file diff --git a/client/src/store/actions/config.ts b/client/src/store/actions/config.ts new file mode 100644 index 0000000..e65415d --- /dev/null +++ b/client/src/store/actions/config.ts @@ -0,0 +1,48 @@ +import axios from 'axios'; +import { Dispatch } from 'redux'; +import { ActionTypes } from './actionTypes'; +import { Config, ApiResponse, WeatherForm } from '../../interfaces'; +import { CreateNotificationAction } from './notification'; + +export interface GetConfigAction { + type: ActionTypes.getConfig; + payload: Config[]; +} + +export const getConfig = () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/config'); + + dispatch({ + type: ActionTypes.getConfig, + payload: res.data.data + }) + } catch (err) { + console.log(err) + } +} + +export interface UpdateConfigAction { + type: ActionTypes.updateConfig; + payload: Config[]; +} + +export const updateConfig = (formData: WeatherForm) => async (dispatch: Dispatch) => { + try { + const res = await axios.put>('/api/config', formData); + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: 'Settings updated' + } + }) + + dispatch({ + type: ActionTypes.updateConfig, + payload: res.data.data + }) + } catch (err) { + console.log(err); + } +} \ No newline at end of file diff --git a/client/src/store/actions/index.ts b/client/src/store/actions/index.ts index 78c86b3..e516e54 100644 --- a/client/src/store/actions/index.ts +++ b/client/src/store/actions/index.ts @@ -2,4 +2,5 @@ export * from './theme'; export * from './app'; export * from './actionTypes'; export * from './bookmark'; -export * from './notification'; \ No newline at end of file +export * from './notification'; +export * from './config'; \ No newline at end of file diff --git a/client/src/store/reducers/config.ts b/client/src/store/reducers/config.ts new file mode 100644 index 0000000..071f461 --- /dev/null +++ b/client/src/store/reducers/config.ts @@ -0,0 +1,36 @@ +import { ActionTypes, Action } from '../actions'; +import { Config } from '../../interfaces'; + +export interface State { + loading: boolean; + config: Config[]; +} + +const initialState: State = { + loading: true, + config: [] +} + +const getConfig = (state: State, action: Action): State => { + return { + loading: false, + config: action.payload + } +} + +const updateConfig = (state: State, action: Action): State => { + return { + ...state, + config: action.payload + } +} + +const configReducer = (state: State = initialState, action: Action) => { + switch(action.type) { + case ActionTypes.getConfig: return getConfig(state, action); + case ActionTypes.updateConfig: return updateConfig(state, action); + default: return state; + } +} + +export default configReducer; \ No newline at end of file diff --git a/client/src/store/reducers/index.ts b/client/src/store/reducers/index.ts index bb0a0d6..96e9f95 100644 --- a/client/src/store/reducers/index.ts +++ b/client/src/store/reducers/index.ts @@ -6,12 +6,14 @@ import themeReducer from './theme'; import appReducer from './app'; import bookmarkReducer from './bookmark'; import notificationReducer from './notification'; +import configReducer from './config'; const rootReducer = combineReducers({ theme: themeReducer, app: appReducer, bookmark: bookmarkReducer, - notification: notificationReducer + notification: notificationReducer, + config: configReducer }) export default rootReducer; \ No newline at end of file diff --git a/client/src/store/store.ts b/client/src/store/store.ts index c0f4be4..22250a7 100644 --- a/client/src/store/store.ts +++ b/client/src/store/store.ts @@ -4,6 +4,4 @@ import thunk from 'redux-thunk'; import rootReducer from './reducers'; const initialState = {}; -const store = createStore(rootReducer, initialState, composeWithDevTools(applyMiddleware(thunk))); - -export default store; \ No newline at end of file +export const store = createStore(rootReducer, initialState, composeWithDevTools(applyMiddleware(thunk))); \ No newline at end of file diff --git a/client/src/utility/index.ts b/client/src/utility/index.ts index bd1db1b..6caa71e 100644 --- a/client/src/utility/index.ts +++ b/client/src/utility/index.ts @@ -1,2 +1,3 @@ export * from './iconParser'; -export * from './urlParser'; \ No newline at end of file +export * from './urlParser'; +export * from './searchConfig'; \ No newline at end of file diff --git a/client/src/utility/searchConfig.ts b/client/src/utility/searchConfig.ts new file mode 100644 index 0000000..0f8ec23 --- /dev/null +++ b/client/src/utility/searchConfig.ts @@ -0,0 +1,24 @@ +import { store } from '../store/store'; + +/** + * Search config store with given key + * @param key Config pair key to search + * @param _default Value to return if key is not found + */ +export const searchConfig = (key: string, _default: any)=> { + const state = store.getState(); + + const pair = state.config.config.find(p => p.key === key); + + if (pair) { + if (pair.valueType === 'number') { + return parseFloat(pair.value); + } else if (pair.valueType === 'boolean') { + return parseInt(pair.value); + } else { + return pair.value; + } + } else { + return _default; + } +} \ No newline at end of file diff --git a/controllers/config.js b/controllers/config.js index 82b6691..f8f3613 100644 --- a/controllers/config.js +++ b/controllers/config.js @@ -96,9 +96,11 @@ exports.updateValues = asyncWrapper(async (req, res, next) => { }) }) + const config = await Config.findAll(); + res.status(200).send({ success: true, - data: {} + data: config }) })