Благодаря функции refine’s headless вы можете включить любой пользовательский интерфейс в свой проект и использовать все его возможности, не беспокоясь о совместимости. Чтобы создать проект в винтажном стиле Windows95
с использованием компонентов пользовательского интерфейса React95, мы воспользуемся функцией refine headless.
Введение
В этом руководстве мы будем использовать базу данных Supabase в бэкенде нашего проекта. Наша цель — создать административную панель в стиле Windows95
, используя функции refine headless и refine Supabase Data Provider.
Настройка проекта
Давайте начнем с создания нашего проекта refine. Вы можете использовать супершаблон для создания проекта refine. Супершаблон быстро создаст наш проект refine в соответствии с выбранными нами функциями.
npx superplate-cli -p refine-react refine-react95-example
✔ What will be the name of your app › refine-react95-example
✔ Package manager: · npm
✔ Do you want to using UI Framework?: · no(headless)
✔ Data Provider: · supabase-data-provider
✔ i18n - Internationalization: · no
Вот и все! После завершения процесса установки наш проект уточнения готов. Кроме того, будут готовы и функции Supabase Data Provider. Как мы уже говорили выше, поскольку мы используем безголовую функцию refine, мы будем сами управлять процессами пользовательского интерфейса. В этом проекте мы будем использовать React95
для пользовательского интерфейса. Давайте продолжим, установив необходимые пакеты в каталог проекта refine.
npm i react95 styled-components
Ручная установка проекта
npm install @pankod/refine-core @pankod/refine-supabase
npm install react95 styled-components
Давайте начнем редактировать наш проект, теперь он готов к использованию.
Использование
refine, автоматически создает supabaseClient
и AuthProvider
для вас. Все, что вам нужно сделать, это определить URL вашей базы данных и Secret_Key. Ниже вы можете увидеть, как это использовать в деталях.
Клиент Supabase
src/utility/supabaseClient.ts:
import { createClient } from "@pankod/refine-supabase";
const SUPABASE_URL = "YOUR_DATABASE_URL";
const SUPABASE_KEY = "YOUR_SUPABASE_KEY";
export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY);
AuthProvider
src/authProvider.ts:
import { AuthProvider } from "@pankod/refine-core";
import { supabaseClient } from "utility";
const authProvider: AuthProvider = {
login: async ({ username, password }) => {
const { user, error } = await supabaseClient.auth.signIn({
email: username,
password,
});
if (error) {
return Promise.reject(error);
}
if (user) {
return Promise.resolve();
}
},
logout: async () => {
const { error } = await supabaseClient.auth.signOut();
if (error) {
return Promise.reject(error);
}
return Promise.resolve("/");
},
checkError: () => Promise.resolve(),
checkAuth: () => {
const session = supabaseClient.auth.session();
if (session) {
return Promise.resolve();
}
return Promise.reject();
},
getPermissions: async () => {
const user = supabaseClient.auth.user();
if (user) {
return Promise.resolve(user.role);
}
},
getUserIdentity: async () => {
const user = supabaseClient.auth.user();
if (user) {
return Promise.resolve({
...user,
name: user.email,
});
}
},
};
export default authProvider;
Настройка Refine для Supabase
src/App.tsx
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";
import { dataProvider } from "@pankod/refine-supabase";
import authProvider from "./authProvider";
import { supabaseClient } from "utility";
function App() {
return (
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider(supabaseClient)}
authProvider={authProvider}
/>
);
}
export default App;
Мы завершили создание структуры нашего проекта. Теперь мы можем легко получить доступ к нашей базе данных Supabase и использовать наши данные в пользовательском интерфейсе. Для начала давайте определим библиотеку React95 и создадим страницу входа для доступа к данным Supabase.
Настройка React95
src/App.tsx:
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";
import { dataProvider } from "@pankod/refine-supabase";
import authProvider from "./authProvider";
import { supabaseClient } from "utility";
import original from "react95/dist/themes/original";
import { ThemeProvider } from "styled-components";
function App() {
return (
<ThemeProvider theme={original}>
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider(supabaseClient)}
authProvider={authProvider}
/>
</ThemeProvider>
);
}
export default App;
В этом шаге мы импортировали и определили библиотеку React95 в нашем проекте Refine. Теперь мы можем гармонично использовать компоненты React95 и функции Refine вместе. Давайте разработаем страницу входа в систему в стиле Windows95!
Страница входа в Refine
src/pages/login/LoginPage.tsx:
import { useState } from "react";
import { useLogin } from "@pankod/refine-core";
import {
Window,
WindowHeader,
WindowContent,
TextField,
Button,
} from "react95";
interface ILoginForm {
username: string;
password: string;
}
export const LoginPage = () => {
const [username, setUsername] = useState("info@refine.dev");
const [password, setPassword] = useState("refine-supabase");
const { mutate: login } = useLogin<ILoginForm>();
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
minHeight: "100vh",
backgroundColor: "rgb(0, 128, 128)",
}}
>
<Window>
<WindowHeader active={true} className="window-header">
<span> Refine Login</span>
</WindowHeader>
<div style={{ marginTop: 8 }}>
<img src="./refine.png" alt="refine-logo" width={100} />
</div>
<WindowContent>
<form
onSubmit={(e) => {
e.preventDefault();
login({ username, password });
}}
>
<div style={{ width: 500 }}>
<div style={{ display: "flex" }}>
<TextField
placeholder="User Name"
fullWidth
value={username}
onChange={(e) => {
setUsername(e.target.value);
}}
/>
</div>
<br />
<TextField
placeholder="Password"
fullWidth
type="password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
<br />
<Button type="submit" value="login">
Sign in
</Button>
</div>
</form>
</WindowContent>
</Window>
</div>
);
};
Мы использовали компоненты React95 для создания дизайна страницы входа в систему. Затем, используя хук refine <AuthProvider>
<useLogin>
, мы выполнили операцию входа в базу данных. Теперь мы можем получить доступ к нашей базе данных и извлечь наши посты и категории, а также создать наши страницы.
Уточнение страницы поста
После процесса входа в систему мы получим посты из нашей базы данных Supabase и отобразим их в таблице. Мы будем использовать компоненты React95 для UI части нашей таблицы, а также пакет refine-react-table для обработки пагинации, сортировки и фильтрации. Вы можете использовать все возможности React Table с помощью адаптера refine-react-table
. На этой странице мы будем использовать этот адаптер refine для управления таблицей.
В этом шаге мы покажем, как использовать пакет refine-react-table для создания таблицы данных. Для начала мы рассмотрим эту страницу в двух частях. На первом этапе мы используем пакет refine-react-table и компоненты пользовательского интерфейса React95, чтобы использовать только наши данные. Затем, на следующем этапе, мы организуем процессы сортировки, пагинации и нашу UI-часть. Давайте начнем!
Для получения подробной информации обратитесь к документации по пакету refine React Table. →
PostList Part I:
import { useMemo } from "react";
import { useOne } from "@pankod/refine-core";
import { useTable, Column } from "@pankod/refine-react-table";
import { IPost, ICategory, ICsvPost } from "interfaces";
import {
Table,
TableBody,
TableHead,
TableRow,
TableHeadCell,
TableDataCell,
Window,
WindowHeader,
WindowContent,
} from "react95";
export const PostList = () => {
const columns: Array<Column> = useMemo(
() => [
{
id: "id",
Header: "ID",
accessor: "id",
},
{
id: "title",
Header: "Title",
accessor: "title",
},
{
id: "category.id",
Header: "Category",
accessor: "category.id",
Cell: ({ cell }) => {
const { data, isLoading } = useOne<ICategory>({
resource: "categories",
id: cell.row.original.categoryId,
});
if (isLoading) {
return <p>loading..</p>;
}
return data?.data.title ?? "Not Found";
},
},
],
[],
);
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable<IPost>({ columns });
return (
<>
<Window style={{ width: "100%" }}>
<WindowHeader>Posts</WindowHeader>
<WindowContent>
<Table {...getTableProps()}>
<TableHead>
{headerGroups.map((headerGroup) => (
<TableRow
{...headerGroup.getHeaderGroupProps()}
>
{headerGroup.headers.map((column) => (
<TableHeadCell
{...column.getHeaderProps()}
>
{column.render("Header")}
</TableHeadCell>
))}
</TableRow>
))}
</TableHead>
<TableBody {...getTableBodyProps()}>
{rows.map((row, i) => {
prepareRow(row);
return (
<TableRow {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<TableDataCell
{...cell.getCellProps()}
>
{cell.render("Cell")}
</TableDataCell>
);
})}
</TableRow>
);
})}
</TableBody>
</Table>
</WindowContent>
</Window>
</>
);
};
Как вы можете видеть, наш первый шаг завершен. Благодаря адаптеру refine-react-table мы получили данные из Supabase и обработали их как данные таблицы. Затем мы поместили эти данные в компоненты React95. Теперь перейдем ко второму шагу.
PostList Part II:
import { useMemo, useRef, useState } from "react";
import { useOne, useNavigation, useDelete } from "@pankod/refine-core";
import {
useTable,
Column,
useSortBy,
usePagination,
useFilters,
} from "@pankod/refine-react-table";
import { IPost, ICategory } from "interfaces";
import {
Table,
TableBody,
TableHead,
TableRow,
TableHeadCell,
TableDataCell,
Window,
WindowHeader,
WindowContent,
Button,
Select,
NumberField,
Progress,
} from "react95";
export const PostList = () => {
const { edit, create } = useNavigation();
const { mutate } = useDelete();
const columns: Array<Column> = useMemo(
() => [
{
id: "id",
Header: "ID",
accessor: "id",
},
{
id: "title",
Header: "Title",
accessor: "title",
},
{
id: "category.id",
Header: "Category",
accessor: "category.id",
Cell: ({ cell }) => {
const { data, isLoading } = useOne<ICategory>({
resource: "categories",
id: cell.row.original.categoryId,
});
if (isLoading) {
return <p>loading..</p>;
}
return data?.data.title ?? "Not Found";
},
},
{
id: "action",
Header: "Action",
accessor: "id",
Cell: ({ value }) => (
<div>
<Button onClick={() => edit("posts", value)}>
Edit
</Button>
<Button
style={{ marginLeft: 4, marginTop: 4 }}
onClick={() =>
mutate({ id: value, resource: "posts" })
}
>
Delete
</Button>
</div>
),
},
],
[],
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
pageOptions,
setPageSize,
gotoPage,
state: { pageIndex, pageSize },
} = useTable<IPost>({ columns }, useFilters, useSortBy, usePagination);
return (
<>
<Window style={{ width: "100%" }}>
<WindowHeader>Posts</WindowHeader>
<WindowContent>
<Table {...getTableProps()}>
<TableHead>
{headerGroups.map((headerGroup) => (
<TableRow
{...headerGroup.getHeaderGroupProps()}
>
{headerGroup.headers.map((column) => (
<TableHeadCell
{...column.getHeaderProps(
column.getSortByToggleProps(),
)}
>
{column.render("Header")}
</TableHeadCell>
))}
</TableRow>
))}
</TableHead>
<TableBody {...getTableBodyProps()}>
{rows.map((row, i) => {
prepareRow(row);
return (
<TableRow {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<TableDataCell
{...cell.getCellProps()}
>
{cell.render("Cell")}
</TableDataCell>
);
})}
</TableRow>
);
})}
</TableBody>
</Table>
</WindowContent>
<div
style={{
display: "flex",
justifyContent: "flex-end",
marginBottom: 8,
marginTop: 8,
alignItems: "flex-end",
}}
>
<Select
style={{ marginLeft: 8 }}
value={pageSize}
onChange={(_, selection) => {
setPageSize(selection.value);
}}
options={opt}
defaultValue={"10"}
></Select>
<span style={{ marginLeft: 8 }}>
Page{" "}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>
<span style={{ marginLeft: 8 }}>
Go to page:
<NumberField
style={{ marginLeft: 8 }}
min={1}
defaultValue={pageIndex + 1}
width={130}
onChange={(value) => {
const page = value ? Number(value) - 1 : 0;
gotoPage(page);
}}
/>
</span>
</span>
</div>
</Window>
</>
);
};
export const opt = [
{ value: 10, label: "10" },
{ value: 20, label: "20" },
{ value: 30, label: "30" },
{ value: 40, label: "40" },
];
Благодаря встроенным функциям refine вы можете быстро выполнять операции сортировки и пагинации, просто добавив несколько строк. Мы завершили нашу страницу Post, добавив к нашей таблице функции пагинации и сортировки, предоставляемые хуком Refine useTable
.
Страница создания и редактирования Refine
Мы создали страницу поста. Теперь мы создадим страницы, на которых мы сможем создавать и редактировать посты. Refine предоставляет адаптер refine-react-hook-form
, который можно использовать с функцией headless. Все возможности React Hook Form работают в гармонии с refine и формой, которую вы будете создавать.
-
Создать страницу
.
import { Controller, useForm } from "@pankod/refine-react-hook-form";
import { useSelect, useNavigation } from "@pankod/refine-core";
import {
Select,
Fieldset,
Button,
TextField,
Window,
WindowHeader,
WindowContent,
ListItem,
} from "react95";
export const PostCreate: React.FC = () => {
const {
refineCore: { onFinish, formLoading },
register,
handleSubmit,
control,
formState: { errors },
} = useForm();
const { goBack } = useNavigation();
const { options } = useSelect({
resource: "categories",
});
return (
<>
<Window style={{ width: "100%", height: "100%" }}>
<WindowHeader active={true} className="window-header">
<span>Create Post</span>
</WindowHeader>
<form onSubmit={handleSubmit(onFinish)}>
<WindowContent>
<label>Title: </label>
<br />
<br />
<TextField
{...register("title", { required: true })}
placeholder="Type here..."
/>
{errors.title && <span>This field is required</span>}
<br />
<br />
<Controller
{...register("categoryId", { required: true })}
control={control}
render={({ field: { onChange, value } }) => (
<Fieldset label={"Category"}>
<Select
options={options}
menuMaxHeight={160}
width={160}
variant="flat"
onChange={onChange}
value={value}
/>
</Fieldset>
)}
/>
{errors.category && <span>This field is required</span>}
<br />
<label>Content: </label>
<br />
<TextField
{...register("content", { required: true })}
multiline
rows={10}
cols={50}
/>
{errors.content && <span>This field is required</span>}
<br />
<Button type="submit" value="Submit">
Submit
</Button>
{formLoading && <p>Loading</p>}
</WindowContent>
</form>
</Window>
</>
);
};
-
Редактировать страницу
import { useEffect } from "react";
import { Controller, useForm } from "@pankod/refine-react-hook-form";
import { useSelect, useNavigation } from "@pankod/refine-core";
import {
Select,
Fieldset,
Button,
TextField,
WindowContent,
Window,
WindowHeader,
ListItem,
} from "react95";
export const PostEdit: React.FC = () => {
const {
refineCore: { onFinish, formLoading, queryResult },
register,
handleSubmit,
resetField,
control,
formState: { errors },
} = useForm();
const { goBack } = useNavigation();
const { options } = useSelect({
resource: "categories",
defaultValue: queryResult?.data?.data.categoryId,
});
useEffect(() => {
resetField("categoryId");
}, [options]);
return (
<>
<Window style={{ width: "100%", height: "100%" }}>
<form onSubmit={handleSubmit(onFinish)}>
<WindowHeader active={true} className="window-header">
<span>Edit Post</span>
</WindowHeader>
<WindowContent>
<label>Title: </label>
<br />
<TextField
{...register("title", { required: true })}
placeholder="Type here..."
/>
{errors.title && <span>This field is required</span>}
<br />
<br />
<Controller
{...register("categoryId", { required: true })}
control={control}
render={({ field: { onChange, value } }) => (
<Fieldset label={"Category"}>
<Select
options={options}
menuMaxHeight={160}
width={160}
variant="flat"
onChange={onChange}
value={value}
/>
</Fieldset>
)}
/>
{errors.category && <span>This field is required</span>}
<br />
<label>Content: </label>
<br />
<TextField
{...register("content", { required: true })}
multiline
rows={10}
cols={50}
/>
{errors.content && <span>This field is required</span>}
<br />
<Button type="submit" value="Submit">
Submit
</Button>
{formLoading && <p>Loading</p>}
</WindowContent>
</form>
</Window>
</>
);
};
Мы можем управлять нашими формами и генерировать посты благодаря адаптеру refine-react-hook-form
, и мы можем сохранить пост, который мы создали с помощью метода refine onFinish
непосредственно в Supabase.
Настройка макета Refine
Наше приложение почти готово. В качестве последнего шага давайте отредактируем наш макет, чтобы сделать наше приложение более похожим на Window95. Сначала создадим компонент нижнего колонтитула, а затем определим его как уточненный макет.
Обратитесь к документации по refine Custom Layout для подробного использования. →
-
Footer
components/Footer.tsx
import React, { useState } from "react";
import { useLogout, useNavigation } from "@pankod/refine-core";
import { AppBar, Toolbar, Button, List, ListItem } from "react95";
export const Footer: React.FC = () => {
const [open, setOpen] = useState(false);
const { mutate: logout } = useLogout();
const { push } = useNavigation();
return (
<AppBar style={{ top: "unset", bottom: 0 }}>
<Toolbar style={{ justifyContent: "space-between" }}>
<div style={{ position: "relative", display: "inline-block" }}>
<Button
onClick={() => setOpen(!open)}
active={open}
style={{ fontWeight: "bold" }}
>
<img
src={"./refine.png"}
alt="refine logo"
style={{ height: "20px", marginRight: 4 }}
/>
</Button>
{open && (
<List
style={{
position: "absolute",
left: "0",
bottom: "100%",
}}
onClick={() => setOpen(false)}
>
<ListItem
onClick={() => {
push("posts");
}}
>
Posts
</ListItem>
<ListItem
onClick={() => {
push("categories");
}}
>
Categories
</ListItem>
<ListItem
onClick={() => {
logout();
}}
>
<span role="img" aria-label="🔙">
🔙
</span>
Logout
</ListItem>
</List>
)}
</div>
</Toolbar>
</AppBar>
);
};
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";
import { dataProvider } from "@pankod/refine-supabase";
import authProvider from "./authProvider";
import { supabaseClient } from "utility";
import original from "react95/dist/themes/original";
import { ThemeProvider } from "styled-components";
import { PostList, PostEdit, PostCreate } from "pages/posts";
import { CategoryList, CategoryCreate, CategoryEdit } from "pages/category";
import { LoginPage } from "pages/login";
import { Footer } from "./components/footer";
import "./app.css";
function App() {
return (
<ThemeProvider theme={original}>
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider(supabaseClient)}
authProvider={authProvider}
LoginPage={LoginPage}
Layout={({ children }) => {
return (
<div className="main">
<div className="layout">{children}</div>
<div>
<Footer />
</div>
</div>
);
}}
resources={[
{
name: "posts",
list: PostList,
create: PostCreate,
edit: PostEdit,
},
]}
/>
</ThemeProvider>
);
}
export default App;
Теперь мы сделаем компонент верхнего меню, характерный для дизайна Windows 95.
Top Menu
components/bar/TopMenu:
import React, { useState } from "react";
import { AppBar, Toolbar, Button, List } from "react95";
type TopMenuProps = {
children: React.ReactNode[] | React.ReactNode;
};
export const TopMenu: React.FC<TopMenuProps> = ({ children }) => {
const [open, setOpen] = useState(false);
return (
<AppBar style={{ zIndex: 1 }}>
<Toolbar>
<Button
variant="menu"
onClick={() => setOpen(!open)}
active={open}
>
File
</Button>
<Button variant="menu" disabled>
Edit
</Button>
<Button variant="menu" disabled>
View
</Button>
<Button variant="menu" disabled>
Format
</Button>
<Button variant="menu" disabled>
Tools
</Button>
<Button variant="menu" disabled>
Table
</Button>
<Button variant="menu" disabled>
Window
</Button>
<Button variant="menu" disabled>
Help
</Button>
{open && (
<List
style={{
position: "absolute",
left: "0",
top: "100%",
}}
onClick={() => setOpen(false)}
>
{children}
</List>
)}
</Toolbar>
</AppBar>
);
};
Обзор проекта
Заключение
refine — это очень мощная и гибкая структура разработки внутренних инструментов. Возможности, которые она предоставляет, значительно сократят время разработки. В этом примере мы показали шаг за шагом, как разработка может быть быстрой и простой с использованием пользовательского пользовательского интерфейса и функций refine-core. refine не ограничивает вас, и он обеспечивает почти все требования вашего проекта с помощью предоставляемых им крючков, независимо от пользовательского интерфейса.
Живой пример CodeSandbox
Ознакомьтесь с подробной информацией о refine. →