React components for structured courses — a provider builds courses and assigns them to their own clients, who work through them at their own pace.
MyCourses) and a course player (CoursePlayer): a sidebar outline showing what is done, what is open and why the rest is locked; each lesson's video, audio, documents, text, embeds, quizzes, live session rooms and your app's own blocks; and Complete & continue. Video and audio pick up where the client left off. Progress saves itself.CourseLibrary), a builder (CourseBuilder) — an outline of sections and lessons, each lesson's blocks, and unlock rules (prerequisites, drip timers, approval) — and a clients screen (CourseClients) to enroll clients from your roster, approve or send back lessons, and follow progress.The host app owns branding (through the MUI theme), users and notifications: the components report what happens through event callbacks. A client's events (a completed lesson, a submission, a reply) are reported once their progress is saved, so a notification never points at work that isn't stored; a submission, reply or question that can't be saved fails rather than looking sent, the player shows where saving stands, and leaving the page with unsaved work gets the browser's warning.
npm install @hiyve/react-courses @hiyve/courses @hiyve/react @hiyve/core @hiyve/react-media-player \
@hiyve/react-assignments @hiyve/react-collaboration @hiyve/react-clips @hiyve/react-ui \
@hiyve/react-semantic-relay @hiyve/rtc-client @hiyve/utilities
The player, PDF viewer, text editor and recorder are Hiyve's own components, so their packages are needed even when you bring your own CourseStorage.
import { useState } from 'react';
import { createMemoryCourseStorage } from '@hiyve/courses';
import { CourseProvider, CoursePlayer, MyCourses } from '@hiyve/react-courses';
const storage = createMemoryCourseStorage({ userId: 'alex@example.com' });
function ClientCourses() {
const [enrollmentId, setEnrollmentId] = useState<string | null>(null);
return (
<CourseProvider storage={storage} currentUser={{ userId: 'alex@example.com', userName: 'Alex' }}>
{enrollmentId ? (
<CoursePlayer enrollmentId={enrollmentId} onBack={() => setEnrollmentId(null)} />
) : (
<MyCourses onOpenCourse={setEnrollmentId} />
)}
</CourseProvider>
);
}
A provider's side looks the same with CourseLibrary, CourseBuilder and CourseClients:
import { CourseBuilder, CourseClients, CourseLibrary, CourseProvider } from '@hiyve/react-courses';
<CourseProvider storage={storage} currentUser={{ userId: 'coach@example.com', userName: 'Jordan' }} onApprovalRequested={notifyProvider}>
{route.view === 'library' && <CourseLibrary onOpenCourse={openBuilder} onOpenClients={openClients} />}
{route.view === 'builder' && <CourseBuilder courseId={route.courseId} onBack={openLibrary} onPreview={openPreview} onOpenClients={openClients} />}
{route.view === 'clients' && <CourseClients courseId={route.courseId} roster={myClients} onBack={openLibrary} onOpenBuilder={openBuilder} />}
{route.view === 'preview' && <CoursePlayer previewCourseId={route.courseId} onBack={openBuilder} />}
</CourseProvider>
Keep courses in Hiyve user files with useHiyveCourseStorage from the /hiyve entry, inside HiyveProvider:
import { CourseProvider, CourseLibrary } from '@hiyve/react-courses';
import { useHiyveCourseStorage } from '@hiyve/react-courses/hiyve';
function Courses({ userId }: { userId: string }) {
const { storage } = useHiyveCourseStorage({ userId });
return (
<CourseProvider storage={storage} currentUser={{ userId }}>
<CourseLibrary onOpenCourse={openBuilder} />
</CourseProvider>
);
}
| Option | Type | Description |
|---|---|---|
userId |
string |
The signed-in user |
location |
string |
Folder for course documents. Default /Courses |
isProviderTrusted |
(providerId: string) => boolean |
Which providers this user accepts courses from, by cleaned user id (cleanUserId from @hiyve/core). Everyone by default. A new function on a later render is used from then on without rebuilding the storage |
Set isProviderTrusted in any app where anyone can share files with the user — by default a course from anyone who can is accepted and listed. It returns { storage, canWrite }. Changing a saved course or saved progress needs a room or no-video room connection: keep one open while course screens are in use. Until it is, canWrite is false and the components hold changes and save them once it opens.
Wraps every course component.
| Prop | Type | Description |
|---|---|---|
storage |
CourseStorage |
Where courses are kept (see @hiyve/courses) |
currentUser |
CourseUser |
{ userId, userName? } of the signed-in user |
blockTypes |
AnyCourseBlockType[] |
Block types your app adds (see Custom blocks) |
labels |
Partial<CourseLabels> |
Text overrides |
colors |
Partial<CourseColors> |
Color overrides |
allowedEmbedOrigins |
string[] |
Sites embed blocks may show. Default: YouTube, Vimeo, Loom |
resolveUserName |
(userId) => string | undefined |
Display names for providers and clients |
now |
() => number |
Clock in epoch ms |
maxUploadBytes |
number |
Largest file anyone can upload through the components — lesson media, submissions, recordings, photos, comment attachments and custom blocks. A larger file is refused with a message before anything is sent. No limit by default; set it to what your storage accepts |
onError |
(error: Error) => void |
Something failed to load or save (the clients screen also shows its own failures inline) |
onLessonCompleted |
({ enrollment, lessonId }) => void |
A client completed a lesson |
onApprovalRequested |
({ enrollment, lessonId }) => void |
A client completed a lesson that needs approval — notify the provider |
onCourseCompleted |
({ enrollment }) => void |
A client finished a course |
onLessonsUnlocked |
({ courseId, enrollmentId, clientId, lessonIds }) => void |
Lessons opened to a client — notify the client |
onClientEnrolled |
({ enrollment }) => void |
The provider enrolled a client |
onLessonDecided |
({ enrollment, lessonId, approved, note? }) => void |
The provider approved a lesson or sent it back |
onJoinLiveSession |
({ block, courseId, lessonId }) => void |
A client wants to join a live session's room — route them to it. Without it, the Join button is disabled |
onSubmissionCreated |
({ enrollment, lessonId, submission }) => void |
A client submitted work — notify the provider |
onSubmissionReviewed |
({ enrollment, submissionId, comment?, status? }) => void |
The provider commented or gave a status — notify the client |
onReplyAdded |
({ enrollment, submissionId, reply }) => void |
A client replied to a review — notify the provider |
onLessonCommentAdded |
(event: { enrollment, lessonId, comment }) => void |
A client asked a question or left a note on a lesson |
onLessonReplyAdded |
(event: { enrollment, lessonId, reply }) => void |
The provider replied on a lesson |
The client's courses: where they left off, and every course with its progress, filterable by All / In progress / Completed.
| Prop | Type | Description |
|---|---|---|
onOpenCourse |
(enrollmentId: string) => void |
The client opens a course |
sx |
SxProps<Theme> |
Styles |
| Prop | Type | Description |
|---|---|---|
enrollmentId |
string |
The client's enrollment to play |
previewCourseId |
string |
A course of the current provider's to preview as a client sees it; everything is open and nothing is saved |
initialLessonId |
string |
Lesson to open first. Default: where the client left off |
onBack |
() => void |
Shows a back button |
sx |
SxProps<Theme> |
Styles |
Submissions: a submission block lets the client answer by recording video or audio in the browser (with the Hiyve clip recorder), taking a photo, uploading a file or writing — whichever the provider allowed. Every submission is kept; the latest shows the provider's status and comments (each pinned to a moment, page or area — select one to jump to it), and the client can reply or submit again. Recording and photos need a secure page (HTTPS) and the browser's camera and microphone permission; on a page that isn't secure, the client is offered an upload instead (on phones this can still open the camera).
If one block can't be shown — a player or viewer fails in the browser — the rest of the lesson keeps working: that block shows a short notice with a link to open its file and, for a required video, audio or custom block, Mark as done so the client isn't stuck. The failure is reported to onError.
How lessons complete: Complete & continue completes the lesson and moves to the next open one. Until every required video and audio block has played (to completeAt, default 90%, or the end), every required quiz is submitted (or passed, with a pass mark), every required submission block has a submission and every required custom block reports completion, the button is disabled and says what is left. Text, files, embeds and live sessions complete when the client continues. A lesson that needs approval then waits for the provider; if sent back, the client sees the note and can submit again.
The provider's courses with client counts, average progress and lessons waiting for approval; create, publish, archive and delete courses. With more than a few courses, a search box and a sort (recently edited, title, most clients, needs attention) appear above the list. A new course starts as a draft: it can be built and previewed, but no one can be enrolled until it is published. Archive takes a course out of the list and the dashboard, keeps it for the clients who have it, and closes it to new ones; Restore brings it back.
| Prop | Type | Description |
|---|---|---|
onOpenCourse |
(courseId: string) => void |
Open a course in the builder (also called after creating one) |
onOpenClients |
(courseId: string) => void |
Open a course's clients. Shows the Clients button and the review banner |
onCourseCreated |
(course: Course) => void |
A course was created (also from a template) |
templates |
CourseTemplate[] |
Starter templates your app offers |
sx |
SxProps<Theme> |
Styles |
An outline of sections and lessons (drag to reorder, or use each item's menu), the selected lesson's details and blocks, and its unlock rules. Details sets the description and the cover — an image shown with the course in lists, or a color from coverPalette when there's no image (without a choice, a color is picked from the course id). A draft shows a Publish course button in place of Enroll clients. Each lesson's unlock rules include a Due date (days after enrollment, or a date); clients see it in the outline and on the lesson, and an unfinished lesson past it reads Overdue, without locking. Changes save in the background; the header shows whether everything is saved. A failed save is tried again after a pause, and if the course or a lesson was saved from somewhere else (another tab) while open, the edits made here are saved over it and onError is told, so your app can say so. A release date is the start of that day in the provider's time zone. A quiz can be auto-scored (answers travel with the lesson) or reviewed by the provider (answers never reach clients); an auto-scored quiz can limit how many tries a client gets, after which it waits for the provider.
| Prop | Type | Description |
|---|---|---|
courseId |
string |
The course to edit |
onBack |
() => void |
Shows a back button |
onPreview |
(courseId: string) => void |
Shows a Preview as client button |
onOpenClients |
(courseId: string) => void |
Shows an Enroll clients button |
sx |
SxProps<Theme> |
Styles |
Enroll clients from your roster, review what is waiting — completed lessons that need approval (with the client's quiz answers and your private score) and new submissions — and follow each client's progress: where they are, when they were last active, pause or resume, unlock a lesson by hand, or remove them.
| Prop | Type | Description |
|---|---|---|
courseId |
string |
The course |
roster |
ClientRef[] |
Your clients, { userId, userName? }, to enroll from |
onBack |
() => void |
Shows the Courses breadcrumb as a link |
onOpenBuilder |
(courseId: string) => void |
Shows an Edit course button |
sx |
SxProps<Theme> |
Styles |
Every lesson ends with Questions & notes: a client can ask a question or leave a note (with a file attached if they like), and the provider answers from the review queue, where a lesson with an unanswered question appears alongside completions and submissions; the reply shows up under the lesson for the client. A lesson's review shows the client's latest quiz answers and, when they tried more than once, how many tries and their best score. Reviewing a submission: play the recording, look at the photo or page through the document, and add comments — pinned to the current moment, page or a dragged-out area of a photo, with an optional recorded or attached reply. Give a status from your own list (Edit statuses sets labels, tone, and whether a status approves the lesson or sends it back); the note is sent with it.
CourseLibrary and CourseClients open lessons that come due (drip timers, approvals) while they are on screen. To keep lessons opening from elsewhere in your provider app, call useAccessSync(courseIds).
The provider's practice at a glance: active clients, what's waiting for review, who is falling behind and how many have finished; a review inbox across every course (the same review cards as CourseClients); a falling behind list (lessons overdue, never started, or no activity for behindAfterDays); and each course's clients, average progress, finishes and pending reviews.
| Prop | Type | Description |
|---|---|---|
onOpenClients |
(courseId: string) => void |
Open a course's clients |
onOpenCourse |
(courseId: string) => void |
Open a course in the builder. Shows an Edit course button |
behindAfterDays |
number |
Days without activity before a client counts as behind. Default 7 |
sx |
SxProps<Theme> |
Styles |
CourseLibrary lists the provider's own templates and your app's starters (templates prop, CourseTemplate[] from @hiyve/courses) with Use template; each course's menu has Duplicate and Save as template. Templates are edited in the builder like any course but can't have clients.CourseClients lists everyone in the course; pausing a client takes their open lessons back until they are resumed; with more than a few, a search box and a sort (name, progress, last active, status) appear above the list. A client's menu has Mark a lesson complete (it counts as done and approved, and opens what follows) and Reset progress (asks first; what they did stops counting and their app clears it when they next open the course), as well as Customize for {name}: hide course lessons from them, and add, edit or remove lessons just for them (the same block editors as the builder). The client sees their own lessons marked Just for you; no one else sees them.Add your app's own activities to courses. Register them on CourseProvider and they appear in the builder's block palette and in the player.
import type { CourseBlockType } from '@hiyve/react-courses';
const checklist: CourseBlockType<{ items: string[] }> = {
kind: 'acme.checklist',
version: 1,
label: 'Checklist',
createDefault: () => ({ items: ['First step'] }),
summarize: (data) => `${data.items.length} steps`,
Editor: ({ data, onChange }) => <ChecklistEditor items={data.items} onChange={(items) => onChange({ items })} />,
View: ({ data, readOnly, onComplete }) => <Checklist items={data.items} disabled={readOnly} onAllChecked={() => onComplete()} />,
};
<CourseProvider storage={storage} currentUser={user} blockTypes={[checklist]}>…</CourseProvider>
| Field | Description |
|---|---|
kind, version |
Unique namespaced kind, and the version of data's shape |
label, icon |
Shown in the block palette |
createDefault() |
Data for a new block |
migrate(data, fromVersion) |
Turn older data into the current shape |
fileRefs(data) |
Stored files the block uses, shared with clients along with the lesson |
summarize(data) |
One line for the builder's block list |
Editor |
CustomBlockEditorProps: data, onChange, uploadMedia (rejects with a message ready to show when a file is over maxUploadBytes), resolveMediaUrl |
View |
CustomBlockViewProps: block, data, progress, readOnly, onProgress, onComplete(result?), resolveMediaUrl |
A block whose kind isn't registered shows a short notice, keeps its data, and completes when the client continues.
Build your own screens on the same logic.
| Hook | Returns |
|---|---|
useMyCourses() |
The client's courses with progress, next lesson and next drip (MyCourseItem[]) |
useCoursePlayer({ enrollmentId | previewCourseId, initialLessonId? }) |
Course, enrollment, progress, lesson states, current lesson and content, what is still pending, and actions (UseCoursePlayerResult) |
useCourses() |
The provider's courses and templates with client counts, average progress and pending approvals, plus createCourse, deleteCourse, duplicate and createFromTemplate (CourseListItem[]) |
useCourseBuilder(courseId) |
The course being edited, the selected lesson's blocks, save state, and every edit action (UseCourseBuilderResult) |
useCourseClients(courseId) |
Client rows, the review queue, the provider's statuses, and enroll / decide / review / pause / remove / unlock / loadReview / saveStatuses (UseCourseClientsResult) |
useProviderDashboard({ behindAfterDays? }) |
Totals, the clients falling behind and each course's numbers (UseProviderDashboardResult, BehindClient, DashboardCourse) |
useAccessSync(courseIds) |
Opens due lessons while mounted |
useCourseContext() |
The surrounding CourseProvider's values (CourseContextValue) |
BlockView shows a single block as the player does. SubmissionViewer shows one submission with its comments and a comment box (SubmissionViewerProps), for building your own review screens. SaveState is 'saved' | 'unsaved' | 'saving' | 'waiting' | 'error'. Other result types: PendingBlock, ClientRow, ClientStatus, ReviewItem, ReviewAnswers, ReviewDetails, NewBlockType, UseMyCoursesResult, UseCoursesResult, UseCoursePlayerOptions, BlockViewProps.
CourseClients lists clients as cards. Wide tables scroll inside their own box rather than widening the page.CourseLabels; pass overrides as labels. defaultCourseLabels and mergeCourseLabels are exported. Labels that take values are functions (e.g. lessonsProgress(done, total), formatDate(iso), relativeTime(iso, now)).completed, awaiting, awaitingBackground, locked, activeBackground; the defaults are darkened from your theme where needed so text in them is readable) and course cover colors (coverPalette, offered in the builder and used when a course has no cover image) are in CourseColors; buildDefaultCourseColors(theme) and mergeCourseColors(colors, theme) are exported.PROGRESS_SAVE_IDLE_MS (5 s), PROGRESS_SAVE_MAX_WAIT_MS (30 s), BUILDER_SAVE_IDLE_MS (1.2 s) and ACCESS_SYNC_INTERVAL_MS (5 min) say how often things save and lessons are checked.CourseProviderProps, ProviderDashboardProps, CourseUser, CourseEvents, CourseLabels, CourseColors, CourseBlockType, AnyCourseBlockType, CustomBlockEditorProps, CustomBlockViewProps, MyCoursesProps, CoursePlayerProps, CourseLibraryProps, CourseBuilderProps, CourseClientsProps.
@mui/material, @mui/icons-material), Emotion.@hiyve/courses, @hiyve/react, @hiyve/core, @hiyve/react-media-player, @hiyve/react-assignments, @hiyve/react-collaboration, @hiyve/react-clips, @hiyve/react-ui, @hiyve/react-semantic-relay, @hiyve/rtc-client, @hiyve/utilities./hiyve entry: HiyveProvider above the components.