views: added simple home view with head component
This commit is contained in:
parent
49d1865b6b
commit
c1841eb88a
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
13
main.go
13
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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
68
server/routes/www/hot_reload.go
Normal file
68
server/routes/www/hot_reload.go
Normal 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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
17
views/components/head.templ
Normal file
17
views/components/head.templ
Normal 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
19
views/context.go
Normal 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,
|
||||
}
|
||||
}
|
|
@ -1,5 +1,11 @@
|
|||
package homeview
|
||||
|
||||
templ Home() {
|
||||
<div>Hello World</div>
|
||||
import "github.com/tigorlazuardi/redmage/views/components"
|
||||
import "github.com/tigorlazuardi/redmage/views"
|
||||
|
||||
templ Home(vc *views.Context) {
|
||||
@components.Doctype() {
|
||||
@components.Head(vc)
|
||||
<div class="text-secondary">Hello World</div>
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue