الفلترة المتقدمة

سننشئ:

  • 🟢 Sidebar جانبي
  • 🟢 فلترة حسب السعر (من – إلى)
  • 🟢 فلترة حسب التصنيفات
  • 🟢 دعم Radio و Checkbox
  • 🟢 مرتبطة بالـ URL (احترافي)
  • 🟢 جاهزة للعمل مع Strapi

سأبنيها لك معماريًا بشكل صحيح.

🎯 أولًا: ما هو السيناريو الصحيح للفلترة؟

عند تغيير أي فلتر:

  • يتم تحديث الـ URL
  • يتم استدعاء API جديد
  • يتم عرض النتائج المفلترة

مثال URL احترافي:

/books?page=1&min=10&max=50&categories=2,4
JavaScript
🏗 الهيكل العام للصفحة
BooksPage
 ├── SidebarFilters
 └── BooksGrid
JavaScript
🟢 الخطوة 1: Sidebar Component
📁 SidebarFilters.jsx
import { useSearchParams } from "react-router-dom";
function SidebarFilters({ categories }) { 
const [searchParams, setSearchParams] = useSearchParams();  
const min = searchParams.get("min") || "";
const max = searchParams.get("max") || "";
const selectedCategories = searchParams.get("categories")?.split(",") || [];  // تحديث السعر
  const handlePriceChange = (type, value) => {
    const params = new URLSearchParams(searchParams); 
     params.set(type, value);
    params.set("page", 1); // إعادة الصفحة للأولى    setSearchParams(params);
  };  // تحديث التصنيفات
  const handleCategoryChange = (id) => {
    const params = new URLSearchParams(searchParams);    let updated = [...selectedCategories];    if (updated.includes(String(id))) {
      updated = updated.filter(c => c !== String(id));
    } else {
      updated.push(String(id));
    }    if (updated.length > 0) {
      params.set("categories", updated.join(","));
    } else {
      params.delete("categories");
    }    params.set("page", 1);
    setSearchParams(params);
  };  return (
    <div className="p-3 border rounded">      <h5>فلترة حسب السعر</h5>      <input
        type="number"
        placeholder="من"
        className="form-control mb-2"
        value={min}
        onChange={(e) => handlePriceChange("min", e.target.value)}
      />      <input
        type="number"
        placeholder="إلى"
        className="form-control mb-3"
        value={max}
        onChange={(e) => handlePriceChange("max", e.target.value)}
      />      <h5>التصنيفات</h5>      {categories.map(cat => (
        <div key={cat.id} className="form-check">
          <input
            type="checkbox"
            className="form-check-input"
            checked={selectedCategories.includes(String(cat.id))}
            onChange={() => handleCategoryChange(cat.id)}
          />
          <label className="form-check-label">
            {cat.name}
          </label>
        </div>
      ))}    </div>
  );
}export default SidebarFilters;
JavaScript
🧠 ماذا يحدث هنا؟
  • نقرأ الفلاتر من URL
  • نعدل URL عند تغيير أي فلتر
  • لا نستخدم useState محلي
  • URL هو مصدر الحقيقة

🔥 هذا احترافي جدًا.

🟢 الخطوة 2: تعديل BooksPage
const [searchParams] = useSearchParams();const page = Number(searchParams.get("page")) || 1;
const min = searchParams.get("min");
const max = searchParams.get("max");
const categories = searchParams.get("categories");useEffect(() => {
  dispatch(getAllBooks({
    page,
    min,
    max,
    categories
  }));
}, [page, min, max, categories]);
JavaScript
🟢 الخطوة 3: تعديل thunk في Redux
export const getAllBooks = createAsyncThunk(
  "books-actions",
  async ({ page = 1, min, max, categories } = {}) => {    let query = `/books?pagination[page]=${page}&populate=*`;    if (min) query += `&filters[price][$gte]=${min}`;
    if (max) query += `&filters[price][$lte]=${max}`;    if (categories) {
      const ids = categories.split(",");
      ids.forEach(id => {
        query += `&filters[categories][id][$in]=${id}`;
      });
    }    const { data } = await api.get(query);
    return data;
  }
);
JavaScript

🔥 الآن الفلترة تتم من Strapi مباشرة.

🎯 إذا أردت Radio بدل Checkbox

بدل:

type="checkbox"
JavaScript

استخدم:

type="radio"
name="category"
JavaScript

لكن عندها يسمح بتصنيف واحد فقط.

🏆 النتيجة النهائية

✔ فلترة حسب السعر
✔ فلترة حسب تصنيفات متعددة
✔ مربوطة بالـ URL
✔ Pagination متوافقة
✔ API نظيف
✔ تجربة احترافية

🚀 المستوى الأعلى القادم

يمكننا تطوير النظام ليصبح:

  • فلترة فورية بدون انتظار زر
  • Debounce للسعر
  • Slider Range بدل input
  • حفظ الفلاتر عند الرجوع
  • زر “إزالة جميع الفلاتر”
  • Sorting (الأكثر مبيعًا – السعر – الأحدث)

هل تريد أن ننتقل إلى:

1️⃣ إضافة Range Slider احترافي؟
2️⃣ إضافة Sorting Dropdown؟
3️⃣ جعل الفلترة فورية مع Debounce احترافي؟

نعم اريد تطوير الفلاتر بأفضل طريقة

ممتاز 👌🔥
الآن سنبني نظام فلترة احترافي Production-Level مثل المتاجر الكبيرة (Amazon / Noon).

