Connect FE with BE api

This commit is contained in:
2024-09-25 22:03:32 +02:00
parent a135f28fd5
commit b2e99fa39e
7 changed files with 222 additions and 64 deletions

View File

@@ -1,7 +1,6 @@
package filesystem package filesystem
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
) )
@@ -10,28 +9,52 @@ type FileSystem struct {
RootDir string RootDir string
} }
type FileNode struct {
Type string `json:"type"`
Name string `json:"name"`
Files []FileNode `json:"files,omitempty"`
}
func New(rootDir string) *FileSystem { func New(rootDir string) *FileSystem {
return &FileSystem{RootDir: rootDir} return &FileSystem{RootDir: rootDir}
} }
func (fs *FileSystem) ListFilesRecursively() ([]string, error) { func (fs *FileSystem) ListFilesRecursively() ([]FileNode, error) {
var files []string return fs.walkDirectory(fs.RootDir)
}
err := filepath.Walk(fs.RootDir, func(path string, info os.FileInfo, err error) error { func (fs *FileSystem) walkDirectory(dir string) ([]FileNode, error) {
var nodes []FileNode
entries, err := os.ReadDir(dir)
if err != nil { if err != nil {
return err return nil, err
} }
if !info.IsDir() {
relPath, _ := filepath.Rel(fs.RootDir, path)
files = append(files, relPath)
}
return nil
})
return files, err for _, entry := range entries {
if entry.IsDir() {
subdir := filepath.Join(dir, entry.Name())
subFiles, err := fs.walkDirectory(subdir)
if err != nil {
return nil, err
}
nodes = append(nodes, FileNode{
Type: "directory",
Name: entry.Name(),
Files: subFiles,
})
} else {
nodes = append(nodes, FileNode{
Type: "file",
Name: entry.Name(),
})
}
}
return nodes, nil
} }
func (fs *FileSystem) GetFileContent(filePath string) ([]byte, error) { func (fs *FileSystem) GetFileContent(filePath string) ([]byte, error) {
fullPath := filepath.Join(fs.RootDir, filePath) fullPath := filepath.Join(fs.RootDir, filePath)
return ioutil.ReadFile(fullPath) return os.ReadFile(fullPath)
} }

View File

@@ -13,6 +13,8 @@
"@codemirror/lang-markdown": "^6.2.5", "@codemirror/lang-markdown": "^6.2.5",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.34.0", "@codemirror/view": "^6.34.0",
"@geist-ui/core": "^2.3.8",
"@geist-ui/icons": "^1.0.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
@@ -1857,7 +1859,6 @@
"version": "7.25.6", "version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz",
"integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
@@ -2111,6 +2112,29 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/@geist-ui/core": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/@geist-ui/core/-/core-2.3.8.tgz",
"integrity": "sha512-OKwGgTA4+fBM41eQbqDoUj4XBycZbYH7Ynrn6LPO5yKX7zeWPu/R7HN3vB4/oHt34VTDQI5sDNb1SirHvNyB5w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.16.7"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@geist-ui/icons": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@geist-ui/icons/-/icons-1.0.2.tgz",
"integrity": "sha512-Npfa0NW6fQ31qw/+iMPWbs1hAcJ/3FqBjSLYgEfITDqy/3TJFpFKeVyK04AC/hTmYTsdNruVYczqPNcham5FOQ==",
"license": "MIT",
"peerDependencies": {
"@geist-ui/core": ">=1.0.0",
"react": ">=16.13.0"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
@@ -5941,7 +5965,6 @@
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/regenerator-transform": { "node_modules/regenerator-transform": {

View File

@@ -27,6 +27,8 @@
"@codemirror/lang-markdown": "^6.2.5", "@codemirror/lang-markdown": "^6.2.5",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.34.0", "@codemirror/view": "^6.34.0",
"@geist-ui/core": "^2.3.8",
"@geist-ui/icons": "^1.0.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"

View File

@@ -1,22 +1,66 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { GeistProvider, CssBaseline } from '@geist-ui/core';
import Editor from './components/Editor'; import Editor from './components/Editor';
import FileTree from './components/FileTree'; import FileTree from './components/FileTree';
import { fetchFileList, fetchFileContent } from './services/api';
import './App.scss'; import './App.scss';
function App() { function App() {
const [content, setContent] = useState('# Welcome to NovaMD\n\nStart editing here!'); const [content, setContent] = useState('# Welcome to NovaMD\n\nStart editing here!');
const [files, setFiles] = useState(['README.md', 'chapter1.md', 'chapter2.md']); const [files, setFiles] = useState([]);
const [selectedFile, setSelectedFile] = useState(null);
const [error, setError] = useState(null);
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.');
}
};
loadFileList();
}, []);
const handleFileSelect = async (filePath) => {
try {
const fileContent = await fetchFileContent(filePath);
setContent(fileContent);
setSelectedFile(filePath);
} catch (error) {
console.error('Failed to load file content:', error);
setError('Failed to load file content. Please try again.');
}
};
return ( return (
<GeistProvider>
<CssBaseline />
<div className="App"> <div className="App">
<div className="sidebar"> <div className="sidebar">
<FileTree files={files} /> {error ? (
<div className="error">{error}</div>
) : (
<FileTree
files={files}
onFileSelect={handleFileSelect}
selectedFile={selectedFile}
/>
)}
</div> </div>
<div className="main-content"> <div className="main-content">
<h1>NovaMD</h1> <h1>NovaMD</h1>
<Editor content={content} onChange={setContent} /> <Editor content={content} onChange={setContent} />
</div> </div>
</div> </div>
</GeistProvider>
); );
} }

