Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Last active June 14, 2025 12:17
Show Gist options
  • Save sunmeat/f18c190fd37bd2a2da81151d17ae021d to your computer and use it in GitHub Desktop.
Save sunmeat/f18c190fd37bd2a2da81151d17ae021d to your computer and use it in GitHub Desktop.
color theme change (second slice) 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;
},
},
});
// создание среза (slice) для управления темой
const themeSlice = createSlice({
name: 'theme', // имя среза
initialState: { // начальное состояние темы
mode: 'dark', // текущая тема (dark, light или neon)
},
reducers: { // редьюсеры для обработки действий
cycleTheme: (state) => { // действие для циклического переключения темы
if (state.mode === 'dark') state.mode = 'light';
else if (state.mode === 'light') state.mode = 'neon';
else state.mode = 'dark';
},
},
});
// извлечение действий для упрощения их использования
const {addTodo, toggleTodo, editTodo, deleteTodo, clearCompleted, setFilter} = todoSlice.actions;
const {cycleTheme} = themeSlice.actions;
// настройка хранилища redux с комбинированными редьюсерами
const store = configureStore({
reducer: {
todo: todoSlice.reducer, // редьюсер для задач
theme: themeSlice.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.todo.filter); // хук для получения фильтра из состояния redux
const themeMode = useSelector((state) => state.theme.mode); // хук для получения текущей темы
// выбор задач на основе текущего фильтра
const todos = useSelector((state) => {
if (state.todo.filter === 'COMPLETED') return state.todo.todos.filter((todo) => todo.completed);
if (state.todo.filter === 'ACTIVE') return state.todo.todos.filter((todo) => !todo.completed);
return state.todo.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 ${themeMode}`}>
<h1 className="todo-title">Список задач</h1>
<button
className="todo-theme-button"
onClick={() => dispatch(cycleTheme())} // отправка действия циклического переключения темы
>
Переключить тему
(Текущая: {themeMode === 'dark' ? 'Тёмная' : themeMode === 'light' ? 'Светлая' : 'Неоновая'})
</button>
<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 === themeMode.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>
);
}
function App() {
return (
<Provider store={store}>
<TodoList/>
</Provider>
);
}
export default App;
=================================================================================================================
App.css:
@import url('https://fonts.googleapis.com/css2?family=Segoe+UI:wght@400;500;600;700&display=swap');
:root {
--bg-gradient-start: #1a2525;
--bg-gradient-end: #2e3b3b;
--text-color: #e6f0fa;
--app-bg-start: rgba(30, 40, 50, 0.85);
--app-bg-end: rgba(50, 60, 70, 0.85);
--input-bg: rgba(80, 100, 120, 0.3);
--input-focus-bg: rgba(100, 120, 140, 0.4);
--placeholder-color: #a0b0c0;
--completed-bg: rgba(40, 150, 80, 0.2);
--completed-text: #90a0b0;
--item-bg: rgba(60, 80, 100, 0.2);
--add-button-bg: linear-gradient(90deg, #4682b4, #6495ed);
--add-button-hover: linear-gradient(90deg, #3a70a0, #5580d0);
--filter-button-bg: rgba(80, 100, 120, 0.3);
--filter-button-hover: rgba(100, 120, 140, 0.4);
--filter-active-bg: linear-gradient(90deg, #4682b4, #6495ed);
--status-done-bg: linear-gradient(90deg, #2e8b57, #3cb371);
--status-not-done-bg: linear-gradient(90deg, #708090, #8797a7);
--edit-button-bg: linear-gradient(90deg, #ffa500, #ffb732);
--edit-button-hover: linear-gradient(90deg, #e69500, #e6a52e);
--delete-button-bg: linear-gradient(90deg, #dc143c, #ff4040);
--delete-button-hover: linear-gradient(90deg, #b22222, #cc3333);
--save-button-bg: linear-gradient(90deg, #4682b4, #6495ed);
--save-button-hover: linear-gradient(90deg, #3a70a0, #5580d0);
--cancel-button-bg: linear-gradient(90deg, #708090, #8797a7);
--cancel-button-hover: linear-gradient(90deg, #607080, #768697);
--clear-button-bg: linear-gradient(90deg, #dc143c, #ff4040);
--clear-button-hover: linear-gradient(90deg, #b22222, #cc3333);
--theme-button-bg: linear-gradient(90deg, #6a5acd, #836fff);
--theme-button-hover: linear-gradient(90deg, #5a4ab8, #7360e6);
--border-glow: rgba(100, 149, 237, 0.5);
}
.light {
--bg-gradient-start: #e6f0fa;
--bg-gradient-end: #f5faff;
--text-color: #2e3b3b;
--app-bg-start: rgba(240, 248, 255, 0.85);
--app-bg-end: rgba(255, 255, 255, 0.85);
--input-bg: rgba(200, 210, 220, 0.3);
--input-focus-bg: rgba(210, 220, 230, 0.4);
--placeholder-color: #6a8299;
--completed-bg: rgba(40, 150, 80, 0.1);
--completed-text: #6a8299;
--item-bg: rgba(220, 230, 240, 0.2);
--add-button-bg: linear-gradient(90deg, #87ceeb, #b0e0e6);
--add-button-hover: linear-gradient(90deg, #73b8d3, #9acad0);
--filter-button-bg: rgba(200, 210, 220, 0.3);
--filter-button-hover: rgba(210, 220, 230, 0.4);
--filter-active-bg: linear-gradient(90deg, #87ceeb, #b0e0e6);
--status-done-bg: linear-gradient(90deg, #32cd32, #3cb371);
--status-not-done-bg: linear-gradient(90deg, #a0a0a0, #b4b4b4);
--edit-button-bg: linear-gradient(90deg, #ffd700, #ffec8b);
--edit-button-hover: linear-gradient(90deg, #e6c200, #e6d47a);
--delete-button-bg: linear-gradient(90deg, #ff4500, #ff6347);
--delete-button-hover: linear-gradient(90deg, #e63d00, #e6583f);
--save-button-bg: linear-gradient(90deg, #87ceeb, #b0e0e6);
--save-button-hover: linear-gradient(90deg, #73b8d3, #9acad0);
--cancel-button-bg: linear-gradient(90deg, #a0a0a0, #b4b4b4);
--cancel-button-hover: linear-gradient(90deg, #909090, #a4a4a4);
--clear-button-bg: linear-gradient(90deg, #ff4500, #ff6347);
--clear-button-hover: linear-gradient(90deg, #e63d00, #e6583f);
--theme-button-bg: linear-gradient(90deg, #20b2aa, #48d1cc);
--theme-button-hover: linear-gradient(90deg, #1c9c95, #40bfb9);
--border-glow: rgba(135, 206, 235, 0.5);
}
.neon {
--bg-gradient-start: #1c2526;
--bg-gradient-end: #2c3e50;
--text-color: #00ffcc;
--app-bg-start: rgba(20, 30, 40, 0.85);
--app-bg-end: rgba(30, 40, 50, 0.85);
--input-bg: rgba(0, 150, 136, 0.3);
--input-focus-bg: rgba(0, 170, 150, 0.4);
--placeholder-color: #26a69a;
--completed-bg: rgba(0, 230, 118, 0.2);
--completed-text: #4db6ac;
--item-bg: rgba(0, 150, 136, 0.2);
--add-button-bg: linear-gradient(90deg, #00ffcc, #26a69a);
--add-button-hover: linear-gradient(90deg, #00e6b8, #219187);
--filter-button-bg: rgba(0, 150, 136, 0.3);
--filter-button-hover: rgba(0, 170, 150, 0.4);
--filter-active-bg: linear-gradient(90deg, #00ffcc, #26a69a);
--status-done-bg: linear-gradient(90deg, #00e676, #00ff7f);
--status-not-done-bg: linear-gradient(90deg, #ff4081, #f06292);
--edit-button-bg: linear-gradient(90deg, #ffeb3b, #fff176);
--edit-button-hover: linear-gradient(90deg, #e6d433, #e6da68);
--delete-button-bg: linear-gradient(90deg, #ff1744, #ff4081);
--delete-button-hover: linear-gradient(90deg, #e6153d, #e63874);
--save-button-bg: linear-gradient(90deg, #00ffcc, #26a69a);
--save-button-hover: linear-gradient(90deg, #00e6b8, #219187);
--cancel-button-bg: linear-gradient(90deg, #ff4081, #f06292);
--cancel-button-hover: linear-gradient(90deg, #e63874, #d95783);
--clear-button-bg: linear-gradient(90deg, #ff1744, #ff4081);
--clear-button-hover: linear-gradient(90deg, #e6153d, #e63874);
--theme-button-bg: linear-gradient(90deg, #ff00ff, #ff69b4);
--theme-button-hover: linear-gradient(90deg, #e600e6, #e661a3);
--border-glow: rgba(0, 255, 204, 0.5);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, var(--bg-gradient-start), var(--bg-gradient-end));
color: var(--text-color);
font-family: 'Segoe UI', 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, var(--app-bg-start), var(--app-bg-end));
backdrop-filter: blur(15px);
padding: 30px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
border: 1px solid var(--border-glow);
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: var(--text-color);
font-size: 2.5em;
font-weight: 600;
margin-bottom: 20px;
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.todo-theme-button {
display: block;
margin: 0 auto 20px;
padding: 10px 20px;
background: var(--theme-button-bg);
color: #ffffff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1em;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.todo-theme-button:hover {
background: var(--theme-button-hover);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
.todo-theme-button:active {
transform: scale(0.95);
}
.todo-input-container {
display: flex;
gap: 15px;
margin-bottom: 25px;
}
.todo-input {
flex: 1;
padding: 12px 16px;
border: none;
border-radius: 6px;
background: var(--input-bg);
color: var(--text-color);
font-size: 1.1em;
transition: all 0.3s ease;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.todo-input::placeholder {
color: var(--placeholder-color);
}
.todo-input:focus {
outline: none;
background: var(--input-focus-bg);
box-shadow: 0 0 0 3px var(--border-glow);
}
.todo-add-button {
padding: 12px 24px;
background: var(--add-button-bg);
color: #ffffff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1.1em;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.todo-add-button:hover {
background: var(--add-button-hover);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
.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: var(--filter-button-bg);
color: var(--text-color);
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1em;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.todo-filter-button:hover {
background: var(--filter-button-hover);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.todo-filter-button.active {
background: var(--filter-active-bg);
color: #ffffff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 15px;
background: var(--item-bg);
border-radius: 8px;
margin-bottom: 12px;
transition: all 0.3s ease;
animation: slideIn 0.3s ease-in-out;
border: 1px solid var(--border-glow);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.todo-item.completed {
background: var(--completed-bg);
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: var(--completed-text);
}
.todo-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.todo-text {
flex: 1;
font-size: 1.1em;
color: var(--text-color);
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;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.todo-status-button.done {
background: var(--status-done-bg);
color: #ffffff;
}
.todo-status-button.not-done {
background: var(--status-not-done-bg);
color: #ffffff;
}
.todo-status-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.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;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.todo-edit-button {
background: var(--edit-button-bg);
color: #ffffff;
}
.todo-edit-button:hover {
background: var(--edit-button-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.todo-delete-button {
background: var(--delete-button-bg);
color: #ffffff;
}
.todo-delete-button:hover {
background: var(--delete-button-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.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: var(--input-bg);
color: var(--text-color);
font-size: 1.1em;
transition: all 0.3s ease;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.todo-edit-input:focus {
outline: none;
background: var(--input-focus-bg);
box-shadow: 0 0 0 3px var(--border-glow);
}
.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;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.todo-save-button {
background: var(--save-button-bg);
color: #ffffff;
}
.todo-save-button:hover {
background: var(--save-button-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.todo-cancel-button {
background: var(--cancel-button-bg);
color: #ffffff;
}
.todo-cancel-button:hover {
background: var(--cancel-button-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.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: var(--clear-button-bg);
color: #ffffff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1.1em;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.todo-clear-completed:hover {
background: var(--clear-button-hover);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
.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,
.todo-theme-button,
.todo-clear-completed {
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