/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package campsite import ( "context" "net/http" "strconv" "github.com/jackc/pgx/v4" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/carousel" "dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/form" httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/template" ) func (h *AdminHandler) carouselHandler(user *auth.User, company *auth.Company, conn *database.Conn, label string) 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 "": switch r.Method { case http.MethodGet: serveCarouselIndex(w, r, user, company, conn, label) case http.MethodPost: addSlide(w, r, user, company, conn, label) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodGet) } case "new": switch r.Method { case http.MethodGet: f := newSlideForm(company, label) f.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } case "order": switch r.Method { case http.MethodPost: orderCarousel(w, r, user, company, conn, label) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } default: mediaID, err := strconv.Atoi(head) if err != nil { http.NotFound(w, r) return } f := newSlideForm(company, label) if err := f.FillFromDatabase(r.Context(), conn, company, mediaID); err != nil { if database.ErrorIsNotFound(err) { http.NotFound(w, r) return } panic(err) } var langTag string langTag, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch langTag { case "": switch r.Method { case http.MethodGet: f.MustRender(w, r, user, company) case http.MethodPut: editSlide(w, r, user, company, conn, f) case http.MethodDelete: deleteSlide(w, r, user, company, conn, label, mediaID) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut, http.MethodDelete) } default: http.NotFound(w, r) } } }) } func serveCarouselIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { slides, err := collectSlideEntries(r.Context(), conn, company, label) if err != nil { panic(err) } page := &carouselIndex{ Label: label, Slides: slides, } page.MustRender(w, r, user, company) } type carouselIndex struct { Label string Slides []*carousel.SlideEntry } func (page *carouselIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "campsite/carousel/index.gohtml", page) } func mustCollectSlides(ctx context.Context, conn *database.Conn, company *auth.Company, loc *locale.Locale, label string) []*carousel.Slide { rows, err := conn.Query(ctx, ` select coalesce(i18n.caption, slide.caption) as l10_caption , media.path from campsite_carousel as slide join campsite using (campsite_id) join media on media.media_id = slide.media_id left join campsite_carousel_i18n as i18n on i18n.campsite_id = slide.campsite_id and i18n.media_id = slide.media_id and lang_tag = $1 where campsite.label = $2 and campsite.company_id = $3 order by slide.position, l10_caption `, loc.Language, label, company.ID) if err != nil { panic(err) } defer rows.Close() var slides []*carousel.Slide for rows.Next() { slide := &carousel.Slide{} err = rows.Scan(&slide.Caption, &slide.Media) if err != nil { panic(err) } slides = append(slides, slide) } if rows.Err() != nil { panic(rows.Err()) } return slides } func collectSlideEntries(ctx context.Context, conn *database.Conn, company *auth.Company, label string) ([]*carousel.SlideEntry, error) { rows, err := conn.Query(ctx, ` select carousel.media_id , media.path , caption from campsite_carousel as carousel join campsite using (campsite_id) join media on media.media_id = carousel.media_id where campsite.label = $1 and campsite.company_id = $2 order by carousel.position, caption `, label, company.ID) if err != nil { return nil, err } defer rows.Close() var slides []*carousel.SlideEntry for rows.Next() { slide := &carousel.SlideEntry{} if err = rows.Scan(&slide.ID, &slide.Media, &slide.Caption); err != nil { return nil, err } slides = append(slides, slide) } return slides, nil } func addSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { f := newSlideForm(company, label) editSlide(w, r, user, company, conn, f) } func editSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *slideForm) { f.process(w, r, user, company, conn, func(ctx context.Context, tx *database.Tx) error { if err := tx.AddCampsiteCarouselSlide(ctx, f.Label, company.ID, f.Media.Int(), f.Caption[f.DefaultLang].Val); err != nil { return nil } for lang := range company.Locales { l := lang.String() if l == f.DefaultLang { continue } if err := tx.TranslateCampsiteCarouselSlide(ctx, f.Label, company.ID, f.Media.Int(), lang, f.Caption[l].Val); err != nil { return err } } return nil }) } func deleteSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string, mediaID int) { if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } if err := conn.RemoveCampsiteCarouselSlide(r.Context(), label, company.ID, mediaID); err != nil { panic(err) } httplib.Redirect(w, r, "/admin/campsites/"+label+"/slides", http.StatusSeeOther) } type slideForm struct { DefaultLang string Label string MediaID int Media *form.Media Caption form.I18nInput } func newSlideForm(company *auth.Company, label string) *slideForm { return &slideForm{ DefaultLang: company.DefaultLanguage.String(), Label: label, Media: &form.Media{ Input: &form.Input{ Name: "media", }, Label: locale.PgettextNoop("Slide image", "input"), Prompt: locale.PgettextNoop("Set slide image", "action"), }, Caption: form.NewI18nInput(company.Locales, "caption"), } } func (f *slideForm) FillFromDatabase(ctx context.Context, conn *database.Conn, company *auth.Company, mediaID int) error { f.MediaID = mediaID var caption database.RecordArray row := conn.QueryRow(ctx, ` select carousel.caption , carousel.media_id::text , array_agg((lang_tag, i18n.caption)) from campsite_carousel as carousel left join campsite_carousel_i18n as i18n using (campsite_id, media_id) join campsite using (campsite_id) where campsite.label = $1 and campsite.company_id = $2 and carousel.media_id = $3 group by carousel.caption , carousel.media_id::text `, pgx.QueryResultFormats{pgx.BinaryFormatCode}, f.Label, company.ID, mediaID) if err := row.Scan(&f.Caption[f.DefaultLang].Val, &f.Media.Val, &caption); err != nil { return err } if err := f.Caption.FillArray(caption); err != nil { return err } return nil } func (f *slideForm) process(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, act func(ctx context.Context, tx *database.Tx) error) { if err := f.Parse(r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil { panic(err) } else if !ok { if !httplib.IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity) } f.MustRender(w, r, user, company) return } tx := conn.MustBegin(r.Context()) if err := act(r.Context(), tx); err == nil { if err := tx.Commit(r.Context()); err != nil { panic(err) } } else { if err := tx.Rollback(r.Context()); err != nil { panic(err) } panic(err) } httplib.Redirect(w, r, "/admin/campsites/"+f.Label+"/slides", http.StatusSeeOther) } func (f *slideForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } f.Caption.FillValue(r) f.Media.FillValue(r) return nil } func (f *slideForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { v := form.NewValidator(l) if v.CheckRequired(f.Media.Input, l.GettextNoop("Slide image can not be empty.")) { if _, err := v.CheckImageMedia(ctx, conn, f.Media.Input, l.GettextNoop("Slide image must be an image media type.")); err != nil { return false, err } } return v.AllOK, nil } func (f *slideForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "campsite/carousel/form.gohtml", f) } func orderCarousel(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, label string) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := user.VerifyCSRFToken(r); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } input := r.PostForm["media_id"] if len(input) > 0 { var ids []int for _, s := range input { if id, err := strconv.Atoi(s); err == nil { ids = append(ids, id) } else { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } } if err := conn.OrderCampsiteCarousel(r.Context(), label, company.ID, ids); err != nil { panic(err) } } serveCarouselIndex(w, r, user, company, conn, label) }