Compare commits

...

37 Commits

Author SHA1 Message Date
a08a6b3f41 implement guestbook visibility and comment toggling 2025-06-02 23:09:14 -07:00
c3f08fb745 scaffolding for guestbook settings ui 2025-05-25 23:08:20 -07:00
019b31aa9f implement guestbook settings db code 2025-05-25 23:07:44 -07:00
260ddbe740 move user settings code 2025-05-25 23:05:53 -07:00
1a41a94b41 use generic SetUserSettings function, remove specific function 2025-05-23 10:15:30 -07:00
3c811c9533 clean up unnecessary code 2025-05-18 11:27:54 -07:00
8b681922f6 restructure settings code 2025-05-18 10:49:24 -07:00
2b9d0e11b8 generic setting update code 2025-05-09 23:59:54 -07:00
3ce55bb870 user settings 2025-05-09 20:24:53 -07:00
82c00b9a01 implement user settings for timezone 2025-05-03 23:14:58 -07:00
8222af6d98 update gitignore 2025-03-25 20:50:30 -07:00
2c10c1f26a delete templ.txt files 2025-03-25 20:49:47 -07:00
025f273f48 improve UI with classless css library and other various modifications 2025-03-24 21:00:19 -07:00
f239b17255 Merge branch '14-cleanup' into dev 2025-03-23 16:16:56 -07:00
1b44d23dd6 prepare for test deployment
embed static files
remove hard tls requirement
2025-03-23 16:16:30 -07:00
a41244fb62 Merge pull request '14-cleanup' (#15) from 14-cleanup into dev
Reviewed-on: #15
2025-03-23 22:16:50 +00:00
64ff095002 add page to display all guestbooks 2025-03-23 15:15:08 -07:00
303e51f53e fix website creation event trigger 2025-03-23 14:53:13 -07:00
e065d5630c implement hiding and unhiding a comment 2025-03-23 14:39:48 -07:00
fa5507e719 convert user deleted field 2025-03-23 14:39:31 -07:00
7537fa2e92 implement comment deletion and debug mode 2025-03-23 12:57:49 -07:00
365e727284 cleanup unused code and add not implemented handler so links don't 404 2025-03-23 08:48:56 -07:00
672dca5bb9 Merge pull request '10-WebsiteModel' (#13) from 10-WebsiteModel into dev
Reviewed-on: #13
2025-03-22 20:15:36 +00:00
c8ff7021c8 finish conversion to templ, add website model
The primary model users will interact with is now Websites, which will have a single guestbook associated with it (though this is not enforced by the database).
2025-03-22 13:13:13 -07:00
11c0815676 check user owns guestbook 2025-03-15 12:18:17 -07:00
6c18752230 new guestbook functions 2025-03-15 11:00:10 -07:00
36bfde1e4d upgrade packages 2025-03-15 10:59:48 -07:00
2a15f4b91d error handling for usermodel and reformatting 2025-03-15 10:57:51 -07:00
adaf6cf87d slug and shortId conversion functions 2025-03-15 10:57:22 -07:00
5c8817aa2a split handlers into multiple files 2025-03-15 10:49:39 -07:00
b58ea19a86 remove unused function and get message queue 2025-03-09 00:02:41 -07:00
e658f15463 separate guestbook from dashboard, add specific comments view 2025-03-01 16:44:35 -07:00
d899769ba0 convert most frontend to templ, implement design wireframes 2025-03-01 13:12:21 -07:00
19364225c9 interface improvements, use integer ids, add htmx for guestbook creation 2024-12-15 10:25:37 -07:00
e54875f943 add user login 2024-11-11 12:55:01 -07:00
741b304032 create and retrieve handlers, basic templates and styling, sessions 2024-10-22 22:25:30 -07:00
4fc9bb5c1f initial commit 2024-10-15 21:34:57 -07:00
37 changed files with 5688 additions and 0 deletions

11
.gitignore vendored
View File

@ -21,3 +21,14 @@
# Go workspace file
go.work
# sqlite3 databases
*.db
# air config
.air.toml
/tmp
tls/
test.db.old
.gitignore
.nvim/session
*templ.txt

6
cmd/web/context.go Normal file
View File

@ -0,0 +1,6 @@
package main
type contextKey string
const isAuthenticatedContextKey = contextKey("isAuthenticated")
const userNameContextKey = contextKey("username")

19
cmd/web/handlers.go Normal file
View File

@ -0,0 +1,19 @@
package main
import (
"net/http"
"git.32bit.cafe/32bitcafe/guestbook/ui/views"
)
func (app *application) home(w http.ResponseWriter, r *http.Request) {
if app.isAuthenticated(r) {
http.Redirect(w, r, "/websites", http.StatusSeeOther)
return
}
views.Home("Home", app.newCommonData(r)).Render(r.Context(), w)
}
func (app *application) notImplemented(w http.ResponseWriter, r *http.Request) {
views.ComingSoon("Coming Soon", app.newCommonData(r)).Render(r.Context(), w)
}

View File

@ -0,0 +1,409 @@
package main
import (
"errors"
"fmt"
"net/http"
"strconv"
"time"
"git.32bit.cafe/32bitcafe/guestbook/internal/forms"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"git.32bit.cafe/32bitcafe/guestbook/internal/validator"
"git.32bit.cafe/32bitcafe/guestbook/ui/views"
)
func (app *application) getGuestbook(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
if !website.Guestbook.Settings.IsVisible {
u := app.getCurrentUser(r)
if u == nil {
app.clientError(w, http.StatusForbidden)
return
}
if u.ID != website.UserId {
app.clientError(w, http.StatusForbidden)
return
}
}
comments, err := app.guestbookComments.GetAll(website.Guestbook.ID)
if err != nil {
app.serverError(w, r, err)
return
}
data := app.newCommonData(r)
views.GuestbookView("Guestbook", data, website, website.Guestbook, comments, forms.CommentCreateForm{}).Render(r.Context(), w)
}
func (app *application) getGuestbookSettings(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
}
website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
}
data := app.newCommonData(r)
views.GuestbookSettingsView(data, website).Render(r.Context(), w)
}
func (app *application) putGuestbookSettings(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
}
var form forms.GuestbookSettingsForm
err = app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
app.serverError(w, r, err)
return
}
form.CheckField(validator.PermittedValue(form.Visibility, "true", "false"), "gb_visible", "Invalid value")
form.CheckField(validator.PermittedValue(form.CommentingEnabled, models.ValidDisableDurations...), "gb_visible", "Invalid value")
form.CheckField(validator.PermittedValue(form.WidgetsEnabled, "true", "false"), "gb_remote", "Invalid value")
if !form.Valid() {
// TODO: rerender template with errors
app.clientError(w, http.StatusUnprocessableEntity)
}
c, err := strconv.ParseBool(form.CommentingEnabled)
if err != nil {
website.Guestbook.Settings.IsCommentingEnabled = false
website.Guestbook.Settings.ReenableCommenting, err = app.durationToTime(form.CommentingEnabled)
if err != nil {
app.serverError(w, r, err)
}
} else {
website.Guestbook.Settings.IsCommentingEnabled = c
}
// can skip error checking for these two since we verify valid values above
website.Guestbook.Settings.IsVisible, err = strconv.ParseBool(form.Visibility)
if err != nil {
app.serverError(w, r, err)
}
website.Guestbook.Settings.AllowRemoteHostAccess, err = strconv.ParseBool(form.WidgetsEnabled)
if err != nil {
app.serverError(w, r, err)
}
err = app.guestbooks.UpdateGuestbookSettings(website.Guestbook.ID, website.Guestbook.Settings)
if err != nil {
app.serverError(w, r, err)
return
}
app.sessionManager.Put(r.Context(), "flash", "Settings changed successfully")
data := app.newCommonData(r)
w.Header().Add("HX-Refresh", "true")
views.GuestbookSettingsView(data, website).Render(r.Context(), w)
}
func (app *application) getGuestbookComments(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
}
comments, err := app.guestbookComments.GetAll(website.Guestbook.ID)
if err != nil {
app.serverError(w, r, err)
return
}
data := app.newCommonData(r)
views.GuestbookDashboardCommentsView("Comments", data, website, website.Guestbook, comments).Render(r.Context(), w)
}
func (app *application) getGuestbookCommentCreate(w http.ResponseWriter, r *http.Request) {
// TODO: This will be the embeddable form
slug := r.PathValue("id")
website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
}
data := app.newCommonData(r)
form := forms.CommentCreateForm{}
views.CreateGuestbookComment("New Comment", data, website, website.Guestbook, form).Render(r.Context(), w)
}
func (app *application) postGuestbookCommentCreate(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
}
if !website.Guestbook.CanComment() {
app.clientError(w, http.StatusForbidden)
return
}
var form forms.CommentCreateForm
err = app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.CheckField(validator.NotBlank(form.AuthorName), "authorName", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.AuthorName, 256), "authorName", "This field cannot be more than 256 characters long")
form.CheckField(validator.NotBlank(form.AuthorEmail), "authorEmail", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.AuthorEmail, 256), "authorEmail", "This field cannot be more than 256 characters long")
form.CheckField(validator.NotBlank(form.AuthorSite), "authorSite", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.AuthorSite, 256), "authorSite", "This field cannot be more than 256 characters long")
form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
if !form.Valid() {
comments, err := app.guestbookComments.GetAll(website.Guestbook.ID)
if err != nil {
app.serverError(w, r, err)
return
}
data := app.newCommonData(r)
views.GuestbookView("Guestbook", data, website, website.Guestbook, comments, forms.CommentCreateForm{}).Render(r.Context(), w)
return
}
shortId := app.createShortId()
_, err = app.guestbookComments.Insert(shortId, website.Guestbook.ID, 0, form.AuthorName, form.AuthorEmail, form.AuthorSite, form.Content, "", true)
if err != nil {
app.serverError(w, r, err)
return
}
// app.sessionManager.Put(r.Context(), "flash", "Comment successfully posted!")
http.Redirect(w, r, fmt.Sprintf("/websites/%s/guestbook", slug), http.StatusSeeOther)
}
func (app *application) getCommentQueue(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
}
comments, err := app.guestbookComments.GetUnpublished(website.Guestbook.ID)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
data := app.newCommonData(r)
views.GuestbookDashboardCommentsView("Message Queue", data, website, website.Guestbook, comments).Render(r.Context(), w)
}
func (app *application) getCommentTrash(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
}
comments, err := app.guestbookComments.GetDeleted(website.Guestbook.ID)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
data := app.newCommonData(r)
views.GuestbookDashboardCommentsView("Trash", data, website, website.Guestbook, comments).Render(r.Context(), w)
}
func (app *application) putHideGuestbookComment(w http.ResponseWriter, r *http.Request) {
user := app.getCurrentUser(r)
wSlug := r.PathValue("id")
website, err := app.websites.Get(slugToShortId(wSlug))
if err != nil {
app.logger.Info("website 404")
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
if user.ID != website.UserId {
app.clientError(w, http.StatusUnauthorized)
}
website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
}
cSlug := r.PathValue("commentId")
comment, err := app.guestbookComments.Get(slugToShortId(cSlug))
if err != nil {
app.logger.Info("comment 404")
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
comment.IsPublished = !comment.IsPublished
err = app.guestbookComments.UpdateComment(&comment)
if err != nil {
app.serverError(w, r, err)
}
}
func (app *application) deleteGuestbookComment(w http.ResponseWriter, r *http.Request) {
user := app.getCurrentUser(r)
wSlug := r.PathValue("id")
website, err := app.websites.Get(slugToShortId(wSlug))
if err != nil {
app.logger.Info("website 404")
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
if user.ID != website.UserId {
app.clientError(w, http.StatusUnauthorized)
}
website.Guestbook, err = app.guestbooks.Get(website.Guestbook.ShortId)
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
}
cSlug := r.PathValue("commentId")
comment, err := app.guestbookComments.Get(slugToShortId(cSlug))
if err != nil {
app.logger.Info("comment 404")
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
comment.Deleted = time.Now().UTC()
err = app.guestbookComments.UpdateComment(&comment)
if err != nil {
app.serverError(w, r, err)
}
}
func (app *application) getAllGuestbooks(w http.ResponseWriter, r *http.Request) {
websites, err := app.websites.GetAll()
if err != nil {
app.serverError(w, r, err)
}
views.AllGuestbooksView(app.newCommonData(r), websites).Render(r.Context(), w)
}

169
cmd/web/handlers_user.go Normal file
View File

@ -0,0 +1,169 @@
package main
import (
"errors"
"net/http"
"time"
"git.32bit.cafe/32bitcafe/guestbook/internal/forms"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"git.32bit.cafe/32bitcafe/guestbook/internal/validator"
"git.32bit.cafe/32bitcafe/guestbook/ui/views"
)
func (app *application) getUserRegister(w http.ResponseWriter, r *http.Request) {
form := forms.UserRegistrationForm{}
data := app.newCommonData(r)
views.UserRegistration("User Registration", data, form).Render(r.Context(), w)
}
func (app *application) getUserLogin(w http.ResponseWriter, r *http.Request) {
views.UserLogin("Login", app.newCommonData(r), forms.UserLoginForm{}).Render(r.Context(), w)
}
func (app *application) postUserRegister(w http.ResponseWriter, r *http.Request) {
var form forms.UserRegistrationForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.CheckField(validator.NotBlank(form.Name), "name", "This field cannot be blank")
form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank")
form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email address")
form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank")
form.CheckField(validator.MinChars(form.Password, 8), "password", "This field must be at least 8 characters long")
if !form.Valid() {
data := app.newCommonData(r)
w.WriteHeader(http.StatusUnprocessableEntity)
views.UserRegistration("User Registration", data, form).Render(r.Context(), w)
return
}
shortId := app.createShortId()
settings := DefaultUserSettings()
err = app.users.Insert(shortId, form.Name, form.Email, form.Password, settings)
if err != nil {
if errors.Is(err, models.ErrDuplicateEmail) {
form.AddFieldError("email", "Email address is already in use")
data := app.newCommonData(r)
w.WriteHeader(http.StatusUnprocessableEntity)
views.UserRegistration("User Registration", data, form).Render(r.Context(), w)
} else {
app.serverError(w, r, err)
}
return
}
app.sessionManager.Put(r.Context(), "flash", "Registration successful. Please log in.")
http.Redirect(w, r, "/users/login", http.StatusSeeOther)
}
func (app *application) postUserLogin(w http.ResponseWriter, r *http.Request) {
var form forms.UserLoginForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
}
form.CheckField(validator.NotBlank(form.Email), "email", "This field cannot be blank")
form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "This field must be a valid email address")
form.CheckField(validator.NotBlank(form.Password), "password", "This field cannot be blank")
if !form.Valid() {
data := app.newCommonData(r)
w.WriteHeader(http.StatusUnprocessableEntity)
views.UserLogin("Login", data, form).Render(r.Context(), w)
return
}
id, err := app.users.Authenticate(form.Email, form.Password)
if err != nil {
if errors.Is(err, models.ErrInvalidCredentials) {
form.AddNonFieldError("Email or password is incorrect")
data := app.newCommonData(r)
views.UserLogin("Login", data, form).Render(r.Context(), w)
} else {
app.serverError(w, r, err)
}
return
}
err = app.sessionManager.RenewToken(r.Context())
if err != nil {
app.serverError(w, r, err)
return
}
app.sessionManager.Put(r.Context(), "authenticatedUserId", id)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (app *application) postUserLogout(w http.ResponseWriter, r *http.Request) {
err := app.sessionManager.RenewToken(r.Context())
if err != nil {
app.serverError(w, r, err)
return
}
app.sessionManager.Remove(r.Context(), "authenticatedUserId")
app.sessionManager.Put(r.Context(), "flash", "You've been logged out successfully!")
w.Header().Add("HX-Redirect", "/")
// http.Redirect(w, r, "/", http.StatusSeeOther)
}
// func (app *application) getUsersList(w http.ResponseWriter, r *http.Request) {
// // skip templ conversion for this view, which will not be available in the final app
// // something similar will be available in the admin panel
// users, err := app.users.GetAll()
// if err != nil {
// app.serverError(w, r, err)
// return
// }
// data := app.newTemplateData(r)
// data.Users = users
// app.render(w, r, http.StatusOK, "userlist.view.tmpl.html", data)
// }
func (app *application) getUser(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
user, err := app.users.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
data := app.newCommonData(r)
views.UserProfile(user.Username, data, user).Render(r.Context(), w)
}
func (app *application) getUserSettings(w http.ResponseWriter, r *http.Request) {
data := app.newCommonData(r)
views.UserSettingsView(data, app.timezones).Render(r.Context(), w)
}
func (app *application) putUserSettings(w http.ResponseWriter, r *http.Request) {
user := app.getCurrentUser(r)
var form forms.UserSettingsForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
app.serverError(w, r, err)
return
}
form.CheckField(validator.PermittedValue(form.LocalTimezone, app.timezones...), "timezone", "Invalid value")
if !form.Valid() {
// TODO: rerender template with errors
app.clientError(w, http.StatusUnprocessableEntity)
}
user.Settings.LocalTimezone, err = time.LoadLocation(form.LocalTimezone)
if err != nil {
app.serverError(w, r, err)
return
}
err = app.users.UpdateUserSettings(user.ID, user.Settings)
if err != nil {
app.serverError(w, r, err)
return
}
app.sessionManager.Put(r.Context(), "flash", "Settings changed successfully")
data := app.newCommonData(r)
w.Header().Add("HX-Refresh", "true")
views.UserSettingsView(data, app.timezones).Render(r.Context(), w)
}

