mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-06 16:04:23 +00:00
Rename root folders
This commit is contained in:
54
app/src/components/editor/ContentView.jsx
Normal file
54
app/src/components/editor/ContentView.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Text, Center } from '@mantine/core';
|
||||
import Editor from './Editor';
|
||||
import MarkdownPreview from './MarkdownPreview';
|
||||
import { getFileUrl } from '../../services/api';
|
||||
import { isImageFile } from '../../utils/fileHelpers';
|
||||
|
||||
const ContentView = ({
|
||||
activeTab,
|
||||
selectedFile,
|
||||
content,
|
||||
handleContentChange,
|
||||
handleSave,
|
||||
handleFileSelect,
|
||||
}) => {
|
||||
if (!selectedFile) {
|
||||
return (
|
||||
<Center style={{ height: '100%' }}>
|
||||
<Text size="xl" weight={500}>
|
||||
No file selected.
|
||||
</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (isImageFile(selectedFile)) {
|
||||
return (
|
||||
<Center className="image-preview">
|
||||
<img
|
||||
src={getFileUrl(selectedFile)}
|
||||
alt={selectedFile}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return activeTab === 'source' ? (
|
||||
<Editor
|
||||
content={content}
|
||||
handleContentChange={handleContentChange}
|
||||
handleSave={handleSave}
|
||||
selectedFile={selectedFile}
|
||||
/>
|
||||
) : (
|
||||
<MarkdownPreview content={content} handleFileSelect={handleFileSelect} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentView;
|
||||
90
app/src/components/editor/Editor.jsx
Normal file
90
app/src/components/editor/Editor.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { EditorView, keymap } from '@codemirror/view';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { useWorkspace } from '../../contexts/WorkspaceContext';
|
||||
|
||||
const Editor = ({ content, handleContentChange, handleSave, selectedFile }) => {
|
||||
const { colorScheme } = useWorkspace();
|
||||
const editorRef = useRef();
|
||||
const viewRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
const handleEditorSave = (view) => {
|
||||
handleSave(selectedFile, view.state.doc.toString());
|
||||
return true;
|
||||
};
|
||||
|
||||
const theme = EditorView.theme({
|
||||
'&': {
|
||||
height: '100%',
|
||||
fontSize: '14px',
|
||||
},
|
||||
'.cm-scroller': {
|
||||
overflow: 'auto',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: colorScheme === 'dark' ? '#1e1e1e' : '#f5f5f5',
|
||||
color: colorScheme === 'dark' ? '#858585' : '#999',
|
||||
border: 'none',
|
||||
},
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: colorScheme === 'dark' ? '#2c313a' : '#e8e8e8',
|
||||
},
|
||||
});
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: content,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
markdown(),
|
||||
EditorView.lineWrapping,
|
||||
keymap.of(defaultKeymap),
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Ctrl-s',
|
||||
run: handleEditorSave,
|
||||
preventDefault: true,
|
||||
},
|
||||
]),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
handleContentChange(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
theme,
|
||||
colorScheme === 'dark' ? oneDark : [],
|
||||
],
|
||||
});
|
||||
|
||||
const view = new EditorView({
|
||||
state,
|
||||
parent: editorRef.current,
|
||||
});
|
||||
|
||||
viewRef.current = view;
|
||||
|
||||
return () => {
|
||||
view.destroy();
|
||||
};
|
||||
}, [colorScheme, handleContentChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewRef.current && content !== viewRef.current.state.doc.toString()) {
|
||||
viewRef.current.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: viewRef.current.state.doc.length,
|
||||
insert: content,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
return <div ref={editorRef} className="editor-container" />;
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
111
app/src/components/editor/MarkdownPreview.jsx
Normal file
111
app/src/components/editor/MarkdownPreview.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
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 }) => (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
onError={(event) => {
|
||||
console.error('Failed to load image:', event.target.src);
|
||||
event.target.alt = 'Failed to load image';
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
a: ({ href, children, ...props }) => (
|
||||
<a
|
||||
href={href}
|
||||
onClick={(e) => handleLinkClick(e, href)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
code: ({ children, className, ...props }) => {
|
||||
const language = className
|
||||
? className.replace('language-', '')
|
||||
: null;
|
||||
return (
|
||||
<pre className={className}>
|
||||
<code {...props}>{children}</code>
|
||||
</pre>
|
||||
);
|
||||
},
|
||||
},
|
||||
}),
|
||||
[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 <div className="markdown-preview">{processedContent}</div>;
|
||||
};
|
||||
|
||||
export default MarkdownPreview;
|
||||
Reference in New Issue
Block a user