أساسيات React.js

بناء مشروع React متكامل

20 دقيقة الدرس 39 من 40

نظرة عامة على المشروع: تطبيق مدير المهام

في هذا الدرس، سنبني تطبيق مدير مهام كامل وجاهز للإنتاج يوضح جميع المفاهيم التي تعلمناها. سيتضمن هذا التطبيق عمليات CRUD والتوجيه وإدارة الحالة وتكامل API والمصادقة وأنماط React الحديثة.

الميزات التي سنبنيها

ميزات التطبيق:
  • مصادقة المستخدم (تسجيل الدخول/التسجيل/تسجيل الخروج)
  • إنشاء وقراءة وتحديث وحذف المهام
  • تصفية المهام حسب الحالة (الكل، نشط، مكتمل)
  • البحث عن المهام بالعنوان
  • وضع علامة على المهام كمكتملة/غير مكتملة
  • بيانات دائمة مع API
  • مسارات محمية
  • حالات التحميل ومعالجة الأخطاء
  • تصميم متجاوب

هيكل المشروع

task-manager/ ├── public/ ├── src/ │ ├── components/ │ │ ├── common/ │ │ │ ├── Button.jsx │ │ │ ├── Input.jsx │ │ │ └── Spinner.jsx │ │ ├── layout/ │ │ │ ├── Header.jsx │ │ │ └── Layout.jsx │ │ └── tasks/ │ │ ├── TaskList.jsx │ │ ├── TaskItem.jsx │ │ ├── TaskForm.jsx │ │ └── TaskFilters.jsx │ ├── features/ │ │ └── auth/ │ │ ├── Login.jsx │ │ ├── Register.jsx │ │ └── useAuth.js │ ├── hooks/ │ │ ├── useLocalStorage.js │ │ └── useTasks.js │ ├── services/ │ │ ├── api.js │ │ └── taskService.js │ ├── context/ │ │ ├── AuthContext.jsx │ │ └── TaskContext.jsx │ ├── routes/ │ │ ├── ProtectedRoute.jsx │ │ └── index.jsx │ ├── App.jsx │ └── index.jsx └── package.json

الخطوة 1: إعداد المشروع

# إنشاء مشروع مع Vite npm create vite@latest task-manager -- --template react cd task-manager npm install # تثبيت التبعيات npm install react-router-dom axios npm install -D tailwindcss postcss autoprefixer # تهيئة Tailwind npx tailwindcss init -p

الخطوة 2: إعداد خدمة API

// src/services/api.js import axios from 'axios'; const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; const api = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); // إضافة رمز المصادقة إلى الطلبات api.interceptors.request.use((config) => { const token = localStorage.getItem('authToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // معالجة انتهاء صلاحية الرمز api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { localStorage.removeItem('authToken'); window.location.href = '/login'; } return Promise.reject(error); } ); export default api;
// src/services/taskService.js import api from './api'; export const taskService = { // الحصول على جميع المهام getAllTasks: async () => { const response = await api.get('/tasks'); return response.data; }, // الحصول على مهمة بواسطة ID getTaskById: async (id) => { const response = await api.get(`/tasks/${id}`); return response.data; }, // إنشاء مهمة جديدة createTask: async (taskData) => { const response = await api.post('/tasks', taskData); return response.data; }, // تحديث مهمة updateTask: async (id, taskData) => { const response = await api.put(`/tasks/${id}`, taskData); return response.data; }, // حذف مهمة deleteTask: async (id) => { const response = await api.delete(`/tasks/${id}`); return response.data; }, // تبديل اكتمال المهمة toggleTask: async (id) => { const response = await api.patch(`/tasks/${id}/toggle`); return response.data; }, };

الخطوة 3: سياق المصادقة