116
cmd/web/handlers_website.go Normal file
View File

@ -0,0 +1,116 @@
package main
import (
"errors"
"fmt"
"net/http"
"git.32bit.cafe/32bitcafe/guestbook/internal/forms"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"git.32bit.cafe/32bitcafe/guestbook/internal/validator"
"git.32bit.cafe/32bitcafe/guestbook/ui/views"
)
func (app *application) getWebsiteCreate(w http.ResponseWriter, r *http.Request) {
form := forms.WebsiteCreateForm{}
data := app.newCommonData(r)
views.WebsiteCreate("Add Website", data, form).Render(r.Context(), w)
}
func (app *application) postWebsiteCreate(w http.ResponseWriter, r *http.Request) {
userId := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId")
var form forms.WebsiteCreateForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.CheckField(validator.NotBlank(form.AuthorName), "authorName", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.AuthorName, 256), "authorName", "This field cannot exceed 256 characters")
form.CheckField(validator.NotBlank(form.Name), "sitename", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.Name, 256), "sitename", "This field cannot exceed 256 characters")
form.CheckField(validator.NotBlank(form.SiteUrl), "siteurl", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.SiteUrl, 512), "siteurl", "This field cannot exceed 512 characters")
if !form.Valid() {
data := app.newCommonData(r)
w.WriteHeader(http.StatusUnprocessableEntity)
views.WebsiteCreate("Add a Website", data, form).Render(r.Context(), w)
}
websiteShortID := app.createShortId()
websiteId, err := app.websites.Insert(websiteShortID, userId, form.Name, form.SiteUrl, form.AuthorName)
if err != nil {
app.serverError(w, r, err)
return
}
// TODO: how to handle website creation success but guestbook creation failure?
guestbookShortID := app.createShortId()
guestbookSettings := models.GuestbookSettings{
IsCommentingEnabled: true,
}
_, err = app.guestbooks.Insert(guestbookShortID, userId, websiteId, guestbookSettings)
if err != nil {
app.serverError(w, r, err)
return
}
app.sessionManager.Put(r.Context(), "flash", "Website successfully registered!")
if r.Header.Get("HX-Request") == "true" {
w.Header().Add("HX-Trigger", "newWebsite")
views.WebsiteCreateButton().Render(r.Context(), w)
return
}
http.Redirect(w, r, fmt.Sprintf("/websites/%s", shortIdToSlug(websiteShortID)), http.StatusSeeOther)
}
func (app *application) getWebsiteDashboard(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
user := app.getCurrentUser(r)
website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
if user.ID != website.UserId {
app.clientError(w, http.StatusUnauthorized)
}
data := app.newCommonData(r)
views.WebsiteDashboard("Guestbook", data, website).Render(r.Context(), w)
}
func (app *application) getWebsiteList(w http.ResponseWriter, r *http.Request) {
userId := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId")
websites, err := app.websites.GetAllUser(userId)
if err != nil {
app.serverError(w, r, err)
return
}
if r.Header.Get("HX-Request") == "true" {
w.Header().Add("HX-Trigger", "newWebsite")
}
data := app.newCommonData(r)
views.WebsiteList("My Websites", data, websites).Render(r.Context(), w)
}
func (app *application) getComingSoon(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("id")
user := app.getCurrentUser(r)
website, err := app.websites.Get(slugToShortId(slug))
if err != nil {
if errors.Is(err, models.ErrNoRecord) {
http.NotFound(w, r)
} else {
app.serverError(w, r, err)
}
return
}
if website.UserId != user.ID {
app.clientError(w, http.StatusForbidden)
}
views.WebsiteDashboardComingSoon("Coming Soon", app.newCommonData(r), website).Render(r.Context(), w)
}

130
cmd/web/helpers.go Normal file
View File

