Initial commit: WYSIWYG Markdown Editor - Go backend + React/TypeScript frontend with Tailwind CSS

Backend:
- Cobra CLI with --data-dir, --port, --host flags
- Gin HTTP server with REST API for markdown CRUD operations
- File storage on disk (.md files only)
- Comprehensive logrus logging
- Backend tests with CRUD round-trip verification

Frontend:
- React 18 + TypeScript + Tailwind CSS
- Markdown editor with live GFM preview (react-markdown + remark-gfm)
- File management UI (list, create, open, save, delete)
- Theme switcher with Dark/Light/System modes
- Responsive design
- Frontend tests with vitest

Testing:
- All backend tests pass (go test ./...)
- All frontend tests pass (npm test)
This commit is contained in:
2026-02-05 15:44:06 -05:00
parent c2a225fd29
commit 482d8a448a
37 changed files with 10585 additions and 0 deletions

115
internal/server/server.go Normal file
View File

@@ -0,0 +1,115 @@
package server
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/sirupsen/logrus"
"markdown-editor/internal/storage"
"markdown-editor/internal/api"
"markdown-editor/internal/logger"
"github.com/gin-gonic/gin"
)
type Server struct {
dataDir string
port int
host string
storage *storage.Storage
api *api.API
router *gin.Engine
httpSrv *http.Server
log *logrus.Logger
}
func NewServer(dataDir string, port int, host string) *Server {
log := logger.GetLogger()
s := &Server{
dataDir: dataDir,
port: port,
host: host,
log: log,
}
var err error
s.storage, err = storage.NewStorage(dataDir)
if err != nil {
log.Fatalf("Failed to initialize storage: %v", err)
}
s.api = api.NewAPI(s.storage)
// Initialize Gin router
s.router = gin.New()
s.router.Use(gin.Logger())
s.router.Use(gin.Recovery())
return s
}
func (s *Server) Start() error {
s.log.Info("Starting HTTP server")
// Register API routes
s.api.RegisterRoutes(s.router)
// Serve static files from frontend build
s.serveStaticAssets()
// Build the URL
url := fmt.Sprintf("%s:%d", s.host, s.port)
s.log.Infof("Server listening on %s", url)
s.httpSrv = &http.Server{
Addr: url,
Handler: s.router,
}
return s.httpSrv.ListenAndServe()
}
func (s *Server) serveStaticAssets() {
// Try to serve from multiple possible locations
possiblePaths := []string{
"./frontend/dist",
"frontend/dist",
}
var assetPath string
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
assetPath = path
break
}
}
if assetPath == "" {
s.log.Warn("Frontend build not found, serving API only")
return
}
s.log.Infof("Serving static assets from: %s", assetPath)
// Serve files from the dist directory
s.router.Static("/", assetPath)
s.router.Static("/assets", filepath.Join(assetPath, "assets"))
}
func (s *Server) Shutdown(ctx context.Context) error {
s.log.Info("Shutting down HTTP server")
// Clean up storage
if s.storage != nil {
s.storage = nil
}
if s.httpSrv != nil {
return s.httpSrv.Shutdown(ctx)
}
return nil
}

View File

@@ -0,0 +1,36 @@
package server
import (
"context"
"testing"
)
func TestServer_Shutdown(t *testing.T) {
s := NewServer("./testdata", 8889, "127.0.0.1")
ctx := context.Background()
err := s.Shutdown(ctx)
if err != nil {
t.Errorf("Shutdown error: %v", err)
}
}
func TestNewServer(t *testing.T) {
s := NewServer("./testdata", 8890, "127.0.0.1")
if s == nil {
t.Error("Server is nil")
}
if s.dataDir != "./testdata" {
t.Errorf("Expected dataDir './testdata', got '%s'", s.dataDir)
}
if s.port != 8890 {
t.Errorf("Expected port 8890, got %d", s.port)
}
if s.host != "127.0.0.1" {
t.Errorf("Expected host '127.0.0.1', got '%s'", s.host)
}
}

0
internal/server/testdata/.gitkeep vendored Normal file
View File