شارك المقالة

بناء متجر إلكتروني باستخدام React و Redux Toolkit خطوة بخطوة


اصنع متجر إلكتروني كامل بـ React و Redux Toolkit مع سلة مشتريات احترافية

في هذا المقال ستتعلم طريقة إنشاء متجر إلكتروني بسيط باستخدام React و Redux Toolkit و React Router و Bootstrap. سنقوم بعرض المنتجات داخل Cards، وإنشاء صفحة تفاصيل المنتج، وإضافة المنتجات إلى سلة المشتريات، مع حساب الكمية والإجمالي وحذف المنتجات من السلة بطريقة عملية ومنظمة.


شرح ملف تشغيل التطبيق الرئيسي في React

هذا الجزء هو نقطة البداية لتشغيل تطبيق React. يتم فيه استيراد ملفات Bootstrap الخاصة بالتصميم والتفاعل، ثم يتم تشغيل التطبيق داخل العنصر الذي يحمل المعرف root. كما يتم استخدام StrictMode للمساعدة في اكتشاف الأخطاء أثناء التطوير، واستخدام BrowserRouter لتفعيل التنقل بين الصفحات، بالإضافة إلى Provider لربط Redux بالتطبيق حتى تستطيع جميع المكونات الوصول إلى البيانات المشتركة مثل المنتجات والسلة.


import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App.jsx'
import './index.css'
import { Provider } from 'react-redux'
import { mainRTK } from './rtk/main_rtk.js'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <BrowserRouter>
      <Provider store={mainRTK()}>
        <App />
      </Provider>
    </BrowserRouter>
  </StrictMode>,
)

شرح ملف App وتنظيم صفحات التطبيق

هذا الجزء مسؤول عن تنظيم صفحات التطبيق وتحديد الصفحة التي يتم عرضها حسب الرابط الحالي. يتم عرض مكون Navbar في أعلى التطبيق بشكل ثابت، ثم يتم استخدام Routes و Route لتعريف مسارات الصفحات. فعند الدخول إلى الصفحة الرئيسية يظهر HomePage، وعند الدخول إلى صفحة السلة يظهر Cart، وعند اختيار منتج معين يتم عرض صفحة تفاصيل المنتج من خلال المسار الذي يحتوي على :id.


import { Route, Routes } from 'react-router-dom';
import Navbar from './componants/componants/Navbar';
import ProductDetail from './componants/ProductDetail';
import HomePage from './pages/HomePage';
import Sidebar from './pages/Sidebar';
import AddProduct from './componants/AddProductDetail';
import Cart from './pages/Cart';

function App() {
  return (
    <>
      <Navbar />

      <div className="container-fluid">
        <div className="col-11">
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/add-product" element={<AddProduct />} />
            <Route path="/products/:id" element={<ProductDetail />} />
            <Route path="/cart" element={<Cart />} />
          </Routes>
        </div>
      </div>
    </>
  )
}

export default App;


شرح ملف إعداد Redux Store في التطبيق

هذا الجزء مسؤول عن إنشاء وإعداد مخزن البيانات الرئيسي في Redux باستخدام configureStore من Redux Toolkit. يتم استيراد reducer الخاص بالمنتجات من ملف producr_slice، واستيراد reducer الخاص بالسلة من ملف cart_slice. بعد ذلك يتم إنشاء الدالة mainRTK التي ترجع store يحتوي على قسمين من البيانات: قسم products لإدارة المنتجات، وقسم cart لإدارة عناصر السلة.


import { configureStore } from "@reduxjs/toolkit";
import productReducer from "./slices/producr_slice";
import cartReducer from "./slices/cart_slice";

export const mainRTK = () => configureStore({
  reducer: {
    products: productReducer,
    cart: cartReducer,
  },
});

شرح ملف الصفحة الرئيسية HomePage

هذا الجزء يمثل الصفحة الرئيسية في التطبيق. يتم فيه استيراد مكون ProductSidebar، وهو المكون المسؤول عن عرض المنتجات داخل الصفحة. ثم يتم عرض هذا المكون داخل دالة HomePage، وبعدها يتم تصدير الصفحة حتى يمكن استخدامها داخل نظام المسارات في ملف App.


import ProductSidebar from "./ProductSidebar";

function HomePage() {
    return (
        <>
            <ProductSidebar />
        </>
    );
}

export default HomePage;

شرح ملف Slice الخاص بجلب المنتجات

