implement user settings for timezone

This commit is contained in:
yequari 2025-05-03 23:14:58 -07:00
parent 8222af6d98
commit 82c00b9a01
18 changed files with 590 additions and 150 deletions

View File

@ -39,7 +39,8 @@ func (app *application) postUserRegister(w http.ResponseWriter, r *http.Request)
return return
} }
shortId := app.createShortId() shortId := app.createShortId()
err = app.users.Insert(shortId, form.Name, form.Email, form.Password) settings := DefaultUserSettings()
err = app.users.Insert(shortId, form.Name, form.Email, form.Password, settings)
if err != nil { if err != nil {
if errors.Is(err, models.ErrDuplicateEmail) { if errors.Is(err, models.ErrDuplicateEmail) {
form.AddFieldError("email", "Email address is already in use") form.AddFieldError("email", "Email address is already in use")
@ -129,3 +130,34 @@ func (app *application) getUser(w http.ResponseWriter, r *http.Request) {
data := app.newCommonData(r) data := app.newCommonData(r)
views.UserProfile(user.Username, data, user).Render(r.Context(), w) 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) {
userId := app.getCurrentUser(r).ID
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() {
// rerender template with errors
app.clientError(w, http.StatusUnprocessableEntity)
}
err = app.users.SetLocalTimezone(userId, form.LocalTimezone)
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)
}

View File

@ -112,3 +112,9 @@ func (app *application) newCommonData(r *http.Request) views.CommonData {
IsHtmx: r.Header.Get("Hx-Request") == "true", IsHtmx: r.Header.Get("Hx-Request") == "true",
} }
} }
func DefaultUserSettings() models.UserSettings {
return models.UserSettings{
LocalTimezone: time.Now().UTC().Location(),
}
}

View File

@ -7,7 +7,9 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
"unicode"
"git.32bit.cafe/32bitcafe/guestbook/internal/models" "git.32bit.cafe/32bitcafe/guestbook/internal/models"
"github.com/alexedwards/scs/sqlite3store" "github.com/alexedwards/scs/sqlite3store"
@ -26,6 +28,7 @@ type application struct {
sessionManager *scs.SessionManager sessionManager *scs.SessionManager
formDecoder *schema.Decoder formDecoder *schema.Decoder
debug bool debug bool
timezones []string
} }
func main() { func main() {
@ -60,6 +63,7 @@ func main() {
guestbookComments: &models.GuestbookCommentModel{DB: db}, guestbookComments: &models.GuestbookCommentModel{DB: db},
formDecoder: formDecoder, formDecoder: formDecoder,
debug: *debug, debug: *debug,
timezones: getAvailableTimezones(),
} }
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
@ -97,3 +101,50 @@ func openDB(dsn string) (*sql.DB, error) {
} }
return db, nil 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
}

View File

@ -28,7 +28,8 @@ func (app *application) routes() http.Handler {
// mux.Handle("GET /users", protected.ThenFunc(app.getUsersList)) // mux.Handle("GET /users", protected.ThenFunc(app.getUsersList))
mux.Handle("GET /users/{id}", protected.ThenFunc(app.getUser)) mux.Handle("GET /users/{id}", protected.ThenFunc(app.getUser))
mux.Handle("POST /users/logout", protected.ThenFunc(app.postUserLogout)) mux.Handle("POST /users/logout", protected.ThenFunc(app.postUserLogout))
mux.Handle("GET /users/settings", protected.ThenFunc(app.notImplemented)) 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 /users/privacy", protected.ThenFunc(app.notImplemented))
mux.Handle("GET /guestbooks", protected.ThenFunc(app.getAllGuestbooks)) mux.Handle("GET /guestbooks", protected.ThenFunc(app.getAllGuestbooks))

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 NOT NULL,
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 NOT NULL,
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

@ -29,3 +29,8 @@ type WebsiteCreateForm struct {
AuthorName string `schema:"authorname"` AuthorName string `schema:"authorname"`
validator.Validator `schema:"-"` validator.Validator `schema:"-"`
} }
type UserSettingsForm struct {
LocalTimezone string `schema:"timezones"`
validator.Validator `schema:"-"`
}

View File