View File

@@ -1,33 +1,49 @@
// Variables
$sidebar-width: 250px;
$border-color: #eaeaea;
$padding: 20px;
.App { .App {
display: flex; display: flex;
height: 100vh; height: 100vh;
.sidebar { .sidebar {
width: 200px; width: $sidebar-width;
background-color: #f0f0f0; padding: $padding;
padding: 20px; border-right: 1px solid $border-color;
overflow-y: auto; overflow-y: auto;
} }
.main-content { .main-content {
flex-grow: 1; flex-grow: 1;
padding: 20px; padding: $padding;
overflow-y: auto; overflow-y: auto;
}
}
h1 {
margin-top: 0;
}
}
}
// Geist UI Tree component customization
:global {
.file-tree { .file-tree {
ul { .label {
list-style-type: none; display: flex;
padding-left: 20px; align-items: center;
} }
li { .icon {
margin-bottom: 5px; margin-right: 8px;
cursor: pointer; }
&:hover { .name {
text-decoration: underline; font-size: 14px;
} }
.selected {
color: #0070f3;
font-weight: bold;
} }
} }
}

View File

@@ -1,15 +1,38 @@
import React from 'react'; import React from 'react';
import { Tree } from '@geist-ui/core';
import { File, Folder } from '@geist-ui/icons';
const FileTree = ({ files }) => { const FileTree = ({
files = [],
onFileSelect = () => {},
selectedFile = null
}) => {
if (files.length === 0) {
return <div>No files to display</div>;
}
const handleSelect = (filePath) => {
onFileSelect(filePath);
};
const renderLabel = (node) => {
const path = getFilePath(node);
return ( return (
<div className="file-tree"> <span style={{ color: path === selectedFile ? '#0070f3' : 'inherit' }}>
<h3>Files</h3> {node.name}
<ul> </span>
{files.map((file, index) => ( );
<li key={index}>{file}</li> };
))}
</ul> const renderIcon = ({ type }) => type === 'directory' ? <Folder /> : <File />;
</div>
return (
<Tree
value={files}
onClick={handleSelect}
renderIcon={renderIcon}
renderLabel={renderLabel}
/>
); );
}; };

View File

@@ -0,0 +1,27 @@
const API_BASE_URL = 'http://localhost:8080/api/v1';
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;
}
};
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;
}
};