Split templates and handlers into admin and public

I need to check that the user is an employee (or admin) in
administration handlers, but i do not want to do it for each handler,
because i am bound to forget it.  Thus, i added the /admin sub-path for
these resources.

The public-facing web is the rest of the resources outside /admin, but
for now there is only home, to test whether it works as expected or not.

The public-facing web can not relay on the user’s language settings, as
the guest user has no way to set that.  I would be happy to just use the
Accept-Language header for that, but apparently Google does not use that
header[0], and they give four alternatives: a country-specific domain,
a subdomain with a generic top-level domain (gTLD), subdirectories with
a gTLD, or URL parameters (e.g., site.com?loc=de).

Of the four, Google does not recommend URL parameters, and the customer
is already using subdirectories with the current site, therefor that’s
what i have chosen.

Google also tells me that it is a very good idea to have links between
localized version of the same resources, either with <link> elements,
Link HTTP response headers, or a sitemap file[1]; they are all
equivalent in the eyes of Google.

I have choosen the Link response headers way, because for that i can
simply “augment” ResponseHeader to automatically add these headers when
the response status is 2xx, otherwise i would need to pass down the
original URL path until it reaches the template.

Even though Camper is supposed to be a “generic”, multi-company
application, i think i will stick to the easiest route and write the
templates for just the “first” customer.

[0]: https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites
[1]: https://developers.google.com/search/docs/specialty/international/localized-versions
This commit is contained in:
jordi fita mas 2023-08-05 03:42:37 +02:00
parent 9349cda5f6
commit e128680e9a
23 changed files with 379 additions and 117 deletions

59
pkg/app/admin.go Normal file
View File

@ -0,0 +1,59 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package app
import (
"net/http"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/campsite"
"dev.tandem.ws/tandem/camper/pkg/database"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/template"
)
type adminHandler struct {
campsite *campsite.Handler
}
func newAdminHandler() *adminHandler {
return &adminHandler{
campsite: campsite.NewHandler(),
}
}
func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *database.Conn, requestPath string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !user.LoggedIn {
w.WriteHeader(http.StatusUnauthorized)
serveLoginForm(w, r, user, company, requestPath)
return
}
if !user.IsEmployee() {
http.Error(w, user.Locale.Gettext("Access forbidden"), http.StatusForbidden)
return
}
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "campsites":
h.campsite.Handler(user, company, conn).ServeHTTP(w, r)
case "":
switch r.Method {
case http.MethodGet:
serveDashboard(w, r, user, company)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
}
})
}
func serveDashboard(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "dashboard.gohtml", nil)
}

View File

