diff --git a/pkg/form/handle.go b/pkg/form/handle.go new file mode 100644 index 0000000..e6669ff --- /dev/null +++ b/pkg/form/handle.go @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package form + +import ( + "io" + "net/http" + + "dev.tandem.ws/tandem/camper/pkg/auth" + httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/locale" +) + +func HandleMultipart(f MultipartForm, w http.ResponseWriter, r *http.Request, user *auth.User) (bool, error) { + if err := f.ParseMultipart(w, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return false, err + } + if err := user.VerifyCSRFToken(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + _ = f.Close() + return false, err + } + if !f.Valid(user.Locale) { + if !httplib.IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + _ = f.Close() + return false, nil + } + return true, nil +} + +type MultipartForm interface { + io.Closer + ParseMultipart(w http.ResponseWriter, r *http.Request) error + Valid(l *locale.Locale) bool +} diff --git a/pkg/media/admin.go b/pkg/media/admin.go index 5736aa3..46a1a7a 100644 --- a/pkg/media/admin.go +++ b/pkg/media/admin.go @@ -9,6 +9,7 @@ import ( "context" "io" "net/http" + "net/url" "strconv" "strings" @@ -49,11 +50,16 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat case "picker": switch r.Method { case http.MethodGet: - serveMediaPicker(w, r, user, company, conn) - case http.MethodPost: - pickMedia(w, r, user, company, conn) + serveMediaPicker(w, r, user, company, conn, newMediaPicker(r.URL.Query())) default: - httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) + httplib.MethodNotAllowed(w, r, http.MethodGet) + } + case "field": + switch r.Method { + case http.MethodGet: + pickMedia(w, r, user, company) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet) } case "upload": switch r.Method { @@ -152,72 +158,77 @@ func (page *mediaIndex) MustRender(w http.ResponseWriter, r *http.Request, user template.MustRenderAdmin(w, r, user, company, "media/index.gohtml", page) } -func serveMediaPicker(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { +func serveMediaPicker(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, picker *mediaPicker) { media, err := collectMediaEntries(r.Context(), company, conn) if err != nil { panic(err) } - query := r.URL.Query() - page := &mediaPicker{ - Media: media, - Field: &form.Media{ - Input: &form.Input{ - Name: query.Get("name"), - Val: query.Get("value"), - }, - Label: query.Get("label"), - Prompt: query.Get("prompt"), - }, - } - page.MustRender(w, r, user, company) + picker.Media = media + picker.MustRender(w, r, user, company) +} + +func pickMedia(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + picker := newMediaPicker(r.URL.Query()) + picker.MustRenderField(w, r, user, company) } type mediaPicker struct { + *uploadForm Media []*mediaEntry Field *form.Media } -func (page *mediaPicker) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { - template.MustRenderNoLayout(w, r, user, company, "media/picker.gohtml", page) +func newMediaPicker(query url.Values) *mediaPicker { + return newMediaPickerWithUploadForm(newUploadForm(), query) } -func pickMedia(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { - if err := r.ParseForm(); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - input := &form.Media{ - Input: &form.Input{ - Name: strings.TrimSpace(r.FormValue("name")), - Val: strings.TrimSpace(r.FormValue("value")), +func newMediaPickerWithUploadForm(f *uploadForm, query url.Values) *mediaPicker { + return &mediaPicker{ + uploadForm: f, + Field: &form.Media{ + Input: &form.Input{ + Name: strings.TrimSpace(query.Get("name")), + Val: strings.TrimSpace(query.Get("value")), + }, + Label: strings.TrimSpace(query.Get("label")), + Prompt: strings.TrimSpace(query.Get("prompt")), }, - Label: strings.TrimSpace(r.FormValue("label")), - Prompt: strings.TrimSpace(r.FormValue("prompt")), } - template.MustRenderNoLayout(w, r, user, company, "media/field.gohtml", input) +} + +func (picker *mediaPicker) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderNoLayout(w, r, user, company, "media/picker.gohtml", picker) +} + +func (picker *mediaPicker) MustRenderField(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderNoLayout(w, r, user, company, "media/field.gohtml", picker.Field) } func uploadMedia(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { f := newUploadForm() - if err := f.Parse(w, r); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + ok, err := form.HandleMultipart(f, w, r, user) + if err != nil { + return + } + isPicker := r.Form.Has("picker") + if !ok { + if isPicker { + serveMediaPicker(w, r, user, company, conn, newMediaPickerWithUploadForm(f, r.Form)) + } else { + f.MustRender(w, r, user, company) + } return } defer f.Close() - if err := user.VerifyCSRFToken(r); err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - if !f.Valid(user.Locale) { - if !httplib.IsHTMxRequest(r) { - w.WriteHeader(http.StatusUnprocessableEntity) - } - f.MustRender(w, r, user, company) - return - } bytes := f.MustReadAllFile() - conn.MustExec(r.Context(), "select add_media($1, $2, $3, $4)", company.ID, f.File.Filename(), f.File.ContentType, bytes) - httplib.Redirect(w, r, "/admin/media", http.StatusSeeOther) + mediaId := conn.MustGetText(r.Context(), "select add_media($1, $2, $3, $4)::text", company.ID, f.File.Filename(), f.File.ContentType, bytes) + if isPicker { + picker := newMediaPickerWithUploadForm(f, r.Form) + picker.Field.Val = mediaId + picker.MustRenderField(w, r, user, company) + } else { + httplib.Redirect(w, r, "/admin/media", http.StatusSeeOther) + } } type uploadForm struct { @@ -233,7 +244,7 @@ func newUploadForm() *uploadForm { } } -func (f *uploadForm) Parse(w http.ResponseWriter, r *http.Request) error { +func (f *uploadForm) ParseMultipart(w http.ResponseWriter, r *http.Request) error { maxSize := f.File.MaxSize + 1024 r.Body = http.MaxBytesReader(w, r.Body, maxSize) if err := r.ParseMultipartForm(maxSize); err != nil { @@ -300,8 +311,8 @@ func (f *mediaForm) MustRender(w http.ResponseWriter, r *http.Request, user *aut template.MustRenderAdmin(w, r, user, company, "media/form.gohtml", f) } -func (f *mediaForm) Parse(w http.ResponseWriter, r *http.Request) error { - if err := f.uploadForm.Parse(w, r); err != nil { +func (f *mediaForm) ParseMultipart(w http.ResponseWriter, r *http.Request) error { + if err := f.uploadForm.ParseMultipart(w, r); err != nil { return err } f.OriginalFilename.FillValue(r) @@ -315,19 +326,9 @@ func (f *mediaForm) Valid(l *locale.Locale) bool { } func editMedia(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *mediaForm) { - if err := f.Parse(w, r); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + if ok, err := form.HandleMultipart(f, w, r, user); err != nil { return - } - defer f.Close() - if err := user.VerifyCSRFToken(r); err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - if !f.Valid(user.Locale) { - if !httplib.IsHTMxRequest(r) { - w.WriteHeader(http.StatusUnprocessableEntity) - } + } else if !ok { f.MustRender(w, r, user, company) return } diff --git a/pkg/template/render.go b/pkg/template/render.go index 830c447..dcc11d2 100644 --- a/pkg/template/render.go +++ b/pkg/template/render.go @@ -10,6 +10,7 @@ import ( "html/template" "io" "net/http" + "net/url" "path" "dev.tandem.ws/tandem/camper/pkg/auth" @@ -65,6 +66,9 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ "humanizeBytes": func(bytes int64) string { return humanizeBytes(bytes) }, + "queryEscape": func(s string) string { + return url.QueryEscape(s) + }, }) templates = append(templates, "form.gohtml") files := make([]string, len(templates)) diff --git a/po/ca.po b/po/ca.po index 116ec1b..25af991 100644 --- a/po/ca.po +++ b/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-09-21 01:41+0200\n" +"POT-Creation-Date: 2023-09-22 00:56+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -591,17 +591,45 @@ msgctxt "title" msgid "Media" msgstr "Mèdia" -#: web/templates/admin/media/picker.gohtml:7 +#: web/templates/admin/media/picker.gohtml:8 msgctxt "title" msgid "Media Picker" msgstr "Selector de mèdia" +#: web/templates/admin/media/picker.gohtml:19 +msgctxt "title" +msgid "Upload New Media" +msgstr "Pujada de nou mèdia" + +#: web/templates/admin/media/picker.gohtml:22 +#: web/templates/admin/media/upload.gohtml:18 +msgctxt "input" +msgid "File" +msgstr "Fitxer" + #: web/templates/admin/media/picker.gohtml:26 +#: web/templates/admin/media/form.gohtml:23 +#: web/templates/admin/media/upload.gohtml:22 +msgid "Maximum upload file size: %s" +msgstr "Mida màxima del fitxer a pujar: %s" + +#: web/templates/admin/media/picker.gohtml:31 +#: web/templates/admin/media/upload.gohtml:27 +msgctxt "action" +msgid "Upload" +msgstr "Puja" + +#: web/templates/admin/media/picker.gohtml:44 +msgctxt "title" +msgid "Choose Existing Media" +msgstr "Elecció d’un mèdia existent" + +#: web/templates/admin/media/picker.gohtml:55 #: web/templates/admin/media/index.gohtml:21 msgid "No media uploaded yet." msgstr "No s’ha pujat cap mèdia encara." -#: web/templates/admin/media/picker.gohtml:29 +#: web/templates/admin/media/picker.gohtml:58 msgctxt "action" msgid "Cancel" msgstr "Canceŀla" @@ -617,11 +645,6 @@ msgctxt "input" msgid "Updated file" msgstr "Fitxer actualitzat" -#: web/templates/admin/media/form.gohtml:23 -#: web/templates/admin/media/upload.gohtml:22 -msgid "Maximum upload file size: %s" -msgstr "Mida màxima del fitxer a pujar: %s" - #: web/templates/admin/media/form.gohtml:28 msgctxt "input" msgid "Filename" @@ -638,16 +661,6 @@ msgctxt "title" msgid "Upload Media" msgstr "Pujada de mèdia" -#: web/templates/admin/media/upload.gohtml:18 -msgctxt "input" -msgid "File" -msgstr "Fitxer" - -#: web/templates/admin/media/upload.gohtml:27 -msgctxt "action" -msgid "Upload" -msgstr "Puja" - #: pkg/carousel/admin.go:233 pkg/campsite/types/admin.go:236 msgctxt "input" msgid "Cover image" @@ -797,11 +810,11 @@ msgstr "No podeu deixar el format del número de factura en blanc." msgid "Cross-site request forgery detected." msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats." -#: pkg/media/admin.go:255 +#: pkg/media/admin.go:278 msgid "Uploaded file can not be empty." msgstr "No podeu deixar el fitxer del mèdia en blanc." -#: pkg/media/admin.go:314 +#: pkg/media/admin.go:337 msgid "Filename can not be empty." msgstr "No podeu deixar el nom del fitxer." diff --git a/po/es.po b/po/es.po index 3321b9b..8161a5c 100644 --- a/po/es.po +++ b/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-09-21 01:41+0200\n" +"POT-Creation-Date: 2023-09-22 00:56+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -591,17 +591,45 @@ msgctxt "title" msgid "Media" msgstr "Medios" -#: web/templates/admin/media/picker.gohtml:7 +#: web/templates/admin/media/picker.gohtml:8 msgctxt "title" msgid "Media Picker" msgstr "Selector de medio" +#: web/templates/admin/media/picker.gohtml:19 +msgctxt "title" +msgid "Upload New Media" +msgstr "Subida de un nuevo medio" + +#: web/templates/admin/media/picker.gohtml:22 +#: web/templates/admin/media/upload.gohtml:18 +msgctxt "input" +msgid "File" +msgstr "Archivo" + #: web/templates/admin/media/picker.gohtml:26 +#: web/templates/admin/media/form.gohtml:23 +#: web/templates/admin/media/upload.gohtml:22 +msgid "Maximum upload file size: %s" +msgstr "Tamaño máximo del archivos a subir: %s" + +#: web/templates/admin/media/picker.gohtml:31 +#: web/templates/admin/media/upload.gohtml:27 +msgctxt "action" +msgid "Upload" +msgstr "Subir" + +#: web/templates/admin/media/picker.gohtml:44 +msgctxt "title" +msgid "Choose Existing Media" +msgstr "Elección de un medio existente" + +#: web/templates/admin/media/picker.gohtml:55 #: web/templates/admin/media/index.gohtml:21 msgid "No media uploaded yet." msgstr "No se ha subido ningún medio todavía." -#: web/templates/admin/media/picker.gohtml:29 +#: web/templates/admin/media/picker.gohtml:58 msgctxt "action" msgid "Cancel" msgstr "Cancelar" @@ -617,11 +645,6 @@ msgctxt "input" msgid "Updated file" msgstr "Archivo actualizado" -#: web/templates/admin/media/form.gohtml:23 -#: web/templates/admin/media/upload.gohtml:22 -msgid "Maximum upload file size: %s" -msgstr "Tamaño máximo del archivos a subir: %s" - #: web/templates/admin/media/form.gohtml:28 msgctxt "input" msgid "Filename" @@ -638,16 +661,6 @@ msgctxt "title" msgid "Upload Media" msgstr "Subida de medio" -#: web/templates/admin/media/upload.gohtml:18 -msgctxt "input" -msgid "File" -msgstr "Archivo" - -#: web/templates/admin/media/upload.gohtml:27 -msgctxt "action" -msgid "Upload" -msgstr "Subir" - #: pkg/carousel/admin.go:233 pkg/campsite/types/admin.go:236 msgctxt "input" msgid "Cover image" @@ -797,11 +810,11 @@ msgstr "No podéis dejar el formato de número de factura en blanco." msgid "Cross-site request forgery detected." msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados." -#: pkg/media/admin.go:255 +#: pkg/media/admin.go:278 msgid "Uploaded file can not be empty." msgstr "No podéis dejar el archivo del medio en blanco." -#: pkg/media/admin.go:314 +#: pkg/media/admin.go:337 msgid "Filename can not be empty." msgstr "No podéis dejar el nombre del archivo en blanco." diff --git a/web/static/camper.js b/web/static/camper.js index 2f71a55..3b8a0d1 100644 --- a/web/static/camper.js +++ b/web/static/camper.js @@ -12,12 +12,9 @@ function ready(fn) { } ready(function () { - const snackBar = Object.assign( - document.body.appendChild(document.createElement('section')), - { - id: 'snackbar', - } - ); + const snackBar = Object.assign(document.body.appendChild(document.createElement('section')), { + id: 'snackbar', + }); const errorMessage = snackBar.appendChild(document.createElement('div')); errorMessage.setAttribute('role', 'alert'); @@ -115,3 +112,25 @@ ready(function () { }); } }) + +function camperUploadForm(el) { + const progress = el.querySelector('progress'); + htmx.on(el, 'drop', function (evt) { + evt.preventDefault(); + [...evt.dataTransfer.items].forEach(function (i) { + console.log(i); + i.getAsString(console.log) + }); + }); + htmx.on(el, 'dragover', function (evt) { + evt.preventDefault(); + }); + htmx.on(el, 'dragleave', function (evt) { + evt.preventDefault(); + }); + htmx.on(el, 'htmx:xhr:progress', function (evt) { + if (progress && evt.detail.lengthComputable) { + progress.setAttribute('value', evt.detail.loaded / evt.detail.total * 100); + } + }); +} diff --git a/web/templates/admin/form.gohtml b/web/templates/admin/form.gohtml index e74de62..32bfe53 100644 --- a/web/templates/admin/form.gohtml +++ b/web/templates/admin/form.gohtml @@ -18,10 +18,10 @@ {{ define "media-picker" -}} {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/form.Media*/ -}} -
+
{{( pgettext .Label "input" )}} - + + + + +
{{ with .Field -}} @@ -13,6 +41,7 @@ {{- end }} {{ if .Media -}}
+ {{( pgettext "Choose Existing Media" "title" )}}
    {{ range .Media -}}
  • diff --git a/web/templates/admin/media/upload.gohtml b/web/templates/admin/media/upload.gohtml index a5c4eef..ba92ddf 100644 --- a/web/templates/admin/media/upload.gohtml +++ b/web/templates/admin/media/upload.gohtml @@ -9,7 +9,7 @@ {{ define "content" -}} {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/media.uploadForm*/ -}} {{ template "settings-tabs" "media" }} - +

    {{( pgettext "Upload Media" "title" )}}

    {{ CSRFInput }}
    @@ -28,24 +28,5 @@ - + {{- end }}