From ee9aefa4fa11799cff399e43c38e5447677d7ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Mon, 8 Nov 2021 23:39:42 +0100 Subject: [PATCH 01/30] Modified models to support resource visibility. Added migration --- db/migrations/02_resource-access.js | 27 +++++++++++++ models/App.js | 59 +++++++++++++++++------------ models/Bookmark.js | 47 +++++++++++++---------- models/Category.js | 41 ++++++++++++-------- 4 files changed, 114 insertions(+), 60 deletions(-) create mode 100644 db/migrations/02_resource-access.js diff --git a/db/migrations/02_resource-access.js b/db/migrations/02_resource-access.js new file mode 100644 index 0000000..717ac38 --- /dev/null +++ b/db/migrations/02_resource-access.js @@ -0,0 +1,27 @@ +const { DataTypes } = require('sequelize'); +const { INTEGER } = DataTypes; + +const tables = ['categories', 'bookmarks', 'apps']; + +const up = async (query) => { + const template = { + type: INTEGER, + allowNull: true, + defaultValue: 0, + }; + + for await (let table of tables) { + await query.addColumn(table, 'isPublic', template); + } +}; + +const down = async (query) => { + for await (let table of tables) { + await query.removeColumn(table, 'isPublic'); + } +}; + +module.exports = { + up, + down, +}; diff --git a/models/App.js b/models/App.js index f521955..c71d350 100644 --- a/models/App.js +++ b/models/App.js @@ -1,31 +1,40 @@ const { DataTypes } = require('sequelize'); const { sequelize } = require('../db'); -const App = sequelize.define('App', { - name: { - type: DataTypes.STRING, - allowNull: false +const App = sequelize.define( + 'App', + { + name: { + type: DataTypes.STRING, + allowNull: false, + }, + url: { + type: DataTypes.STRING, + allowNull: false, + }, + icon: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: 'cancel', + }, + isPinned: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + orderId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + }, + isPublic: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: 0, + }, }, - url: { - type: DataTypes.STRING, - allowNull: false - }, - icon: { - type: DataTypes.STRING, - allowNull: false, - defaultValue: 'cancel' - }, - isPinned: { - type: DataTypes.BOOLEAN, - defaultValue: false - }, - orderId: { - type: DataTypes.INTEGER, - allowNull: true, - defaultValue: null + { + tableName: 'apps', } -}, { - tableName: 'apps' -}); +); -module.exports = App; \ No newline at end of file +module.exports = App; diff --git a/models/Bookmark.js b/models/Bookmark.js index 6c7d27b..652c119 100644 --- a/models/Bookmark.js +++ b/models/Bookmark.js @@ -1,25 +1,34 @@ const { DataTypes } = require('sequelize'); const { sequelize } = require('../db'); -const Bookmark = sequelize.define('Bookmark', { - name: { - type: DataTypes.STRING, - allowNull: false +const Bookmark = sequelize.define( + 'Bookmark', + { + name: { + type: DataTypes.STRING, + allowNull: false, + }, + url: { + type: DataTypes.STRING, + allowNull: false, + }, + categoryId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + icon: { + type: DataTypes.STRING, + defaultValue: '', + }, + isPublic: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: 0, + }, }, - url: { - type: DataTypes.STRING, - allowNull: false - }, - categoryId: { - type: DataTypes.INTEGER, - allowNull: false - }, - icon: { - type: DataTypes.STRING, - defaultValue: '' + { + tableName: 'bookmarks', } -}, { - tableName: 'bookmarks' -}); +); -module.exports = Bookmark; \ No newline at end of file +module.exports = Bookmark; diff --git a/models/Category.js b/models/Category.js index 9c9eda6..582a6c2 100644 --- a/models/Category.js +++ b/models/Category.js @@ -1,22 +1,31 @@ const { DataTypes } = require('sequelize'); const { sequelize } = require('../db'); -const Category = sequelize.define('Category', { - name: { - type: DataTypes.STRING, - allowNull: false +const Category = sequelize.define( + 'Category', + { + name: { + type: DataTypes.STRING, + allowNull: false, + }, + isPinned: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + orderId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + }, + isPublic: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: 0, + }, }, - isPinned: { - type: DataTypes.BOOLEAN, - defaultValue: false - }, - orderId: { - type: DataTypes.INTEGER, - allowNull: true, - defaultValue: null + { + tableName: 'categories', } -}, { - tableName: 'categories' -}); +); -module.exports = Category; \ No newline at end of file +module.exports = Category; From d1738a0a3e73478f0c3c83bb8036ef67924c4327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Mon, 8 Nov 2021 23:40:30 +0100 Subject: [PATCH 02/30] Set app visibility --- .../src/components/Apps/AppForm/AppForm.tsx | 66 +++++----- .../src/components/Apps/AppTable/AppTable.tsx | 5 +- client/src/components/Apps/Apps.tsx | 14 +-- client/src/interfaces/App.ts | 16 ++- client/src/store/reducers/app.ts | 113 ++++++++++-------- .../utility/templateObjects/appTemplate.ts | 17 +++ client/src/utility/templateObjects/index.ts | 1 + 7 files changed, 127 insertions(+), 105 deletions(-) create mode 100644 client/src/utility/templateObjects/appTemplate.ts diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index d44418e..c3ffdad 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -8,6 +8,7 @@ import classes from './AppForm.module.css'; import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; +import { inputHandler, newAppTemplate } from '../../../utility'; interface ComponentProps { modalHandler: () => void; @@ -19,32 +20,27 @@ interface ComponentProps { const AppForm = (props: ComponentProps): JSX.Element => { const [useCustomIcon, toggleUseCustomIcon] = useState(false); const [customIcon, setCustomIcon] = useState(null); - const [formData, setFormData] = useState({ - name: '', - url: '', - icon: '', - }); + const [formData, setFormData] = useState(newAppTemplate); useEffect(() => { if (props.app) { setFormData({ - name: props.app.name, - url: props.app.url, - icon: props.app.icon, + ...props.app, }); } else { - setFormData({ - name: '', - url: '', - icon: '', - }); + setFormData(newAppTemplate); } }, [props.app]); - const inputChangeHandler = (e: ChangeEvent): void => { - setFormData({ - ...formData, - [e.target.name]: e.target.value, + const inputChangeHandler = ( + e: ChangeEvent, + options?: { isNumber?: boolean; isBool?: boolean } + ) => { + inputHandler({ + e, + options, + setStateHandler: setFormData, + state: formData, }); }; @@ -86,11 +82,7 @@ const AppForm = (props: ComponentProps): JSX.Element => { } } - setFormData({ - name: '', - url: '', - icon: '', - }); + setFormData(newAppTemplate); }; return ( @@ -98,6 +90,7 @@ const AppForm = (props: ComponentProps): JSX.Element => { modalHandler={props.modalHandler} formHandler={formSubmitHandler} > + {/* NAME */} { onChange={(e) => inputChangeHandler(e)} /> + + {/* URL */} { value={formData.url} onChange={(e) => inputChangeHandler(e)} /> - - - {' '} - Check supported URL formats - - + + {/* ICON */} {!useCustomIcon ? ( // use mdi icon @@ -182,6 +169,21 @@ const AppForm = (props: ComponentProps): JSX.Element => { )} + + {/* VISIBILITY */} + + + + + {!props.app ? ( ) : ( diff --git a/client/src/components/Apps/AppTable/AppTable.tsx b/client/src/components/Apps/AppTable/AppTable.tsx index 3f68d76..631bd74 100644 --- a/client/src/components/Apps/AppTable/AppTable.tsx +++ b/client/src/components/Apps/AppTable/AppTable.tsx @@ -114,7 +114,7 @@ const AppTable = (props: ComponentProps): JSX.Element => { {(provided) => ( {localApps.map((app: App, index): JSX.Element => { @@ -143,6 +143,9 @@ const AppTable = (props: ComponentProps): JSX.Element => { + {!snapshot.isDragging && (
{app.name} {app.url} {app.icon} + {app.isPublic ? 'Visible' : 'Hidden'} +
{ const [modalIsOpen, setModalIsOpen] = useState(false); const [isInEdit, setIsInEdit] = useState(false); const [isInUpdate, setIsInUpdate] = useState(false); - const [appInUpdate, setAppInUpdate] = useState({ - name: 'string', - url: 'string', - icon: 'string', - isPinned: false, - orderId: 0, - id: 0, - createdAt: new Date(), - updatedAt: new Date(), - }); + const [appInUpdate, setAppInUpdate] = useState(appTemplate); useEffect(() => { if (apps.length === 0) { diff --git a/client/src/interfaces/App.ts b/client/src/interfaces/App.ts index e4314a5..fc4a738 100644 --- a/client/src/interfaces/App.ts +++ b/client/src/interfaces/App.ts @@ -1,15 +1,13 @@ import { Model } from '.'; -export interface App extends Model { - name: string; - url: string; - icon: string; - isPinned: boolean; - orderId: number; -} - export interface NewApp { name: string; url: string; icon: string; -} \ No newline at end of file + isPublic: boolean; +} + +export interface App extends Model, NewApp { + orderId: number; + isPinned: boolean; +} diff --git a/client/src/store/reducers/app.ts b/client/src/store/reducers/app.ts index 0935819..9d4c359 100644 --- a/client/src/store/reducers/app.ts +++ b/client/src/store/reducers/app.ts @@ -11,108 +11,115 @@ export interface State { const initialState: State = { loading: true, apps: [], - errors: undefined -} + errors: undefined, +}; const getApps = (state: State, action: Action): State => { return { ...state, loading: true, - errors: undefined - } -} + errors: undefined, + }; +}; const getAppsSuccess = (state: State, action: Action): State => { return { ...state, loading: false, - apps: action.payload - } -} + apps: action.payload, + }; +}; const getAppsError = (state: State, action: Action): State => { return { ...state, loading: false, - errors: action.payload - } -} + errors: action.payload, + }; +}; const pinApp = (state: State, action: Action): State => { const tmpApps = [...state.apps]; const changedApp = tmpApps.find((app: App) => app.id === action.payload.id); - + if (changedApp) { changedApp.isPinned = action.payload.isPinned; } - + return { ...state, - apps: tmpApps - } -} + apps: tmpApps, + }; +}; const addAppSuccess = (state: State, action: Action): State => { return { ...state, - apps: [...state.apps, action.payload] - } -} + apps: [...state.apps, action.payload], + }; +}; const deleteApp = (state: State, action: Action): State => { - const tmpApps = [...state.apps].filter((app: App) => app.id !== action.payload); - return { ...state, - apps: tmpApps - } -} + apps: [...state.apps].filter((app: App) => app.id !== action.payload), + }; +}; const updateApp = (state: State, action: Action): State => { - const tmpApps = [...state.apps]; - const appInUpdate = tmpApps.find((app: App) => app.id === action.payload.id); - - if (appInUpdate) { - appInUpdate.name = action.payload.name; - appInUpdate.url = action.payload.url; - appInUpdate.icon = action.payload.icon; - } + const appIdx = state.apps.findIndex((app) => app.id === action.payload.id); return { ...state, - apps: tmpApps - } -} + apps: [ + ...state.apps.slice(0, appIdx), + { + ...action.payload, + }, + ...state.apps.slice(appIdx + 1), + ], + }; +}; const reorderApps = (state: State, action: Action): State => { return { ...state, - apps: action.payload - } -} + apps: action.payload, + }; +}; const sortApps = (state: State, action: Action): State => { const sortedApps = sortData(state.apps, action.payload); return { ...state, - apps: sortedApps - } -} + apps: sortedApps, + }; +}; const appReducer = (state = initialState, action: Action) => { switch (action.type) { - case ActionTypes.getApps: return getApps(state, action); - case ActionTypes.getAppsSuccess: return getAppsSuccess(state, action); - case ActionTypes.getAppsError: return getAppsError(state, action); - case ActionTypes.pinApp: return pinApp(state, action); - case ActionTypes.addAppSuccess: return addAppSuccess(state, action); - case ActionTypes.deleteApp: return deleteApp(state, action); - case ActionTypes.updateApp: return updateApp(state, action); - case ActionTypes.reorderApps: return reorderApps(state, action); - case ActionTypes.sortApps: return sortApps(state, action); - default: return state; + case ActionTypes.getApps: + return getApps(state, action); + case ActionTypes.getAppsSuccess: + return getAppsSuccess(state, action); + case ActionTypes.getAppsError: + return getAppsError(state, action); + case ActionTypes.pinApp: + return pinApp(state, action); + case ActionTypes.addAppSuccess: + return addAppSuccess(state, action); + case ActionTypes.deleteApp: + return deleteApp(state, action); + case ActionTypes.updateApp: + return updateApp(state, action); + case ActionTypes.reorderApps: + return reorderApps(state, action); + case ActionTypes.sortApps: + return sortApps(state, action); + default: + return state; } -} +}; -export default appReducer; \ No newline at end of file +export default appReducer; diff --git a/client/src/utility/templateObjects/appTemplate.ts b/client/src/utility/templateObjects/appTemplate.ts new file mode 100644 index 0000000..8dad83d --- /dev/null +++ b/client/src/utility/templateObjects/appTemplate.ts @@ -0,0 +1,17 @@ +import { App, NewApp } from '../../interfaces'; + +export const newAppTemplate: NewApp = { + name: '', + url: '', + icon: '', + isPublic: true, +}; + +export const appTemplate: App = { + ...newAppTemplate, + isPinned: false, + orderId: 0, + id: 0, + createdAt: new Date(), + updatedAt: new Date(), +}; diff --git a/client/src/utility/templateObjects/index.ts b/client/src/utility/templateObjects/index.ts index 3f2d57c..228bed4 100644 --- a/client/src/utility/templateObjects/index.ts +++ b/client/src/utility/templateObjects/index.ts @@ -1,2 +1,3 @@ export * from './configTemplate'; export * from './settingsTemplate'; +export * from './appTemplate'; From 1f2fedf7541a35f88d4f8bf6c07cc63eb6ca3d01 Mon Sep 17 00:00:00 2001 From: Pete Midgley Date: Tue, 9 Nov 2021 21:26:11 +1100 Subject: [PATCH 03/30] Update searchQueries.json Added Wikipedia (English) --- client/src/utility/searchQueries.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/utility/searchQueries.json b/client/src/utility/searchQueries.json index 374763c..ead25c5 100644 --- a/client/src/utility/searchQueries.json +++ b/client/src/utility/searchQueries.json @@ -54,6 +54,11 @@ "name": "YouTube", "prefix": "yt", "template": "https://www.youtube.com/results?search_query=" + }, + { + "name": "Wikipedia", + "prefix": "w", + "template": "https://en.wikipedia.org/w/index.php?search=" } ] } From 7e89ab0204540d388dd3c10d263b9fcf1edc426a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Tue, 9 Nov 2021 12:21:36 +0100 Subject: [PATCH 04/30] App state: refactored reducers and actions for config, theme and notifications --- client/src/store/action-creators/config.ts | 141 +++++++ client/src/store/action-creators/index.ts | 3 + .../src/store/action-creators/notification.ts | 24 ++ client/src/store/action-creators/theme.ts | 26 ++ client/src/store/action-types/index.ts | 14 + client/src/store/actions/actionTypes.ts | 110 ------ client/src/store/actions/app.ts | 205 ---------- client/src/store/actions/bookmark.ts | 371 ------------------ client/src/store/actions/config.ts | 141 +------ client/src/store/actions/index.ts | 33 +- client/src/store/actions/notification.ts | 25 +- client/src/store/actions/theme.ts | 30 +- client/src/store/index.ts | 2 + client/src/store/reducers/app.ts | 125 ------ client/src/store/reducers/bookmark.ts | 178 --------- client/src/store/reducers/config.ts | 101 ++--- client/src/store/reducers/index.ts | 20 +- client/src/store/reducers/notification.ts | 67 ++-- client/src/store/reducers/theme.ts | 16 +- client/src/store/store.ts | 9 +- 20 files changed, 348 insertions(+), 1293 deletions(-) create mode 100644 client/src/store/action-creators/config.ts create mode 100644 client/src/store/action-creators/index.ts create mode 100644 client/src/store/action-creators/notification.ts create mode 100644 client/src/store/action-creators/theme.ts create mode 100644 client/src/store/action-types/index.ts delete mode 100644 client/src/store/actions/actionTypes.ts delete mode 100644 client/src/store/actions/app.ts delete mode 100644 client/src/store/actions/bookmark.ts create mode 100644 client/src/store/index.ts delete mode 100644 client/src/store/reducers/app.ts delete mode 100644 client/src/store/reducers/bookmark.ts diff --git a/client/src/store/action-creators/config.ts b/client/src/store/action-creators/config.ts new file mode 100644 index 0000000..6bbde52 --- /dev/null +++ b/client/src/store/action-creators/config.ts @@ -0,0 +1,141 @@ +import { Dispatch } from 'redux'; +import { + AddQueryAction, + DeleteQueryAction, + FetchQueriesAction, + GetConfigAction, + UpdateConfigAction, + UpdateQueryAction, +} from '../actions/config'; +import axios from 'axios'; +import { + ApiResponse, + Config, + OtherSettingsForm, + Query, + SearchForm, + WeatherForm, +} from '../../interfaces'; +import { ActionType } from '../action-types'; +import { storeUIConfig } from '../../utility'; +import { createNotification } from '.'; + +export const getConfig = () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/config'); + + dispatch({ + type: ActionType.getConfig, + payload: res.data.data, + }); + + // Set custom page title if set + document.title = res.data.data.customTitle; + + // Store settings for priority UI elements + const keys: (keyof Config)[] = [ + 'useAmericanDate', + 'greetingsSchema', + 'daySchema', + 'monthSchema', + ]; + for (let key of keys) { + storeUIConfig(key, res.data.data); + } + } catch (err) { + console.log(err); + } +}; + +export const updateConfig = + (formData: WeatherForm | OtherSettingsForm | SearchForm) => + async (dispatch: Dispatch) => { + try { + const res = await axios.put>('/api/config', formData); + + createNotification({ + title: 'Success', + message: 'Settings updated', + }); + + dispatch({ + type: ActionType.updateConfig, + payload: res.data.data, + }); + + // Store settings for priority UI elements + const keys: (keyof Config)[] = [ + 'useAmericanDate', + 'greetingsSchema', + 'daySchema', + 'monthSchema', + ]; + for (let key of keys) { + storeUIConfig(key, res.data.data); + } + } catch (err) { + console.log(err); + } + }; + +export const fetchQueries = + () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/queries'); + + dispatch({ + type: ActionType.fetchQueries, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export const addQuery = + (query: Query) => async (dispatch: Dispatch) => { + try { + const res = await axios.post>('/api/queries', query); + + dispatch({ + type: ActionType.addQuery, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export const deleteQuery = + (prefix: string) => async (dispatch: Dispatch) => { + try { + const res = await axios.delete>( + `/api/queries/${prefix}` + ); + + dispatch({ + type: ActionType.deleteQuery, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export const updateQuery = + (query: Query, oldPrefix: string) => + async (dispatch: Dispatch) => { + try { + const res = await axios.put>( + `/api/queries/${oldPrefix}`, + query + ); + + dispatch({ + type: ActionType.updateQuery, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; diff --git a/client/src/store/action-creators/index.ts b/client/src/store/action-creators/index.ts new file mode 100644 index 0000000..d7b5e45 --- /dev/null +++ b/client/src/store/action-creators/index.ts @@ -0,0 +1,3 @@ +export * from './theme'; +export * from './config'; +export * from './notification'; diff --git a/client/src/store/action-creators/notification.ts b/client/src/store/action-creators/notification.ts new file mode 100644 index 0000000..b46e27c --- /dev/null +++ b/client/src/store/action-creators/notification.ts @@ -0,0 +1,24 @@ +import { Dispatch } from 'redux'; +import { NewNotification } from '../../interfaces'; +import { ActionType } from '../action-types'; +import { + CreateNotificationAction, + ClearNotificationAction, +} from '../actions/notification'; + +export const createNotification = + (notification: NewNotification) => + (dispatch: Dispatch) => { + dispatch({ + type: ActionType.createNotification, + payload: notification, + }); + }; + +export const clearNotification = + (id: number) => (dispatch: Dispatch) => { + dispatch({ + type: ActionType.clearNotification, + payload: id, + }); + }; diff --git a/client/src/store/action-creators/theme.ts b/client/src/store/action-creators/theme.ts new file mode 100644 index 0000000..ba52d27 --- /dev/null +++ b/client/src/store/action-creators/theme.ts @@ -0,0 +1,26 @@ +import { Dispatch } from 'redux'; +import { SetThemeAction } from '../actions/theme'; +import { ActionType } from '../action-types'; +import { Theme } from '../../interfaces/Theme'; +import { themes } from '../../components/Themer/themes.json'; + +export const setTheme = + (name: string) => (dispatch: Dispatch) => { + const theme = themes.find((theme) => theme.name === name); + + if (theme) { + localStorage.setItem('theme', name); + loadTheme(theme); + + dispatch({ + type: ActionType.setTheme, + payload: theme, + }); + } + }; + +export const loadTheme = (theme: Theme): void => { + for (const [key, value] of Object.entries(theme.colors)) { + document.body.style.setProperty(`--color-${key}`, value); + } +}; diff --git a/client/src/store/action-types/index.ts b/client/src/store/action-types/index.ts new file mode 100644 index 0000000..eb32118 --- /dev/null +++ b/client/src/store/action-types/index.ts @@ -0,0 +1,14 @@ +export enum ActionType { + // THEME + setTheme = 'SET_THEME', + // CONFIG + getConfig = 'GET_CONFIG', + updateConfig = 'UPDATE_CONFIG', + addQuery = 'ADD_QUERY', + deleteQuery = 'DELETE_QUERY', + fetchQueries = 'FETCH_QUERIES', + updateQuery = 'UPDATE_QUERY', + // NOTIFICATIONS + createNotification = 'CREATE_NOTIFICATION', + clearNotification = 'CLEAR_NOTIFICATION', +} diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts deleted file mode 100644 index c670b2f..0000000 --- a/client/src/store/actions/actionTypes.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - // Theme - SetThemeAction, - // Apps - GetAppsAction, - PinAppAction, - AddAppAction, - DeleteAppAction, - UpdateAppAction, - ReorderAppsAction, - SortAppsAction, - // Categories - GetCategoriesAction, - AddCategoryAction, - PinCategoryAction, - DeleteCategoryAction, - UpdateCategoryAction, - SortCategoriesAction, - ReorderCategoriesAction, - // Bookmarks - AddBookmarkAction, - DeleteBookmarkAction, - UpdateBookmarkAction, - // Notifications - CreateNotificationAction, - ClearNotificationAction, - // Config - GetConfigAction, - UpdateConfigAction, -} from './'; -import { - AddQueryAction, - DeleteQueryAction, - FetchQueriesAction, - UpdateQueryAction, -} from './config'; - -export enum ActionTypes { - // Theme - setTheme = 'SET_THEME', - // Apps - getApps = 'GET_APPS', - getAppsSuccess = 'GET_APPS_SUCCESS', - getAppsError = 'GET_APPS_ERROR', - pinApp = 'PIN_APP', - addApp = 'ADD_APP', - addAppSuccess = 'ADD_APP_SUCCESS', - deleteApp = 'DELETE_APP', - updateApp = 'UPDATE_APP', - reorderApps = 'REORDER_APPS', - sortApps = 'SORT_APPS', - // Categories - getCategories = 'GET_CATEGORIES', - getCategoriesSuccess = 'GET_CATEGORIES_SUCCESS', - getCategoriesError = 'GET_CATEGORIES_ERROR', - addCategory = 'ADD_CATEGORY', - pinCategory = 'PIN_CATEGORY', - deleteCategory = 'DELETE_CATEGORY', - updateCategory = 'UPDATE_CATEGORY', - sortCategories = 'SORT_CATEGORIES', - reorderCategories = 'REORDER_CATEGORIES', - // Bookmarks - addBookmark = 'ADD_BOOKMARK', - deleteBookmark = 'DELETE_BOOKMARK', - updateBookmark = 'UPDATE_BOOKMARK', - // Notifications - createNotification = 'CREATE_NOTIFICATION', - clearNotification = 'CLEAR_NOTIFICATION', - // Config - getConfig = 'GET_CONFIG', - updateConfig = 'UPDATE_CONFIG', - fetchQueries = 'FETCH_QUERIES', - addQuery = 'ADD_QUERY', - deleteQuery = 'DELETE_QUERY', - updateQuery = 'UPDATE_QUERY', -} - -export type Action = - // Theme - | SetThemeAction - // Apps - | GetAppsAction - | PinAppAction - | AddAppAction - | DeleteAppAction - | UpdateAppAction - | ReorderAppsAction - | SortAppsAction - // Categories - | GetCategoriesAction - | AddCategoryAction - | PinCategoryAction - | DeleteCategoryAction - | UpdateCategoryAction - | SortCategoriesAction - | ReorderCategoriesAction - // Bookmarks - | AddBookmarkAction - | DeleteBookmarkAction - | UpdateBookmarkAction - // Notifications - | CreateNotificationAction - | ClearNotificationAction - // Config - | GetConfigAction - | UpdateConfigAction - | FetchQueriesAction - | AddQueryAction - | DeleteQueryAction - | UpdateQueryAction; diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts deleted file mode 100644 index b33a78b..0000000 --- a/client/src/store/actions/app.ts +++ /dev/null @@ -1,205 +0,0 @@ -import axios from 'axios'; -import { Dispatch } from 'redux'; -import { ActionTypes } from './actionTypes'; -import { App, ApiResponse, NewApp, Config } from '../../interfaces'; -import { CreateNotificationAction } from './notification'; - -export interface GetAppsAction { - type: - | ActionTypes.getApps - | ActionTypes.getAppsSuccess - | ActionTypes.getAppsError; - payload: T; -} - -export const getApps = () => async (dispatch: Dispatch) => { - dispatch>({ - type: ActionTypes.getApps, - payload: undefined, - }); - - try { - const res = await axios.get>('/api/apps'); - - dispatch>({ - type: ActionTypes.getAppsSuccess, - payload: res.data.data, - }); - } catch (err) { - console.log(err); - } -}; - -export interface PinAppAction { - type: ActionTypes.pinApp; - payload: App; -} - -export const pinApp = (app: App) => async (dispatch: Dispatch) => { - try { - const { id, isPinned, name } = app; - const res = await axios.put>(`/api/apps/${id}`, { - isPinned: !isPinned, - }); - - const status = isPinned - ? 'unpinned from Homescreen' - : 'pinned to Homescreen'; - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `App ${name} ${status}`, - }, - }); - - dispatch({ - type: ActionTypes.pinApp, - payload: res.data.data, - }); - } catch (err) { - console.log(err); - } -}; - -export interface AddAppAction { - type: ActionTypes.addAppSuccess; - payload: App; -} - -export const addApp = - (formData: NewApp | FormData) => async (dispatch: Dispatch) => { - try { - const res = await axios.post>('/api/apps', formData); - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `App added`, - }, - }); - - await dispatch({ - type: ActionTypes.addAppSuccess, - payload: res.data.data, - }); - - // Sort apps - dispatch(sortApps()); - } catch (err) { - console.log(err); - } - }; - -export interface DeleteAppAction { - type: ActionTypes.deleteApp; - payload: number; -} - -export const deleteApp = (id: number) => async (dispatch: Dispatch) => { - try { - await axios.delete>(`/api/apps/${id}`); - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: 'App deleted', - }, - }); - - dispatch({ - type: ActionTypes.deleteApp, - payload: id, - }); - } catch (err) { - console.log(err); - } -}; - -export interface UpdateAppAction { - type: ActionTypes.updateApp; - payload: App; -} - -export const updateApp = - (id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => { - try { - const res = await axios.put>( - `/api/apps/${id}`, - formData - ); - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `App updated`, - }, - }); - - await dispatch({ - type: ActionTypes.updateApp, - payload: res.data.data, - }); - - // Sort apps - dispatch(sortApps()); - } catch (err) { - console.log(err); - } - }; - -export interface ReorderAppsAction { - type: ActionTypes.reorderApps; - payload: App[]; -} - -interface ReorderQuery { - apps: { - id: number; - orderId: number; - }[]; -} - -export const reorderApps = (apps: App[]) => async (dispatch: Dispatch) => { - try { - const updateQuery: ReorderQuery = { apps: [] }; - - apps.forEach((app, index) => - updateQuery.apps.push({ - id: app.id, - orderId: index + 1, - }) - ); - - await axios.put>('/api/apps/0/reorder', updateQuery); - - dispatch({ - type: ActionTypes.reorderApps, - payload: apps, - }); - } catch (err) { - console.log(err); - } -}; - -export interface SortAppsAction { - type: ActionTypes.sortApps; - payload: string; -} - -export const sortApps = () => async (dispatch: Dispatch) => { - try { - const res = await axios.get>('/api/config'); - - dispatch({ - type: ActionTypes.sortApps, - payload: res.data.data.useOrdering, - }); - } catch (err) { - console.log(err); - } -}; diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts deleted file mode 100644 index 6d6fdf5..0000000 --- a/client/src/store/actions/bookmark.ts +++ /dev/null @@ -1,371 +0,0 @@ -import axios from 'axios'; -import { Dispatch } from 'redux'; -import { ActionTypes } from './actionTypes'; -import { - Category, - ApiResponse, - NewCategory, - Bookmark, - NewBookmark, - Config, -} from '../../interfaces'; -import { CreateNotificationAction } from './notification'; - -/** - * GET CATEGORIES - */ -export interface GetCategoriesAction { - type: - | ActionTypes.getCategories - | ActionTypes.getCategoriesSuccess - | ActionTypes.getCategoriesError; - payload: T; -} - -export const getCategories = () => async (dispatch: Dispatch) => { - dispatch>({ - type: ActionTypes.getCategories, - payload: undefined, - }); - - try { - const res = await axios.get>('/api/categories'); - - dispatch>({ - type: ActionTypes.getCategoriesSuccess, - payload: res.data.data, - }); - } catch (err) { - console.log(err); - } -}; - -/** - * ADD CATEGORY - */ -export interface AddCategoryAction { - type: ActionTypes.addCategory; - payload: Category; -} - -export const addCategory = - (formData: NewCategory) => async (dispatch: Dispatch) => { - try { - const res = await axios.post>( - '/api/categories', - formData - ); - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `Category ${formData.name} created`, - }, - }); - - dispatch({ - type: ActionTypes.addCategory, - payload: res.data.data, - }); - - dispatch(sortCategories()); - } catch (err) { - console.log(err); - } - }; - -/** - * ADD BOOKMARK - */ -export interface AddBookmarkAction { - type: ActionTypes.addBookmark; - payload: Bookmark; -} - -export const addBookmark = - (formData: NewBookmark | FormData) => async (dispatch: Dispatch) => { - try { - const res = await axios.post>( - '/api/bookmarks', - formData - ); - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `Bookmark created`, - }, - }); - - dispatch({ - type: ActionTypes.addBookmark, - payload: res.data.data, - }); - } catch (err) { - console.log(err); - } - }; - -/** - * PIN CATEGORY - */ -export interface PinCategoryAction { - type: ActionTypes.pinCategory; - payload: Category; -} - -export const pinCategory = - (category: Category) => async (dispatch: Dispatch) => { - try { - const { id, isPinned, name } = category; - const res = await axios.put>( - `/api/categories/${id}`, - { isPinned: !isPinned } - ); - - const status = isPinned - ? 'unpinned from Homescreen' - : 'pinned to Homescreen'; - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `Category ${name} ${status}`, - }, - }); - - dispatch({ - type: ActionTypes.pinCategory, - payload: res.data.data, - }); - } catch (err) { - console.log(err); - } - }; - -/** - * DELETE CATEGORY - */ -export interface DeleteCategoryAction { - type: ActionTypes.deleteCategory; - payload: number; -} - -export const deleteCategory = (id: number) => async (dispatch: Dispatch) => { - try { - await axios.delete>(`/api/categories/${id}`); - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `Category deleted`, - }, - }); - - dispatch({ - type: ActionTypes.deleteCategory, - payload: id, - }); - } catch (err) { - console.log(err); - } -}; - -/** - * UPDATE CATEGORY - */ -export interface UpdateCategoryAction { - type: ActionTypes.updateCategory; - payload: Category; -} - -export const updateCategory = - (id: number, formData: NewCategory) => async (dispatch: Dispatch) => { - try { - const res = await axios.put>( - `/api/categories/${id}`, - formData - ); - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `Category ${formData.name} updated`, - }, - }); - - dispatch({ - type: ActionTypes.updateCategory, - payload: res.data.data, - }); - - dispatch(sortCategories()); - } catch (err) { - console.log(err); - } - }; - -/** - * DELETE BOOKMARK - */ -export interface DeleteBookmarkAction { - type: ActionTypes.deleteBookmark; - payload: { - bookmarkId: number; - categoryId: number; - }; -} - -export const deleteBookmark = - (bookmarkId: number, categoryId: number) => async (dispatch: Dispatch) => { - try { - await axios.delete>(`/api/bookmarks/${bookmarkId}`); - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: 'Bookmark deleted', - }, - }); - - dispatch({ - type: ActionTypes.deleteBookmark, - payload: { - bookmarkId, - categoryId, - }, - }); - } catch (err) { - console.log(err); - } - }; - -/** - * UPDATE BOOKMARK - */ -export interface UpdateBookmarkAction { - type: ActionTypes.updateBookmark; - payload: Bookmark; -} - -export const updateBookmark = - ( - bookmarkId: number, - formData: NewBookmark | FormData, - category: { - prev: number; - curr: number; - } - ) => - async (dispatch: Dispatch) => { - try { - const res = await axios.put>( - `/api/bookmarks/${bookmarkId}`, - formData - ); - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `Bookmark updated`, - }, - }); - - // Check if category was changed - const categoryWasChanged = category.curr !== category.prev; - - if (categoryWasChanged) { - // Delete bookmark from old category - dispatch({ - type: ActionTypes.deleteBookmark, - payload: { - bookmarkId, - categoryId: category.prev, - }, - }); - - // Add bookmark to the new category - dispatch({ - type: ActionTypes.addBookmark, - payload: res.data.data, - }); - } else { - // Else update only name/url/icon - dispatch({ - type: ActionTypes.updateBookmark, - payload: res.data.data, - }); - } - } catch (err) { - console.log(err); - } - }; - -/** - * SORT CATEGORIES - */ -export interface SortCategoriesAction { - type: ActionTypes.sortCategories; - payload: string; -} - -export const sortCategories = () => async (dispatch: Dispatch) => { - try { - const res = await axios.get>('/api/config'); - - dispatch({ - type: ActionTypes.sortCategories, - payload: res.data.data.useOrdering, - }); - } catch (err) { - console.log(err); - } -}; - -/** - * REORDER CATEGORIES - */ -export interface ReorderCategoriesAction { - type: ActionTypes.reorderCategories; - payload: Category[]; -} - -interface ReorderQuery { - categories: { - id: number; - orderId: number; - }[]; -} - -export const reorderCategories = - (categories: Category[]) => async (dispatch: Dispatch) => { - try { - const updateQuery: ReorderQuery = { categories: [] }; - - categories.forEach((category, index) => - updateQuery.categories.push({ - id: category.id, - orderId: index + 1, - }) - ); - - await axios.put>( - '/api/categories/0/reorder', - updateQuery - ); - - dispatch({ - type: ActionTypes.reorderCategories, - payload: categories, - }); - } catch (err) { - console.log(err); - } - }; diff --git a/client/src/store/actions/config.ts b/client/src/store/actions/config.ts index 88e8ff3..42bf64d 100644 --- a/client/src/store/actions/config.ts +++ b/client/src/store/actions/config.ts @@ -1,157 +1,32 @@ -import axios from 'axios'; -import { Dispatch } from 'redux'; -import { ActionTypes } from './actionTypes'; -import { Config, ApiResponse, Query } from '../../interfaces'; -import { CreateNotificationAction } from './notification'; -import { storeUIConfig } from '../../utility'; +import { ActionType } from '../action-types'; +import { Config, Query } from '../../interfaces'; export interface GetConfigAction { - type: ActionTypes.getConfig; + type: ActionType.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, - }); - - // Set custom page title if set - document.title = res.data.data.customTitle; - - // Store settings for priority UI elements - const keys: (keyof Config)[] = [ - 'useAmericanDate', - 'greetingsSchema', - 'daySchema', - 'monthSchema', - ]; - for (let key of keys) { - storeUIConfig(key, res.data.data); - } - } catch (err) { - console.log(err); - } -}; - export interface UpdateConfigAction { - type: ActionTypes.updateConfig; + type: ActionType.updateConfig; payload: Config; } -export const updateConfig = (formData: any) => 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, - }); - - // Store settings for priority UI elements - const keys: (keyof Config)[] = [ - 'useAmericanDate', - 'greetingsSchema', - 'daySchema', - 'monthSchema', - ]; - for (let key of keys) { - storeUIConfig(key, res.data.data); - } - } catch (err) { - console.log(err); - } -}; - export interface FetchQueriesAction { - type: ActionTypes.fetchQueries; + type: ActionType.fetchQueries; payload: Query[]; } -export const fetchQueries = - () => async (dispatch: Dispatch) => { - try { - const res = await axios.get>('/api/queries'); - - dispatch({ - type: ActionTypes.fetchQueries, - payload: res.data.data, - }); - } catch (err) { - console.log(err); - } - }; - export interface AddQueryAction { - type: ActionTypes.addQuery; + type: ActionType.addQuery; payload: Query; } -export const addQuery = - (query: Query) => async (dispatch: Dispatch) => { - try { - const res = await axios.post>('/api/queries', query); - - dispatch({ - type: ActionTypes.addQuery, - payload: res.data.data, - }); - } catch (err) { - console.log(err); - } - }; - export interface DeleteQueryAction { - type: ActionTypes.deleteQuery; + type: ActionType.deleteQuery; payload: Query[]; } -export const deleteQuery = - (prefix: string) => async (dispatch: Dispatch) => { - try { - const res = await axios.delete>( - `/api/queries/${prefix}` - ); - - dispatch({ - type: ActionTypes.deleteQuery, - payload: res.data.data, - }); - } catch (err) { - console.log(err); - } - }; - export interface UpdateQueryAction { - type: ActionTypes.updateQuery; + type: ActionType.updateQuery; payload: Query[]; } - -export const updateQuery = - (query: Query, oldPrefix: string) => - async (dispatch: Dispatch) => { - try { - const res = await axios.put>( - `/api/queries/${oldPrefix}`, - query - ); - - dispatch({ - type: ActionTypes.updateQuery, - payload: res.data.data, - }); - } catch (err) { - console.log(err); - } - }; diff --git a/client/src/store/actions/index.ts b/client/src/store/actions/index.ts index e516e54..d6bea13 100644 --- a/client/src/store/actions/index.ts +++ b/client/src/store/actions/index.ts @@ -1,6 +1,27 @@ -export * from './theme'; -export * from './app'; -export * from './actionTypes'; -export * from './bookmark'; -export * from './notification'; -export * from './config'; \ No newline at end of file +import { SetThemeAction } from './theme'; +import { + AddQueryAction, + DeleteQueryAction, + FetchQueriesAction, + GetConfigAction, + UpdateConfigAction, + UpdateQueryAction, +} from './config'; +import { + ClearNotificationAction, + CreateNotificationAction, +} from './notification'; + +export type Action = + // Theme + | SetThemeAction + // Config + | GetConfigAction + | UpdateConfigAction + | AddQueryAction + | DeleteQueryAction + | FetchQueriesAction + | UpdateQueryAction + // Notifications + | CreateNotificationAction + | ClearNotificationAction; diff --git a/client/src/store/actions/notification.ts b/client/src/store/actions/notification.ts index a4f17cf..e6e25ab 100644 --- a/client/src/store/actions/notification.ts +++ b/client/src/store/actions/notification.ts @@ -1,27 +1,12 @@ -import { Dispatch } from 'redux'; -import { ActionTypes } from '.'; +import { ActionType } from '../action-types'; import { NewNotification } from '../../interfaces'; export interface CreateNotificationAction { - type: ActionTypes.createNotification, - payload: NewNotification -} - -export const createNotification = (notification: NewNotification) => (dispatch: Dispatch) => { - dispatch({ - type: ActionTypes.createNotification, - payload: notification - }) + type: ActionType.createNotification; + payload: NewNotification; } export interface ClearNotificationAction { - type: ActionTypes.clearNotification, - payload: number + type: ActionType.clearNotification; + payload: number; } - -export const clearNotification = (id: number) => (dispatch: Dispatch) => { - dispatch({ - type: ActionTypes.clearNotification, - payload: id - }) -} \ No newline at end of file diff --git a/client/src/store/actions/theme.ts b/client/src/store/actions/theme.ts index 0134ead..036b1a3 100644 --- a/client/src/store/actions/theme.ts +++ b/client/src/store/actions/theme.ts @@ -1,29 +1,7 @@ -import { Dispatch } from 'redux'; -import { themes } from '../../components/Themer/themes.json'; -import { Theme } from '../../interfaces/Theme'; -import { ActionTypes } from './actionTypes'; +import { ActionType } from '../action-types'; +import { Theme } from '../../interfaces'; export interface SetThemeAction { - type: ActionTypes.setTheme, - payload: Theme + type: ActionType.setTheme; + payload: Theme; } - -export const setTheme = (themeName: string) => (dispatch: Dispatch) => { - const theme = themes.find((theme: Theme) => theme.name === themeName); - - if (theme) { - localStorage.setItem('theme', themeName); - loadTheme(theme); - - dispatch({ - type: ActionTypes.setTheme, - payload: theme - }) - } -} - -export const loadTheme = (theme: Theme): void => { - for (const [key, value] of Object.entries(theme.colors)) { - document.body.style.setProperty(`--color-${key}`, value); - } -} \ No newline at end of file diff --git a/client/src/store/index.ts b/client/src/store/index.ts new file mode 100644 index 0000000..c823dbc --- /dev/null +++ b/client/src/store/index.ts @@ -0,0 +1,2 @@ +export * from './store'; +export * as actionCreators from './action-creators'; diff --git a/client/src/store/reducers/app.ts b/client/src/store/reducers/app.ts deleted file mode 100644 index 9d4c359..0000000 --- a/client/src/store/reducers/app.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { ActionTypes, Action } from '../actions'; -import { App } from '../../interfaces/App'; -import { sortData } from '../../utility'; - -export interface State { - loading: boolean; - apps: App[]; - errors: string | undefined; -} - -const initialState: State = { - loading: true, - apps: [], - errors: undefined, -}; - -const getApps = (state: State, action: Action): State => { - return { - ...state, - loading: true, - errors: undefined, - }; -}; - -const getAppsSuccess = (state: State, action: Action): State => { - return { - ...state, - loading: false, - apps: action.payload, - }; -}; - -const getAppsError = (state: State, action: Action): State => { - return { - ...state, - loading: false, - errors: action.payload, - }; -}; - -const pinApp = (state: State, action: Action): State => { - const tmpApps = [...state.apps]; - const changedApp = tmpApps.find((app: App) => app.id === action.payload.id); - - if (changedApp) { - changedApp.isPinned = action.payload.isPinned; - } - - return { - ...state, - apps: tmpApps, - }; -}; - -const addAppSuccess = (state: State, action: Action): State => { - return { - ...state, - apps: [...state.apps, action.payload], - }; -}; - -const deleteApp = (state: State, action: Action): State => { - return { - ...state, - apps: [...state.apps].filter((app: App) => app.id !== action.payload), - }; -}; - -const updateApp = (state: State, action: Action): State => { - const appIdx = state.apps.findIndex((app) => app.id === action.payload.id); - - return { - ...state, - apps: [ - ...state.apps.slice(0, appIdx), - { - ...action.payload, - }, - ...state.apps.slice(appIdx + 1), - ], - }; -}; - -const reorderApps = (state: State, action: Action): State => { - return { - ...state, - apps: action.payload, - }; -}; - -const sortApps = (state: State, action: Action): State => { - const sortedApps = sortData(state.apps, action.payload); - - return { - ...state, - apps: sortedApps, - }; -}; - -const appReducer = (state = initialState, action: Action) => { - switch (action.type) { - case ActionTypes.getApps: - return getApps(state, action); - case ActionTypes.getAppsSuccess: - return getAppsSuccess(state, action); - case ActionTypes.getAppsError: - return getAppsError(state, action); - case ActionTypes.pinApp: - return pinApp(state, action); - case ActionTypes.addAppSuccess: - return addAppSuccess(state, action); - case ActionTypes.deleteApp: - return deleteApp(state, action); - case ActionTypes.updateApp: - return updateApp(state, action); - case ActionTypes.reorderApps: - return reorderApps(state, action); - case ActionTypes.sortApps: - return sortApps(state, action); - default: - return state; - } -}; - -export default appReducer; diff --git a/client/src/store/reducers/bookmark.ts b/client/src/store/reducers/bookmark.ts deleted file mode 100644 index a554d6e..0000000 --- a/client/src/store/reducers/bookmark.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { ActionTypes, Action } from '../actions'; -import { Category, Bookmark } from '../../interfaces'; -import { sortData } from '../../utility'; - -export interface State { - loading: boolean; - errors: string | undefined; - categories: Category[]; -} - -const initialState: State = { - loading: true, - errors: undefined, - categories: [] -} - -const getCategories = (state: State, action: Action): State => { - return { - ...state, - loading: true, - errors: undefined - } -} - -const getCategoriesSuccess = (state: State, action: Action): State => { - return { - ...state, - loading: false, - categories: action.payload - } -} - -const addCategory = (state: State, action: Action): State => { - return { - ...state, - categories: [ - ...state.categories, - { - ...action.payload, - bookmarks: [] - } - ] - } -} - -const addBookmark = (state: State, action: Action): State => { - const categoryIndex = state.categories.findIndex((category: Category) => category.id === action.payload.categoryId); - - return { - ...state, - categories: [ - ...state.categories.slice(0, categoryIndex), - { - ...state.categories[categoryIndex], - bookmarks: [ - ...state.categories[categoryIndex].bookmarks, - { - ...action.payload - } - ] - }, - ...state.categories.slice(categoryIndex + 1) - ] - } -} - -const pinCategory = (state: State, action: Action): State => { - const tmpCategories = [...state.categories]; - const changedCategory = tmpCategories.find((category: Category) => category.id === action.payload.id); - - if (changedCategory) { - changedCategory.isPinned = action.payload.isPinned; - } - - return { - ...state, - categories: tmpCategories - } -} - -const deleteCategory = (state: State, action: Action): State => { - const categoryIndex = state.categories.findIndex((category: Category) => category.id === action.payload); - - return { - ...state, - categories: [ - ...state.categories.slice(0, categoryIndex), - ...state.categories.slice(categoryIndex + 1) - ] - } -} - -const updateCategory = (state: State, action: Action): State => { - const tmpCategories = [...state.categories]; - const categoryInUpdate = tmpCategories.find((category: Category) => category.id === action.payload.id); - - if (categoryInUpdate) { - categoryInUpdate.name = action.payload.name; - } - - return { - ...state, - categories: tmpCategories - } -} - -const deleteBookmark = (state: State, action: Action): State => { - const tmpCategories = [...state.categories]; - const categoryInUpdate = tmpCategories.find((category: Category) => category.id === action.payload.categoryId); - - if (categoryInUpdate) { - categoryInUpdate.bookmarks = categoryInUpdate.bookmarks.filter((bookmark: Bookmark) => bookmark.id !== action.payload.bookmarkId); - } - - - return { - ...state, - categories: tmpCategories - } -} - -const updateBookmark = (state: State, action: Action): State => { - let categoryIndex = state.categories.findIndex((category: Category) => category.id === action.payload.categoryId); - let bookmarkIndex = state.categories[categoryIndex].bookmarks.findIndex((bookmark: Bookmark) => bookmark.id === action.payload.id); - - return { - ...state, - categories: [ - ...state.categories.slice(0, categoryIndex), - { - ...state.categories[categoryIndex], - bookmarks: [ - ...state.categories[categoryIndex].bookmarks.slice(0, bookmarkIndex), - { - ...action.payload - }, - ...state.categories[categoryIndex].bookmarks.slice(bookmarkIndex + 1) - ] - }, - ...state.categories.slice(categoryIndex + 1) - ] - } -} - -const sortCategories = (state: State, action: Action): State => { - const sortedCategories = sortData(state.categories, action.payload); - - return { - ...state, - categories: sortedCategories - } -} - -const reorderCategories = (state: State, action: Action): State => { - return { - ...state, - categories: action.payload - } -} - -const bookmarkReducer = (state = initialState, action: Action) => { - switch (action.type) { - case ActionTypes.getCategories: return getCategories(state, action); - case ActionTypes.getCategoriesSuccess: return getCategoriesSuccess(state, action); - case ActionTypes.addCategory: return addCategory(state, action); - case ActionTypes.addBookmark: return addBookmark(state, action); - case ActionTypes.pinCategory: return pinCategory(state, action); - case ActionTypes.deleteCategory: return deleteCategory(state, action); - case ActionTypes.updateCategory: return updateCategory(state, action); - case ActionTypes.deleteBookmark: return deleteBookmark(state, action); - case ActionTypes.updateBookmark: return updateBookmark(state, action); - case ActionTypes.sortCategories: return sortCategories(state, action); - case ActionTypes.reorderCategories: return reorderCategories(state, action); - default: return state; - } -} - -export default bookmarkReducer; \ No newline at end of file diff --git a/client/src/store/reducers/config.ts b/client/src/store/reducers/config.ts index d216316..41c57d1 100644 --- a/client/src/store/reducers/config.ts +++ b/client/src/store/reducers/config.ts @@ -1,79 +1,58 @@ -import { ActionTypes, Action } from '../actions'; +import { Action } from '../actions'; +import { ActionType } from '../action-types'; import { Config, Query } from '../../interfaces'; import { configTemplate } from '../../utility'; -export interface State { +interface ConfigState { loading: boolean; config: Config; customQueries: Query[]; } -const initialState: State = { +const initialState: ConfigState = { loading: true, config: { ...configTemplate }, customQueries: [], }; -const getConfig = (state: State, action: Action): State => { - return { - ...state, - loading: false, - config: action.payload, - }; -}; - -const updateConfig = (state: State, action: Action): State => { - return { - ...state, - config: action.payload, - }; -}; - -const fetchQueries = (state: State, action: Action): State => { - return { - ...state, - customQueries: action.payload, - }; -}; - -const addQuery = (state: State, action: Action): State => { - return { - ...state, - customQueries: [...state.customQueries, action.payload], - }; -}; - -const deleteQuery = (state: State, action: Action): State => { - return { - ...state, - customQueries: action.payload, - }; -}; - -const updateQuery = (state: State, action: Action): State => { - return { - ...state, - customQueries: action.payload, - }; -}; - -const configReducer = (state: State = initialState, action: Action) => { +export const configReducer = ( + state: ConfigState = initialState, + action: Action +): ConfigState => { switch (action.type) { - case ActionTypes.getConfig: - return getConfig(state, action); - case ActionTypes.updateConfig: - return updateConfig(state, action); - case ActionTypes.fetchQueries: - return fetchQueries(state, action); - case ActionTypes.addQuery: - return addQuery(state, action); - case ActionTypes.deleteQuery: - return deleteQuery(state, action); - case ActionTypes.updateQuery: - return updateQuery(state, action); + case ActionType.getConfig: + return { + ...state, + loading: false, + config: action.payload, + }; + case ActionType.updateConfig: + return { + ...state, + config: action.payload, + }; + case ActionType.fetchQueries: + return { + ...state, + customQueries: action.payload, + }; + case ActionType.addQuery: + return { + ...state, + customQueries: [...state.customQueries, action.payload], + }; + case ActionType.deleteQuery: + return { + ...state, + customQueries: action.payload, + }; + case ActionType.updateQuery: + return { + ...state, + customQueries: action.payload, + }; + default: return state; } }; - -export default configReducer; diff --git a/client/src/store/reducers/index.ts b/client/src/store/reducers/index.ts index 96e9f95..f30ccd8 100644 --- a/client/src/store/reducers/index.ts +++ b/client/src/store/reducers/index.ts @@ -1,19 +1,13 @@ import { combineReducers } from 'redux'; -import { GlobalState } from '../../interfaces/GlobalState'; +import { themeReducer } from './theme'; +import { configReducer } from './config'; +import { notificationReducer } from './notification'; -import themeReducer from './theme'; -import appReducer from './app'; -import bookmarkReducer from './bookmark'; -import notificationReducer from './notification'; -import configReducer from './config'; - -const rootReducer = combineReducers({ +export const reducers = combineReducers({ theme: themeReducer, - app: appReducer, - bookmark: bookmarkReducer, + config: configReducer, notification: notificationReducer, - config: configReducer -}) +}); -export default rootReducer; \ No newline at end of file +export type State = ReturnType; diff --git a/client/src/store/reducers/notification.ts b/client/src/store/reducers/notification.ts index c0822b7..544402f 100644 --- a/client/src/store/reducers/notification.ts +++ b/client/src/store/reducers/notification.ts @@ -1,45 +1,42 @@ -import { ActionTypes, Action } from '../actions'; +import { Action } from '../actions'; +import { ActionType } from '../action-types'; import { Notification } from '../../interfaces'; -export interface State { +export interface NotificationState { notifications: Notification[]; idCounter: number; } -const initialState: State = { +const initialState: NotificationState = { notifications: [], - idCounter: 0 -} + idCounter: 0, +}; -const createNotification = (state: State, action: Action): State => { - const tmpNotifications = [...state.notifications, { - ...action.payload, - id: state.idCounter - }]; - - return { - ...state, - notifications: tmpNotifications, - idCounter: state.idCounter + 1 - } -} - -const clearNotification = (state: State, action: Action): State => { - const tmpNotifications = [...state.notifications] - .filter((notification: Notification) => notification.id !== action.payload); - - return { - ...state, - notifications: tmpNotifications - } -} - -const notificationReducer = (state = initialState, action: Action) => { +export const notificationReducer = ( + state: NotificationState = initialState, + action: Action +): NotificationState => { switch (action.type) { - case ActionTypes.createNotification: return createNotification(state, action); - case ActionTypes.clearNotification: return clearNotification(state, action); - default: return state; + case ActionType.createNotification: + return { + ...state, + notifications: [ + ...state.notifications, + { + ...action.payload, + id: state.idCounter, + }, + ], + idCounter: state.idCounter + 1, + }; + case ActionType.clearNotification: + return { + ...state, + notifications: [...state.notifications].filter( + (notification) => notification.id !== action.payload + ), + }; + default: + return state; } -} - -export default notificationReducer; \ No newline at end of file +}; diff --git a/client/src/store/reducers/theme.ts b/client/src/store/reducers/theme.ts index 6adc225..ef32495 100644 --- a/client/src/store/reducers/theme.ts +++ b/client/src/store/reducers/theme.ts @@ -1,11 +1,12 @@ -import { ActionTypes, Action } from '../actions'; +import { Action } from '../actions'; +import { ActionType } from '../action-types'; import { Theme } from '../../interfaces/Theme'; -export interface State { +interface ThemeState { theme: Theme; } -const initialState: State = { +const initialState: ThemeState = { theme: { name: 'tron', colors: { @@ -16,13 +17,14 @@ const initialState: State = { }, }; -const themeReducer = (state = initialState, action: Action) => { +export const themeReducer = ( + state: ThemeState = initialState, + action: Action +): ThemeState => { switch (action.type) { - case ActionTypes.setTheme: + case ActionType.setTheme: return { theme: action.payload }; default: return state; } }; - -export default themeReducer; diff --git a/client/src/store/store.ts b/client/src/store/store.ts index 22250a7..263407a 100644 --- a/client/src/store/store.ts +++ b/client/src/store/store.ts @@ -1,7 +1,10 @@ import { createStore, applyMiddleware } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; import thunk from 'redux-thunk'; -import rootReducer from './reducers'; -const initialState = {}; +import { reducers } from './reducers'; -export const store = createStore(rootReducer, initialState, composeWithDevTools(applyMiddleware(thunk))); \ No newline at end of file +export const store = createStore( + reducers, + {}, + composeWithDevTools(applyMiddleware(thunk)) +); From adc017c48d7f684f70dec19698c9154953e2c1e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Tue, 9 Nov 2021 13:19:53 +0100 Subject: [PATCH 05/30] App state: refactored reducers and actions for apps, categories and bookmarks --- client/src/store/action-creators/app.ts | 170 +++++++++++ client/src/store/action-creators/bookmark.ts | 289 +++++++++++++++++++ client/src/store/action-creators/index.ts | 2 + client/src/store/action-types/index.ts | 26 ++ client/src/store/actions/app.ts | 38 +++ client/src/store/actions/bookmark.ts | 58 ++++ client/src/store/actions/index.ts | 49 +++- client/src/store/reducers/app.ts | 92 ++++++ client/src/store/reducers/bookmark.ts | 166 +++++++++++ client/src/store/reducers/index.ts | 4 + 10 files changed, 893 insertions(+), 1 deletion(-) create mode 100644 client/src/store/action-creators/app.ts create mode 100644 client/src/store/action-creators/bookmark.ts create mode 100644 client/src/store/actions/app.ts create mode 100644 client/src/store/actions/bookmark.ts create mode 100644 client/src/store/reducers/app.ts create mode 100644 client/src/store/reducers/bookmark.ts diff --git a/client/src/store/action-creators/app.ts b/client/src/store/action-creators/app.ts new file mode 100644 index 0000000..6f1b505 --- /dev/null +++ b/client/src/store/action-creators/app.ts @@ -0,0 +1,170 @@ +import { ActionType } from '../action-types'; +import { Dispatch } from 'redux'; +import { ApiResponse, App, Config, NewApp } from '../../interfaces'; +import { + AddAppAction, + DeleteAppAction, + GetAppsAction, + PinAppAction, + ReorderAppsAction, + SortAppsAction, + UpdateAppAction, +} from '../actions/app'; +import axios from 'axios'; +import { createNotification } from '.'; + +export const getApps = + () => async (dispatch: Dispatch>) => { + dispatch({ + type: ActionType.getApps, + payload: undefined, + }); + + try { + const res = await axios.get>('/api/apps'); + + dispatch({ + type: ActionType.getAppsSuccess, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export const pinApp = + (app: App) => async (dispatch: Dispatch) => { + try { + const { id, isPinned, name } = app; + const res = await axios.put>(`/api/apps/${id}`, { + isPinned: !isPinned, + }); + + const status = isPinned + ? 'unpinned from Homescreen' + : 'pinned to Homescreen'; + + createNotification({ + title: 'Success', + message: `App ${name} ${status}`, + }); + + dispatch({ + type: ActionType.pinApp, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export const addApp = + (formData: NewApp | FormData) => async (dispatch: Dispatch) => { + try { + const res = await axios.post>('/api/apps', formData); + + createNotification({ + title: 'Success', + message: `App added`, + }); + + await dispatch({ + type: ActionType.addAppSuccess, + payload: res.data.data, + }); + + // Sort apps + // dispatch(sortApps()); + sortApps(); + } catch (err) { + console.log(err); + } + }; + +export const deleteApp = + (id: number) => async (dispatch: Dispatch) => { + try { + await axios.delete>(`/api/apps/${id}`); + + createNotification({ + title: 'Success', + message: 'App deleted', + }); + + dispatch({ + type: ActionType.deleteApp, + payload: id, + }); + } catch (err) { + console.log(err); + } + }; + +export const updateApp = + (id: number, formData: NewApp | FormData) => + async (dispatch: Dispatch) => { + try { + const res = await axios.put>( + `/api/apps/${id}`, + formData + ); + + createNotification({ + title: 'Success', + message: `App updated`, + }); + + await dispatch({ + type: ActionType.updateApp, + payload: res.data.data, + }); + + // Sort apps + dispatch(sortApps()); + } catch (err) { + console.log(err); + } + }; + +export const reorderApps = + (apps: App[]) => async (dispatch: Dispatch) => { + interface ReorderQuery { + apps: { + id: number; + orderId: number; + }[]; + } + + try { + const updateQuery: ReorderQuery = { apps: [] }; + + apps.forEach((app, index) => + updateQuery.apps.push({ + id: app.id, + orderId: index + 1, + }) + ); + + await axios.put>('/api/apps/0/reorder', updateQuery); + + dispatch({ + type: ActionType.reorderApps, + payload: apps, + }); + } catch (err) { + console.log(err); + } + }; + +export const sortApps = () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/config'); + + dispatch({ + type: ActionType.sortApps, + payload: res.data.data.useOrdering, + }); + } catch (err) { + console.log(err); + } +}; diff --git a/client/src/store/action-creators/bookmark.ts b/client/src/store/action-creators/bookmark.ts new file mode 100644 index 0000000..ac7c420 --- /dev/null +++ b/client/src/store/action-creators/bookmark.ts @@ -0,0 +1,289 @@ +import axios from 'axios'; +import { Dispatch } from 'redux'; +import { createNotification } from '.'; +import { + ApiResponse, + Bookmark, + Category, + Config, + NewBookmark, + NewCategory, +} from '../../interfaces'; +import { ActionType } from '../action-types'; +import { + AddBookmarkAction, + AddCategoryAction, + DeleteBookmarkAction, + DeleteCategoryAction, + GetCategoriesAction, + PinCategoryAction, + ReorderCategoriesAction, + SortCategoriesAction, + UpdateBookmarkAction, + UpdateCategoryAction, +} from '../actions/bookmark'; + +export const getCategories = + () => + async (dispatch: Dispatch>) => { + dispatch({ + type: ActionType.getCategories, + payload: undefined, + }); + + try { + const res = await axios.get>('/api/categories'); + + dispatch({ + type: ActionType.getCategoriesSuccess, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export const addCategory = + (formData: NewCategory) => async (dispatch: Dispatch) => { + try { + const res = await axios.post>( + '/api/categories', + formData + ); + + createNotification({ + title: 'Success', + message: `Category ${formData.name} created`, + }); + + dispatch({ + type: ActionType.addCategory, + payload: res.data.data, + }); + + // dispatch(sortCategories()); + sortCategories(); + } catch (err) { + console.log(err); + } + }; + +export const addBookmark = + (formData: NewBookmark | FormData) => + async (dispatch: Dispatch) => { + try { + const res = await axios.post>( + '/api/bookmarks', + formData + ); + + createNotification({ + title: 'Success', + message: `Bookmark created`, + }); + + dispatch({ + type: ActionType.addBookmark, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export const pinCategory = + (category: Category) => async (dispatch: Dispatch) => { + try { + const { id, isPinned, name } = category; + const res = await axios.put>( + `/api/categories/${id}`, + { isPinned: !isPinned } + ); + + const status = isPinned + ? 'unpinned from Homescreen' + : 'pinned to Homescreen'; + + createNotification({ + title: 'Success', + message: `Category ${name} ${status}`, + }); + + dispatch({ + type: ActionType.pinCategory, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export const deleteCategory = + (id: number) => async (dispatch: Dispatch) => { + try { + await axios.delete>(`/api/categories/${id}`); + + createNotification({ + title: 'Success', + message: `Category deleted`, + }); + + dispatch({ + type: ActionType.deleteCategory, + payload: id, + }); + } catch (err) { + console.log(err); + } + }; + +export const updateCategory = + (id: number, formData: NewCategory) => + async (dispatch: Dispatch) => { + try { + const res = await axios.put>( + `/api/categories/${id}`, + formData + ); + createNotification({ + title: 'Success', + message: `Category ${formData.name} updated`, + }); + + dispatch({ + type: ActionType.updateCategory, + payload: res.data.data, + }); + + // dispatch(sortCategories()); + sortCategories(); + } catch (err) { + console.log(err); + } + }; + +export const deleteBookmark = + (bookmarkId: number, categoryId: number) => + async (dispatch: Dispatch) => { + try { + await axios.delete>(`/api/bookmarks/${bookmarkId}`); + + createNotification({ + title: 'Success', + message: 'Bookmark deleted', + }); + + dispatch({ + type: ActionType.deleteBookmark, + payload: { + bookmarkId, + categoryId, + }, + }); + } catch (err) { + console.log(err); + } + }; + +export const updateBookmark = + ( + bookmarkId: number, + formData: NewBookmark | FormData, + category: { + prev: number; + curr: number; + } + ) => + async ( + dispatch: Dispatch< + DeleteBookmarkAction | AddBookmarkAction | UpdateBookmarkAction + > + ) => { + try { + const res = await axios.put>( + `/api/bookmarks/${bookmarkId}`, + formData + ); + + createNotification({ + title: 'Success', + message: `Bookmark updated`, + }); + + // Check if category was changed + const categoryWasChanged = category.curr !== category.prev; + + if (categoryWasChanged) { + // Delete bookmark from old category + dispatch({ + type: ActionType.deleteBookmark, + payload: { + bookmarkId, + categoryId: category.prev, + }, + }); + + // Add bookmark to the new category + dispatch({ + type: ActionType.addBookmark, + payload: res.data.data, + }); + } else { + // Else update only name/url/icon + dispatch({ + type: ActionType.updateBookmark, + payload: res.data.data, + }); + } + } catch (err) { + console.log(err); + } + }; + +export const sortCategories = + () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/config'); + + dispatch({ + type: ActionType.sortCategories, + payload: res.data.data.useOrdering, + }); + } catch (err) { + console.log(err); + } + }; + +export const reorderCategories = + (categories: Category[]) => + async (dispatch: Dispatch) => { + interface ReorderQuery { + categories: { + id: number; + orderId: number; + }[]; + } + + try { + const updateQuery: ReorderQuery = { categories: [] }; + + categories.forEach((category, index) => + updateQuery.categories.push({ + id: category.id, + orderId: index + 1, + }) + ); + + await axios.put>( + '/api/categories/0/reorder', + updateQuery + ); + + dispatch({ + type: ActionType.reorderCategories, + payload: categories, + }); + } catch (err) { + console.log(err); + } + }; diff --git a/client/src/store/action-creators/index.ts b/client/src/store/action-creators/index.ts index d7b5e45..ae038b0 100644 --- a/client/src/store/action-creators/index.ts +++ b/client/src/store/action-creators/index.ts @@ -1,3 +1,5 @@ export * from './theme'; export * from './config'; export * from './notification'; +export * from './app'; +export * from './bookmark'; diff --git a/client/src/store/action-types/index.ts b/client/src/store/action-types/index.ts index eb32118..ab7d5ef 100644 --- a/client/src/store/action-types/index.ts +++ b/client/src/store/action-types/index.ts @@ -4,6 +4,7 @@ export enum ActionType { // CONFIG getConfig = 'GET_CONFIG', updateConfig = 'UPDATE_CONFIG', + // QUERIES addQuery = 'ADD_QUERY', deleteQuery = 'DELETE_QUERY', fetchQueries = 'FETCH_QUERIES', @@ -11,4 +12,29 @@ export enum ActionType { // NOTIFICATIONS createNotification = 'CREATE_NOTIFICATION', clearNotification = 'CLEAR_NOTIFICATION', + // APPS + getApps = 'GET_APPS', + getAppsSuccess = 'GET_APPS_SUCCESS', + getAppsError = 'GET_APPS_ERROR', + pinApp = 'PIN_APP', + addApp = 'ADD_APP', + addAppSuccess = 'ADD_APP_SUCCESS', + deleteApp = 'DELETE_APP', + updateApp = 'UPDATE_APP', + reorderApps = 'REORDER_APPS', + sortApps = 'SORT_APPS', + // CATEGORES + getCategories = 'GET_CATEGORIES', + getCategoriesSuccess = 'GET_CATEGORIES_SUCCESS', + getCategoriesError = 'GET_CATEGORIES_ERROR', + addCategory = 'ADD_CATEGORY', + pinCategory = 'PIN_CATEGORY', + deleteCategory = 'DELETE_CATEGORY', + updateCategory = 'UPDATE_CATEGORY', + sortCategories = 'SORT_CATEGORIES', + reorderCategories = 'REORDER_CATEGORIES', + // BOOKMARKS + addBookmark = 'ADD_BOOKMARK', + deleteBookmark = 'DELETE_BOOKMARK', + updateBookmark = 'UPDATE_BOOKMARK', } diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts new file mode 100644 index 0000000..37f5419 --- /dev/null +++ b/client/src/store/actions/app.ts @@ -0,0 +1,38 @@ +import { ActionType } from '../action-types'; +import { App } from '../../interfaces'; + +export interface GetAppsAction { + type: + | ActionType.getApps + | ActionType.getAppsSuccess + | ActionType.getAppsError; + payload: T; +} +export interface PinAppAction { + type: ActionType.pinApp; + payload: App; +} + +export interface AddAppAction { + type: ActionType.addAppSuccess; + payload: App; +} +export interface DeleteAppAction { + type: ActionType.deleteApp; + payload: number; +} + +export interface UpdateAppAction { + type: ActionType.updateApp; + payload: App; +} + +export interface ReorderAppsAction { + type: ActionType.reorderApps; + payload: App[]; +} + +export interface SortAppsAction { + type: ActionType.sortApps; + payload: string; +} diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts new file mode 100644 index 0000000..e4cfcfd --- /dev/null +++ b/client/src/store/actions/bookmark.ts @@ -0,0 +1,58 @@ +import { Bookmark, Category } from '../../interfaces'; +import { ActionType } from '../action-types'; + +export interface GetCategoriesAction { + type: + | ActionType.getCategories + | ActionType.getCategoriesSuccess + | ActionType.getCategoriesError; + payload: T; +} + +export interface AddCategoryAction { + type: ActionType.addCategory; + payload: Category; +} + +export interface AddBookmarkAction { + type: ActionType.addBookmark; + payload: Bookmark; +} + +export interface PinCategoryAction { + type: ActionType.pinCategory; + payload: Category; +} + +export interface DeleteCategoryAction { + type: ActionType.deleteCategory; + payload: number; +} + +export interface UpdateCategoryAction { + type: ActionType.updateCategory; + payload: Category; +} + +export interface DeleteBookmarkAction { + type: ActionType.deleteBookmark; + payload: { + bookmarkId: number; + categoryId: number; + }; +} + +export interface UpdateBookmarkAction { + type: ActionType.updateBookmark; + payload: Bookmark; +} + +export interface SortCategoriesAction { + type: ActionType.sortCategories; + payload: string; +} + +export interface ReorderCategoriesAction { + type: ActionType.reorderCategories; + payload: Category[]; +} diff --git a/client/src/store/actions/index.ts b/client/src/store/actions/index.ts index d6bea13..af999a6 100644 --- a/client/src/store/actions/index.ts +++ b/client/src/store/actions/index.ts @@ -1,4 +1,5 @@ import { SetThemeAction } from './theme'; + import { AddQueryAction, DeleteQueryAction, @@ -7,11 +8,37 @@ import { UpdateConfigAction, UpdateQueryAction, } from './config'; + import { ClearNotificationAction, CreateNotificationAction, } from './notification'; +import { + GetAppsAction, + PinAppAction, + AddAppAction, + DeleteAppAction, + UpdateAppAction, + ReorderAppsAction, + SortAppsAction, +} from './app'; + +import { App } from '../../interfaces'; + +import { + GetCategoriesAction, + AddCategoryAction, + PinCategoryAction, + DeleteCategoryAction, + UpdateCategoryAction, + SortCategoriesAction, + ReorderCategoriesAction, + AddBookmarkAction, + DeleteBookmarkAction, + UpdateBookmarkAction, +} from './bookmark'; + export type Action = // Theme | SetThemeAction @@ -24,4 +51,24 @@ export type Action = | UpdateQueryAction // Notifications | CreateNotificationAction - | ClearNotificationAction; + | ClearNotificationAction + // Apps + | GetAppsAction + | PinAppAction + | AddAppAction + | DeleteAppAction + | UpdateAppAction + | ReorderAppsAction + | SortAppsAction + // Categories + | GetCategoriesAction + | AddCategoryAction + | PinCategoryAction + | DeleteCategoryAction + | UpdateCategoryAction + | SortCategoriesAction + | ReorderCategoriesAction + // Bookmarks + | AddBookmarkAction + | DeleteBookmarkAction + | UpdateBookmarkAction; diff --git a/client/src/store/reducers/app.ts b/client/src/store/reducers/app.ts new file mode 100644 index 0000000..e6da902 --- /dev/null +++ b/client/src/store/reducers/app.ts @@ -0,0 +1,92 @@ +import { ActionType } from '../action-types'; +import { Action } from '../actions/index'; +import { App } from '../../interfaces'; +import { sortData } from '../../utility'; + +interface AppsState { + loading: boolean; + apps: App[]; + errors: string | undefined; +} + +const initialState: AppsState = { + loading: true, + apps: [], + errors: undefined, +}; + +export const appsReducer = ( + state: AppsState = initialState, + action: Action +): AppsState => { + switch (action.type) { + case ActionType.getApps: + return { + ...state, + loading: true, + errors: undefined, + }; + + case ActionType.getAppsSuccess: + return { + ...state, + loading: false, + apps: action.payload || [], + }; + + case ActionType.pinApp: + const pinnedAppIdx = state.apps.findIndex( + (app) => app.id === action.payload.id + ); + + return { + ...state, + apps: [ + ...state.apps.slice(0, pinnedAppIdx), + action.payload, + ...state.apps.slice(pinnedAppIdx + 1), + ], + }; + + case ActionType.addAppSuccess: + return { + ...state, + apps: [...state.apps, action.payload], + }; + + case ActionType.deleteApp: + return { + ...state, + apps: [...state.apps].filter((app) => app.id !== action.payload), + }; + + case ActionType.updateApp: + const updatedAppIdx = state.apps.findIndex( + (app) => app.id === action.payload.id + ); + + return { + ...state, + apps: [ + ...state.apps.slice(0, updatedAppIdx), + action.payload, + ...state.apps.slice(updatedAppIdx + 1), + ], + }; + + case ActionType.reorderApps: + return { + ...state, + apps: action.payload, + }; + + case ActionType.sortApps: + return { + ...state, + apps: sortData(state.apps, action.payload), + }; + + default: + return state; + } +}; diff --git a/client/src/store/reducers/bookmark.ts b/client/src/store/reducers/bookmark.ts new file mode 100644 index 0000000..ed4cea2 --- /dev/null +++ b/client/src/store/reducers/bookmark.ts @@ -0,0 +1,166 @@ +import { Category } from '../../interfaces'; +import { sortData } from '../../utility'; +import { ActionType } from '../action-types'; +import { Action } from '../actions'; + +interface BookmarksState { + loading: boolean; + errors: string | undefined; + categories: Category[]; +} + +const initialState: BookmarksState = { + loading: true, + errors: undefined, + categories: [], +}; + +export const bookmarksReducer = ( + state: BookmarksState = initialState, + action: Action +): BookmarksState => { + switch (action.type) { + case ActionType.getCategories: + return { + ...state, + loading: true, + errors: undefined, + }; + + case ActionType.getCategoriesSuccess: + return { + ...state, + loading: false, + categories: action.payload, + }; + + case ActionType.addCategory: + return { + ...state, + categories: [...state.categories, { ...action.payload, bookmarks: [] }], + }; + + case ActionType.addBookmark: + const categoryIdx = state.categories.findIndex( + (category) => category.id === action.payload.categoryId + ); + + return { + ...state, + categories: [ + ...state.categories.slice(0, categoryIdx), + { + ...state.categories[categoryIdx], + bookmarks: [ + ...state.categories[categoryIdx].bookmarks, + action.payload, + ], + }, + ...state.categories.slice(categoryIdx + 1), + ], + }; + + case ActionType.pinCategory: + const pinnedCategoryIdx = state.categories.findIndex( + (category) => category.id === action.payload.id + ); + + return { + ...state, + categories: [ + ...state.categories.slice(0, pinnedCategoryIdx), + action.payload, + ...state.categories.slice(pinnedCategoryIdx + 1), + ], + }; + + case ActionType.deleteCategory: + const deletedCategoryIdx = state.categories.findIndex( + (category) => category.id === action.payload + ); + + return { + ...state, + categories: [ + ...state.categories.slice(0, deletedCategoryIdx), + ...state.categories.slice(deletedCategoryIdx + 1), + ], + }; + + case ActionType.updateCategory: + const updatedCategoryIdx = state.categories.findIndex( + (category) => category.id === action.payload.id + ); + + return { + ...state, + categories: [ + ...state.categories.slice(0, updatedCategoryIdx), + action.payload, + ...state.categories.slice(updatedCategoryIdx + 1), + ], + }; + + case ActionType.deleteBookmark: + const categoryInUpdateIdx = state.categories.findIndex( + (category) => category.id === action.payload.categoryId + ); + + return { + ...state, + categories: [ + ...state.categories.slice(0, categoryInUpdateIdx), + { + ...state.categories[categoryInUpdateIdx], + bookmarks: state.categories[categoryInUpdateIdx].bookmarks.filter( + (bookmark) => bookmark.id !== action.payload.bookmarkId + ), + }, + ...state.categories.slice(categoryInUpdateIdx + 1), + ], + }; + + case ActionType.updateBookmark: + const parentCategoryIdx = state.categories.findIndex( + (category) => category.id === action.payload.categoryId + ); + const updatedBookmarkIdx = state.categories[ + parentCategoryIdx + ].bookmarks.findIndex((bookmark) => bookmark.id === action.payload.id); + + return { + ...state, + categories: [ + ...state.categories.slice(0, parentCategoryIdx), + { + ...state.categories[parentCategoryIdx], + bookmarks: [ + ...state.categories[parentCategoryIdx].bookmarks.slice( + 0, + updatedBookmarkIdx + ), + action.payload, + ...state.categories[parentCategoryIdx].bookmarks.slice( + updatedBookmarkIdx + 1 + ), + ], + }, + ...state.categories.slice(parentCategoryIdx + 1), + ], + }; + + case ActionType.sortCategories: + return { + ...state, + categories: sortData(state.categories, action.payload), + }; + + case ActionType.reorderCategories: + return { + ...state, + categories: action.payload, + }; + default: + return state; + } +}; diff --git a/client/src/store/reducers/index.ts b/client/src/store/reducers/index.ts index f30ccd8..1eed183 100644 --- a/client/src/store/reducers/index.ts +++ b/client/src/store/reducers/index.ts @@ -3,11 +3,15 @@ import { combineReducers } from 'redux'; import { themeReducer } from './theme'; import { configReducer } from './config'; import { notificationReducer } from './notification'; +import { appsReducer } from './app'; +import { bookmarksReducer } from './bookmark'; export const reducers = combineReducers({ theme: themeReducer, config: configReducer, notification: notificationReducer, + apps: appsReducer, + bookmarks: bookmarksReducer, }); export type State = ReturnType; From 89d935e27fa290c43a62de2ef3f162a27642a430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Tue, 9 Nov 2021 13:46:07 +0100 Subject: [PATCH 06/30] Components: refactored UI components to use new state. Minor changes to exports and props --- .../UI/Buttons/ActionButton/ActionButton.tsx | 34 +++++++------------ .../components/UI/Buttons/Button/Button.tsx | 17 ++++------ .../UI/Forms/InputGroup/InputGroup.tsx | 17 ++++------ .../UI/Forms/ModalForm/ModalForm.tsx | 22 +++++------- .../UI/Headlines/Headline/Headline.tsx | 18 +++++----- .../SectionHeadline/SectionHeadline.tsx | 12 +++---- .../SettingsHeadline/SettingsHeadline.tsx | 4 +-- .../components/UI/Icons/Icon/Icon.module.css | 4 +-- client/src/components/UI/Icons/Icon/Icon.tsx | 10 +++--- .../UI/Icons/WeatherIcon/WeatherIcon.tsx | 29 ++++++---------- client/src/components/UI/Layout/Layout.tsx | 11 +++--- client/src/components/UI/Modal/Modal.tsx | 21 ++++++------ .../UI/Notification/Notification.tsx | 21 ++++++------ client/src/components/UI/Spinner/Spinner.tsx | 8 ++--- client/src/components/UI/Table/Table.tsx | 22 ++++++------ client/src/components/UI/index.ts | 14 ++++++++ 16 files changed, 118 insertions(+), 146 deletions(-) create mode 100644 client/src/components/UI/index.ts diff --git a/client/src/components/UI/Buttons/ActionButton/ActionButton.tsx b/client/src/components/UI/Buttons/ActionButton/ActionButton.tsx index 3e99935..8100271 100644 --- a/client/src/components/UI/Buttons/ActionButton/ActionButton.tsx +++ b/client/src/components/UI/Buttons/ActionButton/ActionButton.tsx @@ -2,55 +2,45 @@ import { Fragment } from 'react'; import { Link } from 'react-router-dom'; import classes from './ActionButton.module.css'; -import Icon from '../../Icons/Icon/Icon'; +import { Icon } from '../..'; -interface ComponentProps { +interface Props { name: string; icon: string; link?: string; handler?: () => void; } -const ActionButton = (props: ComponentProps): JSX.Element => { +export const ActionButton = (props: Props): JSX.Element => { const body = (
-
- {props.name} -
+
{props.name}
); if (props.link) { return ( - + {body} - ) + ); } else if (props.handler) { return (
{ - if (e.key === 'Enter' && props.handler) props.handler() + if (e.key === 'Enter' && props.handler) props.handler(); }} tabIndex={0} - >{body} -
- ) - } else { - return ( -
+ > {body}
- ) + ); + } else { + return
{body}
; } -} - -export default ActionButton; \ No newline at end of file +}; diff --git a/client/src/components/UI/Buttons/Button/Button.tsx b/client/src/components/UI/Buttons/Button/Button.tsx index 5f113d8..5e9e9d5 100644 --- a/client/src/components/UI/Buttons/Button/Button.tsx +++ b/client/src/components/UI/Buttons/Button/Button.tsx @@ -1,21 +1,16 @@ import classes from './Button.module.css'; -interface ComponentProps { +interface Props { children: string; click?: any; } -const Button = (props: ComponentProps): JSX.Element => { - const { - children, - click - } = props; +export const Button = (props: Props): JSX.Element => { + const { children, click } = props; return ( - - ) -} - -export default Button; \ No newline at end of file + ); +}; diff --git a/client/src/components/UI/Forms/InputGroup/InputGroup.tsx b/client/src/components/UI/Forms/InputGroup/InputGroup.tsx index d39f139..409af27 100644 --- a/client/src/components/UI/Forms/InputGroup/InputGroup.tsx +++ b/client/src/components/UI/Forms/InputGroup/InputGroup.tsx @@ -1,15 +1,10 @@ +import { ReactNode } from 'react'; import classes from './InputGroup.module.css'; -interface ComponentProps { - children: JSX.Element | JSX.Element[]; +interface Props { + children: ReactNode; } -const InputGroup = (props: ComponentProps): JSX.Element => { - return ( -
- {props.children} -
- ) -} - -export default InputGroup; \ No newline at end of file +export const InputGroup = (props: Props): JSX.Element => { + return
{props.children}
; +}; diff --git a/client/src/components/UI/Forms/ModalForm/ModalForm.tsx b/client/src/components/UI/Forms/ModalForm/ModalForm.tsx index 7ab2eac..0db23b5 100644 --- a/client/src/components/UI/Forms/ModalForm/ModalForm.tsx +++ b/client/src/components/UI/Forms/ModalForm/ModalForm.tsx @@ -1,31 +1,27 @@ -import { SyntheticEvent } from 'react'; +import { ReactNode, SyntheticEvent } from 'react'; import classes from './ModalForm.module.css'; -import Icon from '../../Icons/Icon/Icon'; +import { Icon } from '../..'; interface ComponentProps { - children: JSX.Element | JSX.Element[]; + children: ReactNode; modalHandler?: () => void; formHandler: (e: SyntheticEvent) => void; } -const ModalForm = (props: ComponentProps): JSX.Element => { +export const ModalForm = (props: ComponentProps): JSX.Element => { const _modalHandler = (): void => { if (props.modalHandler) { props.modalHandler(); } - } + }; return (
- +
-
props.formHandler(e)}> - {props.children} -
+
props.formHandler(e)}>{props.children}
- ) -} - -export default ModalForm; \ No newline at end of file + ); +}; diff --git a/client/src/components/UI/Headlines/Headline/Headline.tsx b/client/src/components/UI/Headlines/Headline/Headline.tsx index bb70b65..8178e5e 100644 --- a/client/src/components/UI/Headlines/Headline/Headline.tsx +++ b/client/src/components/UI/Headlines/Headline/Headline.tsx @@ -1,18 +1,18 @@ -import { Fragment } from 'react'; +import { Fragment, ReactNode } from 'react'; import classes from './Headline.module.css'; -interface ComponentProps { +interface Props { title: string; - subtitle?: string | JSX.Element; + subtitle?: ReactNode; } -const Headline = (props: ComponentProps): JSX.Element => { +export const Headline = (props: Props): JSX.Element => { return (

{props.title}

- {props.subtitle &&

{props.subtitle}

} + {props.subtitle && ( +

{props.subtitle}

+ )}
- ) -} - -export default Headline; \ No newline at end of file + ); +}; diff --git a/client/src/components/UI/Headlines/SectionHeadline/SectionHeadline.tsx b/client/src/components/UI/Headlines/SectionHeadline/SectionHeadline.tsx index 8fd2a19..474dd52 100644 --- a/client/src/components/UI/Headlines/SectionHeadline/SectionHeadline.tsx +++ b/client/src/components/UI/Headlines/SectionHeadline/SectionHeadline.tsx @@ -2,17 +2,15 @@ import { Link } from 'react-router-dom'; import classes from './SectionHeadline.module.css'; -interface ComponentProps { +interface Props { title: string; - link: string + link: string; } -const SectionHeadline = (props: ComponentProps): JSX.Element => { +export const SectionHeadline = (props: Props): JSX.Element => { return (

{props.title}

- ) -} - -export default SectionHeadline; \ No newline at end of file + ); +}; diff --git a/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx index 5d14949..b3a005e 100644 --- a/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx +++ b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx @@ -4,8 +4,6 @@ interface Props { text: string; } -const SettingsHeadline = (props: Props): JSX.Element => { +export const SettingsHeadline = (props: Props): JSX.Element => { return

{props.text}

; }; - -export default SettingsHeadline; diff --git a/client/src/components/UI/Icons/Icon/Icon.module.css b/client/src/components/UI/Icons/Icon/Icon.module.css index 33b3aa7..ffc847e 100644 --- a/client/src/components/UI/Icons/Icon/Icon.module.css +++ b/client/src/components/UI/Icons/Icon/Icon.module.css @@ -1,6 +1,4 @@ .Icon { color: var(--color-primary); - /* for settings */ - /* color: var(--color-background); */ width: 90%; -} \ No newline at end of file +} diff --git a/client/src/components/UI/Icons/Icon/Icon.tsx b/client/src/components/UI/Icons/Icon/Icon.tsx index 6924086..50da4d8 100644 --- a/client/src/components/UI/Icons/Icon/Icon.tsx +++ b/client/src/components/UI/Icons/Icon/Icon.tsx @@ -2,12 +2,12 @@ import classes from './Icon.module.css'; import { Icon as MDIcon } from '@mdi/react'; -interface ComponentProps { +interface Props { icon: string; color?: string; } -const Icon = (props: ComponentProps): JSX.Element => { +export const Icon = (props: Props): JSX.Element => { const MDIcons = require('@mdi/js'); let iconPath = MDIcons[props.icon]; @@ -22,7 +22,5 @@ const Icon = (props: ComponentProps): JSX.Element => { path={iconPath} color={props.color ? props.color : 'var(--color-primary)'} /> - ) -} - -export default Icon; \ No newline at end of file + ); +}; diff --git a/client/src/components/UI/Icons/WeatherIcon/WeatherIcon.tsx b/client/src/components/UI/Icons/WeatherIcon/WeatherIcon.tsx index 111967e..2664b47 100644 --- a/client/src/components/UI/Icons/WeatherIcon/WeatherIcon.tsx +++ b/client/src/components/UI/Icons/WeatherIcon/WeatherIcon.tsx @@ -1,39 +1,32 @@ import { useEffect } from 'react'; -import { connect } from 'react-redux'; +import { useSelector } from 'react-redux'; import { Skycons } from 'skycons-ts'; -import { GlobalState, Theme } from '../../../../interfaces'; +import { State } from '../../../../store/reducers'; import { IconMapping, TimeOfDay } from './IconMapping'; -interface ComponentProps { - theme: Theme; +interface Props { weatherStatusCode: number; isDay: number; } -const WeatherIcon = (props: ComponentProps): JSX.Element => { +export const WeatherIcon = (props: Props): JSX.Element => { + const { theme } = useSelector((state: State) => state.theme); + const icon = props.isDay ? new IconMapping().mapIcon(props.weatherStatusCode, TimeOfDay.day) : new IconMapping().mapIcon(props.weatherStatusCode, TimeOfDay.night); useEffect(() => { const delay = setTimeout(() => { - const skycons = new Skycons({'color': props.theme.colors.accent}); + const skycons = new Skycons({ color: theme.colors.accent }); skycons.add(`weather-icon`, icon); skycons.play(); }, 1); return () => { clearTimeout(delay); - } - }, [props.weatherStatusCode, icon, props.theme.colors.accent]); + }; + }, [props.weatherStatusCode, icon, theme.colors.accent]); - return -} - -const mapStateToProps = (state: GlobalState) => { - return { - theme: state.theme.theme - } -} - -export default connect(mapStateToProps)(WeatherIcon); \ No newline at end of file + return ; +}; diff --git a/client/src/components/UI/Layout/Layout.tsx b/client/src/components/UI/Layout/Layout.tsx index b7fe50f..8588ef1 100644 --- a/client/src/components/UI/Layout/Layout.tsx +++ b/client/src/components/UI/Layout/Layout.tsx @@ -1,13 +1,10 @@ +import { ReactNode } from 'react'; import classes from './Layout.module.css'; interface ComponentProps { - children: JSX.Element | JSX.Element[]; + children: ReactNode; } export const Container = (props: ComponentProps): JSX.Element => { - return ( -
- {props.children} -
- ) -} \ No newline at end of file + return
{props.children}
; +}; diff --git a/client/src/components/UI/Modal/Modal.tsx b/client/src/components/UI/Modal/Modal.tsx index ccb82be..43fb5e9 100644 --- a/client/src/components/UI/Modal/Modal.tsx +++ b/client/src/components/UI/Modal/Modal.tsx @@ -1,28 +1,29 @@ -import { MouseEvent, useRef } from 'react'; +import { MouseEvent, ReactNode, useRef } from 'react'; import classes from './Modal.module.css'; -interface ComponentProps { +interface Props { isOpen: boolean; setIsOpen: Function; - children: JSX.Element; + children: ReactNode; } -const Modal = (props: ComponentProps): JSX.Element => { +export const Modal = (props: Props): JSX.Element => { const modalRef = useRef(null); - const modalClasses = [classes.Modal, props.isOpen ? classes.ModalOpen : classes.ModalClose].join(' '); + const modalClasses = [ + classes.Modal, + props.isOpen ? classes.ModalOpen : classes.ModalClose, + ].join(' '); const clickHandler = (e: MouseEvent) => { if (e.target === modalRef.current) { props.setIsOpen(false); } - } + }; return (
{props.children}
- ) -} - -export default Modal; \ No newline at end of file + ); +}; diff --git a/client/src/components/UI/Notification/Notification.tsx b/client/src/components/UI/Notification/Notification.tsx index 2bd5185..c03be54 100644 --- a/client/src/components/UI/Notification/Notification.tsx +++ b/client/src/components/UI/Notification/Notification.tsx @@ -1,18 +1,21 @@ import { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; -import { clearNotification } from '../../../store/actions'; +import { useDispatch } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../store'; import classes from './Notification.module.css'; -interface ComponentProps { +interface Props { title: string; message: string; id: number; url: string | null; - clearNotification: (id: number) => void; } -const Notification = (props: ComponentProps): JSX.Element => { +export const Notification = (props: Props): JSX.Element => { + const dispatch = useDispatch(); + const { clearNotification } = bindActionCreators(actionCreators, dispatch); + const [isOpen, setIsOpen] = useState(true); const elementClasses = [ classes.Notification, @@ -24,13 +27,13 @@ const Notification = (props: ComponentProps): JSX.Element => { setIsOpen(false); }, 3500); - const clearNotification = setTimeout(() => { - props.clearNotification(props.id); + const clearNotificationTimeout = setTimeout(() => { + clearNotification(props.id); }, 3600); return () => { window.clearTimeout(closeNotification); - window.clearTimeout(clearNotification); + window.clearTimeout(clearNotificationTimeout); }; }, []); @@ -48,5 +51,3 @@ const Notification = (props: ComponentProps): JSX.Element => {
); }; - -export default connect(null, { clearNotification })(Notification); diff --git a/client/src/components/UI/Spinner/Spinner.tsx b/client/src/components/UI/Spinner/Spinner.tsx index a081c51..c5abb2c 100644 --- a/client/src/components/UI/Spinner/Spinner.tsx +++ b/client/src/components/UI/Spinner/Spinner.tsx @@ -1,11 +1,9 @@ import classes from './Spinner.module.css'; -const Spinner = (): JSX.Element => { +export const Spinner = (): JSX.Element => { return (
Loading...
- ) -} - -export default Spinner; \ No newline at end of file + ); +}; diff --git a/client/src/components/UI/Table/Table.tsx b/client/src/components/UI/Table/Table.tsx index d425dc1..43c4305 100644 --- a/client/src/components/UI/Table/Table.tsx +++ b/client/src/components/UI/Table/Table.tsx @@ -1,26 +1,26 @@ import classes from './Table.module.css'; -interface ComponentProps { - children: JSX.Element | JSX.Element[]; +interface Props { + children: React.ReactNode; headers: string[]; innerRef?: any; } -const Table = (props: ComponentProps): JSX.Element => { +export const Table = (props: Props): JSX.Element => { return (
- {props.headers.map((header: string, index: number): JSX.Element => ())} + {props.headers.map( + (header: string, index: number): JSX.Element => ( + + ) + )} - - {props.children} - + {props.children}
{header}{header}
- ) -} - -export default Table; \ No newline at end of file + ); +}; diff --git a/client/src/components/UI/index.ts b/client/src/components/UI/index.ts new file mode 100644 index 0000000..e1c0917 --- /dev/null +++ b/client/src/components/UI/index.ts @@ -0,0 +1,14 @@ +export * from './Table/Table'; +export * from './Spinner/Spinner'; +export * from './Notification/Notification'; +export * from './Modal/Modal'; +export * from './Layout/Layout'; +export * from './Icons/Icon/Icon'; +export * from './Icons/WeatherIcon/WeatherIcon'; +export * from './Headlines/Headline/Headline'; +export * from './Headlines/SectionHeadline/SectionHeadline'; +export * from './Headlines/SettingsHeadline/SettingsHeadline'; +export * from './Forms/InputGroup/InputGroup'; +export * from './Forms/ModalForm/ModalForm'; +export * from './Buttons/ActionButton/ActionButton'; +export * from './Buttons/Button/Button'; From 969bdb7d2403ceb7791f7eeb09871576d1697bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Tue, 9 Nov 2021 14:33:51 +0100 Subject: [PATCH 07/30] Components: refactored rest of the components to use new state. Minor changes to exports, imports and props --- client/src/App.tsx | 55 ++++---- .../src/components/Apps/AppCard/AppCard.tsx | 24 ++-- .../src/components/Apps/AppForm/AppForm.tsx | 28 ++-- .../src/components/Apps/AppGrid/AppGrid.tsx | 8 +- .../src/components/Apps/AppTable/AppTable.tsx | 70 ++++----- client/src/components/Apps/Apps.tsx | 41 ++---- .../Bookmarks/BookmarkCard/BookmarkCard.tsx | 32 ++--- .../Bookmarks/BookmarkForm/BookmarkForm.tsx | 133 ++++++------------ .../Bookmarks/BookmarkGrid/BookmarkGrid.tsx | 8 +- .../Bookmarks/BookmarkTable/BookmarkTable.tsx | 74 ++++------ client/src/components/Bookmarks/Bookmarks.tsx | 77 +++++----- client/src/components/Home/Header/Header.tsx | 26 ++-- client/src/components/Home/Home.tsx | 73 ++++------ .../NotificationCenter/NotificationCenter.tsx | 25 ++-- client/src/components/SearchBar/SearchBar.tsx | 44 ++---- .../Settings/AppDetails/AppDetails.tsx | 29 ++-- .../Settings/OtherSettings/OtherSettings.tsx | 63 +++------ .../CustomQueries/CustomQueries.tsx | 55 +++----- .../CustomQueries/QueriesForm.tsx | 29 ++-- .../SearchSettings/SearchSettings.tsx | 64 +++------ client/src/components/Settings/Settings.tsx | 20 ++- .../Settings/StyleSettings/StyleSettings.tsx | 53 ++++--- .../WeatherSettings/WeatherSettings.tsx | 60 ++++---- client/src/components/Themer/ThemePreview.tsx | 15 +- client/src/components/Themer/Themer.tsx | 36 ++--- .../Widgets/WeatherWidget/WeatherWidget.tsx | 29 ++-- client/src/index.tsx | 10 +- client/src/interfaces/GlobalState.ts | 13 -- client/src/interfaces/index.ts | 1 - 29 files changed, 462 insertions(+), 733 deletions(-) delete mode 100644 client/src/interfaces/GlobalState.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 3968bcd..28c0350 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,38 +1,41 @@ import { BrowserRouter, Route, Switch } from 'react-router-dom'; -import { fetchQueries, getConfig, setTheme } from './store/actions'; +import { actionCreators } from './store'; import 'external-svg-loader'; -// Redux -import { store } from './store/store'; -import { Provider } from 'react-redux'; - // Utils import { checkVersion } from './utility'; // Routes -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'; - -// Load config -store.dispatch(getConfig()); - -// Set theme -if (localStorage.theme) { - store.dispatch(setTheme(localStorage.theme)); -} - -// Check for updates -checkVersion(); - -// fetch queries -store.dispatch(fetchQueries()); +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'; +import { useDispatch } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { useEffect } from 'react'; const App = (): JSX.Element => { + const dispath = useDispatch(); + const { fetchQueries, getConfig, setTheme } = bindActionCreators( + actionCreators, + dispath + ); + + useEffect(() => { + getConfig(); + + if (localStorage.theme) { + setTheme(localStorage.theme); + } + + checkVersion(); + + fetchQueries(); + }, []); + return ( - + <> @@ -42,7 +45,7 @@ const App = (): JSX.Element => { - + ); }; diff --git a/client/src/components/Apps/AppCard/AppCard.tsx b/client/src/components/Apps/AppCard/AppCard.tsx index 803e5dd..dbd17e6 100644 --- a/client/src/components/Apps/AppCard/AppCard.tsx +++ b/client/src/components/Apps/AppCard/AppCard.tsx @@ -1,17 +1,19 @@ import classes from './AppCard.module.css'; -import Icon from '../../UI/Icons/Icon/Icon'; +import { Icon } from '../../UI'; import { iconParser, urlParser } from '../../../utility'; -import { App, Config, GlobalState } from '../../../interfaces'; -import { connect } from 'react-redux'; +import { App } from '../../../interfaces'; +import { useSelector } from 'react-redux'; +import { State } from '../../../store/reducers'; -interface ComponentProps { +interface Props { app: App; pinHandler?: Function; - config: Config; } -const AppCard = (props: ComponentProps): JSX.Element => { +export const AppCard = (props: Props): JSX.Element => { + const { config } = useSelector((state: State) => state.config); + const [displayUrl, redirectUrl] = urlParser(props.app.url); let iconEl: JSX.Element; @@ -42,7 +44,7 @@ const AppCard = (props: ComponentProps): JSX.Element => { return ( @@ -54,11 +56,3 @@ const AppCard = (props: ComponentProps): JSX.Element => { ); }; - -const mapStateToProps = (state: GlobalState) => { - return { - config: state.config.config, - }; -}; - -export default connect(mapStateToProps)(AppCard); diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index c3ffdad..8c11253 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -1,23 +1,23 @@ import { useState, useEffect, ChangeEvent, SyntheticEvent } from 'react'; -import { connect } from 'react-redux'; -import { addApp, updateApp } from '../../../store/actions'; +import { useDispatch } from 'react-redux'; import { App, NewApp } from '../../../interfaces'; import classes from './AppForm.module.css'; -import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; -import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; -import Button from '../../UI/Buttons/Button/Button'; +import { ModalForm, InputGroup, Button } from '../../UI'; import { inputHandler, newAppTemplate } from '../../../utility'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../store'; -interface ComponentProps { +interface Props { modalHandler: () => void; - addApp: (formData: NewApp | FormData) => any; - updateApp: (id: number, formData: NewApp | FormData) => any; app?: App; } -const AppForm = (props: ComponentProps): JSX.Element => { +export const AppForm = (props: Props): JSX.Element => { + const dispatch = useDispatch(); + const { addApp, updateApp } = bindActionCreators(actionCreators, dispatch); + const [useCustomIcon, toggleUseCustomIcon] = useState(false); const [customIcon, setCustomIcon] = useState(null); const [formData, setFormData] = useState(newAppTemplate); @@ -68,16 +68,16 @@ const AppForm = (props: ComponentProps): JSX.Element => { if (!props.app) { if (customIcon) { const data = createFormData(); - props.addApp(data); + addApp(data); } else { - props.addApp(formData); + addApp(formData); } } else { if (customIcon) { const data = createFormData(); - props.updateApp(props.app.id, data); + updateApp(props.app.id, data); } else { - props.updateApp(props.app.id, formData); + updateApp(props.app.id, formData); props.modalHandler(); } } @@ -192,5 +192,3 @@ const AppForm = (props: ComponentProps): JSX.Element => { ); }; - -export default connect(null, { addApp, updateApp })(AppForm); diff --git a/client/src/components/Apps/AppGrid/AppGrid.tsx b/client/src/components/Apps/AppGrid/AppGrid.tsx index 30d5c8c..6b02443 100644 --- a/client/src/components/Apps/AppGrid/AppGrid.tsx +++ b/client/src/components/Apps/AppGrid/AppGrid.tsx @@ -2,15 +2,15 @@ import classes from './AppGrid.module.css'; import { Link } from 'react-router-dom'; import { App } from '../../../interfaces/App'; -import AppCard from '../AppCard/AppCard'; +import { AppCard } from '../AppCard/AppCard'; -interface ComponentProps { +interface Props { apps: App[]; totalApps?: number; searching: boolean; } -const AppGrid = (props: ComponentProps): JSX.Element => { +export const AppGrid = (props: Props): JSX.Element => { let apps: JSX.Element; if (props.apps.length > 0) { @@ -49,5 +49,3 @@ const AppGrid = (props: ComponentProps): JSX.Element => { return apps; }; - -export default AppGrid; diff --git a/client/src/components/Apps/AppTable/AppTable.tsx b/client/src/components/Apps/AppTable/AppTable.tsx index 631bd74..ee82144 100644 --- a/client/src/components/Apps/AppTable/AppTable.tsx +++ b/client/src/components/Apps/AppTable/AppTable.tsx @@ -8,48 +8,45 @@ import { import { Link } from 'react-router-dom'; // Redux -import { connect } from 'react-redux'; -import { - pinApp, - deleteApp, - reorderApps, - updateConfig, - createNotification, -} from '../../../store/actions'; +import { useDispatch, useSelector } from 'react-redux'; // Typescript -import { App, Config, GlobalState, NewNotification } from '../../../interfaces'; +import { App } from '../../../interfaces'; // CSS import classes from './AppTable.module.css'; // UI -import Icon from '../../UI/Icons/Icon/Icon'; -import Table from '../../UI/Table/Table'; +import { Icon, Table } from '../../UI'; +import { State } from '../../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../store'; -interface ComponentProps { - apps: App[]; - config: Config; - pinApp: (app: App) => void; - deleteApp: (id: number) => void; +interface Props { updateAppHandler: (app: App) => void; - reorderApps: (apps: App[]) => void; - updateConfig: (formData: any) => void; - createNotification: (notification: NewNotification) => void; } -const AppTable = (props: ComponentProps): JSX.Element => { +export const AppTable = (props: Props): JSX.Element => { + const { + apps: { apps }, + config: { config }, + } = useSelector((state: State) => state); + + const dispatch = useDispatch(); + const { pinApp, deleteApp, reorderApps, updateConfig, createNotification } = + bindActionCreators(actionCreators, dispatch); + const [localApps, setLocalApps] = useState([]); const [isCustomOrder, setIsCustomOrder] = useState(false); // Copy apps array useEffect(() => { - setLocalApps([...props.apps]); - }, [props.apps]); + setLocalApps([...apps]); + }, [apps]); // Check ordering useEffect(() => { - const order = props.config.useOrdering; + const order = config.useOrdering; if (order === 'orderId') { setIsCustomOrder(true); @@ -62,7 +59,7 @@ const AppTable = (props: ComponentProps): JSX.Element => { ); if (proceed) { - props.deleteApp(app.id); + deleteApp(app.id); } }; @@ -79,7 +76,7 @@ const AppTable = (props: ComponentProps): JSX.Element => { const dragEndHanlder = (result: DropResult): void => { if (!isCustomOrder) { - props.createNotification({ + createNotification({ title: 'Error', message: 'Custom order is disabled', }); @@ -95,7 +92,7 @@ const AppTable = (props: ComponentProps): JSX.Element => { tmpApps.splice(result.destination.index, 0, movedApp); setLocalApps(tmpApps); - props.reorderApps(tmpApps); + reorderApps(tmpApps); }; return ( @@ -178,9 +175,9 @@ const AppTable = (props: ComponentProps): JSX.Element => {
props.pinApp(app)} + onClick={() => pinApp(app)} onKeyDown={(e) => - keyboardActionHandler(e, app, props.pinApp) + keyboardActionHandler(e, app, pinApp) } tabIndex={0} > @@ -208,20 +205,3 @@ const AppTable = (props: ComponentProps): JSX.Element => { ); }; - -const mapStateToProps = (state: GlobalState) => { - return { - apps: state.app.apps, - config: state.config.config, - }; -}; - -const actions = { - pinApp, - deleteApp, - reorderApps, - updateConfig, - createNotification, -}; - -export default connect(mapStateToProps, actions)(AppTable); diff --git a/client/src/components/Apps/Apps.tsx b/client/src/components/Apps/Apps.tsx index 2c15b35..2185879 100644 --- a/client/src/components/Apps/Apps.tsx +++ b/client/src/components/Apps/Apps.tsx @@ -2,39 +2,37 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; // Redux -import { connect } from 'react-redux'; -import { getApps } from '../../store/actions'; +import { useDispatch, useSelector } from 'react-redux'; // Typescript -import { App, GlobalState } from '../../interfaces'; +import { App } from '../../interfaces'; // CSS import classes from './Apps.module.css'; // UI -import { Container } from '../UI/Layout/Layout'; -import Headline from '../UI/Headlines/Headline/Headline'; -import Spinner from '../UI/Spinner/Spinner'; -import ActionButton from '../UI/Buttons/ActionButton/ActionButton'; -import Modal from '../UI/Modal/Modal'; +import { Headline, Spinner, ActionButton, Modal, Container } from '../UI'; // Subcomponents -import AppGrid from './AppGrid/AppGrid'; -import AppForm from './AppForm/AppForm'; -import AppTable from './AppTable/AppTable'; +import { AppGrid } from './AppGrid/AppGrid'; +import { AppForm } from './AppForm/AppForm'; +import { AppTable } from './AppTable/AppTable'; // Utils import { appTemplate } from '../../utility'; +import { State } from '../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../store'; -interface ComponentProps { - getApps: Function; - apps: App[]; - loading: boolean; +interface Props { searching: boolean; } -const Apps = (props: ComponentProps): JSX.Element => { - const { getApps, apps, loading, searching = false } = props; +export const Apps = (props: Props): JSX.Element => { + const { apps, loading } = useSelector((state: State) => state.apps); + + const dispatch = useDispatch(); + const { getApps } = bindActionCreators(actionCreators, dispatch); const [modalIsOpen, setModalIsOpen] = useState(false); const [isInEdit, setIsInEdit] = useState(false); @@ -95,12 +93,3 @@ const Apps = (props: ComponentProps): JSX.Element => { ); }; - -const mapStateToProps = (state: GlobalState) => { - return { - apps: state.app.apps, - loading: state.app.loading, - }; -}; - -export default connect(mapStateToProps, { getApps })(Apps); diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx index 93ead02..f3b3bd3 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -1,17 +1,23 @@ -import { Bookmark, Category, Config, GlobalState } from '../../../interfaces'; +import { Fragment } from 'react'; + +import { useSelector } from 'react-redux'; +import { State } from '../../../store/reducers'; + +import { Bookmark, Category } from '../../../interfaces'; + import classes from './BookmarkCard.module.css'; -import Icon from '../../UI/Icons/Icon/Icon'; -import { iconParser, urlParser } from '../../../utility'; -import { Fragment } from 'react'; -import { connect } from 'react-redux'; +import { Icon } from '../../UI'; -interface ComponentProps { +import { iconParser, urlParser } from '../../../utility'; + +interface Props { category: Category; - config: Config; } -const BookmarkCard = (props: ComponentProps): JSX.Element => { +export const BookmarkCard = (props: Props): JSX.Element => { + const { config } = useSelector((state: State) => state.config); + return (