@ -11,18 +11,17 @@ import (
"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/campsite"
"dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/database"
httplib "dev.tandem.ws/tandem/camper/pkg/http" 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"
) )
type App struct { type App struct {
db *database.DB db *database.DB
fileHandler http.Handler fileHandler http.Handler
profile *profileHandler profile *profileHandler
campsite *campsite.Handler admin *adminHandler
public *publicHandler
locales locale.Locales locales locale.Locales
defaultLocale *locale.Locale defaultLocale *locale.Locale
languageMatcher language.Matcher languageMatcher language.Matcher
@ -39,7 +38,8 @@ func New(db *database.DB, avatarsDir string) (http.Handler, error) {
db: db, db: db,
fileHandler: static, fileHandler: static,
profile: profile, profile: profile,
campsite: campsite.NewHandler(), admin: newAdminHandler(),
public: newPublicHandler(),
locales: locales, locales: locales,
defaultLocale: locales[language.Catalan], defaultLocale: locales[language.Catalan],
languageMatcher: language.NewMatcher(locales.Tags()), languageMatcher: language.NewMatcher(locales.Tags()),
@ -81,41 +81,38 @@ func (h *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
panic(err) panic(err)
} }
if head == "login" { switch head {
case "":
http.Redirect(w, r, "/"+user.Locale.Language.String()+"/", http.StatusFound)
case "admin":
h.admin.Handle(user, company, conn, requestPath).ServeHTTP(w, r)
case "login":
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
serveLoginForm(w, r, user, company, "/") serveLoginForm(w, r, user, company, "/admin")
case http.MethodPost: case http.MethodPost:
handleLogin(w, r, user, company, conn) handleLogin(w, r, user, company, conn)
default: default:
httplib.MethodNotAllowed(w, r, http.MethodPost, http.MethodGet) httplib.MethodNotAllowed(w, r, http.MethodPost, http.MethodGet)
} }
} else { case "me":
if !user.LoggedIn { h.profile.Handler(user, company, conn, requestPath).ServeHTTP(w, r)
w.WriteHeader(http.StatusUnauthorized) default:
serveLoginForm(w, r, user, company, requestPath) langTag, err := language.Parse(head)
if err != nil {
http.NotFound(w, r)
return return
} }
urlLocale := h.locales[langTag]
switch head { if urlLocale == nil {
case "me":
h.profile.Handler(user, company, conn).ServeHTTP(w, r)
case "campsites":
h.campsite.Handler(user, company, conn).ServeHTTP(w, r)
case "":
switch r.Method {
case http.MethodGet:
serveDashboard(w, r, user, company)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
default:
http.NotFound(w, r) http.NotFound(w, r)
return
} }
user.Locale = urlLocale
if r.Method == http.MethodGet || r.Method == http.MethodHead {
w = httplib.LanguageLinks(w, false, r.Host, r.URL.Path, h.locales)
}
h.public.Handler(user, company, conn).ServeHTTP(w, r)
} }
} }
} }
func serveDashboard(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRender(w, r, user, company, "dashboard.gohtml", nil)
}

View File

@ -46,7 +46,7 @@ func (f *loginForm) Parse(r *http.Request) error {
f.Password.FillValue(r) f.Password.FillValue(r)
f.Redirect.FillValue(r) f.Redirect.FillValue(r)
if f.Redirect.Val == "" { if f.Redirect.Val == "" {
f.Redirect.Val = "/" f.Redirect.Val = "/admin/"
} }
return nil return nil
} }
@ -61,7 +61,7 @@ func (f *loginForm) Valid(l *locale.Locale) bool {
} }
func (f *loginForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { func (f *loginForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRender(w, r, user, company, "login.gohtml", f) template.MustRenderAdmin(w, r, user, company, "login.gohtml", f)
} }
func serveLoginForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, redirectPath string) { func serveLoginForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, redirectPath string) {

34
pkg/app/public.go Normal file
View File

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package app
import (
"net/http"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/template"
)
type publicHandler struct{}
func newPublicHandler() *publicHandler {
return &publicHandler{}
}
func (h *publicHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "":
template.MustRenderPublic(w, r, user, company, "home.gohtml", nil)
default:
http.NotFound(w, r)
}
})
}

View File

