App state: refactored reducers and actions for apps, categories and bookmarks

This commit is contained in:
Paweł Malak 2021-11-09 13:19:53 +01:00
parent 7e89ab0204
commit adc017c48d
10 changed files with 893 additions and 1 deletions

View file

@ -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<GetAppsAction<undefined | App[]>>) => {
dispatch({
type: ActionType.getApps,
payload: undefined,
});
try {
const res = await axios.get<ApiResponse<App[]>>('/api/apps');
dispatch({
type: ActionType.getAppsSuccess,
payload: res.data.data,
});
} catch (err) {
console.log(err);
}
};
export const pinApp =
(app: App) => async (dispatch: Dispatch<PinAppAction>) => {
try {
const { id, isPinned, name } = app;
const res = await axios.put<ApiResponse<App>>(`/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<AddAppAction>) => {
try {
const res = await axios.post<ApiResponse<App>>('/api/apps', formData);
createNotification({
title: 'Success',
message: `App added`,
});
await dispatch({
type: ActionType.addAppSuccess,
payload: res.data.data,
});
// Sort apps
// dispatch<any>(sortApps());
sortApps();
} catch (err) {
console.log(err);
}
};
export const deleteApp =
(id: number) => async (dispatch: Dispatch<DeleteAppAction>) => {
try {
await axios.delete<ApiResponse<{}>>(`/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<UpdateAppAction>) => {
try {
const res = await axios.put<ApiResponse<App>>(
`/api/apps/${id}`,
formData
);
createNotification({
title: 'Success',
message: `App updated`,
});
await dispatch({
type: ActionType.updateApp,
payload: res.data.data,
});
// Sort apps
dispatch<any>(sortApps());
} catch (err) {
console.log(err);
}
};
export const reorderApps =
(apps: App[]) => async (dispatch: Dispatch<ReorderAppsAction>) => {
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<ApiResponse<{}>>('/api/apps/0/reorder', updateQuery);
dispatch({
type: ActionType.reorderApps,
payload: apps,
});
} catch (err) {
console.log(err);
}
};
export const sortApps = () => async (dispatch: Dispatch<SortAppsAction>) => {
try {
const res = await axios.get<ApiResponse<Config>>('/api/config');
dispatch({
type: ActionType.sortApps,
payload: res.data.data.useOrdering,
});
} catch (err) {
console.log(err);
}
};

View file

@ -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<GetCategoriesAction<undefined | Category[]>>) => {
dispatch({
type: ActionType.getCategories,
payload: undefined,
});
try {
const res = await axios.get<ApiResponse<Category[]>>('/api/categories');
dispatch({
type: ActionType.getCategoriesSuccess,
payload: res.data.data,
});
} catch (err) {
console.log(err);
}
};
export const addCategory =
(formData: NewCategory) => async (dispatch: Dispatch<AddCategoryAction>) => {
try {
const res = await axios.post<ApiResponse<Category>>(
'/api/categories',
formData
);
createNotification({
title: 'Success',
message: `Category ${formData.name} created`,
});
dispatch({
type: ActionType.addCategory,
payload: res.data.data,
});
// dispatch<any>(sortCategories());
sortCategories();
} catch (err) {
console.log(err);
}
};
export const addBookmark =
(formData: NewBookmark | FormData) =>
async (dispatch: Dispatch<AddBookmarkAction>) => {
try {
const res = await axios.post<ApiResponse<Bookmark>>(
'/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<PinCategoryAction>) => {
try {
const { id, isPinned, name } = category;
const res = await axios.put<ApiResponse<Category>>(
`/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<DeleteCategoryAction>) => {
try {
await axios.delete<ApiResponse<{}>>(`/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<UpdateCategoryAction>) => {
try {
const res = await axios.put<ApiResponse<Category>>(
`/api/categories/${id}`,
formData
);
createNotification({
title: 'Success',
message: `Category ${formData.name} updated`,
});
dispatch({
type: ActionType.updateCategory,
payload: res.data.data,
});
// dispatch<any>(sortCategories());
sortCategories();
} catch (err) {
console.log(err);
}
};
export const deleteBookmark =
(bookmarkId: number, categoryId: number) =>
async (dispatch: Dispatch<DeleteBookmarkAction>) => {
try {
await axios.delete<ApiResponse<{}>>(`/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<ApiResponse<Bookmark>>(
`/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<SortCategoriesAction>) => {
try {
const res = await axios.get<ApiResponse<Config>>('/api/config');
dispatch({
type: ActionType.sortCategories,
payload: res.data.data.useOrdering,
});
} catch (err) {
console.log(err);
}
};
export const reorderCategories =
(categories: Category[]) =>
async (dispatch: Dispatch<ReorderCategoriesAction>) => {
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<ApiResponse<{}>>(
'/api/categories/0/reorder',
updateQuery
);
dispatch({
type: ActionType.reorderCategories,
payload: categories,
});
} catch (err) {
console.log(err);
}
};

View file

@ -1,3 +1,5 @@
export * from './theme';
export * from './config';
export * from './notification';
export * from './app';
export * from './bookmark';

View file

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

View file

@ -0,0 +1,38 @@
import { ActionType } from '../action-types';
import { App } from '../../interfaces';
export interface GetAppsAction<T> {
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;
}

View file

@ -0,0 +1,58 @@
import { Bookmark, Category } from '../../interfaces';
import { ActionType } from '../action-types';
export interface GetCategoriesAction<T> {
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[];
}

View file

@ -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<undefined | App[]>
| PinAppAction
| AddAppAction
| DeleteAppAction
| UpdateAppAction
| ReorderAppsAction
| SortAppsAction
// Categories
| GetCategoriesAction<any>
| AddCategoryAction
| PinCategoryAction
| DeleteCategoryAction
| UpdateCategoryAction
| SortCategoriesAction
| ReorderCategoriesAction
// Bookmarks
| AddBookmarkAction
| DeleteBookmarkAction
| UpdateBookmarkAction;

View file

@ -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<App>(state.apps, action.payload),
};
default:
return state;
}
};

View file

@ -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<Category>(state.categories, action.payload),
};
case ActionType.reorderCategories:
return {
...state,
categories: action.payload,
};
default:
return state;
}
};

View file

@ -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<typeof reducers>;