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
import (
"io/ioutil"
"os"
"path/filepath"
)
@@ -10,28 +9,52 @@ type FileSystem struct {
RootDir string
}
type FileNode struct {
Type string `json:"type"`
Name string `json:"name"`
Files []FileNode `json:"files,omitempty"`
}
func New(rootDir string) *FileSystem {
return &FileSystem{RootDir: rootDir}
}
func (fs *FileSystem) ListFilesRecursively() ([]string, error) {
var files []string
func (fs *FileSystem) ListFilesRecursively() ([]FileNode, error) {
return fs.walkDirectory(fs.RootDir)
}
err := filepath.Walk(fs.RootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
relPath, _ := filepath.Rel(fs.RootDir, path)
files = append(files, relPath)
}
return nil
})
func (fs *FileSystem) walkDirectory(dir string) ([]FileNode, error) {
var nodes []FileNode
return files, err
entries, err := os.ReadDir(dir)
if err != nil {
return nil, 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) {
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/state": "^6.4.1",
"@codemirror/view": "^6.34.0",
"@geist-ui/core": "^2.3.8",
"@geist-ui/icons": "^1.0.2",
"codemirror": "^6.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
@@ -1857,7 +1859,6 @@
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz",
"integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
@@ -2111,6 +2112,29 @@
"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": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
@@ -5941,7 +5965,6 @@
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true,
"license": "MIT"
},
"node_modules/regenerator-transform": {

View File

@@ -27,6 +27,8 @@
"@codemirror/lang-markdown": "^6.2.5",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.34.0",
"@geist-ui/core": "^2.3.8",
"@geist-ui/icons": "^1.0.2",
"codemirror": "^6.0.1",
"react": "^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 FileTree from './components/FileTree';
import { fetchFileList, fetchFileContent } from './services/api';
import './App.scss';
function App() {
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 (
<div className="App">
<div className="sidebar">
<FileTree files={files} />
<GeistProvider>
<CssBaseline />
<div className="App">
<div className="sidebar">
{error ? (
<div className="error">{error}</div>
) : (
<FileTree
files={files}
onFileSelect={handleFileSelect}
selectedFile={selectedFile}
/>
)}
</div>
<div className="main-content">
<h1>NovaMD</h1>
<Editor content={content} onChange={setContent} />
</div>
</div>
<div className="main-content">
<h1>NovaMD</h1>
<Editor content={content} onChange={setContent} />
</div>
</div>
</GeistProvider>
);
}

View File

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

View File

@@ -1,15 +1,38 @@
import React from 'react';
import { Tree } from '@geist-ui/core';
import { File, Folder } from '@geist-ui/icons';
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 (
<span style={{ color: path === selectedFile ? '#0070f3' : 'inherit' }}>
{node.name}
</span>
);
};
const renderIcon = ({ type }) => type === 'directory' ? <Folder /> : <File />;
const FileTree = ({ files }) => {
return (
<div className="file-tree">
<h3>Files</h3>
<ul>
{files.map((file, index) => (
<li key={index}>{file}</li>
))}
</ul>
</div>
<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;
}
};