Build a blog admin panel with Refine

6

We're going to add CRUD pages to our admin panel so you can list, create, and view blog posts records.

List page

First, create a listing page to show Appwrite API data in a table by copying the code below into src/pages/posts and saving it as list.tsx.

TypeScript
import { IResourceComponentsProps } from "@refinedev/core";
import {
  List,
  useTable,
  EditButton,
  ShowButton,
  getDefaultSortOrder,
  DeleteButton,
} from "@refinedev/antd";
import { Table, Space } from "antd";
import { IPost } from "../../interfaces";

export const PostList: React.FC<IResourceComponentsProps> = () => {
  const { tableProps, sorters } = useTable<IPost>({
    initialSorter: [
      {
        field: "$id",
        order: "asc",
      },
    ],
  });

  return (
    <List>
      <Table {...tableProps} rowKey="id">
        <Table.Column
          dataIndex="id"
          title="ID"
          sorter
          width={100}
          defaultSortOrder={getDefaultSortOrder("id", sorters)}
        />
        <Table.Column dataIndex="title" title="Title" sorter />

        <Table.Column<IPost>
          title="Actions"
          dataIndex="actions"
          fixed="right"
          render={(_, record) => (
            <Space>
              <EditButton hideText size="small" recordItemId={record.id} />
              <ShowButton hideText size="small" recordItemId={record.id} />
              <DeleteButton hideText size="small" recordItemId={record.id} />
            </Space>
          )}
        />
      </Table>
    </List>
  );
};

Create page

Create a new record page for the Appwrite API by copying the following code and saving it as create.tsx.

TypeScript
import { HttpError, IResourceComponentsProps } from "@refinedev/core";
import { Create, useForm } from "@refinedev/antd";
import { Form, Input } from "antd";
import { IPost, IPostVariables } from "../../interfaces";

export const PostCreate: React.FC<IResourceComponentsProps> = () => {
  const { formProps, saveButtonProps } = useForm<
    IPost,
    HttpError,
    IPostVariables
  >();

  return (
    <Create saveButtonProps={saveButtonProps}>
      <Form {...formProps} layout="vertical">
        <Form.Item
          label="Title"
          name="title"
          rules={[
            {
              required: true,
            },
          ]}
        >
          <Input />
        </Form.Item>

        <Form.Item
          label="Content"
          name="content"
          rules={[
            {
              required: true,
            },
          ]}
        >
          <Input.TextArea rows={5} />
        </Form.Item>
      </Form>
    </Create>
  );
};

Edit page

Create a page for editing a records with the following code and saving it as edit.tsx.

TypeScript
import React from "react";
import { HttpError, IResourceComponentsProps } from "@refinedev/core";
import { Edit, useForm } from "@refinedev/antd";
import { Form, Input } from "antd";
import { IPost, IPostVariables } from "../../interfaces";

