Merge pull request #2 from LordMathis/feat/react-context

Feat/react context
This commit is contained in:
2024-10-07 19:29:52 +02:00
committed by GitHub
22 changed files with 689 additions and 549 deletions

View File

@@ -1,94 +1,39 @@
import React, { useState, useEffect } from 'react'; import React from 'react';
import { GeistProvider, CssBaseline, Page, useToasts } from '@geist-ui/core'; import { GeistProvider, CssBaseline, Page } from '@geist-ui/core';
import Header from './components/Header'; import Header from './components/Header';
import MainContent from './components/MainContent'; import MainContent from './components/MainContent';
import useFileManagement from './hooks/useFileManagement'; import { SettingsProvider, useSettings } from './contexts/SettingsContext';
import { fetchUserSettings } from './services/api'; import { ModalProvider } from './contexts/ModalContext';
import './App.scss'; import './App.scss';
function App() { function AppContent() {
const [themeType, setThemeType] = useState('light'); const { settings, loading } = useSettings();
const [userId, setUserId] = useState(1);
const [settings, setSettings] = useState({ gitEnabled: false });
const { setToast } = useToasts();
useEffect(() => { if (loading) {
const loadUserSettings = async () => { return <div>Loading...</div>;
try { }
const fetchedSettings = await fetchUserSettings(userId);
setSettings(fetchedSettings.settings);
setThemeType(fetchedSettings.settings.theme);
} catch (error) {
console.error('Failed to load user settings:', error);
}
};
loadUserSettings();
}, [userId]);
const {
content,
files,
selectedFile,
isNewFile,
hasUnsavedChanges,
error,
handleFileSelect,
handleContentChange,
handleSave,
pullLatestChanges,
lookupFileByName,
} = useFileManagement(settings.gitEnabled);
const handleThemeChange = (newTheme) => {
setThemeType(newTheme);
};
const handleLinkClick = async (filename) => {
try {
const filePaths = await lookupFileByName(filename);
if (filePaths.length === 1) {
handleFileSelect(filePaths[0]);
} else if (filePaths.length > 1) {
setFileOptions(filePaths.map((path) => ({ label: path, value: path })));
setFileSelectionModalVisible(true);
} else {
setToast({ text: `File "${filename}" not found`, type: 'error' });
}
} catch (error) {
console.error('Error looking up file:', error);
setToast({
text: 'Failed to lookup file. Please try again.',
type: 'error',
});
}
};
return ( return (
<GeistProvider themeType={themeType}> <GeistProvider themeType={settings.theme}>
<CssBaseline /> <CssBaseline />
<Page> <Page>
<Header currentTheme={themeType} onThemeChange={handleThemeChange} /> <Header />
<Page.Content className="page-content"> <Page.Content className="page-content">
<MainContent <MainContent />
content={content}
files={files}
selectedFile={selectedFile}
isNewFile={isNewFile}
hasUnsavedChanges={hasUnsavedChanges}
error={error}
onFileSelect={handleFileSelect}
onContentChange={handleContentChange}
onSave={handleSave}
settings={settings}
pullLatestChanges={pullLatestChanges}
onLinkClick={handleLinkClick}
lookupFileByName={lookupFileByName}
/>
</Page.Content> </Page.Content>
</Page> </Page>
</GeistProvider> </GeistProvider>
); );
} }
function App() {
return (
<SettingsProvider>
<ModalProvider>
<AppContent />
</ModalProvider>
</SettingsProvider>
);
}
export default App; export default App;

View File

