DEV Community

Cover image for Simple web application example with Refine
Aydın Akyol for Refine

Posted on • Updated on • Originally published at refine.dev

Simple web application example with Refine

Do you want to develop a web application quickly? You are at the right place! I will develop a simple movie web application with refine on the frontend and Supabase on the backend, you should continue reading. I will try to explain it step by step in a very simple way.

1. Refine setup

There are two alternative methods to set up a refine application.

The recommended way is using the superplate tool. superplate's CLI wizard will let you create and customize your application in seconds.

Alternatively, you may use the create-react-app tool to create an empty React application and then add refine module via npm.

I will use superplate-cli and select a Supabase. You can customize other options as you wish.

Alt Text

2. Create admin panel with refine

  • We should add our Supabase url and key in supabaseClient.tsx
  • Add custom login page in App.tsx

App.tsx

import { Refine } from "@pankod/refine";

import "@pankod/refine/dist/styles.min.css";
import { dataProvider } from "@pankod/refine-supabase";

import authProvider from "./authProvider";
import { supabaseClient } from "utility";
import { Login } from "./pages/login";

function App() {
  return (
    <Refine
      dataProvider={dataProvider(supabaseClient)}
      authProvider={authProvider}
      LoginPage={Login}
    ></Refine>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Login page


import React from "react";
import {
  Row,
  Col,
  AntdLayout,
  Card,
  Typography,
  Form,
  Input,
  Button,
  Checkbox,
} from "@pankod/refine";
import "./styles.css";

import { useLogin } from "@pankod/refine";

const { Text, Title } = Typography;

export interface ILoginForm {
  username: string;
  password: string;
  remember: boolean;
}

export const Login: React.FC = () => {
  const [form] = Form.useForm<ILoginForm>();

  const { mutate: login } = useLogin<ILoginForm>();

  const CardTitle = (
    <Title level={3} className="title">
      Sign in your account
    </Title>
  );

  return (
    <AntdLayout className="layout">
      <Row
        justify="center"
        align="middle"
        style={{
          height: "100vh",
        }}
      >
        <Col xs={22}>
          <div className="container">
            <div className="imageContainer">
              <img src="./refine.svg" alt="Refine Logo" />
            </div>
            <Card title={CardTitle} headStyle={{ borderBottom: 0 }}>
              <Form<ILoginForm>
                layout="vertical"
                form={form}
                onFinish={(values) => {
                  login(values);
                }}
                requiredMark={false}
                initialValues={{
                  remember: false,
                  email: "info+refineflix@refine.dev",
                  password: "refineflix",
                }}
              >
                <Form.Item
                  name="email"
                  label="Email"
                  rules={[{ required: true, type: "email" }]}
                >
                  <Input size="large" placeholder="Email" />
                </Form.Item>
                <Form.Item
                  name="password"
                  label="Password"
                  rules={[{ required: true }]}
                  style={{ marginBottom: "12px" }}
                >
                  <Input type="password" placeholder="●●●●●●●●" size="large" />
                </Form.Item>
                <div style={{ marginBottom: "12px" }}>
                  <Form.Item name="remember" valuePropName="checked" noStyle>
                    <Checkbox
                      style={{
                        fontSize: "12px",
                      }}
                    >
                      Remember me
                    </Checkbox>
                  </Form.Item>

                  <a
                    style={{
                      float: "right",
                      fontSize: "12px",
                    }}
                    href="#"
                  >
                    Forgot password?
                  </a>
                </div>
                <Button type="primary" size="large" htmlType="submit" block>
                  Sign in
                </Button>
              </Form>
              <div style={{ marginTop: 8 }}>
                <Text style={{ fontSize: 12 }}>
                  Don’t have an account?{" "}
                  <a href="#" style={{ fontWeight: "bold" }}>
                    Sign up
                  </a>
                </Text>
              </div>
            </Card>
          </div>
        </Col>
      </Row>
    </AntdLayout>
  );
};

Enter fullscreen mode Exit fullscreen mode
.layout {
    background: radial-gradient(50% 50% at 50% 50%, #63386a 0%, #310438 100%);
    background-size: "cover";
  }

  .container {
    max-width: 408px;
    margin: auto;
  }

  .title {
    text-align: center;
    color: #626262;
    font-size: 30px;
    letter-spacing: -0.04em;
  }

  .imageContainer {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 16px;
  }

Enter fullscreen mode Exit fullscreen mode

You can use default user for login.

Alt Text

  • Create movies list page with add a resource in App.tsx
import { Refine, Resource } from "@pankod/refine";

import "@pankod/refine/dist/styles.min.css";
import { dataProvider } from "@pankod/refine-supabase";

import authProvider from "./authProvider";
import { supabaseClient } from "utility";

import {
  AdminMovieList,
} from "./pages/admin/movies";
import { Login } from "./pages/login";

function App() {
  return (
    <Refine
      dataProvider={dataProvider(supabaseClient)}
      authProvider={authProvider}
      LoginPage={Login}

    >
      <Resource
        name="movies"
        list={AdminMovieList}
        options={{
          route: "admin/movies",
        }}
      />
    </Refine>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode
  • AdminMovieList page
import {
  List,
  Table,
  useTable,
  IResourceComponentsProps,
  Space,
  EditButton,
  ShowButton,
  getDefaultSortOrder,
  CreateButton,
  DeleteButton,
} from "@pankod/refine";

import { IMovies } from "interfaces";

export const AdminMovieList: React.FC<IResourceComponentsProps> = () => {
  const { tableProps, sorter } = useTable<IMovies>({
    initialSorter: [
      {
        field: "id",
        order: "asc",
      },
    ],
  });

  return (
    <List pageHeaderProps={{ extra: <CreateButton /> }}>
      <Table {...tableProps} rowKey="id">
        <Table.Column
          key="id"
          dataIndex="id"
          title="ID"
          sorter
          defaultSortOrder={getDefaultSortOrder("id", sorter)}
        />
        <Table.Column key="name" dataIndex="name" title="name" sorter />

        <Table.Column<IMovies>
          title="Actions"
          dataIndex="actions"
          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>
  );
};

Enter fullscreen mode Exit fullscreen mode
  • Movies interface
export interface IMovies {
  id: string;
  name: string;
  description: string;
  preload: string;
  director: string;
  stars: string;
  premiere: string;
  trailer: string;
  images: IFile[];
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

  • Now we will add create page
      <Resource
        name="movies"
        list={AdminMovieList}
        create={AdminMovieCreate}
        options={{
          route: "admin/movies",
        }}
      />
Enter fullscreen mode Exit fullscreen mode
import {
  Create,
  Form,
  Input,
  IResourceComponentsProps,
  Upload,
  useForm,
  RcFile,
} from "@pankod/refine";
import { IMovies } from "interfaces";
import { supabaseClient, normalizeFile } from "utility";

export const AdminMovieCreate: React.FC<IResourceComponentsProps> = () => {
  const { formProps, saveButtonProps } = useForm<IMovies>();

  return (
    <Create saveButtonProps={saveButtonProps}>
      <Form {...formProps} layout="vertical">
        <Form.Item
          label="Name"
          name="name"
          rules={[
            {
              required: true,
            },
          ]}
        >
          <Input />
        </Form.Item>
        <Form.Item label="Premiere" name="premiere">
          <Input />
        </Form.Item>
        <Form.Item label="Description" name="description">
          <Input />
        </Form.Item>
        <Form.Item label="Director" name="director">
          <Input />
        </Form.Item>
        <Form.Item label="Stars" name="stars">
          <Input />
        </Form.Item>

        <Form.Item label="Images">
          <Form.Item
            name="images"
            valuePropName="fileList"
            normalize={normalizeFile}
            noStyle
          >
            <Upload.Dragger
              name="file"
              listType="picture"
              multiple
              customRequest={async ({ file, onError, onSuccess }) => {
                try {
                  const rcFile = file as RcFile;

                  await supabaseClient.storage
                    .from("refineflix")
                    .upload(`public/${rcFile.name}`, file, {
                      cacheControl: "3600",
                      upsert: true,
                    });

                  const { data } = supabaseClient.storage
                    .from("refineflix")
                    .getPublicUrl(`public/${rcFile.name}`);

                  const xhr = new XMLHttpRequest();
                  onSuccess && onSuccess({ url: data?.publicURL }, xhr);
                } catch (error) {
                  onError && onError(new Error("Upload Error"));
                }
              }}
            >
              <p className="ant-upload-text">Drag & drop a file in this area</p>
            </Upload.Dragger>
          </Form.Item>
        </Form.Item>
      </Form>
    </Create>
  );
};

Enter fullscreen mode Exit fullscreen mode
  • normalize file in utility folder
import { UploadFile } from "@pankod/refine";

interface UploadResponse {
    url: string;
}
interface EventArgs<T = UploadResponse> {
    file: UploadFile<T>;
    fileList: Array<UploadFile<T>>;
}

export const normalizeFile = (event: EventArgs) => {
    const { fileList } = event;

    return fileList.map((item) => {
        const { uid, name, type, size, response, percent, status } = item;

        return {
            uid,
            name,
            url: item.url || response?.url,
            type,
            size,
            percent,
            status,
        };
    });
};

Enter fullscreen mode Exit fullscreen mode

Alt Text

  • Edit page
import React from "react";
import {
  Edit,
  Form,
  Input,
  IResourceComponentsProps,
  RcFile,
  Upload,
  useForm,
} from "@pankod/refine";

import { IMovies } from "interfaces";
import { supabaseClient, normalizeFile } from "utility";

export const AdminMovieEdit: React.FC<IResourceComponentsProps> = () => {
  const { formProps, saveButtonProps } = useForm<IMovies>();

  return (
    <Edit saveButtonProps={saveButtonProps} pageHeaderProps={{ extra: null }}>
      <Form {...formProps} layout="vertical">
        <Form.Item
          label="Name"
          name="name"
          rules={[
            {
              required: true,
            },
          ]}
        >
          <Input />
        </Form.Item>
        <Form.Item label="Premiere" name="premiere">
          <Input />
        </Form.Item>
        <Form.Item label="Description" name="description">
          <Input />
        </Form.Item>
        <Form.Item label="Director" name="director">
          <Input />
        </Form.Item>
        <Form.Item label="Stars" name="stars">
          <Input />
        </Form.Item>
        <Form.Item label="Trailer" name="trailer">
          <Input />
        </Form.Item>
        <Form.Item label="Images">
          <Form.Item
            name="images"
            valuePropName="fileList"
            normalize={normalizeFile}
            noStyle
          >
            <Upload.Dragger
              name="file"
              listType="picture"
              multiple
              customRequest={async ({ file, onError, onSuccess }) => {
                try {
                  const rcFile = file as RcFile;

                  await supabaseClient.storage
                    .from("refineflix")
                    .upload(`public/${rcFile.name}`, file, {
                      cacheControl: "3600",
                      upsert: true,
                    });

                  const { data } = supabaseClient.storage
                    .from("refineflix")
                    .getPublicUrl(`public/${rcFile.name}`);

                  const xhr = new XMLHttpRequest();
                  onSuccess && onSuccess({ url: data?.publicURL }, xhr);
                } catch (error) {
                  onError && onError(new Error("Upload Error"));
                }
              }}
            >
              <p className="ant-upload-text">Drag & drop a file in this area</p>
            </Upload.Dragger>
          </Form.Item>
        </Form.Item>
      </Form>
    </Edit>
  );
};

Enter fullscreen mode Exit fullscreen mode

Alt Text

  • Show page
import {
  useShow,
  Show,
  Typography,
  IResourceComponentsProps,
  Space,
  ImageField,
  RefreshButton,
  EditButton,
  useNavigation,
} from "@pankod/refine";

import { IMovies } from "interfaces";

const { Title, Text } = Typography;

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

  const { push } = useNavigation();

  return (
    <Show
      isLoading={isLoading}
      pageHeaderProps={{
        title: record?.name,
        subTitle: record?.premiere,
        extra: (
          <>
            <EditButton
              onClick={() => push(`/admin/movies/edit/${record?.id}`)}
            />
            <RefreshButton />
          </>
        ),
      }}
    >
      <Title level={5}>Director</Title>
      <Text>{record?.director || "-"}</Text>

      <Title level={5}>Stars</Title>
      <Text>{record?.stars || "-"}</Text>

      <Title level={5}>Trailer</Title>
      {record?.trailer && (
        <video width="400" controls>
          <source src={record.trailer} type="video/mp4" />
        </video>
      )}

      <Title level={5}>Images</Title>
      <Space wrap>
        {record?.images ? (
          record.images.map((img) => (
            <ImageField
              key={img.name}
              value={img.url}
              title={img.name}
              width={200}
            />
          ))
        ) : (
          <Text>Not found any images</Text>
        )}
      </Space>
    </Show>
  );
};

Enter fullscreen mode Exit fullscreen mode

Alt Text

Final version of our <Resource>.

      <Resource
        name="movies"
        list={AdminMovieList}
        create={AdminMovieCreate}
        show={AdminMovieShow}
        edit={AdminMovieEdit}
        options={{
          route: "admin/movies",
        }}
      />
Enter fullscreen mode Exit fullscreen mode

3. Create list page for movies

We will create custom list and show pages for the unauthorized users because of that, we should add custom routes for these pages.

App.tsx

import { Refine, Resource } from "@pankod/refine";

import "@pankod/refine/dist/styles.min.css";
import { dataProvider } from "@pankod/refine-supabase";

import authProvider from "./authProvider";
import { supabaseClient } from "utility";

import {
  AdminMovieList,
  AdminMovieCreate,
  AdminMovieShow,
  AdminMovieEdit,
} from "./pages/admin/movies";
import { MoviesList, MovieShow } from "./pages/movies";
import { Login } from "./pages/login";

function App() {
  return (
    <Refine
      dataProvider={dataProvider(supabaseClient)}
      authProvider={authProvider}
      LoginPage={Login}
      routes={[
        {
          exact: true,
          component: MoviesList,
          path: "/movies",
        },
        {
          exact: true,
          component: MovieShow,
          path: "/:resource(movies)/:action(show)/:id",
        },
      ]}
    >
      <Resource
        name="movies"
        list={AdminMovieList}
        create={AdminMovieCreate}
        show={AdminMovieShow}
        edit={AdminMovieEdit}
        options={{
          route: "admin/movies",
        }}
      />
    </Refine>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode
  • Movies list page
import {
  IResourceComponentsProps,
  Card,
  Space,
  useList,
  useNavigation,
} from "@pankod/refine";
import { Layout } from "components";

import { IMovies } from "interfaces";

export const MoviesList: React.FC<IResourceComponentsProps> = () => {
  const { Meta } = Card;

  const { data, isLoading } = useList<IMovies>({
    resource: "movies",
    queryOptions: {
      staleTime: 0,
    },
  });

  const { push } = useNavigation();

  const renderMovies = () => {
    if (data) {
      return data.data.map((movie) => {
        return (
          <Card
            hoverable
            key={movie.name}
            style={{ width: 240, minHeight: 400 }}
            cover={
              movie.images?.length > 0 ? (
                <img alt={movie.images[0].name} src={movie.images[0].url} />
              ) : (
                <img
                  alt="default"
                  src="https://cdn.pixabay.com/photo/2019/04/24/21/55/cinema-4153289_960_720.jpg"
                />
              )
            }
            loading={isLoading}
            onClick={() => push(`/movies/show/${movie.id}`)}
          >
            <Meta title={movie.name} description={movie.description} />
          </Card>
        );
      });
    }
  };

  return (
    <Layout>
      <Space align="start">{renderMovies()}</Space>
    </Layout>
  );
};

Enter fullscreen mode Exit fullscreen mode

Alt Text

  • Movies detail page
import {
  useShow,
  Show,
  Typography,
  IResourceComponentsProps,
  Space,
  ImageField,
} from "@pankod/refine";
import { Layout } from "components";

import { IMovies } from "interfaces";

const { Title, Text } = Typography;

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

  const renderDetail = () => (
    <>
      <Title level={5}>Director</Title>
      <Text>{record?.director || "-"}</Text>

      <Title level={5}>Stars</Title>
      <Text>{record?.stars || "-"}</Text>
      <Title level={5}>Trailer</Title>
      {record?.trailer && (
        <video width="400" controls>
          <source src={record.trailer} type="video/mp4" />
        </video>
      )}
      <Title level={5}>Images</Title>
      <Space wrap>
        {record?.images ? (
          record.images.map((img) => (
            <ImageField
              key={img.name}
              value={img.url}
              title={img.name}
              width={200}
            />
          ))
        ) : (
          <Text>Not found any images</Text>
        )}
      </Space>
    </>
  );

  return (
    <Layout>
      <Show
        isLoading={isLoading}
        pageHeaderProps={{
          title: record?.name,
          subTitle: record?.premiere,
          extra: null,
        }}
      >
        {renderDetail()}
      </Show>
    </Layout>
  );
};

Enter fullscreen mode Exit fullscreen mode

Alt Text

here is repo

Top comments (0)