Skip to main content
Post image
· 11 min read

Simple Web Application Example with Refine

caution

This post was created using version 3.x.x of refine. Although we plan to update it with the latest version of refine as soon as possible, you can still benefit from the post in the meantime.

You should know that refine version 4.x.x is backward compatible with version 3.x.x, so there is no need to worry. If you want to see the differences between the two versions, check out the migration guide.

Just be aware that the source code example in this post have been updated to version 4.x.x.

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.

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.

cli

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 { dataProvider } from "@refinedev/supabase";
import authProvider from "./authProvider";
import { supabaseClient } from "utility";

import "@pankod/refine/dist/styles.min.css";

import { Login } from "./pages/login";

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

export default App;

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

signin

  • 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 "@refinedev/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}
resources={[
{
name: "movies",
list: AdminMovieList,
meta: {
route: "admin/movies",
},
},
]}
/>
);
}

export default App;
  • 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>({
sorters: {
initial: [
{
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
export interface IMovies {
id: string;
name: string;
description: string;
preload: string;
director: string;
stars: string;
premiere: string;
trailer: string;
images: IFile[];
}
movies

  • Now we will add create page

Create page

    resources={[
{
name: "movies",
list: AdminMovieList,
create: AdminMovieCreate,
meta: {
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,
};
});
};
create

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

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

Final version of our <Resource>.

    resources={[
{
name: "movies",
list: AdminMovieList,
create: AdminMovieCreate,
show: AdminMovieShow,
edit: AdminMovieEdit,
meta: {
route: "admin/movies",
},
},
]}

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 "@refinedev/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}
routerProvider={{
...routerProvider,

routes: [
{
exact: true,
component: MoviesList,
path: "/movies",
},
{
exact: true,
component: MovieShow,
path: "/:resource(movies)/:action(show)/:id",
},
],
}}
resources={[
{
name: "movies",
list: AdminMovieList,
create: AdminMovieCreate,
show: AdminMovieShow,
edit: AdminMovieEdit,

meta: {
route: "admin/movies",
},
},
]}
/>
);
}

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

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

Example

RUN IN YOUR LOCAL
npm create refine-app@latest -- --example blog-refineflix

here is repo



Related Articles

Web Developer
React Admin Panel Tutorial with Chakra UI and Strapi
· 26 min read
Fullstack Developer
Adding Realtime Collaboration
· 10 min read
Software Developer
A Guide to useContext and React Context API
· 13 min read

From Same Author

Software Developer
10 High-Quality Free Resources That Will Make Every Web Developer's Life Easier
· 4 min read
Software Developer
Top React JS Frameworks Every Developer Should Know
· 5 min read