export const PostEdit: React.FC<IResourceComponentsProps> = () => {
  const { formProps, saveButtonProps } = useForm<
    IPost,
    HttpError,
    IPostVariables
  >();

  return (
    <Edit saveButtonProps={saveButtonProps}>
      <Form {...formProps} layout="vertical">
        <Form.Item
          label="Title"
          name="title"
          rules={[
            {
              required: true,
            },
          ]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="Content"
          name="content"
          rules={[
            {
              required: true,
            },
          ]}
        >
          <Input.TextArea rows={5} />
        </Form.Item>
      </Form>
    </Edit>
  );
};

Show page

Create a page for showig the records with the following code and saving it as edit.tsx.

TypeScript
import { useShow, IResourceComponentsProps } from "@refinedev/core";

import { Show, MarkdownField } from "@refinedev/antd";
import { Typography } from "antd";
import { IPost } from "../../interfaces";

const { Title, Text } = Typography;

export const PostShow: React.FC<IResourceComponentsProps> = () => {
  const { queryResult } = useShow<IPost>();
  const { data, isLoading } = queryResult;
  const record = data?.data;

  return (
    <Show isLoading={isLoading}>
      <Title level={5}>Id</Title>
      <Text>{record?.id}</Text>

      <Title level={5}>Title</Title>
      <Text>{record?.title}</Text>

      <Title level={5}>Content</Title>
      <MarkdownField value={record?.content} />
    </Show>
  );
};

Interfaces

We need to add interfaces to src/interfaces/index.d.ts file.

TypeScript
export interface IFile {
  name: string;
  percent: number;
  size: number;
  status: "error" | "success" | "done" | "uploading" | "removed";
  type: string;
  uid: string;
  url: string;
}

export interface IPost {
  id: string;
  title: string;
  content: string;
}

export interface IPostVariables {
  id: string;
  title: string;
  content: string;
}

Connect pages to the App

Finally, import the pages into App.tsx and define them in the <Route> components.

Simply, paste the following into App.tsx.

TypeScript
import { Authenticated, Refine } from "@refinedev/core";
import { dataProvider, liveProvider } from "@refinedev/appwrite";
import {
  AuthPage,
  ErrorComponent,
  RefineThemes,
  ThemedLayoutV2,
  useNotificationProvider,
} from "@refinedev/antd";
import routerProvider, {
  CatchAllNavigate,
  DocumentTitleHandler,
  NavigateToResource,
  UnsavedChangesNotifier,
} from "@refinedev/react-router-v6";
import "@refinedev/antd/dist/reset.css";

import { App as AntdApp, ConfigProvider } from "antd";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";

import { appwriteClient } from "./utility";
import { authProvider } from "./authProvider";
import { PostCreate, PostEdit, PostList, PostShow } from "./pages/posts";

const App: React.FC = () => (
  <BrowserRouter>
    <ConfigProvider theme={RefineThemes.Blue}>
      <AntdApp>
        <Refine
          dataProvider={dataProvider(appwriteClient, {
            databaseId: "<APPWRITE_DATABASE_ID>",
          })}
          liveProvider={liveProvider(appwriteClient, {
            databaseId: "<APPWRITE_DATABASE_ID>",
          })}
          authProvider={authProvider}
          routerProvider={routerProvider}
          resources={[
            {
              name: "<APPWRITE_COLLECTION_ID>",
              list: "/posts",
              create: "/posts/create",
              edit: "/posts/edit/:id",
              show: "/posts/show/:id",
              meta: {
                label: "Posts",
              },
            },
          ]}
          notificationProvider={useNotificationProvider}
          options={{
            liveMode: "auto",
            syncWithLocation: true,
            warnWhenUnsavedChanges: true,
          }}
        >
          <Routes>
            <Route
              element={
                <Authenticated 
                  fallback={<CatchAllNavigate to="/login" />}
                >
                  <ThemedLayoutV2>
                    <Outlet />
                  </ThemedLayoutV2>
                </Authenticated>
              }
            >
              <Route
                index
                element={
                  <NavigateToResource 
                    resource="<APPWRITE_COLLECTION_ID>" 
                  />
                }
              />

              <Route path="/posts">
                <Route index element={<PostList />} />
                <Route path="create" element={<PostCreate />} />
                <Route path="edit/:id" element={<PostEdit />} />
                <Route path="show/:id" element={<PostShow />} />
              </Route>
            </Route>

            <Route
              element={
                <Authenticated fallback={<Outlet />}>
                  <NavigateToResource
                    resource="<APPWRITE_COLLECTION_ID>" 
                  />
                </Authenticated>
              }
            >
              <Route
                path="/login"
                element={<AuthPage forgotPasswordLink={false} />}
              />
              <Route path="/register" 
                element={<AuthPage type="register" />} 
              />
            </Route>

            <Route
              element={
                <Authenticated>
                  <ThemedLayoutV2>
                    <Outlet />
                  </ThemedLayoutV2>
                </Authenticated>
              }
            >
              <Route path="*" element={<ErrorComponent />} />
            </Route>
          </Routes>
          <UnsavedChangesNotifier />
          <DocumentTitleHandler />
        </Refine>
      </AntdApp>
    </ConfigProvider>
  </BrowserRouter>
);

export default App;