diff --git a/src/components/Markdowns/StandaloneMarkdownPage.js b/src/components/Markdowns/StandaloneMarkdownPage.js
new file mode 100644
index 0000000..fa21520
--- /dev/null
+++ b/src/components/Markdowns/StandaloneMarkdownPage.js
@@ -0,0 +1,100 @@
+import React, { useEffect, useState } from "react";
+import { useLocation } from "react-router-dom";
+import "katex/dist/katex.min.css";
+import "./MarkdownContent.css";
+import MarkdownView from "./MarkdownView";
+import { useMarkdown } from "../../utils/queries/markdown-queries";
+import { useMarkdownSetting } from "../../utils/queries/markdown-setting-queries";
+import { useMarkdownTemplate } from "../../utils/queries/markdown-template-queries";
+import { useMarkdownTemplateSetting } from "../../utils/queries/markdown-template-setting-queries";
+import { useTree } from "../../utils/queries/tree-queries";
+import { getMarkdownIdByPath } from "../../utils/pathUtils";
+
+const StandaloneMarkdownPage = () => {
+ const location = useLocation();
+ const [indexTitle, setIndexTitle] = useState(null);
+ const [markdownId, setMarkdownId] = useState(null);
+
+ // Extract path from /pg/project/index -> project/index
+ const pathString = location.pathname.replace(/^\/pg\//, '');
+
+ const { data: tree, isLoading: isTreeLoading } = useTree();
+ const { data: markdown, isLoading: isMarkdownLoading, error } = useMarkdown(markdownId);
+ const { data: setting, isFetching: isSettingFetching } = useMarkdownSetting(markdown?.setting_id);
+ const { data: templateSetting, isFetching: isTemplateSettingFetching } = useMarkdownTemplateSetting(setting?.template_setting_id);
+ const { data: template, isFetching: isTemplateFetching } = useMarkdownTemplate(templateSetting?.template_id);
+
+ // Resolve markdown ID from path using tree
+ useEffect(() => {
+ if (tree && pathString) {
+ const resolvedId = getMarkdownIdByPath(tree, pathString);
+ setMarkdownId(resolvedId);
+ }
+ }, [tree, pathString]);
+
+ useEffect(() => {
+ if (markdown && markdown.title === "index" && pathString) {
+ const pathParts = pathString.split('/').filter(part => part.length > 0);
+
+ if (pathParts.length === 0) {
+ // Root index: /pg/ or /pg
+ setIndexTitle("Home");
+ } else {
+ // Directory index: /pg/Projects or /pg/Projects/project1
+ // Use the last directory name as title
+ const directoryName = pathParts[pathParts.length - 1];
+ setIndexTitle(directoryName);
+ }
+ }
+ }, [markdown, pathString]);
+
+ const notReady = isTreeLoading || isMarkdownLoading || isSettingFetching || isTemplateSettingFetching || isTemplateFetching;
+
+ if (notReady) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
Error: {error.message || "Failed to load content"}
+
+ );
+ }
+
+ if (!notReady && !markdownId) {
+ return (
+
+
Markdown not found for path: {pathString}
+
+ );
+ }
+
+ if (markdown?.isMessage) {
+ return (
+
+
+
{markdown.title}
+
{markdown.content}
+
+
+ );
+ }
+
+ return (
+
+
+
{markdown?.title === "index" ? indexTitle : markdown?.title}
+
+ {markdown && (
+
+ )}
+
+ );
+};
+
+export default StandaloneMarkdownPage;
\ No newline at end of file
diff --git a/src/components/Navigations/MarkdownNode.js b/src/components/Navigations/MarkdownNode.js
index c9dd388..ef49bb4 100644
--- a/src/components/Navigations/MarkdownNode.js
+++ b/src/components/Navigations/MarkdownNode.js
@@ -1,9 +1,30 @@
-import {Link} from "react-router-dom";
+import {Link, useNavigate} from "react-router-dom";
import PermissionGuard from "../PermissionGuard";
import React, {useState} from "react";
import MarkdownSettingModal from "../Modals/MarkdownSettingModal";
+import {useDeleteMarkdown} from "../../utils/queries/markdown-queries";
+import {useDeleteMarkdownSetting} from "../../utils/queries/markdown-setting-queries";
const MarkdownNode = ({markdown, handleMoveMarkdown}) => {
const [isMarkdownSettingModalOpen, setIsMarkdownSettingModalOpen] = useState(false);
+ const navigate = useNavigate();
+ const deleteMarkdown = useDeleteMarkdown();
+ const deleteMarkdownSetting = useDeleteMarkdownSetting();
+
+ const handleDeleteMarkdown = async () => {
+ if (!window.confirm(`delete markdown "${markdown.title}" ? this action cannot be undone.`)) {
+ return;
+ }
+
+ try {
+ await deleteMarkdown.mutateAsync(markdown.id);
+
+ if (window.location.pathname === `/markdown/${markdown.id}`) {
+ navigate('/');
+ }
+ } catch (error) {
+ alert('failed: ' + (error.message || 'unknown error'));
+ }
+ };
return (
@@ -24,6 +45,18 @@ const MarkdownNode = ({markdown, handleMoveMarkdown}) => {
+
+
+
{
+ if (!tree || !pathString) return null;
+
+ const pathSegments = pathString.split('/').filter(segment => segment.length > 0);
+
+ if (pathSegments.length === 0) {
+ const rootIndex = tree.children?.find(
+ child => child.type === 'markdown' && child.title === 'index'
+ );
+ return rootIndex || null;
+ }
+ let currentNode = tree;
+
+ for (let i = 0; i < pathSegments.length; i++) {
+ const segment = pathSegments[i];
+
+ const childPath = currentNode.children?.find(
+ child => child.type === 'path' && child.name === segment
+ );
+
+ if (!childPath) {
+ if (i === pathSegments.length - 1) {
+ const markdownNode = currentNode.children?.find(
+ child => child.type === 'markdown' && child.title === segment
+ );
+ return markdownNode || null;
+ }
+ return null;
+ }
+
+ currentNode = childPath;
+ }
+
+
+ const indexMarkdown = currentNode.children?.find(
+ child => child.type === 'markdown' && child.title === 'index'
+ );
+
+ return indexMarkdown || null;
+};
+
+export const getMarkdownIdByPath = (tree, pathString) => {
+ const markdownNode = findMarkdownByPath(tree, pathString);
+ return markdownNode?.id || null;
+};
\ No newline at end of file
diff --git a/src/utils/queries/markdown-queries.js b/src/utils/queries/markdown-queries.js
index d9f5552..2a1a4bf 100644
--- a/src/utils/queries/markdown-queries.js
+++ b/src/utils/queries/markdown-queries.js
@@ -94,6 +94,24 @@ export const useMoveMarkdown = () => {
});
};
+export const useDeleteMarkdown = () => {
+ const queryClient = useQueryClient();
+ const config = useConfig();
+
+ return useMutation({
+ mutationFn: (markdownId) => {
+ return fetch_(`${config.BACKEND_HOST}/api/markdown/${markdownId}`, {
+ method: "DELETE"
+ });
+ },
+ onSuccess: (data, markdownId) => {
+ queryClient.invalidateQueries({queryKey: ["markdown", markdownId]});
+ queryClient.invalidateQueries({queryKey: ["tree"]});
+ queryClient.invalidateQueries({queryKey: ["markdownsByPath"]});
+ }
+ });
+};
+
export const useSearchMarkdown = (keyword) => {
const config = useConfig();
return useQuery({