@ -0,0 +1,130 @@
package main
import (
"errors"
"fmt"
"math"
"net/http"
"runtime/debug"
"strconv"
"time"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"git.32bit.cafe/32bitcafe/guestbook/ui/views"
"github.com/gorilla/schema"
"github.com/justinas/nosurf"
)
func (app *application) serverError(w http.ResponseWriter, r *http.Request, err error) {
var (
method = r.Method
uri = r.URL.RequestURI()
)
app.logger.Error(err.Error(), "method", method, "uri", uri)
if app.debug {
http.Error(w, string(debug.Stack()), http.StatusInternalServerError)
app.logger.Error(err.Error(), "method", method, "uri", uri, "stack", string(debug.Stack()))
}
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
func (app *application) clientError(w http.ResponseWriter, status int) {
http.Error(w, http.StatusText(status), status)
}
func (app *application) nextSequence() uint16 {
val := app.sequence
if app.sequence == math.MaxUint16 {
app.sequence = 0
} else {
app.sequence += 1
}
return val
}
func (app *application) createShortId() uint64 {
now := time.Now().UTC()
epoch, err := time.Parse(time.RFC822Z, "01 Jan 20 00:00 -0000")
if err != nil {
fmt.Println(err)
return 0
}
d := now.Sub(epoch)
ms := d.Milliseconds()
seq := app.nextSequence()
return (uint64(ms) & 0x0FFFFFFFFFFFFFFF) | (uint64(seq) << 48)
}
func shortIdToSlug(id uint64) string {
slug := strconv.FormatUint(id, 36)
return slug
}
func slugToShortId(slug string) uint64 {
id, _ := strconv.ParseUint(slug, 36, 64)
return id
}
func (app *application) decodePostForm(r *http.Request, dst any) error {
err := r.ParseForm()
if err != nil {
return err
}
err = app.formDecoder.Decode(dst, r.PostForm)
if err != nil {
var multiErrors *schema.MultiError
if !errors.As(err, &multiErrors) {
panic(err)
}
return err
}
return nil
}
func (app *application) isAuthenticated(r *http.Request) bool {
isAuthenticated, ok := r.Context().Value(isAuthenticatedContextKey).(bool)
if !ok {
return false
}
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
}
func (app *application) newCommonData(r *http.Request) views.CommonData {
return views.CommonData{
CurrentYear: time.Now().Year(),
Flash: app.sessionManager.PopString(r.Context(), "flash"),
IsAuthenticated: app.isAuthenticated(r),
CSRFToken: nosurf.Token(r),
CurrentUser: app.getCurrentUser(r),
IsHtmx: r.Header.Get("Hx-Request") == "true",
}
}
func DefaultUserSettings() models.UserSettings {
return models.UserSettings{
LocalTimezone: time.Now().UTC().Location(),
}
}
func (app *application) durationToTime(duration string) (time.Time, error) {
var result time.Time
offset, err := time.ParseDuration(duration)
if err != nil {
return result, nil
}
result = time.Now().UTC().Add(offset)
return result, nil
}

161
cmd/web/main.go Normal file
View File

@ -0,0 +1,161 @@
package main
import (
"crypto/tls"
"database/sql"
"flag"
"log/slog"
"net/http"
"os"
"strings"
"time"
"unicode"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"github.com/alexedwards/scs/sqlite3store"
"github.com/alexedwards/scs/v2"
"github.com/gorilla/schema"
_ "github.com/mattn/go-sqlite3"
)
type application struct {
sequence uint16
logger *slog.Logger
websites *models.WebsiteModel
guestbooks *models.GuestbookModel
users *models.UserModel
guestbookComments *models.GuestbookCommentModel
sessionManager *scs.SessionManager
formDecoder *schema.Decoder
debug bool
timezones []string
}
func main() {
addr := flag.String("addr", ":3000", "HTTP network address")
dsn := flag.String("dsn", "guestbook.db", "data source name")
debug := flag.Bool("debug", false, "enable debug mode")
flag.Parse()
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
db, err := openDB(*dsn)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
defer db.Close()
sessionManager := scs.New()
sessionManager.Store = sqlite3store.New(db)
sessionManager.Lifetime = 12 * time.Hour
formDecoder := schema.NewDecoder()
formDecoder.IgnoreUnknownKeys(true)
app := &application{
sequence: 0,
logger: logger,
sessionManager: sessionManager,
websites: &models.WebsiteModel{DB: db},
guestbooks: &models.GuestbookModel{DB: db},
users: &models.UserModel{DB: db, Settings: make(map[string]models.Setting)},
guestbookComments: &models.GuestbookCommentModel{DB: db},
formDecoder: formDecoder,
debug: *debug,
timezones: getAvailableTimezones(),
}
err = app.users.InitializeSettingsMap()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
err = app.guestbooks.InitializeSettingsMap()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
tlsConfig := &tls.Config{
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
}
srv := &http.Server{
Addr: *addr,
Handler: app.routes(),
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
TLSConfig: tlsConfig,
IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
logger.Info("Starting server", slog.Any("addr", *addr))
if app.debug {
err = srv.ListenAndServeTLS("./tls/cert.pem", "./tls/key.pem")
} else {
err = srv.ListenAndServe()
}
logger.Error(err.Error())
os.Exit(1)
}
func openDB(dsn string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, err
}
if err = db.Ping(); err != nil {
return nil, err
}
return db, nil
}
func getAvailableTimezones() []string {
var zones []string
var zoneDirs = []string{
"/usr/share/zoneinfo/",
"/usr/share/lib/zoneinfo/",
"/usr/lib/locale/TZ/",
}
for _, zd := range zoneDirs {
zones = walkTzDir(zd, zones)
for idx, zone := range zones {
zones[idx] = strings.ReplaceAll(zone, zd+"/", "")
}
}
return zones
}
func walkTzDir(path string, zones []string) []string {
fileInfos, err := os.ReadDir(path)
if err != nil {
return zones
}
isAlpha := func(s string) bool {
for _, r := range s {
if !unicode.IsLetter(r) {
return false
}
}
return true
}
for _, info := range fileInfos {
if info.Name() != strings.ToUpper(info.Name()[:1])+info.Name()[1:] {
continue
}
if !isAlpha(info.Name()[:1]) {
continue
}
newPath := path + "/" + info.Name()
if info.IsDir() {
zones = walkTzDir(newPath, zones)
} else {
zones = append(zones, newPath)
}
}
return zones
}

94
cmd/web/middleware.go Normal file
View File

@ -0,0 +1,94 @@
package main
import (
"context"
"fmt"
"net/http"
"github.com/justinas/nosurf"
)
func (app *application) logRequest (next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
ip = r.RemoteAddr
proto = r.Proto
method = r.Method
uri = r.URL.RequestURI()
)
app.logger.Info("received request", "ip", ip, "proto", proto, "method", method, "uri", uri)
next.ServeHTTP(w, r)
})
}
func commonHeaders (next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com")
w.Header().Set("Referrer-Policy", "origin-when-cross-origin")
w.Header().Set("X-Content-Type-Options", "nosniff")
// w.Header().Set("X-Frame-Options", "deny")
w.Header().Set("X-XSS-Protection", "0")
next.ServeHTTP(w, r)
})
}
func (app *application) recoverPanic(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
w.Header().Set("Connection", "close")
app.serverError(w, r, fmt.Errorf("%s", err))
}
}()
next.ServeHTTP(w, r)
})
}
func (app *application) requireAuthentication(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !app.isAuthenticated(r) {
http.Redirect(w, r, "/users/login", http.StatusSeeOther)
return
}
w.Header().Add("Cache-Control", "no-store")
next.ServeHTTP(w, r)
})
}
func noSurf(next http.Handler) http.Handler {
csrfHandler := nosurf.New(next)
csrfHandler.SetBaseCookie(http.Cookie{
HttpOnly: true,
Path: "/",
Secure: true,
})
return csrfHandler
}
func (app *application) authenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := app.sessionManager.GetInt64(r.Context(), "authenticatedUserId")
if id == 0 {
next.ServeHTTP(w, r)
return
}
exists, err := app.users.Exists(id)
if err != nil {
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)
})
}

52
cmd/web/routes.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"net/http"
"git.32bit.cafe/32bitcafe/guestbook/ui"
"github.com/justinas/alice"
)
func (app *application) routes() http.Handler {
mux := http.NewServeMux()
mux.Handle("GET /static/", http.FileServerFS(ui.Files))
dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)
standard := alice.New(app.recoverPanic, app.logRequest, commonHeaders)
mux.Handle("/{$}", dynamic.ThenFunc(app.home))
mux.Handle("POST /websites/{id}/guestbook/comments/create", standard.ThenFunc(app.postGuestbookCommentCreate))
mux.Handle("GET /websites/{id}/guestbook", dynamic.ThenFunc(app.getGuestbook))
mux.Handle("GET /users/register", dynamic.ThenFunc(app.getUserRegister))
mux.Handle("POST /users/register", dynamic.ThenFunc(app.postUserRegister))
mux.Handle("GET /users/login", dynamic.ThenFunc(app.getUserLogin))
mux.Handle("POST /users/login", dynamic.ThenFunc(app.postUserLogin))
mux.Handle("GET /help", dynamic.ThenFunc(app.notImplemented))
protected := dynamic.Append(app.requireAuthentication)
// mux.Handle("GET /users", protected.ThenFunc(app.getUsersList))
mux.Handle("GET /users/{id}", protected.ThenFunc(app.getUser))
mux.Handle("POST /users/logout", protected.ThenFunc(app.postUserLogout))
mux.Handle("GET /users/settings", protected.ThenFunc(app.getUserSettings))
mux.Handle("PUT /users/settings", protected.ThenFunc(app.putUserSettings))
mux.Handle("GET /users/privacy", protected.ThenFunc(app.notImplemented))
mux.Handle("GET /guestbooks", protected.ThenFunc(app.getAllGuestbooks))
mux.Handle("GET /websites", protected.ThenFunc(app.getWebsiteList))
mux.Handle("GET /websites/create", protected.ThenFunc(app.getWebsiteCreate))
mux.Handle("POST /websites/create", protected.ThenFunc(app.postWebsiteCreate))
mux.Handle("GET /websites/{id}/dashboard", protected.ThenFunc(app.getWebsiteDashboard))
mux.Handle("GET /websites/{id}/dashboard/guestbook/comments", protected.ThenFunc(app.getGuestbookComments))
mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/queue", protected.ThenFunc(app.getCommentQueue))
mux.Handle("DELETE /websites/{id}/dashboard/guestbook/comments/{commentId}", protected.ThenFunc(app.deleteGuestbookComment))
mux.Handle("PUT /websites/{id}/dashboard/guestbook/comments/{commentId}", protected.ThenFunc(app.putHideGuestbookComment))
mux.Handle("GET /websites/{id}/dashboard/guestbook/settings", protected.ThenFunc(app.getGuestbookSettings))
mux.Handle("PUT /websites/{id}/dashboard/guestbook/settings", protected.ThenFunc(app.putGuestbookSettings))
mux.Handle("GET /websites/{id}/dashboard/guestbook/comments/trash", protected.ThenFunc(app.getCommentTrash))
mux.Handle("GET /websites/{id}/dashboard/guestbook/themes", protected.ThenFunc(app.getComingSoon))
mux.Handle("GET /websites/{id}/dashboard/guestbook/customize", protected.ThenFunc(app.getComingSoon))
mux.Handle("GET /websites/{id}/guestbook/comments/create", protected.ThenFunc(app.getGuestbookCommentCreate))
return standard.Then(mux)
}

View File

