| undefined)?.[icon]?.name ??
+ icon;
+
+ const renderHalf = (half: DoubleBannerHalf, index: number) => {
+ const icons = compact([half.icon1, half.icon2, half.icon3]);
+
+ return (
+ <>
+
+ {half.label}
+
+
+ {icons.map((icon, iconIndex) => (
+
+ ))}
+
+
+
+
+
+
+
+ >
+ );
+ };
+
+ return (
+
+ {halves.map((half: DoubleBannerHalf, index: number) => {
+ const content = renderHalf(half, index);
+ // Every half after the first mirrors the previous one: image towards the middle.
+ const halfClassName = clsx('c-block-double-banner__half', {
+ 'c-block-double-banner__half--mirrored': index % 2 === 1,
+ });
+
+ return generateSmartLink(
+ { ...half.link, target: LinkTarget.Self },
+ content,
+ undefined,
+ halfClassName
+ );
+ })}
+
+ );
+};
diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.types.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.types.ts
new file mode 100644
index 00000000..459d3669
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/BlockDoubleBanner.types.ts
@@ -0,0 +1,22 @@
+import type { ButtonAction } from '@viaa/avo2-components';
+import type { DefaultComponentProps } from '~shared/types';
+
+/**
+ * One of the two mirrored halves of the double banner.
+ * The FA requires every field except the icons to be filled in.
+ * https://meemoo.atlassian.net/browse/ARC-3833
+ */
+export interface DoubleBannerHalf {
+ label: string;
+ icon1?: string;
+ icon2?: string;
+ icon3?: string;
+ link: ButtonAction;
+ image: string;
+ textColor: string;
+ backgroundColor: string;
+}
+
+export interface BlockDoubleBannerProps extends DefaultComponentProps {
+ halves: [DoubleBannerHalf, DoubleBannerHalf];
+}
diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/index.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/index.ts
new file mode 100644
index 00000000..cba2f2dc
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockDoubleBanner/index.ts
@@ -0,0 +1,6 @@
+export { BlockDoubleBanner } from './BlockDoubleBanner';
+export {
+ DOUBLE_BANNER_BLOCK_CONFIG,
+ INITIAL_DOUBLE_BANNER_BLOCK_STATE,
+ INITIAL_DOUBLE_BANNER_COMPONENTS_STATE,
+} from './BlockDoubleBanner.editorconfig';
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 9406f588..0cf08327 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
@@ -7,6 +7,7 @@ import { CARDS_WITHOUT_DESCRIPTION_BLOCK_CONFIG } from '~content-blocks/BlockCar
import { CONTENT_ENCLOSE_BLOCK_CONFIG } from '~content-blocks/BlockContentEnclose';
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 { EVENTBRITE_BLOCK_CONFIG } from '~content-blocks/BlockEventbrite';
import { HEADING_BLOCK_CONFIG } from '~content-blocks/BlockHeading';
import { HET_ARCHIEF_HEADER_SEARCH_BLOCK_CONFIG } from '~content-blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.editorconfig';
@@ -95,4 +96,5 @@ export const CONTENT_BLOCK_CONFIG_MAP: Record<
[ContentBlockType.HighlightText]: CONTENT_HIGHLIGHT_TEXT_CONFIG,
[ContentBlockType.ThemeReels]: THEME_REELS_BLOCK_CONFIG,
[ContentBlockType.OverviewThemes]: OVERVIEW_THEMES_BLOCK_CONFIG,
+ [ContentBlockType.DoubleBanner]: DOUBLE_BANNER_BLOCK_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 61b21ca0..55a81bde 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
@@ -7,6 +7,7 @@ import { INITIAL_CARDS_WITHOUT_DESCRIPTION_COMPONENTS_STATE } from '~content-blo
import { INITIAL_CONTENT_ENCLOSE_BLOCK_STATE } from '~content-blocks/BlockContentEnclose/BlockContentEnclose.editorconfig';
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_EVENTBRITE_COMPONENTS_STATE } from '~content-blocks/BlockEventbrite';
import { INITIAL_HEADING_COMPONENTS_STATE } from '~content-blocks/BlockHeading';
import { INITIAL_HET_ARCHIEF_HEADER_SEARCH_BLOCK_STATE } from '~content-blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.editorconfig';
@@ -98,4 +99,5 @@ export const CONTENT_BLOCK_INITIAL_STATE_MAP: {
[ContentBlockType.HighlightText]: INITIAL_CONTENT_HIGHLIGHT_TEXT_BLOCK_STATE,
[ContentBlockType.ThemeReels]: INITIAL_THEME_REELS_COMPONENTS_STATE,
[ContentBlockType.OverviewThemes]: INITIAL_OVERVIEW_THEMES_COMPONENTS_STATE,
+ [ContentBlockType.DoubleBanner]: INITIAL_DOUBLE_BANNER_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 18f589ed..8e955f28 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
@@ -214,6 +214,14 @@ export const GET_CONTENT_BLOCK_TYPE_OPTIONS: () => SelectOption[] = () =
),
value: ContentBlockType.OverviewThemes,
},
+ {
+ label: tText(
+ 'modules/content-page/const/get-content-block-type-options___dubbele-banner',
+ {},
+ [HET_ARCHIEF]
+ ),
+ value: ContentBlockType.DoubleBanner,
+ },
];
// Only show the content blocks that the client enabled through the config object
diff --git a/ui/src/react-admin/modules/content-page/helpers/field-attributes.test.ts b/ui/src/react-admin/modules/content-page/helpers/field-attributes.test.ts
new file mode 100644
index 00000000..d03bddcb
--- /dev/null
+++ b/ui/src/react-admin/modules/content-page/helpers/field-attributes.test.ts
@@ -0,0 +1,60 @@
+import type { SelectOption } from '@viaa/avo2-components';
+import { describe, expect, it, vi } from 'vitest';
+import { Color, ContentBlockEditor } from '~modules/content-page/types/content-block.types';
+import { generateFieldAttributes } from './field-attributes';
+
+interface ColorSelectAttributes {
+ defaultValue?: SelectOption;
+ onChange?: (option: SelectOption) => void;
+}
+
+describe('generateFieldAttributes', () => {
+ it('maps a Color enum default to the option expected by ColorSelect', () => {
+ const whiteOption: SelectOption = {
+ label: 'White',
+ value: Color.White,
+ };
+ const onChange = vi.fn();
+ const attributes = generateFieldAttributes(
+ {
+ editorType: ContentBlockEditor.ColorSelect,
+ editorProps: {
+ options: [whiteOption],
+ defaultValue: Color.White,
+ },
+ },
+ onChange,
+ undefined,
+ 'color',
+ 'color',
+ {}
+ ) as ColorSelectAttributes;
+
+ expect(attributes.defaultValue).toBe(whiteOption);
+ attributes.onChange?.(whiteOption);
+ expect(onChange).toHaveBeenCalledWith(Color.White);
+ });
+
+ it('keeps existing option-object defaults compatible', () => {
+ const blackOption: SelectOption = {
+ label: 'Black',
+ value: Color.Black,
+ };
+ const attributes = generateFieldAttributes(
+ {
+ editorType: ContentBlockEditor.ColorSelect,
+ editorProps: {
+ options: [blackOption],
+ defaultValue: blackOption,
+ },
+ },
+ vi.fn(),
+ undefined,
+ 'color',
+ 'color',
+ {}
+ ) as ColorSelectAttributes;
+
+ expect(attributes.defaultValue).toBe(blackOption);
+ });
+});
diff --git a/ui/src/react-admin/modules/content-page/helpers/field-attributes.ts b/ui/src/react-admin/modules/content-page/helpers/field-attributes.ts
index bd688818..76a51df7 100644
--- a/ui/src/react-admin/modules/content-page/helpers/field-attributes.ts
+++ b/ui/src/react-admin/modules/content-page/helpers/field-attributes.ts
@@ -91,15 +91,31 @@ export const generateFieldAttributes = (
} as DatePickerProps;
case ContentBlockEditor.IconPicker:
- case ContentBlockEditor.ColorSelect:
- return {
+ case ContentBlockEditor.ColorSelect: {
+ const options = field.editorProps.options as SelectOption[];
+ const selectAttributes = {
onChange: ((option: SelectOption) => {
onChange(option?.value || '');
// biome-ignore lint/suspicious/noExplicitAny: todo investigate why this cast is needed
}) as any,
- value: field.editorProps.options.find((opt: SelectOption) => opt.value === value),
+ value: options.find((option) => option.value === value),
};
+ if (field.editorType === ContentBlockEditor.ColorSelect) {
+ const configuredDefaultValue = field.editorProps.defaultValue;
+
+ return {
+ ...selectAttributes,
+ defaultValue:
+ typeof configuredDefaultValue === 'string'
+ ? options.find((option) => option.value === configuredDefaultValue)
+ : configuredDefaultValue,
+ };
+ }
+
+ return selectAttributes;
+ }
+
case ContentBlockEditor.RICH_TEXT_EDITOR: {
// biome-ignore lint/suspicious/noExplicitAny: todo
const html = (state as any)[key] || '';
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 f276f3ce..0d238987 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
@@ -191,6 +191,7 @@ export enum ContentBlockType {
HighlightText = 'HIGHLIGHT_TEXT',
ThemeReels = 'THEME_REELS',
OverviewThemes = 'OVERVIEW_THEMES',
+ DoubleBanner = 'DOUBLE_BANNER',
}
export enum ContentBlockEditor {
diff --git a/ui/src/shared/helpers/admin-core-config.tsx b/ui/src/shared/helpers/admin-core-config.tsx
index d8082acf..31693c5d 100644
--- a/ui/src/shared/helpers/admin-core-config.tsx
+++ b/ui/src/shared/helpers/admin-core-config.tsx
@@ -98,6 +98,7 @@ export function getAdminCoreConfigForLocalTestApp(navigateFunc: NavigateFunction
// ContentBlockType.Breadcrumbs,
ContentBlockType.OverviewWithCarousel,
ContentBlockType.OverviewThemes,
+ ContentBlockType.DoubleBanner,
],
defaultPageWidth: ContentPageWidth.LARGE,
onSaveContentPage: async (contentPageInfo: ContentPageInfo) => {
diff --git a/ui/src/shared/translations/hetArchief/nl.json b/ui/src/shared/translations/hetArchief/nl.json
index 88621ca2..c0215523 100644
--- a/ui/src/shared/translations/hetArchief/nl.json
+++ b/ui/src/shared/translations/hetArchief/nl.json
@@ -674,6 +674,21 @@
"modules/content-page/components/blocks/block-content-enclose/block-content-enclose___titletype": "titeltype",
"modules/content-page/components/blocks/block-content-enclose/block-content-enclose___verwijder-object": "verwijder object",
"modules/content-page/components/blocks/block-content-enclose/block-content-enclose___voeg-object-toe": "voeg object toe",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___achtergrondkleur-tekstvak": "Achtergrondkleur tekstvak",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___achtergrondkleur-tekstvak-is-verplicht": "Achtergrondkleur tekstvak is verplicht",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___afbeelding": "Afbeelding",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___afbeelding-is-verplicht": "Afbeelding is verplicht",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___bestemming": "Bestemming",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___bestemming-is-verplicht": "Bestemming is verplicht",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___dubbele-banner": "Dubbele banner",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___helft": "Helft",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___icoon-1": "Icoon 1",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___icoon-2": "Icoon 2",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___icoon-3": "Icoon 3",
+ "modules/content-page/components/blocks/block-double-banner/block-double-banner___label": "Label",
+ "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-het-archief-header-search/block-het-archief-header-search___aria-label-verplicht": "aria label verplicht",
"modules/content-page/components/blocks/block-het-archief-header-search/block-het-archief-header-search___aria-label-voor-zoekveld": "Aria label voor zoekveld",
"modules/content-page/components/blocks/block-het-archief-header-search/block-het-archief-header-search___zoek-in-de-publieke-catalogus-input-aria-label": "Hiermee kan je zoeken in de publieke catalogus ",
@@ -804,6 +819,7 @@
"modules/content-page/const/content-page___vertalingen": "Vertalingen",
"modules/content-page/const/get-content-block-type-options___breadcrumbs": "Breadcrumbs",
"modules/content-page/const/get-content-block-type-options___content-enclose-grid": "Content insluiten grid",
+ "modules/content-page/const/get-content-block-type-options___dubbele-banner": "Dubbele banner",
"modules/content-page/const/get-content-block-type-options___highlight-text": "Highlight text",
"modules/content-page/const/get-content-block-type-options___homepage-banner": "Homepage banner",
"modules/content-page/const/get-content-block-type-options___overzicht-krantentitels": "Overzicht krantentitels",