Add the services page

This page is more or less similar to home, in terms of database: it
has a carousel and a list of items; in this case, the definition of
campsite services.

As i said early, when adding the home carousel, this carousel has its
own relation and set of functions to manage slides.  They are also
duplicated in Go code, but i think i will need to refactor it later to
a carousel package or something like that, because both relations have
the exact same fields and types, so it makes no sense to have twice the
same code.

I already did it with the CSS and JavaScript code, mostly because it was
easier to replace the `.surroundings div` selector with `.carousel`, and
because that way i can have a single template that loads and initializes
Slick.

There is no UI to create or edit service definitions, although there are
the SQL functions, because i have no more time now, and Oriol needs to
check that the style is correct for that page.
This commit is contained in:
jordi fita mas 2023-09-17 03:42:16 +02:00
parent 8b8dda7969
commit afe77f2296
69 changed files with 2712 additions and 212 deletions

View File

@ -38,6 +38,10 @@ values (52, 'plots.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/plo
, (52, 'home_carousel6.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel6.jpg]])', 'base64')) , (52, 'home_carousel6.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel6.jpg]])', 'base64'))
, (52, 'home_carousel7.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel7.jpg]])', 'base64')) , (52, 'home_carousel7.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel7.jpg]])', 'base64'))
, (52, 'home_carousel8.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel8.jpg]])', 'base64')) , (52, 'home_carousel8.jpg', 'image/jpeg', decode('m4_esyscmd([[base64 -w0 demo/home_carousel8.jpg]])', 'base64'))
, (52, 'services_carousel0.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/services_carousel0.avif]])', 'base64'))
, (52, 'services_carousel1.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/services_carousel1.avif]])', 'base64'))
, (52, 'services_carousel2.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/services_carousel2.avif]])', 'base64'))
, (52, 'services_carousel3.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/services_carousel3.avif]])', 'base64'))
; ;
insert into home_carousel (media_id, caption) insert into home_carousel (media_id, caption)
@ -59,6 +63,25 @@ values (66, 'en', 'Santa Margarida volcano')
, (67, 'es', 'Piletón oscuro Sadernes') , (67, 'es', 'Piletón oscuro Sadernes')
; ;
insert into services_carousel (media_id, caption)
values (75, 'La Garrotxa')
, (76, 'Tenda')
, (77, 'Parceŀles')
, (78, 'Hamaca')
, (63, 'Safari Tents')
;
insert into services_carousel_i18n (media_id, lang_tag, caption)
values (76, 'en', 'Tent')
, (76, 'es', 'Tenda')
, (77, 'en', 'Plots')
, (77, 'es', 'Parcelas')
, (78, 'en', 'Hammock')
, (78, 'es', 'Amaca')
, (63, 'en', 'Safari Tents')
, (63, 'es', 'Tiendas Safari')
;
alter sequence campsite_type_campsite_type_id_seq restart with 72; alter sequence campsite_type_campsite_type_id_seq restart with 72;
insert into campsite_type (company_id, name, media_id, description) insert into campsite_type (company_id, name, media_id, description)
values (52, 'Parceŀles', 62, '') values (52, 'Parceŀles', 62, '')
@ -77,4 +100,59 @@ values (72, 'en', 'Plots', '')
, (75, 'es', 'Cabañas de madera', '') , (75, 'es', 'Cabañas de madera', '')
; ;
alter sequence service_service_id_seq restart with 82;
insert into service (company_id, icon_name, name, description)
values (52, 'information', 'Informació', '<p>A la recepció linformarem del que pot fer des del càmping mateix o pels voltants.</p>')
, (52, 'wifi', 'WiFi', '<p>Un 80 % de làrea del càmping disposa daccés WiFi lliure.</p>')
, (52, 'restaurant', 'Bar & Tapes', '<p>Oberts:</p><ul><li>De l01/07 al 28/08: cada dia</li><li>Dabril a setembre: caps de setmana i ponts</li></ul>')
, (52, 'store', 'Botiga', '<p>Oberta a diari.</p><p>Venda de pa del dia per encàrrec.</p>')
, (52, 'wheelchair', 'Accessibilitat', '<p>Piscines i serveis del càmping adaptats a persones amb mobilitat reduïda.</p>')
, (52, 'toilet', 'Lavabos', '<p>Ubicació central i pràctica. Nets i ben mantinguts.</p>')
, (52, 'shower', 'Dutxa', '<p>Aigua calenta, sense fitxes.</p>')
, (52, 'baby', 'Bany per nadons', '<p>Bany individual per nadons, amb banyera i canviador.</p>')
, (52, 'pool', 'Piscina', '<p>Piscina per adults i piscina infantil.</p><p><em>(Piscines amb aigua salada.)</em></p>')
, (52, 'campfire', 'Barbacoa', '<p>Trobareu una barbacoa comunitària de carbó o la possibilitat de llogar una barbacoa de gas (no es pot fer servir llenya o carbó en les parcel·les).</p>')
, (52, 'rv', 'Estació servei per autocaravanes', '<p>Situada a lentrada del càmping.</p>')
, (52, 'castle', 'Zona de jocs', '<p>Una zona central pels més menuts.</p>')
, (52, 'ball', 'Camp desport', '<p>Amb camp de futbol, voley, tenis-taula i espai per jugar.</p>')
, (52, 'puzzle', 'Sala de jocs i televisió', '<p>Una sala pels dies de mal temps.</p>')
, (52, 'washer', 'Rentadores i assecadores', '<p>Als safareigs del càmping hi ha dues rentadores i una assecadora que funcionen amb fitxes.</p>')
, (52, 'fridge', 'Lloguer de neveres', '<p>Possibilitat de llogar neveres per estades llargues amb <a href="https://www.rentit.es/ca/portal/productes/42/">Rent It</a>.</p>')
;
insert into service_i18n (service_id, lang_tag, name, description)
values (82, 'en', 'Information', '<p>At reception we will inform you of what you can do from the campsite itself or in the surrounding area.</p>')
, (82, 'es', 'Información', '<p>A recepción le informaremos de qué puede hacer en el camping o por los alrededores.</p>')
, (83, 'en', 'WiFi', '<p>80 % of the campsite area has free WiFi access.</p>')
, (83, 'es', 'WiFi', '<p>Un 80 % del área del camping dispone de acceso WiFi libre.</p>')
, (84, 'en', 'Bar & Tapas', '<p>Open:</p><ul><li>From 07/01 to 08/28: everyday</li><li>From April to September: weekends and holidays</li></ul>')
, (84, 'es', 'Bar & Tapas', '<p>Abierto:</p><ul><li>Del 01/07 al 28/08: cada día</li><li>De abril a setiembre: fines de semana y puentes</li></ul>')
, (85, 'en', 'Shop', '<p>Open daily</p><p>Sale of daily bread to order.</p>')
, (85, 'es', 'Tienda', '<p>Abierta a diario.</p><p>Venta de pan del día por encargo.</p>')
, (86, 'en', 'Accessibility', '<p>Swimming pools and campsite services adapted to people with reduced mobility.</p>')
, (86, 'es', 'Acesibilidad', '<p>Piscinas y servicios del camping adaptados a personas con mobilidad reducida.</p>')
, (87, 'en', 'Toilets', '<p>Central and practical location. Clean and well maintained.</p>')
, (87, 'es', 'Lavabos', '<p>Ubicación central y práctica. Limpios y bien mantenidos.</p>')
, (88, 'en', 'Showers', '<p>Hot water, no tokens.</p>')
, (88, 'es', 'Duchas', '<p>Agua caliente, sin fichas.</p>')
, (89, 'en', 'Baby baths', '<p>Individual bathroom for babies, with bathtub and changing table.</p>')
, (89, 'es', 'Baño para bebés', '<p>Baños individuales para bebés, con bañera y cambiador.</p>')
, (90, 'en', 'Swimming pool', '<p>Adult pool and childrens pool.</p><p><em>(Salt water swimming pools.)</em></p>')
, (90, 'es', 'Piscina', '<p>Piscina para adultos y piscina infantil.</p><p><em>(Piscinas con agua salada.)</em></p>')
, (91, 'en', 'Barbecue', '<p>You will find a communal charcoal barbecue or the possibility of renting a gas barbecue (no wood or charcoal can be used on the plots).</p>')
, (91, 'es', 'Barbacoa', '<p>Encontraréis una barbacoa comunitaria de carbón o la posibilidad de alquilar una barbacoa de gas (no se puede utilizar leña o carbón en las parcelas).</p>')
, (92, 'en', 'RV service station', '<p>Located at the entrance of the campsite.</p>')
, (92, 'es', 'Estación servicio para autocaravanas', '<p>Situada en la entrada del camping.</p>')
, (93, 'en', 'Play area', '<p>A central area for the little ones.</p>')
, (93, 'es', 'Zona de juegos', '<p>Una zona central para los más pequeños.</p>')
, (94, 'en', 'Sports area', '<p>With football field, volleyball, table tennis and room to play.</p>')
, (94, 'es', 'Campo de deporte', '<p>Con campo de fútbol, voley, pimpón i espacio para jugar.</p>')
, (95, 'en', 'Games and television room', '<p>A room for bad weather days.</p>')
, (95, 'es', 'Sala de juegos y televisión', '<p>Una sala para los días de mal tiempo.</p>')
, (96, 'en', 'Washing machines and dryers', '<p>There are two token-operated washing machines and a dryer in the campsites laundry facilities.</p>')
, (96, 'es', 'Lavadora y secadoras', '<p>A los lavaderos del camping hay dos lavadoras y una secadora que funcionana con fichas.</p>')
, (97, 'en', 'Fridge rental', '<p>Possibility to rent refrigerators for long stays with <a href="https://www.rentit.es/en/portal/productes/42/">Rent It</a>.</p>')
, (97, 'es', 'Alquiler de neveras', '<p>Posibilidad de alquilar neveras para estancias largas con <a href="https://www.rentit.es/es/portal/productes/42/">Rent It</a>.</p>')
;
commit; commit;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

23
deploy/add_service.sql Normal file
View File

@ -0,0 +1,23 @@
-- Deploy camper:add_service to pg
-- requires: roles
-- requires: schema_camper
-- requires: service
begin;
set search_path to camper, public;
create or replace function add_service(company integer, icon_name text, name text, description text) returns integer as
$$
insert into service (company_id, icon_name, name, description)
values (company, icon_name, name, xmlparse (content description))
returning service_id
;
$$
language sql
;
revoke execute on function add_service(integer, text, text, text) from public;
grant execute on function add_service(integer, text, text, text) to admin;
commit;

View File

@ -0,0 +1,25 @@
-- Deploy camper:add_services_carousel_slide to pg
-- requires: roles
-- requires: schema_camper
-- requires: services_carousel
begin;
set search_path to camper, public;
create or replace function add_services_carousel_slide(media_id integer, caption text) returns void as
$$
insert into services_carousel (media_id, caption)
values (media_id, coalesce(caption, ''))
on conflict (media_id) do update
set caption = excluded.caption
returning media_id
;
$$
language sql
;
revoke execute on function add_services_carousel_slide(integer, text) from public;
grant execute on function add_services_carousel_slide(integer, text) to admin;
commit;

View File

@ -0,0 +1,30 @@
-- Deploy camper:available_icons to pg
-- requires: schema_camper
-- requires: icon
begin;
insert into camper.icon (icon_name)
values ('baby')
, ('ball')
, ('bicycle')
, ('campfire')
, ('castle')
, ('fridge')
, ('information')
, ('kayak')
, ('outing')
, ('pool')
, ('puzzle')
, ('restaurant')
, ('route')
, ('rv')
, ('shower')
, ('store')
, ('toilet')
, ('washer')
, ('wheelchair')
, ('wifi')
;
commit;

25
deploy/edit_service.sql Normal file
View File

@ -0,0 +1,25 @@
-- Deploy camper:edit_service to pg
-- requires: roles
-- requires: schema_camper
-- requires: service
begin;
set search_path to camper, public;
create or replace function edit_service(service_id integer, icon_name text, name text, description text) returns void as
$$
update service
set icon_name = edit_service.icon_name
, name = edit_service.name
, description = xmlparse(content edit_service.description)
where service_id = edit_service.service_id
;
$$
language sql
;
revoke execute on function edit_service(integer, text, text, text) from public;
grant execute on function edit_service(integer, text, text, text) to admin;
commit;

17
deploy/icon.sql Normal file
View File

@ -0,0 +1,17 @@
-- Deploy camper:icon to pg
-- requires: roles
-- requires: schema_camper
begin;
set search_path to camper, public;
create table icon (
icon_name text not null primary key
);
grant select on table icon to guest;
grant select on table icon to employee;
grant select on table icon to admin;
commit;

View File

@ -0,0 +1,22 @@
-- Deploy camper:remove_services_carousel_slide to pg
-- requires: roles
-- requires: schema_camper
-- requires: services_carousel
-- requires: services_carousel_i18n
begin;
set search_path to camper, public;
create or replace function remove_services_carousel_slide(media_id integer) returns void as
$$
delete from services_carousel_i18n where media_id = $1;
delete from services_carousel where media_id = $1;
$$
language sql
;
revoke execute on function remove_services_carousel_slide(integer) from public;
grant execute on function remove_services_carousel_slide (integer) to admin;
commit;

58
deploy/service.sql Normal file
View File

@ -0,0 +1,58 @@
-- Deploy camper:service to pg
-- requires: roles
-- requires: schema_camper
-- requires: company
-- requires: icon
-- requires: user_profile
begin;
set search_path to camper, public;
create table service (
service_id serial primary key,
company_id integer not null references company,
icon_name text not null references icon,
name text not null constraint name_not_empty check(length(trim(name)) > 0),
description xml not null
);
grant select on table service to guest;
grant select on table service to employee;
grant select, insert, update, delete on table service to admin;
grant usage on sequence service_service_id_seq to admin;
alter table service enable row level security;
create policy guest_ok
on service
for select
using (true)
;
create policy insert_to_company
on service
for insert
with check (
company_id in (select company_id from user_profile)
)
;
create policy update_company
on service
for update
using (
company_id in (select company_id from user_profile)
)
;
create policy delete_from_company
on service
for delete
using (
company_id in (select company_id from user_profile)
)
;
commit;

23
deploy/service_i18n.sql Normal file
View File

@ -0,0 +1,23 @@
-- Deploy camper:service_i18n to pg
-- requires: roles
-- requires: schema_camper
-- requires: service
-- requires: language
begin;
set search_path to camper, public;
create table service_i18n (
service_id integer not null references service,
lang_tag text not null references language,
name text not null,
description xml not null,
primary key (service_id, lang_tag)
);
grant select on table service_i18n to guest;
grant select on table service_i18n to employee;
grant select, insert, update, delete on table service_i18n to admin;
commit;

View File

@ -0,0 +1,53 @@
-- Deploy camper:services_carousel to pg
-- requires: roles
-- requires: schema_public
-- requires: company
-- requires: media
-- requires: user_profile
begin;
set search_path to camper, public;
create table services_carousel (
media_id integer not null primary key references media,
caption text not null
);
grant select on table services_carousel to guest;
grant select on table services_carousel to employee;
grant select, insert, update, delete on table services_carousel to admin;
alter table services_carousel enable row level security;
create policy guest_ok
on services_carousel
for select
using (true)
;
create policy insert_to_company
on services_carousel
for insert
with check (
exists (select 1 from media join user_profile using (company_id) where media.media_id = services_carousel.media_id)
)
;
create policy update_company
on services_carousel
for update
using (
exists (select 1 from media join user_profile using (company_id) where media.media_id = services_carousel.media_id)
)
;
create policy delete_from_company
on services_carousel
for delete
using (
exists (select 1 from media join user_profile using (company_id) where media.media_id = services_carousel.media_id)
)
;
commit;

View File

@ -0,0 +1,22 @@
-- Deploy camper:services_carousel_i18n to pg
-- requires: roles
-- requires: schema_camper
-- requires: services_carousel
-- requires: language
begin;
set search_path to camper, public;
create table services_carousel_i18n (
media_id integer not null references services_carousel,
lang_tag text not null references language,
caption text not null,
primary key (media_id, lang_tag)
);
grant select on table services_carousel_i18n to guest;
grant select on table services_carousel_i18n to employee;
grant select, insert, update, delete on table services_carousel_i18n to admin;
commit;

View File

@ -0,0 +1,25 @@
-- Deploy camper:translate_service to pg
-- requires: roles
-- requires: schema_camper
-- requires: service_i18n
begin;
set search_path to camper, public;
create or replace function translate_service(service_id integer, lang_tag text, name text, description text) returns void as
$$
insert into service_i18n (service_id, lang_tag, name, description)
values (service_id, lang_tag, name, xmlparse(content coalesce(description, '')))
on conflict (service_id, lang_tag) do update
set name = excluded.name
, description = excluded.description
;
$$
language sql
;
revoke execute on function translate_service(integer, text, text, text) from public;
grant execute on function translate_service(integer, text, text, text) to admin;
commit;

View File

@ -0,0 +1,23 @@
-- Deploy camper:translate_services_carousel_slide to pg
-- requires: roles
-- requires: schema_camper
-- requires: services_carousel_i18n
begin;
set search_path to camper, public;
create or replace function translate_services_carousel_slide(media_id integer, lang_tag text, caption text) returns void as
$$
insert into services_carousel_i18n (media_id, lang_tag, caption)
values (media_id, lang_tag, coalesce(caption, ''))
on conflict (media_id, lang_tag) do update
set caption = excluded.caption
$$
language sql
;
revoke execute on function translate_services_carousel_slide(integer, text, text) from public;
grant execute on function translate_services_carousel_slide(integer, text, text) to admin;
commit;

View File

@ -16,6 +16,7 @@ import (
httplib "dev.tandem.ws/tandem/camper/pkg/http" httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/season" "dev.tandem.ws/tandem/camper/pkg/season"
"dev.tandem.ws/tandem/camper/pkg/services"
"dev.tandem.ws/tandem/camper/pkg/template" "dev.tandem.ws/tandem/camper/pkg/template"
) )
@ -24,6 +25,7 @@ type adminHandler struct {
company *company.AdminHandler company *company.AdminHandler
home *home.AdminHandler home *home.AdminHandler
season *season.AdminHandler season *season.AdminHandler
services *services.AdminHandler
} }
func newAdminHandler(locales locale.Locales) *adminHandler { func newAdminHandler(locales locale.Locales) *adminHandler {
@ -32,6 +34,7 @@ func newAdminHandler(locales locale.Locales) *adminHandler {
company: company.NewAdminHandler(), company: company.NewAdminHandler(),
home: home.NewAdminHandler(locales), home: home.NewAdminHandler(locales),
season: season.NewAdminHandler(), season: season.NewAdminHandler(),
services: services.NewAdminHandler(locales),
} }
} }
@ -59,6 +62,8 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data
h.home.Handler(user, company, conn).ServeHTTP(w, r) h.home.Handler(user, company, conn).ServeHTTP(w, r)
case "seasons": case "seasons":
h.season.Handler(user, company, conn).ServeHTTP(w, r) h.season.Handler(user, company, conn).ServeHTTP(w, r)
case "services":
h.services.Handler(user, company, conn).ServeHTTP(w, r)
case "": case "":
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:

