Last active
June 14, 2025 11:57
-
-
Save sunmeat/beab21b35348ca9ff6830b757a1a28ed to your computer and use it in GitHub Desktop.
todo list redux example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
App.jsx: | |
import {useSelector, useDispatch, Provider} from 'react-redux'; | |
import {configureStore, createSlice} from '@reduxjs/toolkit'; | |
import {useState} from 'react'; | |
import './App.css'; | |
// создание среза (slice) для управления задачами | |
const todoSlice = createSlice({ | |
name: 'todo', // имя среза | |
initialState: { // начальное состояние хранилища | |
todos: [], // массив задач | |
filter: 'ALL', // текущий фильтр для отображения задач | |
}, | |
reducers: { // редьюсеры для обработки действий | |
addTodo: (state, action) => { // действие для добавления новой задачи | |
state.todos.push({ | |
id: Date.now(), | |
text: action.payload, | |
completed: false, | |
}); | |
}, | |
toggleTodo: (state, action) => { // действие для переключения статуса задачи | |
const todo = state.todos.find((todo) => todo.id === action.payload); | |
if (todo) { | |
todo.completed = !todo.completed; | |
} | |
}, | |
editTodo: (state, action) => { // действие для редактирования текста задачи | |
const {id, text} = action.payload; | |
const todo = state.todos.find((todo) => todo.id === id); | |
if (todo) { | |
todo.text = text; | |
} | |
}, | |
deleteTodo: (state, action) => { // действие для удаления задачи | |
state.todos = state.todos.filter((todo) => todo.id !== action.payload); | |
}, | |
clearCompleted: (state) => { // действие для очистки завершённых задач | |
state.todos = state.todos.filter((todo) => !todo.completed); | |
}, | |
setFilter: (state, action) => { // действие для установки фильтра | |
state.filter = action.payload; | |
}, | |
}, | |
}); | |
// извлечение действий для упрощения их использования | |
const {addTodo, toggleTodo, editTodo, deleteTodo, clearCompleted, setFilter} = todoSlice.actions; | |
// настройка хранилища redux | |
const store = configureStore({ | |
reducer: todoSlice.reducer, // подключение редьюсера среза к хранилищу | |
}); | |
// компонент списка задач | |
function TodoList() { | |
const [input, setInput] = useState(''); | |
const [editingId, setEditingId] = useState(null); | |
const [editText, setEditText] = useState(''); | |
const dispatch = useDispatch(); // хук для отправки действий в redux | |
const filter = useSelector((state) => state.filter); // хук для получения фильтра из состояния redux | |
// выбор задач на основе текущего фильтра | |
const todos = useSelector((state) => { | |
if (state.filter === 'COMPLETED') return state.todos.filter((todo) => todo.completed); | |
if (state.filter === 'ACTIVE') return state.todos.filter((todo) => !todo.completed); | |
return state.todos; | |
}); | |
const handleAddTodo = () => { | |
if (input.trim()) { | |
dispatch(addTodo(input)); // отправка действия добавления задачи | |
setInput(''); | |
} | |
}; | |
const handleEditStart = (todo) => { | |
setEditingId(todo.id); | |
setEditText(todo.text); | |
}; | |
const handleEditSave = (id) => { | |
if (editText.trim()) { | |
dispatch(editTodo({id, text: editText})); // отправка действия редактирования задачи | |
} | |
setEditingId(null); | |
setEditText(''); | |
}; | |
return ( | |
<div className="todo-app"> | |
<h1 className="todo-title">Список задач</h1> | |
<div className="todo-input-container"> | |
<input | |
className="todo-input" | |
value={input} | |
onChange={(e) => setInput(e.target.value)} | |
placeholder="Текст задачи" | |
onKeyPress={(e) => e.key === 'Enter' && handleAddTodo()} | |
/> | |
<button className="todo-add-button" onClick={handleAddTodo}> | |
Добавить | |
</button> | |
</div> | |
<div className="todo-filter-container"> | |
<button | |
className={`todo-filter-button ${filter === 'ALL' ? 'active' : ''}`} | |
onClick={() => dispatch(setFilter('ALL'))} // отправка действия установки фильтра "все" | |
> | |
Все | |
</button> | |
<button | |
className={`todo-filter-button ${filter === 'ACTIVE' ? 'active' : ''}`} | |
onClick={() => dispatch(setFilter('ACTIVE'))} // отправка действия установки фильтра "активные" | |
> | |
В процессе | |
</button> | |
<button | |
className={`todo-filter-button ${filter === 'COMPLETED' ? 'active' : ''}`} | |
onClick={() => dispatch(setFilter('COMPLETED'))} // отправка действия установки фильтра "завершенные" | |
> | |
Выполненные | |
</button> | |
</div> | |
<ul className="todo-list"> | |
{todos.map((todo) => ( | |
<li key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}> | |
{editingId === todo.id ? ( | |
<div className="todo-edit-container"> | |
<input | |
className="todo-edit-input" | |
value={editText} | |
onChange={(e) => setEditText(e.target.value)} | |
onKeyPress={(e) => e.key === 'Enter' && handleEditSave(todo.id)} | |
autoFocus | |
/> | |
<button className="todo-save-button" onClick={() => handleEditSave(todo.id)}> | |
Сохранить | |
</button> | |
<button className="todo-cancel-button" onClick={() => setEditingId(null)}> | |
Отмена | |
</button> | |
</div> | |
) : ( | |
<div className="todo-content"> | |
<span className="todo-text">{todo.text}</span> | |
<div className="todo-actions"> | |
<button | |
className={`todo-status-button ${todo.completed ? 'done' : 'not-done'}`} | |
onClick={() => dispatch(toggleTodo(todo.id))} // отправка действия переключения статуса задачи | |
> | |
{todo.completed ? 'Готово' : 'Отметить как выполненную'} | |
</button> | |
<button | |
className="todo-edit-button" | |
onClick={() => handleEditStart(todo)} | |
> | |
Редактировать | |
</button> | |
<button | |
className="todo-delete-button" | |
onClick={() => dispatch(deleteTodo(todo.id))} // отправка действия удаления задачи | |
> | |
Удалить | |
</button> | |
</div> | |
</div> | |
)} | |
</li> | |
))} | |
</ul> | |
{todos.some((todo) => todo.completed) && ( | |
<button | |
className="todo-clear-completed" | |
onClick={() => dispatch(clearCompleted())} // отправка действия очистки завершённых задач | |
> | |
Удалить выполненные задачи | |
</button> | |
)} | |
</div> | |
); | |
} | |
// корневой компонент с провайдером redux | |
function App() { | |
return ( | |
<Provider store={store}> | |
<TodoList/> | |
</Provider> | |
); | |
} | |
export default App; | |
=============================================================================================================== | |
App.css: | |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
background: linear-gradient(135deg, #1e1e2f 0%, #2a2a4a 100%); | |
color: #e0e0e0; | |
font-family: 'Inter', sans-serif; | |
min-height: 100vh; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
padding: 20px; | |
} | |
.todo-app { | |
max-width: 700px; | |
width: 100%; | |
background: linear-gradient(145deg, rgba(40, 40, 60, 0.9), rgba(60, 60, 80, 0.9)); | |
backdrop-filter: blur(10px); | |
padding: 30px; | |
border-radius: 16px; | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
animation: fadeIn 0.5s ease-in-out; | |
} | |
@keyframes fadeIn { | |
from { | |
opacity: 0; | |
transform: translateY(-20px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
.todo-title { | |
text-align: center; | |
color: #ffffff; | |
font-size: 2.5em; | |
font-weight: 700; | |
margin-bottom: 30px; | |
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); | |
} | |
.todo-input-container { | |
display: flex; | |
gap: 15px; | |
margin-bottom: 25px; | |
} | |
.todo-input { | |
flex: 1; | |
padding: 12px 16px; | |
border: none; | |
border-radius: 8px; | |
background: rgba(255, 255, 255, 0.1); | |
color: #ffffff; | |
font-size: 1.1em; | |
transition: all 0.3s ease; | |
} | |
.todo-input::placeholder { | |
color: #a0a0a0; | |
} | |
.todo-input:focus { | |
outline: none; | |
background: rgba(255, 255, 255, 0.15); | |
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.3); | |
} | |
.todo-add-button { | |
padding: 12px 24px; | |
background: linear-gradient(90deg, #4a90e2, #6bb6ff); | |
color: #ffffff; | |
border: none; | |
border-radius: 8px; | |
cursor: pointer; | |
font-size: 1.1em; | |
font-weight: 500; | |
transition: all 0.3s ease; | |
} | |
.todo-add-button:hover { | |
background: linear-gradient(90deg, #3b7cc1, #5a9de0); | |
transform: translateY(-2px); | |
} | |
.todo-add-button:active { | |
transform: scale(0.95); | |
} | |
.todo-filter-container { | |
display: flex; | |
gap: 12px; | |
justify-content: center; | |
margin-bottom: 30px; | |
} | |
.todo-filter-button { | |
padding: 10px 20px; | |
background: rgba(255, 255, 255, 0.1); | |
color: #e0e0e0; | |
border: none; | |
border-radius: 8px; | |
cursor: pointer; | |
font-size: 1em; | |
font-weight: 500; | |
transition: all 0.3s ease; | |
} | |
.todo-filter-button:hover { | |
background: rgba(255, 255, 255, 0.2); | |
} | |
.todo-filter-button.active { | |
background: linear-gradient(90deg, #4a90e2, #6bb6ff); | |
color: #ffffff; | |
} | |
.todo-list { | |
list-style: none; | |
padding: 0; | |
} | |
.todo-item { | |
display: flex; | |
align-items: center; | |
padding: 15px; | |
background: rgba(255, 255, 255, 0.05); | |
border-radius: 10px; | |
margin-bottom: 12px; | |
transition: all 0.3s ease; | |
animation: slideIn 0.3s ease-in-out; | |
} | |
@keyframes slideIn { | |
from { | |
opacity: 0; | |
transform: translateX(-20px); | |
} | |
to { | |
opacity: 1; | |
transform: translateX(0); | |
} | |
} | |
.todo-item.completed { | |
background: rgba(46, 204, 113, 0.1); | |
} | |
.todo-item.completed .todo-text { | |
text-decoration: line-through; | |
color: #7f8c8d; | |
} | |
.todo-content { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
width: 100%; | |
} | |
.todo-text { | |
flex: 1; | |
font-size: 1.1em; | |
color: #e0e0e0; | |
margin-right: 15px; | |
} | |
.todo-actions { | |
display: flex; | |
gap: 10px; | |
} | |
.todo-status-button { | |
padding: 8px 16px; | |
border: none; | |
border-radius: 6px; | |
cursor: pointer; | |
font-size: 0.95em; | |
font-weight: 500; | |
transition: all 0.3s ease; | |
} | |
.todo-status-button.done { | |
background: linear-gradient(90deg, #2ecc71, #27ae60); | |
color: #ffffff; | |
} | |
.todo-status-button.not-done { | |
background: linear-gradient(90deg, #7f8c8d, #95a5a6); | |
color: #ffffff; | |
} | |
.todo-status-button:hover { | |
transform: translateY(-1px); | |
} | |
.todo-status-button:active { | |
transform: scale(0.95); | |
} | |
.todo-edit-button, | |
.todo-delete-button { | |
padding: 8px 16px; | |
border: none; | |
border-radius: 6px; | |
cursor: pointer; | |
font-size: 0.95em; | |
font-weight: 500; | |
transition: all 0.3s ease; | |
} | |
.todo-edit-button { | |
background: linear-gradient(90deg, #f1c40f, #f39c12); | |
color: #ffffff; | |
} | |
.todo-edit-button:hover { | |
background: linear-gradient(90deg, #d4ac0d, #d68910); | |
transform: translateY(-1px); | |
} | |
.todo-delete-button { | |
background: linear-gradient(90deg, #e74c3c, #c0392b); | |
color: #ffffff; | |
} | |
.todo-delete-button:hover { | |
background: linear-gradient(90deg, #c0392b, #a93226); | |
transform: translateY(-1px); | |
} | |
.todo-edit-button:active, | |
.todo-delete-button:active { | |
transform: scale(0.95); | |
} | |
.todo-edit-container { | |
display: flex; | |
gap: 10px; | |
width: 100%; | |
} | |
.todo-edit-input { | |
flex: 1; | |
padding: 10px; | |
border: none; | |
border-radius: 6px; | |
background: rgba(255, 255, 255, 0.1); | |
color: #ffffff; | |
font-size: 1.1em; | |
transition: all 0.3s ease; | |
} | |
.todo-edit-input:focus { | |
outline: none; | |
background: rgba(255, 255, 255, 0.15); | |
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.3); | |
} | |
.todo-save-button, | |
.todo-cancel-button { | |
padding: 8px 16px; | |
border: none; | |
border-radius: 6px; | |
cursor: pointer; | |
font-size: 0.95em; | |
font-weight: 500; | |
transition: all 0.3s ease; | |
} | |
.todo-save-button { | |
background: linear-gradient(90deg, #4a90e2, #6bb6ff); | |
color: #ffffff; | |
} | |
.todo-save-button:hover { | |
background: linear-gradient(90deg, #3b7cc1, #5a9de0); | |
transform: translateY(-1px); | |
} | |
.todo-cancel-button { | |
background: linear-gradient(90deg, #7f8c8d, #95a5a6); | |
color: #ffffff; | |
} | |
.todo-cancel-button:hover { | |
background: linear-gradient(90deg, #6d7676, #839191); | |
transform: translateY(-1px); | |
} | |
.todo-save-button:active, | |
.todo-cancel-button:active { | |
transform: scale(0.95); | |
} | |
.todo-clear-completed { | |
display: block; | |
margin: 25px auto 0; | |
padding: 12px 24px; | |
background: linear-gradient(90deg, #e74c3c, #c0392b); | |
color: #ffffff; | |
border: none; | |
border-radius: 8px; | |
cursor: pointer; | |
font-size: 1.1em; | |
font-weight: 500; | |
transition: all 0.3s ease; | |
} | |
.todo-clear-completed:hover { | |
background: linear-gradient(90deg, #c0392b, #a93226); | |
transform: translateY(-2px); | |
} | |
.todo-clear-completed:active { | |
transform: scale(0.95); | |
} | |
@media (max-width: 500px) { | |
.todo-app { | |
padding: 20px; | |
} | |
.todo-input-container, | |
.todo-filter-container { | |
flex-direction: column; | |
gap: 10px; | |
} | |
.todo-add-button, | |
.todo-filter-button { | |
width: 100%; | |
padding: 12px; | |
} | |
.todo-actions { | |
flex-wrap: wrap; | |
gap: 8px; | |
} | |
.todo-status-button, | |
.todo-edit-button, | |
.todo-delete-button { | |
flex: 1; | |
text-align: center; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment