feat(courses): multi-step course authoring wizard - #143
Conversation
…lder, draft autosave, and preview
|
@Ahbiz is attempting to deploy a commit to the Deen Bridge Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughA routed ChangesCourse authoring flow
Service-worker precache
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
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (3)
app/dashboard/courses/create/page.jsx (1)
1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep 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 valueOptional: 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-currentwhenisActiveis 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 winConsider making this a server component with async
paramsinstead 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"oruseParams(). Next.js 15's idiomatic pattern for App Router pages is an async server component that awaits theparamsprop, letting the client boundary start atCourseWizard.♻️ 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
📒 Files selected for processing (14)
app/dashboard/courses/create/page.jsxapp/dashboard/courses/edit/[courseId]/page.jsxapp/dashboard/courses/page.jsxcomponents/atoms/form/ComboBox.jsxcomponents/molecules/dashboard/wizard-step-indicator.jsxcomponents/organisms/create/course-wizard.jsxcomponents/organisms/create/wizard-steps/basics.jsxcomponents/organisms/create/wizard-steps/curriculum.jsxcomponents/organisms/create/wizard-steps/media.jsxcomponents/organisms/create/wizard-steps/pricing.jsxcomponents/organisms/create/wizard-steps/review.jsxhooks/useDraftAutosave.jslib/actions/courses/edit-course.jspublic/sw.js
There was a problem hiding this comment.
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 winMake last-lesson removal consistent with the empty state.
disabled={fields.length <= 1}prevents create-mode users from reaching the renderedfields.length === 0state. 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 winAssociate the title validation error with its input.
Add an error ID plus
aria-invalidandaria-describedbyto 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
📒 Files selected for processing (9)
app/dashboard/courses/create/page.jsxapp/dashboard/courses/edit/[courseId]/page.jsxcomponents/molecules/dashboard/wizard-step-indicator.jsxcomponents/organisms/create/course-wizard.jsxcomponents/organisms/create/wizard-steps/curriculum.jsxcomponents/organisms/create/wizard-steps/review.jsxhooks/useDraftAutosave.jslib/actions/courses/edit-course.jspublic/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
| // Debounced save form draft to localStorage on form state changes | ||
| useEffect(() => { | ||
| if (!watch) return; | ||
|
|
||
| let timer = null; | ||
|
|
||
| const subscription = watch((formValues) => { |
There was a problem hiding this comment.
🔒 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.
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
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.5-Step Authoring Flow with Progress Bar:
Draft Autosave & Session Resume (
useDraftAutosave):localStorage(namespaced per user & course ID).React-Hook-Form + Zod Validation:
ComboBox Control Fix (
CategoryCombobox.jsx):Unified Edit Action (
lib/actions/courses/edit-course.js):editCourseaction to update existing course metadata and optional new media uploads seamlessly.Verification & Testing
devnpm run lint): Passed cleanly (0 errors)npm run build): Passed successfully with static/dynamic route generation:○ /dashboard/courses/createƒ /dashboard/courses/edit/[courseId]Summary by CodeRabbit