DEV Community

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

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

19 11

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

import {
List,
Table,
useTable,
IResourceComponentsProps,
Space,
EditButton,
ShowButton,
getDefaultSortOrder,
CreateButton,
DeleteButton,
} from "@pankod/refine";

import { IMovies } from "interfaces";

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

return (
}}>


key="id"
dataIndex="id"
title="ID"
sorter
defaultSortOrder={getDefaultSortOrder("id", 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

  • Now we will add create page


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




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


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

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



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



Alt Text

Final version of our <Resource>.



      <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



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


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

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



Alt Text

here is repo

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

nextjs tutorial video

Youtube Tutorial Series 📺

So you built a Next.js app, but you need a clear view of the entire operation flow to be able to identify performance bottlenecks before you launch. But how do you get started? Get the essentials on tracing for Next.js from @nikolovlazar in this video series 👀

Watch the Youtube series

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay