سنقوم الآن ببناء Banner / Hero Slider System احترافي 100%
بنية صحيحة في Strapi + كود React كامل + Redux + Swiper
وقابل للتوسع مستقبلاً.
سنبنيه وكأننا نبني متجر Production حقيقي 👌
🏗 أولًا: إعداد Strapi بطريقة احترافية
✅ أنشئ Collection Type باسم:
banners
📦 الحقول داخل banners
1️⃣ Basic Content
| Field | Type |
|---|---|
| title | Short text |
| subtitle | Short text |
| description | Rich text |
| buttonText | Short text |
| buttonLink | Short text |
2️⃣ Images
| Field | Type |
|---|---|
| backgroundLight | Media (Single) |
| backgroundDark | Media (Single) |
| sideImage | Media (Single) |
3️⃣ Animated Icons (Optional)
| Field | Type |
|---|---|
| icon1 | Media (Single) |
| icon2 | Media (Single) |
| icon3 | Media (Single) |
4️⃣ Control Fields (احترافي جدًا)
| Field | Type |
|---|---|
| location | Enumeration (homepage, offers, category) |
| order | Integer |
| isActive | Boolean |
| startDate | DateTime (اختياري) |
| endDate | DateTime (اختياري) |
🧠 لماذا هذه الحقول مهمة؟
- location → يمكن استخدام نفس البانر في أكثر من صفحة
- order → ترتيب يدوي
- isActive → تعطيل بدون حذف
- startDate/endDate → جدولة زمنية
🔥 هذا مستوى احترافي جدًا.
🌐 API الاحترافي
/api/banners?filters[location][$eq]=homepage
&filters[isActive][$eq]=true
&sort=order:asc
&populate=*
🏗 ثانيًا: Redux Slice احترافي
📁 store/bannerSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import api from "../api";export const getBanners = createAsyncThunk(
"banners/getBanners",
async (location = "homepage") => { const { data } = await api.get(
`/banners?filters[location][$eq]=${location}&filters[isActive][$eq]=true&sort=order:asc&populate=*`
); return data.data;
}
);const bannerSlice = createSlice({
name: "banners",
initialState: {
data: [],
isLoading: false,
error: null,
},
extraReducers: (builder) => {
builder
.addCase(getBanners.pending, (state) => {
state.isLoading = true;
})
.addCase(getBanners.fulfilled, (state, action) => {
state.data = action.payload;
state.isLoading = false;
})
.addCase(getBanners.rejected, (state, action) => {
state.error = action.error.message;
state.isLoading = false;
});
},
});export const bannersReducer = bannerSlice.reducer;
🏗 ثالثًا: HeroSlider Component احترافي
📁 components/HeroSlider.jsx
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination, Autoplay, EffectFade } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/effect-fade";import { useEffect, useContext } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getBanners } from "../store/bannerSlice";
import { ThemeContext } from "../context/ThemeContext";
import { Link } from "react-router-dom";function HeroSlider() { const dispatch = useDispatch();
const { data: banners } = useSelector(state => state.banners);
const { theme } = useContext(ThemeContext); useEffect(() => {
dispatch(getBanners("homepage"));
}, [dispatch]); return (
<div className="hero-wrapper"> <Swiper
modules={[Navigation, Pagination, Autoplay, EffectFade]}
navigation
pagination={{ clickable: true }}
autoplay={{ delay: 5000 }}
effect="fade"
loop
> {banners.map(banner => { const bgImage =
theme === "dark"
? banner.attributes.backgroundDark?.data?.attributes?.url
: banner.attributes.backgroundLight?.data?.attributes?.url; return (
<SwiperSlide key={banner.id}> <div
className="hero-slide"
style={{
backgroundImage: `url(${import.meta.env.VITE_API_URL}${bgImage})`
}}
> <div className="container">
<div className="row align-items-center"> <div className="col-lg-6 hero-text"> <h5 className="subtitle">
{banner.attributes.subtitle}
</h5> <h1 className="title">
{banner.attributes.title}
</h1> <p className="description">
{banner.attributes.description}
</p> {banner.attributes.buttonText && (
<Link
to={banner.attributes.buttonLink}
className="btn btn-primary mt-3"
>
{banner.attributes.buttonText}
</Link>
)} </div> <div className="col-lg-6 text-center">
<img
src={`${import.meta.env.VITE_API_URL}${banner.attributes.sideImage?.data?.attributes?.url}`}
className="img-fluid side-image"
alt=""
/>
</div> </div>
</div> </div> </SwiperSlide>
);
})} </Swiper> </div>
);
}export default HeroSlider;
🎨 CSS احترافي
.hero-wrapper {
height: 85vh;
}.hero-slide {
height: 85vh;
background-size: cover;
background-position: center;
display: flex;
align-items: center;
position: relative;
}.hero-text {
color: var(--bs-body-color);
}.title {
font-size: 3rem;
font-weight: 700;
}.subtitle {
color: var(--bs-primary);
}.side-image {
animation: float 4s ease-in-out infinite;
}@keyframes float {
0% { transform: translateY(0); }
50% { transform: translateY(-20px); }
100% { transform: translateY(0); }
}
🧠 لماذا هذا النظام احترافي؟
✔ Collection مستقل
✔ قابل للاستخدام في أي صفحة
✔ يدعم Dark/Light
✔ يدعم جدولة مستقبلًا
✔ ترتيب يدوي
✔ قابل للتوسع
✔ API نظيف
✔ متوافق مع Redux
🚀 مستوى أعلى (اختياري)
يمكننا الآن إضافة:
- ✨ Text animation عند تغير slide
- 🌈 Gradient overlay ديناميكي
- 🧠 جدولة startDate / endDate من React
- 🔥 Parallax
- 🎥 Video banner