fix mem leak & ui / preview for editor
This commit is contained in:
1783
package-lock.json
generated
1783
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,10 +12,17 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bulma": "^1.0.2",
|
||||||
|
"katex": "^0.16.11",
|
||||||
"oidc-client-ts": "^3.1.0",
|
"oidc-client-ts": "^3.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^7.0.1"
|
"react-markdown": "^9.0.1",
|
||||||
|
"react-router-dom": "^7.0.1",
|
||||||
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
|
"remark-math": "^6.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, useEffect, useState } from "react";
|
import React, {createContext, useEffect, useMemo, useState} from "react";
|
||||||
import { UserManager } from "oidc-client-ts";
|
import { UserManager } from "oidc-client-ts";
|
||||||
import {appConfig} from "./confs/appConfig";
|
import {appConfig} from "./confs/appConfig";
|
||||||
|
|
||||||
@@ -13,7 +13,11 @@ export const AuthContext = createContext({
|
|||||||
const AuthProvider = ({ children }) => {
|
const AuthProvider = ({ children }) => {
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [roles, setRoles] = useState([]);
|
const [roles, setRoles] = useState([]);
|
||||||
const userManager = new UserManager(appConfig.oidcConfig);
|
const userManager =
|
||||||
|
useMemo(() => new UserManager(appConfig.oidcConfig), []);
|
||||||
|
|
||||||
|
|
||||||
|
//new UserManager(appConfig.oidcConfig);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
userManager.getUser()
|
userManager.getUser()
|
||||||
@@ -24,7 +28,8 @@ const AuthProvider = ({ children }) => {
|
|||||||
const clientRoles = user?.profile?.resource_access?.[appConfig.kc_client_id]?.roles || [];
|
const clientRoles = user?.profile?.resource_access?.[appConfig.kc_client_id]?.roles || [];
|
||||||
setRoles(clientRoles);
|
setRoles(clientRoles);
|
||||||
} else if (user && user.expired) {
|
} else if (user && user.expired) {
|
||||||
userManager.signinSilent()
|
userManager
|
||||||
|
.signinSilent()
|
||||||
.then((newUser) => {
|
.then((newUser) => {
|
||||||
setUser(newUser);
|
setUser(newUser);
|
||||||
localStorage.setItem("accessToken", newUser.access_token);
|
localStorage.setItem("accessToken", newUser.access_token);
|
||||||
|
|||||||
102
src/components/Markdowns/MarkdownEditor.css
Normal file
102
src/components/Markdowns/MarkdownEditor.css
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
|
||||||
|
.markdown-editor-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.markdown-preview {
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-editor-header {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #363636;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.markdown-editor-form .field {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.markdown-editor-form .label {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.markdown-editor-form .input,
|
||||||
|
.markdown-editor-form .textarea {
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #dcdcdc;
|
||||||
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-editor-form .input:focus,
|
||||||
|
.markdown-editor-form .textarea:focus {
|
||||||
|
border-color: #3273dc;
|
||||||
|
box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.markdown-editor-form .button {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-editor-form .button:hover {
|
||||||
|
background-color: #276cda;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.markdown-editor-notification {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.katex-display {
|
||||||
|
margin: 1em 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.katex {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #f8f8f2;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
@@ -1,97 +1,163 @@
|
|||||||
import React, {useContext, useEffect, useState} from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import {AuthContext} from "../../AuthProvider";
|
import { AuthContext } from "../../AuthProvider";
|
||||||
import {useNavigate, useParams} from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import {fetchWithCache} from "../../utils/fetchWithCache";
|
import { fetchWithCache } from "../../utils/fetchWithCache";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
|
import rehypeRaw from "rehype-raw";
|
||||||
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import { okaidia } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||||
|
import "katex/dist/katex.min.css"; // LaTeX 样式
|
||||||
|
import "./MarkdownEditor.css";
|
||||||
|
|
||||||
const MarkdownEditor = () => {
|
const MarkdownEditor = () => {
|
||||||
const {roles} = useContext(AuthContext);
|
const { roles } = useContext(AuthContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {id} = useParams()
|
const { id } = useParams();
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [path, setPath] = useState("");
|
const [path, setPath] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(id){
|
if (id) {
|
||||||
fetchWithCache(`/api/markdown/${id}`)
|
fetchWithCache(`/api/markdown/${id}`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setTitle(data.title);
|
setTitle(data.title);
|
||||||
setContent(data.content);
|
setContent(data.content);
|
||||||
setPath(data.path);
|
setPath(data.path);
|
||||||
})
|
})
|
||||||
.catch((err) => {console.error("failed to load markdown", err)});
|
.catch((err) => {
|
||||||
|
console.error("failed to load markdown", err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
const url = id ? `/api/markdown/${id}` : "/api/markdown";
|
const url = id ? `/api/markdown/${id}` : "/api/markdown";
|
||||||
const method = id ? "PUT" : "POST";
|
const method = id ? "PUT" : "POST";
|
||||||
fetch (url, {
|
fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
|
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({title, content, path}),
|
body: JSON.stringify({ title, content, path }),
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}else {
|
} else {
|
||||||
return res.json().then((data) => {
|
return res.json().then((data) => {
|
||||||
throw new Error(data.error || "Failed to save markdown");
|
throw new Error(data.error || "Failed to save markdown");
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {console.error("failed to load markdown", err)});
|
.catch((err) => {
|
||||||
|
console.error("failed to load markdown", err);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasPermission = roles.includes("admin") || roles.includes("creator");
|
const hasPermission = roles.includes("admin") || roles.includes("creator");
|
||||||
if(! hasPermission)
|
if (!hasPermission)
|
||||||
return (
|
return <div className="notification is-danger">Permission Denied</div>;
|
||||||
<div> Can not crete(Permission Denied)</div>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="container mt-5 markdown-editor-container">
|
||||||
<h2>{id ? "Edit Markdown" : "Create Markdown"}</h2>
|
<h2 className="title is-4">{id ? "Edit Markdown" : "Create Markdown"}</h2>
|
||||||
<form>
|
<div className="columns">
|
||||||
<div>
|
{/* Editor Column */}
|
||||||
<label>
|
<div className="column is-half">
|
||||||
Title:
|
<form>
|
||||||
<input
|
{/* Title Field */}
|
||||||
type="text"
|
<div className="field">
|
||||||
value={title}
|
<label className="label">Title</label>
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
<div className="control">
|
||||||
/>
|
<input
|
||||||
</label>
|
className="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Path Field */}
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Path</label>
|
||||||
|
<div className="control">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter path"
|
||||||
|
value={path}
|
||||||
|
onChange={(e) => setPath(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Field */}
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Content</label>
|
||||||
|
<div className="control">
|
||||||
|
<textarea
|
||||||
|
className="textarea"
|
||||||
|
placeholder="Enter Markdown content"
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<div className="field">
|
||||||
|
<div className="control">
|
||||||
|
<button
|
||||||
|
className="button is-primary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label>
|
{/* Preview Column */}
|
||||||
Path:
|
<div className="column is-half">
|
||||||
<input
|
<h3 className="subtitle is-5">Preview</h3>
|
||||||
type="text"
|
<div className="content markdown-preview">
|
||||||
value={path}
|
<ReactMarkdown
|
||||||
onChange={(e) => setPath(e.target.value)}
|
children={content}
|
||||||
|
remarkPlugins={[remarkMath]}
|
||||||
|
rehypePlugins={[rehypeKatex, rehypeRaw]}
|
||||||
|
components={{
|
||||||
|
code({ node, inline, className, children, ...props }) {
|
||||||
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
return !inline && match ? (
|
||||||
|
<SyntaxHighlighter
|
||||||
|
style={okaidia}
|
||||||
|
language={match[1]}
|
||||||
|
PreTag="div"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{String(children).replace(/\n$/, "")}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
) : (
|
||||||
|
<code className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<label>
|
|
||||||
Content:
|
|
||||||
<textarea
|
|
||||||
value={content}
|
|
||||||
onChange={(e) => setContent(e.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button type="button" onClick={handleSave}>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MarkdownEditor;
|
export default MarkdownEditor;
|
||||||
@@ -3,6 +3,9 @@ import ReactDOM from "react-dom/client";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
import AuthProvider from "./AuthProvider";
|
import AuthProvider from "./AuthProvider";
|
||||||
import {config} from "./confs/appConfig";
|
import {config} from "./confs/appConfig";
|
||||||
|
import "bulma/css/bulma.min.css";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//ReactDOM.render(<App />, document.getElementById("root"));
|
//ReactDOM.render(<App />, document.getElementById("root"));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user