interface improvements, use integer ids, add htmx for guestbook creation

This commit is contained in:
yequari 2024-12-15 10:23:53 -07:00
parent e54875f943
commit 19364225c9
18 changed files with 133 additions and 22 deletions

View File

@ -161,10 +161,15 @@ func (app *application) getUser(w http.ResponseWriter, r *http.Request) {
func (app *application) getGuestbookCreate(w http.ResponseWriter, r* http.Request) { func (app *application) getGuestbookCreate(w http.ResponseWriter, r* http.Request) {
data := app.newTemplateData(r) data := app.newTemplateData(r)
if r.Header.Get("HX-Request") == "true" {
app.renderHTMX(w, r, http.StatusOK, "guestbookcreate.part.html", data)
return
}
app.render(w, r, http.StatusOK, "guestbookcreate.view.tmpl.html", data) app.render(w, r, http.StatusOK, "guestbookcreate.view.tmpl.html", data)
} }
func (app *application) postGuestbookCreate(w http.ResponseWriter, r* http.Request) { func (app *application) postGuestbookCreate(w http.ResponseWriter, r* http.Request) {
userId := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId")
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
app.serverError(w, r, err) app.serverError(w, r, err)
@ -172,23 +177,29 @@ func (app *application) postGuestbookCreate(w http.ResponseWriter, r* http.Reque
} }
siteUrl := r.Form.Get("siteurl") siteUrl := r.Form.Get("siteurl")
shortId := app.createShortId() shortId := app.createShortId()
_, err = app.guestbooks.Insert(shortId, siteUrl, 0) _, err = app.guestbooks.Insert(shortId, siteUrl, userId)
if err != nil { if err != nil {
app.serverError(w, r, err) app.serverError(w, r, err)
return return
} }
app.sessionManager.Put(r.Context(), "flash", "Guestbook successfully created!") app.sessionManager.Put(r.Context(), "flash", "Guestbook successfully created!")
if r.Header.Get("HX-Request") == "true" {
w.Header().Add("HX-Trigger", "newGuestbook")
data := app.newTemplateData(r)
app.renderHTMX(w, r, http.StatusOK, "guestbookcreatebutton.part.html", data)
return
}
http.Redirect(w, r, fmt.Sprintf("/guestbooks/%s", shortIdToSlug(shortId)), http.StatusSeeOther) http.Redirect(w, r, fmt.Sprintf("/guestbooks/%s", shortIdToSlug(shortId)), http.StatusSeeOther)
} }
func (app *application) getGuestbookList(w http.ResponseWriter, r *http.Request) { func (app *application) getGuestbookList(w http.ResponseWriter, r *http.Request) {
userId := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId") userId := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId")
guestbooks, err := app.guestbooks.GetAll(userId) user, err := app.users.GetById(userId)
if err != nil { if err != nil {
app.serverError(w, r, err) app.serverError(w, r, err)
return return
} }
user, err := app.users.GetById(userId) guestbooks, err := app.guestbooks.GetAll(userId)
if err != nil { if err != nil {
app.serverError(w, r, err) app.serverError(w, r, err)
return return
@ -196,6 +207,10 @@ func (app *application) getGuestbookList(w http.ResponseWriter, r *http.Request)
data := app.newTemplateData(r) data := app.newTemplateData(r)
data.Guestbooks = guestbooks data.Guestbooks = guestbooks
data.User = user data.User = user
if r.Header.Get("HX-Request") == "true" {
app.renderHTMX(w, r, http.StatusCreated, "guestbooklist.part.html", data)
return
}
app.render(w, r, http.StatusOK, "guestbooklist.view.tmpl.html", data) app.render(w, r, http.StatusOK, "guestbooklist.view.tmpl.html", data)
} }
@ -312,3 +327,9 @@ func (app *application) postGuestbookCommentCreate(w http.ResponseWriter, r *htt
app.sessionManager.Put(r.Context(), "flash", "Comment successfully posted!") app.sessionManager.Put(r.Context(), "flash", "Comment successfully posted!")
http.Redirect(w, r, fmt.Sprintf("/guestbooks/%s", guestbookSlug), http.StatusSeeOther) http.Redirect(w, r, fmt.Sprintf("/guestbooks/%s", guestbookSlug), http.StatusSeeOther)
} }
func (app *application) deleteGuestbookComment(w http.ResponseWriter, r *http.Request) {
// slug := r.PathValue("id")
// shortId := slugToShortId(slug)
// app.guestbookComments.Delete(shortId)
}

