├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ ├── dependabot-auto-merge.yml │ ├── e2e.yml │ ├── lint.yml │ ├── test.yml │ └── typecheck.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .swcrc ├── LICENSE ├── README.md ├── cypress.config.ts ├── cypress ├── e2e │ ├── add_todo.cy.js │ ├── check_todo.cy.js │ ├── delete_todo.cy.js │ ├── edit_todo.cy.js │ ├── filter.cy.js │ ├── mobile_realworld_usecase.cy.js │ ├── not_found.cy.js │ ├── realworld_usecase.cy.js │ └── toggle_all_button.cy.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── e2e.js ├── images ├── cypress_open.gif └── todolist.gif ├── index.html ├── jest.config.js ├── jest └── fileTransformer.js ├── package.json ├── pnpm-lock.yaml ├── public ├── apple-touch-icon.png ├── favicon.ico ├── logo192.png ├── logo512.png └── manifest.json ├── src ├── App.test.js ├── App │ ├── Copyright.tsx │ ├── NewTodoInput │ │ ├── index.test.tsx │ │ ├── index.tsx │ │ └── style.ts │ ├── TodoList │ │ ├── Item │ │ │ ├── index.test.tsx │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── index.test.tsx │ │ ├── index.tsx │ │ └── style.ts │ ├── TodoMVC.tsx │ ├── UnderBar │ │ ├── FilterLink │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── style.ts │ ├── index.tsx │ └── style.ts ├── ErrorBoundary.test.js ├── ErrorBoundary.tsx ├── NotFound.test.tsx ├── NotFound.tsx ├── dataStructure.ts ├── functions.test.ts ├── functions.ts ├── index.css ├── main.tsx ├── react-app-env.d.ts ├── setupTests.ts └── testUtil.tsx ├── tsconfig.json └── vite.config.ts /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-typescript-todomvc-2022", 3 | "projectOwner": "laststance", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "ryota-murakami", 15 | "name": "ryota-murakami", 16 | "avatar_url": "https://avatars1.githubusercontent.com/u/5501268?s=400&u=7bf6b1580b95930980af2588ef0057f3e9ec1ff8&v=4", 17 | "profile": "http://ryota-murakami.github.io/", 18 | "contributions": [ 19 | "code", 20 | "doc", 21 | "test" 22 | ] 23 | }, 24 | { 25 | "login": "wroscoe", 26 | "name": "Will Roscoe", 27 | "avatar_url": "https://avatars2.githubusercontent.com/u/147582?v=4", 28 | "profile": "http://donkeycar.com", 29 | "contributions": [ 30 | "code" 31 | ] 32 | }, 33 | { 34 | "login": "JunQu", 35 | "name": "Peng Fei", 36 | "avatar_url": "https://avatars2.githubusercontent.com/u/39846309?v=4", 37 | "profile": "https://github.com/JunQu", 38 | "contributions": [ 39 | "bug" 40 | ] 41 | }, 42 | { 43 | "login": "alexpanchuk", 44 | "name": "Alex Panchuk", 45 | "avatar_url": "https://avatars3.githubusercontent.com/u/26270612?v=4", 46 | "profile": "https://github.com/alexpanchuk", 47 | "contributions": [ 48 | "doc" 49 | ] 50 | }, 51 | { 52 | "login": "BurhanMullamitha", 53 | "name": "Burhan Mullamitha", 54 | "avatar_url": "https://avatars1.githubusercontent.com/u/42492054?v=4", 55 | "profile": "https://github.com/BurhanMullamitha", 56 | "contributions": [ 57 | "doc" 58 | ] 59 | }, 60 | { 61 | "login": "hefengxian", 62 | "name": "hefengxian", 63 | "avatar_url": "https://avatars.githubusercontent.com/u/4338497?v=4", 64 | "profile": "https://github.com/hefengxian", 65 | "contributions": [ 66 | "code", 67 | "test" 68 | ] 69 | }, 70 | { 71 | "login": "esetnik", 72 | "name": "Ethan Setnik", 73 | "avatar_url": "https://avatars.githubusercontent.com/u/664434?v=4", 74 | "profile": "http://ethansetnik.com", 75 | "contributions": [ 76 | "doc" 77 | ] 78 | }, 79 | { 80 | "login": "PaoloJN", 81 | "name": "Paolo Nessim", 82 | "avatar_url": "https://avatars.githubusercontent.com/u/87121008?v=4", 83 | "profile": "https://github.com/PaoloJN", 84 | "contributions": [ 85 | "code" 86 | ] 87 | }, 88 | { 89 | "login": "likui628", 90 | "name": "Li Kui", 91 | "avatar_url": "https://avatars.githubusercontent.com/u/90845831?v=4", 92 | "profile": "https://github.com/likui628", 93 | "contributions": [ 94 | "code" 95 | ] 96 | }, 97 | { 98 | "login": "adarsh-gupta101", 99 | "name": "Adarsh Gupta", 100 | "avatar_url": "https://avatars.githubusercontent.com/u/73733229?v=4", 101 | "profile": "https://adarshgupta.live/", 102 | "contributions": [ 103 | "doc" 104 | ] 105 | } 106 | ], 107 | "contributorsPerLine": 7, 108 | "skipCi": true, 109 | "commitType": "docs" 110 | } 111 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | images/ 4 | instrumented/ 5 | .github/ 6 | .nyc_output/ 7 | coverage/ 8 | cypress 9 | cypress.config.ts -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:jsx-a11y/recommended", "ts-prefixer"], 3 | "plugins": ["cypress", "jsx-a11y", "react-hooks"], 4 | "env": { 5 | "cypress/globals": true 6 | }, 7 | "rules": { 8 | "react-hooks/rules-of-hooks": "error" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | images/* linguist-vendored 2 | .nyc_output/* linguist-vendored 3 | coverage/* linguist-vendored 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Install Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | 21 | - uses: pnpm/action-setup@v2 22 | name: Install pnpm 23 | with: 24 | version: 8 25 | run_install: false 26 | 27 | - name: Get pnpm store directory 28 | shell: bash 29 | run: | 30 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 31 | 32 | - uses: actions/cache@v3 33 | name: Setup pnpm cache 34 | with: 35 | path: ${{ env.STORE_PATH }} 36 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 37 | restore-keys: | 38 | ${{ runner.os }}-pnpm-store- 39 | 40 | - name: Install dependencies 41 | run: pnpm install 42 | - run: pnpm build 43 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --merge "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: Cypress E2E 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | cypress-E2E: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 # If you're using actions/checkout@v3 you must set persist-credentials to false in most cases for the deployment to work correctly. 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 18 17 | - uses: pnpm/action-setup@v2 18 | name: Install pnpm 19 | with: 20 | version: 8 21 | run_install: true 22 | - uses: cypress-io/github-action@v5 23 | with: 24 | start: pnpm run start 25 | wait-on: 'http://localhost:3000' 26 | # the entire command will automatically be prefixed with "npm" 27 | # and we need the second "npm" to execute "cypress run ..." command line 28 | command-prefix: 'percy exec -- npx' 29 | env: 30 | PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Install Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | 21 | - uses: pnpm/action-setup@v2 22 | name: Install pnpm 23 | with: 24 | version: 8 25 | run_install: false 26 | 27 | - name: Get pnpm store directory 28 | shell: bash 29 | run: | 30 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 31 | 32 | - uses: actions/cache@v3 33 | name: Setup pnpm cache 34 | with: 35 | path: ${{ env.STORE_PATH }} 36 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 37 | restore-keys: | 38 | ${{ runner.os }}-pnpm-store- 39 | 40 | - name: Install dependencies 41 | run: pnpm install 42 | - run: pnpm lint 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Install Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | 21 | - uses: pnpm/action-setup@v2 22 | name: Install pnpm 23 | with: 24 | version: 8 25 | run_install: false 26 | 27 | - name: Get pnpm store directory 28 | shell: bash 29 | run: | 30 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 31 | 32 | - uses: actions/cache@v3 33 | name: Setup pnpm cache 34 | with: 35 | path: ${{ env.STORE_PATH }} 36 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 37 | restore-keys: | 38 | ${{ runner.os }}-pnpm-store- 39 | 40 | - name: Install dependencies 41 | run: pnpm install 42 | - run: pnpm test 43 | -------------------------------------------------------------------------------- /.github/workflows/typecheck.yml: -------------------------------------------------------------------------------- 1 | name: Typecheck 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | typecheck: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Install Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | 21 | - uses: pnpm/action-setup@v2 22 | name: Install pnpm 23 | with: 24 | version: 8 25 | run_install: false 26 | 27 | - name: Get pnpm store directory 28 | shell: bash 29 | run: | 30 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 31 | 32 | - uses: actions/cache@v3 33 | name: Setup pnpm cache 34 | with: 35 | path: ${{ env.STORE_PATH }} 36 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 37 | restore-keys: | 38 | ${{ runner.os }}-pnpm-store- 39 | 40 | - name: Install dependencies 41 | run: pnpm install 42 | - run: pnpm typecheck 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | cypress/videos/ 3 | cypress/fixtures/ 4 | cypress/screenshots/ 5 | node_modules/ 6 | instrumented/ 7 | .eslintcache 8 | coverage/ 9 | .nyc_output/ 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | images 4 | .eslintignore 5 | .gitignore 6 | LICENSE 7 | .emv 8 | .nyc_output 9 | build 10 | instrumented 11 | node_modules 12 | .gitattributes 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "target": "es2020", 4 | "parser": { 5 | "syntax": "typescript", 6 | "tsx": true, 7 | "decorators": false, 8 | "dynamicImport": false 9 | }, 10 | "transform": { 11 | "react": { 12 | "pragma": "React.createElement", 13 | "pragmaFrag": "React.Fragment", 14 | "throwIfNamespace": true, 15 | "development": false, 16 | "useBuiltins": false, 17 | "runtime": "automatic" 18 | }, 19 | "hidden": { 20 | "jest": true 21 | } 22 | } 23 | }, 24 | "module": { 25 | "type": "commonjs", 26 | "strict": false, 27 | "strictMode": true, 28 | "lazy": false, 29 | "noInterop": false 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ryota Murakami 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 | ## Project Status from the Author (@ryota-murakami, Aug 16 2023): 2 | 3 | I'm considering continue update as a Client Side SPA or Rewrite this app with Server Components. 4 | But Server Components doen't meant obsolete Client Side SPA, both archtecture is great option depends on application type, 5 | dev team type, each dev's skillset type. 6 | 7 | Might be keep update both archtecture TODO is ideal vision for me unless there are planty rest time. 8 | Anyway, I thnik was [TODO MVC](https://todomvc.com/) outdated for current JS frameworks. 9 | I want to renew this project about this winter. 10 | 11 | --- 12 | 13 | # React TypeScript TodoMVC 2022 14 | 15 | [![Netlify Status](https://api.netlify.com/api/v1/badges/877a9a48-c7e1-498c-b56b-81fa8f4d4d8a/deploy-status)](https://app.netlify.com/sites/react-typescript-todomvc/deploys) 16 | [![Build](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/build.yml/badge.svg)](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/build.yml) 17 | [![Cypress E2E](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/e2e.yml/badge.svg)](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/e2e.yml) 18 | [![Lint](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/lint.yml/badge.svg)](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/lint.yml) 19 | [![Test](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/test.yml/badge.svg)](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/test.yml) 20 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 21 | [![Typecheck](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/typecheck.yml/badge.svg)](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/typecheck.yml) 22 | [![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg?style=flat-square)](#contributors) 23 | [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/laststance/react-typescript-todomvc-2022) 24 | 25 | gif 26 | 27 | ## A Modern Code Style Todo Example 📝 28 | 29 | This project was started with the goal of continue to publish TodoMVC Apps in the latest [React](https://reactjs.org/) writing style. 30 | 31 | When you found [React.js](https://reactjs.org/) on [TodoMVC](https://todomvc.com/) top page, you might seen classic style `React.createClass()` based source at first. 32 | I don't complain about it because the old-style codebase React app works all over the world and helps peopleAlmost cases, there is no value that spending time for rerwite new syntax sugar of huge codebase. 33 | 34 | This project aims to assist new React learners and those who have not written React for a long time by providing a handy resource for learning the latest React.js. 35 | I'm glad to even the repo could be useful for your learning. 🤗 36 | 37 | [![Edit react-typescript-todomvc-2022](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/quizzical-blackwell-bvfc5?fontsize=14&hidenavigation=1&theme=dark) 38 | 39 | --- 40 | 41 | ## Getting Started 42 | 43 | - The app assumes that you have installed `Node.js` newer than [v18.16.1 LTS](https://nodejs.org/en/). 44 | If you don't have it yet, follow the official [Node.js Doc](https://nodejs.org/en/) to install it. 45 | 46 | ```bash 47 | npx degit laststance/react-typescript-todomvc-2022 react-typescript-todomvc-2022 48 | ``` 49 | 50 | ```bash 51 | cd react-typescript-todomvc-2022 52 | ``` 53 | 54 | ```bash 55 | npm -g pnpm 56 | ``` 57 | 58 | ```bash 59 | pnpm i 60 | ``` 61 | 62 | ```bash 63 | pnpm start 64 | ``` 65 | 66 | after that auto launch todo app on your default browser and code edit ready. 67 | 68 | --- 69 | 70 | ## Stack 71 | 72 | - [TODO-CSS-Template](https://github.com/Klerith/TODO-CSS-Template) (Borrowing HTML & CSS Thanks! 👍 ) 73 | - [Vite](https://vitejs.dev/) 74 | - [TypeScript](https://www.typescriptlang.org/) [v4.2.4](https://github.com/microsoft/TypeScript/releases/tag/v4.2.4) 75 | - [React Router](https://reactrouter.com/) 76 | - [Styled-Components](https://styled-components.com/): CSS-in-JS 77 | - [Recoil](https://recoiljs.org/): A state management library for React 78 | - [Cypress](https://www.cypress.io/): E2E Testing 79 | - [react-testing-library](https://github.com/testing-library/react-testing-library) 80 | - [ESLint](https://eslint.org/) 81 | - [eslint-config-typescript-react-pro 🌈](https://github.com/laststance/eslint-config-typescript-react-pro) 82 | - [Netlify](https://www.netlify.com/): Deploy & Hosting 83 | - [Github Actions](https://github.com/features/actions): Automation run tests, lint, typecheck, build 84 | - [Depfu](https://depfu.com/github/ryota-murakami/react-typescript-todomvc-2022?project_id=9618): Keep latest npm packages automaticaly 85 | 86 | ## Command 87 | 88 | You can do exact same command with npm, or [install yarn](https://classic.yarnpkg.com/en/docs/install#mac-stable) easily if you have interest. 89 | 90 | ### `yarn` or `yarn install` 91 | 92 | Install all Node Package Modules that depending this project. 93 | 94 | ### `yarn start` 95 | 96 | After that you'll seen the console which are server processes messages. 97 | Let's follow the message and put in URL `http://localhost:3000/` your browsers adressbar, 98 | and then you'll got todo app as same as Demo. let's modify under the `src/` code feel free!! 99 | 100 | ### `yarn build` 101 | 102 | Production build that bundled optimization stuff in `build` directory. 103 | 104 | ### `yarn serve` 105 | 106 | Run production build that generated by `yarn build`. 107 | 108 | ### `yarn lint` 109 | 110 | [ESLint](https://eslint.org/) is at the top. 111 | And setup [TypeScript ESLint](https://github.com/typescript-eslint/typescript-eslint), integrating [Prettier](https://prettier.io/) as a [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier). 112 | Here is [final config list](https://github.com/laststance/eslint-config-typescript-react-pro#explicit-all-rule-set-). 113 | 114 | ### `yarn lint:fix` 115 | 116 | Run wtih eslint --fix option. 117 | Actually frequently use for perform [Prettier](https://prettier.io/) formatting. 118 | 119 | ### `yarn typecheck` 120 | 121 | Check TypeScript error whole porject. 122 | 123 | ### `yarn test` 124 | 125 | Run [Jest](https://jestjs.io/). 126 | Using [react-testing-library](https://github.com/testing-library/react-testing-library) for component integration testing. 127 | 128 | ### `yarn clean` 129 | 130 | Delete `node_modules/*`, `yarn.lock`, `build/*` once. 131 | 132 | ### `yarn prettier` 133 | 134 | Run prettier formatting holeproject without all JS/TS files. 135 | 136 | ### `yarn cypress:open` 137 | 138 | [Cypress](https://www.cypress.io/) is all-in-one E2E Testing tool which can deal testing on real browser. 139 | This command using [Electron](https://www.electronjs.org/) by Cypress default. 140 | 141 | `yarn cypress:open` require `yarn start` before. 142 | 143 | ```bash 144 | yarn start # Launch DevServer 145 | yarn cypress:open 146 | ``` 147 | 148 | ![cypress_open](images/cypress_open.gif) 149 | 150 | ### `yarn cypress:run` 151 | 152 | Run Cypress with [Electron](https://www.electronjs.org/). 153 | That's same as run all test on cypress GUI after run `yarn cypress:open`. 154 | 155 | ```bash 156 | yarn start # Launch DevServer 157 | yarn cypress:run 158 | ``` 159 | 160 | ### `yarn cypress:run:headless` 161 | 162 | Run Cypress with headless [Electron](https://www.electronjs.org/). 163 | That mean this command complete all on a terminal without GUI. 164 | 165 | ```bash 166 | yarn start # Launch DevServer 167 | yarn cypress:run:headless 168 | ``` 169 | 170 | ## 🗒 Note 171 | 172 | **This is not a Best Practice introduction. 173 | There are tons of effective way to create solid software in JavaScript World, you have a lot of other option based on your preference for approaching where, The Repo is just a style of my favorite.** 174 | 175 | "_How to combining TypeScript with massive Babel or JavaScript tools ecosystem?_" 176 | 177 | **I hope this helps you know like that from what I've Published!** 178 | 179 | ## Issues 180 | 181 | Please feel free to post [New Issue](https://github.com/laststance/react-typescript-todomvc-2022/issues/new) or Pull Request 🤗 182 | 183 | ## Questions 184 | 185 | Please feel free to post [New Issue](https://github.com/laststance/react-typescript-todomvc-2022/issues/new) or reply on [Twitter](https://twitter.com/malloc007) 🐦 186 | 187 | If you want to get more generally answers, these community are might be helpful 🍻 188 | 189 | - [Reactiflux on Discord](https://www.reactiflux.com/) 190 | - [Stack Overflow](https://stackoverflow.com/questions/tagged/reactjs) 191 | 192 | ## LICENSE 193 | 194 | MIT 195 | 196 | ## Contributors 197 | 198 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 |
ryota-murakami
ryota-murakami

