jordi fita mas eeaa3b415e Add amenities section and public page
This is more or less the same as the campsites, as public information
goes, but for buildings and other amenities that the camping provides
that are not campsites.
2024-01-27 22:51:41 +01:00

340 lines
9.7 KiB
Go

/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package amenity
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, "amenity/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 amenity_carousel as slide
join amenity using (amenity_id)
join media on media.media_id = slide.media_id
left join amenity_carousel_i18n as i18n
on i18n.amenity_id = slide.amenity_id
and i18n.media_id = slide.media_id
and lang_tag = $1
where amenity.label = $2
and amenity.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 amenity_carousel as carousel
join amenity using (amenity_id)
join media on media.media_id = carousel.media_id
where amenity.label = $1
and amenity.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.AddAmenityCarouselSlide(ctx, company.ID, f.Label, 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.TranslateAmenityCarouselSlide(ctx, company.ID, f.Label, 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.RemoveAmenityCarouselSlide(r.Context(), company.ID, label, mediaID); err != nil {
panic(err)
}
httplib.Redirect(w, r, "/admin/amenities/"+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 amenity_carousel as carousel
left join amenity_carousel_i18n as i18n using (amenity_id, media_id)
join amenity using (amenity_id)
where amenity.label = $1
and amenity.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/amenities/"+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, "amenity/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.OrderAmenityCarousel(r.Context(), company.ID, label, ids); err != nil {
panic(err)
}
}
serveCarouselIndex(w, r, user, company, conn, label)
}