@@ -33,18 +47,12 @@ const ContentView = ({
return activeTab === 'source' ? (
) : (
-
+
);
};
diff --git a/frontend/src/components/Editor.js b/frontend/src/components/Editor.js
index 4b7ee76..1491f8e 100644
--- a/frontend/src/components/Editor.js
+++ b/frontend/src/components/Editor.js
@@ -5,14 +5,16 @@ import { EditorView, keymap } from '@codemirror/view';
import { markdown } from '@codemirror/lang-markdown';
import { defaultKeymap } from '@codemirror/commands';
import { oneDark } from '@codemirror/theme-one-dark';
+import { useSettings } from '../contexts/SettingsContext';
-const Editor = ({ content, onChange, onSave, filePath, themeType }) => {
+const Editor = ({ content, handleContentChange, handleSave, selectedFile }) => {
+ const { settings } = useSettings();
const editorRef = useRef();
const viewRef = useRef();
useEffect(() => {
- const handleSave = (view) => {
- onSave(filePath, view.state.doc.toString());
+ const handleEditorSave = (view) => {
+ handleSave(selectedFile, view.state.doc.toString());
return true;
};
@@ -25,12 +27,12 @@ const Editor = ({ content, onChange, onSave, filePath, themeType }) => {
overflow: 'auto',
},
'.cm-gutters': {
- backgroundColor: themeType === 'dark' ? '#1e1e1e' : '#f5f5f5',
- color: themeType === 'dark' ? '#858585' : '#999',
+ backgroundColor: settings.theme === 'dark' ? '#1e1e1e' : '#f5f5f5',
+ color: settings.theme === 'dark' ? '#858585' : '#999',
border: 'none',
},
'.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([
{
key: 'Ctrl-s',
- run: handleSave,
+ run: handleEditorSave,
preventDefault: true,
},
]),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
- onChange(update.state.doc.toString());
+ handleContentChange(update.state.doc.toString());
}
}),
theme,
- themeType === 'dark' ? oneDark : [],
+ settings.theme === 'dark' ? oneDark : [],
],
});
@@ -68,7 +70,7 @@ const Editor = ({ content, onChange, onSave, filePath, themeType }) => {
return () => {
view.destroy();
};
- }, [filePath, themeType]);
+ }, [settings.theme, handleContentChange]);
useEffect(() => {
if (viewRef.current && content !== viewRef.current.state.doc.toString()) {
diff --git a/frontend/src/components/FileActions.js b/frontend/src/components/FileActions.js
index 6a54f46..78eaf4b 100644
--- a/frontend/src/components/FileActions.js
+++ b/frontend/src/components/FileActions.js
@@ -1,16 +1,21 @@
import React from 'react';
import { Button, Tooltip, ButtonGroup, Spacer } from '@geist-ui/core';
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 (
@@ -18,7 +23,7 @@ const FileActions = ({
icon={}
auto
scale={2 / 3}
- onClick={onCreateFile}
+ onClick={handleCreateFile}
px={0.6}
/>
@@ -31,7 +36,7 @@ const FileActions = ({
icon={}
auto
scale={2 / 3}
- onClick={onDeleteFile}
+ onClick={handleDeleteFile}
disabled={!selectedFile}
type="error"
px={0.6}
@@ -39,24 +44,28 @@ const FileActions = ({
}
auto
scale={2 / 3}
- onClick={onPull}
- disabled={!gitEnabled}
+ onClick={handlePullChanges}
+ disabled={!settings.gitEnabled}
px={0.6}
/>
}
auto
scale={2 / 3}
- onClick={onCommitAndPush}
- disabled={!gitEnabled || gitAutoCommit}
+ onClick={handleCommitAndPush}
+ disabled={!settings.gitEnabled || settings.gitAutoCommit}
px={0.6}
/>
diff --git a/frontend/src/components/FileTree.js b/frontend/src/components/FileTree.js
index 3eb5bcb..682da34 100644
--- a/frontend/src/components/FileTree.js
+++ b/frontend/src/components/FileTree.js
@@ -1,45 +1,27 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { Tree } from '@geist-ui/core';
import { File, Folder, Image } from '@geist-ui/icons';
+import { isImageFile } from '../utils/fileHelpers';
-const FileTree = ({
- files = [],
- onFileSelect = () => {},
- selectedFile = null,
-}) => {
+const FileTree = ({ files, handleFileSelect }) => {
if (files.length === 0) {
return No files to display
;
}
- const handleSelect = (filePath) => {
- onFileSelect(filePath);
- };
-
- const renderLabel = (node) => {
- const path = node.extra;
- return (
-
- {node.name}
-
- );
- };
-
- 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 ;
- return isImageFile(name) ? : ;
- };
+ const renderIcon = useMemo(
+ () =>
+ ({ type, name }) => {
+ if (type === 'directory') return ;
+ return isImageFile(name) ? : ;
+ },
+ []
+ );
return (
handleFileSelect(filePath)}
renderIcon={renderIcon}
- renderLabel={renderLabel}
/>
);
};
diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js
index cbac3aa..1406805 100644
--- a/frontend/src/components/Header.js
+++ b/frontend/src/components/Header.js
@@ -1,13 +1,13 @@
-import React, { useState } from 'react';
+import React from 'react';
import { Page, Text, User, Button, Spacer } from '@geist-ui/core';
import { Settings as SettingsIcon } from '@geist-ui/icons';
import Settings from './Settings';
+import { useModalContext } from '../contexts/ModalContext';
-const Header = ({ currentTheme, onThemeChange }) => {
- const [settingsVisible, setSettingsVisible] = useState(false);
+const Header = () => {
+ const { setSettingsModalVisible } = useModalContext();
- const openSettings = () => setSettingsVisible(true);
- const closeSettings = () => setSettingsVisible(false);
+ const openSettings = () => setSettingsModalVisible(true);
return (
@@ -16,12 +16,7 @@ const Header = ({ currentTheme, onThemeChange }) => {
} onClick={openSettings} />
-
+
);
};
diff --git a/frontend/src/components/MainContent.js b/frontend/src/components/MainContent.js
index 91beee6..1af82a4 100644
--- a/frontend/src/components/MainContent.js
+++ b/frontend/src/components/MainContent.js
@@ -1,132 +1,79 @@
-import React, { useState, useEffect } from 'react';
-import {
- Grid,
- Breadcrumbs,
- Tabs,
- Dot,
- useTheme,
- useToasts,
-} from '@geist-ui/core';
+import React, { useState, useCallback, useEffect } from 'react';
+import { Breadcrumbs, Grid, Tabs, Dot } from '@geist-ui/core';
import { Code, Eye } from '@geist-ui/icons';
-import FileTree from './FileTree';
+
import FileActions from './FileActions';
+import FileTree from './FileTree';
+import ContentView from './ContentView';
import CreateFileModal from './modals/CreateFileModal';
import DeleteFileModal from './modals/DeleteFileModal';
import CommitMessageModal from './modals/CommitMessageModal';
-import ContentView from './ContentView';
-import { commitAndPush, saveFileContent, deleteFile } from '../services/api';
-import { isImageFile } from '../utils/fileHelpers';
-const MainContent = ({
- content,
- files,
- selectedFile,
- hasUnsavedChanges,
- onFileSelect,
- onContentChange,
- onSave,
- settings,
- pullLatestChanges,
- onLinkClick,
- lookupFileByName,
-}) => {
+import { useFileContent } from '../hooks/useFileContent';
+import { useFileList } from '../hooks/useFileList';
+import { useFileOperations } from '../hooks/useFileOperations';
+import { useGitOperations } from '../hooks/useGitOperations';
+import { useFileNavigation } from '../hooks/useFileNavigation';
+import { useSettings } from '../contexts/SettingsContext';
+
+const MainContent = () => {
const [activeTab, setActiveTab] = useState('source');
- const { type: themeType } = useTheme();
- const { setToast } = useToasts();
- const [newFileModalVisible, setNewFileModalVisible] = useState(false);
- const [newFileName, setNewFileName] = useState('');
- const [deleteFileModalVisible, setDeleteFileModalVisible] = useState(false);
- const [commitMessageModalVisible, setCommitMessageModalVisible] =
- useState(false);
+ const { settings } = useSettings();
+ const { files, loadFileList } = useFileList();
+ const { handleLinkClick, selectedFile, handleFileSelect } =
+ useFileNavigation();
+ const {
+ content,
+ hasUnsavedChanges,
+ setHasUnsavedChanges,
+ handleContentChange,
+ } = useFileContent(selectedFile);
+ const { handleSave, handleCreate, handleDelete } = useFileOperations();
+ const { handleCommitAndPush, handlePull } = useGitOperations();
useEffect(() => {
- if (isImageFile(selectedFile)) {
- setActiveTab('preview');
- }
- }, [selectedFile]);
+ loadFileList();
+ }, [settings.gitEnabled]);
const handleTabChange = (value) => {
- if (!isImageFile(selectedFile) || value === 'preview') {
- setActiveTab(value);
- }
+ setActiveTab(value);
};
- const handlePull = async () => {
- try {
- await pullLatestChanges();
- setToast({ text: 'Successfully pulled latest changes', type: 'success' });
- } catch (error) {
- 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',
- });
+ const handleSaveFile = useCallback(
+ async (filePath, content) => {
+ const success = await handleSave(filePath, content);
+ if (success) {
+ setHasUnsavedChanges(false);
}
- }
- setNewFileModalVisible(false);
- setNewFileName('');
- };
+ return success;
+ },
+ [handleSave, setHasUnsavedChanges]
+ );
- const handleDeleteFile = () => {
- setDeleteFileModalVisible(true);
- };
+ const handleCreateFile = useCallback(
+ async (fileName) => {
+ const success = await handleCreate(fileName);
+ if (success) {
+ await loadFileList();
+ handleFileSelect(fileName);
+ }
+ },
+ [handleCreate, loadFileList, handleFileSelect]
+ );
- 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 handleDeleteFile = useCallback(
+ async (filePath) => {
+ const success = await handleDelete(filePath);
+ if (success) {
+ await loadFileList();
+ handleFileSelect(null);
+ }
+ },
+ [handleDelete, loadFileList, handleFileSelect]
+ );
const renderBreadcrumbs = () => {
- if (!selectedFile) return null;
+ if (!selectedFile) return ;
const pathParts = selectedFile.split('/');
return (
@@ -148,18 +95,13 @@ const MainContent = ({
@@ -174,46 +116,28 @@ const MainContent = ({
{renderBreadcrumbs()}
- }
- value="source"
- disabled={isImageFile(selectedFile)}
- />
+ } value="source" />
} value="preview" />
-
setNewFileModalVisible(false)}
- onSubmit={handleNewFileSubmit}
- fileName={newFileName}
- setFileName={setNewFileName}
- />
+
setDeleteFileModalVisible(false)}
- onConfirm={confirmDeleteFile}
- fileName={selectedFile}
- />
- setCommitMessageModalVisible(false)}
- onSubmit={confirmCommitAndPush}
+ onDeleteFile={handleDeleteFile}
+ selectedFile={selectedFile}
/>
+
>
);
};
diff --git a/frontend/src/components/MarkdownPreview.js b/frontend/src/components/MarkdownPreview.js
index 3c81b8e..ea12549 100644
--- a/frontend/src/components/MarkdownPreview.js
+++ b/frontend/src/components/MarkdownPreview.js
@@ -5,14 +5,11 @@ import rehypeKatex from 'rehype-katex';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import 'katex/dist/katex.min.css';
+import { lookupFileByName } from '../services/api';
-const MarkdownPreview = ({
- content,
- baseUrl,
- onLinkClick,
- lookupFileByName,
-}) => {
+const MarkdownPreview = ({ content, handleLinkClick }) => {
const [processedContent, setProcessedContent] = useState(content);
+ const baseUrl = window.API_BASE_URL;
useEffect(() => {
const processContent = async (rawContent) => {
@@ -82,7 +79,7 @@ const MarkdownPreview = ({
};
processContent(content).then(setProcessedContent);
- }, [content, baseUrl, lookupFileByName]);
+ }, [content, baseUrl]);
const handleImageError = (event) => {
console.error('Failed to load image:', event.target.src);
@@ -125,7 +122,7 @@ const MarkdownPreview = ({
href="#"
onClick={(e) => {
e.preventDefault();
- onLinkClick(filePath, heading);
+ handleLinkClick(filePath, heading);
}}
>
{children}
@@ -141,7 +138,7 @@ const MarkdownPreview = ({
style={{ color: 'red', textDecoration: 'underline' }}
onClick={(e) => {
e.preventDefault();
- onLinkClick(fileName);
+ handleLinkClick(fileName);
}}
>
{children}
diff --git a/frontend/src/components/Settings.js b/frontend/src/components/Settings.js
index 2ab0c0b..811cc69 100644
--- a/frontend/src/components/Settings.js
+++ b/frontend/src/components/Settings.js
@@ -1,77 +1,91 @@
-import React, { useState, useEffect, useCallback } from 'react';
-import { Modal, Spacer, useTheme, Dot, useToasts } from '@geist-ui/core';
-import { saveUserSettings, fetchUserSettings } from '../services/api';
+import React, { useReducer, useEffect, useCallback, useRef } from 'react';
+import { Modal, Spacer, Dot, useToasts } from '@geist-ui/core';
+import { useSettings } from '../contexts/SettingsContext';
import AppearanceSettings from './settings/AppearanceSettings';
import EditorSettings from './settings/EditorSettings';
import GitSettings from './settings/GitSettings';
+import { useModalContext } from '../contexts/ModalContext';
-const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => {
- const theme = useTheme();
+const initialState = {
+ 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 [settings, setSettings] = useState({
- 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 [state, dispatch] = useReducer(settingsReducer, initialState);
+ const isInitialMount = useRef(true);
+ const updateThemeTimeoutRef = useRef(null);
- const loadSettings = useCallback(async () => {
- try {
- const userSettings = await fetchUserSettings(1); // Assuming user ID 1 for now
- const { theme, ...otherSettings } = userSettings.settings;
- setSettings(otherSettings);
- setThemeSettings(theme);
- setOriginalSettings(otherSettings);
- setOriginalTheme(theme);
- setHasUnsavedChanges(false);
- setIsInitialized(true);
- } catch (error) {
- console.error('Failed to load user settings:', error);
+ 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(() => {
- if (!isInitialized) {
- loadSettings();
+ const handleThemeChange = useCallback(() => {
+ const newTheme = state.localSettings.theme === 'dark' ? 'light' : 'dark';
+ dispatch({ type: 'UPDATE_LOCAL_SETTINGS', payload: { theme: newTheme } });
+
+ // Debounce the theme update
+ if (updateThemeTimeoutRef.current) {
+ clearTimeout(updateThemeTimeoutRef.current);
}
- }, [isInitialized, loadSettings]);
-
- useEffect(() => {
- const settingsChanged =
- 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);
- };
+ updateThemeTimeoutRef.current = setTimeout(() => {
+ updateTheme(newTheme);
+ }, 0);
+ }, [state.localSettings.theme, updateTheme]);
const handleSubmit = async () => {
try {
- await saveUserSettings({
- userId: 1, // Assuming user ID 1 for now
- settings: { ...settings, theme: themeSettings },
- });
- setOriginalSettings(settings);
- setOriginalTheme(themeSettings);
- setHasUnsavedChanges(false);
+ await updateSettings(state.localSettings);
+ dispatch({ type: 'MARK_SAVED' });
setToast({ text: 'Settings saved successfully', type: 'success' });
- onClose();
+ setSettingsModalVisible(false);
} catch (error) {
console.error('Failed to save settings:', error);
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 (
-
+
Settings
- {hasUnsavedChanges && (
+ {state.hasUnsavedChanges && (
)}
handleInputChange('autoSave', value)}
/>
-
+
Cancel
Save Changes
diff --git a/frontend/src/components/modals/CommitMessageModal.js b/frontend/src/components/modals/CommitMessageModal.js
index db0ecf5..b2a6720 100644
--- a/frontend/src/components/modals/CommitMessageModal.js
+++ b/frontend/src/components/modals/CommitMessageModal.js
@@ -1,16 +1,25 @@
import React, { useState } from 'react';
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 { commitMessageModalVisible, setCommitMessageModalVisible } =
+ useModalContext();
- const handleSubmit = () => {
- onSubmit(message);
- setMessage('');
+ const handleSubmit = async () => {
+ if (message) {
+ await onCommitAndPush(message);
+ setMessage('');
+ setCommitMessageModalVisible(false);
+ }
};
return (
-
+ setCommitMessageModalVisible(false)}
+ >
Enter Commit Message
{
onChange={(e) => setMessage(e.target.value)}
/>
-
+ setCommitMessageModalVisible(false)}>
Cancel
Commit
diff --git a/frontend/src/components/modals/CreateFileModal.js b/frontend/src/components/modals/CreateFileModal.js
index 09dbaa1..1666dda 100644
--- a/frontend/src/components/modals/CreateFileModal.js
+++ b/frontend/src/components/modals/CreateFileModal.js
@@ -1,15 +1,24 @@
-import React from 'react';
+import React, { useState } from 'react';
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 (
-
+ setNewFileModalVisible(false)}
+ >
Create New File
setFileName(e.target.value)}
/>
-
+ setNewFileModalVisible(false)}>
Cancel
- Create
+ Create
);
};
diff --git a/frontend/src/components/modals/DeleteFileModal.js b/frontend/src/components/modals/DeleteFileModal.js
index 7221669..c267466 100644
--- a/frontend/src/components/modals/DeleteFileModal.js
+++ b/frontend/src/components/modals/DeleteFileModal.js
@@ -1,17 +1,29 @@
import React from 'react';
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 (
-
+ setDeleteFileModalVisible(false)}
+ >
Delete File
- Are you sure you want to delete "{fileName}"?
+ Are you sure you want to delete "{selectedFile}"?
-
+ setDeleteFileModalVisible(false)}>
Cancel
- Delete
+ Delete
);
};
diff --git a/frontend/src/contexts/ModalContext.js b/frontend/src/contexts/ModalContext.js
new file mode 100644
index 0000000..697bdc7
--- /dev/null
+++ b/frontend/src/contexts/ModalContext.js
@@ -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 (
+ {children}
+ );
+};
+
+export const useModalContext = () => useContext(ModalContext);
diff --git a/frontend/src/contexts/SettingsContext.js b/frontend/src/contexts/SettingsContext.js
new file mode 100644
index 0000000..3267649
--- /dev/null
+++ b/frontend/src/contexts/SettingsContext.js
@@ -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 (
+
+ {children}
+
+ );
+};
diff --git a/frontend/src/hooks/useFileContent.js b/frontend/src/hooks/useFileContent.js
new file mode 100644
index 0000000..ba12b22
--- /dev/null
+++ b/frontend/src/hooks/useFileContent.js
@@ -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,
+ };
+};
diff --git a/frontend/src/hooks/useFileList.js b/frontend/src/hooks/useFileList.js
new file mode 100644
index 0000000..0c5a285
--- /dev/null
+++ b/frontend/src/hooks/useFileList.js
@@ -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 };
+};
diff --git a/frontend/src/hooks/useFileManagement.js b/frontend/src/hooks/useFileManagement.js
deleted file mode 100644
index 6d7ccaf..0000000
--- a/frontend/src/hooks/useFileManagement.js
+++ /dev/null
@@ -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;
diff --git a/frontend/src/hooks/useFileNavigation.js b/frontend/src/hooks/useFileNavigation.js
new file mode 100644
index 0000000..9bda494
--- /dev/null
+++ b/frontend/src/hooks/useFileNavigation.js
@@ -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 };
+};
diff --git a/frontend/src/hooks/useFileOperations.js b/frontend/src/hooks/useFileOperations.js
new file mode 100644
index 0000000..ca64b84
--- /dev/null
+++ b/frontend/src/hooks/useFileOperations.js
@@ -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 };
+};
diff --git a/frontend/src/hooks/useGitOperations.js b/frontend/src/hooks/useGitOperations.js
new file mode 100644
index 0000000..561b9b1
--- /dev/null
+++ b/frontend/src/hooks/useGitOperations.js
@@ -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 };
+};
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
new file mode 100644
index 0000000..6b7f0a4
--- /dev/null
+++ b/frontend/src/utils/constants.js
@@ -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,
+};
diff --git a/frontend/src/utils/fileHelpers.js b/frontend/src/utils/fileHelpers.js
index f2e95a5..7d0c1a0 100644
--- a/frontend/src/utils/fileHelpers.js
+++ b/frontend/src/utils/fileHelpers.js
@@ -1,4 +1,5 @@
+import { IMAGE_EXTENSIONS } from './constants';
+
export const isImageFile = (filePath) => {
- const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
- return imageExtensions.some((ext) => filePath.toLowerCase().endsWith(ext));
+ return IMAGE_EXTENSIONS.some((ext) => filePath.toLowerCase().endsWith(ext));
};