سنضيف:

  • 🎚 Range Slider احترافي للسعر
  • 🏷 Checkbox للتصنيفات (متعدد)
  • 🔄 Sorting Dropdown
  • 🧠 Debounce حتى لا يضرب API كل ثانية
  • 🔗 ربط كامل بالـ URL
  • ♻️ زر “إزالة جميع الفلاتر”
  • 📦 متوافق مع Pagination
🏗 المعمارية الصحيحة
URL = مصدر الحقيقة

useSearchParams

useEffect

dispatch(getAllBooks)

Strapi Filters
JavaScript

لا نستخدم useState محلي للفلترة (إلا مؤقتًا للسلايدر).

🟢 1️⃣ تطوير فلتر السعر (Range Slider احترافي)

سنستخدم مكتبة خفيفة:

npm install rc-slider
JavaScript
📁 SidebarFilters.jsx (نسخة احترافية)
import { useSearchParams } from "react-router-dom";
import { useState, useEffect } from "react";
import Slider from "rc-slider";
import "rc-slider/assets/index.css";function SidebarFilters({ categories }) {  const [searchParams, setSearchParams] = useSearchParams();  const min = Number(searchParams.get("min")) || 0;
  const max = Number(searchParams.get("max")) || 200;  const selectedCategories =
    searchParams.get("categories")?.split(",") || [];  const sort = searchParams.get("sort") || "";  const [priceRange, setPriceRange] = useState([min, max]);  // 🧠 Debounce للسعر
  useEffect(() => {
    const timer = setTimeout(() => {
      const params = new URLSearchParams(searchParams);      params.set("min", priceRange[0]);
      params.set("max", priceRange[1]);
      params.set("page", 1);      setSearchParams(params);
    }, 600);    return () => clearTimeout(timer);
  }, [priceRange]);  // 🏷 تغيير التصنيفات
  const handleCategoryChange = (id) => {
    const params = new URLSearchParams(searchParams);
    let updated = [...selectedCategories];    if (updated.includes(String(id))) {
      updated = updated.filter(c => c !== String(id));
    } else {
      updated.push(String(id));
    }    if (updated.length) {
      params.set("categories", updated.join(","));
    } else {
      params.delete("categories");
    }    params.set("page", 1);
    setSearchParams(params);
  };  // 🔄 تغيير الترتيب
  const handleSortChange = (value) => {
    const params = new URLSearchParams(searchParams);
    params.set("sort", value);
    params.set("page", 1);
    setSearchParams(params);
  };  // 🗑 إزالة جميع الفلاتر
  const clearFilters = () => {
    setSearchParams({ page: 1 });
  };  return (
    <div className="p-3 border rounded">      <h5>السعر</h5>
      <Slider
        range
        min={0}
        max={200}
        value={priceRange}
        onChange={setPriceRange}
      />
      <div className="d-flex justify-content-between mt-2">
        <span>{priceRange[0]}$</span>
        <span>{priceRange[1]}$</span>
      </div>      <hr />      <h5>التصنيفات</h5>
      {categories.map(cat => (
        <div key={cat.id} className="form-check">
          <input
            type="checkbox"
            className="form-check-input"
            checked={selectedCategories.includes(String(cat.id))}
            onChange={() => handleCategoryChange(cat.id)}
          />
          <label className="form-check-label">
            {cat.name}
          </label>
        </div>
      ))}      <hr />      <h5>الترتيب</h5>
      <select
        className="form-select"
        value={sort}
        onChange={(e) => handleSortChange(e.target.value)}
      >
        <option value="">افتراضي</option>
        <option value="price:asc">السعر من الأقل للأعلى</option>
        <option value="price:desc">السعر من الأعلى للأقل</option>
        <option value="createdAt:desc">الأحدث</option>
      </select>      <button
        className="btn btn-outline-danger w-100 mt-3"
        onClick={clearFilters}
      >
        إزالة جميع الفلاتر
      </button>    </div>
  );
}export default SidebarFilters;
JavaScript
🟢 2️⃣ تعديل getAllBooks في Redux
export const getAllBooks = createAsyncThunk(
  "books-actions",
  async ({ page = 1, min, max, categories, sort } = {}) => {    let query = `/books?pagination[page]=${page}&populate=*`;    if (min) query += `&filters[price][$gte]=${min}`;
    if (max) query += `&filters[price][$lte]=${max}`;    if (categories) {
      categories.split(",").forEach(id => {
        query += `&filters[categories][id][$in]=${id}`;
      });
    }    if (sort) {
      query += `&sort=${sort}`;
    }    const { data } = await api.get(query);
    return data;
  }
);
JavaScript
🟢 3️⃣ BooksPage يقرأ كل شيء من URL
const [searchParams] = useSearchParams();const page = Number(searchParams.get("page")) || 1;
const min = searchParams.get("min");
const max = searchParams.get("max");
const categories = searchParams.get("categories");
const sort = searchParams.get("sort");useEffect(() => {
  dispatch(getAllBooks({
    page,
    min,
    max,
    categories,
    sort
  }));
}, [page, min, max, categories, sort]);
JavaScript
🎯 ماذا أصبح لدينا الآن؟

✔ Range Slider احترافي
✔ فلترة متعددة للتصنيفات
✔ Sorting
✔ Pagination متوافق
✔ URL قابل للمشاركة
✔ Debounce احترافي
✔ زر إعادة ضبط

🚀 مستوى أعلى جدًا (اختياري)

يمكننا الآن إضافة:

  • 🔥 Sticky Sidebar
  • 🔥 Skeleton Loading أثناء الفلترة
  • 🔥 فلترة فورية بدون انتظار
  • 🔥 عرض عدد النتائج لكل فلتر
  • 🔥 تحميل التصنيفات من API
  • 🔥 React Query بدل Redux