سننشئ:
- 🟢 Sidebar جانبي
- 🟢 فلترة حسب السعر (من – إلى)
- 🟢 فلترة حسب التصنيفات
- 🟢 دعم Radio و Checkbox
- 🟢 مرتبطة بالـ URL (احترافي)
- 🟢 جاهزة للعمل مع Strapi
سأبنيها لك معماريًا بشكل صحيح.
🎯 أولًا: ما هو السيناريو الصحيح للفلترة؟
عند تغيير أي فلتر:
- يتم تحديث الـ URL
- يتم استدعاء API جديد
- يتم عرض النتائج المفلترة
مثال URL احترافي:
/books?page=1&min=10&max=50&categories=2,4JavaScript🏗 الهيكل العام للصفحة
BooksPage
├── SidebarFilters
└── BooksGridJavaScript🟢 الخطوة 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 FiltersJavaScriptلا نستخدم useState محلي للفلترة (إلا مؤقتًا للسلايدر).
🟢 1️⃣ تطوير فلتر السعر (Range Slider احترافي)
سنستخدم مكتبة خفيفة:
npm install rc-sliderJavaScript📁 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