💻 📖 ⚠️
Will Roscoe
Will Roscoe

💻
Peng Fei
Peng Fei

🐛
Alex Panchuk
Alex Panchuk

📖
Burhan Mullamitha
Burhan Mullamitha

📖
hefengxian
hefengxian

💻 ⚠️
Ethan Setnik
Ethan Setnik

📖
Paolo Nessim
Paolo Nessim

💻
Li Kui
Li Kui

💻
Adarsh Gupta
Adarsh Gupta

📖
221 | 222 | 223 | 224 | 225 | 226 | 227 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 228 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | // We've imported your old cypress plugins here. 6 | // You may want to clean this up later by importing these. 7 | setupNodeEvents(on, config) { 8 | return require('./cypress/plugins/index.js')(on, config) 9 | }, 10 | }, 11 | video: false, 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/add_todo.cy.js: -------------------------------------------------------------------------------- 1 | context('Add Todo', () => { 2 | it('can add todo', () => { 3 | cy.visit('http://localhost:3000/') 4 | cy.percySnapshot('Initial Top Page') 5 | 6 | // can type text and submit 7 | cy.get('[data-cy=new-todo-input-text]') 8 | .type('can be typing') 9 | .should('have.value', 'can be typing') 10 | .type('{enter}') 11 | .should('not.have.value') 12 | 13 | // it is added a submited todo 14 | cy.get('[data-cy=todo-item]') 15 | .should('exist') 16 | .should('contain', 'can be typing') 17 | 18 | // can add 2 more todos 19 | cy.get('[data-cy=new-todo-input-text]') 20 | .type('two') 21 | .should('have.value', 'two') 22 | .type('{enter}') 23 | .should('not.have.value') 24 | cy.get('[data-cy=todo-item]').should('exist').should('contain', 'two') 25 | 26 | cy.get('[data-cy=new-todo-input-text]') 27 | .type('three') 28 | .should('have.value', 'three') 29 | .type('{enter}') 30 | .should('not.have.value') 31 | cy.get('[data-cy=todo-item]').should('exist').should('contain', 'three') 32 | }) 33 | it('can not add space char only text to todo', () => { 34 | cy.visit('http://localhost:3000/') 35 | 36 | // submit space only input that submit should be disallowed 37 | cy.get('[data-cy=new-todo-input-text]').type(' ').type('{enter}') 38 | 39 | // there is no added todo item 40 | cy.get('[data-cy=todo-item]').should('not.exist') 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /cypress/e2e/check_todo.cy.js: -------------------------------------------------------------------------------- 1 | context('Check Todo', () => { 2 | beforeEach(() => { 3 | cy.submitTripleTodos() 4 | }) 5 | 6 | context('No Exist Checked', () => { 7 | it('remain task counter can display correct number', () => { 8 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should( 9 | 'contain', 10 | '3' 11 | ) 12 | }) 13 | it('does not show "Clear cmpleted" button on footer', () => { 14 | cy.get('[data-cy=clear-completed-button]').should('not.exist') 15 | }) 16 | }) 17 | 18 | it('working check toggle each todo', () => { 19 | // can check todo 'three' as comoleted 20 | cy.get('[data-cy=todo-item]:first-of-type') 21 | .should('have.text', 'three') 22 | .find('[data-cy=todo-item-complete-check]') 23 | .check() 24 | .should('have.checked') 25 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '2') 26 | // it should show "Clear cmpleted" button on footer 27 | cy.get('[data-cy=clear-completed-button]').should('be.visible') 28 | cy.percySnapshot('Checked Completed Checkbox') 29 | 30 | // can check todo 'two' as comoleted 31 | cy.get('[data-cy=todo-item]:nth-of-type(2)') 32 | .should('have.text', 'two') 33 | .find('[data-cy=todo-item-complete-check]') 34 | .check() 35 | .should('have.checked') 36 | // it should show "Clear cmpleted" button on footer 37 | cy.get('[data-cy=clear-completed-button]').should('be.visible') 38 | 39 | // can un-check todo 'three' as un-completed 40 | cy.get('[data-cy=todo-item]:first-of-type') 41 | .should('have.text', 'three') 42 | .find('[data-cy=todo-item-complete-check]') 43 | .click() 44 | .should('not.have.checked') 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /cypress/e2e/delete_todo.cy.js: -------------------------------------------------------------------------------- 1 | context('Delete Todo', () => { 2 | beforeEach(() => { 3 | cy.submitTripleTodos() 4 | }) 5 | 6 | it('can delete mouse-hovering todo by push [x] button', () => { 7 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 8 | .find('[data-cy=delete-todo-btn]') 9 | .click({ force: true }) // Actually this bth is hidden until invole :hover selector. but sypress struggling at the point 10 | // @see https://docs.cypress.io/api/commands/hover.html#Workarounds 11 | 12 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '2') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /cypress/e2e/edit_todo.cy.js: -------------------------------------------------------------------------------- 1 | context('Edit Todo', () => { 2 | beforeEach(() => { 3 | cy.submitTripleTodos() 4 | }) 5 | 6 | it('can edit todo text', () => { 7 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 8 | .find('[data-cy=todo-body-text]') 9 | .should('contain', 'one') 10 | .click() 11 | cy.percySnapshot('Edit Mode') 12 | // should focus actual input element when click todo text label 13 | .focused() 14 | .should('have.value', 'one') 15 | .should('have.attr', 'data-cy', 'todo-edit-input') 16 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 17 | .find('[data-cy=todo-edit-input]') 18 | .type(' of kind') 19 | .type('{enter}') 20 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 21 | .find('[data-cy=todo-body-text]') 22 | .should('contain', 'one of kind') 23 | }) 24 | 25 | it('can edit completed todo', () => { 26 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 27 | .find('[data-cy=todo-item-complete-check]') 28 | .check() 29 | .should('have.checked') 30 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 31 | .find('[data-cy=todo-body-text]') 32 | .should('contain', 'one') 33 | .click() 34 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 35 | .find('[data-cy=todo-edit-input]') 36 | .type(' more') 37 | .type('{enter}') 38 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 39 | .find('[data-cy=todo-body-text]') 40 | .should('contain', 'one more') 41 | }) 42 | 43 | it('can not enter blank input', () => { 44 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 45 | .find('[data-cy=todo-body-text]') 46 | .should('contain', 'one') 47 | .click() 48 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 49 | .find('[data-cy=todo-edit-input]') 50 | .type('{leftarrow}{leftarrow}{leftarrow}{del}{del}{del}') 51 | .type('{enter}') 52 | 53 | // press enter key when input tag is blank, don't finish edit mode and still typing todo text 54 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 55 | .find('[data-cy=todo-edit-input]') 56 | .should('to.be.focused') 57 | 58 | // don't accept only space charcter input. should behave as same as above blank case.(don't finish edit mode and still typing todo text) 59 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 60 | .find('[data-cy=todo-edit-input]') 61 | .type(' ') 62 | .type('{enter}') 63 | 64 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 65 | .find('[data-cy=todo-edit-input]') 66 | .should('to.be.focused') 67 | 68 | // should remove item when doing blur action with blank value 69 | cy.get('body').click(100, 100) 70 | cy.get('[data-cy=todo-item]:nth-of-type(3)').should('not.exist') 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /cypress/e2e/filter.cy.js: -------------------------------------------------------------------------------- 1 | context('Filter', () => { 2 | beforeEach(() => { 3 | cy.submitTripleTodos() 4 | }) 5 | 6 | it('should "All" filter show all todos', () => { 7 | cy.get('[data-cy=todo-item]').should('have.length', 3) 8 | // done the todo 9 | cy.get('[data-cy=todo-item]:first-of-type') 10 | .find('[data-cy=todo-item-complete-check]') 11 | .check() 12 | cy.get('[data-cy=todo-item]').should('have.length', 3) 13 | 14 | // check footer link behavior 15 | cy.location().should((loc) => expect(loc.pathname).to.eq('/')) 16 | cy.get('[data-cy=active-filter]').click() 17 | cy.percySnapshot('Active Filter') 18 | cy.location().should((loc) => expect(loc.pathname).to.eq('/active')) 19 | cy.get('[data-cy=all-filter]').click() 20 | cy.location().should((loc) => expect(loc.pathname).to.eq('/')) 21 | cy.get('[data-cy=todo-item]').should('have.length', 3) 22 | }) 23 | 24 | it('should "Active" filter show un-completed todos and effect to url pathname', () => { 25 | cy.get('[data-cy=active-filter]').click() 26 | cy.location().should((loc) => expect(loc.pathname).to.eq('/active')) 27 | cy.get('[data-cy=todo-item]').should('have.length', 3) 28 | // done the todo 29 | cy.get('[data-cy=todo-item]:first-of-type') 30 | .find('[data-cy=todo-item-complete-check]') 31 | .check() 32 | cy.get('[data-cy=todo-item]').should('have.length', 2) 33 | }) 34 | 35 | it('should "Completed" filter show completed todos and effect to url pathname', () => { 36 | cy.get('[data-cy=completed-filter]').click() 37 | cy.percySnapshot('Completed Filter') 38 | cy.location().should((loc) => expect(loc.pathname).to.eq('/completed')) 39 | cy.get('[data-cy=todo-item]').should('not.exist') 40 | // done the todo 41 | cy.get('[data-cy=all-filter]').click() 42 | cy.get('[data-cy=todo-item]:first-of-type') 43 | .find('[data-cy=todo-item-complete-check]') 44 | .check() 45 | cy.get('[data-cy=completed-filter]').click() 46 | cy.get('[data-cy=todo-item]').should('have.length', 1) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /cypress/e2e/mobile_realworld_usecase.cy.js: -------------------------------------------------------------------------------- 1 | context('Mobile Real World Usecase', () => { 2 | beforeEach(() => { 3 | cy.submitTripleTodos() 4 | cy.viewport('iphone-6') 5 | }) 6 | it('will try all function in one session', () => { 7 | cy.percySnapshot('Moble 3 Todo Items') 8 | // remain task counter can display correct number 9 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '3') 10 | // [state: 3 todo, 0 completed] it doesn't show "Clear cmpleted" button on footer 11 | cy.get('[data-cy=clear-completed-button]').should('not.exist') 12 | 13 | // can check todo 'three' as comoleted 14 | cy.get('[data-cy=todo-item]:first-of-type') 15 | .should('have.text', 'three') 16 | .find('[data-cy=todo-item-complete-check]') 17 | .check() 18 | .should('have.checked') 19 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '2') 20 | // [state: 3 todo, 1 completed] it should show "Clear cmpleted" button on footer 21 | cy.get('[data-cy=clear-completed-button]').should('be.visible') 22 | 23 | // can check todo 'two' as comoleted 24 | cy.get('[data-cy=todo-item]:nth-of-type(2)') 25 | .should('have.text', 'two') 26 | .find('[data-cy=todo-item-complete-check]') 27 | .check() 28 | .should('have.checked') 29 | // [state: 3 todo, 2 completed] it should show "Clear cmpleted" button on footer 30 | cy.get('[data-cy=clear-completed-button]').should('be.visible') 31 | 32 | // can un-check todo 'three' as un-completed 33 | cy.get('[data-cy=todo-item]:first-of-type') 34 | .should('have.text', 'three') 35 | .find('[data-cy=todo-item-complete-check]') 36 | .click() 37 | .should('not.have.checked') 38 | 39 | // can all task checked as completed by click toggle all button 40 | cy.get('[data-cy=toggle-all-btn]').click({ force: true }) // { force: true } reason @see https://github.com/laststance/react-app-typescript-todo-example-2021/issues/288 41 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '0') 42 | // there is no side-effect to todo items value by toggle all button 43 | cy.get('[data-cy=todo-item]:first-of-type').should('have.text', 'three') 44 | cy.get('[data-cy=todo-item]:nth-of-type(2)').should('have.text', 'two') 45 | // can reverse todo state to un-completed after click toggle all button again 46 | cy.get('[data-cy=toggle-all-btn]').click({ force: true }) // { force: true } reason @see https://github.com/laststance/create-react-app-typescript-todo-example-2021/issues/288 47 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '3') 48 | // [state: 3 todo, 0 completed] it doesn't "Clear cmpleted" button on footer 49 | cy.get('[data-cy=clear-completed-button]').should('not.exist') 50 | 51 | // can edit todo text 52 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 53 | .find('[data-cy=todo-body-text]') 54 | .should('contain', 'one') 55 | .click() 56 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 57 | .find('[data-cy=todo-edit-input]') 58 | .type(' of kind') 59 | .type('{enter}') 60 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 61 | .find('[data-cy=todo-body-text]') 62 | .should('contain', 'one of kind') 63 | 64 | // can edit completed todo 65 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 66 | .find('[data-cy=todo-item-complete-check]') 67 | .check() 68 | .should('have.checked') 69 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 70 | .find('[data-cy=todo-body-text]') 71 | .should('contain', 'one of kind') 72 | .dblclick() 73 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 74 | .find('[data-cy=todo-edit-input]') 75 | .type(' more') 76 | .type('{enter}') 77 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 78 | .find('[data-cy=todo-body-text]') 79 | .should('contain', 'one of kind more') 80 | 81 | // can delete mouse-hovering todo by push [x] button 82 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 83 | .find('[data-cy=delete-todo-btn]') 84 | .click({ force: true }) // Actually this bth is hidden until invole :hover selector. but sypress struggling at the point 85 | // @see https://docs.cypress.io/api/commands/hover.html#Workarounds 86 | 87 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '2') 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /cypress/e2e/not_found.cy.js: -------------------------------------------------------------------------------- 1 | context('Routing', () => { 2 | it('should show not found page', () => { 3 | // no exist uri 4 | cy.visit('http://localhost:3000/jiawojefpwielj0rijfpopo') 5 | 6 | cy.percySnapshot('Not Found') 7 | cy.get('[data-cy=not-found-page]') 8 | .should('exist') 9 | .should('contain', 'Page Not Found') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/e2e/realworld_usecase.cy.js: -------------------------------------------------------------------------------- 1 | context('Real World Usecase', () => { 2 | beforeEach(() => { 3 | cy.submitTripleTodos() 4 | }) 5 | it('will try all function in one session', () => { 6 | cy.percySnapshot('3 Todo Items') 7 | // remain task counter can display correct number 8 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '3') 9 | // [state: 3 todo, 0 completed] it doesn't show "Clear cmpleted" button on footer 10 | cy.get('[data-cy=clear-completed-button]').should('not.exist') 11 | 12 | // can check todo 'three' as comoleted 13 | cy.get('[data-cy=todo-item]:first-of-type') 14 | .should('have.text', 'three') 15 | .find('[data-cy=todo-item-complete-check]') 16 | .check() 17 | .should('have.checked') 18 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '2') 19 | // [state: 3 todo, 1 completed] it should show "Clear cmpleted" button on footer 20 | cy.get('[data-cy=clear-completed-button]').should('be.visible') 21 | 22 | // can check todo 'two' as comoleted 23 | cy.get('[data-cy=todo-item]:nth-of-type(2)') 24 | .should('have.text', 'two') 25 | .find('[data-cy=todo-item-complete-check]') 26 | .check() 27 | .should('have.checked') 28 | // [state: 3 todo, 2 completed] it should show "Clear cmpleted" button on footer 29 | cy.get('[data-cy=clear-completed-button]').should('be.visible') 30 | 31 | // can un-check todo 'three' as un-completed 32 | cy.get('[data-cy=todo-item]:first-of-type') 33 | .should('have.text', 'three') 34 | .find('[data-cy=todo-item-complete-check]') 35 | .click() 36 | .should('not.have.checked') 37 | 38 | // can all task checked as completed by click toggle all button 39 | cy.get('[data-cy=toggle-all-btn]').click() 40 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '0') 41 | // there is no side-effect to todo items value by toggle all button 42 | cy.get('[data-cy=todo-item]:first-of-type').should('have.text', 'three') 43 | cy.get('[data-cy=todo-item]:nth-of-type(2)').should('have.text', 'two') 44 | // can reverse todo state to un-completed after click toggle all button again 45 | cy.get('[data-cy=toggle-all-btn]').click() 46 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '3') 47 | // [state: 3 todo, 0 completed] it doesn't "Clear cmpleted" button on footer 48 | cy.get('[data-cy=clear-completed-button]').should('not.exist') 49 | 50 | // can edit todo text 51 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 52 | .find('[data-cy=todo-body-text]') 53 | .should('contain', 'one') 54 | .click() 55 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 56 | .find('[data-cy=todo-edit-input]') 57 | .type(' of kind') 58 | .type('{enter}') 59 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 60 | .find('[data-cy=todo-body-text]') 61 | .should('contain', 'one of kind') 62 | 63 | // can edit completed todo 64 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 65 | .find('[data-cy=todo-item-complete-check]') 66 | .check() 67 | .should('have.checked') 68 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 69 | .find('[data-cy=todo-body-text]') 70 | .should('contain', 'one of kind') 71 | .click() 72 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 73 | .find('[data-cy=todo-edit-input]') 74 | .type(' more') 75 | .type('{enter}') 76 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 77 | .find('[data-cy=todo-body-text]') 78 | .should('contain', 'one of kind more') 79 | 80 | // can delete mouse-hovering todo by push [x] button 81 | cy.get('[data-cy=todo-item]:nth-of-type(3)') 82 | .find('[data-cy=delete-todo-btn]') 83 | .click({ force: true }) // Actually this bth is hidden until invole :hover selector. but sypress struggling at the point 84 | // @see https://docs.cypress.io/api/commands/hover.html#Workarounds 85 | 86 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '2') 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /cypress/e2e/toggle_all_button.cy.js: -------------------------------------------------------------------------------- 1 | context('Toggle All Button', () => { 2 | beforeEach(() => { 3 | cy.submitTripleTodos() 4 | }) 5 | it('can check all task', () => { 6 | cy.get('[data-cy=toggle-all-btn]').click() 7 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '0') 8 | 9 | // there is no side-effect to todo items value by toggle all button 10 | cy.get('[data-cy=todo-item]:first-of-type').should('have.text', 'three') 11 | cy.get('[data-cy=todo-item]:nth-of-type(2)').should('have.text', 'two') 12 | 13 | // can reverse todo state to un-completed after click toggle all button again 14 | cy.get('[data-cy=toggle-all-btn]').click() 15 | cy.get('[data-cy=remaining-uncompleted-todo-count]').should('contain', '3') 16 | // [state: 3 todo, 0 completed] it doesn't "Clear cmpleted" button on footer 17 | cy.get('[data-cy=clear-completed-button]').should('not.exist') 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // IMPORTANT to return the config object 16 | // with the any changed environment variables 17 | return config 18 | } 19 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | // for adding cy.percySnapshot() command. https://docs.percy.io/docs/cypress 27 | import '@percy/cypress' 28 | 29 | // const COMMAND_DELAY = 0 30 | // 31 | // for (const command of [ 32 | // 'visit', 33 | // 'click', 34 | // 'trigger', 35 | // 'type', 36 | // 'clear', 37 | // 'reload', 38 | // 'contains', 39 | // ]) { 40 | // Cypress.Commands.overwrite(command, async (originalFn, ...args) => { 41 | // const origVal = originalFn(...args) 42 | // 43 | // return new Promise((resolve) => { 44 | // setTimeout(() => { 45 | // resolve(origVal) 46 | // }, COMMAND_DELAY) 47 | // }) 48 | // }) 49 | // } 50 | 51 | Cypress.Commands.add('submitTripleTodos', () => { 52 | cy.visit('http://localhost:3000/') 53 | cy.get('[data-cy=new-todo-input-text]') 54 | .type('one') 55 | .type('{enter}') 56 | .type('two') 57 | .type('{enter}') 58 | .type('three') 59 | .type('{enter}') 60 | }) 61 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | import '@percy/cypress' 19 | 20 | // Alternatively you can use CommonJS syntax: 21 | // require('./commands') 22 | -------------------------------------------------------------------------------- /images/cypress_open.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laststance/react-typescript-todomvc-2022/0ae28d70caa34a8ee704b221e09282484e7e307f/images/cypress_open.gif -------------------------------------------------------------------------------- /images/todolist.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laststance/react-typescript-todomvc-2022/0ae28d70caa34a8ee704b221e09282484e7e307f/images/todolist.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React TypeScript TodoMVC 2022 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | moduleNameMapper: { 3 | '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy', 4 | '^react-native$': 'react-native-web', 5 | }, 6 | setupFilesAfterEnv: ['/src/setupTests.ts'], 7 | testEnvironment: 'jsdom', 8 | testMatch: [ 9 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', 10 | '/src/**/*.{spec,test}.{js,jsx,ts,tsx}', 11 | ], 12 | transform: { 13 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$|^.+\\.css$': 14 | '/jest/fileTransformer.js', 15 | '^.+\\.(ts|js|tsx|jsx)$': '@swc/jest', 16 | }, 17 | transformIgnorePatterns: [ 18 | '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$', 19 | '^.+\\.module\\.(css|sass|scss)$', 20 | ], 21 | watchPlugins: [ 22 | 'jest-watch-typeahead/filename', 23 | 'jest-watch-typeahead/testname', 24 | ], 25 | } 26 | 27 | module.exports = config 28 | -------------------------------------------------------------------------------- /jest/fileTransformer.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | process(_sourceText, sourcePath) { 5 | return { 6 | code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`, 7 | } 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-typescript-todomvc-2022", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "author": "Ryota Murakami (https://ryota-murakami.github.io/)", 6 | "browserslist": { 7 | "production": [ 8 | ">0.2%", 9 | "not dead", 10 | "not op_mini all" 11 | ], 12 | "development": [ 13 | "last 1 chrome version", 14 | "last 1 firefox version", 15 | "last 1 safari version" 16 | ] 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/laststance/react-typescript-todomvc-2022" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/laststance/react-typescript-todomvc-2022/issues" 24 | }, 25 | "homepage": "https://github.com/laststance/react-typescript-todomvc-2022", 26 | "dependencies": { 27 | "react": "^18.2.0", 28 | "react-dom": "^18.2.0", 29 | "react-router-dom": "^6.18.0", 30 | "recoil": "^0.7.7", 31 | "styled-components": "^6.1.1", 32 | "stylis": "^4.3.0" 33 | }, 34 | "devDependencies": { 35 | "@percy/cli": "^1.27.4", 36 | "@percy/cypress": "^3.1.2", 37 | "@swc/core": "^1.3.96", 38 | "@swc/jest": "^0.2.29", 39 | "@testing-library/cypress": "^10.0.1", 40 | "@testing-library/jest-dom": "^6.1.4", 41 | "@testing-library/react": "^14.1.0", 42 | "@testing-library/user-event": "^14.5.1", 43 | "@types/jest": "^29.5.8", 44 | "@types/node": "^20.9.0", 45 | "@types/react": "^18.2.37", 46 | "@types/react-dom": "^18.2.15", 47 | "@typescript-eslint/eslint-plugin": "^6.10.0", 48 | "@typescript-eslint/parser": "^6.10.0", 49 | "@vitejs/plugin-react": "^4.1.1", 50 | "cypress": "^13.5.0", 51 | "eslint": "^8.53.0", 52 | "eslint-config-prettier": "^9.0.0", 53 | "eslint-config-ts-prefixer": "^1.12.5", 54 | "eslint-import-resolver-typescript": "^3.6.1", 55 | "eslint-plugin-cypress": "^2.15.1", 56 | "eslint-plugin-import": "^2.29.0", 57 | "eslint-plugin-jsx-a11y": "^6.8.0", 58 | "eslint-plugin-prettier": "^5.0.1", 59 | "eslint-plugin-react-hooks": "^4.6.0", 60 | "eslint-plugin-sort-keys-custom-order": "^1.0.5", 61 | "jest": "^29.7.0", 62 | "jest-environment-jsdom": "^29.7.0", 63 | "jest-watch-typeahead": "^2.2.2", 64 | "prettier": "^3.0.3", 65 | "rimraf": "^5.0.5", 66 | "serve": "^14.2.0", 67 | "typescript": "^5.2.2", 68 | "vite": "^4.5.10" 69 | }, 70 | "scripts": { 71 | "start": "vite", 72 | "build": "vite build", 73 | "serve": "build", 74 | "test": "jest", 75 | "lint": "eslint . --ext .ts,.tsx,.js,.jsx", 76 | "lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix", 77 | "prettier": "prettier --write \"**/*.+(json|yml|css|md|mdx)\"", 78 | "typecheck": "tsc --noEmit", 79 | "clean": "rimraf yarn.lock package-lock.json node_modules build", 80 | "cypress:run": "cypress run", 81 | "cypress:headed": "cypress run --headed", 82 | "cypress:run:chrome": "cypress run --browser chrome", 83 | "cypress:run:chrome:headless": "cypress run --browser chrome --headless", 84 | "cypress:open": "cypress open", 85 | "cypress:percy": "percy exec -- cypress run" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laststance/react-typescript-todomvc-2022/0ae28d70caa34a8ee704b221e09282484e7e307f/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laststance/react-typescript-todomvc-2022/0ae28d70caa34a8ee704b221e09282484e7e307f/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laststance/react-typescript-todomvc-2022/0ae28d70caa34a8ee704b221e09282484e7e307f/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laststance/react-typescript-todomvc-2022/0ae28d70caa34a8ee704b221e09282484e7e307f/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Todo", 3 | "name": "React TypeScript TodoMVC 2022", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "type": "image/x-icon", 8 | "sizes": "64x64" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client' 2 | import { RecoilRoot } from 'recoil' 3 | 4 | import App from './App' 5 | import { recoilState } from './dataStructure' 6 | 7 | it('renders without crashing', () => { 8 | const todo = { 9 | todoList: [ 10 | { 11 | id: 'TsHx9eEN5Y4A', 12 | bodyText: 'monster', 13 | completed: false, 14 | }, 15 | { 16 | id: 'ba91OwrK0Dt8', 17 | bodyText: 'boss black', 18 | completed: false, 19 | }, 20 | { 21 | id: 'QwejYipEf5nk', 22 | bodyText: 'caffe latte', 23 | completed: false, 24 | }, 25 | ], 26 | } 27 | const div = document.createElement('div') 28 | 29 | const root = ReactDOM.createRoot(div) 30 | root.render( 31 | { 33 | set(recoilState, todo) 34 | }} 35 | > 36 | 37 | , 38 | ) 39 | 40 | root.unmount() 41 | }) 42 | -------------------------------------------------------------------------------- /src/App/Copyright.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | 3 | const Copyright: React.FC = memo( 4 | () => ( 5 | 14 | ), 15 | () => true, 16 | ) 17 | 18 | export default Copyright 19 | -------------------------------------------------------------------------------- /src/App/NewTodoInput/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent } from '@testing-library/react' 2 | import React from 'react' 3 | 4 | import { TestRenderer } from '../../testUtil' 5 | 6 | import NewTodoTextInput from './index' 7 | 8 | test('should be render ', () => { 9 | const screen = TestRenderer() 10 | const input = screen.getByTestId('new-todo-input-text') as HTMLInputElement 11 | 12 | // Header big text 13 | expect(screen.getByText('todos')).toBeInTheDocument() 14 | 15 | // Placeholder 16 | expect( 17 | screen.getByPlaceholderText('What needs to be done?'), 18 | ).toBeInTheDocument() 19 | 20 | // type 'tidying my room' 21 | fireEvent.change(input, { 22 | target: { value: 'tidying my room' }, 23 | }) 24 | 25 | // assert input tag 26 | expect(input.value).toBe('tidying my room') 27 | 28 | // submit 29 | fireEvent.keyPress(input, { 30 | charCode: 13, 31 | code: 13, 32 | key: 'Enter', // I had issue that doesn't trigger keyPress event relevant charCode. https://github.com/testing-library/react-testing-library/issues/269 33 | }) 34 | 35 | // text cleard 36 | expect(input.value).toBe('') 37 | }) 38 | -------------------------------------------------------------------------------- /src/App/NewTodoInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef } from 'react' 2 | import { useRecoilState } from 'recoil' 3 | 4 | import type { AppState, Todo } from '../../dataStructure' 5 | import { recoilState } from '../../dataStructure' 6 | import { UUID } from '../../functions' 7 | 8 | import { Layout } from './style' 9 | 10 | const NewTodoTextInput: React.FC = () => { 11 | const [appState, setAppState] = useRecoilState(recoilState) 12 | const textInput: React.RefObject = 13 | createRef() 14 | 15 | function addTodo(e: React.KeyboardEvent): void { 16 | if (textInput.current === null) return 17 | if (e.key === 'Enter' && textInput.current.value.trim().length > 0) { 18 | // make new TODO object 19 | const todo: Todo = { 20 | id: UUID(), 21 | bodyText: textInput.current.value, 22 | completed: false, 23 | } 24 | 25 | // add new TODO to entire TodoList 26 | setAppState({ todoList: [todo, ...appState.todoList] }) 27 | 28 | // reset text input UI value 29 | textInput.current.value = '' 30 | } 31 | } 32 | 33 | return ( 34 | 35 |
36 |