{props.category.name}

@@ -56,7 +62,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => { return ( @@ -69,11 +75,3 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
); }; - -const mapStateToProps = (state: GlobalState) => { - return { - config: state.config.config, - }; -}; - -export default connect(mapStateToProps)(BookmarkCard); diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx index 5162c89..848848b 100644 --- a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx +++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx @@ -8,94 +8,68 @@ import { } from 'react'; // Redux -import { connect } from 'react-redux'; -import { - getCategories, - addCategory, - addBookmark, - updateCategory, - updateBookmark, - createNotification, -} from '../../../store/actions'; +import { useDispatch, useSelector } from 'react-redux'; // Typescript import { Bookmark, Category, - GlobalState, NewBookmark, NewCategory, - NewNotification, } from '../../../interfaces'; import { ContentType } from '../Bookmarks'; // UI -import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; -import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; -import Button from '../../UI/Buttons/Button/Button'; +import { ModalForm, InputGroup, Button } from '../../UI'; // CSS import classes from './BookmarkForm.module.css'; +import { newBookmarkTemplate, newCategoryTemplate } from '../../../utility'; +import { State } from '../../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../store'; -interface ComponentProps { +interface Props { modalHandler: () => void; contentType: ContentType; - categories: Category[]; category?: Category; bookmark?: Bookmark; - addCategory: (formData: NewCategory) => void; - addBookmark: (formData: NewBookmark | FormData) => void; - updateCategory: (id: number, formData: NewCategory) => void; - updateBookmark: ( - id: number, - formData: NewBookmark | FormData, - category: { - prev: number; - curr: number; - } - ) => void; - createNotification: (notification: NewNotification) => void; } -const BookmarkForm = (props: ComponentProps): JSX.Element => { +export const BookmarkForm = (props: Props): JSX.Element => { + const { categories } = useSelector((state: State) => state.bookmarks); + + const dispatch = useDispatch(); + const { + addCategory, + addBookmark, + updateCategory, + updateBookmark, + createNotification, + } = bindActionCreators(actionCreators, dispatch); + const [useCustomIcon, toggleUseCustomIcon] = useState(false); const [customIcon, setCustomIcon] = useState(null); - const [categoryName, setCategoryName] = useState({ - name: '', - }); + const [categoryName, setCategoryName] = + useState(newCategoryTemplate); - const [formData, setFormData] = useState({ - name: '', - url: '', - categoryId: -1, - icon: '', - }); + const [formData, setFormData] = useState(newBookmarkTemplate); // Load category data if provided for editing useEffect(() => { if (props.category) { - setCategoryName({ name: props.category.name }); + setCategoryName({ ...props.category }); } else { - setCategoryName({ name: '' }); + setCategoryName(newCategoryTemplate); } }, [props.category]); // Load bookmark data if provided for editing useEffect(() => { if (props.bookmark) { - setFormData({ - name: props.bookmark.name, - url: props.bookmark.url, - categoryId: props.bookmark.categoryId, - icon: props.bookmark.icon, - }); + setFormData({ ...props.bookmark }); } else { - setFormData({ - name: '', - url: '', - categoryId: -1, - icon: '', - }); + setFormData(newBookmarkTemplate); } }, [props.bookmark]); @@ -118,12 +92,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { // Add new if (props.contentType === ContentType.category) { // Add category - props.addCategory(categoryName); - setCategoryName({ name: '' }); + addCategory(categoryName); + setCategoryName(newCategoryTemplate); } else if (props.contentType === ContentType.bookmark) { // Add bookmark if (formData.categoryId === -1) { - props.createNotification({ + createNotification({ title: 'Error', message: 'Please select category', }); @@ -132,16 +106,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { if (customIcon) { const data = createFormData(); - props.addBookmark(data); + addBookmark(data); } else { - props.addBookmark(formData); + addBookmark(formData); } setFormData({ - name: '', - url: '', + ...newBookmarkTemplate, categoryId: formData.categoryId, - icon: '', }); // setCustomIcon(null); @@ -150,29 +122,24 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { // Update if (props.contentType === ContentType.category && props.category) { // Update category - props.updateCategory(props.category.id, categoryName); - setCategoryName({ name: '' }); + updateCategory(props.category.id, categoryName); + setCategoryName(newCategoryTemplate); } else if (props.contentType === ContentType.bookmark && props.bookmark) { // Update bookmark if (customIcon) { const data = createFormData(); - props.updateBookmark(props.bookmark.id, data, { + updateBookmark(props.bookmark.id, data, { prev: props.bookmark.categoryId, curr: formData.categoryId, }); } else { - props.updateBookmark(props.bookmark.id, formData, { + updateBookmark(props.bookmark.id, formData, { prev: props.bookmark.categoryId, curr: formData.categoryId, }); } - setFormData({ - name: '', - url: '', - categoryId: -1, - icon: '', - }); + setFormData(newBookmarkTemplate); setCustomIcon(null); } @@ -231,7 +198,9 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { placeholder="Social Media" required value={categoryName.name} - onChange={(e) => setCategoryName({ name: e.target.value })} + onChange={(e) => + setCategoryName({ name: e.target.value, isPublic: !!!!!false }) + } /> @@ -249,6 +218,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { onChange={(e) => inputChangeHandler(e)} /> + { + + {!useCustomIcon ? ( // mdi @@ -344,20 +316,3 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { ); }; - -const mapStateToProps = (state: GlobalState) => { - return { - categories: state.bookmark.categories, - }; -}; - -const dispatchMap = { - getCategories, - addCategory, - addBookmark, - updateCategory, - updateBookmark, - createNotification, -}; - -export default connect(mapStateToProps, dispatchMap)(BookmarkForm); diff --git a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx index bf17c81..cb4396f 100644 --- a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx +++ b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx @@ -4,15 +4,15 @@ import classes from './BookmarkGrid.module.css'; import { Category } from '../../../interfaces'; -import BookmarkCard from '../BookmarkCard/BookmarkCard'; +import { BookmarkCard } from '../BookmarkCard/BookmarkCard'; -interface ComponentProps { +interface Props { categories: Category[]; totalCategories?: number; searching: boolean; } -const BookmarkGrid = (props: ComponentProps): JSX.Element => { +export const BookmarkGrid = (props: Props): JSX.Element => { let bookmarks: JSX.Element; if (props.categories.length > 0) { @@ -53,5 +53,3 @@ const BookmarkGrid = (props: ComponentProps): JSX.Element => { return bookmarks; }; - -export default BookmarkGrid; diff --git a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx index 90c34aa..7fd012a 100644 --- a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx +++ b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx @@ -8,45 +8,39 @@ import { import { Link } from 'react-router-dom'; // Redux -import { connect } from 'react-redux'; -import { - pinCategory, - deleteCategory, - deleteBookmark, - createNotification, - reorderCategories, -} from '../../../store/actions'; +import { useDispatch, useSelector } from 'react-redux'; +import { State } from '../../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../store'; // Typescript -import { - Bookmark, - Category, - Config, - GlobalState, - NewNotification, -} from '../../../interfaces'; +import { Bookmark, Category } from '../../../interfaces'; import { ContentType } from '../Bookmarks'; // CSS import classes from './BookmarkTable.module.css'; // UI -import Table from '../../UI/Table/Table'; -import Icon from '../../UI/Icons/Icon/Icon'; +import { Table, Icon } from '../../UI'; -interface ComponentProps { +interface Props { contentType: ContentType; categories: Category[]; - config: Config; - pinCategory: (category: Category) => void; - deleteCategory: (id: number) => void; updateHandler: (data: Category | Bookmark) => void; - deleteBookmark: (bookmarkId: number, categoryId: number) => void; - createNotification: (notification: NewNotification) => void; - reorderCategories: (categories: Category[]) => void; } -const BookmarkTable = (props: ComponentProps): JSX.Element => { +export const BookmarkTable = (props: Props): JSX.Element => { + const { config } = useSelector((state: State) => state.config); + + const dispatch = useDispatch(); + const { + pinCategory, + deleteCategory, + deleteBookmark, + createNotification, + reorderCategories, + } = bindActionCreators(actionCreators, dispatch); + const [localCategories, setLocalCategories] = useState([]); const [isCustomOrder, setIsCustomOrder] = useState(false); @@ -57,7 +51,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => { // Check ordering useEffect(() => { - const order = props.config.useOrdering; + const order = config.useOrdering; if (order === 'orderId') { setIsCustomOrder(true); @@ -70,7 +64,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => { ); if (proceed) { - props.deleteCategory(category.id); + deleteCategory(category.id); } }; @@ -80,7 +74,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => { ); if (proceed) { - props.deleteBookmark(bookmark.id, bookmark.categoryId); + deleteBookmark(bookmark.id, bookmark.categoryId); } }; @@ -96,7 +90,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => { const dragEndHanlder = (result: DropResult): void => { if (!isCustomOrder) { - props.createNotification({ + createNotification({ title: 'Error', message: 'Custom order is disabled', }); @@ -112,7 +106,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => { tmpCategories.splice(result.destination.index, 0, movedApp); setLocalCategories(tmpCategories); - props.reorderCategories(tmpCategories); + reorderCategories(tmpCategories); }; if (props.contentType === ContentType.category) { @@ -186,12 +180,12 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
props.pinCategory(category)} + onClick={() => pinCategory(category)} onKeyDown={(e) => keyboardActionHandler( e, category, - props.pinCategory + pinCategory ) } tabIndex={0} @@ -265,19 +259,3 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => { ); } }; - -const mapStateToProps = (state: GlobalState) => { - return { - config: state.config.config, - }; -}; - -const actions = { - pinCategory, - deleteCategory, - deleteBookmark, - createNotification, - reorderCategories, -}; - -export default connect(mapStateToProps, actions)(BookmarkTable); diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx index 88d9cdb..fffb5ff 100644 --- a/client/src/components/Bookmarks/Bookmarks.tsx +++ b/client/src/components/Bookmarks/Bookmarks.tsx @@ -1,25 +1,30 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import { connect } from 'react-redux'; -import { getCategories } from '../../store/actions'; +// Redux +import { useDispatch, useSelector } from 'react-redux'; +import { State } from '../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../store'; + +// Typescript +import { Category, Bookmark } from '../../interfaces'; + +// CSS import classes from './Bookmarks.module.css'; -import { Container } from '../UI/Layout/Layout'; -import Headline from '../UI/Headlines/Headline/Headline'; -import ActionButton from '../UI/Buttons/ActionButton/ActionButton'; +// UI +import { Container, Headline, ActionButton, Spinner, Modal } from '../UI'; -import BookmarkGrid from './BookmarkGrid/BookmarkGrid'; -import { Category, GlobalState, Bookmark } from '../../interfaces'; -import Spinner from '../UI/Spinner/Spinner'; -import Modal from '../UI/Modal/Modal'; -import BookmarkForm from './BookmarkForm/BookmarkForm'; -import BookmarkTable from './BookmarkTable/BookmarkTable'; +// Components +import { BookmarkGrid } from './BookmarkGrid/BookmarkGrid'; +import { BookmarkForm } from './BookmarkForm/BookmarkForm'; +import { BookmarkTable } from './BookmarkTable/BookmarkTable'; -interface ComponentProps { - loading: boolean; - categories: Category[]; - getCategories: () => void; +// Utils +import { bookmarkTemplate, categoryTemplate } from '../../utility'; + +interface Props { searching: boolean; } @@ -28,8 +33,15 @@ export enum ContentType { bookmark, } -const Bookmarks = (props: ComponentProps): JSX.Element => { - const { getCategories, categories, loading, searching = false } = props; +export const Bookmarks = (props: Props): JSX.Element => { + const { loading, categories } = useSelector( + (state: State) => state.bookmarks + ); + + const dispatch = useDispatch(); + const { getCategories } = bindActionCreators(actionCreators, dispatch); + + const { searching = false } = props; const [modalIsOpen, setModalIsOpen] = useState(false); const [formContentType, setFormContentType] = useState(ContentType.category); @@ -38,24 +50,10 @@ const Bookmarks = (props: ComponentProps): JSX.Element => { ContentType.category ); const [isInUpdate, setIsInUpdate] = useState(false); - const [categoryInUpdate, setCategoryInUpdate] = useState({ - name: '', - id: -1, - isPinned: false, - orderId: 0, - bookmarks: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - const [bookmarkInUpdate, setBookmarkInUpdate] = useState({ - name: '', - url: '', - categoryId: -1, - icon: '', - id: -1, - createdAt: new Date(), - updatedAt: new Date(), - }); + const [categoryInUpdate, setCategoryInUpdate] = + useState(categoryTemplate); + const [bookmarkInUpdate, setBookmarkInUpdate] = + useState(bookmarkTemplate); useEffect(() => { if (categories.length === 0) { @@ -161,12 +159,3 @@ const Bookmarks = (props: ComponentProps): JSX.Element => { ); }; - -const mapStateToProps = (state: GlobalState) => { - return { - loading: state.bookmark.loading, - categories: state.bookmark.categories, - }; -}; - -export default connect(mapStateToProps, { getCategories })(Bookmarks); diff --git a/client/src/components/Home/Header/Header.tsx b/client/src/components/Home/Header/Header.tsx index 3b2841b..f059b49 100644 --- a/client/src/components/Home/Header/Header.tsx +++ b/client/src/components/Home/Header/Header.tsx @@ -1,17 +1,17 @@ import { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; -import { Config, GlobalState } from '../../../interfaces'; -import WeatherWidget from '../../Widgets/WeatherWidget/WeatherWidget'; -import { getDateTime } from './functions/getDateTime'; -import { greeter } from './functions/greeter'; + +// CSS import classes from './Header.module.css'; -interface Props { - config: Config; -} +// Components +import { WeatherWidget } from '../../Widgets/WeatherWidget/WeatherWidget'; -const Header = (props: Props): JSX.Element => { +// Utils +import { getDateTime } from './functions/getDateTime'; +import { greeter } from './functions/greeter'; + +export const Header = (): JSX.Element => { const [dateTime, setDateTime] = useState(getDateTime()); const [greeting, setGreeting] = useState(greeter()); @@ -39,11 +39,3 @@ const Header = (props: Props): JSX.Element => { ); }; - -const mapStateToProps = (state: GlobalState) => { - return { - config: state.config.config, - }; -}; - -export default connect(mapStateToProps)(Header); diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index 017df9c..578054c 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -2,47 +2,38 @@ import { useState, useEffect, Fragment } from 'react'; import { Link } from 'react-router-dom'; // Redux -import { connect } from 'react-redux'; -import { getApps, getCategories } from '../../store/actions'; +import { useDispatch, useSelector } from 'react-redux'; +import { State } from '../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../store'; // Typescript -import { GlobalState } from '../../interfaces/GlobalState'; -import { App, Category, Config } from '../../interfaces'; +import { App, Category } from '../../interfaces'; // UI -import Icon from '../UI/Icons/Icon/Icon'; -import { Container } from '../UI/Layout/Layout'; -import SectionHeadline from '../UI/Headlines/SectionHeadline/SectionHeadline'; -import Spinner from '../UI/Spinner/Spinner'; +import { Icon, Container, SectionHeadline, Spinner } from '../UI'; // CSS import classes from './Home.module.css'; // Components -import AppGrid from '../Apps/AppGrid/AppGrid'; -import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid'; -import SearchBar from '../SearchBar/SearchBar'; -import Header from './Header/Header'; +import { AppGrid } from '../Apps/AppGrid/AppGrid'; +import { BookmarkGrid } from '../Bookmarks/BookmarkGrid/BookmarkGrid'; +import { SearchBar } from '../SearchBar/SearchBar'; +import { Header } from './Header/Header'; -interface ComponentProps { - getApps: Function; - getCategories: Function; - appsLoading: boolean; - apps: App[]; - categoriesLoading: boolean; - categories: Category[]; - config: Config; -} - -const Home = (props: ComponentProps): JSX.Element => { +export const Home = (): JSX.Element => { const { - getApps, - apps, - appsLoading, - getCategories, - categories, - categoriesLoading, - } = props; + apps: { apps, loading: appsLoading }, + bookmarks: { categories, loading: bookmarksLoading }, + config: { config }, + } = useSelector((state: State) => state); + + const dispatch = useDispatch(); + const { getApps, getCategories } = bindActionCreators( + actionCreators, + dispatch + ); // Local search query const [localSearch, setLocalSearch] = useState(null); @@ -90,7 +81,7 @@ const Home = (props: ComponentProps): JSX.Element => { return ( - {!props.config.hideSearch ? ( + {!config.hideSearch ? ( {
)} - {!props.config.hideHeader ?
:
} + {!config.hideHeader ?
:
} - {!props.config.hideApps ? ( + {!config.hideApps ? ( {appsLoading ? ( @@ -124,10 +115,10 @@ const Home = (props: ComponentProps): JSX.Element => {
)} - {!props.config.hideCategories ? ( + {!config.hideCategories ? ( - {categoriesLoading ? ( + {bookmarksLoading ? ( ) : ( { ); }; - -const mapStateToProps = (state: GlobalState) => { - return { - appsLoading: state.app.loading, - apps: state.app.apps, - categoriesLoading: state.bookmark.loading, - categories: state.bookmark.categories, - config: state.config.config, - }; -}; - -export default connect(mapStateToProps, { getApps, getCategories })(Home); diff --git a/client/src/components/NotificationCenter/NotificationCenter.tsx b/client/src/components/NotificationCenter/NotificationCenter.tsx index 733316b..4bda8bf 100644 --- a/client/src/components/NotificationCenter/NotificationCenter.tsx +++ b/client/src/components/NotificationCenter/NotificationCenter.tsx @@ -1,21 +1,20 @@ -import { connect } from 'react-redux'; -import { GlobalState, Notification as _Notification } from '../../interfaces'; +import { useSelector } from 'react-redux'; +import { Notification as NotificationInterface } from '../../interfaces'; import classes from './NotificationCenter.module.css'; -import Notification from '../UI/Notification/Notification'; +import { Notification } from '../UI'; +import { State } from '../../store/reducers'; -interface ComponentProps { - notifications: _Notification[]; -} +export const NotificationCenter = (): JSX.Element => { + const { notifications } = useSelector((state: State) => state.notification); -const NotificationCenter = (props: ComponentProps): JSX.Element => { return (
- {props.notifications.map((notification: _Notification) => { + {notifications.map((notification: NotificationInterface) => { return ( {
); }; - -const mapStateToProps = (state: GlobalState) => { - return { - notifications: state.notification.notifications, - }; -}; - -export default connect(mapStateToProps)(NotificationCenter); diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index 2dad112..46f0bfd 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -1,42 +1,33 @@ import { useRef, useEffect, KeyboardEvent } from 'react'; // Redux -import { connect } from 'react-redux'; -import { createNotification } from '../../store/actions'; +import { useDispatch, useSelector } from 'react-redux'; // Typescript -import { - App, - Category, - Config, - GlobalState, - NewNotification, -} from '../../interfaces'; +import { App, Category } from '../../interfaces'; // CSS import classes from './SearchBar.module.css'; // Utils import { searchParser, urlParser, redirectUrl } from '../../utility'; +import { State } from '../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../store'; -interface ComponentProps { - createNotification: (notification: NewNotification) => void; +interface Props { setLocalSearch: (query: string) => void; appSearchResult: App[] | null; bookmarkSearchResult: Category[] | null; - config: Config; - loading: boolean; } -const SearchBar = (props: ComponentProps): JSX.Element => { - const { - setLocalSearch, - createNotification, - config, - loading, - appSearchResult, - bookmarkSearchResult, - } = props; +export const SearchBar = (props: Props): JSX.Element => { + const { config, loading } = useSelector((state: State) => state.config); + + const dispatch = useDispatch(); + const { createNotification } = bindActionCreators(actionCreators, dispatch); + + const { setLocalSearch, appSearchResult, bookmarkSearchResult } = props; const inputRef = useRef(document.createElement('input')); @@ -126,12 +117,3 @@ const SearchBar = (props: ComponentProps): JSX.Element => {
); }; - -const mapStateToProps = (state: GlobalState) => { - return { - config: state.config.config, - loading: state.config.loading, - }; -}; - -export default connect(mapStateToProps, { createNotification })(SearchBar); diff --git a/client/src/components/Settings/AppDetails/AppDetails.tsx b/client/src/components/Settings/AppDetails/AppDetails.tsx index 109053a..42257ed 100644 --- a/client/src/components/Settings/AppDetails/AppDetails.tsx +++ b/client/src/components/Settings/AppDetails/AppDetails.tsx @@ -1,34 +1,33 @@ import { Fragment } from 'react'; import classes from './AppDetails.module.css'; -import Button from '../../UI/Buttons/Button/Button'; +import { Button } from '../../UI'; import { checkVersion } from '../../../utility'; -const AppDetails = (): JSX.Element => { +export const AppDetails = (): JSX.Element => { return (

+ href="https://github.com/pawelmalak/flame" + target="_blank" + rel="noreferrer" + > Flame - - {' '} + {' '} version {process.env.REACT_APP_VERSION}

- See changelog {' '} + See changelog{' '} + href="https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md" + target="_blank" + rel="noreferrer" + > here

- ) -} - -export default AppDetails; + ); +}; diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index c5b45ae..3ec317f 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -1,41 +1,28 @@ import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; // Redux -import { connect } from 'react-redux'; -import { - createNotification, - updateConfig, - sortApps, - sortCategories, -} from '../../../store/actions'; +import { useDispatch, useSelector } from 'react-redux'; // Typescript -import { - Config, - GlobalState, - NewNotification, - OtherSettingsForm, -} from '../../../interfaces'; +import { OtherSettingsForm } from '../../../interfaces'; // UI -import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; -import Button from '../../UI/Buttons/Button/Button'; -import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline'; +import { InputGroup, Button, SettingsHeadline } from '../../UI'; // Utils import { otherSettingsTemplate, inputHandler } from '../../../utility'; +import { State } from '../../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../store'; -interface ComponentProps { - createNotification: (notification: NewNotification) => void; - updateConfig: (formData: OtherSettingsForm) => void; - sortApps: () => void; - sortCategories: () => void; - loading: boolean; - config: Config; -} +export const OtherSettings = (): JSX.Element => { + const { loading, config } = useSelector((state: State) => state.config); -const OtherSettings = (props: ComponentProps): JSX.Element => { - const { config } = props; + const dispatch = useDispatch(); + const { updateConfig, sortApps, sortCategories } = bindActionCreators( + actionCreators, + dispatch + ); // Initial state const [formData, setFormData] = useState( @@ -47,21 +34,21 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { setFormData({ ...config, }); - }, [props.loading]); + }, [loading]); // Form handler const formSubmitHandler = async (e: FormEvent) => { e.preventDefault(); // Save settings - await props.updateConfig(formData); + await updateConfig(formData); // Update local page title document.title = formData.customTitle; // Sort apps and categories with new settings - props.sortApps(); - props.sortCategories(); + sortApps(); + sortCategories(); }; // Input handler @@ -338,19 +325,3 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { ); }; - -const mapStateToProps = (state: GlobalState) => { - return { - loading: state.config.loading, - config: state.config.config, - }; -}; - -const actions = { - createNotification, - updateConfig, - sortApps, - sortCategories, -}; - -export default connect(mapStateToProps, actions)(OtherSettings); diff --git a/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx b/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx index a694f42..747be3b 100644 --- a/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx +++ b/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx @@ -1,29 +1,31 @@ import { Fragment, useState } from 'react'; -import { connect } from 'react-redux'; +// Redux +import { useDispatch, useSelector } from 'react-redux'; +import { State } from '../../../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../../store'; + +// Typescript +import { Query } from '../../../../interfaces'; + +// CSS import classes from './CustomQueries.module.css'; -import Modal from '../../../UI/Modal/Modal'; -import Icon from '../../../UI/Icons/Icon/Icon'; -import { - Config, - GlobalState, - NewNotification, - Query, -} from '../../../../interfaces'; -import QueriesForm from './QueriesForm'; -import { deleteQuery, createNotification } from '../../../../store/actions'; -import Button from '../../../UI/Buttons/Button/Button'; +// UI +import { Modal, Icon, Button } from '../../../UI'; -interface Props { - customQueries: Query[]; - deleteQuery: (prefix: string) => {}; - createNotification: (notification: NewNotification) => void; - config: Config; -} +// Components +import { QueriesForm } from './QueriesForm'; -const CustomQueries = (props: Props): JSX.Element => { - const { customQueries, deleteQuery, createNotification } = props; +export const CustomQueries = (): JSX.Element => { + const { customQueries, config } = useSelector((state: State) => state.config); + + const dispatch = useDispatch(); + const { deleteQuery, createNotification } = bindActionCreators( + actionCreators, + dispatch + ); const [modalIsOpen, setModalIsOpen] = useState(false); const [editableQuery, setEditableQuery] = useState(null); @@ -34,7 +36,7 @@ const CustomQueries = (props: Props): JSX.Element => { }; const deleteHandler = (query: Query) => { - const currentProvider = props.config.defaultSearchProvider; + const currentProvider = config.defaultSearchProvider; const isCurrent = currentProvider === query.prefix; if (isCurrent) { @@ -105,14 +107,3 @@ const CustomQueries = (props: Props): JSX.Element => { ); }; - -const mapStateToProps = (state: GlobalState) => { - return { - customQueries: state.config.customQueries, - config: state.config.config, - }; -}; - -export default connect(mapStateToProps, { deleteQuery, createNotification })( - CustomQueries -); diff --git a/client/src/components/Settings/SearchSettings/CustomQueries/QueriesForm.tsx b/client/src/components/Settings/SearchSettings/CustomQueries/QueriesForm.tsx index 42ad654..2cb76a9 100644 --- a/client/src/components/Settings/SearchSettings/CustomQueries/QueriesForm.tsx +++ b/client/src/components/Settings/SearchSettings/CustomQueries/QueriesForm.tsx @@ -1,20 +1,26 @@ import { ChangeEvent, FormEvent, useState, useEffect } from 'react'; + +import { useDispatch } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../../store'; + import { Query } from '../../../../interfaces'; -import Button from '../../../UI/Buttons/Button/Button'; -import InputGroup from '../../../UI/Forms/InputGroup/InputGroup'; -import ModalForm from '../../../UI/Forms/ModalForm/ModalForm'; -import { connect } from 'react-redux'; -import { addQuery, updateQuery } from '../../../../store/actions'; + +import { Button, InputGroup, ModalForm } from '../../../UI'; interface Props { modalHandler: () => void; - addQuery: (query: Query) => {}; - updateQuery: (query: Query, Oldprefix: string) => {}; query?: Query; } -const QueriesForm = (props: Props): JSX.Element => { - const { modalHandler, addQuery, updateQuery, query } = props; +export const QueriesForm = (props: Props): JSX.Element => { + const dispatch = useDispatch(); + const { addQuery, updateQuery } = bindActionCreators( + actionCreators, + dispatch + ); + + const { modalHandler, query } = props; const [formData, setFormData] = useState({ name: '', @@ -77,6 +83,7 @@ const QueriesForm = (props: Props): JSX.Element => { onChange={(e) => inputChangeHandler(e)} /> + { onChange={(e) => inputChangeHandler(e)} /> + { onChange={(e) => inputChangeHandler(e)} /> + {query ? : } ); }; - -export default connect(null, { addQuery, updateQuery })(QueriesForm); diff --git a/client/src/components/Settings/SearchSettings/SearchSettings.tsx b/client/src/components/Settings/SearchSettings/SearchSettings.tsx index d05def5..2717b43 100644 --- a/client/src/components/Settings/SearchSettings/SearchSettings.tsx +++ b/client/src/components/Settings/SearchSettings/SearchSettings.tsx @@ -1,58 +1,49 @@ // React import { useState, useEffect, FormEvent, ChangeEvent, Fragment } from 'react'; -import { connect } from 'react-redux'; - -// State -import { createNotification, updateConfig } from '../../../store/actions'; +import { useDispatch, useSelector } from 'react-redux'; // Typescript -import { - Config, - GlobalState, - NewNotification, - Query, - SearchForm, -} from '../../../interfaces'; +import { Query, SearchForm } from '../../../interfaces'; // Components -import CustomQueries from './CustomQueries/CustomQueries'; +import { CustomQueries } from './CustomQueries/CustomQueries'; // UI -import Button from '../../UI/Buttons/Button/Button'; -import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline'; -import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; +import { Button, SettingsHeadline, InputGroup } from '../../UI'; // Utils import { inputHandler, searchSettingsTemplate } from '../../../utility'; // Data import { queries } from '../../../utility/searchQueries.json'; +import { State } from '../../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../store'; -interface Props { - createNotification: (notification: NewNotification) => void; - updateConfig: (formData: SearchForm) => void; - loading: boolean; - customQueries: Query[]; - config: Config; -} +export const SearchSettings = (): JSX.Element => { + const { loading, customQueries, config } = useSelector( + (state: State) => state.config + ); + + const dispatch = useDispatch(); + const { updateConfig } = bindActionCreators(actionCreators, dispatch); -const SearchSettings = (props: Props): JSX.Element => { // Initial state const [formData, setFormData] = useState(searchSettingsTemplate); // Get config useEffect(() => { setFormData({ - ...props.config, + ...config, }); - }, [props.loading]); + }, [loading]); // Form handler const formSubmitHandler = async (e: FormEvent) => { e.preventDefault(); // Save settings - await props.updateConfig(formData); + await updateConfig(formData); }; // Input handler @@ -84,7 +75,7 @@ const SearchSettings = (props: Props): JSX.Element => { value={formData.defaultSearchProvider} onChange={(e) => inputChangeHandler(e)} > - {[...queries, ...props.customQueries].map((query: Query, idx) => { + {[...queries, ...customQueries].map((query: Query, idx) => { const isCustom = idx >= queries.length; return ( @@ -95,6 +86,7 @@ const SearchSettings = (props: Props): JSX.Element => { })} + + + + @@ -142,18 +137,3 @@ const SearchSettings = (props: Props): JSX.Element => { ); }; - -const mapStateToProps = (state: GlobalState) => { - return { - loading: state.config.loading, - customQueries: state.config.customQueries, - config: state.config.config, - }; -}; - -const actions = { - createNotification, - updateConfig, -}; - -export default connect(mapStateToProps, actions)(SearchSettings); diff --git a/client/src/components/Settings/Settings.tsx b/client/src/components/Settings/Settings.tsx index 5df8ec6..0c53693 100644 --- a/client/src/components/Settings/Settings.tsx +++ b/client/src/components/Settings/Settings.tsx @@ -1,4 +1,3 @@ -// import { NavLink, Link, Switch, Route } from 'react-router-dom'; // Typescript @@ -8,21 +7,20 @@ import { Route as SettingsRoute } from '../../interfaces'; import classes from './Settings.module.css'; // Components -import Themer from '../Themer/Themer'; -import WeatherSettings from './WeatherSettings/WeatherSettings'; -import OtherSettings from './OtherSettings/OtherSettings'; -import AppDetails from './AppDetails/AppDetails'; -import StyleSettings from './StyleSettings/StyleSettings'; -import SearchSettings from './SearchSettings/SearchSettings'; +import { Themer } from '../Themer/Themer'; +import { WeatherSettings } from './WeatherSettings/WeatherSettings'; +import { OtherSettings } from './OtherSettings/OtherSettings'; +import { AppDetails } from './AppDetails/AppDetails'; +import { StyleSettings } from './StyleSettings/StyleSettings'; +import { SearchSettings } from './SearchSettings/SearchSettings'; // UI -import { Container } from '../UI/Layout/Layout'; -import Headline from '../UI/Headlines/Headline/Headline'; +import { Container, Headline } from '../UI'; // Data import { routes } from './settings.json'; -const Settings = (): JSX.Element => { +export const Settings = (): JSX.Element => { return ( Go back} /> @@ -57,5 +55,3 @@ const Settings = (): JSX.Element => { ); }; - -export default Settings; diff --git a/client/src/components/Settings/StyleSettings/StyleSettings.tsx b/client/src/components/Settings/StyleSettings/StyleSettings.tsx index 9f45065..b2d7c8e 100644 --- a/client/src/components/Settings/StyleSettings/StyleSettings.tsx +++ b/client/src/components/Settings/StyleSettings/StyleSettings.tsx @@ -2,54 +2,55 @@ import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; import axios from 'axios'; // Redux -import { connect } from 'react-redux'; -import { createNotification } from '../../../store/actions'; +import { useDispatch } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../store'; // Typescript -import { ApiResponse, NewNotification } from '../../../interfaces'; +import { ApiResponse } from '../../../interfaces'; // UI -import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; -import Button from '../../UI/Buttons/Button/Button'; +import { InputGroup, Button } from '../../UI'; -interface ComponentProps { - createNotification: (notification: NewNotification) => void; -} +export const StyleSettings = (): JSX.Element => { + const dispatch = useDispatch(); + const { createNotification } = bindActionCreators(actionCreators, dispatch); -const StyleSettings = (props: ComponentProps): JSX.Element => { const [customStyles, setCustomStyles] = useState(''); useEffect(() => { - axios.get>('/api/config/0/css') - .then(data => setCustomStyles(data.data.data)) - .catch(err => console.log(err.response)); - }, []) + axios + .get>('/api/config/0/css') + .then((data) => setCustomStyles(data.data.data)) + .catch((err) => console.log(err.response)); + }, []); const inputChangeHandler = (e: ChangeEvent) => { e.preventDefault(); setCustomStyles(e.target.value); - } + }; const formSubmitHandler = (e: FormEvent) => { e.preventDefault(); - axios.put>('/api/config/0/css', { styles: customStyles }) + axios + .put>('/api/config/0/css', { styles: customStyles }) .then(() => { - props.createNotification({ + createNotification({ title: 'Success', - message: 'CSS saved. Reload page to see changes' - }) + message: 'CSS saved. Reload page to see changes', + }); }) - .catch(err => console.log(err.response)); - } + .catch((err) => console.log(err.response)); + }; return (
formSubmitHandler(e)}> - +