feat: list markdown files

This commit is contained in:
2025-10-17 17:43:18 +03:00
parent 0cc9367327
commit a256c3e16f
9 changed files with 153 additions and 81 deletions

8
README.md Normal file
View File

@@ -0,0 +1,8 @@
```bash
// hot-reload
go install github.com/air-verse/air@latest
air
// REPL
gore
```

BIN
api/api

Binary file not shown.

View File

@@ -1,22 +1,27 @@
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",
"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
}

View File

@@ -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>
);
}

View File

@@ -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;

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -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"'
]