// src/context/AuthContext.jsx import { createContext, useContext, useState, useEffect } from 'react'; import api from '../services/api'; const AuthContext = createContext(null); export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { // التحقق مما إذا كان المستخدم مسجلاً الدخول const token = localStorage.getItem('authToken'); if (token) { fetchUser(); } else { setLoading(false); } }, []); const fetchUser = async () => { try { const response = await api.get('/auth/me'); setUser(response.data); } catch (error) { localStorage.removeItem('authToken'); } finally { setLoading(false); } }; const login = async (email, password) => { const response = await api.post('/auth/login', { email, password }); const { token, user } = response.data; localStorage.setItem('authToken', token); setUser(user); return user; }; const register = async (name, email, password) => { const response = await api.post('/auth/register', { name, email, password, }); const { token, user } = response.data; localStorage.setItem('authToken', token); setUser(user); return user; }; const logout = () => { localStorage.removeItem('authToken'); setUser(null); }; const value = { user, loading, login, register, logout, isAuthenticated: !!user, }; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; } export const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth يجب استخدامه داخل AuthProvider'); } return context; };

الخطوة 4: سياق المهام مع عمليات CRUD

// src/context/TaskContext.jsx import { createContext, useContext, useState, useEffect } from 'react'; import { taskService } from '../services/taskService'; import { useAuth } from './AuthContext'; const TaskContext = createContext(null); export function TaskProvider({ children }) { const { isAuthenticated } = useAuth(); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [filter, setFilter] = useState('all'); // all, active, completed const [searchQuery, setSearchQuery] = useState(''); useEffect(() => { if (isAuthenticated) { loadTasks(); } }, [isAuthenticated]); const loadTasks = async () => { setLoading(true); setError(null); try { const data = await taskService.getAllTasks(); setTasks(data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; const addTask = async (taskData) => { try { const newTask = await taskService.createTask(taskData); setTasks((prev) => [newTask, ...prev]); return newTask; } catch (err) { setError(err.message); throw err; } }; const updateTask = async (id, taskData) => { try { const updated = await taskService.updateTask(id, taskData); setTasks((prev) => prev.map((task) => (task.id === id ? updated : task))); return updated; } catch (err) { setError(err.message); throw err; } }; const deleteTask = async (id) => { try { await taskService.deleteTask(id); setTasks((prev) => prev.filter((task) => task.id !== id)); } catch (err) { setError(err.message); throw err; } }; const toggleTask = async (id) => { try { const updated = await taskService.toggleTask(id); setTasks((prev) => prev.map((task) => (task.id === id ? updated : task))); } catch (err) { setError(err.message); throw err; } }; // المهام المفلترة بناءً على الفلتر والبحث const filteredTasks = tasks .filter((task) => { if (filter === 'active') return !task.completed; if (filter === 'completed') return task.completed; return true; }) .filter((task) => task.title.toLowerCase().includes(searchQuery.toLowerCase()) ); const value = { tasks: filteredTasks, loading, error, filter, setFilter, searchQuery, setSearchQuery, addTask, updateTask, deleteTask, toggleTask, refreshTasks: loadTasks, }; return <TaskContext.Provider value={value}>{children}</TaskContext.Provider>; } export const useTasks = () => { const context = useContext(TaskContext); if (!context) { throw new Error('useTasks يجب استخدامه داخل TaskProvider'); } return context; };

الخطوة 5: مكون المسار المحمي

// src/routes/ProtectedRoute.jsx import { Navigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import Spinner from '../components/common/Spinner'; export default function ProtectedRoute({ children }) { const { isAuthenticated, loading } = useAuth(); if (loading) { return ( <div className="flex items-center justify-center min-h-screen"> <Spinner /> </div> ); } if (!isAuthenticated) { return <Navigate to="/login" replace />; } return children; }

الخطوة 6: مكونات المهام

