From a5504e6e80eb6934ea553ad16d54b817767ac5ea Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 11 Jun 2021 15:33:06 +0200 Subject: [PATCH 1/4] Added url parser to support wider range of addresses --- README.md | 15 ++++++++- client/src/App.tsx | 1 - .../src/components/Apps/AppCard/AppCard.tsx | 12 +++---- .../Bookmarks/BookmarkCard/BookmarkCard.tsx | 32 +++++++++++-------- client/src/utility/iconParser.ts | 5 +++ client/src/utility/index.ts | 2 ++ client/src/utility/urlParser.ts | 20 ++++++++++++ 7 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 client/src/utility/index.ts create mode 100644 client/src/utility/urlParser.ts diff --git a/README.md b/README.md index 3b2ed3d..25d7436 100644 --- a/README.md +++ b/README.md @@ -59,4 +59,17 @@ docker run -p 5005:5005 -v :/app/data flame - Themes - Customize your page by choosing from 12 color themes -![Homescreen screenshot](./github/_themes.png) \ No newline at end of file +![Homescreen screenshot](./github/_themes.png) + +## Usage +### Supported links for applications and bookmarks +#### Rules +- URL starts with `http://` + - Format: `http://www.domain.com`, `http://domain.com` + - Redirect: `{dest}` +- URL starts with `https://` + - Format: `https://www.domain.com`, `https://domain.com` + - Redirect: `https://{dest}` +- URL without protocol + - Format: `www.domain.com`, `domain.com`, `sub.domain.com`, `local`, `ip`, `ip:port` + - Redirect: `http://{dest}` \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 9875f16..ad7b366 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -11,7 +11,6 @@ import Home from './components/Home/Home'; import Apps from './components/Apps/Apps'; import Settings from './components/Settings/Settings'; import Bookmarks from './components/Bookmarks/Bookmarks'; - import NotificationCenter from './components/NotificationCenter/NotificationCenter'; if (localStorage.theme) { diff --git a/client/src/components/Apps/AppCard/AppCard.tsx b/client/src/components/Apps/AppCard/AppCard.tsx index 18b5dfd..d2ef375 100644 --- a/client/src/components/Apps/AppCard/AppCard.tsx +++ b/client/src/components/Apps/AppCard/AppCard.tsx @@ -1,8 +1,6 @@ -import { Link } from 'react-router-dom'; - import classes from './AppCard.module.css'; import Icon from '../../UI/Icons/Icon/Icon'; -import { iconParser } from '../../../utility/iconParser'; +import { iconParser, urlParser } from '../../../utility'; import { App } from '../../../interfaces'; @@ -12,18 +10,16 @@ interface ComponentProps { } const AppCard = (props: ComponentProps): JSX.Element => { - const redirectHandler = (url: string): void => { - window.open(url); - } + const [displayUrl, redirectUrl] = urlParser(props.app.url); return ( - +
{props.app.name}
- {props.app.url} + {displayUrl}
) diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx index 5f607b2..6ddf0b7 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -2,7 +2,7 @@ import { Bookmark, Category } from '../../../interfaces'; import classes from './BookmarkCard.module.css'; import Icon from '../../UI/Icons/Icon/Icon'; -import { iconParser } from '../../../utility/iconParser'; +import { iconParser, urlParser } from '../../../utility'; interface ComponentProps { category: Category; @@ -13,19 +13,23 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {

{props.category.name}

- {props.category.bookmarks.map((bookmark: Bookmark) => ( - - {bookmark.icon && ( -
- -
- )} - {bookmark.name} -
- ))} + {props.category.bookmarks.map((bookmark: Bookmark) => { + const [displayUrl, redirectUrl] = urlParser(bookmark.url); + + return ( + + {bookmark.icon && ( +
+ +
+ )} + {bookmark.name} +
+ ) + })}
) diff --git a/client/src/utility/iconParser.ts b/client/src/utility/iconParser.ts index e846102..c5c9d8b 100644 --- a/client/src/utility/iconParser.ts +++ b/client/src/utility/iconParser.ts @@ -1,3 +1,8 @@ +/** + * Parse Material Desgin icon name to be used with mdi/js + * @param mdiName Dash separated icon name from MDI, e.g. alert-box-outline + * @returns Parsed icon name to be used with mdi/js, e.g mdiAlertBoxOutline + */ export const iconParser = (mdiName: string): string => { let parsedName = mdiName .split('-') diff --git a/client/src/utility/index.ts b/client/src/utility/index.ts new file mode 100644 index 0000000..bd1db1b --- /dev/null +++ b/client/src/utility/index.ts @@ -0,0 +1,2 @@ +export * from './iconParser'; +export * from './urlParser'; \ No newline at end of file diff --git a/client/src/utility/urlParser.ts b/client/src/utility/urlParser.ts new file mode 100644 index 0000000..c74224a --- /dev/null +++ b/client/src/utility/urlParser.ts @@ -0,0 +1,20 @@ +export const urlParser = (url: string): string[] => { + let parsedUrl: string; + let displayUrl: string; + + if (/https?:\/\//.test(url)) { + // Url starts with http[s]:// -> leave it as it is + parsedUrl = url; + } else { + // No protocol -> apply http:// prefix + parsedUrl = `http://${url}`; + } + + // Create simplified url to display as text + displayUrl = url + .replace(/https?:\/\//, '') + .replace('www.', '') + .replace(/\/$/, ''); + + return [displayUrl, parsedUrl] +} \ No newline at end of file From d257fbf9a391aabb726e3ef2feafaca9e478a664 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 13 Jun 2021 00:16:57 +0200 Subject: [PATCH 2/4] 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 }) }) From f137498e7e168e1d810946221e35c1bbf4bc13b9 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 13 Jun 2021 01:06:42 +0200 Subject: [PATCH 3/4] Added auto-refresh for greeting and date. Fixed multiple React warnings --- README.md | 2 +- client/src/App.module.css | 0 client/src/App.tsx | 2 - .../src/components/Apps/AppCard/AppCard.tsx | 7 +- .../src/components/Apps/AppForm/AppForm.tsx | 10 ++- client/src/components/Apps/Apps.tsx | 18 +++-- .../Bookmarks/BookmarkCard/BookmarkCard.tsx | 3 +- .../Bookmarks/BookmarkForm/BookmarkForm.tsx | 10 ++- .../Bookmarks/BookmarkGrid/BookmarkGrid.tsx | 2 +- client/src/components/Bookmarks/Bookmarks.tsx | 18 +++-- client/src/components/Home/Home.tsx | 80 +++++++++++-------- .../src/components/Home/functions/dateTime.ts | 8 ++ .../src/components/Home/functions/greeter.ts | 12 +++ client/src/components/UI/Modal/Modal.tsx | 2 +- client/src/store/actions/app.ts | 2 +- client/src/store/actions/bookmark.ts | 4 +- 16 files changed, 121 insertions(+), 59 deletions(-) delete mode 100644 client/src/App.module.css create mode 100644 client/src/components/Home/functions/dateTime.ts create mode 100644 client/src/components/Home/functions/greeter.ts diff --git a/README.md b/README.md index 25d7436..d61baa6 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ docker run -p 5005:5005 -v :/app/data flame ![Homescreen screenshot](./github/_themes.png) ## Usage -### Supported links for applications and bookmarks +### Supported URL formats for applications and bookmarks #### Rules - URL starts with `http://` - Format: `http://www.domain.com`, `http://domain.com` diff --git a/client/src/App.module.css b/client/src/App.module.css deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/App.tsx b/client/src/App.tsx index efdf4c8..d58f4ad 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,8 +5,6 @@ import { getConfig, setTheme } from './store/actions'; import { store } from './store/store'; import { Provider } from 'react-redux'; -import classes from './App.module.css'; - import Home from './components/Home/Home'; import Apps from './components/Apps/Apps'; import Settings from './components/Settings/Settings'; diff --git a/client/src/components/Apps/AppCard/AppCard.tsx b/client/src/components/Apps/AppCard/AppCard.tsx index d2ef375..50fb979 100644 --- a/client/src/components/Apps/AppCard/AppCard.tsx +++ b/client/src/components/Apps/AppCard/AppCard.tsx @@ -13,7 +13,12 @@ const AppCard = (props: ComponentProps): JSX.Element => { const [displayUrl, redirectUrl] = urlParser(props.app.url); return ( - +
diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index b833752..cffeb19 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -98,7 +98,15 @@ const AppForm = (props: ComponentProps): JSX.Element => { value={formData.url} onChange={(e) => inputChangeHandler(e)} /> - Only urls without http[s]:// are supported + +
+ {' '}Check supported URL formats + + diff --git a/client/src/components/Apps/Apps.tsx b/client/src/components/Apps/Apps.tsx index 0f8e079..f02fba6 100644 --- a/client/src/components/Apps/Apps.tsx +++ b/client/src/components/Apps/Apps.tsx @@ -1,4 +1,4 @@ -import { Fragment, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; // Redux @@ -30,6 +30,12 @@ interface ComponentProps { } const Apps = (props: ComponentProps): JSX.Element => { + const { + getApps, + apps, + loading + } = props; + const [modalIsOpen, setModalIsOpen] = useState(false); const [isInEdit, setIsInEdit] = useState(false); const [isInUpdate, setIsInUpdate] = useState(false); @@ -44,10 +50,10 @@ const Apps = (props: ComponentProps): JSX.Element => { }) useEffect(() => { - if (props.apps.length === 0) { - props.getApps(); + if (apps.length === 0) { + getApps(); } - }, [props.getApps]); + }, [getApps, apps]); const toggleModal = (): void => { setModalIsOpen(!modalIsOpen); @@ -93,10 +99,10 @@ const Apps = (props: ComponentProps): JSX.Element => {
- {props.loading + {loading ? : (!isInEdit - ? + ? : ) }
diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx index 6ddf0b7..9e8dff9 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -14,12 +14,13 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {

{props.category.name}

{props.category.bookmarks.map((bookmark: Bookmark) => { - const [displayUrl, redirectUrl] = urlParser(bookmark.url); + const redirectUrl = urlParser(bookmark.url)[1]; return ( {bookmark.icon && (
diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx index c5f5bcf..9341565 100644 --- a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx +++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx @@ -184,7 +184,15 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { value={formData.url} onChange={(e) => inputChangeHandler(e)} /> - Only urls without http[s]:// are supported + + + {' '}Check supported URL formats + + diff --git a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx index c6355c6..c316f31 100644 --- a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx +++ b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx @@ -2,7 +2,7 @@ import { Link } from 'react-router-dom'; import classes from './BookmarkGrid.module.css'; -import { Bookmark, Category } from '../../../interfaces'; +import { Category } from '../../../interfaces'; import BookmarkCard from '../BookmarkCard/BookmarkCard'; diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx index 53cca5c..b386706 100644 --- a/client/src/components/Bookmarks/Bookmarks.tsx +++ b/client/src/components/Bookmarks/Bookmarks.tsx @@ -28,6 +28,12 @@ export enum ContentType { } const Bookmarks = (props: ComponentProps): JSX.Element => { + const { + getCategories, + categories, + loading + } = props; + const [modalIsOpen, setModalIsOpen] = useState(false); const [formContentType, setFormContentType] = useState(ContentType.category); const [isInEdit, setIsInEdit] = useState(false); @@ -52,10 +58,10 @@ const Bookmarks = (props: ComponentProps): JSX.Element => { }) useEffect(() => { - if (props.categories.length === 0) { - props.getCategories(); + if (categories.length === 0) { + getCategories(); } - }, [props.getCategories]) + }, [getCategories, categories]) const toggleModal = (): void => { setModalIsOpen(!modalIsOpen); @@ -132,13 +138,13 @@ const Bookmarks = (props: ComponentProps): JSX.Element => { />
- {props.loading + {loading ? : (!isInEdit - ? + ? : ) diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index 1fdd090..ab41ac2 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; // Redux @@ -23,6 +23,10 @@ import AppGrid from '../Apps/AppGrid/AppGrid'; import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid'; import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget'; +// Functions +import { greeter } from './functions/greeter'; +import { dateTime } from './functions/dateTime'; + interface ComponentProps { getApps: Function; getCategories: Function; @@ -33,68 +37,74 @@ interface ComponentProps { } const Home = (props: ComponentProps): JSX.Element => { + const { + getApps, + apps, + appsLoading, + getCategories, + categories, + categoriesLoading + } = props; + + const [header, setHeader] = useState({ + dateTime: dateTime(), + greeting: greeter() + }) + + // Load applications useEffect(() => { - if (props.apps.length === 0) { - props.getApps(); + if (apps.length === 0) { + getApps(); } - }, [props.getApps]); + }, [getApps, apps]); + // Load bookmark categories useEffect(() => { - if (props.categories.length === 0) { - props.getCategories(); + if (categories.length === 0) { + getCategories(); } - }, [props.getCategories]); + }, [getCategories, categories]); - const dateAndTime = (): string => { - const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; - const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + // Refresh greeter and time + useEffect(() => { + const interval = setInterval(() => { + setHeader({ + dateTime: dateTime(), + greeting: greeter() + }) + }, 1000); - const now = new Date(); - - return `${days[now.getDay()]}, ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`; - } - - const greeter = (): string => { - const now = new Date().getHours(); - let msg: string; - - if (now >= 18) msg = 'Good evening!'; - else if (now >= 12) msg = 'Good afternoon!'; - else if (now >= 6) msg = 'Good morning!'; - else if (now >= 0) msg = 'Good night!'; - else msg = 'Hello!'; - - return msg; - } + return () => clearInterval(interval); + }, []) return (
-

{dateAndTime()}

+

{header.dateTime}

Go to Settings -

{greeter()}

+

{header.greeting}

- {props.appsLoading + {appsLoading ? : app.isPinned)} - totalApps={props.apps.length} + apps={apps.filter((app: App) => app.isPinned)} + totalApps={apps.length} /> }
- {props.categoriesLoading + {categoriesLoading ? : category.isPinned)} - totalCategories={props.categories.length} + categories={categories.filter((category: Category) => category.isPinned)} + totalCategories={categories.length} /> } diff --git a/client/src/components/Home/functions/dateTime.ts b/client/src/components/Home/functions/dateTime.ts new file mode 100644 index 0000000..44cc5e1 --- /dev/null +++ b/client/src/components/Home/functions/dateTime.ts @@ -0,0 +1,8 @@ +export const dateTime = (): string => { + const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + + const now = new Date(); + + return `${days[now.getDay()]}, ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`; +} \ No newline at end of file diff --git a/client/src/components/Home/functions/greeter.ts b/client/src/components/Home/functions/greeter.ts new file mode 100644 index 0000000..64cb2ea --- /dev/null +++ b/client/src/components/Home/functions/greeter.ts @@ -0,0 +1,12 @@ +export const greeter = (): string => { + const now = new Date().getHours(); + let msg: string; + + if (now >= 18) msg = 'Good evening!'; + else if (now >= 12) msg = 'Good afternoon!'; + else if (now >= 6) msg = 'Good morning!'; + else if (now >= 0) msg = 'Good night!'; + else msg = 'Hello!'; + + return msg; +} \ No newline at end of file diff --git a/client/src/components/UI/Modal/Modal.tsx b/client/src/components/UI/Modal/Modal.tsx index b8b86eb..ccb82be 100644 --- a/client/src/components/UI/Modal/Modal.tsx +++ b/client/src/components/UI/Modal/Modal.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, useRef, useEffect } from 'react'; +import { MouseEvent, useRef } from 'react'; import classes from './Modal.module.css'; diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts index 1699e31..a651cd2 100644 --- a/client/src/store/actions/app.ts +++ b/client/src/store/actions/app.ts @@ -89,7 +89,7 @@ export interface DeleteAppAction { export const deleteApp = (id: number) => async (dispatch: Dispatch) => { try { - const res = await axios.delete>(`/api/apps/${id}`); + await axios.delete>(`/api/apps/${id}`); dispatch({ type: ActionTypes.createNotification, diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts index 11a8909..9608ebc 100644 --- a/client/src/store/actions/bookmark.ts +++ b/client/src/store/actions/bookmark.ts @@ -130,7 +130,7 @@ export interface DeleteCategoryAction { export const deleteCategory = (id: number) => async (dispatch: Dispatch) => { try { - const res = await axios.delete>(`/api/categories/${id}`); + await axios.delete>(`/api/categories/${id}`); dispatch({ type: ActionTypes.createNotification, @@ -191,7 +191,7 @@ export interface DeleteBookmarkAction { export const deleteBookmark = (bookmarkId: number, categoryId: number) => async (dispatch: Dispatch) => { try { - const res = await axios.delete>(`/api/bookmarks/${bookmarkId}`); + await axios.delete>(`/api/bookmarks/${bookmarkId}`); dispatch({ type: ActionTypes.createNotification, From 5e7cb72b8216b9adb47e52e54bd2265eef27f351 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 13 Jun 2021 23:21:35 +0200 Subject: [PATCH 4/4] Reworked OtherSettings to work with global config state. Fixed bug with certain settings not being synchronized --- client/src/App.tsx | 4 - .../src/components/Apps/AppForm/AppForm.tsx | 4 +- .../Bookmarks/BookmarkForm/BookmarkForm.tsx | 4 +- client/src/components/Home/Home.tsx | 43 ++++++--- .../Settings/OtherSettings/OtherSettings.tsx | 95 ++++++++++--------- .../WeatherSettings/WeatherSettings.tsx | 40 ++++---- .../Widgets/WeatherWidget/WeatherWidget.tsx | 2 +- client/src/interfaces/Forms.ts | 7 ++ client/src/store/actions/config.ts | 10 +- client/src/utility/searchConfig.ts | 2 +- utils/initialConfig.json | 4 + 11 files changed, 124 insertions(+), 91 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index d58f4ad..7210f3d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -18,10 +18,6 @@ if (localStorage.theme) { store.dispatch(setTheme(localStorage.theme)); } -if (localStorage.customTitle) { - document.title = localStorage.customTitle; -} - const App = (): JSX.Element => { return ( diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index cffeb19..e9c7beb 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -100,10 +100,10 @@ const AppForm = (props: ComponentProps): JSX.Element => { /> + > {' '}Check supported URL formats diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx index 9341565..eb83013 100644 --- a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx +++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx @@ -186,10 +186,10 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { /> + > {' '}Check supported URL formats diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index ab41ac2..854c04f 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -27,6 +27,9 @@ import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget'; import { greeter } from './functions/greeter'; import { dateTime } from './functions/dateTime'; +// Utils +import { searchConfig } from '../../utility'; + interface ComponentProps { getApps: Function; getCategories: Function; @@ -67,26 +70,36 @@ const Home = (props: ComponentProps): JSX.Element => { // Refresh greeter and time useEffect(() => { - const interval = setInterval(() => { - setHeader({ - dateTime: dateTime(), - greeting: greeter() - }) - }, 1000); + let interval: any; + + // Start interval only when hideHeader is false + if (searchConfig('hideHeader', 0) !== 1) { + interval = setInterval(() => { + setHeader({ + dateTime: dateTime(), + greeting: greeter() + }) + }, 1000); + } return () => clearInterval(interval); }, []) - + return ( -
-

{header.dateTime}

- Go to Settings - -

{header.greeting}

- -
-
+ {searchConfig('hideHeader', 0) !== 1 + ? ( +
+

{header.dateTime}

+ Go to Settings + +

{header.greeting}

+ +
+
+ ) + :
+ } {appsLoading diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index 7a6090e..5df8be7 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -1,69 +1,56 @@ import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; -import axios from 'axios'; -import { connect } from 'react-redux'; +// Redux +import { connect } from 'react-redux'; +import { createNotification, updateConfig } from '../../../store/actions'; + +// Typescript +import { GlobalState, NewNotification, SettingsForm } from '../../../interfaces'; + +// UI import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; -import { createNotification } from '../../../store/actions'; -import { ApiResponse, Config, NewNotification } from '../../../interfaces'; -interface FormState { - customTitle: string; - pinAppsByDefault: number; - pinCategoriesByDefault: number; -} +// Utils +import { searchConfig } from '../../../utility'; interface ComponentProps { createNotification: (notification: NewNotification) => void; + updateConfig: (formData: SettingsForm) => void; + loading: boolean; } const OtherSettings = (props: ComponentProps): JSX.Element => { - const [formData, setFormData] = useState({ + // Initial state + const [formData, setFormData] = useState({ customTitle: document.title, - pinAppsByDefault: 0, - pinCategoriesByDefault: 0 + pinAppsByDefault: 1, + pinCategoriesByDefault: 1, + hideHeader: 0 }) - // get initial config + // Get config useEffect(() => { - axios.get>('/api/config?keys=customTitle,pinAppsByDefault,pinCategoriesByDefault') - .then(data => { - let tmpFormData = { ...formData }; + setFormData({ + customTitle: searchConfig('customTitle', 'Flame'), + pinAppsByDefault: searchConfig('pinAppsByDefault', 1), + pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1), + hideHeader: searchConfig('hideHeader', 0) + }) + }, [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) => { + // Form handler + const formSubmitHandler = async (e: FormEvent) => { e.preventDefault(); - axios.put>('/api/config', formData) - .then(() => { - props.createNotification({ - title: 'Success', - message: 'Settings updated' - }) - }) - .catch((err) => console.log(err)); + // Save settings + await props.updateConfig(formData); // update local page title - localStorage.setItem('customTitle', formData.customTitle); document.title = formData.customTitle; } + // Input handler const inputChangeHandler = (e: ChangeEvent, isNumber?: boolean) => { let value: string | number = e.target.value; @@ -80,7 +67,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { return (
formSubmitHandler(e)}> - + { + + + +
) } -export default connect(null, { createNotification })(OtherSettings); \ No newline at end of file +const mapStateToProps = (state: GlobalState) => { + return { + loading: state.config.loading + } +} + +export default connect(mapStateToProps, { createNotification, updateConfig })(OtherSettings); \ No newline at end of file diff --git a/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx b/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx index 6f14cfc..912aced 100644 --- a/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx +++ b/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx @@ -22,6 +22,7 @@ interface ComponentProps { } const WeatherSettings = (props: ComponentProps): JSX.Element => { + // Initial state const [formData, setFormData] = useState({ WEATHER_API_KEY: '', lat: 0, @@ -29,19 +30,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => { isCelsius: 1 }) - const inputChangeHandler = (e: ChangeEvent, isNumber?: boolean) => { - let value: string | number = e.target.value; - - if (isNumber) { - value = parseFloat(value); - } - - setFormData({ - ...formData, - [e.target.name]: value - }) - } - + // Get config useEffect(() => { setFormData({ WEATHER_API_KEY: searchConfig('WEATHER_API_KEY', ''), @@ -51,6 +40,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => { }) }, [props.loading]); + // Form handler const formSubmitHandler = async (e: FormEvent) => { e.preventDefault(); @@ -58,7 +48,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => { if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) { props.createNotification({ title: 'Warning', - message: 'API Key is missing. Weather Module will NOT work' + message: 'API key is missing. Weather Module will NOT work' }) } @@ -81,10 +71,24 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => { }); } + // Input handler + const inputChangeHandler = (e: ChangeEvent, isNumber?: boolean) => { + let value: string | number = e.target.value; + + if (isNumber) { + value = parseFloat(value); + } + + setFormData({ + ...formData, + [e.target.name]: value + }) + } + return (
formSubmitHandler(e)}> - + { - + { - + { /> - +