todos

37 | ) => addTodo(e)} 43 | data-testid="new-todo-input-text" 44 | data-cy="new-todo-input-text" 45 | // eslint-disable-next-line jsx-a11y/no-autofocus 46 | autoFocus 47 | /> 48 |
49 |
50 | ) 51 | } 52 | 53 | export default NewTodoTextInput 54 | -------------------------------------------------------------------------------- /src/App/NewTodoInput/style.ts: -------------------------------------------------------------------------------- 1 | /* Creative Commons Attribution 4.0 International (CC-BY-4.0) */ 2 | /* Copyright (c) Sindre Sorhus (sindresorhus.com) */ 3 | /* This source code was getting from https://github.com/tastejs/todomvc-app-css/blob/03e753aa21bd555cbdc2aa09185ecb9905d1bf16/index.css */ 4 | 5 | import { styled } from 'styled-components' 6 | 7 | import { base } from '../style' 8 | 9 | export const Layout = styled.div` 10 | .new-todo { 11 | ${base.textInput}; 12 | padding: 16px 16px 16px 60px; 13 | border: none; 14 | background: rgba(0, 0, 0, 0.003); 15 | box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); 16 | } 17 | ` 18 | -------------------------------------------------------------------------------- /src/App/TodoList/Item/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, screen } from '@testing-library/react' 2 | import React from 'react' 3 | import { useRecoilState } from 'recoil' 4 | 5 | import type { AppState } from '../../../dataStructure' 6 | import { recoilState } from '../../../dataStructure' 7 | import { TestRenderer } from '../../../testUtil' 8 | 9 | import Item from './index' 10 | 11 | const initialRecoilState: AppState = { 12 | todoList: [ 13 | { 14 | id: '8btxpD9kDBlo', 15 | bodyText: 'cut tomato', 16 | completed: false, 17 | }, 18 | ], 19 | } 20 | 21 | const App = () => { 22 | const [appState] = useRecoilState(recoilState) 23 | if (appState.todoList.length === 0) return null 24 | return ( 25 |
26 | 27 |
28 | ) 29 | } 30 | 31 | test('should each initialAppstate todo object value is set to Item element', () => { 32 | TestRenderer( 33 | , 34 | initialRecoilState, 35 | ) 36 | 37 | expect(screen.getByTestId('todo-item')).toBeInTheDocument() 38 | 39 | expect( 40 | (screen.getByTestId('todo-item-complete-check') as HTMLInputElement) 41 | .checked, 42 | ).toBe(false) 43 | expect(screen.getByTestId('todo-body-text')).toHaveTextContent('cut tomato') 44 | expect( 45 | (screen.getByTestId('todo-edit-input') as HTMLInputElement).value, 46 | ).toBe('cut tomato') 47 | }) 48 | 49 | test('should set css classes correctly', () => { 50 | TestRenderer(, initialRecoilState) 51 | 52 | // when not.completed & not.onEdit, SwitchStyle doesn't show .completed .editting selectors 53 | expect(screen.getByTestId('todo-item')).not.toHaveClass('completed') 54 | expect(screen.getByTestId('todo-item')).not.toHaveClass('editing') 55 | }) 56 | 57 | test('should work todo completed checkbox', () => { 58 | TestRenderer(, initialRecoilState) 59 | 60 | // click complete checkbox then should appear completed class 61 | fireEvent.click(screen.getByTestId('todo-item-complete-check')) 62 | expect( 63 | (screen.getByTestId('todo-item-complete-check') as HTMLInputElement) 64 | .checked, 65 | ).toBe(true) 66 | expect(screen.getByTestId('todo-item')).toHaveClass('completed') 67 | 68 | // should working as toggle 69 | fireEvent.click(screen.getByTestId('todo-item-complete-check')) 70 | expect( 71 | (screen.getByTestId('todo-item-complete-check') as HTMLInputElement) 72 | .checked, 73 | ).toBe(false) 74 | expect(screen.getByTestId('todo-item')).not.toHaveClass('completed') 75 | }) 76 | 77 | test('should work edit mode and toggle show/hide', () => { 78 | TestRenderer(, initialRecoilState) 79 | // by default, edit input form is not visible 80 | // expect(screen.getByTestId('todo-edit-input')).not.toBeVisible() this is styled-component@v6 specifc bug, doesn't apply "display:none" property 81 | // click todo text label, then focus and enable todo text edit code 82 | fireEvent.click(screen.getByTestId('todo-body-text')) 83 | expect(screen.getByTestId('todo-item')).toHaveClass('editing') 84 | expect(screen.getByTestId('todo-edit-input')).toBeVisible() 85 | expect(screen.getByTestId('todo-edit-input')).toHaveFocus() 86 | fireEvent.change(screen.getByTestId('todo-edit-input'), { 87 | target: { value: 'cut tomato plus' }, 88 | }) 89 | fireEvent.keyDown(screen.getByTestId('todo-edit-input'), { key: 'Enter' }) 90 | 91 | expect(screen.getByTestId('todo-body-text')).toHaveTextContent( 92 | 'cut tomato plus', 93 | ) 94 | expect(screen.getByTestId('todo-item')).not.toHaveClass('editing') 95 | // expect(screen.getByTestId('todo-edit-input')).not.toBeVisible() this is styled-component@v6 specifc bug, doesn't apply "display:none" property 96 | 97 | // click todo text label, then focus and enable todo text edit code 98 | fireEvent.click(screen.getByTestId('todo-body-text')) 99 | expect(screen.getByTestId('todo-item')).toHaveClass('editing') 100 | expect(screen.getByTestId('todo-edit-input')).toBeVisible() 101 | expect(screen.getByTestId('todo-edit-input')).toHaveFocus() 102 | fireEvent.change(screen.getByTestId('todo-edit-input'), { 103 | target: { value: 'cut tomato plus plus' }, 104 | }) 105 | fireEvent.keyDown(screen.getByTestId('todo-edit-input'), { key: 'Escape' }) 106 | expect(screen.getByTestId('todo-body-text')).toHaveTextContent( 107 | 'cut tomato plus plus', 108 | ) 109 | expect(screen.getByTestId('todo-item')).not.toHaveClass('editing') 110 | // expect(screen.getByTestId('todo-edit-input')).not.toBeVisible() this is styled-component@v6 specifc bug, doesn't apply "display:none" property 111 | }) 112 | 113 | test('delete todo item', () => { 114 | TestRenderer(, initialRecoilState) 115 | 116 | // click delete button, then todo item is removed 117 | expect(screen.getByTestId('todo-item')).toBeInTheDocument() 118 | fireEvent.click(screen.getByTestId('delete-todo-btn')) 119 | expect(screen.queryByTestId('todo-item')).toBe(null) 120 | }) 121 | -------------------------------------------------------------------------------- /src/App/TodoList/Item/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, createRef, useEffect } from 'react' 2 | import { useRecoilState } from 'recoil' 3 | 4 | import type { AppState, Todo, TodoListType } from '../../../dataStructure' 5 | import { recoilState } from '../../../dataStructure' 6 | 7 | import { Layout } from './style' 8 | 9 | interface Props { 10 | todo: Todo 11 | } 12 | 13 | interface State { 14 | onEdit: boolean 15 | } 16 | 17 | const Item: React.FC = ({ todo }) => { 18 | const [appState, setAppState] = useRecoilState(recoilState) 19 | const editInput = createRef() 20 | const init: State = { onEdit: false } 21 | const [state, setState] = useState(init) 22 | 23 | const onClick = (): void => { 24 | setState({ onEdit: true }) 25 | } 26 | 27 | const onBlurEdit = (e: React.FocusEvent): void => { 28 | if (e.currentTarget.value.trim().length > 0) { 29 | setState({ onEdit: false }) 30 | } else { 31 | removeItem(todo.id) 32 | } 33 | } 34 | 35 | const submitEditText = (e: React.KeyboardEvent): void => { 36 | if (e.key === 'Enter' || e.key === 'Escape') { 37 | if (e.currentTarget.value.trim().length > 0) { 38 | setState({ onEdit: false }) 39 | } 40 | } 41 | } 42 | 43 | // Control Todo's CSS based on complex user interaction 44 | const SwitchStyle = (t: Todo, onEdit: boolean): string => { 45 | switch (true) { 46 | case onEdit && t.completed: 47 | return 'completed editing' 48 | case onEdit && !t.completed: 49 | return 'editing' 50 | case !onEdit && t.completed: 51 | return 'completed' 52 | case !onEdit && !t.completed: 53 | return '' 54 | 55 | default: 56 | return '' 57 | } 58 | } 59 | 60 | const reverseCompleted = (id: Todo['id']): void => { 61 | const toggled: TodoListType = appState.todoList.map((t) => { 62 | // search clicked item by id... 63 | if (t.id === id) { 64 | // change complated status only clicked item 65 | return { ...t, completed: !t.completed } 66 | // return other item without any changes 67 | } else { 68 | return t 69 | } 70 | }) 71 | 72 | setAppState({ todoList: toggled }) 73 | } 74 | 75 | const removeItem = (terminate: Todo['id']): void => { 76 | const removed: TodoListType = appState.todoList.filter( 77 | (t: Todo): boolean => t.id !== terminate, 78 | ) 79 | 80 | setAppState({ todoList: removed }) 81 | } 82 | 83 | const handleTodoTextEdit = (e: React.ChangeEvent, onEdit: Todo['id']): void => { /* eslint-disable-line prettier/prettier */ 84 | const edited = appState.todoList.map((t: Todo): Todo => { 85 | if (t.id === onEdit) { 86 | return { ...t, bodyText: e.target.value } 87 | } else { 88 | return t 89 | } 90 | }) 91 | 92 | setAppState({ todoList: edited }) 93 | } 94 | 95 | useEffect(() => { 96 | // For fucus input element when double clicks text label. fix this https://github.com/laststance/create-react-app-typescript-todo-example-2021/issues/50 97 | if (state.onEdit === true && editInput.current !== null) 98 | editInput.current.focus() 99 | }, [editInput, state.onEdit]) 100 | 101 | return ( 102 | 103 |
  • 104 |
    105 | reverseCompleted(todo.id)} 110 | data-cy="todo-item-complete-check" 111 | data-testid="todo-item-complete-check" 112 | /> 113 | 114 | {/* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */} 115 | 122 | {/* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */} 123 |
    130 | ) => onBlurEdit(e)} 133 | className="edit" 134 | value={todo.bodyText} 135 | onChange={(e: React.ChangeEvent) => handleTodoTextEdit(e, todo.id)} /* eslint-disable-line prettier/prettier */ 136 | onKeyDown={(e: React.KeyboardEvent) => submitEditText(e)} /* eslint-disable-line prettier/prettier */ 137 | data-cy="todo-edit-input" 138 | data-testid="todo-edit-input" 139 | /> 140 |
  • 141 |
    142 | ) 143 | } 144 | 145 | export default Item 146 | -------------------------------------------------------------------------------- /src/App/TodoList/Item/style.ts: -------------------------------------------------------------------------------- 1 | /* Creative Commons Attribution 4.0 International (CC-BY-4.0) */ 2 | /* Copyright (c) Sindre Sorhus (sindresorhus.com) */ 3 | /* This source code was getting from https://github.com/tastejs/todomvc-app-css/blob/03e753aa21bd555cbdc2aa09185ecb9905d1bf16/index.css */ 4 | 5 | import { styled } from 'styled-components' 6 | 7 | import { base } from '../../style' 8 | 9 | export const Layout = styled.div` 10 | position: relative; 11 | font-size: 24px; 12 | border-bottom: 1px solid #ededed; 13 | 14 | .edit { 15 | ${base.textInput}; 16 | } 17 | 18 | .edit { 19 | display: none; 20 | } 21 | 22 | &:last-child { 23 | border-bottom: none; 24 | } 25 | 26 | .editing { 27 | border-bottom: none; 28 | padding: 0; 29 | } 30 | 31 | .editing .edit { 32 | display: block !important; 33 | width: calc(100% - 43px); 34 | padding: 12px 16px; 35 | margin: 0 0 0 43px; 36 | } 37 | 38 | .editing .view { 39 | display: none; 40 | } 41 | 42 | .toggle { 43 | text-align: center; 44 | width: 40px; 45 | /* auto, since non-WebKit browsers doesn't support input styling */ 46 | height: auto; 47 | position: absolute; 48 | top: 0; 49 | bottom: 0; 50 | margin: auto 0; 51 | border: none; /* Mobile Safari */ 52 | -webkit-appearance: none; 53 | appearance: none; 54 | opacity: 0; 55 | } 56 | 57 | .toggle + label { 58 | /* 59 | Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 60 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 61 | */ 62 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 63 | background-repeat: no-repeat; 64 | background-position: center left; 65 | } 66 | 67 | .toggle:checked + label { 68 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2021%2021%2034-52z%22/%3E%3C/svg%3E'); 69 | } 70 | 71 | label { 72 | word-break: break-all; 73 | padding: 15px 15px 15px 60px; 74 | display: block; 75 | line-height: 1.2; 76 | transition: color 0.4s; 77 | } 78 | 79 | .completed label { 80 | color: #d9d9d9; 81 | text-decoration: line-through; 82 | } 83 | 84 | .destroy { 85 | display: none; 86 | position: absolute; 87 | top: 0; 88 | right: 10px; 89 | bottom: 0; 90 | width: 40px; 91 | height: 40px; 92 | margin: auto 0; 93 | font-size: 30px; 94 | color: #cc9a9a; 95 | margin-bottom: 11px; 96 | transition: color 0.2s ease-out; 97 | } 98 | 99 | .destroy:hover { 100 | color: #af5b5e; 101 | } 102 | 103 | .destroy:after { 104 | content: '×'; 105 | } 106 | 107 | &:hover .destroy { 108 | display: block; 109 | } 110 | 111 | .editing:last-child { 112 | margin-bottom: -1px; 113 | } 114 | ` 115 | -------------------------------------------------------------------------------- /src/App/TodoList/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent } from '@testing-library/react' 2 | import React from 'react' 3 | 4 | import type { AppState } from '../../dataStructure' 5 | import { TestRenderer } from '../../testUtil' 6 | 7 | import TodoList from './index' 8 | 9 | const initialRecoilState: AppState = { 10 | todoList: [ 11 | { 12 | id: 'TsHx9eEN5Y4A', 13 | bodyText: 'monster', 14 | completed: false, 15 | }, 16 | { 17 | id: 'ba91OwrK0Dt8', 18 | bodyText: 'boss black', 19 | completed: false, 20 | }, 21 | { 22 | id: 'QwejYipEf5nk', 23 | bodyText: 'caffe latte', 24 | completed: false, 25 | }, 26 | ], 27 | } 28 | 29 | test('should be render 3 todo items in initialAppState', () => { 30 | const screen = TestRenderer(, initialRecoilState) 31 | 32 | expect(screen.getByTestId('todo-list')).toBeInTheDocument() 33 | expect(screen.getByTestId('todo-list').children.length).toBe(3) 34 | expect(Array.isArray(screen.getAllByTestId('todo-item'))).toBe(true) 35 | expect(screen.getAllByTestId('todo-item')[0]).toHaveTextContent('monster') 36 | expect(screen.getAllByTestId('todo-item')[1]).toHaveTextContent('boss black') 37 | expect(screen.getAllByTestId('todo-item')[2]).toHaveTextContent('caffe latte') 38 | }) 39 | 40 | test('should be work delete todo button', () => { 41 | const screen = TestRenderer(, initialRecoilState) 42 | 43 | // delete first item 44 | fireEvent.click(screen.getAllByTestId('delete-todo-btn')[0]) 45 | // assertions 46 | expect(screen.getByTestId('todo-list').children.length).toBe(2) 47 | expect(Array.isArray(screen.getAllByTestId('todo-item'))).toBe(true) 48 | expect(screen.getAllByTestId('todo-item')[0]).toHaveTextContent('boss black') 49 | expect(screen.getAllByTestId('todo-item')[1]).toHaveTextContent('caffe latte') 50 | }) 51 | 52 | test('should be work correctly all completed:true|false checkbox toggle button', () => { 53 | const screen = TestRenderer(, initialRecoilState) 54 | 55 | // toggle on 56 | fireEvent.click(screen.getByTestId('toggle-all-btn')) 57 | // should be completed all todo items 58 | expect((screen.getAllByTestId('todo-item-complete-check')[0] as HTMLInputElement).checked).toBe(true) /* eslint-disable-line prettier/prettier */ 59 | expect((screen.getAllByTestId('todo-item-complete-check')[1] as HTMLInputElement).checked).toBe(true) /* eslint-disable-line prettier/prettier */ 60 | expect((screen.getAllByTestId('todo-item-complete-check')[2] as HTMLInputElement).checked).toBe(true) /* eslint-disable-line prettier/prettier */ 61 | 62 | // toggle off 63 | fireEvent.click(screen.getByTestId('toggle-all-btn')) 64 | // should be not comleted all todo items 65 | expect((screen.getAllByTestId('todo-item-complete-check')[0] as HTMLInputElement).checked).toBe(false) /* eslint-disable-line prettier/prettier */ 66 | expect((screen.getAllByTestId('todo-item-complete-check')[1] as HTMLInputElement).checked).toBe(false) /* eslint-disable-line prettier/prettier */ 67 | expect((screen.getAllByTestId('todo-item-complete-check')[2] as HTMLInputElement).checked).toBe(false) /* eslint-disable-line prettier/prettier */ 68 | }) 69 | -------------------------------------------------------------------------------- /src/App/TodoList/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactElement } from 'react' 2 | import React from 'react' 3 | import { useLocation } from 'react-router-dom' 4 | import { useRecoilState } from 'recoil' 5 | 6 | import type { AppState, Todo } from '../../dataStructure' 7 | import { recoilState } from '../../dataStructure' 8 | 9 | import Item from './Item' 10 | import { Layout } from './style' 11 | 12 | const TodoList: React.FC = () => { 13 | const { pathname } = useLocation() 14 | const [appState, setAppState] = useRecoilState(recoilState) 15 | 16 | function toggleAllCheckbox(e: React.ChangeEvent): void { /* eslint-disable-line prettier/prettier */ 17 | // reverse all todo.completed: boolean flag 18 | setAppState({ todoList: appState.todoList.map((t: Todo): Todo => ({ ...t, completed: e.target.checked })) }) /* eslint-disable-line prettier/prettier */ 19 | } 20 | 21 | return ( 22 | 23 |
    24 | 32 | 33 |
      34 | {appState.todoList 35 | .filter((t: Todo): boolean => { 36 | switch (pathname) { 37 | case '/': 38 | return true 39 | case '/active': 40 | return t.completed === false 41 | case '/completed': 42 | return t.completed === true 43 | default: 44 | return true 45 | } 46 | }) 47 | .map((t: Todo): ReactElement => { 48 | return 49 | })} 50 |
    51 |
    52 |
    53 | ) 54 | } 55 | 56 | export default TodoList 57 | -------------------------------------------------------------------------------- /src/App/TodoList/style.ts: -------------------------------------------------------------------------------- 1 | /* Creative Commons Attribution 4.0 International (CC-BY-4.0) */ 2 | /* Copyright (c) Sindre Sorhus (sindresorhus.com) */ 3 | /* This source code was getting from https://github.com/tastejs/todomvc-app-css/blob/03e753aa21bd555cbdc2aa09185ecb9905d1bf16/index.css */ 4 | 5 | import { styled } from 'styled-components' 6 | 7 | export const Layout = styled.div` 8 | .main { 9 | position: relative; 10 | z-index: 2; 11 | border-top: 1px solid #e6e6e6; 12 | } 13 | 14 | .toggle-all { 15 | width: 1px; 16 | height: 1px; 17 | border: none; /* Mobile Safari */ 18 | opacity: 0; 19 | position: absolute; 20 | right: 100%; 21 | bottom: 100%; 22 | } 23 | 24 | .toggle-all + label { 25 | width: 60px; 26 | height: 34px; 27 | font-size: 0; 28 | position: absolute; 29 | top: -52px; 30 | left: -13px; 31 | -webkit-transform: rotate(90deg); 32 | transform: rotate(90deg); 33 | } 34 | 35 | .toggle-all + label:before { 36 | content: '❯'; 37 | font-size: 22px; 38 | color: #e6e6e6; 39 | padding: 10px 27px 10px 27px; 40 | } 41 | 42 | .toggle-all:checked + label:before { 43 | color: #737373; 44 | } 45 | 46 | .todo-list { 47 | margin: 0; 48 | padding: 0; 49 | list-style: none; 50 | } 51 | 52 | /* 53 | Hack to remove background from Mobile Safari. 54 | Can't use it globally since it destroys checkboxes in Firefox 55 | */ 56 | @media screen and (-webkit-min-device-pixel-ratio: 0) { 57 | .toggle-all, 58 | .todo-list li .toggle { 59 | background: none; 60 | } 61 | 62 | .todo-list li .toggle { 63 | height: 40px; 64 | } 65 | } 66 | ` 67 | -------------------------------------------------------------------------------- /src/App/TodoMVC.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useRecoilValue } from 'recoil' 3 | 4 | import type { AppState } from '../dataStructure' 5 | import { recoilState, LocalStorageKey } from '../dataStructure' 6 | 7 | import Copyright from './Copyright' 8 | import NewTodoInput from './NewTodoInput' 9 | import { Layout } from './style' 10 | import TodoList from './TodoList' 11 | import UnderBar from './UnderBar' 12 | 13 | const TodoMVC: React.FC = () => { 14 | const appState = useRecoilValue(recoilState) 15 | 16 | // if appState has changes, save it LocalStorage. 17 | useEffect((): void => { 18 | window.localStorage.setItem( 19 | LocalStorageKey.APP_STATE, 20 | JSON.stringify(appState), // convert JavaScript Object to string 21 | ) 22 | }, [appState]) 23 | 24 | return ( 25 | 26 |
    27 | 28 | {appState.todoList.length ? ( 29 | <> 30 | 31 | 32 | 33 | ) : null} 34 |
    35 | 36 |
    37 | ) 38 | } 39 | 40 | export default TodoMVC 41 | -------------------------------------------------------------------------------- /src/App/UnderBar/FilterLink/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, useLocation } from 'react-router-dom' 3 | 4 | const FilterLink: React.FC = () => { 5 | const { pathname } = useLocation() 6 | return ( 7 |
      8 |
    • 9 | 14 | All 15 | 16 |
    • 17 |
    • 18 | 23 | Active 24 | 25 |
    • 26 |
    • 27 | 32 | Completed 33 | 34 |
    • 35 |
    36 | ) 37 | } 38 | 39 | export default FilterLink 40 | -------------------------------------------------------------------------------- /src/App/UnderBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useRecoilState } from 'recoil' 3 | 4 | import type { AppState, Todo } from '../../dataStructure' 5 | import { recoilState } from '../../dataStructure' 6 | 7 | import FilterLink from './FilterLink' 8 | import { Layout } from './style' 9 | 10 | const UnderBar: React.FC = () => { 11 | const [appState, setAppState] = useRecoilState(recoilState) 12 | const completed: number = appState.todoList.filter(t => t.completed === true).length /* eslint-disable-line prettier/prettier */ 13 | const backlog: number = appState.todoList.filter(t => t.completed === false).length /* eslint-disable-line prettier/prettier */ 14 | 15 | function clearCompleted(): void { 16 | setAppState({ 17 | todoList: appState.todoList.filter((t: Todo) => !t.completed), 18 | }) 19 | } 20 | 21 | return ( 22 | 23 |
    24 | 25 | {backlog}{' '} 26 | item left 27 | 28 | 29 | 30 | {completed > 0 && ( 31 | 38 | )} 39 |
    40 |
    41 | ) 42 | } 43 | 44 | export default UnderBar 45 | -------------------------------------------------------------------------------- /src/App/UnderBar/style.ts: -------------------------------------------------------------------------------- 1 | /* Creative Commons Attribution 4.0 International (CC-BY-4.0) */ 2 | /* Copyright (c) Sindre Sorhus (sindresorhus.com) */ 3 | /* This source code was getting from https://github.com/tastejs/todomvc-app-css/blob/03e753aa21bd555cbdc2aa09185ecb9905d1bf16/index.css */ 4 | 5 | import { styled } from 'styled-components' 6 | 7 | export const Layout = styled.div` 8 | .footer { 9 | color: #777; 10 | padding: 10px 15px; 11 | height: 20px; 12 | text-align: center; 13 | border-top: 1px solid #e6e6e6; 14 | } 15 | 16 | .footer:before { 17 | content: ''; 18 | position: absolute; 19 | right: 0; 20 | bottom: 0; 21 | left: 0; 22 | height: 50px; 23 | overflow: hidden; 24 | box-shadow: 25 | 0 1px 1px rgba(0, 0, 0, 0.2), 26 | 0 8px 0 -3px #f6f6f6, 27 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 28 | 0 16px 0 -6px #f6f6f6, 29 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 30 | } 31 | 32 | .todo-count { 33 | float: left; 34 | text-align: left; 35 | } 36 | 37 | .todo-count strong { 38 | font-weight: 300; 39 | } 40 | 41 | .filters { 42 | margin: 0; 43 | padding: 0; 44 | list-style: none; 45 | position: absolute; 46 | right: 0; 47 | left: 0; 48 | } 49 | 50 | .filters li { 51 | display: inline; 52 | } 53 | 54 | .filters li a { 55 | color: inherit; 56 | margin: 3px; 57 | padding: 3px 7px; 58 | text-decoration: none; 59 | border: 1px solid transparent; 60 | border-radius: 3px; 61 | } 62 | 63 | .filters li a:hover { 64 | border-color: rgba(175, 47, 47, 0.1); 65 | } 66 | 67 | .filters li a.selected { 68 | border-color: rgba(175, 47, 47, 0.2); 69 | } 70 | 71 | .clear-completed, 72 | html .clear-completed:active { 73 | float: right; 74 | position: relative; 75 | line-height: 20px; 76 | text-decoration: none; 77 | cursor: pointer; 78 | } 79 | 80 | .clear-completed:hover { 81 | text-decoration: underline; 82 | } 83 | 84 | @media (max-width: 430px) { 85 | .footer { 86 | height: 50px; 87 | } 88 | 89 | .filters { 90 | bottom: 10px; 91 | } 92 | } 93 | ` 94 | -------------------------------------------------------------------------------- /src/App/index.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from 'react-router-dom' 2 | import { RecoilRoot } from 'recoil' 3 | 4 | import ErrorBoundary from '../ErrorBoundary' 5 | import { NotFound } from '../NotFound' 6 | 7 | import TodoMVC from './TodoMVC' 8 | 9 | const App: React.FC = () => ( 10 | 11 | 12 | 13 | 14 | } /> 15 | } /> 16 | } /> 17 | } /> 18 | 19 | 20 | 21 | 22 | ) 23 | 24 | export default App 25 | -------------------------------------------------------------------------------- /src/App/style.ts: -------------------------------------------------------------------------------- 1 | /* Creative Commons Attribution 4.0 International (CC-BY-4.0) */ 2 | /* Copyright (c) Sindre Sorhus (sindresorhus.com) */ 3 | /* This source code was getting from https://github.com/tastejs/todomvc-app-css/blob/03e753aa21bd555cbdc2aa09185ecb9905d1bf16/index.css */ 4 | 5 | import { styled, css } from 'styled-components' 6 | 7 | export const Layout = styled.div` 8 | .todoapp { 9 | background: #fff; 10 | margin: 130px 0 40px 0; 11 | position: relative; 12 | box-shadow: 13 | 0 2px 4px 0 rgba(0, 0, 0, 0.2), 14 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 15 | } 16 | 17 | .todoapp input::-webkit-input-placeholder { 18 | font-style: italic; 19 | font-weight: 300; 20 | color: #e6e6e6; 21 | } 22 | 23 | .todoapp input::-moz-placeholder { 24 | font-style: italic; 25 | font-weight: 300; 26 | color: #e6e6e6; 27 | } 28 | 29 | .todoapp input::input-placeholder { 30 | font-style: italic; 31 | font-weight: 300; 32 | color: #e6e6e6; 33 | } 34 | 35 | .todoapp h1 { 36 | position: absolute; 37 | top: -155px; 38 | width: 100%; 39 | font-size: 100px; 40 | font-weight: 100; 41 | text-align: center; 42 | color: rgba(175, 47, 47, 0.15); 43 | -webkit-text-rendering: optimizeLegibility; 44 | -moz-text-rendering: optimizeLegibility; 45 | text-rendering: optimizeLegibility; 46 | } 47 | 48 | .info { 49 | margin: 65px auto 0; 50 | color: #bfbfbf; 51 | font-size: 10px; 52 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 53 | text-align: center; 54 | } 55 | 56 | .info p { 57 | line-height: 1; 58 | } 59 | 60 | .info a { 61 | color: inherit; 62 | text-decoration: none; 63 | font-weight: 400; 64 | } 65 | 66 | .info a:hover { 67 | text-decoration: underline; 68 | } 69 | ` 70 | 71 | export const base = { 72 | textInput: css` 73 | position: relative; 74 | margin: 0; 75 | width: 100%; 76 | font-size: 24px; 77 | font-family: inherit; 78 | font-weight: inherit; 79 | line-height: 1.4em; 80 | color: inherit; 81 | padding: 6px; 82 | border: 1px solid #999; 83 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 84 | box-sizing: border-box; 85 | -webkit-font-smoothing: antialiased; 86 | -moz-osx-font-smoothing: grayscale; 87 | `, 88 | } 89 | -------------------------------------------------------------------------------- /src/ErrorBoundary.test.js: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | 3 | import ErrorBoundary from './ErrorBoundary' 4 | 5 | test('should be render fallback page Error was thrown', () => { 6 | const InvalidComponent = () => new Date() 7 | const screen = render( 8 | 9 | 10 | , 11 | ) 12 | expect(screen.getByText('Something Error Ooccurring')).toBeInTheDocument() 13 | }) 14 | -------------------------------------------------------------------------------- /src/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import type { ErrorInfo, ReactNode } from 'react' 2 | import React, { Component } from 'react' 3 | import { styled } from 'styled-components' 4 | 5 | interface Props { 6 | children?: ReactNode 7 | } 8 | 9 | interface State { 10 | error: Error | null 11 | info: ErrorInfo | null 12 | } 13 | class ErrorBoundary extends Component { 14 | state = { 15 | error: null, 16 | info: null, 17 | } 18 | 19 | componentDidCatch(error: Error, info: ErrorInfo): void { 20 | this.setState({ error, info }) 21 | } 22 | 23 | render(): ReactNode { 24 | const { error } = this.state 25 | if (error) { 26 | return 27 | } 28 | return this.props.children 29 | } 30 | } 31 | 32 | export default ErrorBoundary 33 | 34 | const Layout = styled.div` 35 | width: 100%; 36 | height: 100%; 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | ` 41 | 42 | const Message = styled.div` 43 | padding: 40px; 44 | border: 2px #78909c solid; 45 | border-radius: 5px; 46 | font-size: 24px; 47 | color: #78909c; 48 | ` 49 | 50 | const ErrorBoundaryFallbackComponent = () => ( 51 | 52 | 53 | Something Error Ooccurring 54 | 55 | 😞 56 | 57 | 58 | 59 | ) 60 | -------------------------------------------------------------------------------- /src/NotFound.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | import React from 'react' 3 | 4 | import { NotFound } from './NotFound' 5 | 6 | test(' should render Page Not Found message', () => { 7 | const screen = render() 8 | expect(screen.getByText('Page Not Found')).toBeInTheDocument() 9 | }) 10 | -------------------------------------------------------------------------------- /src/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const css: React.CSSProperties = { 4 | alignItems: 'center', 5 | display: 'flex', 6 | height: '100%', 7 | justifyContent: 'center', 8 | width: '100%', 9 | } 10 | 11 | export const NotFound: React.FC = () => ( 12 |
    13 |

    Page Not Found

    14 |
    15 | ) 16 | -------------------------------------------------------------------------------- /src/dataStructure.ts: -------------------------------------------------------------------------------- 1 | import type { RecoilState } from 'recoil' 2 | import { atom } from 'recoil' 3 | 4 | export type Routes = '/' | '/active' | '/completed' 5 | 6 | export interface Todo { 7 | id: string 8 | bodyText: string 9 | completed: boolean 10 | } 11 | 12 | export type TodoListType = Todo[] 13 | 14 | export interface AppState { 15 | todoList: TodoListType 16 | } 17 | 18 | export enum LocalStorageKey { 19 | APP_STATE = 'APP_STATE', 20 | } 21 | 22 | function LoadAppStateFromLocalStorage(): AppState { 23 | const stringifiedJSON: string | null = window.localStorage.getItem( 24 | LocalStorageKey.APP_STATE, 25 | ) 26 | if (typeof stringifiedJSON === 'string') { 27 | const Loaded: AppState = JSON.parse(stringifiedJSON) 28 | return Loaded 29 | } 30 | 31 | const BlankAppState: AppState = { 32 | todoList: [], 33 | } 34 | 35 | return BlankAppState 36 | } 37 | 38 | export const recoilState: RecoilState = atom({ 39 | default: LoadAppStateFromLocalStorage(), 40 | key: 'initialAppState', 41 | }) 42 | -------------------------------------------------------------------------------- /src/functions.test.ts: -------------------------------------------------------------------------------- 1 | import { UUID } from './functions' 2 | 3 | describe('UUID', () => { 4 | test('should generate random 12 length by base62', () => { 5 | const results: string[] = [] 6 | 7 | for (let i = 0; i <= 1000; i++) { 8 | const uuid: string = UUID() 9 | results.push(uuid) 10 | } 11 | 12 | results.forEach((uuid) => 13 | expect(uuid).toMatch( 14 | /^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]{12}$/, 15 | ), 16 | ) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/functions.ts: -------------------------------------------------------------------------------- 1 | export const UUID = (): string => { 2 | let result = '' 3 | const characters = 4 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 5 | const charactersLength = characters.length 6 | for (let i = 0; i < 12; i++) { 7 | result += characters.charAt(Math.floor(Math.random() * charactersLength)) 8 | } 9 | return result 10 | } 11 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* Creative Commons Attribution 4.0 International (CC-BY-4.0) */ 2 | /* Copyright (c) Sindre Sorhus (sindresorhus.com) */ 3 | /* This source code was getting from https://github.com/tastejs/todomvc-app-css/blob/03e753aa21bd555cbdc2aa09185ecb9905d1bf16/index.css */ 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | button { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | background: none; 16 | font-size: 100%; 17 | vertical-align: baseline; 18 | font-family: inherit; 19 | font-weight: inherit; 20 | color: inherit; 21 | -webkit-appearance: none; 22 | appearance: none; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | body { 28 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 29 | line-height: 1.4em; 30 | background: #f5f5f5; 31 | color: #4d4d4d; 32 | min-width: 230px; 33 | max-width: 550px; 34 | margin: 0 auto; 35 | -webkit-font-smoothing: antialiased; 36 | -moz-osx-font-smoothing: grayscale; 37 | font-weight: 300; 38 | } 39 | 40 | :focus { 41 | outline: 0; 42 | } 43 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | 4 | import './index.css' 5 | import App from './App' 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')!) 8 | 9 | root.render() 10 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare namespace NodeJS { 5 | interface ProcessEnv { 6 | readonly NODE_ENV: 'development' | 'production' | 'test' 7 | readonly PUBLIC_URL: string 8 | } 9 | } 10 | 11 | declare module '*.bmp' { 12 | const src: string 13 | export default src 14 | } 15 | 16 | declare module '*.gif' { 17 | const src: string 18 | export default src 19 | } 20 | 21 | declare module '*.jpg' { 22 | const src: string 23 | export default src 24 | } 25 | 26 | declare module '*.jpeg' { 27 | const src: string 28 | export default src 29 | } 30 | 31 | declare module '*.png' { 32 | const src: string 33 | export default src 34 | } 35 | 36 | declare module '*.webp' { 37 | const src: string 38 | export default src 39 | } 40 | 41 | declare module '*.svg' { 42 | import type * as React from 'react' 43 | 44 | export const ReactComponent: React.FunctionComponent< 45 | React.SVGProps 46 | > 47 | 48 | const src: string 49 | export default src 50 | } 51 | 52 | declare module '*.module.css' { 53 | const classes: { [key: string]: string } 54 | export default classes 55 | } 56 | 57 | declare module '*.module.scss' { 58 | const classes: { [key: string]: string } 59 | export default classes 60 | } 61 | 62 | declare module '*.module.sass' { 63 | const classes: { [key: string]: string } 64 | export default classes 65 | } 66 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' 6 | -------------------------------------------------------------------------------- /src/testUtil.tsx: -------------------------------------------------------------------------------- 1 | import type { RenderResult } from '@testing-library/react' 2 | import { render } from '@testing-library/react' 3 | import React from 'react' 4 | import { BrowserRouter } from 'react-router-dom' 5 | import type { MutableSnapshot } from 'recoil' 6 | import { RecoilRoot } from 'recoil' 7 | 8 | import type { AppState } from './dataStructure' 9 | import { recoilState } from './dataStructure' 10 | 11 | const defaultValue: AppState = { 12 | todoList: [], 13 | } 14 | 15 | export const TestRenderer = ( 16 | ui: React.ReactElement, 17 | initialRecoilStateValue: AppState = defaultValue, 18 | ): RenderResult => 19 | render( 20 | 21 | 23 | set(recoilState, initialRecoilStateValue) 24 | } 25 | > 26 | {ui} 27 | 28 | , 29 | ) 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "cypress", 5 | "@testing-library/cypress", 6 | "vite/client", 7 | "@types/jest" 8 | ], 9 | "target": "es2020", 10 | "lib": ["dom", "dom.iterable", "esnext"], 11 | "allowJs": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "jsx": "react-jsx" 24 | }, 25 | "include": [ 26 | "src", 27 | "cypress", 28 | "vite.config.ts", 29 | "jest.config.js", 30 | "jest/fileTransformer.js" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | build: { 6 | outDir: 'build', 7 | }, 8 | plugins: [react()], 9 | server: { 10 | host: true, 11 | open: true, 12 | port: 3000, 13 | }, 14 | }) 15 | --------------------------------------------------------------------------------