# Courses

URL: https://developers.uidu.org/docs/domains/courses

> Courses, lectures, and enrollments — a lightweight LMS surface.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser; Bearer for `getEnrollment`, `listEnrollments`
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu courses`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

A lightweight LMS: courses made of ordered lectures, and the people enrolled in them. Courses and
lectures are public content you can render on a website; enrollments are per-person data you read
from your server.

## Mental model

```
Course                     ← listCourses / getCourse (public token)
  ├─ name, abstract, cover, kind, mode, tags, publicPath
  ├─ body                  rich text — getCourse only
  └─→ Lecture              ← listLectures / getLecture (public token)
        ├─ position        order within the course
        ├─ duration        minutes
        ├─ link            external URL, e.g. the video host
        ├─ body            rich text — getLecture only
        └─ previousLecture / nextLecture   { id, name, position } | null
Enrollment                 ← listEnrollments / getEnrollment (Bearer)
  ├─ course, contact       { id, name } / { id, name, email }
  ├─ currentLecture        where the person is
  ├─ aasmState
  └─ fieldValuesByShortname   custom fields — getEnrollment only
```

* **`kind` and `mode` are strings** from the course's settings: `kind` is `e_learning`, `ilt` or
  `vilt`; `mode` is `external` or `internal`. `tags` is free-form.
* **Lists are summaries.** `listCourses` has no `body`; `listLectures` has no `body` and no
  previous/next. Fetch one record with `getCourse` / `getLecture` for those.
* **Lectures are listed workspace-wide.** `listLectures` has no course argument: it returns
  lectures across every course, and you keep the ones whose `course.id` matches. It also
  returns one page, so a course whose lectures fall past that page comes back short — walk
  every page with `paginate` when you need a full course (see the first use case).
* **`orderBy`, `scopes`, `from`/`to`** are passed through to `listLectures` as strings;
  `orderBy: 'position'` gives course order. `listCourses` and `listEnrollments` take search
  `params` (`SearchParams`: `filters`, `sorting`, `options`) instead.
* **Enrollments are private.** They tie a contact to a course, so reading them needs the
  account Bearer (`apiKey`) on your server. With a public token the list comes back empty
  rather than failing. `aasmState` is the enrollment's state as a string; read it, don't
  hard-code a fixed set of values.
* **Writes** (`createCourse`, `updateCourse`, `deleteCourse`) take `{ input: { attributes } }`
  (plus `id` to update or delete), need the Bearer, and return the payload with `errors` — see
  [Writes](https://developers.uidu.org/docs/foundations/writes.md). Lectures and enrollments have no writes in the SDK yet.

## Use cases

The examples import `uidu` from `lib/uidu.ts`, a client built with `publicToken` and `apiKey`
and imported only from server code, as in [Authentication](https://developers.uidu.org/docs/foundations/authentication.md#bearer--the-workspace).

### Course page with its lectures

Goal: a course's page with its description and the ordered list of its lectures.

```tsx
// app/courses/[id]/page.tsx
import { getCourse, paginate, ListLecturesDocument } from '@uidu/client';
import { RichText } from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';

export default async function CoursePage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const [course, lectures] = await Promise.all([
    getCourse(uidu, { id: decodeURIComponent(id) }),
    // Every lecture in the workspace, every page — then keep this course's.
    paginate(uidu, ListLecturesDocument, {
      variables: { first: 100, orderBy: 'position' },
      extract: (data) => data.currentWorkspace?.lectures,
    }),
  ]);
  if (!course) notFound();

  const courseLectures = lectures
    .filter((l) => l.course?.id === course.id)
    .sort((a, b) => (a.position ?? 0) - (b.position ?? 0));

  return (
    <article>
      <h1>{course.name ?? 'Untitled course'}</h1>
      <p>{course.abstract}</p>
      {course.body && <RichText doc={course.body} />}
      <ol>
        {courseLectures.map((lec) => (
          <li key={lec.id}>
            <a href={`/lectures/${encodeURIComponent(lec.id)}`}>{lec.name}</a>
            {lec.duration && ` (${lec.duration} min)`}
          </li>
        ))}
      </ol>
    </article>
  );
}
```

* For a catalogue page, `listCourses(uidu, { first: 20 })` returns the course summaries:
  `name`, `abstract`, `cover`, `kind`, `mode`, `tags`, `publicPath` (e.g. `/courses/intro-to-x`)
  and `publicUrl`.
* `listLectures(uidu, { first: 100, orderBy: 'position' })` is enough when the workspace has few
  lectures; `paginate` is what makes the list complete. More in
  [Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md).
* The sort after filtering keeps the order right even when lectures of different courses
  interleave across pages.

### Lecture page with previous / next

Goal: one lecture's content, with links to the lectures before and after it.

```tsx
// app/lectures/[id]/page.tsx
import { getLecture } from '@uidu/client';
import { RichText } from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';

