├── .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 | [](https://app.netlify.com/sites/react-typescript-todomvc/deploys)
16 | [](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/build.yml)
17 | [](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/e2e.yml)
18 | [](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/lint.yml)
19 | [](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/test.yml)
20 | [](https://github.com/prettier/prettier)
21 | [](https://github.com/laststance/react-typescript-todomvc-2022/actions/workflows/typecheck.yml)
22 | [](#contributors)
23 | [](https://percy.io/laststance/react-typescript-todomvc-2022)
24 |
25 |
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 | [](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 | 
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------