Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions app/dashboard/courses/categories/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"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();
setCourses(data);
} catch {
setError(true);
} finally {
setLoading(false);
}
Comment on lines +22 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File list ==\n'
git ls-files 'app/dashboard/courses/**' 'lib/actions/courses/**' | sort

printf '\n== Outline relevant files ==\n'
for f in \
  app/dashboard/courses/categories/page.jsx \
  app/dashboard/courses/category/[slug]/page.jsx \
  app/dashboard/courses/page.jsx \
  lib/actions/courses/fetch-courses.js \
  lib/actions/courses/get-bookmarked-courses.js
do
  if [ -f "$f" ]; then
    echo "--- $f"
    wc -l "$f"
    sed -n '1,260p' "$f" | cat -n
  fi
done

printf '\n== Search for fetchCourses usages and error handling ==\n'
rg -n "fetchCourses\\(|getBookmarkedCourses\\(|setError\\(|NetworkErrorComp|no courses|bookmark" app/dashboard/courses lib/actions/courses -S

Repository: Deen-Bridge/dnb-frontend

Length of output: 34836


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in lib/actions/courses/fetch-courses.js app/dashboard/courses/categories/page.jsx app/dashboard/courses/category/[slug]/page.jsx app/dashboard/courses/page.jsx; do
  echo "===== $f ====="
  sed -n '1,220p' "$f" | cat -n
done

Repository: Deen-Bridge/dnb-frontend

Length of output: 27471


🏁 Script executed:

#!/bin/bash
set -euo pipefail

f=lib/actions/courses/bookmark-course.js
echo "===== $f ====="
wc -l "$f"
sed -n '1,220p' "$f" | cat -n

Repository: Deen-Bridge/dnb-frontend

Length of output: 2482


Let fetchCourses surface errors instead of returning an empty list.
On app/dashboard/courses/categories/page.jsx, app/dashboard/courses/category/[slug]/page.jsx, and the non-bookmark branch of app/dashboard/courses/page.jsx, fetchCourses() never reaches the catch, so failures collapse into empty-state UI and the retry button never appears. Keep the error shape consistent across these loaders; only the getBookmarkedCourses() branch should continue to control its own error state.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 23-23: Avoid using the initial state variable in setState
Context: setCourses(data)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

📍 Affects 3 files
  • app/dashboard/courses/categories/page.jsx#L22-L29 (this comment)
  • app/dashboard/courses/category/[slug]/page.jsx#L60-L67
  • app/dashboard/courses/page.jsx#L112-L124
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dashboard/courses/categories/page.jsx` around lines 22 - 29, Update
fetchCourses and its loaders so fetch failures are thrown to the callers instead
of converted to empty course lists, preserving a consistent error shape. Apply
the corresponding handling in app/dashboard/courses/categories/page.jsx lines
22-29, app/dashboard/courses/category/[slug]/page.jsx lines 60-67, and the
non-bookmark branch of app/dashboard/courses/page.jsx lines 112-124; leave the
getBookmarkedCourses branch’s existing error-state control unchanged.

};

useEffect(() => {
loadCourses();
}, []);

// Derive counts from the fetched course list
const counts = useMemo(() => getCategoryCounts(courses), [courses]);

const totalCourses = courses.length;

if (error) {
return (
<NetworkErrorComp
errMsg="Failed to load course categories, please try again."
reset={loadCourses}
/>
);
}

return (
<div className="bg-muted min-h-full w-full">
{/* ── Hero header ── */}
<div className="bg-gradient-to-br from-accent via-green-600 to-highlight px-6 py-10 text-white">
<div className="max-w-4xl mx-auto">
<div className="flex items-center gap-3 mb-3">
<LayoutGrid className="w-8 h-8 opacity-90" />
<h1 className="text-3xl md:text-4xl font-bold">
Course Categories
</h1>
</div>
<p className="text-green-50 text-base md:text-lg max-w-xl">
Browse authentic Islamic knowledge across{" "}
{CATEGORIES.length} categories grouped into{" "}
{CATEGORY_GROUPS.length} disciplines.
</p>
{!loading && (
<p className="mt-2 text-green-100 text-sm">
{totalCourses} course{totalCourses !== 1 ? "s" : ""} available
</p>
)}
<div className="mt-5">
<Link
href="/dashboard/courses"
className="inline-flex items-center gap-2 bg-white text-accent font-semibold px-5 py-2.5 rounded-full text-sm hover:bg-green-50 transition-colors shadow"
>
<BookOpen className="w-4 h-4" />
Browse All Courses
</Link>
</div>
</div>
</div>

{/* ── Loading skeletons ── */}
{loading ? (
<div className="p-6 max-w-6xl mx-auto space-y-10">
{[...Array(3)].map((_, gi) => (
<div key={gi}>
<div className="h-6 w-48 bg-gray-200 rounded animate-pulse mb-4" />
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{[...Array(3)].map((_, ci) => (
<CourseCardSkeleton key={ci} />
))}
</div>
</div>
))}
</div>
) : (
/* ── Category groups ── */
<div className="p-6 max-w-6xl mx-auto space-y-10">
{CATEGORY_GROUPS.map((group) => {
const groupCategories = CATEGORIES.filter(
(c) => c.group === group
);
return (
<section key={group}>
<h2 className="text-xl md:text-2xl font-bold mb-4 text-foreground border-b border-border pb-2">
{group}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{groupCategories.map((cat) => {
const count = counts[cat.slug] || 0;
const isEmpty = count === 0;
return (
<Link
key={cat.slug}
href={`/dashboard/courses/category/${cat.slug}`}
className={`group flex flex-col rounded-2xl p-5 border transition-all ${
isEmpty
? "bg-muted/40 border-border opacity-60 hover:opacity-80"
: "bg-background border-border hover:border-accent hover:shadow-lg hover:scale-[1.02]"
}`}
aria-label={`${cat.label} — ${count} course${count !== 1 ? "s" : ""}`}
>
{/* Icon + count */}
<div className="flex items-start justify-between mb-3">
<span
className="text-3xl"
role="img"
aria-label={cat.label}
>
{cat.icon}
</span>
<span
className={`text-xs font-bold px-2.5 py-1 rounded-full ${
isEmpty
? "bg-muted text-muted-foreground"
: "bg-accent/10 text-accent"
}`}
>
{count} course{count !== 1 ? "s" : ""}
</span>
</div>

{/* Label + description */}
<h3
className={`font-semibold text-base leading-tight mb-1 transition-colors ${
isEmpty
? "text-muted-foreground"
: "text-foreground group-hover:text-accent"
}`}
>
{cat.label}
</h3>
<p className="text-sm text-muted-foreground line-clamp-2 flex-1">
{cat.description}
</p>

{/* CTA row */}
<div
className={`mt-4 flex items-center gap-1 text-xs font-semibold ${
isEmpty
? "text-muted-foreground"
: "text-accent group-hover:gap-2 transition-all"
}`}
>
{isEmpty ? "No courses yet" : "View courses"}
<ArrowRight className="w-3 h-3" />
</div>
</Link>
);
})}
</div>
</section>
);
})}
</div>
)}
</div>
);
}
Loading