views: added simple home view with head component

This commit is contained in:
Tigor Hutasuhut 2024-04-08 21:50:52 +07:00
parent 49d1865b6b
commit c1841eb88a
12 changed files with 186 additions and 5 deletions

View file

@ -23,7 +23,7 @@ poll_interval = 0
post_cmd = [] post_cmd = []
pre_cmd = [ pre_cmd = [
"mkdir -p public", "mkdir -p public",
"tailwindcss -i views/styles.css -o public/style.css", "tailwindcss -i views/style.css -o public/style.css",
"templ generate", "templ generate",
"sqlc generate", "sqlc generate",
] ]

View file

@ -28,10 +28,12 @@ var serveCmd = &cobra.Command{
api := &api.API{ api := &api.API{
Subreddits: subreddits, Subreddits: subreddits,
Config: cfg,
} }
www := &www.WWW{ www := &www.WWW{
Subreddits: subreddits, Subreddits: subreddits,
Config: cfg,
} }
server := server.New(cfg, api, www) server := server.New(cfg, api, www)

View file

@ -14,4 +14,5 @@ var DefaultConfig = map[string]any{
"http.port": "8080", "http.port": "8080",
"http.host": "0.0.0.0", "http.host": "0.0.0.0",
"http.shutdown_timeout": "5s", "http.shutdown_timeout": "5s",
"http.hotreload": false,
} }

13
main.go
View file

@ -3,17 +3,30 @@ package main
import ( import (
"context" "context"
"embed" "embed"
"errors"
"io/fs"
"os" "os"
"github.com/joho/godotenv"
"github.com/tigorlazuardi/redmage/cli" "github.com/tigorlazuardi/redmage/cli"
"github.com/tigorlazuardi/redmage/db" "github.com/tigorlazuardi/redmage/db"
"github.com/tigorlazuardi/redmage/server/routes/www"
) )
//go:embed db/migrations/*.sql //go:embed db/migrations/*.sql
var Migrations embed.FS var Migrations embed.FS
//go:embed public/*
var PublicFS embed.FS
func main() { func main() {
_ = godotenv.Load()
db.Migrations = Migrations 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 { if err := cli.RootCmd.ExecuteContext(context.Background()); err != nil {
os.Exit(1) os.Exit(1)
} }

View file

@ -3,12 +3,14 @@ package api
import ( import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware" chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/tigorlazuardi/redmage/config"
"github.com/tigorlazuardi/redmage/db/queries/subreddits" "github.com/tigorlazuardi/redmage/db/queries/subreddits"
"github.com/tigorlazuardi/redmage/server/routes/middleware" "github.com/tigorlazuardi/redmage/server/routes/middleware"
) )
type API struct { type API struct {
Subreddits *subreddits.Queries Subreddits *subreddits.Queries
Config *config.Config
} }
func (api *API) Register(router chi.Router) { func (api *API) Register(router chi.Router) {

View file

@ -77,3 +77,28 @@ func formatDuration(dur time.Duration) string {
return fmt.Sprintf("%.3fms", nanosecs/float64(time.Millisecond)) 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)
}

View file

@ -3,9 +3,10 @@ package www
import ( import (
"net/http" "net/http"
"github.com/tigorlazuardi/redmage/views"
"github.com/tigorlazuardi/redmage/views/homeview" "github.com/tigorlazuardi/redmage/views/homeview"
) )
func (www *WWW) Home(rw http.ResponseWriter, r *http.Request) { 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)
} }

View file

@ -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"))
}
}
}
}
}

View file

@ -1,14 +1,41 @@
package www package www
import ( import (
"context"
"net/http"
"os"
"github.com/go-chi/chi/v5" "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/db/queries/subreddits"
"github.com/tigorlazuardi/redmage/pkg/log"
"github.com/tigorlazuardi/redmage/server/routes/middleware"
) )
var PublicFS = os.DirFS("public")
type WWW struct { type WWW struct {
Subreddits *subreddits.Queries Subreddits *subreddits.Queries
Config *config.Config
} }
func (www *WWW) Register(router chi.Router) { 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)
})
} }

View file

@ -0,0 +1,17 @@
package components
import "github.com/tigorlazuardi/redmage/views"
templ Head(vc *views.Context) {
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="keywords" content="Reddit, Image, Downloader"/>
<link rel="stylesheet" href="/public/style.css"/>
<link rel="icon" href="/public/favicon.svg"/>
<script src="/public/htmx-1.9.11.min.js"></script>
<script src="/public/htmx-response-targets-1.9.11.min.js"></script>
if vc.Config.Bool("http.hotreload") {
<script src="/public/hot_reload.js"></script>
}
</head>
}

19
views/context.go Normal file
View file

@ -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,
}
}

View file

@ -1,5 +1,11 @@
package homeview package homeview
templ Home() { import "github.com/tigorlazuardi/redmage/views/components"
<div>Hello World</div> import "github.com/tigorlazuardi/redmage/views"
templ Home(vc *views.Context) {
@components.Doctype() {
@components.Head(vc)
<div class="text-secondary">Hello World</div>
}
} }