init refine app

This commit is contained in:
maxim
2025-01-19 17:30:54 +03:00
commit 266c7fa070
32 changed files with 7655 additions and 0 deletions

154
src/App.tsx Normal file
View File

@ -0,0 +1,154 @@
import {
Refine,
GitHubBanner,
WelcomePage,
Authenticated,
} from "@refinedev/core";
import { DevtoolsPanel, DevtoolsProvider } from "@refinedev/devtools";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import {
AuthPage,
ErrorComponent,
useNotificationProvider,
RefineSnackbarProvider,
ThemedLayoutV2,
} from "@refinedev/mui";
import dataProvider from "@refinedev/simple-rest";
import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import { BrowserRouter, Route, Routes, Outlet } from "react-router";
import routerBindings, {
NavigateToResource,
CatchAllNavigate,
UnsavedChangesNotifier,
DocumentTitleHandler,
} from "@refinedev/react-router";
import {
BlogPostList,
BlogPostCreate,
BlogPostEdit,
BlogPostShow,
} from "./pages/blog-posts";
import {
CategoryList,
CategoryCreate,
CategoryEdit,
CategoryShow,
} from "./pages/categories";
import { ColorModeContextProvider } from "./contexts/color-mode";
import { Header } from "./components/header";
import { Login } from "./pages/login";
import { Register } from "./pages/register";
import { ForgotPassword } from "./pages/forgotPassword";
import { authProvider } from "./authProvider";
function App() {
return (
<BrowserRouter>
<GitHubBanner />
<RefineKbarProvider>
<ColorModeContextProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<RefineSnackbarProvider>
<DevtoolsProvider>
<Refine
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
notificationProvider={useNotificationProvider}
routerProvider={routerBindings}
authProvider={authProvider}
resources={[
{
name: "blog_posts",
list: "/blog-posts",
create: "/blog-posts/create",
edit: "/blog-posts/edit/:id",
show: "/blog-posts/show/:id",
meta: {
canDelete: true,
},
},
{
name: "categories",
list: "/categories",
create: "/categories/create",
edit: "/categories/edit/:id",
show: "/categories/show/:id",
meta: {
canDelete: true,
},
},
]}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
useNewQueryKeys: true,
projectId: "Wv044J-t53S3s-PcbJGe",
}}
>
<Routes>
<Route
element={
<Authenticated
key="authenticated-inner"
fallback={<CatchAllNavigate to="/login" />}
>
<ThemedLayoutV2 Header={Header}>
<Outlet />
</ThemedLayoutV2>
</Authenticated>
}
>
<Route
index
element={<NavigateToResource resource="blog_posts" />}
/>
<Route path="/blog-posts">
<Route index element={<BlogPostList />} />
<Route path="create" element={<BlogPostCreate />} />
<Route path="edit/:id" element={<BlogPostEdit />} />
<Route path="show/:id" element={<BlogPostShow />} />
</Route>
<Route path="/categories">
<Route index element={<CategoryList />} />
<Route path="create" element={<CategoryCreate />} />
<Route path="edit/:id" element={<CategoryEdit />} />
<Route path="show/:id" element={<CategoryShow />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Route>
<Route
element={
<Authenticated
key="authenticated-outer"
fallback={<Outlet />}
>
<NavigateToResource />
</Authenticated>
}
>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/forgot-password"
element={<ForgotPassword />}
/>
</Route>
</Routes>
<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
<DevtoolsPanel />
</DevtoolsProvider>
</RefineSnackbarProvider>
</ColorModeContextProvider>
</RefineKbarProvider>
</BrowserRouter>
);
}
export default App;

59
src/authProvider.ts Normal file
View File

@ -0,0 +1,59 @@
import type { AuthProvider } from "@refinedev/core";
export const TOKEN_KEY = "refine-auth";
export const authProvider: AuthProvider = {
login: async ({ username, email, password }) => {
if ((username || email) && password) {
localStorage.setItem(TOKEN_KEY, username);
return {
success: true,
redirectTo: "/",
};
}
return {
success: false,
error: {
name: "LoginError",
message: "Invalid username or password",
},
};
},
logout: async () => {
localStorage.removeItem(TOKEN_KEY);
return {
success: true,
redirectTo: "/login",
};
},
check: async () => {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
return {
authenticated: true,
};
}
return {
authenticated: false,
redirectTo: "/login",
};
},
getPermissions: async () => null,
getIdentity: async () => {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
return {
id: 1,
name: "John Doe",
avatar: "https://i.pravatar.cc/300",
};
}
return null;
},
onError: async (error) => {
console.error(error);
return { error };
},
};

View File

