Add profile form, inside a user menu
This is the first form that uses HTMx, and can not return a 400 error code because otherwise HTMx does not render the content. Since now there are pages that do not render the whole html, with header and body, i need a different layout for these, and moved the common code to handle forms and such a new template file that both layouts can use. I also need the request in template.MustRender to check which layout i should use. Closes #7.
This commit is contained in:
parent
341520def3
commit
f963f54839
2
Makefile
2
Makefile
|
@ -17,7 +17,7 @@ po/%.po: $(POT_FILE)
|
||||||
|
|
||||||
$(POT_FILE): $(HTML_FILES) $(GO_FILES)
|
$(POT_FILE): $(HTML_FILES) $(GO_FILES)
|
||||||
xgettext $(XGETTEXTFLAGS) --language=Scheme --output=$@ --keyword=pgettext:1,2c $(HTML_FILES)
|
xgettext $(XGETTEXTFLAGS) --language=Scheme --output=$@ --keyword=pgettext:1,2c $(HTML_FILES)
|
||||||
xgettext $(XGETTEXTFLAGS) --language=C --output=$@ --keyword=Gettext:1 --keyword=GettextNoop:1 --join-existing $(GO_FILES)
|
xgettext $(XGETTEXTFLAGS) --language=C --output=$@ --keyword=Gettext:1 --keyword=GettextNoop:1 --keyword=Pgettext:1,2c --join-existing $(GO_FILES)
|
||||||
|
|
||||||
test-deploy:
|
test-deploy:
|
||||||
sqitch deploy --db-name $(PGDATABASE)
|
sqitch deploy --db-name $(PGDATABASE)
|
||||||
|
|
|
@ -112,6 +112,6 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *App) serveDashboard(w http.ResponseWriter, _ *http.Request, user *auth.User) {
|
func (h *App) serveDashboard(w http.ResponseWriter, r *http.Request, user *auth.User) {
|
||||||
template.MustRender(w, user, "dashboard.gohtml", nil)
|
template.MustRender(w, r, user, "dashboard.gohtml", nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,10 +60,14 @@ func (f *loginForm) Valid(l *locale.Locale) bool {
|
||||||
return v.AllOK
|
return v.AllOK
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveLoginForm(w http.ResponseWriter, _ *http.Request, user *auth.User, redirectPath string) {
|
func (f *loginForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User) {
|
||||||
|
template.MustRender(w, r, user, "login.gohtml", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveLoginForm(w http.ResponseWriter, r *http.Request, user *auth.User, redirectPath string) {
|
||||||
login := newLoginForm()
|
login := newLoginForm()
|
||||||
login.Redirect.Val = redirectPath
|
login.Redirect.Val = redirectPath
|
||||||
template.MustRender(w, user, "login.gohtml", login)
|
login.MustRender(w, r, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLogin(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) {
|
func handleLogin(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) {
|
||||||
|
@ -84,7 +88,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request, user *auth.User, conn *
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
}
|
}
|
||||||
template.MustRender(w, user, "login.gohtml", login)
|
login.MustRender(w, r, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLogout(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) {
|
func handleLogout(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) {
|
||||||
|
|
114
pkg/app/user.go
114
pkg/app/user.go
|
@ -1,13 +1,22 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||||
|
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *App) getUser(r *http.Request, conn *database.Conn) (*auth.User, error) {
|
func (h *App) getUser(r *http.Request, conn *database.Conn) (*auth.User, error) {
|
||||||
|
@ -61,8 +70,113 @@ func profileHandler(user *auth.User, conn *database.Conn) http.HandlerFunc {
|
||||||
default:
|
default:
|
||||||
methodNotAllowed(w, r, http.MethodDelete)
|
methodNotAllowed(w, r, http.MethodDelete)
|
||||||
}
|
}
|
||||||
|
case "":
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
serveProfileForm(w, r, user, conn)
|
||||||
|
case http.MethodPut:
|
||||||
|
updateProfile(w, r, user, conn)
|
||||||
|
default:
|
||||||
|
methodNotAllowed(w, r, http.MethodGet, http.MethodPut)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serveProfileForm(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) {
|
||||||
|
profile := newProfileForm(r.Context(), user.Locale, conn)
|
||||||
|
profile.Name.Val = conn.MustGetText(r.Context(), "select name from user_profile")
|
||||||
|
profile.Email.Val = user.Email
|
||||||
|
profile.Language.Selected = []string{user.Language.String()}
|
||||||
|
profile.MustRender(w, r, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateProfile(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) {
|
||||||
|
profile := newProfileForm(r.Context(), user.Locale, conn)
|
||||||
|
if err := profile.Parse(r); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := user.VerifyCSRFToken(r); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !profile.Valid(user.Locale) {
|
||||||
|
if !httplib.IsHTMxRequest(r) {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
}
|
||||||
|
profile.MustRender(w, r, user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//goland:noinspection SqlWithoutWhere
|
||||||
|
cookie := conn.MustGetText(r.Context(), "update user_profile set name = $1, email = $2, lang_tag = $3 returning build_cookie()", profile.Name, profile.Email, profile.Language)
|
||||||
|
auth.SetSessionCookie(w, cookie)
|
||||||
|
if profile.Password.Val != "" {
|
||||||
|
conn.MustExec(r.Context(), "select change_password($1)", profile.Password)
|
||||||
|
}
|
||||||
|
if user.Language.String() == profile.Language.String() {
|
||||||
|
httplib.Relocate(w, r, "/me", http.StatusSeeOther)
|
||||||
|
} else {
|
||||||
|
httplib.Redirect(w, r, "/me", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type profileForm struct {
|
||||||
|
Name *form.Input
|
||||||
|
Email *form.Input
|
||||||
|
Password *form.Input
|
||||||
|
PasswordConfirm *form.Input
|
||||||
|
Language *form.Select
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProfileForm(ctx context.Context, l *locale.Locale, conn *database.Conn) *profileForm {
|
||||||
|
automaticOption := l.Pgettext("Automatic", "language option")
|
||||||
|
languages := form.MustGetOptions(ctx, conn, "select 'und', $1 union all select lang_tag, endonym from language where selectable", automaticOption)
|
||||||
|
return &profileForm{
|
||||||
|
Name: &form.Input{
|
||||||
|
Name: "name",
|
||||||
|
},
|
||||||
|
Email: &form.Input{
|
||||||
|
Name: "email",
|
||||||
|
},
|
||||||
|
Password: &form.Input{
|
||||||
|
Name: "password",
|
||||||
|
},
|
||||||
|
PasswordConfirm: &form.Input{
|
||||||
|
Name: "password_confirm",
|
||||||
|
},
|
||||||
|
Language: &form.Select{
|
||||||
|
Name: "language",
|
||||||
|
Options: languages,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *profileForm) Parse(r *http.Request) error {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.Email.FillValue(r)
|
||||||
|
f.Name.FillValue(r)
|
||||||
|
f.Password.FillValue(r)
|
||||||
|
f.PasswordConfirm.FillValue(r)
|
||||||
|
f.Language.FillValue(r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *profileForm) Valid(l *locale.Locale) bool {
|
||||||
|
v := form.NewValidator(l)
|
||||||
|
if v.CheckRequired(f.Email, l.GettextNoop("Email can not be empty.")) {
|
||||||
|
v.CheckValidEmail(f.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com."))
|
||||||
|
}
|
||||||
|
v.CheckRequired(f.Name, l.GettextNoop("Name can not be empty."))
|
||||||
|
v.CheckPasswordConfirmation(f.Password, f.PasswordConfirm, l.GettextNoop("Confirmation does not match password."))
|
||||||
|
v.CheckSelectedOptions(f.Language, l.GettextNoop("Selected language is not valid."))
|
||||||
|
return v.AllOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *profileForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User) {
|
||||||
|
template.MustRender(w, r, user, "profile.gohtml", f)
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,10 @@ type Input struct {
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (input *Input) setError(err error) {
|
||||||
|
input.Error = err
|
||||||
|
}
|
||||||
|
|
||||||
func (input *Input) FillValue(r *http.Request) {
|
func (input *Input) FillValue(r *http.Request) {
|
||||||
input.Val = strings.TrimSpace(r.FormValue(input.Name))
|
input.Val = strings.TrimSpace(r.FormValue(input.Name))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package form
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql/driver"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Select struct {
|
||||||
|
Name string
|
||||||
|
Selected []string
|
||||||
|
Options []*Option
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) setError(err error) {
|
||||||
|
s.Error = err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) Value() (driver.Value, error) {
|
||||||
|
return s.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) String() string {
|
||||||
|
if s.Selected == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s.Selected[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) FillValue(r *http.Request) {
|
||||||
|
s.Selected = r.Form[s.Name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) validOptionsSelected() bool {
|
||||||
|
for _, selected := range s.Selected {
|
||||||
|
if !s.isValidOption(selected) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) isValidOption(selected string) bool {
|
||||||
|
for _, option := range s.Options {
|
||||||
|
if option.Value == selected {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Select) IsSelected(v string) bool {
|
||||||
|
for _, selected := range s.Selected {
|
||||||
|
if selected == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
Value string
|
||||||
|
Label string
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustGetOptions(ctx context.Context, conn *database.Conn, sql string, args ...interface{}) []*Option {
|
||||||
|
rows, err := conn.Query(ctx, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var options []*Option
|
||||||
|
for rows.Next() {
|
||||||
|
option := &Option{}
|
||||||
|
err = rows.Scan(&option.Value, &option.Label)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
options = append(options, option)
|
||||||
|
}
|
||||||
|
if rows.Err() != nil {
|
||||||
|
panic(rows.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
|
@ -32,9 +32,21 @@ func (v *Validator) CheckValidEmail(input *Input, message string) bool {
|
||||||
return v.check(input, err == nil, message)
|
return v.check(input, err == nil, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Validator) check(field *Input, ok bool, message string) bool {
|
func (v *Validator) CheckPasswordConfirmation(password *Input, confirm *Input, message string) bool {
|
||||||
|
return v.check(confirm, password.Val == confirm.Val, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckSelectedOptions(field *Select, message string) bool {
|
||||||
|
return v.check(field, field.validOptionsSelected(), message)
|
||||||
|
}
|
||||||
|
|
||||||
|
type field interface {
|
||||||
|
setError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) check(field field, ok bool, message string) bool {
|
||||||
if !ok {
|
if !ok {
|
||||||
field.Error = errors.New(v.l.Get(message))
|
field.setError(errors.New(v.l.Get(message)))
|
||||||
v.AllOK = false
|
v.AllOK = false
|
||||||
}
|
}
|
||||||
return ok
|
return ok
|
||||||
|
|
|
@ -54,6 +54,10 @@ func (l *Locale) Gettext(str string) string {
|
||||||
return l.GetD(l.GetDomain(), str)
|
return l.GetD(l.GetDomain(), str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Locale) Pgettext(str string, ctx string) string {
|
||||||
|
return l.GetDC(l.GetDomain(), str, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Locale) GettextNoop(str string) string {
|
func (l *Locale) GettextNoop(str string) string {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,18 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
|
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func templateFile(name string) string {
|
func templateFile(name string) string {
|
||||||
return "web/templates/" + name
|
return "web/templates/" + name
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustRender(w io.Writer, user *auth.User, filename string, data interface{}) {
|
func MustRender(w io.Writer, r *http.Request, user *auth.User, filename string, data interface{}) {
|
||||||
layout := "layout.gohtml"
|
layout := "layout.gohtml"
|
||||||
|
if httplib.IsHTMxRequest(r) {
|
||||||
|
layout = "htmx.gohtml"
|
||||||
|
}
|
||||||
mustRenderLayout(w, user, layout, filename, data)
|
mustRenderLayout(w, user, layout, filename, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +45,7 @@ func mustRenderLayout(w io.Writer, user *auth.User, layout string, filename stri
|
||||||
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, auth.CSRFTokenField, user.CSRFToken))
|
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, auth.CSRFTokenField, user.CSRFToken))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if _, err := t.ParseFiles(templateFile(layout), templateFile(filename)); err != nil {
|
if _, err := t.ParseFiles(templateFile(layout), templateFile("form.gohtml"), templateFile(filename)); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if rw, ok := w.(http.ResponseWriter); ok {
|
if rw, ok := w.(http.ResponseWriter); ok {
|
||||||
|
|
64
po/ca.po
64
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: camper\n"
|
"Project-Id-Version: camper\n"
|
||||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||||
"POT-Creation-Date: 2023-07-26 13:28+0200\n"
|
"POT-Creation-Date: 2023-07-26 20:36+0200\n"
|
||||||
"PO-Revision-Date: 2023-07-22 23:45+0200\n"
|
"PO-Revision-Date: 2023-07-22 23:45+0200\n"
|
||||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||||
|
@ -28,12 +28,12 @@ msgctxt "title"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entrada"
|
msgstr "Entrada"
|
||||||
|
|
||||||
#: web/templates/login.gohtml:22
|
#: web/templates/login.gohtml:22 web/templates/profile.gohtml:26
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "Correu-e"
|
msgstr "Correu-e"
|
||||||
|
|
||||||
#: web/templates/login.gohtml:31
|
#: web/templates/login.gohtml:31 web/templates/profile.gohtml:36
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr "Contrasenya"
|
msgstr "Contrasenya"
|
||||||
|
@ -43,20 +43,51 @@ msgctxt "action"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entra"
|
msgstr "Entra"
|
||||||
|
|
||||||
#: web/templates/layout.gohtml:16
|
#: web/templates/profile.gohtml:6 web/templates/profile.gohtml:12
|
||||||
|
#: web/templates/layout.gohtml:28
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Profile"
|
||||||
|
msgstr "Perfil"
|
||||||
|
|
||||||
|
#: web/templates/profile.gohtml:17
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Nom"
|
||||||
|
|
||||||
|
#: web/templates/profile.gohtml:45
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Password Confirmation"
|
||||||
|
msgstr "Confirmació de la contrasenya"
|
||||||
|
|
||||||
|
#: web/templates/profile.gohtml:55
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Language"
|
||||||
|
msgstr "Idioma"
|
||||||
|
|
||||||
|
#: web/templates/profile.gohtml:65
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Save changes"
|
||||||
|
msgstr "Desa els canvis"
|
||||||
|
|
||||||
|
#: web/templates/layout.gohtml:17
|
||||||
msgid "Skip to main content"
|
msgid "Skip to main content"
|
||||||
msgstr "Salta al contingut principal"
|
msgstr "Salta al contingut principal"
|
||||||
|
|
||||||
#: web/templates/layout.gohtml:20
|
#: web/templates/layout.gohtml:24
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "User Menu"
|
||||||
|
msgstr "Menú d’usuari"
|
||||||
|
|
||||||
|
#: web/templates/layout.gohtml:32
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr "Surt"
|
msgstr "Surt"
|
||||||
|
|
||||||
#: pkg/app/login.go:56
|
#: pkg/app/login.go:56 pkg/app/user.go:170
|
||||||
msgid "Email can not be empty."
|
msgid "Email can not be empty."
|
||||||
msgstr "No podeu deixar el correu en blanc."
|
msgstr "No podeu deixar el correu en blanc."
|
||||||
|
|
||||||
#: pkg/app/login.go:57
|
#: pkg/app/login.go:57 pkg/app/user.go:171
|
||||||
msgid "This email is not valid. It should be like name@domain.com."
|
msgid "This email is not valid. It should be like name@domain.com."
|
||||||
msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com."
|
msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com."
|
||||||
|
|
||||||
|
@ -64,10 +95,27 @@ msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com."
|
||||||
msgid "Password can not be empty."
|
msgid "Password can not be empty."
|
||||||
msgstr "No podeu deixar la contrasenya en blanc."
|
msgstr "No podeu deixar la contrasenya en blanc."
|
||||||
|
|
||||||
#: pkg/app/login.go:82
|
#: pkg/app/login.go:86
|
||||||
msgid "Invalid user or password."
|
msgid "Invalid user or password."
|
||||||
msgstr "Nom d’usuari o contrasenya incorrectes."
|
msgstr "Nom d’usuari o contrasenya incorrectes."
|
||||||
|
|
||||||
|
#: pkg/app/user.go:135
|
||||||
|
msgctxt "language option"
|
||||||
|
msgid "Automatic"
|
||||||
|
msgstr "Automàtic"
|
||||||
|
|
||||||
|
#: pkg/app/user.go:173
|
||||||
|
msgid "Name can not be empty."
|
||||||
|
msgstr "No podeu deixar el nom en blanc."
|
||||||
|
|
||||||
|
#: pkg/app/user.go:174
|
||||||
|
msgid "Confirmation does not match password."
|
||||||
|
msgstr "La confirmació no es correspon amb la contrasenya."
|
||||||
|
|
||||||
|
#: pkg/app/user.go:175
|
||||||
|
msgid "Selected language is not valid."
|
||||||
|
msgstr "L’idioma escollit no és vàlid."
|
||||||
|
|
||||||
#: pkg/auth/user.go:39
|
#: pkg/auth/user.go:39
|
||||||
msgid "Cross-site request forgery detected."
|
msgid "Cross-site request forgery detected."
|
||||||
msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats."
|
msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats."
|
||||||
|
|
64
po/es.po
64
po/es.po
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: camper\n"
|
"Project-Id-Version: camper\n"
|
||||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||||
"POT-Creation-Date: 2023-07-26 13:28+0200\n"
|
"POT-Creation-Date: 2023-07-26 20:36+0200\n"
|
||||||
"PO-Revision-Date: 2023-07-22 23:46+0200\n"
|
"PO-Revision-Date: 2023-07-22 23:46+0200\n"
|
||||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||||
|
@ -28,12 +28,12 @@ msgctxt "title"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entrada"
|
msgstr "Entrada"
|
||||||
|
|
||||||
#: web/templates/login.gohtml:22
|
#: web/templates/login.gohtml:22 web/templates/profile.gohtml:26
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "Correo-e"
|
msgstr "Correo-e"
|
||||||
|
|
||||||
#: web/templates/login.gohtml:31
|
#: web/templates/login.gohtml:31 web/templates/profile.gohtml:36
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr "Contraseña"
|
msgstr "Contraseña"
|
||||||
|
@ -43,20 +43,51 @@ msgctxt "action"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entrar"
|
msgstr "Entrar"
|
||||||
|
|
||||||
#: web/templates/layout.gohtml:16
|
#: web/templates/profile.gohtml:6 web/templates/profile.gohtml:12
|
||||||
|
#: web/templates/layout.gohtml:28
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "Profile"
|
||||||
|
msgstr "Perfil"
|
||||||
|
|
||||||
|
#: web/templates/profile.gohtml:17
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Nombre"
|
||||||
|
|
||||||
|
#: web/templates/profile.gohtml:45
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Password Confirmation"
|
||||||
|
msgstr "Confirmación de la contraseña"
|
||||||
|
|
||||||
|
#: web/templates/profile.gohtml:55
|
||||||
|
msgctxt "input"
|
||||||
|
msgid "Language"
|
||||||
|
msgstr "Idioma"
|
||||||
|
|
||||||
|
#: web/templates/profile.gohtml:65
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Save changes"
|
||||||
|
msgstr "Guardar los cambios"
|
||||||
|
|
||||||
|
#: web/templates/layout.gohtml:17
|
||||||
msgid "Skip to main content"
|
msgid "Skip to main content"
|
||||||
msgstr "Saltar al contenido principal"
|
msgstr "Saltar al contenido principal"
|
||||||
|
|
||||||
#: web/templates/layout.gohtml:20
|
#: web/templates/layout.gohtml:24
|
||||||
|
msgctxt "title"
|
||||||
|
msgid "User Menu"
|
||||||
|
msgstr "Menú de usuario"
|
||||||
|
|
||||||
|
#: web/templates/layout.gohtml:32
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr "Salir"
|
msgstr "Salir"
|
||||||
|
|
||||||
#: pkg/app/login.go:56
|
#: pkg/app/login.go:56 pkg/app/user.go:170
|
||||||
msgid "Email can not be empty."
|
msgid "Email can not be empty."
|
||||||
msgstr "No podéis dejar el correo-e en blanco."
|
msgstr "No podéis dejar el correo-e en blanco."
|
||||||
|
|
||||||
#: pkg/app/login.go:57
|
#: pkg/app/login.go:57 pkg/app/user.go:171
|
||||||
msgid "This email is not valid. It should be like name@domain.com."
|
msgid "This email is not valid. It should be like name@domain.com."
|
||||||
msgstr "Este correo-e no es válido. Tiene que ser parecido a nombre@dominio.com."
|
msgstr "Este correo-e no es válido. Tiene que ser parecido a nombre@dominio.com."
|
||||||
|
|
||||||
|
@ -64,10 +95,27 @@ msgstr "Este correo-e no es válido. Tiene que ser parecido a nombre@dominio.com
|
||||||
msgid "Password can not be empty."
|
msgid "Password can not be empty."
|
||||||
msgstr "No podéis dejar la contraseña en blanco."
|
msgstr "No podéis dejar la contraseña en blanco."
|
||||||
|
|
||||||
#: pkg/app/login.go:82
|
#: pkg/app/login.go:86
|
||||||
msgid "Invalid user or password."
|
msgid "Invalid user or password."
|
||||||
msgstr "Usuario o contraseña incorrectos."
|
msgstr "Usuario o contraseña incorrectos."
|
||||||
|
|
||||||
|
#: pkg/app/user.go:135
|
||||||
|
msgctxt "language option"
|
||||||
|
msgid "Automatic"
|
||||||
|
msgstr "Automático"
|
||||||
|
|
||||||
|
#: pkg/app/user.go:173
|
||||||
|
msgid "Name can not be empty."
|
||||||
|
msgstr "No podéis dejar el nombre en blanco."
|
||||||
|
|
||||||
|
#: pkg/app/user.go:174
|
||||||
|
msgid "Confirmation does not match password."
|
||||||
|
msgstr "La confirmación no se corresponde con la contraseña."
|
||||||
|
|
||||||
|
#: pkg/app/user.go:175
|
||||||
|
msgid "Selected language is not valid."
|
||||||
|
msgstr "El idioma escogido no es válido."
|
||||||
|
|
||||||
#: pkg/auth/user.go:39
|
#: pkg/auth/user.go:39
|
||||||
msgid "Cross-site request forgery detected."
|
msgid "Cross-site request forgery detected."
|
||||||
msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados."
|
msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados."
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12.0009 17C15.6633 17 18.8659 18.5751 20.608 20.9247L18.766 21.796C17.3482 20.1157 14.8483 19 12.0009 19C9.15346 19 6.6535 20.1157 5.23577 21.796L3.39453 20.9238C5.13673 18.5747 8.33894 17 12.0009 17ZM12.0009 2C14.7623 2 17.0009 4.23858 17.0009 7V10C17.0009 12.6888 14.8786 14.8818 12.2178 14.9954L12.0009 15C9.23945 15 7.00087 12.7614 7.00087 10V7C7.00087 4.31125 9.12318 2.11818 11.784 2.00462L12.0009 2ZM12.0009 4C10.4032 4 9.09721 5.24892 9.00596 6.82373L9.00087 7V10C9.00087 11.6569 10.344 13 12.0009 13C13.5986 13 14.9045 11.7511 14.9958 10.1763L15.0009 10V7C15.0009 5.34315 13.6577 4 12.0009 4Z"/></svg>
|
After Width: | Height: | Size: 681 B |
|
@ -0,0 +1,17 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
{{ define "error-attrs" }}{{ if .Error }}aria-invalid="true" aria-errormessage="{{ .Name }}-error"{{ end }}{{ end }}
|
||||||
|
|
||||||
|
{{ define "error-message" -}}
|
||||||
|
{{ if .Error -}}
|
||||||
|
<span id="{{ .Name }}-error" class="error">{{ .Error }}</span><br>
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "list-options" -}}
|
||||||
|
{{- range .Options }}
|
||||||
|
<option value="{{ .Value }}" {{ if $.IsSelected .Value }} selected="selected"{{ end }}>{{ .Label }}</option>
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
<title>{{ template "title" . }} — Camper</title>
|
||||||
|
{{ template "content" . }}
|
|
@ -17,8 +17,23 @@
|
||||||
<a href="#main">{{( gettext "Skip to main content" )}}</a>
|
<a href="#main">{{( gettext "Skip to main content" )}}</a>
|
||||||
<h1>camper _ws</h1>
|
<h1>camper _ws</h1>
|
||||||
{{ if isLoggedIn -}}
|
{{ if isLoggedIn -}}
|
||||||
<button data-hx-delete="/me/session" data-hx-headers='{ {{ CSRFHeader }} }'
|
<nav>
|
||||||
>{{( pgettext "Logout" "action" )}}</button>
|
<details>
|
||||||
|
<summary>
|
||||||
|
<img src="/static/default_avatar.svg" alt="{{( gettext "Avatar" )}}" width="70" height="70">
|
||||||
|
<span>{{( pgettext "User Menu" "title" )}}</span>
|
||||||
|
</summary>
|
||||||
|
<ul role="menu">
|
||||||
|
<li role="presentation" class="icon_profile">
|
||||||
|
<a role="menuitem" href="/me">{{( pgettext "Profile" "title" )}}</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="icon_logout">
|
||||||
|
<button role="menuitem" data-hx-delete="/me/session" data-hx-headers='{ {{ CSRFHeader }} }'
|
||||||
|
>{{( pgettext "Logout" "action" )}}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</nav>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</header>
|
</header>
|
||||||
<main id="main">
|
<main id="main">
|
||||||
|
@ -26,10 +41,3 @@
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
{{ define "error-attrs" }}{{ if .Error }}aria-invalid="true" aria-errormessage="{{ .Name }}-error"{{ end }}{{ end }}
|
|
||||||
{{ define "error-message" -}}
|
|
||||||
{{ if .Error -}}
|
|
||||||
<span id="{{ .Name }}-error" class="error">{{ .Error }}</span><br>
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
{{ define "title" -}}
|
||||||
|
{{( pgettext "Profile" "title" )}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "content" -}}
|
||||||
|
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/app.profileForm*/ -}}
|
||||||
|
<form data-hx-put="/me">
|
||||||
|
<h2>{{( pgettext "Profile" "title" )}}</h2>
|
||||||
|
{{ CSRFInput }}
|
||||||
|
<fieldset>
|
||||||
|
{{ with .Name -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Name" "input" )}}<br>
|
||||||
|
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
|
||||||
|
autocomplete="name" required
|
||||||
|
{{ template "error-attrs" . }}><br>
|
||||||
|
</label>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
{{- end }}
|
||||||
|
{{ with .Email -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Email" "input" )}}<br>
|
||||||
|
<input type="email" name="{{ .Name }}" value="{{ .Val }}"
|
||||||
|
autocomplete="username" required
|
||||||
|
{{ template "error-attrs" . }}><br>
|
||||||
|
</label>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
{{- end }}
|
||||||
|
<fieldset>
|
||||||
|
{{ with .Password -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Password" "input" )}}<br>
|
||||||
|
<input type="password" name="{{ .Name }}" value="{{ .Val }}"
|
||||||
|
autocomplete="new-password"
|
||||||
|
{{ template "error-attrs" . }}><br>
|
||||||
|
</label>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
{{- end }}
|
||||||
|
{{ with .PasswordConfirm -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Password Confirmation" "input" )}}<br>
|
||||||
|
<input type="password" name="{{ .Name }}" value="{{ .Val }}"
|
||||||
|
autocomplete="new-password"
|
||||||
|
{{ template "error-attrs" . }}><br>
|
||||||
|
</label>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
{{- end }}
|
||||||
|
</fieldset>
|
||||||
|
{{ with .Language -}}
|
||||||
|
<label>
|
||||||
|
{{( pgettext "Language" "input" )}}<br>
|
||||||
|
<select name="{{ .Name }}"
|
||||||
|
required
|
||||||
|
{{ template "error-attrs" . }}>{{ template "list-options" . }}
|
||||||
|
</select><br>
|
||||||
|
</label>
|
||||||
|
{{ template "error-message" . }}
|
||||||
|
{{- end }}
|
||||||
|
</fieldset>
|
||||||
|
<footer>
|
||||||
|
<button type="submit">{{( pgettext "Save changes" "action" )}}</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
{{- end }}
|
Loading…
Reference in New Issue