Created config global state. Reworked WeatherSettings and WeatherWidget to use new config state.
This commit is contained in:
parent
a5504e6e80
commit
d257fbf9a3
15 changed files with 214 additions and 88 deletions
|
@ -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<any>(getConfig());
|
||||
|
||||
if (localStorage.theme) {
|
||||
store.dispatch<any>(setTheme(localStorage.theme));
|
||||
}
|
||||
|
|
|
@ -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<FormState>({
|
||||
const [formData, setFormData] = useState<WeatherForm>({
|
||||
WEATHER_API_KEY: '',
|
||||
lat: 0,
|
||||
long: 0,
|
||||
|
@ -40,28 +43,15 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
axios.get<ApiResponse<Config[]>>('/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<ApiResponse<{}>>('/api/config', formData)
|
||||
await props.updateConfig(formData);
|
||||
|
||||
// Update weather
|
||||
axios.get<ApiResponse<Weather>>('/api/weather/update')
|
||||
.then(() => {
|
||||
props.createNotification({
|
||||
title: 'Success',
|
||||
message: 'Settings updated'
|
||||
message: 'Weather updated'
|
||||
})
|
||||
|
||||
// Update weather with new settings
|
||||
axios.get<ApiResponse<Weather>>('/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);
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
loading: state.config.loading
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { createNotification, updateConfig })(WeatherSettings);
|
|
@ -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<Weather>({
|
||||
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<ApiResponse<Weather[]>>('/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<ApiResponse<Config>>('/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 (
|
||||
<div className={classes.WeatherWidget}>
|
||||
{isLoading
|
||||
? 'loading'
|
||||
: (weather.id > 0 &&
|
||||
{isLoading || props.configLoading || searchConfig('WEATHER_API_KEY', '') &&
|
||||
(weather.id > 0 &&
|
||||
(<Fragment>
|
||||
<div className={classes.WeatherIcon}>
|
||||
<WeatherIcon
|
||||
|
@ -78,7 +78,7 @@ const WeatherWidget = (): JSX.Element => {
|
|||
/>
|
||||
</div>
|
||||
<div className={classes.WeatherDetails}>
|
||||
{isCelsius
|
||||
{searchConfig('isCelsius', true)
|
||||
? <span>{weather.tempC}°C</span>
|
||||
: <span>{weather.tempF}°F</span>
|
||||
}
|
||||
|
@ -91,4 +91,11 @@ const WeatherWidget = (): JSX.Element => {
|
|||
)
|
||||
}
|
||||
|
||||
export default WeatherWidget;
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
configLoading: state.config.loading,
|
||||
config: state.config.config
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(WeatherWidget);
|
6
client/src/interfaces/Forms.ts
Normal file
6
client/src/interfaces/Forms.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export interface WeatherForm {
|
||||
WEATHER_API_KEY: string;
|
||||
lat: number;
|
||||
long: number;
|
||||
isCelsius: number;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -6,4 +6,5 @@ export * from './Weather';
|
|||
export * from './Bookmark';
|
||||
export * from './Category';
|
||||
export * from './Notification';
|
||||
export * from './Config';
|
||||
export * from './Config';
|
||||
export * from './Forms';
|
|
@ -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;
|
||||
ClearNotificationAction |
|
||||
// Config
|
||||
GetConfigAction |
|
||||
UpdateConfigAction;
|
48
client/src/store/actions/config.ts
Normal file
48
client/src/store/actions/config.ts
Normal file
|
@ -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<ApiResponse<Config[]>>('/api/config');
|
||||
|
||||
dispatch<GetConfigAction>({
|
||||
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<ApiResponse<Config[]>>('/api/config', formData);
|
||||
dispatch<CreateNotificationAction>({
|
||||
type: ActionTypes.createNotification,
|
||||
payload: {
|
||||
title: 'Success',
|
||||
message: 'Settings updated'
|
||||
}
|
||||
})
|
||||
|
||||
dispatch<UpdateConfigAction>({
|
||||
type: ActionTypes.updateConfig,
|
||||
payload: res.data.data
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
|
@ -2,4 +2,5 @@ export * from './theme';
|
|||
export * from './app';
|
||||
export * from './actionTypes';
|
||||
export * from './bookmark';
|
||||
export * from './notification';
|
||||
export * from './notification';
|
||||
export * from './config';
|
36
client/src/store/reducers/config.ts
Normal file
36
client/src/store/reducers/config.ts
Normal file
|
@ -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;
|
|
@ -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<GlobalState>({
|
||||
theme: themeReducer,
|
||||
app: appReducer,
|
||||
bookmark: bookmarkReducer,
|
||||
notification: notificationReducer
|
||||
notification: notificationReducer,
|
||||
config: configReducer
|
||||
})
|
||||
|
||||
export default rootReducer;
|
|
@ -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;
|
||||
export const store = createStore(rootReducer, initialState, composeWithDevTools(applyMiddleware(thunk)));
|
|
@ -1,2 +1,3 @@
|
|||
export * from './iconParser';
|
||||
export * from './urlParser';
|
||||
export * from './urlParser';
|
||||
export * from './searchConfig';
|
24
client/src/utility/searchConfig.ts
Normal file
24
client/src/utility/searchConfig.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in a new issue