View File

@ -13,18 +13,21 @@ import (
"dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/database"
"dev.tandem.ws/tandem/camper/pkg/home" "dev.tandem.ws/tandem/camper/pkg/home"
httplib "dev.tandem.ws/tandem/camper/pkg/http" httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/services"
"dev.tandem.ws/tandem/camper/pkg/template" "dev.tandem.ws/tandem/camper/pkg/template"
) )
type publicHandler struct { type publicHandler struct {
home *home.PublicHandler home *home.PublicHandler
campsite *campsite.PublicHandler campsite *campsite.PublicHandler
services *services.PublicHandler
} }
func newPublicHandler() *publicHandler { func newPublicHandler() *publicHandler {
return &publicHandler{ return &publicHandler{
home: home.NewPublicHandler(), home: home.NewPublicHandler(),
campsite: campsite.NewPublicHandler(), campsite: campsite.NewPublicHandler(),
services: services.NewPublicHandler(),
} }
} }
@ -37,6 +40,8 @@ func (h *publicHandler) Handler(user *auth.User, company *auth.Company, conn *da
h.home.Handler(user, company, conn).ServeHTTP(w, r) h.home.Handler(user, company, conn).ServeHTTP(w, r)
case "campsites": case "campsites":
h.campsite.Handler(user, company, conn).ServeHTTP(w, r) h.campsite.Handler(user, company, conn).ServeHTTP(w, r)
case "services":
h.services.Handler(user, company, conn).ServeHTTP(w, r)
case "surroundings": case "surroundings":
surroundingsHandler(user, company, conn).ServeHTTP(w, r) surroundingsHandler(user, company, conn).ServeHTTP(w, r)
default: default:

64
pkg/services/admin.go Normal file
View File

@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package services
import (
"net/http"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
)
type AdminHandler struct {
locales locale.Locales
}
func NewAdminHandler(locales locale.Locales) *AdminHandler {
return &AdminHandler{locales}
}
func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "":
switch r.Method {
case http.MethodGet:
serveHomeIndex(w, r, user, company, conn)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
case "slides":
h.carouselHandler(user, company, conn).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
func serveHomeIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
slides, err := collectSlideEntries(r.Context(), company, conn)
if err != nil {
panic(err)
}
page := &servicesIndex{
Slides: slides,
}
page.MustRender(w, r, user, company)
}
type servicesIndex struct {
Slides []*slideEntry
}
func (page *servicesIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "services/index.gohtml", page)
}

311
pkg/services/carousel.go Normal file
View File

