diff --git a/ui/all-translations-het-archief.json b/ui/all-translations-het-archief.json index 87537bac2..55a1ce444 100644 --- a/ui/all-translations-het-archief.json +++ b/ui/all-translations-het-archief.json @@ -17859,6 +17859,386 @@ "value": "Achtergrondkleur tekstvak is verplicht", "value_type": "TEXT" }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "driekeuzespeler", + "language": "nl", + "value": "Driekeuzespeler", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "driekeuzespeler", + "language": "en", + "value": "Three-choice player", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "titel", + "language": "nl", + "value": "Titel", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "titel", + "language": "en", + "value": "Title", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "titel-is-verplicht", + "language": "nl", + "value": "Titel is verplicht", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "titel-is-verplicht", + "language": "en", + "value": "Title is required", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "label-van-de-shuffle-knop", + "language": "nl", + "value": "Label van de shuffleknop", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "label-van-de-shuffle-knop", + "language": "en", + "value": "Shuffle button label", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "label-van-de-shuffle-knop-is-verplicht", + "language": "nl", + "value": "Label van de shuffleknop is verplicht", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "label-van-de-shuffle-knop-is-verplicht", + "language": "en", + "value": "Shuffle button label is required", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "kleuren-van-de-tegel", + "language": "nl", + "value": "Tegel", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "kleuren-van-de-tegel", + "language": "en", + "value": "Tile", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "kleuren-van-de-interesse-op-tegel", + "language": "nl", + "value": "Kleuren van de interesse op tegel", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "kleuren-van-de-interesse-op-tegel", + "language": "en", + "value": "Colours of the interest on tile", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "interesse", + "language": "nl", + "value": "Interesse", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "interesse", + "language": "en", + "value": "Interest", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "naam-van-de-interesse", + "language": "nl", + "value": "Naam van de interesse", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "naam-van-de-interesse", + "language": "en", + "value": "Name of the interest", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "naam-van-de-interesse-is-verplicht", + "language": "nl", + "value": "Naam van de interesse is verplicht", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "naam-van-de-interesse-is-verplicht", + "language": "en", + "value": "Name of the interest is required", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "gerelateerd-thema", + "language": "nl", + "value": "Gerelateerd thema", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "gerelateerd-thema", + "language": "en", + "value": "Related theme", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "een-thema-is-verplicht", + "language": "nl", + "value": "Een thema is verplicht", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "een-thema-is-verplicht", + "language": "en", + "value": "A theme is required", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "voeg-een-interesse-toe", + "language": "nl", + "value": "Voeg een interesse toe", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "voeg-een-interesse-toe", + "language": "en", + "value": "Add an interest", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "verwijder-deze-interesse", + "language": "nl", + "value": "Verwijder deze interesse", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "verwijder-deze-interesse", + "language": "en", + "value": "Delete this interest", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "toon-me-iets-anders", + "language": "nl", + "value": "Toon me iets anders", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler", + "key": "toon-me-iets-anders", + "language": "en", + "value": "Show me something else", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/const/get-content-block-type-options", + "key": "driekeuzespeler", + "language": "nl", + "value": "Driekeuzespeler", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "modules/content-page/const/get-content-block-type-options", + "key": "driekeuzespeler", + "language": "en", + "value": "Three-choice player", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "react-admin/modules/shared/components/theme-select/theme-select", + "key": "kies-een-thema", + "language": "nl", + "value": "Kies een thema", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "react-admin/modules/shared/components/theme-select/theme-select", + "key": "kies-een-thema", + "language": "en", + "value": "Choose a theme", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "react-admin/modules/shared/components/theme-select/theme-select", + "key": "geen-themas-gevonden", + "language": "nl", + "value": "Geen thema's gevonden", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "react-admin/modules/shared/components/theme-select/theme-select", + "key": "geen-themas-gevonden", + "language": "en", + "value": "No themes found", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "react-admin/modules/shared/components/theme-select/theme-select", + "key": "bezig-met-laden", + "language": "nl", + "value": "Bezig met laden", + "value_type": "TEXT" + }, + { + "id": "", + "app": "HET_ARCHIEF", + "component": "ADMIN_CORE", + "location": "react-admin/modules/shared/components/theme-select/theme-select", + "key": "bezig-met-laden", + "language": "en", + "value": "Loading", + "value_type": "TEXT" + }, { "id": "", "app": "HET_ARCHIEF", diff --git a/ui/all-translations-het-archief.sql b/ui/all-translations-het-archief.sql index a2dc0945e..c6da24dfe 100644 --- a/ui/all-translations-het-archief.sql +++ b/ui/all-translations-het-archief.sql @@ -1385,6 +1385,44 @@ INSERT INTO app.translations ("component", "location", "key", "value", "value_ty INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-double-banner/block-double-banner', 'tekstkleur', 'Tekstkleur', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Tekstkleur', value_type = 'TEXT'; INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-double-banner/block-double-banner', 'tekstkleur-is-verplicht', 'Tekstkleur is verplicht', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Tekstkleur is verplicht', value_type = 'TEXT'; INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-double-banner/block-double-banner', 'tekstkleur-is-verplicht', 'Tekstkleur is verplicht', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Tekstkleur is verplicht', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'driekeuzespeler', 'Driekeuzespeler', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Driekeuzespeler', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'driekeuzespeler', 'Three-choice player', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Three-choice player', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'titel', 'Titel', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Titel', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'titel', 'Title', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Title', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'titel-is-verplicht', 'Titel is verplicht', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Titel is verplicht', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'titel-is-verplicht', 'Title is required', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Title is required', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'label-van-de-shuffle-knop', 'Label van de shuffleknop', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Label van de shuffleknop', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'label-van-de-shuffle-knop', 'Shuffle button label', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Shuffle button label', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'label-van-de-shuffle-knop-is-verplicht', 'Label van de shuffleknop is verplicht', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Label van de shuffleknop is verplicht', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'label-van-de-shuffle-knop-is-verplicht', 'Shuffle button label is required', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Shuffle button label is required', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'kleuren-van-de-tegel', 'Tegel', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Tegel', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'kleuren-van-de-tegel', 'Tile', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Tile', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'kleuren-van-de-interesse-op-tegel', 'Kleuren van de interesse op tegel', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Kleuren van de interesse op tegel', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'kleuren-van-de-interesse-op-tegel', 'Colours of the interest on tile', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Colours of the interest on tile', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'interesse', 'Interesse', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Interesse', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'interesse', 'Interest', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Interest', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'naam-van-de-interesse', 'Naam van de interesse', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Naam van de interesse', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'naam-van-de-interesse', 'Name of the interest', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Name of the interest', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'naam-van-de-interesse-is-verplicht', 'Naam van de interesse is verplicht', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Naam van de interesse is verplicht', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'naam-van-de-interesse-is-verplicht', 'Name of the interest is required', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Name of the interest is required', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'gerelateerd-thema', 'Gerelateerd thema', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Gerelateerd thema', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'gerelateerd-thema', 'Related theme', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Related theme', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'een-thema-is-verplicht', 'Een thema is verplicht', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Een thema is verplicht', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'een-thema-is-verplicht', 'A theme is required', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'A theme is required', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'voeg-een-interesse-toe', 'Voeg een interesse toe', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Voeg een interesse toe', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'voeg-een-interesse-toe', 'Add an interest', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Add an interest', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'verwijder-deze-interesse', 'Verwijder deze interesse', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Verwijder deze interesse', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'verwijder-deze-interesse', 'Delete this interest', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Delete this interest', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'toon-me-iets-anders', 'Toon me iets anders', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Toon me iets anders', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler', 'toon-me-iets-anders', 'Show me something else', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Show me something else', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/const/get-content-block-type-options', 'driekeuzespeler', 'Driekeuzespeler', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Driekeuzespeler', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/const/get-content-block-type-options', 'driekeuzespeler', 'Three-choice player', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Three-choice player', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'react-admin/modules/shared/components/theme-select/theme-select', 'kies-een-thema', 'Kies een thema', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Kies een thema', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'react-admin/modules/shared/components/theme-select/theme-select', 'kies-een-thema', 'Choose a theme', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Choose a theme', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'react-admin/modules/shared/components/theme-select/theme-select', 'geen-themas-gevonden', 'Geen thema''s gevonden', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Geen thema''s gevonden', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'react-admin/modules/shared/components/theme-select/theme-select', 'geen-themas-gevonden', 'No themes found', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'No themes found', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'react-admin/modules/shared/components/theme-select/theme-select', 'bezig-met-laden', 'Bezig met laden', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Bezig met laden', value_type = 'TEXT'; +INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'react-admin/modules/shared/components/theme-select/theme-select', 'bezig-met-laden', 'Loading', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Loading', value_type = 'TEXT'; INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-hero-carousel/block-hero-carousel', 'achtergrondafbeelding-krijgt-voorrang-op-achtergrondkleur', 'Achtergrondafbeelding (krijgt voorrang op achtergrondkleur)', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Achtergrondafbeelding (krijgt voorrang op achtergrondkleur)', value_type = 'TEXT'; INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-hero-carousel/block-hero-carousel', 'achtergrondafbeelding-krijgt-voorrang-op-achtergrondkleur', 'Achtergrondafbeelding (krijgt voorrang op achtergrondkleur)', 'TEXT', 'nl') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Achtergrondafbeelding (krijgt voorrang op achtergrondkleur)', value_type = 'TEXT'; INSERT INTO app.translations ("component", "location", "key", "value", "value_type", "language") VALUES ('ADMIN_CORE', 'modules/content-page/components/blocks/block-hero-carousel/block-hero-carousel', 'animatie-tekst', 'Animatie tekst', 'TEXT', 'en') ON CONFLICT (component, location, key, language) DO UPDATE SET value = 'Animatie tekst', value_type = 'TEXT'; diff --git a/ui/src/react-admin/core/config/config.types.ts b/ui/src/react-admin/core/config/config.types.ts index d4d3b61a4..969c32231 100644 --- a/ui/src/react-admin/core/config/config.types.ts +++ b/ui/src/react-admin/core/config/config.types.ts @@ -15,6 +15,7 @@ import type { App, Locale } from '~modules/translations/translations.core.types' import type { UserBulkAction } from '~modules/user/user.types'; import type { FlowPlayerWrapperProps } from '~shared/components/FlowPlayerWrapper/FlowPlayerWrapper.types'; import type { IeObjectType } from '~shared/helpers/map-format-to-type.ts'; +import type { PlayableDisplayIeObjectPage } from '~shared/services/ie-objects-service/ie-objects.types'; /** * What a consuming app needs to log a play of an ie-object rendered by a content block. The @@ -130,6 +131,15 @@ export interface AdminConfig { }; defaultAudioStill: string; flowplayer?: FC; // User by avo for + /** + * The IIIF viewer a newspaper opens in, injected by the host the way `flowplayer` is. + * + * It takes only the object's id: the viewer needs the object's page list and a ticket-service + * token per page, and those hooks live in the host app. Admin-core has no equivalent, so a + * block that leaves this unset falls back to the flat IIIF detail image. + * https://meemoo.atlassian.net/browse/ARC-3813 + */ + iiifViewer?: FC; buttonTypes: () => { label: string; value: string }[]; enableMultiLanguage: boolean; }; @@ -202,6 +212,23 @@ export interface AdminConfig { }; } +/** + * What the injected IIIF viewer is handed. The page and overlay state, and a ticket-service token + * per page, stay the host's to resolve -- a ticket is short-lived and access-checked at request + * time, so it can never be handed over already resolved. The page list itself is optional: a + * caller that already has it (e.g. from the driekeuzespeler's own proactive fetch) passes it along + * so the host can skip re-fetching the object just to rebuild a list it already had; a caller with + * only an id leaves it undefined and the host resolves it the way it always has. + * https://meemoo.atlassian.net/browse/ARC-3813 + */ +export interface IiifViewerConfigProps { + schemaIdentifier: string; + /** Names the viewer for assistive technology, since the object title lives outside it. */ + title?: string; + /** Every page's raw (un-ticketed) image/thumbnail/alto urls, when the caller already has them. */ + pages?: PlayableDisplayIeObjectPage[]; +} + /** * The icons admin-core renders through the client config. Every client maps each of these onto one * of its own icons in `icon.componentProps`, so each client decides what eg: a warning looks like. @@ -227,6 +254,9 @@ export enum AdminCoreIconName { ChevronLeft = 'chevronLeft', Clock = 'clock', Collection = 'collection', + // The fixed icon on the driekeuzespeler's shuffle CTA, "collection-shuffle" in the design. + // https://meemoo.atlassian.net/browse/ARC-3813 + CollectionShuffle = 'collectionShuffle', Copy = 'copy', Delete = 'delete', Edit = 'edit', diff --git a/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.const.tsx b/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.const.tsx index 839b7e0f8..fee383886 100644 --- a/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.const.tsx +++ b/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.const.tsx @@ -8,6 +8,7 @@ import { BlockCardsWithoutDescription } from '~content-blocks/BlockCardsWithoutD import { BlockContentPageMeta } from '~content-blocks/BlockContentPageMeta'; import { BlockCTAsWrapper } from '~content-blocks/BlockCTAs'; import { BlockDoubleBanner } from '~content-blocks/BlockDoubleBanner'; +import { BlockDriekeuzespeler } from '~content-blocks/BlockDriekeuzespeler'; import { BlockEventbrite } from '~content-blocks/BlockEventbrite'; import { BlockHeading } from '~content-blocks/BlockHeading'; import { BlockHeroCarousel } from '~content-blocks/BlockHeroCarousel'; @@ -111,6 +112,7 @@ export function GET_BLOCK_COMPONENT( [ContentBlockType.Timeline]: BlockTimeline, [ContentBlockType.ImageCarousel]: BlockImageCarousel, [ContentBlockType.TitleWithParallax]: BlockTitleWithParallax, + [ContentBlockType.ThreeChoicesPlayer]: BlockDriekeuzespeler, // Avo specific blocks [ContentBlockType.MediaGrid]: loadComponentFromConfig(ContentBlockType.MediaGrid), @@ -189,6 +191,7 @@ export const PLAYABLE_DISPLAY_DATA_BLOCKS = [ ContentBlockType.HetArchiefVideo, ContentBlockType.HeroCarousel, ContentBlockType.Timeline, + ContentBlockType.ThreeChoicesPlayer, ]; /** diff --git a/ui/src/react-admin/modules/content-page/components/IeObjectMetadata/IeObjectMetadata.scss b/ui/src/react-admin/modules/content-page/components/IeObjectMetadata/IeObjectMetadata.scss index 2b6476578..4cc170f75 100644 --- a/ui/src/react-admin/modules/content-page/components/IeObjectMetadata/IeObjectMetadata.scss +++ b/ui/src/react-admin/modules/content-page/components/IeObjectMetadata/IeObjectMetadata.scss @@ -27,6 +27,31 @@ $object-meta-gap: 4rem; white-space: nowrap; } + // The labelled pill is the default; the icon-only variant takes over on a narrow screen. Both are + // in the DOM and CSS picks one -- see the comment in the .tsx for why this is not a JS branch. + // 700px, the threshold the `isMobileWidth()` branch used before, so nothing changes per width. + // Written as descendant selectors (two classes) rather than the bare BEM class, so they outrank + // any single-class rule the button library paints `display` with, whatever the stylesheet order. + .c-ie-object-metadata__cta-button--icon { + display: none; + } + + @media (max-width: variables.$g-bp2) { + .c-ie-object-metadata__cta-button--icon { + display: inline-flex; + + // Making the button a flex container is what centers its content here: the library's own + // icon-only styling centers via `text-align`, which a flex container ignores, so without + // these the icon sits against the left edge of the round button instead of in its middle. + align-items: center; + justify-content: center; + } + + .c-ie-object-metadata__cta-button--label { + display: none; + } + } + &__text { flex: 1 1 auto; min-width: 0; @@ -65,4 +90,12 @@ $object-meta-gap: 4rem; display: none; } } + + // Separates the primary CTA/title/maintainer from an optional secondaryCta at the end of the row. + &__divider { + flex: 0 0 auto; + align-self: stretch; + width: 1px; + background-color: rgba(colors.$color-gray-1000, 0.12); + } } diff --git a/ui/src/react-admin/modules/content-page/components/IeObjectMetadata/IeObjectMetadata.tsx b/ui/src/react-admin/modules/content-page/components/IeObjectMetadata/IeObjectMetadata.tsx index dcd4e7b55..e1211c4ee 100644 --- a/ui/src/react-admin/modules/content-page/components/IeObjectMetadata/IeObjectMetadata.tsx +++ b/ui/src/react-admin/modules/content-page/components/IeObjectMetadata/IeObjectMetadata.tsx @@ -2,7 +2,7 @@ import { Button } from '@meemoo/react-components'; import { Icon, type IconName } from '@viaa/avo2-components'; import { AvoCoreContentPickerType } from '@viaa/avo2-types'; import { clsx } from 'clsx'; -import type { FunctionComponent } from 'react'; +import type { FunctionComponent, ReactNode } from 'react'; import React from 'react'; import { SmartLink } from '~shared/components/SmartLink/SmartLink'; import { @@ -10,7 +10,6 @@ import { mapDcTermsFormatToSimpleType, SimpleIeObjectType, } from '~shared/helpers/map-format-to-type.ts'; -import { isMobileWidth } from '~shared/helpers/media-query.ts'; import { tText } from '~shared/helpers/translation-functions'; import type { PlayableDisplayIeObject } from '~shared/services/ie-objects-service/ie-objects.types.ts'; import { HET_ARCHIEF } from '~shared/types'; @@ -62,7 +61,13 @@ export const IeObjectMetadata: FunctionComponent<{ ieObject: PlayableDisplayIeObject; fallbackTitle: string; className?: string; -}> = ({ ieObject, fallbackTitle, className }) => { + /** + * An optional second CTA, rendered after a divider at the end of the same row -- e.g. the + * driekeuzespeler's "Toon meer over [thema]" pill. Purely a slot: this component supplies the + * divider and the layout, the caller supplies (and styles) the content. + */ + secondaryCta?: ReactNode; +}> = ({ ieObject, fallbackTitle, className, secondaryCta }) => { const callToActionLabel = getCallToActionLabel(ieObject.dctermsFormat); return ( @@ -75,10 +80,20 @@ export const IeObjectMetadata: FunctionComponent<{ ariaLabel={callToActionLabel} className="c-ie-object-metadata__cta" > + {/* Both variants are rendered and the stylesheet picks one, rather than branching on + `isMobileWidth()` here. That helper reads `window.innerWidth` during render and nothing + re-renders this component on resize, so the narrow icon-only variant used to stick at + desktop widths once the window had ever been narrow (and vice versa after hydration). + The switch happens at the same 700px the helper used, so each width looks as before. */} + )} + + ); + }; + + return ( +
+ {!!title &&

{title}

} + +
+ {/* The white blobs the design lays on the block's background colour, behind the tiles -- + the "achtergrondkleur met masker" of the FA. Purely decorative, so it is hidden from + assistive technology and the artwork lives in the stylesheet. */} + + +
    + {Array.from({ length: tileCount }, (_unused, tileIndex) => renderTile(tileIndex))} +
+
+ + {/* Shown as soon as there is anything to shuffle. Even with exactly three interests the CTA + still does something: the selection is ordered, so a shuffle moves the interests between + tile positions, and each position carries its own colours and thumbnail. */} + {interests.length > 0 && ( + + )} + + setOpenedInterest(null)} + /> +
+ ); +}; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/BlockDriekeuzespelerModal.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/BlockDriekeuzespelerModal.scss new file mode 100644 index 000000000..bedeaff00 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/BlockDriekeuzespelerModal.scss @@ -0,0 +1,200 @@ +@use "../../../../shared/styles/settings/colors" as colors; +@use "../../../../shared/styles/settings/variables" as variables; + +// The FA asks for the same ratio as the video player on an object's detail page, and for the modal +// to adapt to the screen. 16:9 is that player's ratio. +$media-aspect-ratio: 16 / 9; + +// The dialog still needs an accessible name, so the title stays in the DOM -- just not on screen, +// where the FA wants nothing but the close button over the media. +// https://www.figma.com/design/1yUd3vpjHXcMfI15dTeVYC/hetarchief.be-%E2%80%94-ontdekken?node-id=2354-4569 +.c-driekeuzespeler-modal .c-modal__title { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +// The default close button, restyled from a borderless icon button into the floating black circle +// the FA asks for, sitting on top of the media instead of in a header row. +.c-driekeuzespeler-modal .c-modal__header--transparent { + padding: variables.$g-spacer-unit * 2; + + .c-button--icon { + width: 3.2rem; + height: 3.2rem; + border-radius: 50%; + background-color: #000; + color: #fff; + } +} + +// Edge-to-edge media instead of sitting inside the library's default body padding. +.c-driekeuzespeler-modal .c-modal__body { + padding: 0; +} + +// The library forces the dialog to near-full screen height on mobile regardless of its content +// (`.c-modal-context .c-modal` at $g-bp2), and its own background is white -- so a modal shorter +// than the screen used to leave a blank white void below the metadata card. The FA's design has the +// modal hug its content instead, so height comes back to the content, with a dark background (rather +// than white) for the rare case content is short enough to leave a sliver showing through. +// +// The library puts the caller's className on the outer `.c-modal-context` wrapper, not on the inner +// `.c-modal` -- so this has to be a descendant selector, not a combined one on a single element. +// `.c-modal-context.c-driekeuzespeler-modal .c-modal` (3 classes) beats the library's own +// `.c-modal-context .c-modal` (2 classes) regardless of stylesheet load order. +// https://www.figma.com/design/1yUd3vpjHXcMfI15dTeVYC/hetarchief.be-%E2%80%94-ontdekken?node-id=2382-1623 +@media (max-width: variables.$g-bp2) { + // The context is an unstyled flex row on mobile (the library's own vertical-centering `margin: + // auto` on `.c-modal` only applies above $g-bp2), so a shorter-than-container modal sits at the + // cross-axis start -- the top -- instead of centering. `align-items: center` on the row centers it. + .c-modal-context.c-driekeuzespeler-modal { + align-items: center; + } + + // Transparent, not a colour of its own: the design floats the media and the metadata card over the + // backdrop with a gutter either side, and both carry their own background. Painting the dialog + // would fill that gutter with a second, slightly different dark than the backdrop's. + .c-modal-context.c-driekeuzespeler-modal .c-modal { + height: auto; + max-height: calc(100% - 4rem); + background-color: transparent; + } + + // Lines the close button up with the media's own top-right corner, which is inset by the gutter + // below, rather than with the full-bleed dialog edge. + .c-driekeuzespeler-modal .c-modal__header--transparent { + padding: variables.$g-spacer-unit * 2 variables.$g-spacer-unit * 2.5; + } + + // The modal is mounted with `scrollable={false}`, which the library turns into `overflow: visible` + // on the body -- fine on desktop, but on a phone a tall object (a portrait newspaper, or a player + // that reports a big intrinsic size) can then push the metadata card and its theme CTA off screen + // with no way to reach them. Scrolling the body keeps everything reachable whatever the media does. + .c-modal-context.c-driekeuzespeler-modal .c-modal__body { + overflow-y: auto; + } +} + +.c-driekeuzespeler-modal { + &__media { + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: $media-aspect-ratio; + width: 100%; + + // Keeps a tall newspaper page inside the viewport instead of pushing the metadata panel and + // the close button off screen. + max-height: 70vh; + overflow: hidden; + } + + &__player, + &__newspaper, + &__iiif-viewer { + width: 100%; + height: 100%; + } + + // The viewer sizes itself to its container, so it needs one with a height rather than the + // intrinsic height an image brings. + &__iiif-viewer { + position: relative; + overflow: hidden; + } + + // A newspaper page is portrait, so it fits inside the frame rather than filling it. + &__newspaper { + object-fit: contain; + } + + &__loading { + margin: 0; + } + + // Flush against the media instead of the gap `IeObjectMetadata` keeps for callers that don't sit + // directly under a full-bleed video, and height:auto instead of the shared component's fixed + // 6rem row -- with the row's content centered inside a fixed height, its own vertical padding is + // invisible, but Figma's bar (node 2354:4631) isn't a fixed height at all: it's the CTA button's + // 48px plus 16px of padding top and bottom. Not changed on the shared component itself since + // other blocks (BlockTimeline, carousel) rely on the fixed 6rem row. Chained onto the shared + // class (both land on the same element, same specificity otherwise) so this wins regardless of + // which stylesheet happens to load last. + &__metadata.c-ie-object-metadata { + height: auto; + padding: variables.$g-spacer-unit * 2; + margin-top: 0; + } + + // The FA's secondary CTA, "Toon meer over [thema]": a pill at the end of the metadata bar's own + // row, via its `secondaryCta` slot. + &__theme-cta { + flex: 0 0 auto; + padding: variables.$g-spacer-unit variables.$g-spacer-unit * 2; + border-radius: 9.2rem; + background-color: colors.$color-silver; + font-weight: 700; + white-space: nowrap; + } + + // On mobile the FA insets the video from the screen edges and rounds its top corners, then stacks + // the metadata card under it with the secondary CTA on its own row below a full-width divider, + // instead of desktop's edge-to-edge video and single side-by-side metadata row. + // https://www.figma.com/design/1yUd3vpjHXcMfI15dTeVYC/hetarchief.be-%E2%80%94-ontdekken?node-id=2382-1623 + @media (max-width: variables.$g-bp2) { + &__media { + width: calc(100% - #{variables.$g-spacer-unit * 5}); + margin: 0 auto; + border-radius: variables.$g-spacer-unit * 1.5 variables.$g-spacer-unit * 1.5 0 0; + + // Tighter than the 70vh the desktop rule allows: on a phone the metadata card carries two + // rows plus the theme CTA, so the media has to leave room for it rather than taking most of + // the screen. The design's video is roughly a quarter of the frame height. + max-height: 45vh; + flex: 0 0 auto; + } + + &__metadata.c-ie-object-metadata { + background-color: colors.$white; + border-radius: 0 0 variables.$g-spacer-unit * 1.5 variables.$g-spacer-unit * 1.5; + width: calc(100% - #{variables.$g-spacer-unit * 5}); + margin: 0 auto; + + // The primary CTA/title/maintainer row stays side-by-side; only the secondary CTA drops to + // its own row below the divider. + display: grid; + grid-template-columns: 1fr auto; + grid-template-areas: + 'text cta' + 'divider divider' + 'theme-cta theme-cta'; + gap: variables.$g-spacer-unit * 2; + + .c-ie-object-metadata__cta { + grid-area: cta; + } + + .c-ie-object-metadata__text { + grid-area: text; + } + + .c-ie-object-metadata__divider { + grid-area: divider; + width: 100%; + height: 1px; + } + + .c-driekeuzespeler-modal__theme-cta { + grid-area: theme-cta; + width: fit-content; + } + } + } +} diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/BlockDriekeuzespelerModal.test.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/BlockDriekeuzespelerModal.test.ts new file mode 100644 index 000000000..875f9f0ec --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/BlockDriekeuzespelerModal.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from 'vitest'; +import { PLAYABLE_DISPLAY_DATA_BLOCKS } from '~modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.const'; +import { ContentBlockType } from '~modules/content-page/types/content-block.types'; + +vi.mock('~shared/helpers/translation-functions', () => ({ + tText: (key: string) => key, +})); + +vi.mock('~shared/helpers/is-avo', () => ({ + isAvo: () => false, +})); + +/** + * The modal's own rendering needs a DOM harness this package does not set up for blocks, so these + * cover the wiring that decides whether it can work at all: the block type has to match the db + * lookup value, and the renderer has to hand the block its id. + */ +describe('Driekeuzespeler modal wiring', () => { + it('uses the block type value the database lookup already holds', () => { + // The lookup value predates this work: migration 1784294418766 inserted THREE_CHOICES_PLAYER. + // A mismatch here makes every save fail on a foreign key, which is easy to miss locally. + expect(ContentBlockType.ThreeChoicesPlayer).toBe('THREE_CHOICES_PLAYER'); + }); + + it('is listed as a block that receives its blockId', () => { + // Without this the block gets no id, so the modal would fall back to the unsaved-objects path + // and resolve nothing for an ordinary visitor. + expect(PLAYABLE_DISPLAY_DATA_BLOCKS).toContain(ContentBlockType.ThreeChoicesPlayer); + }); +}); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/BlockDriekeuzespelerModal.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/BlockDriekeuzespelerModal.tsx new file mode 100644 index 000000000..9a4995345 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/BlockDriekeuzespelerModal.tsx @@ -0,0 +1,178 @@ +import { Modal, ModalBody } from '@viaa/avo2-components'; +import { AvoCoreContentPickerType } from '@viaa/avo2-types'; +import type { FunctionComponent, ReactElement } from 'react'; +import React from 'react'; +import { AdminConfigManager } from '~core/config/config.class'; +import { IeObjectFlowPlayerWrapper } from '~modules/content-page/components/IeObjectFlowPlayerWrapper/IeObjectFlowPlayerWrapper.tsx'; +import { IeObjectMetadata } from '~modules/content-page/components/IeObjectMetadata/IeObjectMetadata.tsx'; +import { Locale } from '~modules/translations/translations.core.types.ts'; +import { SmartLink } from '~shared/components/SmartLink/SmartLink.tsx'; +import { isAudioVideoFormat } from '~shared/helpers/is-audio-video-format.ts'; +import { tText } from '~shared/helpers/translation-functions'; +import type { PlayableDisplayIeObject } from '~shared/services/ie-objects-service/ie-objects.types.ts'; +import { HET_ARCHIEF } from '~shared/types'; +import { useGetThemesByIds } from '../BlockOverviewThemes/hooks/useGetThemesByIds'; + +import './BlockDriekeuzespelerModal.scss'; + +export interface BlockDriekeuzespelerModalProps { + /** The interest whose tile was opened, or null when the modal is closed. */ + interest: { + name: string; + mediaItem?: { value?: string }; + theme?: { value?: string }; + } | null; + /** The interest's object, already resolved by the parent along with the rest of the selection. */ + ieObject?: PlayableDisplayIeObject; + /** Whether the parent's proactive fetch for the current selection is still in flight. */ + isFetching: boolean; + onClose: () => void; +} + +/** + * Plays the object behind one interest, on top of the page. The visitor is never navigated + * anywhere, and closing returns to the same three tiles. + * + * https://meemoo.atlassian.net/wiki/spaces/HA2/pages/6218383419 + */ +export const BlockDriekeuzespelerModal: FunctionComponent = ({ + interest, + ieObject, + isFetching, + onClose, +}): ReactElement => { + const isOpen = !!interest; + // The host's IIIF viewer, if it registered one. Read per render rather than at module load, so a + // config set up after this module is imported is still picked up. + const IiifViewer = AdminConfigManager.getConfig().components.iiifViewer; + + // Resolves the interest's theme for the secondary CTA, in the name and slug fields -- not just + // the id the block config stores. + const { data: themes } = useGetThemesByIds(interest?.theme?.value ? [interest.theme.value] : []); + const theme = themes?.[0]; + const locale = AdminConfigManager.getConfig().locale || Locale.Nl; + const themeName = theme ? (locale === Locale.Nl ? theme.nameNl : theme.nameEn) : ''; + + // Only mount the player while the modal is open, so closing stops playback outright instead of + // leaving audio running behind the page. + const renderMedia = (): ReactElement | null => { + if (!ieObject) { + return null; + } + + if (isAudioVideoFormat(ieObject.dctermsFormat)) { + return ( + + ); + } + + // A newspaper opens in the IIIF viewer, as the FA asks. It still needs a ticket-service token + // per page (short-lived and access-checked at request time, so it can't travel with the rest + // of this already-fetched object), so the viewer itself is injected by the host rather than + // living here -- but its page list came along with everything else this modal proactively + // fetched, so passing it through here saves the host from re-fetching the object just to + // rebuild a list it already had. https://meemoo.atlassian.net/browse/ARC-3813 + if (IiifViewer) { + return ( +
+ +
+ ); + } + + // No viewer configured, so fall back to the flat IIIF detail image the timeline and carousel + // blocks show. A newspaper comes back as a self-contained data uri. + if (ieObject.newspaperImage) { + return ( + {ieObject.name} + ); + } + + return null; + }; + + return ( + + ); +}; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/hooks/useGetDriekeuzespelerPlayableObjects.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/hooks/useGetDriekeuzespelerPlayableObjects.ts new file mode 100644 index 000000000..bfce08b87 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/hooks/useGetDriekeuzespelerPlayableObjects.ts @@ -0,0 +1,52 @@ +import { useQuery } from '@tanstack/react-query'; +import { IeObjectsService } from '~shared/services/ie-objects-service/ie-objects.service.ts'; +import type { + PlayableDisplayIeObject, + UnsavedPlayableDisplayDataObject, +} from '~shared/services/ie-objects-service/ie-objects.types.ts'; +import { QUERY_KEYS } from '~shared/types'; + +/** + * Proactively resolves the objects behind the tiles currently on screen -- thumbnail, name and + * everything the modal needs to play one, keyed by schemaIdentifier -- in a single request instead + * of one per tile click. + * + * The ids are part of the query key, so a shuffle to three other interests fetches those and a + * shuffle back to a selection already seen is served from the cache -- including whichever tile + * has since been opened in the modal. + * + * A block being edited has no id yet, so it passes its objects instead, exactly as the timeline and + * carousel blocks do. The proxy honours those only for users who may edit content pages. + */ +export const useGetDriekeuzespelerPlayableObjects = ( + blockId: string | undefined, + schemaIdentifiers: string[], + unsavedObjects?: UnsavedPlayableDisplayDataObject[] +) => { + const identifiers = schemaIdentifiers.filter(Boolean); + const objects = blockId ? undefined : unsavedObjects; + + return useQuery>({ + queryKey: [ + QUERY_KEYS.GET_IE_OBJECTS_PLAYABLE_DISPLAY_DATA, + blockId, + [...identifiers].sort().join(','), + objects && JSON.stringify(objects), + ], + queryFn: async () => { + const entries = await IeObjectsService.getPlayableDisplayData( + blockId, + objects, + identifiers + ); + + return Object.fromEntries( + (entries || []) + .filter((entry): entry is PlayableDisplayIeObject => !!entry) + .map((entry) => [entry.schemaIdentifier, entry]) + ); + }, + enabled: identifiers.length > 0 && (!!blockId || !!objects?.length), + staleTime: 60 * 60 * 1000, // 1 hour, same as the other playable data + }); +}; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/index.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/index.ts new file mode 100644 index 000000000..151d099f4 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDriekeuzespeler/index.ts @@ -0,0 +1,5 @@ +export { + DRIEKEUZESPELER_CONFIG, + INITIAL_DRIEKEUZESPELER_COMPONENTS_STATE, +} from './BlockDriekeuzespeler.editorconfig.ts'; +export { BlockDriekeuzespeler } from './BlockDriekeuzespeler.tsx'; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockTitleWithParallax/BlockTitleWithParallax.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockTitleWithParallax/BlockTitleWithParallax.scss index a7d7a92b4..7f7a0de14 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockTitleWithParallax/BlockTitleWithParallax.scss +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockTitleWithParallax/BlockTitleWithParallax.scss @@ -120,9 +120,11 @@ // No padding, no box-decoration-break: a plain span, so line-height alone decides where its // glyphs sit. Its highlight is painted by &__title-box (below) instead. &__title-text { - color: colors.$white; + display: inline; + + @include typography.sofia-pro-heading-2xl(true); - @include typography.sofia-pro(6.4rem, 800, 6.4rem, true); + color: colors.$white; @media (max-width: variables.$g-bp2) { @include typography.sofia-pro(4rem, 800, 4.4rem, true); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts b/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts index 3fdaf9aaf..f5963426b 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts @@ -1,4 +1,5 @@ import type { CheckboxProps, SelectOption } from '@viaa/avo2-components'; +import { AvoCoreContentPickerType } from '@viaa/avo2-types'; import { GET_ALIGN_OPTIONS } from '~modules/content-page/const/get-align-options'; import { GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF, @@ -13,9 +14,12 @@ import type { RichTextEditorWrapperProps } from '~shared/components/RichTextEdit import type { UserGroupSelectProps } from '~shared/components/UserGroupSelect/UserGroupSelect'; import { RICH_TEXT_EDITOR_OPTIONS_FULL_WITHOUT_ALIGN } from '~shared/consts/rich-text-editor.consts'; import { isAvo } from '~shared/helpers/is-avo'; +import { IeObjectType } from '~shared/helpers/map-format-to-type.ts'; import { tText } from '~shared/helpers/translation-functions'; import { validateRequiredValue } from '~shared/helpers/validation.ts'; +import { HET_ARCHIEF } from '~shared/types'; import { SpecialUserGroups } from '~shared/types/authentication.types'; +import type { PickerItem } from '~shared/types/content-picker.ts'; import { Color, type ContentBlockComponentsConfig, @@ -25,6 +29,7 @@ import { type CustomBackground, type DefaultContentBlockState, type GradientColor, + type IsVisibleFunc, type PaddingFieldState, } from '../../types/content-block.types'; @@ -239,3 +244,41 @@ export const COPYRIGHT_STATE = (): CopyrightComponentState => ({ copyrightIconVisible: true, copyrightText: '', }); + +/** + * The object picker every block uses to point at one ie-object: it searches by title and stores the + * pid, so nobody has to type one by hand. + * + * The state key is `mediaItem` wherever this is used, because `generateFieldAttributes` reads + * `state.item || state.mediaItem` to tell a video-still picker which object to fetch stills for. + * A block without a still picker keeps the name anyway, so the proxy can read every block the same + * way. https://meemoo.atlassian.net/browse/ARC-3813 + */ +export const IE_OBJECT_FIELD = ( + allowedObjectTypes: IeObjectType[], + isVisibleFunc?: IsVisibleFunc, + fieldsToResetOnChange?: string[] +): ContentBlockField => ({ + label: tText('modules/content-page/helpers/snippet-time-fields___object', undefined, [ + HET_ARCHIEF, + ]), + editorType: ContentBlockEditor.ContentPicker, + editorProps: { + allowedTypes: [AvoCoreContentPickerType.IE_OBJECT], + hideTypeDropdown: true, + hideTargetSwitch: true, + ieObjectFormats: allowedObjectTypes, + }, + fieldsToResetOnChange, + validator: (value: PickerItem | undefined) => + value?.value + ? [] + : [ + tText( + 'modules/content-page/helpers/snippet-time-fields___een-object-is-verplicht', + undefined, + [HET_ARCHIEF] + ), + ], + isVisible: isVisibleFunc, +}); diff --git a/ui/src/react-admin/modules/content-page/const/content-block-config-map.ts b/ui/src/react-admin/modules/content-page/const/content-block-config-map.ts index 2f90de465..43afdefb0 100644 --- a/ui/src/react-admin/modules/content-page/const/content-block-config-map.ts +++ b/ui/src/react-admin/modules/content-page/const/content-block-config-map.ts @@ -9,6 +9,7 @@ import { CONTENT_ENCLOSE_BLOCK_CONFIG } from '~content-blocks/BlockContentEnclos import { CONTENT_PAGE_META_BLOCK_CONFIG } from '~content-blocks/BlockContentPageMeta'; import { CTAS_BLOCK_CONFIG } from '~content-blocks/BlockCTAs'; import { DOUBLE_BANNER_BLOCK_CONFIG } from '~content-blocks/BlockDoubleBanner'; +import { DRIEKEUZESPELER_CONFIG } from '~content-blocks/BlockDriekeuzespeler'; import { EVENTBRITE_BLOCK_CONFIG } from '~content-blocks/BlockEventbrite'; import { HEADING_BLOCK_CONFIG } from '~content-blocks/BlockHeading'; import { HERO_CAROUSEL_CONFIG } from '~content-blocks/BlockHeroCarousel'; @@ -111,4 +112,5 @@ export const CONTENT_BLOCK_CONFIG_MAP: Record< [ContentBlockType.HetArchiefVideo]: HETARCHIEF_VIDEO_BLOCK_CONFIG, [ContentBlockType.ImageCarousel]: IMAGE_CAROUSEL_CONFIG, [ContentBlockType.TitleWithParallax]: TITLE_WITH_PARALLAX_CONFIG, + [ContentBlockType.ThreeChoicesPlayer]: DRIEKEUZESPELER_CONFIG, }; diff --git a/ui/src/react-admin/modules/content-page/const/content-block-initial-state-map.ts b/ui/src/react-admin/modules/content-page/const/content-block-initial-state-map.ts index cbeae59ab..4ab541f46 100644 --- a/ui/src/react-admin/modules/content-page/const/content-block-initial-state-map.ts +++ b/ui/src/react-admin/modules/content-page/const/content-block-initial-state-map.ts @@ -9,6 +9,7 @@ import { INITIAL_CONTENT_ENCLOSE_BLOCK_STATE } from '~content-blocks/BlockConten import { INITIAL_CONTENT_PAGE_META_COMPONENTS_STATE } from '~content-blocks/BlockContentPageMeta'; import { INITIAL_CTAS_COMPONENTS_STATE } from '~content-blocks/BlockCTAs'; import { INITIAL_DOUBLE_BANNER_COMPONENTS_STATE } from '~content-blocks/BlockDoubleBanner/BlockDoubleBanner.editorconfig'; +import { INITIAL_DRIEKEUZESPELER_COMPONENTS_STATE } from '~content-blocks/BlockDriekeuzespeler'; import { INITIAL_EVENTBRITE_COMPONENTS_STATE } from '~content-blocks/BlockEventbrite'; import { INITIAL_HEADING_COMPONENTS_STATE } from '~content-blocks/BlockHeading'; import { INITIAL_HERO_CAROUSEL_COMPONENTS_STATE } from '~content-blocks/BlockHeroCarousel'; @@ -114,4 +115,5 @@ export const CONTENT_BLOCK_INITIAL_STATE_MAP: { [ContentBlockType.HetArchiefVideo]: INITIAL_HETARCHIEF_VIDEO_COMPONENTS_STATE, [ContentBlockType.ImageCarousel]: INITIAL_IMAGE_CAROUSEL_COMPONENTS_STATE, [ContentBlockType.TitleWithParallax]: INITIAL_TITLE_WITH_PARALLAX_COMPONENTS_STATE, + [ContentBlockType.ThreeChoicesPlayer]: INITIAL_DRIEKEUZESPELER_COMPONENTS_STATE, }; diff --git a/ui/src/react-admin/modules/content-page/const/get-content-block-type-options.ts b/ui/src/react-admin/modules/content-page/const/get-content-block-type-options.ts index c6bd8579d..3a01c3d03 100644 --- a/ui/src/react-admin/modules/content-page/const/get-content-block-type-options.ts +++ b/ui/src/react-admin/modules/content-page/const/get-content-block-type-options.ts @@ -274,6 +274,14 @@ export const GET_CONTENT_BLOCK_TYPE_OPTIONS: () => SelectOption[] = () = ), value: ContentBlockType.TitleWithParallax, }, + { + label: tText( + 'modules/content-page/const/get-content-block-type-options___driekeuzespeler', + {}, + [HET_ARCHIEF] + ), + value: ContentBlockType.ThreeChoicesPlayer, + }, ]; // Only show the content blocks that the client enabled through the config object diff --git a/ui/src/react-admin/modules/content-page/helpers/snippet-time-fields.ts b/ui/src/react-admin/modules/content-page/helpers/snippet-time-fields.ts index 375e8f57b..dc1cc3718 100644 --- a/ui/src/react-admin/modules/content-page/helpers/snippet-time-fields.ts +++ b/ui/src/react-admin/modules/content-page/helpers/snippet-time-fields.ts @@ -1,7 +1,6 @@ import type { TextInputProps } from '@viaa/avo2-components'; -import { AvoCoreContentPickerType } from '@viaa/avo2-types'; import type { HetArchiefVideoBlockComponentState } from '~content-blocks/BlockHetArchiefVideo'; -import { TEXT_FIELD } from '~content-blocks/defaults.ts'; +import { IE_OBJECT_FIELD, TEXT_FIELD } from '~content-blocks/defaults.ts'; import { type ContentBlockComponentState, ContentBlockEditor, @@ -15,7 +14,6 @@ import { IeObjectType } from '~shared/helpers/map-format-to-type.ts'; import { snippetTimeToSeconds } from '~shared/helpers/parsers/duration.ts'; import { tText } from '~shared/helpers/translation-functions.ts'; import { HET_ARCHIEF } from '~shared/types'; -import type { PickerItem } from '~shared/types/content-picker.ts'; /** * Validates one of the two snippet time fields. @@ -148,33 +146,9 @@ export const IE_OBJECT_WITH_SNIPPET_TIME_FIELDS = ( allowedObjectTypes: IeObjectType[] = Object.values(IeObjectType), isVisibleFunc: IsVisibleFunc = () => true ): Record => ({ - // Named `mediaItem` on purpose: generateFieldAttributes reads `state.item || - // state.mediaItem` to tell the still picker below which object to fetch stills for. - mediaItem: { - label: tText('modules/content-page/helpers/snippet-time-fields___object', undefined, [ - HET_ARCHIEF, - ]), - editorType: ContentBlockEditor.ContentPicker, - editorProps: { - allowedTypes: [AvoCoreContentPickerType.IE_OBJECT], - hideTypeDropdown: true, - hideTargetSwitch: true, - // Only video and audio objects can be played - ieObjectFormats: allowedObjectTypes, - }, - fieldsToResetOnChange: ['startTime', 'endTime'], - validator: (value: PickerItem | undefined) => - value?.value - ? [] - : [ - tText( - 'modules/content-page/helpers/snippet-time-fields___een-object-is-verplicht', - undefined, - [HET_ARCHIEF] - ), - ], - isVisible: isVisibleFunc, - }, + // Changing the object clears the times: they are offsets into that object and mean nothing once + // it is a different one. + mediaItem: IE_OBJECT_FIELD(allowedObjectTypes, isVisibleFunc, ['startTime', 'endTime']), startTime: SNIPPET_TIME_FIELD( 'startTime', tText('modules/content-page/helpers/snippet-time-fields___starttijd', undefined, [HET_ARCHIEF]), diff --git a/ui/src/react-admin/modules/content-page/types/content-block.types.ts b/ui/src/react-admin/modules/content-page/types/content-block.types.ts index 1759bb922..49a4c9902 100644 --- a/ui/src/react-admin/modules/content-page/types/content-block.types.ts +++ b/ui/src/react-admin/modules/content-page/types/content-block.types.ts @@ -217,6 +217,9 @@ export enum ContentBlockType { HetArchiefVideo = 'HETARCHIEF_VIDEO', ImageCarousel = 'IMAGE_CAROUSEL', TitleWithParallax = 'TITLE_WITH_PARALLAX', + // The db lookup value predates this work and reads THREE_CHOICES_PLAYER (migration + // 1784294418766). https://meemoo.atlassian.net/browse/ARC-3813 + ThreeChoicesPlayer = 'THREE_CHOICES_PLAYER', } export enum ContentBlockEditor { @@ -707,3 +710,49 @@ export interface TitleWithParallaxBlockComponentState { subtitle?: string; image?: string; } + +/** + * One "interesse" of the Driekeuzespeler: the label the visitor sees on a tile, the object that + * opens when the tile is clicked, and the theme its secondary CTA searches for. + * + * https://meemoo.atlassian.net/wiki/spaces/HA2/pages/6218383419 + */ +export interface DriekeuzespelerInterestState { + /** The label shown in the pill on the tile. */ + name: string; + /** + * The ie-object that plays in the modal, as the shared object picker stores it. `value` holds the + * pid the FA asks for. Named `mediaItem` like every other block that points at an object, so the + * proxy can read them all the same way. + */ + mediaItem?: PickerItem; + /** The single theme this interest belongs to, as the shared content picker stores it. */ + theme?: PickerItem; +} + +/** The two colours of one tile position, in one fixed-length repeated field group. */ +export interface DriekeuzespelerTileColors { + backgroundColor: Color; + textColor: Color; +} + +/** + * The Driekeuzespeler picks three random interests out of `interests` and renders one tile each. + * + * The tile colors are block-level and positional: entry 0 styles the leftmost tile, whichever + * interest randomization put there. They deliberately do not live on the interest itself, so a + * shuffle keeps the block's color rhythm intact -- which is also why a tile's colour cannot be + * configured next to its label: the label travels with the interest. + * + * The field order here follows the FA: title, the three background colors, the three text colors, + * the shuffle label, then the interests. + */ +export interface DriekeuzespelerBlockComponentState { + title: string; + /** Exactly three entries, one per tile position. */ + tileColors: DriekeuzespelerTileColors[]; + /** Label of the CTA that reshuffles the three tiles. Its icon is fixed. */ + shuffleButtonLabel: string; + /** Between 3 and 200 entries. */ + interests: DriekeuzespelerInterestState[]; +} diff --git a/ui/src/react-admin/modules/shared/services/ie-objects-service/ie-objects.service.ts b/ui/src/react-admin/modules/shared/services/ie-objects-service/ie-objects.service.ts index 0c4156866..3b703c597 100644 --- a/ui/src/react-admin/modules/shared/services/ie-objects-service/ie-objects.service.ts +++ b/ui/src/react-admin/modules/shared/services/ie-objects-service/ie-objects.service.ts @@ -27,9 +27,19 @@ export class IeObjectsService { */ public static async getPlayableDisplayData( blockId: string | undefined, - unsavedObjects?: UnsavedPlayableDisplayDataObject[] + unsavedObjects?: UnsavedPlayableDisplayDataObject[], + /** + * Resolve only these objects out of the block instead of every object it references. The + * proxy still checks that the block references each of them, so this narrows the response + * without widening what a visitor can reach, and it comes back in this order. Used by the + * driekeuzespeler, which can hold 200 interests and shows three at a time. + */ + schemaIdentifiers?: string[] ): Promise<(PlayableDisplayIeObject | null)[]> { - const body = blockId ? { blockId } : { objects: unsavedObjects }; + const body = { + ...(blockId ? { blockId } : { objects: unsavedObjects }), + ...(schemaIdentifiers?.length ? { schemaIdentifiers } : {}), + }; try { return await fetchWithLogoutJson<(PlayableDisplayIeObject | null)[]>( `${IeObjectsService.getBaseUrl()}/playable-display-data`, diff --git a/ui/src/react-admin/modules/shared/services/ie-objects-service/ie-objects.types.ts b/ui/src/react-admin/modules/shared/services/ie-objects-service/ie-objects.types.ts index c4b332133..7c70da54b 100644 --- a/ui/src/react-admin/modules/shared/services/ie-objects-service/ie-objects.types.ts +++ b/ui/src/react-admin/modules/shared/services/ie-objects-service/ie-objects.types.ts @@ -1,5 +1,12 @@ import type { IeObjectType } from '~shared/helpers/map-format-to-type.ts'; +/** One newspaper page's raw (un-ticketed) image/thumbnail/alto urls. */ +export interface PlayableDisplayIeObjectPage { + imageUrl: string; + thumbnailUrl: string | null; + altoUrl: string | null; +} + export interface PlayableDisplayIeObject { schemaIdentifier: string; name: string; @@ -17,6 +24,8 @@ export interface PlayableDisplayIeObject { peakfileData?: number[] | null; /** Non audio/video objects only (e.g. newspapers): self-contained base64 data uri of the IIIF detail image, or null if none is accessible/couldn't be resolved. Use this directly as an */ newspaperImage?: string | null; + /** Non audio/video objects only (e.g. newspapers): every page's raw (un-ticketed) image/thumbnail/alto urls, for a caller building its own multi-page IIIF viewer instead of using newspaperImage's single flattened detail image. A ticket still has to be requested per url before use. Empty when the object has no pages or none carry an image. */ + pages?: PlayableDisplayIeObjectPage[]; snipPoint?: { start?: number; end?: number; diff --git a/ui/src/react-admin/modules/shared/styles/mixins/_typography.scss b/ui/src/react-admin/modules/shared/styles/mixins/_typography.scss index 34d2bf24f..4675b4626 100644 --- a/ui/src/react-admin/modules/shared/styles/mixins/_typography.scss +++ b/ui/src/react-admin/modules/shared/styles/mixins/_typography.scss @@ -26,6 +26,11 @@ $font-family: "SofiaPro", sans-serif; // Presets for the combos used across content blocks - always style through one of these. If a new // block needs a size/weight that isn't here, add a preset for it rather than calling // `sofia-pro(...)` straight from the block, so the set stays consistent and reusable. +// The design system's H1: 64/64. Used by the display titles of the full-bleed blocks. +@mixin sofia-pro-heading-2xl($important: false) { + @include sofia-pro(6.4rem, 800, 6.4rem, $important); +} + @mixin sofia-pro-heading-xl($important: false) { @include sofia-pro(3.2rem, 800, 4rem, $important); } diff --git a/ui/src/react-admin/modules/shared/types/index.ts b/ui/src/react-admin/modules/shared/types/index.ts index 8de4c2a31..72ab876db 100644 --- a/ui/src/react-admin/modules/shared/types/index.ts +++ b/ui/src/react-admin/modules/shared/types/index.ts @@ -43,6 +43,8 @@ export enum QUERY_KEYS { GET_TABLE_COLUMN_PREFERENCE = 'GET_TABLE_COLUMN_PREFERENCE', GET_NAMES_BY_PROFILE_IDS = 'GET_NAMES_BY_PROFILE_IDS', GET_THEMES = 'GET_THEMES', + GET_ALL_THEMES = 'GET_ALL_THEMES', + GET_IE_OBJECTS_BY_IDS = 'GET_IE_OBJECTS_BY_IDS', GET_THEMES_BY_IDS = 'GET_THEMES_BY_IDS', GET_THEME_WITH_OBJECTS = 'GET_THEME_WITH_OBJECTS', GET_IE_OBJECT_PLAYER_DURATION = 'GET_IE_OBJECT_PLAYER_DURATION', diff --git a/ui/src/shared/helpers/admin-core-config.tsx b/ui/src/shared/helpers/admin-core-config.tsx index d16876f84..32facd900 100644 --- a/ui/src/shared/helpers/admin-core-config.tsx +++ b/ui/src/shared/helpers/admin-core-config.tsx @@ -132,6 +132,7 @@ export function getAdminCoreConfigForLocalTestApp(navigateFunc: NavigateFunction componentProps: { add: { name: 'add' }, collection: { name: 'collection' }, + collectionShuffle: { name: 'collection-shuffle' }, view: { name: 'view' }, angleDown: { name: 'down' }, angleUp: { name: 'up' }, diff --git a/ui/src/shared/translations/hetArchief/nl.json b/ui/src/shared/translations/hetArchief/nl.json index 3d459a76f..f9e760f8a 100644 --- a/ui/src/shared/translations/hetArchief/nl.json +++ b/ui/src/shared/translations/hetArchief/nl.json @@ -692,6 +692,25 @@ "modules/content-page/components/blocks/block-double-banner/block-double-banner___label-is-verplicht": "Label is verplicht", "modules/content-page/components/blocks/block-double-banner/block-double-banner___tekstkleur": "Tekstkleur", "modules/content-page/components/blocks/block-double-banner/block-double-banner___tekstkleur-is-verplicht": "Tekstkleur is verplicht", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___achtergrondkleur": "Achtergrondkleur", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___kleuren-van-de-interesse-op-tegel": "Kleuren van de interesse op tegel", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___bezig-met-laden": "Bezig met laden", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___driekeuzespeler": "Driekeuzespeler", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___een-thema-is-verplicht": "Een thema is verplicht", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___gerelateerd-thema": "Gerelateerd thema", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___interesse": "Interesse", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___label-van-de-shuffle-knop": "Label van de shuffleknop", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___label-van-de-shuffle-knop-is-verplicht": "Label van de shuffleknop is verplicht", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___naam-van-de-interesse": "Naam van de interesse", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___naam-van-de-interesse-is-verplicht": "Naam van de interesse is verplicht", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___object-over-interest": "Over {{interest}}", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___tekstkleur": "Tekstkleur", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___titel": "Titel", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___titel-is-verplicht": "Titel is verplicht", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___toon-me-iets-anders": "Toon me iets anders", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___toon-meer-over-thema": "Toon meer rond \"{{theme}}\"", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___verwijder-deze-interesse": "Verwijder deze interesse", + "modules/content-page/components/blocks/block-driekeuzespeler/block-driekeuzespeler___voeg-een-interesse-toe": "Voeg een interesse toe", "modules/content-page/components/blocks/block-hero-carousel/block-hero-carousel-active-slide___afspelen": "Afspelen", "modules/content-page/components/blocks/block-hero-carousel/block-hero-carousel-active-slide___pauzeren": "Pauzeren", "modules/content-page/components/blocks/block-hero-carousel/block-hero-carousel-inactive-slide___afbeelding": "Afbeelding", @@ -914,6 +933,7 @@ "modules/content-page/const/get-content-block-type-options___breadcrumbs": "Breadcrumbs", "modules/content-page/const/get-content-block-type-options___carousel-met-afbeeldingen": "Carousel met afbeeldingen", "modules/content-page/const/get-content-block-type-options___content-enclose-grid": "Content insluiten grid", + "modules/content-page/const/get-content-block-type-options___driekeuzespeler": "Driekeuzespeler", "modules/content-page/const/get-content-block-type-options___dubbele-banner": "Dubbele banner", "modules/content-page/const/get-content-block-type-options___hero-carousel": "Hero carousel", "modules/content-page/const/get-content-block-type-options___highlight-text": "Highlight text", @@ -1186,6 +1206,9 @@ "react-admin/modules/content-page/views/content-page-edit___kopieer-content-pagina": "Kopieer content pagina", "react-admin/modules/shared/components/content-picker/content-picker___custom-navigatie-items": "custom navigatie items", "react-admin/modules/shared/components/content-picker/content-picker___opdrachten": "Opdrachten", + "react-admin/modules/shared/components/theme-select/theme-select___bezig-met-laden": "Bezig met laden", + "react-admin/modules/shared/components/theme-select/theme-select___geen-themas-gevonden": "Geen thema's gevonden", + "react-admin/modules/shared/components/theme-select/theme-select___kies-een-thema": "Kies een thema", "react-admin/modules/shared/components/upload-or-select-video-still/upload-or-select-video-still___annuleer": "Annuleer", "react-admin/modules/shared/components/upload-or-select-video-still/upload-or-select-video-still___eigen-still-uploaden": "Eigen still uploaden", "react-admin/modules/shared/components/upload-or-select-video-still/upload-or-select-video-still___opslaan": "Opslaan",