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) { | ||||
|     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> | ||||
|  | ||||
							
								
								
									
										6
									
								
								ui/html/htmx/guestbookcreate.part.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								ui/html/htmx/guestbookcreate.part.html
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										1
									
								
								ui/html/htmx/guestbookcreatebutton.part.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								ui/html/htmx/guestbookcreatebutton.part.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <button hx-get="/guestbooks/create" hx-target="closest">New Guestbook</button> | ||||
							
								
								
									
										7
									
								
								ui/html/htmx/guestbooklist.part.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								ui/html/htmx/guestbooklist.part.html
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										5
									
								
								ui/html/htmx/newguestbook.part.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								ui/html/htmx/newguestbook.part.html
									
									
									
									
									
										Normal file
									
								
							| @ -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 }} | ||||
|  | ||||
							
								
								
									
										8
									
								
								ui/html/partials/guestbookcreate.part.tmpl.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ui/html/partials/guestbookcreate.part.tmpl.html
									
									
									
									
									
										Normal 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 }} | ||||
| @ -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
									
								
							
							
						
						
									
										1
									
								
								ui/static/js/htmx.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user