├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.js
├── assets
│ ├── db.json
│ └── img
│ │ ├── add.svg
│ │ ├── check.svg
│ │ ├── close.svg
│ │ ├── edit.svg
│ │ ├── list.svg
│ │ └── remove.svg
├── components
│ ├── AddList
│ │ ├── AddList.scss
│ │ └── index.jsx
│ ├── Badge
│ │ ├── Badge.scss
│ │ └── index.jsx
│ ├── List
│ │ ├── List.scss
│ │ └── index.jsx
│ ├── Tasks
│ │ ├── AddTaskForm.jsx
│ │ ├── Task.jsx
│ │ ├── Tasks.scss
│ │ └── index.jsx
│ └── index.js
├── index.js
└── index.scss
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | debug.log
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Список задач (ToDo) по курсу [Разработка списка задача на ReactJS для начинающих](https://www.youtube.com/watch?v=PGZ6HtgSeio&list=PL0FGkDGJQjJGBcY_b625HqAKL4i5iNZGs)
2 |
3 | **Stack:**
4 |
5 | - ReactJS (useState, useReducer, useEffect)
6 | - React Router
7 | - Axios
8 | - classnames
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todo-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.19.0",
7 | "classnames": "^2.2.6",
8 | "json-server": "^0.15.1",
9 | "node-sass": "^4.13.0",
10 | "react": "^16.11.0",
11 | "react-dom": "^16.11.0",
12 | "react-router-dom": "^5.1.2",
13 | "react-scripts": "3.2.0"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject",
20 | "fake-json": "json-server ./src/assets/db.json --watch --port 3001"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Archakov06/react-todo/fd4f9c0e8728a922fd82327c6b412c7a049e0521/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
31 |
35 | React App
36 |
37 |
38 |
39 |
40 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Archakov06/react-todo/fd4f9c0e8728a922fd82327c6b412c7a049e0521/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Archakov06/react-todo/fd4f9c0e8728a922fd82327c6b412c7a049e0521/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
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 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import axios from 'axios';
3 | import { Route, useHistory } from 'react-router-dom';
4 |
5 | import { List, AddList, Tasks } from './components';
6 |
7 | function App() {
8 | const [lists, setLists] = useState(null);
9 | const [colors, setColors] = useState(null);
10 | const [activeItem, setActiveItem] = useState(null);
11 | let history = useHistory();
12 |
13 | useEffect(() => {
14 | axios
15 | .get('http://localhost:3001/lists?_expand=color&_embed=tasks')
16 | .then(({ data }) => {
17 | setLists(data);
18 | });
19 | axios.get('http://localhost:3001/colors').then(({ data }) => {
20 | setColors(data);
21 | });
22 | }, []);
23 |
24 | const onAddList = obj => {
25 | const newList = [...lists, obj];
26 | setLists(newList);
27 | };
28 |
29 | const onAddTask = (listId, taskObj) => {
30 | const newList = lists.map(item => {
31 | if (item.id === listId) {
32 | item.tasks = [...item.tasks, taskObj];
33 | }
34 | return item;
35 | });
36 | setLists(newList);
37 | };
38 |
39 | const onEditTask = (listId, taskObj) => {
40 | const newTaskText = window.prompt('Текст задачи', taskObj.text);
41 |
42 | if (!newTaskText) {
43 | return;
44 | }
45 |
46 | const newList = lists.map(list => {
47 | if (list.id === listId) {
48 | list.tasks = list.tasks.map(task => {
49 | if (task.id === taskObj.id) {
50 | task.text = newTaskText;
51 | }
52 | return task;
53 | });
54 | }
55 | return list;
56 | });
57 | setLists(newList);
58 | axios
59 | .patch('http://localhost:3001/tasks/' + taskObj.id, {
60 | text: newTaskText
61 | })
62 | .catch(() => {
63 | alert('Не удалось обновить задачу');
64 | });
65 | };
66 |
67 | const onRemoveTask = (listId, taskId) => {
68 | if (window.confirm('Вы действительно хотите удалить задачу?')) {
69 | const newList = lists.map(item => {
70 | if (item.id === listId) {
71 | item.tasks = item.tasks.filter(task => task.id !== taskId);
72 | }
73 | return item;
74 | });
75 | setLists(newList);
76 | axios.delete('http://localhost:3001/tasks/' + taskId).catch(() => {
77 | alert('Не удалось удалить задачу');
78 | });
79 | }
80 | };
81 |
82 | const onCompleteTask = (listId, taskId, completed) => {
83 | const newList = lists.map(list => {
84 | if (list.id === listId) {
85 | list.tasks = list.tasks.map(task => {
86 | if (task.id === taskId) {
87 | task.completed = completed;
88 | }
89 | return task;
90 | });
91 | }
92 | return list;
93 | });
94 | setLists(newList);
95 | axios
96 | .patch('http://localhost:3001/tasks/' + taskId, {
97 | completed
98 | })
99 | .catch(() => {
100 | alert('Не удалось обновить задачу');
101 | });
102 | };
103 |
104 | const onEditListTitle = (id, title) => {
105 | const newList = lists.map(item => {
106 | if (item.id === id) {
107 | item.name = title;
108 | }
109 | return item;
110 | });
111 | setLists(newList);
112 | };
113 |
114 | useEffect(() => {
115 | const listId = history.location.pathname.split('lists/')[1];
116 | if (lists) {
117 | const list = lists.find(list => list.id === Number(listId));
118 | setActiveItem(list);
119 | }
120 | }, [lists, history.location.pathname]);
121 |
122 | return (
123 |
124 |
125 |
{
127 | history.push(`/`);
128 | }}
129 | items={[
130 | {
131 | active: history.location.pathname === '/',
132 | icon: (
133 |
145 | ),
146 | name: 'Все задачи'
147 | }
148 | ]}
149 | />
150 | {lists ? (
151 | {
154 | const newLists = lists.filter(item => item.id !== id);
155 | setLists(newLists);
156 | }}
157 | onClickItem={list => {
158 | history.push(`/lists/${list.id}`);
159 | }}
160 | activeItem={activeItem}
161 | isRemovable
162 | />
163 | ) : (
164 | 'Загрузка...'
165 | )}
166 |
167 |
168 |
169 |
170 | {lists &&
171 | lists.map(list => (
172 |
182 | ))}
183 |
184 |
185 | {lists && activeItem && (
186 |
194 | )}
195 |
196 |
197 |
198 | );
199 | }
200 |
201 | export default App;
202 |
--------------------------------------------------------------------------------
/src/assets/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "lists": [
3 | {
4 | "id": 1,
5 | "name": "Продажи",
6 | "colorId": 5
7 | },
8 | {
9 | "id": 2,
10 | "name": "Фронтенд",
11 | "colorId": 4
12 | },
13 | {
14 | "id": 3,
15 | "name": "Фильмы и сериалы",
16 | "colorId": 3
17 | },
18 | {
19 | "id": 4,
20 | "name": "Книги",
21 | "colorId": 2
22 | },
23 | {
24 | "id": 5,
25 | "name": "Личное",
26 | "colorId": 1
27 | },
28 | {
29 | "name": "Спорт",
30 | "colorId": 3,
31 | "id": 6
32 | },
33 | {
34 | "name": "Курс по ReactJS ToDo",
35 | "colorId": 7,
36 | "id": 7
37 | }
38 | ],
39 | "tasks": [
40 | {
41 | "id": 1,
42 | "listId": 2,
43 | "text": "Изучить JavaScript",
44 | "completed": true
45 | },
46 | {
47 | "id": 2,
48 | "listId": 2,
49 | "text": "Изучить паттерны проектирования",
50 | "completed": false
51 | },
52 | {
53 | "id": 3,
54 | "listId": 2,
55 | "text": "ReactJS Hooks (useState, useReducer, useEffect и т.д.)",
56 | "completed": true
57 | },
58 | {
59 | "id": 4,
60 | "listId": 2,
61 | "text": "Redux (redux-observable, redux-saga)",
62 | "completed": false
63 | },
64 | {
65 | "listId": 2,
66 | "text": "123",
67 | "completed": true,
68 | "id": 5
69 | },
70 | {
71 | "listId": 1,
72 | "text": "test",
73 | "completed": false,
74 | "id": 6
75 | },
76 | {
77 | "listId": 1,
78 | "text": "qweqwe",
79 | "completed": false,
80 | "id": 7
81 | },
82 | {
83 | "listId": 1,
84 | "text": "qweqwe",
85 | "completed": true,
86 | "id": 8
87 | },
88 | {
89 | "listId": 1,
90 | "text": "123",
91 | "completed": false,
92 | "id": 9
93 | },
94 | {
95 | "listId": 4,
96 | "text": "Купить 1984!",
97 | "completed": true,
98 | "id": 10
99 | },
100 | {
101 | "listId": 2,
102 | "text": "222",
103 | "completed": true,
104 | "id": 12
105 | },
106 | {
107 | "listId": 7,
108 | "text": "Сделали сайдбар",
109 | "completed": true,
110 | "id": 15
111 | },
112 | {
113 | "listId": 7,
114 | "text": "Сделали список задач",
115 | "completed": true,
116 | "id": 16
117 | },
118 | {
119 | "listId": 7,
120 | "text": "Сделали удаление и редактирование задач и списков",
121 | "completed": true,
122 | "id": 17
123 | },
124 | {
125 | "listId": 8,
126 | "text": "tttt",
127 | "completed": false,
128 | "id": 18
129 | }
130 | ],
131 | "colors": [
132 | {
133 | "id": 1,
134 | "hex": "#C9D1D3",
135 | "name": "grey"
136 | },
137 | {
138 | "id": 2,
139 | "hex": "#42B883",
140 | "name": "green"
141 | },
142 | {
143 | "id": 3,
144 | "hex": "#64C4ED",
145 | "name": "blue"
146 | },
147 | {
148 | "id": 4,
149 | "hex": "#FFBBCC",
150 | "name": "pink"
151 | },
152 | {
153 | "id": 5,
154 | "hex": "#B6E6BD",
155 | "name": "lime"
156 | },
157 | {
158 | "id": 6,
159 | "hex": "#C355F5",
160 | "name": "purple"
161 | },
162 | {
163 | "id": 7,
164 | "hex": "#110133",
165 | "name": "black"
166 | },
167 | {
168 | "id": 8,
169 | "hex": "#FF6464",
170 | "name": "red"
171 | }
172 | ]
173 | }
174 |
--------------------------------------------------------------------------------
/src/assets/img/add.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/img/check.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/img/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/img/edit.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/img/list.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/img/remove.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/AddList/AddList.scss:
--------------------------------------------------------------------------------
1 | .add-list {
2 | &__popup {
3 | height: 150px;
4 | width: 240px;
5 | background: #ffffff;
6 | box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.05);
7 | border-radius: 10px;
8 | padding: 18px;
9 | box-sizing: border-box;
10 | position: relative;
11 | top: -25px;
12 | left: 10px;
13 |
14 | .button {
15 | width: 100%;
16 | margin-top: 12px;
17 | }
18 |
19 | &-close-btn {
20 | position: absolute;
21 | right: -8px;
22 | top: -8px;
23 | cursor: pointer;
24 | }
25 |
26 | &-colors {
27 | display: flex;
28 | justify-content: space-between;
29 | margin-top: 12px;
30 |
31 | .badge {
32 | width: 16px;
33 | height: 16px;
34 | cursor: pointer;
35 | border: 2px solid transparent;
36 |
37 | &.active {
38 | border: 2px solid #525252;
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/AddList/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import axios from 'axios';
3 |
4 | import List from '../List';
5 | import Badge from '../Badge';
6 |
7 | import closeSvg from '../../assets/img/close.svg';
8 |
9 | import './AddList.scss';
10 |
11 | const AddList = ({ colors, onAdd }) => {
12 | const [visiblePopup, setVisiblePopup] = useState(false);
13 | const [seletedColor, selectColor] = useState(3);
14 | const [isLoading, setIsLoading] = useState(false);
15 | const [inputValue, setInputValue] = useState('');
16 |
17 | useEffect(() => {
18 | if (Array.isArray(colors)) {
19 | selectColor(colors[0].id);
20 | }
21 | }, [colors]);
22 |
23 | const onClose = () => {
24 | setVisiblePopup(false);
25 | setInputValue('');
26 | selectColor(colors[0].id);
27 | };
28 |
29 | const addList = () => {
30 | if (!inputValue) {
31 | alert('Введите название списка');
32 | return;
33 | }
34 | setIsLoading(true);
35 | axios
36 | .post('http://localhost:3001/lists', {
37 | name: inputValue,
38 | colorId: seletedColor
39 | })
40 | .then(({ data }) => {
41 | const color = colors.filter(c => c.id === seletedColor)[0];
42 | const listObj = { ...data, color, tasks: [] };
43 | onAdd(listObj);
44 | onClose();
45 | })
46 | .catch(() => {
47 | alert('Ошибка при добавлении списка!');
48 | })
49 | .finally(() => {
50 | setIsLoading(false);
51 | });
52 | };
53 |
54 | return (
55 |
56 |
setVisiblePopup(true)}
58 | items={[
59 | {
60 | className: 'list__add-button',
61 | icon: (
62 |
84 | ),
85 | name: 'Добавить список'
86 | }
87 | ]}
88 | />
89 | {visiblePopup && (
90 |
91 |

97 |
98 |
setInputValue(e.target.value)}
101 | className="field"
102 | type="text"
103 | placeholder="Название списка"
104 | />
105 |
106 |
107 | {colors.map(color => (
108 | selectColor(color.id)}
110 | key={color.id}
111 | color={color.name}
112 | className={seletedColor === color.id && 'active'}
113 | />
114 | ))}
115 |
116 |
119 |
120 | )}
121 |
122 | );
123 | };
124 |
125 | export default AddList;
126 |
--------------------------------------------------------------------------------
/src/components/Badge/Badge.scss:
--------------------------------------------------------------------------------
1 | .badge {
2 | display: inline-block;
3 | width: 10px;
4 | height: 10px;
5 | border-radius: 30px;
6 |
7 | &--grey {
8 | background-color: #c9d1d3;
9 | }
10 |
11 | &--lime {
12 | background-color: #b6e6bd;
13 | }
14 |
15 | &--purple {
16 | background-color: #c355f5;
17 | }
18 |
19 | &--black {
20 | background-color: #08001a;
21 | }
22 |
23 | &--red {
24 | background-color: #ff6464;
25 | }
26 |
27 | &--green {
28 | background-color: #42b883;
29 | }
30 |
31 | &--blue {
32 | background-color: #64c4ed;
33 | }
34 |
35 | &--pink {
36 | background-color: #ffbbcc;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Badge/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | import './Badge.scss';
5 |
6 | const Badge = ({ color, onClick, className }) => (
7 |
11 | );
12 |
13 | export default Badge;
14 |
--------------------------------------------------------------------------------
/src/components/List/List.scss:
--------------------------------------------------------------------------------
1 | .list {
2 | $self: &;
3 |
4 | margin-bottom: 30px;
5 |
6 | &__add-button {
7 | opacity: 0.4;
8 |
9 | &:hover {
10 | opacity: 1;
11 | }
12 | }
13 |
14 | li {
15 | display: flex;
16 | align-items: center;
17 | cursor: pointer;
18 | padding: 10px 12px;
19 | transition: background-color 0.15s ease-in-out;
20 |
21 | &:hover #{$self}__remove-icon {
22 | opacity: 0.2;
23 | }
24 |
25 | #{$self}__remove-icon {
26 | opacity: 0;
27 | transition: opacity 0.15s ease-in-out;
28 |
29 | &:hover {
30 | opacity: 0.8;
31 | }
32 | }
33 |
34 | span {
35 | flex: 1;
36 | text-overflow: ellipsis;
37 | overflow: hidden;
38 | width: 160px;
39 | white-space: nowrap;
40 | }
41 |
42 | &:hover {
43 | background: rgba(255, 255, 255, 0.5);
44 | }
45 |
46 | &.active {
47 | background: #ffffff;
48 | box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.04);
49 | border-radius: 4px;
50 | }
51 |
52 | .badge {
53 | position: relative;
54 | left: 5px;
55 | }
56 |
57 | i {
58 | display: inline-flex;
59 | margin-right: 8px;
60 | svg {
61 | path {
62 | fill: #7c7c7c;
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/List/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import axios from 'axios';
4 |
5 | import removeSvg from '../../assets/img/remove.svg';
6 |
7 | import Badge from '../Badge';
8 |
9 | import './List.scss';
10 |
11 | const List = ({
12 | items,
13 | isRemovable,
14 | onClick,
15 | onRemove,
16 | onClickItem,
17 | activeItem
18 | }) => {
19 | const removeList = item => {
20 | if (window.confirm('Вы действительно хотите удалить список?')) {
21 | axios.delete('http://localhost:3001/lists/' + item.id).then(() => {
22 | onRemove(item.id);
23 | });
24 | }
25 | };
26 |
27 | return (
28 |
29 | {items.map((item, index) => (
30 | - onClickItem(item) : null}
38 | >
39 | {item.icon ? item.icon : }
40 |
41 | {item.name}
42 | {item.tasks && ` (${item.tasks.length})`}
43 |
44 | {isRemovable && (
45 |
removeList(item)}
50 | />
51 | )}
52 |
53 | ))}
54 |
55 | );
56 | };
57 |
58 | export default List;
59 |
--------------------------------------------------------------------------------
/src/components/Tasks/AddTaskForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import axios from 'axios';
3 |
4 | import addSvg from '../../assets/img/add.svg';
5 |
6 | const AddTaskForm = ({ list, onAddTask }) => {
7 | const [visibleForm, setFormVisible] = useState(false);
8 | const [inputValue, setInputValue] = useState('');
9 | const [isLoading, setIsLoading] = useState('');
10 |
11 | const toggleFormVisible = () => {
12 | setFormVisible(!visibleForm);
13 | setInputValue('');
14 | };
15 |
16 | const addTask = () => {
17 | const obj = {
18 | listId: list.id,
19 | text: inputValue,
20 | completed: false
21 | };
22 | setIsLoading(true);
23 | axios
24 | .post('http://localhost:3001/tasks', obj)
25 | .then(({ data }) => {
26 | onAddTask(list.id, data);
27 | toggleFormVisible();
28 | })
29 | .catch(e => {
30 | alert('Ошибка при добавлении задачи!');
31 | })
32 | .finally(() => {
33 | setIsLoading(false);
34 | });
35 | };
36 |
37 | return (
38 |
39 | {!visibleForm ? (
40 |
41 |

42 |
Новая задача
43 |
44 | ) : (
45 |
46 | setInputValue(e.target.value)}
52 | />
53 |
56 |
59 |
60 | )}
61 |
62 | );
63 | };
64 |
65 | export default AddTaskForm;
66 |
--------------------------------------------------------------------------------
/src/components/Tasks/Task.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Task = ({ id, text, completed, list, onRemove, onEdit, onComplete }) => {
4 | const onChangeCheckbox = e => {
5 | onComplete(list.id, id, e.target.checked);
6 | };
7 |
8 | return (
9 |
10 |
11 |
17 |
34 |
35 |
{text}
36 |
37 |
onEdit(list.id, { id, text })}>
38 |
50 |
51 |
onRemove(list.id, id)}>
52 |
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default Task;
71 |
--------------------------------------------------------------------------------
/src/components/Tasks/Tasks.scss:
--------------------------------------------------------------------------------
1 | .tasks {
2 | &:not(:last-of-type) {
3 | margin-bottom: 40px;
4 | }
5 |
6 | a {
7 | text-decoration: none;
8 | }
9 |
10 | &__title {
11 | font-family: Montserrat;
12 | font-weight: bold;
13 | font-size: 32px;
14 | line-height: 39px;
15 | color: #64c4ed;
16 | padding-bottom: 20px;
17 | border-bottom: 1px solid #f2f2f2;
18 |
19 | &:hover {
20 | img {
21 | opacity: 0.2;
22 | }
23 | }
24 |
25 | img {
26 | opacity: 0;
27 | cursor: pointer;
28 | margin-left: 15px;
29 |
30 | &:hover {
31 | opacity: 0.8;
32 | }
33 | }
34 | }
35 |
36 | &__form {
37 | margin-top: 20px;
38 |
39 | &-block {
40 | .button {
41 | margin-right: 10px;
42 | margin-top: 15px;
43 | }
44 | }
45 |
46 | &-new {
47 | display: flex;
48 | align-items: center;
49 | cursor: pointer;
50 | opacity: 0.4;
51 | transition: opacity 0.15s ease-in-out;
52 |
53 | &:hover {
54 | opacity: 0.8;
55 | }
56 |
57 | img {
58 | width: 16px;
59 | height: 16px;
60 | margin-left: 4px;
61 | }
62 |
63 | span {
64 | font-size: 16px;
65 | margin-left: 19px;
66 | }
67 | }
68 | }
69 |
70 | &__items {
71 | margin-top: 30px;
72 |
73 | h2 {
74 | font-family: Montserrat;
75 | font-weight: bold;
76 | font-size: 22px;
77 | color: #c9d1d3;
78 | position: absolute;
79 | left: 50%;
80 | top: 50%;
81 | }
82 |
83 | &-row {
84 | display: flex;
85 | align-items: center;
86 | margin-bottom: 15px;
87 |
88 | &:hover &-actions {
89 | opacity: 1;
90 | }
91 |
92 | &-actions {
93 | display: flex;
94 | opacity: 0;
95 | transition: opacity 0.15s ease-in-out;
96 |
97 | div {
98 | display: flex;
99 | align-items: center;
100 | justify-content: center;
101 | width: 32px;
102 | height: 32px;
103 | background-color: lighten(#f4f6f8, 2%);
104 | border-radius: 6px;
105 | margin-left: 5px;
106 | cursor: pointer;
107 |
108 | &:hover {
109 | background-color: darken(#f4f6f8, 2%);
110 | svg {
111 | opacity: 0.9;
112 | }
113 | }
114 |
115 | svg {
116 | width: 11px;
117 | height: 11px;
118 | opacity: 0.4;
119 | }
120 | }
121 | }
122 |
123 | p {
124 | margin-left: 15px;
125 | border: 0;
126 | font-size: 16px;
127 | width: 100%;
128 | }
129 | }
130 | }
131 |
132 | .checkbox {
133 | input {
134 | display: none;
135 | }
136 |
137 | svg {
138 | transition: opacity 0.15s ease-in-out;
139 | path {
140 | stroke: #f2f2f2;
141 | }
142 | }
143 |
144 | input:checked + label {
145 | background-color: #4dd599;
146 | border-color: #4dd599;
147 |
148 | svg {
149 | opacity: 1;
150 | path {
151 | stroke: #fff;
152 | }
153 | }
154 | }
155 |
156 | &:hover {
157 | label {
158 | background-color: #f2f2f2;
159 | border-color: #f2f2f2;
160 | svg {
161 | opacity: 1;
162 | path {
163 | stroke: #b2b2b2;
164 | }
165 | }
166 | }
167 | }
168 |
169 | label {
170 | display: flex;
171 | align-items: center;
172 | justify-content: center;
173 | border: 2px solid #e8e8e8;
174 | border-radius: 30px;
175 | width: 20px;
176 | height: 20px;
177 | cursor: pointer;
178 | transition: background-color 0.15s ease-in-out,
179 | border-color 0.15s ease-in-out;
180 |
181 | svg {
182 | opacity: 0;
183 | }
184 | }
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/src/components/Tasks/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import axios from 'axios';
3 | import { Link } from 'react-router-dom';
4 |
5 | import editSvg from '../../assets/img/edit.svg';
6 |
7 | import './Tasks.scss';
8 |
9 | import AddTaskForm from './AddTaskForm';
10 | import Task from './Task';
11 |
12 | const Tasks = ({
13 | list,
14 | onEditTitle,
15 | onAddTask,
16 | onRemoveTask,
17 | onEditTask,
18 | onCompleteTask,
19 | withoutEmpty
20 | }) => {
21 | const editTitle = () => {
22 | const newTitle = window.prompt('Название списка', list.name);
23 | if (newTitle) {
24 | onEditTitle(list.id, newTitle);
25 | axios
26 | .patch('http://localhost:3001/lists/' + list.id, {
27 | name: newTitle
28 | })
29 | .catch(() => {
30 | alert('Не удалось обновить название списка');
31 | });
32 | }
33 | };
34 |
35 | return (
36 |
37 |
38 |
39 | {list.name}
40 |
41 |
42 |
43 |
44 |
45 | {!withoutEmpty && list.tasks && !list.tasks.length && (
46 |
Задачи отсутствуют
47 | )}
48 | {list.tasks &&
49 | list.tasks.map(task => (
50 |
58 | ))}
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default Tasks;
66 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as AddList } from './AddList';
2 | export { default as Badge } from './Badge';
3 | export { default as List } from './List';
4 | export { default as Tasks } from './Tasks';
5 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter as Router } from 'react-router-dom';
4 |
5 | import App from './App';
6 |
7 | import './index.scss';
8 |
9 | ReactDOM.render(
10 |
11 |
12 | ,
13 | document.getElementById('root')
14 | );
15 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | list-style: none;
5 | outline: none;
6 | font-family: 'Roboto', -apple-system, system-ui, sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | .todo {
12 | display: flex;
13 | position: absolute;
14 | left: 50%;
15 | top: 50%;
16 | width: calc(100vw - 30px);
17 | height: calc(100vh - 100px);
18 | transform: translate(-50%, -50%);
19 | box-shadow: 1px 2px 20px #f3f3f3;
20 | border-radius: 10px;
21 | border: 1px solid #f1f1f1;
22 | font-size: 14px;
23 |
24 | &__sidebar {
25 | background-color: #f4f6f8;
26 | width: 200px;
27 | height: calc(100% - 120px);
28 | border-right: 1px solid #f1f1f1;
29 | padding: 60px 20px;
30 | }
31 |
32 | &__tasks {
33 | flex: 1;
34 | padding: 60px;
35 | overflow: auto;
36 | }
37 | }
38 |
39 | .button {
40 | background: #4dd599;
41 | border-radius: 4px;
42 | color: #fff;
43 | border: 0;
44 | padding: 10px 20px;
45 | cursor: pointer;
46 | font-size: 14px;
47 |
48 | &:hover {
49 | background: darken(#4dd599, 6%);
50 | }
51 |
52 | &:active {
53 | background: darken(#4dd599, 10%);
54 | }
55 |
56 | &:disabled {
57 | background: #d8d8d8;
58 | }
59 |
60 | &--grey {
61 | background: #f4f6f8;
62 | color: #444444;
63 |
64 | &:hover {
65 | background: darken(#f4f6f8, 3%);
66 | }
67 |
68 | &:active {
69 | background: darken(#f4f6f8, 6%);
70 | }
71 | }
72 | }
73 |
74 | .field {
75 | background: #ffffff;
76 | border: 1px solid #efefef;
77 | box-sizing: border-box;
78 | border-radius: 4px;
79 | padding: 8px 12px;
80 | width: 100%;
81 | font-size: 14px;
82 |
83 | &:focus {
84 | border-color: #dbdbdb;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------