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 ( +
+
Loading...
+
+ ); + } + + 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({