const href = (id: string) => `/lectures/${encodeURIComponent(id)}`;

export default async function LecturePage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const lecture = await getLecture(uidu, { id: decodeURIComponent(id) });
  if (!lecture) notFound();

  const { course, previousLecture: prev, nextLecture: next } = lecture;

  return (
    <article>
      {course && (
        <a href={`/courses/${encodeURIComponent(course.id)}`}>{course.name}</a>
      )}
      <h1>{lecture.name}</h1>
      {lecture.duration && <p>{lecture.duration} min</p>}
      {lecture.link && <a href={lecture.link}>Watch</a>}
      {lecture.body && <RichText doc={lecture.body} />}
      <nav>
        {prev && <a href={href(prev.id)}>← {prev.name}</a>}
        {next && <a href={href(next.id)}>{next.name} →</a>}
      </nav>
    </article>
  );
}
```

* `previousLecture` / `nextLecture` are `{ id, name, position } | null`: no need to fetch the
  whole course to navigate.
* `getLecture` also returns `kind`, `cover`, `startDate` / `startTime` (for scheduled
  lectures), `published` and `draft`.

### Enrollment report

Goal: from your server, list who is enrolled in which course and where they are, and read one
enrollment's custom fields.

```ts
// app/api/enrollments/route.ts — server only, needs the apiKey client
import { listEnrollments, getEnrollment } from '@uidu/client';
import { uidu } from '@/lib/uidu';

export async function GET() {
  const enrollments = await listEnrollments(uidu, { first: 50 });

  const rows = enrollments.map((e) => ({
    course: e.course.name,
    person: e.contact.name,
    email: e.contact.email,
    state: e.aasmState,
    at: e.currentLecture?.name ?? null,
  }));

  // One enrollment in full: its custom fields as a { shortname: value } map.
  const first =
    enrollments[0] && (await getEnrollment(uidu, { id: enrollments[0].id }));

  return Response.json({ rows, fields: first?.fieldValuesByShortname ?? null });
}
```

* Empty with no error means the client has no `apiKey`: enrollments are invisible to a public
  token.
* `fieldValuesByShortname` (custom fields) and `currentLecture.position` are on `getEnrollment`
  only; the list has `currentLecture { id, name }`.
* To read every enrollment, pass `ListEnrollmentsDocument` to `paginate`.

## API at a glance

Every `@uidu/client` function in **Courses**, generated from the source — the [full reference](https://developers.uidu.org/docs/reference/client.md#courses) has the rest of the SDK.

| Function                                                                                         | Kind  | Auth         | CLI              | Description                                                                                                                                      |
| ------------------------------------------------------------------------------------------------ | ----- | ------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`createCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `courses create` | Create a course from `{ input: { attributes } }`. Returns the payload (with `errors`) or null.                                                   |
| [`deleteCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `courses delete` | Delete a course by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                             |
| [`getCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)       | read  | public token | `courses get`    | One course with its rich-text `body`, or null.                                                                                                   |
| [`getEnrollment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)   | read  | Bearer       | —                | One enrollment with its course, contact and custom fields (`fieldValuesByShortname`), or null.                                                   |
| [`getLecture`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)      | read  | public token | —                | One lecture with its `body`, its course and its `nextLecture` / `previousLecture` for navigation, or null.                                       |
| [`listCourses`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)     | read  | public token | `courses list`   | The workspace's courses as a flat array, optionally filtered with search `params`; page with `first`/`after`.                                    |
| [`listEnrollments`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts) | read  | Bearer       | —                | Course enrollments as a flat array, optionally filtered with search `params`; page with `first`/`after`.                                         |
| [`listLectures`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)    | read  | public token | —                | Lectures across every course as a flat array (filter by `course.id` yourself), with `scopes`, `orderBy`, `from`/`to`; page with `first`/`after`. |
| [`updateCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `courses update` | Update a course: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                   |

**Types:** `Course`, `Enrollment`, `Lecture`

**CLI:**

| Entity    | list | get | create | update | delete |
| --------- | :--: | :-: | :----: | :----: | :----: |
| `courses` |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
