Simple web application example with Refine
Tags: react, typescript, tutorial, refine
Published: 2021-10-04 15:14:47

---
canonical_url: https://refine.dev/blog/simple-web-application-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](https://github.com/pankod/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fgdv4sho46twm603jlft.png)

# 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*
```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;

```

**Login page**
```tsx

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>
  );
};

```
```css
.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;
  }
  
```
You can use default user for login.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/joilxf9xt4dg2tu0djsh.png)

* Create movies list page with add a resource in *App.tsx*

```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;
```
* AdminMovieList page
```tsx
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>
  );
};

```

* Movies interface
```tsx
export interface IMovies {
  id: string;
  name: string;
  description: string;
  preload: string;
  director: string;
  stars: string;
  premiere: string;
  trailer: string;
  images: IFile[];
}
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/59rxsg4bu44n3lduzrbz.png)

* Now we will add create page

```tsx
      <Resource
        name="movies"
        list={AdminMovieList}
        create={AdminMovieCreate}
        options={{
          route: "admin/movies",
        }}
      />
```

```tsx
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>
  );
};

```

* normalize file in utility folder

```tsx
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,
        };
    });
};

```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d8x3c4h425kno9e6e96k.png)

* Edit page

```tsx
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>
  );
};

```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lvrjf9lugvw5yr99a5ri.png)

* Show page

```tsx
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>
  );
};

```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ya52xveov0gady1a5ir.png)

Final version of our `<Resource>`.

```tsx
      <Resource
        name="movies"
        list={AdminMovieList}
        create={AdminMovieCreate}
        show={AdminMovieShow}
        edit={AdminMovieEdit}
        options={{
          route: "admin/movies",
        }}
      />
```

# 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*

```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;

```
* Movies list page

```tsx
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>
  );
};

```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6rggnh067j075si5fiw4.png)


* Movies detail page

```tsx
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>
  );
};

```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vvrl1zq58p904drssoir.png)

[here is repo](https://github.com/pankod/refine/tree/master/examples/blog/refineflix)