a11y_agent 0.0.5.pre.alpha.3 → 0.0.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/LICENSE +661 -0
- data/fixtures/react/.eslintrc +13 -0
- data/fixtures/react/.gitignore +3 -0
- data/fixtures/react/package-lock.json +16453 -0
- data/fixtures/react/package.json +42 -0
- data/fixtures/react/public/index.html +19 -0
- data/fixtures/react/readme.md +42 -0
- data/fixtures/react/src/index.js +16 -0
- data/fixtures/react/src/todo/app.css +87 -0
- data/fixtures/react/src/todo/app.jsx +20 -0
- data/fixtures/react/src/todo/components/footer.jsx +46 -0
- data/fixtures/react/src/todo/components/header.jsx +15 -0
- data/fixtures/react/src/todo/components/input.jsx +46 -0
- data/fixtures/react/src/todo/components/item.jsx +55 -0
- data/fixtures/react/src/todo/components/main.jsx +45 -0
- data/fixtures/react/src/todo/constants.js +7 -0
- data/fixtures/react/src/todo/reducer.js +64 -0
- data/fixtures/react/webpack.common.js +43 -0
- data/fixtures/react/webpack.dev.js +18 -0
- data/fixtures/react/webpack.prod.js +35 -0
- data/fixtures/sample.html +93 -0
- data/lib/a11y_agent/version.rb +1 -1
- data/lib/agents/a11y_agent.rb +131 -0
- data/lib/generators/confirmable_fix_generator.rb +56 -0
- data/lib/generators/fix_a11y_generator.rb +35 -0
- data/lib/tasks/release.rake +7 -4
- data/package.json +12 -7
- data/yarn.lock +114 -1810
- metadata +87 -12
- data/eslint.config.mjs +0 -23
- data/lib/a11y_agent.rb +0 -119
- data/lib/fix_a11y_generator.rb +0 -32
@@ -0,0 +1,42 @@
|
|
1
|
+
{
|
2
|
+
"name": "todomvc-react",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "A TodoMVC written in React.",
|
5
|
+
"private": true,
|
6
|
+
"engines": {
|
7
|
+
"node": ">=18.13.0",
|
8
|
+
"npm": ">=8.19.3"
|
9
|
+
},
|
10
|
+
"scripts": {
|
11
|
+
"build": "webpack --config webpack.prod.js",
|
12
|
+
"dev": "webpack serve --open --config webpack.dev.js",
|
13
|
+
"serve": "http-server ./dist -p 7002 -c-1 --cors",
|
14
|
+
"test": "jest"
|
15
|
+
},
|
16
|
+
"devDependencies": {
|
17
|
+
"@babel/core": "^7.21.0",
|
18
|
+
"@babel/preset-env": "^7.20.2",
|
19
|
+
"@babel/preset-react": "^7.18.6",
|
20
|
+
"babel-loader": "^9.1.2",
|
21
|
+
"copy-webpack-plugin": "^12.0.2",
|
22
|
+
"css-loader": "^6.7.3",
|
23
|
+
"css-minimizer-webpack-plugin": "^4.2.2",
|
24
|
+
"eslint-plugin-react": "^7.32.2",
|
25
|
+
"html-webpack-plugin": "^5.5.0",
|
26
|
+
"http-server": "^14.1.1",
|
27
|
+
"mini-css-extract-plugin": "^2.7.2",
|
28
|
+
"style-loader": "^3.3.1",
|
29
|
+
"webpack": "^5.75.0",
|
30
|
+
"webpack-cli": "^5.0.1",
|
31
|
+
"webpack-dev-server": "^4.11.1",
|
32
|
+
"webpack-merge": "^5.8.0"
|
33
|
+
},
|
34
|
+
"dependencies": {
|
35
|
+
"classnames": "^2.2.5",
|
36
|
+
"react": "^17.0.2",
|
37
|
+
"react-dom": "^17.0.2",
|
38
|
+
"react-router-dom": "^6.8.2",
|
39
|
+
"todomvc-app-css": "^2.4.2",
|
40
|
+
"todomvc-common": "^1.0.5"
|
41
|
+
}
|
42
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en" data-framework="react">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8" />
|
5
|
+
<meta name="description" content="A TodoMVC written in React." />
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7
|
+
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
8
|
+
<title>TodoMVC: React</title>
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
<section class="todoapp" id="root"></section>
|
12
|
+
<footer class="info">
|
13
|
+
<p>Double-click to edit a todo</p>
|
14
|
+
<p>Created by the TodoMVC Team</p>
|
15
|
+
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
|
16
|
+
</footer>
|
17
|
+
<script src="./base.js"></script>
|
18
|
+
</body>
|
19
|
+
</html>
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# TodoMVC: React
|
2
|
+
|
3
|
+
## Description
|
4
|
+
|
5
|
+
This application uses React 17.0.2 to implement a todo application.
|
6
|
+
|
7
|
+
- [React](https://reactjs.org/) is a JavaScript library for creating user interfaces.
|
8
|
+
|
9
|
+
## Implementation details
|
10
|
+
|
11
|
+
React focuses mainly on providing composable user interfaces to enable developers to build an appealing website or web app. React does not force the user to utilize a particular design pattern, but it does provide useful hooks to implement an MVC pattern, if desired.
|
12
|
+
|
13
|
+
React:\
|
14
|
+
Model: Todo reducer (reducer.js)\
|
15
|
+
View: React ui components\
|
16
|
+
Controller: App component + useReducer hook
|
17
|
+
|
18
|
+
MVC:\
|
19
|
+
Model: Maintains the data and behavior of an application\
|
20
|
+
View: Displays the model in the ui\
|
21
|
+
Controller: Serves as an interface between view & model components
|
22
|
+
|
23
|
+
## Build steps
|
24
|
+
|
25
|
+
To build the static files, this application utilizes webpack. It minifies and optimizes output files and copies all necessary files to a `dist` folder.
|
26
|
+
|
27
|
+
## Requirements
|
28
|
+
|
29
|
+
The only requirement is an installation of Node, to be able to install dependencies and run scripts to serve a local server.
|
30
|
+
|
31
|
+
```
|
32
|
+
* Node (min version: 18.13.0)
|
33
|
+
* NPM (min version: 8.19.3)
|
34
|
+
```
|
35
|
+
|
36
|
+
## Local preview
|
37
|
+
|
38
|
+
```
|
39
|
+
terminal:
|
40
|
+
1. npm install
|
41
|
+
2. npm run serve
|
42
|
+
```
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import React from "react";
|
2
|
+
import { render } from "react-dom";
|
3
|
+
import { HashRouter, Route, Routes } from "react-router-dom";
|
4
|
+
|
5
|
+
import { App } from "./todo/app";
|
6
|
+
import "todomvc-app-css/index.css";
|
7
|
+
import "todomvc-common/base.css";
|
8
|
+
|
9
|
+
render(
|
10
|
+
<HashRouter>
|
11
|
+
<Routes>
|
12
|
+
<Route path="*" element={<App />} />
|
13
|
+
</Routes>
|
14
|
+
</HashRouter>,
|
15
|
+
document.getElementById("root")
|
16
|
+
);
|
@@ -0,0 +1,87 @@
|
|
1
|
+
.toggle-all {
|
2
|
+
width: 40px !important;
|
3
|
+
height: 60px !important;
|
4
|
+
right: auto !important;
|
5
|
+
}
|
6
|
+
|
7
|
+
.toggle-all-label {
|
8
|
+
pointer-events: none;
|
9
|
+
}
|
10
|
+
|
11
|
+
.todo-list {
|
12
|
+
margin: 0;
|
13
|
+
padding: 0;
|
14
|
+
}
|
15
|
+
|
16
|
+
.todo-item {
|
17
|
+
border-bottom: 1px solid #ededed;
|
18
|
+
font-size: 24px;
|
19
|
+
position: relative;
|
20
|
+
}
|
21
|
+
|
22
|
+
.header-title {
|
23
|
+
color: #b83f45;
|
24
|
+
font-size: 80px;
|
25
|
+
font-weight: 200;
|
26
|
+
position: absolute;
|
27
|
+
text-align: center;
|
28
|
+
-webkit-text-rendering: optimizeLegibility;
|
29
|
+
-moz-text-rendering: optimizeLegibility;
|
30
|
+
text-rendering: optimizeLegibility;
|
31
|
+
top: -100px;
|
32
|
+
width: 100%;
|
33
|
+
}
|
34
|
+
|
35
|
+
.filter-item {
|
36
|
+
display: inline-block;
|
37
|
+
margin: 0 5px;
|
38
|
+
}
|
39
|
+
|
40
|
+
.filter-link {
|
41
|
+
border: 1px solid transparent;
|
42
|
+
border-radius: 3px;
|
43
|
+
color: inherit;
|
44
|
+
margin: 3px;
|
45
|
+
padding: 3px 7px;
|
46
|
+
cursor: pointer;
|
47
|
+
}
|
48
|
+
|
49
|
+
.filter-link:hover {
|
50
|
+
border-color: #db7676;
|
51
|
+
}
|
52
|
+
|
53
|
+
.filter-link.selected {
|
54
|
+
border-color: #ce4646;
|
55
|
+
}
|
56
|
+
|
57
|
+
.checkbox-wrapper {
|
58
|
+
position: relative;
|
59
|
+
width: 40px;
|
60
|
+
height: 40px;
|
61
|
+
display: inline-block;
|
62
|
+
}
|
63
|
+
|
64
|
+
.checkbox {
|
65
|
+
width: 100%;
|
66
|
+
height: 100%;
|
67
|
+
margin: 10px 0 0 15px;
|
68
|
+
cursor: pointer;
|
69
|
+
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%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
|
70
|
+
}
|
71
|
+
|
72
|
+
.checkbox.checked {
|
73
|
+
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%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%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E");
|
74
|
+
}
|
75
|
+
|
76
|
+
.todo-text {
|
77
|
+
display: inline-block;
|
78
|
+
padding: 15px 15px 15px 60px;
|
79
|
+
line-height: 1.2;
|
80
|
+
transition: color .4s;
|
81
|
+
color: #484848;
|
82
|
+
}
|
83
|
+
|
84
|
+
.clear-completed.disabled {
|
85
|
+
opacity: 0.5;
|
86
|
+
cursor: not-allowed;
|
87
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { useReducer } from "react";
|
2
|
+
import { Header } from "./components/header";
|
3
|
+
import { Main } from "./components/main";
|
4
|
+
import { Footer } from "./components/footer";
|
5
|
+
|
6
|
+
import { todoReducer } from "./reducer";
|
7
|
+
|
8
|
+
import "./app.css";
|
9
|
+
|
10
|
+
export function App() {
|
11
|
+
const [todos, dispatch] = useReducer(todoReducer, []);
|
12
|
+
|
13
|
+
return (
|
14
|
+
<>
|
15
|
+
<Header dispatch={dispatch} />
|
16
|
+
<Main todos={todos} dispatch={dispatch} />
|
17
|
+
<Footer todos={todos} dispatch={dispatch} />
|
18
|
+
</>
|
19
|
+
);
|
20
|
+
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
2
|
+
import { useLocation } from "react-router-dom";
|
3
|
+
import classnames from "classnames";
|
4
|
+
|
5
|
+
import { REMOVE_COMPLETED_ITEMS } from "../constants";
|
6
|
+
|
7
|
+
export function Footer({ todos, dispatch }) {
|
8
|
+
const { pathname: route } = useLocation();
|
9
|
+
|
10
|
+
const activeTodos = useMemo(() => todos.filter((todo) => !todo.completed), [todos]);
|
11
|
+
|
12
|
+
const removeCompleted = useCallback(() => dispatch({ type: REMOVE_COMPLETED_ITEMS }), [dispatch]);
|
13
|
+
|
14
|
+
// prettier-ignore
|
15
|
+
if (todos.length === 0)
|
16
|
+
return null;
|
17
|
+
|
18
|
+
return (
|
19
|
+
<div className="footer" data-testid="footer">
|
20
|
+
<div className="todo-count">{`${activeTodos.length} ${activeTodos.length === 1 ? "item" : "items"} left!`}</div>
|
21
|
+
<div className="filters" data-testid="footer-navigation">
|
22
|
+
<div className="filter-item">
|
23
|
+
<div className={classnames("filter-link", { selected: route === "/" })} onClick={() => window.location.hash = "/"}>
|
24
|
+
All
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
<div className="filter-item">
|
28
|
+
<div className={classnames("filter-link", { selected: route === "/active" })} onClick={() => window.location.hash = "/active"}>
|
29
|
+
Active
|
30
|
+
</div>
|
31
|
+
</div>
|
32
|
+
<div className="filter-item">
|
33
|
+
<div className={classnames("filter-link", { selected: route === "/completed" })} onClick={() => window.location.hash = "/completed"}>
|
34
|
+
Completed
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
<div
|
39
|
+
className={classnames("clear-completed", { disabled: activeTodos.length === todos.length })}
|
40
|
+
onClick={activeTodos.length === todos.length ? null : removeCompleted}
|
41
|
+
>
|
42
|
+
Clear completed
|
43
|
+
</div>
|
44
|
+
</div>
|
45
|
+
);
|
46
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import { useCallback } from "react";
|
2
|
+
import { Input } from "./input";
|
3
|
+
|
4
|
+
import { ADD_ITEM } from "../constants";
|
5
|
+
|
6
|
+
export function Header({ dispatch }) {
|
7
|
+
const addItem = useCallback((title) => dispatch({ type: ADD_ITEM, payload: { title } }), [dispatch]);
|
8
|
+
|
9
|
+
return (
|
10
|
+
<div className="header" data-testid="header">
|
11
|
+
<div className="header-title">todos</div>
|
12
|
+
<Input onSubmit={addItem} label="New Todo Input" placeholder="What needs to be done?" />
|
13
|
+
</div>
|
14
|
+
);
|
15
|
+
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { useCallback } from "react";
|
2
|
+
|
3
|
+
const sanitize = (string) => {
|
4
|
+
const map = {
|
5
|
+
"&": "&",
|
6
|
+
"<": "<",
|
7
|
+
">": ">",
|
8
|
+
'"': """,
|
9
|
+
"'": "'",
|
10
|
+
"/": "/",
|
11
|
+
};
|
12
|
+
const reg = /[&<>"'/]/gi;
|
13
|
+
return string.replace(reg, (match) => map[match]);
|
14
|
+
};
|
15
|
+
|
16
|
+
const hasValidMin = (value, min) => {
|
17
|
+
return value.length >= min;
|
18
|
+
};
|
19
|
+
|
20
|
+
export function Input({ onSubmit, placeholder, label, defaultValue, onBlur }) {
|
21
|
+
const handleBlur = useCallback(() => {
|
22
|
+
if (onBlur)
|
23
|
+
onBlur();
|
24
|
+
}, [onBlur]);
|
25
|
+
|
26
|
+
const handleKeyDown = useCallback(
|
27
|
+
(e) => {
|
28
|
+
if (e.key === "Enter") {
|
29
|
+
const value = e.target.value.trim();
|
30
|
+
|
31
|
+
if (!hasValidMin(value, 2))
|
32
|
+
return;
|
33
|
+
|
34
|
+
onSubmit(sanitize(value));
|
35
|
+
e.target.value = "";
|
36
|
+
}
|
37
|
+
},
|
38
|
+
[onSubmit]
|
39
|
+
);
|
40
|
+
|
41
|
+
return (
|
42
|
+
<div className="input-container">
|
43
|
+
<input className="new-todo" id="todo-input" type="text" data-testid="text-input" autoFocus placeholder={placeholder} defaultValue={defaultValue} onBlur={handleBlur} onKeyDown={handleKeyDown} />
|
44
|
+
</div>
|
45
|
+
);
|
46
|
+
}
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import { memo, useState, useCallback } from "react";
|
2
|
+
import classnames from "classnames";
|
3
|
+
|
4
|
+
import { Input } from "./input";
|
5
|
+
|
6
|
+
import { TOGGLE_ITEM, REMOVE_ITEM, UPDATE_ITEM } from "../constants";
|
7
|
+
|
8
|
+
export const Item = memo(function Item({ todo, dispatch, index }) {
|
9
|
+
const [isWritable, setIsWritable] = useState(false);
|
10
|
+
const { title, completed, id } = todo;
|
11
|
+
|
12
|
+
const toggleItem = useCallback(() => dispatch({ type: TOGGLE_ITEM, payload: { id } }), [dispatch]);
|
13
|
+
const removeItem = useCallback(() => dispatch({ type: REMOVE_ITEM, payload: { id } }), [dispatch]);
|
14
|
+
const updateItem = useCallback((id, title) => dispatch({ type: UPDATE_ITEM, payload: { id, title } }), [dispatch]);
|
15
|
+
|
16
|
+
const handleDoubleClick = useCallback(() => {
|
17
|
+
setIsWritable(true);
|
18
|
+
}, []);
|
19
|
+
|
20
|
+
const handleBlur = useCallback(() => {
|
21
|
+
setIsWritable(false);
|
22
|
+
}, []);
|
23
|
+
|
24
|
+
const handleUpdate = useCallback(
|
25
|
+
(title) => {
|
26
|
+
if (title.length === 0)
|
27
|
+
removeItem(id);
|
28
|
+
else
|
29
|
+
updateItem(id, title);
|
30
|
+
|
31
|
+
setIsWritable(false);
|
32
|
+
},
|
33
|
+
[id, removeItem, updateItem]
|
34
|
+
);
|
35
|
+
|
36
|
+
return (
|
37
|
+
<div className={classnames("todo-item", { completed: todo.completed })} data-testid="todo-item">
|
38
|
+
<div className="view">
|
39
|
+
{isWritable ? (
|
40
|
+
<Input onSubmit={handleUpdate} label="Edit Todo Input" defaultValue={title} onBlur={handleBlur} />
|
41
|
+
) : (
|
42
|
+
<>
|
43
|
+
<div className="toggle checkbox-wrapper" data-testid="todo-item-toggle">
|
44
|
+
<div className={classnames("checkbox", { checked: completed })} onClick={toggleItem}></div>
|
45
|
+
</div>
|
46
|
+
<div className="todo-text" data-testid="todo-item-label" onDoubleClick={handleDoubleClick}>
|
47
|
+
{title}
|
48
|
+
</div>
|
49
|
+
<div className="destroy" data-testid="todo-item-button" onClick={removeItem} role="button"></div>
|
50
|
+
</>
|
51
|
+
)}
|
52
|
+
</div>
|
53
|
+
</div>
|
54
|
+
);
|
55
|
+
});
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import { useMemo, useCallback } from "react";
|
2
|
+
import { useLocation } from "react-router-dom";
|
3
|
+
|
4
|
+
import { Item } from "./item";
|
5
|
+
import classnames from "classnames";
|
6
|
+
|
7
|
+
import { TOGGLE_ALL } from "../constants";
|
8
|
+
|
9
|
+
export function Main({ todos, dispatch }) {
|
10
|
+
const { pathname: route } = useLocation();
|
11
|
+
|
12
|
+
const visibleTodos = useMemo(
|
13
|
+
() =>
|
14
|
+
todos.filter((todo) => {
|
15
|
+
if (route === "/active")
|
16
|
+
return !todo.completed;
|
17
|
+
|
18
|
+
if (route === "/completed")
|
19
|
+
return todo.completed;
|
20
|
+
|
21
|
+
return todo;
|
22
|
+
}),
|
23
|
+
[todos, route]
|
24
|
+
);
|
25
|
+
|
26
|
+
const toggleAll = useCallback((e) => dispatch({ type: TOGGLE_ALL, payload: { completed: e.target.checked } }), [dispatch]);
|
27
|
+
|
28
|
+
return (
|
29
|
+
<main className="main" data-testid="main">
|
30
|
+
{visibleTodos.length > 0 ? (
|
31
|
+
<div className="toggle-all-container">
|
32
|
+
<input className="toggle-all" type="checkbox" id="toggle-all" data-testid="toggle-all" checked={visibleTodos.every((todo) => todo.completed)} onChange={toggleAll} />
|
33
|
+
<label className="toggle-all-label" htmlFor="toggle-all">
|
34
|
+
Toggle All Input
|
35
|
+
</label>
|
36
|
+
</div>
|
37
|
+
) : null}
|
38
|
+
<div className={classnames("todo-list")} data-testid="todo-list">
|
39
|
+
{visibleTodos.map((todo, index) => (
|
40
|
+
<Item todo={todo} key={todo.id} dispatch={dispatch} index={index} />
|
41
|
+
))}
|
42
|
+
</div>
|
43
|
+
</main>
|
44
|
+
);
|
45
|
+
}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
export const ADD_ITEM = "ADD_ITEM";
|
2
|
+
export const UPDATE_ITEM = "UPDATE_ITEM";
|
3
|
+
export const REMOVE_ITEM = "REMOVE_ITEM";
|
4
|
+
export const TOGGLE_ITEM = "TOGGLE_ITEM";
|
5
|
+
export const REMOVE_ALL_ITEMS = "REMOVE_ALL_ITEMS";
|
6
|
+
export const TOGGLE_ALL = "TOGGLE_ALL";
|
7
|
+
export const REMOVE_COMPLETED_ITEMS = "REMOVE_COMPLETED_ITEMS";
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import { ADD_ITEM, UPDATE_ITEM, REMOVE_ITEM, TOGGLE_ITEM, REMOVE_ALL_ITEMS, TOGGLE_ALL, REMOVE_COMPLETED_ITEMS } from "./constants";
|
2
|
+
|
3
|
+
/* Borrowed from https://github.com/ai/nanoid/blob/3.0.2/non-secure/index.js
|
4
|
+
|
5
|
+
The MIT License (MIT)
|
6
|
+
|
7
|
+
Copyright 2017 Andrey Sitnik <andrey@sitnik.ru>
|
8
|
+
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
10
|
+
this software and associated documentation files (the "Software"), to deal in
|
11
|
+
the Software without restriction, including without limitation the rights to
|
12
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
13
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
14
|
+
subject to the following conditions:
|
15
|
+
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
17
|
+
copies or substantial portions of the Software.
|
18
|
+
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
21
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
22
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
23
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
24
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
25
|
+
|
26
|
+
// This alphabet uses `A-Za-z0-9_-` symbols.
|
27
|
+
// The order of characters is optimized for better gzip and brotli compression.
|
28
|
+
// References to the same file (works both for gzip and brotli):
|
29
|
+
// `'use`, `andom`, and `rict'`
|
30
|
+
// References to the brotli default dictionary:
|
31
|
+
// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf`
|
32
|
+
let urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
33
|
+
|
34
|
+
function nanoid(size = 21) {
|
35
|
+
let id = "";
|
36
|
+
// A compact alternative for `for (var i = 0; i < step; i++)`.
|
37
|
+
let i = size;
|
38
|
+
while (i--) {
|
39
|
+
// `| 0` is more compact and faster than `Math.floor()`.
|
40
|
+
id += urlAlphabet[(Math.random() * 64) | 0];
|
41
|
+
}
|
42
|
+
return id;
|
43
|
+
}
|
44
|
+
|
45
|
+
export const todoReducer = (state, action) => {
|
46
|
+
switch (action.type) {
|
47
|
+
case ADD_ITEM:
|
48
|
+
return state.concat({ id: nanoid(), title: action.payload.title, completed: false });
|
49
|
+
case UPDATE_ITEM:
|
50
|
+
return state.map((todo) => (todo.id === action.payload.id ? { ...todo, title: action.payload.title } : todo));
|
51
|
+
case REMOVE_ITEM:
|
52
|
+
return state.filter((todo) => todo.id !== action.payload.id);
|
53
|
+
case TOGGLE_ITEM:
|
54
|
+
return state.map((todo) => (todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo));
|
55
|
+
case REMOVE_ALL_ITEMS:
|
56
|
+
return [];
|
57
|
+
case TOGGLE_ALL:
|
58
|
+
return state.map((todo) => (todo.completed !== action.payload.completed ? { ...todo, completed: action.payload.completed } : todo));
|
59
|
+
case REMOVE_COMPLETED_ITEMS:
|
60
|
+
return state.filter((todo) => !todo.completed);
|
61
|
+
}
|
62
|
+
|
63
|
+
throw Error(`Unknown action: ${action.type}`);
|
64
|
+
};
|
@@ -0,0 +1,43 @@
|
|
1
|
+
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
2
|
+
const path = require("path");
|
3
|
+
|
4
|
+
module.exports = {
|
5
|
+
entry: {
|
6
|
+
app: path.resolve(__dirname, "src", "index.js"),
|
7
|
+
},
|
8
|
+
plugins: [
|
9
|
+
new HtmlWebpackPlugin({
|
10
|
+
title: "TodoMVC: React",
|
11
|
+
template: path.resolve(__dirname, "public", "index.html"),
|
12
|
+
}),
|
13
|
+
],
|
14
|
+
output: {
|
15
|
+
filename: "[name].bundle.js",
|
16
|
+
path: path.resolve(__dirname, "dist"),
|
17
|
+
clean: true,
|
18
|
+
},
|
19
|
+
resolve: {
|
20
|
+
extensions: [".js", ".jsx"],
|
21
|
+
},
|
22
|
+
module: {
|
23
|
+
rules: [
|
24
|
+
{
|
25
|
+
test: /\.(js|jsx)$/,
|
26
|
+
exclude: /node_modules/,
|
27
|
+
use: {
|
28
|
+
loader: "babel-loader",
|
29
|
+
options: {
|
30
|
+
presets: [
|
31
|
+
["@babel/preset-env", { targets: "defaults" }],
|
32
|
+
["@babel/preset-react", { runtime: "automatic" }],
|
33
|
+
],
|
34
|
+
},
|
35
|
+
},
|
36
|
+
},
|
37
|
+
{
|
38
|
+
test: /\.(png|svg|jpg|jpeg|gif)$/i,
|
39
|
+
type: "asset/resource",
|
40
|
+
},
|
41
|
+
],
|
42
|
+
},
|
43
|
+
};
|
@@ -0,0 +1,18 @@
|
|
1
|
+
const { merge } = require("webpack-merge");
|
2
|
+
const common = require("./webpack.common.js");
|
3
|
+
|
4
|
+
module.exports = merge(common, {
|
5
|
+
mode: "development",
|
6
|
+
devtool: "inline-source-map",
|
7
|
+
devServer: {
|
8
|
+
static: "./dist",
|
9
|
+
},
|
10
|
+
module: {
|
11
|
+
rules: [
|
12
|
+
{
|
13
|
+
test: /\.css$/i,
|
14
|
+
use: ["style-loader", "css-loader"],
|
15
|
+
},
|
16
|
+
],
|
17
|
+
},
|
18
|
+
});
|
@@ -0,0 +1,35 @@
|
|
1
|
+
const { merge } = require("webpack-merge");
|
2
|
+
const common = require("./webpack.common.js");
|
3
|
+
|
4
|
+
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
5
|
+
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
|
6
|
+
const TerserPlugin = require("terser-webpack-plugin");
|
7
|
+
const CopyPlugin = require("copy-webpack-plugin");
|
8
|
+
|
9
|
+
module.exports = merge(common, {
|
10
|
+
mode: "production",
|
11
|
+
devtool: "source-map",
|
12
|
+
plugins: [
|
13
|
+
new MiniCssExtractPlugin({
|
14
|
+
filename: "[name].css",
|
15
|
+
chunkFilename: "[id].css",
|
16
|
+
}),
|
17
|
+
new CopyPlugin({
|
18
|
+
patterns: [
|
19
|
+
{ from: "./node_modules/todomvc-common/base.js", to: "base.js" },
|
20
|
+
],
|
21
|
+
}),
|
22
|
+
],
|
23
|
+
module: {
|
24
|
+
rules: [
|
25
|
+
{
|
26
|
+
test: /\.css$/,
|
27
|
+
use: [MiniCssExtractPlugin.loader, "css-loader"],
|
28
|
+
},
|
29
|
+
],
|
30
|
+
},
|
31
|
+
optimization: {
|
32
|
+
minimize: true,
|
33
|
+
minimizer: [new CssMinimizerPlugin(), new TerserPlugin()],
|
34
|
+
},
|
35
|
+
});
|