├── .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 | 140 | 144 | 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 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/img/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 69 | 76 | 83 | 84 | ), 85 | name: 'Добавить список' 86 | } 87 | ]} 88 | /> 89 | {visiblePopup && ( 90 |
91 | Close button 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 | 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 | Add icon 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 | 45 | 49 | 50 |
51 |
onRemove(list.id, id)}> 52 | 59 | 63 | 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 | Edit icon 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 | --------------------------------------------------------------------------------