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 = []
|
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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
13
main.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
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
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
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
|
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>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue