feat: list markdown files
This commit is contained in:
8
README.md
Normal file
8
README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
```bash
|
||||
// hot-reload
|
||||
go install github.com/air-verse/air@latest
|
||||
air
|
||||
|
||||
// REPL
|
||||
gore
|
||||
```
|
||||
46
api/main.go
46
api/main.go
@@ -1,23 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
// "os"
|
||||
// "fmt"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
// port = os.Getenv("API_PORT") || 8912
|
||||
router.SetTrustedProxies([]string{"127.0.0.1"}) // TODO: fix
|
||||
router.Use(cors.Default()) // All origins allowed by default
|
||||
router.SetTrustedProxies([]string{"127.0.0.1"}) // TODO: fix All origins allowed by default
|
||||
router.Use(cors.Default())
|
||||
list := listFiles("/Users/madundead/Syncthing/Obsidian/Personal")
|
||||
|
||||
router.GET("/api/v1/ping", func(c *gin.Context) {
|
||||
router.GET("/api/v1/files", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "pong",
|
||||
"status": "ok",
|
||||
"data": list,
|
||||
"status": "ok",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,3 +30,28 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func listFiles(dir string) []string {
|
||||
root := os.DirFS(dir)
|
||||
|
||||
mdFiles, err := fs.Glob(root, "**/*.md")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var files []string
|
||||
for _, v := range mdFiles {
|
||||
files = append(files, path.Join(dir, v))
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func readFile(string filePath) []string {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading file:", err)
|
||||
return
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useRef, type FormEvent } from "react";
|
||||
|
||||
export function APITester() {
|
||||
const responseInputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const testEndpoint = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const form = e.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
const endpoint = formData.get("endpoint") as string;
|
||||
const url = new URL(endpoint, "http://localhost:8912");
|
||||
const method = formData.get("method") as string;
|
||||
const res = await fetch(url, { method });
|
||||
|
||||
const data = await res.json();
|
||||
responseInputRef.current!.value = JSON.stringify(data, null, 2);
|
||||
} catch (error) {
|
||||
responseInputRef.current!.value = String(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-8 mx-auto w-full max-w-2xl text-left flex flex-col gap-4">
|
||||
<form
|
||||
onSubmit={testEndpoint}
|
||||
className="flex items-center gap-2 bg-[#1a1a1a] p-3 rounded-xl font-mono border-2 border-[#fbf0df] transition-colors duration-300 focus-within:border-[#f3d5a3] w-full"
|
||||
>
|
||||
<select
|
||||
name="method"
|
||||
className="bg-[#fbf0df] text-[#1a1a1a] py-1.5 px-3 rounded-lg font-bold text-sm min-w-[0px] appearance-none cursor-pointer hover:bg-[#f3d5a3] transition-colors duration-100"
|
||||
>
|
||||
<option value="GET" className="py-1">
|
||||
GET
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
name="endpoint"
|
||||
defaultValue="/api/v1/ping"
|
||||
className="w-full flex-1 bg-transparent border-0 text-[#fbf0df] font-mono text-base py-1.5 px-2 outline-none focus:text-white placeholder-[#fbf0df]/40"
|
||||
placeholder="/api/v1/ping"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-[#fbf0df] text-[#1a1a1a] border-0 px-5 py-1.5 rounded-lg font-bold transition-all duration-100 hover:bg-[#f3d5a3] hover:-translate-y-px cursor-pointer whitespace-nowrap"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
<textarea
|
||||
ref={responseInputRef}
|
||||
readOnly
|
||||
placeholder="Response will appear here..."
|
||||
className="w-full min-h-[140px] bg-[#1a1a1a] border-2 border-[#fbf0df] rounded-xl p-3 text-[#fbf0df] font-mono resize-y focus:border-[#f3d5a3] placeholder-[#fbf0df]/40"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
import { APITester } from "./APITester";
|
||||
// import { FilesList } from "./components/FilesList";
|
||||
// import { FileContent } from "./components/FileContent";
|
||||
import { Root } from "./components/Root";
|
||||
import "./index.css";
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto p-8 text-center relative z-10">
|
||||
<div className="flex justify-center items-center gap-8 mb-8">
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl font-bold my-4 leading-tight">React + Gin</h1>
|
||||
<APITester />
|
||||
</div>
|
||||
);
|
||||
return (<Root/ >);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
35
frontend/src/components/FileContent.tsx
Normal file
35
frontend/src/components/FileContent.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function FileContent({ filePath }) {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch("http://localhost:8912/api/v1/files"); // Replace with your API endpoint
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []); // Empty dependency array ensures this effect runs only once on mount
|
||||
|
||||
if (loading) return <p>Loading data...</p>;
|
||||
if (error) return <p>Error: {error.message}</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<pre>{filePath}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
frontend/src/components/FilesList.tsx
Normal file
41
frontend/src/components/FilesList.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function FilesList({ handleFileClick }) {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch("http://localhost:8912/api/v1/files"); // Replace with your API endpoint
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
setData(result.data);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []); // Empty dependency array ensures this effect runs only once on mount
|
||||
|
||||
if (loading) return <p>Loading data...</p>;
|
||||
if (error) return <p>Error: {error.message}</p>;
|
||||
|
||||
const onClick = (event) => {
|
||||
handleFileClick(event.target.value); // Call the parent's callback with the new value
|
||||
};
|
||||
|
||||
const listFiles = data.map(filePath => <li onClick={onClick} className="cursor-pointer">{filePath}</li>);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{listFiles}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
frontend/src/components/Root.tsx
Normal file
24
frontend/src/components/Root.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { FilesList } from "./FilesList";
|
||||
import { FileContent } from "./FileContent";
|
||||
|
||||
export function Root() {
|
||||
const [filePath, setFilePath] = useState(''); // State to be shared
|
||||
|
||||
const handleFileClick = () => {
|
||||
setFilePath('1000')
|
||||
alert("Button clicked!");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<FilesList onClick={handleFileClick} />
|
||||
</div>
|
||||
<div className="flex-2">
|
||||
<FileContent filePath={filePath} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ bun = "latest"
|
||||
[tasks.dev]
|
||||
description = 'Run BE & FE in tmux'
|
||||
run = [
|
||||
'tmux new-window "cd api; PORT=8912 air"',
|
||||
'tmux split-window "cd frontend; PORT=8913 bun run dev"',
|
||||
'xdg-open "http://localhost:8913"'
|
||||
'tmux new-window "cd api; go install; PORT=8912 air"',
|
||||
'tmux split-window "cd frontend; bun install; PORT=8913 bun src/index.tsx"',
|
||||
'xdg-open "http://localhost:8913" || open "http://localhost:8913"'
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user