From 9e4158fecb8635324424df52cafdca0d0f82f204 Mon Sep 17 00:00:00 2001 From: Paul Robert Lloyd Date: Wed, 10 Jan 2024 16:31:15 +0000 Subject: [PATCH] feat(endpoint-posts): event post fields --- packages/endpoint-posts/index.js | 1 + .../endpoint-posts/lib/controllers/form.js | 7 +- .../lib/middleware/post-data.js | 8 +- .../lib/middleware/validation.js | 7 +- packages/endpoint-posts/lib/utils.js | 52 +++++++- packages/endpoint-posts/locales/de.json | 17 +++ packages/endpoint-posts/locales/en.json | 17 +++ packages/endpoint-posts/locales/es-419.json | 17 +++ packages/endpoint-posts/locales/es.json | 17 +++ packages/endpoint-posts/locales/fr.json | 17 +++ packages/endpoint-posts/locales/id.json | 17 +++ packages/endpoint-posts/locales/nl.json | 17 +++ packages/endpoint-posts/locales/pl.json | 17 +++ packages/endpoint-posts/locales/pt.json | 17 +++ packages/endpoint-posts/locales/sr.json | 17 +++ .../endpoint-posts/locales/zh-Hans-CN.json | 17 +++ packages/endpoint-posts/test/unit/utils.js | 123 +++++++++++++++++- packages/endpoint-posts/views/post-form.njk | 81 +++++++++++- 18 files changed, 445 insertions(+), 21 deletions(-) diff --git a/packages/endpoint-posts/index.js b/packages/endpoint-posts/index.js index b967c06e1..bee2b9685 100644 --- a/packages/endpoint-posts/index.js +++ b/packages/endpoint-posts/index.js @@ -59,6 +59,7 @@ export default class PostsEndpoint { "reply", "like", "photo", + "event", "rsvp", "repost", "video", diff --git a/packages/endpoint-posts/lib/controllers/form.js b/packages/endpoint-posts/lib/controllers/form.js index a2ebe2804..4bfe32305 100644 --- a/packages/endpoint-posts/lib/controllers/form.js +++ b/packages/endpoint-posts/lib/controllers/form.js @@ -92,10 +92,9 @@ export const formController = { } } - // Convert geo value to object - if (values.geo) { - values.location = getLocationProperty(values.geo); - delete values.geo; + // Derive location from location and/or geo values + if (values.location || values.geo) { + values.location = getLocationProperty(values); } // Delete empty values diff --git a/packages/endpoint-posts/lib/middleware/post-data.js b/packages/endpoint-posts/lib/middleware/post-data.js index 33f34b76d..a5a34c3de 100644 --- a/packages/endpoint-posts/lib/middleware/post-data.js +++ b/packages/endpoint-posts/lib/middleware/post-data.js @@ -2,6 +2,7 @@ import path from "node:path"; import { IndiekitError } from "@indiekit/error"; import { statusTypes } from "../status-types.js"; import { + getGeoValue, getPostName, getPostProperties, getPostTypeName, @@ -56,11 +57,8 @@ export const postData = { throw IndiekitError.notFound(response.locals.__("NotFoundError.page")); } - if (properties.location) { - properties.geo = [ - properties.location.latitude, - properties.location.longitude, - ].toString(); + if (properties.location.geo) { + properties.geo = getGeoValue(properties.geo); } const postType = properties["post-type"]; diff --git a/packages/endpoint-posts/lib/middleware/validation.js b/packages/endpoint-posts/lib/middleware/validation.js index 660027f4c..74458adea 100644 --- a/packages/endpoint-posts/lib/middleware/validation.js +++ b/packages/endpoint-posts/lib/middleware/validation.js @@ -44,10 +44,15 @@ export const validate = [ .if( (value, { req }) => req.body?.["post-type"] === "article" || - req.body?.["post-type"] === "bookmark", + req.body?.["post-type"] === "bookmark" || + req.body?.["post-type"] === "event", ) .notEmpty() .withMessage((value, { req, path }) => req.__(`posts.error.${path}.empty`)), + check("start") + .if((value, { req }) => req.body?.["post-type"] === "event") + .notEmpty() + .withMessage((value, { req, path }) => req.__(`posts.error.${path}.empty`)), check("content") .if( (value, { req }) => diff --git a/packages/endpoint-posts/lib/utils.js b/packages/endpoint-posts/lib/utils.js index 73e7fec06..8f7a16825 100644 --- a/packages/endpoint-posts/lib/utils.js +++ b/packages/endpoint-posts/lib/utils.js @@ -8,23 +8,65 @@ export const LAT_LONG_RE = /^(?(?:-?|\+?)?\d+(?:\.\d+)?),\s*(?(?:-?|\+?)?\d+(?:\.\d+)?)$/; /** - * Get location property + * Get geographic coordinates property * @param {string} geo - Latitude and longitude, comma separated - * @returns {object} JF2 location property + * @returns {object} JF2 geo location property */ -export const getLocationProperty = (geo) => { +export const getGeoProperty = (geo) => { const { latitude, longitude } = geo.match(LAT_LONG_RE).groups; return { type: "geo", - latitude, - longitude, name: formatcoords(geo).format({ decimalPlaces: 2, }), + latitude: Number(latitude), + longitude: Number(longitude), }; }; +/** + * Get comma separated geographic coordinates + * @param {object} geo - JF2 geo location property + * @returns {string} Latitude and longitude, comma separated + */ +export const getGeoValue = (geo) => { + return [geo.latitude, geo.longitude].toString(); +}; + +/** + * Get location property + * @param {object} values - Latitude and longitude, comma separated + * @returns {object} JF2 location property + */ +export const getLocationProperty = (values) => { + const { location, geo } = values; + + // Determine Microformat type + if (location && location.name) { + location.type = "card"; + } else if (location) { + location.type = "adr"; + } + + // Add (or use) any provided geo location properties + if (location && geo) { + location.geo = getGeoProperty(geo); + } else if (geo) { + return getGeoProperty(geo); + } + + // Delete empty values + for (const key in location) { + const noValue = !location[key] || location[key] === ""; + if (Object.prototype.hasOwnProperty.call(location, key) && noValue) { + delete location[key]; + } + } + + return location; +}; + /** * Get post status badges * @param {object} post - Post diff --git a/packages/endpoint-posts/locales/de.json b/packages/endpoint-posts/locales/de.json index 635538ff5..d7cded0bc 100644 --- a/packages/endpoint-posts/locales/de.json +++ b/packages/endpoint-posts/locales/de.json @@ -26,6 +26,9 @@ "name": { "empty": "Titel eingeben" }, + "start": { + "empty": "Geben Sie ein Startdatum ein" + }, "url": { "empty": "Geben Sie eine Webadresse wie %s" } @@ -50,6 +53,12 @@ "label": "Inhalt" }, "continue": "Fortsetzen", + "event": { + "all-day": "Ganztägig", + "end": "Endet", + "label": "Datum und Uhrzeit", + "start": "Beginnt" + }, "geo": { "hint": "Breitengrad und Längengrad, zum Beispiel %s", "label": "Koordinaten des Standorts" @@ -60,6 +69,14 @@ "like-of": { "label": "Wie von" }, + "location": { + "country-name": "Land", + "label": "Standort", + "locality": "Stadt oder Ort", + "name": "Veranstaltungsort", + "postal-code": "Postleitzahl", + "street-address": "Straße" + }, "media": { "label": "Dateipfad oder URL" }, diff --git a/packages/endpoint-posts/locales/en.json b/packages/endpoint-posts/locales/en.json index 41f9f8ebf..96445df69 100644 --- a/packages/endpoint-posts/locales/en.json +++ b/packages/endpoint-posts/locales/en.json @@ -13,6 +13,9 @@ "name": { "empty": "Enter a title" }, + "start": { + "empty": "Enter a start date" + }, "url": { "empty": "Enter a web address like %s" }, @@ -75,6 +78,12 @@ "content": { "label": "Content" }, + "event": { + "label": "Date and time", + "start": "Starts", + "end": "Ends", + "all-day": "All-day" + }, "geo": { "label": "Location coordinates", "hint": "Latitude and longitude, for example %s" @@ -85,6 +94,14 @@ "like-of": { "label": "Like of" }, + "location": { + "label": "Location", + "name": "Venue", + "street-address": "Street address", + "locality": "City or town", + "country-name": "Country", + "postal-code": "Postal code" + }, "name": { "label": "Title" }, diff --git a/packages/endpoint-posts/locales/es-419.json b/packages/endpoint-posts/locales/es-419.json index 6be520c15..9d3ef59aa 100644 --- a/packages/endpoint-posts/locales/es-419.json +++ b/packages/endpoint-posts/locales/es-419.json @@ -26,6 +26,9 @@ "name": { "empty": "Ingresar un título" }, + "start": { + "empty": "Introduce una fecha de inicio" + }, "url": { "empty": "Introducir una dirección web como %s" } @@ -50,6 +53,12 @@ "label": "Contenido" }, "continue": "Continuar", + "event": { + "all-day": "Todo el día", + "end": "Termina", + "label": "Fecha y hora", + "start": "Comienza" + }, "geo": { "hint": "Latitud y longitud, por ejemplo %s", "label": "Coordenadas de ubicación" @@ -60,6 +69,14 @@ "like-of": { "label": "Como de" }, + "location": { + "country-name": "País", + "label": "Ubicación", + "locality": "Ciudad o pueblo", + "name": "Lugar de eventos", + "postal-code": "Código postal", + "street-address": "Dirección de la calle" + }, "media": { "label": "Ruta o URL del archivo" }, diff --git a/packages/endpoint-posts/locales/es.json b/packages/endpoint-posts/locales/es.json index fea2ebb54..711564326 100644 --- a/packages/endpoint-posts/locales/es.json +++ b/packages/endpoint-posts/locales/es.json @@ -26,6 +26,9 @@ "name": { "empty": "Pon un título" }, + "start": { + "empty": "Introduce una fecha de inicio" + }, "url": { "empty": "Introduce una dirección web como %s" } @@ -50,6 +53,12 @@ "label": "Contenido" }, "continue": "Continuar", + "event": { + "all-day": "Todo el día", + "end": "Termina", + "label": "Fecha y hora", + "start": "Comienza" + }, "geo": { "hint": "Latitud y longitud, por ejemplo %s", "label": "Coordenadas de ubicación" @@ -60,6 +69,14 @@ "like-of": { "label": "Como de" }, + "location": { + "country-name": "País", + "label": "Ubicación", + "locality": "Ciudad o pueblo", + "name": "Campo", + "postal-code": "Código postal", + "street-address": "Dirección" + }, "media": { "label": "Ruta o URL del archivo" }, diff --git a/packages/endpoint-posts/locales/fr.json b/packages/endpoint-posts/locales/fr.json index e470425c4..1dd7f0594 100644 --- a/packages/endpoint-posts/locales/fr.json +++ b/packages/endpoint-posts/locales/fr.json @@ -26,6 +26,9 @@ "name": { "empty": "Saisir un titre" }, + "start": { + "empty": "Entrez la date de début" + }, "url": { "empty": "Saisir une adresse Web telle que %s" } @@ -50,6 +53,12 @@ "label": "Contenu" }, "continue": "Continuer", + "event": { + "all-day": "Toute la journée", + "end": "Fin", + "label": "Date et heure", + "start": "Commence" + }, "geo": { "hint": "Latitude et longitude, par exemple %s", "label": "Coordonnées de localisation" @@ -60,6 +69,14 @@ "like-of": { "label": "J’aime" }, + "location": { + "country-name": "Pays", + "label": "Localisation", + "locality": "Ville", + "name": "Lieu", + "postal-code": "Code postal", + "street-address": "Adresse" + }, "media": { "label": "Chemin du fichier ou URL" }, diff --git a/packages/endpoint-posts/locales/id.json b/packages/endpoint-posts/locales/id.json index b17ecc85f..c9147cb06 100644 --- a/packages/endpoint-posts/locales/id.json +++ b/packages/endpoint-posts/locales/id.json @@ -26,6 +26,9 @@ "name": { "empty": "Masukkan judul" }, + "start": { + "empty": "Masukkan tanggal mulai" + }, "url": { "empty": "Masukkan alamat web misalnya %s" } @@ -50,6 +53,12 @@ "label": "Konten" }, "continue": "Lanjutkan", + "event": { + "all-day": "Sepanjang hari", + "end": "Berakhir", + "label": "Tanggal dan waktu", + "start": "Mulai" + }, "geo": { "hint": "Lintang dan bujur, misalnya %s", "label": "Koordinat lokasi" @@ -60,6 +69,14 @@ "like-of": { "label": "Seperti" }, + "location": { + "country-name": "Negara", + "label": "Lokasi", + "locality": "Kota", + "name": "Tempat", + "postal-code": "Kode pos", + "street-address": "Alamat jalan" + }, "media": { "label": "Jalur file atau URL" }, diff --git a/packages/endpoint-posts/locales/nl.json b/packages/endpoint-posts/locales/nl.json index bf8703ee4..079a376d5 100644 --- a/packages/endpoint-posts/locales/nl.json +++ b/packages/endpoint-posts/locales/nl.json @@ -26,6 +26,9 @@ "name": { "empty": "Geef een titel in" }, + "start": { + "empty": "Voer een startdatum in" + }, "url": { "empty": "Voer een webadres in zoals %s" } @@ -50,6 +53,12 @@ "label": "Inhoud" }, "continue": "Doorgaan", + "event": { + "all-day": "Hele dag", + "end": "Eindigt", + "label": "Datum en tijd", + "start": "Begint" + }, "geo": { "hint": "Lengte- en breedtegraad, bijvoorbeeld %s", "label": "Locatiecoördinaten" @@ -60,6 +69,14 @@ "like-of": { "label": "Zoals van" }, + "location": { + "country-name": "Land", + "label": "Locatie", + "locality": "Stad", + "name": "Plaats", + "postal-code": "Postcode", + "street-address": "Adres" + }, "media": { "label": "Bestandspad of URL" }, diff --git a/packages/endpoint-posts/locales/pl.json b/packages/endpoint-posts/locales/pl.json index afb38fa79..84ae183ae 100644 --- a/packages/endpoint-posts/locales/pl.json +++ b/packages/endpoint-posts/locales/pl.json @@ -26,6 +26,9 @@ "name": { "empty": "Wprowadź tytuł" }, + "start": { + "empty": "Wpisz datę rozpoczęcia" + }, "url": { "empty": "Wpisz adres internetowy, taki jak %s" } @@ -50,6 +53,12 @@ "label": "Zawartość" }, "continue": "Kontynuuj", + "event": { + "all-day": "Cały dzień", + "end": "Kończy się", + "label": "Data i godzina", + "start": "Rozpoczyna się" + }, "geo": { "hint": "Szerokość i długość geograficzna, na przykład %s", "label": "Współrzędne lokalizacji" @@ -60,6 +69,14 @@ "like-of": { "label": "Jak z" }, + "location": { + "country-name": "Kraj", + "label": "Lokalizacja", + "locality": "Miasto", + "name": "Miejsce", + "postal-code": "Kod pocztowy", + "street-address": "Adres" + }, "media": { "label": "Ścieżka pliku lub adres URL" }, diff --git a/packages/endpoint-posts/locales/pt.json b/packages/endpoint-posts/locales/pt.json index ae436f9f1..4da24d680 100644 --- a/packages/endpoint-posts/locales/pt.json +++ b/packages/endpoint-posts/locales/pt.json @@ -26,6 +26,9 @@ "name": { "empty": "Introduza um título" }, + "start": { + "empty": "Insira uma data de início" + }, "url": { "empty": "Insira um endereço da web como %s" } @@ -50,6 +53,12 @@ "label": "Conteúdo" }, "continue": "Continuar", + "event": { + "all-day": "Todo o dia", + "end": "Termina", + "label": "Data e hora", + "start": "Começa" + }, "geo": { "hint": "Latitude e longitude, por exemplo %s", "label": "Coordenadas de localização" @@ -60,6 +69,14 @@ "like-of": { "label": "Como de" }, + "location": { + "country-name": "País", + "label": "Localização", + "locality": "Cidade ou vila", + "name": "Local", + "postal-code": "Código postal", + "street-address": "Endereço" + }, "media": { "label": "Caminho do arquivo ou URL" }, diff --git a/packages/endpoint-posts/locales/sr.json b/packages/endpoint-posts/locales/sr.json index ef8524de0..6400ac766 100644 --- a/packages/endpoint-posts/locales/sr.json +++ b/packages/endpoint-posts/locales/sr.json @@ -26,6 +26,9 @@ "name": { "empty": "Unesite naslov" }, + "start": { + "empty": "Unesite datum početka" + }, "url": { "empty": "Unesite veb adresu kao što je %s" } @@ -50,6 +53,12 @@ "label": "Sadržaj" }, "continue": "Nastavi", + "event": { + "all-day": "Ceo dan", + "end": "Krajevi", + "label": "Datum i vreme", + "start": "Počinje" + }, "geo": { "hint": "Geografska širina i dužina, na primer %s", "label": "Koordinate lokacije" @@ -60,6 +69,14 @@ "like-of": { "label": "Kao od" }, + "location": { + "country-name": "Zemlja", + "label": "Lokacija", + "locality": "Grad", + "name": "Mesto održavanja", + "postal-code": "Poštanski kod", + "street-address": "Ulica i broj" + }, "media": { "label": "Putanja ili URL datoteke" }, diff --git a/packages/endpoint-posts/locales/zh-Hans-CN.json b/packages/endpoint-posts/locales/zh-Hans-CN.json index 33dd62287..ee872247c 100644 --- a/packages/endpoint-posts/locales/zh-Hans-CN.json +++ b/packages/endpoint-posts/locales/zh-Hans-CN.json @@ -26,6 +26,9 @@ "name": { "empty": "输入标题" }, + "start": { + "empty": "输入开始日期" + }, "url": { "empty": "输入像 %s 这样的网址" } @@ -50,6 +53,12 @@ "label": "内容" }, "continue": "继续", + "event": { + "all-day": "全天", + "end": "结束", + "label": "日期和时间", + "start": "开始" + }, "geo": { "hint": "纬度和经度,例如 %s", "label": "位置坐标" @@ -60,6 +69,14 @@ "like-of": { "label": "像这样" }, + "location": { + "country-name": "国家", + "label": "地点", + "locality": "城市", + "name": "举办地点", + "postal-code": "邮政编码", + "street-address": "街道地址" + }, "media": { "label": "文件路径或 URL" }, diff --git a/packages/endpoint-posts/test/unit/utils.js b/packages/endpoint-posts/test/unit/utils.js index 368a6d648..f428ce584 100644 --- a/packages/endpoint-posts/test/unit/utils.js +++ b/packages/endpoint-posts/test/unit/utils.js @@ -2,6 +2,8 @@ import { strict as assert } from "node:assert"; import { describe, it } from "node:test"; import { mockResponse } from "mock-req-res"; import { + getGeoProperty, + getGeoValue, getLocationProperty, getPostName, getPostStatusBadges, @@ -33,12 +35,123 @@ const publication = { }; describe("endpoint-posts/lib/utils", () => { - it("Gets location property", () => { - assert.deepEqual(getLocationProperty("12.3456, -65.4321"), { + it("Gets geographic coordinates property", () => { + assert.deepEqual(getGeoProperty("50.8252, -0.1383"), { type: "geo", - latitude: "12.3456", - longitude: "-65.4321", - name: "12° 20′ 44.16″ N 65° 25′ 55.56″ W", + name: "50° 49′ 30.72″ N 0° 8′ 17.88″ W", + latitude: 50.8252, + longitude: -0.1383, + }); + }); + + it("Gets comma separated geographic coordinates", () => { + assert.equal( + getGeoValue({ + type: "geo", + name: "50° 49′ 30.72″ N 0° 8′ 17.88″ W", + latitude: 50.8252, + longitude: -0.1383, + }), + "50.8252,-0.1383", + ); + }); + + it("Gets location property", async (t) => { + await t.test("with address", () => { + assert.deepEqual( + getLocationProperty({ + location: { + "street-address": "Jubilee Street", + locality: "Brighton", + "postal-code": "BN1 1GE", + }, + }), + { + type: "adr", + "street-address": "Jubilee Street", + locality: "Brighton", + "postal-code": "BN1 1GE", + }, + ); + }); + + await t.test("without empty values", () => { + assert.deepEqual( + getLocationProperty({ + location: { + name: "Brighton Jubilee Library", + "street-address": "", + locality: "Brighton", + "postal-code": "", + }, + }), + { + type: "card", + name: "Brighton Jubilee Library", + locality: "Brighton", + }, + ); + }); + + await t.test("with venue", () => { + assert.deepEqual( + getLocationProperty({ + location: { + name: "Brighton Jubilee Library", + "street-address": "Jubilee Street", + locality: "Brighton", + "postal-code": "BN1 1GE", + }, + }), + { + type: "card", + name: "Brighton Jubilee Library", + "street-address": "Jubilee Street", + locality: "Brighton", + "postal-code": "BN1 1GE", + }, + ); + }); + + await t.test("with geographic coordinates", () => { + assert.deepEqual( + getLocationProperty({ + geo: "50.8252, -0.1383", + }), + { + type: "geo", + name: "50° 49′ 30.72″ N 0° 8′ 17.88″ W", + latitude: 50.8252, + longitude: -0.1383, + }, + ); + }); + + await t.test("with venue and geographic coordinates", () => { + assert.deepEqual( + getLocationProperty({ + geo: "50.8252, -0.1383", + location: { + name: "Brighton Jubilee Library", + "street-address": "Jubilee Street", + locality: "Brighton", + "postal-code": "BN1 1GE", + }, + }), + { + type: "card", + name: "Brighton Jubilee Library", + "street-address": "Jubilee Street", + locality: "Brighton", + "postal-code": "BN1 1GE", + geo: { + type: "geo", + name: "50° 49′ 30.72″ N 0° 8′ 17.88″ W", + latitude: 50.8252, + longitude: -0.1383, + }, + }, + ); }); }); diff --git a/packages/endpoint-posts/views/post-form.njk b/packages/endpoint-posts/views/post-form.njk index 65f00994a..3a6e68ac3 100644 --- a/packages/endpoint-posts/views/post-form.njk +++ b/packages/endpoint-posts/views/post-form.njk @@ -9,7 +9,8 @@ %} {% set nameOptional = postType !== "article" and - postType !== "bookmark" %} + postType !== "bookmark" and + postType !== "event" %} {% set nameIgnored = postType === "note" or postType === "like" or @@ -155,6 +156,84 @@ errorMessage: fieldData("name").errorMessage }) | indent(2) if not nameIgnored }} +{% if postType === "event" %} +{% call fieldset({ + classes: "fieldset--group", + legend: __("posts.form.location.label"), + optional: true +}) %} + {{ input({ + name: "location[name]", + value: fieldData("location.name").value, + label: __("posts.form.location.name") + }) | indent(2) if not nameIgnored }} + + {{ input({ + name: "location[street-address]", + value: fieldData("location.street-address").value, + label: __("posts.form.location.street-address"), + autocomplete: "address-line1" + }) | indent(2) }} + + {{ input({ + classes: "input--width-25", + name: "location[locality]", + value: fieldData("location.locality").value, + label: __("posts.form.location.locality"), + autocomplete: "address-level2" + }) | indent(2) }} + + {{ input({ + classes: "input--width-25", + name: "location[country-name]", + value: fieldData("location.country-name").value, + label: __("posts.form.location.country-name"), + autocomplete: "country-name" + }) | indent(2) }} + + {{ input({ + classes: "input--width-10", + name: "location[postal-code]", + value: fieldData("location.postal-code").value, + label: __("posts.form.location.postal-code"), + autocomplete: "postal-code" + }) | indent(2) }} +{% endcall %} + +{% call fieldset({ + classes: "fieldset--group fieldset--inline", + legend: __("posts.form.event.label") +}) %} + + {{ input({ + name: "start", + type: "datetime-local", + value: fieldData("start").value, + label: __("posts.form.event.start"), + errorMessage: fieldData("start").errorMessage + }) | indent(2) }} + + {{ input({ + name: "end", + type: "datetime-local", + value: fieldData("end").value, + label: __("posts.form.event.end"), + optional: true, + errorMessage: fieldData("end").errorMessage + }) | indent(2) }} + + {{ checkboxes({ + name: "all-day", + values: fieldData("all-day").value, + items: [{ + label: __("posts.form.event.all-day"), + value: true + }] + }) | indent(2) }} + +{% endcall %} +{% endif %} + {{ textarea({ name: "content", value: properties.content.text or fieldData("content").value,