mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 23:44:22 +00:00
Connect FE with BE api
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
27
frontend/package-lock.json
generated
27
frontend/package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
27
frontend/src/services/api.js
Normal file
27
frontend/src/services/api.js
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user