سنبني:
1. StudentForm.jsx
2. AddStudent.jsx
3. EditStudent.jsx
4. RoutesDartوسأشرح:
- لماذا هذا الملف موجود
- ما وظيفته
- كيف يعمل
- كيف تتفاعل الملفات مع بعضها
🔥
🎯 أولًا: لماذا الدمج أصلًا؟
قبل الدمج كان عندك:
AddStudent.jsx
EditStudent.jsxDartوكل واحد يحتوي:
- نفس الحقول
- نفس validation
- نفس JSX
- نفس select
- نفس logic
❌
🎯 الحل الاحترافي
إنشاء:
Reusable Form ComponentDartاسمه:
StudentForm.jsxDart🧠 المعمارية النهائية
pages/
└── students/
├── AddStudent.jsx
└── EditStudent.jsx
components/
└── forms/
└── StudentForm.jsxDart🚀 الملف الأول
📄 StudentForm.jsx
🎯 وظيفة هذا الملف
هذا هو:
الفورم الحقيقي الوحيدDartالذي:
- ينشئ الطالب
- يعدل الطالب
📄 components/forms/StudentForm.jsx
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { useDispatch, useSelector } from "react-redux";
import {
addStudent,
updateStudent
} from "../../features/students/studentsSlice";
import { fetchGrades }
from "../../features/grades/gradesSlice";
import { fetchClassRooms }
from "../../features/classrooms/ClassroomsSlice";
import { fetchSections }
from "../../features/sections/SectionsSlice";
const StudentForm = ({ student }) => {
// =====================================
// Hooks
// =====================================
const dispatch = useDispatch();
const navigate = useNavigate();
// =====================================
// Redux State
// =====================================
const { grades } = useSelector(
(state) => state.grades
);
const { classrooms } = useSelector(
(state) => state.classrooms
);
const { sections } = useSelector(
(state) => state.sections
);
// =====================================
// React Hook Form
// =====================================
const {
register,
handleSubmit,
watch,
reset,
formState: { errors }
} = useForm({
mode: "onChange",
defaultValues: {
st_code: student?.st_code || "",
name: student?.name || "",
age: student?.age || "",
phone: student?.phone || "",
email: student?.email || "",
address: student?.address || "",
blood: student?.blood || "",
father: student?.father || "",
mother: student?.mother || "",
grade:
student?.section?.classroom?.grade?.documentId || "",
classroom:
student?.section?.classroom?.documentId || "",
section:
student?.section?.documentId || ""
}
});
// =====================================
// Fetch Data
// =====================================
useEffect(() => {
dispatch(fetchGrades());
dispatch(fetchClassRooms());
dispatch(fetchSections());
}, [dispatch]);
// =====================================
// تعبئة البيانات بالتعديل
// =====================================
useEffect(() => {
if (student) {
reset({
st_code: student.st_code,
name: student.name,
age: student.age,
phone: student.phone,
email: student.email,
address: student.address,
blood: student.blood,
father: student.father,
mother: student.mother,
grade:
student.section?.classroom?.grade?.documentId || "",
classroom:
student.section?.classroom?.documentId || "",
section:
student.section?.documentId || ""
});
}
}, [student, reset]);
// =====================================
// مراقبة الحقول
// =====================================
const selectedGrade = watch("grade");
const selectedClassroom = watch("classroom");
// =====================================
// فلترة الصفوف
// =====================================
const filteredClassRooms = useMemo(() => {
if (!selectedGrade) return [];
return classrooms.filter(
(classroom) =>
classroom.grade?.documentId === selectedGrade
);
}, [selectedGrade, classrooms]);
// =====================================
// فلترة الأقسام
// =====================================
const filteredSections = useMemo(() => {
if (!selectedClassroom) return [];
return sections.filter(
(section) =>
section.classroom?.documentId === selectedClassroom
);
}, [selectedClassroom, sections]);
// =====================================
// Submit Form
// =====================================
const onSubmit = (data) => {
const studentData = {
st_code: data.st_code,
name: data.name,
age: data.age,
phone: data.phone,
email: data.email,
address: data.address,
blood: data.blood,
father: data.father,
mother: data.mother,
// 🔥 نخزن القسم فقط
section: data.section
};
// =================================
// UPDATE
// =================================
if (student) {
dispatch(
updateStudent({
id: student.documentId,
data: studentData
})
)
.unwrap()
.then(() => {
toast.success("تم تعديل الطالب");
navigate("/students");
});
}
// =================================
// CREATE
// =================================
else {
dispatch(addStudent(studentData))
.unwrap()
.then(() => {
toast.success("تم إضافة الطالب");
reset();
navigate("/students");
});
}
};
return (
<div className="card">
{/* Header */}
<div className="card-header">
<h3 className="card-title">
{student
? "تعديل الطالب"
: "إضافة طالب جديد"}
</h3>
</div>
{/* Body */}
<div className="card-body">
<form onSubmit={handleSubmit(onSubmit)}>
{/* =========================== */}
{/* Student Name */}
{/* =========================== */}
<div className="mb-3">
<label className="form-label">
اسم الطالب
</label>
<input
type="text"
className="form-control"
{...register("name", {
required: "اسم الطالب مطلوب",
minLength: {
value: 3,
message:
"الاسم يجب أن يكون 3 أحرف على الأقل"
}
})}
/>
{errors.name && (
<small className="text-danger">
{errors.name.message}
</small>
)}
</div>
{/* =========================== */}
{/* Grade */}
{/* =========================== */}
<div className="mb-3">
<label className="form-label">
المرحلة
</label>
<select
className="form-select"
{...register("grade", {
required: "المرحلة مطلوبة"
})}
>
<option value="">
اختر المرحلة
</option>
{grades?.map((grade) => (
<option
key={grade.documentId}
value={grade.documentId}
>
{grade.name}
</option>
))}
</select>
</div>
{/* =========================== */}
{/* Classroom */}
{/* =========================== */}
<div className="mb-3">
<label className="form-label">
الصف
</label>
<select
className="form-select"
{...register("classroom", {
required: "الصف مطلوب"
})}
>
<option value="">
اختر الصف
</option>
{filteredClassRooms.map((classroom) => (
<option
key={classroom.documentId}
value={classroom.documentId}
>
{classroom.name}
</option>
))}
</select>
</div>
{/* =========================== */}
{/* Section */}
{/* =========================== */}
<div className="mb-3">
<label className="form-label">
القسم
</label>
<select
className="form-select"
{...register("section", {
required: "القسم مطلوب"
})}
>
<option value="">
اختر القسم
</option>
{filteredSections.map((section) => (
<option
key={section.documentId}
value={section.documentId}
>
{section.name}
</option>
))}
</select>
</div>
{/* =========================== */}
{/* Submit */}
{/* =========================== */}
<button
type="submit"
className="btn btn-primary"
>
{student
? "حفظ التعديلات"
: "إضافة الطالب"}
</button>
</form>
</div>
</div>
);
};
export default StudentForm;JSX🧠 ماذا تعلمنا هنا؟
| الشيء | السبب |
|---|---|
| student prop | تحديد add/edit |
| reset | تعبئة بيانات التعديل |
| watch | مراقبة المرحلة والصف |
| useMemo | فلترة محسنة |
| if(student) | تحديد نوع العملية |
🚀 الملف الثاني
📄 AddStudent.jsx
🎯 وظيفته
هذه ليست صفحة فورم حقيقية.
بل فقط:
Wrapper PageDart📄 pages/students/AddStudent.jsx
import StudentForm
from "../../components/forms/StudentForm";
const AddStudent = () => {
return <StudentForm />;
};
export default AddStudent;Dart🧠 ماذا يحدث هنا؟
لم نرسل:
student
إذًا:
student = undefined
👉 الفورم يصبح:
- Create Mode 🔥
🚀 الملف الثالث
📄 EditStudent.jsx
🎯 وظيفته
هذه الصفحة:
- تجلب الطالب من Redux
- ترسله للفورم
📄 pages/students/EditStudent.jsx
import { useParams }
from "react-router-dom";
import { useSelector }
from "react-redux";
import StudentForm
from "../../components/forms/StudentForm";
const EditStudent = () => {
// =================================
// Route Param
// =================================
const { id } = useParams();
// =================================
// Redux State
// =================================
const { students } = useSelector(
(state) => state.students
);
// =================================
// Find Student
// =================================
const student = students.find(
(student) =>
student.documentId === id
);
return (
<StudentForm student={student} />
);
};
export default EditStudent;Dart🧠 ماذا يحدث هنا؟
useParams
تجلب:
/students/abc123/editDartid
تصبح:
abc123Dartثم:
find()Dartيجلب الطالب المطلوب.
ثم:
<StudentForm student={student} />Dart🔥
🚀 الملف الرابع
📄 Routes
داخل App.jsx
<Route
path="/students/create"
element={<AddStudent />}
/>
<Route
path="/students/:id/edit"
element={<EditStudent />}
/>Dart🧠 ماذا يحدث؟
create
يفتح:
Add Mode
edit
يفتح:
Edit Mode
🎯 أهم فكرة بالدمج كله
💡 الفرق الحقيقي بين add/edit
هو فقط:
وجود student أو عدمهDart🔥
🚀 ماذا ربحنا؟
| قبل | بعد |
|---|---|
| كود مكرر | reusable |
| صيانة صعبة | سهلة |
| تعديل مزدوج | مرة واحدة |
| validation مكرر | موحد |
💡 قاعدة احترافية جدًا
إذا كررت JSX أكثر من مرة → اصنع reusable component