init repo
This commit is contained in:
152
src/app/_refine_context.tsx
Normal file
152
src/app/_refine_context.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
"use client";
|
||||
|
||||
import { GitHubBanner, Refine, type AuthProvider } from "@refinedev/core";
|
||||
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
|
||||
import {
|
||||
RefineSnackbarProvider,
|
||||
useNotificationProvider,
|
||||
} from "@refinedev/mui";
|
||||
import { SessionProvider, signIn, signOut, useSession } from "next-auth/react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import routerProvider from "@refinedev/nextjs-router";
|
||||
|
||||
import { ColorModeContextProvider } from "@contexts/color-mode";
|
||||
import { dataProvider } from "@providers/data-provider";
|
||||
|
||||
type RefineContextProps = {
|
||||
defaultMode?: string;
|
||||
};
|
||||
|
||||
export const RefineContext = (
|
||||
props: React.PropsWithChildren<RefineContextProps>
|
||||
) => {
|
||||
return (
|
||||
<SessionProvider>
|
||||
<App {...props} />
|
||||
</SessionProvider>
|
||||
);
|
||||
};
|
||||
|
||||
type AppProps = {
|
||||
defaultMode?: string;
|
||||
};
|
||||
|
||||
const App = (props: React.PropsWithChildren<AppProps>) => {
|
||||
const { data, status } = useSession();
|
||||
const to = usePathname();
|
||||
|
||||
if (status === "loading") {
|
||||
return <span>loading...</span>;
|
||||
}
|
||||
|
||||
const authProvider: AuthProvider = {
|
||||
login: async () => {
|
||||
signIn("keycloak", {
|
||||
callbackUrl: to ? to.toString() : "/",
|
||||
redirect: true,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
logout: async () => {
|
||||
signOut({
|
||||
redirect: true,
|
||||
callbackUrl: "/login",
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
onError: async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
return {
|
||||
logout: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
};
|
||||
},
|
||||
check: async () => {
|
||||
if (status === "unauthenticated") {
|
||||
return {
|
||||
authenticated: false,
|
||||
redirectTo: "/login",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
};
|
||||
},
|
||||
getPermissions: async () => {
|
||||
return null;
|
||||
},
|
||||
getIdentity: async () => {
|
||||
if (data?.user) {
|
||||
const { user } = data;
|
||||
return {
|
||||
name: user.name,
|
||||
avatar: user.image,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const defaultMode = props?.defaultMode;
|
||||
|
||||
return (
|
||||
<>
|
||||
<GitHubBanner />
|
||||
<RefineKbarProvider>
|
||||
<ColorModeContextProvider defaultMode={defaultMode}>
|
||||
<RefineSnackbarProvider>
|
||||
<Refine
|
||||
routerProvider={routerProvider}
|
||||
dataProvider={dataProvider}
|
||||
notificationProvider={useNotificationProvider}
|
||||
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,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
<RefineKbar />
|
||||
</Refine>
|
||||
</RefineSnackbarProvider>
|
||||
</ColorModeContextProvider>
|
||||
</RefineKbarProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
24
src/app/api/auth/[...nextauth]/options.ts
Normal file
24
src/app/api/auth/[...nextauth]/options.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||
|
||||
const authOptions = {
|
||||
// Configure one or more authentication providers
|
||||
providers: [
|
||||
// !!! Should be stored in .env file.
|
||||
KeycloakProvider({
|
||||
clientId: `refine-demo`,
|
||||
clientSecret: `refine`,
|
||||
issuer: `https://lemur-0.cloud-iam.com/auth/realms/refine`,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name ?? profile.preferred_username,
|
||||
email: profile.email,
|
||||
image: `https://faces-img.xcdn.link/thumb-lorem-face-6312_thumb.jpg`,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
secret: `UItTuD1HcGXIj8ZfHUswhYdNd40Lc325R8VlxQPUoR0=`,
|
||||
};
|
||||
|
||||
export default authOptions;
|
||||
5
src/app/api/auth/[...nextauth]/route.ts
Normal file
5
src/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import NextAuth from "next-auth/next";
|
||||
import authOptions from "./options";
|
||||
|
||||
const auth = NextAuth(authOptions);
|
||||
export { auth as GET, auth as POST };
|
||||
122
src/app/blog-posts/create/page.tsx
Normal file
122
src/app/blog-posts/create/page.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { Autocomplete, Box, MenuItem, Select, TextField } from "@mui/material";
|
||||
import { Create, useAutocomplete } from "@refinedev/mui";
|
||||
import { useForm } from "@refinedev/react-hook-form";
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
export default function BlogPostCreate() {
|
||||
const {
|
||||
saveButtonProps,
|
||||
refineCore: { formLoading, onFinish },
|
||||
handleSubmit,
|
||||
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>
|
||||
);
|
||||
}
|
||||
127
src/app/blog-posts/edit/[id]/page.tsx
Normal file
127
src/app/blog-posts/edit/[id]/page.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
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 { Controller } from "react-hook-form";
|
||||
|
||||
export default function BlogPostEdit() {
|
||||
const {
|
||||
saveButtonProps,
|
||||
refineCore: { query, formLoading, onFinish },
|
||||
handleSubmit,
|
||||
register,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm({});
|
||||
|
||||
const blogPostsData = query?.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"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
</Box>
|
||||
</Edit>
|
||||
);
|
||||
}
|
||||
23
src/app/blog-posts/layout.tsx
Normal file
23
src/app/blog-posts/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import authOptions from "@app/api/auth/[...nextauth]/options";
|
||||
import { Header } from "@components/header";
|
||||
import { ThemedLayout } from "@refinedev/mui";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
export default async function Layout({ children }: React.PropsWithChildren) {
|
||||
const data = await getData();
|
||||
|
||||
if (!data.session?.user) {
|
||||
return redirect("/login");
|
||||
}
|
||||
|
||||
return <ThemedLayout Header={Header}>{children}</ThemedLayout>;
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
const session = await getServerSession(authOptions);
|
||||
return {
|
||||
session,
|
||||
};
|
||||
}
|
||||
130
src/app/blog-posts/page.tsx
Normal file
130
src/app/blog-posts/page.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import { Typography } from "@mui/material";
|
||||
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 React from "react";
|
||||
|
||||
export default function BlogPostList() {
|
||||
const { result, dataGridProps } = useDataGrid({
|
||||
syncWithLocation: true,
|
||||
});
|
||||
|
||||
const {
|
||||
result: { data: categories },
|
||||
query: { isLoading: categoryIsLoading },
|
||||
} = useMany({
|
||||
resource: "categories",
|
||||
ids:
|
||||
result?.data?.map((item: any) => item?.category?.id).filter(Boolean) ??
|
||||
[],
|
||||
queryOptions: {
|
||||
enabled: !!result?.data,
|
||||
},
|
||||
});
|
||||
|
||||
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...</>
|
||||
) : (
|
||||
categories?.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} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[categories, categoryIsLoading]
|
||||
);
|
||||
|
||||
return (
|
||||
<List>
|
||||
<DataGrid {...dataGridProps} columns={columns} />
|
||||
</List>
|
||||
);
|
||||
}
|
||||
62
src/app/blog-posts/show/[id]/page.tsx
Normal file
62
src/app/blog-posts/show/[id]/page.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useOne, useShow } from "@refinedev/core";
|
||||
import {
|
||||
DateField,
|
||||
MarkdownField,
|
||||
Show,
|
||||
TextFieldComponent as TextField,
|
||||
} from "@refinedev/mui";
|
||||
|
||||
export default function BlogPostShow() {
|
||||
const { result: record, query } = useShow({});
|
||||
|
||||
const { isLoading } = query;
|
||||
|
||||
const {
|
||||
result: category,
|
||||
query: { 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...</> : <>{category?.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>
|
||||
);
|
||||
}
|
||||
38
src/app/categories/create/page.tsx
Normal file
38
src/app/categories/create/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { Box, TextField } from "@mui/material";
|
||||
import { Create } from "@refinedev/mui";
|
||||
import { useForm } from "@refinedev/react-hook-form";
|
||||
|
||||
export default function 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>
|
||||
);
|
||||
}
|
||||
37
src/app/categories/edit/[id]/page.tsx
Normal file
37
src/app/categories/edit/[id]/page.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { Box, TextField } from "@mui/material";
|
||||
import { Edit } from "@refinedev/mui";
|
||||
import { useForm } from "@refinedev/react-hook-form";
|
||||
|
||||
export default function 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>
|
||||
);
|
||||
}
|
||||
23
src/app/categories/layout.tsx
Normal file
23
src/app/categories/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import authOptions from "@app/api/auth/[...nextauth]/options";
|
||||
import { Header } from "@components/header";
|
||||
import { ThemedLayout } from "@refinedev/mui";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
export default async function Layout({ children }: React.PropsWithChildren) {
|
||||
const data = await getData();
|
||||
|
||||
if (!data.session?.user) {
|
||||
return redirect("/login");
|
||||
}
|
||||
|
||||
return <ThemedLayout Header={Header}>{children}</ThemedLayout>;
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
const session = await getServerSession(authOptions);
|
||||
return {
|
||||
session,
|
||||
};
|
||||
}
|
||||
61
src/app/categories/page.tsx
Normal file
61
src/app/categories/page.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
|
||||
import {
|
||||
DeleteButton,
|
||||
EditButton,
|
||||
List,
|
||||
ShowButton,
|
||||
useDataGrid,
|
||||
} from "@refinedev/mui";
|
||||
import React from "react";
|
||||
|
||||
export default function 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>
|
||||
);
|
||||
}
|
||||
27
src/app/categories/show/[id]/page.tsx
Normal file
27
src/app/categories/show/[id]/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useShow } from "@refinedev/core";
|
||||
import { Show, TextFieldComponent as TextField } from "@refinedev/mui";
|
||||
|
||||
export default function 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>
|
||||
);
|
||||
}
|
||||
BIN
src/app/icon.ico
Normal file
BIN
src/app/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
32
src/app/layout.tsx
Normal file
32
src/app/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import React, { Suspense } from "react";
|
||||
import { RefineContext } from "./_refine_context";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Refine",
|
||||
description: "Generated by create refine app",
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
},
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const cookieStore = await cookies();
|
||||
const theme = cookieStore.get("theme");
|
||||
const defaultMode = theme?.value === "dark" ? "dark" : "light";
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Suspense>
|
||||
<RefineContext defaultMode={defaultMode}>{children}</RefineContext>
|
||||
</Suspense>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
24
src/app/login/layout.tsx
Normal file
24
src/app/login/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import authOptions from "@app/api/auth/[...nextauth]/options";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
export default async function LoginLayout({
|
||||
children,
|
||||
}: React.PropsWithChildren) {
|
||||
const data = await getData();
|
||||
|
||||
if (data.session?.user) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
return {
|
||||
session,
|
||||
};
|
||||
}
|
||||
55
src/app/login/page.tsx
Normal file
55
src/app/login/page.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Container from "@mui/material/Container";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useLogin } from "@refinedev/core";
|
||||
import { ThemedTitle } from "@refinedev/mui";
|
||||
|
||||
export default function Login() {
|
||||
const { mutate: login } = useLogin();
|
||||
|
||||
return (
|
||||
<Container
|
||||
style={{
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
gap="36px"
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
>
|
||||
<ThemedTitle
|
||||
collapsed={false}
|
||||
wrapperStyles={{
|
||||
fontSize: "22px",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{ width: "240px" }}
|
||||
size="large"
|
||||
variant="contained"
|
||||
onClick={() => login({})}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
<Typography align="center" color={"text.secondary"} fontSize="12px">
|
||||
Powered by
|
||||
<img
|
||||
style={{ padding: "0 5px" }}
|
||||
alt="Keycloak"
|
||||
src="https://refine.ams3.cdn.digitaloceanspaces.com/superplate-auth-icons%2Fkeycloak.svg"
|
||||
/>
|
||||
Keycloak
|
||||
</Typography>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
15
src/app/not-found.tsx
Normal file
15
src/app/not-found.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { Authenticated } from "@refinedev/core";
|
||||
import { ErrorComponent } from "@refinedev/mui";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<Suspense>
|
||||
<Authenticated key="not-found">
|
||||
<ErrorComponent />
|
||||
</Authenticated>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
16
src/app/page.tsx
Normal file
16
src/app/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { Authenticated } from "@refinedev/core";
|
||||
import { NavigateToResource } from "@refinedev/nextjs-router";
|
||||
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<Authenticated key="home-page">
|
||||
<NavigateToResource />
|
||||
</Authenticated>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user