Handle the booking cart entirely with HTMx

Besides the dynamic final cart, that was already handled by HTMx, i had
to check the maximum number of guests, whether the accommodation allows
“overflow”, whether dogs are allowed, and that the booking dates were
within the campground’s opening and closing dates.

I could do all of this with AlpineJS, but then i would have to add the
same validation to the backend, prior to accept the payment.  Would not
make more sense to have them in a single place, namely the backend? With
HTMx i can do that.

However, i now have to create the form “piecemeal”, because i may not
have the whole information when the visitor arrives to the booking page,
and i still had the same problem as in commit d2858302efa—parsing the
whole form as is would leave guests and options field empty, rather than
at their minimum values.

One of the fieldsets in that booking form are the arrival and departure
dates, that are the sames we use in the campsite type’s page to
“preselect” these values.  Since now are in a separate struct, i can
reuse the same type and validation logic for both pages, making my
JavaScript code useless, but requiring HTMx.  I think this is a good
tradeoff, in fact.
This commit is contained in:
jordi fita mas 2024-02-10 03:49:44 +01:00
parent b915ba1559
commit e5023a2a41
17 changed files with 1191 additions and 978 deletions

View File

@ -2,49 +2,19 @@ package booking
import (
"context"
"net/http"
"strconv"
"time"
"github.com/jackc/pgx/v4"
"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/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
)
func handleBookingCart(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "":
switch r.Method {
case http.MethodGet:
f := newBookingForm(r.Context(), company, conn, user.Locale)
if err := f.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if cart, err := computeCart(r.Context(), conn, f); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
} else {
cart.MustRender(w, r, user, company)
}
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
default:
http.NotFound(w, r)
}
}
type bookingCart struct {
Lines []*cartLine
Total string
Lines []*cartLine
Total string
Enabled bool
}
type cartLine struct {
@ -53,41 +23,43 @@ type cartLine struct {
Subtotal string
}
func (cart *bookingCart) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderPublicNoLayout(w, r, user, company, "booking/cart.gohtml", cart)
}
func computeCart(ctx context.Context, conn *database.Conn, f *bookingForm) (*bookingCart, error) {
func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, campsiteType string) (*bookingCart, error) {
cart := &bookingCart{
Total: "0.0",
}
campsiteType := f.CampsiteType.String()
if campsiteType == "" {
if f.Dates == nil {
return cart, nil
}
arrivalDate, err := time.Parse(database.ISODateFormat, f.ArrivalDate.Val)
arrivalDate, err := time.Parse(database.ISODateFormat, f.Dates.ArrivalDate.Val)
if err != nil {
return cart, nil
}
departureDate, err := time.Parse(database.ISODateFormat, f.DepartureDate.Val)
departureDate, err := time.Parse(database.ISODateFormat, f.Dates.DepartureDate.Val)
if err != nil {
return cart, nil
}
numAdults, err := strconv.Atoi(f.NumberAdults.Val)
if f.Guests == nil {
return cart, nil
}
numAdults, err := strconv.Atoi(f.Guests.NumberAdults.Val)
if err != nil {
return cart, nil
}
numTeenagers, err := strconv.Atoi(f.NumberTeenagers.Val)
numTeenagers, err := strconv.Atoi(f.Guests.NumberTeenagers.Val)
if err != nil {
return cart, nil
}
numChildren, err := strconv.Atoi(f.NumberChildren.Val)
numChildren, err := strconv.Atoi(f.Guests.NumberChildren.Val)
if err != nil {
return cart, nil
}
optionMap := make(map[int]*campsiteTypeOption)
typeOptions := f.CampsiteTypeOptions[campsiteType]
var typeOptions []*campsiteTypeOption
if f.Options != nil {
typeOptions = f.Options.Options
}
optionIDs := make([]int, 0, len(typeOptions))
optionUnits := make([]int, 0, len(typeOptions))
for _, option := range typeOptions {
@ -103,7 +75,7 @@ func computeCart(ctx context.Context, conn *database.Conn, f *bookingForm) (*boo
row := conn.QueryRow(ctx, `
with per_person as (
select count(*) as num_nights
, sum(cost_per_night)::integer as nights
, sum(cost_per_night * ceiling(($4::numeric + $5::numeric + $6::numeric) / max_campers::numeric)::integer)::integer as nights
, sum(cost_per_adult * $4)::integer as adults
, sum(cost_per_teenager * $5)::integer as teenagers
, sum(cost_per_child * $6)::integer as children
@ -121,10 +93,10 @@ func computeCart(ctx context.Context, conn *database.Conn, f *bookingForm) (*boo
select campsite_type_option_id
, sum(cost_per_night * units)::integer as option_cost
from generate_series($1, $2, interval '1 day') as date(day)
join season_calendar on season_range @> date.day::date
join season_calendar on season_range @> date.day::date
join campsite_type_option_cost using (season_id)
join unnest($7::integer[], $8::integer[]) as option_units(campsite_type_option_id, units) using (campsite_type_option_id)
group by campsite_type_option_id
group by campsite_type_option_id
union all select -1, 0
)
select num_nights
@ -195,6 +167,7 @@ func computeCart(ctx context.Context, conn *database.Conn, f *bookingForm) (*boo
if total != "" {
cart.Total = total
cart.Enabled = f.Guests.Error == nil
}
return cart, nil

View File

@ -9,8 +9,11 @@ import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
@ -37,19 +40,17 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da
case "":
switch r.Method {
case http.MethodGet:
page := newPublicPage(r.Context(), company, conn, user.Locale)
_ = r.ParseForm()
page.Form.CampsiteType.FillValue(r)
page.Form.ArrivalDate.FillValue(r)
page.Form.DepartureDate.FillValue(r)
page, err := newPublicPage(r, company, conn, user.Locale)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
page.MustRender(w, r, user, company, conn)
case http.MethodPost:
makeReservation(w, r, user, company, conn)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
}
case "cart":
handleBookingCart(w, r, user, company, conn)
case "success":
handleSuccessfulPayment(w, r, user, company, conn)
case "failure":
@ -61,8 +62,8 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da
}
func makeReservation(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
f := newBookingForm(r.Context(), company, conn, user.Locale)
if err := f.Parse(r); err != nil {
f, err := newBookingForm(r, company, conn, user.Locale)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
@ -76,17 +77,12 @@ func makeReservation(w http.ResponseWriter, r *http.Request, user *auth.User, co
page.MustRender(w, r, user, company, conn)
return
}
cart, err := computeCart(r.Context(), conn, f)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
schema := httplib.Protocol(r)
authority := httplib.Host(r)
request := &redsys.Request{
TransactionType: redsys.TransactionTypeCharge,
Amount: cart.Total,
Amount: f.Cart.Total,
OrderNumber: randomOrderNumber(),
Product: "Test Booking",
SuccessURL: fmt.Sprintf("%s://%s/%s/booking/success", schema, authority, user.Locale.Language),
@ -113,11 +109,14 @@ func randomOrderNumber() string {
type publicPage struct {
*template.PublicPage
Form *bookingForm
Cart *bookingCart
}
func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Conn, l *locale.Locale) *publicPage {
return newPublicPageWithForm(newBookingForm(ctx, company, conn, l))
func newPublicPage(r *http.Request, company *auth.Company, conn *database.Conn, l *locale.Locale) (*publicPage, error) {
f, err := newBookingForm(r, company, conn, l)
if err != nil {
return nil, err
}
return newPublicPageWithForm(f), nil
}
func newPublicPageWithForm(form *bookingForm) *publicPage {
@ -130,32 +129,52 @@ func newPublicPageWithForm(form *bookingForm) *publicPage {
func (p *publicPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
p.Setup(r, user, company, conn)
var err error
p.Cart, err = computeCart(r.Context(), conn, p.Form)
if err != nil {
panic(err)
}
template.MustRenderPublicFiles(w, r, user, company, p, "booking/form.gohtml", "booking/cart.gohtml")
if httplib.IsHTMxRequest(r) {
template.MustRenderPublicNoLayout(w, r, user, company, "booking/fields.gohtml", p)
} else {
template.MustRenderPublicFiles(w, r, user, company, p, "booking/page.gohtml", "booking/fields.gohtml")
}
}
type bookingForm struct {
FullName *form.Input
Address *form.Input
PostalCode *form.Input
City *form.Input
Country *form.Select
Email *form.Input
Phone *form.Input
CampsiteType *form.Select
CampsiteTypeOptions map[string][]*campsiteTypeOption
ArrivalDate *form.Input
DepartureDate *form.Input
NumberAdults *form.Input
NumberTeenagers *form.Input
NumberChildren *form.Input
NumberDogs *form.Input
ZonePreferences map[string]*form.Input
ACSICard *form.Checkbox
Agreement *form.Checkbox
CampsiteType *form.Select
Dates *DateFields
Guests *bookingGuestFields
Options *bookingOptionFields
Customer *bookingCustomerFields
Cart *bookingCart
}
type DateFields struct {
MinNights int
MaxNights int
ArrivalDate *bookingDateInput
DepartureDate *bookingDateInput
}
type bookingDateInput struct {
*form.Input
MinDate time.Time
MaxDate time.Time
}
type bookingGuestFields struct {
MaxGuests int
OverflowAllowed bool
NumberAdults *form.Input
NumberTeenagers *form.Input
NumberChildren *form.Input
NumberDogs *form.Input
Error error
}
type bookingOptionFields struct {
Legend string
ZonePreferences *form.Input
Options []*campsiteTypeOption
}
type campsiteTypeOption struct {
@ -166,46 +185,331 @@ type campsiteTypeOption struct {
Input *form.Input
}
func newBookingForm(ctx context.Context, company *auth.Company, conn *database.Conn, l *locale.Locale) *bookingForm {
var typeSelectOptions []*form.Option
zonePreferences := make(map[string]*form.Input)
rows, err := conn.Query(ctx, `
select type.slug
, coalesce(i18n.name, type.name) as l10n_name
, ask_zone_preferences
from campsite_type as type
left join campsite_type_i18n as i18n
on type.campsite_type_id = i18n.campsite_type_id
and i18n.lang_tag = $1
where company_id = $2
and active
order by position, l10n_name
`, l.Language, company.ID)
if err != nil {
panic(err)
}
defer rows.Close()
type bookingCustomerFields struct {
FullName *form.Input
Address *form.Input
PostalCode *form.Input
City *form.Input
Country *form.Select
Email *form.Input
Phone *form.Input
ACSICard *form.Checkbox
Agreement *form.Checkbox
}
for rows.Next() {
option := &form.Option{}
var ask bool
if err = rows.Scan(&option.Value, &option.Label, &ask); err != nil {
panic(err)
func newBookingForm(r *http.Request, company *auth.Company, conn *database.Conn, l *locale.Locale) (*bookingForm, error) {
if err := r.ParseForm(); err != nil {
return nil, err
}
f := &bookingForm{
CampsiteType: &form.Select{
Name: "campsite_type",
Options: form.MustGetOptions(r.Context(), conn, "select type.slug, coalesce(i18n.name, type.name) as l10n_name from campsite_type as type left join campsite_type_i18n as i18n on type.campsite_type_id = i18n.campsite_type_id and i18n.lang_tag = $1 where company_id = $2 and active order by position, l10n_name", l.Language, company.ID),
},
}
f.CampsiteType.FillValue(r)
campsiteType := f.CampsiteType.String()
if campsiteType == "" {
return f, nil
}
if !f.CampsiteType.ValidOptionsSelected() {
f.CampsiteType.Error = errors.New(l.Gettext("Selected campsite type is not valid."))
return f, nil
}
var err error
f.Dates, err = NewDateFields(r.Context(), conn, campsiteType)
if err != nil {
return nil, err
}
f.Dates.FillValues(r, l)
if f.Dates.ArrivalDate.Val == "" || f.Dates.ArrivalDate.Error != nil || f.Dates.DepartureDate.Val == "" || f.Dates.DepartureDate.Error != nil {
return f, nil
}
f.Guests, err = newBookingGuestFields(r.Context(), conn, campsiteType)
if err != nil {
return nil, err
}
f.Guests.FillValues(r, l)
f.Options, err = newBookingOptionFields(r.Context(), conn, campsiteType, l)
if err != nil {
return nil, err
}
if f.Options != nil {
f.Options.FillValues(r)
}
f.Customer = newBookingCustomerFields(r.Context(), conn, l)
f.Customer.FillValues(r)
f.Cart, err = newBookingCart(r.Context(), conn, f, campsiteType)
if err != nil {
return nil, err
}
return f, nil
}
func (f *bookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
v := form.NewValidator(l)
if f.Dates == nil {
return false, errors.New("no booking date fields")
}
if f.Guests == nil {
return false, errors.New("no guests fields")
}
if f.Customer == nil {
return false, errors.New("no customer fields")
}
v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid."))
f.Dates.Valid(v, l)
f.Guests.Valid(v, l)
f.Options.Valid(v, l)
if f.Options != nil {
if err := f.Customer.Valid(ctx, conn, v, l); err != nil {
return false, err
}
typeSelectOptions = append(typeSelectOptions, option)
if ask {
zonePreferences[option.Value] = &form.Input{
Name: "zone_preferences_" + option.Value,
}
return v.AllOK, nil
}
func NewDateFields(ctx context.Context, conn *database.Conn, campsiteType string) (*DateFields, error) {
row := conn.QueryRow(ctx, `
select lower(bookable_nights),
upper(bookable_nights) - 1,
greatest(min(lower(season_range)), current_timestamp::date),
max(upper(season_range))
from campsite_type
join campsite_type_cost using (campsite_type_id)
join season_calendar using (season_id)
where campsite_type.slug = $1
and season_range >> daterange(date_trunc('year', current_timestamp)::date, date_trunc('year', current_timestamp)::date + 1)
group by bookable_nights;
`, campsiteType)
f := &DateFields{
ArrivalDate: &bookingDateInput{
Input: &form.Input{Name: "arrival_date"},
},
DepartureDate: &bookingDateInput{
Input: &form.Input{Name: "departure_date"},
},
}
if err := row.Scan(&f.MinNights, &f.MaxNights, &f.ArrivalDate.MinDate, &f.DepartureDate.MaxDate); err != nil {
return nil, err
}
f.ArrivalDate.MaxDate = f.DepartureDate.MaxDate.AddDate(0, 0, -f.MinNights)
f.DepartureDate.MinDate = f.ArrivalDate.MinDate.AddDate(0, 0, f.MinNights)
return f, nil
}
func (f *DateFields) FillValues(r *http.Request, l *locale.Locale) {
f.ArrivalDate.FillValue(r)
f.DepartureDate.FillValue(r)
if f.ArrivalDate.Val != "" {
arrivalDate, err := time.Parse(database.ISODateFormat, f.ArrivalDate.Val)
if err != nil {
f.ArrivalDate.Error = errors.New(l.Gettext("Arrival date must be a valid date."))
return
}
f.DepartureDate.MinDate = arrivalDate.AddDate(0, 0, f.MinNights)
maxDate := arrivalDate.AddDate(0, 0, f.MaxNights)
if maxDate.Before(f.DepartureDate.MaxDate) {
f.DepartureDate.MaxDate = maxDate
}
if f.DepartureDate.Val == "" {
f.DepartureDate.Val = f.DepartureDate.MinDate.Format(database.ISODateFormat)
} else {
departureDate, err := time.Parse(database.ISODateFormat, f.DepartureDate.Val)
if err != nil {
f.DepartureDate.Error = errors.New(l.Gettext("Departure date must be a valid date."))
return
}
if departureDate.Before(f.DepartureDate.MinDate) {
f.DepartureDate.Val = f.DepartureDate.MinDate.Format(database.ISODateFormat)
} else if departureDate.After(f.DepartureDate.MaxDate) {
f.DepartureDate.Val = f.DepartureDate.MaxDate.Format(database.ISODateFormat)
}
}
}
if rows.Err() != nil {
panic(rows.Err())
}
func (f *DateFields) Valid(v *form.Validator, l *locale.Locale) {
var validBefore bool
if v.CheckRequired(f.ArrivalDate.Input, l.GettextNoop("Arrival date can not be empty")) {
if v.CheckValidDate(f.ArrivalDate.Input, l.GettextNoop("Arrival date must be a valid date.")) {
if v.CheckMinDate(f.ArrivalDate.Input, f.ArrivalDate.MinDate, fmt.Sprintf(l.Gettext("Arrival date must be %s or after."), f.ArrivalDate.MinDate.Format(database.ISODateFormat))) {
v.CheckMaxDate(f.ArrivalDate.Input, f.ArrivalDate.MaxDate, fmt.Sprintf(l.Gettext("Arrival date must be %s or before."), f.ArrivalDate.MaxDate.Format(database.ISODateFormat)))
}
}
}
if v.CheckRequired(f.DepartureDate.Input, l.GettextNoop("Departure date can not be empty")) {
if v.CheckValidDate(f.DepartureDate.Input, l.GettextNoop("Departure date must be a valid date.")) && validBefore {
if v.CheckMinDate(f.DepartureDate.Input, f.DepartureDate.MinDate, fmt.Sprintf(l.Gettext("Departure date must be %s or after."), f.DepartureDate.MinDate.Format(database.ISODateFormat))) {
v.CheckMaxDate(f.DepartureDate.Input, f.DepartureDate.MaxDate, fmt.Sprintf(l.Gettext("Departure date must be %s or before."), f.DepartureDate.MaxDate.Format(database.ISODateFormat)))
}
}
}
}
func newBookingGuestFields(ctx context.Context, conn *database.Conn, campsiteType string) (*bookingGuestFields, error) {
f := &bookingGuestFields{
NumberAdults: &form.Input{Name: "number_adults"},
NumberTeenagers: &form.Input{Name: "number_teenagers"},
NumberChildren: &form.Input{Name: "number_children"},
}
row := conn.QueryRow(ctx, `
select max_campers
, overflow_allowed
, dogs_allowed
from campsite_type
where slug = $1
`, campsiteType)
var dogsAllowed bool
if err := row.Scan(&f.MaxGuests, &f.OverflowAllowed, &dogsAllowed); err != nil {
return nil, err
}
if dogsAllowed {
f.NumberDogs = &form.Input{Name: "number_dogs"}
}
return f, nil
}
func (f *bookingGuestFields) FillValues(r *http.Request, l *locale.Locale) {
numGuests := 0
numGuests += fillNumericField(f.NumberAdults, r, 1)
numGuests += fillNumericField(f.NumberTeenagers, r, 0)
numGuests += fillNumericField(f.NumberChildren, r, 0)
if f.NumberDogs != nil {
fillNumericField(f.NumberDogs, r, 0)
}
if !f.OverflowAllowed && numGuests > f.MaxGuests {
f.Error = fmt.Errorf(l.Gettext("There can be at most %d guests in this accommodation."), f.MaxGuests)
}
}
func fillNumericField(input *form.Input, r *http.Request, min int) int {
input.FillValue(r)
if input.Val == "" {
input.Val = strconv.Itoa(min)
return min
}
val, err := strconv.Atoi(input.Val)
if err != nil {
input.Val = strconv.Itoa(min)
val = min
}
return val
}
func (f *bookingGuestFields) Valid(v *form.Validator, l *locale.Locale) {
if v.CheckRequired(f.NumberAdults, l.GettextNoop("Number of adults can not be empty")) {
if v.CheckValidInteger(f.NumberAdults, l.GettextNoop("Number of adults must be an integer.")) {
v.CheckMinInteger(f.NumberAdults, 1, l.GettextNoop("There must be at least one adult."))
}
}
if v.CheckRequired(f.NumberTeenagers, l.GettextNoop("Number of teenagers can not be empty")) {
if v.CheckValidInteger(f.NumberTeenagers, l.GettextNoop("Number of teenagers must be an integer.")) {
v.CheckMinInteger(f.NumberTeenagers, 0, l.GettextNoop("Number of teenagers can not be negative."))
}
}
if v.CheckRequired(f.NumberChildren, l.GettextNoop("Number of children can not be empty")) {
if v.CheckValidInteger(f.NumberChildren, l.GettextNoop("Number of children must be an integer.")) {
v.CheckMinInteger(f.NumberChildren, 0, l.GettextNoop("Number of children can not be negative."))
}
}
if v.CheckRequired(f.NumberDogs, l.GettextNoop("Number of dogs can not be empty")) {
if v.CheckValidInteger(f.NumberDogs, l.GettextNoop("Number of dogs must be an integer.")) {
v.CheckMinInteger(f.NumberDogs, 0, l.GettextNoop("Number of dogs can not be negative."))
}
}
}
func newBookingOptionFields(ctx context.Context, conn *database.Conn, campsiteType string, l *locale.Locale) (*bookingOptionFields, error) {
f := &bookingOptionFields{}
row := conn.QueryRow(ctx, `
select coalesce(i18n.name, campsite_type.name)
, ask_zone_preferences
from campsite_type
left join campsite_type_i18n as i18n on i18n.campsite_type_id = campsite_type.campsite_type_id and i18n.lang_tag = $1
where slug = $2
`, l.Language, campsiteType)
var askZonePreferences bool
if err := row.Scan(&f.Legend, &askZonePreferences); err != nil {
return nil, err
}
if askZonePreferences {
f.ZonePreferences = &form.Input{Name: "zone_preferences"}
}
return &bookingForm{
CampsiteTypeOptions: mustGetCampsiteTypeOptions(ctx, conn, company, l),
ZonePreferences: zonePreferences,
rows, err := conn.Query(ctx, `
select option.campsite_type_option_id
, 'campsite_type_option_' || option.campsite_type_option_id
, coalesce(i18n.name, option.name) as l10_name
, lower(range)::text
, lower(range)
, upper(range)
from campsite_type_option as option
join campsite_type using (campsite_type_id)
left join campsite_type_option_i18n as i18n on i18n.campsite_type_option_id = option.campsite_type_option_id and i18n.lang_tag = $1
where slug = $2
and campsite_type.active
order by option.position, l10_name
`, l.Language, campsiteType)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
option := &campsiteTypeOption{
Input: &form.Input{},
}
if err = rows.Scan(&option.ID, &option.Input.Name, &option.Label, &option.Input.Val, &option.Min, &option.Max); err != nil {
return nil, err
}
f.Options = append(f.Options, option)
}
if rows.Err() != nil {
return nil, rows.Err()
}
if f.ZonePreferences == nil && len(f.Options) == 0 {
return nil, nil
}
return f, nil
}
func (f *bookingOptionFields) FillValues(r *http.Request) {
if f.ZonePreferences != nil {
f.ZonePreferences.FillValue(r)
}
for _, option := range f.Options {
fillNumericField(option.Input, r, option.Min)
}
}
func (f *bookingOptionFields) Valid(v *form.Validator, l *locale.Locale) {
for _, option := range f.Options {
if v.CheckRequired(option.Input, fmt.Sprintf(l.Gettext("%s can not be empty"), option.Label)) {
if v.CheckValidInteger(option.Input, fmt.Sprintf(l.Gettext("%s must be an integer."), option.Label)) {
if v.CheckMinInteger(option.Input, option.Min, fmt.Sprintf(l.Gettext("%s must be %d or greater."), option.Label, option.Min)) {
v.CheckMaxInteger(option.Input, option.Max, fmt.Sprintf(l.Gettext("%s must be at most %d."), option.Label, option.Max))
}
}
}
}
}
func newBookingCustomerFields(ctx context.Context, conn *database.Conn, l *locale.Locale) *bookingCustomerFields {
return &bookingCustomerFields{
FullName: &form.Input{
Name: "full_name",
},
@ -228,32 +532,6 @@ func newBookingForm(ctx context.Context, company *auth.Company, conn *database.C
Phone: &form.Input{
Name: "phone",
},
CampsiteType: &form.Select{
Name: "campsite_type",
Options: typeSelectOptions,
},
ArrivalDate: &form.Input{
Name: "arrival_date",
},
DepartureDate: &form.Input{
Name: "departure_date",
},
NumberAdults: &form.Input{
Name: "number_adults",
Val: "1",
},
NumberTeenagers: &form.Input{
Name: "number_teenagers",
Val: "0",
},
NumberChildren: &form.Input{
Name: "number_children",
Val: "0",
},
NumberDogs: &form.Input{
Name: "number_dogs",
Val: "0",
},
ACSICard: &form.Checkbox{
Name: "acsi_card",
},
@ -263,47 +541,7 @@ func newBookingForm(ctx context.Context, company *auth.Company, conn *database.C
}
}
func mustGetCampsiteTypeOptions(ctx context.Context, conn *database.Conn, company *auth.Company, l *locale.Locale) map[string][]*campsiteTypeOption {
rows, err := conn.Query(ctx, `
select option.campsite_type_option_id
, 'campsite_type_option_' || option.campsite_type_option_id
, slug
, coalesce(i18n.name, option.name) as l10_name
, lower(range)::text
, lower(range)
, upper(range)
from campsite_type_option as option
join campsite_type using (campsite_type_id)
left join campsite_type_option_i18n as i18n on i18n.campsite_type_option_id = option.campsite_type_option_id and i18n.lang_tag = $1
where company_id = $2
and campsite_type.active
order by option.position, l10_name
`, l.Language, company.ID)
if err != nil {
panic(err)
}
defer rows.Close()
options := make(map[string][]*campsiteTypeOption)
for rows.Next() {
var slug string
option := &campsiteTypeOption{
Input: &form.Input{},
}
if err = rows.Scan(&option.ID, &option.Input.Name, &slug, &option.Label, &option.Input.Val, &option.Min, &option.Max); err != nil {
panic(err)
}
options[slug] = append(options[slug], option)
}
if rows.Err() != nil {
panic(rows.Err())
}
return options
}
func (f *bookingForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
func (f *bookingCustomerFields) FillValues(r *http.Request) {
f.FullName.FillValue(r)
f.Address.FillValue(r)
f.PostalCode.FillValue(r)
@ -311,29 +549,11 @@ func (f *bookingForm) Parse(r *http.Request) error {
f.Country.FillValue(r)
f.Email.FillValue(r)
f.Phone.FillValue(r)
f.CampsiteType.FillValue(r)
f.ArrivalDate.FillValue(r)
f.DepartureDate.FillValue(r)
f.NumberAdults.FillValue(r)
f.NumberTeenagers.FillValue(r)
f.NumberChildren.FillValue(r)
f.NumberDogs.FillValue(r)
f.ACSICard.FillValue(r)
f.Agreement.FillValue(r)
for _, preferences := range f.ZonePreferences {
preferences.FillValue(r)
}
for _, options := range f.CampsiteTypeOptions {
for _, option := range options {
option.Input.FillValue(r)
}
}
return nil
}
func (f *bookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
v := form.NewValidator(l)
func (f *bookingCustomerFields) Valid(ctx context.Context, conn *database.Conn, v *form.Validator, l *locale.Locale) error {
var country string
if v.CheckSelectedOptions(f.Country, l.GettextNoop("Selected country is not valid.")) {
country = f.Country.Selected[0]
@ -345,7 +565,7 @@ func (f *bookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale.
if country != "" && f.PostalCode.Val != "" {
if _, err := v.CheckValidPostalCode(ctx, conn, f.PostalCode, country, l.GettextNoop("This postal code is not valid.")); err != nil {
return false, err
return err
}
}
if v.CheckRequired(f.Email, l.GettextNoop("Email can not be empty.")) {
@ -353,53 +573,10 @@ func (f *bookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale.
}
if v.CheckRequired(f.Phone, l.GettextNoop("Phone can not be empty.")) {
if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil {
return false, err
}
}
var validBefore bool
v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid."))
if v.CheckRequired(f.ArrivalDate, l.GettextNoop("Arrival date can not be empty")) {
if v.CheckValidDate(f.ArrivalDate, l.GettextNoop("Arrival date must be a valid date.")) {
validBefore = true
}
}
if v.CheckRequired(f.DepartureDate, l.GettextNoop("Departure date can not be empty")) {
if v.CheckValidDate(f.DepartureDate, l.GettextNoop("Departure date must be a valid date.")) && validBefore {
v.CheckDateAfter(f.DepartureDate, f.ArrivalDate, l.GettextNoop("The departure date must be after the arrival date."))
return err
}
}
if v.CheckRequired(f.NumberAdults, l.GettextNoop("Number of adults can not be empty")) {
if v.CheckValidInteger(f.NumberAdults, l.GettextNoop("Number of adults must be an integer.")) {
v.CheckMinInteger(f.NumberAdults, 1, l.GettextNoop("There must be at least one adult."))
}
}
if v.CheckRequired(f.NumberTeenagers, l.GettextNoop("Number of teenagers can not be empty")) {
if v.CheckValidInteger(f.NumberTeenagers, l.GettextNoop("Number of teenagers must be an integer.")) {
v.CheckMinInteger(f.NumberTeenagers, 0, l.GettextNoop("Number of teenagers can not be negative."))
}
}
if v.CheckRequired(f.NumberTeenagers, l.GettextNoop("Number of children can not be empty")) {
if v.CheckValidInteger(f.NumberTeenagers, l.GettextNoop("Number of children must be an integer.")) {
v.CheckMinInteger(f.NumberTeenagers, 0, l.GettextNoop("Number of children can not be negative."))
}
}
if v.CheckRequired(f.NumberTeenagers, l.GettextNoop("Number of dogs can not be empty")) {
if v.CheckValidInteger(f.NumberTeenagers, l.GettextNoop("Number of dogs must be an integer.")) {
v.CheckMinInteger(f.NumberTeenagers, 0, l.GettextNoop("Number of dogs can not be negative."))
}
}
v.Check(f.Agreement, f.Agreement.Checked, l.GettextNoop("It is mandatory to agree to the reservation conditions."))
for _, options := range f.CampsiteTypeOptions {
for _, option := range options {
if v.CheckRequired(option.Input, fmt.Sprintf(l.Gettext("%s can not be empty"), option.Label)) {
if v.CheckValidInteger(option.Input, fmt.Sprintf(l.Gettext("%s must be an integer."), option.Label)) {
if v.CheckMinInteger(option.Input, option.Min, fmt.Sprintf(l.Gettext("%s must be %d or greater."), option.Label, option.Min)) {
v.CheckMaxInteger(option.Input, option.Max, fmt.Sprintf(l.Gettext("%s must be at most %d."), option.Label, option.Max))
}
}
}
}
}
return v.AllOK, nil
return nil
}

View File

@ -7,14 +7,15 @@ package types
import (
"context"
"github.com/jackc/pgx/v4"
gotemplate "html/template"
"net/http"
"time"
"github.com/jackc/pgx/v4"
"golang.org/x/text/language"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/booking"
"dev.tandem.ws/tandem/camper/pkg/carousel"
"dev.tandem.ws/tandem/camper/pkg/database"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
@ -54,6 +55,21 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
case "dates":
switch r.Method {
case http.MethodGet:
bookingDates, err := booking.NewDateFields(r.Context(), conn, typeUuid)
if err != nil {
panic(err)
}
if err := r.ParseForm(); err != nil {
panic(err)
}
bookingDates.FillValues(r, user.Locale)
template.MustRenderPublicNoLayout(w, r, user, company, "campsite/dates.gohtml", bookingDates)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
default:
http.NotFound(w, r)
}
@ -78,8 +94,7 @@ type publicPage struct {
AdditionalInfo gotemplate.HTML
CheckIn string
CheckOut string
MinNights int
MaxNights int
BookingDates *booking.DateFields
}
type typePrice struct {
@ -124,8 +139,6 @@ func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Co
, coalesce(i18n.additional_info, campsite_type.additional_info)::text as l10n_description
, coalesce(i18n.check_in, campsite_type.check_in)::text as l10n_check_in
, coalesce(i18n.check_out, campsite_type.check_out)::text as l10n_check_out
, lower(bookable_nights)
, upper(bookable_nights) - 1
, dogs_allowed
, '3.50' as dogs_prices
from campsite_type
@ -142,8 +155,6 @@ func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Co
&page.AdditionalInfo,
&page.CheckIn,
&page.CheckOut,
&page.MinNights,
&page.MaxNights,
&page.DogsAllowed,
&page.DogsPrice,
); err != nil {
@ -152,6 +163,10 @@ func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Co
if err = conn.QueryRow(ctx, "select to_price(tourist_tax, $1) from company where company_id = $2", company.DecimalDigits, company.ID).Scan(&page.TouristTax); err != nil {
return nil, err
}
page.BookingDates, err = booking.NewDateFields(ctx, conn, slug)
if err != nil {
return nil, err
}
page.Prices, err = collectPrices(ctx, conn, loc.Language, slug, page.Name)
if err != nil {
return nil, err
@ -280,5 +295,5 @@ func collectFeatures(ctx context.Context, conn *database.Conn, language language
func (p *publicPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
p.Setup(r, user, company, conn)
template.MustRenderPublicFiles(w, r, user, company, p, "campsite/type.gohtml", "campsite/calendar.gohtml")
template.MustRenderPublicFiles(w, r, user, company, p, "campsite/type.gohtml", "campsite/calendar.gohtml", "campsite/dates.gohtml")
}

View File

@ -48,7 +48,7 @@ func (s *Select) FillValue(r *http.Request) {
s.Selected = r.Form[s.Name]
}
func (s *Select) validOptionsSelected() bool {
func (s *Select) ValidOptionsSelected() bool {
for _, selected := range s.Selected {
if !s.isValidOption(selected) {
return false

View File

@ -130,12 +130,22 @@ func (v *Validator) CheckDateAfter(field *Input, beforeField *Input, message str
return v.Check(field, date.After(before), message)
}
func (v *Validator) CheckMinDate(field *Input, min time.Time, message string) bool {
date, _ := time.Parse(database.ISODateFormat, field.Val)
return v.Check(field, !date.Before(min), message)
}
func (v *Validator) CheckMaxDate(field *Input, max time.Time, message string) bool {
date, _ := time.Parse(database.ISODateFormat, field.Val)
return v.Check(field, !date.After(max), message)
}
func (v *Validator) CheckPasswordConfirmation(password *Input, confirm *Input, message string) bool {
return v.Check(confirm, password.Val == confirm.Val, message)
}
func (v *Validator) CheckSelectedOptions(field *Select, message string) bool {
return v.Check(field, field.validOptionsSelected(), message)
return v.Check(field, field.ValidOptionsSelected(), message)
}
func (v *Validator) CheckImageFile(field *File, message string) bool {

View File

@ -107,11 +107,8 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ
"formatDate": func(time time.Time) template.HTML {
return template.HTML(`<time datetime="` + time.Format(database.ISODateFormat) + `">` + time.Format("02/01/2006") + "</time>")
},
"today": func() string {
return time.Now().Format(database.ISODateFormat)
},
"tomorrow": func() string {
return time.Now().AddDate(0, 0, 1).Format(database.ISODateFormat)
"formatDateAttr": func(time time.Time) string {
return time.Format(database.ISODateFormat)
},
"queryEscape": func(s string) string {
return url.QueryEscape(s)

264
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-02-04 06:23+0100\n"
"POT-Creation-Date: 2024-02-10 03:29+0100\n"
"PO-Revision-Date: 2024-02-06 10:04+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n"
@ -95,7 +95,7 @@ msgid "The campsite offers many different services."
msgstr "El càmping disposa de diversos serveis."
#: web/templates/public/amenity.gohtml:39
#: web/templates/public/campsite/type.gohtml:113
#: web/templates/public/campsite/type.gohtml:106
#: web/templates/public/campsite/page.gohtml:39
msgctxt "title"
msgid "Features"
@ -154,89 +154,79 @@ msgstr "A menys duna hora de <strong>Girona</strong>, a una de <strong>La Bis
msgid "Discover"
msgstr "Descobreix"
#: web/templates/public/campsite/type.gohtml:41
msgctxt "input"
msgid "Check-in Date"
msgstr "Data dentrada"
#: web/templates/public/campsite/type.gohtml:47
msgctxt "input"
msgid "Check-out Date"
msgstr "Data de sortida"
#: web/templates/public/campsite/type.gohtml:56
#: web/templates/public/booking/cart.gohtml:25
#: web/templates/public/campsite/type.gohtml:49
#: web/templates/public/booking/fields.gohtml:240
msgctxt "action"
msgid "Book"
msgstr "Reserva"
#: web/templates/public/campsite/type.gohtml:64
#: web/templates/public/campsite/type.gohtml:57
#: web/templates/admin/season/index.gohtml:54
msgctxt "title"
msgid "Calendar"
msgstr "Calendari"
#: web/templates/public/campsite/type.gohtml:75
#: web/templates/public/campsite/type.gohtml:68
#: web/templates/admin/campsite/type/form.gohtml:143
#: web/templates/admin/campsite/type/option/form.gohtml:70
msgctxt "title"
msgid "Prices"
msgstr "Preus"
#: web/templates/public/campsite/type.gohtml:88
#: web/templates/public/campsite/type.gohtml:81
msgid "%s: %s/night"
msgstr "%s: %s/nit"
#: web/templates/public/campsite/type.gohtml:90
#: web/templates/public/campsite/type.gohtml:83
msgid "%s/night"
msgstr "%s/nit"
#: web/templates/public/campsite/type.gohtml:95
#: web/templates/public/campsite/type.gohtml:88
msgid "*Minimum %d nights per stay"
msgstr "*Mínim %d nits per estada"
#: web/templates/public/campsite/type.gohtml:100
#: web/templates/public/campsite/type.gohtml:93
msgid "10 % VAT included."
msgstr "IVA del 10 % inclòs."
#: web/templates/public/campsite/type.gohtml:101
#: web/templates/public/campsite/type.gohtml:94
msgid "Tourist tax: %s/night per person aged 17 or older."
msgstr "Impost turístic: %s/nit per persona major de 16 anys."
#: web/templates/public/campsite/type.gohtml:103
#: web/templates/public/campsite/type.gohtml:96
msgid "Dogs: %s/night, tied, accompanied, and minimal barking."
msgstr "Gossos: %s/nit, lligats, acompanyats i el mínim de lladrucs."
#: web/templates/public/campsite/type.gohtml:105
#: web/templates/public/campsite/type.gohtml:98
msgid "No dogs allowed."
msgstr "No es permeten gossos."
#: web/templates/public/campsite/type.gohtml:124
#: web/templates/public/campsite/type.gohtml:117
msgctxt "title"
msgid "Info"
msgstr "Informació"
#: web/templates/public/campsite/type.gohtml:128
#: web/templates/public/campsite/type.gohtml:121
msgctxt "title"
msgid "Facilities"
msgstr "Equipaments"
#: web/templates/public/campsite/type.gohtml:132
#: web/templates/public/campsite/type.gohtml:125
msgctxt "title"
msgid "Description"
msgstr "Descripció"
#: web/templates/public/campsite/type.gohtml:136
#: web/templates/public/campsite/type.gohtml:129
msgctxt "title"
msgid "Additional Information"
msgstr "Informació addicional"
#: web/templates/public/campsite/type.gohtml:139
#: web/templates/public/campsite/type.gohtml:132
msgctxt "time"
msgid "Check-in"
msgstr "Entrada"
#: web/templates/public/campsite/type.gohtml:143
#: web/templates/public/campsite/type.gohtml:136
msgctxt "time"
msgid "Check-out"
msgstr "Sortida"
@ -283,6 +273,18 @@ msgctxt "day"
msgid "Sun"
msgstr "dg"
#: web/templates/public/campsite/dates.gohtml:4
#: web/templates/public/booking/fields.gohtml:26
msgctxt "input"
msgid "Arrival date"
msgstr "Data darribada"
#: web/templates/public/campsite/dates.gohtml:15
#: web/templates/public/booking/fields.gohtml:37
msgctxt "input"
msgid "Departure date"
msgstr "Data de sortida"
#: web/templates/public/surroundings.gohtml:30
msgctxt "title"
msgid "What to Do Outside the Campsite?"
@ -516,8 +518,7 @@ msgid "Campsites"
msgstr "Allotjaments"
#: web/templates/public/layout.gohtml:70
#: web/templates/public/booking/form.gohtml:7
#: web/templates/public/booking/form.gohtml:16
#: web/templates/public/booking/page.gohtml:7
msgctxt "title"
msgid "Booking"
msgstr "Reserva"
@ -536,119 +537,113 @@ msgstr "Obertura"
msgid "<abbr title=\"Catalonia Tourism Registry\">RTC</abbr> <abbr title=\"Number\">#</abbr>%s"
msgstr "<abbr title=\"Número\">Núm.</abbr> <abbr title=\"Registre de Turisme de Catalunya\">RTC</abbr> %s"
#: web/templates/public/booking/form.gohtml:29
#: web/templates/public/booking/fields.gohtml:13
msgctxt "title"
msgid "Accommodation"
msgstr "Allotjaments"
#: web/templates/public/booking/form.gohtml:43
#: web/templates/public/booking/fields.gohtml:23
msgctxt "title"
msgid "Booking Period"
msgstr "Període de reserva"
#: web/templates/public/booking/form.gohtml:46
msgctxt "input"
msgid "Arrival date"
msgstr "Data darribada"
#: web/templates/public/booking/form.gohtml:57
msgctxt "input"
msgid "Departure date"
msgstr "Data de sortida"
#: web/templates/public/booking/form.gohtml:72
#: web/templates/public/booking/fields.gohtml:50
msgctxt "title"
msgid "Guests"
msgstr "Hostes"
#: web/templates/public/booking/form.gohtml:76
#: web/templates/public/booking/fields.gohtml:54
msgctxt "input"
msgid "Adults aged 17 or older"
msgstr "Adults de 17 anys o més"
#: web/templates/public/booking/form.gohtml:86
#: web/templates/public/booking/fields.gohtml:65
msgctxt "input"
msgid "Teenagers from 11 to 16 years old"
msgstr "Adolescents dentre 11 i 16 anys"
#: web/templates/public/booking/form.gohtml:96
#: web/templates/public/booking/fields.gohtml:76
msgctxt "input"
msgid "Children from 2 to 10 years old"
msgstr "Nens dentre 2 i 10 anys)"
#: web/templates/public/booking/form.gohtml:106
#: web/templates/public/booking/fields.gohtml:91
msgctxt "input"
msgid "Dogs"
msgstr "Gossos"
#: web/templates/public/booking/form.gohtml:127
#: web/templates/public/booking/fields.gohtml:100
msgid "Note: This accommodation does <strong>not</strong> allow dogs."
msgstr "Nota: A aquest allotjament <strong>no</strong> shi permeten gossos."
#: web/templates/public/booking/fields.gohtml:110
msgctxt "input"
msgid "Area preferences (optional)"
msgstr "Preferències dàrea (opcional)"
#: web/templates/public/booking/form.gohtml:129
#: web/templates/public/booking/fields.gohtml:112
msgid "Campground map"
msgstr "Mapa del càmping"
#: web/templates/public/booking/form.gohtml:156
#: web/templates/public/booking/fields.gohtml:135
msgctxt "title"
msgid "Customer Details"
msgstr "Detalls del client"
#: web/templates/public/booking/form.gohtml:159
#: web/templates/public/booking/fields.gohtml:138
msgctxt "input"
msgid "Full name"
msgstr "Nom i cognoms"
#: web/templates/public/booking/form.gohtml:168
#: web/templates/public/booking/fields.gohtml:147
msgctxt "input"
msgid "Address (optional)"
msgstr "Adreça (opcional)"
#: web/templates/public/booking/form.gohtml:177
#: web/templates/public/booking/fields.gohtml:156
msgctxt "input"
msgid "Postcode (optional)"
msgstr "Codi postal (opcional)"
#: web/templates/public/booking/form.gohtml:186
#: web/templates/public/booking/fields.gohtml:165
msgctxt "input"
msgid "Town or village (optional)"
msgstr "Població (opcional)"
#: web/templates/public/booking/form.gohtml:195
#: web/templates/public/booking/fields.gohtml:174
#: web/templates/admin/taxDetails.gohtml:101
msgctxt "input"
msgid "Country"
msgstr "País"
#: web/templates/public/booking/form.gohtml:198
#: web/templates/public/booking/fields.gohtml:177
msgid "Choose a country"
msgstr "Esculli un país"
#: web/templates/public/booking/form.gohtml:206
#: web/templates/public/booking/fields.gohtml:185
#: web/templates/admin/login.gohtml:27 web/templates/admin/profile.gohtml:38
#: web/templates/admin/taxDetails.gohtml:53
msgctxt "input"
msgid "Email"
msgstr "Correu-e"
#: web/templates/public/booking/form.gohtml:215
#: web/templates/public/booking/fields.gohtml:194
#: web/templates/admin/taxDetails.gohtml:45
msgctxt "input"
msgid "Phone"
msgstr "Telèfon"
#: web/templates/public/booking/form.gohtml:226
#: web/templates/public/booking/fields.gohtml:205
msgctxt "input"
msgid "ACSI card? (optional)"
msgstr "Targeta ACSI? (opcional)"
#: web/templates/public/booking/form.gohtml:233
#: web/templates/public/booking/fields.gohtml:212
msgctxt "input"
msgid "I have read and I accept %[1]sthe reservation conditions%[2]s"
msgstr "He llegit i accepto %[1]sles condicions de reserves%[2]s"
#: web/templates/public/booking/cart.gohtml:14
#: web/templates/public/booking/fields.gohtml:229
msgctxt "cart"
msgid "Total"
msgstr "Total"
@ -1973,12 +1968,12 @@ msgid "Slide image must be an image media type."
msgstr "La imatge de la diapositiva ha de ser un mèdia de tipus imatge."
#: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:217
#: pkg/booking/public.go:351
#: pkg/booking/public.go:571
msgid "Email can not be empty."
msgstr "No podeu deixar el correu-e en blanc."
#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:218
#: pkg/booking/public.go:352
#: pkg/booking/public.go:572
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."
@ -2150,22 +2145,23 @@ msgstr "El preu per nen ha de ser un número decimal."
msgid "Price per child must be zero or greater."
msgstr "El preu per nen ha de ser com a mínim zero."
#: pkg/campsite/types/public.go:229
#: pkg/campsite/types/public.go:244
msgctxt "header"
msgid "Adults"
msgstr "Adults"
#: pkg/campsite/types/public.go:235
#: pkg/campsite/types/public.go:250
msgctxt "header"
msgid "Teenagers (aged 11 to 16)"
msgstr "Adolescents (entre 11 i 16 anys)"
#: pkg/campsite/types/public.go:241
#: pkg/campsite/types/public.go:256
msgctxt "header"
msgid "Children (aged 2 to 10)"
msgstr "Mainada (entre 2 i 10 anys)"
#: pkg/campsite/admin.go:275 pkg/booking/public.go:360
#: pkg/campsite/admin.go:275 pkg/booking/public.go:217
#: pkg/booking/public.go:269
msgid "Selected campsite type is not valid."
msgstr "El tipus dallotjament escollit no és vàlid."
@ -2321,7 +2317,7 @@ msgstr "No podeu deixar ladreça de lenllaç en blanc."
msgid "This web address is not valid. It should be like https://domain.com/."
msgstr "Aquesta adreça web no és vàlida. Hauria de ser similar a https://domini.com/."
#: pkg/company/admin.go:200 pkg/booking/public.go:338
#: pkg/company/admin.go:200 pkg/booking/public.go:558
msgid "Selected country is not valid."
msgstr "El país escollit no és vàlid."
@ -2341,11 +2337,11 @@ msgstr "No podeu deixar el NIF en blanc."
msgid "This VAT number is not valid."
msgstr "Aquest NIF no és vàlid."
#: pkg/company/admin.go:212 pkg/booking/public.go:354
#: pkg/company/admin.go:212 pkg/booking/public.go:574
msgid "Phone can not be empty."
msgstr "No podeu deixar el telèfon en blanc."
#: pkg/company/admin.go:213 pkg/booking/public.go:355
#: pkg/company/admin.go:213 pkg/booking/public.go:575
msgid "This phone number is not valid."
msgstr "Aquest número de telèfon no és vàlid."
@ -2365,7 +2361,7 @@ msgstr "No podeu deixar la província en blanc."
msgid "Postal code can not be empty."
msgstr "No podeu deixar el codi postal en blanc."
#: pkg/company/admin.go:227 pkg/booking/public.go:347
#: pkg/company/admin.go:227 pkg/booking/public.go:567
msgid "This postal code is not valid."
msgstr "Aquest codi postal no és vàlid."
@ -2405,27 +2401,27 @@ msgstr "No podeu deixar el fitxer del mèdia en blanc."
msgid "Filename can not be empty."
msgstr "No podeu deixar el nom del fitxer en blanc."
#: pkg/booking/cart.go:171
#: pkg/booking/cart.go:142
msgctxt "cart"
msgid "Night"
msgstr "Nit"
#: pkg/booking/cart.go:172
#: pkg/booking/cart.go:143
msgctxt "cart"
msgid "Adult"
msgstr "Adult"
#: pkg/booking/cart.go:173
#: pkg/booking/cart.go:144
msgctxt "cart"
msgid "Teenager"
msgstr "Adolescent"
#: pkg/booking/cart.go:174
#: pkg/booking/cart.go:145
msgctxt "cart"
msgid "Child"
msgstr "Nen"
#: pkg/booking/cart.go:193
#: pkg/booking/cart.go:164
msgctxt "cart"
msgid "Tourist tax"
msgstr "Impost turístic"
@ -2487,106 +2483,138 @@ msgstr "La integració escollida no és vàlida."
msgid "The merchant key is not valid."
msgstr "Aquesta clau del comerç no és vàlid."
#: pkg/booking/public.go:342
msgid "Full name can not be empty."
msgstr "No podeu deixar el nom i els cognoms en blanc."
#: pkg/booking/public.go:343
msgid "Full name must have at least one letter."
msgstr "El nom i els cognoms han de tenir com a mínim una lletra."
#: pkg/booking/public.go:361
msgid "Arrival date can not be empty"
msgstr "No podeu deixar la data darribada en blanc."
#: pkg/booking/public.go:362
#: pkg/booking/public.go:318 pkg/booking/public.go:347
msgid "Arrival date must be a valid date."
msgstr "La data darribada ha de ser una data vàlida."
#: pkg/booking/public.go:366
msgid "Departure date can not be empty"
msgstr "No podeu deixar la data de sortida en blanc."
#: pkg/booking/public.go:367
#: pkg/booking/public.go:332 pkg/booking/public.go:354
msgid "Departure date must be a valid date."
msgstr "La data de sortida ha de ser una data vàlida."
#: pkg/booking/public.go:368
msgid "The departure date must be after the arrival date."
msgstr "La data de sortida ha de ser posterior a la darribada."
#: pkg/booking/public.go:346
msgid "Arrival date can not be empty"
msgstr "No podeu deixar la data darribada en blanc."
#: pkg/booking/public.go:372
#: pkg/booking/public.go:348
#, c-format
msgid "Arrival date must be %s or after."
msgstr "La data darribada ha de ser igual o posterior a %s."
#: pkg/booking/public.go:349
#, c-format
msgid "Arrival date must be %s or before."
msgstr "La data darribada ha de ser anterior o igual a %s."
#: pkg/booking/public.go:353
msgid "Departure date can not be empty"
msgstr "No podeu deixar la data de sortida en blanc."
#: pkg/booking/public.go:355
#, c-format
msgid "Departure date must be %s or after."
msgstr "La data de sortida ha de ser igual o posterior a %s."
#: pkg/booking/public.go:356
#, c-format
msgid "Departure date must be %s or before."
msgstr "La data de sortida ha de ser anterior o igual a %s."
#: pkg/booking/public.go:394
#, c-format
msgid "There can be at most %d guests in this accommodation."
msgstr "Hi poden haver com a màxim %d convidats a aquest allotjament."
#: pkg/booking/public.go:413
msgid "Number of adults can not be empty"
msgstr "No podeu deixar el número dadults en blanc."
#: pkg/booking/public.go:373
#: pkg/booking/public.go:414
msgid "Number of adults must be an integer."
msgstr "El número dadults ha de ser enter."
#: pkg/booking/public.go:374
#: pkg/booking/public.go:415
msgid "There must be at least one adult."
msgstr "Hi ha dhaver com a mínim un adult."
#: pkg/booking/public.go:377
#: pkg/booking/public.go:418
msgid "Number of teenagers can not be empty"
msgstr "No podeu deixar el número dadolescents en blanc."
#: pkg/booking/public.go:378
#: pkg/booking/public.go:419
msgid "Number of teenagers must be an integer."
msgstr "El número dadolescents ha de ser enter."
#: pkg/booking/public.go:379
#: pkg/booking/public.go:420
msgid "Number of teenagers can not be negative."
msgstr "El número dadolescents no pot ser negatiu."
#: pkg/booking/public.go:382
#: pkg/booking/public.go:423
msgid "Number of children can not be empty"
msgstr "No podeu deixar el número de nens en blanc."
#: pkg/booking/public.go:383
#: pkg/booking/public.go:424
msgid "Number of children must be an integer."
msgstr "El número de nens ha de ser enter."
#: pkg/booking/public.go:384
#: pkg/booking/public.go:425
msgid "Number of children can not be negative."
msgstr "El número de nens no pot ser negatiu."
#: pkg/booking/public.go:387
#: pkg/booking/public.go:428
msgid "Number of dogs can not be empty"
msgstr "No podeu deixar el número de gossos en blanc."
#: pkg/booking/public.go:388
#: pkg/booking/public.go:429
msgid "Number of dogs must be an integer."
msgstr "El número de gossos ha de ser enter."
#: pkg/booking/public.go:389
#: pkg/booking/public.go:430
msgid "Number of dogs can not be negative."
msgstr "El número de gossos no pot ser negatiu."
#: pkg/booking/public.go:392
msgid "It is mandatory to agree to the reservation conditions."
msgstr "És obligatori acceptar les condicions de reserves."
#: pkg/booking/public.go:395
#: pkg/booking/public.go:501
#, c-format
msgid "%s can not be empty"
msgstr "No podeu deixar %s en blanc."
#: pkg/booking/public.go:396
#: pkg/booking/public.go:502
#, c-format
msgid "%s must be an integer."
msgstr "%s ha de ser un número enter."
#: pkg/booking/public.go:397
#: pkg/booking/public.go:503
#, c-format
msgid "%s must be %d or greater."
msgstr "El valor de %s ha de ser com a mínim %d."
#: pkg/booking/public.go:398
#: pkg/booking/public.go:504
#, c-format
msgid "%s must be at most %d."
msgstr "El valor de %s ha de ser com a màxim %d."
#: pkg/booking/public.go:562
msgid "Full name can not be empty."
msgstr "No podeu deixar el nom i els cognoms en blanc."
#: pkg/booking/public.go:563
msgid "Full name must have at least one letter."
msgstr "El nom i els cognoms han de tenir com a mínim una lletra."
#: pkg/booking/public.go:580
msgid "It is mandatory to agree to the reservation conditions."
msgstr "És obligatori acceptar les condicions de reserves."
#~ msgctxt "input"
#~ msgid "Check-in Date"
#~ msgstr "Data dentrada"
#~ msgctxt "input"
#~ msgid "Check-out Date"
#~ msgstr "Data de sortida"
#~ msgid "The departure date must be after the arrival date."
#~ msgstr "La data de sortida ha de ser posterior a la darribada."
#~ msgid "Campsite Montagut is an ideal starting point for quiet outings, climbing, swimming in the river and gorges, volcanoes, the Fageda den Jordà, cycle tours for all ages…."
#~ msgstr "El Càmping Montagut és ideal com a punt de partida dexcursions tranquil·les, escalada, banyar-se en el riu i gorgues, volcans, la Fageda den Jordà, sortides amb bicicleta per a tots els nivells…."

263
po/es.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-02-04 06:23+0100\n"
"POT-Creation-Date: 2024-02-10 03:31+0100\n"
"PO-Revision-Date: 2024-02-06 10:04+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n"
@ -95,7 +95,7 @@ msgid "The campsite offers many different services."
msgstr "El camping dispone de varios servicios."
#: web/templates/public/amenity.gohtml:39
#: web/templates/public/campsite/type.gohtml:113
#: web/templates/public/campsite/type.gohtml:106
#: web/templates/public/campsite/page.gohtml:39
msgctxt "title"
msgid "Features"
@ -154,89 +154,79 @@ msgstr "A menos de una hora de <strong>Girona</strong>, a una de <strong>La Bisb
msgid "Discover"
msgstr "Descubre"
#: web/templates/public/campsite/type.gohtml:41
msgctxt "input"
msgid "Check-in Date"
msgstr "Fecha de entrada"
#: web/templates/public/campsite/type.gohtml:47
msgctxt "input"
msgid "Check-out Date"
msgstr "Fecha de salida"
#: web/templates/public/campsite/type.gohtml:56
#: web/templates/public/booking/cart.gohtml:25
#: web/templates/public/campsite/type.gohtml:49
#: web/templates/public/booking/fields.gohtml:240
msgctxt "action"
msgid "Book"
msgstr "Reservar"
#: web/templates/public/campsite/type.gohtml:64
#: web/templates/public/campsite/type.gohtml:57
#: web/templates/admin/season/index.gohtml:54
msgctxt "title"
msgid "Calendar"
msgstr "Calendario"
#: web/templates/public/campsite/type.gohtml:75
#: web/templates/public/campsite/type.gohtml:68
#: web/templates/admin/campsite/type/form.gohtml:143
#: web/templates/admin/campsite/type/option/form.gohtml:70
msgctxt "title"
msgid "Prices"
msgstr "Precios"
#: web/templates/public/campsite/type.gohtml:88
#: web/templates/public/campsite/type.gohtml:81
msgid "%s: %s/night"
msgstr "%s: %s/noche"
#: web/templates/public/campsite/type.gohtml:90
#: web/templates/public/campsite/type.gohtml:83
msgid "%s/night"
msgstr "%s/noche"
#: web/templates/public/campsite/type.gohtml:95
#: web/templates/public/campsite/type.gohtml:88
msgid "*Minimum %d nights per stay"
msgstr "*Mínimo %d noches por estancia"
#: web/templates/public/campsite/type.gohtml:100
#: web/templates/public/campsite/type.gohtml:93
msgid "10 % VAT included."
msgstr "IVA del 10 % incluido."
#: web/templates/public/campsite/type.gohtml:101
#: web/templates/public/campsite/type.gohtml:94
msgid "Tourist tax: %s/night per person aged 17 or older."
msgstr "Impuesto turístico: %s/noche por persona mayor de 16 años."
#: web/templates/public/campsite/type.gohtml:103
#: web/templates/public/campsite/type.gohtml:96
msgid "Dogs: %s/night, tied, accompanied, and minimal barking."
msgstr "Perros: %s/noche, atados, acompañados y con mínimo de ladrido."
#: web/templates/public/campsite/type.gohtml:105
#: web/templates/public/campsite/type.gohtml:98
msgid "No dogs allowed."
msgstr "No se permiten perros"
#: web/templates/public/campsite/type.gohtml:124
#: web/templates/public/campsite/type.gohtml:117
msgctxt "title"
msgid "Info"
msgstr "Información"
#: web/templates/public/campsite/type.gohtml:128
#: web/templates/public/campsite/type.gohtml:121
msgctxt "title"
msgid "Facilities"
msgstr "Equipamiento"
#: web/templates/public/campsite/type.gohtml:132
#: web/templates/public/campsite/type.gohtml:125
msgctxt "title"
msgid "Description"
msgstr "Descripción"
#: web/templates/public/campsite/type.gohtml:136
#: web/templates/public/campsite/type.gohtml:129
msgctxt "title"
msgid "Additional Information"
msgstr "Información adicional"
#: web/templates/public/campsite/type.gohtml:139
#: web/templates/public/campsite/type.gohtml:132
msgctxt "time"
msgid "Check-in"
msgstr "Entrada"
#: web/templates/public/campsite/type.gohtml:143
#: web/templates/public/campsite/type.gohtml:136
msgctxt "time"
msgid "Check-out"
msgstr "Salida"
@ -283,6 +273,18 @@ msgctxt "day"
msgid "Sun"
msgstr "do"
#: web/templates/public/campsite/dates.gohtml:4
#: web/templates/public/booking/fields.gohtml:26
msgctxt "input"
msgid "Arrival date"
msgstr "Fecha de llegada"
#: web/templates/public/campsite/dates.gohtml:15
#: web/templates/public/booking/fields.gohtml:37
msgctxt "input"
msgid "Departure date"
msgstr "Fecha de salida"
#: web/templates/public/surroundings.gohtml:30
msgctxt "title"
msgid "What to Do Outside the Campsite?"
@ -516,8 +518,7 @@ msgid "Campsites"
msgstr "Alojamientos"
#: web/templates/public/layout.gohtml:70
#: web/templates/public/booking/form.gohtml:7
#: web/templates/public/booking/form.gohtml:16
#: web/templates/public/booking/page.gohtml:7
msgctxt "title"
msgid "Booking"
msgstr "Reserva"
@ -536,119 +537,113 @@ msgstr "Apertura"
msgid "<abbr title=\"Catalonia Tourism Registry\">RTC</abbr> <abbr title=\"Number\">#</abbr>%s"
msgstr "<abbr title=\"Número\">Nº</abbr> <abbr title=\"Registro de Turismo de Cataluña\">RTC</abbr> %s"
#: web/templates/public/booking/form.gohtml:29
#: web/templates/public/booking/fields.gohtml:13
msgctxt "title"
msgid "Accommodation"
msgstr "Alojamientos"
#: web/templates/public/booking/form.gohtml:43
#: web/templates/public/booking/fields.gohtml:23
msgctxt "title"
msgid "Booking Period"
msgstr "Periodo de reserva"
#: web/templates/public/booking/form.gohtml:46
msgctxt "input"
msgid "Arrival date"
msgstr "Fecha de llegada"
#: web/templates/public/booking/form.gohtml:57
msgctxt "input"
msgid "Departure date"
msgstr "Fecha de salida"
#: web/templates/public/booking/form.gohtml:72
#: web/templates/public/booking/fields.gohtml:50
msgctxt "title"
msgid "Guests"
msgstr "Huéspedes"
#: web/templates/public/booking/form.gohtml:76
#: web/templates/public/booking/fields.gohtml:54
msgctxt "input"
msgid "Adults aged 17 or older"
msgstr "Adultos de 17 años o más"
#: web/templates/public/booking/form.gohtml:86
#: web/templates/public/booking/fields.gohtml:65
msgctxt "input"
msgid "Teenagers from 11 to 16 years old"
msgstr "Adolescentes de 11 a 16 años"
#: web/templates/public/booking/form.gohtml:96
#: web/templates/public/booking/fields.gohtml:76
msgctxt "input"
msgid "Children from 2 to 10 years old"
msgstr "Niños de 2 a 10 años"
#: web/templates/public/booking/form.gohtml:106
#: web/templates/public/booking/fields.gohtml:91
msgctxt "input"
msgid "Dogs"
msgstr "Perros"
#: web/templates/public/booking/form.gohtml:127
#: web/templates/public/booking/fields.gohtml:100
msgid "Note: This accommodation does <strong>not</strong> allow dogs."
msgstr "Nota: En este alojamiento <strong>no</strong se permiten perros."
#: web/templates/public/booking/fields.gohtml:110
msgctxt "input"
msgid "Area preferences (optional)"
msgstr "Preferencias de área (opcional)"
#: web/templates/public/booking/form.gohtml:129
#: web/templates/public/booking/fields.gohtml:112
msgid "Campground map"
msgstr "Mapa del camping"
#: web/templates/public/booking/form.gohtml:156
#: web/templates/public/booking/fields.gohtml:135
msgctxt "title"
msgid "Customer Details"
msgstr "Detalles del cliente"
#: web/templates/public/booking/form.gohtml:159
#: web/templates/public/booking/fields.gohtml:138
msgctxt "input"
msgid "Full name"
msgstr "Nombre y apellidos"
#: web/templates/public/booking/form.gohtml:168
#: web/templates/public/booking/fields.gohtml:147
msgctxt "input"
msgid "Address (optional)"
msgstr "Dirección (opcional)"
#: web/templates/public/booking/form.gohtml:177
#: web/templates/public/booking/fields.gohtml:156
msgctxt "input"
msgid "Postcode (optional)"
msgstr "Código postal (opcional)"
#: web/templates/public/booking/form.gohtml:186
#: web/templates/public/booking/fields.gohtml:165
msgctxt "input"
msgid "Town or village (optional)"
msgstr "Población (opcional)"
#: web/templates/public/booking/form.gohtml:195
#: web/templates/public/booking/fields.gohtml:174
#: web/templates/admin/taxDetails.gohtml:101
msgctxt "input"
msgid "Country"
msgstr "País"
#: web/templates/public/booking/form.gohtml:198
#: web/templates/public/booking/fields.gohtml:177
msgid "Choose a country"
msgstr "Escoja un país"
#: web/templates/public/booking/form.gohtml:206
#: web/templates/public/booking/fields.gohtml:185
#: web/templates/admin/login.gohtml:27 web/templates/admin/profile.gohtml:38
#: web/templates/admin/taxDetails.gohtml:53
msgctxt "input"
msgid "Email"
msgstr "Correo-e"
#: web/templates/public/booking/form.gohtml:215
#: web/templates/public/booking/fields.gohtml:194
#: web/templates/admin/taxDetails.gohtml:45
msgctxt "input"
msgid "Phone"
msgstr "Teléfono"
#: web/templates/public/booking/form.gohtml:226
#: web/templates/public/booking/fields.gohtml:205
msgctxt "input"
msgid "ACSI card? (optional)"
msgstr "¿Tarjeta ACSI? (opcional)"
#: web/templates/public/booking/form.gohtml:233
#: web/templates/public/booking/fields.gohtml:212
msgctxt "input"
msgid "I have read and I accept %[1]sthe reservation conditions%[2]s"
msgstr "He leído y acepto %[1]slas condiciones de reserva%[2]s"
#: web/templates/public/booking/cart.gohtml:14
#: web/templates/public/booking/fields.gohtml:229
msgctxt "cart"
msgid "Total"
msgstr "Total"
@ -1973,12 +1968,12 @@ msgid "Slide image must be an image media type."
msgstr "La imagen de la diapositiva tiene que ser un medio de tipo imagen."
#: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:217
#: pkg/booking/public.go:351
#: pkg/booking/public.go:571
msgid "Email can not be empty."
msgstr "No podéis dejar el correo-e en blanco."
#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:218
#: pkg/booking/public.go:352
#: pkg/booking/public.go:572
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."
@ -2150,22 +2145,23 @@ msgstr "El precio por niño tiene que ser un número decimal."
msgid "Price per child must be zero or greater."
msgstr "El precio por niño tiene que ser como mínimo cero."
#: pkg/campsite/types/public.go:229
#: pkg/campsite/types/public.go:244
msgctxt "header"
msgid "Adults"
msgstr "Adultos"
#: pkg/campsite/types/public.go:235
#: pkg/campsite/types/public.go:250
msgctxt "header"
msgid "Teenagers (aged 11 to 16)"
msgstr "Adolescentes (de 11 a 16 años)"
#: pkg/campsite/types/public.go:241
#: pkg/campsite/types/public.go:256
msgctxt "header"
msgid "Children (aged 2 to 10)"
msgstr "Niños (de 2 a 10 años)"
#: pkg/campsite/admin.go:275 pkg/booking/public.go:360
#: pkg/campsite/admin.go:275 pkg/booking/public.go:217
#: pkg/booking/public.go:269
msgid "Selected campsite type is not valid."
msgstr "El tipo de alojamiento escogido no es válido."
@ -2321,7 +2317,7 @@ msgstr "No podéis dejar la dirección del enlace en blanco."
msgid "This web address is not valid. It should be like https://domain.com/."
msgstr "Esta dirección web no es válida. Tiene que ser parecido a https://dominio.com/."
#: pkg/company/admin.go:200 pkg/booking/public.go:338
#: pkg/company/admin.go:200 pkg/booking/public.go:558
msgid "Selected country is not valid."
msgstr "El país escogido no es válido."
@ -2341,11 +2337,11 @@ msgstr "No podéis dejar el NIF en blanco."
msgid "This VAT number is not valid."
msgstr "Este NIF no es válido."
#: pkg/company/admin.go:212 pkg/booking/public.go:354
#: pkg/company/admin.go:212 pkg/booking/public.go:574
msgid "Phone can not be empty."
msgstr "No podéis dejar el teléfono en blanco."
#: pkg/company/admin.go:213 pkg/booking/public.go:355
#: pkg/company/admin.go:213 pkg/booking/public.go:575
msgid "This phone number is not valid."
msgstr "Este teléfono no es válido."
@ -2365,7 +2361,7 @@ msgstr "No podéis dejar la provincia en blanco."
msgid "Postal code can not be empty."
msgstr "No podéis dejar el código postal en blanco."
#: pkg/company/admin.go:227 pkg/booking/public.go:347
#: pkg/company/admin.go:227 pkg/booking/public.go:567
msgid "This postal code is not valid."
msgstr "Este código postal no es válido."
@ -2405,27 +2401,27 @@ msgstr "No podéis dejar el archivo del medio en blanco."
msgid "Filename can not be empty."
msgstr "No podéis dejar el nombre del archivo en blanco."
#: pkg/booking/cart.go:171
#: pkg/booking/cart.go:142
msgctxt "cart"
msgid "Night"
msgstr "Noche"
#: pkg/booking/cart.go:172
#: pkg/booking/cart.go:143
msgctxt "cart"
msgid "Adult"
msgstr "Adulto"
#: pkg/booking/cart.go:173
#: pkg/booking/cart.go:144
msgctxt "cart"
msgid "Teenager"
msgstr "Adolescente"
#: pkg/booking/cart.go:174
#: pkg/booking/cart.go:145
msgctxt "cart"
msgid "Child"
msgstr "Niño"
#: pkg/booking/cart.go:193
#: pkg/booking/cart.go:164
msgctxt "cart"
msgid "Tourist tax"
msgstr "Impuesto turístico"
@ -2487,106 +2483,137 @@ msgstr "La integración escogida no es válida."
msgid "The merchant key is not valid."
msgstr "Esta clave del comercio no es válida."
#: pkg/booking/public.go:342
msgid "Full name can not be empty."
msgstr "No podéis dejar el nombre y los apellidos en blanco."
#: pkg/booking/public.go:343
msgid "Full name must have at least one letter."
msgstr "El nombre y los apellidos tienen que tener como mínimo una letra."
#: pkg/booking/public.go:361
msgid "Arrival date can not be empty"
msgstr "No podéis dejar la fecha de llegada en blanco."
#: pkg/booking/public.go:362
#: pkg/booking/public.go:318 pkg/booking/public.go:347
msgid "Arrival date must be a valid date."
msgstr "La fecha de llegada tiene que ser una fecha válida."
#: pkg/booking/public.go:366
msgid "Departure date can not be empty"
msgstr "No podéis dejar la fecha de partida en blanco."
#: pkg/booking/public.go:367
#: pkg/booking/public.go:332 pkg/booking/public.go:354
msgid "Departure date must be a valid date."
msgstr "La fecha de partida tiene que ser una fecha válida."
#: pkg/booking/public.go:368
msgid "The departure date must be after the arrival date."
msgstr "La fecha de partida tiene que ser posterior a la de llegada."
#: pkg/booking/public.go:346
msgid "Arrival date can not be empty"
msgstr "No podéis dejar la fecha de llegada en blanco."
#: pkg/booking/public.go:372
#: pkg/booking/public.go:348
msgid "Arrival date must be %s or after."
msgstr "La fecha de llegada tiene que ser igual o posterior a %s."
#: pkg/booking/public.go:349
#, c-format
msgid "Arrival date must be %s or before."
msgstr "La fecha de llegada tiene que ser anterior o igual a %s."
#: pkg/booking/public.go:353
msgid "Departure date can not be empty"
msgstr "No podéis dejar la fecha de partida en blanco."
#: pkg/booking/public.go:355
#, c-format
msgid "Departure date must be %s or after."
msgstr "La fecha de partida tiene que igual o posterior a %s."
#: pkg/booking/public.go:356
#, c-format
msgid "Departure date must be %s or before."
msgstr "La fecha de partida tiene que ser anterior o igual a %s."
#: pkg/booking/public.go:394
#, c-format
msgid "There can be at most %d guests in this accommodation."
msgstr "Solo puede haber como máximo %d invitados en este alojamiento."
#: pkg/booking/public.go:413
msgid "Number of adults can not be empty"
msgstr "No podéis dejar el número de adultos blanco."
#: pkg/booking/public.go:373
#: pkg/booking/public.go:414
msgid "Number of adults must be an integer."
msgstr "El número de adultos tiene que ser entero."
#: pkg/booking/public.go:374
#: pkg/booking/public.go:415
msgid "There must be at least one adult."
msgstr "Tiene que haber como mínimo un adulto."
#: pkg/booking/public.go:377
#: pkg/booking/public.go:418
msgid "Number of teenagers can not be empty"
msgstr "No podéis dejar el número de adolescentes en blanco."
#: pkg/booking/public.go:378
#: pkg/booking/public.go:419
msgid "Number of teenagers must be an integer."
msgstr "El número de adolescentes tiene que ser entero."
#: pkg/booking/public.go:379
#: pkg/booking/public.go:420
msgid "Number of teenagers can not be negative."
msgstr "El número de adolescentes no puede ser negativo."
#: pkg/booking/public.go:382
#: pkg/booking/public.go:423
msgid "Number of children can not be empty"
msgstr "No podéis dejar el número de niños en blanco."
#: pkg/booking/public.go:383
#: pkg/booking/public.go:424
msgid "Number of children must be an integer."
msgstr "El número de niños tiene que ser entero."
#: pkg/booking/public.go:384
#: pkg/booking/public.go:425
msgid "Number of children can not be negative."
msgstr "El número de niños no puede ser negativo."
#: pkg/booking/public.go:387
#: pkg/booking/public.go:428
msgid "Number of dogs can not be empty"
msgstr "No podéis dejar el número de perros en blanco."
#: pkg/booking/public.go:388
#: pkg/booking/public.go:429
msgid "Number of dogs must be an integer."
msgstr "El número de perros tiene que ser entero."
#: pkg/booking/public.go:389
#: pkg/booking/public.go:430
msgid "Number of dogs can not be negative."
msgstr "El número de perros no puede ser negativo."
#: pkg/booking/public.go:392
msgid "It is mandatory to agree to the reservation conditions."
msgstr "Es obligatorio aceptar las condiciones de reserva."
#: pkg/booking/public.go:395
#: pkg/booking/public.go:501
#, c-format
msgid "%s can not be empty"
msgstr "No podéis dejar %s en blanco."
#: pkg/booking/public.go:396
#: pkg/booking/public.go:502
#, c-format
msgid "%s must be an integer."
msgstr "%s tiene que ser un número entero."
#: pkg/booking/public.go:397
#: pkg/booking/public.go:503
#, c-format
msgid "%s must be %d or greater."
msgstr "%s tiene que ser como mínimo %d."
#: pkg/booking/public.go:398
#: pkg/booking/public.go:504
#, c-format
msgid "%s must be at most %d."
msgstr "%s tiene que ser como máximo %d"
#: pkg/booking/public.go:562
msgid "Full name can not be empty."
msgstr "No podéis dejar el nombre y los apellidos en blanco."
#: pkg/booking/public.go:563
msgid "Full name must have at least one letter."
msgstr "El nombre y los apellidos tienen que tener como mínimo una letra."
#: pkg/booking/public.go:580
msgid "It is mandatory to agree to the reservation conditions."
msgstr "Es obligatorio aceptar las condiciones de reserva."
#~ msgctxt "input"
#~ msgid "Check-in Date"
#~ msgstr "Fecha de entrada"
#~ msgctxt "input"
#~ msgid "Check-out Date"
#~ msgstr "Fecha de salida"
#~ msgid "The departure date must be after the arrival date."
#~ msgstr "La fecha de partida tiene que ser posterior a la de llegada."
#~ msgid "Campsite Montagut is an ideal starting point for quiet outings, climbing, swimming in the river and gorges, volcanoes, the Fageda den Jordà, cycle tours for all ages…."
#~ msgstr "El Camping Montagut es ideal como punto de salida de excursiones tranquilas, escalada, bañarse en el río y piletones, volcanes, la Fageda den Jordà, salidas en bicicleta para todos los niveles…."

264
po/fr.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2024-02-04 06:23+0100\n"
"POT-Creation-Date: 2024-02-10 03:31+0100\n"
"PO-Revision-Date: 2024-02-06 10:05+0100\n"
"Last-Translator: Oriol Carbonell <info@oriolcarbonell.cat>\n"
"Language-Team: French <traduc@traduc.org>\n"
@ -95,7 +95,7 @@ msgid "The campsite offers many different services."
msgstr "Le camping propose de nombreux services différents."
#: web/templates/public/amenity.gohtml:39
#: web/templates/public/campsite/type.gohtml:113
#: web/templates/public/campsite/type.gohtml:106
#: web/templates/public/campsite/page.gohtml:39
msgctxt "title"
msgid "Features"
@ -154,89 +154,79 @@ msgstr "À moins dune heure de <strong>Gérone</strong>, un de <strong>La Bis
msgid "Discover"
msgstr "Découvrir"
#: web/templates/public/campsite/type.gohtml:41
msgctxt "input"
msgid "Check-in Date"
msgstr "Date d'arrivée"
#: web/templates/public/campsite/type.gohtml:47
msgctxt "input"
msgid "Check-out Date"
msgstr "Date de départ"
#: web/templates/public/campsite/type.gohtml:56
#: web/templates/public/booking/cart.gohtml:25
#: web/templates/public/campsite/type.gohtml:49
#: web/templates/public/booking/fields.gohtml:240
msgctxt "action"
msgid "Book"
msgstr "Réserver"
#: web/templates/public/campsite/type.gohtml:64
#: web/templates/public/campsite/type.gohtml:57
#: web/templates/admin/season/index.gohtml:54
msgctxt "title"
msgid "Calendar"
msgstr "Calendrier"
#: web/templates/public/campsite/type.gohtml:75
#: web/templates/public/campsite/type.gohtml:68
#: web/templates/admin/campsite/type/form.gohtml:143
#: web/templates/admin/campsite/type/option/form.gohtml:70
msgctxt "title"
msgid "Prices"
msgstr "Prix"
#: web/templates/public/campsite/type.gohtml:88
#: web/templates/public/campsite/type.gohtml:81
msgid "%s: %s/night"
msgstr "%s : %s/nuit"
#: web/templates/public/campsite/type.gohtml:90
#: web/templates/public/campsite/type.gohtml:83
msgid "%s/night"
msgstr "%s/nuit"
#: web/templates/public/campsite/type.gohtml:95
#: web/templates/public/campsite/type.gohtml:88
msgid "*Minimum %d nights per stay"
msgstr "*Minimum %d nuits par séjour"
#: web/templates/public/campsite/type.gohtml:100
#: web/templates/public/campsite/type.gohtml:93
msgid "10 % VAT included."
msgstr "10 % TVA incluse."
#: web/templates/public/campsite/type.gohtml:101
#: web/templates/public/campsite/type.gohtml:94
msgid "Tourist tax: %s/night per person aged 17 or older."
msgstr "Taxe touristique: %s/nuit par personne de plus de 16 ans."
#: web/templates/public/campsite/type.gohtml:103
#: web/templates/public/campsite/type.gohtml:96
msgid "Dogs: %s/night, tied, accompanied, and minimal barking."
msgstr "Chiens : %s/nuit, attachés, accompagnés et aboiements minimes."
#: web/templates/public/campsite/type.gohtml:105
#: web/templates/public/campsite/type.gohtml:98
msgid "No dogs allowed."
msgstr "Chiens interdits."
#: web/templates/public/campsite/type.gohtml:124
#: web/templates/public/campsite/type.gohtml:117
msgctxt "title"
msgid "Info"
msgstr "Info"
#: web/templates/public/campsite/type.gohtml:128
#: web/templates/public/campsite/type.gohtml:121
msgctxt "title"
msgid "Facilities"
msgstr "Installations"
#: web/templates/public/campsite/type.gohtml:132
#: web/templates/public/campsite/type.gohtml:125
msgctxt "title"
msgid "Description"
msgstr "Description"
#: web/templates/public/campsite/type.gohtml:136
#: web/templates/public/campsite/type.gohtml:129
msgctxt "title"
msgid "Additional Information"
msgstr "Informations Complémentaires"
#: web/templates/public/campsite/type.gohtml:139
#: web/templates/public/campsite/type.gohtml:132
msgctxt "time"
msgid "Check-in"
msgstr "Arrivée"
#: web/templates/public/campsite/type.gohtml:143
#: web/templates/public/campsite/type.gohtml:136
msgctxt "time"
msgid "Check-out"
msgstr "Départ"
@ -283,6 +273,18 @@ msgctxt "day"
msgid "Sun"
msgstr "Dim."
#: web/templates/public/campsite/dates.gohtml:4
#: web/templates/public/booking/fields.gohtml:26
msgctxt "input"
msgid "Arrival date"
msgstr "Date darrivée"
#: web/templates/public/campsite/dates.gohtml:15
#: web/templates/public/booking/fields.gohtml:37
msgctxt "input"
msgid "Departure date"
msgstr "Date de depart"
#: web/templates/public/surroundings.gohtml:30
msgctxt "title"
msgid "What to Do Outside the Campsite?"
@ -516,8 +518,7 @@ msgid "Campsites"
msgstr "Locatifs"
#: web/templates/public/layout.gohtml:70
#: web/templates/public/booking/form.gohtml:7
#: web/templates/public/booking/form.gohtml:16
#: web/templates/public/booking/page.gohtml:7
msgctxt "title"
msgid "Booking"
msgstr "Réservation"
@ -536,119 +537,113 @@ msgstr "Ouverture"
msgid "<abbr title=\"Catalonia Tourism Registry\">RTC</abbr> <abbr title=\"Number\">#</abbr>%s"
msgstr "<abbr title=\"Registre du tourisme de Catalogne\"># RTC</abbr> %s"
#: web/templates/public/booking/form.gohtml:29
#: web/templates/public/booking/fields.gohtml:13
msgctxt "title"
msgid "Accommodation"
msgstr "Hébergement"
#: web/templates/public/booking/form.gohtml:43
#: web/templates/public/booking/fields.gohtml:23
msgctxt "title"
msgid "Booking Period"
msgstr "Période de réservation"
#: web/templates/public/booking/form.gohtml:46
msgctxt "input"
msgid "Arrival date"
msgstr "Date darrivée"
#: web/templates/public/booking/form.gohtml:57
msgctxt "input"
msgid "Departure date"
msgstr "Date de depart"
#: web/templates/public/booking/form.gohtml:72
#: web/templates/public/booking/fields.gohtml:50
msgctxt "title"
msgid "Guests"
msgstr "Personnes logeant"
#: web/templates/public/booking/form.gohtml:76
#: web/templates/public/booking/fields.gohtml:54
msgctxt "input"
msgid "Adults aged 17 or older"
msgstr "Adultes âgés 17 ans ou plus"
#: web/templates/public/booking/form.gohtml:86
#: web/templates/public/booking/fields.gohtml:65
msgctxt "input"
msgid "Teenagers from 11 to 16 years old"
msgstr "Adolescents de 11 à 16 ans"
#: web/templates/public/booking/form.gohtml:96
#: web/templates/public/booking/fields.gohtml:76
msgctxt "input"
msgid "Children from 2 to 10 years old"
msgstr "Enfants de 2 à 10 ans"
#: web/templates/public/booking/form.gohtml:106
#: web/templates/public/booking/fields.gohtml:91
msgctxt "input"
msgid "Dogs"
msgstr "Chiens"
#: web/templates/public/booking/form.gohtml:127
#: web/templates/public/booking/fields.gohtml:100
msgid "Note: This accommodation does <strong>not</strong> allow dogs."
msgstr "Remarque: Dans cet hébergement les chiens <strong>ne</strong> sont pas autorisés."
#: web/templates/public/booking/fields.gohtml:110
msgctxt "input"
msgid "Area preferences (optional)"
msgstr "Préférences de zone (facultatif)"
#: web/templates/public/booking/form.gohtml:129
#: web/templates/public/booking/fields.gohtml:112
msgid "Campground map"
msgstr "Plan du camping"
#: web/templates/public/booking/form.gohtml:156
#: web/templates/public/booking/fields.gohtml:135
msgctxt "title"
msgid "Customer Details"
msgstr "Détails du client"
#: web/templates/public/booking/form.gohtml:159
#: web/templates/public/booking/fields.gohtml:138
msgctxt "input"
msgid "Full name"
msgstr "Nom et prénom"
#: web/templates/public/booking/form.gohtml:168
#: web/templates/public/booking/fields.gohtml:147
msgctxt "input"
msgid "Address (optional)"
msgstr "Adresse (Facultatif)"
#: web/templates/public/booking/form.gohtml:177
#: web/templates/public/booking/fields.gohtml:156
msgctxt "input"
msgid "Postcode (optional)"
msgstr "Code postal (Facultatif)"
#: web/templates/public/booking/form.gohtml:186
#: web/templates/public/booking/fields.gohtml:165
msgctxt "input"
msgid "Town or village (optional)"
msgstr "Ville (Facultatif)"
#: web/templates/public/booking/form.gohtml:195
#: web/templates/public/booking/fields.gohtml:174
#: web/templates/admin/taxDetails.gohtml:101
msgctxt "input"
msgid "Country"
msgstr "Pays"
#: web/templates/public/booking/form.gohtml:198
#: web/templates/public/booking/fields.gohtml:177
msgid "Choose a country"
msgstr "Choisissez un pays"
#: web/templates/public/booking/form.gohtml:206
#: web/templates/public/booking/fields.gohtml:185
#: web/templates/admin/login.gohtml:27 web/templates/admin/profile.gohtml:38
#: web/templates/admin/taxDetails.gohtml:53
msgctxt "input"
msgid "Email"
msgstr "E-mail"
#: web/templates/public/booking/form.gohtml:215
#: web/templates/public/booking/fields.gohtml:194
#: web/templates/admin/taxDetails.gohtml:45
msgctxt "input"
msgid "Phone"
msgstr "Téléphone"
#: web/templates/public/booking/form.gohtml:226
#: web/templates/public/booking/fields.gohtml:205
msgctxt "input"
msgid "ACSI card? (optional)"
msgstr "Carte ACSI ? (Facultatif)"
#: web/templates/public/booking/form.gohtml:233
#: web/templates/public/booking/fields.gohtml:212
msgctxt "input"
msgid "I have read and I accept %[1]sthe reservation conditions%[2]s"
msgstr "Jai lu et jaccepte %[1]sles conditions de réservation%[2]s"
#: web/templates/public/booking/cart.gohtml:14
#: web/templates/public/booking/fields.gohtml:229
msgctxt "cart"
msgid "Total"
msgstr "Totale"
@ -1973,12 +1968,12 @@ msgid "Slide image must be an image media type."
msgstr "Limage de la diapositive doit être de type média dimage."
#: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:217
#: pkg/booking/public.go:351
#: pkg/booking/public.go:571
msgid "Email can not be empty."
msgstr "Le-mail ne peut pas être vide."
#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:218
#: pkg/booking/public.go:352
#: pkg/booking/public.go:572
msgid "This email is not valid. It should be like name@domain.com."
msgstr "Cette adresse e-mail nest pas valide. Il devrait en être name@domain.com."
@ -2150,22 +2145,23 @@ msgstr "Le prix par enfant doit être un nombre décimal."
msgid "Price per child must be zero or greater."
msgstr "Le prix par enfant doit être égal ou supérieur."
#: pkg/campsite/types/public.go:229
#: pkg/campsite/types/public.go:244
msgctxt "header"
msgid "Adults"
msgstr "Adultes"
#: pkg/campsite/types/public.go:235
#: pkg/campsite/types/public.go:250
msgctxt "header"
msgid "Teenagers (aged 11 to 16)"
msgstr "Adolescents (de 11 à 16 anys)"
#: pkg/campsite/types/public.go:241
#: pkg/campsite/types/public.go:256
msgctxt "header"
msgid "Children (aged 2 to 10)"
msgstr "Enfants (de 2 à 10 anys)"
#: pkg/campsite/admin.go:275 pkg/booking/public.go:360
#: pkg/campsite/admin.go:275 pkg/booking/public.go:217
#: pkg/booking/public.go:269
msgid "Selected campsite type is not valid."
msgstr "Le type demplacement sélectionné nest pas valide."
@ -2321,7 +2317,7 @@ msgstr "Laddresse du lien ne peut pas être vide."
msgid "This web address is not valid. It should be like https://domain.com/."
msgstr "Cette adresse web nest pas valide. Il devrait en être https://domain.com/."
#: pkg/company/admin.go:200 pkg/booking/public.go:338
#: pkg/company/admin.go:200 pkg/booking/public.go:558
msgid "Selected country is not valid."
msgstr "Le pays sélectionné nest pas valide."
@ -2341,11 +2337,11 @@ msgstr "Le numéro de TVA ne peut pas être vide."
msgid "This VAT number is not valid."
msgstr "Ce numéro de TVA nest pas valide."
#: pkg/company/admin.go:212 pkg/booking/public.go:354
#: pkg/company/admin.go:212 pkg/booking/public.go:574
msgid "Phone can not be empty."
msgstr "Le téléphone ne peut pas être vide."
#: pkg/company/admin.go:213 pkg/booking/public.go:355
#: pkg/company/admin.go:213 pkg/booking/public.go:575
msgid "This phone number is not valid."
msgstr "Ce numéro de téléphone nest pas valide."
@ -2365,7 +2361,7 @@ msgstr "La province ne peut pas être vide."
msgid "Postal code can not be empty."
msgstr "Le code postal ne peut pas être vide."
#: pkg/company/admin.go:227 pkg/booking/public.go:347
#: pkg/company/admin.go:227 pkg/booking/public.go:567
msgid "This postal code is not valid."
msgstr "Ce code postal nest pas valide."
@ -2405,27 +2401,27 @@ msgstr "Le fichier téléchargé ne peut pas être vide."
msgid "Filename can not be empty."
msgstr "Le nom de fichier ne peut pas être vide."
#: pkg/booking/cart.go:171
#: pkg/booking/cart.go:142
msgctxt "cart"
msgid "Night"
msgstr "Nuit"
#: pkg/booking/cart.go:172
#: pkg/booking/cart.go:143
msgctxt "cart"
msgid "Adult"
msgstr "Adulte"
#: pkg/booking/cart.go:173
#: pkg/booking/cart.go:144
msgctxt "cart"
msgid "Teenager"
msgstr "Adolescent"
#: pkg/booking/cart.go:174
#: pkg/booking/cart.go:145
msgctxt "cart"
msgid "Child"
msgstr "Enfant"
#: pkg/booking/cart.go:193
#: pkg/booking/cart.go:164
msgctxt "cart"
msgid "Tourist tax"
msgstr "Taxe touristique"
@ -2487,106 +2483,138 @@ msgstr "Lintégration sélectionnée nest pas valide."
msgid "The merchant key is not valid."
msgstr "La clé marchand nest pas valide."
#: pkg/booking/public.go:342
msgid "Full name can not be empty."
msgstr "Le nom complet ne peut pas être vide."
#: pkg/booking/public.go:343
msgid "Full name must have at least one letter."
msgstr "Le nom complet doit comporter au moins une lettre."
#: pkg/booking/public.go:361
msgid "Arrival date can not be empty"
msgstr "La date darrivée ne peut pas être vide"
#: pkg/booking/public.go:362
#: pkg/booking/public.go:318 pkg/booking/public.go:347
msgid "Arrival date must be a valid date."
msgstr "La date darrivée doit être une date valide."
#: pkg/booking/public.go:366
msgid "Departure date can not be empty"
msgstr "La date de départ ne peut pas être vide"
#: pkg/booking/public.go:367
#: pkg/booking/public.go:332 pkg/booking/public.go:354
msgid "Departure date must be a valid date."
msgstr "La date de départ doit être une date valide."
#: pkg/booking/public.go:368
msgid "The departure date must be after the arrival date."
msgstr "La date de départ doit être postérieure à la date darrivée."
#: pkg/booking/public.go:346
msgid "Arrival date can not be empty"
msgstr "La date darrivée ne peut pas être vide"
#: pkg/booking/public.go:372
#: pkg/booking/public.go:348
#, c-format
msgid "Arrival date must be %s or after."
msgstr "La date darrivée doit être égale ou postérieure à %s."
#: pkg/booking/public.go:349
#, c-format
msgid "Arrival date must be %s or before."
msgstr "La date darrivée doit être antérieure ou égale à %s."
#: pkg/booking/public.go:353
msgid "Departure date can not be empty"
msgstr "La date de départ ne peut pas être vide"
#: pkg/booking/public.go:355
#, c-format
msgid "Departure date must be %s or after."
msgstr "La date de départ doit être égale ou postérieure à %s."
#: pkg/booking/public.go:356
#, c-format
msgid "Departure date must be %s or before."
msgstr "La date de départ doit être antérieure ou égale à %s."
#: pkg/booking/public.go:394
#, c-format
msgid "There can be at most %d guests in this accommodation."
msgstr "Il peut y avoir au plus %d invités dans cet hébergement."
#: pkg/booking/public.go:413
msgid "Number of adults can not be empty"
msgstr "Le nombre dadultes ne peut pas être vide."
#: pkg/booking/public.go:373
#: pkg/booking/public.go:414
msgid "Number of adults must be an integer."
msgstr "Le nombre dadultes doit être un entier."
#: pkg/booking/public.go:374
#: pkg/booking/public.go:415
msgid "There must be at least one adult."
msgstr "Il doit y avoir au moins un adulte."
#: pkg/booking/public.go:377
#: pkg/booking/public.go:418
msgid "Number of teenagers can not be empty"
msgstr "Le nombre dadolescents ne peut pas être vide."
#: pkg/booking/public.go:378
#: pkg/booking/public.go:419
msgid "Number of teenagers must be an integer."
msgstr "Le nombre dadolescents doit être un entier."
#: pkg/booking/public.go:379
#: pkg/booking/public.go:420
msgid "Number of teenagers can not be negative."
msgstr "Le nombre dadolescents ne peut pas être négatif."
#: pkg/booking/public.go:382
#: pkg/booking/public.go:423
msgid "Number of children can not be empty"
msgstr "Le nombre denfants ne peut pas être vide."
#: pkg/booking/public.go:383
#: pkg/booking/public.go:424
msgid "Number of children must be an integer."
msgstr "Le nombre denfants doit être un entier."
#: pkg/booking/public.go:384
#: pkg/booking/public.go:425
msgid "Number of children can not be negative."
msgstr "Le nombre denfants ne peut pas être négatif."
#: pkg/booking/public.go:387
#: pkg/booking/public.go:428
msgid "Number of dogs can not be empty"
msgstr "Le nombre de chiens ne peut pas être vide."
#: pkg/booking/public.go:388
#: pkg/booking/public.go:429
msgid "Number of dogs must be an integer."
msgstr "Le nombre de chiens nuits être un entier."
#: pkg/booking/public.go:389
#: pkg/booking/public.go:430
msgid "Number of dogs can not be negative."
msgstr "Le nombre de chiens ne peut pas être négatif."
#: pkg/booking/public.go:392
msgid "It is mandatory to agree to the reservation conditions."
msgstr "Il est obligatoire daccepter les conditions de réservation."
#: pkg/booking/public.go:395
#: pkg/booking/public.go:501
#, c-format
msgid "%s can not be empty"
msgstr "%s ne peut pas être vide"
#: pkg/booking/public.go:396
#: pkg/booking/public.go:502
#, c-format
msgid "%s must be an integer."
msgstr "%s doit être un entier."
#: pkg/booking/public.go:397
#: pkg/booking/public.go:503
#, c-format
msgid "%s must be %d or greater."
msgstr "%s doit être %d ou plus."
#: pkg/booking/public.go:398
#: pkg/booking/public.go:504
#, c-format
msgid "%s must be at most %d."
msgstr "%s doit être tout au plus %d."
#: pkg/booking/public.go:562
msgid "Full name can not be empty."
msgstr "Le nom complet ne peut pas être vide."
#: pkg/booking/public.go:563
msgid "Full name must have at least one letter."
msgstr "Le nom complet doit comporter au moins une lettre."
#: pkg/booking/public.go:580
msgid "It is mandatory to agree to the reservation conditions."
msgstr "Il est obligatoire daccepter les conditions de réservation."
#~ msgctxt "input"
#~ msgid "Check-in Date"
#~ msgstr "Date d'arrivée"
#~ msgctxt "input"
#~ msgid "Check-out Date"
#~ msgstr "Date de départ"
#~ msgid "The departure date must be after the arrival date."
#~ msgstr "La date de départ doit être postérieure à la date darrivée."
#~ msgid "Campsite Montagut is an ideal starting point for quiet outings, climbing, swimming in the river and gorges, volcanoes, the Fageda den Jordà, cycle tours for all ages…."
#~ msgstr "Le camping Montagut est un point de départ idéal pour des sorties tranquilles, de lescalade, des baignades dans la rivière et les gorges, les volcans, la Fageda dâen Jordã, des randonnées à vélo pour tous les âges…."

View File

@ -1,58 +0,0 @@
(function () {
'use strict';
function updateDepartureDate(arrivalDateField) {
const arrivalDate = new Date(arrivalDateField.value);
if (isNaN(arrivalDate.getTime())) {
return;
}
const departureDateField = document.querySelector('[name="departure_date"]');
if (!departureDateField) {
return;
}
function updateDepartureDate(date) {
departureDateField.value = date;
departureDateField.dispatchEvent(new Event('input', {bubbles: true}));
}
const minNights = Math.max(1, parseInt(departureDateField.dataset.minNights, 10) || 0);
arrivalDate.setUTCDate(arrivalDate.getUTCDate() + minNights);
const minDate = formatDate(arrivalDate);
departureDateField.setAttribute('min', minDate);
const departureDate = new Date(departureDateField.value);
const validDepartureDate = !isNaN(departureDate.getTime())
if (!validDepartureDate || departureDate < arrivalDate) {
updateDepartureDate(minDate);
}
const maxNights = parseInt(departureDateField.dataset.maxNights, 10) || 0;
if (maxNights > 0) {
arrivalDate.setUTCDate(arrivalDate.getUTCDate() + maxNights - minNights);
const maxDate = formatDate(arrivalDate);
departureDateField.setAttribute('max', maxDate);
if (validDepartureDate && departureDate >= arrivalDate) {
updateDepartureDate(maxDate);
}
}
}
function formatDate(date) {
return `${date.getFullYear()}-${zeroPad(date.getMonth() + 1)}-${zeroPad(date.getDate())}`;
}
function zeroPad(num) {
return `${num < 10 ? '0' : ''}${num}`
}
const arrivalDateField = document.querySelector('[name="arrival_date"]');
if (!arrivalDateField) {
return;
}
arrivalDateField.addEventListener('change', function (event) {
updateDepartureDate(event.target);
});
updateDepartureDate(arrivalDateField);
})();

View File

@ -673,8 +673,8 @@ dl, .nature > div, .outside_activities > div, .campsite_info {
@media (max-width: 48rem) {
.campsite_type_title {
padding-bottom: 6rem;
}
padding-bottom: 6rem;
}
.carousel {
overflow: unset;
@ -1290,6 +1290,10 @@ button {
color: var(--contrast);
}
button[disabled] {
background-color: var(--contrast-3);
border-color: var(--contrast-3);
}
/* radio buttins + checkbox */
@ -1371,14 +1375,6 @@ input[type="checkbox"]:focus {
flex: .33;
position: sticky;
top: 13rem;
opacity: 0;
visibility: hidden;
transition: opacity .5s ease;
}
#booking > footer.is-visible {
opacity: 1;
visibility: visible;
}
#booking br {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,245 +0,0 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-FileCopyrightText: 2023 Oriol Carbonell <info@oriolcarbonell.cat>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Booking" "title" )}}
{{- end }}
{{ define "head" -}}
{{ template "alpineScript" }}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.publicPage*/ -}}
<h2>{{( pgettext "Booking" "title" )}}</h2>
{{ with .Form -}}
<form id="booking" action="/{{ currentLocale }}/booking" method="post"
x-data="{campsiteType: '', arrivalDate: '', departureDate: '', hasCampsite() { return this.campsiteType !== '' }, hasDates() { return this.arrivalDate !== '' && this.departureDate !== ''} }"
x-init="campsiteType = (document.querySelector('[x-model=campsiteType]:checked') || {}).value || ''"
>
<fieldset
data-hx-get="/{{ currentLocale }}/booking/cart"
data-hx-include="#booking"
data-hx-trigger="load,change"
data-hx-target="#booking footer"
>
<fieldset class="accommodation">
<legend>{{( pgettext "Accommodation" "title" )}}</legend>
{{ range .CampsiteType.Options -}}
<label><input type="radio" name="{{ $.Form.CampsiteType.Name }}" value="{{ .Value }}"
x-model="campsiteType"
{{ if $.Form.CampsiteType.IsSelected .Value }}checked{{ end }}
> {{ .Label }}</label><br>
{{- end }}
{{ template "error-message" .CampsiteType }}
</fieldset>
<fieldset class="booking-period"
x-cloak
x-show="hasCampsite()"
x-transition.duration.250ms
>
<legend>{{( pgettext "Booking Period" "title" )}}</legend>
{{ with .ArrivalDate -}}
<label>
{{( pgettext "Arrival date" "input" )}}<br>
<input type="date" required
min="{{ today }}"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
x-model.fill="arrivalDate"
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .DepartureDate -}}
<label>
{{( pgettext "Departure date" "input" )}}<br>
<input type="date" required
min="{{ tomorrow }}"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
x-model.fill="departureDate"
><br>
</label>
{{ template "error-message" . }}
{{- end }}
</fieldset>
<fieldset class="guests campsite-options"
x-cloak
x-show="hasCampsite() && hasDates()"
x-transition.duration.250ms
>
<legend>{{( pgettext "Guests" "title" )}}</legend>
{{ with .NumberAdults -}}
<label>
{{( pgettext "Adults aged 17 or older" "input" )}}<br>
<input type="number" required
name="{{ .Name }}" value="{{ .Val }}" min="1"
{{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .NumberTeenagers -}}
<label>
{{( pgettext "Teenagers from 11 to 16 years old" "input" )}}<br>
<input type="number" required
name="{{ .Name }}" value="{{ .Val }}" min="0"
{{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .NumberChildren -}}
<label>
{{( pgettext "Children from 2 to 10 years old" "input" )}}<br>
<input type="number" required
name="{{ .Name }}" value="{{ .Val }}" min="0"
{{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .NumberDogs -}}
<label>
{{( pgettext "Dogs" "input" )}}<br>
<input type="number" required
name="{{ .Name }}" value="{{ .Val }}" min="0"
{{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
</fieldset>
{{ range $campsiteType := .CampsiteType.Options -}}
{{ $options := index $.Form.CampsiteTypeOptions .Value }}
{{ $zonePreferences := index $.Form.ZonePreferences .Value }}
{{ if or $options $zonePreferences }}
<fieldset class="campsite-options"
x-cloak
x-show="campsiteType === '{{ $campsiteType.Value }}' && hasDates()"
x-transition.duration.250ms>
<legend>{{ .Label }}</legend>
{{ with $zonePreferences -}}
<label>
<span>
{{( pgettext "Area preferences (optional)" "input" )}}
<a href="/{{ currentLocale }}/campground?zones"
target="_blank">{{( gettext "Campground map" )}}</a>
</span><br>
<input type="text"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ range $options -}}
<label>
{{ .Label }}<br>
<input type="number" required
name="{{ .Input.Name }}" value="{{ .Input.Val }}"
min="{{ .Min }}" max="{{ .Max }}"
{{ template "error-attrs" .Input }}
><br>
</label>
{{ template "error-message" .Input }}
{{- end }}
</fieldset>
{{- end }}
{{- end }}
<fieldset class="customer-details"
x-cloak
x-show="hasCampsite() && hasDates()"
x-transition.duration.250ms
>
<legend>{{( pgettext "Customer Details" "title" )}}</legend>
{{ with .FullName -}}
<label>
{{( pgettext "Full name" "input" )}}<br>
<input type="text" required autocomplete="name" minlength="2"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .Address -}}
<label>
{{( pgettext "Address (optional)" "input" )}}<br>
<input type="text" autocomplete="billing street-address"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .PostalCode -}}
<label>
{{( pgettext "Postcode (optional)" "input" )}}<br>
<input type="text" autocomplete="billing postal-code"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .City -}}
<label>
{{( pgettext "Town or village (optional)" "input" )}}<br>
<input type="text"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .Country -}}
<label>
{{( pgettext "Country" "input" )}}<br>
<select name="{{ .Name }}"
required autocomplete="country">
<option>{{( gettext "Choose a country" )}}</option>
{{ template "error-attrs" . }}>{{ template "list-options" . }}
</select><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .Email -}}
<label>
{{( pgettext "Email" "input" )}}<br>
<input type="email" required autocomplete="email"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .Phone -}}
<label>
{{( pgettext "Phone" "input" )}}<br>
<input type="tel" required autocomplete="tel"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .ACSICard -}}
<label class="full-row">
<input type="checkbox" name="{{ .Name }}" {{ if .Checked}}checked{{ end }}
{{ template "error-attrs" . }}
> {{( pgettext "ACSI card? (optional)" "input" )}}</label><br>
{{ template "error-message" . }}
{{- end }}
{{ with .Agreement -}}
<label class="full-row">
<input type="checkbox" required name="{{ .Name }}" {{ if .Checked}}checked{{ end }}
{{ template "error-attrs" . }}
> {{ printf ( pgettext "I have read and I accept %[1]sthe reservation conditions%[2]s" "input" ) (printf "<a href=\"/%s/legal/reservation\" rel=\"terms-of-service\" target=\"_blank\">" currentLocale) (print "</a>") | raw }}
</label><br>
{{ template "error-message" . }}
{{- end }}
</fieldset>
</fieldset>
<footer :class="hasCampsite() && hasDates() && 'is-visible'">
{{ template "cart.gohtml" $.Cart }}
</footer>
</form>
{{- end }}
<script src="/static/booking-dates.js"></script>
{{- end }}

View File

@ -0,0 +1,20 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-FileCopyrightText: 2023 Oriol Carbonell <info@oriolcarbonell.cat>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Booking" "title" )}}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.publicPage*/ -}}
<h2>{{ template "title" . }}</h2>
<form id="booking" action="/{{ currentLocale }}/booking" method="post"
data-hx-include="this"
data-hx-target="this"
data-hx-replace-url="true"
>
{{ template "fields.gohtml" . }}
</form>
{{- end }}

View File

@ -0,0 +1,23 @@
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/booking.DateFields*/ -}}
{{ with .ArrivalDate -}}
<label>
{{( pgettext "Arrival date" "input" )}}<br>
<input type="date" required
min="{{ formatDateAttr .MinDate }}"
max="{{ formatDateAttr .MaxDate }}"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .DepartureDate -}}
<label>
{{( pgettext "Departure date" "input" )}}<br>
<input type="date" required
min="{{ formatDateAttr .MinDate }}"
max="{{ formatDateAttr .MaxDate }}"
name="{{ .Name }}" value="{{ .Val }}" {{ template "error-attrs" . }}
><br>
</label>
{{ template "error-message" . }}
{{- end }}

View File

@ -34,23 +34,16 @@
</div>
{{- end }}
<form action="/{{ currentLocale }}/booking" method="get" class="campsite_type_booking">
<form action="/{{ currentLocale }}/booking" method="get" class="campsite_type_booking"
data-hx-include="this"
>
<input type="hidden" name="campsite_type" value="{{ .Slug }}">
<fieldset>
<label>
{{( pgettext "Check-in Date" "input")}}
<br>
<input name="arrival_date" type="date" min="{{ today }}" required>
<br>
</label>
<label>
{{( pgettext "Check-out Date" "input")}}
<br>
<input name="departure_date" type="date" min="{{ tomorrow }}"
data-min-nights="{{ .MinNights}}" data-max-nights="{{ .MaxNights }}"
>
<br>
</label>
<fieldset
data-hx-get="/{{ currentLocale }}/campsites/types/{{ .Slug }}/dates"
data-hx-trigger="change"
data-hx-target="this"
>
{{ template "dates.gohtml" .BookingDates }}
</fieldset>
<footer>
<button type="submit">{{( pgettext "Book" "action" )}} <span>→</span></button>
@ -91,8 +84,8 @@
{{- end -}}
</dd>
{{- end }}
{{ if gt $.MinNights 1 -}}
<dd x-show="open">{{ printf (gettext "*Minimum %d nights per stay") $.MinNights }}</dd>
{{ if gt $.BookingDates.MinNights 1 -}}
<dd x-show="open">{{ printf (gettext "*Minimum %d nights per stay") $.BookingDates.MinNights }}</dd>
{{- end }}
</div>
{{- end }}
@ -164,6 +157,16 @@
const right = calendar.querySelector('header button:last-of-type');
right.addEventListener('click', () => carousel.scrollLeft += month.clientWidth);
})();
(function () {
'use strict';
const arrivalDate = document.querySelector('input[name="arrival_date"]');
if (arrivalDate && arrivalDate.value !== '') {
const load = htmx.on('htmx:load', function () {
htmx.off('htmx:load', load);
htmx.trigger(arrivalDate, 'change', {bubbles: true});
});
}
})();
</script>
<script src="/static/booking-dates.js?v={{ camperVersion }}"></script>
{{- end }}