// src/components/tasks/TaskForm.jsx import { useState } from 'react'; import { useTasks } from '../../context/TaskContext'; import Button from '../common/Button'; import Input from '../common/Input'; export default function TaskForm() { const { addTask } = useTasks(); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); if (!title.trim()) return; setLoading(true); try { await addTask({ title: title.trim(), description: description.trim(), completed: false, }); setTitle(''); setDescription(''); } catch (error) { console.error('فشل إضافة المهمة:', error); } finally { setLoading(false); } }; return ( <form onSubmit={handleSubmit} className="space-y-4 mb-6"> <Input type="text" placeholder="عنوان المهمة..." value={title} onChange={(e) => setTitle(e.target.value)} disabled={loading} /> <textarea className="w-full px-4 py-2 border rounded-lg" placeholder="الوصف (اختياري)..." value={description} onChange={(e) => setDescription(e.target.value)} disabled={loading} rows={3} /> <Button type="submit" disabled={loading || !title.trim()}> {loading ? 'جاري الإضافة...' : 'إضافة مهمة'} </Button> </form> ); }
// src/components/tasks/TaskItem.jsx import { useState } from 'react'; import { useTasks } from '../../context/TaskContext'; import Button from '../common/Button'; export default function TaskItem({ task }) { const { updateTask, deleteTask, toggleTask } = useTasks(); const [isEditing, setIsEditing] = useState(false); const [title, setTitle] = useState(task.title); const handleUpdate = async () => { if (title.trim() && title !== task.title) { await updateTask(task.id, { ...task, title: title.trim() }); } setIsEditing(false); }; const handleDelete = async () => { if (confirm('هل أنت متأكد من حذف هذه المهمة؟')) { await deleteTask(task.id); } }; return ( <div className="flex items-center gap-3 p-4 bg-white rounded-lg shadow"> <input type="checkbox" checked={task.completed} onChange={() => toggleTask(task.id)} className="w-5 h-5" /> {isEditing ? ( <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} onBlur={handleUpdate} onKeyPress={(e) => e.key === 'Enter' && handleUpdate()} className="flex-1 px-2 py-1 border rounded" autoFocus /> ) : ( <div className="flex-1"> <h3 className={`font-medium ${ task.completed ? 'line-through text-gray-500' : '' }`} > {task.title} </h3> {task.description && ( <p className="text-sm text-gray-600">{task.description}</p> )} </div> )} <div className="flex gap-2"> <Button variant="secondary" onClick={() => setIsEditing(!isEditing)}> {isEditing ? 'إلغاء' : 'تعديل'} </Button> <Button variant="danger" onClick={handleDelete}> حذف </Button> </div> </div> ); }

الخطوة 7: إعدادات التوجيه

// src/App.jsx import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { AuthProvider } from './context/AuthContext'; import { TaskProvider } from './context/TaskContext'; import ProtectedRoute from './routes/ProtectedRoute'; import Layout from './components/layout/Layout'; import Login from './features/auth/Login'; import Register from './features/auth/Register'; import Dashboard from './pages/Dashboard'; export default function App() { return ( <BrowserRouter> <AuthProvider> <TaskProvider> <Routes> <Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} /> <Route path="/dashboard" element={ <ProtectedRoute> <Layout> <Dashboard /> </Layout> </ProtectedRoute> } /> <Route path="/" element={<Navigate to="/dashboard" replace />} /> </Routes> </TaskProvider> </AuthProvider> </BrowserRouter> ); }
فوائد الهيكل:
  • فصل الاهتمامات: السياق للحالة، الخدمات لـ API، المكونات لواجهة المستخدم
  • قابلية إعادة الاستخدام: المكونات الشائعة مستخدمة في كل مكان
  • قابلية التوسع: سهولة إضافة ميزات جديدة
  • قابلية الصيانة: هيكل واضح، سهولة العثور على الكود
  • قابلية الاختبار: كل طبقة يمكن اختبارها بشكل مستقل
تمرين 1: قم بتوسيع مدير المهام بميزات إضافية:
  • أضف تواريخ الاستحقاق للمهام
  • نفّذ أولويات المهام (منخفضة، متوسطة، عالية)
  • أضف فئات/علامات المهام
  • نفّذ الفرز (حسب التاريخ، الأولوية، أبجدياً)
  • أضف ترقيم الصفحات لقوائم المهام الكبيرة
تمرين 2: حسّن تجربة المستخدم:
  • أضف تحديثات متفائلة (تحديث واجهة المستخدم قبل استجابة API)
  • نفّذ وظيفة التراجع للحذف
  • أضف إشعارات toast لرسائل النجاح/الخطأ
  • نفّذ السحب والإفلات لإعادة ترتيب المهام
  • أضف اختصارات لوحة المفاتيح (مثل Ctrl+N لمهمة جديدة)
تمرين 3: أضف الاختبار والنشر:
  • اكتب اختبارات الوحدة لموفري السياق
  • اكتب اختبارات التكامل لعمليات CRUD للمهام
  • قم بإعداد حدود الخطأ للتعامل الرشيق مع الأخطاء
  • قم بتكوين متغيرات البيئة لـ dev/staging/prod
  • انشر إلى Vercel أو Netlify