improve: add production stage
This commit is contained in:
@@ -7,10 +7,11 @@
|
||||
.content-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
padding: 1rem 1rem 100px 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
16
src/App.js
16
src/App.js
@@ -5,11 +5,12 @@ import SideNavigation from "./components/Navigations/SideNavigation";
|
||||
import MarkdownContent from "./components/Markdowns/MarkdownContent";
|
||||
import MarkdownEditor from "./components/Markdowns/MarkdownEditor";
|
||||
import "./App.css";
|
||||
import Callback from "./Callback";
|
||||
import config from "./config";
|
||||
import Callback from "./components/KeycloakCallbacks/Callback";
|
||||
import Footer from "./components/Footer";
|
||||
import PopupCallback from "./components/KeycloakCallbacks/PopupCallback";
|
||||
import SilentCallback from "./components/KeycloakCallbacks/SilentCallback";
|
||||
|
||||
const App = () => {
|
||||
console.log(config)
|
||||
return (
|
||||
<Router>
|
||||
<div className="app-container">
|
||||
@@ -22,14 +23,17 @@ const App = () => {
|
||||
<Route path="/markdown/:id" element={<MarkdownContent />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route path="/test" element={<h1>TEST</h1>}></Route>
|
||||
<Route path="/markdown/create" element={<MarkdownEditor />}></Route>
|
||||
<Route path="/markdown/edit/:id" element={<MarkdownEditor />}></Route>
|
||||
<Route path="/markdown/create" element={<MarkdownEditor />} />
|
||||
<Route path="/markdown/edit/:id" element={<MarkdownEditor />} />
|
||||
<Route path="/popup_callback" element={<PopupCallback />} />
|
||||
<Route path="silent_callback" element={<SilentCallback />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
||||
@@ -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 config from "./config";
|
||||
import { ConfigContext } from "./ConfigProvider";
|
||||
|
||||
export const AuthContext = createContext({
|
||||
user: null,
|
||||
@@ -10,11 +11,20 @@ export const AuthContext = createContext({
|
||||
});
|
||||
|
||||
const AuthProvider = ({ children }) => {
|
||||
const { config, isLoading, error } = useContext(ConfigContext);
|
||||
const [user, setUser] = useState(null);
|
||||
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(() => {
|
||||
if (isLoading || error || !userManager) return;
|
||||
|
||||
userManager.getUser()
|
||||
.then((user) => {
|
||||
if (user && !user.expired) {
|
||||
@@ -62,19 +72,23 @@ const AuthProvider = ({ children }) => {
|
||||
userManager.events.removeUserLoaded(onUserLoaded);
|
||||
userManager.events.removeUserUnloaded(onUserUnloaded);
|
||||
};
|
||||
}, [userManager]);
|
||||
}, [userManager, isLoading, error, config]);
|
||||
|
||||
const login = () => {
|
||||
userManager
|
||||
.signinRedirect()
|
||||
.catch((err) => {
|
||||
console.log(config);
|
||||
console.log(err);
|
||||
});
|
||||
if (userManager) {
|
||||
userManager
|
||||
.signinRedirect()
|
||||
.catch((err) => {
|
||||
console.log(config);
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
userManager.signoutRedirect();
|
||||
if (userManager) {
|
||||
userManager.signoutRedirect();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -84,4 +98,4 @@ const AuthProvider = ({ children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthProvider;
|
||||
export default AuthProvider;
|
||||
|
||||
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 config from "./config";
|
||||
import {ConfigContext} from "../../ConfigProvider";
|
||||
|
||||
|
||||
const Callback = () => {
|
||||
const config = useContext(ConfigContext).config;
|
||||
useEffect(() => {
|
||||
const userManager = new UserManager(config.OIDC_CONFIG);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background-color: #f4f4f4;
|
||||
|
||||
@@ -32,18 +32,18 @@ pre {
|
||||
|
||||
.markdown-preview ul,
|
||||
.markdown-preview ol {
|
||||
padding-left: 1.5rem; /* 设置左侧缩进 */
|
||||
margin-bottom: 1rem; /* 每个列表的底部间距 */
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.markdown-preview ul {
|
||||
list-style-type: disc; /* 确保无序列表使用圆点 */
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.markdown-preview ol {
|
||||
list-style-type: decimal; /* 确保有序列表使用数字 */
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.markdown-preview li {
|
||||
margin-bottom: 0.5rem; /* 列表项之间的间距 */
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
/* src/components/SideNavigation.css */
|
||||
.menu {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background-color: #f9f9f9;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
|
||||
@@ -2,10 +2,10 @@ import PermissionGuard from "../PermissionGuard";
|
||||
import PathNode from "./PathNode";
|
||||
import "./SideNavigation.css";
|
||||
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/path-queries";
|
||||
import React from 'react';
|
||||
|
||||
const SideNavigation = () => {
|
||||
|
||||
const {data: paths, isLoading, error} = usePaths(1);
|
||||
const {data: paths, isLoading, error } = usePaths(1);
|
||||
const deletePath = useDeletePath();
|
||||
const updatePath = useUpdatePath();
|
||||
|
||||
@@ -20,7 +20,7 @@ const SideNavigation = () => {
|
||||
};
|
||||
|
||||
const handleSave = (id, newName) => {
|
||||
updatePath.mutate({ id, data: {name: newName}} , {
|
||||
updatePath.mutate({ id, data: {name: newName }} , {
|
||||
onError: (err) => {
|
||||
alert("Failed to update path");
|
||||
}
|
||||
|
||||
@@ -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 { useQueryClient } from "react-query";
|
||||
import "./PathManager.css";
|
||||
import {fetch_} from "../utils/request-utils";
|
||||
import config from "../config";
|
||||
import {ConfigContext} from "../ConfigProvider";
|
||||
|
||||
const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
||||
const [currentPath, setCurrentPath] = useState([{ name: "Root", id: 1 }]);
|
||||
@@ -17,6 +16,7 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: subPaths, isLoading: isSubPathsLoading, error: subPathsError } = usePaths(currentPathId);
|
||||
const createPath = useCreatePath();
|
||||
const config = useContext(ConfigContext).config;
|
||||
|
||||
const buildPath = async (pathId) => {
|
||||
const path = [];
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const config = {
|
||||
BACKEND_HOST: null,
|
||||
FRONTEND_HOST: null,
|
||||
KC_CLIENT_ID: null,
|
||||
OIDC_CONFIG: {},
|
||||
}
|
||||
|
||||
export default config;
|
||||
17
src/index.js
17
src/index.js
@@ -4,6 +4,7 @@ import App from "./App";
|
||||
import AuthProvider, {AuthContext} from "./AuthProvider";
|
||||
import "bulma/css/bulma.min.css";
|
||||
import {QueryClient, QueryClientProvider} from "react-query"
|
||||
import ConfigProvider from "./ConfigProvider";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -46,11 +47,13 @@ const EnhancedAuthProvider = ({children}) => {
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<EnhancedAuthProvider>
|
||||
<App />
|
||||
</EnhancedAuthProvider>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
<ConfigProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<EnhancedAuthProvider>
|
||||
<App />
|
||||
</EnhancedAuthProvider>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {useQuery, useMutation, useQueryClient} from 'react-query';
|
||||
import {fetch_} from "./request-utils";
|
||||
import config from "../config";
|
||||
import {useConfig} from "../ConfigProvider";
|
||||
|
||||
|
||||
|
||||
export const useMarkdown = (id) => {
|
||||
const config = useConfig();
|
||||
return useQuery(
|
||||
["markdown", id],
|
||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/${id}`),
|
||||
@@ -13,6 +16,7 @@ export const useMarkdown = (id) => {
|
||||
|
||||
export const useIndexMarkdown = (path_id) => {
|
||||
const queryClient = useQueryClient();
|
||||
const config = useConfig();
|
||||
return useQuery(
|
||||
["index_markdown", 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) => {
|
||||
const config = useConfig();
|
||||
return useQuery(
|
||||
["markdownsByPath", pathId],
|
||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/by_path/${pathId}`),
|
||||
@@ -36,6 +41,7 @@ export const useMarkdownsByPath = (pathId) => {
|
||||
|
||||
export const useSaveMarkdown = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const config = useConfig();
|
||||
return useMutation(({id, data}) => {
|
||||
const url = id
|
||||
? `${config.BACKEND_HOST}/api/markdown/${id}`
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "react-query";
|
||||
import { fetch_ } from "./request-utils";
|
||||
import config from "../config";
|
||||
import {useConfig} from "../ConfigProvider";
|
||||
|
||||
|
||||
export const usePaths = (parent_id) => {
|
||||
const queryClient = useQueryClient();
|
||||
const config = useConfig();
|
||||
return useQuery(
|
||||
["paths", parent_id],
|
||||
() => fetch_(`${config.BACKEND_HOST}/api/path/parent/${parent_id}`),
|
||||
@@ -24,6 +25,7 @@ export const usePaths = (parent_id) => {
|
||||
|
||||
|
||||
export const usePath = (id) => {
|
||||
const config = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
const cachedData = queryClient.getQueryData(["path", id]);
|
||||
|
||||
@@ -41,6 +43,7 @@ export const usePath = (id) => {
|
||||
};
|
||||
|
||||
export const useCreatePath = () => {
|
||||
const config = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
@@ -58,6 +61,7 @@ export const useCreatePath = () => {
|
||||
};
|
||||
|
||||
export const useUpdatePath = () => {
|
||||
const config = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
@@ -75,6 +79,7 @@ export const useUpdatePath = () => {
|
||||
};
|
||||
|
||||
export const useDeletePath = () => {
|
||||
const config = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
|
||||
Reference in New Issue
Block a user