Skip to content

feat: course category taxonomy, hub, landing pages & URL-driven filters - #122

Open
dehonesty2-svg wants to merge 1 commit into
Deen-Bridge:devfrom
dehonesty2-svg:feat/course-category-taxonomy-113
Open

feat: course category taxonomy, hub, landing pages & URL-driven filters#122
dehonesty2-svg wants to merge 1 commit into
Deen-Bridge:devfrom
dehonesty2-svg:feat/course-category-taxonomy-113

Conversation

@dehonesty2-svg

@dehonesty2-svg dehonesty2-svg commented Jul 22, 2026

Copy link
Copy Markdown

Summary

Resolves the category discovery gap described in the issue: categories were write-only (educators could pick one at creation time, but learners had no way to browse by it). This PR builds the full taxonomy + discovery experience.

Closes #113


What was built

1. lib/categories.js — single source of truth

  • 31 Islamic categories with slug, label, group, description, icon across 6 parent groups
  • resolveSlug(raw) — maps free-text/legacy category strings to known slugs; logs console.warn for unknowns, never crashes
  • getCategoryCounts(courses) — derives slug→count from the fetched course list; structured so swapping in a GET /api/courses/categories endpoint is a one-function change
  • getGroupedCategories() — grouped shape for ComboBox and the hub
  • islamicCategoriesCompat — backward-compat shim preserving the old lib/data.js shape

2. ComboBox.jsx — controlled component fix

  • Removed the internal value state that ignored the category prop
  • Now fully controlled by category / setCategory props
  • Imports taxonomy from lib/categories.js (grouped headings preserved, icons added)

3. courseCard.jsx — navigable category badge

  • Calls resolveSlug() on the stored category value
  • Known slug → badge wrapped in <Link href="/dashboard/courses/category/[slug]">
  • Unknown/legacy value → plain decorative badge, no crash

4. app/dashboard/courses/page.jsx — URL-driven filters

  • Horizontally scrollable category chips (all 31)
  • Text search input + sort dropdown (newest / price asc / price desc / rating)
  • All filter state lives in ?category=&sort=&q= — survives refresh, shareable links
  • "Browse Categories" link to the hub
  • Clear Filters button

5. app/dashboard/courses/categories/page.jsx — category hub

  • Hero header with total course count
  • All 6 groups rendered as section grids
  • Each card: icon, live course count badge, label, description, CTA
  • Empty categories de-emphasised (opacity-60) but never hidden
  • Loading skeletons (CourseCardSkeleton) + NetworkErrorComp

6. app/dashboard/courses/category/[slug]/page.jsx — category landing

  • Hero with breadcrumb, category icon, description, and live count
  • Filtered + sorted course grid (sort persisted in local state)
  • Designed empty state: category icon, copy, and "Create the first course" CTA that opens the existing CreateCourseForm modal
  • Unknown slug → NotFoundComp (no crash)
  • Loading skeletons + NetworkErrorComp

Acceptance criteria checklist

  • /dashboard/courses/categories renders all category groups with live course counts
  • /dashboard/courses/category/[slug] shows only that category's courses with working sort and a designed empty state; unknown slugs render a not-found state, not a crash
  • Main courses page has category chips + sort, state persisted in the URL query string; refreshing keeps the filter
  • Category badge on every course card navigates to the matching category page
  • Categories/labels/slugs defined in exactly one module (lib/categories.js); ComboBox and all new pages import from it; ComboBox respects its category prop
  • Courses whose stored category doesn't match the taxonomy still render and are reachable via the fallback bucket
  • npm run lint ✅ and npm run build ✅ — CI green

Testing

  • npm run lint — passes (zero new warnings/errors)
  • npm run build — compiled successfully; both new routes appear in the manifest:
    • ○ /dashboard/courses/categories
    • ƒ /dashboard/courses/category/[slug]

Summary by CodeRabbit

  • New Features
    • Added a course category hub with grouped categories, descriptions, icons, and course counts.
    • Added category landing pages with breadcrumbs, course listings, sorting, empty states, and course creation access.
    • Added URL-based search, category filtering, sorting, and bookmark views.
    • Added loading placeholders, retry options, and clearer no-results messaging.
    • Course category badges now link directly to relevant category pages.
    • Improved category selection with grouped, searchable options.

- Add lib/categories.js as single source of truth for 31 Islamic
  categories (slug, label, group, description, icon) across 6 groups.
  Includes resolveSlug() with legacy-value fallback + console.warn,
  getCategoryCounts() for client-side counts (backend-ready),
  getGroupedCategories(), and an islamicCategoriesCompat shim.

