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