Compare commits

...

12 Commits

Author SHA1 Message Date
b1c653e7de Add redirect from logout to login 2023-01-31 15:47:29 +01:00
e0abf98bb1 Add custom function to get the current locale from templates
This is just to set the correct `lang` attribute on the HTML, so that
text readers can do its job and the `(optional)` suffix of labels gets
the correct ”translation”.
2023-01-31 15:45:51 +01:00
3b3c3bd302 Fix “translation” of ‘(opcional)’ for fields in Spanish and Catalan 2023-01-31 15:43:47 +01:00
e56e08a68f Tell IDE to shut up about an update without where of a single record 2023-01-31 15:41:05 +01:00
9f17f55547 Validate profile form and use templates for fields
Let’s start first with a non-fancy validation method with just if
conditionals instead of bringing yet another complicated library.  I
hope i do not regret it.

I wanted to move all the input field to a template because all that
gobbledygook with the .input div and repeating the label in the
placeholder was starting to annoy me.  Now with error messages was even
more concerning.

I did not know whether the label should be a part of the input fields
or something that the template should do.  At the end i decided that
it makes more sense to be part of the input field because in the error
messages i use that same label, thus the template does not have a say
in that, and, besides, it was just easier to write the template.

The same with the error messages: i’ve seen frameworks that have a map
with the field’s id/name to the error slice, but then it would be
a bit harder to write the template.

I added AddError functions instead of just using append inside the
validator function, and have a local variable for whether it all went
OK, because i was worried that i would leave out the `ok = false`
in some conditions.

I had started writing “constructors” functions for InputField and
SelectField, but then had to add other methods to change the required
field and who knows what else, and in the end it was easier to just
construct the field inline.
2023-01-31 15:40:12 +01:00
89256d5b4c Add nav link to dashboard 2023-01-31 13:29:56 +01:00
3117c9a268 Rename #profilemenu to #profile-menu, for consistency 2023-01-31 13:25:57 +01:00
873d36abab Ignore an Intellij’s warning for remixicon font 2023-01-31 13:24:26 +01:00
93ec8b74c0 Move nav’s padding into its links
Otherwise, the padding is white on hover and looks weird.
2023-01-31 13:21:10 +01:00
5fc92a5748 Remove unused Remixicon files
The symbols.svg files is for referencing from other SVG files with
xlink; the .glyph.json seems to be used for the search app; and the
.less file is useless to me because i do not use less.
2023-01-31 13:17:51 +01:00
4d452c5522 Fix a duplicate attribute in the _method hidden field 2023-01-31 13:07:55 +01:00
9aee33511a Move page titles to their respective templates
I have been thinking about that, and it does not make that much sense to
have the titles in the Go source anymore: most of them are static text
that i have to remember to set in the controller each time, and when
the time come i have to face a dynamic title i am sure i will manage
with only the template capabilities—worst comes worst, i can always
define a function.

On the other hand, there is no way i can define a template without its
title and i know that everytime that template is used, no matter what
controller rendered it, it will always have that title.
2023-01-31 13:07:17 +01:00
23 changed files with 490 additions and 16243 deletions

View File

