Skip to content

feat(courses): multi-step course authoring wizard - #143

Merged
zeemscript merged 2 commits into
Deen-Bridge:devfrom
Ahbiz:feature/course-authoring-wizard
Jul 24, 2026
Merged

feat(courses): multi-step course authoring wizard#143
zeemscript merged 2 commits into
Deen-Bridge:devfrom
Ahbiz:feature/course-authoring-wizard

Conversation

@Ahbiz

@Ahbiz Ahbiz commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #129

Replaces the cramped single-form modal with a guided, multi-step course authoring wizard on a dedicated route (/dashboard/courses/create), featuring a curriculum lesson builder, draft autosave, learner preview, and unified edit mode.


Key Features Added

  1. Dedicated Full-Page Wizard Routes:

    • /dashboard/courses/create: Dedicated creation wizard route (replacing the cramped max-w-md modal).
    • /dashboard/courses/edit/[courseId]: Dedicated edit route reusing the same wizard component.
  2. 5-Step Authoring Flow with Progress Bar:

    • Step 0 — Basics: Course title, description, and category selection.
    • Step 1 — Curriculum Builder: Add, reorder (move up/down), and remove lessons/modules with order numbers and duration metadata.
    • Step 2 — Media: Cover thumbnail image picker and per-lesson video file selection.
    • Step 3 — Pricing: Free vs. Paid course options with price input ($).
    • Step 4 — Review & Learner Preview: Read-only preview showing course card presentation, instructor info, syllabus, and listed price before publishing.
  3. Draft Autosave & Session Resume (useDraftAutosave):

    • Automatically debounces and saves form progress to localStorage (namespaced per user & course ID).
    • Shows a "Resume Draft" prompt banner when returning to an un-submitted session.
    • Automatically clears the draft upon successful course publish.
  4. React-Hook-Form + Zod Validation:

    • Integrated full schema validation with per-step field validation on "Next".
    • Shows inline error messages for missing required fields.
  5. ComboBox Control Fix (CategoryCombobox.jsx):

    • Fixed category combobox state initialization so existing course categories pre-fill correctly in edit mode.
  6. Unified Edit Action (lib/actions/courses/edit-course.js):

    • Implemented editCourse action to update existing course metadata and optional new media uploads seamlessly.

Verification & Testing

  • Target Branch: dev
  • Linting (npm run lint): Passed cleanly (0 errors)
  • Production Build (npm run build): Passed successfully with static/dynamic route generation:
    • ○ /dashboard/courses/create
    • ƒ /dashboard/courses/edit/[courseId]
  • Tested back/next step navigation and field validation.
  • Verified draft autosave, reload resume prompt, and draft cleanup on publish.

Summary by CodeRabbit

  • New Features
    • Added a guided, multi-step workflow for creating and editing courses with a step-progress indicator.
    • Added Course Basics, Curriculum & Lessons, Media, Pricing & Enrollment, and Review & Learner Preview steps.
    • Added draft autosave with resume/discard options.
    • Added thumbnail and per-lesson video uploads with upload progress during publish.
    • Added dedicated pages for course creation and editing.
  • Bug Fixes
    • Improved category selection to stay synchronized with saved or edited course data.
  • Improvements
    • Course creation now uses a dedicated page rather than an in-page modal.

@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

@Ahbiz 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 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

A routed CourseWizard replaces the course-creation modal with create and edit pages. The wizard adds validated multi-step form state, lesson management, media uploads, pricing, review preview, draft autosave, edit hydration, submission handling, and updated service-worker precache metadata.

Changes

Course authoring flow

