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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// "os"
|
"fmt"
|
||||||
// "fmt"
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
// port = os.Getenv("API_PORT") || 8912
|
router.SetTrustedProxies([]string{"127.0.0.1"}) // TODO: fix All origins allowed by default
|
||||||
router.SetTrustedProxies([]string{"127.0.0.1"}) // TODO: fix
|
router.Use(cors.Default())
|
||||||
router.Use(cors.Default()) // All origins allowed by 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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "pong",
|
"data": list,
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -25,3 +30,28 @@ func main() {
|
|||||||
panic(err)
|
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";
|
import "./index.css";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (<Root/ >);
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
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]
|
[tasks.dev]
|
||||||
description = 'Run BE & FE in tmux'
|
description = 'Run BE & FE in tmux'
|
||||||
run = [
|
run = [
|
||||||
'tmux new-window "cd api; PORT=8912 air"',
|
'tmux new-window "cd api; go install; PORT=8912 air"',
|
||||||
'tmux split-window "cd frontend; PORT=8913 bun run dev"',
|
'tmux split-window "cd frontend; bun install; PORT=8913 bun src/index.tsx"',
|
||||||
'xdg-open "http://localhost:8913"'
|
'xdg-open "http://localhost:8913" || open "http://localhost:8913"'
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user