diff --git a/app/src/components/editor/ContentView.jsx b/app/src/components/editor/ContentView.tsx similarity index 54% rename from app/src/components/editor/ContentView.jsx rename to app/src/components/editor/ContentView.tsx index 5235339..4d43b14 100644 --- a/app/src/components/editor/ContentView.jsx +++ b/app/src/components/editor/ContentView.tsx @@ -2,10 +2,21 @@ import React from 'react'; import { Text, Center } from '@mantine/core'; import Editor from './Editor'; import MarkdownPreview from './MarkdownPreview'; -import { getFileUrl } from '../../api/git'; -import { isImageFile } from '../../utils/fileHelpers'; +import { getFileUrl, isImageFile } from '../../utils/fileHelpers'; +import { useWorkspace } from '@/contexts/WorkspaceContext'; -const ContentView = ({ +type ViewTab = 'source' | 'preview'; + +interface ContentViewProps { + activeTab: ViewTab; + selectedFile: string | null; + content: string; + handleContentChange: (content: string) => void; + handleSave: (filePath: string, content: string) => Promise; + handleFileSelect: (filePath: string | null) => Promise; +} + +const ContentView: React.FC = ({ activeTab, selectedFile, content, @@ -13,10 +24,21 @@ const ContentView = ({ handleSave, handleFileSelect, }) => { + const { currentWorkspace } = useWorkspace(); + if (!currentWorkspace) { + return ( +
+ + No workspace selected. + +
+ ); + } + if (!selectedFile) { return (
- + No file selected.
@@ -27,7 +49,7 @@ const ContentView = ({ return (
{selectedFile} { +interface EditorProps { + content: string; + handleContentChange: (content: string) => void; + handleSave: (filePath: string, content: string) => Promise; + selectedFile: string; +} + +const Editor: React.FC = ({ + content, + handleContentChange, + handleSave, + selectedFile, +}) => { const { colorScheme } = useWorkspace(); - const editorRef = useRef(); - const viewRef = useRef(); + const editorRef = useRef(null); + const viewRef = useRef(null); useEffect(() => { - const handleEditorSave = (view) => { + const handleEditorSave = (view: EditorView): boolean => { handleSave(selectedFile, view.state.doc.toString()); return true; }; @@ -36,6 +48,8 @@ const Editor = ({ content, handleContentChange, handleSave, selectedFile }) => { }, }); + if (!editorRef.current) return; + const state = EditorState.create({ doc: content, extensions: [ @@ -69,8 +83,9 @@ const Editor = ({ content, handleContentChange, handleSave, selectedFile }) => { return () => { view.destroy(); + viewRef.current = null; }; - }, [colorScheme, handleContentChange]); + }, [colorScheme, handleContentChange, handleSave, selectedFile]); useEffect(() => { if (viewRef.current && content !== viewRef.current.state.doc.toString()) { diff --git a/app/src/components/editor/MarkdownPreview.jsx b/app/src/components/editor/MarkdownPreview.jsx deleted file mode 100644 index 51b80ee..0000000 --- a/app/src/components/editor/MarkdownPreview.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { unified } from 'unified'; -import remarkParse from 'remark-parse'; -import remarkMath from 'remark-math'; -import remarkRehype from 'remark-rehype'; -import rehypeMathjax from 'rehype-mathjax'; -import rehypeReact from 'rehype-react'; -import rehypePrism from 'rehype-prism'; -import * as prod from 'react/jsx-runtime'; -import { notifications } from '@mantine/notifications'; -import { remarkWikiLinks } from '../../utils/remarkWikiLinks'; -import { useWorkspace } from '../../contexts/WorkspaceContext'; - -const MarkdownPreview = ({ content, handleFileSelect }) => { - const [processedContent, setProcessedContent] = useState(null); - const baseUrl = window.API_BASE_URL; - const { currentWorkspace } = useWorkspace(); - - const handleLinkClick = (e, href) => { - e.preventDefault(); - - if (href.startsWith(`${baseUrl}/internal/`)) { - // For existing files, extract the path and directly select it - const [filePath] = decodeURIComponent( - href.replace(`${baseUrl}/internal/`, '') - ).split('#'); - handleFileSelect(filePath); - } else if (href.startsWith(`${baseUrl}/notfound/`)) { - // For non-existent files, show a notification - const fileName = decodeURIComponent( - href.replace(`${baseUrl}/notfound/`, '') - ); - notifications.show({ - title: 'File Not Found', - message: `The file "${fileName}" does not exist.`, - color: 'red', - }); - } - }; - - const processor = useMemo( - () => - unified() - .use(remarkParse) - .use(remarkWikiLinks, currentWorkspace?.name) - .use(remarkMath) - .use(remarkRehype) - .use(rehypeMathjax) - .use(rehypePrism) - .use(rehypeReact, { - production: true, - jsx: prod.jsx, - jsxs: prod.jsxs, - Fragment: prod.Fragment, - components: { - img: ({ src, alt, ...props }) => ( - {alt} { - console.error('Failed to load image:', event.target.src); - event.target.alt = 'Failed to load image'; - }} - {...props} - /> - ), - a: ({ href, children, ...props }) => ( - handleLinkClick(e, href)} - {...props} - > - {children} - - ), - code: ({ children, className, ...props }) => { - const language = className - ? className.replace('language-', '') - : null; - return ( -
-                  {children}
-                
- ); - }, - }, - }), - [baseUrl, handleFileSelect, currentWorkspace?.name] - ); - - useEffect(() => { - const processContent = async () => { - if (!currentWorkspace) { - return; - } - - try { - const result = await processor.process(content); - setProcessedContent(result.result); - } catch (error) { - console.error('Error processing markdown:', error); - } - }; - - processContent(); - }, [content, processor, currentWorkspace]); - - return
{processedContent}
; -}; - -export default MarkdownPreview; diff --git a/app/src/components/editor/MarkdownPreview.tsx b/app/src/components/editor/MarkdownPreview.tsx new file mode 100644 index 0000000..ebbe6e6 --- /dev/null +++ b/app/src/components/editor/MarkdownPreview.tsx @@ -0,0 +1,141 @@ +import React, { useState, useEffect, useMemo, ReactNode } from 'react'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkMath from 'remark-math'; +import remarkRehype from 'remark-rehype'; +import rehypeMathjax from 'rehype-mathjax'; +import rehypeReact from 'rehype-react'; +import rehypePrism from 'rehype-prism'; +import * as prod from 'react/jsx-runtime'; +import { notifications } from '@mantine/notifications'; +import { remarkWikiLinks } from '../../utils/remarkWikiLinks'; +import { useWorkspace } from '../../contexts/WorkspaceContext'; + +interface MarkdownPreviewProps { + content: string; + handleFileSelect: (filePath: string | null) => Promise; +} + +interface MarkdownImageProps { + src: string; + alt?: string; + [key: string]: any; +} + +interface MarkdownLinkProps { + href: string; + children: ReactNode; + [key: string]: any; +} + +interface MarkdownCodeProps { + children: ReactNode; + className?: string; + [key: string]: any; +} + +const MarkdownPreview: React.FC = ({ + content, + handleFileSelect, +}) => { + const [processedContent, setProcessedContent] = useState( + null + ); + const baseUrl = window.API_BASE_URL; + const { currentWorkspace } = useWorkspace(); + + const handleLinkClick = ( + e: React.MouseEvent, + href: string + ): void => { + e.preventDefault(); + + if (href.startsWith(`${baseUrl}/internal/`)) { + // For existing files, extract the path and directly select it + const [filePath] = decodeURIComponent( + href.replace(`${baseUrl}/internal/`, '') + ).split('#'); + handleFileSelect(filePath); + } else if (href.startsWith(`${baseUrl}/notfound/`)) { + // For non-existent files, show a notification + const fileName = decodeURIComponent( + href.replace(`${baseUrl}/notfound/`, '') + ); + notifications.show({ + title: 'File Not Found', + message: `The file "${fileName}" does not exist.`, + color: 'red', + }); + } + }; + + const processor = useMemo(() => { + // Only create the processor if we have a workspace name + if (!currentWorkspace?.name) { + return unified(); + } + + return unified() + .use(remarkParse) + .use(remarkWikiLinks, currentWorkspace.name) // Now we know this is defined + .use(remarkMath) + .use(remarkRehype) + .use(rehypeMathjax) + .use(rehypePrism) + .use(rehypeReact as any, { + production: true, + jsx: prod.jsx, + jsxs: prod.jsxs, + Fragment: prod.Fragment, + components: { + img: ({ src, alt, ...props }: MarkdownImageProps) => ( + {alt { + console.error('Failed to load image:', event.currentTarget.src); + event.currentTarget.alt = 'Failed to load image'; + }} + {...props} + /> + ), + a: ({ href, children, ...props }: MarkdownLinkProps) => ( + handleLinkClick(e, href)} {...props}> + {children} + + ), + code: ({ children, className, ...props }: MarkdownCodeProps) => { + const language = className + ? className.replace('language-', '') + : null; + return ( +
+                {children}
+              
+ ); + }, + }, + }); + }, [baseUrl, handleFileSelect, currentWorkspace?.name]); + + useEffect(() => { + const processContent = async (): Promise => { + if (!currentWorkspace) { + return; + } + + try { + const result = await processor.process(content); + setProcessedContent(result.result as ReactNode); + } catch (error) { + console.error('Error processing markdown:', error); + } + }; + + processContent(); + }, [content, processor, currentWorkspace]); + + return
{processedContent}
; +}; + +export default MarkdownPreview; diff --git a/app/src/utils/fileHelpers.ts b/app/src/utils/fileHelpers.ts index 7ed9c0b..f589377 100644 --- a/app/src/utils/fileHelpers.ts +++ b/app/src/utils/fileHelpers.ts @@ -1,3 +1,4 @@ +import { API_BASE_URL } from '@/types/authApi'; import { IMAGE_EXTENSIONS } from '../types/file'; /** @@ -8,3 +9,9 @@ import { IMAGE_EXTENSIONS } from '../types/file'; export const isImageFile = (filePath: string): boolean => { return IMAGE_EXTENSIONS.some((ext) => filePath.toLowerCase().endsWith(ext)); }; + +export const getFileUrl = (workspaceName: string, filePath: string) => { + return `${API_BASE_URL}/workspaces/${encodeURIComponent( + workspaceName + )}/files/${encodeURIComponent(filePath)}`; +};