@ -80,8 +80,14 @@ func newProfileHandler(static http.Handler, avatarsDir string) (*profileHandler,
return handler, nil return handler, nil
} }
func (h *profileHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc { func (h *profileHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn, requestPath string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if !user.LoggedIn {
w.WriteHeader(http.StatusUnauthorized)
serveLoginForm(w, r, user, company, requestPath)
return
}
var head string var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path) head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
@ -249,7 +255,7 @@ func (f *profileForm) Valid(l *locale.Locale) bool {
} }
func (f *profileForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { func (f *profileForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRender(w, r, user, company, "profile.gohtml", f) template.MustRenderAdmin(w, r, user, company, "profile.gohtml", f)
} }
func (f *profileForm) HasAvatarFile() bool { func (f *profileForm) HasAvatarFile() bool {

View File

@ -39,3 +39,8 @@ func (user *User) VerifyCSRFToken(r *http.Request) error {
} }
return errors.New(user.Locale.Gettext("Cross-site request forgery detected.")) return errors.New(user.Locale.Gettext("Cross-site request forgery detected."))
} }
func (user *User) IsEmployee() bool {
role := user.Role[0]
return role == 'e' || role == 'a'
}

View File

@ -89,7 +89,7 @@ type typeIndex struct {
} }
func (page *typeIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { func (page *typeIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRender(w, r, user, company, "campsite/type/index.gohtml", page) template.MustRenderAdmin(w, r, user, company, "campsite/type/index.gohtml", page)
} }
func addType(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { func addType(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
@ -145,5 +145,5 @@ func (f *typeForm) Valid(l *locale.Locale) bool {
} }
func (f *typeForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { func (f *typeForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRender(w, r, user, company, "campsite/type/new.gohtml", f) template.MustRenderAdmin(w, r, user, company, "campsite/type/new.gohtml", f)
} }

49
pkg/http/links.go Normal file
View File

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package http
import (
"fmt"
"net/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
)
type languageLinks struct {
http.ResponseWriter
schemaAuthority string
path string
locales locale.Locales
wroteHeader bool
}
func (w *languageLinks) WriteHeader(statusCode int) {
if statusCode >= 200 && statusCode < 300 {
for k := range w.locales {
tag := k.String()
w.Header().Add("Link", fmt.Sprintf(`<%[1]s/%[2]s%[3]s>; rel="alternate"; hreflang="%[2]s"`, w.schemaAuthority, tag, w.path))
}
}
w.wroteHeader = true
w.ResponseWriter.WriteHeader(statusCode)
}
func (w *languageLinks) Write(data []byte) (int, error) {
if !w.wroteHeader {
w.WriteHeader(http.StatusOK)
}
return w.ResponseWriter.Write(data)
}
func LanguageLinks(w http.ResponseWriter, https bool, authority string, path string, locales locale.Locales) http.ResponseWriter {
var schema string
if https {
schema = "https"
} else {
schema = "http"
}
return &languageLinks{w, schema + "://" + authority, path, locales, false}
}

View File

@ -15,19 +15,28 @@ import (
httplib "dev.tandem.ws/tandem/camper/pkg/http" httplib "dev.tandem.ws/tandem/camper/pkg/http"
) )
func templateFile(name string) string { func adminTemplateFile(name string) string {
return "web/templates/" + name return "web/templates/admin/" + name
} }
func MustRender(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) { func publicTemplateFile(name string) string {
return "web/templates/public/" + name
}
func MustRenderAdmin(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) {
layout := "layout.gohtml" layout := "layout.gohtml"
if httplib.IsHTMxRequest(r) { if httplib.IsHTMxRequest(r) {
layout = "htmx.gohtml" layout = "htmx.gohtml"
} }
mustRenderLayout(w, user, company, layout, filename, data) mustRenderLayout(w, user, company, adminTemplateFile, layout, filename, data)
} }
func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, layout string, filename string, data interface{}) { func MustRenderPublic(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) {
layout := "layout.gohtml"
mustRenderLayout(w, user, company, publicTemplateFile, layout, filename, data)
}
func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templateFile func(string) string, layout string, filename string, data interface{}) {
t := template.New(filename) t := template.New(filename)
t.Funcs(template.FuncMap{ t.Funcs(template.FuncMap{
"gettext": user.Locale.Get, "gettext": user.Locale.Get,

View File

@ -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-08-04 19:51+0200\n" "POT-Creation-Date: 2023-08-05 03:23+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"
@ -18,123 +18,133 @@ 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/templates/campsite/type/new.gohtml:14 #: web/templates/public/home.gohtml:6
#: web/templates/campsite/type/new.gohtml:20 msgctxt "title"
msgid "Home"
msgstr "Inici"
#: web/templates/public/layout.gohtml:10 web/templates/public/layout.gohtml:17
msgid "Campsite Montagut"
msgstr "Càmping Montagut"
#: web/templates/public/layout.gohtml:16 web/templates/admin/layout.gohtml:18
msgid "Skip to main content"
msgstr "Salta al contingut principal"
#: web/templates/admin/campsite/type/new.gohtml:14
#: web/templates/admin/campsite/type/new.gohtml:20
msgctxt "title" msgctxt "title"
msgid "New Campsite Type" msgid "New Campsite Type"
msgstr "Nou tipus dallotjament" msgstr "Nou tipus dallotjament"
#: web/templates/campsite/type/new.gohtml:25 web/templates/profile.gohtml:26 #: web/templates/admin/campsite/type/new.gohtml:25
#: web/templates/admin/profile.gohtml:26
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: web/templates/campsite/type/new.gohtml:33 #: web/templates/admin/campsite/type/new.gohtml:33
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: web/templates/campsite/type/new.gohtml:40 #: web/templates/admin/campsite/type/new.gohtml:40
msgctxt "action" msgctxt "action"
msgid "Add" msgid "Add"
msgstr "Afegeix" msgstr "Afegeix"
#: web/templates/campsite/type/index.gohtml:6 #: web/templates/admin/campsite/type/index.gohtml:6
#: web/templates/campsite/type/index.gohtml:12 #: web/templates/admin/campsite/type/index.gohtml:12
msgctxt "title" msgctxt "title"
msgid "Campsite Types" msgid "Campsite Types"
msgstr "Tipus dallotjaments" msgstr "Tipus dallotjaments"
#: web/templates/campsite/type/index.gohtml:11 #: web/templates/admin/campsite/type/index.gohtml:11
msgctxt "action" msgctxt "action"
msgid "Add Type" msgid "Add Type"
msgstr "Afegeix tipus" msgstr "Afegeix tipus"
#: web/templates/campsite/type/index.gohtml:17 #: web/templates/admin/campsite/type/index.gohtml:17
msgctxt "header" msgctxt "header"
msgid "Name" msgid "Name"
msgstr "Nom" msgstr "Nom"
#: web/templates/campsite/type/index.gohtml:29 #: web/templates/admin/campsite/type/index.gohtml:29
msgid "No campsite types added yet." msgid "No campsite types added yet."
msgstr "No sha afegit cap tipus dallotjament encara." msgstr "No sha afegit cap tipus dallotjament encara."
#: web/templates/dashboard.gohtml:6 web/templates/dashboard.gohtml:10 #: web/templates/admin/dashboard.gohtml:6
#: web/templates/layout.gohtml:44 #: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:44
msgctxt "title" msgctxt "title"
msgid "Dashboard" msgid "Dashboard"
msgstr "Tauler" msgstr "Tauler"
#: web/templates/login.gohtml:6 web/templates/login.gohtml:13 #: web/templates/admin/login.gohtml:6 web/templates/admin/login.gohtml:13
msgctxt "title" msgctxt "title"
msgid "Login" msgid "Login"
msgstr "Entrada" msgstr "Entrada"
#: web/templates/login.gohtml:22 web/templates/profile.gohtml:35 #: web/templates/admin/login.gohtml:22 web/templates/admin/profile.gohtml:35
msgctxt "input" msgctxt "input"
msgid "Email" msgid "Email"
msgstr "Correu-e" msgstr "Correu-e"
#: web/templates/login.gohtml:31 web/templates/profile.gohtml:46 #: web/templates/admin/login.gohtml:31 web/templates/admin/profile.gohtml:46
msgctxt "input" msgctxt "input"
msgid "Password" msgid "Password"
msgstr "Contrasenya" msgstr "Contrasenya"
#: web/templates/login.gohtml:40 #: web/templates/admin/login.gohtml:40
msgctxt "action" msgctxt "action"
msgid "Login" msgid "Login"
msgstr "Entra" msgstr "Entra"
#: web/templates/profile.gohtml:6 web/templates/profile.gohtml:12 #: web/templates/admin/profile.gohtml:6 web/templates/admin/profile.gohtml:12
#: web/templates/layout.gohtml:29 #: web/templates/admin/layout.gohtml:29
msgctxt "title" msgctxt "title"
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
#: web/templates/profile.gohtml:17 #: web/templates/admin/profile.gohtml:17
msgctxt "inut" msgctxt "inut"
msgid "Profile Image" msgid "Profile Image"
msgstr "Imatge del perfil" msgstr "Imatge del perfil"
#: web/templates/profile.gohtml:43 #: web/templates/admin/profile.gohtml:43
msgctxt "legend" msgctxt "legend"
msgid "Change password" msgid "Change password"
msgstr "Canvi de contrasenya" msgstr "Canvi de contrasenya"
#: web/templates/profile.gohtml:55 #: web/templates/admin/profile.gohtml:55
msgctxt "input" msgctxt "input"
msgid "Password Confirmation" msgid "Password Confirmation"
msgstr "Confirmació de la contrasenya" msgstr "Confirmació de la contrasenya"
#: web/templates/profile.gohtml:65 #: web/templates/admin/profile.gohtml:65
msgctxt "input" msgctxt "input"
msgid "Language" msgid "Language"
msgstr "Idioma" msgstr "Idioma"
#: web/templates/profile.gohtml:75 #: web/templates/admin/profile.gohtml:75
msgctxt "action" msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Desa els canvis" msgstr "Desa els canvis"
#: web/templates/layout.gohtml:18 #: web/templates/admin/layout.gohtml:25
msgid "Skip to main content"
msgstr "Salta al contingut principal"
#: web/templates/layout.gohtml:25
msgctxt "title" msgctxt "title"
msgid "User Menu" msgid "User Menu"
msgstr "Menú dusuari" msgstr "Menú dusuari"
#: web/templates/layout.gohtml:33 #: web/templates/admin/layout.gohtml:33
msgctxt "action" msgctxt "action"
msgid "Logout" msgid "Logout"
msgstr "Surt" msgstr "Surt"
#: pkg/app/login.go:56 pkg/app/user.go:239 #: pkg/app/login.go:56 pkg/app/user.go:245
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/user.go:240 #: pkg/app/login.go:57 pkg/app/user.go:246
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."
@ -146,27 +156,31 @@ msgstr "No podeu deixar la contrasenya en blanc."
msgid "Invalid user or password." msgid "Invalid user or password."
msgstr "Nom dusuari o contrasenya incorrectes." msgstr "Nom dusuari o contrasenya incorrectes."
#: pkg/app/user.go:190 #: pkg/app/user.go:196
msgctxt "language option" msgctxt "language option"
msgid "Automatic" msgid "Automatic"
msgstr "Automàtic" msgstr "Automàtic"
#: pkg/app/user.go:242 pkg/campsite/type.go:143 #: pkg/app/user.go:248 pkg/campsite/type.go:143
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podeu deixar el nom en blanc." msgstr "No podeu deixar el nom en blanc."
#: pkg/app/user.go:243 #: pkg/app/user.go:249
msgid "Confirmation does not match password." msgid "Confirmation does not match password."
msgstr "La confirmació no es correspon amb la contrasenya." msgstr "La confirmació no es correspon amb la contrasenya."
#: pkg/app/user.go:244 #: pkg/app/user.go:250
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "Lidioma escollit no és vàlid." msgstr "Lidioma escollit no és vàlid."
#: pkg/app/user.go:246 #: pkg/app/user.go:252
msgid "File must be a valid PNG or JPEG image." msgid "File must be a valid PNG or JPEG image."
msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida." msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida."
#: pkg/app/admin.go:37
msgid "Access forbidden"
msgstr "Accés prohibit"
#: pkg/auth/user.go:40 #: pkg/auth/user.go:40
msgid "Cross-site request forgery detected." msgid "Cross-site request forgery detected."
msgstr "Sha detectat un intent de falsificació de petició a llocs creuats." msgstr "Sha detectat un intent de falsificació de petició a llocs creuats."

View File

@ -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-08-04 19:51+0200\n" "POT-Creation-Date: 2023-08-05 03:23+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"
@ -18,123 +18,133 @@ 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/templates/campsite/type/new.gohtml:14 #: web/templates/public/home.gohtml:6
#: web/templates/campsite/type/new.gohtml:20 msgctxt "title"
msgid "Home"
msgstr "Inicio"
#: web/templates/public/layout.gohtml:10 web/templates/public/layout.gohtml:17
msgid "Campsite Montagut"
msgstr "Camping Montagut"
#: web/templates/public/layout.gohtml:16 web/templates/admin/layout.gohtml:18
msgid "Skip to main content"
msgstr "Saltar al contenido principal"
#: web/templates/admin/campsite/type/new.gohtml:14
#: web/templates/admin/campsite/type/new.gohtml:20
msgctxt "title" msgctxt "title"
msgid "New Campsite Type" msgid "New Campsite Type"
msgstr "Nuevo tipo de alojamiento" msgstr "Nuevo tipo de alojamiento"
#: web/templates/campsite/type/new.gohtml:25 web/templates/profile.gohtml:26 #: web/templates/admin/campsite/type/new.gohtml:25
#: web/templates/admin/profile.gohtml:26
msgctxt "input" msgctxt "input"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: web/templates/campsite/type/new.gohtml:33 #: web/templates/admin/campsite/type/new.gohtml:33
msgctxt "input" msgctxt "input"
msgid "Description" msgid "Description"
msgstr "Descripción" msgstr "Descripción"
#: web/templates/campsite/type/new.gohtml:40 #: web/templates/admin/campsite/type/new.gohtml:40
msgctxt "action" msgctxt "action"
msgid "Add" msgid "Add"
msgstr "Añadir" msgstr "Añadir"
#: web/templates/campsite/type/index.gohtml:6 #: web/templates/admin/campsite/type/index.gohtml:6
#: web/templates/campsite/type/index.gohtml:12 #: web/templates/admin/campsite/type/index.gohtml:12
msgctxt "title" msgctxt "title"
msgid "Campsite Types" msgid "Campsite Types"
msgstr "Tipos de alojamientos" msgstr "Tipos de alojamientos"
#: web/templates/campsite/type/index.gohtml:11 #: web/templates/admin/campsite/type/index.gohtml:11
msgctxt "action" msgctxt "action"
msgid "Add Type" msgid "Add Type"
msgstr "Añadir tipo" msgstr "Añadir tipo"
#: web/templates/campsite/type/index.gohtml:17 #: web/templates/admin/campsite/type/index.gohtml:17
msgctxt "header" msgctxt "header"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: web/templates/campsite/type/index.gohtml:29 #: web/templates/admin/campsite/type/index.gohtml:29
msgid "No campsite types added yet." msgid "No campsite types added yet."
msgstr "No se ha añadido ningún tipo de alojamiento todavía." msgstr "No se ha añadido ningún tipo de alojamiento todavía."
#: web/templates/dashboard.gohtml:6 web/templates/dashboard.gohtml:10 #: web/templates/admin/dashboard.gohtml:6
#: web/templates/layout.gohtml:44 #: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:44
msgctxt "title" msgctxt "title"
msgid "Dashboard" msgid "Dashboard"
msgstr "Panel" msgstr "Panel"
#: web/templates/login.gohtml:6 web/templates/login.gohtml:13 #: web/templates/admin/login.gohtml:6 web/templates/admin/login.gohtml:13
msgctxt "title" msgctxt "title"
msgid "Login" msgid "Login"
msgstr "Entrada" msgstr "Entrada"
#: web/templates/login.gohtml:22 web/templates/profile.gohtml:35 #: web/templates/admin/login.gohtml:22 web/templates/admin/profile.gohtml:35
msgctxt "input" msgctxt "input"
msgid "Email" msgid "Email"
msgstr "Correo-e" msgstr "Correo-e"
#: web/templates/login.gohtml:31 web/templates/profile.gohtml:46 #: web/templates/admin/login.gohtml:31 web/templates/admin/profile.gohtml:46
msgctxt "input" msgctxt "input"
msgid "Password" msgid "Password"
msgstr "Contraseña" msgstr "Contraseña"
#: web/templates/login.gohtml:40 #: web/templates/admin/login.gohtml:40
msgctxt "action" msgctxt "action"
msgid "Login" msgid "Login"
msgstr "Entrar" msgstr "Entrar"
#: web/templates/profile.gohtml:6 web/templates/profile.gohtml:12 #: web/templates/admin/profile.gohtml:6 web/templates/admin/profile.gohtml:12
#: web/templates/layout.gohtml:29 #: web/templates/admin/layout.gohtml:29
msgctxt "title" msgctxt "title"
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
#: web/templates/profile.gohtml:17 #: web/templates/admin/profile.gohtml:17
msgctxt "inut" msgctxt "inut"
msgid "Profile Image" msgid "Profile Image"
msgstr "Imagen del perfil" msgstr "Imagen del perfil"
#: web/templates/profile.gohtml:43 #: web/templates/admin/profile.gohtml:43
msgctxt "legend" msgctxt "legend"
msgid "Change password" msgid "Change password"
msgstr "Cambio de contraseña" msgstr "Cambio de contraseña"
#: web/templates/profile.gohtml:55 #: web/templates/admin/profile.gohtml:55
msgctxt "input" msgctxt "input"
msgid "Password Confirmation" msgid "Password Confirmation"
msgstr "Confirmación de la contraseña" msgstr "Confirmación de la contraseña"
#: web/templates/profile.gohtml:65 #: web/templates/admin/profile.gohtml:65
msgctxt "input" msgctxt "input"
msgid "Language" msgid "Language"
msgstr "Idioma" msgstr "Idioma"
#: web/templates/profile.gohtml:75 #: web/templates/admin/profile.gohtml:75
msgctxt "action" msgctxt "action"
msgid "Save changes" msgid "Save changes"
msgstr "Guardar los cambios" msgstr "Guardar los cambios"
#: web/templates/layout.gohtml:18 #: web/templates/admin/layout.gohtml:25
msgid "Skip to main content"
msgstr "Saltar al contenido principal"
#: web/templates/layout.gohtml:25
msgctxt "title" msgctxt "title"
msgid "User Menu" msgid "User Menu"
msgstr "Menú de usuario" msgstr "Menú de usuario"
#: web/templates/layout.gohtml:33 #: web/templates/admin/layout.gohtml:33
msgctxt "action" msgctxt "action"
msgid "Logout" msgid "Logout"
msgstr "Salir" msgstr "Salir"
#: pkg/app/login.go:56 pkg/app/user.go:239 #: pkg/app/login.go:56 pkg/app/user.go:245
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/user.go:240 #: pkg/app/login.go:57 pkg/app/user.go:246
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."
@ -146,27 +156,31 @@ msgstr "No podéis dejar la contraseña en blanco."
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:190 #: pkg/app/user.go:196
msgctxt "language option" msgctxt "language option"
msgid "Automatic" msgid "Automatic"
msgstr "Automático" msgstr "Automático"
#: pkg/app/user.go:242 pkg/campsite/type.go:143 #: pkg/app/user.go:248 pkg/campsite/type.go:143
msgid "Name can not be empty." msgid "Name can not be empty."
msgstr "No podéis dejar el nombre en blanco." msgstr "No podéis dejar el nombre en blanco."
#: pkg/app/user.go:243 #: pkg/app/user.go:249
msgid "Confirmation does not match password." msgid "Confirmation does not match password."
msgstr "La confirmación no se corresponde con la contraseña." msgstr "La confirmación no se corresponde con la contraseña."
#: pkg/app/user.go:244 #: pkg/app/user.go:250
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "El idioma escogido no es válido." msgstr "El idioma escogido no es válido."
#: pkg/app/user.go:246 #: pkg/app/user.go:252
msgid "File must be a valid PNG or JPEG image." msgid "File must be a valid PNG or JPEG image."
msgstr "El archivo tiene que ser una imagen PNG o JPEG válida." msgstr "El archivo tiene que ser una imagen PNG o JPEG válida."
#: pkg/app/admin.go:37
msgid "Access forbidden"
msgstr "Acceso prohibido"
#: pkg/auth/user.go:40 #: pkg/auth/user.go:40
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."

41
web/static/public.css Normal file
View File

@ -0,0 +1,41 @@
/**
* SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
* SPDX-License-Identifier: AGPL-3.0-only
*/
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html, body {
height: 100%;
}
html {
font-size: 62.5%;
}
body {
font-size: 1.6rem;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
background-color: white;
color: #3f3b37;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}

View File

@ -8,7 +8,7 @@
{{ define "content" -}} {{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite.typeIndex*/ -}} {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite.typeIndex*/ -}}
<a href="/campsites/types/new">{{( pgettext "Add Type" "action" )}}</a> <a href="/admin/campsites/types/new">{{( pgettext "Add Type" "action" )}}</a>
<h2>{{( pgettext "Campsite Types" "title" )}}</h2> <h2>{{( pgettext "Campsite Types" "title" )}}</h2>
{{ if .Types -}} {{ if .Types -}}
<table> <table>
@ -20,7 +20,7 @@
<tbody> <tbody>
{{ range .Types -}} {{ range .Types -}}
<tr> <tr>
<td><a href="/campsites/type/{{ .Slug }}">{{ .Name }}</a></td> <td><a href="/admin/campsites/type/{{ .Slug }}">{{ .Name }}</a></td>
</tr> </tr>
{{- end }} {{- end }}
</tbody> </tbody>

View File

@ -16,7 +16,7 @@
{{ define "content" -}} {{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite.typeForm*/ -}} {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/campsite.typeForm*/ -}}
<form action="/campsites/types" method="post"> <form action="/admin/campsites/types" method="post">
<h2>{{( pgettext "New Campsite Type" "title" )}}</h2> <h2>{{( pgettext "New Campsite Type" "title" )}}</h2>
{{ CSRFInput }} {{ CSRFInput }}
<fieldset> <fieldset>

View File

@ -41,7 +41,7 @@
<nav> <nav>
<ul role="menu"> <ul role="menu">
<li role="presentation"> <li role="presentation">
<a role="menuitem" href="/">{{( pgettext "Dashboard" "title" )}}</a> <a role="menuitem" href="/admin/">{{( pgettext "Dashboard" "title" )}}</a>
</li> </li>
</ul> </ul>
</nav> </nav>

View File

@ -0,0 +1 @@
../admin/form.gohtml

View File

@ -0,0 +1,10 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Home" "title" )}}
{{- end }}
{{ define "content" -}}
{{- end }}

View File

@ -0,0 +1,23 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
<!doctype html>
<html lang="{{ currentLocale }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ template "title" . }} — {{( gettext "Campsite Montagut" )}}</title>
<link rel="stylesheet" media="screen" href="/static/public.css">
{{ block "head" . }}{{ end }}
</head>
<body>
<header>
<a href="#content">{{( gettext "Skip to main content" )}}</a>
<h1>{{( gettext "Campsite Montagut" )}}</h1>
</header>
<main id="content">
{{- template "content" . }}
</main>
</body>
</html>