Layer / File(s) Summary
Routed wizard entrypoints
app/dashboard/courses/page.jsx, app/dashboard/courses/create/page.jsx, app/dashboard/courses/edit/[courseId]/page.jsx
Course creation navigates to a dedicated route, while create and edit routes render CourseWizard with the edit route’s courseId.
Wizard contracts and step UI
components/organisms/create/course-wizard.jsx, components/organisms/create/wizard-steps/*, components/molecules/dashboard/wizard-step-indicator.jsx
The wizard defines Zod validation and renders basics, curriculum, media, pricing, review, and step-navigation interfaces.
Draft and edit state
hooks/useDraftAutosave.js, components/atoms/form/ComboBox.jsx, components/organisms/create/course-wizard.jsx
Form changes are stored in user/course-scoped local storage, drafts can be resumed or discarded, edit data hydrates the form, and category values follow prop updates.
Submission and wizard rendering
lib/actions/courses/edit-course.js, components/organisms/create/course-wizard.jsx
The wizard uploads media, submits create or edit payloads, clears successful drafts, displays loading and upload states, and navigates after completion.

Service-worker precache

Layer / File(s) Summary
Precache manifest update
public/sw.js
The embedded service-worker precache entries and associated revision metadata are updated.

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

Sequence Diagram(s)

sequenceDiagram
  participant CourseAuthor
  participant CoursesPage
  participant CourseWizard
  participant CloudinaryUpload
  participant CourseAPI

  CourseAuthor->>CoursesPage: Select Create Course
  CoursesPage->>CourseWizard: Navigate to create route
  CourseAuthor->>CourseWizard: Complete wizard steps
  CourseWizard->>CloudinaryUpload: Upload selected media
  CloudinaryUpload-->>CourseWizard: Return media URLs
  CourseWizard->>CourseAPI: Create or update course
  CourseAPI-->>CourseWizard: Return saved course
  CourseWizard-->>CourseAuthor: Navigate to course detail
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The public/sw.js precache manifest update is unrelated to the course wizard feature and looks like incidental build churn. Remove the service worker manifest churn unless PWA caching changes were intentionally part of this PR.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a multi-step course authoring wizard.
Linked Issues check ✅ Passed The PR implements the wizard, curriculum builder, autosave/resume, preview, edit hydration, and RHF/Zod validation requested in #129.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 8

🧹 Nitpick comments (3)
app/dashboard/courses/create/page.jsx (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep this route page server-rendered.

This page only renders CourseWizard; Server Components can render Client Components. Removing "use client" avoids expanding the client boundary unnecessarily.

As per path instructions, app/** requires flagging “client components that could be server components.”

🤖 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/create/page.jsx` at line 1, Remove the "use client"
directive from the route page while leaving the CourseWizard rendering
unchanged. Keep the page server-rendered, relying on the ability of Server
Components to render the client component CourseWizard.

Source: Path instructions

components/molecules/dashboard/wizard-step-indicator.jsx (1)

24-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: add aria-current="step" to the active step button for screen readers.

The visual active/completed states are conveyed with color/icon only; adding aria-current when isActive is true gives assistive tech users the same signal.

♻️ Proposed tweak
               <button
                 type="button"
                 disabled={!isClickable}
+                aria-current={isActive ? "step" : undefined}
                 onClick={() => isClickable && onStepClick && onStepClick(idx)}
🤖 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 `@components/molecules/dashboard/wizard-step-indicator.jsx` around lines 24 -
31, Update the step button in the wizard step indicator to set aria-current to
"step" when isActive is true, and leave it unset for inactive steps. Preserve
the existing click, disabled, and className behavior.
app/dashboard/courses/edit/[courseId]/page.jsx (1)

6-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider making this a server component with async params instead of a client component.

This page only reads the route param and forwards it to CourseWizard (which is itself "use client"), so the page doesn't need "use client" or useParams(). Next.js 15's idiomatic pattern for App Router pages is an async server component that awaits the params prop, letting the client boundary start at CourseWizard.

♻️ Proposed refactor
-"use client";
-import React from "react";
-import { useParams } from "next/navigation";
-import CourseWizard from "`@/components/organisms/create/course-wizard`";
-
-export default function EditCoursePage() {
-  const params = useParams();
-  const courseId = params?.courseId;
-
-  return <CourseWizard courseId={courseId} />;
-}
+import CourseWizard from "`@/components/organisms/create/course-wizard`";
+
+export default async function EditCoursePage({ params }) {
+  const { courseId } = await params;
+
+  return <CourseWizard courseId={courseId} />;
+}

As per path instructions, "Flag use of deprecated patterns... client components that could be server components."

🤖 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/edit/`[courseId]/page.jsx around lines 6 - 11, Convert
EditCoursePage into an async server component that receives the route params
prop and awaits params to obtain courseId, removing the useParams() dependency
and any client-component directive. Continue passing courseId to the existing
client CourseWizard component so the client boundary remains there.

Source: Path instructions

🤖 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 `@components/organisms/create/course-wizard.jsx`:
- Line 24: Update the thumbnailFile field in courseSchema to be required rather
than optional, ensuring STEP_FIELDS[2] validation fails when no thumbnail is
selected and users cannot proceed or publish without one. Keep the existing
thumbnail validation behavior for selected files.
- Around line 198-236: The handlePublish flow currently uploads only
data.lessons[0].videoFile, leaving later lesson files unsaved. Update
handlePublish to upload every selected lesson video through
videoUpload.uploadFile, replace each lesson’s videoFile with its uploaded URL in
the payload passed to createCourse or editCourse, and preserve lessons without
selected files; if the backend cannot persist multiple lesson URLs, gate the
per-lesson upload UI instead of accepting unsupported files.
- Around line 131-166: Update the loadCourse flow in the edit-mode useEffect to
handle a falsy result from getCourseById as a load failure, not as a successful
empty course. Show the existing failure toast or equivalent error state and
prevent the wizard from remaining usable with blank defaults, while preserving
the normal reset behavior when course data is returned.

In `@components/organisms/create/wizard-steps/curriculum.jsx`:
- Around line 113-147: Update the repeated lesson fields in the curriculum form
to use stable, index-based IDs and matching htmlFor attributes: associate each
title, duration, and summary Label with its corresponding Input or Textarea.
Apply this consistently within the lesson-rendering block without changing
validation or field registration.

In `@components/organisms/create/wizard-steps/review.jsx`:
- Around line 18-22: Update the thumbnail preview logic around thumbnailUrl to
create object URLs inside a useEffect and store the current URL in useState,
rather than calling URL.createObjectURL during render. Revoke the previous blob
URL in the effect cleanup and when the thumbnail file changes, while preserving
the existing formValues.thumbnailUrl and fallback image behavior.

In `@hooks/useDraftAutosave.js`:
- Around line 17-26: Update the serialization callback in useDraftAutosave so
File values persist as null rather than {_file: true, ...} metadata. Ensure
reset(draft) cannot restore file-like objects as selected media, requiring users
to re-select files before publishing while preserving serialization of other
form values.
- Around line 13-33: Debounce the autosave handler inside the useEffect watching
form changes so serialization and localStorage.setItem occur only after a short
inactivity period. Track the pending timeout, reset it on each watch event, and
clear it during effect cleanup before unsubscribing; preserve the existing
serialization and error handling in the delayed save.

In `@lib/actions/courses/edit-course.js`:
- Around line 10-24: Update the courseData construction in the course update
flow to include form.lessons, preserving each lesson’s title, description,
duration, order, and media URLs so curriculum edits are sent by the
axiosInstance.put request. If the API contract cannot accept lesson updates,
explicitly gate or disable curriculum editing instead of allowing unsaved
changes to appear successful.

---

Nitpick comments:
In `@app/dashboard/courses/create/page.jsx`:
- Line 1: Remove the "use client" directive from the route page while leaving
the CourseWizard rendering unchanged. Keep the page server-rendered, relying on
the ability of Server Components to render the client component CourseWizard.

In `@app/dashboard/courses/edit/`[courseId]/page.jsx:
- Around line 6-11: Convert EditCoursePage into an async server component that
receives the route params prop and awaits params to obtain courseId, removing
the useParams() dependency and any client-component directive. Continue passing
courseId to the existing client CourseWizard component so the client boundary
remains there.

In `@components/molecules/dashboard/wizard-step-indicator.jsx`:
- Around line 24-31: Update the step button in the wizard step indicator to set
aria-current to "step" when isActive is true, and leave it unset for inactive
steps. Preserve the existing click, disabled, and className behavior.
🪄 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: 94ecf007-4d77-41c1-96ab-0550122479d4

📥 Commits

Reviewing files that changed from the base of the PR and between 933ccb9 and 3630c22.

📒 Files selected for processing (14)
  • app/dashboard/courses/create/page.jsx
  • app/dashboard/courses/edit/[courseId]/page.jsx
  • app/dashboard/courses/page.jsx
  • components/atoms/form/ComboBox.jsx
  • components/molecules/dashboard/wizard-step-indicator.jsx
  • components/organisms/create/course-wizard.jsx
  • components/organisms/create/wizard-steps/basics.jsx
  • components/organisms/create/wizard-steps/curriculum.jsx
  • components/organisms/create/wizard-steps/media.jsx
  • components/organisms/create/wizard-steps/pricing.jsx
  • components/organisms/create/wizard-steps/review.jsx
  • hooks/useDraftAutosave.js
  • lib/actions/courses/edit-course.js
  • public/sw.js

Comment thread components/organisms/create/course-wizard.jsx Outdated
Comment thread components/organisms/create/course-wizard.jsx Outdated
Comment thread components/organisms/create/course-wizard.jsx
Comment thread components/organisms/create/wizard-steps/curriculum.jsx
Comment thread components/organisms/create/wizard-steps/review.jsx Outdated
Comment thread hooks/useDraftAutosave.js
Comment thread hooks/useDraftAutosave.js Outdated
Comment thread lib/actions/courses/edit-course.js

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
components/organisms/create/wizard-steps/curriculum.jsx (2)

101-106: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make last-lesson removal consistent with the empty state.

disabled={fields.length <= 1} prevents create-mode users from reaching the rendered fields.length === 0 state. If one lesson is required, remove or replace the empty-state UI; otherwise allow the final removal and rely on schema validation before advancing or publishing.

Also applies to: 162-174

🤖 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 `@components/organisms/create/wizard-steps/curriculum.jsx` around lines 101 -
106, Update the lesson removal logic around the ShadcnButton and the
fields.length === 0 empty-state rendering so their behavior is consistent:
either remove/replace the unreachable empty state when one lesson is required,
or allow removing the final lesson and rely on schema validation before
advancing or publishing. Apply the same change to the corresponding removal
control near the referenced second location.

117-129: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Associate the title validation error with its input.

Add an error ID plus aria-invalid and aria-describedby to the title input. The current red styling is visual only, so screen-reader users may not learn why the step cannot proceed.

🤖 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 `@components/organisms/create/wizard-steps/curriculum.jsx` around lines 117 -
129, Update the title Input associated with register in the curriculum step to
expose validation state: add a unique error-message ID, set aria-invalid when
lessonError?.title exists, and set aria-describedby to that ID when the error is
present. Apply the same ID to the conditional title error paragraph so assistive
technologies associate it with the input while preserving the existing styling
and message.
🤖 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 `@hooks/useDraftAutosave.js`:
- Around line 12-18: Update useDraftAutosave and its storageKey derivation so no
draft is persisted under a shared "anon" key: return a null key until a stable
authenticated user ID exists, and skip the subscription/persistence effect when
storageKey is null. Add the same !storageKey guard to hasDraft, loadDraft, and
clearDraft so helpers neither read nor modify shared storage during auth
hydration or logout.

---

Outside diff comments:
In `@components/organisms/create/wizard-steps/curriculum.jsx`:
- Around line 101-106: Update the lesson removal logic around the ShadcnButton
and the fields.length === 0 empty-state rendering so their behavior is
consistent: either remove/replace the unreachable empty state when one lesson is
required, or allow removing the final lesson and rely on schema validation
before advancing or publishing. Apply the same change to the corresponding
removal control near the referenced second location.
- Around line 117-129: Update the title Input associated with register in the
curriculum step to expose validation state: add a unique error-message ID, set
aria-invalid when lessonError?.title exists, and set aria-describedby to that ID
when the error is present. Apply the same ID to the conditional title error
paragraph so assistive technologies associate it with the input while preserving
the existing styling and message.
🪄 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: 8b7dd8e1-3fa5-45ca-9552-70b440fa1936

📥 Commits

Reviewing files that changed from the base of the PR and between 3630c22 and 041b84a.

📒 Files selected for processing (9)
  • app/dashboard/courses/create/page.jsx
  • app/dashboard/courses/edit/[courseId]/page.jsx
  • components/molecules/dashboard/wizard-step-indicator.jsx
  • components/organisms/create/course-wizard.jsx
  • components/organisms/create/wizard-steps/curriculum.jsx
  • components/organisms/create/wizard-steps/review.jsx
  • hooks/useDraftAutosave.js
  • lib/actions/courses/edit-course.js
  • public/sw.js
💤 Files with no reviewable changes (1)
  • app/dashboard/courses/create/page.jsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/actions/courses/edit-course.js
  • components/molecules/dashboard/wizard-step-indicator.jsx
  • components/organisms/create/course-wizard.jsx

Comment thread hooks/useDraftAutosave.js
Comment on lines +12 to +18
// Debounced save form draft to localStorage on form state changes
useEffect(() => {
if (!watch) return;

let timer = null;

const subscription = watch((formValues) => {

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not autosave under a shared "anon" key.

When useAuth().user is temporarily unavailable, storageKey uses "anon" while this effect immediately subscribes to form changes. Drafts created during auth hydration or logout can therefore be read by another account on the same browser, and hasDraft() may show that user another person’s course content.

Return a null key until a stable user ID exists and skip persistence/helpers for that state, or generate a truly per-session anonymous identifier.

🔒 Proposed fix
-  const storageKey = `${DRAFT_KEY_PREFIX}${user?._id || "anon"}${
-    courseId ? `_${courseId}` : ""
-  }`;
+  const storageKey = user?._id
+    ? `${DRAFT_KEY_PREFIX}${user._id}${courseId ? `_${courseId}` : ""}`
+    : null;

   useEffect(() => {
-    if (!watch) return;
+    if (!watch || !storageKey) return;

Apply the same !storageKey guard in hasDraft, loadDraft, and clearDraft.

🤖 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 `@hooks/useDraftAutosave.js` around lines 12 - 18, Update useDraftAutosave and
its storageKey derivation so no draft is persisted under a shared "anon" key:
return a null key until a stable authenticated user ID exists, and skip the
subscription/persistence effect when storageKey is null. Add the same
!storageKey guard to hasDraft, loadDraft, and clearDraft so helpers neither read
nor modify shared storage during auth hydration or logout.

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.

2 participants