interface improvements, use integer ids, add htmx for guestbook creation
This commit is contained in:
parent
e54875f943
commit
19364225c9
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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},
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 "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 }}
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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>
|
||||||
<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}}">
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue