From 5cac35040c7e2ab4932467b1eaf15a1a5ce7b863 Mon Sep 17 00:00:00 2001 From: username Date: Tue, 5 May 2020 17:59:09 +0900 Subject: [PATCH 1/6] feat: add material-table --- package-lock.json | 141 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index ce5254d..8294541 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1241,6 +1241,15 @@ "regenerator-runtime": "^0.13.4" } }, + "@babel/runtime-corejs2": { + "version": "7.9.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.9.2.tgz", + "integrity": "sha512-ayjSOxuK2GaSDJFCtLgHnYjuMyIpViNujWrZo8GUpN60/n7juzJKK5yOo6RFVb0zdU9ACJFK+MsZrUnj3OmXMw==", + "requires": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.4" + } + }, "@babel/template": { "version": "7.8.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", @@ -1314,6 +1323,19 @@ "to-fast-properties": "^2.0.0" } }, + "@date-io/core": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", + "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==" + }, + "@date-io/date-fns": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.3.13.tgz", + "integrity": "sha512-yXxGzcRUPcogiMj58wVgFjc9qUYrCnnU9eLcyNbsQCmae4jPuZCDoIBR21j8ZURsM7GRtU62VOw5yNd4dDHunA==", + "requires": { + "@date-io/core": "^1.3.13" + } + }, "@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -1362,6 +1384,19 @@ "@babel/runtime": "^7.4.4" } }, + "@material-ui/pickers": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.10.tgz", + "integrity": "sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w==", + "requires": { + "@babel/runtime": "^7.6.0", + "@date-io/core": "1.x", + "@types/styled-jsx": "^2.2.8", + "clsx": "^1.0.2", + "react-transition-group": "^4.0.0", + "rifm": "^0.7.0" + } + }, "@material-ui/react-transition-group": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@material-ui/react-transition-group/-/react-transition-group-4.3.0.tgz", @@ -1527,7 +1562,6 @@ "version": "2.2.8", "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz", "integrity": "sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg==", - "dev": true, "requires": { "@types/react": "*" } @@ -2354,6 +2388,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -2539,6 +2578,11 @@ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + }, "core-js-compat": { "version": "3.6.5", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", @@ -2654,6 +2698,14 @@ "urix": "^0.1.0" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -2887,6 +2939,16 @@ "type": "^1.0.1" } }, + "date-fns": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.12.0.tgz", + "integrity": "sha512-qJgn99xxKnFgB1qL4jpxU7Q2t0LOn1p8KMIveef3UZD7kqjT3tpFNNdXJelEHhE+rUgffriXriw/sOSU+cS1Hw==" + }, + "debounce": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", + "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3515,6 +3577,11 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "optional": true }, + "filefy": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz", + "integrity": "sha512-VgoRVOOY1WkTpWH+KBy8zcU1G7uQTVsXqhWEgzryB9A5hg2aqCyZ6aQ/5PSzlqM5+6cnVrX6oYV0XqD3HZSnmQ==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4433,6 +4500,30 @@ "object-visit": "^1.0.0" } }, + "material-table": { + "version": "1.57.2", + "resolved": "https://registry.npmjs.org/material-table/-/material-table-1.57.2.tgz", + "integrity": "sha512-hiJdRTrqu8pyYwSxzmcG1TnR4KWG2gtXrFB3XL9h4ij3A68EOJmlss6VH/LXh3NLlUce1TteK6W7fGa7YcnKGg==", + "requires": { + "@date-io/date-fns": "^1.1.0", + "@material-ui/pickers": "^3.2.2", + "classnames": "^2.2.6", + "date-fns": "^2.0.0-alpha.27", + "debounce": "^1.2.0", + "fast-deep-equal": "2.0.1", + "filefy": "0.1.10", + "prop-types": "^15.6.2", + "react-beautiful-dnd": "11.0.3", + "react-double-scrollbar": "0.0.15" + }, + "dependencies": { + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + } + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -4453,6 +4544,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -5886,6 +5982,11 @@ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" }, + "raf-schd": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz", + "integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -5936,6 +6037,21 @@ "prop-types": "^15.6.2" } }, + "react-beautiful-dnd": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz", + "integrity": "sha512-2FX2SnOlKMmfn90xUHCav7cxRWXwY7FeRa6TzdxWeX7DdP5JTvVQcsWgiOkdbJSj+J+1q1nA9QO4/HQ52D0DAA==", + "requires": { + "@babel/runtime-corejs2": "^7.4.4", + "css-box-model": "^1.1.2", + "memoize-one": "^5.0.4", + "raf-schd": "^4.0.0", + "react-redux": "^7.0.3", + "redux": "^4.0.1", + "tiny-invariant": "^1.0.4", + "use-memo-one": "^1.1.0" + } + }, "react-display-name": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/react-display-name/-/react-display-name-0.2.5.tgz", @@ -5953,6 +6069,11 @@ "scheduler": "^0.19.1" } }, + "react-double-scrollbar": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz", + "integrity": "sha1-6RWrjLO5WYdwdfSUNt6/2wQoj+Q=" + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6241,6 +6362,14 @@ "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" }, + "rifm": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz", + "integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==", + "requires": { + "@babel/runtime": "^7.3.1" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -7047,6 +7176,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, "tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -7379,6 +7513,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-memo-one": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz", + "integrity": "sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==" + }, "use-subscription": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.1.1.tgz", diff --git a/package.json b/package.json index 0aaf1b2..e03a99c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@reduxjs/toolkit": "^1.3.5", "compression": "^1.7.4", "express": "^4.17.1", + "material-table": "^1.57.2", "next": "^9.3.5", "next-redux-wrapper": "^6.0.0-rc.7", "react": "^16.13.1", From 614ae20c0170fca2a3be1a8475271a6fdc6aeb6d Mon Sep 17 00:00:00 2001 From: username Date: Tue, 5 May 2020 17:59:49 +0900 Subject: [PATCH 2/6] feat: ignore .idea directory --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index df5dd2a..07d2263 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,8 @@ node_modules # Optional REPL history .node_repl_history +# Intellij IDEA +.idea ## Application .next From a67e02eda054ea35a7a25002312120f20be8be75 Mon Sep 17 00:00:00 2001 From: username Date: Tue, 5 May 2020 18:05:49 +0900 Subject: [PATCH 3/6] refactor: refactor structure --- store/configureStore.ts | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/store/configureStore.ts b/store/configureStore.ts index 93f310c..d532cb9 100644 --- a/store/configureStore.ts +++ b/store/configureStore.ts @@ -5,22 +5,17 @@ import { } from "@reduxjs/toolkit" import { MakeStore } from "next-redux-wrapper" import { Env } from "../constants" -import { combinedReducers } from "./reducers" -import { InitialStateType } from "./states" +import { rootReducer, RootState } from "./reducers" /** - * Create redux store - * @see https://redux-toolkit.js.org/api/configureStore#full-example + * @see https://redux-toolkit.js.org/usage/usage-with-typescript#correct-typings-for-the-dispatch-type */ -export const makeStore: MakeStore = ( - initialState?: InitialStateType -): EnhancedStore => { - const middlewares = [...getDefaultMiddleware()] - const store = configureStore({ - reducer: combinedReducers, - middleware: middlewares, - devTools: Env.NODE_ENV === "development", - preloadedState: initialState, - }) - return store -} +const middlewares = [...getDefaultMiddleware()] + +const store = configureStore({ + reducer: rootReducer, + middleware: middlewares, + devTools: Env.NODE_ENV === "development", +}) + +export const makeStore: MakeStore = (_?: RootState): EnhancedStore => store From 38d39a40e765dfacb10a0d08f70a46dbdaa96650 Mon Sep 17 00:00:00 2001 From: username Date: Tue, 5 May 2020 18:06:26 +0900 Subject: [PATCH 4/6] refactor: deleted because of automatic generation. --- store/states.ts | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 store/states.ts diff --git a/store/states.ts b/store/states.ts deleted file mode 100644 index 2635cf7..0000000 --- a/store/states.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { CounterStateType } from "./counter" -import { PageStateType } from "./page" - -/** - * Initial state tree - */ -export type InitialStateType = { - counter: Readonly - page: Readonly -} From 92531d747b600cd0ef46918b177df192ea53f1c9 Mon Sep 17 00:00:00 2001 From: username Date: Tue, 5 May 2020 18:18:58 +0900 Subject: [PATCH 5/6] feat: add todo sample application --- components/molecules/TodoList.tsx | 80 +++++++++++++++++++++++++++++ components/molecules/index.ts | 1 + constants/Page.ts | 12 ++++- hooks/index.ts | 1 + hooks/useTodo.ts | 83 +++++++++++++++++++++++++++++++ model/ApiErrorResponse.ts | 8 +++ model/TestData.ts | 14 ++++++ model/Todo.tsx | 10 ++++ model/index.ts | 1 + pages/_document.tsx | 4 ++ pages/api/todo/[id].ts | 73 +++++++++++++++++++++++++++ pages/api/todo/index.ts | 51 +++++++++++++++++++ pages/index.tsx | 2 +- pages/redux.tsx | 2 +- pages/todo/index.tsx | 47 +++++++++++++++++ store/counter/counter.ts | 62 ++++++++++++----------- store/featureKey.ts | 8 +++ store/page/page.ts | 58 ++++++++++----------- store/reducers.ts | 11 +++- store/todo/action.ts | 81 ++++++++++++++++++++++++++++++ store/todo/index.ts | 4 ++ store/todo/reducer.ts | 82 ++++++++++++++++++++++++++++++ store/todo/selector.ts | 39 +++++++++++++++ store/todo/state.ts | 16 ++++++ types/redux-thunk.d.ts | 15 ++++++ 25 files changed, 702 insertions(+), 63 deletions(-) create mode 100644 components/molecules/TodoList.tsx create mode 100644 hooks/useTodo.ts create mode 100644 model/ApiErrorResponse.ts create mode 100644 model/TestData.ts create mode 100644 model/Todo.tsx create mode 100644 model/index.ts create mode 100644 pages/api/todo/[id].ts create mode 100644 pages/api/todo/index.ts create mode 100644 pages/todo/index.tsx create mode 100644 store/featureKey.ts create mode 100644 store/todo/action.ts create mode 100644 store/todo/index.ts create mode 100644 store/todo/reducer.ts create mode 100644 store/todo/selector.ts create mode 100644 store/todo/state.ts create mode 100644 types/redux-thunk.d.ts diff --git a/components/molecules/TodoList.tsx b/components/molecules/TodoList.tsx new file mode 100644 index 0000000..159ae3b --- /dev/null +++ b/components/molecules/TodoList.tsx @@ -0,0 +1,80 @@ +import MaterialTable from "material-table" +import React, { useEffect } from "react" +import { useTodo } from "../../hooks" + +type Props = {} + +/** + * TODO list + * @param props Props + */ +export const TodoList = function (props: Props) { + const {} = props + const { + isFetching, + fetchAllTodos, + addTodo, + editTodo, + deleteTodo, + todos, + } = useTodo() + + useEffect(() => { + fetchAllTodos() + }, []) + + return ( + + new Promise((resolve, reject) => { + addTodo({ + todo: newData, + }) + .then(() => resolve(todos)) + .catch((e) => reject(e)) + }), + onRowUpdate: (newData, _) => + new Promise((resolve, reject) => { + editTodo({ + todo: newData, + }) + .then((payload) => resolve(payload)) + .catch((e) => reject(e)) + }), + onRowDelete: (oldData) => + new Promise((resolve, reject) => { + deleteTodo({ + id: oldData.id, + }) + .then(() => resolve(todos)) + .catch((e) => reject(e)) + }), + }} + /> + ) +} diff --git a/components/molecules/index.ts b/components/molecules/index.ts index bc9f7eb..26621be 100644 --- a/components/molecules/index.ts +++ b/components/molecules/index.ts @@ -1,2 +1,3 @@ export * from "./NextListItem" export * from "./PageHeader" +export * from "./TodoList" diff --git a/constants/Page.ts b/constants/Page.ts index 59319ac..7ffd6b1 100644 --- a/constants/Page.ts +++ b/constants/Page.ts @@ -1,5 +1,5 @@ import { Color } from "@material-ui/core" -import { blue, pink, red } from "@material-ui/core/colors" +import { blue, pink, red, yellow } from "@material-ui/core/colors" import { SvgIconProps } from "@material-ui/core/SvgIcon" import { Home, Info, Save } from "@material-ui/icons" import { IEnum } from "." @@ -34,6 +34,16 @@ export class Page implements IEnum { Save, blue ) + public static readonly TODO = new Page( + 3, + "TODO", + "TODO sample", + "TODO sample | sample", + "The TODO sample application using createAsyncThunk and createEntityAdapter.", + "/todo", + Save, + yellow + ) public static readonly ERROR = new Page( 99, "Error", diff --git a/hooks/index.ts b/hooks/index.ts index fe73dbd..9b19c43 100644 --- a/hooks/index.ts +++ b/hooks/index.ts @@ -1,2 +1,3 @@ export * from "./useCounter" export * from "./usePage" +export * from "./useTodo" diff --git a/hooks/useTodo.ts b/hooks/useTodo.ts new file mode 100644 index 0000000..069ab07 --- /dev/null +++ b/hooks/useTodo.ts @@ -0,0 +1,83 @@ +import { unwrapResult } from "@reduxjs/toolkit" +import { useCallback } from "react" +import { useDispatch, useSelector } from "react-redux" +import { Todo } from "../model" +import { + addTodoAction, + deleteTodoAction, + editTodoAction, + fetchAllTodosAction, + fetchTodoAction, +} from "../store/todo/action" +import { + allTodoSelector, + isFetchingSelector, + todoSelector, +} from "../store/todo/selector" + +/** + * TODO custom hook + */ +export const useTodo = () => { + const dispatch = useDispatch() + const isFetching = useSelector(isFetchingSelector) + const todo = useSelector(todoSelector) + const todos = useSelector(allTodoSelector)?.map((t) => ({ + id: t.id, + name: t.name, + complete: t.complete, + createdAt: t.createdAt, + updatedAt: t.updatedAt, + })) + + const fetchAllTodos = useCallback( + (arg?: { offset?: number; limit?: number }) => { + return dispatch( + fetchAllTodosAction({ + offset: arg?.offset || 0, + limit: arg?.limit || 5, + }) + ).then(unwrapResult) + }, + [dispatch] + ) + + const fetchTodo = useCallback( + (arg: { id: number }) => { + return dispatch(fetchTodoAction(arg)).then(unwrapResult) + }, + [dispatch] + ) + + const addTodo = useCallback( + (arg: { todo: Todo }) => { + return dispatch(addTodoAction(arg)).then(unwrapResult) + }, + [dispatch] + ) + + const editTodo = useCallback( + (arg: { todo: Todo }) => { + return dispatch(editTodoAction(arg)).then(unwrapResult) + }, + [dispatch] + ) + + const deleteTodo = useCallback( + (arg: { id: number }) => { + return dispatch(deleteTodoAction(arg)).then(unwrapResult) + }, + [dispatch] + ) + + return { + isFetching, + todos, + todo, + fetchAllTodos, + fetchTodo, + addTodo, + editTodo, + deleteTodo, + } as const +} diff --git a/model/ApiErrorResponse.ts b/model/ApiErrorResponse.ts new file mode 100644 index 0000000..7c005a3 --- /dev/null +++ b/model/ApiErrorResponse.ts @@ -0,0 +1,8 @@ +/** + * Api error response + */ +export type ApiErrorResponse = { + statusCode: number + message: string + error?: Error +} diff --git a/model/TestData.ts b/model/TestData.ts new file mode 100644 index 0000000..7ed9b15 --- /dev/null +++ b/model/TestData.ts @@ -0,0 +1,14 @@ +import { Todo } from "./Todo" + +// test data +export let testTodos: Todo[] = [] + +for (let i = 0; i < 6; i++) { + testTodos.push({ + id: i + 1, + name: `Task ${i + 1}`, + complete: i % 2 == 0, + createdAt: new Date(), + updatedAt: new Date(), + }) +} diff --git a/model/Todo.tsx b/model/Todo.tsx new file mode 100644 index 0000000..d93c0e9 --- /dev/null +++ b/model/Todo.tsx @@ -0,0 +1,10 @@ +/** + * TODO model + */ +export type Todo = { + id: number + name: string + complete: boolean + createdAt: Date + updatedAt: Date +} diff --git a/model/index.ts b/model/index.ts new file mode 100644 index 0000000..d5dbbf5 --- /dev/null +++ b/model/index.ts @@ -0,0 +1 @@ +export * from "./Todo" diff --git a/pages/_document.tsx b/pages/_document.tsx index 2e77438..05562a1 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -61,6 +61,10 @@ class MyDocument extends Document { rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" /> +
diff --git a/pages/api/todo/[id].ts b/pages/api/todo/[id].ts new file mode 100644 index 0000000..7648971 --- /dev/null +++ b/pages/api/todo/[id].ts @@ -0,0 +1,73 @@ +import { NextApiRequest, NextApiResponse } from "next" +import { Todo } from "../../../model" +import { ApiErrorResponse } from "../../../model/ApiErrorResponse" +import { testTodos } from "../../../model/TestData" + +/** + * TODO restful-api with path-parameter + * @param req NextApiRequest + * @param res NextApiResponse + */ +export default (req: NextApiRequest, res: NextApiResponse) => { + const { + query: { id }, + method, + body, + } = req + + try { + res.setHeader("Content-Type", "application/json") + + // validation + const idStr = String(id) + if (!/^\d+$/.test(idStr)) { + const error: ApiErrorResponse = { + statusCode: 400, + message: "Please enter the todo-id as a number.", + } + res.status(400).json(error) + return + } + + // find data + const todoId = Number(idStr) + const currentTodo = testTodos + .filter((todo) => todo.id === todoId) + .find((todo) => !!todo) + if (!currentTodo) { + const error: ApiErrorResponse = { + statusCode: 404, + message: `todo ${id} is not found.`, + } + res.status(404).json(error) + return + } + + switch (method) { + case "GET": + res.status(200).json(currentTodo) + break + case "PUT": + const newTodo: Todo = body + newTodo.updatedAt = new Date() + testTodos[todoId] = body + res.status(200).json(newTodo) + break + case "DELETE": + const deleteTargetId = testTodos.findIndex((todo) => todo.id === todoId) + testTodos.splice(deleteTargetId, 1) + res.status(204).end() + break + default: + res.setHeader("Allow", ["GET", "POST", "PUT", "DELETE"]) + res.status(405).end(`Method ${method} Not Allowed`) + break + } + } catch (e) { + const error: ApiErrorResponse = { + statusCode: 500, + message: `Internal server error. ${e}`, + } + res.status(500).json(error) + } +} diff --git a/pages/api/todo/index.ts b/pages/api/todo/index.ts new file mode 100644 index 0000000..28ef923 --- /dev/null +++ b/pages/api/todo/index.ts @@ -0,0 +1,51 @@ +import { NextApiRequest, NextApiResponse } from "next" +import { Todo } from "../../../model" +import { ApiErrorResponse } from "../../../model/ApiErrorResponse" +import { testTodos } from "../../../model/TestData" + +/** + * TODO restful-api + * @param req NextApiRequest + * @param res NextApiResponse + */ +export default (req: NextApiRequest, res: NextApiResponse) => { + const { method, body } = req + + try { + res.setHeader("Content-Type", "application/json") + + switch (method) { + case "GET": + res.status(200).json(testTodos) + break + case "POST": + if (!body) { + const error: ApiErrorResponse = { + statusCode: 400, + message: `Request body is required.`, + } + res.status(400).json(error) + return + } + + const newTodo: Todo = body + const lastTodo = testTodos.slice(-1)[0] + newTodo.id = lastTodo.id + 1 + newTodo.createdAt = new Date() + newTodo.updatedAt = new Date() + testTodos.push(newTodo) + res.status(201).json(newTodo) + break + default: + res.setHeader("Allow", ["GET", "POST"]) + res.status(405).end(`Method ${method} Not Allowed`) + break + } + } catch (e) { + const error: ApiErrorResponse = { + statusCode: 500, + message: `Internal server error. ${e}`, + } + res.status(500).json(error) + } +} diff --git a/pages/index.tsx b/pages/index.tsx index 8ac7aaa..708e2cb 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -36,7 +36,7 @@ function Index(props: Props) { } /** - * Server side rendering + * @see https://nextjs.org/docs/api-reference/data-fetching/getInitialProps */ Index.getInitialProps = async (ctx: AppContext): Promise => { const { store } = ctx diff --git a/pages/redux.tsx b/pages/redux.tsx index 9b5b446..f9fb21e 100644 --- a/pages/redux.tsx +++ b/pages/redux.tsx @@ -103,7 +103,7 @@ function Redux(props: Props) { } /** - * Server side rendering + * @see https://nextjs.org/docs/api-reference/data-fetching/getInitialProps */ Redux.getInitialProps = async (ctx: AppContext): Promise => { const { store } = ctx diff --git a/pages/todo/index.tsx b/pages/todo/index.tsx new file mode 100644 index 0000000..65d7c9b --- /dev/null +++ b/pages/todo/index.tsx @@ -0,0 +1,47 @@ +import { createStyles, makeStyles, Theme } from "@material-ui/core" +import React from "react" +import { AppContext } from "../../components/AppContext" +import { SpacingPaper } from "../../components/atoms" +import { TodoList } from "../../components/molecules" +import { HeaderArticleContainer } from "../../components/organisms" +import { Layout } from "../../components/templates" +import { Page } from "../../constants" +import { changePage } from "../../store/page" + +const useStyles = makeStyles((_: Theme) => + createStyles({ + root: {}, + }) +) + +type Props = {} + +function Todo(props: Props) { + const {} = props + const classes = useStyles(props) + + return ( + + + + + + + + ) +} + +/** + * @see https://nextjs.org/docs/api-reference/data-fetching/getInitialProps + */ +Todo.getInitialProps = async (ctx: AppContext): Promise => { + const { store } = ctx + store.dispatch( + changePage({ + id: Page.TODO.id, + }) + ) + return {} +} + +export default Todo diff --git a/store/counter/counter.ts b/store/counter/counter.ts index 269efb5..75500c8 100644 --- a/store/counter/counter.ts +++ b/store/counter/counter.ts @@ -1,47 +1,49 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit" -import { InitialStateType } from "../states" +import { FeatureKey } from "../featureKey" +import { RootState } from "../reducers" -//--------------------------------------------------- -// Payload -//--------------------------------------------------- -export type CounterPayloadType = { +/** + * Payload + */ +export type CounterPayload = { inputNumber: number } -//--------------------------------------------------- -// State -//--------------------------------------------------- -export type CounterStateType = { +/** + * State + */ +export type CounterState = { count: number } -const initialState: CounterStateType = { +const initialState: CounterState = { count: 1, } -//--------------------------------------------------- -// Slice -//--------------------------------------------------- +/** + * Slice + * @see https://redux-toolkit.js.org/api/createslice + */ const slice = createSlice({ - name: "counter", + name: FeatureKey.COUNTER, initialState, reducers: { - increment: (state: CounterStateType): CounterStateType => { + increment: (state: CounterState): CounterState => { return { ...state, count: state.count + 1, } }, - decrement: (state: CounterStateType): CounterStateType => { + decrement: (state: CounterState): CounterState => { return { ...state, count: state.count - 1, } }, calculate: ( - state: CounterStateType, - action: PayloadAction - ): CounterStateType => { + state: CounterState, + action: PayloadAction + ): CounterState => { const { payload } = action return { ...state, @@ -51,18 +53,18 @@ const slice = createSlice({ }, }) -//--------------------------------------------------- -// Reducer -//--------------------------------------------------- +/** + * Reducer + */ export const counterReducer = slice.reducer -//--------------------------------------------------- -// Action -//--------------------------------------------------- +/** + * Action + */ export const { increment, decrement, calculate } = slice.actions -//--------------------------------------------------- -// Selector -//--------------------------------------------------- -export const counterSelector = (state: InitialStateType): CounterStateType => - state.counter +/** + * Selector + * @param state CounterState + */ +export const counterSelector = (state: RootState): CounterState => state.counter diff --git a/store/featureKey.ts b/store/featureKey.ts new file mode 100644 index 0000000..2ed25d4 --- /dev/null +++ b/store/featureKey.ts @@ -0,0 +1,8 @@ +/** + * State feature key (prefix of action name) + */ +export const FeatureKey = { + COUNTER: "COUNTER", + PAGE: "PAGE", + TODO: "TODO", +} as const diff --git a/store/page/page.ts b/store/page/page.ts index 11be14a..e229308 100644 --- a/store/page/page.ts +++ b/store/page/page.ts @@ -1,18 +1,19 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit" import { Page } from "../../constants" -import { InitialStateType } from "../states" +import { FeatureKey } from "../featureKey" +import { RootState } from "../reducers" -//--------------------------------------------------- -// Payload -//--------------------------------------------------- -export type PagePayloadType = { +/** + * Payload + */ +export type PagePayload = { id: number } -//--------------------------------------------------- -// State -//--------------------------------------------------- -export type PageStateType = { +/** + * State + */ +export type PageState = { id: number pageTitle: string pageDescription: string @@ -20,7 +21,7 @@ export type PageStateType = { metaDescription: string } -const initialState: PageStateType = { +const initialState: PageState = { id: Page.TOP.id, pageTitle: Page.TOP.pageTitle, pageDescription: Page.TOP.pageDescription, @@ -28,17 +29,18 @@ const initialState: PageStateType = { metaDescription: Page.TOP.metaDescription, } -//--------------------------------------------------- -// Slice -//--------------------------------------------------- +/** + * Slice + * @see https://redux-toolkit.js.org/api/createslice + */ const slice = createSlice({ - name: "page", + name: FeatureKey.PAGE, initialState, reducers: { changePage: ( - state: PageStateType, - action: PayloadAction - ): PageStateType => { + state: PageState, + action: PayloadAction + ): PageState => { const { id } = action.payload const selectedPage: Page = Page.of(id) return { @@ -53,18 +55,18 @@ const slice = createSlice({ }, }) -//--------------------------------------------------- -// Reducer -//--------------------------------------------------- +/** + * Reducer + */ export const pageReducer = slice.reducer -//--------------------------------------------------- -// Action -//--------------------------------------------------- +/** + * Action + */ export const { changePage } = slice.actions -//--------------------------------------------------- -// Selector -//--------------------------------------------------- -export const pageSelector = (state: InitialStateType): PageStateType => - state.page +/** + * Selector + * @param state PageStateType + */ +export const pageSelector = (state: RootState): PageState => state.page diff --git a/store/reducers.ts b/store/reducers.ts index bc6f23a..7a3a828 100644 --- a/store/reducers.ts +++ b/store/reducers.ts @@ -1,9 +1,16 @@ import { combineReducers } from "redux" import { counterReducer } from "./counter" import { pageReducer } from "./page" -import { InitialStateType } from "./states" +import { todoReducer } from "./todo" -export const combinedReducers = combineReducers({ +/** + * Combine reducers + * @see https://redux-toolkit.js.org/usage/usage-with-typescript + */ +export const rootReducer = combineReducers({ counter: counterReducer, page: pageReducer, + todo: todoReducer, }) + +export type RootState = ReturnType diff --git a/store/todo/action.ts b/store/todo/action.ts new file mode 100644 index 0000000..ff9d707 --- /dev/null +++ b/store/todo/action.ts @@ -0,0 +1,81 @@ +import { createAsyncThunk } from "@reduxjs/toolkit" +import { Todo } from "../../model" +import { FeatureKey } from "../featureKey" + +/** + * Fetch all todo action + */ +export const fetchAllTodosAction = createAsyncThunk( + `${FeatureKey.TODO}/fetchAll`, + async (arg: { offset: number; limit: number }) => { + const { offset, limit } = arg + const url = `/api/todo?offset=${offset}&limit=${limit}` + const result: Todo[] = await fetch(url, { + method: "get", + }).then((response: Response) => response.json()) + return { todos: result } + } +) + +/** + * Fetch todo action + */ +export const fetchTodoAction = createAsyncThunk( + `${FeatureKey.TODO}/fetch`, + async (arg: { id: number }) => { + const { id } = arg + const url = `/api/todo/${id}` + const result: Todo = await fetch(url, { + method: "get", + }).then((response: Response) => response.json()) + return { todo: result } + } +) + +/** + * Add todo action + */ +export const addTodoAction = createAsyncThunk( + `${FeatureKey.TODO}/add`, + async (arg: { todo: Todo }) => { + const { todo } = arg + const url = `/api/todo` + const result: Todo = await fetch(url, { + method: "post", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(todo), + }).then((response: Response) => response.json()) + return { todo: result } + } +) + +/** + * Edit todo action + */ +export const editTodoAction = createAsyncThunk( + `${FeatureKey.TODO}/edit`, + async (arg: { todo: Todo }) => { + const { todo } = arg + const url = `/api/todo/${todo.id}` + const result: Todo = await fetch(url, { + method: "put", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(todo), + }).then((response: Response) => response.json()) + return { todo: result } + } +) + +/** + * Delete todo action + */ +export const deleteTodoAction = createAsyncThunk( + `${FeatureKey.TODO}/delete`, + async (arg: { id: number }) => { + const { id } = arg + const url = `/api/todo/${id}` + await fetch(url, { + method: "delete", + }) + } +) diff --git a/store/todo/index.ts b/store/todo/index.ts new file mode 100644 index 0000000..f267fc4 --- /dev/null +++ b/store/todo/index.ts @@ -0,0 +1,4 @@ +export * from "./action" +export * from "./reducer" +export * from "./selector" +export * from "./state" diff --git a/store/todo/reducer.ts b/store/todo/reducer.ts new file mode 100644 index 0000000..a555507 --- /dev/null +++ b/store/todo/reducer.ts @@ -0,0 +1,82 @@ +import { ActionReducerMapBuilder, createReducer } from "@reduxjs/toolkit" +import { + addTodoAction, + deleteTodoAction, + editTodoAction, + fetchAllTodosAction, + fetchTodoAction, +} from "./action" +import { adapter, initialState, TodoState } from "./state" + +/** + * TODO reducer + */ +export const todoReducer = createReducer( + initialState, + (builder: ActionReducerMapBuilder) => + builder + .addCase(fetchAllTodosAction.pending, (state) => { + return { ...state, isFetching: true } + }) + .addCase(fetchAllTodosAction.fulfilled, (state, action) => { + const { todos } = action.payload + return adapter.setAll({ ...state, isFetching: false }, todos) + }) + .addCase(fetchAllTodosAction.rejected, (state) => { + return { ...state, isFetching: false } + }) + //------------------------------------------------------------------------------- + .addCase(fetchTodoAction.pending, (state, action) => { + const { id } = action.meta.arg + return { ...state, isFetching: true, selectedId: id } + }) + .addCase(fetchTodoAction.fulfilled, (state, action) => { + const { todo } = action.payload + return adapter.upsertOne({ ...state, isFetching: false }, todo) + }) + .addCase(fetchTodoAction.rejected, (state) => { + return { ...state, isFetching: false } + }) + //------------------------------------------------------------------------------- + .addCase(addTodoAction.pending, (state, action) => { + const { todo } = action.meta.arg + return { ...state, isFetching: true, selectedId: todo?.id } + }) + .addCase(addTodoAction.fulfilled, (state, action) => { + const { todo } = action.payload + return adapter.addOne({ ...state, isFetching: false }, todo) + }) + .addCase(addTodoAction.rejected, (state) => { + return { ...state, isFetching: false } + }) + //------------------------------------------------------------------------------- + .addCase(editTodoAction.pending, (state, action) => { + const { todo } = action.meta.arg + return { ...state, isFetching: true, selectedId: todo?.id } + }) + .addCase(editTodoAction.fulfilled, (state, action) => { + const { todo } = action.payload + return adapter.updateOne( + { ...state, isFetching: false }, + { + id: todo.id, + changes: todo, + } + ) + }) + .addCase(editTodoAction.rejected, (state) => { + return { ...state, isFetching: false } + }) + //------------------------------------------------------------------------------- + .addCase(deleteTodoAction.pending, (state, action) => { + const { id } = action.meta.arg + return { ...state, isFetching: true, selectedId: id } + }) + .addCase(deleteTodoAction.fulfilled, (state, action) => { + const { id } = action.meta.arg + return adapter.removeOne({ ...state, isFetching: false }, id) + }) + .addCase(deleteTodoAction.rejected, (state) => { + return { ...state, isFetching: false } + }) +) diff --git a/store/todo/selector.ts b/store/todo/selector.ts new file mode 100644 index 0000000..bcdb610 --- /dev/null +++ b/store/todo/selector.ts @@ -0,0 +1,39 @@ +import { createSelector } from "@reduxjs/toolkit" +import { RootState } from "../reducers" +import { adapter, TodoState } from "./state" + +const { selectAll, selectEntities } = adapter.getSelectors() + +const featureStateSelector = (state: RootState) => state.todo + +const entitiesSelector = createSelector(featureStateSelector, selectEntities) + +/** + * isFetching selector + */ +export const isFetchingSelector = createSelector( + featureStateSelector, + (state: TodoState) => state?.isFetching +) + +/** + * selectedId selector + */ +export const selectedIdSelector = createSelector( + featureStateSelector, + (state: TodoState) => state?.selectedId +) + +/** + * all todo selector + */ +export const allTodoSelector = createSelector(featureStateSelector, selectAll) + +/** + * todo selector + */ +export const todoSelector = createSelector( + entitiesSelector, + selectedIdSelector, + (entities, id) => (id ? entities[id] || null : null) +) diff --git a/store/todo/state.ts b/store/todo/state.ts new file mode 100644 index 0000000..44d033a --- /dev/null +++ b/store/todo/state.ts @@ -0,0 +1,16 @@ +import { createEntityAdapter, EntityState } from "@reduxjs/toolkit" +import { Todo } from "../../model" + +export interface TodoState extends EntityState { + isFetching: boolean + selectedId: number | null +} + +export const adapter = createEntityAdapter({ + selectId: (todo: Todo) => todo.id, +}) + +export const initialState: TodoState = adapter.getInitialState({ + isFetching: false, + selectedId: null, +}) diff --git a/types/redux-thunk.d.ts b/types/redux-thunk.d.ts new file mode 100644 index 0000000..74f19ed --- /dev/null +++ b/types/redux-thunk.d.ts @@ -0,0 +1,15 @@ +import { ThunkAction } from "redux-thunk" + +// Dispatch overload for redux-thunk +// https://github.com/reduxjs/redux-thunk/pull/278 +declare module "redux" { + /* + * Overload to add thunk support to Redux's dispatch() function. + * Useful for react-redux or any other library which could use this type. + */ + export interface Dispatch = AnyAction> { + ( + thunkAction: ThunkAction + ): TReturnType + } +} From 5367564cd30a55e51727249d2f22ec95705cbc99 Mon Sep 17 00:00:00 2001 From: username Date: Tue, 5 May 2020 18:19:04 +0900 Subject: [PATCH 6/6] docs: add createSlice and createAsyncThunk and createEntityAdapter. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 5409fc3..5433034 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,14 @@ This is a sample for `server-side rendering` using `TypeScript` , `Next.js` , `Redux Toolkit` , and `Material-UI` . +I also used the latest features such as `createSlice` , `createAsyncThunk` , and `createEntityAdapter` . + `VSCode` , `prettier` and `TSLint` provide real-time formatting, syntax checking and organizing of unused imports. これは、 `TypeScript` , `Next.js` , `Redux Toolkit` , `Material-UI` を使った `サーバーサイドレンダリング` に対応したサンプルです。 +`createSlice` ・ `createAsyncThunk` ・ `createEntityAdapter` といった最新機能も使ってみました。 + `VSCode` と `prettier` と `TSLint` によって、リアルタイムに整形と構文チェックと未使用 import の整理が行われます。 ## Live demo @@ -18,8 +22,14 @@ This is a sample for `server-side rendering` using `TypeScript` , `Next.js` , `R - [Typescript](https://www.typescriptlang.org/) - [Next.js](https://nextjs.org/) - [Material-UI](https://material-ui.com/) +- [material-table](https://material-table.com/#/) - [Redux](https://redux.js.org/) - [Redux Toolkit](https://redux-toolkit.js.org/) + - [createSlice](https://redux-toolkit.js.org/api/createSlice) + - [createAsyncThunk](https://redux-toolkit.js.org/api/createAsyncThunk) + - [createEntityAdapter](https://redux-toolkit.js.org/api/createEntityAdapter) + - [createSelector](https://redux-toolkit.js.org/api/createSelector) + - It using most of the major features of the redux toolkit !! - [TSLint](https://palantir.github.io/tslint/) ## Requirement