Skip to main content
Post image
· 10 min read

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.

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 "@pankod/refine-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 "@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}
resources={[
{
name: "movies",
list: AdminMovieList,
options: {
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>({
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
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,
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,
};
});
};
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,
options: {
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 "@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}
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,

options: {
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

Live Codesandbox Example

here is repo



Related Articles

Frontend Developer
We are going back to 1995! The perfect harmony of Modern stack and Win95
· 15 min read
Frontend Developer
The Advantages and Disadvantages of Working on Open Source Projects
· 7 min read
Frontend developer
React Hook Form Validation with Complete Examples
· 18 min read

From Same Author

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