@ -83,7 +83,6 @@ type Tax struct {
} }
type TaxDetailsPage struct { type TaxDetailsPage struct {
Title string
BusinessName string BusinessName string
VATIN string VATIN string
TradeName string TradeName string
@ -104,9 +103,7 @@ type TaxDetailsPage struct {
func CompanyTaxDetailsHandler() http.Handler { func CompanyTaxDetailsHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
locale := getLocale(r) locale := getLocale(r)
page := &TaxDetailsPage{ page := &TaxDetailsPage{}
Title: pgettext("title", "Tax Details", locale),
}
company := mustGetCompany(r) company := mustGetCompany(r)
conn := getConn(r) conn := getConn(r)
if r.Method == "POST" { if r.Method == "POST" {

View File

@ -12,7 +12,6 @@ type ContactEntry struct {
} }
type ContactsIndexPage struct { type ContactsIndexPage struct {
Title string
Contacts []*ContactEntry Contacts []*ContactEntry
} }
@ -38,9 +37,7 @@ func ContactsHandler() http.Handler {
conn.MustExec(r.Context(), "insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, province, city, postal_code, country_code) values ($1, $2, ($12 || $3)::vatin, $4, parse_packed_phone_number($5, $12), $6, $7, $8, $9, $10, $11, $12)", company.Id, page.BusinessName, page.VATIN, page.TradeName, page.Phone, page.Email, page.Web, page.Address, page.City, page.Province, page.PostalCode, page.CountryCode) conn.MustExec(r.Context(), "insert into contact (company_id, business_name, vatin, trade_name, phone, email, web, address, province, city, postal_code, country_code) values ($1, $2, ($12 || $3)::vatin, $4, parse_packed_phone_number($5, $12), $6, $7, $8, $9, $10, $11, $12)", company.Id, page.BusinessName, page.VATIN, page.TradeName, page.Phone, page.Email, page.Web, page.Address, page.City, page.Province, page.PostalCode, page.CountryCode)
http.Redirect(w, r, "/company/"+company.Slug+"/contacts", http.StatusSeeOther) http.Redirect(w, r, "/company/"+company.Slug+"/contacts", http.StatusSeeOther)
} else { } else {
locale := getLocale(r)
page := &ContactsIndexPage{ page := &ContactsIndexPage{
Title: pgettext("title", "Customers", locale),
Contacts: mustGetContactEntries(r.Context(), conn, company), Contacts: mustGetContactEntries(r.Context(), conn, company),
} }
mustRenderAppTemplate(w, r, "contacts-index.gohtml", page) mustRenderAppTemplate(w, r, "contacts-index.gohtml", page)
@ -72,7 +69,6 @@ func mustGetContactEntries(ctx context.Context, conn *Conn, company *Company) []
} }
type NewContactPage struct { type NewContactPage struct {
Title string
BusinessName string BusinessName string
VATIN string VATIN string
TradeName string TradeName string
@ -92,7 +88,6 @@ func NewContactHandler() http.Handler {
locale := getLocale(r) locale := getLocale(r)
conn := getConn(r) conn := getConn(r)
page := &NewContactPage{ page := &NewContactPage{
Title: pgettext("title", "New Contact", locale),
CountryCode: "ES", CountryCode: "ES",
Countries: mustGetCountryOptions(r.Context(), conn, locale), Countries: mustGetCountryOptions(r.Context(), conn, locale),
} }

73
pkg/form.go Normal file
View File

@ -0,0 +1,73 @@
package pkg
import (
"context"
"net/mail"
)
type InputField struct {
Name string
Label string
Type string
Value string
Required bool
Errors []error
}
func (field *InputField) Equals(other *InputField) bool {
return field.Value == other.Value
}
func (field *InputField) IsEmpty() bool {
return field.Value == ""
}
func (field *InputField) HasValidEmail() bool {
_, err := mail.ParseAddress(field.Value)
return err == nil
}
type SelectOption struct {
Value string
Label string
}
type SelectField struct {
Name string
Label string
Selected string
Options []*SelectOption
Errors []error
}
func (field *SelectField) HasValidOption() bool {
for _, option := range field.Options {
if option.Value == field.Selected {
return true
}
}
return false
}
func MustGetOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*SelectOption {
rows, err := conn.Query(ctx, sql, args...)
if err != nil {
panic(err)
}
defer rows.Close()
var options []*SelectOption
for rows.Next() {
option := &SelectOption{}
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
}

View File

@ -66,6 +66,10 @@ func pgettext(context string, str string, locale *Locale) string {
return locale.GetC(str, context) return locale.GetC(str, context)
} }
func gettext(str string, locale *Locale) string {
return locale.Get(str)
}
func mustGetAvailableLanguages(db *Db) []language.Tag { func mustGetAvailableLanguages(db *Db) []language.Tag {
rows, err := db.Query(context.Background(), "select lang_tag from language where selectable") rows, err := db.Query(context.Background(), "select lang_tag from language where selectable")
if err != nil { if err != nil {

View File

@ -64,6 +64,7 @@ func LogoutHandler() http.Handler {
conn := getConn(r) conn := getConn(r)
conn.MustExec(r.Context(), "select logout()") conn.MustExec(r.Context(), "select logout()")
http.SetCookie(w, createSessionCookie("", -24*time.Hour)) http.SetCookie(w, createSessionCookie("", -24*time.Hour))
http.Redirect(w, r, "/login", http.StatusSeeOther)
}) })
} }

View File

@ -2,7 +2,9 @@ package pkg
import ( import (
"context" "context"
"errors"
"net/http" "net/http"
"strings"
) )
type LanguageOption struct { type LanguageOption struct {
@ -10,14 +12,88 @@ type LanguageOption struct {
Name string Name string
} }
type ProfilePage struct { type profileForm struct {
Title string Name *InputField
Name string Email *InputField
Email string Password *InputField
Password string PasswordConfirm *InputField
PasswordConfirm string Language *SelectField
Language string Valid bool
Languages []LanguageOption }
func newProfileForm(ctx context.Context, conn *Conn, locale *Locale) *profileForm {
automaticOption := pgettext("language option", "Automatic", locale)
languages := MustGetOptions(ctx, conn, "select 'und', $1 union all select lang_tag, endonym from language where selectable", automaticOption)
return &profileForm{
Name: &InputField{
Name: "name",
Label: pgettext("input", "User name", locale),
Type: "text",
Required: true,
},
Email: &InputField{
Name: "email",
Label: pgettext("input", "Email", locale),
Type: "email",
Required: true,
},
Password: &InputField{
Name: "password",
Label: pgettext("input", "Password", locale),
Type: "password",
},
PasswordConfirm: &InputField{
Name: "password_confirm",
Label: pgettext("input", "Password Confirmation", locale),
Type: "password",
},
Language: &SelectField{
Name: "language",
Label: pgettext("input", "Language", locale),
Options: languages,
},
}
}
func (form *profileForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
form.Email.Value = strings.TrimSpace(r.FormValue("email"))
form.Name.Value = strings.TrimSpace(r.FormValue("name"))
form.Password.Value = strings.TrimSpace(r.FormValue("password"))
form.PasswordConfirm.Value = strings.TrimSpace(r.FormValue("password_confirm"))
form.Language.Selected = r.FormValue("language")
return nil
}
func (form *profileForm) Validate(locale *Locale) bool {
form.Valid = true
if form.Email.IsEmpty() {
form.AppendInputError(form.Email, errors.New(gettext("Email can not be empty.", locale)))
} else if !form.Email.HasValidEmail() {
form.AppendInputError(form.Email, errors.New(gettext("This value is not a valid email. It should be like name@domain.com.", locale)))
}
if form.Name.IsEmpty() {
form.AppendInputError(form.Name, errors.New(gettext("Name can not be empty.", locale)))
}
if !form.PasswordConfirm.Equals(form.Password) {
form.AppendInputError(form.PasswordConfirm, errors.New(gettext("Confirmation does not match password.", locale)))
}
if !form.Language.HasValidOption() {
form.AppendSelectError(form.Language, errors.New(gettext("Selected language is not valid.", locale)))
}
return form.Valid
}
func (form *profileForm) AppendInputError(field *InputField, err error) {
field.Errors = append(field.Errors, err)
form.Valid = false
}
func (form *profileForm) AppendSelectError(field *SelectField, err error) {
field.Errors = append(field.Errors, err)
form.Valid = false
} }
func ProfileHandler() http.Handler { func ProfileHandler() http.Handler {
@ -25,54 +101,29 @@ func ProfileHandler() http.Handler {
user := getUser(r) user := getUser(r)
conn := getConn(r) conn := getConn(r)
locale := getLocale(r) locale := getLocale(r)
page := ProfilePage{ form := newProfileForm(r.Context(), conn, locale)
Title: pgettext("title", "User Settings", locale),
Email: user.Email,
Language: user.Language.String(),
}
if r.Method == "POST" { if r.Method == "POST" {
r.ParseForm() if err := form.Parse(r); err != nil {
page.Email = r.FormValue("email") http.Error(w, err.Error(), http.StatusBadRequest)
page.Name = r.FormValue("name") return
page.Password = r.FormValue("password")
page.PasswordConfirm = r.FormValue("password_confirm")
page.Language = r.FormValue("language")
cookie := conn.MustGetText(r.Context(), "", "update user_profile set name = $1, email = $2, lang_tag = $3 returning build_cookie()", page.Name, page.Email, page.Language)
setSessionCookie(w, cookie)
if page.Password != "" && page.Password == page.PasswordConfirm {
conn.MustExec(r.Context(), "select change_password($1)", page.Password)
} }
http.Redirect(w, r, "/profile", http.StatusSeeOther) if ok := form.Validate(locale); ok {
return //goland:noinspection SqlWithoutWhere
cookie := conn.MustGetText(r.Context(), "", "update user_profile set name = $1, email = $2, lang_tag = $3 returning build_cookie()", form.Name.Value, form.Email.Value, form.Language.Selected)
setSessionCookie(w, cookie)
if form.Password.Value != "" {
conn.MustExec(r.Context(), "select change_password($1)", form.Password.Value)
}
company := getCompany(r)
http.Redirect(w, r, "/company/"+company.Slug+"/profile", http.StatusSeeOther)
return
}
w.WriteHeader(http.StatusUnprocessableEntity)
} else { } else {
page.Languages = mustGetLanguageOptions(r.Context(), conn) form.Name.Value = conn.MustGetText(r.Context(), "", "select name from user_profile")
if err := conn.QueryRow(r.Context(), "select name from user_profile").Scan(&page.Name); err != nil { form.Email.Value = user.Email
panic(nil) form.Language.Selected = user.Language.String()
}
} }
mustRenderAppTemplate(w, r, "profile.gohtml", page) mustRenderAppTemplate(w, r, "profile.gohtml", form)
}) })
} }
func mustGetLanguageOptions(ctx context.Context, conn *Conn) []LanguageOption {
rows, err := conn.Query(ctx, "select lang_tag, endonym from language where selectable")
if err != nil {
panic(err)
}
defer rows.Close()
var langs []LanguageOption
for rows.Next() {
var lang LanguageOption
err = rows.Scan(&lang.Tag, &lang.Name)
if err != nil {
panic(err)
}
langs = append(langs, lang)
}
if rows.Err() != nil {
panic(rows.Err())
}
return langs
}

View File

@ -17,6 +17,9 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
t.Funcs(template.FuncMap{ t.Funcs(template.FuncMap{
"gettext": locale.Get, "gettext": locale.Get,
"pgettext": locale.GetC, "pgettext": locale.GetC,
"currentLocale": func() string {
return locale.Language.String()
},
"companyURI": func(uri string) string { "companyURI": func(uri string) string {
if company == nil { if company == nil {
return uri return uri
@ -24,7 +27,7 @@ func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename s
return "/company/" + company.Slug + uri return "/company/" + company.Slug + uri
}, },
}) })
if _, err := t.ParseFiles(templateFile(filename), templateFile(layout)); err != nil { if _, err := t.ParseFiles(templateFile(filename), templateFile(layout), templateFile("form.gohtml")); err != nil {
panic(err) panic(err)
} }
if err := t.ExecuteTemplate(wr, layout, data); err != nil { if err := t.ExecuteTemplate(wr, layout, data); err != nil {

155
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-01-30 16:46+0100\n" "POT-Creation-Date: 2023-01-31 15:17+0100\n"
"PO-Revision-Date: 2023-01-18 17:08+0100\n" "PO-Revision-Date: 2023-01-18 17:08+0100\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"
@ -17,10 +17,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: web/template/web.gohtml:6 web/template/login.gohtml:9 #: web/template/dashboard.gohtml:2
msgctxt "title" msgctxt "title"
msgid "Login" msgid "Dashboard"
msgstr "Entrada" msgstr "Tauler"
#: web/template/app.gohtml:20 #: web/template/app.gohtml:20
msgctxt "menu" msgctxt "menu"
@ -39,198 +39,233 @@ msgstr "Surt"
#: web/template/app.gohtml:42 #: web/template/app.gohtml:42
msgctxt "nav" msgctxt "nav"
msgid "Dashboard"
msgstr "Tauler"
#: web/template/app.gohtml:43
msgctxt "nav"
msgid "Customers" msgid "Customers"
msgstr "Clients" msgstr "Clients"
#: web/template/login.gohtml:5 #: web/template/login.gohtml:2 web/template/login.gohtml:13
msgctxt "title"
msgid "Login"
msgstr "Entrada"
#: web/template/login.gohtml:9
msgid "Invalid user or password" msgid "Invalid user or password"
msgstr "Nom dusuari o contrasenya incorrectes" msgstr "Nom dusuari o contrasenya incorrectes"
#: web/template/login.gohtml:13 web/template/profile.gohtml:15 #: web/template/login.gohtml:17 web/template/tax-details.gohtml:27
#: web/template/tax-details.gohtml:23 web/template/contacts-new.gohtml:23 #: web/template/contacts-new.gohtml:27 pkg/profile.go:37
msgctxt "input" msgctxt "input"
msgid "Email" msgid "Email"
msgstr "Correu-e" msgstr "Correu-e"
#: web/template/login.gohtml:18 web/template/profile.gohtml:23 #: web/template/login.gohtml:22 pkg/profile.go:43
msgctxt "input" msgctxt "input"
msgid "Password" msgid "Password"
msgstr "Contrasenya" msgstr "Contrasenya"
#: web/template/login.gohtml:21 #: web/template/login.gohtml:25
msgctxt "action" msgctxt "action"
msgid "Login" msgid "Login"
msgstr "Entra" msgstr "Entra"
#: web/template/profile.gohtml:3 pkg/profile.go:29 #: web/template/profile.gohtml:2 web/template/profile.gohtml:7
msgctxt "title" msgctxt "title"
msgid "User Settings" msgid "User Settings"
msgstr "Configuració usuari" msgstr "Configuració usuari"
#: web/template/profile.gohtml:6 #: web/template/profile.gohtml:10
msgctxt "title" msgctxt "title"
msgid "User Access Data" msgid "User Access Data"
msgstr "Dades accés usuari" msgstr "Dades accés usuari"
#: web/template/profile.gohtml:10 #: web/template/profile.gohtml:16
msgctxt "input"
msgid "User name"
msgstr "Nom dusuari"
#: web/template/profile.gohtml:19
msgctxt "title" msgctxt "title"
msgid "Password Change" msgid "Password Change"
msgstr "Canvi contrasenya" msgstr "Canvi contrasenya"
#: web/template/profile.gohtml:28 #: web/template/profile.gohtml:23
msgctxt "input" msgctxt "title"
msgid "Password Confirmation"
msgstr "Confirmació contrasenya"
#: web/template/profile.gohtml:33
msgctxt "input"
msgid "Language" msgid "Language"
msgstr "Idioma" msgstr "Idioma"
#: web/template/profile.gohtml:36 #: web/template/profile.gohtml:27 web/template/tax-details.gohtml:133
msgctxt "language option"
msgid "Automatic"
msgstr "Automàtic"
#: web/template/profile.gohtml:42 web/template/tax-details.gohtml:127
msgctxt "action" msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Desa canvis" msgstr "Desa canvis"
#: web/template/contacts-index.gohtml:2 web/template/contacts-new.gohtml:56 #: web/template/contacts-index.gohtml:2
msgctxt "title"
msgid "Customers"
msgstr "Clients"
#: web/template/contacts-index.gohtml:6 web/template/contacts-new.gohtml:60
msgctxt "action" msgctxt "action"
msgid "New contact" msgid "New contact"
msgstr "Nou contacte" msgstr "Nou contacte"
#: web/template/contacts-index.gohtml:7 #: web/template/contacts-index.gohtml:11
msgctxt "contact" msgctxt "contact"
msgid "All" msgid "All"
msgstr "Tots" msgstr "Tots"
#: web/template/contacts-index.gohtml:8 #: web/template/contacts-index.gohtml:12
msgctxt "title" msgctxt "title"
msgid "Customer" msgid "Customer"
msgstr "Client" msgstr "Client"
#: web/template/contacts-index.gohtml:9 #: web/template/contacts-index.gohtml:13
msgctxt "title" msgctxt "title"
msgid "Email" msgid "Email"
msgstr "Correu-e" msgstr "Correu-e"
#: web/template/contacts-index.gohtml:10 #: web/template/contacts-index.gohtml:14
msgctxt "title" msgctxt "title"
msgid "Phone" msgid "Phone"
msgstr "Telèfon" msgstr "Telèfon"
#: web/template/contacts-index.gohtml:25 #: web/template/contacts-index.gohtml:29
msgid "No customers added yet." msgid "No customers added yet."
msgstr "No hi ha cap client." msgstr "No hi ha cap client."
#: web/template/tax-details.gohtml:3 pkg/company.go:108 #: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:7
msgctxt "title" msgctxt "title"
msgid "Tax Details" msgid "Tax Details"
msgstr "Configuració fiscal" msgstr "Configuració fiscal"
#: web/template/tax-details.gohtml:7 web/template/contacts-new.gohtml:7 #: web/template/tax-details.gohtml:11 web/template/contacts-new.gohtml:11
msgctxt "input" msgctxt "input"
msgid "Business name" msgid "Business name"
msgstr "Nom i cognom" msgstr "Nom i cognom"
#: web/template/tax-details.gohtml:11 web/template/contacts-new.gohtml:11 #: web/template/tax-details.gohtml:15 web/template/contacts-new.gohtml:15
msgctxt "input" msgctxt "input"
msgid "VAT number" msgid "VAT number"
msgstr "DNI / NIF" msgstr "DNI / NIF"
#: web/template/tax-details.gohtml:15 web/template/contacts-new.gohtml:15 #: web/template/tax-details.gohtml:19 web/template/contacts-new.gohtml:19
msgctxt "input" msgctxt "input"
msgid "Trade name" msgid "Trade name"
msgstr "Nom comercial" msgstr "Nom comercial"
#: web/template/tax-details.gohtml:19 web/template/contacts-new.gohtml:19 #: web/template/tax-details.gohtml:23 web/template/contacts-new.gohtml:23
msgctxt "input" msgctxt "input"
msgid "Phone" msgid "Phone"
msgstr "Telèfon" msgstr "Telèfon"
#: web/template/tax-details.gohtml:27 web/template/contacts-new.gohtml:27 #: web/template/tax-details.gohtml:31 web/template/contacts-new.gohtml:31
msgctxt "input" msgctxt "input"
msgid "Web" msgid "Web"
msgstr "Web" msgstr "Web"
#: web/template/tax-details.gohtml:31 web/template/contacts-new.gohtml:31 #: web/template/tax-details.gohtml:35 web/template/contacts-new.gohtml:35
msgctxt "input" msgctxt "input"
msgid "Address" msgid "Address"
msgstr "Adreça" msgstr "Adreça"
#: web/template/tax-details.gohtml:35 web/template/contacts-new.gohtml:35 #: web/template/tax-details.gohtml:39 web/template/contacts-new.gohtml:39
msgctxt "input" msgctxt "input"
msgid "City" msgid "City"
msgstr "Població" msgstr "Població"
#: web/template/tax-details.gohtml:39 web/template/contacts-new.gohtml:39 #: web/template/tax-details.gohtml:43 web/template/contacts-new.gohtml:43
msgctxt "input" msgctxt "input"
msgid "Province" msgid "Province"
msgstr "Província" msgstr "Província"
#: web/template/tax-details.gohtml:43 web/template/contacts-new.gohtml:43 #: web/template/tax-details.gohtml:47 web/template/contacts-new.gohtml:47
msgctxt "input" msgctxt "input"
msgid "Postal code" msgid "Postal code"
msgstr "Codi postal" msgstr "Codi postal"
#: web/template/tax-details.gohtml:52 web/template/contacts-new.gohtml:52 #: web/template/tax-details.gohtml:56 web/template/contacts-new.gohtml:56
msgctxt "input" msgctxt "input"
msgid "Country" msgid "Country"
msgstr "País" msgstr "País"
#: web/template/tax-details.gohtml:56 #: web/template/tax-details.gohtml:60
msgctxt "input" msgctxt "input"
msgid "Currency" msgid "Currency"
msgstr "Moneda" msgstr "Moneda"
#: web/template/tax-details.gohtml:74 #: web/template/tax-details.gohtml:78
msgctxt "title" msgctxt "title"
msgid "Tax Name" msgid "Tax Name"
msgstr "Nom import" msgstr "Nom import"
#: web/template/tax-details.gohtml:75 #: web/template/tax-details.gohtml:79
msgctxt "title" msgctxt "title"
msgid "Rate (%)" msgid "Rate (%)"
msgstr "Percentatge" msgstr "Percentatge"
#: web/template/tax-details.gohtml:96 #: web/template/tax-details.gohtml:100
msgid "No taxes added yet." msgid "No taxes added yet."
msgstr "No hi ha cap impost." msgstr "No hi ha cap impost."
#: web/template/tax-details.gohtml:102 #: web/template/tax-details.gohtml:106
msgctxt "title" msgctxt "title"
msgid "New Line" msgid "New Line"
msgstr "Nova línia" msgstr "Nova línia"
#: web/template/tax-details.gohtml:106 #: web/template/tax-details.gohtml:111
msgctxt "input" msgctxt "input"
msgid "Tax name" msgid "Tax name"
msgstr "Nom impost" msgstr "Nom impost"
#: web/template/tax-details.gohtml:112 #: web/template/tax-details.gohtml:118
msgctxt "input" msgctxt "input"
msgid "Rate (%)" msgid "Rate (%)"
msgstr "Percentatge" msgstr "Percentatge"
#: web/template/tax-details.gohtml:119 #: web/template/tax-details.gohtml:125
msgctxt "action" msgctxt "action"
msgid "Add new tax" msgid "Add new tax"
msgstr "Afegeix nou impost" msgstr "Afegeix nou impost"
#: web/template/contacts-new.gohtml:3 pkg/contacts.go:95 #: web/template/contacts-new.gohtml:2 web/template/contacts-new.gohtml:7
msgctxt "title" msgctxt "title"
msgid "New Contact" msgid "New Contact"
msgstr "Nou contacte" msgstr "Nou contacte"
#: pkg/contacts.go:43 #: pkg/profile.go:26
msgctxt "title" msgctxt "language option"
msgid "Customers" msgid "Automatic"
msgstr "Clients" msgstr "Automàtic"
#: pkg/profile.go:31
msgctxt "input"
msgid "User name"
msgstr "Nom dusuari"
#: pkg/profile.go:48
msgctxt "input"
msgid "Password Confirmation"
msgstr "Confirmació contrasenya"
#: pkg/profile.go:53
msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: pkg/profile.go:74
msgid "Email can not be empty."
msgstr "No podeu deixar el correu-e en blanc."
#: pkg/profile.go:76
msgid "This value is not a valid email. It should be like name@domain.com."
msgstr "Aquest valor no és un correu-e vàlid. Hauria de ser similar a nom@domini.cat."
#: pkg/profile.go:79
msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc."
#: pkg/profile.go:82
msgid "Confirmation does not match password."
msgstr "La confirmació no és igual a la contrasenya."
#: pkg/profile.go:85
msgid "Selected language is not valid."
msgstr "Heu seleccionat un idioma que no és vàlid."

155
po/es.po
View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: numerus\n" "Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-01-30 16:46+0100\n" "POT-Creation-Date: 2023-01-31 15:17+0100\n"
"PO-Revision-Date: 2023-01-18 17:45+0100\n" "PO-Revision-Date: 2023-01-18 17:45+0100\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"
@ -17,10 +17,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: web/template/web.gohtml:6 web/template/login.gohtml:9 #: web/template/dashboard.gohtml:2
msgctxt "title" msgctxt "title"
msgid "Login" msgid "Dashboard"
msgstr "Entrada" msgstr "Panel"
#: web/template/app.gohtml:20 #: web/template/app.gohtml:20
msgctxt "menu" msgctxt "menu"
@ -39,198 +39,233 @@ msgstr "Salir"
#: web/template/app.gohtml:42 #: web/template/app.gohtml:42
msgctxt "nav" msgctxt "nav"
msgid "Dashboard"
msgstr "Panel"
#: web/template/app.gohtml:43
msgctxt "nav"
msgid "Customers" msgid "Customers"
msgstr "Clientes" msgstr "Clientes"
#: web/template/login.gohtml:5 #: web/template/login.gohtml:2 web/template/login.gohtml:13
msgctxt "title"
msgid "Login"
msgstr "Entrada"
#: web/template/login.gohtml:9
msgid "Invalid user or password" msgid "Invalid user or password"
msgstr "Nombre de usuario o contraseña inválido" msgstr "Nombre de usuario o contraseña inválido"
#: web/template/login.gohtml:13 web/template/profile.gohtml:15 #: web/template/login.gohtml:17 web/template/tax-details.gohtml:27
#: web/template/tax-details.gohtml:23 web/template/contacts-new.gohtml:23 #: web/template/contacts-new.gohtml:27 pkg/profile.go:37
msgctxt "input" msgctxt "input"
msgid "Email" msgid "Email"
msgstr "Correo-e" msgstr "Correo-e"
#: web/template/login.gohtml:18 web/template/profile.gohtml:23 #: web/template/login.gohtml:22 pkg/profile.go:43
msgctxt "input" msgctxt "input"
msgid "Password" msgid "Password"
msgstr "Contraseña" msgstr "Contraseña"
#: web/template/login.gohtml:21 #: web/template/login.gohtml:25
msgctxt "action" msgctxt "action"
msgid "Login" msgid "Login"
msgstr "Entrar" msgstr "Entrar"
#: web/template/profile.gohtml:3 pkg/profile.go:29 #: web/template/profile.gohtml:2 web/template/profile.gohtml:7
msgctxt "title" msgctxt "title"
msgid "User Settings" msgid "User Settings"
msgstr "Configuración usuario" msgstr "Configuración usuario"
#: web/template/profile.gohtml:6 #: web/template/profile.gohtml:10
msgctxt "title" msgctxt "title"
msgid "User Access Data" msgid "User Access Data"
msgstr "Datos acceso usuario" msgstr "Datos acceso usuario"
#: web/template/profile.gohtml:10 #: web/template/profile.gohtml:16
msgctxt "input"
msgid "User name"
msgstr "Nombre de usuario"
#: web/template/profile.gohtml:19
msgctxt "title" msgctxt "title"
msgid "Password Change" msgid "Password Change"
msgstr "Cambio de contraseña" msgstr "Cambio de contraseña"
#: web/template/profile.gohtml:28 #: web/template/profile.gohtml:23
msgctxt "input" msgctxt "title"
msgid "Password Confirmation"
msgstr "Confirmación contrasenya"
#: web/template/profile.gohtml:33
msgctxt "input"
msgid "Language" msgid "Language"
msgstr "Idioma" msgstr "Idioma"
#: web/template/profile.gohtml:36 #: web/template/profile.gohtml:27 web/template/tax-details.gohtml:133
msgctxt "language option"
msgid "Automatic"
msgstr "Automático"
#: web/template/profile.gohtml:42 web/template/tax-details.gohtml:127
msgctxt "action" msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Guardar cambios" msgstr "Guardar cambios"
#: web/template/contacts-index.gohtml:2 web/template/contacts-new.gohtml:56 #: web/template/contacts-index.gohtml:2
msgctxt "title"
msgid "Customers"
msgstr "Clientes"
#: web/template/contacts-index.gohtml:6 web/template/contacts-new.gohtml:60
msgctxt "action" msgctxt "action"
msgid "New contact" msgid "New contact"
msgstr "Nuevo contacto" msgstr "Nuevo contacto"
#: web/template/contacts-index.gohtml:7 #: web/template/contacts-index.gohtml:11
msgctxt "contact" msgctxt "contact"
msgid "All" msgid "All"
msgstr "Todos" msgstr "Todos"
#: web/template/contacts-index.gohtml:8 #: web/template/contacts-index.gohtml:12
msgctxt "title" msgctxt "title"
msgid "Customer" msgid "Customer"
msgstr "Cliente" msgstr "Cliente"
#: web/template/contacts-index.gohtml:9 #: web/template/contacts-index.gohtml:13
msgctxt "title" msgctxt "title"
msgid "Email" msgid "Email"
msgstr "Correo-e" msgstr "Correo-e"
#: web/template/contacts-index.gohtml:10 #: web/template/contacts-index.gohtml:14
msgctxt "title" msgctxt "title"
msgid "Phone" msgid "Phone"
msgstr "Teléfono" msgstr "Teléfono"
#: web/template/contacts-index.gohtml:25 #: web/template/contacts-index.gohtml:29
msgid "No customers added yet." msgid "No customers added yet."
msgstr "No hay clientes." msgstr "No hay clientes."
#: web/template/tax-details.gohtml:3 pkg/company.go:108 #: web/template/tax-details.gohtml:2 web/template/tax-details.gohtml:7
msgctxt "title" msgctxt "title"
msgid "Tax Details" msgid "Tax Details"
msgstr "Configuración fiscal" msgstr "Configuración fiscal"
#: web/template/tax-details.gohtml:7 web/template/contacts-new.gohtml:7 #: web/template/tax-details.gohtml:11 web/template/contacts-new.gohtml:11
msgctxt "input" msgctxt "input"
msgid "Business name" msgid "Business name"
msgstr "Nombre y apellidos" msgstr "Nombre y apellidos"
#: web/template/tax-details.gohtml:11 web/template/contacts-new.gohtml:11 #: web/template/tax-details.gohtml:15 web/template/contacts-new.gohtml:15
msgctxt "input" msgctxt "input"
msgid "VAT number" msgid "VAT number"
msgstr "DNI / NIF" msgstr "DNI / NIF"
#: web/template/tax-details.gohtml:15 web/template/contacts-new.gohtml:15 #: web/template/tax-details.gohtml:19 web/template/contacts-new.gohtml:19
msgctxt "input" msgctxt "input"
msgid "Trade name" msgid "Trade name"
msgstr "Nombre comercial" msgstr "Nombre comercial"
#: web/template/tax-details.gohtml:19 web/template/contacts-new.gohtml:19 #: web/template/tax-details.gohtml:23 web/template/contacts-new.gohtml:23
msgctxt "input" msgctxt "input"
msgid "Phone" msgid "Phone"
msgstr "Teléfono" msgstr "Teléfono"
#: web/template/tax-details.gohtml:27 web/template/contacts-new.gohtml:27 #: web/template/tax-details.gohtml:31 web/template/contacts-new.gohtml:31
msgctxt "input" msgctxt "input"
msgid "Web" msgid "Web"
msgstr "Web" msgstr "Web"
#: web/template/tax-details.gohtml:31 web/template/contacts-new.gohtml:31 #: web/template/tax-details.gohtml:35 web/template/contacts-new.gohtml:35
msgctxt "input" msgctxt "input"
msgid "Address" msgid "Address"
msgstr "Dirección" msgstr "Dirección"
#: web/template/tax-details.gohtml:35 web/template/contacts-new.gohtml:35 #: web/template/tax-details.gohtml:39 web/template/contacts-new.gohtml:39
msgctxt "input" msgctxt "input"
msgid "City" msgid "City"
msgstr "Población" msgstr "Población"
#: web/template/tax-details.gohtml:39 web/template/contacts-new.gohtml:39 #: web/template/tax-details.gohtml:43 web/template/contacts-new.gohtml:43
msgctxt "input" msgctxt "input"
msgid "Province" msgid "Province"
msgstr "Provincia" msgstr "Provincia"
#: web/template/tax-details.gohtml:43 web/template/contacts-new.gohtml:43 #: web/template/tax-details.gohtml:47 web/template/contacts-new.gohtml:47
msgctxt "input" msgctxt "input"
msgid "Postal code" msgid "Postal code"
msgstr "Código postal" msgstr "Código postal"
#: web/template/tax-details.gohtml:52 web/template/contacts-new.gohtml:52 #: web/template/tax-details.gohtml:56 web/template/contacts-new.gohtml:56
msgctxt "input" msgctxt "input"
msgid "Country" msgid "Country"
msgstr "País" msgstr "País"
#: web/template/tax-details.gohtml:56 #: web/template/tax-details.gohtml:60
msgctxt "input" msgctxt "input"
msgid "Currency" msgid "Currency"
msgstr "Moneda" msgstr "Moneda"
#: web/template/tax-details.gohtml:74 #: web/template/tax-details.gohtml:78
msgctxt "title" msgctxt "title"
msgid "Tax Name" msgid "Tax Name"
msgstr "Nombre impuesto" msgstr "Nombre impuesto"
#: web/template/tax-details.gohtml:75 #: web/template/tax-details.gohtml:79
msgctxt "title" msgctxt "title"
msgid "Rate (%)" msgid "Rate (%)"
msgstr "Porcentage" msgstr "Porcentage"
#: web/template/tax-details.gohtml:96 #: web/template/tax-details.gohtml:100
msgid "No taxes added yet." msgid "No taxes added yet."
msgstr "No hay impuestos." msgstr "No hay impuestos."
#: web/template/tax-details.gohtml:102 #: web/template/tax-details.gohtml:106
msgctxt "title" msgctxt "title"
msgid "New Line" msgid "New Line"
msgstr "Nueva línea" msgstr "Nueva línea"
#: web/template/tax-details.gohtml:106 #: web/template/tax-details.gohtml:111
msgctxt "input" msgctxt "input"
msgid "Tax name" msgid "Tax name"
msgstr "Nombre impuesto" msgstr "Nombre impuesto"
#: web/template/tax-details.gohtml:112 #: web/template/tax-details.gohtml:118
msgctxt "input" msgctxt "input"
msgid "Rate (%)" msgid "Rate (%)"
msgstr "Porcentage" msgstr "Porcentage"
#: web/template/tax-details.gohtml:119 #: web/template/tax-details.gohtml:125
msgctxt "action" msgctxt "action"
msgid "Add new tax" msgid "Add new tax"
msgstr "Añadir nuevo impuesto" msgstr "Añadir nuevo impuesto"
#: web/template/contacts-new.gohtml:3 pkg/contacts.go:95 #: web/template/contacts-new.gohtml:2 web/template/contacts-new.gohtml:7
msgctxt "title" msgctxt "title"
msgid "New Contact" msgid "New Contact"
msgstr "Nuevo contacto" msgstr "Nuevo contacto"
#: pkg/contacts.go:43 #: pkg/profile.go:26
msgctxt "title" msgctxt "language option"
msgid "Customers" msgid "Automatic"
msgstr "Clientes" msgstr "Automático"
#: pkg/profile.go:31
msgctxt "input"
msgid "User name"
msgstr "Nombre de usuario"
#: pkg/profile.go:48
msgctxt "input"
msgid "Password Confirmation"
msgstr "Confirmación contrasenya"
#: pkg/profile.go:53
msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: pkg/profile.go:74
msgid "Email can not be empty."
msgstr "No podéis dejar el correo-e en blanco."
#: pkg/profile.go:76
msgid "This value is not a valid email. It should be like name@domain.com."
msgstr "Este valor no es un correo-e válido. Tiene que ser parecido a nombre@dominio.es."
#: pkg/profile.go:79
msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco."
#: pkg/profile.go:82
msgid "Confirmation does not match password."
msgstr "La confirmación no corresponde con la contraseña."
#: pkg/profile.go:85
msgid "Selected language is not valid."
msgstr "Habéis escogido un idioma que no es válido."

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 877 KiB

View File

@ -246,7 +246,7 @@ header {
background-color: var(--numerus--header--background-color); background-color: var(--numerus--header--background-color);
} }
header, nav { header, nav a {
padding: 0 3rem; padding: 0 3rem;
} }
@ -320,13 +320,19 @@ input.width-2x {
top: 1rem; top: 1rem;
} }
.input ul {
font-size: .8em;
padding-left: 1em;
color: var(--numerus--color--red);
}
[lang="en"] input:not([required]) + label::after { [lang="en"] input:not([required]) + label::after {
content: " (optional)" content: " (optional)"
} }
[lang="ca"] input:not([required]) + label::after [lang="ca"] input:not([required]) + label::after
, [lang="es"] input:not([required]) + label::after { , [lang="es"] input:not([required]) + label::after {
content: " (optional)" content: " (opcional)"
} }
.input label, .input input:focus ~ label { .input label, .input input:focus ~ label {
@ -386,11 +392,11 @@ fieldset {
/* Profile Menu */ /* Profile Menu */
#profilemenu { #profile-menu {
position: relative; position: relative;
} }
#profilemenu summary { #profile-menu summary {
width: 7rem; width: 7rem;
height: 7rem; height: 7rem;
margin: 1rem 0; margin: 1rem 0;
@ -401,19 +407,19 @@ fieldset {
border: none; border: none;
} }
#profilemenu summary::-webkit-details-marker { #profile-menu summary::-webkit-details-marker {
display: none; display: none;
} }
#profilemenu summary, #profilemenu button { #profile-menu summary, #profile-menu button {
cursor: pointer; cursor: pointer;
} }
#profilemenu summary, #profilemenu ul { #profile-menu summary, #profile-menu ul {
background-color: var(--numerus--background-color); background-color: var(--numerus--background-color);
} }
#profilemenu[open] summary::before { #profile-menu[open] summary::before {
background-color: var(--numerus--header--background-color); background-color: var(--numerus--header--background-color);
position: fixed; position: fixed;
top: 0; top: 0;
@ -426,7 +432,7 @@ fieldset {
mix-blend-mode: multiply; mix-blend-mode: multiply;
} }
#profilemenu ul { #profile-menu ul {
list-style: none; list-style: none;
position: absolute; position: absolute;
right: -1.875em; right: -1.875em;
@ -435,11 +441,11 @@ fieldset {
z-index: 20; z-index: 20;
} }
#profilemenu li + li { #profile-menu li + li {
border-top: 1px solid var(--numerus--color--black); border-top: 1px solid var(--numerus--color--black);
} }
#profilemenu button, #profilemenu a { #profile-menu button, #profile-menu a {
font-size: 2rem; font-size: 2rem;
font-style: italic; font-style: italic;
height: 8rem; height: 8rem;
@ -453,15 +459,15 @@ fieldset {
text-transform: initial; text-transform: initial;
} }
#profilemenu li i[class^='ri-'] { #profile-menu li i[class^='ri-'] {
margin-right: 2rem; margin-right: 2rem;
color: var(--numerus--color--dark-gray); color: var(--numerus--color--dark-gray);
} }
#profilemenu summary:hover #profile-menu summary:hover
, #profilemenu summary:focus , #profile-menu summary:focus
, #profilemenu button:hover , #profile-menu button:hover
, #profilemenu a:hover , #profile-menu a:hover
, nav a:hover , nav a:hover
{ {
background-color: var(--numerus--color--light-gray); background-color: var(--numerus--color--light-gray);
@ -482,6 +488,7 @@ fieldset {
} }
[class^='ri-'], [class*=' ri-'] { [class^='ri-'], [class*=' ri-'] {
/*noinspection CssNoGenericFontName*/
font-family: 'remixicon' !important; font-family: 'remixicon' !important;
font-style: normal; font-style: normal;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;

View File

@ -1,15 +1,15 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="{{ currentLocale }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .Title }} — Numerus</title> <title>{{ template "title" . }} — Numerus</title>
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css"> <link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css">
</head> </head>
<body> <body>
<header> <header>
<h1><img src="/static/numerus.svg" alt="Numerus" width="261" height="33"></h1> <h1><img src="/static/numerus.svg" alt="Numerus" width="261" height="33"></h1>
<details id="profilemenu"> <details id="profile-menu">
<summary> <summary>
<i class="ri-eye-close-line ri-3x"></i> <i class="ri-eye-close-line ri-3x"></i>
</summary> </summary>
@ -39,6 +39,7 @@
</header> </header>
<nav aria-label="{{( pgettext "Main" "title" )}}"> <nav aria-label="{{( pgettext "Main" "title" )}}">
<ul> <ul>
<li><a href="{{ companyURI "/" }}">{{( pgettext "Dashboard" "nav" )}}</a></li>
<li><a href="{{ companyURI "/contacts" }}">{{( pgettext "Customers" "nav" )}}</a></li> <li><a href="{{ companyURI "/contacts" }}">{{( pgettext "Customers" "nav" )}}</a></li>
</ul> </ul>
</nav> </nav>

View File

@ -1,3 +1,7 @@
{{ define "title" -}}
{{( pgettext "Customers" "title" )}}
{{- end }}
{{ define "content" }} {{ define "content" }}
<a class="primary button" href="{{ companyURI "/contacts/new" }}">{{( pgettext "New contact" "action" )}}</a> <a class="primary button" href="{{ companyURI "/contacts/new" }}">{{( pgettext "New contact" "action" )}}</a>

View File

@ -1,3 +1,7 @@
{{ define "title" -}}
{{( pgettext "New Contact" "title" )}}
{{- end }}
{{ define "content" }} {{ define "content" }}
<section class="dialog-content"> <section class="dialog-content">
<h2>{{(pgettext "New Contact" "title")}}</h2> <h2>{{(pgettext "New Contact" "title")}}</h2>

View File

@ -1,2 +1,6 @@
{{ define "title" -}}
{{( pgettext "Dashboard" "title" )}}
{{- end }}
{{ define "content" }} {{ define "content" }}
{{- end }} {{- end }}

33
web/template/form.gohtml Normal file
View File

@ -0,0 +1,33 @@
{{ define "input-field" -}}
<div class="input {{ if .Errors }}has-errors{{ end }}">
<input type="{{ .Type }}" name="{{ .Name }}" id="{{ .Name }}-field"
{{ if .Required }}required="required"{{ end }} value="{{ .Value }}" placeholder="{{ .Label }}">
<label for="{{ .Name }}-field">{{ .Label }}</label>
{{ if .Errors }}
<ul>
{{- range $error := .Errors }}
<li>{{ . }}</li>
{{- end }}
</ul>
{{ end }}
</div>
{{- end }}
{{ define "select-field" -}}
<div class="input {{ if .Errors }}has-errors{{ end }}">
<select id="{{ .Name }}-field" name="{{ .Name }}">
{{- range $option := .Options }}
<option value="{{ .Value }}"
{{ if eq .Value $.Selected }}selected="selected"{{ end }}>{{ .Label }}</option>
{{- end }}
</select>
<label for="{{ .Name }}-field">{{ .Label }}</label>
{{ if .Errors }}
<ul>
{{- range $error := .Errors }}
<li>{{ . }}</li>
{{- end }}
</ul>
{{ end }}
</div>
{{- end }}

View File

@ -1,3 +1,7 @@
{{ define "title" -}}
{{( pgettext "Login" "title" )}}
{{- end }}
{{ define "content" }} {{ define "content" }}
<h1><img src="/static/numerus.svg" alt="Numerus" width="620" height="77"></h1> <h1><img src="/static/numerus.svg" alt="Numerus" width="620" height="77"></h1>
{{ if .LoginError -}} {{ if .LoginError -}}

View File

@ -1,46 +1,31 @@
{{ define "content" }} {{ define "title" -}}
<section class="dialog-content"> {{( pgettext "User Settings" "title" )}}
<h2>{{(pgettext "User Settings" "title")}}</h2> {{- end }}
<form method="POST" action="/profile">
<fieldset class="full-width"> {{ define "content" }}
<legend>{{( pgettext "User Access Data" "title" )}}</legend> <section class="dialog-content">
<h2>{{(pgettext "User Settings" "title")}}</h2>
<div class="input"> <form method="POST">
<input type="text" name="name" id="name" required="required" value="{{ .Name }}" placeholder="{{( pgettext "User name" "input" )}}"> <fieldset class="full-width">
<label for="name">{{( pgettext "User name" "input" )}}</label> <legend>{{( pgettext "User Access Data" "title" )}}</legend>
</div>
{{ template "input-field" .Name }}
<div class="input"> {{ template "input-field" .Email }}
<input type="email" name="email" id="email" required="required" value="{{ .Email }}" placeholder="{{( pgettext "Email" "input" )}}"> </fieldset>
<label for="email">{{( pgettext "Email" "input" )}}</label> <fieldset class="full-width">
</div> <legend>{{( pgettext "Password Change" "title" )}}</legend>
</fieldset>
<fieldset class="full-width"> {{ template "input-field" .Password }}
<legend>{{( pgettext "Password Change" "title" )}}</legend> {{ template "input-field" .PasswordConfirm }}
</fieldset>
<div class="input">
<input type="password" name="password" id="password" value="{{ .Password }}" placeholder="{{( pgettext "Password" "input" )}}"> <fieldset>
<label for="password">{{( pgettext "Password" "input" )}}</label> <legend id="language-legend">{{( pgettext "Language" "title" )}}</legend>
</div>
{{ template "select-field" .Language }}
<div class="input">
<input type="password" name="password_confirm" id="password_confirm" value="{{ .PasswordConfirm }}" placeholder="{{( pgettext "Password Confirmation" "input" )}}"> <button type="submit">{{( pgettext "Save changes" "action" )}}</button>
<label for="password_confirm">{{( pgettext "Password Confirmation" "input" )}}</label> </fieldset>
</div> </form>
</fieldset> </section>
<fieldset>
<legend id="language-legend">{{( pgettext "Language" "input" )}}</legend>
<select id="language" name="language" aria-labelledby="language-legend">
<option value="und">{{( pgettext "Automatic" "language option" )}}</option>
{{- range $language := .Languages }}
<option value="{{ .Tag }}" {{ if eq .Tag $.Language }}selected="selected"{{ end }}>{{ .Name }}</option>
{{- end }}
</select>
<button type="submit">{{( pgettext "Save changes" "action" )}}</button>
</fieldset>
</form>
</section>
{{- end }} {{- end }}

View File

@ -1,3 +1,7 @@
{{ define "title" -}}
{{( pgettext "Tax Details" "title" )}}
{{- end }}
{{ define "content" }} {{ define "content" }}
<section class="dialog-content"> <section class="dialog-content">
<h2>{{(pgettext "Tax Details" "title")}}</h2> <h2>{{(pgettext "Tax Details" "title")}}</h2>
@ -85,7 +89,7 @@
<td>{{ .Rate }}</td> <td>{{ .Rate }}</td>
<td> <td>
<form method="POST" action="{{ companyURI "/tax"}}/{{ .Id }}"> <form method="POST" action="{{ companyURI "/tax"}}/{{ .Id }}">
<input type="hidden" name="_method" name="DELETE"/> <input type="hidden" name="_method" value="DELETE"/>
<button class="icon" aria-label="{{( gettext "Delete tax" )}}" type="submit"><i class="ri-delete-back-2-line"></i></button> <button class="icon" aria-label="{{( gettext "Delete tax" )}}" type="submit"><i class="ri-delete-back-2-line"></i></button>
</form> </form>
</td> </td>

View File

@ -1,9 +1,9 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="{{ currentLocale }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{( pgettext "Login" "title" )}} — Numerus</title> <title>{{ template "title" . }} — Numerus</title>
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css"> <link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css">
</head> </head>
<body class="web"> <body class="web">