feat: implement WYSIWYG markdown editor
Add full-stack markdown editor with Go backend and React frontend. Backend: - Cobra CLI with --data-dir, --port, --host flags - REST API for markdown file CRUD operations - File storage with flat directory structure - logrus logging for all operations - Static file serving for frontend - Comprehensive tests for CRUD and static assets Frontend: - React + TypeScript + Vite + Tailwind CSS - Live markdown preview with marked (GFM) - File management: list, create, open, save, delete - Theme system: Dark/Light/System with persistence - Responsive design (320px to 1920px) - Component tests for Editor, Preview, Sidebar Build: - Makefile for build, test, and run automation - Single command testing (make test) Closes SPEC.md requirements
This commit is contained in:
176
backend/handlers/handlers.go
Normal file
176
backend/handlers/handlers.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"markdown-editor-backend/storage"
|
||||
)
|
||||
|
||||
// Handler contains HTTP handlers for the API
|
||||
type Handler struct {
|
||||
storage *storage.Storage
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
// New creates a new Handler instance
|
||||
func New(storage *storage.Storage, log *logrus.Logger) *Handler {
|
||||
return &Handler{
|
||||
storage: storage,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// FileRequest represents a request body for file operations
|
||||
type FileRequest struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// FileResponse represents a file response
|
||||
type FileResponse struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorResponse represents an error response
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// FilesHandler handles list and create operations
|
||||
func (h *Handler) FilesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
h.listFiles(w, r)
|
||||
case http.MethodPost:
|
||||
h.createFile(w, r)
|
||||
default:
|
||||
h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
// FileHandler handles get, update, and delete operations for a specific file
|
||||
func (h *Handler) FileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract filename from URL path
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/files/")
|
||||
if path == "" {
|
||||
h.sendError(w, http.StatusBadRequest, "filename required")
|
||||
return
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(path, ".md")
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
h.getFile(w, r, name)
|
||||
case http.MethodPut:
|
||||
h.updateFile(w, r, name)
|
||||
case http.MethodDelete:
|
||||
h.deleteFile(w, r, name)
|
||||
default:
|
||||
h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) listFiles(w http.ResponseWriter, r *http.Request) {
|
||||
h.log.Debug("Handling list files request")
|
||||
|
||||
files, err := h.storage.List()
|
||||
if err != nil {
|
||||
h.log.WithError(err).Error("Failed to list files")
|
||||
h.sendError(w, http.StatusInternalServerError, "failed to list files")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(files)
|
||||
}
|
||||
|
||||
func (h *Handler) createFile(w http.ResponseWriter, r *http.Request) {
|
||||
h.log.Debug("Handling create file request")
|
||||
|
||||
var req FileRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.log.WithError(err).Warn("Invalid request body")
|
||||
h.sendError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
h.sendError(w, http.StatusBadRequest, "name is required")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.storage.Save(req.Name, req.Content); err != nil {
|
||||
h.log.WithError(err).Error("Failed to create file")
|
||||
h.sendError(w, http.StatusInternalServerError, "failed to create file")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(FileResponse{Name: req.Name, Content: req.Content})
|
||||
}
|
||||
|
||||
func (h *Handler) getFile(w http.ResponseWriter, r *http.Request, name string) {
|
||||
h.log.WithField("name", name).Debug("Handling get file request")
|
||||
|
||||
content, err := h.storage.Get(name)
|
||||
if err != nil {
|
||||
if err.Error() == "file not found" {
|
||||
h.sendError(w, http.StatusNotFound, "file not found")
|
||||
return
|
||||
}
|
||||
h.log.WithError(err).Error("Failed to get file")
|
||||
h.sendError(w, http.StatusInternalServerError, "failed to get file")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(FileResponse{Name: name, Content: content})
|
||||
}
|
||||
|
||||
func (h *Handler) updateFile(w http.ResponseWriter, r *http.Request, name string) {
|
||||
h.log.WithField("name", name).Debug("Handling update file request")
|
||||
|
||||
var req FileRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.log.WithError(err).Warn("Invalid request body")
|
||||
h.sendError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.storage.Save(name, req.Content); err != nil {
|
||||
h.log.WithError(err).Error("Failed to update file")
|
||||
h.sendError(w, http.StatusInternalServerError, "failed to update file")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(FileResponse{Name: name, Content: req.Content})
|
||||
}
|
||||
|
||||
func (h *Handler) deleteFile(w http.ResponseWriter, r *http.Request, name string) {
|
||||
h.log.WithField("name", name).Debug("Handling delete file request")
|
||||
|
||||
if err := h.storage.Delete(name); err != nil {
|
||||
if err.Error() == "file not found" {
|
||||
h.sendError(w, http.StatusNotFound, "file not found")
|
||||
return
|
||||
}
|
||||
h.log.WithError(err).Error("Failed to delete file")
|
||||
h.sendError(w, http.StatusInternalServerError, "failed to delete file")
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) sendError(w http.ResponseWriter, status int, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Error: message})
|
||||
}
|
||||
Reference in New Issue
Block a user