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)
|
||||
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:
|
||||
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) {
|
||||
template.MustRender(w, user, "dashboard.gohtml", nil)
|
||||
func (h *App) serveDashboard(w http.ResponseWriter, r *http.Request, user *auth.User) {
|
||||
template.MustRender(w, r, user, "dashboard.gohtml", nil)
|
||||
}
|
||||
|
|
|
@ -60,10 +60,14 @@ func (f *loginForm) Valid(l *locale.Locale) bool {
|
|||
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.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) {
|
||||
|
@ -84,7 +88,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request, user *auth.User, conn *
|
|||
} else {
|
||||
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) {
|
||||
|
|
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"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/template"
|
||||
)
|
||||
|
||||
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:
|
||||
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:
|
||||
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
|
||||
}
|
||||
|
||||
func (input *Input) setError(err error) {
|
||||
input.Error = err
|
||||
}
|
||||
|
||||
func (input *Input) FillValue(r *http.Request) {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
field.Error = errors.New(v.l.Get(message))
|
||||
field.setError(errors.New(v.l.Get(message)))
|
||||
v.AllOK = false
|
||||
}
|
||||
return ok
|
||||
|
|
|
@ -54,6 +54,10 @@ func (l *Locale) Gettext(str string) string {
|
|||
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 {
|
||||
return str
|
||||
}
|
||||
|
|
|
@ -12,14 +12,18 @@ import (
|
|||
"net/http"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
)
|
||||
|
||||
func templateFile(name string) string {
|
||||
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"
|
||||
if httplib.IsHTMxRequest(r) {
|
||||
layout = "htmx.gohtml"
|
||||
}
|
||||
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))
|
||||
},
|
||||
})
|
||||
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)
|
||||
}
|
||||
if rw, ok := w.(http.ResponseWriter); ok {
|
||||
|
|
64
po/ca.po
64
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: camper\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"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||
|
@ -28,12 +28,12 @@ msgctxt "title"
|
|||
msgid "Login"
|
||||
msgstr "Entrada"
|
||||
|
||||
#: web/templates/login.gohtml:22
|
||||
#: web/templates/login.gohtml:22 web/templates/profile.gohtml:26
|
||||
msgctxt "input"
|
||||
msgid "Email"
|
||||
msgstr "Correu-e"
|
||||
|
||||
#: web/templates/login.gohtml:31
|
||||
#: web/templates/login.gohtml:31 web/templates/profile.gohtml:36
|
||||
msgctxt "input"
|
||||
msgid "Password"
|
||||
msgstr "Contrasenya"
|
||||
|
@ -43,20 +43,51 @@ msgctxt "action"
|
|||
msgid "Login"
|
||||
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"
|
||||
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"
|
||||
msgid "Logout"
|
||||
msgstr "Surt"
|
||||
|
||||
#: pkg/app/login.go:56
|
||||
#: pkg/app/login.go:56 pkg/app/user.go:170
|
||||
msgid "Email can not be empty."
|
||||
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."
|
||||
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."
|
||||
msgstr "No podeu deixar la contrasenya en blanc."
|
||||
|
||||
#: pkg/app/login.go:82
|
||||
#: pkg/app/login.go:86
|
||||
msgid "Invalid user or password."
|
||||
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
|
||||
msgid "Cross-site request forgery detected."
|
||||
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 ""
|
||||
"Project-Id-Version: camper\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"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||
|
@ -28,12 +28,12 @@ msgctxt "title"
|
|||
msgid "Login"
|
||||
msgstr "Entrada"
|
||||
|
||||
#: web/templates/login.gohtml:22
|
||||
#: web/templates/login.gohtml:22 web/templates/profile.gohtml:26
|
||||
msgctxt "input"
|
||||
msgid "Email"
|
||||
msgstr "Correo-e"
|
||||
|
||||
#: web/templates/login.gohtml:31
|
||||
#: web/templates/login.gohtml:31 web/templates/profile.gohtml:36
|
||||
msgctxt "input"
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
@ -43,20 +43,51 @@ msgctxt "action"
|
|||
msgid "Login"
|
||||
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"
|
||||
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"
|
||||
msgid "Logout"
|
||||
msgstr "Salir"
|
||||
|
||||
#: pkg/app/login.go:56
|
||||
#: pkg/app/login.go:56 pkg/app/user.go:170
|
||||
msgid "Email can not be empty."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
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
|
||||
msgid "Cross-site request forgery detected."
|
||||
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>
|
||||
<h1>camper _ws</h1>
|
||||
{{ if isLoggedIn -}}
|
||||
<button data-hx-delete="/me/session" data-hx-headers='{ {{ CSRFHeader }} }'
|
||||
<nav>
|
||||
<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 }}
|
||||
</header>
|
||||
<main id="main">
|
||||
|
@ -26,10 +41,3 @@
|
|||
</main>
|
||||
</body>
|
||||
</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