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) {
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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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