This content originally appeared on DEV Community and was authored by Aydın Akyol
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.
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;
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.
- 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;
- 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[];
}
- 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,
};
});
};
- 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>
);
};
- 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>
);
};
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>
);
};
- 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>
);
};
This content originally appeared on DEV Community and was authored by Aydın Akyol
Aydın Akyol | Sciencx (2021-10-04T15:14:47+00:00) Simple web application example with Refine. Retrieved from https://www.scien.cx/2021/10/04/simple-web-application-example-with-refine/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.