Add preview mode

This commit is contained in:
2024-09-26 21:47:23 +02:00
parent 79bd172f70
commit 27be41ba62
10 changed files with 2115 additions and 112 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -30,8 +30,13 @@
"@geist-ui/core": "^2.3.8",
"@geist-ui/icons": "^1.0.2",
"codemirror": "^6.0.1",
"katex": "^0.16.11",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0"
},
"eslintConfig": {
"extends": [

View File

@@ -1,102 +1,40 @@
import React, { useState, useEffect, useCallback } from 'react';
import { GeistProvider, CssBaseline, useToasts, Page, Text, User, Button, Spacer, Breadcrumbs } from '@geist-ui/core';
import { Settings } from '@geist-ui/icons';
import Editor from './components/Editor';
import FileTree from './components/FileTree';
import { fetchFileList, fetchFileContent, saveFileContent } from './services/api';
import React from 'react';
import { GeistProvider, CssBaseline, Page } from '@geist-ui/core';
import Header from './components/Header';
import MainContent from './components/MainContent';
import useFileManagement from './hooks/useFileManagement';
import './App.scss';
function App() {
const [content, setContent] = useState('# Welcome to NovaMD\n\nStart editing here!');
const [files, setFiles] = useState([]);
const [selectedFile, setSelectedFile] = useState(null);
const [error, setError] = useState(null);
const { setToast } = useToasts();
useEffect(() => {
const loadFileList = async () => {
try {
const fileList = await fetchFileList();
if (Array.isArray(fileList)) {
setFiles(fileList);
} else {
throw new Error('File list is not an array');
}
} catch (error) {
console.error('Failed to load file list:', error);
setError('Failed to load file list. Please try again later.');
}
};
loadFileList();
}, []);
const handleFileSelect = async (filePath) => {
try {
const fileContent = await fetchFileContent(filePath);
setContent(fileContent);
setSelectedFile(filePath);
setError(null);
} catch (error) {
console.error('Failed to load file content:', error);
setError('Failed to load file content. Please try again.');
}
};
const handleSave = useCallback(async (filePath, fileContent) => {
try {
await saveFileContent(filePath, fileContent);
setToast({ text: 'File saved successfully', type: 'success' });
} catch (error) {
console.error('Error saving file:', error);
setToast({ text: 'Failed to save file. Please try again.', type: 'error' });
}
}, [setToast]);
const renderBreadcrumbs = () => {
if (!selectedFile) return null;
const pathParts = selectedFile.split('/');
return (
<Breadcrumbs>
{pathParts.map((part, index) => (
<Breadcrumbs.Item key={index}>{part}</Breadcrumbs.Item>
))}
</Breadcrumbs>
);
};
const {
content,
files,
selectedFile,
isNewFile,
hasUnsavedChanges,
error,
handleFileSelect,
handleContentChange,
handleSave,
} = useFileManagement();
return (
<GeistProvider>
<CssBaseline />
<Page>
<Page.Header className="custom-navbar">
<Text b>NovaMD</Text>
<Spacer w={1} />
<User src="https://via.placeholder.com/40" name="User" />
<Spacer w={0.5} />
<Button auto icon={<Settings />} />
</Page.Header>
<Page.Content className="main-container">
<div className="sidebar">
{error ? (
<div className="error">{error}</div>
) : (
<FileTree
files={files}
onFileSelect={handleFileSelect}
selectedFile={selectedFile}
/>
)}
</div>
<div className="main-content">
{renderBreadcrumbs()}
<Editor
<Header />
<Page.Content>
<MainContent
content={content}
onChange={setContent}
files={files}
selectedFile={selectedFile}
isNewFile={isNewFile}
hasUnsavedChanges={hasUnsavedChanges}
error={error}
onFileSelect={handleFileSelect}
onContentChange={handleContentChange}
onSave={handleSave}
filePath={selectedFile}
/>
</div>
</Page.Content>
</Page>
</GeistProvider>

View File

@@ -1,5 +1,4 @@
// Variables
$sidebar-width: 250px;
$border-color: #eaeaea;
$padding: 20px;
$navbar-height: 64px; // Adjust this value based on your preference
@@ -13,27 +12,63 @@ $navbar-height: 64px; // Adjust this value based on your preference
border-bottom: 1px solid $border-color;
}
.main-container {
display: flex;
height: calc(100vh - #{$navbar-height});
padding: 0 !important; // Override Geist UI's default padding
}
.sidebar {
width: $sidebar-width;
padding: $padding;
border-right: 1px solid $border-color;
overflow-y: auto;
padding: $padding;
}
.main-content {
flex-grow: 1;
padding: $padding;
overflow-y: auto;
display: flex;
flex-direction: column;
overflow: hidden;
}
// Add styles for breadcrumbs
.breadcrumbs {
margin-bottom: $padding;
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: $padding;
border-bottom: 1px solid $border-color;
.breadcrumbs-container {
flex-grow: 1;
display: flex;
align-items: center;
.unsaved-indicator {
margin-left: 8px;
}
}
.tabs {
flex-shrink: 0;
}
}
.content-body {
flex-grow: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.editor-container, .markdown-preview {
flex-grow: 1;
overflow-y: auto;
padding: $padding;
}
// Ensure CodeMirror takes full height of its container
.editor-container {
height: 100%;
.cm-editor {
height: 100%;
}
.cm-scroller {
overflow: auto;
}
}

View File

@@ -20,6 +20,7 @@ const Editor = ({ content, onChange, onSave, filePath }) => {
extensions: [
basicSetup,
markdown(),
EditorView.lineWrapping,
keymap.of(defaultKeymap),
keymap.of([{
key: "Ctrl-s",
@@ -54,7 +55,7 @@ const Editor = ({ content, onChange, onSave, filePath }) => {
}
}, [content]);
return <div ref={editorRef} />;
return <div ref={editorRef} className="editor-container" />;
};
export default Editor;

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Page, Text, User, Button, Spacer } from '@geist-ui/core';
import { Settings } from '@geist-ui/icons';
const Header = () => {
return (
<Page.Header className="custom-navbar">
<Text b>NovaMD</Text>
<Spacer w={1} />
<User src="https://via.placeholder.com/40" name="User" />
<Spacer w={0.5} />
<Button auto icon={<Settings />} />
</Page.Header>
);
};
export default Header;

View File

@@ -0,0 +1,73 @@
import React, { useState } from 'react';
import { Grid, Breadcrumbs, Tabs, Dot } from '@geist-ui/core';
import { Code, Eye } from '@geist-ui/icons';
import Editor from './Editor';
import FileTree from './FileTree';
import MarkdownPreview from './MarkdownPreview';
const MainContent = ({
content,
files,
selectedFile,
hasUnsavedChanges,
error,
onFileSelect,
onContentChange,
onSave,
}) => {
const [activeTab, setActiveTab] = useState('source');
const renderBreadcrumbs = () => {
if (!selectedFile) return null;
const pathParts = selectedFile.split('/');
return (
<div className="breadcrumbs-container">
<Breadcrumbs>
{pathParts.map((part, index) => (
<Breadcrumbs.Item key={index}>{part}</Breadcrumbs.Item>
))}
</Breadcrumbs>
{hasUnsavedChanges && <Dot type="warning" className="unsaved-indicator" />}
</div>
);
};
return (
<Grid.Container gap={1} height="calc(100vh - 64px)">
<Grid xs={24} sm={6} md={5} lg={4} height="100%" className="sidebar">
{error ? (
<div className="error">{error}</div>
) : (
<FileTree
files={files}
onFileSelect={onFileSelect}
selectedFile={selectedFile}
/>
)}
</Grid>
<Grid xs={24} sm={18} md={19} lg={20} height="100%" className="main-content">
<div className="content-header">
{renderBreadcrumbs()}
<Tabs value={activeTab} onChange={setActiveTab}>
<Tabs.Item label={<Code />} value="source" />
<Tabs.Item label={<Eye />} value="preview" />
</Tabs>
</div>
<div className="content-body">
{activeTab === 'source' ? (
<Editor
content={content}
onChange={onContentChange}
onSave={onSave}
filePath={selectedFile}
/>
) : (
<MarkdownPreview content={content} />
)}
</div>
</Grid>
</Grid.Container>
);
};
export default MainContent;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import 'katex/dist/katex.min.css';
const MarkdownPreview = ({ content }) => {
return (
<div className="markdown-preview">
<ReactMarkdown
remarkPlugins={[remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
code({node, inline, className, children, ...props}) {
const match = /language-(\w+)/.exec(className || '')
return !inline && match ? (
<SyntaxHighlighter
style={vscDarkPlus}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{content}
</ReactMarkdown>
</div>
);
};
export default MarkdownPreview;

View File

@@ -0,0 +1,91 @@
import { useState, useEffect, useCallback } from 'react';
import { useToasts } from '@geist-ui/core';
import { fetchFileList, fetchFileContent, saveFileContent } from '../services/api';
const DEFAULT_FILE = {
name: 'New File.md',
path: 'New File.md',
content: '# Welcome to NovaMD\n\nStart editing here!'
};
const useFileManagement = () => {
const [content, setContent] = useState(DEFAULT_FILE.content);
const [files, setFiles] = useState([]);
const [selectedFile, setSelectedFile] = useState(DEFAULT_FILE.path);
const [isNewFile, setIsNewFile] = useState(true);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [error, setError] = useState(null);
const { setToast } = useToasts();
useEffect(() => {
const loadFileList = async () => {
try {
const fileList = await fetchFileList();
if (Array.isArray(fileList)) {
setFiles(fileList);
} else {
throw new Error('File list is not an array');
}
} catch (error) {
console.error('Failed to load file list:', error);
setError('Failed to load file list. Please try again later.');
}
};
loadFileList();
}, []);
const handleFileSelect = async (filePath) => {
if (hasUnsavedChanges) {
const confirmSwitch = window.confirm('You have unsaved changes. Are you sure you want to switch files?');
if (!confirmSwitch) return;
}
try {
const fileContent = await fetchFileContent(filePath);
setContent(fileContent);
setSelectedFile(filePath);
setIsNewFile(false);
setHasUnsavedChanges(false);
setError(null);
} catch (error) {
console.error('Failed to load file content:', error);
setError('Failed to load file content. Please try again.');
}
};
const handleContentChange = (newContent) => {
setContent(newContent);
setHasUnsavedChanges(true);
};
const handleSave = useCallback(async (filePath, fileContent) => {
try {
await saveFileContent(filePath, fileContent);
setToast({ text: 'File saved successfully', type: 'success' });
setIsNewFile(false);
setHasUnsavedChanges(false);
if (isNewFile) {
const updatedFileList = await fetchFileList();
setFiles(updatedFileList);
}
} catch (error) {
console.error('Error saving file:', error);
setToast({ text: 'Failed to save file. Please try again.', type: 'error' });
}
}, [setToast, isNewFile]);
return {
content,
files,
selectedFile,
isNewFile,
hasUnsavedChanges,
error,
handleFileSelect,
handleContentChange,
handleSave,
};
};
export default useFileManagement;

View File

@@ -18,6 +18,10 @@ module.exports = {
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [