From c1841eb88a4147a7f6bec9598d9c90583841e2e9 Mon Sep 17 00:00:00 2001 From: Tigor Hutasuhut Date: Mon, 8 Apr 2024 21:50:52 +0700 Subject: [PATCH] views: added simple home view with head component --- .air.toml | 2 +- cli/serve.go | 2 + config/default.go | 1 + main.go | 13 ++++++ server/routes/api/api.go | 2 + server/routes/middleware/logger.go | 25 +++++++++++ server/routes/www/home.go | 3 +- server/routes/www/hot_reload.go | 68 ++++++++++++++++++++++++++++++ server/routes/www/www.go | 29 ++++++++++++- views/components/head.templ | 17 ++++++++ views/context.go | 19 +++++++++ views/homeview/homeview.templ | 10 ++++- 12 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 server/routes/www/hot_reload.go create mode 100644 views/components/head.templ create mode 100644 views/context.go diff --git a/.air.toml b/.air.toml index 50f4c85..ca4ce5d 100644 --- a/.air.toml +++ b/.air.toml @@ -23,7 +23,7 @@ poll_interval = 0 post_cmd = [] pre_cmd = [ "mkdir -p public", - "tailwindcss -i views/styles.css -o public/style.css", + "tailwindcss -i views/style.css -o public/style.css", "templ generate", "sqlc generate", ] diff --git a/cli/serve.go b/cli/serve.go index 9266b8f..21fc466 100644 --- a/cli/serve.go +++ b/cli/serve.go @@ -28,10 +28,12 @@ var serveCmd = &cobra.Command{ api := &api.API{ Subreddits: subreddits, + Config: cfg, } www := &www.WWW{ Subreddits: subreddits, + Config: cfg, } server := server.New(cfg, api, www) diff --git a/config/default.go b/config/default.go index 5b8896a..dc22f96 100644 --- a/config/default.go +++ b/config/default.go @@ -14,4 +14,5 @@ var DefaultConfig = map[string]any{ "http.port": "8080", "http.host": "0.0.0.0", "http.shutdown_timeout": "5s", + "http.hotreload": false, } diff --git a/main.go b/main.go index ea67d7e..bd09360 100644 --- a/main.go +++ b/main.go @@ -3,17 +3,30 @@ package main import ( "context" "embed" + "errors" + "io/fs" "os" + "github.com/joho/godotenv" "github.com/tigorlazuardi/redmage/cli" "github.com/tigorlazuardi/redmage/db" + "github.com/tigorlazuardi/redmage/server/routes/www" ) //go:embed db/migrations/*.sql var Migrations embed.FS +//go:embed public/* +var PublicFS embed.FS + func main() { + _ = godotenv.Load() db.Migrations = Migrations + var err error + www.PublicFS, err = fs.Sub(PublicFS, "public") + if err != nil { + panic(errors.New("failed to create sub filesystem")) + } if err := cli.RootCmd.ExecuteContext(context.Background()); err != nil { os.Exit(1) } diff --git a/server/routes/api/api.go b/server/routes/api/api.go index 3b58842..66921a7 100644 --- a/server/routes/api/api.go +++ b/server/routes/api/api.go @@ -3,12 +3,14 @@ package api import ( "github.com/go-chi/chi/v5" chimiddleware "github.com/go-chi/chi/v5/middleware" + "github.com/tigorlazuardi/redmage/config" "github.com/tigorlazuardi/redmage/db/queries/subreddits" "github.com/tigorlazuardi/redmage/server/routes/middleware" ) type API struct { Subreddits *subreddits.Queries + Config *config.Config } func (api *API) Register(router chi.Router) { diff --git a/server/routes/middleware/logger.go b/server/routes/middleware/logger.go index ce4fb8b..0d29557 100644 --- a/server/routes/middleware/logger.go +++ b/server/routes/middleware/logger.go @@ -77,3 +77,28 @@ func formatDuration(dur time.Duration) string { return fmt.Sprintf("%.3fms", nanosecs/float64(time.Millisecond)) } + +type ChiSimpleLogger struct{} + +func (ChiSimpleLogger) NewLogEntry(r *http.Request) chimiddleware.LogEntry { + return &ChiSimpleEntry{request: r} +} + +type ChiSimpleEntry struct { + request *http.Request +} + +func (ch *ChiSimpleEntry) Write(status int, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { + elapsedStr := formatDuration(elapsed) + message := fmt.Sprintf("%s %s %d %s", ch.request.Method, ch.request.URL, status, elapsedStr) + + level := slog.LevelInfo + if status >= 400 { + level = slog.LevelError + } + log.New(ch.request.Context()).Level(level).Log(message) +} + +func (ch *ChiSimpleEntry) Panic(v interface{}, stack []byte) { + (&ChiEntry{ch.request}).Panic(v, stack) +} diff --git a/server/routes/www/home.go b/server/routes/www/home.go index a0b48f6..d6379b3 100644 --- a/server/routes/www/home.go +++ b/server/routes/www/home.go @@ -3,9 +3,10 @@ package www import ( "net/http" + "github.com/tigorlazuardi/redmage/views" "github.com/tigorlazuardi/redmage/views/homeview" ) func (www *WWW) Home(rw http.ResponseWriter, r *http.Request) { - _ = homeview.Home().Render(r.Context(), rw) + _ = homeview.Home(views.NewContext(www.Config, r)).Render(r.Context(), rw) } diff --git a/server/routes/www/hot_reload.go b/server/routes/www/hot_reload.go new file mode 100644 index 0000000..c671c07 --- /dev/null +++ b/server/routes/www/hot_reload.go @@ -0,0 +1,68 @@ +package www + +import ( + "errors" + "io" + "net/http" + "sync" + + "github.com/tigorlazuardi/redmage/pkg/log" +) + +func (www *WWW) CreateHotReloadRoute() http.HandlerFunc { + var mu sync.Mutex + knownClients := make(map[string]chan struct{}) + firstTime := make(chan struct{}, 1) + firstTime <- struct{}{} + return func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + var ch chan struct{} + if oldChannel, known := knownClients[id]; known { + ch = oldChannel + } else { + ch = make(chan struct{}, 1) + ch <- struct{}{} + knownClients[id] = ch + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.WriteHeader(200) + for { + select { + case <-r.Context().Done(): + return + case <-firstTime: + // dispose own's channel buffer to prevent deadlock. + // because it will be filled with new signal below. + if len(ch) > 0 { + <-ch + } + // broadcast to all connected clients + // that hot reload is triggered. + // + // The sender only send one signal, + // and the receiver is only one, and chosen at random, so + // we have to propagate the signal to all + // connected clients. + mu.Lock() + for _, ch := range knownClients { + ch <- struct{}{} + } + mu.Unlock() + case <-ch: + _, err := io.WriteString(w, "data: Hot reload triggered\n\n") + if err != nil { + log.New(r.Context()).Err(err).Error("failed to send hot reload trigger", "channel_id", id) + return + } + if w, ok := w.(http.Flusher); ok { + w.Flush() + } else { + panic(errors.New("HotReload: ResponseWriter does not implement http.Flusher interface")) + } + } + } + } +} diff --git a/server/routes/www/www.go b/server/routes/www/www.go index 8d54fea..d318863 100644 --- a/server/routes/www/www.go +++ b/server/routes/www/www.go @@ -1,14 +1,41 @@ package www import ( + "context" + "net/http" + "os" + "github.com/go-chi/chi/v5" + chimiddleware "github.com/go-chi/chi/v5/middleware" + "github.com/tigorlazuardi/redmage/config" "github.com/tigorlazuardi/redmage/db/queries/subreddits" + "github.com/tigorlazuardi/redmage/pkg/log" + "github.com/tigorlazuardi/redmage/server/routes/middleware" ) +var PublicFS = os.DirFS("public") + type WWW struct { Subreddits *subreddits.Queries + Config *config.Config } func (www *WWW) Register(router chi.Router) { - router.Get("/", www.Home) + router.Use(chimiddleware.RequestID) + router.Use(chimiddleware.RealIP) + router. + With(chimiddleware.RequestLogger(middleware.ChiSimpleLogger{})). + Mount("/public", http.StripPrefix("/public", http.FileServer(http.FS(PublicFS)))) + + if www.Config.Bool("http.hotreload") { + log.New(context.Background()).Debug("enabled hot reload") + router. + With(chimiddleware.RequestLogger(middleware.ChiSimpleLogger{})). + Get("/hot_reload", www.CreateHotReloadRoute()) + } + + router.Group(func(r chi.Router) { + r.Use(chimiddleware.RequestLogger(middleware.ChiLogger{})) + r.Get("/", www.Home) + }) } diff --git a/views/components/head.templ b/views/components/head.templ new file mode 100644 index 0000000..6518f26 --- /dev/null +++ b/views/components/head.templ @@ -0,0 +1,17 @@ +package components + +import "github.com/tigorlazuardi/redmage/views" + +templ Head(vc *views.Context) { + + + + + + + + if vc.Config.Bool("http.hotreload") { + + } + +} diff --git a/views/context.go b/views/context.go new file mode 100644 index 0000000..ec2e3b1 --- /dev/null +++ b/views/context.go @@ -0,0 +1,19 @@ +package views + +import ( + "net/http" + + "github.com/tigorlazuardi/redmage/config" +) + +type Context struct { + Config *config.Config + Request *http.Request +} + +func NewContext(config *config.Config, request *http.Request) *Context { + return &Context{ + Config: config, + Request: request, + } +} diff --git a/views/homeview/homeview.templ b/views/homeview/homeview.templ index fa152ae..9eb4455 100644 --- a/views/homeview/homeview.templ +++ b/views/homeview/homeview.templ @@ -1,5 +1,11 @@ package homeview -templ Home() { -
Hello World
+import "github.com/tigorlazuardi/redmage/views/components" +import "github.com/tigorlazuardi/redmage/views" + +templ Home(vc *views.Context) { + @components.Doctype() { + @components.Head(vc) +
Hello World
+ } }