├── .editorconfig ├── .env ├── .eslintrc.js ├── .gcloudignore ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── app.yaml ├── components ├── AppContext.ts ├── MuiTheme.ts ├── atoms │ ├── SpacingPaper.tsx │ └── index.ts ├── molecules │ ├── NextListItem.tsx │ ├── PageHeader.tsx │ ├── ReduxSagaResponse.tsx │ └── index.ts ├── organisms │ ├── HeaderArticleContainer.tsx │ ├── ReduxSagaSample.tsx │ ├── ResponsiveDrawer.tsx │ ├── Sidenavi.tsx │ └── index.ts └── templates │ ├── Layout.tsx │ └── index.ts ├── constants ├── Env.ts ├── IEnum.ts ├── Page.ts ├── SagaSetting.ts ├── SiteInfo.ts └── index.ts ├── deploy-appengine.sh ├── hooks ├── index.ts ├── useCounter.ts ├── usePage.ts └── useThinOut.ts ├── model ├── InputResponseModel.ts └── 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 ├── about.tsx ├── api │ └── input.tsx ├── index.tsx ├── redux-saga.tsx └── redux.tsx ├── server.js ├── store ├── api │ ├── InputApi.ts │ └── index.ts ├── configureStore.development.ts ├── configureStore.production.ts ├── configureStore.ts ├── counter │ ├── actions.ts │ ├── index.ts │ ├── reducers.ts │ ├── selectors.ts │ └── states.ts ├── page │ ├── actions.ts │ ├── index.ts │ ├── reducers.ts │ ├── selectors.ts │ └── states.ts ├── reducers.ts ├── redux-saga │ ├── actions.ts │ ├── index.ts │ ├── reducers.ts │ ├── sagas.ts │ ├── selectors.ts │ └── states.ts └── sagas.ts ├── styles └── main.css ├── tsconfig.json └── types ├── nodejs.d.ts ├── styled-jsx.d.ts ├── styles.d.ts └── window.d.ts /.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: -------------------------------------------------------------------------------- 1 | API_SERVER_URL=/ 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ 18 | .vscode/ 19 | next.config.js 20 | .editorconfig 21 | .prettierrc 22 | deploy.sh 23 | yarn-error.log 24 | -------------------------------------------------------------------------------- /.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 | 39 | ## Next.js 40 | .next 41 | 42 | .now -------------------------------------------------------------------------------- /.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 | ".now": true 10 | }, 11 | "files.insertFinalNewline": true, 12 | "editor.defaultFormatter": "esbenp.prettier-vscode", 13 | "javascript.format.enable": false, 14 | "eslint.enable": true, 15 | "eslint.format.enable": true, 16 | "editor.codeActionsOnSave": { 17 | "source.fixAll.eslint": true 18 | }, 19 | "[javascript]": { 20 | "editor.formatOnSave": true, 21 | "editor.codeActionsOnSave": { 22 | "source.organizeImports": true 23 | } 24 | }, 25 | "[typescript]": { 26 | "editor.formatOnSave": true, 27 | "editor.codeActionsOnSave": { 28 | "source.organizeImports": true 29 | } 30 | }, 31 | "[typescriptreact]": { 32 | "editor.formatOnSave": true, 33 | "editor.codeActionsOnSave": { 34 | "source.organizeImports": true 35 | } 36 | }, 37 | "[jsonc]": { 38 | "editor.formatOnSave": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typescript-nextjs-redux-material-ui-example 2 | 3 | This using typescript, next.js, redux, material-ui is simple, and is a sample corresponding to the server side rendering. 4 | 5 | By VSCode and prettier and ESLint, realtime code format and realtime sentence structure check and rearranging of unused import are carried out in real time. 6 | 7 | これは、typescript, next.js, redux, material-ui を使った、シンプルでサーバーサイドレンダリングに対応したサンプルです。 8 | 9 | VSCode と prettier と ESLint によって、リアルタイムに整形と構文チェックと未使用 import の整理が行われます。 10 | 11 | ## Live demo 12 | 13 | [Live demo](https://typescript-nextjs-redux-material-ui-example.now.sh/) 14 | 15 | ## Screenshot 16 | 17 | ### For desktop 18 | 19 | ![For desktop 1](https://user-images.githubusercontent.com/12574048/46964420-f9fb9180-d0e2-11e8-9c05-e1594c533947.png) 20 | ![For desktop 2](https://user-images.githubusercontent.com/12574048/71005010-3337f300-2126-11ea-844c-d113f5d87255.png) 21 | 22 | ### For mobile 23 | 24 | ![For mobile](https://user-images.githubusercontent.com/12574048/46964454-126bac00-d0e3-11e8-8bdc-ebf47c907ed1.png) 25 | 26 | ## Features 27 | 28 | - [Google App Engine Node.js Standard Environment](https://cloud.google.com/appengine/docs/standard/nodejs/) 29 | - [Visual Studio Code](https://code.visualstudio.com/) 30 | - [Typescript v3](https://www.typescriptlang.org/) 31 | - [Next.js v9](https://nextjs.org/) 32 | - [MATERIAL-UI v4](https://material-ui.com/) 33 | - [Redux](https://redux.js.org/) 34 | - [redux-saga](https://redux-saga.js.org/) 35 | - [typescript-fsa](https://github.com/aikoven/typescript-fsa) 36 | - [typescript-fsa-reducer](https://github.com/dphilipson/typescript-fsa-reducers) 37 | - [ESLint](https://eslint.org/) 38 | 39 | ## Requirement 40 | 41 | - [Google Chrome](https://www.google.com/intl/ja_ALL/chrome/) 42 | - [Visual Studio Code](https://code.visualstudio.com/) 43 | - TypeScript v3.7 or higher( [require Optional Chaining](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#optional-chaining) ) 44 | 45 | ## Install Google Chrome addon 46 | 47 | - [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ja) 48 | 49 | ## Recommended VSCode addons 50 | 51 | - [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) 52 | - [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 53 | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 54 | - [Bracket Pair Colorizer 2](https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer-2) 55 | 56 | ## Usage 57 | 58 | ### Download and install 59 | 60 | ```bash 61 | git clone https://github.com/treetips/typescript-nextjs-redux-material-ui-example.git 62 | cd typescript-nextjs-redux-material-ui-example 63 | npm i 64 | ``` 65 | 66 | ### Start local 67 | 68 | ```bash 69 | npm run dev 70 | ``` 71 | 72 | ### Build and start production express server 73 | 74 | ```bash 75 | npm run build 76 | npm start 77 | ``` 78 | 79 | ## For google appengine 80 | 81 | ### [Optional] appengine deploy Settings 82 | 83 | ```bash 84 | vi ./deploy-appengine.sh 85 | ``` 86 | 87 | ### Deploy appengine 88 | 89 | ```bash 90 | ./deploy-appengine.sh 91 | ``` 92 | 93 | ## Related repository 94 | 95 | * [typescript-nextjs-redux-toolkit-material-ui-example](https://github.com/treetips/typescript-nextjs-redux-toolkit-material-ui-example) 96 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | service: default 2 | runtime: nodejs8 3 | default_expiration: "1m" 4 | 5 | handlers: 6 | - url: /.* 7 | script: server.js 8 | -------------------------------------------------------------------------------- /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/ReduxSagaResponse.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 2 | import React from "react" 3 | 4 | const useStyles = makeStyles((_: Theme) => 5 | createStyles({ 6 | root: {}, 7 | }) 8 | ) 9 | 10 | type Props = { 11 | responses: string[] 12 | } 13 | 14 | /** 15 | * redux-saga response component 16 | * @param props Props 17 | */ 18 | export const ReduxSagaResponse = function (props: Props) { 19 | const classes = useStyles(props) 20 | const { responses } = props 21 | return ( 22 |
    23 | {responses.map((value: string, index: number) => ( 24 |
  • 25 | [{String(index + 1).padStart(2, "0")}] {value} 26 |
  • 27 | ))} 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /components/molecules/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./NextListItem" 2 | export * from "./PageHeader" 3 | export * from "./ReduxSagaResponse" 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/ReduxSagaSample.tsx: -------------------------------------------------------------------------------- 1 | import { Box, InputAdornment, TextField, Typography } from "@material-ui/core" 2 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 3 | import { Create, Timer } from "@material-ui/icons" 4 | import React, { useEffect, useState } from "react" 5 | import { IReduxSagaState } from "../../store/redux-saga" 6 | import { ReduxSagaResponse } from "../molecules" 7 | 8 | const useStyles = makeStyles((theme: Theme) => 9 | createStyles({ 10 | root: {}, 11 | title: { 12 | fontSize: "50px", 13 | marginBottom: theme.spacing(2), 14 | }, 15 | subTitle: { 16 | fontSize: "35px", 17 | }, 18 | section: { 19 | marginBottom: theme.spacing(4), 20 | }, 21 | }) 22 | ) 23 | 24 | type Props = { 25 | title: string 26 | description?: React.ReactNode 27 | onChange: (inputValue: string) => void 28 | storeState?: IReduxSagaState 29 | responseResultMax: number 30 | interval: number 31 | } 32 | 33 | /** 34 | * redux-saga sample component 35 | * @param props {Props} props 36 | */ 37 | export const ReduxSagaSample = function (props: Props) { 38 | const { 39 | title, 40 | description, 41 | onChange, 42 | storeState, 43 | responseResultMax, 44 | interval, 45 | } = props 46 | const classes = useStyles(props) 47 | const [requestValue, setRequestValue] = useState("") 48 | const [responseValues, setResponseValues] = useState([]) 49 | const [previousResponseValue, setPreviousResponseValue] = useState("") 50 | 51 | useEffect(() => { 52 | if (!storeState?.timestamp) { 53 | return 54 | } 55 | const fetchResult = `${storeState.timestamp} - ${storeState.input}` 56 | if (fetchResult === previousResponseValue) { 57 | return 58 | } 59 | responseValues.unshift(fetchResult) 60 | if (responseValues && responseResultMax < responseValues.length) { 61 | responseValues.pop() 62 | } 63 | setResponseValues(responseValues) 64 | setPreviousResponseValue(fetchResult) 65 | }, [storeState?.timestamp]) 66 | 67 | const handleChangeInput = (e: React.ChangeEvent) => { 68 | const value = e.target.value || "" 69 | setRequestValue(value) 70 | onChange(value) 71 | } 72 | 73 | return ( 74 | <> 75 | 76 | {title} with redux-saga 77 | 78 | 79 | {description && {description}} 80 | 81 | 82 | 91 | 92 | 93 | ), 94 | }} 95 | /> 96 | 97 | 98 | 99 | 108 | 109 | 110 | ), 111 | }} 112 | /> 113 | 114 | 115 | 116 | {title} response 117 | 118 | 119 | 120 | {responseValues && } 121 | 122 | 123 | ) 124 | } 125 | -------------------------------------------------------------------------------- /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 | const handleChangePage = (page: Page) => () => changePage(page) 57 | 58 | return ( 59 |
