3. JS Todo App
Создай полноценное приложение для управления задачами (To-Do List) с использованием чистого JavaScript. Без фреймворков — только HTML, CSS и Vanilla JS!
Что будем делать
Заголовок раздела «Что будем делать»- Добавление задач — ввод и создание новых задач
- Удаление задач — кнопка для удаления завершённых
- Отметка выполнения — чекбоксы для отметки задач
- Фильтрация — показать все/активные/завершённые
- LocalStorage — сохранение данных в браузере
- Drag & Drop — перетаскивание задач (бонус)
Этап 1: Подготовка
Заголовок раздела «Этап 1: Подготовка»Инструменты
Заголовок раздела «Инструменты»✅ VS Code — редактор кода
Скачать VS Code →
✅ Браузер — Chrome, Firefox или Edge
(Для отладки в DevTools — F12)
✅ Live Server — расширение для VS Code
Установить Live Server →
Создай структуру проекта
Заголовок раздела «Создай структуру проекта»todo-app/├── index.html # Главная страница├── style.css # Стили├── app.js # Логика приложения└── README.md # Описание проекта💡 Как запустить:
Открой index.html в браузере или используй Live Server в VS Code (правая кнопка → Open with Live Server).
Этап 2: HTML разметка
Заголовок раздела «Этап 2: HTML разметка»Создай файл index.html:
<!DOCTYPE html><html lang="ru"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Todo App — JS Project</title> <link rel="stylesheet" href="style.css"></head><body> <div class="container"> <!-- Header --> <header class="header"> <h1>📝 My Tasks</h1> <p class="subtitle">Управляй своими делами эффективно</p> </header>
<!-- Input --> <div class="input-section"> <input type="text" id="taskInput" placeholder="Добавить новую задачу..." maxlength="100" > <button id="addBtn">Добавить</button> </div>
<!-- Filters --> <div class="filters"> <button class="filter-btn active" data-filter="all">Все</button> <button class="filter-btn" data-filter="active">Активные</button> <button class="filter-btn" data-filter="completed">Завершённые</button> </div>
<!-- Task List --> <ul id="taskList" class="task-list"> <!-- Задачи добавляются через JS --> </ul>
<!-- Footer Stats --> <div class="footer-stats"> <span id="totalTasks">Всего: 0</span> <span id="activeTasks">Активных: 0</span> <button id="clearCompleted">Очистить завершённые</button> </div> </div>
<script src="app.js"></script></body></html>Что здесь:
- ✅ Input field для ввода задач
- ✅ Кнопки фильтрации (все/активные/завершённые)
- ✅ Список задач (
<ul id="taskList">) - ✅ Счётчики задач (всего/активных)
Этап 3: CSS стили
Заголовок раздела «Этап 3: CSS стили»Создай файл style.css:
/* === RESET & BASE === */* { margin: 0; padding: 0; box-sizing: border-box;}
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px;}
.container { background: white; max-width: 600px; width: 100%; border-radius: 20px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); overflow: hidden;}
/* === HEADER === */.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px 20px; text-align: center;}
.header h1 { font-size: 2rem; margin-bottom: 5px;}
.subtitle { font-size: 0.9rem; opacity: 0.9;}
/* === INPUT SECTION === */.input-section { padding: 20px; display: flex; gap: 10px;}
#taskInput { flex: 1; padding: 12px 15px; border: 2px solid #e0e0e0; border-radius: 10px; font-size: 1rem; transition: border-color 0.3s;}
#taskInput:focus { outline: none; border-color: #667eea;}
#addBtn { padding: 12px 30px; background: #667eea; color: white; border: none; border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: background 0.3s;}
#addBtn:hover { background: #5568d3;}
/* === FILTERS === */.filters { padding: 0 20px 10px; display: flex; gap: 10px; justify-content: center;}
.filter-btn { padding: 8px 20px; border: 2px solid #e0e0e0; background: white; border-radius: 20px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s;}
.filter-btn:hover { border-color: #667eea; color: #667eea;}
.filter-btn.active { background: #667eea; color: white; border-color: #667eea;}
/* === TASK LIST === */.task-list { list-style: none; padding: 10px 20px; max-height: 400px; overflow-y: auto;}
.task-item { display: flex; align-items: center; padding: 15px; margin-bottom: 10px; background: #f9f9f9; border-radius: 10px; transition: all 0.3s; cursor: pointer;}
.task-item:hover { background: #f0f0f0; transform: translateX(5px);}
.task-item.completed { opacity: 0.6;}
.task-item.completed .task-text { text-decoration: line-through; color: #999;}
.task-checkbox { width: 20px; height: 20px; margin-right: 15px; cursor: pointer;}
.task-text { flex: 1; font-size: 1rem; word-break: break-word;}
.delete-btn { background: #e74c3c; color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; font-size: 0.85rem; transition: background 0.3s;}
.delete-btn:hover { background: #c0392b;}
/* === FOOTER STATS === */.footer-stats { padding: 15px 20px; background: #f9f9f9; display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem; color: #666;}
#clearCompleted { background: #e74c3c; color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; font-size: 0.85rem; transition: background 0.3s;}
#clearCompleted:hover { background: #c0392b;}
/* === EMPTY STATE === */.empty-state { text-align: center; padding: 40px 20px; color: #999;}
.empty-state p { font-size: 1.1rem; margin-bottom: 10px;}
/* === SCROLLBAR === */.task-list::-webkit-scrollbar { width: 8px;}
.task-list::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 10px;}
.task-list::-webkit-scrollbar-thumb { background: #888; border-radius: 10px;}
.task-list::-webkit-scrollbar-thumb:hover { background: #555;}Что здесь:
- ✅ Gradient background (фиолетовый фон)
- ✅ Card design (белая карточка с тенью)
- ✅ Hover effects (плавные анимации)
- ✅ Completed state (перечёркнутый текст)
- ✅ Custom scrollbar (кастомный скроллбар)
Этап 4: JavaScript логика
Заголовок раздела «Этап 4: JavaScript логика»Создай файл app.js:
// === DOM ELEMENTS ===const taskInput = document.getElementById('taskInput');const addBtn = document.getElementById('addBtn');const taskList = document.getElementById('taskList');const filterBtns = document.querySelectorAll('.filter-btn');const clearCompletedBtn = document.getElementById('clearCompleted');const totalTasksEl = document.getElementById('totalTasks');const activeTasksEl = document.getElementById('activeTasks');
// === STATE ===let tasks = [];let currentFilter = 'all';
// === LOAD FROM LOCALSTORAGE ===function loadTasks() { const savedTasks = localStorage.getItem('tasks'); if (savedTasks) { tasks = JSON.parse(savedTasks); renderTasks(); }}
// === SAVE TO LOCALSTORAGE ===function saveTasks() { localStorage.setItem('tasks', JSON.stringify(tasks));}
// === ADD TASK ===function addTask() { const text = taskInput.value.trim();
if (text === '') { alert('⚠️ Введите текст задачи!'); return; }
const newTask = { id: Date.now(), text: text, completed: false, createdAt: new Date().toISOString() };
tasks.push(newTask); taskInput.value = ''; saveTasks(); renderTasks();}
// === DELETE TASK ===function deleteTask(id) { tasks = tasks.filter(task => task.id !== id); saveTasks(); renderTasks();}
// === TOGGLE TASK ===function toggleTask(id) { tasks = tasks.map(task => task.id === id ? { ...task, completed: !task.completed } : task ); saveTasks(); renderTasks();}
// === CLEAR COMPLETED ===function clearCompleted() { tasks = tasks.filter(task => !task.completed); saveTasks(); renderTasks();}
// === RENDER TASKS ===function renderTasks() { // Filter tasks let filteredTasks = tasks;
if (currentFilter === 'active') { filteredTasks = tasks.filter(task => !task.completed); } else if (currentFilter === 'completed') { filteredTasks = tasks.filter(task => task.completed); }
// Empty state if (filteredTasks.length === 0) { taskList.innerHTML = ` <div class="empty-state"> <p>📭 Нет задач</p> <small>Добавьте первую задачу выше</small> </div> `; } else { // Render task items taskList.innerHTML = filteredTasks.map(task => ` <li class="task-item ${task.completed ? 'completed' : ''}" data-id="${task.id}"> <input type="checkbox" class="task-checkbox" ${task.completed ? 'checked' : ''} onchange="toggleTask(${task.id})" > <span class="task-text">${task.text}</span> <button class="delete-btn" onclick="deleteTask(${task.id})"> 🗑️ Удалить </button> </li> `).join(''); }
// Update stats updateStats();}
// === UPDATE STATS ===function updateStats() { const total = tasks.length; const active = tasks.filter(task => !task.completed).length;
totalTasksEl.textContent = `Всего: ${total}`; activeTasksEl.textContent = `Активных: ${active}`;}
// === SET FILTER ===function setFilter(filter) { currentFilter = filter;
// Update active button filterBtns.forEach(btn => { btn.classList.remove('active'); if (btn.dataset.filter === filter) { btn.classList.add('active'); } });
renderTasks();}
// === EVENT LISTENERS ===addBtn.addEventListener('click', addTask);
taskInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { addTask(); }});
filterBtns.forEach(btn => { btn.addEventListener('click', () => { setFilter(btn.dataset.filter); });});
clearCompletedBtn.addEventListener('click', clearCompleted);
// === INIT APP ===loadTasks();Что здесь:
- ✅ State management — массив
tasksхранит все задачи - ✅ LocalStorage — сохранение при каждом изменении
- ✅ CRUD операции — Create, Read, Update, Delete
- ✅ Фильтрация — показ только нужных задач
- ✅ Event delegation — обработка кликов
Этап 5: Улучшения (бонус)
Заголовок раздела «Этап 5: Улучшения (бонус)»1. Добавить редактирование задач
Заголовок раздела «1. Добавить редактирование задач»function editTask(id, newText) { tasks = tasks.map(task => task.id === id ? { ...task, text: newText } : task ); saveTasks(); renderTasks();}2. Drag & Drop (перетаскивание)
Заголовок раздела «2. Drag & Drop (перетаскивание)»// Add draggable="true" to task items<li class="task-item" draggable="true" ...>
// Handle drag eventstaskList.addEventListener('dragstart', (e) => { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/html', e.target.innerHTML); e.target.classList.add('dragging');});
taskList.addEventListener('dragover', (e) => { e.preventDefault(); const draggingItem = document.querySelector('.dragging'); const afterElement = getDragAfterElement(taskList, e.clientY);
if (afterElement == null) { taskList.appendChild(draggingItem); } else { taskList.insertBefore(draggingItem, afterElement); }});
function getDragAfterElement(container, y) { const draggableElements = [...container.querySelectorAll('.task-item:not(.dragging)')];
return draggableElements.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) { return { offset: offset, element: child }; } else { return closest; } }, { offset: Number.NEGATIVE_INFINITY }).element;}3. Анимации при добавлении
Заголовок раздела «3. Анимации при добавлении»@keyframes slideIn { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); }}
.task-item { animation: slideIn 0.3s ease-out;}4. Категории задач
Заголовок раздела «4. Категории задач»const newTask = { id: Date.now(), text: text, completed: false, category: 'work', // work, personal, urgent createdAt: new Date().toISOString()};Этап 6: Тестирование
Заголовок раздела «Этап 6: Тестирование»✅ Что проверить:
Заголовок раздела «✅ Что проверить:»-
Добавление задач:
- Пустая строка → должна показать alert
- Длинный текст → должен переноситься
- Enter на клавиатуре → должен добавлять задачу
-
Удаление задач:
- Клик на “Удалить” → задача исчезает
- Удаление последней задачи → показывается empty state
-
Отметка выполнения:
- Клик на чекбокс → задача перечёркивается
- Повторный клик → восстанавливается
-
Фильтрация:
- “Все” → показывает все задачи
- “Активные” → только незавершённые
- “Завершённые” → только выполненные
-
LocalStorage:
- Добавь задачи, закрой вкладку, открой снова → должны остаться
-
Счётчики:
- Добавление → общее число увеличивается
- Отметка → активные уменьшаются
Этап 7: Деплой
Заголовок раздела «Этап 7: Деплой»Вариант 1: GitHub Pages
Заголовок раздела «Вариант 1: GitHub Pages»# 1. Создай репозиторий на GitHub# 2. Инициализируй Gitgit initgit add .git commit -m "Initial commit: Todo App"
# 3. Подключи удалённый репозиторийgit remote add origin https://github.com/username/todo-app.git
# 4. Запуш кодgit push -u origin main
# 5. Включи GitHub Pages (Settings → Pages)# Branch: main, Folder: / (root)Твоё приложение будет доступно по адресу:
https://username.github.io/todo-app/
Вариант 2: Netlify
Заголовок раздела «Вариант 2: Netlify»- Перейди на netlify.com
- Drag & Drop папку
todo-app/в браузер - Готово! Ссылка будет вида
https://random-name.netlify.app
Вариант 3: Vercel
Заголовок раздела «Вариант 3: Vercel»# Установи Vercel CLInpm i -g vercel
# В папке проекта:vercel
# Следуй инструкциям в терминале✅ Что ты создал:
- Полноценное Todo приложение на чистом JS
- LocalStorage для сохранения данных
- Фильтрацию и счётчики задач
- Красивый UI с анимациями
- Деплой в интернет
✅ Что ты освоил:
- DOM manipulation (создание, удаление элементов)
- Event handling (клики, Enter, checkbox)
- LocalStorage API (сохранение/загрузка)
- Array methods (map, filter, reduce)
- State management (управление состоянием приложения)
- CSS animations (плавные переходы)
Следующие шаги
Заголовок раздела «Следующие шаги»- Добавь категории (работа, личное, срочно) с цветовой маркировкой
- Сортировку (по дате, по имени, по приоритету)
- Поиск (фильтрация задач по тексту)
- Dark mode (переключение тёмной темы)
- Уведомления (напоминания через Notification API)
Практика
Заголовок раздела «Практика»Попробуй базовую реализацию Todo App прямо здесь: