مشروع قائمة المهام بـ JavaScript — دليل عملي شامل 2026
مشروع عملي لبناء تطبيق قائمة مهام كامل بـ JavaScript — إضافة، حذف، تعديل، حفظ تلقائي، مع الكود الكامل والشرح
في المقال السابق، بنيت آلة حاسبة كاملة. الآن سنرتقي بمستوى المشروع — سنبني تطبيق قائمة مهام (Todo App) احترافي، مع حفظ تلقائي للبيانات.
هذا المشروع سيعلمك:
- التعامل مع النماذج (Forms).
- إنشاء عناصر ديناميكياً.
- حفظ البيانات في
localStorage. - تعديل العناصر وحذفها.
- تصفية المهام (الكل، النشطة، المكتملة).
ما سنبنيه
تطبيق قائمة مهام بوظائف:
- إضافة مهمة جديدة.
- تعديل مهمة موجودة.
- حذف مهمة.
- تعليم مهمة كمكتملة.
- تصفية المهام (الكل، النشطة، المكتملة).
- حذف المهام المكتملة.
- حفظ تلقائي في المتصفح.
هيكل المشروع
سننشئ 3 ملفات:
todo-app/
├── index.html
├── style.css
└── script.js
الخطوة 1: HTML — هيكل التطبيق
index.html:
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>قائمة المهام | JavaScript</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app">
<header class="app-header">
<h1>📝 قائمة المهام</h1>
<p class="date" id="date"></p>
</header>
<form class="todo-form" id="todoForm">
<input
type="text"
id="todoInput"
placeholder="أضف مهمة جديدة..."
autocomplete="off"
>
<button type="submit" class="btn-add">إضافة</button>
</form>
<div class="filters">
<button class="filter active" data-filter="all">الكل</button>
<button class="filter" data-filter="active">النشطة</button>
<button class="filter" data-filter="completed">المكتملة</button>
</div>
<ul class="todo-list" id="todoList"></ul>
<footer class="app-footer">
<span id="itemsLeft">0 مهام متبقية</span>
<button id="clearCompleted" class="btn-clear">حذف المكتملة</button>
</footer>
</div>
<script src="script.js"></script>
</body>
</html>
شرح:
header: العنوان والتاريخ.form: حقل الإدخال وزر الإضافة.filters: أزرار التصفية.todoList: قائمة المهام (فارغة، تُملأ بـ JavaScript).footer: عدد المهام المتبقية وزر حذف المكتملة.
الخطوة 2: CSS — تصميم التطبيق
style.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, sans-serif;
}
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 40px 20px;
}
.app {
background: #ffffff;
border-radius: 20px;
padding: 30px;
width: 100%;
max-width: 500px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
}
.app-header {
text-align: center;
margin-bottom: 25px;
}
.app-header h1 {
color: #1e293b;
font-size: 1.8rem;
margin-bottom: 8px;
}
.date {
color: #64748b;
font-size: 0.9rem;
}
/* ========== النموذج ========== */
.todo-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
#todoInput {
flex: 1;
padding: 14px 18px;
border: 2px solid #e2e8f0;
border-radius: 12px;
font-size: 1rem;
transition: border-color 0.2s;
}
#todoInput:focus {
outline: none;
border-color: #667eea;
}
.btn-add {
padding: 14px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.btn-add:hover {
background: #5568d3;
transform: translateY(-2px);
}
/* ========== الفلاتر ========== */
.filters {
display: flex;
gap: 8px;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #f1f5f9;
}
.filter {
flex: 1;
padding: 10px;
background: transparent;
border: 2px solid #e2e8f0;
border-radius: 10px;
color: #64748b;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
}
.filter:hover {
border-color: #667eea;
color: #667eea;
}
.filter.active {
background: #667eea;
color: white;
border-color: #667eea;
}
/* ========== القائمة ========== */
.todo-list {
list-style: none;
margin-bottom: 20px;
max-height: 400px;
overflow-y: auto;
}
.todo-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px;
background: #f8fafc;
border-radius: 12px;
margin-bottom: 10px;
transition: all 0.2s;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.todo-item:hover {
background: #f1f5f9;
}
.todo-checkbox {
width: 22px;
height: 22px;
cursor: pointer;
accent-color: #667eea;
flex-shrink: 0;
}
.todo-text {
flex: 1;
color: #1e293b;
font-size: 1rem;
word-break: break-word;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #94a3b8;
}
.todo-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.btn-icon {
width: 34px;
height: 34px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.btn-edit {
background: #dbeafe;
color: #1e40af;
}
.btn-edit:hover {
background: #bfdbfe;
}
.btn-delete {
background: #fee2e2;
color: #991b1b;
}
.btn-delete:hover {
background: #fecaca;
}
/* ========== التذييل ========== */
.app-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 15px;
border-top: 2px solid #f1f5f9;
font-size: 0.9rem;
color: #64748b;
}
.btn-clear {
background: transparent;
border: none;
color: #ef4444;
font-size: 0.9rem;
cursor: pointer;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.2s;
}
.btn-clear:hover {
background: #fee2e2;
}
/* ========== الحالة الفارغة ========== */
.empty-state {
text-align: center;
padding: 40px 20px;
color: #94a3b8;
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: 12px;
}
/* ========== الوضع الداكن ========== */
@media (prefers-color-scheme: dark) {
.app {
background: #1e293b;
}
.app-header h1,
.todo-text {
color: #f1f5f9;
}
.todo-item {
background: #334155;
}
.todo-item:hover {
background: #475569;
}
#todoInput {
background: #334155;
border-color: #475569;
color: #f1f5f9;
}
}
الخطوة 3: JavaScript — منطق التطبيق
script.js:
// ==================== المتغيرات ====================
let todos = [];
let currentFilter = "all";
// ==================== العناصر ====================
const form = document.querySelector("#todoForm");
const input = document.querySelector("#todoInput");
const todoList = document.querySelector("#todoList");
const itemsLeft = document.querySelector("#itemsLeft");
const clearCompletedBtn = document.querySelector("#clearCompleted");
const filterBtns = document.querySelectorAll(".filter");
const dateEl = document.querySelector("#date");
// ==================== التاريخ ====================
function displayDate() {
const options = { weekday: "long", year: "numeric", month: "long", day: "numeric" };
dateEl.textContent = new Date().toLocaleDateString("ar-EG", options);
}
// ==================== localStorage ====================
function saveTodos() {
localStorage.setItem("todos", JSON.stringify(todos));
}
function loadTodos() {
const stored = localStorage.getItem("todos");
if (stored) {
todos = JSON.parse(stored);
}
}
// ==================== إضافة مهمة ====================
function addTodo(text) {
const todo = {
id: Date.now(),
text: text.trim(),
completed: false,
createdAt: new Date().toISOString(),
};
todos.push(todo);
saveTodos();
render();
}
// ==================== حذف مهمة ====================
function deleteTodo(id) {
todos = todos.filter((t) => t.id !== id);
saveTodos();
render();
}
// ==================== تعديل مهمة ====================
function editTodo(id) {
const todo = todos.find((t) => t.id === id);
if (!todo) return;
const newText = prompt("عدّل المهمة:", todo.text);
if (newText !== null && newText.trim() !== "") {
todo.text = newText.trim();
saveTodos();
render();
}
}
// ==================== تعليم كمكتملة ====================
function toggleTodo(id) {
const todo = todos.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed;
saveTodos();
render();
}
}
// ==================== حذف المكتملة ====================
function clearCompleted() {
const completedCount = todos.filter((t) => t.completed).length;
if (completedCount === 0) return;
if (confirm(`هل تريد حذف ${completedCount} مهمة مكتملة؟`)) {
todos = todos.filter((t) => !t.completed);
saveTodos();
render();
}
}
// ==================== الفلترة ====================
function getFilteredTodos() {
switch (currentFilter) {
case "active":
return todos.filter((t) => !t.completed);
case "completed":
return todos.filter((t) => t.completed);
default:
return todos;
}
}
// ==================== العرض ====================
function render() {
const filtered = getFilteredTodos();
// القائمة الفارغة
if (filtered.length === 0) {
todoList.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<p>لا توجد مهام هنا</p>
</div>
`;
} else {
todoList.innerHTML = filtered
.map(
(todo) => `
<li class="todo-item ${todo.completed ? "completed" : ""}" data-id="${todo.id}">
<input
type="checkbox"
class="todo-checkbox"
${todo.completed ? "checked" : ""}
data-action="toggle"
>
<span class="todo-text">${escapeHtml(todo.text)}</span>
<div class="todo-actions">
<button class="btn-icon btn-edit" data-action="edit" title="تعديل">✏️</button>
<button class="btn-icon btn-delete" data-action="delete" title="حذف">🗑️</button>
</div>
</li>
`
)
.join("");
}
// عدد المهام
const activeCount = todos.filter((t) => !t.completed).length;
itemsLeft.textContent = `${activeCount} ${
activeCount === 1 ? "مهمة متبقية" : "مهام متبقية"
}`;
}
// ==================== حماية من XSS ====================
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// ==================== الأحداث ====================
// إرسال النموذج
form.addEventListener("submit", (e) => {
e.preventDefault();
const text = input.value.trim();
if (text === "") return;
addTodo(text);
input.value = "";
input.focus();
});
// أحداث القائمة (Event Delegation)
todoList.addEventListener("click", (e) => {
const target = e.target;
const todoItem = target.closest(".todo-item");
if (!todoItem) return;
const id = Number(todoItem.dataset.id);
const action = target.dataset.action;
if (action === "toggle") {
toggleTodo(id);
} else if (action === "edit") {
editTodo(id);
} else if (action === "delete") {
deleteTodo(id);
}
});
// الفلاتر
filterBtns.forEach((btn) => {
btn.addEventListener("click", () => {
filterBtns.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
currentFilter = btn.dataset.filter;
render();
});
});
// حذف المكتملة
clearCompletedBtn.addEventListener("click", clearCompleted);
// ==================== التهيئة ====================
displayDate();
loadTodos();
render();
شرح الكود خطوة بخطوة
1. هيكل البيانات
const todo = {
id: Date.now(), // معرف فريد
text: "نص المهمة", // نص المهمة
completed: false, // هل مكتملة؟
createdAt: "...", // تاريخ الإنشاء
};
2. حفظ البيانات في localStorage
function saveTodos() {
localStorage.setItem("todos", JSON.stringify(todos));
}
function loadTodos() {
const stored = localStorage.getItem("todos");
if (stored) todos = JSON.parse(stored);
}
الفائدة: البيانات تبقى حتى بعد إغلاق المتصفح.
3. إضافة مهمة
function addTodo(text) {
todos.push({
id: Date.now(),
text: text.trim(),
completed: false,
});
saveTodos();
render();
}
4. الفلترة
function getFilteredTodos() {
switch (currentFilter) {
case "active": return todos.filter((t) => !t.completed);
case "completed": return todos.filter((t) => t.completed);
default: return todos;
}
}
5. Event Delegation
todoList.addEventListener("click", (e) => {
const target = e.target;
const todoItem = target.closest(".todo-item");
if (!todoItem) return;
const id = Number(todoItem.dataset.id);
const action = target.dataset.action;
if (action === "toggle") toggleTodo(id);
else if (action === "edit") editTodo(id);
else if (action === "delete") deleteTodo(id);
});
الفائدة: مستمع واحد لكل العناصر (حتى المضافة ديناميكياً).
6. حماية من XSS
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
الفائدة: منع تنفيذ أكواد ضارة من المستخدم.
تمارين إضافية
تمرين 1: إضافة أولوية للمهمة
أضف خيار "أولوية" (عالية، متوسطة، منخفضة).
الحل:
// في addTodo:
todos.push({
id: Date.now(),
text: text.trim(),
priority: "متوسطة", // الافتراضي
completed: false,
});
// في HTML للعرض:
// <span class="priority-${todo.priority}">${todo.priority}</span>
تمرين 2: إضافة تاريخ استحقاق
أضف تاريخ استحقاق لكل مهمة.
الحل:
todos.push({
id: Date.now(),
text: text.trim(),
dueDate: new Date().toISOString(),
completed: false,
});
// عرض التاريخ في القائمة
تمرين 3: البحث في المهام
أضف حقل بحث لتصفية المهام.
الحل:
<input type="text" id="searchInput" placeholder="ابحث...">
const searchInput = document.querySelector("#searchInput");
let searchQuery = "";
searchInput.addEventListener("input", (e) => {
searchQuery = e.target.value.toLowerCase();
render();
});
// في getFilteredTodos:
return todos.filter((t) =>
t.text.toLowerCase().includes(searchQuery)
);
تمرين 4: التعديل المباشر (Inline Edit)
بدلاً من prompt، اجعل النص قابلاً للتعديل مباشرة.
الحل:
function editTodo(id) {
const todoItem = document.querySelector(`.todo-item[data-id="${id}"]`);
const textSpan = todoItem.querySelector(".todo-text");
const originalText = textSpan.textContent;
textSpan.contentEditable = true;
textSpan.focus();
textSpan.addEventListener("blur", () => {
const newText = textSpan.textContent.trim();
if (newText && newText !== originalText) {
const todo = todos.find((t) => t.id === id);
todo.text = newText;
saveTodos();
}
textSpan.contentEditable = false;
render();
});
}
تمرين 5: الإشعارات
أضف إشعار عند إكمال مهمة.
الحل:
function notify(message) {
if ("Notification" in window && Notification.permission === "granted") {
new Notification("قائمة المهام", { body: message });
}
}
// في toggleTodo:
if (todo.completed) {
notify(`أكملت: ${todo.text}`);
}
حل المشاكل الشائعة
🔴 المشكلة 1: البيانات تُفقد عند التحديث
السبب: لم يتم حفظ البيانات.
الحل: استخدم localStorage (saveTodos بعد كل تعديل).
🔴 المشكلة 2: XSS (حقن أكواد)
السبب: استخدام innerHTML مع بيانات المستخدم مباشرة.
الحل:
// خطأ:
todoList.innerHTML = `<span>${todo.text}</span>`;
// صحيح:
todoList.innerHTML = `<span>${escapeHtml(todo.text)}</span>`;
🔴 المشكلة 3: Date.now() ينتج نفس المعرف
السبب: إضافة مهام في نفس المللي ثانية.
الحل: استخدم crypto.randomUUID():
id: crypto.randomUUID(),
🔴 المشكلة 4: المهام لا تظهر بعد الإضافة
السبب: نسيت استدعاء render().
الحل: تأكد من استدعاء render() بعد كل تعديل.
قائمة تحقق نهائية
| المهمة | الحالة |
|---|---|
| إنشاء ملف index.html | ⬜ |
| إنشاء ملف style.css | ⬜ |
| إنشاء ملف script.js | ⬜ |
| تشغيل التطبيق في المتصفح | ⬜ |
| اختبار إضافة مهمة | ⬜ |
| اختبار تعديل وحذف مهمة | ⬜ |
| اختبار الفلاتر | ⬜ |
| التأكد من حفظ البيانات بعد التحديث | ⬜ |
| حل تمرين واحد على الأقل إضافي | ⬜ |
ماذا بعد هذا المشروع؟
🎉 مبروك! لقد أكملت مسار JavaScript بالكامل!
الآن أنت جاهز لـ:
- تطوير مشاريع أكثر تعقيداً: مدونات، متاجر إلكترونية، تطبيقات.
- تعلم أطر العمل: React، Vue، Angular.
- تعلم Backend: Node.js، Express.
- تعلم قواعد البيانات: MongoDB، PostgreSQL.
- بناء معرض أعمالك: ارفع مشاريعك على GitHub.
الخلاصة
في هذا المشروع، تعلمت:
- ✅ إدارة الحالة — تخزين المهام في مصفوفة.
- ✅ localStorage — حفظ البيانات في المتصفح.
- ✅ DOM المتقدم — إنشاء وتعديل العناصر.
- ✅ Event Delegation — مستمع واحد للعناصر المتعددة.
- ✅ الفلترة — عرض المهام حسب الحالة.
- ✅ حماية XSS — تنظيف النصوص.
- ✅ تصميم تفاعلي — CSS مع animations.
هذا المشروع هو أساس كل تطبيقات المهام الاحترافية (مثل Todoist و Microsoft To Do). احتفظ بالكود، وطور فيه بنفسك!
🎉 هل أكملت مسار JavaScript بالكامل؟ شاركنا في التعليقات!
📚 مقالات ذات صلة
مشروع آلة حاسبة بـ JavaScript — دليل عملي شامل 2026
مشروع عملي لبناء آلة حاسبة كاملة بـ JavaScript و HTML و CSS — خطوة بخطوة مع الكود الكامل والشرح
التعامل مع DOM في JavaScript — دليل شامل 2026
دليل عملي مفصل للتعامل مع DOM في JavaScript — التحديد، التعديل، الأحداث، الإنشاء، مع تمارين وحلول
المصفوفات والكائنات في JavaScript — دليل شامل 2026
دليل عملي مفصل للمصفوفات والكائنات في JavaScript — الإنشاء، التعديل، الطرق الشائعة، مع تمارين وحلول