├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .storybook ├── main.js └── preview.js ├── LICENSE ├── README.md ├── next-env.d.ts ├── package.json ├── public ├── favicon.ico └── vercel.svg ├── src ├── components │ ├── atoms │ │ ├── ArchivedStateButton │ │ │ ├── ArchivedStateButton.stories.tsx │ │ │ └── ArchivedStateButton.tsx │ │ ├── CloseButton │ │ │ ├── CloseButton.stories.tsx │ │ │ └── CloseButton.tsx │ │ ├── HamburgerButton │ │ │ ├── HamburgerButton.stories.tsx │ │ │ └── HamburgerButton.tsx │ │ ├── SiteLogo │ │ │ ├── SiteLogo.stories.tsx │ │ │ └── SiteLogo.tsx │ │ ├── StatusIndicator │ │ │ ├── StatusIndicator.stories.tsx │ │ │ └── StatusIndicator.tsx │ │ ├── TextInput │ │ │ ├── TextInput.stories.tsx │ │ │ └── TextInput.tsx │ │ └── ToggleInput │ │ │ ├── ToggleInput.stories.tsx │ │ │ └── ToggleInput.tsx │ ├── molecules │ │ ├── DrawerLinkList │ │ │ └── DrawerLinkList.tsx │ │ ├── Navbar │ │ │ ├── Navbar.stories.tsx │ │ │ └── Navbar.tsx │ │ └── TaskListItem │ │ │ ├── TaskListItem.stories.tsx │ │ │ └── TaskListItem.tsx │ ├── organisms │ │ ├── ArchivedTaskList │ │ │ └── ArchivedTaskList.tsx │ │ ├── MiniVariantDrawer │ │ │ ├── MiniVariantDrawer.stories.tsx │ │ │ └── MiniVariantDrawer.tsx │ │ ├── Navigation │ │ │ ├── Navigation.stories.tsx │ │ │ └── Navigation.tsx │ │ ├── TaskList │ │ │ ├── TaskList.stories.tsx │ │ │ └── TaskList.tsx │ │ ├── TaskRegisterForm │ │ │ ├── TaskRegisterForm.stories.tsx │ │ │ └── TaskRegisterForm.tsx │ │ └── TemporaryDrawer │ │ │ └── TemporaryDrawer.tsx │ └── templates │ │ ├── WithNavigationLayout.stories.tsx │ │ └── WithNavigationLayout.tsx ├── configs │ └── theme.ts ├── models │ └── Task │ │ ├── Factory.ts │ │ ├── Id.ts │ │ ├── Mock.ts │ │ ├── Status.ts │ │ ├── Task.ts │ │ └── Title.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── index.tsx │ └── tasks │ │ └── archived.tsx ├── states │ ├── nav │ │ ├── actions.ts │ │ ├── operations.ts │ │ ├── reducers.ts │ │ ├── selectors.ts │ │ └── types.ts │ ├── store.ts │ └── tasks │ │ ├── actions.ts │ │ ├── operations.ts │ │ ├── reducers.ts │ │ ├── selectors.ts │ │ └── types.ts └── styles │ └── globals.css ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [ 6 | [ 7 | "babel-plugin-styled-components", 8 | { 9 | "ssr": true, 10 | "displayName": true 11 | } 12 | ] 13 | ] 14 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "airbnb", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react-hooks/recommended", 11 | "plugin:import/react", 12 | "prettier" 13 | ], 14 | "settings": { 15 | "import/resolver": { 16 | "node": { 17 | "extensions": [".tsx", ".js", ".jsx", ".ts"] 18 | } 19 | } 20 | }, 21 | "parser": "@typescript-eslint/parser", 22 | "parserOptions": { 23 | "ecmaFeatures": { 24 | "jsx": true 25 | }, 26 | "ecmaVersion": 2021, 27 | "sourceType": "module" 28 | }, 29 | "plugins": ["react", "@typescript-eslint", "unused-imports"], 30 | "rules": { 31 | "import/extensions": [ 32 | "error", 33 | "ignorePackages", 34 | { 35 | "js": "never", 36 | "jsx": "never", 37 | "ts": "never", 38 | "tsx": "never" 39 | } 40 | ], 41 | "react/jsx-filename-extension": [ 42 | "error", 43 | { 44 | "extensions": [".tsx"] 45 | } 46 | ], 47 | "no-use-before-define": "off", 48 | "arrow-body-style": "off", 49 | "react/prop-types": "off", 50 | "react/react-in-jsx-scope": "off", 51 | "react/jsx-props-no-spreading": "off", 52 | "react/destructuring-assignment": "off", 53 | "import/no-extraneous-dependencies": [ 54 | "error", 55 | { 56 | "devDependencies": ["**/*.stories.tsx"], 57 | "peerDependencies": false 58 | } 59 | ], 60 | "no-useless-constructor": "off", 61 | "@typescript-eslint/no-unused-vars": "off", 62 | "unused-imports/no-unused-imports-ts": "warn", 63 | "@typescript-eslint/no-use-before-define": "error", 64 | "@typescript-eslint/no-explicit-any": "off", 65 | "@typescript-eslint/explicit-module-boundary-types": "off" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # custom 37 | .idea/ 38 | 39 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 3 | addons: ["@storybook/addon-links", "@storybook/addon-essentials"], 4 | }; 5 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { MuiThemeProvider, StylesProvider } from "@material-ui/core"; 2 | import theme from "../src/configs/theme"; 3 | import { ThemeProvider } from "styled-components"; 4 | import { Provider } from "react-redux"; 5 | import store from "../src/states/store"; 6 | import React from "react"; 7 | 8 | export const parameters = { 9 | actions: { argTypesRegex: "^on[A-Z].*" }, 10 | controls: { 11 | matchers: { 12 | color: /(background|color)$/i, 13 | date: /Date$/, 14 | }, 15 | }, 16 | }; 17 | 18 | export const decorators = [ 19 | (Story) => ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ), 30 | ]; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 yudwig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Todo App Example 2 | 3 | 🚀 Deployed on Vercel 4 | https://next-redux-todo.vercel.app/ 5 | 6 | スクリーンショット 2021-05-27 21 34 39 7 | 8 | 9 | ## Features 10 | 11 | The following technologies are used. 12 | 13 | | Category | Technology | Link | 14 | |:--|:--|:--| 15 | | Language | TypeScript | https://www.typescriptlang.org/ | 16 | | Library | React | https://reactjs.org/ | 17 | | Framework | Next.js | https://nextjs.org/ | 18 | | Documentation | Storybook | https://storybook.js.org/ | 19 | | UI Library | Material UI | https://material-ui.com/ | 20 | | Styling | styled components | https://styled-components.com/ | 21 | | Skelton | create-next-app | https://nextjs.org/docs/api-reference/create-next-app | 22 | | App State Management | Redux (with re-ducks pattern) | https://redux.js.org/ | 23 | | Component State Management | useState hook | https://reactjs.org/docs/hooks-state.html | 24 | | Code Linter | ESLint | https://eslint.org/ | 25 | | Code Formatter | Prettier | https://prettier.io/ | 26 | | Design System | Atomic Design | https://atomicdesign.bradfrost.com/ | 27 | 28 | ## How to run 29 | 30 | * Run the development server 31 | 32 | ```bash 33 | yarn dev 34 | ``` 35 | 36 | * Run the storybook server 37 | 38 | ```bash 39 | yarn storybook 40 | ``` 41 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-redux-todo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "storybook": "start-storybook -p 6006", 10 | "build-storybook": "build-storybook" 11 | }, 12 | "dependencies": { 13 | "@material-ui/core": "^4.11.3", 14 | "@material-ui/icons": "^4.11.2", 15 | "@reduxjs/toolkit": "^1.5.1", 16 | "@types/react-redux": "^7.1.16", 17 | "nanoid": "^3.1.23", 18 | "next": "10.1.3", 19 | "react": "17.0.2", 20 | "react-dom": "17.0.2", 21 | "react-redux": "^7.2.4", 22 | "redux": "^4.0.5", 23 | "reselect": "^4.0.0", 24 | "styled-components": "^5.2.3" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.13.15", 28 | "@storybook/addon-actions": "^6.2.8", 29 | "@storybook/addon-essentials": "^6.2.8", 30 | "@storybook/addon-links": "^6.2.8", 31 | "@storybook/react": "^6.2.8", 32 | "@types/node": "^14.14.39", 33 | "@types/react": "^17.0.3", 34 | "@types/react-dom": "^17.0.3", 35 | "@types/styled-components": "^5.1.9", 36 | "@typescript-eslint/eslint-plugin": "^4.22.0", 37 | "@typescript-eslint/parser": "^4.22.0", 38 | "babel-loader": "^8.2.2", 39 | "babel-plugin-styled-components": "^1.12.0", 40 | "eslint": "^7.24.0", 41 | "eslint-config-airbnb": "^18.2.1", 42 | "eslint-config-prettier": "^8.2.0", 43 | "eslint-plugin-import": "^2.22.1", 44 | "eslint-plugin-jsx-a11y": "^6.4.1", 45 | "eslint-plugin-react": "^7.23.2", 46 | "eslint-plugin-react-hooks": "^4.2.0", 47 | "eslint-plugin-unused-imports": "^1.1.1", 48 | "jest": "^26.6.3", 49 | "prettier": "^2.2.1", 50 | "redux-devtools-extension": "^2.13.9", 51 | "typescript": "^4.2.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yudwig/next-redux-todo/df32ebb014bfdff663c70ef44649a9df1a48dfac/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/atoms/ArchivedStateButton/ArchivedStateButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import ArchivedStateButton from "./ArchivedStateButton"; 3 | 4 | export default { 5 | title: "atoms/ArchivedStateButton", 6 | component: ArchivedStateButton, 7 | argTypes: { 8 | iconType: { 9 | control: { 10 | type: "select", 11 | labels: { 12 | archive: "archive", 13 | unarchive: "unarchive", 14 | }, 15 | }, 16 | }, 17 | }, 18 | } as Meta; 19 | 20 | const Template: Story = (args) => ( 21 | 22 | ); 23 | export const Archive = Template.bind({}); 24 | Archive.args = { 25 | iconType: "archive", 26 | }; 27 | export const Unarchive = Template.bind({}); 28 | Unarchive.args = { 29 | iconType: "unarchive", 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/atoms/ArchivedStateButton/ArchivedStateButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; 3 | import { ButtonProps, IconButton } from "@material-ui/core"; 4 | import styled from "styled-components"; 5 | import UnarchiveOutlinedIcon from "@material-ui/icons/UnarchiveOutlined"; 6 | 7 | const Button = styled(IconButton)` 8 | padding: 9px; 9 | `; 10 | 11 | interface Props extends ButtonProps { 12 | iconType: "archive" | "unarchive"; 13 | } 14 | 15 | const ArchivedStateButton: React.FC = (props) => { 16 | return ( 17 | 24 | ); 25 | }; 26 | 27 | export default ArchivedStateButton; 28 | -------------------------------------------------------------------------------- /src/components/atoms/CloseButton/CloseButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import CloseButton from "./CloseButton"; 3 | 4 | export default { 5 | title: "atoms/CloseButton", 6 | component: CloseButton, 7 | } as Meta; 8 | 9 | const Template: Story = () => ; 10 | 11 | export const index = Template; 12 | -------------------------------------------------------------------------------- /src/components/atoms/CloseButton/CloseButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import CloseIcon from "@material-ui/icons/Close"; 3 | import styled from "styled-components"; 4 | import { IconButton, IconButtonProps } from "@material-ui/core"; 5 | 6 | const Button = styled(IconButton)` 7 | padding: 9px; 8 | `; 9 | 10 | const CloseButton: React.FC = (props) => { 11 | return ( 12 | 15 | ); 16 | }; 17 | 18 | export default CloseButton; 19 | -------------------------------------------------------------------------------- /src/components/atoms/HamburgerButton/HamburgerButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import HamburgerButton from "./HamburgerButton"; 3 | 4 | export default { 5 | title: "atoms/HamburgerButton", 6 | component: HamburgerButton, 7 | } as Meta; 8 | 9 | const Template: Story = () => ; 10 | 11 | export const index = Template; 12 | -------------------------------------------------------------------------------- /src/components/atoms/HamburgerButton/HamburgerButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IconButton, IconButtonProps } from "@material-ui/core"; 3 | import MenuIcon from "@material-ui/icons/Menu"; 4 | 5 | const HamburgerButton: React.FC = (props) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default HamburgerButton; 14 | -------------------------------------------------------------------------------- /src/components/atoms/SiteLogo/SiteLogo.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import SiteLogo from "./SiteLogo"; 3 | 4 | export default { 5 | title: "atoms/SiteLogo", 6 | component: SiteLogo, 7 | } as Meta; 8 | 9 | const Template: Story = () => ; 10 | 11 | export const index = Template; 12 | -------------------------------------------------------------------------------- /src/components/atoms/SiteLogo/SiteLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Link as MuiLink } from "@material-ui/core"; 3 | import Link from "next/link"; 4 | 5 | interface Props { 6 | title: string; 7 | } 8 | 9 | const SiteLogo: React.FC = ({ title }) => { 10 | return ( 11 | 12 | 13 | {title} 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default SiteLogo; 20 | -------------------------------------------------------------------------------- /src/components/atoms/StatusIndicator/StatusIndicator.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import StatusIndicator from "./StatusIndicator"; 3 | 4 | export default { 5 | title: "atoms/StatusIndicator", 6 | component: StatusIndicator, 7 | argTypes: { 8 | status: { 9 | control: { 10 | type: "boolean", 11 | }, 12 | }, 13 | disabled: { 14 | control: { 15 | type: "boolean", 16 | }, 17 | }, 18 | }, 19 | } as Meta; 20 | 21 | const Template: Story = (args) => ( 22 | 23 | ); 24 | export const Active = Template.bind({}); 25 | Active.args = { 26 | status: false, 27 | disabled: false, 28 | }; 29 | export const Done = Template.bind({}); 30 | Done.args = { 31 | status: true, 32 | disabled: false, 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/atoms/StatusIndicator/StatusIndicator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Checkbox, IconButton } from "@material-ui/core"; 3 | import styled from "styled-components"; 4 | 5 | interface Props { 6 | status: boolean; 7 | disabled: boolean; 8 | } 9 | 10 | const Button = styled(IconButton)` 11 | padding: 0; 12 | margin-left: 9px; 13 | `; 14 | 15 | const StatusIndicator: React.FC = (props) => { 16 | return ( 17 | 24 | ); 25 | }; 26 | 27 | export default StatusIndicator; 28 | -------------------------------------------------------------------------------- /src/components/atoms/TextInput/TextInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import TextInput from "./TextInput"; 3 | 4 | export default { 5 | title: "atoms/TextInput", 6 | component: TextInput, 7 | } as Meta; 8 | 9 | const Template: Story = () => ; 10 | 11 | export const index = Template; 12 | -------------------------------------------------------------------------------- /src/components/atoms/TextInput/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styled from "styled-components"; 3 | import { TextField, TextFieldProps } from "@material-ui/core"; 4 | 5 | const Field = styled(TextField)` 6 | margin-left: 0; 7 | `; 8 | 9 | const TextInput: React.FC = (props) => { 10 | return ; 11 | }; 12 | 13 | export default TextInput; 14 | -------------------------------------------------------------------------------- /src/components/atoms/ToggleInput/ToggleInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import ToggleInput from "./ToggleInput"; 3 | 4 | export default { 5 | title: "atoms/ToggleInput", 6 | component: ToggleInput, 7 | } as Meta; 8 | 9 | const Template: Story = () => ( 10 | { 12 | return 0; 13 | }} 14 | title="test" 15 | /> 16 | ); 17 | 18 | export const index = Template; 19 | -------------------------------------------------------------------------------- /src/components/atoms/ToggleInput/ToggleInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box } from "@material-ui/core"; 3 | import { FormEvent, KeyboardEvent, useRef, useState } from "react"; 4 | import TextInput from "../TextInput/TextInput"; 5 | 6 | interface Props { 7 | title: string; 8 | onEnter: (title: string) => void; 9 | } 10 | 11 | const ToggleInput: React.FC = (props) => { 12 | const [isUpdateMode, setUpdateMode] = useState(false); 13 | const input = useRef(null); 14 | 15 | const enter = () => { 16 | if (!input || !input.current || input.current.value === props.title) { 17 | return; 18 | } 19 | props.onEnter( 20 | input && input.current && input.current.value ? input.current.value : "" 21 | ); 22 | }; 23 | 24 | const onKeyDown = (e: KeyboardEvent) => { 25 | if (e.key === "Escape") { 26 | if (!input || !input.current) { 27 | return; 28 | } 29 | input.current.value = props.title; 30 | setUpdateMode(false); 31 | } 32 | }; 33 | 34 | const onSubmit = (e: FormEvent) => { 35 | e.preventDefault(); 36 | enter(); 37 | setUpdateMode(false); 38 | }; 39 | 40 | const onBlur = (e: any) => { 41 | enter(); 42 | setUpdateMode(false); 43 | }; 44 | 45 | const changeToUpdateMode = (e: any) => { 46 | setUpdateMode(true); 47 | // e.preventDefault(); 48 | setTimeout(() => { 49 | if (!input || !input.current) { 50 | return; 51 | } 52 | input.current.focus(); 53 | }, 1); 54 | }; 55 | 56 | const titleDisplay = {props.title}; 57 | const titleInput = ( 58 | 59 |
60 | 67 | 68 |
69 | ); 70 | 71 | return <>{isUpdateMode ? titleInput : titleDisplay}; 72 | }; 73 | 74 | export default ToggleInput; 75 | -------------------------------------------------------------------------------- /src/components/molecules/DrawerLinkList/DrawerLinkList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Box, 4 | List, 5 | ListItem, 6 | ListItemIcon, 7 | ListItemText, 8 | } from "@material-ui/core"; 9 | import CheckBoxOutlinedIcon from "@material-ui/icons/CheckBoxOutlined"; 10 | import styled from "styled-components"; 11 | import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; 12 | import Link from "next/link"; 13 | import { useRouter } from "next/router"; 14 | 15 | const StyledIcon = styled(ListItemIcon)` 16 | &.MuiListItemIcon-root { 17 | min-width: 0; 18 | } 19 | `; 20 | 21 | const StyledText = styled(ListItemText)` 22 | margin: 0 15px; 23 | `; 24 | 25 | interface Props { 26 | shortMode?: boolean; 27 | onClickLink?: () => void; 28 | } 29 | 30 | const DrawerLinkList: React.FC = (props) => { 31 | const router = useRouter() || { pathname: "" }; 32 | const short: boolean = 33 | props.shortMode !== undefined ? props.shortMode : false; 34 | 35 | const onClickLink = () => { 36 | if (props.onClickLink !== undefined) { 37 | props.onClickLink(); 38 | } 39 | }; 40 | 41 | return ( 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | export default DrawerLinkList; 72 | -------------------------------------------------------------------------------- /src/components/molecules/Navbar/Navbar.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import Navbar from "./Navbar"; 3 | 4 | export default { 5 | title: "molecules/Navbar", 6 | component: Navbar, 7 | argTypes: { 8 | title: { 9 | defaultValue: "Test Logo", 10 | control: { 11 | type: "text", 12 | }, 13 | }, 14 | onToggleDrawer: { 15 | action: "clicked", 16 | }, 17 | }, 18 | } as Meta; 19 | 20 | const Template: Story = ({ title, onToggleDrawer }) => ( 21 | 22 | ); 23 | 24 | export const index = Template; 25 | -------------------------------------------------------------------------------- /src/components/molecules/Navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { AppBar, Box, Toolbar } from "@material-ui/core"; 3 | import SiteLogo from "../../atoms/SiteLogo/SiteLogo"; 4 | import HamburgerButton from "../../atoms/HamburgerButton/HamburgerButton"; 5 | 6 | interface Props { 7 | title: string; 8 | onToggleDrawer: () => void; 9 | } 10 | 11 | const Navbar: React.FC = (props) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default Navbar; 25 | -------------------------------------------------------------------------------- /src/components/molecules/TaskListItem/TaskListItem.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import TaskListItem from "./TaskListItem"; 3 | 4 | export default { 5 | title: "molecules/TaskListItem", 6 | component: TaskListItem, 7 | argTypes: { 8 | title: { 9 | defaultValue: "this is task", 10 | control: { 11 | type: "text", 12 | }, 13 | }, 14 | completed: { 15 | control: { 16 | type: "boolean", 17 | }, 18 | }, 19 | disabled: { 20 | control: { 21 | type: "boolean", 22 | }, 23 | }, 24 | buttonType: { 25 | control: { 26 | type: "select", 27 | labels: { 28 | archive: "archive", 29 | unarchive: "unarchive", 30 | }, 31 | }, 32 | }, 33 | }, 34 | } as Meta; 35 | 36 | const Template: Story = (args) => ( 37 | 43 | ); 44 | 45 | export const Active = Template.bind({}); 46 | Active.args = { 47 | title: "Active Task", 48 | status: false, 49 | }; 50 | 51 | export const Done = Template.bind({}); 52 | Done.args = { 53 | title: "Done Task", 54 | status: true, 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/molecules/TaskListItem/TaskListItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, FormControlLabel, ListItem } from "@material-ui/core"; 3 | import styled from "styled-components"; 4 | import StatusIndicator from "../../atoms/StatusIndicator/StatusIndicator"; 5 | import ArchivedStateButton from "../../atoms/ArchivedStateButton/ArchivedStateButton"; 6 | import ToggleInput from "../../atoms/ToggleInput/ToggleInput"; 7 | 8 | const Flex = styled(Box)` 9 | display: flex; 10 | align-items: center; 11 | flex-direction: row; 12 | `; 13 | 14 | const Label = styled(FormControlLabel)` 15 | width: 100%; 16 | span.MuiTypography-root { 17 | width: 100%; 18 | } 19 | `; 20 | 21 | const Item: any = styled(ListItem)` 22 | padding-left: 0; 23 | padding-right: 0; 24 | `; 25 | 26 | interface Props { 27 | title: string; 28 | completed: boolean; 29 | disabled: boolean; 30 | buttonType: "archive" | "unarchive"; 31 | onClickStatus?: () => void; 32 | onClickButton?: () => void; 33 | onEnterTitle?: (title: string) => void; 34 | } 35 | 36 | const TaskListItem: React.FC = (props) => { 37 | return ( 38 | 39 | 40 | 66 | 67 | ); 68 | }; 69 | 70 | export default TaskListItem; 71 | -------------------------------------------------------------------------------- /src/components/organisms/ArchivedTaskList/ArchivedTaskList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { ViewObject } from "../../../states/tasks/selectors"; 4 | import TaskListItem from "../../molecules/TaskListItem/TaskListItem"; 5 | import operations from "../../../states/tasks/operations"; 6 | 7 | interface Props { 8 | tasks: ViewObject[]; 9 | } 10 | 11 | const ArchivedTaskList: React.FC = (props) => { 12 | const dispatch = useDispatch(); 13 | 14 | const findTask = (id: string) => 15 | props.tasks.find((task) => task.props.id === id); 16 | 17 | const onClickUnarchiveButton = (id: string) => { 18 | const task = findTask(id); 19 | if (task) { 20 | dispatch(operations.unarchive(task.entity)); 21 | } 22 | }; 23 | 24 | const taskList = props.tasks.map((task) => ( 25 | onClickUnarchiveButton(task.props.id)} 32 | /> 33 | )); 34 | 35 | return
{taskList}
; 36 | }; 37 | 38 | export default ArchivedTaskList; 39 | -------------------------------------------------------------------------------- /src/components/organisms/MiniVariantDrawer/MiniVariantDrawer.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import * as React from "react"; 3 | import MiniVariantDrawer from "./MiniVariantDrawer"; 4 | import DrawerLinkList from "../../molecules/DrawerLinkList/DrawerLinkList"; 5 | 6 | export default { 7 | title: "molecules/MiniVariantDrawer", 8 | component: MiniVariantDrawer, 9 | argTypes: { 10 | open: { 11 | control: { 12 | type: "boolean", 13 | }, 14 | }, 15 | }, 16 | } as Meta; 17 | 18 | const Template: Story = (args) => { 19 | return ( 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export const Open = Template.bind({}); 27 | Open.args = { 28 | open: true, 29 | }; 30 | 31 | export const Close = Template.bind({}); 32 | Close.args = { 33 | open: false, 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/organisms/MiniVariantDrawer/MiniVariantDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Drawer, DrawerProps } from "@material-ui/core"; 3 | import styled from "styled-components"; 4 | 5 | const StyledDrawer = styled(Drawer)` 6 | .text { 7 | display: ${(props) => (props.open ? "block" : "none")}; 8 | } 9 | .MuiPaper-root { 10 | z-index: 1; 11 | padding-top: 70px; 12 | } 13 | &, 14 | .MuiDrawer-root, 15 | .MuiPaper-root { 16 | width: ${(props) => (props.open ? "200px" : "58px;")}; 17 | } 18 | `; 19 | 20 | const MiniVariantDrawer: React.FC = (props) => { 21 | return ( 22 | 23 | {props.children} 24 | 25 | ); 26 | }; 27 | export default MiniVariantDrawer; 28 | -------------------------------------------------------------------------------- /src/components/organisms/Navigation/Navigation.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import Navigation from "./Navigation"; 3 | 4 | export default { 5 | title: "organisms/Navigation", 6 | component: Navigation, 7 | } as Meta; 8 | 9 | const Template: Story = () => ; 10 | 11 | export const index = Template; 12 | -------------------------------------------------------------------------------- /src/components/organisms/Navigation/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, Container, useMediaQuery, useTheme } from "@material-ui/core"; 3 | import styled from "styled-components"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import Navbar from "../../molecules/Navbar/Navbar"; 6 | import DrawerLinkList from "../../molecules/DrawerLinkList/DrawerLinkList"; 7 | import MiniVariantDrawer from "../MiniVariantDrawer/MiniVariantDrawer"; 8 | import TemporaryDrawer from "../TemporaryDrawer/TemporaryDrawer"; 9 | import operations from "../../../states/nav/operations"; 10 | import selectors from "../../../states/nav/selectors"; 11 | 12 | const Content = styled.main` 13 | width: 100%; 14 | `; 15 | 16 | interface Props { 17 | title: string; 18 | } 19 | 20 | const Navigation: React.FC = (props) => { 21 | const dispatch = useDispatch(); 22 | const theme = useTheme(); 23 | const matches = useMediaQuery(theme.breakpoints.up("sm")); 24 | const nav = useSelector(selectors.getNav); 25 | 26 | const toggleOpenDrawer = () => { 27 | dispatch(operations.toggleOpenDrawer(nav.isOpenDrawer)); 28 | }; 29 | 30 | const toggleWidthDrawer = () => { 31 | dispatch(operations.toggleWidthDrawer(nav.isExpandDrawer)); 32 | }; 33 | 34 | const closeDrawer = () => { 35 | dispatch(operations.closeDrawer()); 36 | }; 37 | 38 | const miniVariantDrawer = ( 39 | 40 | 41 | 42 | ); 43 | 44 | const temporaryDrawer = ( 45 | 50 | 51 | 52 | ); 53 | 54 | return ( 55 | <> 56 | 60 | 61 | {matches ? miniVariantDrawer : temporaryDrawer} 62 | 63 | 64 | {props.children} 65 | 66 | 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default Navigation; 73 | -------------------------------------------------------------------------------- /src/components/organisms/TaskList/TaskList.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import { useSelector } from "react-redux"; 3 | import TaskList from "./TaskList"; 4 | import selectors from "../../../states/tasks/selectors"; 5 | 6 | export default { 7 | title: "organisms/TaskList", 8 | component: TaskList, 9 | } as Meta; 10 | 11 | const Template: Story = () => { 12 | const tasks = useSelector(selectors.getInboxTasks) || []; 13 | return ; 14 | }; 15 | 16 | export const index = Template; 17 | -------------------------------------------------------------------------------- /src/components/organisms/TaskList/TaskList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import TaskListItem from "../../molecules/TaskListItem/TaskListItem"; 4 | import { ViewObject } from "../../../states/tasks/selectors"; 5 | import operations from "../../../states/tasks/operations"; 6 | 7 | interface Props { 8 | tasks: ViewObject[]; 9 | } 10 | 11 | const TaskList: React.FC = (props) => { 12 | const dispatch = useDispatch(); 13 | 14 | const findTask = (id: string) => 15 | props.tasks.find((task) => task.props.id === id); 16 | 17 | const onClickStatusIndicator = (id: string) => { 18 | const task = findTask(id); 19 | if (task) { 20 | dispatch(operations.toggleStatus(task.entity)); 21 | } 22 | }; 23 | 24 | const onClickArchiveButton = (id: string) => { 25 | const task = findTask(id); 26 | if (task) { 27 | dispatch(operations.archive(task.entity)); 28 | } 29 | }; 30 | 31 | const onEnterTitle = (id: string, title: string) => { 32 | const task = findTask(id); 33 | if (task) { 34 | dispatch(operations.updateTitle(task.entity, title)); 35 | } 36 | }; 37 | 38 | const taskList = props.tasks.map((task) => ( 39 | onClickStatusIndicator(task.props.id)} 46 | onClickButton={() => onClickArchiveButton(task.props.id)} 47 | onEnterTitle={(title: string) => onEnterTitle(task.props.id, title)} 48 | /> 49 | )); 50 | 51 | return
{taskList}
; 52 | }; 53 | 54 | export default TaskList; 55 | -------------------------------------------------------------------------------- /src/components/organisms/TaskRegisterForm/TaskRegisterForm.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import TaskRegisterForm from "./TaskRegisterForm"; 3 | 4 | export default { 5 | title: "organisms/TaskRegisterForm", 6 | component: TaskRegisterForm, 7 | } as Meta; 8 | 9 | const Template: Story = () => { 10 | return ; 11 | }; 12 | 13 | export const index = Template; 14 | -------------------------------------------------------------------------------- /src/components/organisms/TaskRegisterForm/TaskRegisterForm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { FormEvent, useRef, KeyboardEvent } from "react"; 3 | import { useDispatch } from "react-redux"; 4 | import TextInput from "../../atoms/TextInput/TextInput"; 5 | import * as taskOperations from "../../../states/tasks/operations"; 6 | 7 | const TaskRegisterForm: React.FC = (props) => { 8 | const input = useRef(null); 9 | const dispatch = useDispatch(); 10 | 11 | const onKeyDown = (e: KeyboardEvent) => { 12 | if (!input || !input.current) { 13 | return; 14 | } 15 | if (e.key === "Escape") { 16 | input.current.value = ""; 17 | } 18 | }; 19 | 20 | const onSubmit = (e: FormEvent) => { 21 | e.preventDefault(); 22 | if (!input || !input.current) { 23 | return; 24 | } 25 | const val: string = input.current.value; 26 | if (!val) { 27 | return; 28 | } 29 | dispatch(taskOperations.create(val)); 30 | input.current.value = ""; 31 | }; 32 | 33 | return ( 34 |
35 | 42 | 43 | ); 44 | }; 45 | 46 | export default TaskRegisterForm; 47 | -------------------------------------------------------------------------------- /src/components/organisms/TemporaryDrawer/TemporaryDrawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, Drawer, DrawerProps } from "@material-ui/core"; 3 | import styled from "styled-components"; 4 | import CloseButton from "../../atoms/CloseButton/CloseButton"; 5 | 6 | const StyledDrawer = styled(Drawer)` 7 | &, 8 | .MuiDrawer-root, 9 | .MuiPaper-root { 10 | width: 200px; 11 | } 12 | `; 13 | 14 | const DrawerHeader = styled(Box)` 15 | height: 70px; 16 | padding: 9px; 17 | `; 18 | 19 | interface Props extends DrawerProps { 20 | onClickCloseButton: () => void; 21 | } 22 | 23 | const TemporaryDrawer: React.FC = (props) => { 24 | return ( 25 | 26 | 27 | 28 | 29 | {props.children} 30 | 31 | ); 32 | }; 33 | 34 | export default TemporaryDrawer; 35 | -------------------------------------------------------------------------------- /src/components/templates/WithNavigationLayout.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/react"; 2 | import WithNavigationLayout from "./WithNavigationLayout"; 3 | 4 | export default { 5 | title: "templates/WithNavigationLayout", 6 | component: WithNavigationLayout, 7 | } as Meta; 8 | 9 | const Template: Story = () => ; 10 | 11 | export const index = Template; 12 | -------------------------------------------------------------------------------- /src/components/templates/WithNavigationLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import Navigation from "../organisms/Navigation/Navigation"; 4 | 5 | interface Props { 6 | pageTitle: string; 7 | } 8 | 9 | const WithNavigationLayout: React.FC = (props) => { 10 | return ( 11 |
12 | 13 | 14 | 15 | {props.pageTitle} 16 | 17 | {props.children} 18 |
19 | ); 20 | }; 21 | 22 | export default WithNavigationLayout; 23 | -------------------------------------------------------------------------------- /src/configs/theme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme, Theme } from "@material-ui/core/styles"; 2 | 3 | const theme: Theme = createMuiTheme({ 4 | palette: { 5 | primary: { 6 | light: "#484848", 7 | main: "#212121", 8 | dark: "#000000", 9 | contrastText: "#ffffff", 10 | }, 11 | secondary: { 12 | light: "#99d066", 13 | main: "#689f38", 14 | dark: "#387002", 15 | contrastText: "#000000", 16 | }, 17 | }, 18 | }); 19 | 20 | export default theme; 21 | -------------------------------------------------------------------------------- /src/models/Task/Factory.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from "nanoid"; 2 | import Task from "./Task"; 3 | import { Id } from "./Id"; 4 | import { Status } from "./Status"; 5 | import { Title } from "./Title"; 6 | 7 | export interface Input { 8 | id?: string; 9 | title: string; 10 | status?: number; 11 | archived?: boolean; 12 | createdAt?: number; 13 | } 14 | 15 | export class Factory { 16 | public static create(props: Input): Task { 17 | return new Task({ 18 | id: new Id(props.id ? props.id : nanoid()), 19 | title: new Title(props.title), 20 | status: new Status( 21 | props.status !== undefined ? props.status : Status.READY 22 | ), 23 | archived: props.archived !== undefined ? props.archived : false, 24 | createdAt: props.createdAt ? new Date(props.createdAt) : new Date(), 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/models/Task/Id.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from "nanoid"; 2 | 3 | export class Id { 4 | readonly val: string; 5 | 6 | constructor(id?: string) { 7 | this.val = id || nanoid(); 8 | } 9 | 10 | public getValue(): string { 11 | return this.val; 12 | } 13 | } 14 | 15 | export default { 16 | Id, 17 | }; 18 | -------------------------------------------------------------------------------- /src/models/Task/Mock.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from "./Factory"; 2 | 3 | const getTaskList = () => { 4 | return [ 5 | Factory.create({ 6 | id: "abcdefghijkLMNOPQRST1", 7 | title: "task 1", 8 | status: 0, 9 | }), 10 | Factory.create({ 11 | id: "abcdefghijkLMNOPQRST2", 12 | title: "task 2", 13 | status: 1, 14 | }), 15 | Factory.create({ 16 | id: "abcdefghijkLMNOPQRST3", 17 | title: "task 3", 18 | status: 0, 19 | }), 20 | Factory.create({ 21 | id: "abcdefghijkLMNOPQRST4", 22 | title: "task 4", 23 | status: 0, 24 | archived: true, 25 | }), 26 | Factory.create({ 27 | id: "abcdefghijkLMNOPQRST5", 28 | title: "task 5", 29 | status: 1, 30 | archived: true, 31 | }), 32 | ]; 33 | }; 34 | 35 | const getSerializedTaskList = () => { 36 | const tasks = getTaskList(); 37 | return tasks.map((task) => task.serialize()); 38 | }; 39 | 40 | export default { 41 | getTaskList, 42 | getSerializedTaskList, 43 | }; 44 | -------------------------------------------------------------------------------- /src/models/Task/Status.ts: -------------------------------------------------------------------------------- 1 | export class Status { 2 | readonly val: number; 3 | 4 | static READY = 0; 5 | 6 | static COMPLETED = 1; 7 | 8 | constructor(status?: number) { 9 | this.val = status || Status.READY; 10 | } 11 | 12 | public isCompleted(): boolean { 13 | return this.val === Status.COMPLETED; 14 | } 15 | 16 | public getValue(): number { 17 | return this.val; 18 | } 19 | } 20 | 21 | export default { 22 | Status, 23 | }; 24 | -------------------------------------------------------------------------------- /src/models/Task/Task.ts: -------------------------------------------------------------------------------- 1 | import { Id } from "./Id"; 2 | import { Status } from "./Status"; 3 | import { Title } from "./Title"; 4 | 5 | export class Task { 6 | private id: Id; 7 | 8 | private title: Title; 9 | 10 | private status: Status; 11 | 12 | private archived: boolean; 13 | 14 | private createdAt: Date; 15 | 16 | constructor(props: { 17 | id: Id; 18 | title: Title; 19 | status: Status; 20 | archived: boolean; 21 | createdAt: Date; 22 | }) { 23 | this.id = props.id; 24 | this.title = props.title; 25 | this.status = props.status; 26 | this.archived = props.archived; 27 | this.createdAt = props.createdAt; 28 | } 29 | 30 | public getId(): string { 31 | return this.id.getValue(); 32 | } 33 | 34 | public getTitle(): string { 35 | return this.title.getValue(); 36 | } 37 | 38 | public isCompleted(): boolean { 39 | return this.status.isCompleted(); 40 | } 41 | 42 | public isArchived(): boolean { 43 | return this.archived; 44 | } 45 | 46 | public complete(): void { 47 | this.status = new Status(Status.COMPLETED); 48 | } 49 | 50 | public incomplete(): void { 51 | this.status = new Status(Status.READY); 52 | } 53 | 54 | public archive(): void { 55 | this.archived = true; 56 | } 57 | 58 | public unarchive(): void { 59 | this.archived = false; 60 | } 61 | 62 | public changeTitle(title: string): void { 63 | this.title = new Title(title); 64 | } 65 | 66 | public getCreatedTimestamp(): number { 67 | return this.createdAt.getTime(); 68 | } 69 | 70 | public serialize() { 71 | return { 72 | id: this.getId(), 73 | title: this.getTitle(), 74 | status: this.status.getValue(), 75 | archived: this.archived, 76 | createdAt: this.createdAt.getTime(), 77 | }; 78 | } 79 | } 80 | 81 | export default Task; 82 | -------------------------------------------------------------------------------- /src/models/Task/Title.ts: -------------------------------------------------------------------------------- 1 | export class Title { 2 | readonly val: string; 3 | 4 | constructor(title: string) { 5 | if (title.length === 0) { 6 | throw new Error("Title is null or empty."); 7 | } 8 | this.val = title; 9 | } 10 | 11 | public getValue(): string { 12 | return this.val; 13 | } 14 | } 15 | 16 | export default { 17 | Title, 18 | }; 19 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { AppProps } from "next/app"; 3 | import { MuiThemeProvider, StylesProvider } from "@material-ui/core"; 4 | import CssBaseline from "@material-ui/core/CssBaseline"; 5 | import { ThemeProvider } from "styled-components"; 6 | import { Provider } from "react-redux"; 7 | import theme from "../configs/theme"; 8 | import store from "../states/store"; 9 | 10 | const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => { 11 | useEffect(() => { 12 | const jssStyles = document.querySelector("#jss-server-side"); 13 | if (jssStyles && jssStyles.parentElement) { 14 | jssStyles.parentElement.removeChild(jssStyles); 15 | } 16 | }, []); 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default MyApp; 33 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | DocumentContext, 3 | Head, 4 | Html, 5 | Main, 6 | NextScript, 7 | } from "next/document"; 8 | import { ServerStyleSheet as StyledComponentSheets } from "styled-components"; 9 | import { ServerStyleSheets as MaterialUiServerStyleSheets } from "@material-ui/core"; 10 | 11 | export default class MyDocument extends Document { 12 | static async getInitialProps(ctx: DocumentContext) { 13 | const styledComponentSheets = new StyledComponentSheets(); 14 | const materialUiServerStyleSheets = new MaterialUiServerStyleSheets(); 15 | const originalRenderPage = ctx.renderPage; 16 | 17 | try { 18 | ctx.renderPage = () => 19 | originalRenderPage({ 20 | enhanceApp: (App) => (props) => 21 | styledComponentSheets.collectStyles( 22 | materialUiServerStyleSheets.collect() 23 | ), 24 | }); 25 | 26 | const initialProps = await Document.getInitialProps(ctx); 27 | return { 28 | ...initialProps, 29 | styles: ( 30 | <> 31 | {initialProps.styles} 32 | {styledComponentSheets.getStyleElement()} 33 | {materialUiServerStyleSheets.getStyleElement()} 34 | 35 | ), 36 | }; 37 | } finally { 38 | styledComponentSheets.seal(); 39 | } 40 | } 41 | 42 | render() { 43 | return ( 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Container } from "@material-ui/core"; 3 | import { useSelector } from "react-redux"; 4 | import WithNavigationLayout from "../components/templates/WithNavigationLayout"; 5 | import TaskRegisterForm from "../components/organisms/TaskRegisterForm/TaskRegisterForm"; 6 | import TaskList from "../components/organisms/TaskList/TaskList"; 7 | import selectors from "../states/tasks/selectors"; 8 | 9 | const Index: React.FC = () => { 10 | const tasks = useSelector(selectors.getInboxTasks) || []; 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default Index; 25 | -------------------------------------------------------------------------------- /src/pages/tasks/archived.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Box, Container } from "@material-ui/core"; 3 | import { useSelector } from "react-redux"; 4 | import WithNavigationLayout from "../../components/templates/WithNavigationLayout"; 5 | import selectors from "../../states/tasks/selectors"; 6 | import ArchivedTaskList from "../../components/organisms/ArchivedTaskList/ArchivedTaskList"; 7 | 8 | const Archived: React.FC = () => { 9 | const tasks = useSelector(selectors.getArchivedTasks) || []; 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Archived; 23 | -------------------------------------------------------------------------------- /src/states/nav/actions.ts: -------------------------------------------------------------------------------- 1 | import types from "./types"; 2 | 3 | export const openDrawer = () => ({ 4 | type: types.OPEN_DRAWER, 5 | }); 6 | 7 | export const closeDrawer = () => ({ 8 | type: types.CLOSE_DRAWER, 9 | }); 10 | 11 | export const shrinkDrawer = () => ({ 12 | type: types.SHRINK_DRAWER, 13 | }); 14 | 15 | export const expandDrawer = () => ({ 16 | type: types.EXPAND_DRAWER, 17 | }); 18 | 19 | export default { 20 | openDrawer, 21 | closeDrawer, 22 | shrinkDrawer, 23 | expandDrawer, 24 | }; 25 | -------------------------------------------------------------------------------- /src/states/nav/operations.ts: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | 3 | export const toggleWidthDrawer = (isNowExpand: boolean) => { 4 | return isNowExpand ? actions.shrinkDrawer() : actions.expandDrawer(); 5 | }; 6 | 7 | export const toggleOpenDrawer = (isNowOpen: boolean) => { 8 | return isNowOpen ? actions.closeDrawer() : actions.openDrawer(); 9 | }; 10 | 11 | export const closeDrawer = () => { 12 | return actions.closeDrawer(); 13 | }; 14 | 15 | export default { 16 | toggleWidthDrawer, 17 | toggleOpenDrawer, 18 | closeDrawer, 19 | }; 20 | -------------------------------------------------------------------------------- /src/states/nav/reducers.ts: -------------------------------------------------------------------------------- 1 | import types from "./types"; 2 | 3 | const nav = (state: any, action: any) => { 4 | switch (action.type) { 5 | case types.OPEN_DRAWER: 6 | return { ...state, isOpenDrawer: true }; 7 | case types.CLOSE_DRAWER: 8 | return { ...state, isOpenDrawer: false }; 9 | case types.EXPAND_DRAWER: 10 | return { ...state, isExpandDrawer: true }; 11 | case types.SHRINK_DRAWER: 12 | return { ...state, isExpandDrawer: false }; 13 | default: 14 | return { ...state }; 15 | } 16 | }; 17 | 18 | export default nav; 19 | -------------------------------------------------------------------------------- /src/states/nav/selectors.ts: -------------------------------------------------------------------------------- 1 | export interface ViewObject { 2 | isOpenDrawer: boolean; 3 | isExpandDrawer: boolean; 4 | } 5 | 6 | const getNav = (state: any): ViewObject => { 7 | return { 8 | isOpenDrawer: state.nav.isOpenDrawer, 9 | isExpandDrawer: state.nav.isExpandDrawer, 10 | }; 11 | }; 12 | 13 | export default { 14 | getNav, 15 | }; 16 | -------------------------------------------------------------------------------- /src/states/nav/types.ts: -------------------------------------------------------------------------------- 1 | const OPEN_DRAWER = "OPEN_DRAWER"; 2 | const CLOSE_DRAWER = "CLOSE_DRAWER"; 3 | const SHRINK_DRAWER = "SHRINK_DRAWER"; 4 | const EXPAND_DRAWER = "EXPAND_DRAWER"; 5 | 6 | export default { 7 | OPEN_DRAWER, 8 | CLOSE_DRAWER, 9 | SHRINK_DRAWER, 10 | EXPAND_DRAWER, 11 | }; 12 | -------------------------------------------------------------------------------- /src/states/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import tasks from "./tasks/reducers"; 3 | import nav from "./nav/reducers"; 4 | import mock from "../models/Task/Mock"; 5 | 6 | const preloadedState = { 7 | tasks: mock.getSerializedTaskList(), 8 | nav: { 9 | isOpenDrawer: false, 10 | isExpandDrawer: true, 11 | }, 12 | }; 13 | 14 | export default configureStore({ 15 | reducer: { 16 | tasks, 17 | nav, 18 | }, 19 | preloadedState, 20 | devTools: process.env.NODE_ENV !== "production", 21 | }); 22 | -------------------------------------------------------------------------------- /src/states/tasks/actions.ts: -------------------------------------------------------------------------------- 1 | import types from "./types"; 2 | 3 | export const createTask = (task: any) => ({ 4 | type: types.CREATE_TASK, 5 | task, 6 | }); 7 | 8 | export const updateTaskTitle = (id: string, task: any) => ({ 9 | type: types.UPDATE_TASK_TITLE, 10 | id, 11 | task, 12 | }); 13 | 14 | export const errorUpdateTaskTitle = (id: string, message: string) => ({ 15 | type: types.ERROR_UPDATE_TASK_TITLE, 16 | id, 17 | message, 18 | }); 19 | 20 | export const completeTask = (id: string, task: any) => ({ 21 | type: types.COMPLETE_TASK, 22 | id, 23 | task, 24 | }); 25 | 26 | export const incompleteTask = (id: string, task: any) => ({ 27 | type: types.INCOMPLETE_TASK, 28 | id, 29 | task, 30 | }); 31 | 32 | export const archiveTask = (id: string, task: any) => ({ 33 | type: types.ARCHIVE_TASK, 34 | id, 35 | task, 36 | }); 37 | 38 | export const unarchiveTask = (id: string, task: any) => ({ 39 | type: types.UNARCHIVE_TASK, 40 | id, 41 | task, 42 | }); 43 | 44 | export default { 45 | createTask, 46 | updateTaskTitle, 47 | completeTask, 48 | incompleteTask, 49 | archiveTask, 50 | unarchiveTask, 51 | errorUpdateTaskTitle, 52 | }; 53 | -------------------------------------------------------------------------------- /src/states/tasks/operations.ts: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import { Factory } from "../../models/Task/Factory"; 3 | import Task from "../../models/Task/Task"; 4 | 5 | export const create = (title: string) => { 6 | const task = Factory.create({ title }); 7 | return actions.createTask(task.serialize()); 8 | }; 9 | 10 | export const updateTitle = (task: Task, title: string) => { 11 | try { 12 | task.changeTitle(title); 13 | } catch (e) { 14 | return actions.errorUpdateTaskTitle(task.getId(), e.toString()); 15 | } 16 | return actions.updateTaskTitle(task.getId(), task.serialize()); 17 | }; 18 | 19 | export const toggleStatus = (task: Task) => { 20 | if (task.isCompleted()) { 21 | task.incomplete(); 22 | return actions.incompleteTask(task.getId(), task.serialize()); 23 | } 24 | task.complete(); 25 | return actions.completeTask(task.getId(), task.serialize()); 26 | }; 27 | 28 | export const archive = (task: Task) => { 29 | task.archive(); 30 | return actions.archiveTask(task.getId(), task.serialize()); 31 | }; 32 | 33 | export const unarchive = (task: Task) => { 34 | task.unarchive(); 35 | return actions.unarchiveTask(task.getId(), task.serialize()); 36 | }; 37 | 38 | export default { 39 | create, 40 | updateTitle, 41 | toggleStatus, 42 | archive, 43 | unarchive, 44 | }; 45 | -------------------------------------------------------------------------------- /src/states/tasks/reducers.ts: -------------------------------------------------------------------------------- 1 | import types from "./types"; 2 | 3 | const update = (state: any, id: string, data: any) => { 4 | const index = state.findIndex((row: any) => row.id === id); 5 | if (index < 0) { 6 | return state.slice(); 7 | } 8 | const clone = state.slice(); 9 | clone.splice(index, 1, data); 10 | return clone; 11 | }; 12 | 13 | const create = (state: any, data: any) => { 14 | return [...state, data]; 15 | }; 16 | 17 | const commandTypes = { 18 | create: [types.CREATE_TASK], 19 | update: [ 20 | types.UPDATE_TASK_TITLE, 21 | types.UPDATE_TASK_TITLE, 22 | types.COMPLETE_TASK, 23 | types.INCOMPLETE_TASK, 24 | types.ARCHIVE_TASK, 25 | types.UNARCHIVE_TASK, 26 | ], 27 | }; 28 | 29 | const getCommandTypes = (type: string) => { 30 | if (commandTypes.create.includes(type)) { 31 | return "create"; 32 | } 33 | if (commandTypes.update.includes(type)) { 34 | return "update"; 35 | } 36 | return "other"; 37 | }; 38 | 39 | const tasks = (state: any, action: any) => { 40 | switch (getCommandTypes(action.type)) { 41 | case "create": 42 | return create(state, action.task); 43 | case "update": 44 | return update(state, action.id, action.task); 45 | default: 46 | return state ? state.slice() : []; 47 | } 48 | }; 49 | 50 | export default tasks; 51 | -------------------------------------------------------------------------------- /src/states/tasks/selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from "reselect"; 2 | import { Factory } from "../../models/Task/Factory"; 3 | import Task from "../../models/Task/Task"; 4 | 5 | export interface ViewObject { 6 | entity: Task; 7 | props: { 8 | id: string; 9 | title: string; 10 | isCompleted: boolean; 11 | }; 12 | } 13 | 14 | const createViewObject = (task: Task) => ({ 15 | entity: task, 16 | props: { 17 | id: task.getId(), 18 | title: task.getTitle(), 19 | isCompleted: task.isCompleted(), 20 | }, 21 | }); 22 | 23 | const tasksSelector = (state: any) => { 24 | const tasks = state.tasks.map((task: any) => 25 | createViewObject(Factory.create(task)) 26 | ); 27 | tasks.sort((a: ViewObject, b: ViewObject) => 28 | a.entity.getCreatedTimestamp() < b.entity.getCreatedTimestamp() ? 1 : -1 29 | ); 30 | return tasks; 31 | }; 32 | 33 | const getInboxTasks = createSelector([tasksSelector], (vos): [ViewObject] => 34 | vos.filter((vo: ViewObject) => !vo.entity.isArchived()) 35 | ); 36 | 37 | const getArchivedTasks = createSelector([tasksSelector], (vos): [ViewObject] => 38 | vos.filter((vo: ViewObject) => vo.entity.isArchived()) 39 | ); 40 | 41 | export default { 42 | getInboxTasks, 43 | getArchivedTasks, 44 | }; 45 | -------------------------------------------------------------------------------- /src/states/tasks/types.ts: -------------------------------------------------------------------------------- 1 | const CREATE_TASK = "CREATE_TASK"; 2 | const UPDATE_TASK_TITLE = "UPDATE_TASK_TITLE"; 3 | const ARCHIVE_TASK = "ARCHIVE_TASK"; 4 | const UNARCHIVE_TASK = "UNARCHIVE_TASK"; 5 | const COMPLETE_TASK = "COMPLETE_TASK"; 6 | const INCOMPLETE_TASK = "INCOMPLETE_TASK"; 7 | const ERROR_UPDATE_TASK_TITLE = "ERROR_UPDATE_TASK_TITLE"; 8 | 9 | export default { 10 | CREATE_TASK, 11 | UPDATE_TASK_TITLE, 12 | COMPLETE_TASK, 13 | INCOMPLETE_TASK, 14 | ARCHIVE_TASK, 15 | UNARCHIVE_TASK, 16 | ERROR_UPDATE_TASK_TITLE, 17 | }; 18 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 3 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "sourceMap": true, 5 | "target": "es6", 6 | "jsx": "preserve", 7 | "lib": [ 8 | "dom", 9 | "esnext" 10 | ], 11 | "noEmit": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noImplicitAny": true, 16 | "moduleResolution": "node", 17 | "allowJs": true, 18 | "module": "esnext", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true 21 | }, 22 | "include": [ 23 | "src/pages/**/*", 24 | "next-env.d.ts" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } --------------------------------------------------------------------------------