@ -10,6 +10,14 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
const (
u_timezone = 1
)
type UserSettings struct {
LocalTimezone *time.Location
}
type User struct { type User struct {
ID int64 ID int64
ShortId uint64 ShortId uint64
@ -19,20 +27,21 @@ type User struct {
IsBanned bool IsBanned bool
HashedPassword []byte HashedPassword []byte
Created time.Time Created time.Time
Settings UserSettings
} }
type UserModel struct { type UserModel struct {
DB *sql.DB DB *sql.DB
} }
func (m *UserModel) Insert(shortId uint64, username string, email string, password string) error { func (m *UserModel) Insert(shortId uint64, username string, email string, password string, settings UserSettings) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil { if err != nil {
return err return err
} }
stmt := `INSERT INTO users (ShortId, Username, Email, IsBanned, HashedPassword, Created) stmt := `INSERT INTO users (ShortId, Username, Email, IsBanned, HashedPassword, Created)
VALUES (?, ?, ?, FALSE, ?, ?)` VALUES (?, ?, ?, FALSE, ?, ?)`
_, err = m.DB.Exec(stmt, shortId, username, email, hashedPassword, time.Now().UTC()) result, err := m.DB.Exec(stmt, shortId, username, email, hashedPassword, time.Now().UTC())
if err != nil { if err != nil {
if sqliteError, ok := err.(sqlite3.Error); ok { if sqliteError, ok := err.(sqlite3.Error); ok {
if sqliteError.ExtendedCode == 2067 && strings.Contains(sqliteError.Error(), "Email") { if sqliteError.ExtendedCode == 2067 && strings.Contains(sqliteError.Error(), "Email") {
@ -41,6 +50,17 @@ func (m *UserModel) Insert(shortId uint64, username string, email string, passwo
} }
return err return err
} }
id, err := result.LastInsertId()
if err != nil {
return err
}
settingsStmt := `INSERT INTO user_settings
(UserId, SettingId, AllowedSettingValueId, UnconstrainedValue)
VALUES (?, ?, ?, ?)`
_, err = m.DB.Exec(settingsStmt, id, u_timezone, nil, settings.LocalTimezone.String())
if err != nil {
return err
}
return nil return nil
} }
@ -55,6 +75,11 @@ func (m *UserModel) Get(id uint64) (User, error) {
} }
return User{}, err return User{}, err
} }
settings, err := m.GetSettings(u.ID)
if err != nil {
return u, err
}
u.Settings = settings
return u, nil return u, nil
} }
@ -69,6 +94,11 @@ func (m *UserModel) GetById(id int64) (User, error) {
} }
return User{}, err return User{}, err
} }
settings, err := m.GetSettings(u.ID)
if err != nil {
return u, err
}
u.Settings = settings
return u, nil return u, nil
} }
@ -125,3 +155,40 @@ func (m *UserModel) Exists(id int64) (bool, error) {
err := m.DB.QueryRow(stmt, id).Scan(&exists) err := m.DB.QueryRow(stmt, id).Scan(&exists)
return exists, err 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.SettingId = a.SettingId
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 u_timezone:
settings.LocalTimezone, err = time.LoadLocation(unconstrainedValue.String)
if err != nil {
panic(err)
}
}
}
return settings, err
}
func (m *UserModel) SetLocalTimezone(userId int64, timezone string) error {
stmt := `UPDATE user_settings SET UnconstrainedValue = ? WHERE UserId = ?`
_, err := m.DB.Exec(stmt, timezone, userId)
if err != nil {
return err
}
return nil
}

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833 // templ: version: v0.3.857
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -48,7 +48,7 @@ templ GuestbookDashboardCommentView(data CommonData, w models.Website, c models.
| <a href={ templ.URL(externalUrl(c.AuthorSite))} target="_blank">{ c.AuthorSite }</a> | <a href={ templ.URL(externalUrl(c.AuthorSite))} target="_blank">{ c.AuthorSite }</a>
} }
<p> <p>
{ c.Created.Format("01-02-2006 03:04PM") } { c.Created.In(data.CurrentUser.Settings.LocalTimezone).Format("01-02-2006 03:04PM") }
</p> </p>
</div> </div>
<p> <p>
@ -167,4 +167,4 @@ templ AllGuestbooksView(data CommonData, websites []models.Website) {
</ul> </ul>
</div> </div>
} }
} }

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833 // templ: version: v0.3.857
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@ -275,9 +275,9 @@ func GuestbookDashboardCommentView(data CommonData, w models.Website, c models.G
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(c.Created.Format("01-02-2006 03:04PM")) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(c.Created.In(data.CurrentUser.Settings.LocalTimezone).Format("01-02-2006 03:04PM"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 51, Col: 56} return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/guestbooks.templ`, Line: 51, Col: 100}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833 // templ: version: v0.3.857
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -79,5 +79,11 @@ templ UserProfile (title string, data CommonData, user models.User) {
} }
} }
templ UserSettings () { templ UserSettings (title string, data CommonData, user models.User) {
@base(title, data) {
<form hx-put="/users/settings">
<input type="text">
<button type="submit">Submit</button>
</form>
}
} }

View File

@ -0,0 +1,24 @@
package views
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 }/>
<h3>Timezone</h3>
<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>
}
}

View File

@ -0,0 +1,141 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.857
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 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_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
user := data.CurrentUser
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, "<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_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_settings.templ`, Line: 9, Col: 65}
}
_, 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, "\"><h3>Timezone</h3><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, 3, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users_settings.templ`, Line: 14, Col: 25}
}
_, 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, "\" selected=\"true\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users_settings.templ`, Line: 14, Col: 48}
}
_, 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, 5, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users_settings.templ`, Line: 16, Col: 25}
}
_, 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, 7, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users_settings.templ`, Line: 16, Col: 32}
}
_, 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, 8, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</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_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833 // templ: version: v0.3.857
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@ -400,7 +400,7 @@ func UserProfile(title string, data CommonData, user models.User) templ.Componen
}) })
} }
func UserSettings() templ.Component { func UserSettings(title string, data CommonData, user models.User) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@ -421,6 +421,28 @@ func UserSettings() templ.Component {
templ_7745c5c3_Var20 = templ.NopComponent templ_7745c5c3_Var20 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
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, "<form hx-put=\"/users/settings\"><input type=\"text\"> <button type=\"submit\">Submit</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = base(title, data).Render(templ.WithChildren(ctx, templ_7745c5c3_Var21), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil return nil
}) })
} }

