diff --git a/frontend/src/App.js b/frontend/src/App.js
index 772aeaf..25c669c 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -9,6 +9,22 @@ import './App.scss';
function App() {
const [themeType, setThemeType] = useState('light');
const [userId, setUserId] = useState(1);
+ const [settings, setSettings] = useState({ gitEnabled: false });
+
+ useEffect(() => {
+ const loadUserSettings = async () => {
+ 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,
@@ -19,20 +35,8 @@ function App() {
handleFileSelect,
handleContentChange,
handleSave,
- } = useFileManagement();
-
- useEffect(() => {
- const loadUserSettings = async () => {
- try {
- const settings = await fetchUserSettings(userId);
- setThemeType(settings.settings.theme);
- } catch (error) {
- console.error('Failed to load user settings:', error);
- }
- };
-
- loadUserSettings();
- }, [userId]);
+ pullLatestChanges,
+ } = useFileManagement(settings.gitEnabled);
const setTheme = (newTheme) => {
setThemeType(newTheme);
@@ -43,7 +47,7 @@ function App() {
-
+
diff --git a/frontend/src/App.scss b/frontend/src/App.scss
index 3859d21..6dc8740 100644
--- a/frontend/src/App.scss
+++ b/frontend/src/App.scss
@@ -6,13 +6,25 @@ $navbar-height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
- padding: 0 $padding;
- height: $navbar-height;
}
.sidebar {
- overflow-y: auto;
+ overflow: auto;
padding: $padding;
+
+ .file-tree-container {
+ width: 100%;
+
+ &>div {
+ padding: 0;
+ margin: 0;
+ }
+ }
+
+}
+
+.page-content {
+ padding: 0 $padding;
}
.main-content {
@@ -25,7 +37,7 @@ $navbar-height: 64px;
display: flex;
justify-content: space-between;
align-items: center;
- padding: $padding;
+ padding: 0 $padding;
.breadcrumbs-container {
flex-grow: 1;
@@ -50,6 +62,7 @@ $navbar-height: 64px;
.setting-group {
margin-bottom: 1rem;
}
+
.setting-item {
display: flex;
justify-content: space-between;
@@ -64,7 +77,8 @@ $navbar-height: 64px;
flex-direction: column;
}
-.editor-container, .markdown-preview {
+.editor-container,
+.markdown-preview {
flex-grow: 1;
overflow-y: auto;
padding: $padding;
@@ -72,7 +86,7 @@ $navbar-height: 64px;
.editor-container {
height: 100%;
-
+
.cm-editor {
height: 100%;
}
@@ -82,6 +96,10 @@ $navbar-height: 64px;
}
}
+.tree {
+ padding-top: $padding;
+}
+
// Geist UI Tree component customization
:global {
.file-tree {
diff --git a/frontend/src/components/FileTree.js b/frontend/src/components/FileTree.js
index ebb2a56..91e4a84 100644
--- a/frontend/src/components/FileTree.js
+++ b/frontend/src/components/FileTree.js
@@ -1,22 +1,28 @@
import React from 'react';
-import { Tree } from '@geist-ui/core';
-import { File, Folder } from '@geist-ui/icons';
+import { Tree, Button, Tooltip, Spacer, ButtonGroup } from '@geist-ui/core';
+import { File, Folder, GitPullRequest, GitCommit, Plus, Trash } from '@geist-ui/icons';
const FileTree = ({
files = [],
onFileSelect = () => {},
- selectedFile = null
+ selectedFile = null,
+ gitEnabled = false,
+ gitAutoCommit = false,
+ onPull = () => {},
+ onCommitAndPush = () => {},
+ onCreateFile = () => {},
+ onDeleteFile = () => {}
}) => {
if (files.length === 0) {
return
No files to display
;
}
const handleSelect = (filePath) => {
- onFileSelect(filePath);
+ onFileSelect(filePath);
};
const renderLabel = (node) => {
- const path = getFilePath(node);
+ const path = node.extra;
return (
{node.name}
@@ -27,12 +33,63 @@ const FileTree = ({
const renderIcon = ({ type }) => type === 'directory' ? : ;
return (
-
+
+
+
+ }
+ auto
+ scale={2/3}
+ onClick={onCreateFile}
+ px={0.6}
+ />
+
+
+
+ }
+ auto
+ scale={2/3}
+ onClick={onDeleteFile}
+ disabled={!selectedFile}
+ type="error"
+ px={0.6}
+ />
+
+
+
+ }
+ auto
+ scale={2/3}
+ onClick={onPull}
+ disabled={!gitEnabled}
+ px={0.6}
+ />
+
+
+
+ }
+ auto
+ scale={2/3}
+ onClick={onCommitAndPush}
+ disabled={!gitEnabled || gitAutoCommit}
+ px={0.6}
+ />
+
+
+
+
);
};
diff --git a/frontend/src/components/MainContent.js b/frontend/src/components/MainContent.js
index fcdb1e5..f0417fe 100644
--- a/frontend/src/components/MainContent.js
+++ b/frontend/src/components/MainContent.js
@@ -1,9 +1,10 @@
import React, { useState } from 'react';
-import { Grid, Breadcrumbs, Tabs, Dot, useTheme } from '@geist-ui/core';
+import { Grid, Breadcrumbs, Tabs, Dot, useTheme, useToasts, Modal, Input, Button } from '@geist-ui/core';
import { Code, Eye } from '@geist-ui/icons';
import Editor from './Editor';
import FileTree from './FileTree';
import MarkdownPreview from './MarkdownPreview';
+import { commitAndPush, saveFileContent, deleteFile } from '../services/api';
const MainContent = ({
content,
@@ -14,9 +15,71 @@ const MainContent = ({
onFileSelect,
onContentChange,
onSave,
+ settings,
+ pullLatestChanges,
}) => {
const [activeTab, setActiveTab] = useState('source');
const { type: themeType } = useTheme();
+ const { setToast } = useToasts();
+ const [newFileModalVisible, setNewFileModalVisible] = useState(false);
+ const [newFileName, setNewFileName] = useState('');
+
+ 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 handleCommitAndPush = async () => {
+ try {
+ const message = prompt('Enter commit message:');
+ if (message) {
+ await commitAndPush(message);
+ setToast({ text: 'Changes committed and pushed successfully', type: 'success' });
+ await pullLatestChanges(); // Pull changes after successful push
+ }
+ } catch (error) {
+ setToast({ text: 'Failed to commit and push 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(); // Refresh file list
+ onFileSelect(newFileName); // Select the new file
+ } catch (error) {
+ setToast({ text: 'Failed to create new file: ' + error.message, type: 'error' });
+ }
+ }
+ setNewFileModalVisible(false);
+ setNewFileName('');
+ };
+
+ const handleDeleteFile = async () => {
+ if (selectedFile) {
+ const confirmDelete = window.confirm(`Are you sure you want to delete "${selectedFile}"?`);
+ if (confirmDelete) {
+ try {
+ await deleteFile(selectedFile);
+ setToast({ text: 'File deleted successfully', type: 'success' });
+ await pullLatestChanges(); // Refresh file list
+ onFileSelect(null); // Deselect the file
+ } catch (error) {
+ setToast({ text: 'Failed to delete file: ' + error.message, type: 'error' });
+ }
+ }
+ }
+ };
const renderBreadcrumbs = () => {
if (!selectedFile) return null;
@@ -34,41 +97,60 @@ const MainContent = ({
};
return (
-
-
- {error ? (
- {error}
- ) : (
-
- )}
-
-
-
- {renderBreadcrumbs()}
-
- } value="source" />
- } value="preview" />
-
-
-
- {activeTab === 'source' ? (
-
+
+
+
+
- ) : (
-
- )}
-
-
-
+
+
+
+
+ {renderBreadcrumbs()}
+
+ } value="source" />
+ } value="preview" />
+
+
+
+ {activeTab === 'source' ? (
+
+ ) : (
+
+ )}
+
+
+
+ setNewFileModalVisible(false)}>
+ Create New File
+
+ setNewFileName(e.target.value)}
+ />
+
+ setNewFileModalVisible(false)}>Cancel
+ Create
+
+ >
);
};
diff --git a/frontend/src/hooks/useFileManagement.js b/frontend/src/hooks/useFileManagement.js
index 688a505..aa38829 100644
--- a/frontend/src/hooks/useFileManagement.js
+++ b/frontend/src/hooks/useFileManagement.js
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { useToasts } from '@geist-ui/core';
-import { fetchFileList, fetchFileContent, saveFileContent } from '../services/api';
+import { fetchFileList, fetchFileContent, saveFileContent, pullChanges } from '../services/api';
const DEFAULT_FILE = {
name: 'New File.md',
@@ -8,7 +8,7 @@ const DEFAULT_FILE = {
content: '# Welcome to NovaMD\n\nStart editing here!'
};
-const useFileManagement = () => {
+const useFileManagement = (gitEnabled = false) => {
const [content, setContent] = useState(DEFAULT_FILE.content);
const [files, setFiles] = useState([]);
const [selectedFile, setSelectedFile] = useState(DEFAULT_FILE.path);
@@ -17,23 +17,41 @@ const useFileManagement = () => {
const [error, setError] = useState(null);
const { setToast } = useToasts();
- useEffect(() => {
- const loadFileList = async () => {
- try {
- const fileList = await fetchFileList();
- if (Array.isArray(fileList)) {
- setFiles(fileList);
- } else {
- throw new Error('File list is not an array');
- }
- } catch (error) {
- console.error('Failed to load file list:', error);
- setError('Failed to load file list. Please try again later.');
+ 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 () => {
+ await pullLatestChanges();
+ await loadFileList();
};
- loadFileList();
- }, []);
+ initializeFileSystem();
+ }, [pullLatestChanges, loadFileList]);
const handleFileSelect = async (filePath) => {
if (hasUnsavedChanges) {
@@ -66,14 +84,13 @@ const useFileManagement = () => {
setIsNewFile(false);
setHasUnsavedChanges(false);
if (isNewFile) {
- const updatedFileList = await fetchFileList();
- setFiles(updatedFileList);
+ await loadFileList();
}
} catch (error) {
console.error('Error saving file:', error);
setToast({ text: 'Failed to save file. Please try again.', type: 'error' });
}
- }, [setToast, isNewFile]);
+ }, [setToast, isNewFile, loadFileList]);
return {
content,
@@ -85,6 +102,7 @@ const useFileManagement = () => {
handleFileSelect,
handleContentChange,
handleSave,
+ pullLatestChanges,
};
};
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index 3a2280e..8a5d13d 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -42,6 +42,21 @@ export const saveFileContent = async (filePath, content) => {
return await response.text();
};
+ export const deleteFile = async (filePath) => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/files/${filePath}`, {
+ method: 'DELETE',
+ });
+ if (!response.ok) {
+ throw new Error('Failed to delete file');
+ }
+ return await response.text();
+ } catch (error) {
+ console.error('Error deleting file:', error);
+ throw error;
+ }
+ };
+
export const fetchUserSettings = async (userId) => {
try {
const response = await fetch(`${API_BASE_URL}/settings?userId=${userId}`);
@@ -75,4 +90,38 @@ export const saveUserSettings = async (settings) => {
console.error('Error saving user settings:', error);
throw error;
}
-};
\ No newline at end of file
+};
+
+export const pullChanges = async () => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/git/pull`, {
+ method: 'POST',
+ });
+ if (!response.ok) {
+ throw new Error('Failed to pull changes');
+ }
+ return await response.json();
+ } catch (error) {
+ console.error('Error pulling changes:', error);
+ throw error;
+ }
+ };
+
+ export const commitAndPush = async (message) => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/git/commit`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ message }),
+ });
+ if (!response.ok) {
+ throw new Error('Failed to commit and push changes');
+ }
+ return await response.json();
+ } catch (error) {
+ console.error('Error committing and pushing changes:', error);
+ throw error;
+ }
+ };
\ No newline at end of file