60 |
61 | {SiteInfo.SITE_NAME} 62 |
63 | 64 | 65 | {Page.values.map((page) => { 66 | const Icon = page.icon 67 | return ( 68 | 80 | 81 | 82 | } 83 | onClick={handleChangePage(page)} 84 | /> 85 | ) 86 | })} 87 | 88 |
89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /components/organisms/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./HeaderArticleContainer" 2 | export * from "./ReduxSagaSample" 3 | export * from "./ResponsiveDrawer" 4 | export * from "./Sidenavi" 5 | -------------------------------------------------------------------------------- /components/templates/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles" 2 | import Head from "next/head" 3 | import * as 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, orange, pink, red, teal } from "@material-ui/core/colors" 3 | import { SvgIconProps } from "@material-ui/core/SvgIcon" 4 | import { Home, Info, Save, Whatshot } 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 REDUX_SAGAA = new Page( 38 | 3, 39 | "Redux Saga", 40 | "Redux Saga sample", 41 | "Redux Saga sample | sample", 42 | "Basic redux-saga examples with typescript-fsa and immer.", 43 | "/redux-saga", 44 | Whatshot, 45 | teal 46 | ) 47 | public static readonly ABOUT = new Page( 48 | 10, 49 | "About", 50 | "About this site", 51 | "About | sample", 52 | "Site about page.", 53 | "/about", 54 | Info, 55 | orange 56 | ) 57 | public static readonly ERROR = new Page( 58 | 99, 59 | "Error", 60 | "Error", 61 | "Error | sample", 62 | "Error.", 63 | "/error", 64 | Info, 65 | red 66 | ) 67 | 68 | /** 69 | * constructor 70 | * @param number page id 71 | * @param pageTitle page title 72 | * @param pageDescription page description 73 | * @param title seo title 74 | * @param metaDescription seo meta description 75 | * @param relativeUrl relative url 76 | * @param icon page icon 77 | * @param iconColor page icon color 78 | */ 79 | private constructor( 80 | public readonly id: number, 81 | public readonly pageTitle: string, 82 | public readonly pageDescription: string, 83 | public readonly title: string, 84 | public readonly metaDescription: string, 85 | public readonly relativeUrl: string, 86 | public readonly icon: React.ComponentType, 87 | public readonly iconColor: Color 88 | ) { 89 | Page._values.push(this) 90 | } 91 | 92 | /** 93 | * Instance array 94 | */ 95 | static get values(): Page[] { 96 | return this._values 97 | } 98 | 99 | /** 100 | * @inheritdoc 101 | */ 102 | equals = (target: Page): boolean => this.id === target.id 103 | 104 | /** 105 | * @inheritdoc 106 | */ 107 | toString = (): string => 108 | `${this.id}, ${this.pageTitle}, ${this.pageDescription}` 109 | } 110 | -------------------------------------------------------------------------------- /constants/SagaSetting.ts: -------------------------------------------------------------------------------- 1 | export const SagaSetting = { 2 | DEBOUNCE_INTERVAL: 2000, 3 | THROTTLE_INTERVAL: 1000, 4 | } 5 | -------------------------------------------------------------------------------- /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 "./SagaSetting" 5 | export * from "./SiteInfo" 6 | -------------------------------------------------------------------------------- /deploy-appengine.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | GAE_PRJ_ID="<>" 3 | GAE_APP_YML="app.yaml" 4 | GAE_DEPLOY_VERSION="<>" 5 | GAE_URL="https://${GAE_DEPLOY_VERSION}-dot-${GAE_PRJ_ID}.appspot.com" 6 | 7 | npm run build 8 | 9 | if [ $? -ne 0 ]; then 10 | echo "npm build failed." 11 | exit 1 12 | fi 13 | 14 | echo "Y" | \ 15 | gcloud --project $GAE_PRJ_ID \ 16 | app \ 17 | deploy $GAE_APP_YML \ 18 | --version $GAE_DEPLOY_VERSION \ 19 | --no-promote 20 | 21 | open $GAE_URL 22 | -------------------------------------------------------------------------------- /hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useCounter" 2 | export * from "./usePage" 3 | export * from "./useThinOut" 4 | -------------------------------------------------------------------------------- /hooks/useCounter.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react" 2 | import { useDispatch, useSelector } from "react-redux" 3 | import { CounterActions, countSelector } from "../store/counter" 4 | 5 | type CounterOperators = { 6 | count: number 7 | increment: () => void 8 | decrement: () => void 9 | calculate: (inputNumber: number) => void 10 | } 11 | 12 | /** 13 | * Counter custom-hooks 14 | * @see https://reactjs.org/docs/hooks-custom.html 15 | */ 16 | export const useCounter = (): Readonly => { 17 | const dispatch = useDispatch() 18 | 19 | return { 20 | count: useSelector(countSelector), 21 | increment: useCallback(() => dispatch(CounterActions.increment()), [ 22 | dispatch, 23 | ]), 24 | decrement: useCallback(() => dispatch(CounterActions.decrement()), [ 25 | dispatch, 26 | ]), 27 | calculate: useCallback( 28 | (inputNumber: number) => { 29 | dispatch( 30 | CounterActions.calculate({ 31 | inputNumber: inputNumber, 32 | }) 33 | ) 34 | }, 35 | [dispatch] 36 | ), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /hooks/usePage.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react" 2 | import { useDispatch, useSelector } from "react-redux" 3 | import { Page } from "../constants" 4 | import { PageActions, selectedPageSelector } from "../store/page" 5 | 6 | type PageOperators = { 7 | selectedPage: Page 8 | changePage: (selectedPage: Page) => 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(selectedPageSelector) 18 | 19 | return { 20 | selectedPage: pageState, 21 | changePage: useCallback( 22 | (selectedPage: Page) => { 23 | dispatch( 24 | PageActions.changePage({ 25 | selectedPage: selectedPage, 26 | }) 27 | ) 28 | }, 29 | [dispatch] 30 | ), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /hooks/useThinOut.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react" 2 | import { useDispatch, useSelector } from "react-redux" 3 | import { 4 | IReduxSagaState, 5 | ReduxSagaActions, 6 | reduxSagaDebounceSelector, 7 | reduxSagaThrottleSelector, 8 | } from "../store/redux-saga" 9 | 10 | type ThinOutOperators = { 11 | debounce: (inputValue: string) => void 12 | debounceState: IReduxSagaState 13 | throttle: (inputValue: string) => void 14 | throttleState: IReduxSagaState 15 | } 16 | 17 | /** 18 | * Debounce and throttle custom-hooks 19 | * @see https://reactjs.org/docs/hooks-custom.html 20 | */ 21 | export const useThinOut = (): Readonly => { 22 | const dispatch = useDispatch() 23 | const reduxSagaDebounceState = useSelector(reduxSagaDebounceSelector) 24 | const reduxSagaThrottleState = useSelector(reduxSagaThrottleSelector) 25 | 26 | const handleDebounce = useCallback( 27 | (inputValue: string) => { 28 | dispatch( 29 | ReduxSagaActions.fetchDebounce({ 30 | input: inputValue, 31 | }) 32 | ) 33 | }, 34 | [dispatch] 35 | ) 36 | 37 | const handleThrottle = useCallback( 38 | (inputValue: string) => { 39 | dispatch( 40 | ReduxSagaActions.fetchThrottle({ 41 | input: inputValue, 42 | }) 43 | ) 44 | }, 45 | [dispatch] 46 | ) 47 | 48 | return { 49 | debounce: handleDebounce, 50 | debounceState: reduxSagaDebounceState, 51 | throttle: handleThrottle, 52 | throttleState: reduxSagaThrottleState, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /model/InputResponseModel.ts: -------------------------------------------------------------------------------- 1 | export interface InputResponseModel { 2 | input: string 3 | timestamp: string 4 | error?: Error 5 | } 6 | -------------------------------------------------------------------------------- /model/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./InputResponseModel" 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 | "API_SERVER_URL": "@api_server_url" 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 | "compression": "^1.7.4", 21 | "express": "^4.17.1", 22 | "immer": "^8.0.1", 23 | "next": "^9.3.5", 24 | "next-redux-wrapper": "^6.0.0-rc.7", 25 | "react": "^16.13.1", 26 | "react-dom": "^16.13.1", 27 | "react-redux": "^7.2.0", 28 | "redux": "^4.0.5", 29 | "redux-saga": "^1.1.3", 30 | "typescript-fsa": "^3.0.0", 31 | "typescript-fsa-reducers": "^1.2.1" 32 | }, 33 | "devDependencies": { 34 | "@babel/plugin-proposal-decorators": "^7.8.3", 35 | "@types/next-redux-wrapper": "^3.0.0", 36 | "@types/node": "^13.13.2", 37 | "@types/react": "^16.9.34", 38 | "@types/react-dom": "^16.9.6", 39 | "@types/react-jss": "^10.0.0", 40 | "@types/react-redux": "^7.1.7", 41 | "@types/redux": "^3.6.0", 42 | "@types/styled-jsx": "^2.2.8", 43 | "@typescript-eslint/eslint-plugin": "^2.31.0", 44 | "@typescript-eslint/parser": "^2.31.0", 45 | "dotenv-webpack": "^1.7.0", 46 | "eslint": "^7.0.0", 47 | "eslint-config-prettier": "^6.11.0", 48 | "eslint-plugin-import": "^2.20.2", 49 | "eslint-plugin-react": "^7.19.0", 50 | "prettier": "^2.0.5", 51 | "redux-devtools-extension": "^2.13.8", 52 | "typescript": "^3.8.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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 | function NotFoundError(props: Props) { 19 | const classes = useStyles(props) 20 | const { changePage } = usePage() 21 | 22 | useEffect(() => { 23 | changePage(Page.ERROR) 24 | }, []) 25 | 26 | return ( 27 | 28 | 29 | 30 | 404 Page NotFound :( 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | export default NotFoundError 38 | -------------------------------------------------------------------------------- /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 { configureStore } 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(configureStore, { debug: false })(MyApp) 42 | -------------------------------------------------------------------------------- /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 | return { 31 | ...initialProps, 32 | pageProps, 33 | // Styles fragment is rendered after the app and page rendering finish. 34 | styles: ( 35 | <> 36 | {sheets.getStyleElement()} 37 | {flush() || null} 38 | 39 | ), 40 | } 41 | } 42 | 43 | render() { 44 | const { pageProps } = this.props 45 | const page = pageProps.page.selectedPage 46 | 47 | return ( 48 | 49 | 50 | 51 | {/* Use minimum-scale=1 to enable GPU rasterization */} 52 | 56 | {/* PWA primary color */} 57 | 58 | 59 | 63 | 64 | 65 |
66 | 67 | 68 | 69 | ) 70 | } 71 | } 72 | 73 | export default MyDocument 74 | -------------------------------------------------------------------------------- /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 | import { Page } from "../constants" 9 | import { IPagePayload, PageActions } from "../store/page" 10 | 11 | const useStyles = makeStyles((_: Theme) => 12 | createStyles({ 13 | root: {}, 14 | }) 15 | ) 16 | 17 | type Props = { 18 | httpStatusCode?: number 19 | } 20 | 21 | function Error(props: Props) { 22 | const { httpStatusCode } = props 23 | const classes = useStyles(props) 24 | return ( 25 | 26 | 27 | 28 | 29 | Http status code {httpStatusCode} error ! 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | /** 38 | * Server side rendering 39 | */ 40 | Error.getInitialProps = async (ctx: AppContext): Promise => { 41 | const { res, store } = ctx 42 | 43 | const pagePayload: IPagePayload = { 44 | selectedPage: Page.ERROR, 45 | } 46 | store.dispatch({ 47 | type: PageActions.changePage.toString(), 48 | payload: pagePayload, 49 | }) 50 | return { 51 | httpStatusCode: res?.statusCode, 52 | } 53 | } 54 | 55 | export default Error 56 | -------------------------------------------------------------------------------- /pages/about.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 { IPagePayload, PageActions } from "../store/page" 10 | 11 | const useStyles = makeStyles((_: Theme) => 12 | createStyles({ 13 | root: {}, 14 | }) 15 | ) 16 | 17 | type Props = {} 18 | 19 | function About(props: Props) { 20 | const classes = useStyles(props) 21 | return ( 22 | 23 | 24 | 25 | About page !! 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | /** 33 | * Server side rendering 34 | */ 35 | About.getInitialProps = async (ctx: AppContext): Promise => { 36 | const { store } = ctx 37 | 38 | const pagePayload: IPagePayload = { 39 | selectedPage: Page.ABOUT, 40 | } 41 | store.dispatch({ 42 | type: PageActions.changePage.toString(), 43 | payload: pagePayload, 44 | }) 45 | return {} 46 | } 47 | 48 | export default About 49 | -------------------------------------------------------------------------------- /pages/api/input.tsx: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next" 2 | import { InputResponseModel } from "../../model" 3 | 4 | const getCurrentTimestamp = () => { 5 | const padding = (value: number, num: number): string => 6 | String(value).padStart(num, "0") 7 | const d = new Date() 8 | const year = d.getFullYear() 9 | const month = padding(d.getMonth() + 1, 2) 10 | const date = padding(d.getDate(), 2) 11 | const hour = padding(d.getHours(), 2) 12 | const minute = padding(d.getMinutes(), 2) 13 | const second = padding(d.getSeconds(), 2) 14 | const microSecond = padding(d.getMilliseconds(), 3) 15 | 16 | return `${year}/${month}/${date} ${hour}:${minute}:${second}.${microSecond}` 17 | } 18 | 19 | export default (req: NextApiRequest, res: NextApiResponse) => { 20 | const inputValue = String(req.query["value"]) 21 | const responseBody: InputResponseModel = { 22 | input: inputValue, 23 | timestamp: getCurrentTimestamp(), 24 | } 25 | res.status(200).json(responseBody) 26 | } 27 | -------------------------------------------------------------------------------- /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 { IPagePayload, PageActions } 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 | * Server side rendering 41 | */ 42 | Index.getInitialProps = async (ctx: AppContext): Promise => { 43 | const { store } = ctx 44 | 45 | const pagePayload: IPagePayload = { 46 | selectedPage: Page.TOP, 47 | } 48 | store.dispatch({ 49 | type: PageActions.changePage.toString(), 50 | payload: pagePayload, 51 | }) 52 | return {} 53 | } 54 | 55 | export default Index 56 | -------------------------------------------------------------------------------- /pages/redux-saga.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 { 7 | HeaderArticleContainer, 8 | ReduxSagaSample, 9 | } from "../components/organisms" 10 | import { Layout } from "../components/templates" 11 | import { Page, SagaSetting } from "../constants" 12 | import { useThinOut } from "../hooks" 13 | import { IPagePayload, PageActions } from "../store/page" 14 | 15 | const useStyles = makeStyles((_: Theme) => 16 | createStyles({ 17 | root: {}, 18 | }) 19 | ) 20 | 21 | type Props = {} 22 | 23 | function ReduxSaga(props: Props) { 24 | const {} = props 25 | const classes = useStyles(props) 26 | const { debounce, debounceState, throttle, throttleState } = useThinOut() 27 | 28 | return ( 29 | 30 | 31 | 32 | 36 | 37 | Open DevTools of Google Chrome, open the network tab, and 38 | check the execution frequency and timing of api. 39 | 40 | 41 | } 42 | storeState={debounceState} 43 | responseResultMax={10} 44 | interval={SagaSetting.DEBOUNCE_INTERVAL} 45 | onChange={(inputValue: string) => debounce(inputValue)} 46 | /> 47 | 48 | 49 | 50 | throttle(inputValue)} 56 | /> 57 | 58 | 59 | 60 | ) 61 | } 62 | 63 | /** 64 | * Server side rendering 65 | */ 66 | ReduxSaga.getInitialProps = async (ctx: AppContext): Promise => { 67 | const { store } = ctx 68 | 69 | const pagePayload: IPagePayload = { 70 | selectedPage: Page.REDUX_SAGAA, 71 | } 72 | store.dispatch({ 73 | type: PageActions.changePage.toString(), 74 | payload: pagePayload, 75 | }) 76 | return {} 77 | } 78 | 79 | export default ReduxSaga 80 | -------------------------------------------------------------------------------- /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 { IPagePayload, PageActions } 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 { defaultInputNumber: defaultCount } = props 39 | const classes = useStyles(props) 40 | const [inputNumber, setInputNumber] = useState(defaultCount) 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 | * Server side rendering 107 | */ 108 | Redux.getInitialProps = async (ctx: AppContext): Promise => { 109 | const { store } = ctx 110 | 111 | const pagePayload: IPagePayload = { 112 | selectedPage: Page.REDUX, 113 | } 114 | store.dispatch({ 115 | type: PageActions.changePage.toString(), 116 | payload: pagePayload, 117 | }) 118 | return { 119 | defaultInputNumber: 2, 120 | } 121 | } 122 | 123 | export default Redux 124 | -------------------------------------------------------------------------------- /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/api/InputApi.ts: -------------------------------------------------------------------------------- 1 | import { Env } from "../../constants" 2 | import { InputResponseModel } from "../../model" 3 | import { IReduxSagaFetchPayload } from "../redux-saga" 4 | 5 | export const fetchInputApi = ( 6 | payload: IReduxSagaFetchPayload 7 | ): Promise => { 8 | const url = `${Env.API_SERVER_URL}api/input?value=${payload.input}` 9 | return fetch(url).then((response) => response.json()) 10 | } 11 | -------------------------------------------------------------------------------- /store/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./InputApi" 2 | -------------------------------------------------------------------------------- /store/configureStore.development.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from "redux" 2 | import { composeWithDevTools } from "redux-devtools-extension" 3 | import createSagaMiddleware from "redux-saga" 4 | import { combinedReducers, RootState } from "./reducers" 5 | import { rootSaga } from "./sagas" 6 | 7 | const sagaMiddleware = createSagaMiddleware() 8 | 9 | export function configureStore(initialState?: RootState) { 10 | const store = createStore( 11 | combinedReducers, 12 | initialState, 13 | composeWithDevTools(applyMiddleware(sagaMiddleware)) 14 | ) 15 | sagaMiddleware.run(rootSaga) 16 | return store 17 | } 18 | -------------------------------------------------------------------------------- /store/configureStore.production.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from "redux" 2 | import createSagaMiddleware from "redux-saga" 3 | import { combinedReducers, RootState } from "./reducers" 4 | import { rootSaga } from "./sagas" 5 | 6 | const sagaMiddleware = createSagaMiddleware() 7 | 8 | export function configureStore(initialState?: RootState) { 9 | const store = createStore( 10 | combinedReducers, 11 | initialState, 12 | applyMiddleware(sagaMiddleware) 13 | ) 14 | sagaMiddleware.run(rootSaga) 15 | return store 16 | } 17 | -------------------------------------------------------------------------------- /store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { Env } from "../constants" 2 | import { RootState } from "./reducers" 3 | 4 | const configureStoreComponent = (() => { 5 | if (Env.NODE_ENV === "production") { 6 | return require("./configureStore.production") 7 | } 8 | return require("./configureStore.development") 9 | })() 10 | 11 | export const configureStore = (initialState?: RootState) => 12 | configureStoreComponent.configureStore(initialState) 13 | -------------------------------------------------------------------------------- /store/counter/actions.ts: -------------------------------------------------------------------------------- 1 | import actionCreatorFactory from "typescript-fsa" 2 | 3 | const actionCreator = actionCreatorFactory("counter") 4 | 5 | export interface ICounterPayload { 6 | inputNumber: number 7 | } 8 | 9 | export const CounterActions = { 10 | increment: actionCreator("increment"), 11 | decrement: actionCreator("decrement"), 12 | calculate: actionCreator("calculate"), 13 | } 14 | -------------------------------------------------------------------------------- /store/counter/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./actions" 2 | export * from "./selectors" 3 | export * from "./states" 4 | -------------------------------------------------------------------------------- /store/counter/reducers.ts: -------------------------------------------------------------------------------- 1 | import produce from "immer" 2 | import { reducerWithInitialState } from "typescript-fsa-reducers" 3 | import { 4 | CounterActions, 5 | CounterInitialState, 6 | ICounterPayload, 7 | ICounterState, 8 | } from "." 9 | 10 | export const countReducer = reducerWithInitialState(CounterInitialState) 11 | .case( 12 | CounterActions.increment, 13 | (state: Readonly): ICounterState => { 14 | return produce(state, (draft: ICounterState) => { 15 | draft.count = state.count + 1 16 | }) 17 | } 18 | ) 19 | .case( 20 | CounterActions.decrement, 21 | (state: Readonly): ICounterState => { 22 | return produce(state, (draft: ICounterState) => { 23 | draft.count = state.count - 1 24 | }) 25 | } 26 | ) 27 | .case( 28 | CounterActions.calculate, 29 | ( 30 | state: Readonly, 31 | payload: ICounterPayload 32 | ): ICounterState => { 33 | const { inputNumber } = payload 34 | return produce(state, (draft: ICounterState) => { 35 | draft.count = state.count + inputNumber 36 | }) 37 | } 38 | ) 39 | -------------------------------------------------------------------------------- /store/counter/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from "../reducers" 2 | 3 | export const countSelector = (state: RootState) => state.counter.count 4 | -------------------------------------------------------------------------------- /store/counter/states.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Counter 3 | */ 4 | export interface ICounterState { 5 | count: number 6 | } 7 | export const CounterInitialState: ICounterState = { 8 | count: 1, 9 | } 10 | -------------------------------------------------------------------------------- /store/page/actions.ts: -------------------------------------------------------------------------------- 1 | import actionCreatorFactory from "typescript-fsa" 2 | import { Page } from "../../constants" 3 | 4 | const actionCreator = actionCreatorFactory("page") 5 | 6 | export interface IPagePayload { 7 | selectedPage: Page 8 | } 9 | 10 | export const PageActions = { 11 | changePage: actionCreator("changePage"), 12 | } 13 | -------------------------------------------------------------------------------- /store/page/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./actions" 2 | export * from "./selectors" 3 | export * from "./states" 4 | -------------------------------------------------------------------------------- /store/page/reducers.ts: -------------------------------------------------------------------------------- 1 | import produce from "immer" 2 | import { reducerWithInitialState } from "typescript-fsa-reducers" 3 | import { IPagePayload, IPageState, PageActions, PageInitialState } from "." 4 | 5 | export const pageReducer = reducerWithInitialState(PageInitialState).case( 6 | PageActions.changePage, 7 | ( 8 | state: Readonly, 9 | payload: Readonly 10 | ): IPageState => { 11 | return produce(state, (draft: IPageState) => { 12 | draft.selectedPage = payload.selectedPage 13 | }) 14 | } 15 | ) 16 | -------------------------------------------------------------------------------- /store/page/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from "../reducers" 2 | 3 | export const selectedPageSelector = (state: RootState) => 4 | state.page.selectedPage 5 | -------------------------------------------------------------------------------- /store/page/states.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "../../constants" 2 | 3 | /** 4 | * Page 5 | */ 6 | export interface IPageState { 7 | selectedPage: Page 8 | } 9 | export const PageInitialState: IPageState = { 10 | selectedPage: Page.TOP, 11 | } 12 | -------------------------------------------------------------------------------- /store/reducers.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux" 2 | import { countReducer } from "./counter/reducers" 3 | import { pageReducer } from "./page/reducers" 4 | import { 5 | reduxSagaDebounceReducer, 6 | reduxSagaThrottleReducer, 7 | } from "./redux-saga/reducers" 8 | 9 | export const combinedReducers = combineReducers({ 10 | counter: countReducer, 11 | page: pageReducer, 12 | reduxSagaDebounce: reduxSagaDebounceReducer, 13 | reduxSagaThrottle: reduxSagaThrottleReducer, 14 | }) 15 | 16 | export type RootState = ReturnType 17 | -------------------------------------------------------------------------------- /store/redux-saga/actions.ts: -------------------------------------------------------------------------------- 1 | import actionCreatorFactory from "typescript-fsa" 2 | 3 | const actionCreator = actionCreatorFactory("redux-saga") 4 | 5 | export interface IReduxSagaFetchPayload { 6 | input: string 7 | } 8 | 9 | //------------------------------------------------------- 10 | // debounce 11 | //------------------------------------------------------- 12 | export interface IReduxSagaDebounceSuccessPayload { 13 | input: string 14 | timestamp: string 15 | } 16 | 17 | export interface IReduxSagaDebounceFailurePayload { 18 | error: Error 19 | } 20 | 21 | //------------------------------------------------------- 22 | // throttle 23 | //------------------------------------------------------- 24 | export interface IReduxSagaThrottleSuccessPayload { 25 | input: string 26 | timestamp: string 27 | } 28 | 29 | export interface IReduxSagaThrottleFailurePayload { 30 | error: Error 31 | } 32 | 33 | export const ReduxSagaActions = { 34 | // debounce 35 | fetchDebounce: actionCreator("fetch debounce"), 36 | debounceSuccess: actionCreator( 37 | "debounce success" 38 | ), 39 | debounceFailure: actionCreator( 40 | "debounce failure" 41 | ), 42 | 43 | // throttle 44 | fetchThrottle: actionCreator("fetch throttle"), 45 | throttleSuccess: actionCreator( 46 | "throttle success" 47 | ), 48 | throttleFailure: actionCreator( 49 | "throttle failure" 50 | ), 51 | } 52 | 53 | export type ReduxSagaActionTypes = 54 | | ReturnType 55 | | ReturnType 56 | | ReturnType 57 | | ReturnType 58 | | ReturnType 59 | | ReturnType 60 | -------------------------------------------------------------------------------- /store/redux-saga/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./actions" 2 | export * from "./selectors" 3 | export * from "./states" 4 | -------------------------------------------------------------------------------- /store/redux-saga/reducers.ts: -------------------------------------------------------------------------------- 1 | import produce from "immer" 2 | import { reducerWithInitialState } from "typescript-fsa-reducers" 3 | import { 4 | IReduxSagaDebounceFailurePayload, 5 | IReduxSagaDebounceSuccessPayload, 6 | IReduxSagaThrottleFailurePayload, 7 | IReduxSagaThrottleSuccessPayload, 8 | ReduxSagaActions, 9 | } from "./actions" 10 | import { IReduxSagaState, ReduxSagaInitialState } from "./states" 11 | 12 | export const reduxSagaDebounceReducer = reducerWithInitialState( 13 | ReduxSagaInitialState 14 | ) 15 | .case( 16 | ReduxSagaActions.fetchDebounce, 17 | (state: Readonly): IReduxSagaState => { 18 | return state 19 | } 20 | ) 21 | .case( 22 | ReduxSagaActions.debounceSuccess, 23 | ( 24 | state: Readonly, 25 | payload: IReduxSagaDebounceSuccessPayload 26 | ): IReduxSagaState => { 27 | const { input, timestamp } = payload 28 | return produce(state, (draft: IReduxSagaState) => { 29 | draft.input = input 30 | draft.timestamp = timestamp 31 | }) 32 | } 33 | ) 34 | .case( 35 | ReduxSagaActions.debounceFailure, 36 | ( 37 | state: Readonly, 38 | payload: IReduxSagaDebounceFailurePayload 39 | ): IReduxSagaState => { 40 | const { error } = payload 41 | return produce(state, (draft: IReduxSagaState) => { 42 | draft.error = error 43 | }) 44 | } 45 | ) 46 | 47 | export const reduxSagaThrottleReducer = reducerWithInitialState( 48 | ReduxSagaInitialState 49 | ) 50 | .case( 51 | ReduxSagaActions.fetchThrottle, 52 | (state: Readonly): IReduxSagaState => { 53 | return state 54 | } 55 | ) 56 | .case( 57 | ReduxSagaActions.throttleSuccess, 58 | ( 59 | state: Readonly, 60 | payload: IReduxSagaThrottleSuccessPayload 61 | ): IReduxSagaState => { 62 | const { input, timestamp } = payload 63 | return produce(state, (draft: IReduxSagaState) => { 64 | draft.input = input 65 | draft.timestamp = timestamp 66 | }) 67 | } 68 | ) 69 | .case( 70 | ReduxSagaActions.throttleFailure, 71 | ( 72 | state: Readonly, 73 | payload: IReduxSagaThrottleFailurePayload 74 | ): IReduxSagaState => { 75 | const { error } = payload 76 | return produce(state, (draft: IReduxSagaState) => { 77 | draft.error = error 78 | }) 79 | } 80 | ) 81 | -------------------------------------------------------------------------------- /store/redux-saga/sagas.ts: -------------------------------------------------------------------------------- 1 | import { call, debounce, put, throttle } from "redux-saga/effects" 2 | import { SagaSetting } from "../../constants" 3 | import { InputResponseModel } from "../../model" 4 | import { fetchInputApi } from "../api" 5 | import { 6 | IReduxSagaDebounceFailurePayload, 7 | IReduxSagaDebounceSuccessPayload, 8 | IReduxSagaFetchPayload, 9 | IReduxSagaThrottleFailurePayload, 10 | IReduxSagaThrottleSuccessPayload, 11 | ReduxSagaActions, 12 | ReduxSagaActionTypes, 13 | } from "./actions" 14 | 15 | function* executeFetchDebounce(action: ReduxSagaActionTypes) { 16 | const fetchPayload = action.payload as IReduxSagaFetchPayload 17 | 18 | try { 19 | // call api 20 | const fetchResult: InputResponseModel = yield call( 21 | fetchInputApi, 22 | fetchPayload 23 | ) 24 | 25 | // Pack the fetch result into a redux successful action, 26 | // and the caller gets the result from there 27 | const successPayload: IReduxSagaDebounceSuccessPayload = { 28 | input: fetchResult.input, 29 | timestamp: fetchResult.timestamp, 30 | } 31 | yield put({ 32 | type: ReduxSagaActions.debounceSuccess.toString(), 33 | payload: successPayload, 34 | }) 35 | } catch (e) { 36 | console.error(e) 37 | 38 | // Pack exception error object into failure action, caller gets it 39 | const failurePayload: IReduxSagaDebounceFailurePayload = { 40 | error: e, 41 | } 42 | yield put({ 43 | type: ReduxSagaActions.debounceFailure.toString(), 44 | payload: failurePayload, 45 | }) 46 | } 47 | } 48 | 49 | /** 50 | * Monitor specific redux-debounce-action fire when detected 51 | */ 52 | export const watchFetchDebounce = function* () { 53 | yield debounce( 54 | SagaSetting.DEBOUNCE_INTERVAL, 55 | ReduxSagaActions.fetchDebounce, 56 | executeFetchDebounce 57 | ) 58 | } 59 | 60 | function* executeFetchThrottle(action: ReduxSagaActionTypes) { 61 | const fetchPayload = action.payload as IReduxSagaFetchPayload 62 | 63 | try { 64 | const fetchResult: InputResponseModel = yield call( 65 | fetchInputApi, 66 | fetchPayload 67 | ) 68 | 69 | const successPayload: IReduxSagaThrottleSuccessPayload = { 70 | input: fetchResult.input, 71 | timestamp: fetchResult.timestamp, 72 | } 73 | yield put({ 74 | type: ReduxSagaActions.throttleSuccess.toString(), 75 | payload: successPayload, 76 | }) 77 | } catch (e) { 78 | console.error(e) 79 | 80 | const failurePayload: IReduxSagaThrottleFailurePayload = { 81 | error: e, 82 | } 83 | yield put({ 84 | type: ReduxSagaActions.throttleFailure.toString(), 85 | payload: failurePayload, 86 | }) 87 | } 88 | } 89 | 90 | /** 91 | * Monitor specific redux-throttle-action fire when detected 92 | */ 93 | export const watchFetchThrottle = function* () { 94 | yield throttle( 95 | SagaSetting.THROTTLE_INTERVAL, 96 | ReduxSagaActions.fetchThrottle, 97 | executeFetchThrottle 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /store/redux-saga/selectors.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from "../reducers" 2 | import { IReduxSagaState } from "./states" 3 | 4 | export const reduxSagaDebounceSelector = (state: RootState): IReduxSagaState => 5 | state.reduxSagaDebounce 6 | 7 | export const reduxSagaThrottleSelector = (state: RootState): IReduxSagaState => 8 | state.reduxSagaThrottle 9 | -------------------------------------------------------------------------------- /store/redux-saga/states.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * redux-saga 3 | */ 4 | export interface IReduxSagaState { 5 | input?: string 6 | timestamp?: string 7 | error?: Error 8 | } 9 | export const ReduxSagaInitialState: IReduxSagaState = { 10 | input: undefined, 11 | timestamp: undefined, 12 | } 13 | -------------------------------------------------------------------------------- /store/sagas.ts: -------------------------------------------------------------------------------- 1 | import { all, fork } from "redux-saga/effects" 2 | import { watchFetchDebounce, watchFetchThrottle } from "./redux-saga/sagas" 3 | 4 | export const rootSaga = function* root() { 5 | yield all([fork(watchFetchDebounce), fork(watchFetchThrottle)]) 6 | } 7 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------