@ -0,0 +1,311 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package services
import (
"context"
"io"
"net/http"
"strconv"
"github.com/jackc/pgx/v4"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
"dev.tandem.ws/tandem/camper/pkg/form"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
)
func (h *AdminHandler) carouselHandler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "":
switch r.Method {
case http.MethodPost:
addSlide(w, r, user, company, conn)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
case "new":
switch r.Method {
case http.MethodGet:
f := newSlideForm()
f.MustRender(w, r, user, company)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
default:
id, err := strconv.Atoi(head)
if err != nil {
http.NotFound(w, r)
}
f := newSlideForm()
if err := f.FillFromDatabase(r.Context(), conn, id); err != nil {
if database.ErrorIsNotFound(err) {
http.NotFound(w, r)
return
}
panic(err)
}
var langTag string
langTag, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch langTag {
case "":
switch r.Method {
case http.MethodGet:
f.MustRender(w, r, user, company)
case http.MethodPut:
editSlide(w, r, user, company, conn, f)
case http.MethodDelete:
deleteSlide(w, r, user, conn, id)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
}
default:
loc, ok := h.locales.Get(langTag)
if !ok {
http.NotFound(w, r)
return
}
l10n := newSlideL10nForm(f, loc)
if err := l10n.FillFromDatabase(r.Context(), conn); err != nil {
panic(err)
}
switch r.Method {
case http.MethodGet:
l10n.MustRender(w, r, user, company)
case http.MethodPut:
editSlideL10n(w, r, user, company, conn, l10n)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
}
}
}
})
}
type carouselSlide struct {
Media string
Caption string
}
func mustCollectCarouselSlides(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale) []*carouselSlide {
rows, err := conn.Query(ctx, `
select coalesce(i18n.caption, slide.caption) as l10_caption
, media.path
from services_carousel as slide
join media using (media_id)
left join services_carousel_i18n as i18n on i18n.media_id = slide.media_id and lang_tag = $1
where media.company_id = $2
`, loc.Language, company.ID)
if err != nil {
panic(err)
}
defer rows.Close()
var carousel []*carouselSlide
for rows.Next() {
slide := &carouselSlide{}
err = rows.Scan(&slide.Caption, &slide.Media)
if err != nil {
panic(err)
}
carousel = append(carousel, slide)
}
if rows.Err() != nil {
panic(rows.Err())
}
return carousel
}
type slideEntry struct {
carouselSlide
ID int
Translations []*translation
}
type translation struct {
Language string
Endonym string
Missing bool
}
func collectSlideEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*slideEntry, error) {
rows, err := conn.Query(ctx, `
select media_id
, media.path
, caption
, array_agg((lang_tag, endonym, not exists (select 1 from services_carousel_i18n as i18n where i18n.media_id = services_carousel.media_id and i18n.lang_tag = language.lang_tag)) order by endonym)
from services_carousel
join media using (media_id)
join company using (company_id)
, language
where lang_tag <> default_lang_tag
and language.selectable
and media.company_id = $1
group by media_id
, media.path
, caption
order by caption
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, company.ID)
if err != nil {
return nil, err
}
defer rows.Close()
var slides []*slideEntry
for rows.Next() {
slide := &slideEntry{}
var translations database.RecordArray
if err = rows.Scan(&slide.ID, &slide.Media, &slide.Caption, &translations); err != nil {
return nil, err
}
for _, el := range translations.Elements {
slide.Translations = append(slide.Translations, &translation{
el.Fields[0].Get().(string),
el.Fields[1].Get().(string),
el.Fields[2].Get().(bool),
})
}
slides = append(slides, slide)
}
return slides, nil
}
func addSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
f := newSlideForm()
editSlide(w, r, user, company, conn, f)
}
func editSlide(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *slideForm) {
f.process(w, r, user, company, false, func(ctx context.Context) {
bytes := f.MustReadAllMedia()
if bytes == nil {
conn.MustExec(ctx, "select add_services_carousel_slide($1, $2)", f.ID, f.Caption)
} else {
tx := conn.MustBegin(ctx)
defer tx.Rollback(ctx)
f.ID = tx.MustGetInt(ctx, "select add_media($1, $2, $3, $4)", company.ID, f.Media.Filename(), f.Media.ContentType, bytes)
tx.MustExec(ctx, "select add_services_carousel_slide($1, $2)", f.ID, f.Caption)
tx.MustCommit(ctx)
}
})
}
func deleteSlide(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn, id int) {
if err := user.VerifyCSRFToken(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
conn.MustExec(r.Context(), "select remove_services_carousel_slide($1)", id)
httplib.Redirect(w, r, "/admin/services", http.StatusSeeOther)
}
type slideForm struct {
ID int
Media *form.File
Caption *form.Input
}
func newSlideForm() *slideForm {
return &slideForm{
Media: &form.File{
Name: "media",
MaxSize: 1 << 20,
},
Caption: &form.Input{
Name: "caption",
},
}
}
func (f *slideForm) FillFromDatabase(ctx context.Context, conn *database.Conn, id int) error {
f.ID = id
row := conn.QueryRow(ctx, `
select caption
, media.path
from services_carousel
join media using (media_id)
where media_id = $1
`, id)
return row.Scan(&f.Caption.Val, &f.Media.Val)
}
func (f *slideForm) process(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, mediaRequired bool, act func(ctx context.Context)) {
if err := f.Parse(w, r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer f.Close()
if err := user.VerifyCSRFToken(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !f.Valid(user.Locale, mediaRequired) {
if !httplib.IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
f.MustRender(w, r, user, company)
return
}
act(r.Context())
httplib.Redirect(w, r, "/admin/services", http.StatusSeeOther)
}
func (f *slideForm) Parse(w http.ResponseWriter, r *http.Request) error {
maxSize := f.Media.MaxSize + 1024
r.Body = http.MaxBytesReader(w, r.Body, maxSize)
if err := r.ParseMultipartForm(maxSize); err != nil {
return err
}
f.Caption.FillValue(r)
if err := f.Media.FillValue(r); err != nil {
return err
}
return nil
}
func (f *slideForm) Close() error {
return f.Media.Close()
}
func (f *slideForm) Valid(l *locale.Locale, mediaRequired bool) bool {
v := form.NewValidator(l)
if f.HasMediaFile() {
v.CheckImageFile(f.Media, l.GettextNoop("File must be a valid PNG or JPEG image."))
} else {
v.Check(f.Media, !mediaRequired, l.GettextNoop("Slide image can not be empty."))
}
return v.AllOK
}
func (f *slideForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "services/carousel/form.gohtml", f)
}
func (f *slideForm) HasMediaFile() bool {
return f.Media.HasData()
}
func (f *slideForm) MustReadAllMedia() []byte {
if !f.HasMediaFile() {
return nil
}
bytes, err := io.ReadAll(f.Media)
if err != nil {
panic(err)
}
return bytes
}

79
pkg/services/l10n.go Normal file
View File

@ -0,0 +1,79 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package services
import (
"context"
"net/http"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
"dev.tandem.ws/tandem/camper/pkg/form"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
)
type slideL10nForm struct {
Locale *locale.Locale
ID int
Caption *form.L10nInput
}
func newSlideL10nForm(f *slideForm, loc *locale.Locale) *slideL10nForm {
return &slideL10nForm{
Locale: loc,
ID: f.ID,
Caption: f.Caption.L10nInput(),
}
}
func (l10n *slideL10nForm) FillFromDatabase(ctx context.Context, conn *database.Conn) error {
row := conn.QueryRow(ctx, `
select coalesce(i18n.caption, '') as l10n_caption
from services_carousel
left join services_carousel_i18n as i18n on services_carousel.media_id = i18n.media_id and i18n.lang_tag = $1
where services_carousel.media_id = $2
`, l10n.Locale.Language, l10n.ID)
return row.Scan(&l10n.Caption.Val)
}
func (l10n *slideL10nForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "services/carousel/l10n.gohtml", l10n)
}
func editSlideL10n(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, l10n *slideL10nForm) {
if err := l10n.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := user.VerifyCSRFToken(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !l10n.Valid(user.Locale) {
if !httplib.IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
l10n.MustRender(w, r, user, company)
return
}
conn.MustExec(r.Context(), "select translate_services_carousel_slide($1, $2, $3)", l10n.ID, l10n.Locale.Language, l10n.Caption)
httplib.Redirect(w, r, "/admin/services", http.StatusSeeOther)
}
func (l10n *slideL10nForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
l10n.Caption.FillValue(r)
return nil
}
func (l10n *slideL10nForm) Valid(l *locale.Locale) bool {
v := form.NewValidator(l)
return v.AllOK
}

96
pkg/services/public.go Normal file
View File

@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package services
import (
"context"
"net/http"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
)
type PublicHandler struct {
}
func NewPublicHandler() *PublicHandler {
return &PublicHandler{}
}
func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "":
switch r.Method {
case http.MethodGet:
home := newServicesPage()
home.MustRender(w, r, user, company, conn)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
default:
http.NotFound(w, r)
}
})
}
type servicesPage struct {
*template.PublicPage
Services []*service
Carousel []*carouselSlide
}
func newServicesPage() *servicesPage {
return &servicesPage{PublicPage: template.NewPublicPage()}
}
func (p *servicesPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
p.Setup(r, user, company, conn)
p.Services = mustCollectServices(r.Context(), company, conn, user.Locale)
p.Carousel = mustCollectCarouselSlides(r.Context(), company, conn, user.Locale)
template.MustRenderPublic(w, r, user, company, "services.gohtml", p)
}
type service struct {
IconName string
Name string
Description string
}
func mustCollectServices(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale) []*service {
rows, err := conn.Query(ctx, `
select icon_name
, coalesce(i18n.name, service.name) as l10_name
, coalesce(i18n.description, service.description)::text as l10_description
from service
left join service_i18n as i18n on service.service_id = i18n.service_id and lang_tag = $1
where service.company_id = $2
`, loc.Language, company.ID)
if err != nil {
panic(err)
}
defer rows.Close()
var items []*service
for rows.Next() {
item := &service{}
err = rows.Scan(&item.IconName, &item.Name, &item.Description)
if err != nil {
panic(err)
}
items = append(items, item)
}
if rows.Err() != nil {
panic(rows.Err())
}
return items
}

198
po/ca.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: camper\n" "Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-09-16 23:43+0200\n" "POT-Creation-Date: 2023-09-17 03:28+0200\n"
"PO-Revision-Date: 2023-07-22 23:45+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n" "Language-Team: Catalan <ca@dodds.net>\n"
@ -18,7 +18,17 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: web/templates/public/home.gohtml:6 web/templates/public/layout.gohtml:28 #: web/templates/public/services.gohtml:6
#: web/templates/public/services.gohtml:15
msgctxt "title"
msgid "Services"
msgstr "Serveis"
#: web/templates/public/services.gohtml:18
msgid "The campsite offers many different services."
msgstr "El càmping disposa de diversos serveis."
#: web/templates/public/home.gohtml:6 web/templates/public/layout.gohtml:29
msgctxt "title" msgctxt "title"
msgid "Home" msgid "Home"
msgstr "Inici" msgstr "Inici"
@ -37,8 +47,11 @@ msgid "Our services"
msgstr "Els nostres serveis" msgstr "Els nostres serveis"
#: web/templates/public/home.gohtml:34 #: web/templates/public/home.gohtml:34
#: web/templates/public/surroundings.gohtml:6
#: web/templates/public/surroundings.gohtml:10
msgctxt "title"
msgid "Surroundings" msgid "Surroundings"
msgstr "Entorn" msgstr "Lentorn"
#: web/templates/public/home.gohtml:37 #: web/templates/public/home.gohtml:37
msgid "Located in <strong>Alta Garrotxa</strong>, between the <strong>Pyrenees</strong> and the <strong>Costa Brava</strong>." msgid "Located in <strong>Alta Garrotxa</strong>, between the <strong>Pyrenees</strong> and the <strong>Costa Brava</strong>."
@ -60,12 +73,6 @@ msgstr "Descobreix lentorn"
msgid "Come and enjoy!" msgid "Come and enjoy!"
msgstr "Vine a gaudir!" msgstr "Vine a gaudir!"
#: web/templates/public/surroundings.gohtml:6
#: web/templates/public/surroundings.gohtml:10
msgctxt "title"
msgid "Surroundings"
msgstr "Lentorn"
#: web/templates/public/surroundings.gohtml:13 #: web/templates/public/surroundings.gohtml:13
msgctxt "title" msgctxt "title"
msgid "What to Do Outside the Campsite?" msgid "What to Do Outside the Campsite?"
@ -124,8 +131,8 @@ msgstr "Caiac"
msgid "There are several points where you can go by kayak, from sections of the Ter river as well as on the coast…." msgid "There are several points where you can go by kayak, from sections of the Ter river as well as on the coast…."
msgstr "Hi ha diversos punts on poder anar amb caiac, des de trams del riu Ter com també a la costa…." msgstr "Hi ha diversos punts on poder anar amb caiac, des de trams del riu Ter com també a la costa…."
#: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:23 #: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:24
#: web/templates/public/layout.gohtml:58 #: web/templates/public/layout.gohtml:59
msgid "Campsite Montagut" msgid "Campsite Montagut"
msgstr "Càmping Montagut" msgstr "Càmping Montagut"
@ -133,7 +140,7 @@ msgstr "Càmping Montagut"
msgid "Skip to main content" msgid "Skip to main content"
msgstr "Salta al contingut principal" msgstr "Salta al contingut principal"
#: web/templates/public/layout.gohtml:32 #: web/templates/public/layout.gohtml:33
msgid "Singular Lodges" msgid "Singular Lodges"
msgstr "Allotjaments singulars" msgstr "Allotjaments singulars"
@ -172,6 +179,7 @@ msgstr "Etiqueta"
#: web/templates/admin/campsite/form.gohtml:71 #: web/templates/admin/campsite/form.gohtml:71
#: web/templates/admin/campsite/type/form.gohtml:77 #: web/templates/admin/campsite/type/form.gohtml:77
#: web/templates/admin/season/form.gohtml:65 #: web/templates/admin/season/form.gohtml:65
#: web/templates/admin/services/carousel/form.gohtml:58
#: web/templates/admin/home/carousel/form.gohtml:58 #: web/templates/admin/home/carousel/form.gohtml:58
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
@ -180,6 +188,7 @@ msgstr "Actualitza"
#: web/templates/admin/campsite/form.gohtml:73 #: web/templates/admin/campsite/form.gohtml:73
#: web/templates/admin/campsite/type/form.gohtml:79 #: web/templates/admin/campsite/type/form.gohtml:79
#: web/templates/admin/season/form.gohtml:67 #: web/templates/admin/season/form.gohtml:67
#: web/templates/admin/services/carousel/form.gohtml:60
#: web/templates/admin/home/carousel/form.gohtml:60 #: web/templates/admin/home/carousel/form.gohtml:60
msgctxt "action" msgctxt "action"
msgid "Add" msgid "Add"
@ -250,6 +259,7 @@ msgid "Name"
msgstr "Nom" msgstr "Nom"
#: web/templates/admin/campsite/type/form.gohtml:59 #: web/templates/admin/campsite/type/form.gohtml:59
#: web/templates/admin/services/carousel/form.gohtml:39
#: web/templates/admin/home/carousel/form.gohtml:39 #: web/templates/admin/home/carousel/form.gohtml:39
msgctxt "input" msgctxt "input"
msgid "Cover image" msgid "Cover image"
@ -280,6 +290,7 @@ msgid "Name"
msgstr "Nom" msgstr "Nom"
#: web/templates/admin/campsite/type/index.gohtml:19 #: web/templates/admin/campsite/type/index.gohtml:19
#: web/templates/admin/services/index.gohtml:20
#: web/templates/admin/home/index.gohtml:20 #: web/templates/admin/home/index.gohtml:20
msgctxt "campsite type" msgctxt "campsite type"
msgid "Translations" msgid "Translations"
@ -297,18 +308,21 @@ msgstr "Traducció del tipus dallotjament a %s"
#: web/templates/admin/campsite/type/l10n.gohtml:22 #: web/templates/admin/campsite/type/l10n.gohtml:22
#: web/templates/admin/campsite/type/l10n.gohtml:34 #: web/templates/admin/campsite/type/l10n.gohtml:34
#: web/templates/admin/services/carousel/l10n.gohtml:22
#: web/templates/admin/home/carousel/l10n.gohtml:22 #: web/templates/admin/home/carousel/l10n.gohtml:22
msgid "Source:" msgid "Source:"
msgstr "Origen:" msgstr "Origen:"
#: web/templates/admin/campsite/type/l10n.gohtml:24 #: web/templates/admin/campsite/type/l10n.gohtml:24
#: web/templates/admin/campsite/type/l10n.gohtml:37 #: web/templates/admin/campsite/type/l10n.gohtml:37
#: web/templates/admin/services/carousel/l10n.gohtml:24
#: web/templates/admin/home/carousel/l10n.gohtml:24 #: web/templates/admin/home/carousel/l10n.gohtml:24
msgctxt "input" msgctxt "input"
msgid "Translation:" msgid "Translation:"
msgstr "Traducció:" msgstr "Traducció:"
#: web/templates/admin/campsite/type/l10n.gohtml:46 #: web/templates/admin/campsite/type/l10n.gohtml:46
#: web/templates/admin/services/carousel/l10n.gohtml:33
#: web/templates/admin/home/carousel/l10n.gohtml:33 #: web/templates/admin/home/carousel/l10n.gohtml:33
msgctxt "action" msgctxt "action"
msgid "Translate" msgid "Translate"
@ -385,6 +399,85 @@ msgctxt "action"
msgid "Login" msgid "Login"
msgstr "Entra" msgstr "Entra"
#: web/templates/admin/services/carousel/form.gohtml:8
#: web/templates/admin/services/carousel/form.gohtml:27
#: web/templates/admin/home/carousel/form.gohtml:8
#: web/templates/admin/home/carousel/form.gohtml:27
msgctxt "title"
msgid "Edit Carousel Slide"
msgstr "Edició de la diapositiva del carrusel"
#: web/templates/admin/services/carousel/form.gohtml:10
#: web/templates/admin/services/carousel/form.gohtml:29
#: web/templates/admin/home/carousel/form.gohtml:10
#: web/templates/admin/home/carousel/form.gohtml:29
msgctxt "title"
msgid "New Carousel Slide"
msgstr "Nova diapositiva del carrusel"
#: web/templates/admin/services/carousel/form.gohtml:48
#: web/templates/admin/services/carousel/l10n.gohtml:21
#: web/templates/admin/home/carousel/form.gohtml:48
#: web/templates/admin/home/carousel/l10n.gohtml:21
msgctxt "input"
msgid "Caption"
msgstr "Llegenda"
#: web/templates/admin/services/carousel/l10n.gohtml:7
#: web/templates/admin/services/carousel/l10n.gohtml:15
#: web/templates/admin/home/carousel/l10n.gohtml:7
#: web/templates/admin/home/carousel/l10n.gohtml:15
msgctxt "title"
msgid "Translate Carousel Slide to %s"
msgstr "Traducció de la diapositiva del carrusel a %s"
#: web/templates/admin/services/index.gohtml:6
#: web/templates/admin/layout.gohtml:76 web/templates/admin/home/index.gohtml:6
msgctxt "title"
msgid "Home Page"
msgstr "Pàgina dinici"
#: web/templates/admin/services/index.gohtml:12
#: web/templates/admin/home/index.gohtml:12
msgctxt "title"
msgid "Carousel"
msgstr "Carrusel"
#: web/templates/admin/services/index.gohtml:13
#: web/templates/admin/home/index.gohtml:13
msgctxt "action"
msgid "Add slide"
msgstr "Afegeix diapositiva"
#: web/templates/admin/services/index.gohtml:18
#: web/templates/admin/home/index.gohtml:18
msgctxt "header"
msgid "Image"
msgstr "Imatge"
#: web/templates/admin/services/index.gohtml:19
#: web/templates/admin/home/index.gohtml:19
msgctxt "header"
msgid "Caption"
msgstr "Llegenda"
#: web/templates/admin/services/index.gohtml:21
#: web/templates/admin/home/index.gohtml:21
msgctxt "campsite type"
msgid "Actions"
msgstr "Accions"
#: web/templates/admin/services/index.gohtml:40
#: web/templates/admin/home/index.gohtml:40
msgctxt "action"
msgid "Delete"
msgstr "Esborra"
#: web/templates/admin/services/index.gohtml:48
#: web/templates/admin/home/index.gohtml:48
msgid "No slides added yet."
msgstr "No sha afegit cap diapositiva encara."
#: web/templates/admin/profile.gohtml:6 web/templates/admin/profile.gohtml:12 #: web/templates/admin/profile.gohtml:6 web/templates/admin/profile.gohtml:12
#: web/templates/admin/layout.gohtml:29 #: web/templates/admin/layout.gohtml:29
msgctxt "title" msgctxt "title"
@ -505,68 +598,11 @@ msgctxt "action"
msgid "Logout" msgid "Logout"
msgstr "Surt" msgstr "Surt"
#: web/templates/admin/layout.gohtml:76 web/templates/admin/home/index.gohtml:6 #: web/templates/admin/layout.gohtml:79
#, fuzzy
msgctxt "title" msgctxt "title"
msgid "Home Page" msgid "Services Page"
msgstr "Pàgina dinici" msgstr "Serveis"
#: web/templates/admin/home/carousel/form.gohtml:8
#: web/templates/admin/home/carousel/form.gohtml:27
msgctxt "title"
msgid "Edit Carousel Slide"
msgstr "Edició de la diapositiva del carrusel"
#: web/templates/admin/home/carousel/form.gohtml:10
#: web/templates/admin/home/carousel/form.gohtml:29
msgctxt "title"
msgid "New Carousel Slide"
msgstr "Nova diapositiva del carrusel"
#: web/templates/admin/home/carousel/form.gohtml:48
#: web/templates/admin/home/carousel/l10n.gohtml:21
msgctxt "input"
msgid "Caption"
msgstr "Llegenda"
#: web/templates/admin/home/carousel/l10n.gohtml:7
#: web/templates/admin/home/carousel/l10n.gohtml:15
msgctxt "title"
msgid "Translate Carousel Slide to %s"
msgstr "Traducció de la diapositiva del carrusel a %s"
#: web/templates/admin/home/index.gohtml:12
msgctxt "title"
msgid "Carousel"
msgstr "Carrusel"
#: web/templates/admin/home/index.gohtml:13
msgctxt "action"
msgid "Add slide"
msgstr "Afegeix diapositiva"
#: web/templates/admin/home/index.gohtml:18
msgctxt "header"
msgid "Image"
msgstr "Imatge"
#: web/templates/admin/home/index.gohtml:19
msgctxt "header"
msgid "Caption"
msgstr "Llegenda"
#: web/templates/admin/home/index.gohtml:21
msgctxt "campsite type"
msgid "Actions"
msgstr "Accions"
#: web/templates/admin/home/index.gohtml:40
msgctxt "action"
msgid "Delete"
msgstr "Esborra"
#: web/templates/admin/home/index.gohtml:48
msgid "No slides added yet."
msgstr "No sha afegit cap diapositiva encara."
#: web/templates/admin/media/index.gohtml:6 #: web/templates/admin/media/index.gohtml:6
#: web/templates/admin/media/index.gohtml:12 #: web/templates/admin/media/index.gohtml:12
@ -637,11 +673,12 @@ msgstr "La confirmació no es correspon amb la contrasenya."
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "Lidioma escollit no és vàlid." msgstr "Lidioma escollit no és vàlid."
#: pkg/app/user.go:253 pkg/campsite/types/admin.go:296 pkg/home/carousel.go:287 #: pkg/app/user.go:253 pkg/campsite/types/admin.go:296
#: pkg/services/carousel.go:287 pkg/home/carousel.go:287
msgid "File must be a valid PNG or JPEG image." msgid "File must be a valid PNG or JPEG image."
msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida." msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida."
#: pkg/app/admin.go:47 #: pkg/app/admin.go:50
msgid "Access forbidden" msgid "Access forbidden"
msgstr "Accés prohibit" msgstr "Accés prohibit"
@ -665,6 +702,10 @@ msgstr "No podeu deixar el color en blanc."
msgid "This color is not valid. It must be like #123abc." msgid "This color is not valid. It must be like #123abc."
msgstr "Aquest color no és vàlid. Hauria de ser similar a #123abc." msgstr "Aquest color no és vàlid. Hauria de ser similar a #123abc."
#: pkg/services/carousel.go:289 pkg/home/carousel.go:289
msgid "Slide image can not be empty."
msgstr "No podeu deixar la imatge de la diapositiva en blanc."
#: pkg/company/admin.go:186 #: pkg/company/admin.go:186
msgid "Selected country is not valid." msgid "Selected country is not valid."
msgstr "El país escollit no és vàlid." msgstr "El país escollit no és vàlid."
@ -729,14 +770,13 @@ msgstr "No podeu deixar el format del número de factura en blanc."
msgid "Cross-site request forgery detected." msgid "Cross-site request forgery detected."
msgstr "Sha detectat un intent de falsificació de petició a llocs creuats." msgstr "Sha detectat un intent de falsificació de petició a llocs creuats."
#: pkg/home/carousel.go:289
msgid "Slide image can not be empty."
msgstr "No podeu deixar la imatge de la diapositiva en blanc."
#: pkg/media/admin.go:164 #: pkg/media/admin.go:164
msgid "Uploaded file can not be empty." msgid "Uploaded file can not be empty."
msgstr "No podeu deixar el fitxer del mèdia en blanc." msgstr "No podeu deixar el fitxer del mèdia en blanc."
#~ msgid "Surroundings"
#~ msgstr "Entorn"
#~ msgid "Legend" #~ msgid "Legend"
#~ msgstr "Llegenda" #~ msgstr "Llegenda"

198
po/es.po
View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: camper\n" "Project-Id-Version: camper\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-09-16 23:55+0200\n" "POT-Creation-Date: 2023-09-17 03:28+0200\n"
"PO-Revision-Date: 2023-07-22 23:46+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n" "Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n" "Language-Team: Spanish <es@tp.org.es>\n"
@ -18,7 +18,17 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: web/templates/public/home.gohtml:6 web/templates/public/layout.gohtml:28 #: web/templates/public/services.gohtml:6
#: web/templates/public/services.gohtml:15
msgctxt "title"
msgid "Services"
msgstr "Servicios"
#: web/templates/public/services.gohtml:18
msgid "The campsite offers many different services."
msgstr "El camping dispone de varios servicios."
#: web/templates/public/home.gohtml:6 web/templates/public/layout.gohtml:29
msgctxt "title" msgctxt "title"
msgid "Home" msgid "Home"
msgstr "Inicio" msgstr "Inicio"
@ -37,8 +47,11 @@ msgid "Our services"
msgstr "Nuestros servicios" msgstr "Nuestros servicios"
#: web/templates/public/home.gohtml:34 #: web/templates/public/home.gohtml:34
#: web/templates/public/surroundings.gohtml:6
#: web/templates/public/surroundings.gohtml:10
msgctxt "title"
msgid "Surroundings" msgid "Surroundings"
msgstr "Entorno" msgstr "El entorno"
#: web/templates/public/home.gohtml:37 #: web/templates/public/home.gohtml:37
msgid "Located in <strong>Alta Garrotxa</strong>, between the <strong>Pyrenees</strong> and the <strong>Costa Brava</strong>." msgid "Located in <strong>Alta Garrotxa</strong>, between the <strong>Pyrenees</strong> and the <strong>Costa Brava</strong>."
@ -60,12 +73,6 @@ msgstr "Descubre el entorno"
msgid "Come and enjoy!" msgid "Come and enjoy!"
msgstr "¡Ven a disfrutar!" msgstr "¡Ven a disfrutar!"
#: web/templates/public/surroundings.gohtml:6
#: web/templates/public/surroundings.gohtml:10
msgctxt "title"
msgid "Surroundings"
msgstr "El entorno"
#: web/templates/public/surroundings.gohtml:13 #: web/templates/public/surroundings.gohtml:13
msgctxt "title" msgctxt "title"
msgid "What to Do Outside the Campsite?" msgid "What to Do Outside the Campsite?"
@ -125,8 +132,8 @@ msgstr "Kayak"
msgid "There are several points where you can go by kayak, from sections of the Ter river as well as on the coast…." msgid "There are several points where you can go by kayak, from sections of the Ter river as well as on the coast…."
msgstr "Hay diversos puntos dónde podéis ir en kayak, desde tramos del río Ter como también en la costa…." msgstr "Hay diversos puntos dónde podéis ir en kayak, desde tramos del río Ter como también en la costa…."
#: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:23 #: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:24
#: web/templates/public/layout.gohtml:58 #: web/templates/public/layout.gohtml:59
msgid "Campsite Montagut" msgid "Campsite Montagut"
msgstr "Camping Montagut" msgstr "Camping Montagut"
@ -134,7 +141,7 @@ msgstr "Camping Montagut"
msgid "Skip to main content" msgid "Skip to main content"
msgstr "Saltar al contenido principal" msgstr "Saltar al contenido principal"
#: web/templates/public/layout.gohtml:32 #: web/templates/public/layout.gohtml:33
msgid "Singular Lodges" msgid "Singular Lodges"
msgstr "Alojamientos singulares" msgstr "Alojamientos singulares"
@ -173,6 +180,7 @@ msgstr "Etiqueta"
#: web/templates/admin/campsite/form.gohtml:71 #: web/templates/admin/campsite/form.gohtml:71
#: web/templates/admin/campsite/type/form.gohtml:77 #: web/templates/admin/campsite/type/form.gohtml:77
#: web/templates/admin/season/form.gohtml:65 #: web/templates/admin/season/form.gohtml:65
#: web/templates/admin/services/carousel/form.gohtml:58
#: web/templates/admin/home/carousel/form.gohtml:58 #: web/templates/admin/home/carousel/form.gohtml:58
msgctxt "action" msgctxt "action"
msgid "Update" msgid "Update"
@ -181,6 +189,7 @@ msgstr "Actualizar"
#: web/templates/admin/campsite/form.gohtml:73 #: web/templates/admin/campsite/form.gohtml:73
#: web/templates/admin/campsite/type/form.gohtml:79 #: web/templates/admin/campsite/type/form.gohtml:79
#: web/templates/admin/season/form.gohtml:67 #: web/templates/admin/season/form.gohtml:67
#: web/templates/admin/services/carousel/form.gohtml:60
#: web/templates/admin/home/carousel/form.gohtml:60 #: web/templates/admin/home/carousel/form.gohtml:60
msgctxt "action" msgctxt "action"
msgid "Add" msgid "Add"
@ -251,6 +260,7 @@ msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: web/templates/admin/campsite/type/form.gohtml:59 #: web/templates/admin/campsite/type/form.gohtml:59
#: web/templates/admin/services/carousel/form.gohtml:39
#: web/templates/admin/home/carousel/form.gohtml:39 #: web/templates/admin/home/carousel/form.gohtml:39
msgctxt "input" msgctxt "input"
msgid "Cover image" msgid "Cover image"
@ -281,6 +291,7 @@ msgid "Name"
msgstr "Nombre" msgstr "Nombre"
#: web/templates/admin/campsite/type/index.gohtml:19 #: web/templates/admin/campsite/type/index.gohtml:19
#: web/templates/admin/services/index.gohtml:20
#: web/templates/admin/home/index.gohtml:20 #: web/templates/admin/home/index.gohtml:20
msgctxt "campsite type" msgctxt "campsite type"
msgid "Translations" msgid "Translations"
@ -298,18 +309,21 @@ msgstr "Traducción de tipo de alojamiento a %s"
#: web/templates/admin/campsite/type/l10n.gohtml:22 #: web/templates/admin/campsite/type/l10n.gohtml:22
#: web/templates/admin/campsite/type/l10n.gohtml:34 #: web/templates/admin/campsite/type/l10n.gohtml:34
#: web/templates/admin/services/carousel/l10n.gohtml:22
#: web/templates/admin/home/carousel/l10n.gohtml:22 #: web/templates/admin/home/carousel/l10n.gohtml:22
msgid "Source:" msgid "Source:"
msgstr "Origen:" msgstr "Origen:"
#: web/templates/admin/campsite/type/l10n.gohtml:24 #: web/templates/admin/campsite/type/l10n.gohtml:24
#: web/templates/admin/campsite/type/l10n.gohtml:37 #: web/templates/admin/campsite/type/l10n.gohtml:37
#: web/templates/admin/services/carousel/l10n.gohtml:24
#: web/templates/admin/home/carousel/l10n.gohtml:24 #: web/templates/admin/home/carousel/l10n.gohtml:24
msgctxt "input" msgctxt "input"
msgid "Translation:" msgid "Translation:"
msgstr "Traducción" msgstr "Traducción"
#: web/templates/admin/campsite/type/l10n.gohtml:46 #: web/templates/admin/campsite/type/l10n.gohtml:46
#: web/templates/admin/services/carousel/l10n.gohtml:33
#: web/templates/admin/home/carousel/l10n.gohtml:33 #: web/templates/admin/home/carousel/l10n.gohtml:33
msgctxt "action" msgctxt "action"
msgid "Translate" msgid "Translate"
@ -386,6 +400,85 @@ msgctxt "action"
msgid "Login" msgid "Login"
msgstr "Entrar" msgstr "Entrar"
#: web/templates/admin/services/carousel/form.gohtml:8
#: web/templates/admin/services/carousel/form.gohtml:27
#: web/templates/admin/home/carousel/form.gohtml:8
#: web/templates/admin/home/carousel/form.gohtml:27
msgctxt "title"
msgid "Edit Carousel Slide"
msgstr "Edición de la diapositiva del carrusel"
#: web/templates/admin/services/carousel/form.gohtml:10
#: web/templates/admin/services/carousel/form.gohtml:29
#: web/templates/admin/home/carousel/form.gohtml:10
#: web/templates/admin/home/carousel/form.gohtml:29
msgctxt "title"
msgid "New Carousel Slide"
msgstr "Nueva diapositiva del carrusel"
#: web/templates/admin/services/carousel/form.gohtml:48
#: web/templates/admin/services/carousel/l10n.gohtml:21
#: web/templates/admin/home/carousel/form.gohtml:48
#: web/templates/admin/home/carousel/l10n.gohtml:21
msgctxt "input"
msgid "Caption"
msgstr "Leyenda"
#: web/templates/admin/services/carousel/l10n.gohtml:7
#: web/templates/admin/services/carousel/l10n.gohtml:15
#: web/templates/admin/home/carousel/l10n.gohtml:7
#: web/templates/admin/home/carousel/l10n.gohtml:15
msgctxt "title"
msgid "Translate Carousel Slide to %s"
msgstr "Traducción de la diapositiva de carrusel a %s"
#: web/templates/admin/services/index.gohtml:6
#: web/templates/admin/layout.gohtml:76 web/templates/admin/home/index.gohtml:6
msgctxt "title"
msgid "Home Page"
msgstr "Página de inicio"
#: web/templates/admin/services/index.gohtml:12
#: web/templates/admin/home/index.gohtml:12
msgctxt "title"
msgid "Carousel"
msgstr "Carrusel"
#: web/templates/admin/services/index.gohtml:13
#: web/templates/admin/home/index.gohtml:13
msgctxt "action"
msgid "Add slide"
msgstr "Añadir diapositiva"
#: web/templates/admin/services/index.gohtml:18
#: web/templates/admin/home/index.gohtml:18
msgctxt "header"
msgid "Image"
msgstr "Imagen"
#: web/templates/admin/services/index.gohtml:19
#: web/templates/admin/home/index.gohtml:19
msgctxt "header"
msgid "Caption"
msgstr "Leyenda"
#: web/templates/admin/services/index.gohtml:21
#: web/templates/admin/home/index.gohtml:21
msgctxt "campsite type"
msgid "Actions"
msgstr "Acciones"
#: web/templates/admin/services/index.gohtml:40
#: web/templates/admin/home/index.gohtml:40
msgctxt "action"
msgid "Delete"
msgstr "Borrar"
#: web/templates/admin/services/index.gohtml:48
#: web/templates/admin/home/index.gohtml:48
msgid "No slides added yet."
msgstr "No se ha añadido ninguna diapositiva todavía."
#: web/templates/admin/profile.gohtml:6 web/templates/admin/profile.gohtml:12 #: web/templates/admin/profile.gohtml:6 web/templates/admin/profile.gohtml:12
#: web/templates/admin/layout.gohtml:29 #: web/templates/admin/layout.gohtml:29
msgctxt "title" msgctxt "title"
@ -506,68 +599,11 @@ msgctxt "action"
msgid "Logout" msgid "Logout"
msgstr "Salir" msgstr "Salir"
#: web/templates/admin/layout.gohtml:76 web/templates/admin/home/index.gohtml:6 #: web/templates/admin/layout.gohtml:79
#, fuzzy
msgctxt "title" msgctxt "title"
msgid "Home Page" msgid "Services Page"
msgstr "Página de inicio" msgstr "Servicios"
#: web/templates/admin/home/carousel/form.gohtml:8
#: web/templates/admin/home/carousel/form.gohtml:27
msgctxt "title"
msgid "Edit Carousel Slide"
msgstr "Edición de la diapositiva del carrusel"
#: web/templates/admin/home/carousel/form.gohtml:10
#: web/templates/admin/home/carousel/form.gohtml:29
msgctxt "title"
msgid "New Carousel Slide"
msgstr "Nueva diapositiva del carrusel"
#: web/templates/admin/home/carousel/form.gohtml:48
#: web/templates/admin/home/carousel/l10n.gohtml:21
msgctxt "input"
msgid "Caption"
msgstr "Leyenda"
#: web/templates/admin/home/carousel/l10n.gohtml:7
#: web/templates/admin/home/carousel/l10n.gohtml:15
msgctxt "title"
msgid "Translate Carousel Slide to %s"
msgstr "Traducción de la diapositiva de carrusel a %s"
#: web/templates/admin/home/index.gohtml:12
msgctxt "title"
msgid "Carousel"
msgstr "Carrusel"
#: web/templates/admin/home/index.gohtml:13
msgctxt "action"
msgid "Add slide"
msgstr "Añadir diapositiva"
#: web/templates/admin/home/index.gohtml:18
msgctxt "header"
msgid "Image"
msgstr "Imagen"
#: web/templates/admin/home/index.gohtml:19
msgctxt "header"
msgid "Caption"
msgstr "Leyenda"
#: web/templates/admin/home/index.gohtml:21
msgctxt "campsite type"
msgid "Actions"
msgstr "Acciones"
#: web/templates/admin/home/index.gohtml:40
msgctxt "action"
msgid "Delete"
msgstr "Borrar"
#: web/templates/admin/home/index.gohtml:48
msgid "No slides added yet."
msgstr "No se ha añadido ninguna diapositiva todavía."
#: web/templates/admin/media/index.gohtml:6 #: web/templates/admin/media/index.gohtml:6
#: web/templates/admin/media/index.gohtml:12 #: web/templates/admin/media/index.gohtml:12
@ -638,11 +674,12 @@ msgstr "La confirmación no se corresponde con la contraseña."
msgid "Selected language is not valid." msgid "Selected language is not valid."
msgstr "El idioma escogido no es válido." msgstr "El idioma escogido no es válido."
#: pkg/app/user.go:253 pkg/campsite/types/admin.go:296 pkg/home/carousel.go:287 #: pkg/app/user.go:253 pkg/campsite/types/admin.go:296
#: pkg/services/carousel.go:287 pkg/home/carousel.go:287
msgid "File must be a valid PNG or JPEG image." msgid "File must be a valid PNG or JPEG image."
msgstr "El archivo tiene que ser una imagen PNG o JPEG válida." msgstr "El archivo tiene que ser una imagen PNG o JPEG válida."
#: pkg/app/admin.go:47 #: pkg/app/admin.go:50
msgid "Access forbidden" msgid "Access forbidden"
msgstr "Acceso prohibido" msgstr "Acceso prohibido"
@ -666,6 +703,10 @@ msgstr "No podéis dejar el color en blanco."
msgid "This color is not valid. It must be like #123abc." msgid "This color is not valid. It must be like #123abc."
msgstr "Este color no es válido. Tiene que ser parecido a #123abc." msgstr "Este color no es válido. Tiene que ser parecido a #123abc."
#: pkg/services/carousel.go:289 pkg/home/carousel.go:289
msgid "Slide image can not be empty."
msgstr "No podéis dejar la imagen de la diapositiva en blanco."
#: pkg/company/admin.go:186 #: pkg/company/admin.go:186
msgid "Selected country is not valid." msgid "Selected country is not valid."
msgstr "El país escogido no es válido." msgstr "El país escogido no es válido."
@ -730,14 +771,13 @@ msgstr "No podéis dejar el formato de número de factura en blanco."
msgid "Cross-site request forgery detected." msgid "Cross-site request forgery detected."
msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados." msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados."
#: pkg/home/carousel.go:289
msgid "Slide image can not be empty."
msgstr "No podéis dejar la imagen de la diapositiva en blanco."
#: pkg/media/admin.go:164 #: pkg/media/admin.go:164
msgid "Uploaded file can not be empty." msgid "Uploaded file can not be empty."
msgstr "No podéis dejar el archivo del medio en blanco." msgstr "No podéis dejar el archivo del medio en blanco."
#~ msgid "Surroundings"
#~ msgstr "Entorno"
#~ msgid "Legend" #~ msgid "Legend"
#~ msgstr "Leyenda" #~ msgstr "Leyenda"

7
revert/add_service.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:add_service from pg
begin;
drop function if exists camper.add_service(integer, text, text, text);
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:add_services_carousel_slide from pg
begin;
drop function if exists camper.add_services_carousel_slide(integer, text);
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:available_icons from pg
begin;
delete from camper.icon;
commit;

7
revert/edit_service.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:edit_service from pg
begin;
drop function if exists camper.edit_service(integer, text, text, text);
commit;

7
revert/icon.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:icon from pg
begin;
drop table if exists camper.icon;
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:remove_services_carousel_slide from pg
begin;
drop function if exists camper.remove_services_carousel_slide(integer);
commit;

7
revert/service.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:service from pg
begin;
drop table if exists camper.service;
commit;

7
revert/service_i18n.sql Normal file
View File

@ -0,0 +1,7 @@
-- Revert camper:service_i18n from pg
begin;
drop table if exists camper.service_i18n;
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:services_carousel from pg
begin;
drop table if exists camper.services_carousel;
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:services_carousel_i18n from pg
begin;
drop table if exists camper.services_carousel_i18n;
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:translate_service from pg
begin;
drop function if exists camper.translate_service(integer, text, text, text);
commit;

View File

@ -0,0 +1,7 @@
-- Revert camper:translate_services_carousel_slide from pg
begin;
drop function if exists camper.translate_services_carousel_slide(integer, text, text);
commit;

View File

@ -64,3 +64,15 @@ home_carousel_i18n [roles schema_camper home_carousel language] 2023-09-13T23:22
add_home_carousel_slide [roles schema_camper home_carousel] 2023-09-14T17:49:21Z jordi fita mas <jordi@tandem.blog> # Add function to create slides for the home carousel add_home_carousel_slide [roles schema_camper home_carousel] 2023-09-14T17:49:21Z jordi fita mas <jordi@tandem.blog> # Add function to create slides for the home carousel
translate_home_carousel_slide [roles schema_camper home_carousel_i18n] 2023-09-14T18:17:36Z jordi fita mas <jordi@tandem.blog> # Add function to translate a home carousel slider translate_home_carousel_slide [roles schema_camper home_carousel_i18n] 2023-09-14T18:17:36Z jordi fita mas <jordi@tandem.blog> # Add function to translate a home carousel slider
remove_home_carousel_slide [roles schema_camper home_carousel home_carousel_i18n] 2023-09-14T21:57:48Z jordi fita mas <jordi@tandem.blog> # Add function to remove sliders from the home carousel remove_home_carousel_slide [roles schema_camper home_carousel home_carousel_i18n] 2023-09-14T21:57:48Z jordi fita mas <jordi@tandem.blog> # Add function to remove sliders from the home carousel
services_carousel [roles schema_public company media user_profile] 2023-09-16T22:37:47Z jordi fita mas <jordi@tandem.blog> # Add relation for services image carousel
services_carousel_i18n [roles schema_camper services_carousel language] 2023-09-16T22:42:14Z jordi fita mas <jordi@tandem.blog> # Add relation for services carousel translations
add_services_carousel_slide [roles schema_camper services_carousel] 2023-09-16T22:45:49Z jordi fita mas <jordi@tandem.blog> # Add function to create slides for the services carousel
translate_services_carousel_slide [roles schema_camper services_carousel_i18n] 2023-09-16T22:46:43Z jordi fita mas <jordi@tandem.blog> # Add function to translate a services carousel slide
remove_services_carousel_slide [roles schema_camper services_carousel services_carousel_i18n] 2023-09-16T22:47:54Z jordi fita mas <jordi@tandem.blog> # Add function to remove slides from the services carousel
icon [roles schema_camper] 2023-09-16T23:11:48Z jordi fita mas <jordi@tandem.blog> # Add relation for icon
available_icons [schema_camper icon] 2023-09-16T23:15:03Z jordi fita mas <jordi@tandem.blog> # Add the list of available icons
service [roles schema_camper company icon user_profile] 2023-09-16T23:48:19Z jordi fita mas <jordi@tandem.blog> # Add relation of services definition
add_service [roles schema_camper service] 2023-09-17T00:00:00Z jordi fita mas <jordi@tandem.blog> # Add function to create services
edit_service [roles schema_camper service] 2023-09-17T00:01:16Z jordi fita mas <jordi@tandem.blog> # Add function to edit services
service_i18n [roles schema_camper service language] 2023-09-17T00:13:42Z jordi fita mas <jordi@tandem.blog> # Add relation for service translations
translate_service [roles schema_camper service_i18n] 2023-09-17T00:17:00Z jordi fita mas <jordi@tandem.blog> # Add function to translate a service

55
test/add_service.sql Normal file
View File

@ -0,0 +1,55 @@
-- Test add_service
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(12);
set search_path to camper, public;
select has_function('camper', 'add_service', array['integer', 'text', 'text', 'text']);
select function_lang_is('camper', 'add_service', array['integer', 'text', 'text', 'text'], 'sql');
select function_returns('camper', 'add_service', array['integer', 'text', 'text', 'text'], 'integer');
select isnt_definer('camper', 'add_service', array['integer', 'text', 'text', 'text']);
select volatility_is('camper', 'add_service', array['integer', 'text', 'text', 'text'], 'volatile');
select function_privs_are('camper', 'add_service', array ['integer', 'text', 'text', 'text'], 'guest', array[]::text[]);
select function_privs_are('camper', 'add_service', array ['integer', 'text', 'text', 'text'], 'employee', array[]::text[]);
select function_privs_are('camper', 'add_service', array ['integer', 'text', 'text', 'text'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'add_service', array ['integer', 'text', 'text', 'text'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate service cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
, (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca')
;
select lives_ok(
$$ select add_service(1, 'information', 'Info', '<!-- block --><h2>This is what, exactly?</h2><!-- /block --><p>Dunno</p>') $$,
'Should be able to add a service to the first company'
);
select lives_ok(
$$ select add_service(2, 'toilet', 'Is this Google?', '') $$,
'Should be able to add a service to the second company'
);
select bag_eq(
$$ select company_id, icon_name, name, description::text from service $$,
$$ values (1, 'information', 'Info', '<!-- block --><h2>This is what, exactly?</h2><!-- /block --><p>Dunno</p>')
, (2, 'toilet', 'Is this Google?', '')
$$,
'Should have added all two service'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,77 @@
-- Test add_services_carousel_slide
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(14);
set search_path to camper, public;
select has_function('camper', 'add_services_carousel_slide', array['integer', 'text']);
select function_lang_is('camper', 'add_services_carousel_slide', array['integer', 'text'], 'sql');
select function_returns('camper', 'add_services_carousel_slide', array['integer', 'text'], 'void');
select isnt_definer('camper', 'add_services_carousel_slide', array['integer', 'text']);
select volatility_is('camper', 'add_services_carousel_slide', array['integer', 'text'], 'volatile');
select function_privs_are('camper', 'add_services_carousel_slide', array['integer', 'text'], 'guest', array[]::text[]);
select function_privs_are('camper', 'add_services_carousel_slide', array['integer', 'text'], 'employee', array[]::text[]);
select function_privs_are('camper', 'add_services_carousel_slide', array['integer', 'text'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'add_services_carousel_slide', array['integer', 'text'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate services_carousel_i18n cascade;
truncate services_carousel cascade;
truncate media cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
;
insert into media (media_id, company_id, original_filename, media_type, content)
values (5, 1, 'text.txt', 'text/plain', 'hello, world!')
, (6, 1, 'image.svg', 'image/svg+xml', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
, (7, 1, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
;
insert into services_carousel (media_id, caption)
values (5, 'Previous caption')
;
select lives_ok(
$$ select add_services_carousel_slide(6, 'A caption') $$,
'Should be able to add a carousel slide with a caption'
);
select lives_ok(
$$ select add_services_carousel_slide(7, null) $$,
'Should be able to add a carousel slide without caption'
);
select lives_ok(
$$ select add_services_carousel_slide(5, 'New caption') $$,
'Should be able to overwrite a slide with a new caption'
);
select bag_eq(
$$ select media_id, caption from services_carousel $$,
$$ values (5, 'New caption')
, (6, 'A caption')
, (7, '')
$$,
'Should have all three slides'
);
select is_empty(
$$ select * from services_carousel_i18n $$,
'Should not have added any translation'
);
select *
from finish();
rollback;

58
test/edit_service.sql Normal file
View File

@ -0,0 +1,58 @@
-- Test edit_service
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(12);
set search_path to camper, public;
select has_function('camper', 'edit_service', array['integer', 'text', 'text', 'text']);
select function_lang_is('camper', 'edit_service', array['integer', 'text', 'text', 'text'], 'sql');
select function_returns('camper', 'edit_service', array['integer', 'text', 'text', 'text'], 'void');
select isnt_definer('camper', 'edit_service', array['integer', 'text', 'text', 'text']);
select volatility_is('camper', 'edit_service', array['integer', 'text', 'text', 'text'], 'volatile');
select function_privs_are('camper', 'edit_service', array ['integer', 'text', 'text', 'text'], 'guest', array[]::text[]);
select function_privs_are('camper', 'edit_service', array ['integer', 'text', 'text', 'text'], 'employee', array[]::text[]);
select function_privs_are('camper', 'edit_service', array ['integer', 'text', 'text', 'text'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'edit_service', array ['integer', 'text', 'text', 'text'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate service cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
;
insert into service (service_id, company_id, icon_name, name, description)
values (5, 1, 'information', 'Service A', '<p>A</p>')
, (6, 1, 'toilet', 'Service B', '<p>B</p>')
;
select lives_ok(
$$ select edit_service(5, 'wifi', 'Service 1', '<p>1</p>') $$,
'Should be able to edit the first service'
);
select lives_ok(
$$ select edit_service(6, 'baby', 'Service 2', '<p>2</p>') $$,
'Should be able to edit the second service'
);
select bag_eq(
$$ select service_id, icon_name, name, description::text from service $$,
$$ values (5, 'wifi', 'Service 1', '<p>1</p>')
, (6, 'baby', 'Service 2', '<p>2</p>')
$$,
'Should have updated all services.'
);
select *
from finish();
rollback;

30
test/icon.sql Normal file
View File

@ -0,0 +1,30 @@
-- Test icon
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(11);
set search_path to camper, public;
select has_table('icon');
select has_pk('icon');
select table_privs_are('icon', 'guest', array['SELECT']);
select table_privs_are('icon', 'employee', array['SELECT']);
select table_privs_are('icon', 'admin', array['SELECT']);
select table_privs_are('icon', 'authenticator', array[]::text[]);
select has_column('icon', 'icon_name');
select col_is_pk('icon', 'icon_name');
select col_type_is('icon', 'icon_name', 'text');
select col_not_null('icon', 'icon_name');
select col_hasnt_default('icon', 'icon_name');
select *
from finish();
rollback;

View File

@ -0,0 +1,82 @@
-- Test remove_services_carousel_slide
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(12);
set search_path to camper, public;
select has_function('camper', 'remove_services_carousel_slide', array['integer']);
select function_lang_is('camper', 'remove_services_carousel_slide', array['integer'], 'sql');
select function_returns('camper', 'remove_services_carousel_slide', array['integer'], 'void');
select isnt_definer('camper', 'remove_services_carousel_slide', array['integer']);
select volatility_is('camper', 'remove_services_carousel_slide', array['integer'], 'volatile');
select function_privs_are('camper', 'remove_services_carousel_slide', array['integer'], 'guest', array[]::text[]);
select function_privs_are('camper', 'remove_services_carousel_slide', array['integer'], 'employee', array[]::text[]);
select function_privs_are('camper', 'remove_services_carousel_slide', array['integer'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'remove_services_carousel_slide', array['integer'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate services_carousel_i18n cascade;
truncate services_carousel cascade;
truncate media cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
;
insert into media (media_id, company_id, original_filename, media_type, content)
values (5, 1, 'text.txt', 'text/plain', 'hello, world!')
, (6, 1, 'image.svg', 'image/svg+xml', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
, (7, 1, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
;
insert into services_carousel (media_id, caption)
values (5, 'Source caption')
, (6, 'Another caption')
, (7, 'N/A')
;
insert into services_carousel_i18n (media_id, lang_tag, caption)
values (5, 'en', 'Target caption')
, (5, 'es', 'Target caption (spanish)')
, (6, 'en', 'Target caption')
, (6, 'es', 'Target caption (spanish)')
, (7, 'en', 'Target caption')
, (7, 'es', 'Target caption (spanish)')
;
select lives_ok(
$$ select remove_services_carousel_slide(6) $$,
'Should be able to delete a slide'
);
select bag_eq(
$$ select media_id, caption from services_carousel $$,
$$ values (5, 'Source caption')
, (7, 'N/A')
$$,
'Should have removed the slide'
);
select bag_eq(
$$ select media_id, lang_tag, caption from services_carousel_i18n $$,
$$ values (5, 'en', 'Target caption')
, (5, 'es', 'Target caption (spanish)')
, (7, 'en', 'Target caption')
, (7, 'es', 'Target caption (spanish)')
$$,
'Should have removed the slides translations'
);
select *
from finish();
rollback;

199
test/service.sql Normal file
View File

@ -0,0 +1,199 @@
-- Test service
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(51);
set search_path to camper, public;
select has_table('service');
select has_pk('service');
select table_privs_are('service', 'guest', array['SELECT']);
select table_privs_are('service', 'employee', array['SELECT']);
select table_privs_are('service', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('service', 'authenticator', array[]::text[]);
select has_sequence('service_service_id_seq');
select sequence_privs_are('service_service_id_seq', 'guest', array[]::text[]);
select sequence_privs_are('service_service_id_seq', 'employee', array[]::text[]);
select sequence_privs_are('service_service_id_seq', 'admin', array['USAGE']);
select sequence_privs_are('service_service_id_seq', 'authenticator', array[]::text[]);
select has_column('service', 'service_id');
select col_is_pk('service', 'service_id');
select col_type_is('service', 'service_id', 'integer');
select col_not_null('service', 'service_id');
select col_has_default('service', 'service_id');
select col_default_is('service', 'service_id', 'nextval(''service_service_id_seq''::regclass)');
select has_column('service', 'company_id');
select col_is_fk('service', 'company_id');
select fk_ok('service', 'company_id', 'company', 'company_id');
select col_type_is('service', 'company_id', 'integer');
select col_not_null('service', 'company_id');
select col_hasnt_default('service', 'company_id');
select has_column('service', 'icon_name');
select col_is_fk('service', 'icon_name');
select fk_ok('service', 'icon_name', 'icon', 'icon_name');
select col_type_is('service', 'icon_name', 'text');
select col_not_null('service', 'icon_name');
select col_hasnt_default('service', 'icon_name');
select has_column('service', 'name');
select col_type_is('service', 'name', 'text');
select col_not_null('service', 'name');
select col_hasnt_default('service', 'name');
select has_column('service', 'description');
select col_type_is('service', 'description', 'xml');
select col_not_null('service', 'description');
select col_hasnt_default('service', 'description');
set client_min_messages to warning;
truncate service cascade;
truncate company_host cascade;
truncate company_user cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca')
;
insert into company_user (company_id, user_id, role)
values (2, 1, 'admin')
, (4, 5, 'admin')
;
insert into company_host (company_id, host)
values (2, 'co2')
, (4, 'co4')
;
insert into service (company_id, icon_name, name, description)
values (2, 'information', 'Information', '')
, (4, 'wifi', 'WiFi', '')
;
prepare service_data as
select company_id, name
from service
order by company_id, name;
set role guest;
select bag_eq(
'service_data',
$$ values (2, 'Information')
, (4, 'WiFi')
$$,
'Everyone should be able to list all services across all companies'
);
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
select lives_ok(
$$ insert into service(company_id, icon_name, name, description) values (2, 'restaurant', 'Restaurant', '') $$,
'Admin from company 2 should be able to insert a new services to that company.'
);
select bag_eq(
'service_data',
$$ values (2, 'Information')
, (2, 'Restaurant')
, (4, 'WiFi')
$$,
'The new row should have been added'
);
select lives_ok(
$$ update service set name = 'Bar' where company_id = 2 and name = 'Restaurant' $$,
'Admin from company 2 should be able to update services of that company.'
);
select bag_eq(
'service_data',
$$ values (2, 'Information')
, (2, 'Bar')
, (4, 'WiFi')
$$,
'The row should have been updated.'
);
select lives_ok(
$$ delete from service where company_id = 2 and name = 'Bar' $$,
'Admin from company 2 should be able to delete services from that company.'
);
select bag_eq(
'service_data',
$$ values (2, 'Information')
, (4, 'WiFi')
$$,
'The row should have been deleted.'
);
select throws_ok(
$$ insert into service (company_id, icon_name, name, description) values (4, 'store', 'Store', '') $$,
'42501', 'new row violates row-level security policy for table "service"',
'Admin from company 2 should NOT be able to insert new services to company 4.'
);
select lives_ok(
$$ update service set name = 'Nope' where company_id = 4 $$,
'Admin from company 2 should not be able to update new services of company 4, but no error if company_id is not changed.'
);
select bag_eq(
'service_data',
$$ values (2, 'Information')
, (4, 'WiFi')
$$,
'No row should have been changed.'
);
select throws_ok(
$$ update service set company_id = 4 where company_id = 2 $$,
'42501', 'new row violates row-level security policy for table "service"',
'Admin from company 2 should NOT be able to move services to company 4'
);
select lives_ok(
$$ delete from service where company_id = 4 $$,
'Admin from company 2 should NOT be able to delete services from company 4, but not error is thrown'
);
select bag_eq(
'service_data',
$$ values (2, 'Information')
, (4, 'WiFi')
$$,
'No row should have been changed'
);
select throws_ok(
$$ insert into service (company_id, icon_name, name, description) values (2, 'toilet', ' ', '') $$,
'23514', 'new row for relation "service" violates check constraint "name_not_empty"',
'Should not be able to insert services with a blank name.'
);
reset role;
select *
from finish();
rollback;

49
test/service_i18n.sql Normal file
View File

@ -0,0 +1,49 @@
-- Test service_i18n
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(27);
set search_path to camper, public;
select has_table('service_i18n');
select has_pk('service_i18n');
select col_is_pk('service_i18n', array['service_id', 'lang_tag']);
select table_privs_are('service_i18n', 'guest', array['SELECT']);
select table_privs_are('service_i18n', 'employee', array['SELECT']);
select table_privs_are('service_i18n', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('service_i18n', 'authenticator', array[]::text[]);
select has_column('service_i18n', 'service_id');
select col_is_fk('service_i18n', 'service_id');
select fk_ok('service_i18n', 'service_id', 'service', 'service_id');
select col_type_is('service_i18n', 'service_id', 'integer');
select col_not_null('service_i18n', 'service_id');
select col_hasnt_default('service_i18n', 'service_id');
select has_column('service_i18n', 'lang_tag');
select col_is_fk('service_i18n', 'lang_tag');
select fk_ok('service_i18n', 'lang_tag', 'language', 'lang_tag');
select col_type_is('service_i18n', 'lang_tag', 'text');
select col_not_null('service_i18n', 'lang_tag');
select col_hasnt_default('service_i18n', 'lang_tag');
select has_column('service_i18n', 'name');
select col_type_is('service_i18n', 'name', 'text');
select col_not_null('service_i18n', 'name');
select col_hasnt_default('service_i18n', 'name');
select has_column('service_i18n', 'description');
select col_type_is('service_i18n', 'description', 'xml');
select col_not_null('service_i18n', 'description');
select col_hasnt_default('service_i18n', 'description');
select *
from finish();
rollback;

178
test/services_carousel.sql Normal file
View File

@ -0,0 +1,178 @@
-- Test services_carousel
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(30);
set search_path to camper, public;
select has_table('services_carousel');
select has_pk('services_carousel');
select table_privs_are('services_carousel', 'guest', array['SELECT']);
select table_privs_are('services_carousel', 'employee', array['SELECT']);
select table_privs_are('services_carousel', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('services_carousel', 'authenticator', array[]::text[]);
select has_column('services_carousel', 'media_id');
select col_is_pk('services_carousel', 'media_id');
select col_is_fk('services_carousel', 'media_id');
select fk_ok('services_carousel', 'media_id', 'media', 'media_id');
select col_type_is('services_carousel', 'media_id', 'integer');
select col_not_null('services_carousel', 'media_id');
select col_hasnt_default('services_carousel', 'media_id');
select has_column('services_carousel', 'caption');
select col_type_is('services_carousel', 'caption', 'text');
select col_not_null('services_carousel', 'caption');
select col_hasnt_default('services_carousel', 'caption');
set client_min_messages to warning;
truncate services_carousel cascade;
truncate media cascade;
truncate company_host cascade;
truncate company_user cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 'ca')
;
insert into company_user (company_id, user_id, role)
values (2, 1, 'admin')
, (4, 5, 'admin')
;
insert into company_host (company_id, host)
values (2, 'co2')
, (4, 'co4')
;
insert into media (media_id, company_id, original_filename, media_type, content)
values ( 7, 2, 'text2.txt', 'text/plain', 'content2')
, ( 8, 2, 'text3.txt', 'text/plain', 'content3')
, ( 9, 4, 'text4.txt', 'text/plain', 'content4')
, (10, 4, 'text5.txt', 'text/plain', 'content5')
;
insert into services_carousel (media_id, caption)
values (7, 'Caption 7')
, (9, 'Caption 9')
;
prepare carousel_data as
select media_id, caption
from services_carousel
order by media_id, caption;
set role guest;
select bag_eq(
'carousel_data',
$$ values (7, 'Caption 7')
, (9, 'Caption 9')
$$,
'Everyone should be able to list all carousel media across all companies'
);
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2');
select lives_ok(
$$ insert into services_carousel(media_id, caption)
values (8, 'Caption 8') $$,
'Admin from company 2 should be able to insert a new carousel media to that company.'
);
select bag_eq(
'carousel_data',
$$ values (7, 'Caption 7')
, (8, 'Caption 8')
, (9, 'Caption 9')
$$,
'The new row should have been added'
);
select lives_ok(
$$ update services_carousel set caption = 'Caption 8.8' where media_id = 8 $$,
'Admin from company 2 should be able to update carousel media of that company.'
);
select bag_eq(
'carousel_data',
$$ values (7, 'Caption 7')
, (8, 'Caption 8.8')
, (9, 'Caption 9')
$$,
'The row should have been updated.'
);
select lives_ok(
$$ delete from services_carousel where media_id = 8 $$,
'Admin from company 2 should be able to delete carousel media from that company.'
);
select bag_eq(
'carousel_data',
$$ values (7, 'Caption 7')
, (9, 'Caption 9')
$$,
'The row should have been deleted.'
);
select throws_ok(
$$ insert into services_carousel (media_id, caption)
values (10, 'Caption 10') $$,
'42501', 'new row violates row-level security policy for table "services_carousel"',
'Admin from company 2 should NOT be able to insert new media to company 4.'
);
select lives_ok(
$$ update services_carousel set caption = 'Nope' where media_id = 9 $$,
'Admin from company 2 should not be able to update new carousel media of company 4, but no error if media_id is not changed.'
);
select bag_eq(
'carousel_data',
$$ values (7, 'Caption 7')
, (9, 'Caption 9')
$$,
'No row should have been changed.'
);
select throws_ok(
$$ update services_carousel set media_id = 10 where media_id = 7 $$,
'42501', 'new row violates row-level security policy for table "services_carousel"',
'Admin from company 2 should NOT be able to move carousel media to company 4'
);
select lives_ok(
$$ delete from services_carousel where media_id = 9 $$,
'Admin from company 2 should NOT be able to delete carousel media from company 4, but no error is thrown'
);
select bag_eq(
'carousel_data',
$$ values (7, 'Caption 7')
, (9, 'Caption 9')
$$,
'No row should have been changed'
);
reset role;
select *
from finish();
rollback;

View File

@ -0,0 +1,44 @@
-- Test services_carousel_i18n
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(23);
set search_path to camper, public;
select has_table('services_carousel_i18n');
select has_pk('services_carousel_i18n');
select col_is_pk('services_carousel_i18n', array['media_id', 'lang_tag']);
select table_privs_are('services_carousel_i18n', 'guest', array['SELECT']);
select table_privs_are('services_carousel_i18n', 'employee', array['SELECT']);
select table_privs_are('services_carousel_i18n', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']);
select table_privs_are('services_carousel_i18n', 'authenticator', array[]::text[]);
select has_column('services_carousel_i18n', 'media_id');
select col_is_fk('services_carousel_i18n', 'media_id');
select fk_ok('services_carousel_i18n', 'media_id', 'services_carousel', 'media_id');
select col_type_is('services_carousel_i18n', 'media_id', 'integer');
select col_not_null('services_carousel_i18n', 'media_id');
select col_hasnt_default('services_carousel_i18n', 'media_id');
select has_column('services_carousel_i18n', 'lang_tag');
select col_is_fk('services_carousel_i18n', 'lang_tag');
select fk_ok('services_carousel_i18n', 'lang_tag', 'language', 'lang_tag');
select col_type_is('services_carousel_i18n', 'lang_tag', 'text');
select col_not_null('services_carousel_i18n', 'lang_tag');
select col_hasnt_default('services_carousel_i18n', 'lang_tag');
select has_column('services_carousel_i18n', 'caption');
select col_type_is('services_carousel_i18n', 'caption', 'text');
select col_not_null('services_carousel_i18n', 'caption');
select col_hasnt_default('services_carousel_i18n', 'caption');
select *
from finish();
rollback;

View File

@ -0,0 +1,69 @@
-- Test translate_service
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to camper, public;
select has_function('camper', 'translate_service', array['integer', 'text', 'text', 'text']);
select function_lang_is('camper', 'translate_service', array['integer', 'text', 'text', 'text'], 'sql');
select function_returns('camper', 'translate_service', array['integer', 'text', 'text', 'text'], 'void');
select isnt_definer('camper', 'translate_service', array['integer', 'text', 'text', 'text']);
select volatility_is('camper', 'translate_service', array['integer', 'text', 'text', 'text'], 'volatile');
select function_privs_are('camper', 'translate_service', array['integer', 'text', 'text', 'text'], 'guest', array[]::text[]);
select function_privs_are('camper', 'translate_service', array['integer', 'text', 'text', 'text'], 'employee', array[]::text[]);
select function_privs_are('camper', 'translate_service', array['integer', 'text', 'text', 'text'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'translate_service', array['integer', 'text', 'text', 'text'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate service cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
;
insert into service (service_id, company_id, icon_name, name, description)
values (5, 1, 'information', 'Service A', '<p>A</p>')
, (6, 1, 'toilet', 'Service B', '<p>B</p>')
;
insert into service_i18n (service_id, lang_tag, name, description)
values (6, 'ca', 'serveib', '<p>B</p>')
;
select lives_ok(
$$ select translate_service(5, 'ca', 'Servei A', '<p>a</p>') $$,
'Should be able to translate the first service'
);
select lives_ok(
$$ select translate_service(6, 'es', 'Servicio B', '<p>b</p>') $$,
'Should be able to translate the second service'
);
select lives_ok(
$$ select translate_service(6, 'ca', 'Servei B', null) $$,
'Should be able to overwrite the catalan translation of the second service, with no description'
);
select bag_eq(
$$ select service_id, lang_tag, name, description::text from service_i18n $$,
$$ values (5, 'ca', 'Servei A', '<p>a</p>')
, (6, 'ca', 'Servei B', '')
, (6, 'es', 'Servicio B', '<p>b</p>')
$$,
'Should have added and updated all translations.'
);
select *
from finish();
rollback;

View File

@ -0,0 +1,78 @@
-- Test translate_services_carousel_slide
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(13);
set search_path to camper, public;
select has_function('camper', 'translate_services_carousel_slide', array['integer', 'text', 'text']);
select function_lang_is('camper', 'translate_services_carousel_slide', array['integer', 'text', 'text'], 'sql');
select function_returns('camper', 'translate_services_carousel_slide', array['integer', 'text', 'text'], 'void');
select isnt_definer('camper', 'translate_services_carousel_slide', array['integer', 'text', 'text']);
select volatility_is('camper', 'translate_services_carousel_slide', array['integer', 'text', 'text'], 'volatile');
select function_privs_are('camper', 'translate_services_carousel_slide', array['integer', 'text', 'text'], 'guest', array[]::text[]);
select function_privs_are('camper', 'translate_services_carousel_slide', array['integer', 'text', 'text'], 'employee', array[]::text[]);
select function_privs_are('camper', 'translate_services_carousel_slide', array['integer', 'text', 'text'], 'admin', array['EXECUTE']);
select function_privs_are('camper', 'translate_services_carousel_slide', array['integer', 'text', 'text'], 'authenticator', array[]::text[]);
set client_min_messages to warning;
truncate services_carousel_i18n cascade;
truncate services_carousel cascade;
truncate media cascade;
truncate company cascade;
reset client_min_messages;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, default_lang_tag)
values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 'ca')
;
insert into media (media_id, company_id, original_filename, media_type, content)
values (5, 1, 'text.txt', 'text/plain', 'hello, world!')
, (6, 1, 'image.svg', 'image/svg+xml', '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>')
, (7, 1, 'cover4.xpm', 'image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};')
;
insert into services_carousel (media_id, caption)
values (5, 'Source caption')
, (6, 'Another caption')
, (7, 'N/A')
;
insert into services_carousel_i18n (media_id, lang_tag, caption)
values (5, 'en', 'Target caption')
;
select lives_ok(
$$ select translate_services_carousel_slide(5, 'ca', 'Traducció') $$,
'Should be able to translate a carousel slide'
);
select lives_ok(
$$ select translate_services_carousel_slide(6, 'es', null) $$,
'Should be able to “translate” a carousel slide to the empty string'
);
select lives_ok(
$$ select translate_services_carousel_slide(5, 'en', 'Not anymore') $$,
'Should be able to overwrite a slides translation'
);
select bag_eq(
$$ select media_id, lang_tag, caption from services_carousel_i18n $$,
$$ values (5, 'ca', 'Traducció')
, (5, 'en', 'Not anymore')
, (6, 'es', '')
$$,
'Should have all three slides'
);
select *
from finish();
rollback;

7
verify/add_service.sql Normal file
View File

@ -0,0 +1,7 @@
-- Verify camper:add_service on pg
begin;
select has_function_privilege('camper.add_service(integer, text, text, text)', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify camper:add_services_carousel_slide on pg
begin;
select has_function_privilege('camper.add_services_carousel_slide(integer, text)', 'execute');
rollback;

View File

@ -0,0 +1,28 @@
-- Verify camper:available_icons on pg
begin;
set search_path to camper;
select 1 / count(*) from icon where icon_name = 'baby';
select 1 / count(*) from icon where icon_name = 'ball';
select 1 / count(*) from icon where icon_name = 'bicycle';
select 1 / count(*) from icon where icon_name = 'campfire';
select 1 / count(*) from icon where icon_name = 'castle';
select 1 / count(*) from icon where icon_name = 'fridge';
select 1 / count(*) from icon where icon_name = 'information';
select 1 / count(*) from icon where icon_name = 'kayak';
select 1 / count(*) from icon where icon_name = 'outing';
select 1 / count(*) from icon where icon_name = 'pool';
select 1 / count(*) from icon where icon_name = 'puzzle';
select 1 / count(*) from icon where icon_name = 'restaurant';
select 1 / count(*) from icon where icon_name = 'route';
select 1 / count(*) from icon where icon_name = 'rv';
select 1 / count(*) from icon where icon_name = 'shower';
select 1 / count(*) from icon where icon_name = 'store';
select 1 / count(*) from icon where icon_name = 'toilet';
select 1 / count(*) from icon where icon_name = 'washer';
select 1 / count(*) from icon where icon_name = 'wheelchair';
select 1 / count(*) from icon where icon_name = 'wifi';
rollback;

7
verify/edit_service.sql Normal file
View File

@ -0,0 +1,7 @@
-- Verify camper:edit_service on pg
begin;
select has_function_privilege('camper.edit_service(integer, text, text, text)', 'execute');
rollback;

9
verify/icon.sql Normal file
View File

@ -0,0 +1,9 @@
-- Verify camper:icon on pg
begin;
select icon_name
from camper.icon
where false;
rollback;

View File

@ -0,0 +1,7 @@
-- Verify camper:remove_services_carousel_slide on pg
begin;
select has_function_privilege('camper.remove_services_carousel_slide(integer)', 'execute');
rollback;

19
verify/service.sql Normal file
View File

@ -0,0 +1,19 @@
-- Verify camper:service on pg
begin;
select service_id
, company_id
, icon_name
, name
, description
from camper.service
where false;
select 1 / count(*) from pg_class where oid = 'camper.service'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.service'::regclass;
select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.service'::regclass;
select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.service'::regclass;
select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.service'::regclass;
rollback;

12
verify/service_i18n.sql Normal file
View File

@ -0,0 +1,12 @@
-- Verify camper:service_i18n on pg
begin;
select service_id
, lang_tag
, name
, description
from camper.service_i18n
where false;
rollback;

View File

@ -0,0 +1,16 @@
-- Verify camper:services_carousel on pg
begin;
select media_id
, caption
from camper.services_carousel
where false;
select 1 / count(*) from pg_class where oid = 'camper.services_carousel'::regclass and relrowsecurity;
select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.services_carousel'::regclass;
select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.services_carousel'::regclass;
select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.services_carousel'::regclass;
select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.services_carousel'::regclass;
rollback;

View File

@ -0,0 +1,11 @@
-- Verify camper:services_carousel_i18n on pg
begin;
select media_id
, lang_tag
, caption
from camper.services_carousel_i18n
where false;
rollback;

View File

@ -0,0 +1,7 @@
-- Verify camper:translate_service on pg
begin;
select has_function_privilege('camper.translate_service(integer, text, text, text)', 'execute');
rollback;

View File

@ -0,0 +1,7 @@
-- Verify camper:translate_services_carousel_slide on pg
begin;
select has_function_privilege('camper.translate_services_carousel_slide(integer, text, text)', 'execute');
rollback;

View File

@ -135,6 +135,10 @@ p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
p + p {
margin-top: 1.5em;
}
h2 { h2 {
font-size: 4.2rem; font-size: 4.2rem;
font-weight: 400; font-weight: 400;
@ -432,28 +436,33 @@ dl, .nature div + div, .outside_activities > div {
background-color: var(--accent); background-color: var(--accent);
} }
.surroundings .spiel { .carousel {
display: none;
}
.carousel .spiel {
font-size: 2.4rem; font-size: 2.4rem;
padding-right: 4rem; padding-right: 4rem;
} }
.surroundings .spiel p { .carousel .spiel p {
margin-top: 0;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.surroundings figure, .surroundings .slick-track > img { .carousel figure, .carousel .slick-track > img {
margin-right: 5rem; margin-right: 5rem;
position: relative; position: relative;
} }
.surroundings img { .carousel img {
height: 40rem; height: 40rem;
width: 100%; width: 100%;
border-radius: 5px; border-radius: 5px;
object-fit: cover; object-fit: cover;
} }
.surroundings figcaption { .carousel figcaption {
padding: 10px 15px; padding: 10px 15px;
background: var(--clar); background: var(--clar);
width: fit-content; width: fit-content;
@ -465,23 +474,23 @@ dl, .nature div + div, .outside_activities > div {
font-size: 1.7rem; font-size: 1.7rem;
} }
.surroundings .slick-list { .carousel .slick-list {
order: 1; order: 1;
padding: 0 20% 0 0 !important; padding: 0 20% 0 0 !important;
} }
.surroundings .slick-track { .carousel .slick-track {
display: flex; display: flex;
align-items: start; align-items: start;
} }
.surroundings .slick-slider { .carousel.slick-slider {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: end; justify-content: end;
} }
.surroundings .slick-arrow { .carousel .slick-arrow {
font-size: 6rem; font-size: 6rem;
line-height: 1; line-height: 1;
width: 5rem; width: 5rem;
@ -492,25 +501,25 @@ dl, .nature div + div, .outside_activities > div {
transition: transform 0.5s ease; transition: transform 0.5s ease;
} }
.surroundings .slick-prev.slick-arrow, .surroundings .slick-next.slick-arrow { .carousel .slick-prev.slick-arrow, .carousel .slick-next.slick-arrow {
opacity: 1; opacity: 1;
} }
.surroundings .slick-prev { .carousel .slick-prev {
order: 2; order: 2;
margin: 2.5rem 4rem 0 0; margin: 2.5rem 4rem 0 0;
} }
.surroundings .slick-prev:hover { .carousel .slick-prev:hover {
transform: translateX(-1.3rem); transform: translateX(-1.3rem);
} }
.surroundings .slick-next { .carousel .slick-next {
order: 3; order: 3;
margin: 2.5rem 7rem 0 0; margin: 2.5rem 7rem 0 0;
} }
.surroundings .slick-next:hover { .carousel .slick-next:hover {
transform: translateX(1.3rem); transform: translateX(1.3rem);
} }
@ -526,7 +535,7 @@ dl {
} }
dl div { dl div {
flex: 1; flex-basis: calc(25% - 5rem + 5rem / 4);
} }
dt { dt {
@ -544,32 +553,103 @@ dt {
dl { dl {
flex-direction: column; flex-direction: column;
} }
del div {
flex-basis: 100%;
}
}
.icon_baby {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M92,136a8,8,0,1,1,8-8A8,8,0,0,1,92,136Zm72-16a8,8,0,1,0,8,8A8,8,0,0,0,164,120Zm-10.13,44.62a49,49,0,0,1-51.74,0,4,4,0,0,0-4.26,6.76,57,57,0,0,0,60.26,0,4,4,0,1,0-4.26-6.76ZM228,128A100,100,0,1,1,128,28,100.11,100.11,0,0,1,228,128Zm-8,0a92.11,92.11,0,0,0-90.06-92C116.26,54.07,116,71.83,116,72a12,12,0,0,0,24,0,4,4,0,0,1,8,0,20,20,0,0,1-40,0c0-.78.16-17.31,12-35.64A92,92,0,1,0,220,128Z"/%3E%3C/svg%3E');
}
.icon_ball {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M128,28A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm85,135.19a92,92,0,0,1-102.18,2.57L130.31,132h89.6A91.61,91.61,0,0,1,213,163.19ZM85.52,46.42A91.11,91.11,0,0,1,116,36.79,92,92,0,0,1,169.29,124h-39ZM219.91,124H177.29a100.06,100.06,0,0,0-46-87.93A92.11,92.11,0,0,1,219.91,124ZM78.59,50.42l21.3,36.89a100.09,100.09,0,0,0-53.16,83.77A91.92,91.92,0,0,1,78.59,50.42ZM55,183.94a92,92,0,0,1,48.87-89.7L123.38,128,78.59,205.58A92.75,92.75,0,0,1,55,183.94ZM128,220a91.37,91.37,0,0,1-42.48-10.42l21.3-36.89a100.07,100.07,0,0,0,99.1,4.16A92,92,0,0,1,128,220Z"/%3E%3C/svg%3E');
} }
.icon_bicycle { .icon_bicycle {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M208,116a43.66,43.66,0,0,0-18.62,4.15L159,68h33a12,12,0,0,1,12,12,4,4,0,0,0,8,0,20,20,0,0,0-20-20H152a4,4,0,0,0-3.46,6L163.7,92H97L79.46,62A4,4,0,0,0,76,60H48a4,4,0,0,0,0,8H73.7L89.89,95.76,70.57,122.25A44.21,44.21,0,1,0,77,127L94.29,103.3,128.54,162a4,4,0,0,0,3.46,2,4.11,4.11,0,0,0,2-.54,4,4,0,0,0,1.44-5.48l-33.83-58h66.74l14.11,24.19A44,44,0,1,0,208,116ZM84,160a36,36,0,1,1-18.16-31.25L44.77,157.64a4,4,0,0,0,6.46,4.72l21.07-28.9A35.92,35.92,0,0,1,84,160Zm124,36a36,36,0,0,1-21.47-64.88l18,30.9a4,4,0,0,0,3.46,2,4.11,4.11,0,0,0,2-.54,4,4,0,0,0,1.44-5.48l-18-30.89A36,36,0,1,1,208,196Z"/%3E%3C/svg%3E'); background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M208,116a43.66,43.66,0,0,0-18.62,4.15L159,68h33a12,12,0,0,1,12,12,4,4,0,0,0,8,0,20,20,0,0,0-20-20H152a4,4,0,0,0-3.46,6L163.7,92H97L79.46,62A4,4,0,0,0,76,60H48a4,4,0,0,0,0,8H73.7L89.89,95.76,70.57,122.25A44.21,44.21,0,1,0,77,127L94.29,103.3,128.54,162a4,4,0,0,0,3.46,2,4.11,4.11,0,0,0,2-.54,4,4,0,0,0,1.44-5.48l-33.83-58h66.74l14.11,24.19A44,44,0,1,0,208,116ZM84,160a36,36,0,1,1-18.16-31.25L44.77,157.64a4,4,0,0,0,6.46,4.72l21.07-28.9A35.92,35.92,0,0,1,84,160Zm124,36a36,36,0,0,1-21.47-64.88l18,30.9a4,4,0,0,0,3.46,2,4.11,4.11,0,0,0,2-.54,4,4,0,0,0,1.44-5.48l-18-30.89A36,36,0,1,1,208,196Z"/%3E%3C/svg%3E');
} }
.icon_route { .icon_campfire {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M226.46,52.85a4,4,0,0,0-3.43-.73L160.47,67.76,97.79,36.42a4,4,0,0,0-2.76-.3l-64,16A4,4,0,0,0,28,56V200a4,4,0,0,0,5,3.88l62.56-15.64,62.68,31.34a4,4,0,0,0,2.76.3l64-16a4,4,0,0,0,3-3.88V56A4,4,0,0,0,226.46,52.85ZM100,46.47l56,28V209.53l-56-28ZM36,59.12l56-14V180.88l-56,14ZM220,196.88l-56,14V75.12l56-14Z"/%3E%3C/svg%3E'); background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M219.81,225.21A4,4,0,0,1,216,228a3.92,3.92,0,0,1-1.21-.19L128,200.2,41.21,227.81A3.92,3.92,0,0,1,40,228a4,4,0,0,1-1.21-7.81l76-24.19-76-24.19a4,4,0,1,1,2.42-7.62L128,191.8l86.79-27.61a4,4,0,1,1,2.42,7.62l-76,24.19,76,24.19A4,4,0,0,1,219.81,225.21ZM72,108c0-19,9.38-38.85,27.12-57.27A152,152,0,0,1,125.9,28.59a4,4,0,0,1,4.2,0,152,152,0,0,1,26.78,22.14C174.62,69.15,184,89,184,108a56,56,0,0,1-54.56,56c-.48,0-1,0-1.44,0s-1,0-1.44,0A56,56,0,0,1,72,108Zm56,48a20,20,0,0,0,20-20c0-17.39-14.37-30.53-20-35-5.63,4.48-20,17.62-20,35A20,20,0,0,0,128,156ZM80,108a48,48,0,0,0,23.28,41.13A27.83,27.83,0,0,1,100,136c0-25.84,24.73-42.63,25.78-43.33a4,4,0,0,1,4.44,0c1.05.7,25.78,17.49,25.78,43.33a27.83,27.83,0,0,1-3.28,13.13A48,48,0,0,0,176,108c0-36.37-38.49-64.76-48-71.21C118.5,43.25,80,71.68,80,108Z"/%3E%3C/svg%3E');
} }
.icon_outing { .icon_castle {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M164,44.17V32a20,20,0,0,0-20-20H112A20,20,0,0,0,92,32V44.17A52.05,52.05,0,0,0,44,96V216a12,12,0,0,0,12,12H200a12,12,0,0,0,12-12V96A52.05,52.05,0,0,0,164,44.17ZM112,20h32a12,12,0,0,1,12,12V44H100V32A12,12,0,0,1,112,20Zm60,144H84V152a12,12,0,0,1,12-12h64a12,12,0,0,1,12,12Zm-88,8h56v12a4,4,0,0,0,8,0V172h24v48H84Zm120,44a4,4,0,0,1-4,4H180V152a20,20,0,0,0-20-20H96a20,20,0,0,0-20,20v68H56a4,4,0,0,1-4-4V96A44.05,44.05,0,0,1,96,52h64a44.05,44.05,0,0,1,44,44ZM148,88a4,4,0,0,1-4,4H112a4,4,0,0,1,0-8h32A4,4,0,0,1,148,88Z"/%3E%3C/svg%3E'); background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M200,28H184a12,12,0,0,0-12,12V56a4,4,0,0,1-4,4H152a4,4,0,0,1-4-4V40a12,12,0,0,0-12-12H120a12,12,0,0,0-12,12V56a4,4,0,0,1-4,4H88a4,4,0,0,1-4-4V40A12,12,0,0,0,72,28H56A12,12,0,0,0,44,40V84.69a11.93,11.93,0,0,0,3.51,8.48l11.32,11.32A4,4,0,0,1,60,107.31V216a12,12,0,0,0,12,12H184a12,12,0,0,0,12-12V107.31a4,4,0,0,1,1.17-2.82l11.32-11.32A11.93,11.93,0,0,0,212,84.69V40A12,12,0,0,0,200,28ZM148,220H108V152a20,20,0,0,1,40,0ZM204,84.69a4,4,0,0,1-1.17,2.82L191.51,98.83a11.93,11.93,0,0,0-3.51,8.48V216a4,4,0,0,1-4,4H156V152a28,28,0,0,0-56,0v68H72a4,4,0,0,1-4-4V107.31a11.93,11.93,0,0,0-3.51-8.48L53.17,87.51A4,4,0,0,1,52,84.69V40a4,4,0,0,1,4-4H72a4,4,0,0,1,4,4V56A12,12,0,0,0,88,68h16a12,12,0,0,0,12-12V40a4,4,0,0,1,4-4h16a4,4,0,0,1,4,4V56a12,12,0,0,0,12,12h16a12,12,0,0,0,12-12V40a4,4,0,0,1,4-4h16a4,4,0,0,1,4,4Z"/%3E%3C/svg%3E');
}
.icon_fridge {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.57617 28.04395"%3E%3Cpath stroke-width="0" d="m15.62988,0H1.94629C.87305,0,0,.87305,0,1.94629v25.59766c0,.27637.22363.5.5.5h16.57617c.27637,0,.5-.22363.5-.5V1.94629c0-1.07324-.87305-1.94629-1.94629-1.94629ZM1.94629,1h13.68359c.52148,0,.94629.42432.94629.94629v7.41357H1V1.94629c0-.52197.42432-.94629.94629-.94629Zm-.94629,26.04395V10.35986h15.57617v16.68408H1Z"/%3E%3Cpath stroke-width="0" d="m3.64453,5.36426h2.04248c.27637,0,.5-.22363.5-.5s-.22363-.5-.5-.5h-2.04248c-.27637,0-.5.22363-.5.5s.22363.5.5.5Z"/%3E%3Cpath stroke-width="0" d="m5.68701,13.271h-2.04248c-.27637,0-.5.22363-.5.5s.22363.5.5.5h2.04248c.27637,0,.5-.22363.5-.5s-.22363-.5-.5-.5Z"/%3E%3C/svg%3E');
}
.icon_information {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M140,176a4,4,0,0,1-4,4,12,12,0,0,1-12-12V128a4,4,0,0,0-4-4,4,4,0,0,1,0-8,12,12,0,0,1,12,12v40a4,4,0,0,0,4,4A4,4,0,0,1,140,176ZM124,92a8,8,0,1,0-8-8A8,8,0,0,0,124,92Zm104,36A100,100,0,1,1,128,28,100.11,100.11,0,0,1,228,128Zm-8,0a92,92,0,1,0-92,92A92.1,92.1,0,0,0,220,128Z"/%3E%3C/svg%3E');
} }
.icon_kayak { .icon_kayak {
background-image: url('data:image/svg+xml,%3Csvg viewBox="0 0 29.52905 28.08545" xmlns="http://www.w3.org/2000/svg" id="uuid-87ec619a-4896-40b8-8478-aa076dee3f7c"%3E%3Cpath stroke-width="0" d="m29.46728,11.26074l-.57617-2.15186c-.08398-.31299-.24609-.59229-.45801-.78613-.26758-.24658-.59863-.3418-.90625-.25879l-3.4834.93213c-.26953.07227-.48535.26807-.6084.54932l-.5752,1.30713c-.0719.16376-.10333.34686-.10742.53485l-3.05627.81787c-.23956-3.37067-1.03204-8.79126-2.35193-10.66425l-.22363-.31836C16.58057.45752,15.69971.00049,14.76318,0h-.00049c-.94873,0-1.80566.44434-2.35156,1.21875-1.71973,2.44043-2.65771,9.75146-2.65771,12.82422,0,.24457.00812.52399.0199.81769l-3.25604.87134c-.09741-.16071-.21613-.30347-.36005-.40924l-1.15088-.84424c-.24854-.18213-.53125-.24463-.80127-.17236l-3.48389.93164c-.56689.15234-.85693.84961-.65918,1.58691l.57617,2.15137c.0835.31348.24561.59277.45654.78613.20508.18848.4458.28809.68506.28809.07471,0,.14893-.00879.22168-.02832l3.48486-.93359c.26807-.07227.48389-.26758.60693-.54883l.57568-1.30664c.07172-.16357.10309-.3465.10718-.5343l3.05591-.81793c.25531,3.52673,1.14368,8.94983,2.5791,10.98602.5459.77441,1.40332,1.21875,2.35205,1.21875.93652,0,1.81738-.45703,2.35645-1.22266l.22461-.31738c1.60352-2.27637,2.43066-9.79492,2.43066-12.50244,0-.23499-.00854-.51709-.021-.81805l3.25732-.87183c.09747.16071.21625.30347.36035.40912l1.15039.84473c.18066.13281.38086.20166.5791.20166.07422,0,.14941-.00977.22168-.0293l3.48535-.93262c.56641-.15234.85645-.84863.65918-1.58594Zm-24.24072,6.86182l-3.41602.93262c-.03516-.01465-.14844-.12109-.20654-.33691l-.57617-2.15137c-.05713-.21484-.01318-.3623-.04688-.3623h-.00098l3.4165-.93164s.00586.00293.01709.01074c.00049,0,.00098.00098.00098.00098l1.00842.74017-1.90881.5108c-.2666.07227-.4248.3457-.35352.6123.05957.22363.26172.37109.48242.37109.04297,0,.08643-.00586.12988-.0166l1.91559-.5127-.46198,1.13281Zm5.52686-4.07959c0-3.07959.93994-10.06934,2.4751-12.24805.35596-.50488.91504-.79492,1.53418-.79492.62109.00049,1.18213.2915,1.54053.79932l.22266.31689c1.17902,1.67261,1.96533,7.08936,2.18457,10.35278l-1.50305.40228c-.19397-2.55505-1.02423-4.83514-2.44324-4.83514-1.63379,0-2.48828,3.02197-2.48828,6.00684,0,.04913.00214.09814.00262.14728l-1.51538.40552c-.00574-.19403-.0097-.38147-.0097-.5528Zm2.52692-.12079c.02441-2.93951.89984-4.88605,1.48383-4.88605.53003,0,1.29828,1.6062,1.45496,4.09961l-2.93878.78644Zm2.96765.24115c-.02441,2.93933-.89978,4.88599-1.48383,4.88599-.53003,0-1.29834-1.60632-1.45496-4.09937l2.93878-.78662Zm2.52692-.12036c0,2.66846-.84961,9.94092-2.24902,11.92725l-.22363.31641c-.71387,1.01367-2.36084,1.01562-3.07373.00391-1.31152-1.86127-2.18622-7.22681-2.414-10.67285l1.50641-.4032c.19397,2.55505,1.02417,4.83484,2.44324,4.83484,1.63379,0,2.48828-3.02148,2.48828-6.00635,0-.04926-.00214-.09845-.00262-.14777l1.51514-.40558c.00659.20087.00995.38617.00995.55334Zm6.35742-1.23047s-.00684-.00293-.01855-.01123l-1.00897-.74091,1.90936-.51105c.26758-.07129.42578-.34521.35449-.6123-.07227-.26562-.3418-.42725-.6123-.35352l-1.91577.5127.46069-1.13379,3.41699-.93164c.03516.01416.14844.12012.20605.33691l.57617,2.15186c.05957.21973.01953.36816.04883.36133l-3.41699.93164Z"/%3E%3C/svg%3E'); background-image: url('data:image/svg+xml,%3Csvg viewBox="0 0 29.52905 28.08545" xmlns="http://www.w3.org/2000/svg" id="uuid-87ec619a-4896-40b8-8478-aa076dee3f7c"%3E%3Cpath stroke-width="0" d="m29.46728,11.26074l-.57617-2.15186c-.08398-.31299-.24609-.59229-.45801-.78613-.26758-.24658-.59863-.3418-.90625-.25879l-3.4834.93213c-.26953.07227-.48535.26807-.6084.54932l-.5752,1.30713c-.0719.16376-.10333.34686-.10742.53485l-3.05627.81787c-.23956-3.37067-1.03204-8.79126-2.35193-10.66425l-.22363-.31836C16.58057.45752,15.69971.00049,14.76318,0h-.00049c-.94873,0-1.80566.44434-2.35156,1.21875-1.71973,2.44043-2.65771,9.75146-2.65771,12.82422,0,.24457.00812.52399.0199.81769l-3.25604.87134c-.09741-.16071-.21613-.30347-.36005-.40924l-1.15088-.84424c-.24854-.18213-.53125-.24463-.80127-.17236l-3.48389.93164c-.56689.15234-.85693.84961-.65918,1.58691l.57617,2.15137c.0835.31348.24561.59277.45654.78613.20508.18848.4458.28809.68506.28809.07471,0,.14893-.00879.22168-.02832l3.48486-.93359c.26807-.07227.48389-.26758.60693-.54883l.57568-1.30664c.07172-.16357.10309-.3465.10718-.5343l3.05591-.81793c.25531,3.52673,1.14368,8.94983,2.5791,10.98602.5459.77441,1.40332,1.21875,2.35205,1.21875.93652,0,1.81738-.45703,2.35645-1.22266l.22461-.31738c1.60352-2.27637,2.43066-9.79492,2.43066-12.50244,0-.23499-.00854-.51709-.021-.81805l3.25732-.87183c.09747.16071.21625.30347.36035.40912l1.15039.84473c.18066.13281.38086.20166.5791.20166.07422,0,.14941-.00977.22168-.0293l3.48535-.93262c.56641-.15234.85645-.84863.65918-1.58594Zm-24.24072,6.86182l-3.41602.93262c-.03516-.01465-.14844-.12109-.20654-.33691l-.57617-2.15137c-.05713-.21484-.01318-.3623-.04688-.3623h-.00098l3.4165-.93164s.00586.00293.01709.01074c.00049,0,.00098.00098.00098.00098l1.00842.74017-1.90881.5108c-.2666.07227-.4248.3457-.35352.6123.05957.22363.26172.37109.48242.37109.04297,0,.08643-.00586.12988-.0166l1.91559-.5127-.46198,1.13281Zm5.52686-4.07959c0-3.07959.93994-10.06934,2.4751-12.24805.35596-.50488.91504-.79492,1.53418-.79492.62109.00049,1.18213.2915,1.54053.79932l.22266.31689c1.17902,1.67261,1.96533,7.08936,2.18457,10.35278l-1.50305.40228c-.19397-2.55505-1.02423-4.83514-2.44324-4.83514-1.63379,0-2.48828,3.02197-2.48828,6.00684,0,.04913.00214.09814.00262.14728l-1.51538.40552c-.00574-.19403-.0097-.38147-.0097-.5528Zm2.52692-.12079c.02441-2.93951.89984-4.88605,1.48383-4.88605.53003,0,1.29828,1.6062,1.45496,4.09961l-2.93878.78644Zm2.96765.24115c-.02441,2.93933-.89978,4.88599-1.48383,4.88599-.53003,0-1.29834-1.60632-1.45496-4.09937l2.93878-.78662Zm2.52692-.12036c0,2.66846-.84961,9.94092-2.24902,11.92725l-.22363.31641c-.71387,1.01367-2.36084,1.01562-3.07373.00391-1.31152-1.86127-2.18622-7.22681-2.414-10.67285l1.50641-.4032c.19397,2.55505,1.02417,4.83484,2.44324,4.83484,1.63379,0,2.48828-3.02148,2.48828-6.00635,0-.04926-.00214-.09845-.00262-.14777l1.51514-.40558c.00659.20087.00995.38617.00995.55334Zm6.35742-1.23047s-.00684-.00293-.01855-.01123l-1.00897-.74091,1.90936-.51105c.26758-.07129.42578-.34521.35449-.6123-.07227-.26562-.3418-.42725-.6123-.35352l-1.91577.5127.46069-1.13379,3.41699-.93164c.03516.01416.14844.12012.20605.33691l.57617,2.15186c.05957.21973.01953.36816.04883.36133l-3.41699.93164Z"/%3E%3C/svg%3E');
} }
.icon_outing {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M164,44.17V32a20,20,0,0,0-20-20H112A20,20,0,0,0,92,32V44.17A52.05,52.05,0,0,0,44,96V216a12,12,0,0,0,12,12H200a12,12,0,0,0,12-12V96A52.05,52.05,0,0,0,164,44.17ZM112,20h32a12,12,0,0,1,12,12V44H100V32A12,12,0,0,1,112,20Zm60,144H84V152a12,12,0,0,1,12-12h64a12,12,0,0,1,12,12Zm-88,8h56v12a4,4,0,0,0,8,0V172h24v48H84Zm120,44a4,4,0,0,1-4,4H180V152a20,20,0,0,0-20-20H96a20,20,0,0,0-20,20v68H56a4,4,0,0,1-4-4V96A44.05,44.05,0,0,1,96,52h64a44.05,44.05,0,0,1,44,44ZM148,88a4,4,0,0,1-4,4H112a4,4,0,0,1,0-8h32A4,4,0,0,1,148,88Z"/%3E%3C/svg%3E');
}
.icon_pool {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M88,145.39a4,4,0,0,0,4-4V124h72v19.29a4,4,0,0,0,8,0V32a4,4,0,0,0-8,0V52H92V32a4,4,0,0,0-8,0V141.39A4,4,0,0,0,88,145.39ZM92,116V92h72v24Zm72-56V84H92V60ZM28,168a4,4,0,0,1,4-4c13.21,0,20.12,4.61,26.22,8.67,5.9,3.93,11,7.33,21.78,7.33s15.88-3.4,21.78-7.33c6.09-4.06,13-8.67,26.21-8.67s20.13,4.61,26.22,8.67c5.9,3.93,11,7.33,21.79,7.33s15.88-3.4,21.78-7.33c6.1-4.06,13-8.67,26.22-8.67a4,4,0,0,1,0,8c-10.79,0-15.88,3.4-21.78,7.33-6.1,4.06-13,8.67-26.22,8.67s-20.13-4.61-26.22-8.67c-5.9-3.93-11-7.33-21.79-7.33s-15.88,3.4-21.78,7.33c-6.09,4.06-13,8.67-26.21,8.67s-20.12-4.61-26.22-8.67C47.88,175.4,42.79,172,32,172A4,4,0,0,1,28,168Zm200,40a4,4,0,0,1-4,4c-10.79,0-15.88,3.4-21.78,7.33-6.1,4.06-13,8.67-26.22,8.67s-20.13-4.61-26.22-8.67c-5.9-3.93-11-7.33-21.79-7.33s-15.88,3.4-21.78,7.33c-6.09,4.06-13,8.67-26.21,8.67s-20.12-4.61-26.22-8.67C47.88,215.4,42.79,212,32,212a4,4,0,0,1,0-8c13.21,0,20.12,4.61,26.22,8.67,5.9,3.93,11,7.33,21.78,7.33s15.88-3.4,21.78-7.33c6.09-4.06,13-8.67,26.21-8.67s20.13,4.61,26.22,8.67c5.9,3.93,11,7.33,21.79,7.33s15.88-3.4,21.78-7.33c6.1-4.06,13-8.67,26.22-8.67A4,4,0,0,1,228,208Z"/%3E%3C/svg%3E');
}
.icon_puzzle {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M218.14,161.93a4,4,0,0,0-3.86-.24,24,24,0,0,1-34.23-23.25,24,24,0,0,1,34.23-20.13A4,4,0,0,0,220,114.7V72a12,12,0,0,0-12-12H167a32,32,0,1,0-62.91-10.33A32.57,32.57,0,0,0,105,60H64A12,12,0,0,0,52,72v37a32,32,0,1,0-10.33,62.91A32.28,32.28,0,0,0,52,171v37a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V165.31A4,4,0,0,0,218.14,161.93ZM212,208a4,4,0,0,1-4,4H64a4,4,0,0,1-4-4V165.31a4,4,0,0,0-1.86-3.38,4,4,0,0,0-3.85-.24,24,24,0,0,1-34.24-20.13,24,24,0,0,1,34.24-23.25A4,4,0,0,0,60,114.7V72a4,4,0,0,1,4-4h46.69a4,4,0,0,0,3.62-5.71,24,24,0,0,1,20.13-34.24,24,24,0,0,1,23.25,34.24A4,4,0,0,0,161.31,68H208a4,4,0,0,1,4,4v37a32.57,32.57,0,0,0-10.33-.94A32,32,0,1,0,212,171Z"/%3E%3C/svg%3E');
}
.icon_restaurant {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M76,88V40a4,4,0,0,1,8,0V88a4,4,0,0,1-8,0ZM212,40V224a4,4,0,0,1-8,0V172H152a4,4,0,0,1-4-4,264.27,264.27,0,0,1,7.11-55.94c9.47-39.22,27.21-65.41,51.31-75.74A4,4,0,0,1,212,40Zm-8,6.46C162.25,70.33,156.81,145.75,156.1,164H204Zm-88-7.12a4,4,0,0,0-7.9,1.32l8,47.66a36,36,0,0,1-72,0l8-47.66a4,4,0,0,0-7.9-1.32l-8,48A4.89,4.89,0,0,0,36,88a44.06,44.06,0,0,0,40,43.81V224a4,4,0,0,0,8,0V131.81A44.06,44.06,0,0,0,124,88a4.89,4.89,0,0,0,0-.66Z"/%3E%3C/svg%3E');
}
.icon_route {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M226.46,52.85a4,4,0,0,0-3.43-.73L160.47,67.76,97.79,36.42a4,4,0,0,0-2.76-.3l-64,16A4,4,0,0,0,28,56V200a4,4,0,0,0,5,3.88l62.56-15.64,62.68,31.34a4,4,0,0,0,2.76.3l64-16a4,4,0,0,0,3-3.88V56A4,4,0,0,0,226.46,52.85ZM100,46.47l56,28V209.53l-56-28ZM36,59.12l56-14V180.88l-56,14ZM220,196.88l-56,14V75.12l56-14Z"/%3E%3C/svg%3E');
}
.icon_rv {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 29 23.832"%3E%3Cpath stroke-width="0" d="m15.6527,16.65094h-3.92455c-.68449,0-1.23939-.55489-1.23939-1.23939V6.08515c0-.68422.55467-1.2389,1.2389-1.2389h3.92406c.68476,0,1.23987.55511,1.23987,1.23987v9.32592c0,.68422-.55467,1.2389-1.2389,1.2389Zm-3.92504-10.80469c-.13139,0-.2389.1075-.2389.2389v9.32689c0,.13194.10696.2389.2389.2389h3.92406c.13248,0,.23987-.1074.23987-.23987V6.08515c0-.13194-.10696-.2389-.2389-.2389h-3.92504Z"/%3E%3Cpath stroke-width="0" d="m7.83386,11.2486H3.45423c-.68422,0-1.2389-.55467-1.2389-1.2389v-3.92455c0-.68422.55467-1.2389,1.2389-1.2389h4.38012c.68422,0,1.2389.55467,1.2389,1.2389v3.92406c0,.68449-.55489,1.23939-1.23939,1.23939ZM3.45423,5.84626c-.13194,0-.2389.10696-.2389.2389v3.92406c0,.13166.10772.23939.23939.23939h4.37914c.13166,0,.23939-.10772.23939-.23939v-3.92406c0-.13194-.10696-.2389-.2389-.2389H3.45423Z"/%3E%3Cpath stroke-width="0" d="m28.875,11.00897l-3.31604-5.19989h.18054c.72272,0,1.3103-.58594,1.3103-1.30615,0-3.03418-2.47571-4.50293-5.51849-4.50293H1.30981C.58759,0,0,.58594,0,1.30615v18.02783c0,.82843.67157,1.5,1.5,1.5h2.53625c.27747,1.91296,2.05322,3.23883,3.96619,2.9613,1.53394-.22247,2.73883-1.42737,2.9613-2.9613h7.07251c.27747,1.91296,2.05322,3.23883,3.96619,2.9613,1.53394-.22247,2.73883-1.42737,2.9613-2.9613h2.53625c.82843,0,1.5-.67157,1.5-1.5v-8c-.0014-.11981-.04572-.23511-.125-.32501Zm-1.46252-.17499h-8.41248v-4.20093c0-.44131.35776-.79907.79907-.79907h4.2334c.15027-.00006.29254.06744.38751.18378l2.99249,4.81622ZM7.5,22.83398c-1.38074,0-2.5-1.11926-2.5-2.5s1.11926-2.5,2.5-2.5,2.5,1.11932,2.5,2.5-1.11926,2.5-2.5,2.5Zm14,0c-1.38074,0-2.5-1.11926-2.5-2.5s1.11926-2.5,2.5-2.5,2.5,1.11932,2.5,2.5-1.11926,2.5-2.5,2.5Zm6.5-3.5c0,.27614-.22386.5-.5.5h-2.53625c-.27747-1.91296-2.05322-3.23877-3.96619-2.9613-1.53394.22247-2.73883,1.42737-2.9613,2.9613h-7.07251c-.27747-1.91296-2.05322-3.23877-3.96619-2.9613-1.53394.22247-2.73883,1.42737-2.9613,2.9613H1.49993c-.27613,0-.49997-.22387-.49993-.5l.00281-18.02702c.00002-.16883.13818-.30696.30701-.30696h20.2215c2.49295,0,4.51981,1.02413,4.51563,3.51052-.00028.16481-.14263.29856-.30744.29856h-6.07078c-.91711,0-1.66082.74301-1.66169,1.66012l-.00898,9.43901c0,.27637.22363.5.5.5s.5-.22363.5-.5v-4.07422h9.00195v7.5Z"/%3E%3C/svg%3E');
}
.icon_shower {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M60,236a8,8,0,1,1-8-8A8,8,0,0,1,60,236Zm24-40a8,8,0,1,0,8,8A8,8,0,0,0,84,196Zm-64,0a8,8,0,1,0,8,8A8,8,0,0,0,20,196Zm32-32a8,8,0,1,0,8,8A8,8,0,0,0,52,164ZM252,40a4,4,0,0,1-4,4H219.31a4,4,0,0,0-2.82,1.17L187.73,73.93,165.86,202a12,12,0,0,1-8.17,9.44A12.09,12.09,0,0,1,154,212a12,12,0,0,1-8.46-3.52l-98-98A12,12,0,0,1,54,90.14l128-21.87,28.76-28.76A11.93,11.93,0,0,1,219.31,36H248A4,4,0,0,1,252,40ZM179.11,76.89,55.37,98a4,4,0,0,0-2.19,6.78l98,98a4,4,0,0,0,6.78-2.17Z"/%3E%3C/svg%3E');
}
.icon_store {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M28,112a36,36,0,0,0,16,29.92V208a12,12,0,0,0,12,12H200a12,12,0,0,0,12-12V141.92A36,36,0,0,0,228,112l0-16a4.09,4.09,0,0,0-.13-1.1L213.5,44.7A12,12,0,0,0,202,36H54A12,12,0,0,0,42.5,44.7L28.15,94.9A4.09,4.09,0,0,0,28,96ZM50.19,46.9A4,4,0,0,1,54,44H202a4,4,0,0,1,3.84,2.9L218.7,92H37.3ZM100,100h56v12a28,28,0,0,1-56,0ZM36,112V100H92v12a28,28,0,0,1-56,0Zm168,96a4,4,0,0,1-4,4H56a4,4,0,0,1-4-4V145.94a36,36,0,0,0,44-17.48,36,36,0,0,0,64,0,36,36,0,0,0,44,17.48Zm-12-68a28,28,0,0,1-28-28V100h56v12A28,28,0,0,1,192,140Z"/%3E%3C/svg%3E');
}
.icon_toilet {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M116,64a4,4,0,0,1-4,4H96a4,4,0,0,1,0-8h16A4,4,0,0,1,116,64Zm52,130.86,3.92,27.44A12,12,0,0,1,160,236H96a12,12,0,0,1-11.88-13.7L88,194.86A92.11,92.11,0,0,1,36,112a4,4,0,0,1,4-4H60V40A12,12,0,0,1,72,28H184a12,12,0,0,1,12,12v68h20a4,4,0,0,1,4,4A92.11,92.11,0,0,1,168,194.86ZM68,108H188V40a4,4,0,0,0-4-4H72a4,4,0,0,0-4,4Zm92.34,90.13a92,92,0,0,1-64.68,0L92,223.43a4,4,0,0,0,.94,3.19A3.93,3.93,0,0,0,96,228h64a3.93,3.93,0,0,0,3-1.38,4,4,0,0,0,.94-3.19ZM211.91,116H44.09a84,84,0,0,0,167.82,0Z"/%3E%3C/svg%3E');
}
.icon_washer {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.57617 28.04346"%3E%3Cpath stroke-width="0" d="m22.62988,0H1.94629C.87305,0,0,.87354,0,1.94678v25.59668c0,.27637.22363.5.5.5h23.57617c.27637,0,.5-.22363.5-.5V1.94678c0-1.07324-.87305-1.94678-1.94629-1.94678ZM1.94629,1h20.68359c.52148,0,.94629.4248.94629.94678v3.74756H1V1.94678c0-.52197.42432-.94678.94629-.94678Zm-.94629,26.04346V6.69434h22.57617v20.34912H1Z"/%3E%3Cpath stroke-width="0" d="m3.92139,4.02881h.55273c.27637,0,.5-.22363.5-.5s-.22363-.5-.5-.5h-.55273c-.27637,0-.5.22363-.5.5s.22363.5.5.5Z"/%3E%3Cpath stroke-width="0" d="m6.99805,4.02881h.55322c.27637,0,.5-.22363.5-.5s-.22363-.5-.5-.5h-.55322c-.27637,0-.5.22363-.5.5s.22363.5.5.5Z"/%3E%3Cpath stroke-width="0" d="m12.28809,8.35986c-4.63818,0-8.41162,3.77344-8.41162,8.41211,0,4.6377,3.77344,8.41113,8.41162,8.41113,4.6377,0,8.41113-3.77344,8.41113-8.41113,0-4.63867-3.77344-8.41211-8.41113-8.41211Zm0,1c3.78308,0,6.9071,2.85101,7.3515,6.51611-.02045.01282-.04352.01862-.06244.03467-.01562.01367-1.58887,1.35449-3.57617,1.35645h-.00391c-1.39941,0-2.19141-.55273-3.02979-1.1377-.83887-.58496-1.70703-1.19043-3.1167-1.19043-2.3396,0-4.15607,1.26996-4.96783,1.95667-.00067-.04156-.00629-.08191-.00629-.12366,0-4.08691,3.32471-7.41211,7.41162-7.41211Zm0,14.82324c-3.62976,0-6.6524-2.62415-7.28448-6.07397.05127-.02527.10077-.05597.1424-.09985.01953-.02051,1.99365-2.07031,4.70459-2.07031,1.0957,0,1.76709.46875,2.54492,1.01074.88477.61816,1.8877,1.31738,3.60156,1.31738h.00391c1.63293-.00134,2.9729-.72321,3.68317-1.19769-.15778,3.94849-3.40955,7.11371-7.39606,7.11371Z"/%3E%3C/svg%3E');
}
.icon_wheelchair {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M176,76a28,28,0,1,0-28-28A28,28,0,0,0,176,76Zm0-48a20,20,0,1,1-20,20A20,20,0,0,1,176,28ZM164,168a60,60,0,1,1-60-60,4,4,0,0,1,0,8,52,52,0,1,0,52,52,4,4,0,0,1,8,0Zm39.09-34.54a4,4,0,0,1,.83,3.32l-16,80A4,4,0,0,1,184,220a3.44,3.44,0,0,1-.78-.08,4,4,0,0,1-3.14-4.7l15-75.22H128a4,4,0,0,1-3.47-6l22.08-38.42a84.05,84.05,0,0,0-96.06,7.61A4,4,0,0,1,45.45,97a92,92,0,0,1,108.73-6.15,4,4,0,0,1,1.29,5.34L134.91,132H200A4,4,0,0,1,203.09,133.46Z"/%3E%3C/svg%3E');
}
.icon_wifi {
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="%23000000" height="32" width="32"%3E%3Cpath d="M136,204a8,8,0,1,1-8-8A8,8,0,0,1,136,204ZM234.54,90.1a168,168,0,0,0-213.08,0,4,4,0,1,0,5.08,6.18,160,160,0,0,1,202.92,0,4,4,0,0,0,5.08-6.18Zm-32.06,35.81a120,120,0,0,0-149,0,4,4,0,0,0,5,6.27,112,112,0,0,1,139,0,4,4,0,0,0,5-6.27Zm-32.13,35.86a72,72,0,0,0-84.7,0,4,4,0,1,0,4.7,6.46,64.07,64.07,0,0,1,75.3,0,4,4,0,0,0,5.58-.87A4,4,0,0,0,170.35,161.77Z"/%3E%3C/svg%3E');
}
.outside_activities { .outside_activities {
margin-top: 2rem; margin-top: 2rem;
} }
.outside_activities h3 { .outside_activities h3, .campsite_services .spiel {
font-size: calc(2.2rem + 4vw); font-size: calc(2.2rem + 4vw);
font-weight: 600; font-weight: 600;
line-height: .9em; line-height: .9em;
}
.outside_activities h3 {
margin-bottom: 10rem; margin-bottom: 10rem;
} }
@ -628,10 +708,6 @@ dt {
width: 20%; width: 20%;
} }
.outside_activities > div:last-child p {
margin-bottom: 1.5em;
}
.campsite_activities { .campsite_activities {
margin-top: 10rem; margin-top: 10rem;
padding-top: 10rem; padding-top: 10rem;
@ -645,6 +721,10 @@ dt {
margin-bottom: 5rem; margin-bottom: 5rem;
} }
.campsite_services.carousel .slick-track {
align-items: center;
}
footer { footer {
font-size: 1.5rem; font-size: 1.5rem;
text-align: center; text-align: center;

View File

@ -75,6 +75,9 @@
<li> <li>
<a {{ if ne . "home"}}href="/admin/home"{{ end }}>{{( pgettext "Home Page" "title" )}}</a> <a {{ if ne . "home"}}href="/admin/home"{{ end }}>{{( pgettext "Home Page" "title" )}}</a>
</li> </li>
<li>
<a {{ if ne . "services"}}href="/admin/services"{{ end }}>{{( pgettext "Services Page" "title" )}}</a>
</li>
</ul> </ul>
</nav> </nav>
{{- end }} {{- end }}

View File

@ -0,0 +1,65 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/services.slideForm*/ -}}
{{ if .ID}}
{{( pgettext "Edit Carousel Slide" "title" )}}
{{ else }}
{{( pgettext "New Carousel Slide" "title" )}}
{{ end }}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/services.slideForm*/ -}}
{{ template "settings-tabs" "services" }}
<form
enctype="multipart/form-data"
{{ if .ID }}
data-hx-put="/admin/services/slides/{{ .ID }}"
{{ else }}
action="/admin/services/slides" method="post"
{{ end }}
>
<h2>
{{ if .ID }}
{{( pgettext "Edit Carousel Slide" "title" )}}
{{ else }}
{{( pgettext "New Carousel Slide" "title" )}}
{{ end }}
</h2>
{{ CSRFInput }}
<fieldset>
{{ with .Media -}}
{{ if .Val -}}
<img src="{{ .Val }}" alt="">
{{- end }}
<label>
{{( pgettext "Cover image" "input" )}}
<input type="file" name="{{ .Name }}"
{{ if not $.ID }}required{{ end }}
{{ template "error-attrs" . }}><br>
</label>
{{ template "error-message" . }}
{{- end }}
{{ with .Caption -}}
<label>
{{( pgettext "Caption" "input")}}<br>
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
{{ template "error-attrs" . }}><br>
</label>
{{ template "error-message" . }}
{{- end }}
</fieldset>
<footer>
<button type="submit">
{{ if .ID }}
{{( pgettext "Update" "action" )}}
{{ else }}
{{( pgettext "Add" "action" )}}
{{ end }}
</button>
</footer>
</form>
{{- end }}

View File

@ -0,0 +1,36 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/services.slideL10nForm*/ -}}
{{printf (pgettext "Translate Carousel Slide to %s" "title") .Locale.Endonym }}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/services.slideL10nForm*/ -}}
{{ template "settings-tabs" "campsiteTypes" }}
<form data-hx-put="/admin/services/slides/{{ .ID }}/{{ .Locale.Language }}">
<h2>
{{printf (pgettext "Translate Carousel Slide to %s" "title") .Locale.Endonym }}
</h2>
{{ CSRFInput }}
<fieldset>
{{ with .Caption -}}
<fieldset>
<legend>{{( pgettext "Caption" "input")}}</legend>
{{( gettext "Source:" )}} {{ .Source }}<br>
<label>
{{( pgettext "Translation:" "input" )}}
<input type="text" name="{{ .Name }}" value="{{ .Val }}"
{{ template "error-attrs" . }}><br>
</label>
{{ template "error-message" . }}
</fieldset>
{{- end }}
</fieldset>
<footer>
<button type="submit">{{( pgettext "Translate" "action" )}}</button>
</footer>
</form>
{{- end }}

View File

@ -0,0 +1,50 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Home Page" "title" )}}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/services.servicesIndex*/ -}}
{{ template "settings-tabs" "services" }}
<h2>{{( pgettext "Carousel" "title" )}}</h2>
<a href="/admin/services/slides/new">{{( pgettext "Add slide" "action" )}}</a>
{{ if .Slides -}}
<table>
<thead>
<tr>
<th scope="col">{{( pgettext "Image" "header" )}}</th>
<th scope="col">{{( pgettext "Caption" "header" )}}</th>
<th scope="col">{{( pgettext "Translations" "campsite type" )}}</th>
<th scope="col">{{( pgettext "Actions" "campsite type" )}}</th>
</tr>
</thead>
<tbody>
{{ range $slide := .Slides -}}
<tr>
<td><a href="/admin/services/slides/{{ .ID }}"><img src="{{ .Media }}" alt=""></a></td>
<td><a href="/admin/services/slides/{{ .ID }}">{{ .Caption }}</a></td>
<td>
{{ range .Translations }}
<a
{{ if .Missing }}
class="missing-translation"
{{ end }}
href="/admin/services/slides/{{ $slide.ID }}/{{ .Language }}">{{ .Endonym }}</a>
{{ end }}
</td>
<td>
<form data-hx-delete="/admin/services/slides/{{ .ID }}" data-hx-headers='{ {{ CSRFHeader }} }'>
<button type="submit">{{( pgettext "Delete" "action" )}}</button>
</form>
</td>
</tr>
{{- end }}
</tbody>
</table>
{{ else -}}
<p>{{( gettext "No slides added yet." )}}</p>
{{- end }}
{{- end }}

View File

@ -7,11 +7,11 @@
{{- end }} {{- end }}
{{ define "head" -}} {{ define "head" -}}
<link rel="stylesheet" media="screen" href="/static/slick@1.8.1.css"> {{ template "carouselStyle" }}
{{- end }} {{- end }}
{{ define "content" -}} {{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/app.homePage*/ -}} {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/home.homePage*/ -}}
<section class="nature"> <section class="nature">
<div style="--background-image:url('/static/images/todd-trapani-5LHzBpiTuzQ-unsplash.jpg')"> <div style="--background-image:url('/static/images/todd-trapani-5LHzBpiTuzQ-unsplash.jpg')">
<h2>{{ (gettext "The pleasure of camping in the middle of nature…")}}</h2> <h2>{{ (gettext "The pleasure of camping in the middle of nature…")}}</h2>
@ -31,8 +31,8 @@
<h2><a href="/{{ currentLocale }}/services">{{( gettext "Our services")}} <span>→</span></a></h2> <h2><a href="/{{ currentLocale }}/services">{{( gettext "Our services")}} <span>→</span></a></h2>
</section> </section>
<section class="surroundings"> <section class="surroundings">
<h2 class="sr-only">{{ (gettext "Surroundings")}}</h2> <h2 class="sr-only">{{( pgettext "Surroundings" "title" )}}</h2>
<div> <div class="carousel">
<div class="spiel"> <div class="spiel">
<p>{{(gettext "Located in <strong>Alta Garrotxa</strong>, between the <strong>Pyrenees</strong> and the <strong>Costa Brava</strong>.") | raw}}</p> <p>{{(gettext "Located in <strong>Alta Garrotxa</strong>, between the <strong>Pyrenees</strong> and the <strong>Costa Brava</strong>.") | raw}}</p>
<p>{{(gettext "Nearby there are the <strong>gorges of Sadernes</strong>, <strong>volcanoes</strong>, <strong>La Fageda den Jordà</strong>, the Jewish quarter of <strong>Besalú</strong>, the basaltic cliff of <strong>Castellfollit de la Roca</strong>… much to see and much to do.") | raw}}</p> <p>{{(gettext "Nearby there are the <strong>gorges of Sadernes</strong>, <strong>volcanoes</strong>, <strong>La Fageda den Jordà</strong>, the Jewish quarter of <strong>Besalú</strong>, the basaltic cliff of <strong>Castellfollit de la Roca</strong>… much to see and much to do.") | raw}}</p>
@ -53,26 +53,5 @@
</section> </section>
<p class="enjoy">{{( gettext "Come and enjoy!")}}</p> <p class="enjoy">{{( gettext "Come and enjoy!")}}</p>
<script src="/static/jquery@3.7.1.min.js"></script> {{ template "carouselInit" }}
<script src="/static/slick@1.8.1.min.js"></script>
<script>
jQuery(function () {
jQuery('.surroundings > div').slick({
slidesToShow: 2,
slidesToScroll: 1,
infinite: false,
arrows: true,
prevArrow: '<button type="button" class="slick-prev">←</button>',
nextArrow: '<button type="button" class="slick-next">→</button>',
responsive: [
{
breakpoint: 768,
settings: {
slidesToShow: 1,
}
},
]
});
});
</script>
{{- end }} {{- end }}

View File

@ -62,3 +62,32 @@
{{ define "alternateAnchor" -}} {{ define "alternateAnchor" -}}
<a rel="alternate" href="{{ .HRef }}" hreflang="{{ .Lang }}" lang="{{ .Lang }}">{{ .Endonym }}</a> <a rel="alternate" href="{{ .HRef }}" hreflang="{{ .Lang }}" lang="{{ .Lang }}">{{ .Endonym }}</a>
{{- end }} {{- end }}
{{ define "carouselStyle" -}}
<link rel="stylesheet" media="screen" href="/static/slick@1.8.1.css">
{{- end }}
{{ define "carouselInit" -}}
<script src="/static/jquery@3.7.1.min.js"></script>
<script src="/static/slick@1.8.1.min.js"></script>
<script>
jQuery(function () {
jQuery('.carousel').slick({
slidesToShow: 2,
slidesToScroll: 1,
infinite: false,
arrows: true,
prevArrow: '<button type="button" class="slick-prev">←</button>',
nextArrow: '<button type="button" class="slick-next">→</button>',
responsive: [
{
breakpoint: 768,
settings: {
slidesToShow: 1,
}
},
]
});
});
</script>
{{- end }}

View File

@ -0,0 +1,42 @@
<!--
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
SPDX-License-Identifier: AGPL-3.0-only
-->
{{ define "title" -}}
{{( pgettext "Services" "title" )}}
{{- end }}
{{ define "head" -}}
{{ template "carouselStyle" }}
{{- end }}
{{ define "content" -}}
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/services.servicesPage*/ -}}
<h2>{{( pgettext "Services" "title" )}}</h2>
<div class="campsite_services carousel">
<div class="spiel">
<p>{{(gettext "The campsite offers many different services.")}}</p>
</div>
{{ range .Carousel -}}
{{ if .Caption -}}
<figure>
<img src="{{ .Media }}" alt=""/>
<figcaption>{{ .Caption }}</figcaption>
</figure>
{{- else -}}
<img src="{{ .Media }}" alt=""/>
{{- end }}
{{- end }}
</div>
{{ template "carouselInit" }}
<dl>
{{ range .Services -}}
<div>
<dt class="icon_{{ .IconName }}">{{ .Name }}</dt>
<dd>{{ .Description | raw }}</dd>
</div>
{{- end }}
</dl>
{{- end }}