View File

@ -8,6 +8,7 @@ import (
"strconv" "strconv"
"time" "time"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"github.com/gorilla/schema" "github.com/gorilla/schema"
) )
@ -25,6 +26,21 @@ func (app *application) clientError(w http.ResponseWriter, status int) {
http.Error(w, http.StatusText(status), status) http.Error(w, http.StatusText(status), status)
} }
func (app *application) renderHTMX(w http.ResponseWriter, r *http.Request, status int, page string, data templateData) {
ts, ok := app.templateCacheHTMX[page]
if !ok {
err := fmt.Errorf("the template %s does not exist", page)
app.serverError(w, r, err)
return
}
w.WriteHeader(status)
err := ts.Execute(w, data)
if err != nil {
app.serverError(w, r, err)
}
}
func (app *application) render(w http.ResponseWriter, r *http.Request, status int, page string, data templateData) { func (app *application) render(w http.ResponseWriter, r *http.Request, status int, page string, data templateData) {
ts, ok := app.templateCache[page] ts, ok := app.templateCache[page]
if !ok { if !ok {
@ -97,3 +113,14 @@ func (app *application) isAuthenticated(r *http.Request) bool {
} }
return isAuthenticated return isAuthenticated
} }
func (app *application) getCurrentUser(r *http.Request) *models.User {
if !app.isAuthenticated(r) {
return nil
}
user, ok := r.Context().Value(userNameContextKey).(models.User)
if !ok {
return nil
}
return &user
}

View File

@ -21,6 +21,7 @@ type application struct {
sequence uint16 sequence uint16
logger *slog.Logger logger *slog.Logger
templateCache map[string]*template.Template templateCache map[string]*template.Template
templateCacheHTMX map[string]*template.Template
guestbooks *models.GuestbookModel guestbooks *models.GuestbookModel
users *models.UserModel users *models.UserModel
guestbookComments *models.GuestbookCommentModel guestbookComments *models.GuestbookCommentModel
@ -48,6 +49,12 @@ func main() {
os.Exit(1) os.Exit(1)
} }
templateCacheHTMX, err := newHTMXTemplateCache()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
sessionManager := scs.New() sessionManager := scs.New()
sessionManager.Store = sqlite3store.New(db) sessionManager.Store = sqlite3store.New(db)
sessionManager.Lifetime = 12 * time.Hour sessionManager.Lifetime = 12 * time.Hour
@ -58,6 +65,7 @@ func main() {
app := &application{ app := &application{
sequence: 0, sequence: 0,
templateCache: templateCache, templateCache: templateCache,
templateCacheHTMX: templateCacheHTMX,
logger: logger, logger: logger,
sessionManager: sessionManager, sessionManager: sessionManager,
guestbooks: &models.GuestbookModel{DB: db}, guestbooks: &models.GuestbookModel{DB: db},

View File

@ -79,8 +79,14 @@ func (app *application) authenticate(next http.Handler) http.Handler {
app.serverError(w, r, err) app.serverError(w, r, err)
return return
} }
user, err := app.users.GetById(id)
if err != nil {
app.serverError(w, r, err)
return
}
if exists { if exists {
ctx := context.WithValue(r.Context(), isAuthenticatedContextKey, true) ctx := context.WithValue(r.Context(), isAuthenticatedContextKey, true)
ctx = context.WithValue(ctx, userNameContextKey, user)
r = r.WithContext(ctx) r = r.WithContext(ctx)
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)

View File

@ -22,6 +22,7 @@ type templateData struct {
Form any Form any
IsAuthenticated bool IsAuthenticated bool
CSRFToken string CSRFToken string
CurrentUser *models.User
} }
func humanDate(t time.Time) string { func humanDate(t time.Time) string {
@ -34,6 +35,23 @@ var functions = template.FuncMap {
"slugToShortId": slugToShortId, "slugToShortId": slugToShortId,
} }
func newHTMXTemplateCache() (map[string]*template.Template, error) {
cache := map[string]*template.Template{}
pages, err := filepath.Glob("./ui/html/htmx/*.part.html")
if err != nil {
return nil, err
}
for _, page := range pages {
name := filepath.Base(page)
ts, err := template.New(name).Funcs(functions).ParseFiles(page)
if err != nil {
return nil, err
}
cache[name] = ts
}
return cache, nil
}
func newTemplateCache() (map[string]*template.Template, error) { func newTemplateCache() (map[string]*template.Template, error) {
cache := map[string]*template.Template{} cache := map[string]*template.Template{}
pages, err := filepath.Glob("./ui/html/pages/*.tmpl.html") pages, err := filepath.Glob("./ui/html/pages/*.tmpl.html")
@ -65,5 +83,6 @@ func (app *application) newTemplateData(r *http.Request) templateData {
Flash: app.sessionManager.PopString(r.Context(), "flash"), Flash: app.sessionManager.PopString(r.Context(), "flash"),
IsAuthenticated: app.isAuthenticated(r), IsAuthenticated: app.isAuthenticated(r),
CSRFToken: nosurf.Token(r), CSRFToken: nosurf.Token(r),
CurrentUser: app.getCurrentUser(r),
} }
} }

View File

@ -13,7 +13,7 @@ CREATE TABLE guestbooks (
Id integer primary key autoincrement, Id integer primary key autoincrement,
ShortId integer UNIQUE NOT NULL, ShortId integer UNIQUE NOT NULL,
SiteUrl varchar(512) NOT NULL, SiteUrl varchar(512) NOT NULL,
UserId blob(16) NOT NULL, UserId integer NOT NULL,
Created datetime NOT NULL, Created datetime NOT NULL,
IsDeleted boolean NOT NULL DEFAULT FALSE, IsDeleted boolean NOT NULL DEFAULT FALSE,
IsActive boolean NOT NULL DEFAULT TRUE, IsActive boolean NOT NULL DEFAULT TRUE,
@ -25,8 +25,8 @@ CREATE TABLE guestbooks (
CREATE TABLE guestbook_comments ( CREATE TABLE guestbook_comments (
Id integer primary key autoincrement, Id integer primary key autoincrement,
ShortId integer UNIQUE NOT NULL, ShortId integer UNIQUE NOT NULL,
GuestbookId blob(16) NOT NULL, GuestbookId integer NOT NULL,
ParentId blob(16), ParentId integer,
AuthorName varchar(256) NOT NULL, AuthorName varchar(256) NOT NULL,
AuthorEmail varchar(256) NOT NULL, AuthorEmail varchar(256) NOT NULL,
AuthorSite varchar(256), AuthorSite varchar(256),

View File

@ -2,14 +2,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>{{ template "title" }} - Guestbook</title> <title>{{ template "title" }} - webweav.ing</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/css/style.css" rel="stylesheet"> <link href="/static/css/style.css" rel="stylesheet">
<script src="/static/js/htmx.min.js"></script>
</head> </head>
<body> <body>
<header> <header>
<h1><a href="/">Guestbook</a></h1> <h1><a href="/">webweav.ing</a></h1>
</header> </header>
{{ template "nav" . }} {{ template "nav" . }}
<main> <main>

View File

@ -0,0 +1,6 @@
<form hx-post="/guestbooks/create" hx-target="closest div">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label for="siteurl">Site URL: </label>
<input type="text" name="siteurl" id="siteurl" required />
<button type="submit">Submit</button>
</form>

View File

@ -0,0 +1 @@
<button hx-get="/guestbooks/create" hx-target="closest">New Guestbook</button>

View File

@ -0,0 +1,7 @@
<ul id="guestbooks" hx-get="/guestbooks" hx-trigger="newGuestbook from:body">
{{ range .Guestbooks }}
<li><a href="/guestbooks/{{ shortIdToSlug .ShortId }}">{{ with .SiteUrl }}{{ . }}{{ else }}Untitled{{ end }}</a></li>
{{ else }}
<p>No Guestbooks yet</p>
{{ end }}
</ul>

View File

@ -0,0 +1,5 @@
{{ with .Guestbook }}
<li>
<a href="/guestbooks/{{ shortIdToSlug .ShortId }}">{{ with .SiteUrl }}{{ . }}{{ else }}Untitled{{ end }}</a>
</li>
{{ end }}

View File

@ -1,6 +1,7 @@
{{ define "title" }}New Comment{{ end }} {{ define "title" }}New Comment{{ end }}
{{ define "main" }} {{ define "main" }}
<form action="/guestbooks/{{ shortIdToSlug .Guestbook.ShortId }}/comments/create" method="post"> <form action="/guestbooks/{{ shortIdToSlug .Guestbook.ShortId }}/comments/create" method="post">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<div> <div>
<label for="authorname">Name: </label> <label for="authorname">Name: </label>
{{ with .Form.FieldErrors.authorName }} {{ with .Form.FieldErrors.authorName }}

View File

@ -1,9 +1,4 @@
{{ define "title" }}Create a Guestbook{{ end }} {{ define "title" }}Create a Guestbook{{ end }}
{{ define "main" }} {{ define "main" }}
<form action="/guestbooks/create" method="post"> {{ template "guestbookcreate" }}
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label for="siteurl">Site URL: </label>
<input type="text" name="siteurl" id="siteurl" required />
<input type="submit" />
</form>
{{ end }} {{ end }}

View File

@ -1,7 +1,10 @@
{{ define "title" }} Guestbooks {{ end }} {{ define "title" }} Guestbooks {{ end }}
{{ define "main" }} {{ define "main" }}
<h1>Guestbooks run by {{ .User.Username }}</h1> <h1>Guestbooks run by {{ .User.Username }}</h1>
<ul> <div>
<button hx-get="/guestbooks/create" hx-target="closest div">New Guestbook</button>
</div>
<ul id="guestbooks" hx-get="/guestbooks" hx-trigger="newGuestbook from:body" hx-swap="outerHTML">
{{ range .Guestbooks }} {{ range .Guestbooks }}
<li><a href="/guestbooks/{{ shortIdToSlug .ShortId }}">{{ with .SiteUrl }}{{ . }}{{ else }}Untitled{{ end }}</a></li> <li><a href="/guestbooks/{{ shortIdToSlug .ShortId }}">{{ with .SiteUrl }}{{ . }}{{ else }}Untitled{{ end }}</a></li>
{{ else }} {{ else }}

View File

@ -1,10 +1,12 @@
{{ define "title" }}Home{{ end }} {{ define "title" }}Home{{ end }}
{{ define "main" }} {{ define "main" }}
<h2>Latest Guestbooks</h2> {{ if .IsAuthenticated }}
<h2>Tools</h2>
<p> <p>
<a href="/guestbooks">View Guestbooks</a> <a href="/guestbooks">Guestbooks</a>
</p>
<p>
<a href="/guestbooks/create">Create a Guestbook</a>
</p> </p>
{{ else }}
<h2>Welcome</h2>
Welcome to webweav.ing, a collection of webmastery tools created by the <a href="https://32bit.cafe">32-Bit Cafe</a>.
{{ end }}
{{ end }} {{ end }}

View File

@ -0,0 +1,8 @@
{{ define "guestbookcreate" }}
<form action="/guestbooks/create" method="post">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label for="siteurl">Site URL: </label>
<input type="text" name="siteurl" id="siteurl" required />
<input type="submit" />
</form>
{{ end }}

View File

@ -6,8 +6,8 @@
</div> </div>
<div> <div>
{{ if .IsAuthenticated }} {{ if .IsAuthenticated }}
{{ with .User }} {{ with .CurrentUser }}
<a href="/users/{{ .User.ShortId }}">{{ .User.Username }}</a> Welcome, {{ .Username }}
{{ end }} {{ end }}
<form action="/users/logout" method="post"> <form action="/users/logout" method="post">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}"> <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">

1
ui/static/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long