@ -0,0 +1,80 @@
import DarkModeOutlined from "@mui/icons-material/DarkModeOutlined";
import LightModeOutlined from "@mui/icons-material/LightModeOutlined";
import AppBar from "@mui/material/AppBar";
import Avatar from "@mui/material/Avatar";
import IconButton from "@mui/material/IconButton";
import Stack from "@mui/material/Stack";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import { useGetIdentity } from "@refinedev/core";
import { HamburgerMenu, RefineThemedLayoutV2HeaderProps } from "@refinedev/mui";
import React, { useContext } from "react";
import { ColorModeContext } from "../../contexts/color-mode";
type IUser = {
id: number;
name: string;
avatar: string;
};
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
sticky = true,
}) => {
const { mode, setMode } = useContext(ColorModeContext);
const { data: user } = useGetIdentity<IUser>();
return (
<AppBar position={sticky ? "sticky" : "relative"}>
<Toolbar>
<Stack
direction="row"
width="100%"
justifyContent="flex-end"
alignItems="center"
>
<HamburgerMenu />
<Stack
direction="row"
width="100%"
justifyContent="flex-end"
alignItems="center"
>
<IconButton
color="inherit"
onClick={() => {
setMode();
}}
>
{mode === "dark" ? <LightModeOutlined /> : <DarkModeOutlined />}
</IconButton>
{(user?.avatar || user?.name) && (
<Stack
direction="row"
gap="16px"
alignItems="center"
justifyContent="center"
>
{user?.name && (
<Typography
sx={{
display: {
xs: "none",
sm: "inline-block",
},
}}
variant="subtitle2"
>
{user?.name}
</Typography>
)}
<Avatar src={user?.avatar} alt={user?.name} />
</Stack>
)}
</Stack>
</Stack>
</Toolbar>
</AppBar>
);
};

1
src/components/index.ts Normal file
View File

@ -0,0 +1 @@
export { Header } from "./header";

View File

@ -0,0 +1,59 @@
import React, {
PropsWithChildren,
createContext,
useEffect,
useState,
} from "react";
import { ThemeProvider } from "@mui/material/styles";
import { RefineThemes } from "@refinedev/mui";
type ColorModeContextType = {
mode: string;
setMode: () => void;
};
export const ColorModeContext = createContext<ColorModeContextType>(
{} as ColorModeContextType
);
export const ColorModeContextProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
const colorModeFromLocalStorage = localStorage.getItem("colorMode");
const isSystemPreferenceDark = window?.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
const systemPreference = isSystemPreferenceDark ? "dark" : "light";
const [mode, setMode] = useState(
colorModeFromLocalStorage || systemPreference
);
useEffect(() => {
window.localStorage.setItem("colorMode", mode);
}, [mode]);
const setColorMode = () => {
if (mode === "light") {
setMode("dark");
} else {
setMode("light");
}
};
return (
<ColorModeContext.Provider
value={{
setMode: setColorMode,
mode,
}}
>
<ThemeProvider
// you can change the theme colors here. example: mode === "light" ? RefineThemes.Magenta : RefineThemes.MagentaDark
theme={mode === "light" ? RefineThemes.Blue : RefineThemes.BlueDark}
>
{children}
</ThemeProvider>
</ColorModeContext.Provider>
);
};

13
src/index.tsx Normal file
View File

