Code snippets

Code snippets

This is the documentation for the latest version of Next Admin. If you are using an older version (<5.0.0), please refer to the documentation

This page contains code snippets that you can use in your projects. These are not a part of Next Admin, but may be useful for your projects.

Some of the snippets are implemented in the example project. You can check them out in the example project or in the source code.

Authentication

The library does not provide an authentication system. If you want to add your own, you can do so by adding a role check in the page:

The following example uses next-auth to handle authentication

app/api/admin/[[...nextadmin]]/route.ts
const { run } = createHandler({
  options,
  prisma,
  apiBasePath: "/api/admin",
  schema,
  onRequest: (req) => {
    const session = await getServerSession(authOptions);
    const isAdmin = session?.user?.role === "SUPERADMIN";
 
    if (!isAdmin) {
      return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
    }
  }
});
 
export { run as POST, run as GET, run as DELETE };
pages/admin/[[...nextadmin]].tsx
export default async function AdminPage({
  params,
  searchParams,
}: {
  params: { [key: string]: string[] };
  searchParams: { [key: string]: string | string[] | undefined } | undefined;
}) {
  const session = await getServerSession(authOptions);
  const isAdmin = session?.user?.role === "SUPERADMIN"; // your role check
 
  if (!isAdmin) {
    redirect('/', { permanent: false })
  }
 
  const props = await getNextAdminProps({
    params: params.nextadmin,
    searchParams,
    basePath: "/admin",
    apiBasePath: "/api/admin",
    prisma,
    schema,
  });
 
  return <NextAdmin {...props} dashboard={Dashboard} />;
}

Export data

By using exports options, you can export data. Next Admin only implements the CSV export button, it’s actually a link pointing to a provided url.

The API endpoint route must be defined in the exports property of the options object. This is an example of how to implement the export API endpoint:

app/api/admin/[[...nextadmin]]/route.ts
import { prisma } from "@/prisma";
 
export async function GET() {
  const users = await prisma.user.findMany();
  const csv = users.map((user) => {
    return `${user.id},${user.name},${user.email},${user.role},${user.birthDate}`;
  });
 
  const headers = new Headers();
  headers.set("Content-Type", "text/csv");
  headers.set("Content-Disposition", `attachment; filename="users.csv"`);
 
  return new Response(csv.join("\n"), {
    headers,
  });
}

or with a stream response:

app/api/admin/[[...nextadmin]]/route.ts
import { prisma } from "@/prisma";
const BATCH_SIZE = 1000;
 
export async function GET() {
  const headers = new Headers();
  headers.set("Content-Type", "text/csv");
  headers.set("Content-Disposition", `attachment; filename="users.csv"`);
 
  const stream = new ReadableStream({
    async start(controller) {
      try {
        const batchSize = BATCH_SIZE;
        let skip = 0;
        let users;
 
        do {
          users = await prisma.user.findMany({
            skip,
            take: batchSize,
          });
 
          const csv = users
            .map((user) => {
              return `${user.id},${user.name},${user.email},${user.role},${user.birthDate}\n`;
            })
            .join("");
 
          controller.enqueue(Buffer.from(csv));
 
          skip += batchSize;
        } while (users.length === batchSize);
      } catch (error) {
        controller.error(error);
      } finally {
        controller.close();
      }
    },
  });
 
  return new Response(stream, { headers });
}

Note that you must secure the export route if you don’t want to expose your data to the public by adding authentication middleware.

There are two example files in the example project:

Intercepting data submission

If you want to add data to the form data before submitting it, you can make use of the edit.hooks field of a model options. This is an example of how to add createdBy and updatedBy fields on a post based on the user id, and submit an email to inform the creation or edition of the post:

{
  model: {
    Post: {
      edit: {
        hooks: {
          async beforeDb(data, mode, request) {
            const userId = 1 // retrieve from the request / cookies, etc
 
            // your own permission check
            if (!userHasPermission(userId)) {
              throw new HookError(403, { error: "Forbidden" });
            }
 
            if (mode === "create") {
              data.createdBy = userId;
            } else {
              data.updatedBy = userId;
            }
 
            return data
          },
          async afterDb(data, mode, request) {
            const userId = 1 // retrieve from the request / cookies, etc
 
            sendMail({
              to: "some@email.com",
              subject: "Post updated",
              text: `Post ${data.title} has been ${mode === "create" ? "created" : "updated"} by ${userId}`,
            })
          }
        }
      }
    }
  }
}

Note that this example assumes that you have a createdBy and updatedBy field on each model, if you need to check the model name, you can use params.params[0].

This snippet is not implemented in the example project.

Custom input form

If you want to customize the input form, you can create a custom input component. This is an example of how to create a custom input component for the birthDate field:

app/components/inputs/BirthDateInput.tsx
"use client";
import { CustomInputProps } from "@premieroctet/next-admin";
import DateTimePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
 
type Props = CustomInputProps;
 
const BirthDateInput = ({ value, name, onChange, disabled, required }: Props) => {
  return (
    <>
      <DateTimePicker
        selected={value ? new Date(value) : null}
        onChange={(date) =>
          onChange?.({
            // @ts-expect-error
            target: { value: date?.toISOString() ?? new Date().toISOString() },
          })
        }
        showTimeSelect
        dateFormat="dd/MM/yyyy HH:mm"
        timeFormat="HH:mm"
        wrapperClassName="w-full"
        disabled={disabled}
        required={required}
        className="dark:bg-dark-nextadmin-background-subtle dark:ring-dark-nextadmin-border-strong text-nextadmin-content-inverted dark:text-dark-nextadmin-content-inverted ring-nextadmin-border-default focus:ring-nextadmin-brand-default dark:focus:ring-dark-nextadmin-brand-default block w-full rounded-md border-0 px-2 py-1.5 text-sm shadow-sm ring-1 ring-inset transition-all duration-300 placeholder:text-gray-400 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:leading-6 [&>div]:border-none"
      />
      <input type="hidden" name={name} value={value ?? ""} />
    </>
  );
};
 
export default BirthDateInput;

The CustomInputProps type is provided by Next Admin.

Note that we use a hidden input to store the value because the DateTimePicker component needs a different value format than what is expected by the form submission.

You can find an example of this component in the example project:

Explicit many-to-many

You might want to add sorting on a relationship, for example sort the categories of a post in a specific order. To achieve this, you have to explicitly define a model in the Prisma schema that will act as the join table. This is an example of how to implement this:

model Post {
  id         Int                 @id @default(autoincrement())
  title      String
  content    String?
  published  Boolean             @default(false)
  author     User                @relation("author", fields: [authorId], references: [id]) // Many-to-one relation
  authorId   Int
  categories CategoriesOnPosts[]
  rate       Decimal?            @db.Decimal(5, 2)
  order      Int                 @default(0)
}
 
model Category {
  id        Int                 @id @default(autoincrement())
  name      String
  posts     CategoriesOnPosts[]
  createdAt DateTime            @default(now())
  updatedAt DateTime            @default(now()) @updatedAt
}
 
model CategoriesOnPosts {
  id         Int      @default(autoincrement())
  post       Post     @relation(fields: [postId], references: [id])
  postId     Int
  category   Category @relation(fields: [categoryId], references: [id])
  categoryId Int
  order      Int      @default(0)
 
  @@id([postId, categoryId])
}

In the Next Admin options, you will then need to define, on a specific field, which field in the join table will be used for sorting:

{
  model: {
    Post: {
      edit: {
        fields: {
          categories: {
            relationOptionFormatter: (category) => {
              return `${category.name} Cat.${category.id}`;
            },
            display: "list",
            orderField: "order", // The field used in CategoriesOnPosts for sorting
            relationshipSearchField: "category", // The field to use in CategoriesOnPosts
          },
        }
      }
    }
  }
}

Note that you will need to use relationOptionFormatter instead of optionFormatter to format the content of the select input.

With the list display, if the orderField property is defined, you will be able to apply drag and drop sorting on the categories. Upon form submission, the order will be updated accordingly, starting from 0.

The orderField property can also be applied for one-to-many relationships. In that case, drag and drop will also be available.

Custom pages

You can create custom pages in the Next Admin UI. By reusing the MainLayout component, you can create a new page with the same layout as the Next Admin pages. This is an example of how to create a custom page:

app/custom/page.tsx
 
import { MainLayout } from "@premieroctet/next-admin";
import { getMainLayoutProps } from "@premieroctet/next-admin/appRouter";
import { options } from "@/options";
import { prisma } from "@/prisma";
 
const CustomPage = async () => {
  const mainLayoutProps = getMainLayoutProps({
    basePath: "/admin",
    apiBasePath: "/api/admin",
    /*options*/
  });
 
  return (
    <MainLayout {...mainLayoutProps}>
      {/*Page content*/}
    </MainLayout>
  );
};
 
export default CustomPage;

Then, if you want that route to be available on the sidebar, you can add a new route - more info:

{
  [...]
  pages: {
    "/custom": {
      title: "Custom page",
      icon: "PresentationChartBarIcon",
    },
  },
  [...]
}