- Fix ComboBox.jsx: remove internal value state bug; now fully
  controlled by the category prop; imports from lib/categories.js.

- Update courseCard.jsx: category badge links to the category landing
  page via resolveSlug(); unknown/legacy categories render as a plain
  decorative badge (no crash).

- Update courses page with URL-driven filters: horizontally scrollable
  category chips, text search, sort (newest/price/rating), all state
  persisted in ?category=&sort=&q= query params; survives refresh.

- Add /dashboard/courses/categories: browsable hub showing all 6 groups
  and 31 category cards with live course counts; empty categories are
  de-emphasised (not hidden).

- Add /dashboard/courses/category/[slug]: landing page with hero,
  breadcrumb, filtered & sorted course grid, empty state with educator
  CTA, not-found state for unknown slugs.

Closes Deen-Bridge#113
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

@dehonesty2-svg is attempting to deploy a commit to the Deen Bridge Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a shared course-category taxonomy, category hub and landing pages, URL-persisted filtering and sorting, controlled category selection, and navigable category badges with loading and error states.

Changes

Course category discovery

Layer / File(s) Summary
Canonical category taxonomy
lib/categories.js
Defines category metadata, grouping, legacy-value resolution, compatibility data, and course-count utilities.
Category hub and landing routes
app/dashboard/courses/categories/page.jsx, app/dashboard/courses/category/[slug]/page.jsx
Adds grouped category browsing and category-specific course pages with sorting, loading/error handling, empty states, and course creation.
URL-driven course filtering
app/dashboard/courses/page.jsx
Synchronizes category, search, and sort filters with query parameters and derives the displayed course list from fetched data.
Shared category navigation
components/atoms/form/ComboBox.jsx, components/molecules/dashboard/cards/courseCard.jsx
Uses grouped taxonomy data in the controlled combobox and links recognized course categories to their landing pages.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Learner
  participant CategoryHubPage
  participant fetchCourses
  participant CategoryLandingPage
  Learner->>CategoryHubPage: Open category hub
  CategoryHubPage->>fetchCourses: Fetch courses
  fetchCourses-->>CategoryHubPage: Return course list
  CategoryHubPage-->>Learner: Show grouped categories and counts
  Learner->>CategoryLandingPage: Open category slug
  CategoryLandingPage->>fetchCourses: Fetch courses
  fetchCourses-->>CategoryLandingPage: Return course list
  CategoryLandingPage-->>Learner: Show filtered and sorted courses
Loading

Possibly related issues

  • Deen-Bridge/dnb-backend issue 42 — Covers the backend category taxonomy, slugs, metadata, and counts that this frontend change consumes and currently derives client-side.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change set: course taxonomy plus category pages and URL-driven filters.
Linked Issues check ✅ Passed The PR appears to implement the shared taxonomy, category hub/landing pages, URL filters, badge links, controlled ComboBox, and legacy-category fallbacks requested by #113.
Out of Scope Changes check ✅ Passed The changed files all map to the category taxonomy and discovery work; no unrelated code changes are evident in the summary.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with 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.

Inline comments:
In `@app/dashboard/courses/categories/page.jsx`:
- Around line 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.

In `@lib/categories.js`:
- Around line 270-275: Update the _bySlug and _labelToSlug lookup maps,
including the related lookup logic around lines 283-301, to reject inherited
keys such as "constructor" and "__proto__". Guard membership checks with
Object.hasOwn (or use null-prototype maps) so only actual taxonomy entries match
and unknown values continue through the existing fallback.
- Around line 295-307: Update resolveSlug so normalized empty input returns null
before any lookup or partial matching. Replace the current first-match partial
label logic with an unambiguous approach: return a slug only when the normalized
value matches an explicit legacy alias or exactly one category label; otherwise
return null.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 70e6d79f-c002-4dc1-9e30-1c4ea42fe464

📥 Commits

Reviewing files that changed from the base of the PR and between 37c25df and d076f6b.

📒 Files selected for processing (6)
  • app/dashboard/courses/categories/page.jsx
  • app/dashboard/courses/category/[slug]/page.jsx
  • app/dashboard/courses/page.jsx
  • components/atoms/form/ComboBox.jsx
  • components/molecules/dashboard/cards/courseCard.jsx
  • lib/categories.js

Comment on lines +22 to +29
try {
const data = await fetchCourses();
setCourses(data);
} catch {
setError(true);
} finally {
setLoading(false);
}

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.

Comment thread lib/categories.js
Comment on lines +270 to +275
const _bySlug = Object.fromEntries(CATEGORIES.map((c) => [c.slug, c]));