هذا الجزء مسؤول عن إدارة بيانات المنتجات داخل Redux. يتم استخدام createAsyncThunk لإنشاء عملية غير متزامنة باسم fetechProducts تقوم بجلب المنتجات من API خارجي، ثم ترجع البيانات عند نجاح الطلب، أو ترجع رسالة الخطأ عند فشل الطلب. بعد ذلك يتم إنشاء productSlice وفيه الحالة الابتدائية التي تحتوي على قائمة المنتجات، وحالة التحميل، ورسالة الخطأ. يتم التعامل مع حالات الطلب الثلاث داخل extraReducers: أثناء التحميل، عند نجاح جلب البيانات، وعند حدوث خطأ.


import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";

export const fetechProducts = createAsyncThunk(
    "products/fetechProducts",
    async (id, thunkAPI) => {
        try {
            const res = await fetch(`https://fakestoreapi.com/products`);
            const data = await res.json();
            return data;
        } catch (error) {
            return thunkAPI.rejectWithValue(error.message);
        }
    }
);

export const productSlice = createSlice({
    name: "products",
    initialState: {
        products: [],
        loading: false,
        error: null,
    },
    reducers: {},
    extraReducers: (builder) => {
        builder.addCase(fetechProducts.pending, (state) => {
            state.loading = true;
        });
        builder.addCase(fetechProducts.fulfilled, (state, action) => {
            state.loading = false;
            state.products = action.payload;
        });
        builder.addCase(fetechProducts.rejected, (state, action) => {
            state.loading = false;
            state.error = action.payload;
        });
    }
});

export default productSlice.reducer;

شرح ملف Slice الخاص بسلة المشتريات

هذا الجزء مسؤول عن إدارة سلة المشتريات داخل Redux. الحالة الابتدائية هنا عبارة عن مصفوفة فارغة يتم تخزين المنتجات المضافة إليها. يحتوي cartSlice على ثلاث عمليات أساسية: addToCart لإضافة منتج إلى السلة أو زيادة كميته إذا كان موجودًا بالفعل، و removeFromCart لحذف منتج معين من السلة حسب رقم المنتج، و clearCart لحذف جميع المنتجات من السلة. في النهاية يتم تصدير العمليات لاستخدامها داخل المكونات، وتصدير reducer لربطه داخل store.


import { createSlice } from "@reduxjs/toolkit";

export const cartSlice = createSlice({
    name: "cart",
    initialState: [],
    reducers: {
        addToCart: (state, action) => {
            const item = state.find((item) => item.id === action.payload.id);
            if (item) {
                item.quantity += 1;
            } else {
                state.push({ ...action.payload, quantity: 1 });
            }
        },
        removeFromCart: (state, action) => {
            return state.filter((item) => item.id !== action.payload);
        },
        clearCart: () => [],
    },
});

export const { addToCart, removeFromCart, clearCart } = cartSlice.actions;

export default cartSlice.reducer;

شرح مكون عرض المنتجات ProductSidebar

هذا الجزء مسؤول عن عرض المنتجات داخل الصفحة على شكل بطاقات. يتم استخدام useSelector لجلب بيانات المنتجات وحالة التحميل والخطأ من Redux، ويتم استخدام useDispatch لإرسال الأوامر إلى Redux. عند تشغيل المكون لأول مرة يتم استدعاء fetechProducts داخل useEffect لجلب المنتجات من API. كما توجد دالة handleAddToCart لإضافة المنتج إلى السلة عند الضغط على زر Add To Cart. وفي حالة التحميل تظهر رسالة انتظار، وفي حالة وجود خطأ تظهر رسالة الخطأ، أما عند نجاح جلب البيانات فيتم عرض المنتجات داخل Cards تحتوي على الصورة والاسم والوصف والسعر وأزرار العرض والإضافة للسلة.


import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { fetechProducts } from "../rtk/slices/producr_slice";
import "./ProductSidebarDesign.css";
import { addToCart } from "../rtk/slices/cart_slice";

