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:
115
internal/server/server.go
Normal file
115
internal/server/server.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user