diff --git a/demo/demo.sql b/demo/demo.sql index 39dd4ad..2cf9080 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -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_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, '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) @@ -59,6 +63,25 @@ values (66, 'en', 'Santa Margarida volcano') , (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; insert into campsite_type (company_id, name, media_id, description) values (52, 'Parceŀles', 62, '') @@ -77,4 +100,59 @@ values (72, 'en', 'Plots', '') , (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ó', '

A la recepció l’informarem del que pot fer des del càmping mateix o pels voltants.

') + , (52, 'wifi', 'WiFi', '

Un 80 % de l’àrea del càmping disposa d’accés WiFi lliure.

') + , (52, 'restaurant', 'Bar & Tapes', '

Oberts:

') + , (52, 'store', 'Botiga', '

Oberta a diari.

Venda de pa del dia per encàrrec.

') + , (52, 'wheelchair', 'Accessibilitat', '

Piscines i serveis del càmping adaptats a persones amb mobilitat reduïda.

') + , (52, 'toilet', 'Lavabos', '

Ubicació central i pràctica. Nets i ben mantinguts.

') + , (52, 'shower', 'Dutxa', '

Aigua calenta, sense fitxes.

') + , (52, 'baby', 'Bany per nadons', '

Bany individual per nadons, amb banyera i canviador.

') + , (52, 'pool', 'Piscina', '

Piscina per adults i piscina infantil.

(Piscines amb aigua salada.)

') + , (52, 'campfire', 'Barbacoa', '

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).

') + , (52, 'rv', 'Estació servei per autocaravanes', '

Situada a l’entrada del càmping.

') + , (52, 'castle', 'Zona de jocs', '

Una zona central pels més menuts.

') + , (52, 'ball', 'Camp d’esport', '

Amb camp de futbol, voley, tenis-taula i espai per jugar.

') + , (52, 'puzzle', 'Sala de jocs i televisió', '

Una sala pels dies de mal temps.

') + , (52, 'washer', 'Rentadores i assecadores', '

Als safareigs del càmping hi ha dues rentadores i una assecadora que funcionen amb fitxes.

') + , (52, 'fridge', 'Lloguer de neveres', '

Possibilitat de llogar neveres per estades llargues amb Rent It.

') +; + +insert into service_i18n (service_id, lang_tag, name, description) +values (82, 'en', 'Information', '

At reception we will inform you of what you can do from the campsite itself or in the surrounding area.

') + , (82, 'es', 'Información', '

A recepción le informaremos de qué puede hacer en el camping o por los alrededores.

') + , (83, 'en', 'WiFi', '

80 % of the campsite area has free WiFi access.

') + , (83, 'es', 'WiFi', '

Un 80 % del área del camping dispone de acceso WiFi libre.

') + , (84, 'en', 'Bar & Tapas', '

Open:

') + , (84, 'es', 'Bar & Tapas', '

Abierto:

') + , (85, 'en', 'Shop', '

Open daily

Sale of daily bread to order.

') + , (85, 'es', 'Tienda', '

Abierta a diario.

Venta de pan del día por encargo.

') + , (86, 'en', 'Accessibility', '

Swimming pools and campsite services adapted to people with reduced mobility.

') + , (86, 'es', 'Acesibilidad', '

Piscinas y servicios del camping adaptados a personas con mobilidad reducida.

') + , (87, 'en', 'Toilets', '

Central and practical location. Clean and well maintained.

') + , (87, 'es', 'Lavabos', '

Ubicación central y práctica. Limpios y bien mantenidos.

') + , (88, 'en', 'Showers', '

Hot water, no tokens.

') + , (88, 'es', 'Duchas', '

Agua caliente, sin fichas.

') + , (89, 'en', 'Baby baths', '

Individual bathroom for babies, with bathtub and changing table.

') + , (89, 'es', 'Baño para bebés', '

Baños individuales para bebés, con bañera y cambiador.

') + , (90, 'en', 'Swimming pool', '

Adult pool and children’s pool.

(Salt water swimming pools.)

') + , (90, 'es', 'Piscina', '

Piscina para adultos y piscina infantil.

(Piscinas con agua salada.)

') + , (91, 'en', 'Barbecue', '

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).

') + , (91, 'es', 'Barbacoa', '

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).

') + , (92, 'en', 'RV service station', '

Located at the entrance of the campsite.

') + , (92, 'es', 'Estación servicio para autocaravanas', '

Situada en la entrada del camping.

') + , (93, 'en', 'Play area', '

A central area for the little ones.

') + , (93, 'es', 'Zona de juegos', '

Una zona central para los más pequeños.

') + , (94, 'en', 'Sports area', '

With football field, volleyball, table tennis and room to play.

') + , (94, 'es', 'Campo de deporte', '

Con campo de fútbol, voley, pimpón i espacio para jugar.

') + , (95, 'en', 'Games and television room', '

A room for bad weather days.

') + , (95, 'es', 'Sala de juegos y televisión', '

Una sala para los días de mal tiempo.

') + , (96, 'en', 'Washing machines and dryers', '

There are two token-operated washing machines and a dryer in the campsite’s laundry facilities.

') + , (96, 'es', 'Lavadora y secadoras', '