View File

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

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833 // templ: version: v0.3.857
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.
@ -46,7 +46,7 @@ func wSidebar(website models.Website) templ.Component {
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 16, Col: 30} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -189,7 +189,7 @@ func displayWebsites(websites []models.Website) templ.Component {
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(w.Name) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(w.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 58, Col: 73} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -237,7 +237,7 @@ func websiteCreateForm(csrfToken string, form forms.WebsiteCreateForm) templ.Com
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken) templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 66, Col: 59} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -260,7 +260,7 @@ func websiteCreateForm(csrfToken string, form forms.WebsiteCreateForm) templ.Com
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(err) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(err)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 71, Col: 38} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -288,7 +288,7 @@ func websiteCreateForm(csrfToken string, form forms.WebsiteCreateForm) templ.Com
var templ_7745c5c3_Var18 string var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(err) templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(err)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 79, Col: 38} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -316,7 +316,7 @@ func websiteCreateForm(csrfToken string, form forms.WebsiteCreateForm) templ.Com
var templ_7745c5c3_Var19 string var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(err) templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(err)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 87, Col: 38} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -403,7 +403,7 @@ func WebsiteList(title string, data CommonData, websites []models.Website) templ
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<h1>My Websites</h1><div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<h1>My Websites</h1><p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -411,7 +411,7 @@ func WebsiteList(title string, data CommonData, websites []models.Website) templ
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</div><div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</p><div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -482,7 +482,7 @@ func WebsiteDashboard(title string, data CommonData, website models.Website) tem
var templ_7745c5c3_Var25 string var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name) templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 121, Col: 34} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -550,7 +550,7 @@ func WebsiteDashboardComingSoon(title string, data CommonData, website models.We
var templ_7745c5c3_Var28 string var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name) templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(website.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/websites.templ`, Line: 135, Col: 34} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {