improve: add production stage
This commit is contained in:
@@ -1,36 +1,31 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
rm -f /app/src/config.js;
|
|
||||||
if [ -z "$BACKEND_HOST" ]; then
|
|
||||||
BACKEND_HOST="http://localhost:5000"
|
|
||||||
fi
|
|
||||||
if [ -z "$FRONTEND_HOST" ]; then
|
|
||||||
FRONTEND_HOST="http://localhost:3000"
|
|
||||||
fi
|
|
||||||
if [ -z "$KC_CLIENT_ID" ]; then
|
|
||||||
KC_CLIENT_ID="labdev"
|
|
||||||
fi
|
|
||||||
if [ -z "$KC_HOST" ]; then
|
|
||||||
KC_HOST="https://login.hangman-lab.top"
|
|
||||||
fi
|
|
||||||
if [ -z "$KC_REALM" ]; then
|
|
||||||
KC_REALM="Hangman-Lab"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p /app/src
|
BACKEND_HOST="${BACKEND_HOST:-http://localhost:5000}"
|
||||||
|
FRONTEND_HOST="${FRONTEND_HOST:-http://localhost:80}"
|
||||||
|
KC_CLIENT_ID="${KC_CLIENT_ID:-labdev}"
|
||||||
|
KC_HOST="${KC_HOST:-https://login.hangman-lab.top}"
|
||||||
|
KC_REALM="${KC_REALM:-Hangman-Lab}"
|
||||||
|
|
||||||
echo "
|
rm -f /usr/share/nginx/html/config.js
|
||||||
const config = {
|
|
||||||
BACKEND_HOST: \"${BACKEND_HOST}\",
|
|
||||||
FRONTEND_HOST: \"${FRONTEND_HOST}\",
|
|
||||||
KC_CLIENT_ID: \"${KC_CLIENT_ID}\",
|
cat <<EOL > /usr/share/nginx/html/config.json
|
||||||
OIDC_CONFIG: {
|
{
|
||||||
authority: \"${KC_HOST}/realms/${KC_REALM}\",
|
"BACKEND_HOST": "${BACKEND_HOST}",
|
||||||
client_id: \"${KC_CLIENT_ID}\",
|
"FRONTEND_HOST": "${FRONTEND_HOST}",
|
||||||
redirect_uri: \"${FRONTEND_HOST}/callback\",
|
"KC_CLIENT_ID": "${KC_CLIENT_ID}",
|
||||||
post_logout_redirect_uri: \"${FRONTEND_HOST}\",
|
"OIDC_CONFIG": {
|
||||||
response_type: \"code\",
|
"authority": "${KC_HOST}/realms/${KC_REALM}",
|
||||||
scope: \"openid profile email roles\",
|
"client_id": "${KC_CLIENT_ID}",
|
||||||
},
|
"redirect_uri": "${FRONTEND_HOST}/callback",
|
||||||
};
|
"post_logout_redirect_uri": "${FRONTEND_HOST}",
|
||||||
export default config;
|
"response_type": "code",
|
||||||
" > /app/src/config.js;
|
"scope": "openid profile email roles",
|
||||||
|
"popup_redirect_uri": "${FRONTEND_HOST}/popup_callback",
|
||||||
|
"silent_redirect_uri": "${FRONTEND_HOST}/silent_callback"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
22
Dockerfile
22
Dockerfile
@@ -1,15 +1,27 @@
|
|||||||
FROM node:18 as build-stage
|
FROM node:18-alpine AS build-stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY BuildConfig.sh /app/BuildConfig.sh
|
|
||||||
RUN chmod +x /app/BuildConfig.sh
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
CMD ["/bin/bash", "-c", "/app/BuildConfig.sh && npm start"]
|
FROM nginx:stable-alpine AS production-stage
|
||||||
|
|
||||||
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
|
COPY --from=build-stage /app/public/icons /usr/share/nginx/html/icons
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY BuildConfig.sh /docker-entrypoint.d/10-build-config.sh
|
||||||
|
RUN chmod a+x /docker-entrypoint.d/10-build-config.sh
|
||||||
|
EXPOSE $FRONTEND_PORT
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
138
README.md
Normal file
138
README.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# Hangman Lab Frontend
|
||||||
|
|
||||||
|
This project serves as a lightweight frontend solution for personal websites, designed to work seamlessly with Keycloak for authentication and authorization. It provides an easy-to-use interface for managing and publishing markdown files, allowing users to maintain their personal content effortlessly.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Features](#features)
|
||||||
|
- [Technologies Used](#technologies-used)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Development](#development)
|
||||||
|
- [Building for Production](#building-for-production)
|
||||||
|
- [Docker Deployment](#docker-deployment)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **User Authentication**: Secure login and logout functionalities using **Keycloak** and **OpenID Connect (OIDC)**.
|
||||||
|
- **Markdown Management**: Create, edit, and view markdown files with live preview and syntax highlighting for a streamlined content creation experience.
|
||||||
|
- **Docker Support**: Containerized setup for easy deployment and scalability.
|
||||||
|
|
||||||
|
|
||||||
|
## Technologies Used
|
||||||
|
|
||||||
|
This project uses the following open-source libraries, all of which are licensed under the MIT License:
|
||||||
|
|
||||||
|
- [Bulma](https://bulma.io/) (MIT)
|
||||||
|
- [Katex](https://katex.org/) (MIT)
|
||||||
|
- [oidc-client-ts](https://github.com/authts/oidc-client-ts) (Apache-2.0)
|
||||||
|
- [React](https://reactjs.org/) (MIT)
|
||||||
|
- [React-DOM](https://reactjs.org/) (MIT)
|
||||||
|
- [React-Markdown](https://github.com/remarkjs/react-markdown) (MIT)
|
||||||
|
- [React-Query](https://react-query-v3.tanstack.com/) (MIT)
|
||||||
|
- [React-Router-DOM](https://reactrouter.com/) (MIT)
|
||||||
|
- [React-Syntax-Highlighter](https://github.com/react-syntax-highlighter/react-syntax-highlighter) (MIT)
|
||||||
|
- [Rehype-Katex](https://github.com/remarkjs/remark-math/tree/main/packages/rehype-katex) (MIT)
|
||||||
|
- [Rehype-Raw](https://github.com/rehypejs/rehype-raw) (MIT)
|
||||||
|
- [Remark-Math](https://github.com/remarkjs/remark-math) (MIT)
|
||||||
|
|
||||||
|
The development dependencies (e.g., Babel, Webpack, and loaders) are also licensed under the MIT License.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
### Prerequisites
|
||||||
|
- **Node.js** (v14.x or higher)
|
||||||
|
- **npm** (v6.x or higher) or **Yarn**
|
||||||
|
- **Docker** (optional, for containerized deployment)
|
||||||
|
- **Keycloak Server**: Ensure you have access to a Keycloak server for authentication.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. **Clone the Repository**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.hangman-lab.top/hzhang/HangmanLab.Frontend.git
|
||||||
|
cd HangmanLab.Frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install Dependencies**
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Or using Yarn:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure environment variables**
|
||||||
|
|
||||||
|
See Configuration[#configuration]
|
||||||
|
|
||||||
|
4. **BuildConfig.sh**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x BuildConfig
|
||||||
|
./BuildConfig.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
Set the following environment variables before running BuildConfig.sh
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
- `BACKEND_HOST`: URL of the backend server (default: `http://localhost:5000`)
|
||||||
|
- `FRONTEND_HOST`: URL of the frontend server (default: `http://localhost:3000`)
|
||||||
|
- `KC_CLIENT_ID`: Keycloak client ID
|
||||||
|
- `KC_HOST`: Keycloak server URL
|
||||||
|
- `KC_REALM`: Keycloak realm
|
||||||
|
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running the Development Server
|
||||||
|
|
||||||
|
Start the development server with hot reloading:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Or using Yarn:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be accessible at `http://localhost:3000` by default.
|
||||||
|
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
### Image
|
||||||
|
Pull the latest Docker image from the repository:
|
||||||
|
```bash
|
||||||
|
docker pull git.hangman-lab.top/hzhang/hangman-lab-frontend:latest
|
||||||
|
```
|
||||||
|
### Building the docker Image
|
||||||
|
```bash
|
||||||
|
docker build -t hangman-lab-frontend:latest .
|
||||||
|
```
|
||||||
|
### Running the Docker Container
|
||||||
|
Ensure your backend service (e.g., running at http://localhost:5000) is up and accessible. Then, run the Docker container:
|
||||||
|
```bash
|
||||||
|
docker run -d -p 80:80 --name hangman-lab-frontend hangman-lab-frontend:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Note:
|
||||||
|
- The container listens on port 80. Adjust the port mapping (-p) as needed.
|
||||||
|
- Make sure to set the appropriate environment variables either in the Dockerfile or via Docker environment variable options.
|
||||||
|
## License
|
||||||
|
[MIT][license] © [hzhang][author]
|
||||||
|
|
||||||
|
<!-- Definitions -->
|
||||||
|
[author]: https://hangman-lab.top
|
||||||
|
[license]: license
|
||||||
22
licence
Normal file
22
licence
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) hzhang <hzhang@hangman-lab.top>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
'Software'), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
13
nginx.conf
Normal file
13
nginx.conf
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
location /icons/ {
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
error_page 404 /index.html;
|
||||||
|
}
|
||||||
34
package-lock.json
generated
34
package-lock.json
generated
@@ -28,6 +28,7 @@
|
|||||||
"@babel/preset-react": "^7.25.9",
|
"@babel/preset-react": "^7.25.9",
|
||||||
"babel-loader": "^9.2.1",
|
"babel-loader": "^9.2.1",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
|
"dotenv-webpack": "^8.1.0",
|
||||||
"html-webpack-plugin": "^5.6.3",
|
"html-webpack-plugin": "^5.6.3",
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.81.0",
|
||||||
"sass-loader": "^16.0.3",
|
"sass-loader": "^16.0.3",
|
||||||
@@ -3611,6 +3612,39 @@
|
|||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "8.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
|
||||||
|
"integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv-defaults": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^8.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv-webpack": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-owK1JcsPkIobeqjVrk6h7jPED/W6ZpdFsMPR+5ursB7/SdgDyO+VzAU+szK8C8u3qUhtENyYnj8eyXMR5kkGag==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv-defaults": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"webpack": "^4 || ^5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"@babel/preset-react": "^7.25.9",
|
"@babel/preset-react": "^7.25.9",
|
||||||
"babel-loader": "^9.2.1",
|
"babel-loader": "^9.2.1",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
|
"dotenv-webpack": "^8.1.0",
|
||||||
"html-webpack-plugin": "^5.6.3",
|
"html-webpack-plugin": "^5.6.3",
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.81.0",
|
||||||
"sass-loader": "^16.0.3",
|
"sass-loader": "^16.0.3",
|
||||||
|
|||||||
BIN
public/icons/email.png
Normal file
BIN
public/icons/email.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
public/icons/git.png
Normal file
BIN
public/icons/git.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
public/icons/linkedin.png
Normal file
BIN
public/icons/linkedin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -7,10 +7,11 @@
|
|||||||
.content-container {
|
.content-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 1rem;
|
padding: 1rem 1rem 100px 1rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
14
src/App.js
14
src/App.js
@@ -5,11 +5,12 @@ import SideNavigation from "./components/Navigations/SideNavigation";
|
|||||||
import MarkdownContent from "./components/Markdowns/MarkdownContent";
|
import MarkdownContent from "./components/Markdowns/MarkdownContent";
|
||||||
import MarkdownEditor from "./components/Markdowns/MarkdownEditor";
|
import MarkdownEditor from "./components/Markdowns/MarkdownEditor";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import Callback from "./Callback";
|
import Callback from "./components/KeycloakCallbacks/Callback";
|
||||||
import config from "./config";
|
import Footer from "./components/Footer";
|
||||||
|
import PopupCallback from "./components/KeycloakCallbacks/PopupCallback";
|
||||||
|
import SilentCallback from "./components/KeycloakCallbacks/SilentCallback";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
console.log(config)
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div className="app-container">
|
<div className="app-container">
|
||||||
@@ -22,12 +23,15 @@ const App = () => {
|
|||||||
<Route path="/markdown/:id" element={<MarkdownContent />} />
|
<Route path="/markdown/:id" element={<MarkdownContent />} />
|
||||||
<Route path="/callback" element={<Callback />} />
|
<Route path="/callback" element={<Callback />} />
|
||||||
<Route path="/test" element={<h1>TEST</h1>}></Route>
|
<Route path="/test" element={<h1>TEST</h1>}></Route>
|
||||||
<Route path="/markdown/create" element={<MarkdownEditor />}></Route>
|
<Route path="/markdown/create" element={<MarkdownEditor />} />
|
||||||
<Route path="/markdown/edit/:id" element={<MarkdownEditor />}></Route>
|
<Route path="/markdown/edit/:id" element={<MarkdownEditor />} />
|
||||||
|
<Route path="/popup_callback" element={<PopupCallback />} />
|
||||||
|
<Route path="silent_callback" element={<SilentCallback />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { createContext, useEffect, useMemo, useState } from "react";
|
// src/AuthProvider.js
|
||||||
|
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { UserManager } from "oidc-client-ts";
|
import { UserManager } from "oidc-client-ts";
|
||||||
import config from "./config";
|
import { ConfigContext } from "./ConfigProvider";
|
||||||
|
|
||||||
export const AuthContext = createContext({
|
export const AuthContext = createContext({
|
||||||
user: null,
|
user: null,
|
||||||
@@ -10,11 +11,20 @@ export const AuthContext = createContext({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const AuthProvider = ({ children }) => {
|
const AuthProvider = ({ children }) => {
|
||||||
|
const { config, isLoading, error } = useContext(ConfigContext);
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [roles, setRoles] = useState([]);
|
const [roles, setRoles] = useState([]);
|
||||||
const userManager = useMemo(() => new UserManager(config.OIDC_CONFIG), []);
|
|
||||||
|
const userManager = useMemo(() => {
|
||||||
|
if (config && config.OIDC_CONFIG) {
|
||||||
|
return new UserManager(config.OIDC_CONFIG);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isLoading || error || !userManager) return;
|
||||||
|
|
||||||
userManager.getUser()
|
userManager.getUser()
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
if (user && !user.expired) {
|
if (user && !user.expired) {
|
||||||
@@ -62,19 +72,23 @@ const AuthProvider = ({ children }) => {
|
|||||||
userManager.events.removeUserLoaded(onUserLoaded);
|
userManager.events.removeUserLoaded(onUserLoaded);
|
||||||
userManager.events.removeUserUnloaded(onUserUnloaded);
|
userManager.events.removeUserUnloaded(onUserUnloaded);
|
||||||
};
|
};
|
||||||
}, [userManager]);
|
}, [userManager, isLoading, error, config]);
|
||||||
|
|
||||||
const login = () => {
|
const login = () => {
|
||||||
|
if (userManager) {
|
||||||
userManager
|
userManager
|
||||||
.signinRedirect()
|
.signinRedirect()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(config);
|
console.log(config);
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
|
if (userManager) {
|
||||||
userManager.signoutRedirect();
|
userManager.signoutRedirect();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
62
src/ConfigProvider.js
Normal file
62
src/ConfigProvider.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const ConfigContext = createContext({
|
||||||
|
config: {
|
||||||
|
BACKEND_HOST: null,
|
||||||
|
FRONTEND_HOST: null,
|
||||||
|
KC_CLIENT_ID: null,
|
||||||
|
OIDC_CONFIG: {},
|
||||||
|
},
|
||||||
|
isLoading: true,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useConfig = () => useContext(ConfigContext).config;
|
||||||
|
|
||||||
|
const ConfigProvider = ({ children }) => {
|
||||||
|
const [config, setConfig] = useState({
|
||||||
|
BACKEND_HOST: null,
|
||||||
|
FRONTEND_HOST: null,
|
||||||
|
KC_CLIENT_ID: null,
|
||||||
|
OIDC_CONFIG: {},
|
||||||
|
});
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/config.json')
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch config: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
console.log(data);
|
||||||
|
setConfig(data);
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error fetching config:", err);
|
||||||
|
setError(err);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading configuration...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>Error loading configuration: {error.message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigContext.Provider value={{ config, isLoading, error }}>
|
||||||
|
{children}
|
||||||
|
</ConfigContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConfigProvider;
|
||||||
97
src/components/Footer.css
Normal file
97
src/components/Footer.css
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
.footer {
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
padding: 1rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
bottom: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: visible;
|
||||||
|
max-height: 5rem;
|
||||||
|
transition: max-height 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer.expanded {
|
||||||
|
max-height: 20rem;
|
||||||
|
}
|
||||||
|
.footer-details{
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: opacity 0.3s ease, max-height 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-details.expanded {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
margin-bottom: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icon:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icons a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #00d1b2;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icons a:hover {
|
||||||
|
color: #00a6a2;
|
||||||
|
}
|
||||||
|
.toggle-button {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
|
transition: transform 0.2s ease, background-color 0.3s ease;
|
||||||
|
position: absolute;
|
||||||
|
top: -1rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
transform: scale(1.1) translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);
|
||||||
|
}
|
||||||
78
src/components/Footer.js
Normal file
78
src/components/Footer.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import React from "react";
|
||||||
|
import "./Footer.css";
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
|
const [isVisible, setIsVisible] = React.useState(false);
|
||||||
|
const toggleExpand = () => {
|
||||||
|
if(!isExpanded) {
|
||||||
|
setIsVisible(true);
|
||||||
|
}
|
||||||
|
setIsExpanded((prev) => !prev);
|
||||||
|
};
|
||||||
|
const onTransitionEnd = () => {
|
||||||
|
if(!isExpanded) {
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<footer className={`footer ${isExpanded ? "expanded" : ""}`}>
|
||||||
|
<button
|
||||||
|
className="toggle-button"
|
||||||
|
onClick={toggleExpand}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{isExpanded ? "↓" : "↑"}
|
||||||
|
</button>
|
||||||
|
<div className={`footer-content`}>
|
||||||
|
<p>© {new Date().getFullYear()} Hangman Lab. All rights reserved.</p>
|
||||||
|
{
|
||||||
|
isVisible && (
|
||||||
|
<div
|
||||||
|
className={`footer-details ${isExpanded ? "expanded" : ""}`}
|
||||||
|
onTransitionEnd={onTransitionEnd}
|
||||||
|
>
|
||||||
|
<div className="footer-icons">
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/zhhrozhh/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/icons/linkedin.png"
|
||||||
|
alt="LinkedIn"
|
||||||
|
className="footer-icon"
|
||||||
|
/>
|
||||||
|
LinkedIn
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://git.hangman-lab.top/hzhang/HangmanLab"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/icons/git.png"
|
||||||
|
alt="GitHub"
|
||||||
|
className="footer-icon"
|
||||||
|
/>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
<a href="mailto:hzhang@hangman-lab.top">
|
||||||
|
<img
|
||||||
|
src="/icons/email.png"
|
||||||
|
alt="Email"
|
||||||
|
className="footer-icon"
|
||||||
|
/>
|
||||||
|
Email
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, {useContext, useEffect} from "react";
|
||||||
import { UserManager } from "oidc-client-ts";
|
import { UserManager } from "oidc-client-ts";
|
||||||
import config from "./config";
|
import {ConfigContext} from "../../ConfigProvider";
|
||||||
|
|
||||||
|
|
||||||
const Callback = () => {
|
const Callback = () => {
|
||||||
|
const config = useContext(ConfigContext).config;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const userManager = new UserManager(config.OIDC_CONFIG);
|
const userManager = new UserManager(config.OIDC_CONFIG);
|
||||||
userManager.signinRedirectCallback()
|
userManager.signinRedirectCallback()
|
||||||
24
src/components/KeycloakCallbacks/PopupCallback.js
Normal file
24
src/components/KeycloakCallbacks/PopupCallback.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React, { useEffect, useContext } from "react";
|
||||||
|
import { UserManager } from "oidc-client-ts";
|
||||||
|
import {ConfigContext} from "../../ConfigProvider";
|
||||||
|
|
||||||
|
|
||||||
|
const PopupCallback = () => {
|
||||||
|
const { config } = useContext(ConfigContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const userManager = new UserManager(config.OIDC_CONFIG);
|
||||||
|
userManager.signinPopupCallback()
|
||||||
|
.then(() => {
|
||||||
|
window.close();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Popup callback error:", err);
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
return <div>Processing...</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PopupCallback;
|
||||||
21
src/components/KeycloakCallbacks/SilentCallback.js
Normal file
21
src/components/KeycloakCallbacks/SilentCallback.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React, { useEffect, useContext } from "react";
|
||||||
|
import { UserManager } from "oidc-client-ts";
|
||||||
|
import { ConfigContext } from "../../ConfigProvider";
|
||||||
|
|
||||||
|
const SilentCallback = () => {
|
||||||
|
const { config } = useContext(ConfigContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const userManager = new UserManager(config.OIDC_CONFIG);
|
||||||
|
userManager.signinSilentCallback()
|
||||||
|
.then(() => {
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Silent callback error:", err);
|
||||||
|
});
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
return <div>Renew...</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SilentCallback;
|
||||||
@@ -80,7 +80,6 @@
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Courier New', Courier, monospace;
|
||||||
background-color: #f4f4f4;
|
background-color: #f4f4f4;
|
||||||
|
|||||||
@@ -32,18 +32,18 @@ pre {
|
|||||||
|
|
||||||
.markdown-preview ul,
|
.markdown-preview ul,
|
||||||
.markdown-preview ol {
|
.markdown-preview ol {
|
||||||
padding-left: 1.5rem; /* 设置左侧缩进 */
|
padding-left: 1.5rem;
|
||||||
margin-bottom: 1rem; /* 每个列表的底部间距 */
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ul {
|
.markdown-preview ul {
|
||||||
list-style-type: disc; /* 确保无序列表使用圆点 */
|
list-style-type: disc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ol {
|
.markdown-preview ol {
|
||||||
list-style-type: decimal; /* 确保有序列表使用数字 */
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview li {
|
.markdown-preview li {
|
||||||
margin-bottom: 0.5rem; /* 列表项之间的间距 */
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
/* src/components/SideNavigation.css */
|
|
||||||
.menu {
|
.menu {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-label {
|
.menu-label {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import PermissionGuard from "../PermissionGuard";
|
|||||||
import PathNode from "./PathNode";
|
import PathNode from "./PathNode";
|
||||||
import "./SideNavigation.css";
|
import "./SideNavigation.css";
|
||||||
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/path-queries";
|
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/path-queries";
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
const SideNavigation = () => {
|
const SideNavigation = () => {
|
||||||
|
|
||||||
const {data: paths, isLoading, error } = usePaths(1);
|
const {data: paths, isLoading, error } = usePaths(1);
|
||||||
const deletePath = useDeletePath();
|
const deletePath = useDeletePath();
|
||||||
const updatePath = useUpdatePath();
|
const updatePath = useUpdatePath();
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// src/components/PathManager.js
|
|
||||||
|
|
||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, {useEffect, useState, useRef, useContext} from "react";
|
||||||
import { useCreatePath, usePaths } from "../utils/path-queries";
|
import { useCreatePath, usePaths } from "../utils/path-queries";
|
||||||
import { useQueryClient } from "react-query";
|
import { useQueryClient } from "react-query";
|
||||||
import "./PathManager.css";
|
import "./PathManager.css";
|
||||||
import {fetch_} from "../utils/request-utils";
|
import {fetch_} from "../utils/request-utils";
|
||||||
import config from "../config";
|
import {ConfigContext} from "../ConfigProvider";
|
||||||
|
|
||||||
const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
||||||
const [currentPath, setCurrentPath] = useState([{ name: "Root", id: 1 }]);
|
const [currentPath, setCurrentPath] = useState([{ name: "Root", id: 1 }]);
|
||||||
@@ -17,6 +16,7 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: subPaths, isLoading: isSubPathsLoading, error: subPathsError } = usePaths(currentPathId);
|
const { data: subPaths, isLoading: isSubPathsLoading, error: subPathsError } = usePaths(currentPathId);
|
||||||
const createPath = useCreatePath();
|
const createPath = useCreatePath();
|
||||||
|
const config = useContext(ConfigContext).config;
|
||||||
|
|
||||||
const buildPath = async (pathId) => {
|
const buildPath = async (pathId) => {
|
||||||
const path = [];
|
const path = [];
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
const config = {
|
|
||||||
BACKEND_HOST: null,
|
|
||||||
FRONTEND_HOST: null,
|
|
||||||
KC_CLIENT_ID: null,
|
|
||||||
OIDC_CONFIG: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -4,6 +4,7 @@ import App from "./App";
|
|||||||
import AuthProvider, {AuthContext} from "./AuthProvider";
|
import AuthProvider, {AuthContext} from "./AuthProvider";
|
||||||
import "bulma/css/bulma.min.css";
|
import "bulma/css/bulma.min.css";
|
||||||
import {QueryClient, QueryClientProvider} from "react-query"
|
import {QueryClient, QueryClientProvider} from "react-query"
|
||||||
|
import ConfigProvider from "./ConfigProvider";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -46,6 +47,7 @@ const EnhancedAuthProvider = ({children}) => {
|
|||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
root.render(
|
root.render(
|
||||||
|
<ConfigProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<EnhancedAuthProvider>
|
<EnhancedAuthProvider>
|
||||||
@@ -53,4 +55,5 @@ root.render(
|
|||||||
</EnhancedAuthProvider>
|
</EnhancedAuthProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import {useQuery, useMutation, useQueryClient} from 'react-query';
|
import {useQuery, useMutation, useQueryClient} from 'react-query';
|
||||||
import {fetch_} from "./request-utils";
|
import {fetch_} from "./request-utils";
|
||||||
import config from "../config";
|
import {useConfig} from "../ConfigProvider";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const useMarkdown = (id) => {
|
export const useMarkdown = (id) => {
|
||||||
|
const config = useConfig();
|
||||||
return useQuery(
|
return useQuery(
|
||||||
["markdown", id],
|
["markdown", id],
|
||||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/${id}`),
|
() => fetch_(`${config.BACKEND_HOST}/api/markdown/${id}`),
|
||||||
@@ -13,6 +16,7 @@ export const useMarkdown = (id) => {
|
|||||||
|
|
||||||
export const useIndexMarkdown = (path_id) => {
|
export const useIndexMarkdown = (path_id) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
return useQuery(
|
return useQuery(
|
||||||
["index_markdown", path_id],
|
["index_markdown", path_id],
|
||||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/get_index/${path_id}`),{
|
() => fetch_(`${config.BACKEND_HOST}/api/markdown/get_index/${path_id}`),{
|
||||||
@@ -26,6 +30,7 @@ export const useIndexMarkdown = (path_id) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useMarkdownsByPath = (pathId) => {
|
export const useMarkdownsByPath = (pathId) => {
|
||||||
|
const config = useConfig();
|
||||||
return useQuery(
|
return useQuery(
|
||||||
["markdownsByPath", pathId],
|
["markdownsByPath", pathId],
|
||||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/by_path/${pathId}`),
|
() => fetch_(`${config.BACKEND_HOST}/api/markdown/by_path/${pathId}`),
|
||||||
@@ -36,6 +41,7 @@ export const useMarkdownsByPath = (pathId) => {
|
|||||||
|
|
||||||
export const useSaveMarkdown = () => {
|
export const useSaveMarkdown = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
return useMutation(({id, data}) => {
|
return useMutation(({id, data}) => {
|
||||||
const url = id
|
const url = id
|
||||||
? `${config.BACKEND_HOST}/api/markdown/${id}`
|
? `${config.BACKEND_HOST}/api/markdown/${id}`
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from "react-query";
|
import { useQuery, useMutation, useQueryClient } from "react-query";
|
||||||
import { fetch_ } from "./request-utils";
|
import { fetch_ } from "./request-utils";
|
||||||
import config from "../config";
|
import {useConfig} from "../ConfigProvider";
|
||||||
|
|
||||||
|
|
||||||
export const usePaths = (parent_id) => {
|
export const usePaths = (parent_id) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
return useQuery(
|
return useQuery(
|
||||||
["paths", parent_id],
|
["paths", parent_id],
|
||||||
() => fetch_(`${config.BACKEND_HOST}/api/path/parent/${parent_id}`),
|
() => fetch_(`${config.BACKEND_HOST}/api/path/parent/${parent_id}`),
|
||||||
@@ -24,6 +25,7 @@ export const usePaths = (parent_id) => {
|
|||||||
|
|
||||||
|
|
||||||
export const usePath = (id) => {
|
export const usePath = (id) => {
|
||||||
|
const config = useConfig();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const cachedData = queryClient.getQueryData(["path", id]);
|
const cachedData = queryClient.getQueryData(["path", id]);
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ export const usePath = (id) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useCreatePath = () => {
|
export const useCreatePath = () => {
|
||||||
|
const config = useConfig();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
@@ -58,6 +61,7 @@ export const useCreatePath = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdatePath = () => {
|
export const useUpdatePath = () => {
|
||||||
|
const config = useConfig();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
@@ -75,6 +79,7 @@ export const useUpdatePath = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useDeletePath = () => {
|
export const useDeletePath = () => {
|
||||||
|
const config = useConfig();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
//webpack.config.js
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const Dotenv = require('dotenv-webpack');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: './src/index.js',
|
entry: './src/index.js',
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, './dist'),
|
path: path.resolve(__dirname, './dist'),
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
publicPath: '/',
|
publicPath: './',
|
||||||
clean: true,
|
clean: true,
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
@@ -28,6 +28,7 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: "./public/index.html",
|
template: "./public/index.html",
|
||||||
|
inject: true
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
devServer: {
|
devServer: {
|
||||||
|
|||||||
Reference in New Issue
Block a user