@@ -1,19 +1,33 @@
import React from 'react'; import React from 'react';
import Editor from './Editor'; import Editor from './Editor';
import MarkdownPreview from './MarkdownPreview'; import MarkdownPreview from './MarkdownPreview';
import { Text } from '@geist-ui/core';
import { getFileUrl } from '../services/api'; import { getFileUrl } from '../services/api';
import { isImageFile } from '../utils/fileHelpers'; import { isImageFile } from '../utils/fileHelpers';
const ContentView = ({ const ContentView = ({
activeTab, activeTab,
content,
selectedFile, selectedFile,
onContentChange, content,
onSave, handleContentChange,
themeType, handleSave,
onLinkClick, handleLinkClick,
lookupFileByName,
}) => { }) => {
if (!selectedFile) {
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
>
<Text h3>No file selected.</Text>
</div>
);
}
if (isImageFile(selectedFile)) { if (isImageFile(selectedFile)) {
return ( return (
<div className="image-preview"> <div className="image-preview">
@@ -33,18 +47,12 @@ const ContentView = ({
return activeTab === 'source' ? ( return activeTab === 'source' ? (
<Editor <Editor
content={content} content={content}
onChange={onContentChange} handleContentChange={handleContentChange}
onSave={onSave} handleSave={handleSave}
filePath={selectedFile} selectedFile={selectedFile}
themeType={themeType}
/> />
) : ( ) : (
<MarkdownPreview <MarkdownPreview content={content} handleLinkClick={handleLinkClick} />
content={content}
baseUrl={window.API_BASE_URL}
onLinkClick={onLinkClick}
lookupFileByName={lookupFileByName}
/>
); );
}; };

View File

@@ -5,14 +5,16 @@ import { EditorView, keymap } from '@codemirror/view';
import { markdown } from '@codemirror/lang-markdown'; import { markdown } from '@codemirror/lang-markdown';
import { defaultKeymap } from '@codemirror/commands'; import { defaultKeymap } from '@codemirror/commands';
import { oneDark } from '@codemirror/theme-one-dark'; import { oneDark } from '@codemirror/theme-one-dark';
import { useSettings } from '../contexts/SettingsContext';
const Editor = ({ content, onChange, onSave, filePath, themeType }) => { const Editor = ({ content, handleContentChange, handleSave, selectedFile }) => {
const { settings } = useSettings();
const editorRef = useRef(); const editorRef = useRef();
const viewRef = useRef(); const viewRef = useRef();
useEffect(() => { useEffect(() => {
const handleSave = (view) => { const handleEditorSave = (view) => {
onSave(filePath, view.state.doc.toString()); handleSave(selectedFile, view.state.doc.toString());
return true; return true;
}; };
@@ -25,12 +27,12 @@ const Editor = ({ content, onChange, onSave, filePath, themeType }) => {
overflow: 'auto', overflow: 'auto',
}, },
'.cm-gutters': { '.cm-gutters': {
backgroundColor: themeType === 'dark' ? '#1e1e1e' : '#f5f5f5', backgroundColor: settings.theme === 'dark' ? '#1e1e1e' : '#f5f5f5',
color: themeType === 'dark' ? '#858585' : '#999', color: settings.theme === 'dark' ? '#858585' : '#999',
border: 'none', border: 'none',
}, },
'.cm-activeLineGutter': { '.cm-activeLineGutter': {
backgroundColor: themeType === 'dark' ? '#2c313a' : '#e8e8e8', backgroundColor: settings.theme === 'dark' ? '#2c313a' : '#e8e8e8',
}, },
}); });
@@ -44,17 +46,17 @@ const Editor = ({ content, onChange, onSave, filePath, themeType }) => {
keymap.of([ keymap.of([
{ {
key: 'Ctrl-s', key: 'Ctrl-s',
run: handleSave, run: handleEditorSave,
preventDefault: true, preventDefault: true,
}, },
]), ]),
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
if (update.docChanged) { if (update.docChanged) {
onChange(update.state.doc.toString()); handleContentChange(update.state.doc.toString());
} }
}), }),
theme, theme,
themeType === 'dark' ? oneDark : [], settings.theme === 'dark' ? oneDark : [],
], ],
}); });
@@ -68,7 +70,7 @@ const Editor = ({ content, onChange, onSave, filePath, themeType }) => {
return () => { return () => {
view.destroy(); view.destroy();
}; };
}, [filePath, themeType]); }, [settings.theme, handleContentChange]);
useEffect(() => { useEffect(() => {
if (viewRef.current && content !== viewRef.current.state.doc.toString()) { if (viewRef.current && content !== viewRef.current.state.doc.toString()) {

View File

@@ -1,16 +1,21 @@
import React from 'react'; import React from 'react';
import { Button, Tooltip, ButtonGroup, Spacer } from '@geist-ui/core'; import { Button, Tooltip, ButtonGroup, Spacer } from '@geist-ui/core';
import { Plus, Trash, GitPullRequest, GitCommit } from '@geist-ui/icons'; import { Plus, Trash, GitPullRequest, GitCommit } from '@geist-ui/icons';
import { useSettings } from '../contexts/SettingsContext';
import { useModalContext } from '../contexts/ModalContext';
const FileActions = ({ handlePullChanges, selectedFile }) => {
const { settings } = useSettings();
const {
setNewFileModalVisible,
setDeleteFileModalVisible,
setCommitMessageModalVisible,
} = useModalContext();
const handleCreateFile = () => setNewFileModalVisible(true);
const handleDeleteFile = () => setDeleteFileModalVisible(true);
const handleCommitAndPush = () => setCommitMessageModalVisible(true);
const FileActions = ({
selectedFile,
gitEnabled,
gitAutoCommit,
onPull,
onCommitAndPush,
onCreateFile,
onDeleteFile,
}) => {
return ( return (
<ButtonGroup className="file-actions"> <ButtonGroup className="file-actions">
<Tooltip text="Create new file" type="dark"> <Tooltip text="Create new file" type="dark">
@@ -18,7 +23,7 @@ const FileActions = ({
icon={<Plus />} icon={<Plus />}
auto auto
scale={2 / 3} scale={2 / 3}
onClick={onCreateFile} onClick={handleCreateFile}
px={0.6} px={0.6}
/> />
</Tooltip> </Tooltip>
@@ -31,7 +36,7 @@ const FileActions = ({
icon={<Trash />} icon={<Trash />}
auto auto
scale={2 / 3} scale={2 / 3}
onClick={onDeleteFile} onClick={handleDeleteFile}
disabled={!selectedFile} disabled={!selectedFile}
type="error" type="error"
px={0.6} px={0.6}
@@ -39,24 +44,28 @@ const FileActions = ({
</Tooltip> </Tooltip>
<Spacer w={0.5} /> <Spacer w={0.5} />
<Tooltip <Tooltip
text={gitEnabled ? 'Pull changes from remote' : 'Git is not enabled'} text={
settings.gitEnabled
? 'Pull changes from remote'
: 'Git is not enabled'
}
type="dark" type="dark"
> >
<Button <Button
icon={<GitPullRequest />} icon={<GitPullRequest />}
auto auto
scale={2 / 3} scale={2 / 3}
onClick={onPull} onClick={handlePullChanges}
disabled={!gitEnabled} disabled={!settings.gitEnabled}
px={0.6} px={0.6}
/> />
</Tooltip> </Tooltip>
<Spacer w={0.5} /> <Spacer w={0.5} />
<Tooltip <Tooltip
text={ text={
!gitEnabled !settings.gitEnabled
? 'Git is not enabled' ? 'Git is not enabled'
: gitAutoCommit : settings.gitAutoCommit
? 'Auto-commit is enabled' ? 'Auto-commit is enabled'
: 'Commit and push changes' : 'Commit and push changes'
} }
@@ -66,8 +75,8 @@ const FileActions = ({
icon={<GitCommit />} icon={<GitCommit />}
auto auto
scale={2 / 3} scale={2 / 3}
onClick={onCommitAndPush} onClick={handleCommitAndPush}
disabled={!gitEnabled || gitAutoCommit} disabled={!settings.gitEnabled || settings.gitAutoCommit}
px={0.6} px={0.6}
/> />
</Tooltip> </Tooltip>

View File

@@ -1,45 +1,27 @@
import React from 'react'; import React, { useMemo } from 'react';
import { Tree } from '@geist-ui/core'; import { Tree } from '@geist-ui/core';
import { File, Folder, Image } from '@geist-ui/icons'; import { File, Folder, Image } from '@geist-ui/icons';
import { isImageFile } from '../utils/fileHelpers';
const FileTree = ({ const FileTree = ({ files, handleFileSelect }) => {
files = [],
onFileSelect = () => {},
selectedFile = null,
}) => {
if (files.length === 0) { if (files.length === 0) {
return <div>No files to display</div>; return <div>No files to display</div>;
} }
const handleSelect = (filePath) => { const renderIcon = useMemo(
onFileSelect(filePath); () =>
}; ({ type, name }) => {
if (type === 'directory') return <Folder />;
const renderLabel = (node) => { return isImageFile(name) ? <Image /> : <File />;
const path = node.extra; },
return ( []
<span style={{ color: path === selectedFile ? '#0070f3' : 'inherit' }}> );
{node.name}
</span>
);
};
const isImageFile = (fileName) => {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
return imageExtensions.some((ext) => fileName.toLowerCase().endsWith(ext));
};
const renderIcon = ({ type, name }) => {
if (type === 'directory') return <Folder />;
return isImageFile(name) ? <Image /> : <File />;
};
return ( return (
<Tree <Tree
value={files} value={files}
onClick={handleSelect} onClick={(filePath) => handleFileSelect(filePath)}
renderIcon={renderIcon} renderIcon={renderIcon}
renderLabel={renderLabel}
/> />
); );
}; };

View File

@@ -1,13 +1,13 @@
import React, { useState } from 'react'; import React from 'react';
import { Page, Text, User, Button, Spacer } from '@geist-ui/core'; import { Page, Text, User, Button, Spacer } from '@geist-ui/core';
import { Settings as SettingsIcon } from '@geist-ui/icons'; import { Settings as SettingsIcon } from '@geist-ui/icons';
import Settings from './Settings'; import Settings from './Settings';
import { useModalContext } from '../contexts/ModalContext';
const Header = ({ currentTheme, onThemeChange }) => { const Header = () => {
const [settingsVisible, setSettingsVisible] = useState(false); const { setSettingsModalVisible } = useModalContext();
const openSettings = () => setSettingsVisible(true); const openSettings = () => setSettingsModalVisible(true);
const closeSettings = () => setSettingsVisible(false);
return ( return (
<Page.Header className="custom-navbar"> <Page.Header className="custom-navbar">
@@ -16,12 +16,7 @@ const Header = ({ currentTheme, onThemeChange }) => {
<User src="https://via.placeholder.com/40" name="User" /> <User src="https://via.placeholder.com/40" name="User" />
<Spacer w={0.5} /> <Spacer w={0.5} />
<Button auto icon={<SettingsIcon />} onClick={openSettings} /> <Button auto icon={<SettingsIcon />} onClick={openSettings} />
<Settings <Settings />
visible={settingsVisible}
onClose={closeSettings}
currentTheme={currentTheme}
onThemeChange={onThemeChange}
/>
</Page.Header> </Page.Header>
); );
}; };

View File

@@ -1,132 +1,79 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { import { Breadcrumbs, Grid, Tabs, Dot } from '@geist-ui/core';
Grid,
Breadcrumbs,
Tabs,
Dot,
useTheme,
useToasts,
} from '@geist-ui/core';
import { Code, Eye } from '@geist-ui/icons'; import { Code, Eye } from '@geist-ui/icons';
import FileTree from './FileTree';
import FileActions from './FileActions'; import FileActions from './FileActions';
import FileTree from './FileTree';
import ContentView from './ContentView';
import CreateFileModal from './modals/CreateFileModal'; import CreateFileModal from './modals/CreateFileModal';
import DeleteFileModal from './modals/DeleteFileModal'; import DeleteFileModal from './modals/DeleteFileModal';
import CommitMessageModal from './modals/CommitMessageModal'; import CommitMessageModal from './modals/CommitMessageModal';
import ContentView from './ContentView';
import { commitAndPush, saveFileContent, deleteFile } from '../services/api';
import { isImageFile } from '../utils/fileHelpers';
const MainContent = ({ import { useFileContent } from '../hooks/useFileContent';
content, import { useFileList } from '../hooks/useFileList';
files, import { useFileOperations } from '../hooks/useFileOperations';
selectedFile, import { useGitOperations } from '../hooks/useGitOperations';
hasUnsavedChanges, import { useFileNavigation } from '../hooks/useFileNavigation';
onFileSelect, import { useSettings } from '../contexts/SettingsContext';
onContentChange,
onSave, const MainContent = () => {
settings,
pullLatestChanges,
onLinkClick,
lookupFileByName,
}) => {
const [activeTab, setActiveTab] = useState('source'); const [activeTab, setActiveTab] = useState('source');
const { type: themeType } = useTheme(); const { settings } = useSettings();
const { setToast } = useToasts(); const { files, loadFileList } = useFileList();
const [newFileModalVisible, setNewFileModalVisible] = useState(false); const { handleLinkClick, selectedFile, handleFileSelect } =
const [newFileName, setNewFileName] = useState(''); useFileNavigation();
const [deleteFileModalVisible, setDeleteFileModalVisible] = useState(false); const {
const [commitMessageModalVisible, setCommitMessageModalVisible] = content,
useState(false); hasUnsavedChanges,
setHasUnsavedChanges,
handleContentChange,
} = useFileContent(selectedFile);
const { handleSave, handleCreate, handleDelete } = useFileOperations();
const { handleCommitAndPush, handlePull } = useGitOperations();
useEffect(() => { useEffect(() => {
if (isImageFile(selectedFile)) { loadFileList();
setActiveTab('preview'); }, [settings.gitEnabled]);
}
}, [selectedFile]);
const handleTabChange = (value) => { const handleTabChange = (value) => {
if (!isImageFile(selectedFile) || value === 'preview') { setActiveTab(value);
setActiveTab(value);
}
}; };
const handlePull = async () => { const handleSaveFile = useCallback(
try { async (filePath, content) => {
await pullLatestChanges(); const success = await handleSave(filePath, content);
setToast({ text: 'Successfully pulled latest changes', type: 'success' }); if (success) {
} catch (error) { setHasUnsavedChanges(false);
setToast({
text: 'Failed to pull changes: ' + error.message,
type: 'error',
});
}
};
const handleCreateFile = () => {
setNewFileModalVisible(true);
};
const handleNewFileSubmit = async () => {
if (newFileName) {
try {
await saveFileContent(newFileName, '');
setToast({ text: 'New file created successfully', type: 'success' });
await pullLatestChanges();
onFileSelect(newFileName);
} catch (error) {
setToast({
text: 'Failed to create new file: ' + error.message,
type: 'error',
});
} }
} return success;
setNewFileModalVisible(false); },
setNewFileName(''); [handleSave, setHasUnsavedChanges]
}; );
const handleDeleteFile = () => { const handleCreateFile = useCallback(
setDeleteFileModalVisible(true); async (fileName) => {
}; const success = await handleCreate(fileName);
if (success) {
await loadFileList();
handleFileSelect(fileName);
}
},
[handleCreate, loadFileList, handleFileSelect]
);
const confirmDeleteFile = async () => { const handleDeleteFile = useCallback(
try { async (filePath) => {
await deleteFile(selectedFile); const success = await handleDelete(filePath);
setToast({ text: 'File deleted successfully', type: 'success' }); if (success) {
await pullLatestChanges(); await loadFileList();
onFileSelect(null); handleFileSelect(null);
} catch (error) { }
setToast({ },
text: 'Failed to delete file: ' + error.message, [handleDelete, loadFileList, handleFileSelect]
type: 'error', );
});
}
setDeleteFileModalVisible(false);
};
const handleCommitAndPush = () => {
setCommitMessageModalVisible(true);
};
const confirmCommitAndPush = async (message) => {
try {
await commitAndPush(message);
setToast({
text: 'Changes committed and pushed successfully',
type: 'success',
});
await pullLatestChanges();
} catch (error) {
setToast({
text: 'Failed to commit and push changes: ' + error.message,
type: 'error',
});
}
setCommitMessageModalVisible(false);
};
const renderBreadcrumbs = () => { const renderBreadcrumbs = () => {
if (!selectedFile) return null; if (!selectedFile) return <div className="breadcrumbs-container"></div>;
const pathParts = selectedFile.split('/'); const pathParts = selectedFile.split('/');
return ( return (
<div className="breadcrumbs-container"> <div className="breadcrumbs-container">
@@ -148,18 +95,13 @@ const MainContent = ({
<Grid xs={24} sm={6} md={5} lg={4} height="100%" className="sidebar"> <Grid xs={24} sm={6} md={5} lg={4} height="100%" className="sidebar">
<div className="file-tree-container"> <div className="file-tree-container">
<FileActions <FileActions
handlePullChanges={handlePull}
selectedFile={selectedFile} selectedFile={selectedFile}
gitEnabled={settings.gitEnabled}
gitAutoCommit={settings.gitAutoCommit}
onPull={handlePull}
onCommitAndPush={handleCommitAndPush}
onCreateFile={handleCreateFile}
onDeleteFile={handleDeleteFile}
/> />
<FileTree <FileTree
files={files} files={files}
onFileSelect={onFileSelect}
selectedFile={selectedFile} selectedFile={selectedFile}
handleFileSelect={handleFileSelect}
/> />
</div> </div>
</Grid> </Grid>
@@ -174,46 +116,28 @@ const MainContent = ({
<div className="content-header"> <div className="content-header">
{renderBreadcrumbs()} {renderBreadcrumbs()}
<Tabs value={activeTab} onChange={handleTabChange}> <Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.Item <Tabs.Item label={<Code />} value="source" />
label={<Code />}
value="source"
disabled={isImageFile(selectedFile)}
/>
<Tabs.Item label={<Eye />} value="preview" /> <Tabs.Item label={<Eye />} value="preview" />
</Tabs> </Tabs>
</div> </div>
<div className="content-body"> <div className="content-body">
<ContentView <ContentView
activeTab={activeTab} activeTab={activeTab}
content={content}
selectedFile={selectedFile} selectedFile={selectedFile}
onContentChange={onContentChange} content={content}
onSave={onSave} handleContentChange={handleContentChange}
themeType={themeType} handleSave={handleSaveFile}
onLinkClick={onLinkClick} handleLinkClick={handleLinkClick}
lookupFileByName={lookupFileByName}
/> />
</div> </div>
</Grid> </Grid>
</Grid.Container> </Grid.Container>
<CreateFileModal <CreateFileModal onCreateFile={handleCreateFile} />
visible={newFileModalVisible}
onClose={() => setNewFileModalVisible(false)}
onSubmit={handleNewFileSubmit}
fileName={newFileName}
setFileName={setNewFileName}
/>
<DeleteFileModal <DeleteFileModal
visible={deleteFileModalVisible} onDeleteFile={handleDeleteFile}
onClose={() => setDeleteFileModalVisible(false)} selectedFile={selectedFile}
onConfirm={confirmDeleteFile}
fileName={selectedFile}
/>
<CommitMessageModal
visible={commitMessageModalVisible}
onClose={() => setCommitMessageModalVisible(false)}
onSubmit={confirmCommitAndPush}
/> />
<CommitMessageModal onCommitAndPush={handleCommitAndPush} />
</> </>
); );
}; };

View File

@@ -5,14 +5,11 @@ import rehypeKatex from 'rehype-katex';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
import { lookupFileByName } from '../services/api';
const MarkdownPreview = ({ const MarkdownPreview = ({ content, handleLinkClick }) => {
content,
baseUrl,
onLinkClick,
lookupFileByName,
}) => {
const [processedContent, setProcessedContent] = useState(content); const [processedContent, setProcessedContent] = useState(content);
const baseUrl = window.API_BASE_URL;
useEffect(() => { useEffect(() => {
const processContent = async (rawContent) => { const processContent = async (rawContent) => {
@@ -82,7 +79,7 @@ const MarkdownPreview = ({
}; };
processContent(content).then(setProcessedContent); processContent(content).then(setProcessedContent);
}, [content, baseUrl, lookupFileByName]); }, [content, baseUrl]);
const handleImageError = (event) => { const handleImageError = (event) => {
console.error('Failed to load image:', event.target.src); console.error('Failed to load image:', event.target.src);
@@ -125,7 +122,7 @@ const MarkdownPreview = ({
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
onLinkClick(filePath, heading); handleLinkClick(filePath, heading);
}} }}
> >
{children} {children}
@@ -141,7 +138,7 @@ const MarkdownPreview = ({
style={{ color: 'red', textDecoration: 'underline' }} style={{ color: 'red', textDecoration: 'underline' }}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
onLinkClick(fileName); handleLinkClick(fileName);
}} }}
> >
{children} {children}

View File

@@ -1,77 +1,91 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useReducer, useEffect, useCallback, useRef } from 'react';
import { Modal, Spacer, useTheme, Dot, useToasts } from '@geist-ui/core'; import { Modal, Spacer, Dot, useToasts } from '@geist-ui/core';
import { saveUserSettings, fetchUserSettings } from '../services/api'; import { useSettings } from '../contexts/SettingsContext';
import AppearanceSettings from './settings/AppearanceSettings'; import AppearanceSettings from './settings/AppearanceSettings';
import EditorSettings from './settings/EditorSettings'; import EditorSettings from './settings/EditorSettings';
import GitSettings from './settings/GitSettings'; import GitSettings from './settings/GitSettings';
import { useModalContext } from '../contexts/ModalContext';
const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => { const initialState = {
const theme = useTheme(); localSettings: {},
initialSettings: {},
hasUnsavedChanges: false,
};
function settingsReducer(state, action) {
switch (action.type) {
case 'INIT_SETTINGS':
return {
...state,
localSettings: action.payload,
initialSettings: action.payload,
hasUnsavedChanges: false,
};
case 'UPDATE_LOCAL_SETTINGS':
const newLocalSettings = { ...state.localSettings, ...action.payload };
const hasChanges =
JSON.stringify(newLocalSettings) !==
JSON.stringify(state.initialSettings);
return {
...state,
localSettings: newLocalSettings,
hasUnsavedChanges: hasChanges,
};
case 'MARK_SAVED':
return {
...state,
initialSettings: state.localSettings,
hasUnsavedChanges: false,
};
case 'RESET':
return {
...state,
localSettings: state.initialSettings,
hasUnsavedChanges: false,
};
default:
return state;
}
}
const Settings = () => {
const { settings, updateSettings, updateTheme } = useSettings();
const { settingsModalVisible, setSettingsModalVisible } = useModalContext();
const { setToast } = useToasts(); const { setToast } = useToasts();
const [settings, setSettings] = useState({ const [state, dispatch] = useReducer(settingsReducer, initialState);
autoSave: false, const isInitialMount = useRef(true);
gitEnabled: false, const updateThemeTimeoutRef = useRef(null);
gitUrl: '',
gitUser: '',
gitToken: '',
gitAutoCommit: false,
gitCommitMsgTemplate: '',
});
const [themeSettings, setThemeSettings] = useState(currentTheme);
const [originalSettings, setOriginalSettings] = useState({});
const [originalTheme, setOriginalTheme] = useState(currentTheme);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const loadSettings = useCallback(async () => { useEffect(() => {
try { if (isInitialMount.current) {
const userSettings = await fetchUserSettings(1); // Assuming user ID 1 for now isInitialMount.current = false;
const { theme, ...otherSettings } = userSettings.settings; dispatch({ type: 'INIT_SETTINGS', payload: settings });
setSettings(otherSettings);
setThemeSettings(theme);
setOriginalSettings(otherSettings);
setOriginalTheme(theme);
setHasUnsavedChanges(false);
setIsInitialized(true);
} catch (error) {
console.error('Failed to load user settings:', error);
} }
}, [settings]);
const handleInputChange = useCallback((key, value) => {
dispatch({ type: 'UPDATE_LOCAL_SETTINGS', payload: { [key]: value } });
}, []); }, []);
useEffect(() => { const handleThemeChange = useCallback(() => {
if (!isInitialized) { const newTheme = state.localSettings.theme === 'dark' ? 'light' : 'dark';
loadSettings(); dispatch({ type: 'UPDATE_LOCAL_SETTINGS', payload: { theme: newTheme } });
// Debounce the theme update
if (updateThemeTimeoutRef.current) {
clearTimeout(updateThemeTimeoutRef.current);
} }
}, [isInitialized, loadSettings]); updateThemeTimeoutRef.current = setTimeout(() => {
updateTheme(newTheme);
useEffect(() => { }, 0);
const settingsChanged = }, [state.localSettings.theme, updateTheme]);
JSON.stringify(settings) !== JSON.stringify(originalSettings);
const themeChanged = themeSettings !== originalTheme;
setHasUnsavedChanges(settingsChanged || themeChanged);
}, [settings, themeSettings, originalSettings, originalTheme]);
const handleInputChange = (key, value) => {
setSettings((prev) => ({ ...prev, [key]: value }));
};
const handleThemeChange = () => {
const newTheme = themeSettings === 'dark' ? 'light' : 'dark';
setThemeSettings(newTheme);
onThemeChange(newTheme);
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
await saveUserSettings({ await updateSettings(state.localSettings);
userId: 1, // Assuming user ID 1 for now dispatch({ type: 'MARK_SAVED' });
settings: { ...settings, theme: themeSettings },
});
setOriginalSettings(settings);
setOriginalTheme(themeSettings);
setHasUnsavedChanges(false);
setToast({ text: 'Settings saved successfully', type: 'success' }); setToast({ text: 'Settings saved successfully', type: 'success' });
onClose(); setSettingsModalVisible(false);
} catch (error) { } catch (error) {
console.error('Failed to save settings:', error); console.error('Failed to save settings:', error);
setToast({ setToast({
@@ -81,36 +95,57 @@ const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => {
} }
}; };
const handleClose = useCallback(() => {
if (state.hasUnsavedChanges) {
updateTheme(state.initialSettings.theme); // Revert theme if not saved
dispatch({ type: 'RESET' });
}
setSettingsModalVisible(false);
}, [
state.hasUnsavedChanges,
state.initialSettings.theme,
updateTheme,
setSettingsModalVisible,
]);
useEffect(() => {
return () => {
if (updateThemeTimeoutRef.current) {
clearTimeout(updateThemeTimeoutRef.current);
}
};
}, []);
return ( return (
<Modal visible={visible} onClose={onClose}> <Modal visible={settingsModalVisible} onClose={handleClose}>
<Modal.Title> <Modal.Title>
Settings Settings
{hasUnsavedChanges && ( {state.hasUnsavedChanges && (
<Dot type="warning" style={{ marginLeft: '8px' }} /> <Dot type="warning" style={{ marginLeft: '8px' }} />
)} )}
</Modal.Title> </Modal.Title>
<Modal.Content> <Modal.Content>
<AppearanceSettings <AppearanceSettings
themeSettings={themeSettings} themeSettings={state.localSettings.theme}
onThemeChange={handleThemeChange} onThemeChange={handleThemeChange}
/> />
<Spacer h={1} /> <Spacer h={1} />
<EditorSettings <EditorSettings
autoSave={settings.autoSave} autoSave={state.localSettings.autoSave}
onAutoSaveChange={(value) => handleInputChange('autoSave', value)} onAutoSaveChange={(value) => handleInputChange('autoSave', value)}
/> />
<Spacer h={1} /> <Spacer h={1} />
<GitSettings <GitSettings
gitEnabled={settings.gitEnabled} gitEnabled={state.localSettings.gitEnabled}
gitUrl={settings.gitUrl} gitUrl={state.localSettings.gitUrl}
gitUser={settings.gitUser} gitUser={state.localSettings.gitUser}
gitToken={settings.gitToken} gitToken={state.localSettings.gitToken}
gitAutoCommit={settings.gitAutoCommit} gitAutoCommit={state.localSettings.gitAutoCommit}
gitCommitMsgTemplate={settings.gitCommitMsgTemplate} gitCommitMsgTemplate={state.localSettings.gitCommitMsgTemplate}
onInputChange={handleInputChange} onInputChange={handleInputChange}
/> />
</Modal.Content> </Modal.Content>
<Modal.Action passive onClick={onClose}> <Modal.Action passive onClick={handleClose}>
Cancel Cancel
</Modal.Action> </Modal.Action>
<Modal.Action onClick={handleSubmit}>Save Changes</Modal.Action> <Modal.Action onClick={handleSubmit}>Save Changes</Modal.Action>

View File

@@ -1,16 +1,25 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Modal, Input } from '@geist-ui/core'; import { Modal, Input } from '@geist-ui/core';
import { useModalContext } from '../../contexts/ModalContext';
const CommitMessageModal = ({ visible, onClose, onSubmit }) => { const CommitMessageModal = ({ onCommitAndPush }) => {
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const { commitMessageModalVisible, setCommitMessageModalVisible } =
useModalContext();
const handleSubmit = () => { const handleSubmit = async () => {
onSubmit(message); if (message) {
setMessage(''); await onCommitAndPush(message);
setMessage('');
setCommitMessageModalVisible(false);
}
}; };
return ( return (
<Modal visible={visible} onClose={onClose}> <Modal
visible={commitMessageModalVisible}
onClose={() => setCommitMessageModalVisible(false)}
>
<Modal.Title>Enter Commit Message</Modal.Title> <Modal.Title>Enter Commit Message</Modal.Title>
<Modal.Content> <Modal.Content>
<Input <Input
@@ -20,7 +29,7 @@ const CommitMessageModal = ({ visible, onClose, onSubmit }) => {
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
/> />
</Modal.Content> </Modal.Content>
<Modal.Action passive onClick={onClose}> <Modal.Action passive onClick={() => setCommitMessageModalVisible(false)}>
Cancel Cancel
</Modal.Action> </Modal.Action>
<Modal.Action onClick={handleSubmit}>Commit</Modal.Action> <Modal.Action onClick={handleSubmit}>Commit</Modal.Action>

View File

@@ -1,15 +1,24 @@
import React from 'react'; import React, { useState } from 'react';
import { Modal, Input } from '@geist-ui/core'; import { Modal, Input } from '@geist-ui/core';
import { useModalContext } from '../../contexts/ModalContext';
const CreateFileModal = ({ onCreateFile }) => {
const [fileName, setFileName] = useState('');
const { newFileModalVisible, setNewFileModalVisible } = useModalContext();
const handleSubmit = async () => {
if (fileName) {
await onCreateFile(fileName);
setFileName('');
setNewFileModalVisible(false);
}
};
const CreateFileModal = ({
visible,
onClose,
onSubmit,
fileName,
setFileName,
}) => {
return ( return (
<Modal visible={visible} onClose={onClose}> <Modal
visible={newFileModalVisible}
onClose={() => setNewFileModalVisible(false)}
>
<Modal.Title>Create New File</Modal.Title> <Modal.Title>Create New File</Modal.Title>
<Modal.Content> <Modal.Content>
<Input <Input
@@ -19,10 +28,10 @@ const CreateFileModal = ({
onChange={(e) => setFileName(e.target.value)} onChange={(e) => setFileName(e.target.value)}
/> />
</Modal.Content> </Modal.Content>
<Modal.Action passive onClick={onClose}> <Modal.Action passive onClick={() => setNewFileModalVisible(false)}>
Cancel Cancel
</Modal.Action> </Modal.Action>
<Modal.Action onClick={onSubmit}>Create</Modal.Action> <Modal.Action onClick={handleSubmit}>Create</Modal.Action>
</Modal> </Modal>
); );
}; };

View File

@@ -1,17 +1,29 @@
import React from 'react'; import React from 'react';
import { Modal, Text } from '@geist-ui/core'; import { Modal, Text } from '@geist-ui/core';
import { useModalContext } from '../../contexts/ModalContext';
const DeleteFileModal = ({ onDeleteFile, selectedFile }) => {
const { deleteFileModalVisible, setDeleteFileModalVisible } =
useModalContext();
const handleConfirm = async () => {
await onDeleteFile(selectedFile);
setDeleteFileModalVisible(false);
};
const DeleteFileModal = ({ visible, onClose, onConfirm, fileName }) => {
return ( return (
<Modal visible={visible} onClose={onClose}> <Modal
visible={deleteFileModalVisible}
onClose={() => setDeleteFileModalVisible(false)}
>
<Modal.Title>Delete File</Modal.Title> <Modal.Title>Delete File</Modal.Title>
<Modal.Content> <Modal.Content>
<Text>Are you sure you want to delete "{fileName}"?</Text> <Text>Are you sure you want to delete "{selectedFile}"?</Text>
</Modal.Content> </Modal.Content>
<Modal.Action passive onClick={onClose}> <Modal.Action passive onClick={() => setDeleteFileModalVisible(false)}>
Cancel Cancel
</Modal.Action> </Modal.Action>
<Modal.Action onClick={onConfirm}>Delete</Modal.Action> <Modal.Action onClick={handleConfirm}>Delete</Modal.Action>
</Modal> </Modal>
); );
}; };

View File

@@ -0,0 +1,28 @@
import React, { createContext, useContext, useState } from 'react';
const ModalContext = createContext();
export const ModalProvider = ({ children }) => {
const [newFileModalVisible, setNewFileModalVisible] = useState(false);
const [deleteFileModalVisible, setDeleteFileModalVisible] = useState(false);
const [commitMessageModalVisible, setCommitMessageModalVisible] =
useState(false);
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
const value = {
newFileModalVisible,
setNewFileModalVisible,
deleteFileModalVisible,
setDeleteFileModalVisible,
commitMessageModalVisible,
setCommitMessageModalVisible,
settingsModalVisible,
setSettingsModalVisible,
};
return (
<ModalContext.Provider value={value}>{children}</ModalContext.Provider>
);
};
export const useModalContext = () => useContext(ModalContext);

View File

@@ -0,0 +1,69 @@
import React, {
createContext,
useState,
useContext,
useEffect,
useMemo,
} from 'react';
import { fetchUserSettings, saveUserSettings } from '../services/api';
import { DEFAULT_SETTINGS } from '../utils/constants';
const SettingsContext = createContext();
export const useSettings = () => useContext(SettingsContext);
export const SettingsProvider = ({ children }) => {
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadSettings = async () => {
try {
const userSettings = await fetchUserSettings(1);
setSettings(userSettings.settings);
} catch (error) {
console.error('Failed to load user settings:', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const updateSettings = async (newSettings) => {
try {
await saveUserSettings({
userId: 1,
settings: newSettings,
});
setSettings(newSettings);
} catch (error) {
console.error('Failed to save settings:', error);
throw error;
}
};
const updateTheme = (newTheme) => {
setSettings((prevSettings) => ({
...prevSettings,
theme: newTheme,
}));
};
const contextValue = useMemo(
() => ({
settings,
updateSettings,
updateTheme,
loading,
}),
[settings, loading]
);
return (
<SettingsContext.Provider value={contextValue}>
{children}
</SettingsContext.Provider>
);
};

View File

@@ -0,0 +1,54 @@
import { useState, useCallback, useEffect } from 'react';
import { fetchFileContent } from '../services/api';
import { isImageFile } from '../utils/fileHelpers';
import { DEFAULT_FILE } from '../utils/constants';
export const useFileContent = (selectedFile) => {
const [content, setContent] = useState(DEFAULT_FILE.content);
const [originalContent, setOriginalContent] = useState(DEFAULT_FILE.content);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const loadFileContent = useCallback(async (filePath) => {
try {
let newContent;
if (filePath === DEFAULT_FILE.path) {
newContent = DEFAULT_FILE.content;
} else if (!isImageFile(filePath)) {
newContent = await fetchFileContent(filePath);
} else {
newContent = ''; // Set empty content for image files
}
setContent(newContent);
setOriginalContent(newContent);
setHasUnsavedChanges(false);
} catch (err) {
console.error('Error loading file content:', err);
setContent(''); // Set empty content on error
setOriginalContent('');
setHasUnsavedChanges(false);
}
}, []);
useEffect(() => {
if (selectedFile) {
loadFileContent(selectedFile);
}
}, [selectedFile, loadFileContent]);
const handleContentChange = useCallback(
(newContent) => {
setContent(newContent);
setHasUnsavedChanges(newContent !== originalContent);
},
[originalContent]
);
return {
content,
setContent,
hasUnsavedChanges,
setHasUnsavedChanges,
loadFileContent,
handleContentChange,
};
};

View File

@@ -0,0 +1,21 @@
import { useState, useEffect, useCallback } from 'react';
import { fetchFileList } from '../services/api';
export const useFileList = () => {
const [files, setFiles] = useState([]);
const loadFileList = useCallback(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);
}
}, []);
return { files, loadFileList };
};

View File

@@ -1,138 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import { useToasts } from '@geist-ui/core';
import {
fetchFileList,
fetchFileContent,
saveFileContent,
pullChanges,
lookupFileByName,
} from '../services/api';
import { isImageFile } from '../utils/fileHelpers';
const DEFAULT_FILE = {
name: 'New File.md',
path: 'New File.md',
content: '# Welcome to NovaMD\n\nStart editing here!',
};
const useFileManagement = (gitEnabled = false) => {
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();
const loadFileList = useCallback(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.');
}
}, []);
const pullLatestChanges = useCallback(async () => {
if (gitEnabled) {
try {
await pullChanges();
setToast({
text: 'Latest changes pulled successfully',
type: 'success',
});
await loadFileList(); // Reload file list after pulling changes
} catch (error) {
console.error('Failed to pull latest changes:', error);
setToast({
text: 'Failed to pull latest changes: ' + error.message,
type: 'error',
});
}
}
}, [gitEnabled, loadFileList, setToast]);
useEffect(() => {
const initializeFileSystem = async () => {
if (gitEnabled) {
await pullLatestChanges();
} else {
await loadFileList();
}
};
initializeFileSystem();
}, [gitEnabled]);
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 {
if (!isImageFile(filePath)) {
const fileContent = await fetchFileContent(filePath);
setContent(fileContent);
} else {
setContent(''); // Set empty content for image files
}
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) {
await loadFileList();
}
} catch (error) {
console.error('Error saving file:', error);
setToast({
text: 'Failed to save file. Please try again.',
type: 'error',
});
}
},
[setToast, isNewFile, loadFileList]
);
return {
content,
files,
selectedFile,
isNewFile,
hasUnsavedChanges,
error,
handleFileSelect,
handleContentChange,
handleSave,
pullLatestChanges,
lookupFileByName,
};
};
export default useFileManagement;

View File

@@ -0,0 +1,38 @@
import { useState, useCallback } from 'react';
import { useToasts } from '@geist-ui/core';
import { lookupFileByName } from '../services/api';
import { DEFAULT_FILE } from '../utils/constants';
export const useFileNavigation = () => {
const { setToast } = useToasts();
const [selectedFile, setSelectedFile] = useState(DEFAULT_FILE.path);
const [isNewFile, setIsNewFile] = useState(true);
const handleFileSelect = useCallback((filePath) => {
setSelectedFile(filePath);
setIsNewFile(filePath === DEFAULT_FILE.path);
}, []);
const handleLinkClick = useCallback(
async (filename) => {
try {
const filePaths = await lookupFileByName(filename);
if (filePaths.length >= 1) {
handleFileSelect(filePaths[0]);
} else {
setToast({ text: `File "${filename}" not found`, type: 'error' });
}
} catch (error) {
console.error('Error looking up file:', error);
setToast({
text: 'Failed to lookup file.',
type: 'error',
});
}
},
[handleFileSelect, setToast]
);
return { handleLinkClick, selectedFile, isNewFile, handleFileSelect };
};

View File

@@ -0,0 +1,54 @@
import { useCallback } from 'react';
import { saveFileContent, deleteFile } from '../services/api';
import { useToasts } from '@geist-ui/core';
export const useFileOperations = () => {
const { setToast } = useToasts();
const handleSave = useCallback(
async (filePath, content) => {
try {
await saveFileContent(filePath, content);
setToast({ text: 'File saved successfully', type: 'success' });
return true;
} catch (error) {
console.error('Error saving file:', error);
setToast({ text: 'Failed to save file', type: 'error' });
return false;
}
},
[setToast]
);
const handleDelete = useCallback(
async (filePath) => {
try {
await deleteFile(filePath);
setToast({ text: 'File deleted successfully', type: 'success' });
return true;
} catch (error) {
setToast({ text: `Error deleting file`, type: 'error' });
console.error('Error deleting file:', error);
return false;
}
},
[setToast]
);
const handleCreate = useCallback(
async (fileName, initialContent = '') => {
try {
await saveFileContent(fileName, initialContent);
setToast({ text: 'File created successfully', type: 'success' });
return true;
} catch (error) {
setToast({ text: `Error creating new file`, type: 'error' });
console.error('Error creating new file:', error);
return false;
}
},
[setToast]
);
return { handleSave, handleDelete, handleCreate };
};

View File

@@ -0,0 +1,38 @@
import { useCallback } from 'react';
import { pullChanges, commitAndPush } from '../services/api';
export const useGitOperations = (gitEnabled) => {
const handlePull = useCallback(async () => {
if (!gitEnabled) return false;
try {
await pullChanges();
setToast({ text: 'Successfully pulled latest changes', type: 'success' });
return true;
} catch (error) {
console.error('Failed to pull latest changes:', error);
setToast({ text: 'Failed to pull latest changes', type: 'error' });
return false;
}
}, [gitEnabled]);
const handleCommitAndPush = useCallback(
async (message) => {
if (!gitEnabled) return false;
try {
await commitAndPush(message);
setToast({
text: 'Successfully committed and pushed changes',
type: 'success',
});
return true;
} catch (error) {
console.error('Failed to commit and push changes:', error);
setToast({ text: 'Failed to commit and push changes', type: 'error' });
return false;
}
},
[gitEnabled]
);
return { handlePull, handleCommitAndPush };
};

View File

@@ -0,0 +1,48 @@
export const API_BASE_URL = window.API_BASE_URL;
export const THEMES = {
LIGHT: 'light',
DARK: 'dark',
};
export const FILE_ACTIONS = {
CREATE: 'create',
DELETE: 'delete',
RENAME: 'rename',
};
export const MODAL_TYPES = {
NEW_FILE: 'newFile',
DELETE_FILE: 'deleteFile',
COMMIT_MESSAGE: 'commitMessage',
};
export const IMAGE_EXTENSIONS = [
'.jpg',
'.jpeg',
'.png',
'.gif',
'.webp',
'.svg',
];
export const DEFAULT_SETTINGS = {
theme: THEMES.LIGHT,
autoSave: false,
gitEnabled: false,
gitUrl: '',
gitUser: '',
gitToken: '',
gitAutoCommit: false,
gitCommitMsgTemplate: 'Update ${filename}',
};
export const DEFAULT_FILE = {
name: 'New File.md',
path: 'New File.md',
content: '# Welcome to NovaMD\n\nStart editing here!',
};
export const MARKDOWN_REGEX = {
WIKILINK: /(!?)\[\[(.*?)\]\]/g,
};

View File

@@ -1,4 +1,5 @@
import { IMAGE_EXTENSIONS } from './constants';
export const isImageFile = (filePath) => { export const isImageFile = (filePath) => {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']; return IMAGE_EXTENSIONS.some((ext) => filePath.toLowerCase().endsWith(ext));
return imageExtensions.some((ext) => filePath.toLowerCase().endsWith(ext));
}; };