A los lavaderos del camping hay dos lavadoras y una secadora que funcionana con fichas.

') + , (97, 'en', 'Fridge rental', '

Possibility to rent refrigerators for long stays with Rent It.

') + , (97, 'es', 'Alquiler de neveras', '

Posibilidad de alquilar neveras para estancias largas con Rent It.

') +; + commit; diff --git a/demo/services_carousel0.avif b/demo/services_carousel0.avif new file mode 100644 index 0000000..7e41e80 Binary files /dev/null and b/demo/services_carousel0.avif differ diff --git a/demo/services_carousel1.avif b/demo/services_carousel1.avif new file mode 100644 index 0000000..489ed98 Binary files /dev/null and b/demo/services_carousel1.avif differ diff --git a/demo/services_carousel2.avif b/demo/services_carousel2.avif new file mode 100644 index 0000000..0e1254f Binary files /dev/null and b/demo/services_carousel2.avif differ diff --git a/demo/services_carousel3.avif b/demo/services_carousel3.avif new file mode 100644 index 0000000..6909fa7 Binary files /dev/null and b/demo/services_carousel3.avif differ diff --git a/deploy/add_service.sql b/deploy/add_service.sql new file mode 100644 index 0000000..1bc8827 --- /dev/null +++ b/deploy/add_service.sql @@ -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; diff --git a/deploy/add_services_carousel_slide.sql b/deploy/add_services_carousel_slide.sql new file mode 100644 index 0000000..c919243 --- /dev/null +++ b/deploy/add_services_carousel_slide.sql @@ -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; diff --git a/deploy/available_icons.sql b/deploy/available_icons.sql new file mode 100644 index 0000000..f585ac6 --- /dev/null +++ b/deploy/available_icons.sql @@ -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; diff --git a/deploy/edit_service.sql b/deploy/edit_service.sql new file mode 100644 index 0000000..bf1ab85 --- /dev/null +++ b/deploy/edit_service.sql @@ -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; diff --git a/deploy/icon.sql b/deploy/icon.sql new file mode 100644 index 0000000..7c06667 --- /dev/null +++ b/deploy/icon.sql @@ -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; diff --git a/deploy/remove_services_carousel_slide.sql b/deploy/remove_services_carousel_slide.sql new file mode 100644 index 0000000..daa2afe --- /dev/null +++ b/deploy/remove_services_carousel_slide.sql @@ -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; diff --git a/deploy/service.sql b/deploy/service.sql new file mode 100644 index 0000000..831991b --- /dev/null +++ b/deploy/service.sql @@ -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; diff --git a/deploy/service_i18n.sql b/deploy/service_i18n.sql new file mode 100644 index 0000000..324a905 --- /dev/null +++ b/deploy/service_i18n.sql @@ -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; diff --git a/deploy/services_carousel.sql b/deploy/services_carousel.sql new file mode 100644 index 0000000..95a421f --- /dev/null +++ b/deploy/services_carousel.sql @@ -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; diff --git a/deploy/services_carousel_i18n.sql b/deploy/services_carousel_i18n.sql new file mode 100644 index 0000000..53aed69 --- /dev/null +++ b/deploy/services_carousel_i18n.sql @@ -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; diff --git a/deploy/translate_service.sql b/deploy/translate_service.sql new file mode 100644 index 0000000..5e73288 --- /dev/null +++ b/deploy/translate_service.sql @@ -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; diff --git a/deploy/translate_services_carousel_slide.sql b/deploy/translate_services_carousel_slide.sql new file mode 100644 index 0000000..88a9ff0 --- /dev/null +++ b/deploy/translate_services_carousel_slide.sql @@ -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; diff --git a/pkg/app/admin.go b/pkg/app/admin.go index 0485027..e3b8bf2 100644 --- a/pkg/app/admin.go +++ b/pkg/app/admin.go @@ -16,6 +16,7 @@ import ( httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/season" + "dev.tandem.ws/tandem/camper/pkg/services" "dev.tandem.ws/tandem/camper/pkg/template" ) @@ -24,6 +25,7 @@ type adminHandler struct { company *company.AdminHandler home *home.AdminHandler season *season.AdminHandler + services *services.AdminHandler } func newAdminHandler(locales locale.Locales) *adminHandler { @@ -32,6 +34,7 @@ func newAdminHandler(locales locale.Locales) *adminHandler { company: company.NewAdminHandler(), home: home.NewAdminHandler(locales), 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) case "seasons": h.season.Handler(user, company, conn).ServeHTTP(w, r) + case "services": + h.services.Handler(user, company, conn).ServeHTTP(w, r) case "": switch r.Method { case http.MethodGet: diff --git a/pkg/app/public.go b/pkg/app/public.go index 99dbd0a..9b3a3fa 100644 --- a/pkg/app/public.go +++ b/pkg/app/public.go @@ -13,18 +13,21 @@ import ( "dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/home" httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/services" "dev.tandem.ws/tandem/camper/pkg/template" ) type publicHandler struct { home *home.PublicHandler campsite *campsite.PublicHandler + services *services.PublicHandler } func newPublicHandler() *publicHandler { return &publicHandler{ home: home.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) case "campsites": h.campsite.Handler(user, company, conn).ServeHTTP(w, r) + case "services": + h.services.Handler(user, company, conn).ServeHTTP(w, r) case "surroundings": surroundingsHandler(user, company, conn).ServeHTTP(w, r) default: diff --git a/pkg/services/admin.go b/pkg/services/admin.go new file mode 100644 index 0000000..9397a43 --- /dev/null +++ b/pkg/services/admin.go @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * 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) +} diff --git a/pkg/services/carousel.go b/pkg/services/carousel.go new file mode 100644 index 0000000..04c4c21 --- /dev/null +++ b/pkg/services/carousel.go @@ -0,0 +1,311 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * 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 +} diff --git a/pkg/services/l10n.go b/pkg/services/l10n.go new file mode 100644 index 0000000..791966a --- /dev/null +++ b/pkg/services/l10n.go @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * 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 +} diff --git a/pkg/services/public.go b/pkg/services/public.go new file mode 100644 index 0000000..6f33740 --- /dev/null +++ b/pkg/services/public.go @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * 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 +} diff --git a/po/ca.po b/po/ca.po index 6967c81..e782ef2 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-09-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" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -18,7 +18,17 @@ msgstr "" "Content-Transfer-Encoding: 8bit\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" msgid "Home" msgstr "Inici" @@ -37,8 +47,11 @@ msgid "Our services" msgstr "Els nostres serveis" #: web/templates/public/home.gohtml:34 +#: web/templates/public/surroundings.gohtml:6 +#: web/templates/public/surroundings.gohtml:10 +msgctxt "title" msgid "Surroundings" -msgstr "Entorn" +msgstr "L’entorn" #: web/templates/public/home.gohtml:37 msgid "Located in Alta Garrotxa, between the Pyrenees and the Costa Brava." @@ -60,12 +73,6 @@ msgstr "Descobreix l’entorn" msgid "Come and enjoy!" msgstr "Vine a gaudir!" -#: web/templates/public/surroundings.gohtml:6 -#: web/templates/public/surroundings.gohtml:10 -msgctxt "title" -msgid "Surroundings" -msgstr "L’entorn" - #: web/templates/public/surroundings.gohtml:13 msgctxt "title" 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…." 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:58 +#: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:24 +#: web/templates/public/layout.gohtml:59 msgid "Campsite Montagut" msgstr "Càmping Montagut" @@ -133,7 +140,7 @@ msgstr "Càmping Montagut" msgid "Skip to main content" msgstr "Salta al contingut principal" -#: web/templates/public/layout.gohtml:32 +#: web/templates/public/layout.gohtml:33 msgid "Singular Lodges" msgstr "Allotjaments singulars" @@ -172,6 +179,7 @@ msgstr "Etiqueta" #: web/templates/admin/campsite/form.gohtml:71 #: web/templates/admin/campsite/type/form.gohtml:77 #: web/templates/admin/season/form.gohtml:65 +#: web/templates/admin/services/carousel/form.gohtml:58 #: web/templates/admin/home/carousel/form.gohtml:58 msgctxt "action" msgid "Update" @@ -180,6 +188,7 @@ msgstr "Actualitza" #: web/templates/admin/campsite/form.gohtml:73 #: web/templates/admin/campsite/type/form.gohtml:79 #: web/templates/admin/season/form.gohtml:67 +#: web/templates/admin/services/carousel/form.gohtml:60 #: web/templates/admin/home/carousel/form.gohtml:60 msgctxt "action" msgid "Add" @@ -250,6 +259,7 @@ msgid "Name" msgstr "Nom" #: web/templates/admin/campsite/type/form.gohtml:59 +#: web/templates/admin/services/carousel/form.gohtml:39 #: web/templates/admin/home/carousel/form.gohtml:39 msgctxt "input" msgid "Cover image" @@ -280,6 +290,7 @@ msgid "Name" msgstr "Nom" #: web/templates/admin/campsite/type/index.gohtml:19 +#: web/templates/admin/services/index.gohtml:20 #: web/templates/admin/home/index.gohtml:20 msgctxt "campsite type" msgid "Translations" @@ -297,18 +308,21 @@ msgstr "Traducció del tipus d’allotjament a %s" #: web/templates/admin/campsite/type/l10n.gohtml:22 #: web/templates/admin/campsite/type/l10n.gohtml:34 +#: web/templates/admin/services/carousel/l10n.gohtml:22 #: web/templates/admin/home/carousel/l10n.gohtml:22 msgid "Source:" msgstr "Origen:" #: web/templates/admin/campsite/type/l10n.gohtml:24 #: web/templates/admin/campsite/type/l10n.gohtml:37 +#: web/templates/admin/services/carousel/l10n.gohtml:24 #: web/templates/admin/home/carousel/l10n.gohtml:24 msgctxt "input" msgid "Translation:" msgstr "Traducció:" #: web/templates/admin/campsite/type/l10n.gohtml:46 +#: web/templates/admin/services/carousel/l10n.gohtml:33 #: web/templates/admin/home/carousel/l10n.gohtml:33 msgctxt "action" msgid "Translate" @@ -385,6 +399,85 @@ msgctxt "action" msgid "Login" 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 d’inici" + +#: 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 s’ha afegit cap diapositiva encara." + #: web/templates/admin/profile.gohtml:6 web/templates/admin/profile.gohtml:12 #: web/templates/admin/layout.gohtml:29 msgctxt "title" @@ -505,68 +598,11 @@ msgctxt "action" msgid "Logout" msgstr "Surt" -#: web/templates/admin/layout.gohtml:76 web/templates/admin/home/index.gohtml:6 +#: web/templates/admin/layout.gohtml:79 +#, fuzzy msgctxt "title" -msgid "Home Page" -msgstr "Pàgina d’inici" - -#: 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 s’ha afegit cap diapositiva encara." +msgid "Services Page" +msgstr "Serveis" #: web/templates/admin/media/index.gohtml:6 #: 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." msgstr "L’idioma 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." 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" 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." 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 msgid "Selected country is not valid." 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." msgstr "S’ha 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 msgid "Uploaded file can not be empty." msgstr "No podeu deixar el fitxer del mèdia en blanc." +#~ msgid "Surroundings" +#~ msgstr "Entorn" + #~ msgid "Legend" #~ msgstr "Llegenda" diff --git a/po/es.po b/po/es.po index 4f24595..75bf5b8 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-09-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" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -18,7 +18,17 @@ msgstr "" "Content-Transfer-Encoding: 8bit\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" msgid "Home" msgstr "Inicio" @@ -37,8 +47,11 @@ msgid "Our services" msgstr "Nuestros servicios" #: web/templates/public/home.gohtml:34 +#: web/templates/public/surroundings.gohtml:6 +#: web/templates/public/surroundings.gohtml:10 +msgctxt "title" msgid "Surroundings" -msgstr "Entorno" +msgstr "El entorno" #: web/templates/public/home.gohtml:37 msgid "Located in Alta Garrotxa, between the Pyrenees and the Costa Brava." @@ -60,12 +73,6 @@ msgstr "Descubre el entorno" msgid "Come and enjoy!" 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 msgctxt "title" 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…." 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:58 +#: web/templates/public/layout.gohtml:11 web/templates/public/layout.gohtml:24 +#: web/templates/public/layout.gohtml:59 msgid "Campsite Montagut" msgstr "Camping Montagut" @@ -134,7 +141,7 @@ msgstr "Camping Montagut" msgid "Skip to main content" msgstr "Saltar al contenido principal" -#: web/templates/public/layout.gohtml:32 +#: web/templates/public/layout.gohtml:33 msgid "Singular Lodges" msgstr "Alojamientos singulares" @@ -173,6 +180,7 @@ msgstr "Etiqueta" #: web/templates/admin/campsite/form.gohtml:71 #: web/templates/admin/campsite/type/form.gohtml:77 #: web/templates/admin/season/form.gohtml:65 +#: web/templates/admin/services/carousel/form.gohtml:58 #: web/templates/admin/home/carousel/form.gohtml:58 msgctxt "action" msgid "Update" @@ -181,6 +189,7 @@ msgstr "Actualizar" #: web/templates/admin/campsite/form.gohtml:73 #: web/templates/admin/campsite/type/form.gohtml:79 #: web/templates/admin/season/form.gohtml:67 +#: web/templates/admin/services/carousel/form.gohtml:60 #: web/templates/admin/home/carousel/form.gohtml:60 msgctxt "action" msgid "Add" @@ -251,6 +260,7 @@ msgid "Name" msgstr "Nombre" #: web/templates/admin/campsite/type/form.gohtml:59 +#: web/templates/admin/services/carousel/form.gohtml:39 #: web/templates/admin/home/carousel/form.gohtml:39 msgctxt "input" msgid "Cover image" @@ -281,6 +291,7 @@ msgid "Name" msgstr "Nombre" #: web/templates/admin/campsite/type/index.gohtml:19 +#: web/templates/admin/services/index.gohtml:20 #: web/templates/admin/home/index.gohtml:20 msgctxt "campsite type" 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:34 +#: web/templates/admin/services/carousel/l10n.gohtml:22 #: web/templates/admin/home/carousel/l10n.gohtml:22 msgid "Source:" msgstr "Origen:" #: web/templates/admin/campsite/type/l10n.gohtml:24 #: web/templates/admin/campsite/type/l10n.gohtml:37 +#: web/templates/admin/services/carousel/l10n.gohtml:24 #: web/templates/admin/home/carousel/l10n.gohtml:24 msgctxt "input" msgid "Translation:" msgstr "Traducción" #: web/templates/admin/campsite/type/l10n.gohtml:46 +#: web/templates/admin/services/carousel/l10n.gohtml:33 #: web/templates/admin/home/carousel/l10n.gohtml:33 msgctxt "action" msgid "Translate" @@ -386,6 +400,85 @@ msgctxt "action" msgid "Login" 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/layout.gohtml:29 msgctxt "title" @@ -506,68 +599,11 @@ msgctxt "action" msgid "Logout" msgstr "Salir" -#: web/templates/admin/layout.gohtml:76 web/templates/admin/home/index.gohtml:6 +#: web/templates/admin/layout.gohtml:79 +#, fuzzy msgctxt "title" -msgid "Home Page" -msgstr "Página de inicio" - -#: 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." +msgid "Services Page" +msgstr "Servicios" #: web/templates/admin/media/index.gohtml:6 #: 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." 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." 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" 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." 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 msgid "Selected country is not valid." 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." 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 msgid "Uploaded file can not be empty." msgstr "No podéis dejar el archivo del medio en blanco." +#~ msgid "Surroundings" +#~ msgstr "Entorno" + #~ msgid "Legend" #~ msgstr "Leyenda" diff --git a/revert/add_service.sql b/revert/add_service.sql new file mode 100644 index 0000000..9fa33d8 --- /dev/null +++ b/revert/add_service.sql @@ -0,0 +1,7 @@ +-- Revert camper:add_service from pg + +begin; + +drop function if exists camper.add_service(integer, text, text, text); + +commit; diff --git a/revert/add_services_carousel_slide.sql b/revert/add_services_carousel_slide.sql new file mode 100644 index 0000000..3af4135 --- /dev/null +++ b/revert/add_services_carousel_slide.sql @@ -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; diff --git a/revert/available_icons.sql b/revert/available_icons.sql new file mode 100644 index 0000000..5dc0667 --- /dev/null +++ b/revert/available_icons.sql @@ -0,0 +1,7 @@ +-- Revert camper:available_icons from pg + +begin; + +delete from camper.icon; + +commit; diff --git a/revert/edit_service.sql b/revert/edit_service.sql new file mode 100644 index 0000000..8f6ea58 --- /dev/null +++ b/revert/edit_service.sql @@ -0,0 +1,7 @@ +-- Revert camper:edit_service from pg + +begin; + +drop function if exists camper.edit_service(integer, text, text, text); + +commit; diff --git a/revert/icon.sql b/revert/icon.sql new file mode 100644 index 0000000..8345f52 --- /dev/null +++ b/revert/icon.sql @@ -0,0 +1,7 @@ +-- Revert camper:icon from pg + +begin; + +drop table if exists camper.icon; + +commit; diff --git a/revert/remove_services_carousel_slide.sql b/revert/remove_services_carousel_slide.sql new file mode 100644 index 0000000..90b9824 --- /dev/null +++ b/revert/remove_services_carousel_slide.sql @@ -0,0 +1,7 @@ +-- Revert camper:remove_services_carousel_slide from pg + +begin; + +drop function if exists camper.remove_services_carousel_slide(integer); + +commit; diff --git a/revert/service.sql b/revert/service.sql new file mode 100644 index 0000000..2612be7 --- /dev/null +++ b/revert/service.sql @@ -0,0 +1,7 @@ +-- Revert camper:service from pg + +begin; + +drop table if exists camper.service; + +commit; diff --git a/revert/service_i18n.sql b/revert/service_i18n.sql new file mode 100644 index 0000000..204c79a --- /dev/null +++ b/revert/service_i18n.sql @@ -0,0 +1,7 @@ +-- Revert camper:service_i18n from pg + +begin; + +drop table if exists camper.service_i18n; + +commit; diff --git a/revert/services_carousel.sql b/revert/services_carousel.sql new file mode 100644 index 0000000..7a1991a --- /dev/null +++ b/revert/services_carousel.sql @@ -0,0 +1,7 @@ +-- Revert camper:services_carousel from pg + +begin; + +drop table if exists camper.services_carousel; + +commit; diff --git a/revert/services_carousel_i18n.sql b/revert/services_carousel_i18n.sql new file mode 100644 index 0000000..39d4cfa --- /dev/null +++ b/revert/services_carousel_i18n.sql @@ -0,0 +1,7 @@ +-- Revert camper:services_carousel_i18n from pg + +begin; + +drop table if exists camper.services_carousel_i18n; + +commit; diff --git a/revert/translate_service.sql b/revert/translate_service.sql new file mode 100644 index 0000000..1bf7b4c --- /dev/null +++ b/revert/translate_service.sql @@ -0,0 +1,7 @@ +-- Revert camper:translate_service from pg + +begin; + +drop function if exists camper.translate_service(integer, text, text, text); + +commit; diff --git a/revert/translate_services_carousel_slide.sql b/revert/translate_services_carousel_slide.sql new file mode 100644 index 0000000..51f6ac4 --- /dev/null +++ b/revert/translate_services_carousel_slide.sql @@ -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; diff --git a/sqitch.plan b/sqitch.plan index cbd5391..0ff0533 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -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 # 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 # 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 # 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 # Add relation for services’ image carousel +services_carousel_i18n [roles schema_camper services_carousel language] 2023-09-16T22:42:14Z jordi fita mas # Add relation for services carousel translations +add_services_carousel_slide [roles schema_camper services_carousel] 2023-09-16T22:45:49Z jordi fita mas # 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 # 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 # Add function to remove slides from the services carousel +icon [roles schema_camper] 2023-09-16T23:11:48Z jordi fita mas # Add relation for icon +available_icons [schema_camper icon] 2023-09-16T23:15:03Z jordi fita mas # Add the list of available icons +service [roles schema_camper company icon user_profile] 2023-09-16T23:48:19Z jordi fita mas # Add relation of services definition +add_service [roles schema_camper service] 2023-09-17T00:00:00Z jordi fita mas # Add function to create services +edit_service [roles schema_camper service] 2023-09-17T00:01:16Z jordi fita mas # Add function to edit services +service_i18n [roles schema_camper service language] 2023-09-17T00:13:42Z jordi fita mas # Add relation for service translations +translate_service [roles schema_camper service_i18n] 2023-09-17T00:17:00Z jordi fita mas # Add function to translate a service diff --git a/test/add_service.sql b/test/add_service.sql new file mode 100644 index 0000000..9e9b206 --- /dev/null +++ b/test/add_service.sql @@ -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', '

This is what, exactly?

Dunno

') $$, + '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', '

This is what, exactly?

Dunno

') + , (2, 'toilet', 'Is this Google?', '') + $$, + 'Should have added all two service' +); + + +select * +from finish(); + +rollback; diff --git a/test/add_services_carousel_slide.sql b/test/add_services_carousel_slide.sql new file mode 100644 index 0000000..f35cf94 --- /dev/null +++ b/test/add_services_carousel_slide.sql @@ -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', '') + , (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; diff --git a/test/edit_service.sql b/test/edit_service.sql new file mode 100644 index 0000000..8d52d74 --- /dev/null +++ b/test/edit_service.sql @@ -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', '

A

') + , (6, 1, 'toilet', 'Service B', '

B

') +; + +select lives_ok( + $$ select edit_service(5, 'wifi', 'Service 1', '

1

') $$, + 'Should be able to edit the first service' +); + +select lives_ok( + $$ select edit_service(6, 'baby', 'Service 2', '

2

') $$, + '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', '

1

') + , (6, 'baby', 'Service 2', '

2

') + $$, + 'Should have updated all services.' +); + +select * +from finish(); + +rollback; diff --git a/test/icon.sql b/test/icon.sql new file mode 100644 index 0000000..0f2b0fc --- /dev/null +++ b/test/icon.sql @@ -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; + diff --git a/test/remove_services_carousel_slide.sql b/test/remove_services_carousel_slide.sql new file mode 100644 index 0000000..1c7b2f5 --- /dev/null +++ b/test/remove_services_carousel_slide.sql @@ -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', '') + , (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 slide’s translations' +); + + +select * +from finish(); + +rollback; diff --git a/test/service.sql b/test/service.sql new file mode 100644 index 0000000..f109113 --- /dev/null +++ b/test/service.sql @@ -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; + diff --git a/test/service_i18n.sql b/test/service_i18n.sql new file mode 100644 index 0000000..e37582a --- /dev/null +++ b/test/service_i18n.sql @@ -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; + diff --git a/test/services_carousel.sql b/test/services_carousel.sql new file mode 100644 index 0000000..ec02bba --- /dev/null +++ b/test/services_carousel.sql @@ -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; + diff --git a/test/services_carousel_i18n.sql b/test/services_carousel_i18n.sql new file mode 100644 index 0000000..8503b52 --- /dev/null +++ b/test/services_carousel_i18n.sql @@ -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; + diff --git a/test/translate_service.sql b/test/translate_service.sql new file mode 100644 index 0000000..8abf2cb --- /dev/null +++ b/test/translate_service.sql @@ -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', '

A

') + , (6, 1, 'toilet', 'Service B', '

B

') +; + +insert into service_i18n (service_id, lang_tag, name, description) +values (6, 'ca', 'serveib', '

B

') +; + +select lives_ok( + $$ select translate_service(5, 'ca', 'Servei A', '

a

') $$, + 'Should be able to translate the first service' +); + +select lives_ok( + $$ select translate_service(6, 'es', 'Servicio B', '

b

') $$, + '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', '

a

') + , (6, 'ca', 'Servei B', '') + , (6, 'es', 'Servicio B', '

b

') + $$, + 'Should have added and updated all translations.' +); + + +select * +from finish(); + +rollback; diff --git a/test/translate_services_carousel_slide.sql b/test/translate_services_carousel_slide.sql new file mode 100644 index 0000000..73d1e31 --- /dev/null +++ b/test/translate_services_carousel_slide.sql @@ -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', '') + , (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 slide’s 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; diff --git a/verify/add_service.sql b/verify/add_service.sql new file mode 100644 index 0000000..5981e8d --- /dev/null +++ b/verify/add_service.sql @@ -0,0 +1,7 @@ +-- Verify camper:add_service on pg + +begin; + +select has_function_privilege('camper.add_service(integer, text, text, text)', 'execute'); + +rollback; diff --git a/verify/add_services_carousel_slide.sql b/verify/add_services_carousel_slide.sql new file mode 100644 index 0000000..4625053 --- /dev/null +++ b/verify/add_services_carousel_slide.sql @@ -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; diff --git a/verify/available_icons.sql b/verify/available_icons.sql new file mode 100644 index 0000000..3b3b5bc --- /dev/null +++ b/verify/available_icons.sql @@ -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; diff --git a/verify/edit_service.sql b/verify/edit_service.sql new file mode 100644 index 0000000..4c60aea --- /dev/null +++ b/verify/edit_service.sql @@ -0,0 +1,7 @@ +-- Verify camper:edit_service on pg + +begin; + +select has_function_privilege('camper.edit_service(integer, text, text, text)', 'execute'); + +rollback; diff --git a/verify/icon.sql b/verify/icon.sql new file mode 100644 index 0000000..85bdc4c --- /dev/null +++ b/verify/icon.sql @@ -0,0 +1,9 @@ +-- Verify camper:icon on pg + +begin; + +select icon_name +from camper.icon +where false; + +rollback; diff --git a/verify/remove_services_carousel_slide.sql b/verify/remove_services_carousel_slide.sql new file mode 100644 index 0000000..e7e4063 --- /dev/null +++ b/verify/remove_services_carousel_slide.sql @@ -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; diff --git a/verify/service.sql b/verify/service.sql new file mode 100644 index 0000000..57f9a32 --- /dev/null +++ b/verify/service.sql @@ -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; diff --git a/verify/service_i18n.sql b/verify/service_i18n.sql new file mode 100644 index 0000000..cb2289a --- /dev/null +++ b/verify/service_i18n.sql @@ -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; diff --git a/verify/services_carousel.sql b/verify/services_carousel.sql new file mode 100644 index 0000000..4fe85fa --- /dev/null +++ b/verify/services_carousel.sql @@ -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; diff --git a/verify/services_carousel_i18n.sql b/verify/services_carousel_i18n.sql new file mode 100644 index 0000000..bc3e78c --- /dev/null +++ b/verify/services_carousel_i18n.sql @@ -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; diff --git a/verify/translate_service.sql b/verify/translate_service.sql new file mode 100644 index 0000000..96aebd5 --- /dev/null +++ b/verify/translate_service.sql @@ -0,0 +1,7 @@ +-- Verify camper:translate_service on pg + +begin; + +select has_function_privilege('camper.translate_service(integer, text, text, text)', 'execute'); + +rollback; diff --git a/verify/translate_services_carousel_slide.sql b/verify/translate_services_carousel_slide.sql new file mode 100644 index 0000000..14120da --- /dev/null +++ b/verify/translate_services_carousel_slide.sql @@ -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; diff --git a/web/static/public.css b/web/static/public.css index 2e221d7..35c52a0 100644 --- a/web/static/public.css +++ b/web/static/public.css @@ -135,6 +135,10 @@ p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; } +p + p { + margin-top: 1.5em; +} + h2 { font-size: 4.2rem; font-weight: 400; @@ -301,16 +305,16 @@ nav .has-submenu:hover ul, nav .has-submenu:focus-within ul { #menuShowHide:checked ~ nav { display: block; } - + #menuShowHide:checked ~ label[for="menuShowHide"]::before { content: "✕"; } - + nav ul { flex-direction: column; align-items: start; } - + nav .has-submenu ul { display: flex; position: static; @@ -432,28 +436,33 @@ dl, .nature div + div, .outside_activities > div { background-color: var(--accent); } -.surroundings .spiel { +.carousel { + display: none; +} + +.carousel .spiel { font-size: 2.4rem; padding-right: 4rem; } -.surroundings .spiel p { +.carousel .spiel p { + margin-top: 0; margin-bottom: 2rem; } -.surroundings figure, .surroundings .slick-track > img { +.carousel figure, .carousel .slick-track > img { margin-right: 5rem; position: relative; } -.surroundings img { +.carousel img { height: 40rem; width: 100%; border-radius: 5px; object-fit: cover; } -.surroundings figcaption { +.carousel figcaption { padding: 10px 15px; background: var(--clar); width: fit-content; @@ -465,23 +474,23 @@ dl, .nature div + div, .outside_activities > div { font-size: 1.7rem; } -.surroundings .slick-list { +.carousel .slick-list { order: 1; padding: 0 20% 0 0 !important; } -.surroundings .slick-track { +.carousel .slick-track { display: flex; align-items: start; } -.surroundings .slick-slider { +.carousel.slick-slider { display: flex; flex-wrap: wrap; justify-content: end; } -.surroundings .slick-arrow { +.carousel .slick-arrow { font-size: 6rem; line-height: 1; width: 5rem; @@ -492,25 +501,25 @@ dl, .nature div + div, .outside_activities > div { 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; } -.surroundings .slick-prev { +.carousel .slick-prev { order: 2; margin: 2.5rem 4rem 0 0; } -.surroundings .slick-prev:hover { +.carousel .slick-prev:hover { transform: translateX(-1.3rem); } -.surroundings .slick-next { +.carousel .slick-next { order: 3; margin: 2.5rem 7rem 0 0; } -.surroundings .slick-next:hover { +.carousel .slick-next:hover { transform: translateX(1.3rem); } @@ -526,7 +535,7 @@ dl { } dl div { - flex: 1; + flex-basis: calc(25% - 5rem + 5rem / 4); } dt { @@ -544,32 +553,103 @@ dt { dl { 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 { - 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 { - 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_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="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 { - 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_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="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 { 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 { margin-top: 2rem; } -.outside_activities h3 { +.outside_activities h3, .campsite_services .spiel { font-size: calc(2.2rem + 4vw); font-weight: 600; line-height: .9em; +} + +.outside_activities h3 { margin-bottom: 10rem; } @@ -628,10 +708,6 @@ dt { width: 20%; } -.outside_activities > div:last-child p { - margin-bottom: 1.5em; -} - .campsite_activities { margin-top: 10rem; padding-top: 10rem; @@ -645,6 +721,10 @@ dt { margin-bottom: 5rem; } +.campsite_services.carousel .slick-track { + align-items: center; +} + footer { font-size: 1.5rem; text-align: center; diff --git a/web/templates/admin/layout.gohtml b/web/templates/admin/layout.gohtml index cf850e0..b234ef9 100644 --- a/web/templates/admin/layout.gohtml +++ b/web/templates/admin/layout.gohtml @@ -75,6 +75,9 @@
  • {{( pgettext "Home Page" "title" )}}
  • +
  • + {{( pgettext "Services Page" "title" )}} +
  • {{- end }} diff --git a/web/templates/admin/services/carousel/form.gohtml b/web/templates/admin/services/carousel/form.gohtml new file mode 100644 index 0000000..5e12cc4 --- /dev/null +++ b/web/templates/admin/services/carousel/form.gohtml @@ -0,0 +1,65 @@ + +{{ 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" }} +
    +

    + {{ if .ID }} + {{( pgettext "Edit Carousel Slide" "title" )}} + {{ else }} + {{( pgettext "New Carousel Slide" "title" )}} + {{ end }} +

    + {{ CSRFInput }} +
    + {{ with .Media -}} + {{ if .Val -}} + + {{- end }} + + {{ template "error-message" . }} + {{- end }} + {{ with .Caption -}} + + {{ template "error-message" . }} + {{- end }} +
    +
    + +
    +
    +{{- end }} diff --git a/web/templates/admin/services/carousel/l10n.gohtml b/web/templates/admin/services/carousel/l10n.gohtml new file mode 100644 index 0000000..6a6c385 --- /dev/null +++ b/web/templates/admin/services/carousel/l10n.gohtml @@ -0,0 +1,36 @@ + +{{ 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" }} +
    +

    + {{printf (pgettext "Translate Carousel Slide to %s" "title") .Locale.Endonym }} +

    + {{ CSRFInput }} +
    + {{ with .Caption -}} +
    + {{( pgettext "Caption" "input")}} + {{( gettext "Source:" )}} {{ .Source }}
    + + {{ template "error-message" . }} +
    + {{- end }} +
    +
    + +
    +
    +{{- end }} diff --git a/web/templates/admin/services/index.gohtml b/web/templates/admin/services/index.gohtml new file mode 100644 index 0000000..e4c621a --- /dev/null +++ b/web/templates/admin/services/index.gohtml @@ -0,0 +1,50 @@ + +{{ define "title" -}} + {{( pgettext "Home Page" "title" )}} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/services.servicesIndex*/ -}} + {{ template "settings-tabs" "services" }} +

    {{( pgettext "Carousel" "title" )}}

    + {{( pgettext "Add slide" "action" )}} + {{ if .Slides -}} + + + + + + + + + + + {{ range $slide := .Slides -}} + + + + + + + {{- end }} + +
    {{( pgettext "Image" "header" )}}{{( pgettext "Caption" "header" )}}{{( pgettext "Translations" "campsite type" )}}{{( pgettext "Actions" "campsite type" )}}
    {{ .Caption }} + {{ range .Translations }} + {{ .Endonym }} + {{ end }} + +
    + +
    +
    + {{ else -}} +

    {{( gettext "No slides added yet." )}}

    + {{- end }} +{{- end }} diff --git a/web/templates/public/home.gohtml b/web/templates/public/home.gohtml index db618a8..3c6b852 100644 --- a/web/templates/public/home.gohtml +++ b/web/templates/public/home.gohtml @@ -7,11 +7,11 @@ {{- end }} {{ define "head" -}} - + {{ template "carouselStyle" }} {{- end }} {{ define "content" -}} - {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/app.homePage*/ -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/home.homePage*/ -}}

    {{ (gettext "The pleasure of camping in the middle of nature…")}}

    @@ -31,8 +31,8 @@

    {{( gettext "Our services")}}

    -

    {{ (gettext "Surroundings")}}

    -
    +

    {{( pgettext "Surroundings" "title" )}}

    +

    {{( gettext "Come and enjoy!")}}

    - - - + {{ template "carouselInit" }} {{- end }} diff --git a/web/templates/public/layout.gohtml b/web/templates/public/layout.gohtml index 826931d..6e20c03 100644 --- a/web/templates/public/layout.gohtml +++ b/web/templates/public/layout.gohtml @@ -62,3 +62,32 @@ {{ define "alternateAnchor" -}} {{ .Endonym }} {{- end }} + +{{ define "carouselStyle" -}} + +{{- end }} + +{{ define "carouselInit" -}} + + + +{{- end }} diff --git a/web/templates/public/services.gohtml b/web/templates/public/services.gohtml new file mode 100644 index 0000000..a9fe6b5 --- /dev/null +++ b/web/templates/public/services.gohtml @@ -0,0 +1,42 @@ + +{{ define "title" -}} + {{( pgettext "Services" "title" )}} +{{- end }} + +{{ define "head" -}} + {{ template "carouselStyle" }} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/services.servicesPage*/ -}} +

    {{( pgettext "Services" "title" )}}

    + + + {{ template "carouselInit" }} + +
    + {{ range .Services -}} +
    +
    {{ .Name }}
    +
    {{ .Description | raw }}
    +
    + {{- end }} +
    +{{- end }}