@msflib/react-courses¶
Standalone courses module built on TanStack Query. Wraps the LMS backend's
courses module — browsing/searching courses, enrolling, and managing a
course's topics and topic items (lessons, assignments, quizzes, etc.),
including per-learner progress tracking.
Install¶
pnpm add @msflib/react-courses
Peer deps¶
- react >= 18
- @tanstack/react-query >= 5
Usage¶
Wrap your app with the module Provider inside your QueryClientProvider.
In Next.js App Router, Provider must be in a client component (
"use client").
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { CoursesProvider, useCourses } from '@msflib/react-courses';
const queryClient = new QueryClient();
<QueryClientProvider client={queryClient}>
<CoursesProvider
options={{ requireAuth: true, isAuthenticated: true, listParams: { limit: 20 } }}
>
<CourseCatalog />
</CoursesProvider>
</QueryClientProvider>;
function CourseCatalog() {
const { courses, myCourses, loading } = useCourses();
return null;
}
Browsing and enrolling¶
courses (admin/catalog listing) and myCourses (the current user's
enrolled courses) are both fetched automatically on mount. searchCourses
runs an on-demand filtered search without touching either cached list.
import { useCourses } from '@msflib/react-courses';
function CourseSearch() {
const { searchCourses, registerCourse } = useCourses();
async function findAndEnroll() {
const results = await searchCourses({ title: 'Intro', tag_id: 3 });
const course = results[0];
if (course) {
await registerCourse(course.id, { registration_key: 'ABC123' });
}
}
return null;
}
SSO-based enrollment (e.g. a learner arriving via a signed link) uses
ssoRegisterCourse instead, trading a registration_key for a token.
Managing a course's content¶
A course is made of topics, each with ordered items (lessons,
assignments, quizzes, projects). createCourse/updateCourse and
addTopicItem/updateTopicItem send multipart/form-data — build the
FormData yourself (they accept an optional file upload alongside the
regular fields).
import { useCourses } from '@msflib/react-courses';
function CourseEditor({ courseId }: { courseId: number }) {
const { addTopics, addTopicItem } = useCourses();
async function addWeekOne() {
const [topic] = await addTopics(courseId, [
{ title: 'Week 1', label: 'week-1', order: 0 },
]);
const formData = new FormData();
formData.append('title', 'Welcome video');
formData.append('type', 'lesson');
formData.append('media_source', 'external');
formData.append('duration', '600');
formData.append('order', '0');
formData.append('url', 'https://example.com/video.mp4');
await addTopicItem(topic.id, formData);
}
return null;
}
Tracking progress¶
updateItemProgress records a learner's progress against a specific topic
item (status, score, timestamps) — this is what a learner-facing player
would call as the user starts/completes an item.
import { useCourses } from '@msflib/react-courses';
function LessonPlayer({ courseId, topicId, itemId }: { courseId: number; topicId: number; itemId: number }) {
const { updateItemProgress } = useCourses();
async function onComplete() {
await updateItemProgress(courseId, topicId, itemId, {
status: 'completed',
completed_at: new Date().toISOString(),
score: 100,
});
}
return null;
}
The item's progress field (present on the learner-facing me/detail
responses, absent on the admin catalog) reflects this after a refetch.
Standalone hooks¶
useCourse(courseId) fetches a single course outside the provider's
cached list/me queries — handy for a course detail page that shouldn't
wait on the catalog or "my courses" fetch.
import { useCourse } from '@msflib/react-courses';
function CoursePage({ courseId }: { courseId: number }) {
const { data: course, isPending } = useCourse(courseId);
return null;
}
Endpoints¶
Configure paths through configureApplication({ endpoints: { courses } }).
All routes are path-scoped by workspace (/{workspace_slug}/courses/...).
| Key | Default path | Methods / routes |
|---|---|---|
courses |
/courses |
base path used for scoping/matching |
Routes built off courses:
| Route | Method | Context method |
|---|---|---|
${courses} |
GET |
listCourses / courses |
${courses} |
POST (multipart) |
createCourse |
${courses}/me |
GET |
listMyCourses / myCourses |
${courses}/search |
GET |
searchCourses |
${courses}/sso-register |
POST |
ssoRegisterCourse |
${courses}/{course_id} |
GET |
getCourse |
${courses}/{course_id} |
PUT (multipart) |
updateCourse |
${courses}/{course_id} |
DELETE |
deleteCourse |
${courses}/{course_id}/register |
POST |
registerCourse |
${courses}/{course_id}/topics |
POST |
addTopics |
${courses}/topics/{topic_id} |
PUT |
updateTopic |
${courses}/topics/{topic_id} |
DELETE |
deleteTopic |
${courses}/topics/{topic_id}/items |
POST (multipart) |
addTopicItem |
${courses}/topics/items/{item_id} |
PUT (multipart) |
updateTopicItem |
${courses}/topics/items/{item_id} |
DELETE |
deleteTopicItem |
${courses}/progress/{course_id}/topics/{topic_id}/items/{item_id} |
PUT |
updateItemProgress |
Notes:
- All 16 endpoints require an authenticated session (OAuth2PasswordBearer) except search, which is open.
- createCourse, updateCourse, addTopicItem, and updateTopicItem send multipart/form-data; every other write sends JSON.
- Any mutation that changes course/topic/item state invalidates both the courses and myCourses caches, since either view could be affected.