init repo
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
**/node_modules/
|
||||
**/dist
|
||||
.git
|
||||
npm-debug.log
|
||||
.coverage
|
||||
.coverage.*
|
||||
.env
|
||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
43
Dockerfile
Normal file
43
Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
FROM refinedev/node:18 AS base
|
||||
|
||||
FROM base AS deps
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
|
||||
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
COPY --from=deps /app/refine/node_modules ./node_modules
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM base AS runner
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
COPY --from=builder /app/refine/public ./public
|
||||
|
||||
RUN mkdir .next
|
||||
RUN chown refine:nodejs .next
|
||||
|
||||
COPY --from=builder --chown=refine:nodejs /app/refine/.next/standalone ./
|
||||
COPY --from=builder --chown=refine:nodejs /app/refine/.next/static ./.next/static
|
||||
|
||||
USER refine
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
48
README.MD
Normal file
48
README.MD
Normal file
@@ -0,0 +1,48 @@
|
||||
# my-refine-app
|
||||
|
||||
<div align="center" style="margin: 30px;">
|
||||
<a href="https://refine.dev">
|
||||
<img alt="refine logo" src="https://refine.ams3.cdn.digitaloceanspaces.com/readme/refine-readme-banner.png">
|
||||
</a>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
This [Refine](https://github.com/refinedev/refine) project was generated with [create refine-app](https://github.com/refinedev/refine/tree/master/packages/create-refine-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility ✨
|
||||
|
||||
Refine's hooks and components simplifies the development process and eliminates the repetitive tasks by providing industry-standard solutions for crucial aspects of a project, including authentication, access control, routing, networking, state management, and i18n.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### Running the development server.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Building for production.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Running the production server.
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about **Refine**, please check out the [Documentation](https://refine.dev/docs)
|
||||
|
||||
- **REST Data Provider** [Docs](https://refine.dev/docs/core/providers/data-provider/#overview)
|
||||
- **Material UI** [Docs](https://refine.dev/docs/ui-frameworks/mui/tutorial/)
|
||||
- **Custom Auth Provider** [Docs](https://refine.dev/docs/core/providers/auth-provider/)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
6
next.config.mjs
Normal file
6
next.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
12264
package-lock.json
generated
Normal file
12264
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
package.json
Normal file
49
package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "my-refine-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_OPTIONS=--max_old_space_size=4096 refine dev",
|
||||
"build": "refine build",
|
||||
"start": "refine start",
|
||||
"lint": "next lint",
|
||||
"refine": "refine"
|
||||
},
|
||||
"dependencies": {
|
||||
"@refinedev/cli": "^2.16.48",
|
||||
"@refinedev/core": "^5.0.0",
|
||||
"@refinedev/devtools": "^2.0.1",
|
||||
"@refinedev/nextjs-router": "^7.0.0",
|
||||
"@refinedev/kbar": "^2.0.0",
|
||||
"next": "^15.2.4",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"@refinedev/simple-rest": "^6.0.0",
|
||||
"@refinedev/mui": "^7.0.0",
|
||||
"@refinedev/react-hook-form": "^5.0.0",
|
||||
"@mui/icons-material": "^6.1.6",
|
||||
"@emotion/react": "^11.8.2",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/lab": "^6.0.0-beta.14",
|
||||
"@mui/material": "^6.1.7",
|
||||
"@mui/x-data-grid": "^7.22.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"next-auth": "^4.24.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"@types/node": "^20",
|
||||
"typescript": "^5.8.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"@types/js-cookie": "^3.0.6"
|
||||
},
|
||||
"refine": {
|
||||
"projectId": "4LwOCL-BBaV29-qUYMAJ"
|
||||
}
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
82
src/components/header/index.tsx
Normal file
82
src/components/header/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { ColorModeContext } from "@contexts/color-mode";
|
||||
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, RefineThemedLayoutHeaderProps } from "@refinedev/mui";
|
||||
import React, { useContext } from "react";
|
||||
|
||||
type IUser = {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
export const Header: React.FC<RefineThemedLayoutHeaderProps> = ({
|
||||
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>
|
||||
);
|
||||
};
|
||||
72
src/contexts/color-mode/index.tsx
Normal file
72
src/contexts/color-mode/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import GlobalStyles from "@mui/material/GlobalStyles";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { RefineThemes } from "@refinedev/mui";
|
||||
import Cookies from "js-cookie";
|
||||
import React, {
|
||||
type PropsWithChildren,
|
||||
createContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
type ColorModeContextType = {
|
||||
mode: string;
|
||||
setMode: () => void;
|
||||
};
|
||||
|
||||
export const ColorModeContext = createContext<ColorModeContextType>(
|
||||
{} as ColorModeContextType
|
||||
);
|
||||
|
||||
type ColorModeContextProviderProps = {
|
||||
defaultMode?: string;
|
||||
};
|
||||
|
||||
export const ColorModeContextProvider: React.FC<
|
||||
PropsWithChildren<ColorModeContextProviderProps>
|
||||
> = ({ children, defaultMode }) => {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [mode, setMode] = useState(defaultMode || "light");
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
const systemTheme = useMediaQuery(`(prefers-color-scheme: dark)`);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMounted) {
|
||||
const theme = Cookies.get("theme") || (systemTheme ? "dark" : "light");
|
||||
setMode(theme);
|
||||
}
|
||||
}, [isMounted, systemTheme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const nextTheme = mode === "light" ? "dark" : "light";
|
||||
|
||||
setMode(nextTheme);
|
||||
Cookies.set("theme", nextTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<ColorModeContext.Provider
|
||||
value={{
|
||||
setMode: toggleTheme,
|
||||
mode,
|
||||
}}
|
||||
>
|
||||
<ThemeProvider
|
||||
// you can change the theme colors here. example: mode === "light" ? RefineThemes.Magenta : RefineThemes.MagentaDark
|
||||
theme={mode === "light" ? RefineThemes.Blue : RefineThemes.BlueDark}
|
||||
>
|
||||
<CssBaseline />
|
||||
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</ColorModeContext.Provider>
|
||||
);
|
||||
};
|
||||
16
src/interfaces/theme.d.ts
vendored
Normal file
16
src/interfaces/theme.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
RefineTheme,
|
||||
ThemeOptions as RefineThemeOptions,
|
||||
} from "@refinedev/mui";
|
||||
|
||||
export interface CustomTheme {
|
||||
// Add custom variables here like below:
|
||||
// status: {
|
||||
// danger: string;
|
||||
// };
|
||||
}
|
||||
|
||||
declare module "@refinedev/mui" {
|
||||
interface Theme extends RefineTheme, CustomTheme {}
|
||||
interface ThemeOptions extends RefineThemeOptions, CustomTheme {}
|
||||
}
|
||||
7
src/providers/data-provider/index.ts
Normal file
7
src/providers/data-provider/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import dataProviderSimpleRest from "@refinedev/simple-rest";
|
||||
|
||||
const API_URL = "https://api.fake-rest.refine.dev";
|
||||
|
||||
export const dataProvider = dataProviderSimpleRest(API_URL);
|
||||
16
src/providers/devtools/index.tsx
Normal file
16
src/providers/devtools/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
DevtoolsPanel,
|
||||
DevtoolsProvider as DevtoolsProviderBase,
|
||||
} from "@refinedev/devtools";
|
||||
import React from "react";
|
||||
|
||||
export const DevtoolsProvider = (props: React.PropsWithChildren) => {
|
||||
return (
|
||||
<DevtoolsProviderBase>
|
||||
{props.children}
|
||||
<DevtoolsPanel />
|
||||
</DevtoolsProviderBase>
|
||||
);
|
||||
};
|
||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@*": ["./src/*"],
|
||||
"@pages/*": ["./pages/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user