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 بالكامل؟ شاركنا في التعليقات!**