JavaScript

مشروع قائمة المهام بـ JavaScript — دليل عملي شامل 2026

📅 2026-09-29⏱ 10 دقائق قراءة
في المقال السابق، بنيت آلة حاسبة كاملة. الآن سنرتقي بمستوى المشروع — سنبني **تطبيق قائمة مهام (Todo App)** احترافي، مع حفظ تلقائي للبيانات. هذا المشروع سيعلمك: - التعامل مع النماذج (Forms). - إنشاء عناصر ديناميكياً. - حفظ البيانات في `localStorage`. - تعديل العناصر وحذفها. - تصفية المهام (الكل، النشطة، المكتملة). ## ما سنبنيه تطبيق قائمة مهام بوظائف: - **إضافة مهمة جديدة.** - **تعديل مهمة موجودة.** - **حذف مهمة.** - **تعليم مهمة كمكتملة.** - **تصفية المهام (الكل، النشطة، المكتملة).** - **حذف المهام المكتملة.** - **حفظ تلقائي في المتصفح.** ## هيكل المشروع سننشئ 3 ملفات: ``` todo-app/ ├── index.html ├── style.css └── script.js ``` ## الخطوة 1: HTML — هيكل التطبيق **`index.html`:** ```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`:** ```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`:** ```javascript // ==================== المتغيرات ==================== 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. هيكل البيانات ```javascript const todo = { id: Date.now(), // معرف فريد text: "نص المهمة", // نص المهمة completed: false, // هل مكتملة؟ createdAt: "...", // تاريخ الإنشاء }; ``` ### 2. حفظ البيانات في localStorage ```javascript function saveTodos() { localStorage.setItem("todos", JSON.stringify(todos)); } function loadTodos() { const stored = localStorage.getItem("todos"); if (stored) todos = JSON.parse(stored); } ``` **الفائدة:** البيانات تبقى حتى بعد إغلاق المتصفح. ### 3. إضافة مهمة ```javascript function addTodo(text) { todos.push({ id: Date.now(), text: text.trim(), completed: false, }); saveTodos(); render(); } ``` ### 4. الفلترة ```javascript 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 ```javascript 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 ```javascript function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } ``` **الفائدة:** منع تنفيذ أكواد ضارة من المستخدم. ## تمارين إضافية ### تمرين 1: إضافة أولوية للمهمة أضف خيار "أولوية" (عالية، متوسطة، منخفضة). **الحل:** ```javascript // في addTodo: todos.push({ id: Date.now(), text: text.trim(), priority: "متوسطة", // الافتراضي completed: false, }); // في HTML للعرض: // <span class="priority-${todo.priority}">${todo.priority}</span> ``` ### تمرين 2: إضافة تاريخ استحقاق أضف تاريخ استحقاق لكل مهمة. **الحل:** ```javascript todos.push({ id: Date.now(), text: text.trim(), dueDate: new Date().toISOString(), completed: false, }); // عرض التاريخ في القائمة ``` ### تمرين 3: البحث في المهام أضف حقل بحث لتصفية المهام. **الحل:** ```html <input type="text" id="searchInput" placeholder="ابحث..."> ``` ```javascript 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`، اجعل النص قابلاً للتعديل مباشرة. **الحل:** ```javascript 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: الإشعارات أضف إشعار عند إكمال مهمة. **الحل:** ```javascript 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` مع بيانات المستخدم مباشرة. **الحل:** ```javascript // خطأ: todoList.innerHTML = `<span>${todo.text}</span>`; // صحيح: todoList.innerHTML = `<span>${escapeHtml(todo.text)}</span>`; ``` ### 🔴 المشكلة 3: `Date.now()` ينتج نفس المعرف **السبب:** إضافة مهام في نفس المللي ثانية. **الحل:** استخدم `crypto.randomUUID()`: ```javascript id: crypto.randomUUID(), ``` ### 🔴 المشكلة 4: المهام لا تظهر بعد الإضافة **السبب:** نسيت استدعاء `render()`. **الحل:** تأكد من استدعاء `render()` بعد كل تعديل. ## قائمة تحقق نهائية <table> <thead> <tr> <th>المهمة</th> <th>الحالة</th> </tr> </thead> <tbody> <tr> <td>إنشاء ملف index.html</td> <td>⬜</td> </tr> <tr> <td>إنشاء ملف style.css</td> <td>⬜</td> </tr> <tr> <td>إنشاء ملف script.js</td> <td>⬜</td> </tr> <tr> <td>تشغيل التطبيق في المتصفح</td> <td>⬜</td> </tr> <tr> <td>اختبار إضافة مهمة</td> <td>⬜</td> </tr> <tr> <td>اختبار تعديل وحذف مهمة</td> <td>⬜</td> </tr> <tr> <td>اختبار الفلاتر</td> <td>⬜</td> </tr> <tr> <td>التأكد من حفظ البيانات بعد التحديث</td> <td>⬜</td> </tr> <tr> <td>حل تمرين واحد على الأقل إضافي</td> <td>⬜</td> </tr> </tbody> </table> ## ماذا بعد هذا المشروع؟ 🎉 **مبروك! لقد أكملت مسار JavaScript بالكامل!** الآن أنت جاهز لـ: 1. **تطوير مشاريع أكثر تعقيداً:** مدونات، متاجر إلكترونية، تطبيقات. 2. **تعلم أطر العمل:** React، Vue، Angular. 3. **تعلم Backend:** Node.js، Express. 4. **تعلم قواعد البيانات:** MongoDB، PostgreSQL. 5. **بناء معرض أعمالك:** ارفع مشاريعك على GitHub. ## الخلاصة في هذا المشروع، تعلمت: - ✅ **إدارة الحالة** — تخزين المهام في مصفوفة. - ✅ **localStorage** — حفظ البيانات في المتصفح. - ✅ **DOM المتقدم** — إنشاء وتعديل العناصر. - ✅ **Event Delegation** — مستمع واحد للعناصر المتعددة. - ✅ **الفلترة** — عرض المهام حسب الحالة. - ✅ **حماية XSS** — تنظيف النصوص. - ✅ **تصميم تفاعلي** — CSS مع animations. **هذا المشروع هو أساس كل تطبيقات المهام الاحترافية (مثل Todoist و Microsoft To Do).** احتفظ بالكود، وطور فيه بنفسك! **🎉 هل أكملت مسار JavaScript بالكامل؟ شاركنا في التعليقات!**