Set up and run eslint and prettier

This commit is contained in:
2024-09-29 21:52:30 +02:00
parent eb497c7ccb
commit 05f5ca2877
15 changed files with 359 additions and 214 deletions

31
.eslintrc.json Normal file
View File

@@ -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"
}
}
}

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -13,7 +13,6 @@
},
"keywords": [
"markdown",
"hypermd",
"editor"
],
"author": "Matúš Námešný",

View File

@@ -47,7 +47,7 @@ function App() {
<CssBaseline />
<Page>
<Header currentTheme={themeType} onThemeChange={setTheme} />
<Page.Content className='page-content'>
<Page.Content className="page-content">
<MainContent
content={content}
files={files}

View File

@@ -15,12 +15,11 @@ $navbar-height: 64px;
.file-tree-container {
width: 100%;
&>div {
& > div {
padding: 0;
margin: 0;
}
}
}
.page-content {

View File

@@ -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]);

View File

@@ -1,6 +1,13 @@
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 = [],
@@ -11,7 +18,7 @@ const FileTree = ({
onPull = () => {},
onCommitAndPush = () => {},
onCreateFile = () => {},
onDeleteFile = () => {}
onDeleteFile = () => {},
}) => {
if (files.length === 0) {
return <div>No files to display</div>;
@@ -30,26 +37,30 @@ const FileTree = ({
);
};
const renderIcon = ({ type }) => type === 'directory' ? <Folder /> : <File />;
const renderIcon = ({ type }) =>
type === 'directory' ? <Folder /> : <File />;
return (
<div>
<ButtonGroup className='file-tree-buttons'>
<ButtonGroup className="file-tree-buttons">
<Tooltip text="Create new file" type="dark">
<Button
icon={<Plus />}
auto
scale={2/3}
scale={2 / 3}
onClick={onCreateFile}
px={0.6}
/>
</Tooltip>
<Spacer w={0.5} />
<Tooltip text={selectedFile ? "Delete current file" : "No file selected"} type="dark">
<Tooltip
text={selectedFile ? 'Delete current file' : 'No file selected'}
type="dark"
>
<Button
icon={<Trash />}
auto
scale={2/3}
scale={2 / 3}
onClick={onDeleteFile}
disabled={!selectedFile}
type="error"
@@ -57,26 +68,34 @@ const FileTree = ({
/>
</Tooltip>
<Spacer w={0.5} />
<Tooltip text={gitEnabled ? "Pull changes from remote" : "Git is not enabled"} type="dark">
<Tooltip
text={gitEnabled ? 'Pull changes from remote' : 'Git is not enabled'}
type="dark"
>
<Button
icon={<GitPullRequest />}
auto
scale={2/3}
scale={2 / 3}
onClick={onPull}
disabled={!gitEnabled}
px={0.6}
/>
</Tooltip>
<Spacer w={0.5} />
<Tooltip text={
!gitEnabled ? "Git is not enabled" :
gitAutoCommit ? "Auto-commit is enabled" :
"Commit and push changes"
} type="dark">
<Tooltip
text={
!gitEnabled
? 'Git is not enabled'
: gitAutoCommit
? 'Auto-commit is enabled'
: 'Commit and push changes'
}
type="dark"
>
<Button
icon={<GitCommit />}
auto
scale={2/3}
scale={2 / 3}
onClick={onCommitAndPush}
disabled={!gitEnabled || gitAutoCommit}
px={0.6}

View File

@@ -1,5 +1,15 @@
import React, { useState } from 'react';
import { Grid, Breadcrumbs, Tabs, Dot, useTheme, useToasts, Modal, Input, Button } 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';
@@ -29,7 +39,10 @@ const MainContent = ({
await pullLatestChanges();
setToast({ text: 'Successfully pulled latest changes', type: 'success' });
} catch (error) {
setToast({ text: 'Failed to pull changes: ' + error.message, type: 'error' });
setToast({
text: 'Failed to pull changes: ' + error.message,
type: 'error',
});
}
};
@@ -38,11 +51,17 @@ const MainContent = ({
const message = prompt('Enter commit message:');
if (message) {
await commitAndPush(message);
setToast({ text: 'Changes committed and pushed successfully', type: 'success' });
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' });
setToast({
text: 'Failed to commit and push changes: ' + error.message,
type: 'error',
});
}
};
@@ -58,7 +77,10 @@ const MainContent = ({
await pullLatestChanges(); // Refresh file list
onFileSelect(newFileName); // Select the new file
} catch (error) {
setToast({ text: 'Failed to create new file: ' + error.message, type: 'error' });
setToast({
text: 'Failed to create new file: ' + error.message,
type: 'error',
});
}
}
setNewFileModalVisible(false);
@@ -67,7 +89,9 @@ const MainContent = ({
const handleDeleteFile = async () => {
if (selectedFile) {
const confirmDelete = window.confirm(`Are you sure you want to delete "${selectedFile}"?`);
const confirmDelete = window.confirm(
`Are you sure you want to delete "${selectedFile}"?`
);
if (confirmDelete) {
try {
await deleteFile(selectedFile);
@@ -75,7 +99,10 @@ const MainContent = ({
await pullLatestChanges(); // Refresh file list
onFileSelect(null); // Deselect the file
} catch (error) {
setToast({ text: 'Failed to delete file: ' + error.message, type: 'error' });
setToast({
text: 'Failed to delete file: ' + error.message,
type: 'error',
});
}
}
}
@@ -91,7 +118,9 @@ const MainContent = ({
<Breadcrumbs.Item key={index}>{part}</Breadcrumbs.Item>
))}
</Breadcrumbs>
{hasUnsavedChanges && <Dot type="warning" className="unsaved-indicator" />}
{hasUnsavedChanges && (
<Dot type="warning" className="unsaved-indicator" />
)}
</div>
);
};
@@ -114,7 +143,14 @@ const MainContent = ({
/>
</div>
</Grid>
<Grid xs={24} sm={18} md={19} lg={20} height="100%" className="main-content">
<Grid
xs={24}
sm={18}
md={19}
lg={20}
height="100%"
className="main-content"
>
<div className="content-header">
{renderBreadcrumbs()}
<Tabs value={activeTab} onChange={setActiveTab}>
@@ -137,7 +173,10 @@ const MainContent = ({
</div>
</Grid>
</Grid.Container>
<Modal visible={newFileModalVisible} onClose={() => setNewFileModalVisible(false)}>
<Modal
visible={newFileModalVisible}
onClose={() => setNewFileModalVisible(false)}
>
<Modal.Title>Create New File</Modal.Title>
<Modal.Content>
<Input
@@ -147,7 +186,9 @@ const MainContent = ({
onChange={(e) => setNewFileName(e.target.value)}
/>
</Modal.Content>
<Modal.Action passive onClick={() => setNewFileModalVisible(false)}>Cancel</Modal.Action>
<Modal.Action passive onClick={() => setNewFileModalVisible(false)}>
Cancel
</Modal.Action>
<Modal.Action onClick={handleNewFileSubmit}>Create</Modal.Action>
</Modal>
</>

View File

@@ -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 ? (
<SyntaxHighlighter
style={vscDarkPlus}
@@ -28,8 +28,8 @@ const MarkdownPreview = ({ content }) => {
<code className={className} {...props}>
{children}
</code>
)
}
);
},
}}
>
{content}

View File

@@ -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',
});
}
};
@@ -111,7 +125,9 @@ const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => {
<Text>Enable Git</Text>
<Toggle
checked={settings.gitEnabled}
onChange={(e) => handleInputChange('gitEnabled', e.target.checked)}
onChange={(e) =>
handleInputChange('gitEnabled', e.target.checked)
}
/>
</div>
<div className={settings.gitEnabled ? '' : 'disabled'}>
@@ -143,7 +159,9 @@ const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => {
<Text>Auto Commit</Text>
<Toggle
checked={settings.gitAutoCommit}
onChange={(e) => handleInputChange('gitAutoCommit', e.target.checked)}
onChange={(e) =>
handleInputChange('gitAutoCommit', e.target.checked)
}
disabled={!settings.gitEnabled}
/>
</div>
@@ -152,13 +170,17 @@ 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}
/>
</div>
</div>
</Modal.Content>
<Modal.Action passive onClick={onClose}>Cancel</Modal.Action>
<Modal.Action passive onClick={onClose}>
Cancel
</Modal.Action>
<Modal.Action onClick={handleSubmit}>Save Changes</Modal.Action>
</Modal>
);

View File

@@ -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,

View File

@@ -1,8 +1,8 @@
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(
<React.StrictMode>
<App />

View File

@@ -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');
}
};
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;
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;
}
};