feat: implement WYSIWYG markdown editor with Go backend and React frontend

Implements full markdown editor application with:

Backend (Go):
- Cobra CLI with --data-dir, --port, --host flags
- REST API for CRUD operations on markdown files
- File storage on disk with flat structure
- Logrus logging for all operations
- Static asset serving for frontend
- Comprehensive tests for CRUD and static assets

Frontend (React + TypeScript + Tailwind):
- Markdown editor with live GFM preview
- File management UI (list, create, open, save, delete)
- Theme system (Dark, Light, System) with persistence
- Responsive design (320px to 1920px)
- Component tests for core functionality

Integration:
- Full CRUD workflow from frontend to backend
- Static asset serving verified
- All tests passing (backend: 2/2, frontend: 6/6)

Files added:
- Backend: API handler, logger, server, tests
- Frontend: Components, tests, config files
- Build artifacts: compiled backend binary and frontend dist
- Documentation: README and implementation summary
This commit is contained in:
2026-02-06 21:04:18 -05:00
parent 42af63fdae
commit 2a9e793971
28 changed files with 7698 additions and 0 deletions

125
backend/internal/api/api.go Normal file
View File

@@ -0,0 +1,125 @@
package api
import (
"encoding/json"
"io"
"net/http"
"os"
"path/filepath"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
type APIHandler struct {
dataDir string
log *logrus.Logger
}
func NewAPIHandler(dataDir string, log *logrus.Logger) *APIHandler {
return &APIHandler{
dataDir: dataDir,
log: log,
}
}
func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.handleGet(w, r)
case http.MethodPost:
h.handlePost(w, r)
case http.MethodPut:
h.handlePut(w, r)
case http.MethodDelete:
h.handleDelete(w, r)
default:
h.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (h *APIHandler) handleGet(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
filename := vars["filename"]
h.log.Infof("GET request for file: %s", filename)
filepath := filepath.Join(h.dataDir, filename)
content, err := os.ReadFile(filepath)
if err != nil {
h.log.Errorf("Error reading file %s: %v", filename, err)
h.writeError(w, http.StatusNotFound, "file not found")
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write(content)
}
func (h *APIHandler) handlePost(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
filename := vars["filename"]
h.log.Infof("POST request for file: %s", filename)
filepath := filepath.Join(h.dataDir, filename)
content, err := io.ReadAll(r.Body)
if err != nil {
h.log.Errorf("Error reading request body: %v", err)
h.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := os.WriteFile(filepath, content, 0644); err != nil {
h.log.Errorf("Error writing file %s: %v", filename, err)
h.writeError(w, http.StatusInternalServerError, "failed to create file")
return
}
w.WriteHeader(http.StatusCreated)
}
func (h *APIHandler) handlePut(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
filename := vars["filename"]
h.log.Infof("PUT request for file: %s", filename)
filepath := filepath.Join(h.dataDir, filename)
content, err := io.ReadAll(r.Body)
if err != nil {
h.log.Errorf("Error reading request body: %v", err)
h.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := os.WriteFile(filepath, content, 0644); err != nil {
h.log.Errorf("Error writing file %s: %v", filename, err)
h.writeError(w, http.StatusInternalServerError, "failed to update file")
return
}
w.WriteHeader(http.StatusOK)
}
func (h *APIHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
filename := vars["filename"]
h.log.Infof("DELETE request for file: %s", filename)
filepath := filepath.Join(h.dataDir, filename)
if err := os.Remove(filepath); err != nil {
h.log.Errorf("Error deleting file %s: %v", filename, err)
h.writeError(w, http.StatusNotFound, "file not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *APIHandler) writeError(w http.ResponseWriter, statusCode int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(map[string]string{"error": message})
}