@ -0,0 +1,76 @@
CREATE TABLE setting_groups (
Id integer primary key autoincrement,
Description varchar(256) NOT NULL
);
CREATE TABLE setting_data_types (
Id integer primary key autoincrement,
Description varchar(64) NOT NULL
);
CREATE TABLE settings (
Id integer primary key autoincrement,
Description varchar(256) NOT NULL,
Constrained boolean NOT NULL,
DataType integer NOT NULL,
SettingGroup int NOT NULL,
MinValue varchar(6),
MaxValue varchar(6),
FOREIGN KEY (DataType) REFERENCES setting_data_types(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT,
FOREIGN KEY (SettingGroup) REFERENCES setting_groups(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
CREATE TABLE allowed_setting_values (
Id integer primary key autoincrement,
SettingId integer NOT NULL,
ItemValue varchar(256),
Caption varchar(256),
FOREIGN KEY (SettingId) REFERENCES settings(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
CREATE TABLE user_settings (
Id integer primary key autoincrement,
UserId integer NOT NULL,
SettingId integer NOT NULL,
AllowedSettingValueId integer,
UnconstrainedValue varchar(256),
FOREIGN KEY (UserId) REFERENCES users(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT,
FOREIGN KEY (SettingId) REFERENCES settings(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT,
FOREIGN KEY (AllowedSettingValueId) REFERENCES allowed_setting_values(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
CREATE TABLE guestbook_settings (
Id integer primary key autoincrement,
GuestbookId integer NOT NULL,
SettingId integer NOT NULL,
AllowedSettingValueId integer,
UnconstrainedValue varchar(256),
FOREIGN KEY (GuestbookId) REFERENCES guestbooks(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT,
FOREIGN KEY (SettingId) REFERENCES settings(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT,
FOREIGN KEY (AllowedSettingValueId) REFERENCES allowed_setting_values(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
INSERT INTO setting_groups (Description) VALUES ('guestbook');
INSERT INTO setting_groups (Description) VALUES ('user');
INSERT INTO setting_data_types (Description) VALUES ('alphanumeric');
INSERT INTO setting_data_types (Description) VALUES ('integer');
INSERT INTO setting_data_types (Description) VALUES ('datetime');

9
db/create-settings.sql Normal file
View File

@ -0,0 +1,9 @@
INSERT INTO setting_groups (Description) VALUES ("guestbook");
INSERT INTO setting_groups (Description) VALUES ("user");
INSERT INTO setting_data_types (Description) VALUES ("alphanumeric")
INSERT INTO setting_data_types (Description) VALUES ("integer")
INSERT INTO setting_data_types (Description) VALUES ("datetime")
INSERT INTO settings (Description, Constrained, DataType, SettingGroup)
VALUES ("Local Timezone", 0, 1, 1);

View File

@ -0,0 +1,69 @@
CREATE TABLE users (
Id integer primary key autoincrement,
ShortId integer UNIQUE NOT NULL,
Username varchar(32) NOT NULL,
Email varchar(256) UNIQUE NOT NULL,
Deleted datetime,
IsBanned boolean NOT NULL DEFAULT FALSE,
HashedPassword char(60) NOT NULL,
Created datetime NOT NULL
);
CREATE TABLE websites (
Id integer primary key autoincrement,
ShortId integer UNIQUE NOT NULL,
Name varchar(256) NOT NULL,
SiteUrl varchar(512) NOT NULL,
AuthorName varchar(512) NOT NULL,
UserId integer NOT NULL,
Created datetime NOT NULL,
Deleted datetime,
FOREIGN KEY (UserId) REFERENCES users(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
CREATE TABLE guestbooks (
Id integer primary key autoincrement,
ShortId integer UNIQUE NOT NULL,
WebsiteId integer UNIQUE NOT NULL,
UserId integer NOT NULL,
Created datetime NOT NULL,
Deleted datetime,
IsActive boolean NOT NULL DEFAULT TRUE,
FOREIGN KEY (UserId) REFERENCES users(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
FOREIGN KEY (WebsiteId) REFERENCES websites(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
CREATE TABLE guestbook_comments (
Id integer primary key autoincrement,
ShortId integer UNIQUE NOT NULL,
GuestbookId integer NOT NULL,
ParentId integer,
AuthorName varchar(256) NOT NULL,
AuthorEmail varchar(256) NOT NULL,
AuthorSite varchar(256),
CommentText text NOT NULL,
PageUrl varchar(256),
Created datetime NOT NULL,
IsPublished boolean NOT NULL DEFAULT TRUE,
Deleted datetime,
FOREIGN KEY (GuestbookId)
REFERENCES guestbooks(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT,
FOREIGN KEY (ParentId)
REFERENCES guestbook_comments(Id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
CREATE TABLE sessions (
token CHAR(43) primary key,
data BLOB NOT NULL,
expiry TEXT NOT NULL
);

14
go.mod Normal file
View File

@ -0,0 +1,14 @@
module git.32bit.cafe/32bitcafe/guestbook
go 1.23.1
require (
github.com/a-h/templ v0.3.833
github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c
github.com/alexedwards/scs/v2 v2.8.0
github.com/gorilla/schema v1.4.1
github.com/justinas/alice v1.2.0
github.com/justinas/nosurf v1.1.1
github.com/mattn/go-sqlite3 v1.14.24
golang.org/x/crypto v0.36.0
)

24
go.sum Normal file
View File

@ -0,0 +1,24 @@
github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885 h1:+DCxWg/ojncqS+TGAuRUoV7OfG/S4doh0pcpAwEcow0=
github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c h1:0gBCIsmH3+aaWK55APhhY7/Z+uv5IdbMqekI97V9shU=
github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=

43
internal/forms/forms.go Normal file
View File

@ -0,0 +1,43 @@
package forms
import "git.32bit.cafe/32bitcafe/guestbook/internal/validator"
type UserRegistrationForm struct {
Name string `schema:"username"`
Email string `schema:"email"`
Password string `schema:"password"`
validator.Validator `schema:"-"`
}
type UserLoginForm struct {
Email string `schema:"email"`
Password string `schema:"password"`
validator.Validator `schema:"-"`
}
type CommentCreateForm struct {
AuthorName string `schema:"authorname"`
AuthorEmail string `schema:"authoremail"`
AuthorSite string `schema:"authorsite"`
Content string `schema:"content,required"`
validator.Validator `schema:"-"`
}
type WebsiteCreateForm struct {
Name string `schema:"sitename"`
SiteUrl string `schema:"siteurl"`
AuthorName string `schema:"authorname"`
validator.Validator `schema:"-"`
}
type UserSettingsForm struct {
LocalTimezone string `schema:"timezones"`
validator.Validator `schema:"-"`
}
type GuestbookSettingsForm struct {
Visibility string `schema:"gb_visible"`
CommentingEnabled string `schema:"gb_commenting"`
WidgetsEnabled string `schema:"gb_remote"`
validator.Validator `schema:"-"`
}

13
internal/models/errors.go Normal file
View File

@ -0,0 +1,13 @@
package models
import "errors"
var (
ErrNoRecord = errors.New("models: no matching record found")
ErrInvalidCredentials = errors.New("models: invalid credentials")
ErrDuplicateEmail = errors.New("models: duplicate email")
ErrInvalidSettingValue = errors.New("models: invalid setting value")
)

View File

@ -0,0 +1,284 @@
package models
import (
"database/sql"
"errors"
"strconv"
"time"
)
type GuestbookSettings struct {
IsCommentingEnabled bool
ReenableCommenting time.Time
IsVisible bool
FilteredWords []string
AllowRemoteHostAccess bool
}
var ValidDisableDurations = []string{"true", "false", "1h", "4h", "8h", "24h", "72h", "168h"}
const (
SettingGbCommentingEnabled = "commenting_enabled"
SettingGbReenableComments = "reenable_comments"
SettingGbVisible = "is_visible"
SettingGbFilteredWords = "filtered_words"
SettingGbAllowRemote = "remote_enabled"
)
type Guestbook struct {
ID int64
ShortId uint64
UserId int64
WebsiteId int64
Created time.Time
Deleted time.Time
IsActive bool
Settings GuestbookSettings
}
func (g Guestbook) CanComment() bool {
now := time.Now().UTC()
return g.Settings.IsCommentingEnabled && g.Settings.ReenableCommenting.Before(now)
}
type GuestbookModel struct {
DB *sql.DB
Settings map[string]Setting
}
func (m *GuestbookModel) InitializeSettingsMap() error {
if m.Settings == nil {
m.Settings = make(map[string]Setting)
}
stmt := `SELECT settings.Id, settings.Description, Constrained, d.Id, d.Description, g.Id, g.Description, MinValue, MaxValue
FROM settings
LEFT JOIN setting_data_types d ON settings.DataType = d.Id
LEFT JOIN setting_groups g ON settings.SettingGroup = g.Id
WHERE SettingGroup = (SELECT Id FROM setting_groups WHERE Description = 'guestbook' LIMIT 1)`
result, err := m.DB.Query(stmt)
if err != nil {
return err
}
for result.Next() {
var s Setting
var mn sql.NullString
var mx sql.NullString
err := result.Scan(&s.id, &s.description, &s.constrained, &s.dataType.id, &s.dataType.description, &s.settingGroup.id, &s.settingGroup.description, &mn, &mx)
if mn.Valid {
s.minValue = mn.String
}
if mx.Valid {
s.maxValue = mx.String
}
if err != nil {
return err
}
m.Settings[s.description] = s
}
return nil
}
func (m *GuestbookModel) Insert(shortId uint64, userId int64, websiteId int64, settings GuestbookSettings) (int64, error) {
stmt := `INSERT INTO guestbooks (ShortId, UserId, WebsiteId, Created, IsActive)
VALUES(?, ?, ?, ?, TRUE)`
result, err := m.DB.Exec(stmt, shortId, userId, websiteId, time.Now().UTC())
if err != nil {
return -1, err
}
id, err := result.LastInsertId()
if err != nil {
return -1, err
}
err = m.initializeGuestbookSettings(id, settings)
if err != nil {
return id, err
}
return id, nil
}
func (m *GuestbookModel) Get(shortId uint64) (Guestbook, error) {
stmt := `SELECT Id, ShortId, UserId, WebsiteId, Created, Deleted, IsActive FROM guestbooks
WHERE ShortId = ?`
row := m.DB.QueryRow(stmt, shortId)
var g Guestbook
var t sql.NullTime
err := row.Scan(&g.ID, &g.ShortId, &g.UserId, &g.WebsiteId, &g.Created, &t, &g.IsActive)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return Guestbook{}, ErrNoRecord
}
return Guestbook{}, err
}
if t.Valid {
g.Deleted = t.Time
}
settings, err := m.GetSettings(g.ID)
if err != nil {
return g, err
}
g.Settings = settings
return g, nil
}
func (m *GuestbookModel) GetAll(userId int64) ([]Guestbook, error) {
stmt := `SELECT Id, ShortId, UserId, WebsiteId, Created, IsActive FROM guestbooks
WHERE UserId = ? AND DELETED IS NULL`
rows, err := m.DB.Query(stmt, userId)
if err != nil {
return nil, err
}
var guestbooks []Guestbook
for rows.Next() {
var g Guestbook
err = rows.Scan(&g.ID, &g.ShortId, &g.UserId, &g.WebsiteId, &g.Created, &g.IsActive)
if err != nil {
return nil, err
}
guestbooks = append(guestbooks, g)
}
if err = rows.Err(); err != nil {
return nil, err
}
return guestbooks, nil
}
func (m *GuestbookModel) GetSettings(guestbookId int64) (GuestbookSettings, error) {
stmt := `SELECT g.SettingId, a.ItemValue, g.UnconstrainedValue FROM guestbook_settings AS g
LEFT JOIN allowed_setting_values AS a ON g.AllowedSettingValueId = a.Id
WHERE GuestbookId = ?`
var settings GuestbookSettings
rows, err := m.DB.Query(stmt, guestbookId)
if err != nil {
return settings, err
}
for rows.Next() {
var id int
var itemValue sql.NullString
var unconstrainedValue sql.NullString
err = rows.Scan(&id, &itemValue, &unconstrainedValue)
if err != nil {
return settings, err
}
switch id {
case m.Settings[SettingGbCommentingEnabled].id:
settings.IsCommentingEnabled, err = strconv.ParseBool(itemValue.String)
if err != nil {
return settings, err
}
break
case m.Settings[SettingGbReenableComments].id:
settings.ReenableCommenting, err = time.Parse(time.RFC3339, unconstrainedValue.String)
if err != nil {
return settings, err
}
break
case m.Settings[SettingGbVisible].id:
settings.IsVisible, err = strconv.ParseBool(itemValue.String)
if err != nil {
return settings, err
}
break
case m.Settings[SettingGbAllowRemote].id:
settings.AllowRemoteHostAccess, err = strconv.ParseBool(itemValue.String)
if err != nil {
return settings, err
}
break
}
}
// if comment disable setting has expired, enable commenting
if time.Now().UTC().After(settings.ReenableCommenting) {
settings.IsCommentingEnabled = true
m.UpdateSetting(guestbookId, m.Settings[SettingGbCommentingEnabled], "true")
}
return settings, nil
}
func (m *GuestbookModel) initializeGuestbookSettings(guestbookId int64, settings GuestbookSettings) error {
stmt := `INSERT INTO guestbook_settings (GuestbookId, SettingId, AllowedSettingValueId, UnconstrainedValue) VALUES
(?, ?, ?, ?),
(?, ?, ?, ?),
(?, ?, ?, ?),
(?, ?, ?, ?),
(?, ?, ?, ?)`
_, err := m.DB.Exec(stmt,
guestbookId, m.Settings[SettingGbCommentingEnabled].id, settings.IsCommentingEnabled, nil,
guestbookId, m.Settings[SettingGbReenableComments].id, nil, settings.ReenableCommenting.String(),
guestbookId, m.Settings[SettingGbVisible].id, settings.IsVisible, nil,
guestbookId, m.Settings[SettingGbAllowRemote].id, settings.AllowRemoteHostAccess, nil)
if err != nil {
return err
}
return nil
}
func (m *GuestbookModel) UpdateGuestbookSettings(guestbookId int64, settings GuestbookSettings) error {
err := m.UpdateSetting(guestbookId, m.Settings[SettingGbVisible], strconv.FormatBool(settings.IsVisible))
if err != nil {
return err
}
err = m.UpdateSetting(guestbookId, m.Settings[SettingGbAllowRemote], strconv.FormatBool(settings.AllowRemoteHostAccess))
if err != nil {
return err
}
err = m.UpdateSetting(guestbookId, m.Settings[SettingGbCommentingEnabled], strconv.FormatBool(settings.IsCommentingEnabled))
if err != nil {
return err
}
err = m.UpdateSetting(guestbookId, m.Settings[SettingGbReenableComments], settings.ReenableCommenting.Format(time.RFC3339))
if err != nil {
return err
}
return nil
}
func (m *GuestbookModel) InsertSetting(guestbookId int64, setting Setting, value string) error {
stmt := `
INSERT INTO guestbook_settings (GuestbookId, SettingId, AllowedSettingValueId, UnconstrainedValue)
SELECT ?,
settings.Id,
(SELECT Id FROM allowed_setting_values WHERE SettingId = settings.id AND ItemValue = ?),
CASE WHEN NOT EXISTS (SELECT 1 FROM settings AS s where s.Id = settings.Id AND s.Constrained = 1) THEN ? ELSE NULL END
FROM settings
WHERE settings.id = ?
`
result, err := m.DB.Exec(stmt, guestbookId, value, value, setting.id)
if err != nil {
return err
}
_, err = result.LastInsertId()
if err != nil {
return err
}
return nil
}
func (m *GuestbookModel) UpdateSetting(guestbookId int64, setting Setting, value string) error {
stmt := `UPDATE guestbook_settings SET
AllowedSettingValueId=IFNULL(
(SELECT Id FROM allowed_setting_values WHERE SettingId = guestbook_settings.SettingId AND ItemValue = ?), AllowedSettingValueId
),
UnconstrainedValue=(SELECT ? FROM settings WHERE settings.Id = guestbook_settings.SettingId AND settings.Constrained=0)
WHERE GuestbookId = ?
AND SettingId = (SELECT Id from Settings WHERE Description=?);`
result, err := m.DB.Exec(stmt, value, value, guestbookId, setting.description)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows != 1 {
return ErrInvalidSettingValue
}
return nil
}
func (m *GuestbookModel) AddFilteredWord(guestbookId int64, word string) error {
return nil
}
func (m *GuestbookModel) RemoveFilteredWord(guestbookId int64, word string) error {
return nil
}

View File

@ -0,0 +1,160 @@
package models
import (
"database/sql"
"time"
)
type GuestbookComment struct {
ID int64
ShortId uint64
GuestbookId int64
ParentId int64
AuthorName string
AuthorEmail string
AuthorSite string
CommentText string
PageUrl string
Created time.Time
Deleted time.Time
IsPublished bool
}
type GuestbookCommentModel struct {
DB *sql.DB
}
func (m *GuestbookCommentModel) Insert(shortId uint64, guestbookId, parentId int64, authorName,
authorEmail, authorSite, commentText, pageUrl string, isPublished bool) (int64, error) {
stmt := `INSERT INTO guestbook_comments (ShortId, GuestbookId, ParentId, AuthorName,
AuthorEmail, AuthorSite, CommentText, PageUrl, Created, IsPublished)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
result, err := m.DB.Exec(stmt, shortId, guestbookId, parentId, authorName, authorEmail,
authorSite, commentText, pageUrl, time.Now().UTC(), isPublished)
if err != nil {
return -1, err
}
id, err := result.LastInsertId()
if err != nil {
return -1, err
}
return id, nil
}
func (m *GuestbookCommentModel) Get(shortId uint64) (GuestbookComment, error) {
stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, Created, IsPublished, Deleted FROM guestbook_comments WHERE ShortId = ?`
row := m.DB.QueryRow(stmt, shortId)
var c GuestbookComment
var t sql.NullTime
err := row.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite,
&c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &t)
if err != nil {
return GuestbookComment{}, err
}
if t.Valid {
c.Deleted = t.Time
}
return c, nil
}
func (m *GuestbookCommentModel) GetAll(guestbookId int64) ([]GuestbookComment, error) {
stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, Created, IsPublished
FROM guestbook_comments
WHERE GuestbookId = ? AND IsPublished = TRUE AND DELETED IS NULL
ORDER BY Created DESC`
rows, err := m.DB.Query(stmt, guestbookId)
if err != nil {
return nil, err
}
var comments []GuestbookComment
for rows.Next() {
var c GuestbookComment
err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite,
&c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished)
if err != nil {
return nil, err
}
comments = append(comments, c)
}
if err = rows.Err(); err != nil {
return nil, err
}
return comments, nil
}
func (m *GuestbookCommentModel) GetDeleted(guestbookId int64) ([]GuestbookComment, error) {
stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, Created, IsPublished, Deleted
FROM guestbook_comments
WHERE GuestbookId = ? AND Deleted IS NOT NULL
ORDER BY Created DESC`
rows, err := m.DB.Query(stmt, guestbookId)
if err != nil {
return nil, err
}
var comments []GuestbookComment
for rows.Next() {
var c GuestbookComment
var t sql.NullTime
err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite,
&c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished, &t)
if err != nil {
return nil, err
}
if t.Valid {
c.Deleted = t.Time
}
comments = append(comments, c)
}
if err = rows.Err(); err != nil {
return nil, err
}
return comments, nil
}
func (m *GuestbookCommentModel) GetUnpublished(guestbookId int64) ([]GuestbookComment, error) {
stmt := `SELECT Id, ShortId, GuestbookId, ParentId, AuthorName, AuthorEmail, AuthorSite,
CommentText, PageUrl, Created, IsPublished
FROM guestbook_comments
WHERE GuestbookId = ? AND Deleted IS NULL AND IsPublished = FALSE
ORDER BY Created DESC`
rows, err := m.DB.Query(stmt, guestbookId)
if err != nil {
return nil, err
}
var comments []GuestbookComment
for rows.Next() {
var c GuestbookComment
err = rows.Scan(&c.ID, &c.ShortId, &c.GuestbookId, &c.ParentId, &c.AuthorName, &c.AuthorEmail, &c.AuthorSite,
&c.CommentText, &c.PageUrl, &c.Created, &c.IsPublished)
if err != nil {
return nil, err
}
comments = append(comments, c)
}
if err = rows.Err(); err != nil {
return nil, err
}
return comments, nil
}
func (m *GuestbookCommentModel) UpdateComment(comment *GuestbookComment) error {
stmt := `UPDATE guestbook_comments
SET CommentText = ?,
PageUrl = ?,
IsPublished = ?,
Deleted = ?
WHERE Id = ?`
var err error
if comment.Deleted.IsZero() {
_, err = m.DB.Exec(stmt, comment.CommentText, comment.PageUrl, comment.IsPublished, nil, comment.ID)
} else {
_, err = m.DB.Exec(stmt, comment.CommentText, comment.PageUrl, comment.IsPublished, comment.Deleted, comment.ID)
}
if err != nil {
return err
}
return nil
}

154
internal/models/settings.go Normal file
View File

@ -0,0 +1,154 @@
package models
import (
"strconv"
"time"
)
type SettingGroup struct {
id int
description string
}
func (g *SettingGroup) Id() int {
return g.id
}
func (g *SettingGroup) Description() string {
return g.description
}
const (
SETTING_GROUP_USER = "user"
SETTING_GROUP_GUESTBOOK = "guestbook"
)
type SettingDataType struct {
id int
description string
}
func (d *SettingDataType) Id() int {
return d.id
}
func (d *SettingDataType) Description() string {
return d.description
}
const (
SETTING_TYPE_INTEGER = "integer"
SETTING_TYPE_STRING = "alphanumeric"
SETTING_TYPE_DATE = "datetime"
)
type Setting struct {
id int
description string
constrained bool
dataType SettingDataType
settingGroup SettingGroup
minValue string
maxValue string
}
func (s *Setting) Id() int {
return s.id
}
func (s *Setting) Description() string {
return s.description
}
func (s *Setting) Constrained() bool {
return s.constrained
}
func (s *Setting) DataType() SettingDataType {
return s.dataType
}
func (s *Setting) SettingGroup() SettingGroup {
return s.settingGroup
}
func (s *Setting) MinValue() string {
return s.minValue
}
func (s *Setting) MaxValue() string {
return s.maxValue
}
func (s *Setting) Validate(value string) bool {
switch s.dataType.description {
case SETTING_TYPE_INTEGER:
return s.validateInt(value)
case SETTING_TYPE_STRING:
return s.validateAlphanum(value)
case SETTING_TYPE_DATE:
return s.validateDatetime(value)
}
return false
}
func (s *Setting) validateInt(value string) bool {
v, err := strconv.ParseInt(value, 10, 0)
if err != nil {
return false
}
var min int64
var max int64
if len(s.minValue) > 0 {
min, err = strconv.ParseInt(s.minValue, 10, 0)
if err != nil {
return false
}
if v < min {
return false
}
}
if len(s.maxValue) > 0 {
max, err = strconv.ParseInt(s.maxValue, 10, 0)
if err != nil {
return false
}
if v < max {
return false
}
}
return true
}
func (s *Setting) validateDatetime(value string) bool {
v, err := time.Parse(time.RFC3339, value)
if err != nil {
return false
}
var min time.Time
var max time.Time
if len(s.minValue) > 0 {
min, err = time.Parse(time.RFC3339, s.minValue)
if err != nil {
return false
}
if v.Before(min) {
return false
}
}
if len(s.maxValue) > 0 {
max, err = time.Parse(time.RFC3339, s.maxValue)
if err != nil {
return false
}
if v.After(max) {
return false
}
}
return false
}
func (s *Setting) validateAlphanum(value string) bool {
return len(value) >= 0
}

259
internal/models/user.go Normal file
View File

@ -0,0 +1,259 @@
package models
import (
"database/sql"
"errors"
"strings"
"time"
"github.com/mattn/go-sqlite3"
"golang.org/x/crypto/bcrypt"
)
type UserSettings struct {
LocalTimezone *time.Location
}
const (
SettingUserTimezone = "local_timezone"
)
type User struct {
ID int64
ShortId uint64
Username string
Email string
Deleted bool
IsBanned bool
HashedPassword []byte
Created time.Time
Settings UserSettings
}
type UserModel struct {
DB *sql.DB
Settings map[string]Setting
}
func (m *UserModel) InitializeSettingsMap() error {
if m.Settings == nil {
m.Settings = make(map[string]Setting)
}
stmt := `SELECT settings.Id, settings.Description, Constrained, d.Id, d.Description, g.Id, g.Description, MinValue, MaxValue
FROM settings
LEFT JOIN setting_data_types d ON settings.DataType = d.Id
LEFT JOIN setting_groups g ON settings.SettingGroup = g.Id
WHERE SettingGroup = (SELECT Id FROM setting_groups WHERE Description = 'user' LIMIT 1)`
result, err := m.DB.Query(stmt)
if err != nil {
return err
}
for result.Next() {
var s Setting
var mn sql.NullString
var mx sql.NullString
err := result.Scan(&s.id, &s.description, &s.constrained, &s.dataType.id, &s.dataType.description, &s.settingGroup.id, &s.settingGroup.description, &mn, &mx)
if mn.Valid {
s.minValue = mn.String
}
if mx.Valid {
s.maxValue = mx.String
}
if err != nil {
return err
}
m.Settings[s.description] = s
}
return nil
}
func (m *UserModel) Insert(shortId uint64, username string, email string, password string, settings UserSettings) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return err
}
stmt := `INSERT INTO users (ShortId, Username, Email, IsBanned, HashedPassword, Created)
VALUES (?, ?, ?, FALSE, ?, ?)`
result, err := m.DB.Exec(stmt, shortId, username, email, hashedPassword, time.Now().UTC())
if err != nil {
if sqliteError, ok := err.(sqlite3.Error); ok {
if sqliteError.ExtendedCode == 2067 && strings.Contains(sqliteError.Error(), "Email") {
return ErrDuplicateEmail
}
}
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
err = m.initializeUserSettings(id, settings)
if err != nil {
return err
}
return nil
}
func (m *UserModel) Get(id uint64) (User, error) {
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE ShortId = ? AND Deleted IS NULL`
row := m.DB.QueryRow(stmt, id)
var u User
err := row.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return User{}, ErrNoRecord
}
return User{}, err
}
settings, err := m.GetSettings(u.ID)
if err != nil {
return u, err
}
u.Settings = settings
return u, nil
}
func (m *UserModel) GetById(id int64) (User, error) {
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE Id = ? AND Deleted IS NULL`
row := m.DB.QueryRow(stmt, id)
var u User
err := row.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return User{}, ErrNoRecord
}
return User{}, err
}
settings, err := m.GetSettings(u.ID)
if err != nil {
return u, err
}
u.Settings = settings
return u, nil
}
func (m *UserModel) GetAll() ([]User, error) {
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE DELETED IS NULL`
rows, err := m.DB.Query(stmt)
if err != nil {
return nil, err
}
var users []User
for rows.Next() {
var u User
err = rows.Scan(&u.ID, &u.ShortId, &u.Username, &u.Email, &u.Created)
if err != nil {
return nil, err
}
users = append(users, u)
}
if err = rows.Err(); err != nil {
return nil, err
}
return users, nil
}
func (m *UserModel) Authenticate(email, password string) (int64, error) {
var id int64
var hashedPassword []byte
stmt := `SELECT Id, HashedPassword FROM users WHERE Email = ?`
err := m.DB.QueryRow(stmt, email).Scan(&id, &hashedPassword)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, ErrInvalidCredentials
} else {
return 0, err
}
}
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return 0, ErrInvalidCredentials
} else {
return 0, err
}
}
return id, nil
}
func (m *UserModel) Exists(id int64) (bool, error) {
var exists bool
stmt := `SELECT EXISTS(SELECT true FROM users WHERE Id = ? AND DELETED IS NULL)`
err := m.DB.QueryRow(stmt, id).Scan(&exists)
return exists, err
}
func (m *UserModel) GetSettings(userId int64) (UserSettings, error) {
stmt := `SELECT u.SettingId, a.ItemValue, u.UnconstrainedValue FROM user_settings AS u
LEFT JOIN allowed_setting_values AS a ON u.AllowedSettingValueId = a.Id
WHERE UserId = ?`
var settings UserSettings
rows, err := m.DB.Query(stmt, userId)
if err != nil {
return settings, err
}
for rows.Next() {
var id int
var itemValue sql.NullString
var unconstrainedValue sql.NullString
err = rows.Scan(&id, &itemValue, &unconstrainedValue)
if err != nil {
return settings, err
}
switch id {
case m.Settings[SettingUserTimezone].id:
settings.LocalTimezone, err = time.LoadLocation(unconstrainedValue.String)
if err != nil {
panic(err)
}
}
}
return settings, err
}
func (m *UserModel) initializeUserSettings(userId int64, settings UserSettings) error {
stmt := `INSERT INTO user_settings (UserId, SettingId, AllowedSettingValueId, UnconstrainedValue)
VALUES (?, ?, ?, ?)`
_, err := m.DB.Exec(stmt, userId, m.Settings[SettingUserTimezone].id, nil, settings.LocalTimezone.String())
if err != nil {
return err
}
return nil
}
func (m *UserModel) UpdateUserSettings(userId int64, settings UserSettings) error {
err := m.UpdateSetting(userId, m.Settings[SettingUserTimezone], settings.LocalTimezone.String())
if err != nil {
return err
}
return nil
}
func (m *UserModel) UpdateSetting(userId int64, setting Setting, value string) error {
valid := setting.Validate(value)
if !valid {
return ErrInvalidSettingValue
}
stmt := `UPDATE user_settings SET
AllowedSettingValueId=IFNULL(
(SELECT Id FROM allowed_setting_values WHERE SettingId = user_settings.SettingId AND ItemValue = ?), AllowedSettingValueId
),
UnconstrainedValue=(SELECT ? FROM settings WHERE settings.Id = user_settings.SettingId AND settings.Constrained=0)
WHERE userId = ?
AND SettingId = (SELECT Id from Settings WHERE Description=?);`
result, err := m.DB.Exec(stmt, value, value, userId, setting.description)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows != 1 {
return ErrInvalidSettingValue
}
return nil
}

115
internal/models/website.go Normal file
View File

@ -0,0 +1,115 @@
package models
import (
"database/sql"
"time"
)
type Website struct {
ID int64
ShortId uint64
Name string
SiteUrl string
AuthorName string
UserId int64
Created time.Time
Deleted time.Time
Guestbook Guestbook
}
type WebsiteModel struct {
DB *sql.DB
}
func (m *WebsiteModel) Insert(shortId uint64, userId int64, siteName, siteUrl, authorName string) (int64, error) {
stmt := `INSERT INTO websites (ShortId, Name, SiteUrl, AuthorName, UserId, Created)
VALUES (?, ?, ?, ?, ?, ?)`
result, err := m.DB.Exec(stmt, shortId, siteName, siteUrl, authorName, userId, time.Now().UTC())
if err != nil {
return -1, err
}
id, err := result.LastInsertId()
if err != nil {
return -1, err
}
return id, nil
}
func (m *WebsiteModel) Get(shortId uint64) (Website, error) {
stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created,
g.Id, g.ShortId
FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId
WHERE w.ShortId = ? AND w.DELETED IS NULL`
row := m.DB.QueryRow(stmt, shortId)
var w Website
err := row.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created,
&w.Guestbook.ID, &w.Guestbook.ShortId)
if err != nil {
return Website{}, err
}
return w, nil
}
func (m *WebsiteModel) GetById(id int64) (Website, error) {
stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created,
g.Id, g.ShortId, g.Created, g.IsActive
FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId
WHERE w.Id = ? AND w.DELETED IS NULL`
row := m.DB.QueryRow(stmt, id)
var w Website
err := row.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created,
&w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsActive)
if err != nil {
return Website{}, err
}
return w, nil
}
func (m *WebsiteModel) GetAllUser(userId int64) ([]Website, error) {
stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created,
g.Id, g.ShortId, g.Created, g.IsActive
FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId
WHERE w.UserId = ?`
rows, err := m.DB.Query(stmt, userId)
if err != nil {
return nil, err
}
var websites []Website
for rows.Next() {
var w Website
err := rows.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created,
&w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsActive)
if err != nil {
return nil, err
}
websites = append(websites, w)
}
if err = rows.Err(); err != nil {
return nil, err
}
return websites, nil
}
func (m *WebsiteModel) GetAll() ([]Website, error) {
stmt := `SELECT w.Id, w.ShortId, w.Name, w.SiteUrl, w.AuthorName, w.UserId, w.Created,
g.Id, g.ShortId, g.Created, g.IsActive
FROM websites AS w INNER JOIN guestbooks AS g ON w.Id = g.WebsiteId`
rows, err := m.DB.Query(stmt)
if err != nil {
return nil, err
}
var websites []Website
for rows.Next() {
var w Website
err := rows.Scan(&w.ID, &w.ShortId, &w.Name, &w.SiteUrl, &w.AuthorName, &w.UserId, &w.Created,
&w.Guestbook.ID, &w.Guestbook.ShortId, &w.Guestbook.Created, &w.Guestbook.IsActive)
if err != nil {
return nil, err
}
websites = append(websites, w)
}
if err = rows.Err(); err != nil {
return nil, err
}
return websites, nil
}

View File

@ -0,0 +1,58 @@
package validator
import (
"regexp"
"slices"
"strings"
"unicode/utf8"
)
var EmailRX = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
type Validator struct {
NonFieldErrors []string
FieldErrors map[string]string
}
func (v *Validator) Valid() bool {
return len(v.FieldErrors) == 0 && len(v.NonFieldErrors) == 0
}
func (v *Validator) AddFieldError(key, message string) {
if v.FieldErrors == nil {
v.FieldErrors = make(map[string]string)
}
if _, exists := v.FieldErrors[key]; !exists {
v.FieldErrors[key] = message
}
}
func (v *Validator) AddNonFieldError(message string) {
v.NonFieldErrors = append(v.NonFieldErrors, message)
}
func (v *Validator) CheckField(ok bool, key, message string) {
if !ok {
v.AddFieldError(key, message)
}
}
func NotBlank(value string) bool {
return strings.TrimSpace(value) != ""
}
func MaxChars(value string, n int) bool {
return utf8.RuneCountInString(value) <= n
}
func PermittedValue[T comparable](value T, permittedValues ...T) bool {
return slices.Contains(permittedValues, value)
}
func MinChars(value string, n int) bool {
return utf8.RuneCountInString(value) >= n
}
func Matches(value string, rx *regexp.Regexp) bool {
return rx.MatchString(value)
}

6
ui/efs.go Normal file
View File

@ -0,0 +1,6 @@
package ui
import "embed"
//go:embed "static"
var Files embed.FS

1
ui/static/css/classless.min.css vendored Normal file

File diff suppressed because one or more lines are too long

79
ui/static/css/style.css Normal file
View File

@ -0,0 +1,79 @@
/* html {
background: lightgray;
} */
body {
max-width: 1024px;
margin: 1rem auto;
padding: 1rem;
/* background: white; */
font-size: 1.2rem;
line-height: 1.5;
font-family: Arial, Helvetica, sans-serif;
}
header {
text-align: center;
}
body > nav {
display: flex;
justify-content: space-between;
}
body > nav ul {
list-style: none;
margin: 0 1rem;
padding: 0;
}
body > nav li {
display: inline-block;
padding: 0 0.5rem;
}
nav form {
display: inline-block;
}
nav button {
border: none;
background: none;
font-family: unset;
font-size: unset;
/* color: blue; */
/* text-decoration: underline; */
cursor: pointer;
}
main {
padding: 1rem;
}
div#dashboard {
display: flex;
flex-flow: row wrap;
}
div#dashboard nav {
flex: 1 1 25%;
margin-top: 2rem;
}
div#dashboard > div {
flex: 10 1 40%;
}
main nav ul {
list-style: none;
margin: 1rem;
padding: 0;
}
footer {
text-align: center;
}
a {
/* color: blue; */
}

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

File diff suppressed because one or more lines are too long

92
ui/views/common.templ Normal file
View File

@ -0,0 +1,92 @@
package views
import "git.32bit.cafe/32bitcafe/guestbook/internal/models"
import "strconv"
import "fmt"
import "strings"
type CommonData struct {
CurrentYear int
Flash string
IsAuthenticated bool
CSRFToken string
CurrentUser *models.User
IsHtmx bool
}
func shortIdToSlug(shortId uint64) string {
return strconv.FormatUint(shortId, 36)
}
func slugToShortId(slug string) uint64 {
id, _ := strconv.ParseUint(slug, 36, 64)
return id
}
func externalUrl(url string) string {
if !strings.HasPrefix(url, "http") {
return "http://" + url
}
return url
}
templ commonHeader() {
<header>
<h1><a href="/">webweav.ing</a></h1>
</header>
}
templ topNav(data CommonData) {
{{ hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) }}
<nav>
<div>
if data.IsAuthenticated {
Welcome, { data.CurrentUser.Username }
}
</div>
<div>
if data.IsAuthenticated {
<a href="/guestbooks">All Guestbooks</a> |
<a href="/websites">My Websites</a> |
<a href="/users/settings">Settings</a> |
<a href="#" hx-post="/users/logout" hx-headers={ hxHeaders }>Logout</a>
} else {
<a href="/users/register">Create an Account</a> |
<a href="/users/login">Login</a>
}
</div>
</nav>
}
templ commonFooter() {
<footer>
<p>A <a href="https://32bit.cafe">32bit.cafe</a> Project</p>
</footer>
}
templ base(title string, data CommonData) {
<!DOCTYPE html>
<html lang="en">
<head>
<title>{ title } - webweav.ing</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/css/classless.min.css" rel="stylesheet">
<link href="/static/css/style.css" rel="stylesheet">
<script src="/static/js/htmx.min.js"></script>
</head>
<body>
@commonHeader()
@topNav(data)
<main>
if data.Flash != "" {
<div class="flash">{ data.Flash }</div>
}
{ children... }
</main>
@commonFooter()
</body>
</html>
}

265
ui/views/common_templ.go Normal file
View File

@ -0,0 +1,265 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "git.32bit.cafe/32bitcafe/guestbook/internal/models"
import "strconv"
import "fmt"
import "strings"
type CommonData struct {
CurrentYear int
Flash string
IsAuthenticated bool
CSRFToken string
CurrentUser *models.User
IsHtmx bool
}
func shortIdToSlug(shortId uint64) string {
return strconv.FormatUint(shortId, 36)
}
func slugToShortId(slug string) uint64 {
id, _ := strconv.ParseUint(slug, 36, 64)
return id
}
func externalUrl(url string) string {
if !strings.HasPrefix(url, "http") {
return "http://" + url
}
return url
}
func commonHeader() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<header><h1><a href=\"/\">webweav.ing</a></h1></header>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func topNav(data CommonData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<nav><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.IsAuthenticated {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Welcome, ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.CurrentUser.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 44, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.IsAuthenticated {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<a href=\"/guestbooks\">All Guestbooks</a> | <a href=\"/websites\">My Websites</a> | <a href=\"/users/settings\">Settings</a> | <a href=\"#\" hx-post=\"/users/logout\" hx-headers=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(hxHeaders)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 52, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">Logout</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"/users/register\">Create an Account</a> | <a href=\"/users/login\">Login</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func commonFooter() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<footer><p>A <a href=\"https://32bit.cafe\">32bit.cafe</a> Project</p></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func base(title string, data CommonData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<!doctype html><html lang=\"en\"><head><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 72, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " - webweav.ing</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/classless.min.css\" rel=\"stylesheet\"><link href=\"/static/css/style.css\" rel=\"stylesheet\"><script src=\"/static/js/htmx.min.js\"></script></head><body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = commonHeader().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = topNav(data).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Flash != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"flash\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.Flash)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 84, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templ_7745c5c3_Var6.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = commonFooter().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

231
ui/views/guestbooks.templ Normal file
View File

@ -0,0 +1,231 @@
package views
import "fmt"
import "git.32bit.cafe/32bitcafe/guestbook/internal/models"
import "git.32bit.cafe/32bitcafe/guestbook/internal/forms"
import "time"
templ GuestbookDashboardCommentsView(title string, data CommonData, website models.Website, guestbook models.Guestbook, comments []models.GuestbookComment) {
@base(title, data) {
<div id="dashboard">
@wSidebar(website)
<div>
<h1>Comments on { website.SiteUrl }</h1>
<hr/>
if len(comments) == 0 {
<p>No comments yet!</p>
}
for _, c := range comments {
@GuestbookDashboardCommentView(data, website, c)
}
</div>
</div>
}
}
templ GuestbookDashboardCommentView(data CommonData, w models.Website, c models.GuestbookComment) {
{{ commentUrl := fmt.Sprintf("%s/dashboard/guestbook/comments/%s", wUrl(w), shortIdToSlug(c.ShortId)) }}
{{ hxHeaders := fmt.Sprintf("{\"X-CSRF-Token\": \"%s\"}", data.CSRFToken) }}
<div class="comment">
<div>
if c.Deleted.IsZero() {
<button class="danger" hx-delete={ commentUrl } hx-target="closest div.comment" hx-headers={ hxHeaders }>Delete</button>
<button class="outline" hx-put={ commentUrl } hx-target="closest div.comment" hx-headers={ hxHeaders }>
if !c.IsPublished {
Publish
} else {
Hide
}
</button>
}
</div>
<div>
<strong>{ c.AuthorName }</strong>
if len(c.AuthorEmail) > 0 {
{{ email := "mailto:" + c.AuthorEmail }}
| <a href={ templ.URL(email) } target="_blank">{ c.AuthorEmail }</a>
}
if len(c.AuthorSite) > 0 {
| <a href={ templ.URL(externalUrl(c.AuthorSite)) } target="_blank">{ c.AuthorSite }</a>
}
<p>
{ c.Created.In(data.CurrentUser.Settings.LocalTimezone).Format("01-02-2006 03:04PM") }
</p>
</div>
<p>
{ c.CommentText }
</p>
<hr/>
</div>
}
templ commentForm(form forms.CommentCreateForm) {
<div>
<label for="authorname">Name: </label>
{{ error, exists := form.FieldErrors["authorName"] }}
if exists {
<label class="error">{ error }</label>
}
<input type="text" name="authorname" id="authorname"/>
</div>
<div>
<label for="authoremail">Email: </label>
{{ error, exists = form.FieldErrors["authorEmail"] }}
if exists {
<label class="error">{ error }</label>
}
<input type="text" name="authoremail" id="authoremail"/>
</div>
<div>
<label for="authorsite">Site Url: </label>
{{ error, exists = form.FieldErrors["authorSite"] }}
if exists {
<label class="error">{ error }</label>
}
<input type="text" name="authorsite" id="authorsite"/>
</div>
<div>
<label for="content">Comment: </label>
{{ error, exists = form.FieldErrors["content"] }}
if exists {
<label class="error">{ error }</label>
}
<textarea name="content" id="content"></textarea>
</div>
<div>
<input type="submit" value="Submit"/>
</div>
}
templ GuestbookView(title string, data CommonData, website models.Website, guestbook models.Guestbook, comments []models.GuestbookComment, form forms.CommentCreateForm) {
{{ postUrl := fmt.Sprintf("/websites/%s/guestbook/comments/create", shortIdToSlug(website.ShortId)) }}
if data.IsHtmx {
@commentForm(form)
} else {
<html>
<head>
<title>{ title }</title>
<link href="/static/css/classless.min.css" rel="stylesheet"/>
</head>
<body>
<main>
<div>
<h1>Guestbook for { website.Name }</h1>
<form action={ templ.URL(postUrl) } method="post">
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
@commentForm(form)
</form>
</div>
<div id="comments">
if len(comments) == 0 {
<p>No comments yet!</p>
}
for _, c := range comments {
<div>
<h3>{ c.AuthorName }</h3>
{ c.Created.Format("01-02-2006 03:04PM") }
<p>
{ c.CommentText }
</p>
</div>
}
</div>
</main>
</body>
</html>
}
}
templ CreateGuestbookComment(title string, data CommonData, website models.Website, guestbook models.Guestbook, form forms.CommentCreateForm) {
{{ postUrl := fmt.Sprintf("/websites/%s/guestbook/comments/create", shortIdToSlug(website.ShortId)) }}
if data.IsHtmx {
<form hx-post={ postUrl } hx-target="closest div">
@commentForm(form)
</form>
} else {
@base(title, data) {
<form action={ templ.URL(postUrl) } method="post">
@commentForm(form)
</form>
}
}
}
templ settingRadio(selected bool, name, id, value string) {
<input type="radio" name={ name } id={ id } value={ value } selected?={ selected }/>
}
templ GuestbookSettingsView(data CommonData, website models.Website) {
{{ putUrl := fmt.Sprintf("/websites/%s/dashboard/guestbook/settings", shortIdToSlug(website.ShortId)) }}
{{ gb := website.Guestbook }}
@base("Guestbook Settings", data) {
<div id="dashboard">
@wSidebar(website)
<div>
<h1>Guestbook Settings</h1>
<form hx-put={ putUrl }>
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
<div>
<label>Guestbook Visibility</label>
<label for="gb_visible_true">
<input type="radio" name="gb_visible" id="gb_visible_true" value="true" checked?={ gb.Settings.IsVisible }/>
Public
</label>
<label for="gb_visible_false">
<input type="radio" name="gb_visible" id="gb_visible_false" value="false" checked?={ !gb.Settings.IsVisible }/>
Private
</label>
</div>
<div>
<label>Guestbook Commenting</label>
<select name="gb_commenting" id="gb-commenting">
<option value="true" selected?={ gb.Settings.IsCommentingEnabled }>Enabled</option>
<option value="1h">Disabled for 1 Hour</option>
<option value="4h">Disabled for 4 Hours</option>
<option value="8h">Disabled for 8 Hours</option>
<option value="24h">Disabled for 1 Day</option>
<option value="72h">Disabled for 3 Days</option>
<option value="168h">Disabled for 7 Days</option>
<option value="false" selected?={ !gb.Settings.IsCommentingEnabled }>Disabled</option>
</select>
if !website.Guestbook.CanComment() {
{{ localtime := gb.Settings.ReenableCommenting.In(data.CurrentUser.Settings.LocalTimezone) }}
<label>Commenting re-enabled on <time value={ localtime.Format(time.RFC3339) }>{ localtime.Format("2 January 2006") } at { localtime.Format("3:04PM MST") }</time></label>
}
</div>
<div>
<label>Enable Widgets</label>
<label for="gb_remote_true">
<input type="radio" name="gb_remote" id="gb_remote_true" value="true" checked?={ gb.Settings.AllowRemoteHostAccess }/>
Yes
</label>
<label for="gb_remote_false">
<input type="radio" name="gb_remote" id="gb_remote_false" value="false" checked?={ !gb.Settings.AllowRemoteHostAccess }/>
No
</label>
</div>
<input type="submit" value="Submit"/>
</form>
</div>
</div>
}
}
templ AllGuestbooksView(data CommonData, websites []models.Website) {
@base("All Guestbooks", data) {
<div>
<h1>All Guestbooks</h1>
<p>
This page exists only for testing the service.
</p>
<ul>
for _, w := range websites {
<li>
{{ gbUrl := fmt.Sprintf("/websites/%s/guestbook", shortIdToSlug(w.ShortId)) }}
<a href={ templ.URL(gbUrl) } target="_blank">{ w.Name }</a>
</li>
}
</ul>
</div>
}
}

1029
ui/views/guestbooks_templ.go Normal file

File diff suppressed because it is too large Load Diff

19
ui/views/home.templ Normal file
View File

@ -0,0 +1,19 @@
package views
templ Home(title string, data CommonData) {
@base(title, data) {
<h2>Welcome</h2>
<p>
Welcome to webweav.ing, a collection of webmastery tools created by the <a href="https://32bit.cafe">32-Bit Cafe</a>.
</p>
<p>
Note this service is in a pre-alpha state. Your account and data can disappear at any time.
</p>
}
}
templ ComingSoon(title string, data CommonData) {
@base(title, data) {
<h2>Coming Soon</h2>
}
}

105
ui/views/home_templ.go Normal file
View File

@ -0,0 +1,105 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func Home(title string, data CommonData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h2>Welcome</h2><p>Welcome to webweav.ing, a collection of webmastery tools created by the <a href=\"https://32bit.cafe\">32-Bit Cafe</a>.</p><p>Note this service is in a pre-alpha state. Your account and data can disappear at any time. </p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func ComingSoon(title string, data CommonData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<h2>Coming Soon</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

103
ui/views/users.templ Normal file
View File

@ -0,0 +1,103 @@
package views
import (
"git.32bit.cafe/32bitcafe/guestbook/internal/forms"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
)
templ UserLogin(title string, data CommonData, form forms.UserLoginForm) {
@base(title, data) {
<h1>Login</h1>
<form action="/users/login" method="POST" novalidate>
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
for _, error := range form.NonFieldErrors {
<div class="error">{ error }</div>
}
<div>
<label>Email: </label>
{{ error, exists := form.FieldErrors["email"] }}
if exists {
<label class="error">{ error }</label>
}
<input type="email" name="email" value={ form.Email }/>
</div>
<div>
<label>Password: </label>
{{ error, exists = form.FieldErrors["password"] }}
if exists {
<label class="error">{ error }</label>
}
<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="login"/>
</div>
</form>
}
}
templ UserRegistration(title string, data CommonData, form forms.UserRegistrationForm) {
@base(title, data) {
<h1>User Registration</h1>
<form action="/users/register" method="post">
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
<div>
{{ error, exists := form.FieldErrors["name"] }}
<label for="username">Username: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="text" name="username" id="username" value={ form.Name } required/>
</div>
<div>
{{ error, exists = form.FieldErrors["email"] }}
<label for="email">Email: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="text" name="email" id="email" value={ form.Email } required/>
</div>
<div>
{{ error, exists = form.FieldErrors["password"] }}
<label for="password">Password: </label>
if exists {
<label class="error">{ error }</label>
}
<input type="password" name="password" id="password"/>
</div>
<div>
<input type="submit" value="Register"/>
</div>
</form>
}
}
templ UserProfile(title string, data CommonData, user models.User) {
@base(title, data) {
<h1>{ user.Username }</h1>
<p>{ user.Email }</p>
}
}
templ UserSettingsView(data CommonData, timezones []string) {
{{ user := data.CurrentUser }}
@base("User Settings", data) {
<div>
<h1>User Settings</h1>
<form hx-put="/users/settings">
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
<label>Local Timezone</label>
<select name="timezones" id="timezone-select">
for _, tz := range timezones {
if tz == user.Settings.LocalTimezone.String() {
<option value={ tz } selected="true">{ tz }</option>
} else {
<option value={ tz }>{ tz }</option>
}
}
</select>
<input type="submit" value="Submit"/>
</form>
</div>
}
}

533
ui/views/users_templ.go Normal file
View File

@ -0,0 +1,533 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"git.32bit.cafe/32bitcafe/guestbook/internal/forms"
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
)
func UserLogin(title string, data CommonData, form forms.UserLoginForm) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1>Login</h1><form action=\"/users/login\" method=\"POST\" novalidate><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 12, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, error := range form.NonFieldErrors {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 14, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div><label>Email: </label>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
error, exists := form.FieldErrors["email"]
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 20, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<input type=\"email\" name=\"email\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 22, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"></div><div><label>Password: </label>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
error, exists = form.FieldErrors["password"]
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 28, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"password\" name=\"password\"></div><div><input type=\"submit\" value=\"login\"></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func UserRegistration(title string, data CommonData, form forms.UserRegistrationForm) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var9 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h1>User Registration</h1><form action=\"/users/register\" method=\"post\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 43, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
error, exists := form.FieldErrors["name"]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<label for=\"username\">Username: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 48, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<input type=\"text\" name=\"username\" id=\"username\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(form.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 50, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" required></div><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
error, exists = form.FieldErrors["email"]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<label for=\"email\">Email: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 56, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<input type=\"text\" name=\"email\" id=\"email\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 58, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" required></div><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
error, exists = form.FieldErrors["password"]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<label for=\"password\">Password: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(error)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 64, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<input type=\"password\" name=\"password\" id=\"password\"></div><div><input type=\"submit\" value=\"Register\"></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var9), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func UserProfile(title string, data CommonData, user models.User) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
if templ_7745c5c3_Var16 == nil {
templ_7745c5c3_Var16 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var17 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 77, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</h1><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 78, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var17), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func UserSettingsView(data CommonData, timezones []string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
if templ_7745c5c3_Var20 == nil {
templ_7745c5c3_Var20 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
user := data.CurrentUser
templ_7745c5c3_Var21 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div><h1>User Settings</h1><form hx-put=\"/users/settings\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 88, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"> <label>Local Timezone</label> <select name=\"timezones\" id=\"timezone-select\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, tz := range timezones {
if tz == user.Settings.LocalTimezone.String() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 93, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" selected=\"true\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 93, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 95, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 95, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</select> <input type=\"submit\" value=\"Submit\"></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = base("User Settings", data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var21), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

154
ui/views/websites.templ Normal file
View File

@ -0,0 +1,154 @@
package views
import "fmt"
import "git.32bit.cafe/32bitcafe/guestbook/internal/models"
import "git.32bit.cafe/32bitcafe/guestbook/internal/forms"
func wUrl(w models.Website) string {
return fmt.Sprintf("/websites/%s", shortIdToSlug(w.ShortId))
}
templ wSidebar(website models.Website) {
{{ dashUrl := wUrl(website) + "/dashboard" }}
{{ gbUrl := wUrl(website) + "/guestbook" }}
<nav>
<div>
<h2>{ website.Name }</h2>
<ul>
<li><a href={ templ.URL(dashUrl) }>Dashboard</a></li>
<li><a href={ templ.URL(externalUrl(website.SiteUrl)) } target="_blank">View Website</a></li>
</ul>
<h3>Guestbook</h3>
<ul>
<li><a href={ templ.URL(gbUrl) } target="_blank">View Guestbook</a></li>
</ul>
<ul>
<li><a href={ templ.URL(dashUrl + "/guestbook/comments") }>Manage messages</a></li>
<li><a href={ templ.URL(dashUrl + "/guestbook/comments/queue") }>Review message queue</a></li>
<li><a href={ templ.URL(dashUrl + "/guestbook/comments/trash") }>Trash</a></li>
<li><a href={ templ.URL(dashUrl + "/guestbook/settings") }>Settings</a></li>
</ul>
<ul>
<li><a href={ templ.URL(dashUrl + "/guestbook/themes") }>Themes</a></li>
<li><a href={ templ.URL(dashUrl + "/guestbook/customize") }>Custom CSS</a></li>
</ul>
</div>
<div>
<h3>Feeds</h3>
<p>Coming Soon</p>
</div>
<div>
<h3>Account</h3>
<ul>
<li><a href="/users/settings">Settings</a></li>
<li><a href="/users/privacy">Privacy</a></li>
<li><a href="/help">Help</a></li>
</ul>
</div>
</nav>
}
templ displayWebsites(websites []models.Website) {
if len(websites) == 0 {
<p>No Websites yet. <a href="">Register a website.</a></p>
} else {
<ul id="websites" hx-get="/websites" hx-trigger="newWebsite from:body" hx-swap="outerHTML">
for _, w := range websites {
<li>
<a href={ templ.URL(wUrl(w) + "/dashboard") }>{ w.Name }</a>
</li>
}
</ul>
}
}
templ websiteCreateForm(csrfToken string, form forms.WebsiteCreateForm) {
<input type="hidden" name="csrf_token" value={ csrfToken }/>
<div>
{{ err, exists := form.FieldErrors["sitename"] }}
<label for="sitename">Site Name: </label>
if exists {
<label class="error">{ err }</label>
}
<input type="text" name="sitename" id="sitename" required/>
</div>
<div>
{{ err, exists = form.FieldErrors["siteurl"] }}
<label for="siteurl">Site URL: </label>
if exists {
<label class="error">{ err }</label>
}
<input type="text" name="siteurl" id="siteurl" required/>
</div>
<div>
{{ err, exists = form.FieldErrors["authorname"] }}
<label for="authorname">Site Author: </label>
if exists {
<label class="error">{ err }</label>
}
<input type="text" name="authorname" id="authorname" required/>
</div>
<div>
<button type="submit">Submit</button>
</div>
}
templ WebsiteCreateButton() {
<button hx-get="/websites/create" hx-target="closest div">Add Website</button>
}
templ WebsiteList(title string, data CommonData, websites []models.Website) {
if data.IsHtmx {
@displayWebsites(websites)
} else {
@base(title, data) {
<h1>My Websites</h1>
<p>
@WebsiteCreateButton()
</p>
<div>
@displayWebsites(websites)
</div>
}
}
}
templ WebsiteDashboard(title string, data CommonData, website models.Website) {
@base(title, data) {
<div id="dashboard">
@wSidebar(website)
<div>
<h1>{ website.Name }</h1>
<p>
Stats and stuff will go here.
</p>
</div>
</div>
}
}
templ WebsiteDashboardComingSoon(title string, data CommonData, website models.Website) {
@base(title, data) {
<div id="dashboard">
@wSidebar(website)
<div>
<h1>{ website.Name }</h1>
<p>
Coming Soon
</p>
</div>
</div>
}
}
templ WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm) {
if data.IsHtmx {
<form hx-post="/websites/create" hx-target="closest div">
@websiteCreateForm(data.CSRFToken, form)
</form>
} else {
<form action="/websites/create" method="post">
@websiteCreateForm(data.CSRFToken, form)
</form>
}
}

625
ui/views/websites_templ.go Normal file
View File

@ -0,0 +1,625 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "fmt"
import "git.32bit.cafe/32bitcafe/guestbook/internal/models"
import "git.32bit.cafe/32bitcafe/guestbook/internal/forms"
func wUrl(w models.Website) string {
return fmt.Sprintf("/websites/%s", shortIdToSlug(w.ShortId))
}
func wSidebar(website models.Website) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
dashUrl := wUrl(website) + "/dashboard"
gbUrl := wUrl(website) + "/guestbook"
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<nav><div><h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 16, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h2><ul><li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL = templ.URL(dashUrl)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">Dashboard</a></li><li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL = templ.URL(externalUrl(website.SiteUrl))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" target=\"_blank\">View Website</a></li></ul><h3>Guestbook</h3><ul><li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL = templ.URL(gbUrl)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var5)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" target=\"_blank\">View Guestbook</a></li></ul><ul><li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 templ.SafeURL = templ.URL(dashUrl + "/guestbook/comments")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var6)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">Manage messages</a></li><li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 templ.SafeURL = templ.URL(dashUrl + "/guestbook/comments/queue")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var7)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">Review message queue</a></li><li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL = templ.URL(dashUrl + "/guestbook/comments/trash")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var8)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">Trash</a></li><li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 templ.SafeURL = templ.URL(dashUrl + "/guestbook/settings")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var9)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">Settings</a></li></ul><ul><li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 templ.SafeURL = templ.URL(dashUrl + "/guestbook/themes")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var10)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">Themes</a></li><li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 templ.SafeURL = templ.URL(dashUrl + "/guestbook/customize")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var11)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">Custom CSS</a></li></ul></div><div><h3>Feeds</h3><p>Coming Soon</p></div><div><h3>Account</h3><ul><li><a href=\"/users/settings\">Settings</a></li><li><a href=\"/users/privacy\">Privacy</a></li><li><a href=\"/help\">Help</a></li></ul></div></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func displayWebsites(websites []models.Website) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
if templ_7745c5c3_Var12 == nil {
templ_7745c5c3_Var12 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if len(websites) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<p>No Websites yet. <a href=\"\">Register a website.</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<ul id=\"websites\" hx-get=\"/websites\" hx-trigger=\"newWebsite from:body\" hx-swap=\"outerHTML\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, w := range websites {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 templ.SafeURL = templ.URL(wUrl(w) + "/dashboard")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var13)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(w.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 58, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
func websiteCreateForm(csrfToken string, form forms.WebsiteCreateForm) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var15 := templ.GetChildren(ctx)
if templ_7745c5c3_Var15 == nil {
templ_7745c5c3_Var15 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 66, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
err, exists := form.FieldErrors["sitename"]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<label for=\"sitename\">Site Name: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(err)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 71, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<input type=\"text\" name=\"sitename\" id=\"sitename\" required></div><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
err, exists = form.FieldErrors["siteurl"]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<label for=\"siteurl\">Site URL: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(err)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 79, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<input type=\"text\" name=\"siteurl\" id=\"siteurl\" required></div><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
err, exists = form.FieldErrors["authorname"]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<label for=\"authorname\">Site Author: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(err)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 87, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<input type=\"text\" name=\"authorname\" id=\"authorname\" required></div><div><button type=\"submit\">Submit</button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func WebsiteCreateButton() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
if templ_7745c5c3_Var20 == nil {
templ_7745c5c3_Var20 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<button hx-get=\"/websites/create\" hx-target=\"closest div\">Add Website</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func WebsiteList(title string, data CommonData, websites []models.Website) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var21 := templ.GetChildren(ctx)
if templ_7745c5c3_Var21 == nil {
templ_7745c5c3_Var21 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if data.IsHtmx {
templ_7745c5c3_Err = displayWebsites(websites).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Var22 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<h1>My Websites</h1><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = WebsiteCreateButton().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</p><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = displayWebsites(websites).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var22), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
func WebsiteDashboard(title string, data CommonData, website models.Website) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var23 := templ.GetChildren(ctx)
if templ_7745c5c3_Var23 == nil {
templ_7745c5c3_Var23 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var24 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div id=\"dashboard\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = wSidebar(website).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div><h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 121, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</h1><p>Stats and stuff will go here.</p></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var24), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func WebsiteDashboardComingSoon(title string, data CommonData, website models.Website) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var26 := templ.GetChildren(ctx)
if templ_7745c5c3_Var26 == nil {
templ_7745c5c3_Var26 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var27 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div id=\"dashboard\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = wSidebar(website).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<div><h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 135, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</h1><p>Coming Soon</p></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var27), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func WebsiteCreate(title string, data CommonData, form forms.WebsiteCreateForm) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var29 := templ.GetChildren(ctx)
if templ_7745c5c3_Var29 == nil {
templ_7745c5c3_Var29 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if data.IsHtmx {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<form hx-post=\"/websites/create\" hx-target=\"closest div\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = websiteCreateForm(data.CSRFToken, form).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<form action=\"/websites/create\" method=\"post\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = websiteCreateForm(data.CSRFToken, form).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate