Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Last active June 14, 2025 11:57
Show Gist options
  • Save sunmeat/beab21b35348ca9ff6830b757a1a28ed to your computer and use it in GitHub Desktop.
Save sunmeat/beab21b35348ca9ff6830b757a1a28ed to your computer and use it in GitHub Desktop.
todo list redux example
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