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

@ -161,10 +161,15 @@ func (app *application) getUser(w http.ResponseWriter, r *http.Request) {
func (app *application) getGuestbookCreate(w http.ResponseWriter, r* http.Request) {
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)
}
func (app *application) postGuestbookCreate(w http.ResponseWriter, r* http.Request) {
userId := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId")
err := r.ParseForm()
if err != nil {
app.serverError(w, r, err)
@ -172,23 +177,29 @@ func (app *application) postGuestbookCreate(w http.ResponseWriter, r* http.Reque
}
siteUrl := r.Form.Get("siteurl")
shortId := app.createShortId()
_, err = app.guestbooks.Insert(shortId, siteUrl, 0)
_, err = app.guestbooks.Insert(shortId, siteUrl, userId)
if err != nil {
app.serverError(w, r, err)
return
}
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)
}
func (app *application) getGuestbookList(w http.ResponseWriter, r *http.Request) {
userId := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId")
guestbooks, err := app.guestbooks.GetAll(userId)
user, err := app.users.GetById(userId)
if err != nil {
app.serverError(w, r, err)
return
}
user, err := app.users.GetById(userId)
guestbooks, err := app.guestbooks.GetAll(userId)
if err != nil {
app.serverError(w, r, err)
return
@ -196,6 +207,10 @@ func (app *application) getGuestbookList(w http.ResponseWriter, r *http.Request)
data := app.newTemplateData(r)
data.Guestbooks = guestbooks
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)
}
@ -312,3 +327,9 @@ func (app *application) postGuestbookCommentCreate(w http.ResponseWriter, r *htt
app.sessionManager.Put(r.Context(), "flash", "Comment successfully posted!")
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)
}

@ -8,6 +8,7 @@ import (
"strconv"
"time"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"github.com/gorilla/schema"
)
@ -25,6 +26,21 @@ func (app *application) clientError(w http.ResponseWriter, status int) {
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) {
ts, ok := app.templateCache[page]
if !ok {
@ -97,3 +113,14 @@ func (app *application) isAuthenticated(r *http.Request) bool {
}
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
}

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

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

@ -22,6 +22,7 @@ type templateData struct {
Form any
IsAuthenticated bool
CSRFToken string
CurrentUser *models.User
}
func humanDate(t time.Time) string {
@ -34,6 +35,23 @@ var functions = template.FuncMap {
"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) {
cache := map[string]*template.Template{}
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"),
IsAuthenticated: app.isAuthenticated(r),
CSRFToken: nosurf.Token(r),
CurrentUser: app.getCurrentUser(r),
}
}

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

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

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

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

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

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

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

@ -1,9 +1,4 @@
{{ define "title" }}Create a Guestbook{{ end }}
{{ define "main" }}
<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>
{{ template "guestbookcreate" }}
{{ end }}

@ -1,7 +1,10 @@
{{ define "title" }} Guestbooks {{ end }}
{{ define "main" }}
<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 }}
<li><a href="/guestbooks/{{ shortIdToSlug .ShortId }}">{{ with .SiteUrl }}{{ . }}{{ else }}Untitled{{ end }}</a></li>
{{ else }}

@ -1,10 +1,12 @@
{{ define "title" }}Home{{ end }}
{{ define "main" }}
<h2>Latest Guestbooks</h2>
{{ if .IsAuthenticated }}
<h2>Tools</h2>
<p>
<a href="/guestbooks">View Guestbooks</a>
</p>
<p>
<a href="/guestbooks/create">Create a Guestbook</a>
<a href="/guestbooks">Guestbooks</a>
</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 }}

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

@ -6,8 +6,8 @@
</div>
<div>
{{ if .IsAuthenticated }}
{{ with .User }}
<a href="/users/{{ .User.ShortId }}">{{ .User.Username }}</a>
{{ with .CurrentUser }}
Welcome, {{ .Username }}
{{ end }}
<form action="/users/logout" method="post">
<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