diff --git a/Frontend/EduLiteFrontend/src/components/common/ConfirmationModal.tsx b/Frontend/EduLiteFrontend/src/components/common/ConfirmationModal.tsx index da7f0d8..f515040 100644 --- a/Frontend/EduLiteFrontend/src/components/common/ConfirmationModal.tsx +++ b/Frontend/EduLiteFrontend/src/components/common/ConfirmationModal.tsx @@ -39,11 +39,11 @@ const ConfirmationModal: React.FC = ({ /> {/* Modal */} -
+
{/* Close button */}
) : ( @@ -338,7 +338,7 @@ const MembersTab: React.FC = ({ choices={ROLE_CHOICES} showLabel={false} disabled={changingRole === member.id} - className="!py-1 !pl-2 !pr-8 text-sm" + className="!py-1 !ps-2 !pe-8 text-sm" />
) : ( @@ -350,32 +350,32 @@ const MembersTab: React.FC = ({ )} {isTeacher && ( -
+
{member.status === "pending" && ( <> )} {member.user !== currentUserId && ( )}
diff --git a/Frontend/EduLiteFrontend/src/components/courses/ModulesTab.tsx b/Frontend/EduLiteFrontend/src/components/courses/ModulesTab.tsx index 05e433b..e69af3e 100644 --- a/Frontend/EduLiteFrontend/src/components/courses/ModulesTab.tsx +++ b/Frontend/EduLiteFrontend/src/components/courses/ModulesTab.tsx @@ -192,47 +192,47 @@ const ModulesTab: React.FC = ({ {/* Teacher actions */} {isTeacher && ( -
+
{/* Reorder */} {/* Edit */} {/* Delete */}
)} diff --git a/Frontend/EduLiteFrontend/src/i18n/locales/ar.json b/Frontend/EduLiteFrontend/src/i18n/locales/ar.json index ac40538..0b04478 100644 --- a/Frontend/EduLiteFrontend/src/i18n/locales/ar.json +++ b/Frontend/EduLiteFrontend/src/i18n/locales/ar.json @@ -314,6 +314,7 @@ "filterCountry": "البلد", "filterVisibility": "الظهور", "clearFilters": "مسح الفلاتر", + "filtersLabel": "فلاتر", "visibilityPublic": "عامة", "visibilityRestricted": "مقيدة", "visibilityPrivate": "خاصة" diff --git a/Frontend/EduLiteFrontend/src/i18n/locales/en.json b/Frontend/EduLiteFrontend/src/i18n/locales/en.json index dcbe457..e52d101 100644 --- a/Frontend/EduLiteFrontend/src/i18n/locales/en.json +++ b/Frontend/EduLiteFrontend/src/i18n/locales/en.json @@ -313,6 +313,7 @@ "filterCountry": "Country", "filterVisibility": "Visibility", "clearFilters": "Clear filters", + "filtersLabel": "Filters", "visibilityPublic": "Public", "visibilityRestricted": "Restricted", "visibilityPrivate": "Private" diff --git a/Frontend/EduLiteFrontend/src/pages/CourseDetailPage.tsx b/Frontend/EduLiteFrontend/src/pages/CourseDetailPage.tsx index 47c8d82..f94bd1a 100644 --- a/Frontend/EduLiteFrontend/src/pages/CourseDetailPage.tsx +++ b/Frontend/EduLiteFrontend/src/pages/CourseDetailPage.tsx @@ -738,9 +738,9 @@ const CourseDetailPage = () => { {/* ===== SAVE CHANGES BANNER ===== */} {isEditing && isDirty && (
-
+
-
+

{t("course.detail.unsavedChanges")}

@@ -748,14 +748,14 @@ const CourseDetailPage = () => {
@@ -808,7 +808,11 @@ const CourseDetailPage = () => { {/* Enrollment actions (join/leave/pending/invited) */} {!isEditing && ( - + )}
diff --git a/Frontend/EduLiteFrontend/src/pages/CourseFormPage.tsx b/Frontend/EduLiteFrontend/src/pages/CourseFormPage.tsx index 277416a..074fe3b 100644 --- a/Frontend/EduLiteFrontend/src/pages/CourseFormPage.tsx +++ b/Frontend/EduLiteFrontend/src/pages/CourseFormPage.tsx @@ -278,10 +278,8 @@ const CourseFormPage: React.FC = () => { return Object.keys(newErrors).length === 0; }; - // Submit - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - + // Core submit logic (used by both form submit and mobile sticky bar) + const doSubmit = async () => { if (!validateForm()) { toast.error(t("course.form.fixErrors")); return; @@ -383,7 +381,13 @@ const CourseFormPage: React.FC = () => {

-
+ { + e.preventDefault(); + doSubmit(); + }} + className="space-y-6" + > {/* Basic Info Section */}
{ />
- {/* Submit Button */} -
+ {/* Submit Button — desktop inline */} +
+ + {/* Spacer for sticky bottom bar on mobile */} +
@@ -618,6 +625,53 @@ const CourseFormPage: React.FC = () => { onCancel={handleCancelNavigation} message={t("course.form.unsavedChangesMessage")} /> + + {/* Sticky bottom bar — mobile only */} +
+
+ + +
+
); }; diff --git a/Frontend/EduLiteFrontend/src/pages/CourseListPage.tsx b/Frontend/EduLiteFrontend/src/pages/CourseListPage.tsx index fd6da5a..985e585 100644 --- a/Frontend/EduLiteFrontend/src/pages/CourseListPage.tsx +++ b/Frontend/EduLiteFrontend/src/pages/CourseListPage.tsx @@ -160,8 +160,13 @@ const CourseListPage: React.FC = ({ view: propView }) => { filterVisibility, ]); - const hasActiveFilters = - filterSubject || filterLanguage || filterCountry || filterVisibility; + const activeFilterCount = [ + filterSubject, + filterLanguage, + filterCountry, + filterVisibility, + ].filter(Boolean).length; + const hasActiveFilters = activeFilterCount > 0; const clearFilters = () => { setFilterSubject(""); @@ -477,8 +482,13 @@ const CourseListPage: React.FC = ({ view: propView }) => { }`} > + + {t("course.list.filtersLabel")} + {hasActiveFilters && ( - + + {activeFilterCount} + )} {hasActiveFilters && ( @@ -493,8 +503,12 @@ const CourseListPage: React.FC = ({ view: propView }) => {
{/* Filters Panel */} - {showFilters && ( -
+
+
= ({ view: propView }) => { onClick={() => setVisibilityDropdownOpen(!visibilityDropdownOpen) } - className="w-full px-4 py-3 pr-10 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-xl shadow-sm text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-all duration-200 cursor-pointer hover:border-gray-400 dark:hover:border-gray-500 font-medium text-left" + className="w-full px-4 py-3 pe-10 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-xl shadow-sm text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-all duration-200 cursor-pointer hover:border-gray-400 dark:hover:border-gray-500 font-medium text-start" > = ({ view: propView }) => { )?.label || t("course.list.filterVisibility")} @@ -564,7 +578,7 @@ const CourseListPage: React.FC = ({ view: propView }) => { setFilterVisibility(option.value); setVisibilityDropdownOpen(false); }} - className={`w-full px-4 py-3 text-left flex items-center justify-between transition-colors duration-150 text-gray-700 dark:text-gray-200 cursor-pointer ${ + className={`w-full px-4 py-3 text-start flex items-center justify-between transition-colors duration-150 text-gray-700 dark:text-gray-200 cursor-pointer ${ filterVisibility === option.value ? "bg-blue-500 text-white hover:bg-blue-600" : "hover:bg-gray-50 dark:hover:bg-gray-700/50" @@ -586,7 +600,7 @@ const CourseListPage: React.FC = ({ view: propView }) => { )}
- )} +
{/* Error State */} {error && ( @@ -643,25 +657,25 @@ const CourseListPage: React.FC = ({ view: propView }) => { - - - - - - - @@ -682,7 +696,7 @@ const CourseListPage: React.FC = ({ view: propView }) => { : "" } ${ course.is_member - ? "border-l-3 border-l-green-500 dark:border-l-green-400" + ? "border-s-3 border-s-green-500 dark:border-s-green-400" : "" }`} > @@ -730,7 +744,7 @@ const CourseListPage: React.FC = ({ view: propView }) => { -
+ {t("course.list.titleColumn")} + {t("course.list.subjectColumn")} + {t("course.list.languageColumn")} + {t("course.list.visibilityColumn")} + {t("course.list.membersColumn")} + {t("course.list.startDateColumn")} + {/* Actions column */}
{formatDate(course.start_date)} + {t("course.list.pageOf", { @@ -838,7 +855,7 @@ const CourseListPage: React.FC = ({ view: propView }) => { className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200" aria-label="Next page" > - + diff --git a/Frontend/EduLiteFrontend/src/pages/__tests__/CourseListPage.test.tsx b/Frontend/EduLiteFrontend/src/pages/__tests__/CourseListPage.test.tsx index e355741..b104b74 100644 --- a/Frontend/EduLiteFrontend/src/pages/__tests__/CourseListPage.test.tsx +++ b/Frontend/EduLiteFrontend/src/pages/__tests__/CourseListPage.test.tsx @@ -599,25 +599,19 @@ describe("CourseListPage", () => { expect(screen.queryByText(/loading courses/i)).not.toBeInTheDocument(); }); - // Filter labels should not be visible initially - expect( - screen.queryByText("Subject", { selector: "label" }), - ).not.toBeInTheDocument(); + // Filter panel should be hidden initially (collapsed via max-h-0) + const filterPanel = screen + .getByText("Subject", { selector: "label" }) + .closest('[class*="max-h-"]'); + expect(filterPanel).toHaveClass("max-h-0"); - // Click the filter button — find the button with no text content (icon-only) - const actionsButtons = screen.getAllByRole("button"); - const filterBtn = actionsButtons.find( - (btn) => !btn.textContent?.trim() || btn.textContent?.trim() === "", - ); - if (filterBtn) { - await user.click(filterBtn); - } + // Click the filter button + const filterBtn = screen.getByText("Filters").closest("button")!; + await user.click(filterBtn); - // Now filter labels should be visible + // Now filter panel should be expanded await waitFor(() => { - expect( - screen.getByText("Subject", { selector: "label" }), - ).toBeInTheDocument(); + expect(filterPanel).not.toHaveClass("max-h-0"); }); }); @@ -635,11 +629,8 @@ describe("CourseListPage", () => { }); // Open filters - const buttons = screen.getAllByRole("button"); - const filterBtn = buttons.find( - (btn) => !btn.textContent?.trim() || btn.textContent?.trim() === "", - ); - if (filterBtn) await user.click(filterBtn); + const filterBtn = screen.getByText("Filters").closest("button")!; + await user.click(filterBtn); await waitFor(() => { expect( @@ -660,11 +651,8 @@ describe("CourseListPage", () => { }); // Open filters - const buttons2 = screen.getAllByRole("button"); - const filterBtn2 = buttons2.find( - (btn) => !btn.textContent?.trim() || btn.textContent?.trim() === "", - ); - if (filterBtn2) await user.click(filterBtn2); + const filterBtn2 = screen.getByText("Filters").closest("button")!; + await user.click(filterBtn2); await waitFor(() => { expect(