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": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bulma": "^1.0.2",
|
||||
"katex": "^0.16.11",
|
||||
"oidc-client-ts": "^3.1.0",
|
||||
"react": "^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": {
|
||||
"@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 {appConfig} from "./confs/appConfig";
|
||||
|
||||
@@ -13,7 +13,11 @@ export const AuthContext = createContext({
|
||||
const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [roles, setRoles] = useState([]);
|
||||
const userManager = new UserManager(appConfig.oidcConfig);
|
||||
const userManager =
|
||||
useMemo(() => new UserManager(appConfig.oidcConfig), []);
|
||||
|
||||
|
||||
//new UserManager(appConfig.oidcConfig);
|
||||
|
||||
useEffect(() => {
|
||||
userManager.getUser()
|
||||
@@ -24,7 +28,8 @@ const AuthProvider = ({ children }) => {
|
||||
const clientRoles = user?.profile?.resource_access?.[appConfig.kc_client_id]?.roles || [];
|
||||
setRoles(clientRoles);
|
||||
} else if (user && user.expired) {
|
||||
userManager.signinSilent()
|
||||
userManager
|
||||
.signinSilent()
|
||||
.then((newUser) => {
|
||||
setUser(newUser);
|
||||
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 {AuthContext} from "../../AuthProvider";
|
||||
import {useNavigate, useParams} from "react-router-dom";
|
||||
import {fetchWithCache} from "../../utils/fetchWithCache";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { AuthContext } from "../../AuthProvider";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
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 {roles} = useContext(AuthContext);
|
||||
const { roles } = useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
const {id} = useParams()
|
||||
const { id } = useParams();
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [path, setPath] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if(id){
|
||||
if (id) {
|
||||
fetchWithCache(`/api/markdown/${id}`)
|
||||
.then((data) => {
|
||||
setTitle(data.title);
|
||||
setContent(data.content);
|
||||
setPath(data.path);
|
||||
})
|
||||
.catch((err) => {console.error("failed to load markdown", err)});
|
||||
.catch((err) => {
|
||||
console.error("failed to load markdown", err);
|
||||
});
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const handleSave = () => {
|
||||
const url = id ? `/api/markdown/${id}` : "/api/markdown";
|
||||
const method = id ? "PUT" : "POST";
|
||||
fetch (url, {
|
||||
fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
|
||||
},
|
||||
body: JSON.stringify({title, content, path}),
|
||||
body: JSON.stringify({ title, content, path }),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
navigate("/");
|
||||
}else {
|
||||
} else {
|
||||
return res.json().then((data) => {
|
||||
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");
|
||||
if(! hasPermission)
|
||||
return (
|
||||
<div> Can not crete(Permission Denied)</div>
|
||||
);
|
||||
if (!hasPermission)
|
||||
return <div className="notification is-danger">Permission Denied</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{id ? "Edit Markdown" : "Create Markdown"}</h2>
|
||||
<form>
|
||||
<div>
|
||||
<label>
|
||||
Title:
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<div className="container mt-5 markdown-editor-container">
|
||||
<h2 className="title is-4">{id ? "Edit Markdown" : "Create Markdown"}</h2>
|
||||
<div className="columns">
|
||||
{/* Editor Column */}
|
||||
<div className="column is-half">
|
||||
<form>
|
||||
{/* Title Field */}
|
||||
<div className="field">
|
||||
<label className="label">Title</label>
|
||||
<div className="control">
|
||||
<input
|
||||
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>
|
||||
<label>
|
||||
Path:
|
||||
<input
|
||||
type="text"
|
||||
value={path}
|
||||
onChange={(e) => setPath(e.target.value)}
|
||||
|
||||
{/* Preview Column */}
|
||||
<div className="column is-half">
|
||||
<h3 className="subtitle is-5">Preview</h3>
|
||||
<div className="content markdown-preview">
|
||||
<ReactMarkdown
|
||||
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>
|
||||
<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;
|
||||
@@ -3,6 +3,9 @@ import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import AuthProvider from "./AuthProvider";
|
||||
import {config} from "./confs/appConfig";
|
||||
import "bulma/css/bulma.min.css";
|
||||
|
||||
|
||||
|
||||
//ReactDOM.render(<App />, document.getElementById("root"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user