Перейти к содержимому

3. JS Todo App

Создай полноценное приложение для управления задачами (To-Do List) с использованием чистого JavaScript. Без фреймворков — только HTML, CSS и Vanilla JS!


  • Добавление задач — ввод и создание новых задач
  • Удаление задач — кнопка для удаления завершённых
  • Отметка выполнения — чекбоксы для отметки задач
  • Фильтрация — показать все/активные/завершённые
  • LocalStorage — сохранение данных в браузере
  • Drag & Drop — перетаскивание задач (бонус)

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).


Создай файл 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">)
  • Счётчики задач (всего/активных)

Создай файл 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 (кастомный скроллбар)

Создай файл 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 — обработка кликов

function editTask(id, newText) {
tasks = tasks.map(task =>
task.id === id ? { ...task, text: newText } : task
);
saveTasks();
renderTasks();
}
// Add draggable="true" to task items
<li class="task-item" draggable="true" ...>
// Handle drag events
taskList.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;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.task-item {
animation: slideIn 0.3s ease-out;
}
const newTask = {
id: Date.now(),
text: text,
completed: false,
category: 'work', // work, personal, urgent
createdAt: new Date().toISOString()
};

  1. Добавление задач:

    • Пустая строка → должна показать alert
    • Длинный текст → должен переноситься
    • Enter на клавиатуре → должен добавлять задачу
  2. Удаление задач:

    • Клик на “Удалить” → задача исчезает
    • Удаление последней задачи → показывается empty state
  3. Отметка выполнения:

    • Клик на чекбокс → задача перечёркивается
    • Повторный клик → восстанавливается
  4. Фильтрация:

    • “Все” → показывает все задачи
    • “Активные” → только незавершённые
    • “Завершённые” → только выполненные
  5. LocalStorage:

    • Добавь задачи, закрой вкладку, открой снова → должны остаться
  6. Счётчики:

    • Добавление → общее число увеличивается
    • Отметка → активные уменьшаются

Окно терминала
# 1. Создай репозиторий на GitHub
# 2. Инициализируй Git
git init
git 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/

  1. Перейди на netlify.com
  2. Drag & Drop папку todo-app/ в браузер
  3. Готово! Ссылка будет вида https://random-name.netlify.app
Окно терминала
# Установи Vercel CLI
npm 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 (плавные переходы)

  1. Добавь категории (работа, личное, срочно) с цветовой маркировкой
  2. Сортировку (по дате, по имени, по приоритету)
  3. Поиск (фильтрация задач по тексту)
  4. Dark mode (переключение тёмной темы)
  5. Уведомления (напоминания через Notification API)

Попробуй базовую реализацию Todo App прямо здесь: