diff --git a/app/[locale]/dashboard/courses/page.jsx b/app/[locale]/dashboard/courses/page.jsx
index 3b1c7e2e..6f3cddcb 100644
--- a/app/[locale]/dashboard/courses/page.jsx
+++ b/app/[locale]/dashboard/courses/page.jsx
@@ -62,9 +62,11 @@ const CoursesPageContent = () => {
setCourses(response.bookmarks || []);
} else {
const response = await fetchCourses();
+ if (!response) throw new Error("No data returned");
setCourses(response);
}
- } catch (error) {
+ } catch (err) {
+ console.error("[CoursesPage] Failed to load courses:", err);
setError(true);
} finally {
setLoading(false);
diff --git a/app/dashboard/courses/categories/page.jsx b/app/dashboard/courses/categories/page.jsx
new file mode 100644
index 00000000..d1514257
--- /dev/null
+++ b/app/dashboard/courses/categories/page.jsx
@@ -0,0 +1,182 @@
+"use client";
+import { useEffect, useState, useMemo } from "react";
+import Link from "next/link";
+import { fetchCourses } from "@/lib/actions/courses/fetch-courses";
+import CourseCardSkeleton from "@/components/atoms/skeletons/CourseCardSkeleton";
+import NetworkErrorComp from "@/components/molecules/errors/NetworkError";
+import {
+ CATEGORY_GROUPS,
+ CATEGORIES,
+ getCategoryCounts,
+} from "@/lib/categories";
+import { BookOpen, ArrowRight, LayoutGrid } from "lucide-react";
+
+export default function CategoryHubPage() {
+ const [courses, setCourses] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+
+ const loadCourses = async () => {
+ setLoading(true);
+ setError(false);
+ try {
+ const data = await fetchCourses();
+ if (!data) throw new Error("No data returned");
+ setCourses(data);
+ } catch (err) {
+ console.error("[CategoryHub] Failed to load courses:", err);
+ setError(true);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadCourses();
+ }, []);
+
+ // Derive counts from the fetched course list
+ const counts = useMemo(() => getCategoryCounts(courses), [courses]);
+
+ const totalCourses = courses.length;
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* ── Hero header ── */}
+
+
+
+
+
+ Course Categories
+
+
+
+ Browse authentic Islamic knowledge across{" "}
+ {CATEGORIES.length} categories grouped into{" "}
+ {CATEGORY_GROUPS.length} disciplines.
+
+ {!loading && (
+
+ {totalCourses} course{totalCourses !== 1 ? "s" : ""} available
+
+ )}
+
+
+
+ Browse All Courses
+
+
+
+
+
+ {/* ── Loading skeletons ── */}
+ {loading ? (
+
+ {[...Array(3)].map((_, gi) => (
+
+
+
+ {[...Array(3)].map((_, ci) => (
+
+ ))}
+
+
+ ))}
+
+ ) : (
+ /* ── Category groups ── */
+
+ {CATEGORY_GROUPS.map((group) => {
+ const groupCategories = CATEGORIES.filter(
+ (c) => c.group === group
+ );
+ return (
+
+
+ {group}
+
+
+ {groupCategories.map((cat) => {
+ const count = counts[cat.slug] || 0;
+ const isEmpty = count === 0;
+ return (
+
+ {/* Icon + count */}
+
+
+ {cat.icon}
+
+
+ {count} course{count !== 1 ? "s" : ""}
+
+
+
+ {/* Label + description */}
+
+ {cat.label}
+
+
+ {cat.description}
+
+
+ {/* CTA row */}
+
+ {isEmpty ? "No courses yet" : "View courses"}
+
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/app/dashboard/courses/category/[slug]/page.jsx b/app/dashboard/courses/category/[slug]/page.jsx
new file mode 100644
index 00000000..1786438c
--- /dev/null
+++ b/app/dashboard/courses/category/[slug]/page.jsx
@@ -0,0 +1,249 @@
+"use client";
+import { useEffect, useState, useMemo } from "react";
+import { use } from "react";
+import Link from "next/link";
+import { fetchCourses } from "@/lib/actions/courses/fetch-courses";
+import CourseCard from "@/components/molecules/dashboard/cards/courseCard";
+import CourseCardSkeleton from "@/components/atoms/skeletons/CourseCardSkeleton";
+import NetworkErrorComp from "@/components/molecules/errors/NetworkError";
+import NotFoundComp from "@/components/molecules/errors/NotFound";
+import Modal from "@/components/molecules/Modal";
+import CreateCourseForm from "@/components/organisms/create/course-create-form";
+import { getCategoryBySlug, resolveSlug } from "@/lib/categories";
+import { getAverageRating } from "@/hooks/getAverageRating";
+import { ArrowLeft, BookOpen, Plus } from "lucide-react";
+
+// Sort options
+const SORT_OPTIONS = [
+ { value: "newest", label: "Newest" },
+ { value: "price-asc", label: "Price: Low to High" },
+ { value: "price-desc", label: "Price: High to Low" },
+ { value: "rating", label: "Top Rated" },
+];
+
+function sortCourses(courses, sort) {
+ const sorted = [...courses];
+ switch (sort) {
+ case "price-asc":
+ return sorted.sort((a, b) => (a.price || 0) - (b.price || 0));
+ case "price-desc":
+ return sorted.sort((a, b) => (b.price || 0) - (a.price || 0));
+ case "rating":
+ return sorted.sort(
+ (a, b) =>
+ getAverageRating(b.reviews || []) -
+ getAverageRating(a.reviews || [])
+ );
+ case "newest":
+ default:
+ return sorted.sort(
+ (a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)
+ );
+ }
+}
+
+export default function CategoryLandingPage({ params }) {
+ // Unwrap params using React.use() for Next.js 15+
+ const { slug } = use(params);
+
+ const category = getCategoryBySlug(slug);
+
+ const [allCourses, setAllCourses] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+ const [sort, setSort] = useState("newest");
+ const [modalOpen, setModalOpen] = useState(false);
+
+ const loadCourses = async () => {
+ setLoading(true);
+ setError(false);
+ try {
+ const data = await fetchCourses();
+ if (!data) throw new Error("No data returned");
+ setAllCourses(data);
+ } catch (err) {
+ console.error("[CategoryLanding] Failed to load courses:", err);
+ setError(true);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadCourses();
+ }, [slug]);
+
+ // Filter to this category's courses
+ const categoryCourses = useMemo(
+ () =>
+ sortCourses(
+ allCourses.filter((c) => resolveSlug(c.category) === slug),
+ sort
+ ),
+ [allCourses, slug, sort]
+ );
+
+ // Unknown slug → graceful not-found, no crash
+ if (!category) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+ {/* ── Hero header ── */}
+
+
+ {/* Breadcrumb */}
+
+
+ {/* Icon + title */}
+
+
+ {category.icon}
+
+
+
+ {category.label}
+
+
+ {category.description}
+
+ {!loading && (
+
+ {categoryCourses.length} course
+ {categoryCourses.length !== 1 ? "s" : ""}
+
+ )}
+
+
+
+ {/* Group badge */}
+
+
+ {category.group}
+
+
+
+
+
+ {/* ── Controls ── */}
+
+
+
+
+ {loading ? "Loading…" : `${categoryCourses.length} course${categoryCourses.length !== 1 ? "s" : ""}`}
+
+
+
+ {/* Sort */}
+
+ {/* Create course CTA */}
+
+
+
+
+ {/* ── Course grid ── */}
+
+ {loading ? (
+
+ {[...Array(6)].map((_, idx) => (
+
+ ))}
+
+ ) : categoryCourses.length === 0 ? (
+ /* ── Empty state ── */
+
+
+ {category.icon}
+
+
+ No courses yet in {category.label}
+
+
+ Be the first educator to share knowledge in this discipline. The
+ Ummah is waiting for you!
+
+
+
+ Browse other categories
+
+
+ ) : (
+
+ {categoryCourses.map((course) => (
+
+ ))}
+
+ )}
+
+
+
+ {/* Create Course modal */}
+ setModalOpen(false)}
+ className="max-w-md w-full"
+ >
+
+
+ >
+ );
+}
diff --git a/app/dashboard/layout.jsx b/app/dashboard/layout.jsx
new file mode 100644
index 00000000..c3524f09
--- /dev/null
+++ b/app/dashboard/layout.jsx
@@ -0,0 +1,17 @@
+"use client";
+
+import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
+import { SidebarLeft } from "@/components/organisms/dashboard/sidebar-left";
+import NavHeader from "@/components/molecules/dashboard/nav-header";
+
+export default function DashboardLayout({ children }) {
+ return (
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/app/layout.js b/app/layout.js
new file mode 100644
index 00000000..ee5f8b66
--- /dev/null
+++ b/app/layout.js
@@ -0,0 +1,14 @@
+import "../styles/globals.css";
+
+export const metadata = {
+ title: "Deen Bridge",
+ description: "Islamic education platform",
+};
+
+export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/molecules/dashboard/cards/courseCard.jsx b/components/molecules/dashboard/cards/courseCard.jsx
index 03fd08bd..505ce1ec 100644
--- a/components/molecules/dashboard/cards/courseCard.jsx
+++ b/components/molecules/dashboard/cards/courseCard.jsx
@@ -1,4 +1,5 @@
import { Progress } from "@/components/ui/progress";
+import { Badge } from "@/components/ui/badge";
import Button from "@/components/atoms/form/Button";
import Link from "next/link";
import { Ellipsis, CheckCircle } from "lucide-react";
@@ -7,7 +8,7 @@ import Image from "next/image";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useBookmark } from "@/hooks/useBookmark";
import BookmarkButton from "@/components/atoms/BookmarkButton";
-import { resolveCategorySlug } from "@/lib/categories";
+import { resolveSlug } from "@/lib/categories";
import { cn } from "@/lib/utils";
import {
poppins_400,
@@ -39,6 +40,12 @@ const CourseCard = ({ course, onBookmarkChange, initialIsBookmarked, progress })
e.stopPropagation();
await toggle();
};
+
+ // Resolve the stored category string to a known slug.
+ // Unknown / legacy values get slug=null -> fallback to decorative badge only.
+ const categorySlug = resolveSlug(course.category);
+ const categoryLabel = course.category || "General";
+
return (
{/* Image */}
@@ -52,19 +59,22 @@ const CourseCard = ({ course, onBookmarkChange, initialIsBookmarked, progress })
/>
- {/* Category */}
-
-
- {course.category || "General"}
-
+ {/* Category badge — links to category landing page when slug is known */}
+
+ {categorySlug ? (
+ e.stopPropagation()}
+ >
+
+ {categoryLabel}
+
+
+ ) : (
+
+ {categoryLabel}
+
+ )}
{user?._id === course?.createdBy?._id ? (
}))
);
+// Alias for backward compatibility — some pages import CATEGORIES directly.
+export const CATEGORIES = ISLAMIC_CATEGORIES;
+
export const CATEGORY_MAP = Object.fromEntries(
ISLAMIC_CATEGORIES.map((category) => [category.slug, category])
);
@@ -426,10 +429,44 @@ export function resolveCategorySlug(value) {
return FALLBACK_SLUG;
}
+/**
+ * Resolve a raw category string to a known slug or null.
+ * Returns null (not FALLBACK_SLUG) when no match is found, so callers can
+ * distinguish "no match" from "matched the fallback".
+ *
+ * @param {string | undefined | null} raw
+ * @returns {string | null}
+ */
+export function resolveSlug(raw) {
+ if (!raw) return null;
+ const normalised = raw.toLowerCase().trim();
+ if (!normalised) return null;
+
+ // Exact slug match
+ if (Object.hasOwn(CATEGORY_MAP, normalised)) return normalised;
+ // Exact label match
+ const byLabel = ISLAMIC_CATEGORIES.find(
+ (c) => c.label.toLowerCase().trim() === normalised
+ );
+ if (byLabel) return byLabel.slug;
+
+ // Partial label match — only return if EXACTLY ONE match (unambiguous)
+ const partialMatches = ISLAMIC_CATEGORIES.filter(
+ (c) =>
+ c.label.toLowerCase().includes(normalised) ||
+ normalised.includes(c.label.toLowerCase())
+ );
+ if (partialMatches.length === 1) return partialMatches[0].slug;
+
+ // Unknown value — log and return null
+ console.warn(`[resolveSlug] Unknown category value: "${raw}"`);
+ return null;
+}
+
export function getCategoryBySlug(slug) {
if (!slug) return null;
if (slug === FALLBACK_SLUG) return FALLBACK_CATEGORY;
- return CATEGORY_MAP[slug] || null;
+ return Object.hasOwn(CATEGORY_MAP, slug) ? CATEGORY_MAP[slug] : null;
}
export function getCategoryLabel(slug) {
@@ -437,6 +474,19 @@ export function getCategoryLabel(slug) {
return category ? category.label : "General";
}
+/**
+ * Get categories grouped by their parent group, as an array of objects.
+ * Useful for building grouped UIs (ComboBox, category hub grid).
+ *
+ * @returns {{ group: string, categories: Category[] }[]}
+ */
+export function getGroupedCategories() {
+ return CATEGORY_GROUPS.map((group) => ({
+ group: group.label,
+ categories: group.categories,
+ }));
+}
+
// Course list -> { slug: count }. Empty / legacy / free-text values count
// toward the fallback bucket.
export function getCategoryCounts(courses) {