React

مشروع React متكامل — تطبيق متجر إلكتروني 2026

📅 2026-10-19⏱ 13 دقائق قراءة
🎉 **مبروك! وصلت إلى المقال الأخير في مسار React!** لقد تعلمت كل الأساسيات: JSX، المكونات، State، الأحداث، useEffect، القوائم، Router، و Context API. الآن حان الوقت لتطبيق كل هذه المفاهيم في **مشروع حقيقي متكامل**. في هذا الدليل العملي، سنبني **متجراً إلكترونياً كاملاً** بـ React و TypeScript. ## ما سنبنيه متجر إلكتروني بوظائف: - **عرض المنتجات** في شبكة أنيقة. - **البحث** في المنتجات. - **التصفية** حسب الفئة والسعر. - **سلة التسوق** مع إضافة/حذف. - **صفحة تفاصيل المنتج**. - **حفظ السلة** في localStorage. - **عدد المنتجات** في السلة (Header). - **إجمالي السعر** مع الضريبة. ## هيكل المشروع ``` react-shop/ ├── src/ │ ├── components/ │ │ ├── Header.tsx │ │ ├── ProductCard.tsx │ │ ├── ProductGrid.tsx │ │ ├── SearchBar.tsx │ │ ├── Filters.tsx │ │ └── CartSidebar.tsx │ ├── contexts/ │ │ └── CartContext.tsx │ ├── pages/ │ │ ├── Home.tsx │ │ ├── ProductDetail.tsx │ │ └── NotFound.tsx │ ├── data/ │ │ └── products.ts │ ├── types/ │ │ └── index.ts │ ├── App.tsx │ ├── main.tsx │ └── index.css ``` ## الخطوة 1: إنشاء المشروع ```bash npm create vite@latest react-shop -- --template react-ts cd react-shop npm install npm install react-router-dom npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` ## الخطوة 2: إعداد Tailwind CSS **`tailwind.config.js`:** ```javascript export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {} }, plugins: [], }; ``` **`src/index.css`:** ```css @tailwind base; @tailwind components; @tailwind utilities; body { font-family: 'Cairo', sans-serif; direction: rtl; background: #f9fafb; } ``` ## الخطوة 3: تعريف الأنواع **`src/types/index.ts`:** ```typescript export interface Product { id: number; name: string; description: string; price: number; image: string; category: string; rating: number; inStock: boolean; } export interface CartItem extends Product { quantity: number; } export type Category = "الكل" | "إلكترونيات" | "ملابس" | "كتب" | "منزل"; ``` ## الخطوة 4: بيانات المنتجات **`src/data/products.ts`:** ```typescript import { Product } from "../types"; export const products: Product[] = [ { id: 1, name: "لابتوب Dell XPS 15", description: "لابتوب احترافي بشاشة 15 بوصة ومعالج i7", price: 5500, image: "https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=400", category: "إلكترونيات", rating: 4.8, inStock: true, }, { id: 2, name: "iPhone 15 Pro", description: "أحدث هاتف من Apple بكاميرا احترافية", price: 4200, image: "https://images.unsplash.com/photo-1592750475338-74b7b21085ab?w=400", category: "إلكترونيات", rating: 4.9, inStock: true, }, { id: 3, name: "قميص قطني", description: "قميص رجالي قطني 100%", price: 120, image: "https://images.unsplash.com/photo-1596755094514-f87e34085b2c?w=400", category: "ملابس", rating: 4.5, inStock: true, }, { id: 4, name: "كتاب تعلم React", description: "دليل شامل لتعلم React من الصفر", price: 85, image: "https://images.unsplash.com/photo-1544947950-fa07a98d237f?w=400", category: "كتب", rating: 4.7, inStock: true, }, { id: 5, name: "مصباح طاولة", description: "مصباح حديث بتصميم أنيق", price: 250, image: "https://images.unsplash.com/photo-1507473885765-e6ed057f782c?w=400", category: "منزل", rating: 4.3, inStock: false, }, { id: 6, name: "سماعات Sony WH-1000XM5", description: "سماعات لاسلكية مع إلغاء الضوضاء", price: 1800, image: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=400", category: "إلكترونيات", rating: 4.9, inStock: true, }, { id: 7, name: "حقيبة جلد", description: "حقيبة ظهر جلدية عملية", price: 450, image: "https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=400", category: "ملابس", rating: 4.6, inStock: true, }, { id: 8, name: "ساعة ذكية", description: "ساعة ذكية بميزات صحية متقدمة", price: 1200, image: "https://images.unsplash.com/photo-1546868871-7041f2a55e12?w=400", category: "إلكترونيات", rating: 4.4, inStock: true, }, ]; ``` ## الخطوة 5: Cart Context **`src/contexts/CartContext.tsx`:** ```tsx import { createContext, useContext, useEffect, useState, ReactNode } from "react"; import { CartItem, Product } from "../types"; interface CartContextType { items: CartItem[]; addItem: (product: Product) => void; removeItem: (id: number) => void; updateQuantity: (id: number, quantity: number) => void; clearCart: () => void; total: number; itemCount: number; } const CartContext = createContext<CartContextType | undefined>(undefined); export function CartProvider({ children }: { children: ReactNode }) { const [items, setItems] = useState<CartItem[]>(() => { try { const stored = localStorage.getItem("cart"); return stored ? JSON.parse(stored) : []; } catch { return []; } }); useEffect(() => { localStorage.setItem("cart", JSON.stringify(items)); }, [items]); const addItem = (product: Product) => { setItems((prev) => { const existing = prev.find((item) => item.id === product.id); if (existing) { return prev.map((item) => item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item ); } return [...prev, { ...product, quantity: 1 }]; }); }; const removeItem = (id: number) => { setItems((prev) => prev.filter((item) => item.id !== id)); }; const updateQuantity = (id: number, quantity: number) => { if (quantity <= 0) { removeItem(id); return; } setItems((prev) => prev.map((item) => (item.id === id ? { ...item, quantity } : item)) ); }; const clearCart = () => setItems([]); const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0); const itemCount = items.reduce((sum, item) => sum + item.quantity, 0); return ( <CartContext.Provider value={{ items, addItem, removeItem, updateQuantity, clearCart, total, itemCount }} > {children} </CartContext.Provider> ); } export function useCart() { const context = useContext(CartContext); if (!context) throw new Error("useCart must be used within CartProvider"); return context; } ``` ## الخطوة 6: Header مع السلة **`src/components/Header.tsx`:** ```tsx import { Link } from "react-router-dom"; import { useCart } from "../contexts/CartContext"; interface HeaderProps { onToggleCart: () => void; } export function Header({ onToggleCart }: HeaderProps) { const { itemCount } = useCart(); return ( <header className="bg-white shadow-sm sticky top-0 z-40"> <div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between"> <Link to="/" className="text-2xl font-bold text-gray-900"> 🛒 متجري </Link> <button onClick={onToggleCart} className="relative bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition" > السلة 🛍️ {itemCount > 0 && ( <span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs w-6 h-6 rounded-full flex items-center justify-center font-bold"> {itemCount} </span> )} </button> </div> </header> ); } ``` ## الخطوة 7: ProductCard **`src/components/ProductCard.tsx`:** ```tsx import { Link } from "react-router-dom"; import { Product } from "../types"; import { useCart } from "../contexts/CartContext"; export function ProductCard({ product }: { product: Product }) { const { addItem } = useCart(); return ( <div className="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-lg transition border border-gray-100 group"> <Link to={`/product/${product.id}`}> <div className="relative h-48 overflow-hidden"> <img src={product.image} alt={product.name} className="w-full h-full object-cover group-hover:scale-105 transition duration-300" /> {!product.inStock && ( <div className="absolute inset-0 bg-black/60 flex items-center justify-center"> <span className="text-white font-bold">غير متوفر</span> </div> )} </div> </Link> <div className="p-4"> <span className="inline-block bg-blue-100 text-blue-700 text-xs px-2 py-1 rounded-full mb-2"> {product.category} </span> <Link to={`/product/${product.id}`}> <h3 className="font-semibold text-gray-900 mb-1 hover:text-blue-600 transition"> {product.name} </h3> </Link> <div className="flex items-center gap-1 mb-3 text-sm text-yellow-500"> ⭐ {product.rating} </div> <div className="flex items-center justify-between"> <span className="text-lg font-bold text-blue-600">{product.price} $</span> <button onClick={() => addItem(product)} disabled={!product.inStock} className="bg-blue-600 text-white px-3 py-1 rounded-lg text-sm hover:bg-blue-700 transition disabled:opacity-50 disabled:cursor-not-allowed" > أضف 🛒 </button> </div> </div> </div> ); } ``` ## الخطوة 8: البحث والتصفية **`src/components/SearchBar.tsx`:** ```tsx interface SearchBarProps { value: string; onChange: (value: string) => void; } export function SearchBar({ value, onChange }: SearchBarProps) { return ( <div className="relative"> <input type="text" value={value} onChange={(e) => onChange(e.target.value)} placeholder="ابحث عن منتج..." className="w-full px-4 py-3 pr-10 rounded-xl border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500" /> <span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400"> 🔍 </span> </div> ); } ``` **`src/components/Filters.tsx`:** ```tsx import { Category } from "../types"; interface FiltersProps { category: Category; onCategoryChange: (cat: Category) => void; sortBy: "default" | "price-asc" | "price-desc"; onSortChange: (sort: "default" | "price-asc" | "price-desc") => void; } const categories: Category[] = ["الكل", "إلكترونيات", "ملابس", "كتب", "منزل"]; export function Filters({ category, onCategoryChange, sortBy, onSortChange }: FiltersProps) { return ( <div className="flex flex-wrap gap-4 mb-6"> <div className="flex flex-wrap gap-2"> {categories.map((cat) => ( <button key={cat} onClick={() => onCategoryChange(cat)} className={`px-4 py-2 rounded-lg transition ${ category === cat ? "bg-blue-600 text-white" : "bg-white text-gray-700 border border-gray-200 hover:border-blue-300" }`} > {cat} </button> ))} </div> <select value={sortBy} onChange={(e) => onSortChange(e.target.value as any)} className="px-4 py-2 rounded-lg border border-gray-200" > <option value="default">ترتيب افتراضي</option> <option value="price-asc">السعر: من الأقل</option> <option value="price-desc">السعر: من الأعلى</option> </select> </div> ); } ``` ## الخطوة 9: الصفحة الرئيسية **`src/pages/Home.tsx`:** ```tsx import { useMemo, useState } from "react"; import { ProductCard } from "../components/ProductCard"; import { SearchBar } from "../components/SearchBar"; import { Filters } from "../components/Filters"; import { products } from "../data/products"; import { Category } from "../types"; export function Home() { const [search, setSearch] = useState(""); const [category, setCategory] = useState<Category>("الكل"); const [sortBy, setSortBy] = useState<"default" | "price-asc" | "price-desc">("default"); const filtered = useMemo(() => { let result = [...products]; // البحث if (search) { result = result.filter((p) => p.name.toLowerCase().includes(search.toLowerCase()) ); } // التصفية if (category !== "الكل") { result = result.filter((p) => p.category === category); } // الترتيب if (sortBy === "price-asc") { result.sort((a, b) => a.price - b.price); } else if (sortBy === "price-desc") { result.sort((a, b) => b.price - a.price); } return result; }, [search, category, sortBy]); return ( <main className="max-w-7xl mx-auto px-4 py-8"> <h1 className="text-3xl font-bold text-gray-900 mb-6"> 🛍️ تصفح المنتجات </h1> <div className="mb-6"> <SearchBar value={search} onChange={setSearch} /> </div> <Filters category={category} onCategoryChange={setCategory} sortBy={sortBy} onSortChange={setSortBy} /> {filtered.length === 0 ? ( <div className="text-center py-16"> <span className="text-6xl mb-4 inline-block">🔍</span> <p className="text-gray-500">لا توجد منتجات مطابقة</p> </div> ) : ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> {filtered.map((product) => ( <ProductCard key={product.id} product={product} /> ))} </div> )} </main> ); } ``` ## الخطوة 10: صفحة تفاصيل المنتج **`src/pages/ProductDetail.tsx`:** ```tsx import { useParams, Link } from "react-router-dom"; import { products } from "../data/products"; import { useCart } from "../contexts/CartContext"; export function ProductDetail() { const { id } = useParams<{ id: string }>(); const product = products.find((p) => p.id === Number(id)); const { addItem } = useCart(); if (!product) { return ( <div className="max-w-4xl mx-auto px-4 py-16 text-center"> <h1 className="text-3xl font-bold text-gray-900 mb-4">المنتج غير موجود</h1> <Link to="/" className="text-blue-600 hover:underline"> العودة للمنتجات </Link> </div> ); } return ( <main className="max-w-6xl mx-auto px-4 py-8"> <Link to="/" className="text-blue-600 hover:underline mb-6 inline-block"> ← العودة للمنتجات </Link> <div className="grid md:grid-cols-2 gap-8 bg-white rounded-2xl p-8 shadow-sm"> <div> <img src={product.image} alt={product.name} className="w-full h-96 object-cover rounded-xl" /> </div> <div> <span className="inline-block bg-blue-100 text-blue-700 text-sm px-3 py-1 rounded-full mb-4"> {product.category} </span> <h1 className="text-3xl font-bold text-gray-900 mb-4"> {product.name} </h1> <div className="flex items-center gap-2 mb-4 text-yellow-500"> ⭐ {product.rating} / 5 </div> <p className="text-gray-600 leading-relaxed mb-6"> {product.description} </p> <p className="text-4xl font-bold text-blue-600 mb-6"> {product.price} $ </p> <button onClick={() => addItem(product)} disabled={!product.inStock} className="w-full bg-blue-600 text-white py-3 rounded-xl font-bold hover:bg-blue-700 transition disabled:opacity-50" > {product.inStock ? "🛒 أضف للسلة" : "غير متوفر"} </button> </div> </div> </main> ); } ``` ## الخطوة 11: شريط جانبي للسلة **`src/components/CartSidebar.tsx`:** ```tsx import { useCart } from "../contexts/CartContext"; interface CartSidebarProps { isOpen: boolean; onClose: () => void; } export function CartSidebar({ isOpen, onClose }: CartSidebarProps) { const { items, updateQuantity, removeItem, total, clearCart } = useCart(); const TAX_RATE = 0.15; const tax = total * TAX_RATE; const finalTotal = total + tax; if (!isOpen) return null; return ( <> {/* Overlay */} <div className="fixed inset-0 bg-black/60 z-50" onClick={onClose} /> {/* Sidebar */} <div className="fixed top-0 left-0 h-full w-full max-w-md bg-white z-50 shadow-2xl flex flex-col"> <div className="p-4 border-b flex items-center justify-between"> <h2 className="text-xl font-bold">🛍️ سلة التسوق</h2> <button onClick={onClose} className="text-gray-500 hover:text-gray-700 text-2xl" > ✕ </button> </div> <div className="flex-1 overflow-y-auto p-4"> {items.length === 0 ? ( <div className="text-center py-16"> <span className="text-6xl mb-4 inline-block">🛒</span> <p className="text-gray-500">السلة فارغة</p> </div> ) : ( <ul className="space-y-4"> {items.map((item) => ( <li key={item.id} className="flex gap-4 bg-gray-50 p-3 rounded-xl"> <img src={item.image} alt={item.name} className="w-20 h-20 object-cover rounded-lg" /> <div className="flex-1"> <h3 className="font-semibold text-sm mb-1">{item.name}</h3> <p className="text-blue-600 font-bold text-sm"> {item.price} $ </p> <div className="flex items-center gap-2 mt-2"> <button onClick={() => updateQuantity(item.id, item.quantity - 1)} className="w-7 h-7 bg-gray-200 rounded-full" > - </button> <span className="font-bold">{item.quantity}</span> <button onClick={() => updateQuantity(item.id, item.quantity + 1)} className="w-7 h-7 bg-gray-200 rounded-full" > + </button> <button onClick={() => removeItem(item.id)} className="text-red-500 mr-auto" > 🗑️ </button> </div> </div> </li> ))} </ul> )} </div> {items.length > 0 && ( <div className="p-4 border-t"> <div className="space-y-2 mb-4 text-sm"> <div className="flex justify-between"> <span>المجموع:</span> <span>{total.toFixed(2)} $</span> </div> <div className="flex justify-between text-gray-500"> <span>الضريبة (15%):</span> <span>{tax.toFixed(2)} $</span> </div> <div className="flex justify-between text-lg font-bold border-t pt-2"> <span>الإجمالي:</span> <span className="text-blue-600">{finalTotal.toFixed(2)} $</span> </div> </div> <button onClick={() => { alert("تم الطلب بنجاح!"); clearCart(); onClose(); }} className="w-full bg-blue-600 text-white py-3 rounded-xl font-bold hover:bg-blue-700 transition mb-2" > ✅ إتمام الطلب </button> <button onClick={clearCart} className="w-full text-red-600 py-2 text-sm hover:bg-red-50 rounded-lg transition" > إفراغ السلة </button> </div> )} </div> </> ); } ``` ## الخطوة 12: App.tsx **`src/App.tsx`:** ```tsx import { useState } from "react"; import { Routes, Route } from "react-router-dom"; import { Header } from "./components/Header"; import { CartSidebar } from "./components/CartSidebar"; import { Home } from "./pages/Home"; import { ProductDetail } from "./pages/ProductDetail"; function App() { const [isCartOpen, setIsCartOpen] = useState(false); return ( <div className="min-h-screen"> <Header onToggleCart={() => setIsCartOpen(true)} /> <Routes> <Route path="/" element={<Home />} /> <Route path="/product/:id" element={<ProductDetail />} /> </Routes> <CartSidebar isOpen={isCartOpen} onClose={() => setIsCartOpen(false)} /> <footer className="bg-gray-900 text-white text-center py-6 mt-16"> <p>© 2026 متجري — مشروع React متكامل</p> </footer> </div> ); } export default App; ``` ## الخطوة 13: main.tsx **`src/main.tsx`:** ```tsx import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import { CartProvider } from "./contexts/CartContext"; import App from "./App"; import "./index.css"; createRoot(document.getElementById("root")!).render( <StrictMode> <BrowserRouter> <CartProvider> <App /> </CartProvider> </BrowserRouter> </StrictMode> ); ``` ## 🎉 تشغيل المشروع ```bash npm run dev ``` افتح: `http://localhost:5173` **🎉 مبروك! لديك متجر إلكتروني كامل!** ## ما تعلمته في هذا المشروع في هذا المشروع، طبقت: - ✅ **React Router** — التنقل بين الصفحات. - ✅ **Context API** — إدارة السلة عالمياً. - ✅ **useState** — للحالة المحلية. - ✅ **useEffect** — لحفظ السلة في localStorage. - ✅ **useMemo** — لتحسين أداء التصفية. - ✅ **TypeScript** — للأنواع الآمنة. - ✅ **Tailwind CSS** — لتصميم جميل. - ✅ **Props** — لتمرير البيانات. - ✅ **القوائم** — لعرض المنتجات. - ✅ **الشروط** — لعرض "غير متوفر". ## تمارين إضافية ### تمرين 1: إضافة صفحة "من نحن" أضف صفحة `/about` بتصميم بسيط. ### تمرين 2: تصفية حسب السعر أضف نطاق سعر (min - max) للتصفية. ### تمرين 3: تقييم المنتج أضف إمكانية تقييم المنتجات (نجوم). ### تمرين 4: المفضلة أضف زر "المفضلة" لكل منتج. ### تمرين 5: Dark Mode أضف وضعاً داكناً مع Context. ## حل المشاكل الشائعة ### 🔴 المشكلة 1: `useCart must be used within CartProvider` **الحل:** تأكد من تغليف App بـ `<CartProvider>`. ### 🔴 المشكلة 2: الصفحة تُحدّث عند التنقل **الحل:** تأكد من استخدام `<Link>` وليس `<a>`. ### 🔴 المشكلة 3: 404 في الإنتاج **الحل:** أضف `rewrites` في `firebase.json`: ```json "rewrites": [{ "source": "**", "destination": "/index.html" }] ``` ### 🔴 المشكلة 4: السلة لا تُحفظ **السبب:** localStorage لا يعمل في SSR. **الحل:** استخدم `useState` مع lazy initialization. ## 🎉 مبروك! أكملت مسار React! لقد وصلت إلى نهاية المسار. أنت الآن تعرف: - ✅ **أساسيات React:** JSX، المكونات، Props. - ✅ **إدارة الحالة:** useState، useEffect. - ✅ **الأحداث والنماذج.** - ✅ **القوائم والشروط.** - ✅ **React Router.** - ✅ **Context API.** - ✅ **مشروع متكامل.** ## ماذا بعد React؟ الآن أنت جاهز لـ: 1. **Next.js** — إطار عمل كامل (SSR, SSG). 2. **Redux Toolkit** — إدارة حالة متقدمة. 3. **React Query** — جلب البيانات. 4. **TypeScript متقدم.** 5. **بناء معرض أعمالك** — ارفع مشاريعك على GitHub. ## الخلاصة في هذا المشروع، بنيت: - ✅ **متجر إلكتروني كامل.** - ✅ **بحث وتصفية.** - ✅ **سلة تسوق مع حفظ تلقائي.** - ✅ **صفحة تفاصيل المنتج.** - ✅ **حساب الضريبة والإجمالي.** **هذا المشروع هو أساس كل تطبيقات التجارة الإلكترونية.** احتفظ بالكود، وطور فيه بنفسك! **🎉 هل أكملت مسار React بالكامل؟ شاركنا في التعليقات!**