├── .dependabot └── config.yml ├── .editorconfig ├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── components ├── AppContext.ts ├── MuiTheme.ts ├── atoms │ ├── SpacingPaper.tsx │ └── index.ts ├── molecules │ ├── NextListItem.tsx │ ├── PageHeader.tsx │ ├── TodoList.tsx │ └── index.ts ├── organisms │ ├── HeaderArticleContainer.tsx │ ├── ResponsiveDrawer.tsx │ ├── Sidenavi.tsx │ └── index.ts └── templates │ ├── Layout.tsx │ └── index.ts ├── constants ├── Env.ts ├── IEnum.ts ├── Page.ts ├── SiteInfo.ts └── index.ts ├── hooks ├── index.ts ├── useCounter.ts ├── usePage.ts └── useTodo.ts ├── model ├── ApiErrorResponse.ts ├── TestData.ts ├── Todo.tsx └── index.ts ├── next-env.d.ts ├── next.config.js ├── now.json ├── package-lock.json ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── _document.tsx ├── _error.tsx ├── api │ └── todo │ │ ├── [id].ts │ │ └── index.ts ├── index.tsx ├── redux.tsx └── todo │ └── index.tsx ├── server.js ├── store ├── configureStore.ts ├── counter │ ├── counter.ts │ └── index.ts ├── featureKey.ts ├── page │ ├── index.ts │ └── page.ts ├── reducers.ts └── todo │ ├── action.ts │ ├── index.ts │ ├── reducer.ts │ ├── selector.ts │ └── state.ts ├── styles └── main.css ├── tsconfig.json └── types ├── nodejs.d.ts ├── redux-thunk.d.ts ├── styled-jsx.d.ts ├── styles.d.ts └── window.d.ts /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | - package_manager: javascript 4 | directory: / 5 | update_schedule: daily 6 | automerged_updates: 7 | - match: 8 | dependency_type: all 9 | update_type: semver:minor 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treetips/typescript-nextjs-redux-toolkit-material-ui-example/5bb587088e1f99f0891dcb724d3deb37d7d7ae34/.env -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | parserOptions: { 4 | project: "tsconfig.json", 5 | sourceType: "module", 6 | ecmaFeatures: { 7 | jsx: true, 8 | }, 9 | useJSXTextNode: true, 10 | }, 11 | plugins: ["@typescript-eslint/eslint-plugin", "react"], 12 | extends: [ 13 | "plugin:@typescript-eslint/eslint-recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "prettier/@typescript-eslint", 16 | "plugin:react/recommended", 17 | "prettier/react", 18 | ], 19 | root: true, 20 | env: { 21 | node: true, 22 | jest: true, 23 | }, 24 | rules: { 25 | "@typescript-eslint/interface-name-prefix": "off", 26 | "@typescript-eslint/explicit-function-return-type": "off", 27 | "@typescript-eslint/no-explicit-any": "off", 28 | "@typescript-eslint/no-unused-vars": [ 29 | "error", 30 | { 31 | argsIgnorePattern: "^_", 32 | }, 33 | ], 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/656f766bfc75f912d611a973158be0fe9e2ba2f2/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 30 | node_modules 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | 38 | # Intellij IDEA 39 | .idea 40 | 41 | ## Application 42 | .next 43 | .now 44 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "es5", 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.DS_Store": true, 4 | "**/node_modules": true 5 | }, 6 | "search.exclude": { 7 | "**/node_modules": true, 8 | ".next": true 9 | }, 10 | "files.insertFinalNewline": true, 11 | "javascript.format.enable": false, 12 | "eslint.enable": true, 13 | "eslint.format.enable": true, 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll.eslint": true 16 | }, 17 | "[javascript]": { 18 | "editor.formatOnSave": true, 19 | "editor.formatOnPaste": true, 20 | "editor.codeActionsOnSave": { 21 | "source.organizeImports": true 22 | } 23 | }, 24 | "[typescript]": { 25 | "editor.formatOnSave": true, 26 | "editor.formatOnPaste": true, 27 | "editor.codeActionsOnSave": { 28 | "source.organizeImports": true 29 | } 30 | }, 31 | "[typescriptreact]": { 32 | "editor.formatOnSave": true, 33 | "editor.formatOnPaste": true, 34 | "editor.codeActionsOnSave": { 35 | "source.organizeImports": true 36 | } 37 | }, 38 | "[jsonc]": { 39 | "editor.formatOnSave": true, 40 | "editor.formatOnPaste": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typescript-nextjs-redux-toolkit-material-ui-example 2 | 3 | This is a sample for `server-side rendering` using `TypeScript` , `Next.js` , `Redux Toolkit` , and `Material-UI` . 4 | 5 | I also used the latest features such as `createSlice` , `createAsyncThunk` , and `createEntityAdapter` . 6 | 7 | `VSCode` , `prettier` and `ESLint` provide real-time formatting, syntax checking and organizing of unused imports. 8 | 9 | これは、 `TypeScript` , `Next.js` , `Redux Toolkit` , `Material-UI` を使った `サーバーサイドレンダリング` に対応したサンプルです。 10 | 11 | `createSlice` ・ `createAsyncThunk` ・ `createEntityAdapter` といった最新機能も使ってみました。 12 | 13 | `VSCode` と `prettier` と `ESLint` によって、リアルタイムに整形と構文チェックと未使用 import の整理が行われます。 14 | 15 | ## Live demo 16 | 17 | [live demo](https://typescript-nextjs-redux-toolkit-material-ui-example.now.sh/) 18 | 19 | ## Features 20 | 21 | - [Visual Studio Code](https://code.visualstudio.com/) 22 | - [Typescript](https://www.typescriptlang.org/) 23 | - [Next.js](https://nextjs.org/) 24 | - [Material-UI](https://material-ui.com/) 25 | - [material-table](https://material-table.com/#/) 26 | - [Redux](https://redux.js.org/) 27 | - [Redux Toolkit](https://redux-toolkit.js.org/) 28 | - [createSlice](https://redux-toolkit.js.org/api/createSlice) 29 | - [createAsyncThunk](https://redux-toolkit.js.org/api/createAsyncThunk) 30 | - [createEntityAdapter](https://redux-toolkit.js.org/api/createEntityAdapter) 31 | - [createSelector](https://redux-toolkit.js.org/api/createSelector) 32 | - It using most of the major features of the redux toolkit !! 33 | - [ESLint](https://eslint.org/) 34 | 35 | ## Requirement 36 | 37 | - [Google Chrome](https://www.google.com/intl/ja_ALL/chrome/) 38 | - [Visual Studio Code](https://code.visualstudio.com/) 39 | - TypeScript v3.7 or higher( [require Optional Chaining](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#optional-chaining) ) 40 | 41 | ## Install Google Chrome addon 42 | 43 | - [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ja) 44 | 45 | ## Recommended VSCode addons 46 | 47 | - [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) 48 | - [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 49 | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 50 | 51 | ## Usage 52 | 53 | ### Download and install 54 | 55 | ```bash 56 | git clone https://github.com/treetips/typescript-nextjs-redux-toolkit-material-ui-example.git 57 | cd typescript-nextjs-redux-toolkit-material-ui-example 58 | npm i 59 | ``` 60 | 61 | ### Start local 62 | 63 | ```bash 64 | npm run dev 65 | ``` 66 | 67 | ### Build and start production express server 68 | 69 | ```bash 70 | npm run build 71 | npm start 72 | ``` 73 | 74 | ## Related repository 75 | 76 | * [typescript-nextjs-redux-material-ui-example](https://github.com/treetips/typescript-nextjs-redux-material-ui-example) 77 | -------------------------------------------------------------------------------- /components/AppContext.ts: -------------------------------------------------------------------------------- 1 | import { DocumentContext } from "next/document" 2 | import { Store } from "redux" 3 | 4 | /** 5 | * NextDocumentContext with redux store context 6 | * @tree 7 | */ 8 | export type AppContext = DocumentContext & { 9 | readonly store: Store 10 | } 11 | -------------------------------------------------------------------------------- /components/MuiTheme.ts: -------------------------------------------------------------------------------- 1 | import green from "@material-ui/core/colors/green" 2 | import grey from "@material-ui/core/colors/grey" 3 | import { createMuiTheme } from "@material-ui/core/styles" 4 | 5 | /** 6 | * material-ui theme color pallete 7 | * @see https://material-ui.com/style/color/ 8 | */ 9 | export const MuiTheme = createMuiTheme({ 10 | palette: { 11 | primary: { 12 | light: grey[700], 13 | main: grey[800], 14 | dark: grey[900], 15 | }, 16 | secondary: { 17 | light: green[300], 18 | main: green[500], 19 | dark: green[700], 20 | }, 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /components/atoms/SpacingPaper.tsx: -------------------------------------------------------------------------------- 1 | import { Paper } from "@material-ui/core" 2 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 3 | import React from "react" 4 | 5 | const useStyles = makeStyles((theme: Theme) => 6 | createStyles({ 7 | root: (props: Props) => ({ 8 | padding: props.noPadding === true ? theme.spacing(0) : theme.spacing(2), 9 | marginBottom: theme.spacing(2), 10 | }), 11 | }) 12 | ) 13 | 14 | type Props = { 15 | /** 16 | * shildren 17 | */ 18 | children: React.ReactNode 19 | /** 20 | * zero-padding flag 21 | */ 22 | noPadding?: boolean 23 | } 24 | 25 | /** 26 | * Paper with spacing 27 | * @param props IProps 28 | */ 29 | export const SpacingPaper = (props: Props) => { 30 | const { children } = props 31 | const classes = useStyles(props) 32 | return ( 33 | 34 | {children} 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /components/atoms/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SpacingPaper" 2 | -------------------------------------------------------------------------------- /components/molecules/NextListItem.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | ListItem, 4 | ListItemAvatar, 5 | ListItemText, 6 | } from "@material-ui/core" 7 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 8 | import Link from "next/link" 9 | import React from "react" 10 | 11 | const useStyles = makeStyles((theme: Theme) => 12 | createStyles({ 13 | root: {}, 14 | anchorLink: { 15 | width: "100%", 16 | textDecoration: "none", 17 | }, 18 | listItemPrimary: { 19 | color: theme.palette.primary.contrastText, 20 | fontWeight: "bold", 21 | fontSize: "20px", 22 | }, 23 | listItemSecondary: { 24 | color: theme.palette.primary.contrastText, 25 | }, 26 | }) 27 | ) 28 | 29 | type Props = { 30 | /** 31 | * 32 | */ 33 | href: string 34 | /** 35 | * 36 | */ 37 | primary: React.ReactNode 38 | /** 39 | * 40 | */ 41 | secondary?: React.ReactNode 42 | /** 43 | * List item icon 44 | */ 45 | icon: JSX.Element 46 | /** 47 | * class 48 | */ 49 | className?: string 50 | /** 51 | * onClick event 52 | */ 53 | onClick?: (event: React.MouseEvent) => void 54 | } 55 | 56 | /** 57 | * Next.js optimized 58 | * @param props Props 59 | */ 60 | export const NextListItem = function (props: Props) { 61 | const { className, href, icon, primary, secondary, onClick } = props 62 | const classes = useStyles(props) 63 | const AvatorIcon = () => icon 64 | return ( 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {primary}} 75 | secondary={ 76 | {secondary} 77 | } 78 | /> 79 | 80 | 81 | 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /components/molecules/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Paper, Typography } from "@material-ui/core" 2 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 3 | import React from "react" 4 | import { usePage } from "../../hooks" 5 | 6 | const useStyles = makeStyles((theme: Theme) => 7 | createStyles({ 8 | root: { 9 | backgroundColor: theme.palette.primary.main, 10 | color: theme.palette.primary.contrastText, 11 | padding: theme.spacing(2), 12 | textAlign: "center", 13 | }, 14 | title: { 15 | display: "flex", 16 | justifyContent: "center", 17 | alignItems: "center", 18 | fontWeight: "bold", 19 | fontSize: "3em", 20 | padding: theme.spacing(2), 21 | }, 22 | description: {}, 23 | }) 24 | ) 25 | 26 | type Props = {} 27 | 28 | /** 29 | * Page header component 30 | * @param props Props 31 | */ 32 | export const PageHeader = function (props: Props) { 33 | const classes = useStyles(props) 34 | const { selectedPage } = usePage() 35 | 36 | return ( 37 | 38 | 39 | {selectedPage.pageTitle} 40 | 41 | 46 | {selectedPage.metaDescription} 47 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /components/molecules/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import MaterialTable from "material-table" 2 | import React, { useEffect } from "react" 3 | import { useTodo } from "../../hooks" 4 | 5 | type Props = {} 6 | 7 | /** 8 | * TODO list 9 | * @param props Props 10 | */ 11 | export const TodoList = function (props: Props) { 12 | const {} = props 13 | const { 14 | isFetching, 15 | fetchAllTodos, 16 | addTodo, 17 | editTodo, 18 | deleteTodo, 19 | todos, 20 | } = useTodo() 21 | 22 | useEffect(() => { 23 | fetchAllTodos() 24 | }, []) 25 | 26 | return ( 27 | 54 | new Promise((resolve, reject) => { 55 | addTodo({ 56 | todo: newData, 57 | }) 58 | .then(() => resolve(todos)) 59 | .catch((e) => reject(e)) 60 | }), 61 | onRowUpdate: (newData, _) => 62 | new Promise((resolve, reject) => { 63 | editTodo({ 64 | todo: newData, 65 | }) 66 | .then((payload) => resolve(payload)) 67 | .catch((e) => reject(e)) 68 | }), 69 | onRowDelete: (oldData) => 70 | new Promise((resolve, reject) => { 71 | deleteTodo({ 72 | id: oldData.id, 73 | }) 74 | .then(() => resolve(todos)) 75 | .catch((e) => reject(e)) 76 | }), 77 | }} 78 | /> 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /components/molecules/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./NextListItem" 2 | export * from "./PageHeader" 3 | export * from "./TodoList" 4 | -------------------------------------------------------------------------------- /components/organisms/HeaderArticleContainer.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 2 | import React from "react" 3 | import { PageHeader } from "../molecules" 4 | 5 | const useStyles = makeStyles((theme: Theme) => 6 | createStyles({ 7 | root: {}, 8 | contentsContainer: { 9 | padding: theme.spacing(1), 10 | }, 11 | }) 12 | ) 13 | 14 | type Props = { 15 | /** 16 | * children 17 | */ 18 | children: React.ReactNode 19 | } 20 | 21 | /** 22 | * Header and article container component 23 | * @param props Props 24 | */ 25 | export const HeaderArticleContainer = function (props: Props) { 26 | const { children } = props 27 | const classes = useStyles(props) 28 | return ( 29 | <> 30 | 31 |
{children}
32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /components/organisms/ResponsiveDrawer.tsx: -------------------------------------------------------------------------------- 1 | import { AppBar, Drawer, Hidden, Toolbar, Typography } from "@material-ui/core" 2 | import IconButton from "@material-ui/core/IconButton" 3 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 4 | import MenuIcon from "@material-ui/icons/Menu" 5 | import React, { useState } from "react" 6 | import { usePage } from "../../hooks" 7 | import { Sidenavi } from "../organisms" 8 | 9 | const drawerWidth = 290 10 | 11 | const useStyles = makeStyles((theme: Theme) => 12 | createStyles({ 13 | root: { 14 | flexGrow: 1, 15 | overflow: "hidden", 16 | position: "relative", 17 | display: "flex", 18 | width: "100%", 19 | }, 20 | appBar: { 21 | position: "absolute", 22 | marginLeft: drawerWidth, 23 | [theme.breakpoints.up("md")]: { 24 | width: `calc(100% - ${drawerWidth}px)`, 25 | }, 26 | }, 27 | navIconHide: { 28 | [theme.breakpoints.up("md")]: { 29 | display: "none", 30 | }, 31 | }, 32 | toolbar: theme.mixins.toolbar, 33 | drawerPaper: { 34 | width: drawerWidth, 35 | borderRightColor: theme.palette.primary.dark, // sidenavi border right 36 | [theme.breakpoints.up("md")]: { 37 | position: "relative", 38 | }, 39 | }, 40 | content: { 41 | flexGrow: 1, 42 | }, 43 | title: { 44 | fontSize: 25, 45 | }, 46 | }) 47 | ) 48 | 49 | type Props = { 50 | children: React.ReactNode 51 | } 52 | 53 | /** 54 | * Responsive drawer 55 | * @see https://material-ui.com/demos/drawers/#responsive-drawer 56 | */ 57 | export const ResponsiveDrawer = function (props: Props) { 58 | const { children } = props 59 | const classes = useStyles(props) 60 | const { selectedPage } = usePage() 61 | const [mobileOpen, setMobileOpen] = useState(false) 62 | /** 63 | * Toggle drawer 64 | */ 65 | const handleDrawerToggle = () => { 66 | setMobileOpen(!mobileOpen) 67 | } 68 | 69 | return ( 70 |
71 | 72 | 73 | 79 | 80 | 81 | 87 | {selectedPage.pageTitle} 88 | 89 | 90 | 91 | 92 | 93 | 105 | 106 | 107 | 108 | 109 | 110 | 117 | 118 | 119 | 120 | 121 |
122 |
123 | {children} 124 |
125 |
126 | ) 127 | } 128 | -------------------------------------------------------------------------------- /components/organisms/Sidenavi.tsx: -------------------------------------------------------------------------------- 1 | import { List } from "@material-ui/core" 2 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 3 | import SvgIcon from "@material-ui/core/SvgIcon" 4 | import React from "react" 5 | import { Page, SiteInfo } from "../../constants" 6 | import { usePage } from "../../hooks" 7 | import { NextListItem } from "../molecules" 8 | 9 | const useStyles = makeStyles((theme: Theme) => 10 | createStyles({ 11 | root: { 12 | backgroundColor: theme.palette.primary.main, 13 | }, 14 | siteNameContainer: { 15 | fontSize: "30px", 16 | display: "flex", 17 | justifyContent: "center", 18 | alignItems: "center", 19 | backgroundColor: theme.palette.primary.main, 20 | color: theme.palette.primary.contrastText, 21 | fontWeight: "bold", 22 | boxShadow: theme.shadows[4], 23 | zIndex: theme.zIndex.drawer + 1, 24 | }, 25 | toolbar: theme.mixins.toolbar, 26 | list: { 27 | padding: 0, 28 | border: 0, 29 | }, 30 | listItem: { 31 | border: 0, 32 | boxShadow: theme.shadows[3], 33 | }, 34 | deactive: { 35 | transition: "background-color 1.2s", // mouse out 36 | "&:hover": { 37 | backgroundColor: theme.palette.primary.light, 38 | transition: "background-color 0.4s", // mouse over 39 | }, 40 | }, 41 | active: { 42 | backgroundColor: theme.palette.primary.light, 43 | }, 44 | }) 45 | ) 46 | 47 | type Props = {} 48 | 49 | /** 50 | * Side navigation component 51 | * @param props Props 52 | */ 53 | export const Sidenavi = function (props: Props) { 54 | const classes = useStyles(props) 55 | const { selectedPage, changePage } = usePage() 56 | 57 | const handleChangePage = (id: number) => () => changePage(id) 58 | 59 | return ( 60 |
61 |
62 | {SiteInfo.SITE_NAME} 63 |
64 | 65 | 66 | {Page.values.map((page) => { 67 | const Icon = page.icon 68 | return ( 69 | 81 | 82 | 83 | } 84 | onClick={handleChangePage(page.id)} 85 | /> 86 | ) 87 | })} 88 | 89 |
90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /components/organisms/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./HeaderArticleContainer" 2 | export * from "./ResponsiveDrawer" 3 | export * from "./Sidenavi" 4 | -------------------------------------------------------------------------------- /components/templates/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 2 | import Head from "next/head" 3 | import React from "react" 4 | import { usePage } from "../../hooks" 5 | import { ResponsiveDrawer } from "../organisms" 6 | 7 | const useStyles = makeStyles((_: Theme) => 8 | createStyles({ 9 | root: { 10 | height: "100%", 11 | }, 12 | }) 13 | ) 14 | 15 | type Props = { 16 | children: React.ReactNode 17 | className?: string 18 | } 19 | 20 | export const Layout = function (props: Props) { 21 | const { children, className } = props 22 | const classes = useStyles(props) 23 | const { selectedPage } = usePage() 24 | 25 | return ( 26 |
27 | 28 | {selectedPage.title} 29 | 30 | 31 |
{children}
32 |
33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /components/templates/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Layout" 2 | -------------------------------------------------------------------------------- /constants/Env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Environment variables 3 | */ 4 | export const Env = { 5 | NODE_ENV: process.env.NODE_ENV, 6 | API_SERVER_URL: process.env.API_SERVER_URL, 7 | } 8 | -------------------------------------------------------------------------------- /constants/IEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum like interface 3 | */ 4 | export interface IEnum { 5 | equals(t: T): boolean 6 | toString(): string 7 | } 8 | -------------------------------------------------------------------------------- /constants/Page.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "@material-ui/core" 2 | import { blue, pink, red, yellow } from "@material-ui/core/colors" 3 | import { SvgIconProps } from "@material-ui/core/SvgIcon" 4 | import { Home, Info, Save } from "@material-ui/icons" 5 | import { IEnum } from "." 6 | 7 | /** 8 | * Page constants 9 | * @author tree 10 | */ 11 | export class Page implements IEnum { 12 | /** 13 | * For values() array 14 | */ 15 | private static _values = new Array() 16 | 17 | public static readonly TOP = new Page( 18 | 1, 19 | "Top", 20 | "Top page", 21 | "Top page | sample", 22 | "Feat typescript and next.js and redux and material-ui !!", 23 | "/", 24 | Home, 25 | pink 26 | ) 27 | public static readonly REDUX = new Page( 28 | 2, 29 | "Redux", 30 | "Redux sample", 31 | "Redux sample | sample", 32 | "Basic redux examples with typescript-fsa and immer.", 33 | "/redux", 34 | Save, 35 | blue 36 | ) 37 | public static readonly TODO = new Page( 38 | 3, 39 | "TODO", 40 | "TODO sample", 41 | "TODO sample | sample", 42 | "The TODO sample application using createAsyncThunk and createEntityAdapter.", 43 | "/todo", 44 | Save, 45 | yellow 46 | ) 47 | public static readonly ERROR = new Page( 48 | 99, 49 | "Error", 50 | "Error", 51 | "Error | sample", 52 | "Error.", 53 | "/error", 54 | Info, 55 | red 56 | ) 57 | 58 | /** 59 | * constructor 60 | * @param number page id 61 | * @param pageTitle page title 62 | * @param pageDescription page description 63 | * @param title seo title 64 | * @param metaDescription seo meta description 65 | * @param relativeUrl relative url 66 | * @param icon page icon 67 | * @param iconColor page icon color 68 | */ 69 | private constructor( 70 | public readonly id: number, 71 | public readonly pageTitle: string, 72 | public readonly pageDescription: string, 73 | public readonly title: string, 74 | public readonly metaDescription: string, 75 | public readonly relativeUrl: string, 76 | public readonly icon: React.ComponentType, 77 | public readonly iconColor: Color 78 | ) { 79 | Page._values.push(this) 80 | } 81 | 82 | /** 83 | * Instance array 84 | */ 85 | static get values(): Page[] { 86 | return this._values 87 | } 88 | 89 | /** 90 | * @inheritdoc 91 | */ 92 | equals = (target: Page): boolean => this.id === target.id 93 | 94 | /** 95 | * @inheritdoc 96 | */ 97 | toString = (): string => 98 | `${this.id}, ${this.pageTitle}, ${this.pageDescription}` 99 | 100 | /** 101 | * get instance 102 | * @param id id 103 | */ 104 | static of(id: number): Page { 105 | const page = Page.values.filter((e) => id === e.id).find((e) => !!e) 106 | if (!page) { 107 | throw new Error(`Get instance failed. id=${id}`) 108 | } 109 | return page 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /constants/SiteInfo.ts: -------------------------------------------------------------------------------- 1 | export enum SiteInfo { 2 | SITE_NAME = "Sample site", 3 | } 4 | -------------------------------------------------------------------------------- /constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Env" 2 | export * from "./IEnum" 3 | export * from "./Page" 4 | export * from "./SiteInfo" 5 | -------------------------------------------------------------------------------- /hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useCounter" 2 | export * from "./usePage" 3 | export * from "./useTodo" 4 | -------------------------------------------------------------------------------- /hooks/useCounter.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react" 2 | import { useDispatch, useSelector } from "react-redux" 3 | import { 4 | calculate, 5 | counterSelector, 6 | decrement, 7 | increment, 8 | } from "../store/counter" 9 | 10 | type CounterOperators = { 11 | count: number 12 | increment: () => void 13 | decrement: () => void 14 | calculate: (inputNumber: number) => void 15 | } 16 | 17 | /** 18 | * Counter custom-hooks 19 | * @see https://reactjs.org/docs/hooks-custom.html 20 | */ 21 | export const useCounter = (): Readonly => { 22 | const dispatch = useDispatch() 23 | const counterState = useSelector(counterSelector) 24 | 25 | return { 26 | count: counterState.count, 27 | increment: useCallback(() => dispatch(increment()), [dispatch]), 28 | decrement: useCallback(() => dispatch(decrement()), [dispatch]), 29 | calculate: useCallback( 30 | (inputNumber: number) => { 31 | dispatch( 32 | calculate({ 33 | inputNumber: inputNumber, 34 | }) 35 | ) 36 | }, 37 | [dispatch] 38 | ), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /hooks/usePage.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react" 2 | import { useDispatch, useSelector } from "react-redux" 3 | import { Page } from "../constants" 4 | import { changePage, pageSelector } from "../store/page" 5 | 6 | type PageOperators = { 7 | selectedPage: Page 8 | changePage: (id: number) => void 9 | } 10 | 11 | /** 12 | * Page custom-hooks 13 | * @see https://reactjs.org/docs/hooks-custom.html 14 | */ 15 | export const usePage = (): Readonly => { 16 | const dispatch = useDispatch() 17 | const pageState = useSelector(pageSelector) 18 | const page: Page = Page.of(pageState.id) 19 | 20 | return { 21 | selectedPage: page, 22 | changePage: useCallback( 23 | (id: number) => { 24 | dispatch( 25 | changePage({ 26 | id: id, 27 | }) 28 | ) 29 | }, 30 | [dispatch] 31 | ), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /hooks/useTodo.ts: -------------------------------------------------------------------------------- 1 | import { unwrapResult } from "@reduxjs/toolkit" 2 | import { useCallback } from "react" 3 | import { useDispatch, useSelector } from "react-redux" 4 | import { Todo } from "../model" 5 | import { 6 | addTodoAction, 7 | deleteTodoAction, 8 | editTodoAction, 9 | fetchAllTodosAction, 10 | fetchTodoAction, 11 | } from "../store/todo/action" 12 | import { 13 | allTodoSelector, 14 | isFetchingSelector, 15 | todoSelector, 16 | } from "../store/todo/selector" 17 | 18 | /** 19 | * TODO custom hook 20 | */ 21 | export const useTodo = () => { 22 | const dispatch = useDispatch() 23 | const isFetching = useSelector(isFetchingSelector) 24 | const todo = useSelector(todoSelector) 25 | const todos = useSelector(allTodoSelector)?.map((t) => ({ 26 | id: t.id, 27 | name: t.name, 28 | complete: t.complete, 29 | createdAt: t.createdAt, 30 | updatedAt: t.updatedAt, 31 | })) 32 | 33 | const fetchAllTodos = useCallback( 34 | (arg?: { offset?: number; limit?: number }) => { 35 | return dispatch( 36 | fetchAllTodosAction({ 37 | offset: arg?.offset || 0, 38 | limit: arg?.limit || 5, 39 | }) 40 | ).then(unwrapResult) 41 | }, 42 | [dispatch] 43 | ) 44 | 45 | const fetchTodo = useCallback( 46 | (arg: { id: number }) => { 47 | return dispatch(fetchTodoAction(arg)).then(unwrapResult) 48 | }, 49 | [dispatch] 50 | ) 51 | 52 | const addTodo = useCallback( 53 | (arg: { todo: Todo }) => { 54 | return dispatch(addTodoAction(arg)).then(unwrapResult) 55 | }, 56 | [dispatch] 57 | ) 58 | 59 | const editTodo = useCallback( 60 | (arg: { todo: Todo }) => { 61 | return dispatch(editTodoAction(arg)).then(unwrapResult) 62 | }, 63 | [dispatch] 64 | ) 65 | 66 | const deleteTodo = useCallback( 67 | (arg: { id: number }) => { 68 | return dispatch(deleteTodoAction(arg)).then(unwrapResult) 69 | }, 70 | [dispatch] 71 | ) 72 | 73 | return { 74 | isFetching, 75 | todos, 76 | todo, 77 | fetchAllTodos, 78 | fetchTodo, 79 | addTodo, 80 | editTodo, 81 | deleteTodo, 82 | } as const 83 | } 84 | -------------------------------------------------------------------------------- /model/ApiErrorResponse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Api error response 3 | */ 4 | export type ApiErrorResponse = { 5 | statusCode: number 6 | message: string 7 | error?: Error 8 | } 9 | -------------------------------------------------------------------------------- /model/TestData.ts: -------------------------------------------------------------------------------- 1 | import { Todo } from "./Todo" 2 | 3 | // test data 4 | export const testTodos: Todo[] = [] 5 | 6 | for (let i = 0; i < 6; i++) { 7 | testTodos.push({ 8 | id: i + 1, 9 | name: `Task ${i + 1}`, 10 | complete: i % 2 == 0, 11 | createdAt: new Date(), 12 | updatedAt: new Date(), 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /model/Todo.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * TODO model 3 | */ 4 | export type Todo = { 5 | id: number 6 | name: string 7 | complete: boolean 8 | createdAt: Date 9 | updatedAt: Date 10 | } 11 | -------------------------------------------------------------------------------- /model/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Todo" 2 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const Dotenv = require("dotenv-webpack") 3 | 4 | module.exports = { 5 | webpack: (config) => { 6 | config.plugins = [ 7 | ...config.plugins, 8 | new Dotenv({ 9 | path: path.join(__dirname, ".env"), 10 | systemvars: true, 11 | }), 12 | ] 13 | return config 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "env": { 4 | 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tree-maps", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next", 8 | "build": "next build", 9 | "start": "NODE_ENV=production node server.js", 10 | "format": "prettier --write \"{components,constants,hooks,model,pages,store,types}/**/*.{ts,tsx}\"", 11 | "lint": "eslint \"{components,constants,hooks,model,pages,store,types}/**/*.{ts,tsx}\" --fix" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@material-ui/core": "^4.9.11", 18 | "@material-ui/icons": "^4.9.1", 19 | "@material-ui/styles": "^4.9.10", 20 | "@reduxjs/toolkit": "^1.3.5", 21 | "compression": "^1.7.4", 22 | "express": "^4.17.1", 23 | "material-table": "^1.57.2", 24 | "next": "^9.3.5", 25 | "next-redux-wrapper": "^6.0.0-rc.7", 26 | "react": "^16.13.1", 27 | "react-dom": "^16.13.1", 28 | "react-redux": "^7.2.0", 29 | "redux": "^4.0.5" 30 | }, 31 | "devDependencies": { 32 | "@babel/plugin-proposal-decorators": "^7.8.3", 33 | "@types/next-redux-wrapper": "^3.0.0", 34 | "@types/node": "^13.13.2", 35 | "@types/react": "^16.9.34", 36 | "@types/react-dom": "^16.9.6", 37 | "@types/react-jss": "^10.0.0", 38 | "@types/react-redux": "^7.1.7", 39 | "@types/redux": "^3.6.0", 40 | "@types/styled-jsx": "^2.2.8", 41 | "@typescript-eslint/eslint-plugin": "^2.31.0", 42 | "@typescript-eslint/parser": "^2.31.0", 43 | "dotenv-webpack": "^1.7.0", 44 | "eslint": "^7.0.0", 45 | "eslint-config-prettier": "^6.11.0", 46 | "eslint-plugin-import": "^2.20.2", 47 | "eslint-plugin-prettier": "^3.1.3", 48 | "eslint-plugin-react": "^7.19.0", 49 | "prettier": "^2.0.5", 50 | "typescript": "^3.8.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@material-ui/core" 2 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 3 | import React, { useEffect } from "react" 4 | import { SpacingPaper } from "../components/atoms" 5 | import { HeaderArticleContainer } from "../components/organisms" 6 | import { Layout } from "../components/templates" 7 | import { Page } from "../constants" 8 | import { usePage } from "../hooks" 9 | 10 | const useStyles = makeStyles((_: Theme) => 11 | createStyles({ 12 | root: {}, 13 | }) 14 | ) 15 | 16 | type Props = {} 17 | 18 | /** 19 | * 404 Error page without getInitialProps(SSR) 20 | * @see https://nextjs.org/docs/advanced-features/custom-error-page#customizing-the-404-page 21 | * @see https://github.com/zeit/next.js/blob/master/errors/custom-error-no-custom-404.md 22 | */ 23 | function NotFoundError(props: Props) { 24 | const classes = useStyles(props) 25 | const { changePage } = usePage() 26 | 27 | useEffect(() => { 28 | changePage(Page.ERROR.id) 29 | }, []) 30 | 31 | return ( 32 | 33 | 34 | 35 | 404 Page NotFound :( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | export default NotFoundError 43 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import CssBaseline from "@material-ui/core/CssBaseline" 2 | import { ThemeProvider } from "@material-ui/styles" 3 | import withRedux from "next-redux-wrapper" 4 | import App from "next/app" 5 | import React from "react" 6 | import { Provider } from "react-redux" 7 | import { MuiTheme } from "../components/MuiTheme" 8 | import { makeStore } from "../store/configureStore" 9 | import "../styles/main.css" 10 | 11 | type Props = { 12 | Component: React.Component 13 | store: any 14 | } 15 | 16 | /** 17 | * @see https://github.com/mui-org/material-ui/blob/master/examples/nextjs-with-typescript/pages/_app.tsx 18 | */ 19 | class MyApp extends App { 20 | componentDidMount() { 21 | // Remove the server-side injected CSS. 22 | const jssStyles = document.querySelector("#jss-server-side") 23 | jssStyles?.parentNode?.removeChild(jssStyles) 24 | } 25 | 26 | render() { 27 | const { store, Component, pageProps } = this.props 28 | 29 | return ( 30 | 31 | 32 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} 33 | 34 | 35 | 36 | 37 | ) 38 | } 39 | } 40 | 41 | export default withRedux(makeStore, { 42 | debug: false, 43 | })(MyApp) 44 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { ServerStyleSheets } from "@material-ui/styles" 2 | import Document, { Head, Main, NextScript } from "next/document" 3 | import React from "react" 4 | import flush from "styled-jsx/server" 5 | import { AppContext } from "../components/AppContext" 6 | import { MuiTheme } from "../components/MuiTheme" 7 | 8 | type Props = { 9 | pageProps: any 10 | } 11 | 12 | /** 13 | * @see https://github.com/mui-org/material-ui/blob/master/examples/nextjs-with-typescript/pages/_document.tsx 14 | */ 15 | class MyDocument extends Document { 16 | static getInitialProps = async (ctx: AppContext): Promise => { 17 | // Render app and page and get the context of the page with collected side effects. 18 | const sheets = new ServerStyleSheets() 19 | 20 | const originalRenderPage = ctx.renderPage 21 | 22 | ctx.renderPage = () => 23 | originalRenderPage({ 24 | enhanceApp: (App) => (props) => sheets.collect(), 25 | }) 26 | 27 | const initialProps = await Document.getInitialProps(ctx) 28 | 29 | const pageProps = ctx.store.getState() 30 | 31 | return { 32 | ...initialProps, 33 | pageProps, 34 | // Styles fragment is rendered after the app and page rendering finish. 35 | styles: ( 36 | <> 37 | {sheets.getStyleElement()} 38 | {flush() || null} 39 | 40 | ), 41 | } 42 | } 43 | 44 | render() { 45 | const { pageProps } = this.props 46 | const page = pageProps?.page 47 | 48 | return ( 49 | 50 | 51 | 52 | {/* Use minimum-scale=1 to enable GPU rasterization */} 53 | 57 | {/* PWA primary color */} 58 | 59 | 60 | 64 | 68 | 69 | 70 |
71 | 72 | 73 | 74 | ) 75 | } 76 | } 77 | 78 | export default MyDocument 79 | -------------------------------------------------------------------------------- /pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@material-ui/core" 2 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 3 | import React from "react" 4 | import { AppContext } from "../components/AppContext" 5 | import { SpacingPaper } from "../components/atoms" 6 | import { HeaderArticleContainer } from "../components/organisms" 7 | import { Layout } from "../components/templates" 8 | 9 | const useStyles = makeStyles((_: Theme) => 10 | createStyles({ 11 | root: {}, 12 | }) 13 | ) 14 | 15 | type Props = { 16 | statusCode?: number 17 | } 18 | 19 | /** 20 | * Error page 21 | * @see https://nextjs.org/docs/advanced-features/custom-error-page#500-page 22 | */ 23 | function Error(props: Props) { 24 | const { statusCode } = props 25 | const classes = useStyles(props) 26 | return ( 27 | 28 | 29 | 30 | 31 | Http status code {statusCode} error ! 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | Error.getInitialProps = async (ctx: AppContext): Promise => { 40 | const { err, res } = ctx 41 | const statusCode = res ? res.statusCode : err ? err.statusCode : 404 42 | return { statusCode } 43 | } 44 | 45 | export default Error 46 | -------------------------------------------------------------------------------- /pages/api/todo/[id].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next" 2 | import { Todo } from "../../../model" 3 | import { ApiErrorResponse } from "../../../model/ApiErrorResponse" 4 | import { testTodos } from "../../../model/TestData" 5 | 6 | /** 7 | * TODO restful-api with path-parameter 8 | * @param req NextApiRequest 9 | * @param res NextApiResponse 10 | */ 11 | export default (req: NextApiRequest, res: NextApiResponse) => { 12 | const { 13 | query: { id }, 14 | method, 15 | body, 16 | } = req 17 | 18 | try { 19 | res.setHeader("Content-Type", "application/json") 20 | 21 | // validation 22 | const idStr = String(id) 23 | if (!/^\d+$/.test(idStr)) { 24 | const error: ApiErrorResponse = { 25 | statusCode: 400, 26 | message: "Please enter the todo-id as a number.", 27 | } 28 | res.status(400).json(error) 29 | return 30 | } 31 | 32 | // find data 33 | const todoId = Number(idStr) 34 | const currentTodo = testTodos 35 | .filter((todo) => todo.id === todoId) 36 | .find((todo) => !!todo) 37 | if (!currentTodo) { 38 | const error: ApiErrorResponse = { 39 | statusCode: 404, 40 | message: `todo ${id} is not found.`, 41 | } 42 | res.status(404).json(error) 43 | return 44 | } 45 | 46 | switch (method) { 47 | case "GET": 48 | res.status(200).json(currentTodo) 49 | break 50 | case "PUT": 51 | const newTodo: Todo = body 52 | newTodo.updatedAt = new Date() 53 | testTodos[todoId] = body 54 | res.status(200).json(newTodo) 55 | break 56 | case "DELETE": 57 | const deleteTargetId = testTodos.findIndex((todo) => todo.id === todoId) 58 | testTodos.splice(deleteTargetId, 1) 59 | res.status(204).end() 60 | break 61 | default: 62 | res.setHeader("Allow", ["GET", "POST", "PUT", "DELETE"]) 63 | res.status(405).end(`Method ${method} Not Allowed`) 64 | break 65 | } 66 | } catch (e) { 67 | const error: ApiErrorResponse = { 68 | statusCode: 500, 69 | message: `Internal server error. ${e}`, 70 | } 71 | res.status(500).json(error) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pages/api/todo/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next" 2 | import { Todo } from "../../../model" 3 | import { ApiErrorResponse } from "../../../model/ApiErrorResponse" 4 | import { testTodos } from "../../../model/TestData" 5 | 6 | /** 7 | * TODO restful-api 8 | * @param req NextApiRequest 9 | * @param res NextApiResponse 10 | */ 11 | export default (req: NextApiRequest, res: NextApiResponse) => { 12 | const { method, body } = req 13 | 14 | try { 15 | res.setHeader("Content-Type", "application/json") 16 | 17 | switch (method) { 18 | case "GET": 19 | res.status(200).json(testTodos) 20 | break 21 | case "POST": 22 | if (!body) { 23 | const error: ApiErrorResponse = { 24 | statusCode: 400, 25 | message: `Request body is required.`, 26 | } 27 | res.status(400).json(error) 28 | return 29 | } 30 | 31 | const newTodo: Todo = body 32 | const lastTodo = testTodos.slice(-1)[0] 33 | newTodo.id = lastTodo.id + 1 34 | newTodo.createdAt = new Date() 35 | newTodo.updatedAt = new Date() 36 | testTodos.push(newTodo) 37 | res.status(201).json(newTodo) 38 | break 39 | default: 40 | res.setHeader("Allow", ["GET", "POST"]) 41 | res.status(405).end(`Method ${method} Not Allowed`) 42 | break 43 | } 44 | } catch (e) { 45 | const error: ApiErrorResponse = { 46 | statusCode: 500, 47 | message: `Internal server error. ${e}`, 48 | } 49 | res.status(500).json(error) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@material-ui/core" 2 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 3 | import React from "react" 4 | import { AppContext } from "../components/AppContext" 5 | import { SpacingPaper } from "../components/atoms" 6 | import { HeaderArticleContainer } from "../components/organisms" 7 | import { Layout } from "../components/templates" 8 | import { Page } from "../constants" 9 | import { changePage } from "../store/page" 10 | 11 | const useStyles = makeStyles((_: Theme) => 12 | createStyles({ 13 | root: {}, 14 | }) 15 | ) 16 | 17 | type Props = {} 18 | 19 | function Index(props: Props) { 20 | const classes = useStyles(props) 21 | return ( 22 | 23 | 24 | 25 | Hello Next.js 👋 26 | 27 | 28 | 29 | zero padding paper 30 | 31 | This component use makeStyles refer to Theme and Props. 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | /** 40 | * @see https://nextjs.org/docs/api-reference/data-fetching/getInitialProps 41 | */ 42 | Index.getInitialProps = async (ctx: AppContext): Promise => { 43 | const { store } = ctx 44 | store.dispatch( 45 | changePage({ 46 | id: Page.TOP.id, 47 | }) 48 | ) 49 | return {} 50 | } 51 | 52 | export default Index 53 | -------------------------------------------------------------------------------- /pages/redux.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | Button, 4 | FormControl, 5 | TextField, 6 | Typography, 7 | } from "@material-ui/core" 8 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 9 | import React, { useState } from "react" 10 | import { AppContext } from "../components/AppContext" 11 | import { SpacingPaper } from "../components/atoms" 12 | import { HeaderArticleContainer } from "../components/organisms" 13 | import { Layout } from "../components/templates" 14 | import { Page } from "../constants" 15 | import { useCounter } from "../hooks" 16 | import { changePage } from "../store/page" 17 | 18 | const useStyles = makeStyles((theme: Theme) => 19 | createStyles({ 20 | root: {}, 21 | counter: { 22 | margin: 10, 23 | backgroundColor: theme.palette.primary.main, 24 | color: theme.palette.primary.contrastText, 25 | }, 26 | title: { 27 | fontSize: "2em", 28 | }, 29 | }) 30 | ) 31 | 32 | type Props = { 33 | // passed from getInitialProps 34 | defaultInputNumber: number 35 | } 36 | 37 | function Redux(props: Props) { 38 | const classes = useStyles(props) 39 | const { defaultInputNumber } = props 40 | const [inputNumber, setInputNumber] = useState(defaultInputNumber) 41 | const { count, increment, decrement, calculate } = useCounter() 42 | 43 | /** 44 | * Change inputNumber value 45 | */ 46 | const handleChangeCount = (e: React.ChangeEvent) => { 47 | const val = e.target.value 48 | // ignore not number 49 | if (val.match(/^([1-9]|0)+[0-9]*$/i)) { 50 | setInputNumber(Number(val)) 51 | } 52 | } 53 | 54 | const CurrentNumber = () => ( 55 | {count} 56 | ) 57 | 58 | return ( 59 | 60 | 61 | 62 | 63 | Increment / Decrement 64 | 65 | 66 | 69 |   70 | 73 | 74 | 75 | 76 | 77 | 78 | Calculate 79 | 80 | 81 | 82 | 83 | 90 | 91 | 98 | 99 | 100 | 101 | 102 | ) 103 | } 104 | 105 | /** 106 | * @see https://nextjs.org/docs/api-reference/data-fetching/getInitialProps 107 | */ 108 | Redux.getInitialProps = async (ctx: AppContext): Promise => { 109 | const { store } = ctx 110 | store.dispatch( 111 | changePage({ 112 | id: Page.REDUX.id, 113 | }) 114 | ) 115 | return { 116 | defaultInputNumber: 2, 117 | } 118 | } 119 | 120 | export default Redux 121 | -------------------------------------------------------------------------------- /pages/todo/index.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles, Theme } from "@material-ui/core" 2 | import React from "react" 3 | import { AppContext } from "../../components/AppContext" 4 | import { SpacingPaper } from "../../components/atoms" 5 | import { TodoList } from "../../components/molecules" 6 | import { HeaderArticleContainer } from "../../components/organisms" 7 | import { Layout } from "../../components/templates" 8 | import { Page } from "../../constants" 9 | import { changePage } from "../../store/page" 10 | 11 | const useStyles = makeStyles((_: Theme) => 12 | createStyles({ 13 | root: {}, 14 | }) 15 | ) 16 | 17 | type Props = {} 18 | 19 | function Todo(props: Props) { 20 | const {} = props 21 | const classes = useStyles(props) 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | /** 35 | * @see https://nextjs.org/docs/api-reference/data-fetching/getInitialProps 36 | */ 37 | Todo.getInitialProps = async (ctx: AppContext): Promise => { 38 | const { store } = ctx 39 | store.dispatch( 40 | changePage({ 41 | id: Page.TODO.id, 42 | }) 43 | ) 44 | return {} 45 | } 46 | 47 | export default Todo 48 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express") 2 | const next = require("next") 3 | const compression = require("compression") 4 | 5 | const dev = process.env.NODE_ENV !== "production" 6 | const app = next({ dev }) 7 | const handle = app.getRequestHandler() 8 | const port = dev ? 3000 : 8080 9 | 10 | app 11 | .prepare() 12 | .then(() => { 13 | const server = express() 14 | // support gzip 15 | server.use(compression()) 16 | 17 | server.get("*", (req, res) => { 18 | return handle(req, res) 19 | }) 20 | 21 | server.listen(port, err => { 22 | if (err) throw err 23 | console.log(`> Ready on http://localhost:${port}`) 24 | }) 25 | }) 26 | .catch(ex => { 27 | console.error(ex.stack) 28 | process.exit(1) 29 | }) 30 | -------------------------------------------------------------------------------- /store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | configureStore, 3 | EnhancedStore, 4 | getDefaultMiddleware, 5 | } from "@reduxjs/toolkit" 6 | import { MakeStore } from "next-redux-wrapper" 7 | import { Env } from "../constants" 8 | import { rootReducer, RootState } from "./reducers" 9 | 10 | /** 11 | * @see https://redux-toolkit.js.org/usage/usage-with-typescript#correct-typings-for-the-dispatch-type 12 | */ 13 | const middlewares = [...getDefaultMiddleware()] 14 | 15 | const store = configureStore({ 16 | reducer: rootReducer, 17 | middleware: middlewares, 18 | devTools: Env.NODE_ENV === "development", 19 | }) 20 | 21 | export const makeStore: MakeStore = (_?: RootState): EnhancedStore => store 22 | -------------------------------------------------------------------------------- /store/counter/counter.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit" 2 | import { FeatureKey } from "../featureKey" 3 | import { RootState } from "../reducers" 4 | 5 | /** 6 | * Payload 7 | */ 8 | export type CounterPayload = { 9 | inputNumber: number 10 | } 11 | 12 | /** 13 | * State 14 | */ 15 | export type CounterState = { 16 | count: number 17 | } 18 | 19 | const initialState: CounterState = { 20 | count: 1, 21 | } 22 | 23 | /** 24 | * Slice 25 | * @see https://redux-toolkit.js.org/api/createslice 26 | */ 27 | const slice = createSlice({ 28 | name: FeatureKey.COUNTER, 29 | initialState, 30 | reducers: { 31 | increment: (state: CounterState): CounterState => { 32 | return { 33 | ...state, 34 | count: state.count + 1, 35 | } 36 | }, 37 | decrement: (state: CounterState): CounterState => { 38 | return { 39 | ...state, 40 | count: state.count - 1, 41 | } 42 | }, 43 | calculate: ( 44 | state: CounterState, 45 | action: PayloadAction 46 | ): CounterState => { 47 | const { payload } = action 48 | return { 49 | ...state, 50 | count: state.count + payload.inputNumber, 51 | } 52 | }, 53 | }, 54 | }) 55 | 56 | /** 57 | * Reducer 58 | */ 59 | export const counterReducer = slice.reducer 60 | 61 | /** 62 | * Action 63 | */ 64 | export const { increment, decrement, calculate } = slice.actions 65 | 66 | /** 67 | * Selector 68 | * @param state CounterState 69 | */ 70 | export const counterSelector = (state: RootState): CounterState => state.counter 71 | -------------------------------------------------------------------------------- /store/counter/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./counter" 2 | -------------------------------------------------------------------------------- /store/featureKey.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * State feature key (prefix of action name) 3 | */ 4 | export const FeatureKey = { 5 | COUNTER: "COUNTER", 6 | PAGE: "PAGE", 7 | TODO: "TODO", 8 | } as const 9 | -------------------------------------------------------------------------------- /store/page/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./page" 2 | -------------------------------------------------------------------------------- /store/page/page.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit" 2 | import { Page } from "../../constants" 3 | import { FeatureKey } from "../featureKey" 4 | import { RootState } from "../reducers" 5 | 6 | /** 7 | * Payload 8 | */ 9 | export type PagePayload = { 10 | id: number 11 | } 12 | 13 | /** 14 | * State 15 | */ 16 | export type PageState = { 17 | id: number 18 | pageTitle: string 19 | pageDescription: string 20 | title: string 21 | metaDescription: string 22 | } 23 | 24 | const initialState: PageState = { 25 | id: Page.TOP.id, 26 | pageTitle: Page.TOP.pageTitle, 27 | pageDescription: Page.TOP.pageDescription, 28 | title: Page.TOP.title, 29 | metaDescription: Page.TOP.metaDescription, 30 | } 31 | 32 | /** 33 | * Slice 34 | * @see https://redux-toolkit.js.org/api/createslice 35 | */ 36 | const slice = createSlice({ 37 | name: FeatureKey.PAGE, 38 | initialState, 39 | reducers: { 40 | changePage: ( 41 | state: PageState, 42 | action: PayloadAction 43 | ): PageState => { 44 | const { id } = action.payload 45 | const selectedPage: Page = Page.of(id) 46 | return { 47 | ...state, 48 | id: selectedPage.id, 49 | pageTitle: selectedPage.pageTitle, 50 | pageDescription: selectedPage.pageDescription, 51 | title: selectedPage.title, 52 | metaDescription: selectedPage.metaDescription, 53 | } 54 | }, 55 | }, 56 | }) 57 | 58 | /** 59 | * Reducer 60 | */ 61 | export const pageReducer = slice.reducer 62 | 63 | /** 64 | * Action 65 | */ 66 | export const { changePage } = slice.actions 67 | 68 | /** 69 | * Selector 70 | * @param state PageStateType 71 | */ 72 | export const pageSelector = (state: RootState): PageState => state.page 73 | -------------------------------------------------------------------------------- /store/reducers.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux" 2 | import { counterReducer } from "./counter" 3 | import { pageReducer } from "./page" 4 | import { todoReducer } from "./todo" 5 | 6 | /** 7 | * Combine reducers 8 | * @see https://redux-toolkit.js.org/usage/usage-with-typescript 9 | */ 10 | export const rootReducer = combineReducers({ 11 | counter: counterReducer, 12 | page: pageReducer, 13 | todo: todoReducer, 14 | }) 15 | 16 | export type RootState = ReturnType 17 | -------------------------------------------------------------------------------- /store/todo/action.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from "@reduxjs/toolkit" 2 | import { Todo } from "../../model" 3 | import { FeatureKey } from "../featureKey" 4 | 5 | /** 6 | * Fetch all todo action 7 | */ 8 | export const fetchAllTodosAction = createAsyncThunk( 9 | `${FeatureKey.TODO}/fetchAll`, 10 | async (arg: { offset: number; limit: number }) => { 11 | const { offset, limit } = arg 12 | const url = `/api/todo?offset=${offset}&limit=${limit}` 13 | const result: Todo[] = await fetch(url, { 14 | method: "get", 15 | }).then((response: Response) => response.json()) 16 | return { todos: result } 17 | } 18 | ) 19 | 20 | /** 21 | * Fetch todo action 22 | */ 23 | export const fetchTodoAction = createAsyncThunk( 24 | `${FeatureKey.TODO}/fetch`, 25 | async (arg: { id: number }) => { 26 | const { id } = arg 27 | const url = `/api/todo/${id}` 28 | const result: Todo = await fetch(url, { 29 | method: "get", 30 | }).then((response: Response) => response.json()) 31 | return { todo: result } 32 | } 33 | ) 34 | 35 | /** 36 | * Add todo action 37 | */ 38 | export const addTodoAction = createAsyncThunk( 39 | `${FeatureKey.TODO}/add`, 40 | async (arg: { todo: Todo }) => { 41 | const { todo } = arg 42 | const url = `/api/todo` 43 | const result: Todo = await fetch(url, { 44 | method: "post", 45 | headers: { "Content-Type": "application/json" }, 46 | body: JSON.stringify(todo), 47 | }).then((response: Response) => response.json()) 48 | return { todo: result } 49 | } 50 | ) 51 | 52 | /** 53 | * Edit todo action 54 | */ 55 | export const editTodoAction = createAsyncThunk( 56 | `${FeatureKey.TODO}/edit`, 57 | async (arg: { todo: Todo }) => { 58 | const { todo } = arg 59 | const url = `/api/todo/${todo.id}` 60 | const result: Todo = await fetch(url, { 61 | method: "put", 62 | headers: { "Content-Type": "application/json" }, 63 | body: JSON.stringify(todo), 64 | }).then((response: Response) => response.json()) 65 | return { todo: result } 66 | } 67 | ) 68 | 69 | /** 70 | * Delete todo action 71 | */ 72 | export const deleteTodoAction = createAsyncThunk( 73 | `${FeatureKey.TODO}/delete`, 74 | async (arg: { id: number }) => { 75 | const { id } = arg 76 | const url = `/api/todo/${id}` 77 | await fetch(url, { 78 | method: "delete", 79 | }) 80 | } 81 | ) 82 | -------------------------------------------------------------------------------- /store/todo/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./action" 2 | export * from "./reducer" 3 | export * from "./selector" 4 | export * from "./state" 5 | -------------------------------------------------------------------------------- /store/todo/reducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducerMapBuilder, createReducer } from "@reduxjs/toolkit" 2 | import { 3 | addTodoAction, 4 | deleteTodoAction, 5 | editTodoAction, 6 | fetchAllTodosAction, 7 | fetchTodoAction, 8 | } from "./action" 9 | import { adapter, initialState, TodoState } from "./state" 10 | 11 | /** 12 | * TODO reducer 13 | */ 14 | export const todoReducer = createReducer( 15 | initialState, 16 | (builder: ActionReducerMapBuilder) => 17 | builder 18 | .addCase(fetchAllTodosAction.pending, (state) => { 19 | return { ...state, isFetching: true } 20 | }) 21 | .addCase(fetchAllTodosAction.fulfilled, (state, action) => { 22 | const { todos } = action.payload 23 | return adapter.setAll({ ...state, isFetching: false }, todos) 24 | }) 25 | .addCase(fetchAllTodosAction.rejected, (state) => { 26 | return { ...state, isFetching: false } 27 | }) 28 | //------------------------------------------------------------------------------- 29 | .addCase(fetchTodoAction.pending, (state, action) => { 30 | const { id } = action.meta.arg 31 | return { ...state, isFetching: true, selectedId: id } 32 | }) 33 | .addCase(fetchTodoAction.fulfilled, (state, action) => { 34 | const { todo } = action.payload 35 | return adapter.upsertOne({ ...state, isFetching: false }, todo) 36 | }) 37 | .addCase(fetchTodoAction.rejected, (state) => { 38 | return { ...state, isFetching: false } 39 | }) 40 | //------------------------------------------------------------------------------- 41 | .addCase(addTodoAction.pending, (state, action) => { 42 | const { todo } = action.meta.arg 43 | return { ...state, isFetching: true, selectedId: todo?.id } 44 | }) 45 | .addCase(addTodoAction.fulfilled, (state, action) => { 46 | const { todo } = action.payload 47 | return adapter.addOne({ ...state, isFetching: false }, todo) 48 | }) 49 | .addCase(addTodoAction.rejected, (state) => { 50 | return { ...state, isFetching: false } 51 | }) 52 | //------------------------------------------------------------------------------- 53 | .addCase(editTodoAction.pending, (state, action) => { 54 | const { todo } = action.meta.arg 55 | return { ...state, isFetching: true, selectedId: todo?.id } 56 | }) 57 | .addCase(editTodoAction.fulfilled, (state, action) => { 58 | const { todo } = action.payload 59 | return adapter.updateOne( 60 | { ...state, isFetching: false }, 61 | { 62 | id: todo.id, 63 | changes: todo, 64 | } 65 | ) 66 | }) 67 | .addCase(editTodoAction.rejected, (state) => { 68 | return { ...state, isFetching: false } 69 | }) 70 | //------------------------------------------------------------------------------- 71 | .addCase(deleteTodoAction.pending, (state, action) => { 72 | const { id } = action.meta.arg 73 | return { ...state, isFetching: true, selectedId: id } 74 | }) 75 | .addCase(deleteTodoAction.fulfilled, (state, action) => { 76 | const { id } = action.meta.arg 77 | return adapter.removeOne({ ...state, isFetching: false }, id) 78 | }) 79 | .addCase(deleteTodoAction.rejected, (state) => { 80 | return { ...state, isFetching: false } 81 | }) 82 | ) 83 | -------------------------------------------------------------------------------- /store/todo/selector.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from "@reduxjs/toolkit" 2 | import { RootState } from "../reducers" 3 | import { adapter, TodoState } from "./state" 4 | 5 | const { selectAll, selectEntities } = adapter.getSelectors() 6 | 7 | const featureStateSelector = (state: RootState) => state.todo 8 | 9 | const entitiesSelector = createSelector(featureStateSelector, selectEntities) 10 | 11 | /** 12 | * isFetching selector 13 | */ 14 | export const isFetchingSelector = createSelector( 15 | featureStateSelector, 16 | (state: TodoState) => state?.isFetching 17 | ) 18 | 19 | /** 20 | * selectedId selector 21 | */ 22 | export const selectedIdSelector = createSelector( 23 | featureStateSelector, 24 | (state: TodoState) => state?.selectedId 25 | ) 26 | 27 | /** 28 | * all todo selector 29 | */ 30 | export const allTodoSelector = createSelector(featureStateSelector, selectAll) 31 | 32 | /** 33 | * todo selector 34 | */ 35 | export const todoSelector = createSelector( 36 | entitiesSelector, 37 | selectedIdSelector, 38 | (entities, id) => (id ? entities[id] || null : null) 39 | ) 40 | -------------------------------------------------------------------------------- /store/todo/state.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, EntityState } from "@reduxjs/toolkit" 2 | import { Todo } from "../../model" 3 | 4 | export interface TodoState extends EntityState { 5 | isFetching: boolean 6 | selectedId: number | null 7 | } 8 | 9 | export const adapter = createEntityAdapter({ 10 | selectId: (todo: Todo) => todo.id, 11 | }) 12 | 13 | export const initialState: TodoState = adapter.getInitialState({ 14 | isFetching: false, 15 | selectedId: null, 16 | }) 17 | -------------------------------------------------------------------------------- /styles/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | body > div:first-child, 4 | #__next, 5 | #__next > div, 6 | #__next > div > div { 7 | height: 100%; 8 | } 9 | 10 | /** Prevent a blank-blink until normarise.css is loaded complete */ 11 | html, body { 12 | margin: 0; 13 | padding: 0; 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "jsx": "preserve", 6 | "lib": ["dom", "es2017"], 7 | "moduleResolution": "node", 8 | "allowJs": true, 9 | "noEmit": true, 10 | "allowSyntheticDefaultImports": true, 11 | "skipLibCheck": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "removeComments": false, 15 | "preserveConstEnums": true, 16 | "sourceMap": true, 17 | "experimentalDecorators": true, 18 | "typeRoots": ["./node_modules/@types"], 19 | "strict": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "esModuleInterop": true, 22 | "resolveJsonModule": true, 23 | "isolatedModules": true 24 | }, 25 | "exclude": ["node_modules"], 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 27 | } 28 | -------------------------------------------------------------------------------- /types/nodejs.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface Process { 3 | browser: boolean 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /types/redux-thunk.d.ts: -------------------------------------------------------------------------------- 1 | import { ThunkAction } from "redux-thunk" 2 | 3 | // Dispatch overload for redux-thunk 4 | // https://github.com/reduxjs/redux-thunk/pull/278 5 | declare module "redux" { 6 | /* 7 | * Overload to add thunk support to Redux's dispatch() function. 8 | * Useful for react-redux or any other library which could use this type. 9 | */ 10 | export interface Dispatch = AnyAction> { 11 | ( 12 | thunkAction: ThunkAction 13 | ): TReturnType 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /types/styled-jsx.d.ts: -------------------------------------------------------------------------------- 1 | import "react" 2 | 3 | // https://github.com/soulmachine/with-react-intl/blob/master/src/%40types/styled-jsx.d.ts 4 | declare module "react" { 5 | interface StyleHTMLAttributes extends React.HTMLAttributes { 6 | jsx?: boolean 7 | global?: boolean 8 | } 9 | 10 | type LoadingAttributeType = "auto" | "lazy" | "eager" 11 | interface HTMLAttributes extends AriaAttributes, DOMAttributes { 12 | loading?: LoadingAttributeType 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /types/styles.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css" { 2 | const content: any 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /types/window.d.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/soulmachine/with-react-intl/blob/master/src/%40types/window.d.ts 2 | declare interface Window { 3 | ReactIntlLocaleData: { 4 | [index: string]: ReactIntl.Locale | ReactIntl.Locale[] 5 | } 6 | __NEXT_DATA__: { 7 | props: { 8 | locale: ReactIntl.Locale 9 | messages: object 10 | antdLocale: object 11 | now: Date 12 | } 13 | } 14 | } 15 | --------------------------------------------------------------------------------