@ -0,0 +1,13 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const container = document.getElementById("root") as HTMLElement;
const root = createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,120 @@
import { Autocomplete, Box, MenuItem, Select, TextField } from "@mui/material";
import { Create, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import React from "react";
import { Controller } from "react-hook-form";
export const BlogPostCreate = () => {
const {
saveButtonProps,
refineCore: { formLoading },
register,
control,
formState: { errors },
} = useForm({});
const { autocompleteProps: categoryAutocompleteProps } = useAutocomplete({
resource: "categories",
});
return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("title", {
required: "This field is required",
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={"Title"}
name="title"
/>
<TextField
{...register("content", {
required: "This field is required",
})}
error={!!(errors as any)?.content}
helperText={(errors as any)?.content?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
multiline
label={"Content"}
name="content"
/>
<Controller
control={control}
name={"category.id"}
rules={{ required: "This field is required" }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete
{...categoryAutocompleteProps}
{...field}
onChange={(_, value) => {
field.onChange(value.id);
}}
getOptionLabel={(item) => {
return (
categoryAutocompleteProps?.options?.find((p) => {
const itemId =
typeof item === "object"
? item?.id?.toString()
: item?.toString();
const pId = p?.id?.toString();
return itemId === pId;
})?.title ?? ""
);
}}
isOptionEqualToValue={(option, value) => {
const optionId = option?.id?.toString();
const valueId =
typeof value === "object"
? value?.id?.toString()
: value?.toString();
return value === undefined || optionId === valueId;
}}
renderInput={(params) => (
<TextField
{...params}
label={"Category"}
margin="normal"
variant="outlined"
error={!!(errors as any)?.category?.id}
helperText={(errors as any)?.category?.id?.message}
required
/>
)}
/>
)}
/>
<Controller
name="status"
control={control}
render={({ field }) => {
return (
<Select
{...field}
value={field?.value || "draft"}
label={"Status"}
>
<MenuItem value="draft">Draft</MenuItem>
<MenuItem value="published">Published</MenuItem>
<MenuItem value="rejected">Rejected</MenuItem>
</Select>
);
}}
/>
</Box>
</Create>
);
};

View File

@ -0,0 +1,125 @@
import { Autocomplete, Box, Select, TextField } from "@mui/material";
import MenuItem from "@mui/material/MenuItem";
import { Edit, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import React from "react";
import { Controller } from "react-hook-form";
export const BlogPostEdit = () => {
const {
saveButtonProps,
refineCore: { queryResult, formLoading },
register,
control,
formState: { errors },
} = useForm({});
const blogPostsData = queryResult?.data?.data;
const { autocompleteProps: categoryAutocompleteProps } = useAutocomplete({
resource: "categories",
defaultValue: blogPostsData?.category?.id,
});
return (
<Edit isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("title", {
required: "This field is required",
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={"Title"}
name="title"
/>
<TextField
{...register("content", {
required: "This field is required",
})}
error={!!(errors as any)?.content}
helperText={(errors as any)?.content?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
multiline
label={"Content"}
name="content"
rows={4}
/>
<Controller
control={control}
name={"category.id"}
rules={{ required: "This field is required" }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete
{...categoryAutocompleteProps}
{...field}
onChange={(_, value) => {
field.onChange(value.id);
}}
getOptionLabel={(item) => {
return (
categoryAutocompleteProps?.options?.find((p) => {
const itemId =
typeof item === "object"
? item?.id?.toString()
: item?.toString();
const pId = p?.id?.toString();
return itemId === pId;
})?.title ?? ""
);
}}
isOptionEqualToValue={(option, value) => {
const optionId = option?.id?.toString();
const valueId =
typeof value === "object"
? value?.id?.toString()
: value?.toString();
return value === undefined || optionId === valueId;
}}
renderInput={(params) => (
<TextField
{...params}
label={"Category"}
margin="normal"
variant="outlined"
error={!!(errors as any)?.category?.id}
helperText={(errors as any)?.category?.id?.message}
required
/>
)}
/>
)}
/>
<Controller
name="status"
control={control}
render={({ field }) => {
return (
<Select
{...field}
value={field?.value || "draft"}
label={"Status"}
>
<MenuItem value="draft">Draft</MenuItem>
<MenuItem value="published">Published</MenuItem>
<MenuItem value="rejected">Rejected</MenuItem>
</Select>
);
}}
/>
</Box>
</Edit>
);
};

View File

@ -0,0 +1,4 @@
export * from "./create";
export * from "./edit";
export * from "./list";
export * from "./show";

View File

@ -0,0 +1,124 @@
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
import { useMany } from "@refinedev/core";
import {
DateField,
DeleteButton,
EditButton,
List,
ShowButton,
useDataGrid,
} from "@refinedev/mui";
import { Typography } from "@mui/material";
import React from "react";
export const BlogPostList = () => {
const { dataGridProps } = useDataGrid({});
const { data: categoryData, isLoading: categoryIsLoading } = useMany({
resource: "categories",
ids:
dataGridProps?.rows
?.map((item: any) => item?.category?.id)
.filter(Boolean) ?? [],
queryOptions: {
enabled: !!dataGridProps?.rows,
},
});
const columns = React.useMemo<GridColDef[]>(
() => [
{
field: "id",
headerName: "ID",
type: "number",
minWidth: 50,
display: "flex",
align: "left",
headerAlign: "left",
},
{
field: "title",
headerName: "Title",
minWidth: 200,
display: "flex",
},
{
field: "content",
flex: 1,
headerName: "Content",
minWidth: 250,
display: "flex",
renderCell: function render({ value }) {
if (!value) return "-";
return (
<Typography
component="p"
whiteSpace="pre"
overflow="hidden"
textOverflow="ellipsis"
>
{value}
</Typography>
);
},
},
{
field: "category",
headerName: "Category",
minWidth: 160,
display: "flex",
valueGetter: (_, row) => {
const value = row?.category;
return value;
},
renderCell: function render({ value }) {
return categoryIsLoading ? (
<>Loading...</>
) : (
categoryData?.data?.find((item) => item.id === value?.id)?.title
);
},
},
{
field: "status",
headerName: "Status",
minWidth: 80,
display: "flex",
},
{
field: "createdAt",
headerName: "Created at",
minWidth: 120,
display: "flex",
renderCell: function render({ value }) {
return <DateField value={value} />;
},
},
{
field: "actions",
headerName: "Actions",
align: "right",
headerAlign: "right",
minWidth: 120,
sortable: false,
display: "flex",
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
</>
);
},
},
],
[categoryData, categoryIsLoading]
);
return (
<List>
<DataGrid {...dataGridProps} columns={columns} />
</List>
);
};

View File

@ -0,0 +1,59 @@
import { Stack, Typography } from "@mui/material";
import { useOne, useShow } from "@refinedev/core";
import {
DateField,
MarkdownField,
NumberField,
Show,
TextFieldComponent as TextField,
} from "@refinedev/mui";
export const BlogPostShow = () => {
const { query } = useShow({});
const { data, isLoading } = query;
const record = data?.data;
const { data: categoryData, isLoading: categoryIsLoading } = useOne({
resource: "categories",
id: record?.category?.id || "",
queryOptions: {
enabled: !!record,
},
});
return (
<Show isLoading={isLoading}>
<Stack gap={1}>
<Typography variant="body1" fontWeight="bold">
{"ID"}
</Typography>
<TextField value={record?.id} />
<Typography variant="body1" fontWeight="bold">
{"Title"}
</Typography>
<TextField value={record?.title} />
<Typography variant="body1" fontWeight="bold">
{"Content"}
</Typography>
<MarkdownField value={record?.content} />
<Typography variant="body1" fontWeight="bold">
{"Category"}
</Typography>
{categoryIsLoading ? <>Loading...</> : <>{categoryData?.data?.title}</>}
<Typography variant="body1" fontWeight="bold">
{"Status"}
</Typography>
<TextField value={record?.status} />
<Typography variant="body1" fontWeight="bold">
{"CreatedAt"}
</Typography>
<DateField value={record?.createdAt} />
</Stack>
</Show>
);
};

View File

@ -0,0 +1,36 @@
import { Box, TextField } from "@mui/material";
import { Create } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
export const CategoryCreate = () => {
const {
saveButtonProps,
refineCore: { formLoading },
register,
formState: { errors },
} = useForm({});
return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("title", {
required: "This field is required",
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={"Title"}
name="title"
/>
</Box>
</Create>
);
};

View File

@ -0,0 +1,35 @@
import { Box, TextField } from "@mui/material";
import { Edit } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
export const CategoryEdit = () => {
const {
saveButtonProps,
register,
formState: { errors },
} = useForm({});
return (
<Edit saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("title", {
required: "This field is required",
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={"Title"}
name="title"
/>
</Box>
</Edit>
);
};

View File

@ -0,0 +1,4 @@
export * from "./create";
export * from "./edit";
export * from "./list";
export * from "./show";

View File

@ -0,0 +1,59 @@
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
import {
DeleteButton,
EditButton,
List,
ShowButton,
useDataGrid,
} from "@refinedev/mui";
import React from "react";
export const CategoryList = () => {
const { dataGridProps } = useDataGrid({});
const columns = React.useMemo<GridColDef[]>(
() => [
{
field: "id",
headerName: "ID",
type: "number",
minWidth: 50,
display: "flex",
align: "left",
headerAlign: "left",
},
{
field: "title",
flex: 1,
headerName: "Title",
minWidth: 200,
display: "flex",
},
{
field: "actions",
headerName: "Actions",
align: "right",
headerAlign: "right",
minWidth: 120,
sortable: false,
display: "flex",
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
</>
);
},
},
],
[]
);
return (
<List>
<DataGrid {...dataGridProps} columns={columns} />
</List>
);
};

View File

@ -0,0 +1,29 @@
import { Stack, Typography } from "@mui/material";
import { useShow } from "@refinedev/core";
import {
NumberField,
Show,
TextFieldComponent as TextField,
} from "@refinedev/mui";
export const CategoryShow = () => {
const { query } = useShow({});
const { data, isLoading } = query;
const record = data?.data;
return (
<Show isLoading={isLoading}>
<Stack gap={1}>
<Typography variant="body1" fontWeight="bold">
{"ID"}
</Typography>
<TextField value={record?.id} />
<Typography variant="body1" fontWeight="bold">
{"Title"}
</Typography>
<TextField value={record?.title} />
</Stack>
</Show>
);
};

View File

@ -0,0 +1,5 @@
import { AuthPage } from "@refinedev/mui";
export const ForgotPassword = () => {
return <AuthPage type="forgotPassword" />;
};

12
src/pages/login/index.tsx Normal file
View File

@ -0,0 +1,12 @@
import { AuthPage } from "@refinedev/mui";
export const Login = () => {
return (
<AuthPage
type="login"
formProps={{
defaultValues: { email: "demo@refine.dev", password: "demodemo" },
}}
/>
);
};

View File

@ -0,0 +1,5 @@
import { AuthPage } from "@refinedev/mui";
export const Register = () => {
return <AuthPage type="register" />;
};

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />