# Lesson player

> One lesson's content with a Mark as done button, or its quiz, which the database grades on submit.

Source: https://bettersupabase.com/docs/registry/ui-recipes/lesson-player

{/* Generated by scripts/build-recipes.ts from the example apps. Edit the examples or the script, then run `pnpm recipes`. */}

It loads its data on the server through the block's client; the [courses block](https://bettersupabase.com/blocks/courses) must be installed (`npx better-supabase add courses`). Its page on bettersupabase.com is [/recipes/lesson-player](https://bettersupabase.com/recipes/lesson-player).

Web, with shadcn components:

```bash
npx shadcn add alert badge button card checkbox field input select skeleton textarea
```

```tsx title="apps/examples/nextjs/src/features/courses/components/lesson-player.tsx"
import type { CourseLessonKind } from "better-supabase/blocks/courses";

import { ArrowLeftIcon, TriangleAlertIcon } from "lucide-react";
import { getExtracted } from "next-intl/server";
import { refresh } from "next/cache";

import {
  Alert,
  AlertAction,
  AlertDescription,
  AlertTitle,
} from "@/components/reui/alert";
import { Badge } from "@/components/reui/badge";
import { Button, buttonVariants } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Link } from "@/i18n/navigation";

import { getLesson } from "../courses-queries";
import { CompleteLessonButton } from "./course-controls";
import { QuizForm } from "./quiz-form";

// oxlint-disable-next-line typescript/require-await -- a Server Action must be async, and refresh() returns nothing to await
async function retry() {
  "use server";
  refresh();
}

/** Render inside `<Suspense>`: one lesson's content, or its quiz. */
export async function LessonPlayer({
  params,
}: {
  params: Promise<{ courseId: string; lessonId: string }>;
}) {
  const { courseId, lessonId } = await params;

  const [view, t] = await Promise.all([
    getLesson(lessonId),
    getExtracted("courses"),
  ]);

  const back = (
    <Link
      href={`/courses/${courseId}`}
      className={buttonVariants({
        variant: "ghost",
        size: "sm",
        className: "w-fit",
      })}
    >
      <ArrowLeftIcon />
      {t("Course outline")}
    </Link>
  );

  if (view.kind === "no-organization") {
    return null;
  }

  if (view.kind === "error") {
    return (
      <div data-recipe="lesson-player" className="flex flex-col gap-4">
        {back}
        <Alert variant="destructive">
          <TriangleAlertIcon />
          <AlertTitle>{t("The lesson didn't load")}</AlertTitle>
          <AlertDescription>{t("Try again in a moment.")}</AlertDescription>
          <AlertAction>
            <form action={retry}>
              <Button size="sm" variant="outline" type="submit">
                {t("Retry")}
              </Button>
            </form>
          </AlertAction>
        </Alert>
      </div>
    );
  }

  const lesson = view.data;

  const kindLabels: Record<CourseLessonKind, string> = {
    content: t("Reading"),
    video: t("Video"),
    quiz: t("Quiz"),
  };

  return (
    <div data-recipe="lesson-player" className="flex flex-col gap-4">
      {back}
      <Card>
        <CardHeader>
          <div className="flex flex-wrap items-center gap-2">
            <CardTitle>{lesson.title}</CardTitle>
            <Badge variant="outline">{kindLabels[lesson.kind]}</Badge>
            {lesson.done ? (
              <Badge variant="success-light">{t("Done")}</Badge>
            ) : null}
          </div>
          {lesson.lastScore === null ? null : (
            <CardDescription>
              {t("Last score: {score, number} of {max, number}", {
                score: lesson.lastScore.score,
                max: lesson.lastScore.max,
              })}
            </CardDescription>
          )}
        </CardHeader>
        {lesson.kind === "quiz" ? (
          lesson.quiz === null || lesson.quiz.length === 0 ? (
            <CardContent>
              <Alert variant="warning">
                <TriangleAlertIcon />
                <AlertTitle>{t("This quiz has no questions")}</AlertTitle>
                <AlertDescription>
                  {t("A manager publishes the quiz form first.")}
                </AlertDescription>
              </Alert>
            </CardContent>
          ) : (
            <QuizForm lessonId={lesson.id} fields={lesson.quiz} />
          )
        ) : (
          <>
            <CardContent className="flex flex-col gap-4">
              {lesson.text === null ? null : (
                <p className="text-sm whitespace-pre-line">{lesson.text}</p>
              )}
              {lesson.mediaUrl === null ? null : (
                <a
                  href={lesson.mediaUrl}
                  target="_blank"
                  rel="noreferrer"
                  className="text-sm text-primary transition-colors hover:text-primary/80"
                >
                  {t("Open the video")}
                </a>
              )}
            </CardContent>
            {lesson.done ? null : (
              <CardFooter className="flex justify-end">
                <CompleteLessonButton lessonId={lesson.id} />
              </CardFooter>
            )}
          </>
        )}
      </Card>
    </div>
  );
}

export function LessonPlayerSkeleton() {
  return <Skeleton className="h-64 rounded-xl" aria-busy="true" />;
}
```