mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-06 07:54:22 +00:00
Merge pull request #2 from LordMathis/feat/react-context
Feat/react context
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|
||||||
const renderLabel = (node) => {
|
|
||||||
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 />;
|
if (type === 'directory') return <Folder />;
|
||||||
return isImageFile(name) ? <Image /> : <File />;
|
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}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 handleSaveFile = useCallback(
|
||||||
|
async (filePath, content) => {
|
||||||
|
const success = await handleSave(filePath, content);
|
||||||
|
if (success) {
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
}
|
}
|
||||||
};
|
return success;
|
||||||
|
},
|
||||||
|
[handleSave, setHasUnsavedChanges]
|
||||||
|
);
|
||||||
|
|
||||||
const handlePull = async () => {
|
const handleCreateFile = useCallback(
|
||||||
try {
|
async (fileName) => {
|
||||||
await pullLatestChanges();
|
const success = await handleCreate(fileName);
|
||||||
setToast({ text: 'Successfully pulled latest changes', type: 'success' });
|
if (success) {
|
||||||
} catch (error) {
|
await loadFileList();
|
||||||
setToast({
|
handleFileSelect(fileName);
|
||||||
text: 'Failed to pull changes: ' + error.message,
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[handleCreate, loadFileList, handleFileSelect]
|
||||||
|
);
|
||||||
|
|
||||||
const handleCreateFile = () => {
|
const handleDeleteFile = useCallback(
|
||||||
setNewFileModalVisible(true);
|
async (filePath) => {
|
||||||
};
|
const success = await handleDelete(filePath);
|
||||||
|
if (success) {
|
||||||
const handleNewFileSubmit = async () => {
|
await loadFileList();
|
||||||
if (newFileName) {
|
handleFileSelect(null);
|
||||||
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',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
setNewFileModalVisible(false);
|
[handleDelete, loadFileList, handleFileSelect]
|
||||||
setNewFileName('');
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteFile = () => {
|
|
||||||
setDeleteFileModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDeleteFile = async () => {
|
|
||||||
try {
|
|
||||||
await deleteFile(selectedFile);
|
|
||||||
setToast({ text: 'File deleted successfully', type: 'success' });
|
|
||||||
await pullLatestChanges();
|
|
||||||
onFileSelect(null);
|
|
||||||
} catch (error) {
|
|
||||||
setToast({
|
|
||||||
text: 'Failed to delete file: ' + error.message,
|
|
||||||
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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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: {},
|
||||||
const { setToast } = useToasts();
|
initialSettings: {},
|
||||||
const [settings, setSettings] = useState({
|
hasUnsavedChanges: false,
|
||||||
autoSave: false,
|
};
|
||||||
gitEnabled: false,
|
|
||||||
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 () => {
|
function settingsReducer(state, action) {
|
||||||
try {
|
switch (action.type) {
|
||||||
const userSettings = await fetchUserSettings(1); // Assuming user ID 1 for now
|
case 'INIT_SETTINGS':
|
||||||
const { theme, ...otherSettings } = userSettings.settings;
|
return {
|
||||||
setSettings(otherSettings);
|
...state,
|
||||||
setThemeSettings(theme);
|
localSettings: action.payload,
|
||||||
setOriginalSettings(otherSettings);
|
initialSettings: action.payload,
|
||||||
setOriginalTheme(theme);
|
hasUnsavedChanges: false,
|
||||||
setHasUnsavedChanges(false);
|
};
|
||||||
setIsInitialized(true);
|
case 'UPDATE_LOCAL_SETTINGS':
|
||||||
} catch (error) {
|
const newLocalSettings = { ...state.localSettings, ...action.payload };
|
||||||
console.error('Failed to load user settings:', error);
|
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 [state, dispatch] = useReducer(settingsReducer, initialState);
|
||||||
|
const isInitialMount = useRef(true);
|
||||||
|
const updateThemeTimeoutRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitialMount.current) {
|
||||||
|
isInitialMount.current = false;
|
||||||
|
dispatch({ type: 'INIT_SETTINGS', payload: settings });
|
||||||
|
}
|
||||||
|
}, [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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
await onCommitAndPush(message);
|
||||||
setMessage('');
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
28
frontend/src/contexts/ModalContext.js
Normal file
28
frontend/src/contexts/ModalContext.js
Normal 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);
|
||||||
69
frontend/src/contexts/SettingsContext.js
Normal file
69
frontend/src/contexts/SettingsContext.js
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
54
frontend/src/hooks/useFileContent.js
Normal file
54
frontend/src/hooks/useFileContent.js
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
21
frontend/src/hooks/useFileList.js
Normal file
21
frontend/src/hooks/useFileList.js
Normal 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 };
|
||||||
|
};
|
||||||
@@ -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;
|
|
||||||
38
frontend/src/hooks/useFileNavigation.js
Normal file
38
frontend/src/hooks/useFileNavigation.js
Normal 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 };
|
||||||
|
};
|
||||||
54
frontend/src/hooks/useFileOperations.js
Normal file
54
frontend/src/hooks/useFileOperations.js
Normal 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 };
|
||||||
|
};
|
||||||
38
frontend/src/hooks/useGitOperations.js
Normal file
38
frontend/src/hooks/useGitOperations.js
Normal 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 };
|
||||||
|
};
|
||||||
48
frontend/src/utils/constants.js
Normal file
48
frontend/src/utils/constants.js
Normal 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,
|
||||||
|
};
|
||||||
@@ -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));
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user