/** Normalised label → slug (for mapping legacy free-text category values) */
const _labelToSlug = Object.fromEntries(
CATEGORIES.map((c) => [c.label.toLowerCase().trim(), c.slug])
);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject inherited keys in the lookup maps.

Object.fromEntries() creates ordinary objects, so values such as "constructor" and "__proto__" are treated as matches even though they are not taxonomy entries. This bypasses the unknown-slug fallback used by app/dashboard/courses/category/[slug]/page.jsx and can create bogus count keys. Guard lookups with Object.hasOwn(...) or use null-prototype maps.

Proposed fix
 export function getCategoryBySlug(slug) {
-  return _bySlug[slug];
+  return Object.hasOwn(_bySlug, slug) ? _bySlug[slug] : undefined;
 }

 export function resolveSlug(raw) {
   if (!raw) return null;
   const normalised = raw.toLowerCase().trim();
   // Exact slug match
-  if (_bySlug[normalised]) return normalised;
+  if (Object.hasOwn(_bySlug, normalised)) return normalised;
   // Exact label match
-  if (_labelToSlug[normalised]) return _labelToSlug[normalised];
+  if (Object.hasOwn(_labelToSlug, normalised)) {
+    return _labelToSlug[normalised];
+  }

Also applies to: 283-301

🤖 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 `@lib/categories.js` around lines 270 - 275, Update the _bySlug and
_labelToSlug lookup maps, including the related lookup logic around lines
283-301, to reject inherited keys such as "constructor" and "__proto__". Guard
membership checks with Object.hasOwn (or use null-prototype maps) so only actual
taxonomy entries match and unknown values continue through the existing
fallback.

Comment thread lib/categories.js
Comment on lines +295 to +307
export function resolveSlug(raw) {
if (!raw) return null;
const normalised = raw.toLowerCase().trim();
// Exact slug match
if (_bySlug[normalised]) return normalised;
// Exact label match
if (_labelToSlug[normalised]) return _labelToSlug[normalised];
// Partial label match (handles e.g. "Fiqh (Islamic Jurisprudence)" stored as just "Fiqh")
const partialMatch = CATEGORIES.find((c) =>
c.label.toLowerCase().includes(normalised) ||
normalised.includes(c.label.toLowerCase())
);
if (partialMatch) return partialMatch.slug;

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make legacy matching explicit or unambiguous.

A whitespace-only value becomes "", and includes("") resolves it to the first category (quran-tafsir). More generally, an ambiguous value such as "Islamic" can resolve to fiqh merely because that word appears in one label. Prefer an explicit legacy-alias table, or only accept a non-empty unique match.

Proposed fix
   const normalised = raw.toLowerCase().trim();
+  if (!normalised) return null;

-  const partialMatch = CATEGORIES.find((c) =>
+  const partialMatches = CATEGORIES.filter((c) =>
     c.label.toLowerCase().includes(normalised) ||
     normalised.includes(c.label.toLowerCase())
   );
-  if (partialMatch) return partialMatch.slug;
+  if (partialMatches.length === 1) return partialMatches[0].slug;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function resolveSlug(raw) {
if (!raw) return null;
const normalised = raw.toLowerCase().trim();
// Exact slug match
if (_bySlug[normalised]) return normalised;
// Exact label match
if (_labelToSlug[normalised]) return _labelToSlug[normalised];
// Partial label match (handles e.g. "Fiqh (Islamic Jurisprudence)" stored as just "Fiqh")
const partialMatch = CATEGORIES.find((c) =>
c.label.toLowerCase().includes(normalised) ||
normalised.includes(c.label.toLowerCase())
);
if (partialMatch) return partialMatch.slug;
export function resolveSlug(raw) {
if (!raw) return null;
const normalised = raw.toLowerCase().trim();
if (!normalised) return null;
// Exact slug match
if (_bySlug[normalised]) return normalised;
// Exact label match
if (_labelToSlug[normalised]) return _labelToSlug[normalised];
// Partial label match (handles e.g. "Fiqh (Islamic Jurisprudence)" stored as just "Fiqh")
const partialMatches = CATEGORIES.filter((c) =>
c.label.toLowerCase().includes(normalised) ||
normalised.includes(c.label.toLowerCase())
);
if (partialMatches.length === 1) return partialMatches[0].slug;
🤖 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 `@lib/categories.js` around lines 295 - 307, Update resolveSlug so normalized
empty input returns null before any lookup or partial matching. Replace the
current first-match partial label logic with an unambiguous approach: return a
slug only when the normalized value matches an explicit legacy alias or exactly
one category label; otherwise return null.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant