init repo

This commit is contained in:
JIANG
2025-09-09 10:01:21 +08:00
commit d70d1709d3
34 changed files with 13652 additions and 0 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
**/node_modules/
**/dist
.git
npm-debug.log
.coverage
.coverage.*
.env

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

36
.gitignore vendored Normal file
View 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

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
legacy-peer-deps=true
strict-peer-dependencies=false

43
Dockerfile Normal file
View 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
View 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
View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
};
export default nextConfig;

12264
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
package.json Normal file
View 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
View 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>
</>
);
};

View 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;

View 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 };

View 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>
);
}

View 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>
);
}

View 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
View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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,
};
}

View 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>
);
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

32
src/app/layout.tsx Normal file
View 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
View 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
View 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
View 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
View 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>
);
}

View 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>
);
};

View 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
View 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 {}
}

View 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);

View 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
View 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"]
}