feat: course category taxonomy, hub, landing pages & URL-driven filters - #122
feat: course category taxonomy, hub, landing pages & URL-driven filters#122dehonesty2-svg wants to merge 1 commit into
Conversation
- 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
|
@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. |
WalkthroughAdds 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. ChangesCourse category discovery
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
Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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
ESLint install timed out. The project may have too many dependencies for the sandbox. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
app/dashboard/courses/categories/page.jsxapp/dashboard/courses/category/[slug]/page.jsxapp/dashboard/courses/page.jsxcomponents/atoms/form/ComboBox.jsxcomponents/molecules/dashboard/cards/courseCard.jsxlib/categories.js
| try { | ||
| const data = await fetchCourses(); | ||
| setCourses(data); | ||
| } catch { | ||
| setError(true); | ||
| } finally { | ||
| setLoading(false); | ||
| } |
There was a problem hiding this comment.
🩺 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 -SRepository: 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
doneRepository: 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 -nRepository: 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-L67app/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.
| 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]) | ||
| ); |
There was a problem hiding this comment.
🎯 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.
| 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; |
There was a problem hiding this comment.
🎯 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.
| 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.
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 truthslug,label,group,description,iconacross 6 parent groupsresolveSlug(raw)— maps free-text/legacy category strings to known slugs; logsconsole.warnfor unknowns, never crashesgetCategoryCounts(courses)— derives slug→count from the fetched course list; structured so swapping in aGET /api/courses/categoriesendpoint is a one-function changegetGroupedCategories()— grouped shape for ComboBox and the hubislamicCategoriesCompat— backward-compat shim preserving the oldlib/data.jsshape2.
ComboBox.jsx— controlled component fixvaluestate that ignored thecategorypropcategory/setCategorypropslib/categories.js(grouped headings preserved, icons added)3.
courseCard.jsx— navigable category badgeresolveSlug()on the stored category value<Link href="/dashboard/courses/category/[slug]">4.
app/dashboard/courses/page.jsx— URL-driven filters?category=&sort=&q=— survives refresh, shareable links5.
app/dashboard/courses/categories/page.jsx— category hub6.
app/dashboard/courses/category/[slug]/page.jsx— category landingAcceptance criteria checklist
/dashboard/courses/categoriesrenders 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 crashlib/categories.js); ComboBox and all new pages import from it; ComboBox respects itscategorypropnpm run lint✅ andnpm run build✅ — CI greenTesting
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