function ProductSidebar() {
    const { products, loading, error } = useSelector((state) => state.products);
    const dispatch = useDispatch();

    useEffect(() => {
        dispatch(fetechProducts());
    }, [dispatch]);

    const handleAddToCart = (product) => {
        dispatch(addToCart(product));
    };

    if (loading) {
        return <p className="text-center mt-5">Loading products...</p>;
    }

    if (error) {
        return <p className="text-center text-danger mt-5">Error: {error}</p>;
    }

    return (
        <div className="container mt-4">
            <div className="row g-4">
                {products.map((product) => (
                    <div className="col-md-4 col-lg-3" key={product.id}>
                        <div className="card product-card h-100">
                            <img
                                src={product.image}
                                className="card-img-top product-img"
                                style={{
                                    height: "350px",
                                    border: "none",
                                    borderRadius: "15px",
                                    overflow: "hidden",
                                    transition: "0.3s ease",
                                }}
                                alt={product.title}
                            />

                            <div className="card-body d-flex flex-column">
                                <h5 className="card-title">
                                    {product.title.slice(0, 35)}...
                                </h5>

                                <p className="card-text text-muted">
                                    {product.description.slice(0, 80)}...
                                </p>

                                <h6 className="price mb-3">${product.price}</h6>

                                <div className="mt-auto">
                                    <Link
                                        to={`/products/${product.id}`}
                                        state={{ product }}
                                        className="btn btn-info btn-sm me-2"
                                    >
                                        View
                                    </Link>

                                    <button 
                                        className="btn btn-primary btn-sm"
                                        onClick={() => handleAddToCart(product)}
                                    >
                                        Add To Cart
                                    </button>
                                </div>
                            </div>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    );
}

export default ProductSidebar;

شرح مكون سلة المشتريات Cart

هذا الجزء مسؤول عن عرض المنتجات الموجودة داخل سلة المشتريات. يتم جلب عناصر السلة من Redux باستخدام useSelector، ويتم استخدام useDispatch لتنفيذ عمليات الحذف. يتم حساب السعر الإجمالي باستخدام reduce عن طريق ضرب سعر كل منتج في الكمية الخاصة به. يحتوي المكون على دالة handleRemoveItem لحذف منتج معين من السلة، ودالة handleClearCart لحذف جميع المنتجات. إذا كانت السلة فارغة تظهر رسالة توضح ذلك، أما إذا كانت تحتوي على منتجات فيتم عرضها داخل جدول يحتوي على الصورة واسم المنتج والسعر والكمية والإجمالي وزر حذف لكل منتج، وفي نهاية الجدول يظهر السعر الكلي لجميع المنتجات.


import { useDispatch, useSelector } from "react-redux";
import { clearCart, removeFromCart } from "../rtk/slices/cart_slice";

function Cart() {
  const cartItems = useSelector((state) => state.cart);
  const dispatch = useDispatch();

  const totalPrice = cartItems.reduce((total, item) => {
    return total + item.price * item.quantity;
  }, 0);

  const handleRemoveItem = (id) => {
    dispatch(removeFromCart(id));
  };

  const handleClearCart = () => {
    dispatch(clearCart());
  };

  if (cartItems.length === 0) {
    return (
      <div
        className="container"
        style={{
          marginTop: "100px",
          textAlign: "center",
        }}
      >
        <h2 style={{
            color: "#333",
        }}
        >Your Cart is Empty</h2>
        <p className="text-muted">No products added to cart yet.</p>
      </div>
    );
  }

  return (
    <div
      className="container"
      style={{
        marginTop: "100px",
      }}
    >
      <div
        className="d-flex justify-content-between align-items-center mb-4"
      >
        <h2 style={{
            color: "#333",
        }}>Shopping Cart</h2>

        <button
          className="btn btn-danger"
          onClick={handleClearCart}
        >
          Clear All
        </button>
      </div>

      <div
        className="table-responsive"
        style={{
          boxShadow: "0 4px 15px rgba(0, 0, 0, 0.12)",
          borderRadius: "12px",
          overflow: "hidden",
        }}
      >
        <table className="table table-striped table-hover text-center align-middle mb-0">
          <thead className="table-dark">
            <tr>
              <th>Image</th>
              <th>Product Name</th>
              <th>Price</th>
              <th>Quantity</th>
              <th>Total</th>
              <th>Operation</th>
            </tr>
          </thead>

          <tbody>
            {cartItems.map((item) => (
              <tr key={item.id}>
                <td>
                  <img
                    src={item.image}
                    alt={item.title}
                    style={{
                      width: "70px",
                      height: "70px",
                      objectFit: "contain",
                    }}
                  />
                </td>

                <td
                  style={{
                    maxWidth: "250px",
                    fontWeight: "500",
                  }}
                >
                  {item.title}
                </td>

                <td>${item.price}</td>

                <td>{item.quantity}</td>

                <td>
                  ${(item.price * item.quantity).toFixed(2)}
                </td>

                <td>
                  <button
                    className="btn btn-sm btn-danger"
                    onClick={() => handleRemoveItem(item.id)}
                  >
                    Delete
                  </button>
                </td>
              </tr>
            ))}
          </tbody>

          <tfoot>
            <tr>
              <td colSpan="4" className="text-end fw-bold">
                Total Price:
              </td>

              <td className="fw-bold text-success">
                ${totalPrice.toFixed(2)}
              </td>

              <td></td>
            </tr>
          </tfoot>
        </table>
      </div>
    </div>
  );
}

export default Cart;

شرح مكون شريط التنقل Navbar

هذا الجزء مسؤول عن عرض شريط التنقل العلوي في التطبيق. يحتوي المكون على روابط للتنقل بين الصفحات مثل الصفحة الرئيسية وصفحة About، بالإضافة إلى أيقونة سلة المشتريات. يتم استخدام useSelector لجلب عدد المنتجات الموجودة في السلة من Redux عن طريق قراءة طول المصفوفة state.cart.length. بعد ذلك يتم عرض العدد داخل دائرة حمراء صغيرة فوق أيقونة السلة، وعند الضغط على الأيقونة ينتقل المستخدم إلى صفحة cart. كما يتم استخدام مكونات Bootstrap لتنسيق شريط التنقل وجعله متجاوبًا مع الشاشات المختلفة.


import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";

function Navbar() {
  const dispatch = useDispatch();
  const cartCount = useSelector((state) => state.cart.length);

  return (
    <nav className="navbar navbar-expand-lg bg-light fixed-top">
      <div className="container">
        <Link className="navbar-brand" to="/">Logo</Link>

        <button
          className="navbar-toggler"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#navbarSupportedContent"
          aria-controls="navbarSupportedContent"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span className="navbar-toggler-icon"></span>
        </button>

        <div className="collapse navbar-collapse" id="navbarSupportedContent">
          <ul className="navbar-nav ms-auto mb-2 mb-lg-0">
            <li className="nav-item">
              <Link className="nav-link active" aria-current="page" to="/">
                Home
              </Link>
            </li>

            <li className="nav-item">
              <Link className="nav-link" to="/about">
                About
              </Link>
            </li>

            <li className="nav-item ms-lg-3">
              <Link
                to="/cart"
                className="nav-link position-relative"
                style={{
                  fontSize: "24px",
                  width: "45px",
                  height: "45px",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                }}
              >
                🛒

                <span
                  style={{
                    position: "absolute",
                    top: "0px",
                    right: "0px",
                    backgroundColor: "red",
                    color: "white",
                    borderRadius: "50%",
                    width: "20px",
                    height: "20px",
                    fontSize: "12px",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    fontWeight: "bold",
                  }}
                >
                  {cartCount.toString()}
                </span>
              </Link>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  );
}

export default Navbar;

المكتبات المطلوبة لتشغيل المشروع

لتشغيل هذا المشروع تحتاج إلى مجموعة من المكتبات الأساسية. أولًا تحتاج إلى مكتبة react-router-dom لاستخدام التنقل بين الصفحات مثل الصفحة الرئيسية وصفحة السلة وصفحة تفاصيل المنتج. وتحتاج أيضًا إلى مكتبة bootstrap لاستخدام التصميم الجاهز مثل الأزرار والجداول وشريط التنقل. بالإضافة إلى ذلك تحتاج إلى مكتبات Redux وهي @reduxjs/toolkit و react-redux لإدارة البيانات المشتركة داخل التطبيق مثل المنتجات وسلة المشتريات.


npm install react-router-dom
npm install bootstrap
npm install @reduxjs/toolkit react-redux

تثبيت جميع المكتبات بأمر واحد

يمكنك بدلًا من تثبيت كل مكتبة بشكل منفصل أن تقوم بتثبيت جميع المكتبات المطلوبة باستخدام أمر واحد فقط داخل Terminal في مجلد المشروع. هذا الأمر سيقوم بتثبيت React Router و Bootstrap و Redux Toolkit و React Redux.


npm install react-router-dom bootstrap @reduxjs/toolkit react-redux


لمزيد من المقالات : كيفيه تثبيت Flutter على Mac – خطوة بخطوة
شاهد أيضًا
مقالات ذات صلة

🚫 مانع الإعلانات مفعل

يجب إيقاف مانع الإعلانات لاستكمال تصفح الموقع