diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..ab13d32 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,31 @@ +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended" + ], + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "rules": { + "react/prop-types": "off", + "no-unused-vars": "warn" + }, + "settings": { + "react": { + "version": "detect" + } + } + } \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..25579a5 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5" + } + \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index cc8e08f..573202b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,6 @@ }, "keywords": [ "markdown", - "hypermd", "editor" ], "author": "Matúš Námešný", diff --git a/frontend/src/App.js b/frontend/src/App.js index 25c669c..075136d 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -47,7 +47,7 @@ function App() {
- + div { + & > div { padding: 0; margin: 0; } } - } .page-content { @@ -121,4 +120,4 @@ $navbar-height: 64px; font-weight: bold; } } -} \ No newline at end of file +} diff --git a/frontend/src/components/Editor.js b/frontend/src/components/Editor.js index d6f4237..4b7ee76 100644 --- a/frontend/src/components/Editor.js +++ b/frontend/src/components/Editor.js @@ -1,10 +1,10 @@ -import React, { useEffect, useRef } from "react"; -import { basicSetup } from "codemirror"; -import { EditorState } from "@codemirror/state"; -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 React, { useEffect, useRef } from 'react'; +import { basicSetup } from 'codemirror'; +import { EditorState } from '@codemirror/state'; +import { EditorView, keymap } from '@codemirror/view'; +import { markdown } from '@codemirror/lang-markdown'; +import { defaultKeymap } from '@codemirror/commands'; +import { oneDark } from '@codemirror/theme-one-dark'; const Editor = ({ content, onChange, onSave, filePath, themeType }) => { const editorRef = useRef(); @@ -17,20 +17,20 @@ const Editor = ({ content, onChange, onSave, filePath, themeType }) => { }; const theme = EditorView.theme({ - "&": { - height: "100%", - fontSize: "14px", + '&': { + height: '100%', + fontSize: '14px', }, - ".cm-scroller": { - overflow: "auto", + '.cm-scroller': { + overflow: 'auto', }, - ".cm-gutters": { - backgroundColor: themeType === "dark" ? "#1e1e1e" : "#f5f5f5", - color: themeType === "dark" ? "#858585" : "#999", - border: "none", + '.cm-gutters': { + backgroundColor: themeType === 'dark' ? '#1e1e1e' : '#f5f5f5', + color: themeType === 'dark' ? '#858585' : '#999', + border: 'none', }, - ".cm-activeLineGutter": { - backgroundColor: themeType === "dark" ? "#2c313a" : "#e8e8e8", + '.cm-activeLineGutter': { + backgroundColor: themeType === 'dark' ? '#2c313a' : '#e8e8e8', }, }); @@ -41,18 +41,20 @@ const Editor = ({ content, onChange, onSave, filePath, themeType }) => { markdown(), EditorView.lineWrapping, keymap.of(defaultKeymap), - keymap.of([{ - key: "Ctrl-s", - run: handleSave, - preventDefault: true - }]), + keymap.of([ + { + key: 'Ctrl-s', + run: handleSave, + preventDefault: true, + }, + ]), EditorView.updateListener.of((update) => { if (update.docChanged) { onChange(update.state.doc.toString()); } }), theme, - themeType === "dark" ? oneDark : [], + themeType === 'dark' ? oneDark : [], ], }); @@ -71,7 +73,11 @@ const Editor = ({ content, onChange, onSave, filePath, themeType }) => { useEffect(() => { if (viewRef.current && content !== viewRef.current.state.doc.toString()) { viewRef.current.dispatch({ - changes: { from: 0, to: viewRef.current.state.doc.length, insert: content } + changes: { + from: 0, + to: viewRef.current.state.doc.length, + insert: content, + }, }); } }, [content]); @@ -79,4 +85,4 @@ const Editor = ({ content, onChange, onSave, filePath, themeType }) => { return
; }; -export default Editor; \ No newline at end of file +export default Editor; diff --git a/frontend/src/components/FileTree.js b/frontend/src/components/FileTree.js index 91e4a84..b440757 100644 --- a/frontend/src/components/FileTree.js +++ b/frontend/src/components/FileTree.js @@ -1,17 +1,24 @@ import React from 'react'; import { Tree, Button, Tooltip, Spacer, ButtonGroup } from '@geist-ui/core'; -import { File, Folder, GitPullRequest, GitCommit, Plus, Trash } from '@geist-ui/icons'; +import { + File, + Folder, + GitPullRequest, + GitCommit, + Plus, + Trash, +} from '@geist-ui/icons'; -const FileTree = ({ - files = [], - onFileSelect = () => {}, +const FileTree = ({ + files = [], + onFileSelect = () => {}, selectedFile = null, gitEnabled = false, gitAutoCommit = false, onPull = () => {}, onCommitAndPush = () => {}, onCreateFile = () => {}, - onDeleteFile = () => {} + onDeleteFile = () => {}, }) => { if (files.length === 0) { return
No files to display
; @@ -30,26 +37,30 @@ const FileTree = ({ ); }; - const renderIcon = ({ type }) => type === 'directory' ? : ; + const renderIcon = ({ type }) => + type === 'directory' ? : ; return (
- +
); }; @@ -101,9 +130,9 @@ const MainContent = ({
-
- +
{renderBreadcrumbs()} @@ -124,9 +160,9 @@ const MainContent = ({
{activeTab === 'source' ? ( - - setNewFileModalVisible(false)}> + setNewFileModalVisible(false)} + > Create New File - setNewFileName(e.target.value)} /> - setNewFileModalVisible(false)}>Cancel + setNewFileModalVisible(false)}> + Cancel + Create ); }; -export default MainContent; \ No newline at end of file +export default MainContent; diff --git a/frontend/src/components/MarkdownPreview.js b/frontend/src/components/MarkdownPreview.js index 3fc4d79..8f7af2d 100644 --- a/frontend/src/components/MarkdownPreview.js +++ b/frontend/src/components/MarkdownPreview.js @@ -13,8 +13,8 @@ const MarkdownPreview = ({ content }) => { remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]} components={{ - code({node, inline, className, children, ...props}) { - const match = /language-(\w+)/.exec(className || '') + code({ node, inline, className, children, ...props }) { + const match = /language-(\w+)/.exec(className || ''); return !inline && match ? ( { {children} - ) - } + ); + }, }} > {content} @@ -38,4 +38,4 @@ const MarkdownPreview = ({ content }) => { ); }; -export default MarkdownPreview; \ No newline at end of file +export default MarkdownPreview; diff --git a/frontend/src/components/Settings.js b/frontend/src/components/Settings.js index 67205c7..03123e2 100644 --- a/frontend/src/components/Settings.js +++ b/frontend/src/components/Settings.js @@ -1,5 +1,15 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { Modal, Text, Toggle, Input, Spacer, useTheme, Button, Dot, useToasts } from '@geist-ui/core'; +import { + Modal, + Text, + Toggle, + Input, + Spacer, + useTheme, + Button, + Dot, + useToasts, +} from '@geist-ui/core'; import { saveUserSettings, fetchUserSettings } from '../services/api'; const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => { @@ -42,13 +52,14 @@ const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => { }, [isInitialized, loadSettings]); useEffect(() => { - const settingsChanged = JSON.stringify(settings) !== JSON.stringify(originalSettings); + 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 })); + setSettings((prev) => ({ ...prev, [key]: value })); }; const handleThemeChange = () => { @@ -70,7 +81,10 @@ const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => { onClose(); } catch (error) { console.error('Failed to save settings:', error); - setToast({ text: 'Failed to save settings: ' + error.message, type: 'error' }); + setToast({ + text: 'Failed to save settings: ' + error.message, + type: 'error', + }); } }; @@ -87,7 +101,7 @@ const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => { Appearance
Dark Mode - @@ -111,7 +125,9 @@ const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => { Enable Git handleInputChange('gitEnabled', e.target.checked)} + onChange={(e) => + handleInputChange('gitEnabled', e.target.checked) + } />
@@ -143,7 +159,9 @@ const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => { Auto Commit handleInputChange('gitAutoCommit', e.target.checked)} + onChange={(e) => + handleInputChange('gitAutoCommit', e.target.checked) + } disabled={!settings.gitEnabled} />
@@ -152,16 +170,20 @@ const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => { width="100%" label="Commit Message Template" value={settings.gitCommitMsgTemplate} - onChange={(e) => handleInputChange('gitCommitMsgTemplate', e.target.value)} + onChange={(e) => + handleInputChange('gitCommitMsgTemplate', e.target.value) + } disabled={!settings.gitEnabled} />
- Cancel + + Cancel + Save Changes ); }; -export default Settings; \ No newline at end of file +export default Settings; diff --git a/frontend/src/hooks/useFileManagement.js b/frontend/src/hooks/useFileManagement.js index c94eebe..2105816 100644 --- a/frontend/src/hooks/useFileManagement.js +++ b/frontend/src/hooks/useFileManagement.js @@ -1,11 +1,16 @@ import { useState, useEffect, useCallback } from 'react'; import { useToasts } from '@geist-ui/core'; -import { fetchFileList, fetchFileContent, saveFileContent, pullChanges } from '../services/api'; +import { + fetchFileList, + fetchFileContent, + saveFileContent, + pullChanges, +} from '../services/api'; const DEFAULT_FILE = { name: 'New File.md', path: 'New File.md', - content: '# Welcome to NovaMD\n\nStart editing here!' + content: '# Welcome to NovaMD\n\nStart editing here!', }; const useFileManagement = (gitEnabled = false) => { @@ -35,11 +40,17 @@ const useFileManagement = (gitEnabled = false) => { if (gitEnabled) { try { await pullChanges(); - setToast({ text: 'Latest changes pulled successfully', type: 'success' }); + 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' }); + setToast({ + text: 'Failed to pull latest changes: ' + error.message, + type: 'error', + }); } } }, [gitEnabled, loadFileList, setToast]); @@ -58,7 +69,9 @@ const useFileManagement = (gitEnabled = false) => { const handleFileSelect = async (filePath) => { if (hasUnsavedChanges) { - const confirmSwitch = window.confirm('You have unsaved changes. Are you sure you want to switch files?'); + const confirmSwitch = window.confirm( + 'You have unsaved changes. Are you sure you want to switch files?' + ); if (!confirmSwitch) return; } @@ -80,20 +93,26 @@ const useFileManagement = (gitEnabled = false) => { 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(); + 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', + }); } - } catch (error) { - console.error('Error saving file:', error); - setToast({ text: 'Failed to save file. Please try again.', type: 'error' }); - } - }, [setToast, isNewFile, loadFileList]); + }, + [setToast, isNewFile, loadFileList] + ); return { content, @@ -109,4 +128,4 @@ const useFileManagement = (gitEnabled = false) => { }; }; -export default useFileManagement; \ No newline at end of file +export default useFileManagement; diff --git a/frontend/src/index.js b/frontend/src/index.js index bd365f0..593edf1 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,10 +1,10 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App"; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; -const root = ReactDOM.createRoot(document.getElementById("root")); +const root = ReactDOM.createRoot(document.getElementById('root')); root.render( -); \ No newline at end of file +); diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 8a5d13d..151cc1c 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -1,127 +1,129 @@ const API_BASE_URL = window.API_BASE_URL; export const fetchFileList = async () => { - try { - const response = await fetch(`${API_BASE_URL}/files`); - if (!response.ok) { - throw new Error('Failed to fetch file list'); - } - return await response.json(); - } catch (error) { - console.error('Error fetching file list:', error); - throw error; + try { + const response = await fetch(`${API_BASE_URL}/files`); + if (!response.ok) { + throw new Error('Failed to fetch file list'); } + return await response.json(); + } catch (error) { + console.error('Error fetching file list:', error); + throw error; + } }; export const fetchFileContent = async (filePath) => { - try { - const response = await fetch(`${API_BASE_URL}/files/${filePath}`); - if (!response.ok) { - throw new Error('Failed to fetch file content'); - } - return await response.text(); - } catch (error) { - console.error('Error fetching file content:', error); - throw error; + try { + const response = await fetch(`${API_BASE_URL}/files/${filePath}`); + if (!response.ok) { + throw new Error('Failed to fetch file content'); } + return await response.text(); + } catch (error) { + console.error('Error fetching file content:', error); + throw error; + } }; export const saveFileContent = async (filePath, content) => { + const response = await fetch(`${API_BASE_URL}/files/${filePath}`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: content, + }); + + if (!response.ok) { + throw new Error('Failed to save file'); + } + + return await response.text(); +}; + +export const deleteFile = async (filePath) => { + try { const response = await fetch(`${API_BASE_URL}/files/${filePath}`, { - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - }, - body: content, + method: 'DELETE', }); - if (!response.ok) { - throw new Error('Failed to save file'); + throw new Error('Failed to delete file'); } - return await response.text(); - }; + } catch (error) { + console.error('Error deleting file:', error); + throw error; + } +}; - 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}`); - if (!response.ok) { - throw new Error('Failed to fetch user settings'); - } - return await response.json(); - } catch (error) { - console.error('Error fetching user settings:', error); - throw error; +export const fetchUserSettings = async (userId) => { + try { + const response = await fetch(`${API_BASE_URL}/settings?userId=${userId}`); + if (!response.ok) { + throw new Error('Failed to fetch user settings'); } + return await response.json(); + } catch (error) { + console.error('Error fetching user settings:', error); + throw error; + } }; export const saveUserSettings = async (settings) => { - try { - const response = await fetch(`${API_BASE_URL}/settings`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(settings), - }); + try { + const response = await fetch(`${API_BASE_URL}/settings`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(settings), + }); - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new Error(errorData?.message || `HTTP error! status: ${response.status}`); - } - - return await response.json(); - } catch (error) { - console.error('Error saving user settings:', error); - throw error; + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error( + errorData?.message || `HTTP error! status: ${response.status}` + ); } + + return await response.json(); + } catch (error) { + console.error('Error saving user settings:', error); + throw error; + } }; 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; + try { + const response = await fetch(`${API_BASE_URL}/git/pull`, { + method: 'POST', + }); + if (!response.ok) { + throw new Error('Failed to pull changes'); } - }; - - 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; + 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'); } - }; \ No newline at end of file + return await response.json(); + } catch (error) { + console.error('Error committing and pushing changes:', error); + throw error; + } +}; diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 4448a94..e2625ba 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -46,4 +46,4 @@ module.exports = (env, argv) => { open: true, }, }; -}; \ No newline at end of file +};