From b2e99fa39eb28c7b75dd6a1f1b31793bcd136996 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Wed, 25 Sep 2024 22:03:32 +0200 Subject: [PATCH] Connect FE with BE api --- backend/internal/filesystem/filesystem.go | 53 ++++++++++++----- frontend/package-lock.json | 27 ++++++++- frontend/package.json | 2 + frontend/src/App.js | 66 +++++++++++++++++---- frontend/src/App.scss | 70 ++++++++++++++--------- frontend/src/components/FileTree.js | 41 ++++++++++--- frontend/src/services/api.js | 27 +++++++++ 7 files changed, 222 insertions(+), 64 deletions(-) create mode 100644 frontend/src/services/api.js diff --git a/backend/internal/filesystem/filesystem.go b/backend/internal/filesystem/filesystem.go index ce02617..3f2b1b1 100644 --- a/backend/internal/filesystem/filesystem.go +++ b/backend/internal/filesystem/filesystem.go @@ -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) } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 603294b..29371d6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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": { diff --git a/frontend/package.json b/frontend/package.json index 0521433..aa21624 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" diff --git a/frontend/src/App.js b/frontend/src/App.js index 817a10e..9cb3f91 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,23 +1,67 @@ -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 ( -
-
- + + +
+
+ {error ? ( +
{error}
+ ) : ( + + )} +
+
+

NovaMD

+ +
-
-

NovaMD

- -
-
+ ); } -export default App; +export default App; \ No newline at end of file diff --git a/frontend/src/App.scss b/frontend/src/App.scss index a8e8b8d..ebde6d5 100644 --- a/frontend/src/App.scss +++ b/frontend/src/App.scss @@ -1,33 +1,49 @@ +// Variables +$sidebar-width: 250px; +$border-color: #eaeaea; +$padding: 20px; + .App { - 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; + display: flex; + height: 100vh; + + .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; - - &:hover { - text-decoration: underline; - } + + .icon { + margin-right: 8px; } - } \ No newline at end of file + + .name { + font-size: 14px; + } + + .selected { + color: #0070f3; + font-weight: bold; + } + } +} \ No newline at end of file diff --git a/frontend/src/components/FileTree.js b/frontend/src/components/FileTree.js index 3b51943..ebb2a56 100644 --- a/frontend/src/components/FileTree.js +++ b/frontend/src/components/FileTree.js @@ -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
No files to display
; + } + + const handleSelect = (filePath) => { + onFileSelect(filePath); + }; + + const renderLabel = (node) => { + const path = getFilePath(node); + return ( + + {node.name} + + ); + }; + + const renderIcon = ({ type }) => type === 'directory' ? : ; -const FileTree = ({ files }) => { return ( -
-

Files

-
    - {files.map((file, index) => ( -
  • {file}
  • - ))} -
-
+ ); }; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js new file mode 100644 index 0000000..3056dbe --- /dev/null +++ b/frontend/src/services/api.js @@ -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; + } +}; \ No newline at end of file