diff --git a/.gitignore b/.gitignore index 54881ef68..14e13f8d2 100644 --- a/.gitignore +++ b/.gitignore @@ -255,3 +255,7 @@ schema.graphql /.husky api/.claude/ + +*.pdf + +ui/.claude/worktrees/ diff --git a/api/package-lock.json b/api/package-lock.json index 1a2ae3d30..ad0caa240 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -20,7 +20,7 @@ "@smithy/protocol-http": "4.1.8", "@smithy/signature-v4": "4.2.4", "@studiohyperdrive/pagination": "1.0.0", - "@viaa/avo2-types": "5.0.18", + "@viaa/avo2-types": "5.0.19", "app-root-path": "3.0.0", "blend-promise-utils": "^1.28.0", "cache-manager": "5.2.3", @@ -7026,9 +7026,9 @@ "dev": true }, "node_modules/@viaa/avo2-types": { - "version": "5.0.18", - "resolved": "http://do-prd-mvn-01.do.viaa.be:8081/repository/npm-viaa/@viaa/avo2-types/-/avo2-types-5.0.18.tgz", - "integrity": "sha512-C2dfThmIfv4rFkcHDl2coqsMJ2JlszPZxBjneuICU1ylGPqQxfrwC+verxJKsdypC2hR31q1lRK/83aqDTNpsg==" + "version": "5.0.19", + "resolved": "http://do-prd-mvn-01.do.viaa.be:8081/repository/npm-viaa/@viaa/avo2-types/-/avo2-types-5.0.19.tgz", + "integrity": "sha512-3Xu9iCLl9e44javHBmzTeQeChw5xQW4gP4mQ4Ituo9/VOYSnD6H586qBgCi2j0dGNSISNm5ojn2+ETyxB9wUuw==" }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", diff --git a/api/package.json b/api/package.json index 9c9ded66b..02b1d3632 100644 --- a/api/package.json +++ b/api/package.json @@ -45,7 +45,7 @@ "@smithy/protocol-http": "4.1.8", "@smithy/signature-v4": "4.2.4", "@studiohyperdrive/pagination": "1.0.0", - "@viaa/avo2-types": "5.0.18", + "@viaa/avo2-types": "5.0.19", "app-root-path": "3.0.0", "blend-promise-utils": "^1.28.0", "cache-manager": "5.2.3", diff --git a/ui/package-lock.json b/ui/package-lock.json index 15bd61064..29109d859 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -77,11 +77,11 @@ "@emotion/react": "11.14.0", "@floating-ui/react": "0.27.18", "@hookform/resolvers": "2.9.11", - "@meemoo/react-components": "6.0.7", + "@meemoo/react-components": "6.0.8", "@studiohyperdrive/pagination": "1.0.0", "@tanstack/react-query": "5.99.0", - "@viaa/avo2-components": "7.0.1", - "@viaa/avo2-types": "5.0.18", + "@viaa/avo2-components": "7.0.2", + "@viaa/avo2-types": "5.0.19", "autosize": "6.0.1", "blend-promise-utils": "1.29.2", "caniuse-lite": "1.0.30001712", @@ -2476,9 +2476,9 @@ "license": "MIT" }, "node_modules/@meemoo/react-components": { - "version": "6.0.7", - "resolved": "http://do-prd-mvn-01.do.viaa.be:8081/repository/npm-viaa/@meemoo/react-components/-/react-components-6.0.7.tgz", - "integrity": "sha512-WTvGXD35QfPDwB0vuA6/5kz/fhsfBLYshaYu4U8dDzGUNy/hhp1sdFHUZqqVX17yqHjIVivrrPYM4yG0RiUZ0w==", + "version": "6.0.8", + "resolved": "http://do-prd-mvn-01.do.viaa.be:8081/repository/npm-viaa/@meemoo/react-components/-/react-components-6.0.8.tgz", + "integrity": "sha512-kRY2WmpdH+/06sYOSbFMKVJIlj9IMbv+cOEAFd8wlGkCtZP2+YO3xo/kvJ7W5QuqkZdXTkb5yd1xjc3TaSmr4w==", "peer": true, "dependencies": { "use-sync-external-store": "1.4.0" @@ -2488,7 +2488,7 @@ "@floating-ui/react": "0.27.18", "@flowplayer/player": "3.32.1", "@tanstack/react-table": "8.21.3", - "@viaa/avo2-types": "5.0.18", + "@viaa/avo2-types": "5.0.19", "autosize": "6.0.1", "clsx": "2.0.0", "date-fns": "3.6.0", @@ -4569,13 +4569,13 @@ } }, "node_modules/@viaa/avo2-components": { - "version": "7.0.1", - "resolved": "http://do-prd-mvn-01.do.viaa.be:8081/repository/npm-viaa/@viaa/avo2-components/-/avo2-components-7.0.1.tgz", - "integrity": "sha512-5dsmWKlA5MzJNHBrCU197k4DYS1JmFvk0xLY09XKRgxytPMMqOKTnvvMpAQTHZYj0wsZx9Dpn/x90ehLd5+oOQ==", + "version": "7.0.2", + "resolved": "http://do-prd-mvn-01.do.viaa.be:8081/repository/npm-viaa/@viaa/avo2-components/-/avo2-components-7.0.2.tgz", + "integrity": "sha512-BGThaG4qtUjzyyO1ZHLPKZkayGKefN91oTerBGX6JqYGfo7gAhVLCNRo9we0JMd08s1UDNkRsCwodMvOh5qIiQ==", "peer": true, "peerDependencies": { "@popperjs/core": "2.11.6", - "@viaa/avo2-types": "5.0.18", + "@viaa/avo2-types": "5.0.19", "autosize": "6.0.1", "clsx": "2.0.0", "date-fns": "3.6.0", @@ -4592,9 +4592,9 @@ } }, "node_modules/@viaa/avo2-types": { - "version": "5.0.18", - "resolved": "http://do-prd-mvn-01.do.viaa.be:8081/repository/npm-viaa/@viaa/avo2-types/-/avo2-types-5.0.18.tgz", - "integrity": "sha512-C2dfThmIfv4rFkcHDl2coqsMJ2JlszPZxBjneuICU1ylGPqQxfrwC+verxJKsdypC2hR31q1lRK/83aqDTNpsg==", + "version": "5.0.19", + "resolved": "http://do-prd-mvn-01.do.viaa.be:8081/repository/npm-viaa/@viaa/avo2-types/-/avo2-types-5.0.19.tgz", + "integrity": "sha512-3Xu9iCLl9e44javHBmzTeQeChw5xQW4gP4mQ4Ituo9/VOYSnD6H586qBgCi2j0dGNSISNm5ojn2+ETyxB9wUuw==", "peer": true }, "node_modules/@vitejs/plugin-react": { diff --git a/ui/package.json b/ui/package.json index b087d36e1..287fb4e95 100644 --- a/ui/package.json +++ b/ui/package.json @@ -76,11 +76,11 @@ "@emotion/react": "11.14.0", "@floating-ui/react": "0.27.18", "@hookform/resolvers": "2.9.11", - "@meemoo/react-components": "6.0.7", + "@meemoo/react-components": "6.0.8", "@studiohyperdrive/pagination": "1.0.0", "@tanstack/react-query": "5.99.0", - "@viaa/avo2-components": "7.0.1", - "@viaa/avo2-types": "5.0.18", + "@viaa/avo2-components": "7.0.2", + "@viaa/avo2-types": "5.0.19", "autosize": "6.0.1", "blend-promise-utils": "1.29.2", "caniuse-lite": "1.0.30001712", diff --git a/ui/src/client.ts b/ui/src/client.ts index 1588c6387..f3eb78100 100644 --- a/ui/src/client.ts +++ b/ui/src/client.ts @@ -19,6 +19,7 @@ export { BlockIntro } from '~content-blocks/BlockIntro'; export { BlockKlaar } from '~content-blocks/BlockKlaar'; export { BlockMaintainersGrid } from '~content-blocks/BlockMaintainersGrid'; export { BlockOverviewNewspaperTitles } from '~content-blocks/BlockOverviewNewspaperTitles'; +export { BlockOverviewThemes } from '~content-blocks/BlockOverviewThemes'; export { BlockOverviewWithCarousel } from '~content-blocks/BlockOverviewWithCarousel'; export { BlockPageOverviewWrapper } from '~content-blocks/BlockPageOverview/BlockPageOverview.wrapper'; export { BlockQuote } from '~content-blocks/BlockQuote'; 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 2b41e4d26..d9179c4fb 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 @@ -22,6 +22,7 @@ import { BlockIntro } from '~content-blocks/BlockIntro'; import { BlockKlaar } from '~content-blocks/BlockKlaar'; import { BlockMaintainersGrid } from '~content-blocks/BlockMaintainersGrid'; import { BlockOverviewNewspaperTitles } from '~content-blocks/BlockOverviewNewspaperTitles'; +import { BlockOverviewThemes } from '~content-blocks/BlockOverviewThemes/BlockOverviewThemes.tsx'; import { BlockOverviewWithCarousel } from '~content-blocks/BlockOverviewWithCarousel/BlockOverviewWithCarousel.tsx'; import { BlockPageOverviewWrapper } from '~content-blocks/BlockPageOverview'; import { BlockQuote } from '~content-blocks/BlockQuote'; @@ -87,6 +88,7 @@ export function GET_BLOCK_COMPONENT( [ContentBlockType.Breadcrumbs]: BlockBreadcrumbs, [ContentBlockType.HetArchiefImageTextBackground]: BlockHetArchiefImageTextBackground, [ContentBlockType.OverviewWithCarousel]: BlockOverviewWithCarousel, + [ContentBlockType.OverviewThemes]: BlockOverviewThemes, // Avo specific blocks [ContentBlockType.MediaGrid]: loadComponentFromConfig(ContentBlockType.MediaGrid), @@ -111,6 +113,7 @@ export const REPEATABLE_CONTENT_BLOCKS = [ ContentBlockType.ThreeClickableTiles, ContentBlockType.TagsWithLink, ContentBlockType.CardsWithoutDescription, + ContentBlockType.OverviewThemes, // ContentBlockType.ContentEncloseGrid, ]; diff --git a/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.scss b/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.scss index 4c64bae06..d4a99654b 100644 --- a/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.scss +++ b/ui/src/react-admin/modules/content-page/components/ContentBlockRenderer/ContentBlockRenderer.scss @@ -40,6 +40,10 @@ } } +.c-content-block-preview { + overflow-x: hidden; +} + .c-content-block-preview--dark { // Here you can overwrite styles on content-blocks for previews with a dark background .c-block-media-list { diff --git a/ui/src/react-admin/modules/content-page/components/FieldGenerator/FieldGenerator.tsx b/ui/src/react-admin/modules/content-page/components/FieldGenerator/FieldGenerator.tsx index 986ec6f87..4d7b7a968 100644 --- a/ui/src/react-admin/modules/content-page/components/FieldGenerator/FieldGenerator.tsx +++ b/ui/src/react-admin/modules/content-page/components/FieldGenerator/FieldGenerator.tsx @@ -142,6 +142,13 @@ export const FieldGenerator: FunctionComponent = ({ <> {/* biome-ignore lint/suspicious/noExplicitAny: todo */} {currentState.map((innerState: any, index: number) => { + const fieldId = `${fieldKey}-${index}`; + const defaultProps = { + ...field.editorProps, + editorId: fieldId, + name: fieldId, + }; + // biome-ignore lint/suspicious/noExplicitAny: todo const editorProps: any = generateFieldAttributes( field as ContentBlockField, @@ -151,7 +158,7 @@ export const FieldGenerator: FunctionComponent = ({ }, // biome-ignore lint/suspicious/noExplicitAny: todo innerState as any, - `${fieldKey}-${index}`, + fieldId, fieldKey, currentState ); @@ -159,21 +166,27 @@ export const FieldGenerator: FunctionComponent = ({ return ( // biome-ignore lint/suspicious/noArrayIndexKey: We don't have a better key at this time - - - - - - - {renderNote(field)} - - - {currentState.length > 1 && ( - - {renderDeleteButton(currentState, field?.repeat?.deleteButtonLabel, index)} - + {/**/} + {/* /!* `center` + `orientation="vertical"` renders as flex-direction: column with*/} + {/* align-items: center, which centers this item at its content width instead of*/} + {/* stretching it — force it to take the full row width regardless. *!/*/} + {/* */} + + + + + {renderNote(field)} + + {/**/} + {currentState.length > 1 && + renderDeleteButton( + currentState, + field?.repeat?.deleteButtonLabel, + index, + 'u-align-right' )} - + + {/**/} ); })} @@ -249,7 +262,8 @@ export const FieldGenerator: FunctionComponent = ({ // biome-ignore lint/suspicious/noExplicitAny: todo stateCopy: any, label?: string, - index?: number + index?: number, + className?: string ) => { // biome-ignore lint/suspicious/noExplicitAny: todo const handleFieldDelete = (index: any) => { @@ -268,6 +282,7 @@ export const FieldGenerator: FunctionComponent = ({ title={label} ariaLabel={label} type="danger" + className={className} /> ); }; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.editorconfig.ts new file mode 100644 index 000000000..57225c9d1 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.editorconfig.ts @@ -0,0 +1,119 @@ +import { AvoCoreContentPickerType } from '@viaa/avo2-types'; +import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, TEXT_FIELD } from '~content-blocks/defaults'; +import { GET_FULL_HEADING_TYPE_OPTIONS } from '~modules/content-page/const/get-heading-type-options'; +import { + type ContentBlockConfig, + ContentBlockEditor, + ContentBlockType, + type DefaultContentBlockState, +} from '~modules/content-page/types/content-block.types'; +import { tText } from '~shared/helpers/translation-functions'; +import { validateRequiredValue } from '~shared/helpers/validation'; +import { HET_ARCHIEF } from '~shared/types'; + +const INITIAL_OVERVIEW_THEMES_THEME_STATE = () => ({ + label: '', + type: AvoCoreContentPickerType.IE_OBJECT_THEME, + value: '', +}); + +const INITIAL_OVERVIEW_THEMES_GROUP_STATE = () => ({ + title: '', + titleType: 'h2', + themes: [INITIAL_OVERVIEW_THEMES_THEME_STATE()], +}); + +// `components.state` for a repeatable block must be an array: the editor pushes/splices entries +// into it directly (see content-edit.reducer.ts), and `ContentBlockRenderer` passes it straight +// through as the `elements` prop. +export const INITIAL_OVERVIEW_THEMES_COMPONENTS_STATE = () => [INITIAL_OVERVIEW_THEMES_GROUP_STATE()]; + +export const INITIAL_OVERVIEW_THEMES_BLOCK_STATE = (): DefaultContentBlockState => + BLOCK_STATE_DEFAULTS(); + +export const OVERVIEW_THEMES_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ + position, + name: tText('modules/content-page/const/get-content-block-type-options___overzicht-themas', {}, [ + HET_ARCHIEF, + ]), + type: ContentBlockType.OverviewThemes, + components: { + name: tText( + 'modules/content-page/components/blocks/block-overview-themes/block-overview-themes___themagroep', + {}, + [HET_ARCHIEF] + ), + limits: { + min: 1, + }, + state: INITIAL_OVERVIEW_THEMES_COMPONENTS_STATE(), + fields: { + title: TEXT_FIELD( + { + label: tText( + 'modules/content-page/components/blocks/block-overview-themes/block-overview-themes___titel', + {}, + [HET_ARCHIEF] + ), + }, + tText( + 'modules/content-page/components/blocks/block-overview-themes/block-overview-themes___titel-is-verplicht', + {}, + [HET_ARCHIEF] + ) + ), + titleType: { + label: tText( + 'modules/content-page/components/blocks/block-overview-themes/block-overview-themes___titel-type', + {}, + [HET_ARCHIEF] + ), + editorType: ContentBlockEditor.Select, + editorProps: { + options: GET_FULL_HEADING_TYPE_OPTIONS(), + }, + validator: (value: string) => + validateRequiredValue( + value, + tText( + 'modules/content-page/components/blocks/block-overview-themes/block-overview-themes___titel-type-is-verplicht', + {}, + [HET_ARCHIEF] + ) + ), + }, + themes: { + label: tText( + 'modules/content-page/components/blocks/block-overview-themes/block-overview-themes___thema', + {}, + [HET_ARCHIEF] + ), + editorType: ContentBlockEditor.ContentPicker, + editorProps: { + allowedTypes: [AvoCoreContentPickerType.IE_OBJECT_THEME], + hideTypeDropdown: true, + hideTargetSwitch: true, + }, + repeat: { + defaultState: INITIAL_OVERVIEW_THEMES_THEME_STATE(), + addButtonLabel: tText( + 'modules/content-page/components/blocks/block-overview-themes/block-overview-themes___voeg-thema-toe', + {}, + [HET_ARCHIEF] + ), + deleteButtonLabel: tText( + 'modules/content-page/components/blocks/block-overview-themes/block-overview-themes___verwijder-thema', + {}, + [HET_ARCHIEF] + ), + }, + }, + }, + }, + block: { + state: INITIAL_OVERVIEW_THEMES_BLOCK_STATE(), + fields: { + ...BLOCK_FIELD_DEFAULTS(), + }, + }, +}); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.scss new file mode 100644 index 000000000..9b48fd620 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.scss @@ -0,0 +1,161 @@ +@use "../../../../shared/styles/settings/variables" as variables; +@use "../../../../shared/styles/settings/colors" as colors; + +.c-block-overview-themes { + // The group band breaks out to a fixed 5000px width for full-bleed coverage; it must stay + // visible here so it isn't clipped by this element's own bounds. + overflow-x: visible; + font-family: SofiaPro, sans-serif; + font-size: 3.2rem; + font-style: normal; + font-weight: 800; + line-height: 4rem; + + &__group { + position: relative; + padding: 3.2rem 2.4rem 0; + + @media (min-width: variables.$g-bp3) { + padding: 4.8rem 4rem 0; + } + } + + // Full-bleed colored band behind the title + first grid row. Its height is measured from + // the real rendered layout (see BlockOverviewThemes.tsx) since a "tall" tile can span from + // row 1 into row 2. The 5000px width is just a generous fixed size to guarantee it always + // covers the viewport, breaking out of any max-width content container. + &__group-band { + position: absolute; + top: 0; + left: -2500px; + z-index: 0; + width: 5000px; + } + + &__group-shape{ + width: 10cqw; + height: 10cqw; + background-color: colors.$white; + position: absolute; + top: 0; + right: 0; + } + + &__group-title { + position: relative; + z-index: 1; + + // Reset so the heading level (h1-h4, chosen for semantics only) never affects the visual style. + margin: 0 0 2.4rem; + color: colors.$color-gray-1000; + + @media (min-width: variables.$g-bp3) { + font-size: 3.2rem; + margin-bottom: 3.2rem; + } + } + + &__grid { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: 1fr; + + // The "snake" tile spans (see getThemeTileSpans.ts) only make sense in a 3-column grid; + // below this breakpoint every tile is forced back to a single stacked column. + @media (min-width: variables.$g-bp2) { + grid-template-columns: repeat(3, 1fr); + grid-auto-rows: 20rem; + } + + @media (min-width: variables.$g-bp4) { + grid-auto-rows: 24rem; + } + } + + &__tile { + position: relative; + display: block; + aspect-ratio: 4/3; + overflow: hidden; + text-decoration: none; + + @media (min-width: variables.$g-bp2) { + // Size comes from the grid row/column spans below instead of a fixed aspect ratio. + aspect-ratio: auto; + grid-column: span 1; + grid-row: span 1; + + &--tall { + grid-row: span 2; + } + + &--wide { + grid-column: span 2; + } + + &--full-width { + grid-column: span 3; + } + } + + // Below the breakpoint the grid collapses to a single column: neutralise any computed + // span so every tile is still a simple, plainly-stacked full-width card. + @media (max-width: variables.$g-bp2 - 1) { + grid-column: span 1 !important; + grid-row: span 1 !important; + } + + &:focus-visible { + outline: 2px solid colors.$color-ocean-green; + outline-offset: 2px; + } + + &:hover &-image > img { + transform: scale(1.08); + } + + &::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgb(0 0 0 / 0%) 40%, rgb(0 0 0 / 72%) 100%); + pointer-events: none; + } + } + + &__tile-image { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + + > img { + object-fit: cover; + object-position: center; + width: 100%; + height: 100%; + transition: transform 0.5s ease-out; + } + } + + &__tile-title { + position: absolute; + bottom: 0; + left: 0; + z-index: 1; + padding: 3rem; + color: colors.$white; + font-family: SofiaPro, sans-serif; + font-size: 1.8rem; + font-style: normal; + font-weight: 800; + line-height: 2.4rem; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.tsx new file mode 100644 index 000000000..d2d6fdbdb --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.tsx @@ -0,0 +1,41 @@ +import clsx from 'clsx'; +import { keyBy } from 'es-toolkit'; +import type { FunctionComponent, ReactElement } from 'react'; +import React, { useMemo } from 'react'; +import type { BlockOverviewThemesProps } from '~content-blocks/BlockOverviewThemes/BlockOverviewThemes.types.tsx'; +import { BlockOverviewThemesGroupSection } from '~content-blocks/BlockOverviewThemes/BlockOverviewThemesGroupSection.tsx'; +import { GET_SECONDARY_BACKGROUND_COLOR_OPTIONS_ARCHIEF } from '~modules/content-page/const/get-color-options'; +import { useGetThemesByIds } from './hooks/useGetThemesByIds'; +import './BlockOverviewThemes.scss'; + +export const BlockOverviewThemes: FunctionComponent = ({ + className, + elements, +}): ReactElement => { + const themeIds = useMemo( + () => + Array.from( + new Set( + (elements || []).flatMap((group) => (group.themes || []).map((theme) => theme.value)) + ) + ), + [elements] + ); + const { data: themes } = useGetThemesByIds(themeIds); + const themesById = useMemo(() => keyBy(themes || [], (theme) => theme.id), [themes]); + + return ( +
+ {(elements || []).map((group, groupIndex) => ( + + ))} +
+ ); +}; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.types.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.types.tsx new file mode 100644 index 000000000..0fde559db --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemes.types.tsx @@ -0,0 +1,21 @@ +import type { HeadingTypeOption } from '~modules/content-page/types/content-block.types.ts'; +import type { Theme } from '~modules/shared/services/themes-service/themes.types'; +import type { DefaultComponentProps } from '~shared/types'; +import type { PickerItem } from '~shared/types/content-picker.ts'; + +export interface BlockOverviewThemesGroup { + title: string; + titleType: HeadingTypeOption; + themes: PickerItem[]; +} + +export interface BlockOverviewThemesProps extends DefaultComponentProps { + elements: BlockOverviewThemesGroup[]; +} + +export interface BlockOverviewThemesGroupSectionProps { + group: BlockOverviewThemesGroup; + groupIndex: number; + themesById: Record; + bandColor: string; +} diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemesGroupSection.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemesGroupSection.tsx new file mode 100644 index 000000000..dca5b7a72 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/BlockOverviewThemesGroupSection.tsx @@ -0,0 +1,155 @@ +import { Image } from '@viaa/avo2-components'; +import clsx from 'clsx'; +import { stringifyUrl } from 'query-string'; +import type { CSSProperties, FunctionComponent } from 'react'; +import React, { useLayoutEffect, useRef, useState } from 'react'; +import type { BlockOverviewThemesGroupSectionProps } from '~content-blocks/BlockOverviewThemes/BlockOverviewThemes.types.tsx'; +import { Link } from '~shared/components/Link/Link'; +import { ROUTE_PARTS } from '~shared/consts/routes'; +import type { Theme } from '~shared/services/themes-service/themes.types'; +import { getThemeTileSpans, type ThemeTileSpan } from './getThemeTileSpans'; +import './BlockOverviewThemes.scss'; + +/** + * Renders a single theme group with a full-bleed colored band behind the title and first grid + * row. Since a "tall" tile can genuinely span from row 1 into row 2, the grid can't be split into + * separate "row 1" and "rest" DOM trees without breaking that span, so the band's height is + * measured from the actual rendered layout instead of hardcoded. + */ +export const BlockOverviewThemesGroupSection: FunctionComponent< + BlockOverviewThemesGroupSectionProps +> = ({ group, groupIndex, themesById, bandColor }) => { + const gridRef = useRef(null); + const [bandHeight, setBandHeight] = useState(null); + + useLayoutEffect(() => { + const gridEl = gridRef.current; + if (!gridEl) { + return; + } + + const measure = () => { + const firstRowHeight = Number.parseFloat( + getComputedStyle(gridEl).gridTemplateRows.split(' ')[0] || '0' + ); + setBandHeight(firstRowHeight ? gridEl.offsetTop + firstRowHeight : null); + }; + + measure(); + const resizeObserver = new ResizeObserver(measure); + resizeObserver.observe(gridEl); + return () => resizeObserver.disconnect(); + }, []); + + // Only 4 combinations occur (see getThemeTileSpans): 1x1 needs no modifier, the rest map to a + // fixed CSS class since `Link` (an app-provided router link) doesn't accept an inline `style`. + const getTileSpanClassName = (span: ThemeTileSpan): string | undefined => { + if (span.colSpan === 1 && span.rowSpan === 2) { + return 'c-block-overview-themes__tile--tall'; + } + if (span.colSpan === 2 && span.rowSpan === 1) { + return 'c-block-overview-themes__tile--wide'; + } + if (span.colSpan === 3 && span.rowSpan === 1) { + return 'c-block-overview-themes__tile--full-width'; + } + return undefined; + }; + + const resolvedThemes = (group.themes || []) + .map((pickerItem) => themesById[pickerItem.value]) + .filter((theme): theme is Theme => !!theme); + const spans = getThemeTileSpans(resolvedThemes.length); + + /** + * Renders the white meemoo logo shapes in the colors bands behind the theme group title and first row + * @param groupIndex + */ + const renderGroupShapes = (groupIndex: number) => { + const rectangleStyles: CSSProperties = { width: '6cqw' }; + const circleStyles: CSSProperties = { borderRadius: '50%' }; + const shapeStyles: [CSSProperties, CSSProperties][] = [ + [circleStyles, rectangleStyles], + [rectangleStyles, circleStyles], + [rectangleStyles, rectangleStyles], + ]; + const positionStyles: [CSSProperties, CSSProperties][] = [ + [ + // Circle + { right: '5%', top: '6rem' }, + // Rectangle + { right: '25%', top: '3rem', transform: 'rotate(35deg)' }, + ], + [ + // Rectangle + { right: '0%', top: '5rem', transform: 'rotate(-45deg)' }, + // Circle + { right: '25%', top: '6rem' }, + ], + [ + // Rectangle + { right: '30%', top: '1rem', transform: 'rotate(85deg)' }, + // Rectangle + { right: '45%', top: '-4rem', transform: 'rotate(20deg)' }, + ], + ]; + return ( + <> +
+
+ + ); + }; + + return ( +
+ {!!bandHeight && ( + <> +
+ {renderGroupShapes(groupIndex)} + + )} + {group.title && + // Rendered as the semantic titleType tag directly (not via BlockHeading) so the + // heading level only affects HTML semantics, never the visual style. + React.createElement( + group.titleType || 'h2', + { className: 'c-block-overview-themes__group-title' }, + group.title + )} +
+ {resolvedThemes.map((theme, tileIndex) => { + const span = spans[tileIndex]; + + return ( + + {theme.nameNl} + {theme.nameNl} + + ); + })} +
+
+ ); +}; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/getThemeTileSpans.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/getThemeTileSpans.ts new file mode 100644 index 000000000..79ff6c5b2 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/getThemeTileSpans.ts @@ -0,0 +1,66 @@ +export interface ThemeTileSpan { + colSpan: 1 | 2 | 3; + rowSpan: 1 | 2; +} + +const SQUARE: ThemeTileSpan = { colSpan: 1, rowSpan: 1 }; +const TALL: ThemeTileSpan = { colSpan: 1, rowSpan: 2 }; +const WIDE: ThemeTileSpan = { colSpan: 2, rowSpan: 1 }; +const FULL_WIDTH: ThemeTileSpan = { colSpan: 3, rowSpan: 1 }; + +/** + * Computes a "braided snake" layout for a 3-column grid: a chain of tall tiles alternates + * between the outer columns (each overlapping the row below), the middle column is always a + * plain square, and the final row closes out the grid with either 2 squares or 1 wide tile — + * whichever exactly uses up the remaining items, so the grid is always a filled rectangle. + * + * This relies on the browser's default (non-dense) CSS grid auto-placement: as long as tiles + * are rendered in the order returned here, each one lands in the correct cell on its own — + * no explicit grid-column/grid-row line numbers are needed. + */ +export const getThemeTileSpans = (count: number): ThemeTileSpan[] => { + if (count <= 0) { + return []; + } + if (count === 1) { + return [FULL_WIDTH]; + } + + const spans: ThemeTileSpan[] = []; + let remaining = count; + + // Row 0: no incoming tall continuation yet. + if (remaining > 3) { + spans.push(SQUARE, SQUARE, TALL); + remaining -= 3; + } else if (remaining === 3) { + spans.push(SQUARE, SQUARE, SQUARE); + return spans; + } else { + // remaining === 2 + spans.push(SQUARE, WIDE); + return spans; + } + + // Subsequent rows: the previous row's tall reserves one outer column, so the next empty + // cell alternates between "the other outer column" and "the middle column" first. + let tallLandsFirst = true; + while (remaining > 2) { + if (tallLandsFirst) { + spans.push(TALL, SQUARE); + } else { + spans.push(SQUARE, TALL); + } + tallLandsFirst = !tallLandsFirst; + remaining -= 2; + } + + if (remaining === 2) { + spans.push(SQUARE, SQUARE); + } else { + // remaining === 1 + spans.push(WIDE); + } + + return spans; +}; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/hooks/useGetThemesByIds.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/hooks/useGetThemesByIds.ts new file mode 100644 index 000000000..f5e07c3c7 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/hooks/useGetThemesByIds.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { ThemesService } from '~shared/services/themes-service/themes.service'; +import type { Theme } from '~shared/services/themes-service/themes.types'; +import { QUERY_KEYS } from '~shared/types'; + +export const useGetThemesByIds = (ids: string[]) => { + return useQuery({ + queryKey: [QUERY_KEYS.GET_THEMES_BY_IDS, ...ids], + queryFn: () => ThemesService.fetchThemesByIds(ids), + enabled: ids.length > 0, + }); +}; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/index.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/index.ts new file mode 100644 index 000000000..e03c509bc --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockOverviewThemes/index.ts @@ -0,0 +1,6 @@ +export { BlockOverviewThemes } from './BlockOverviewThemes'; +export { + INITIAL_OVERVIEW_THEMES_BLOCK_STATE, + INITIAL_OVERVIEW_THEMES_COMPONENTS_STATE, + OVERVIEW_THEMES_BLOCK_CONFIG, +} from './BlockOverviewThemes.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 470d2a55d..fc4595c54 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 @@ -24,6 +24,7 @@ import { KLAAR_BLOCK_CONFIG } from '~content-blocks/BlockKlaar'; import { MAINTAINERS_GRID_BLOCK_CONFIG } from '~content-blocks/BlockMaintainersGrid'; import { MEDIA_GRID_BLOCK_CONFIG } from '~content-blocks/BlockMediaGrid'; import { OVERVIEW_NEWSPAPER_TITLES_BLOCK_CONFIG } from '~content-blocks/BlockOverviewNewspaperTitles'; +import { OVERVIEW_THEMES_BLOCK_CONFIG } from '~content-blocks/BlockOverviewThemes'; import { CONTENT_OVERVIEW_WITH_CAROUSEL_CONFIG } from '~content-blocks/BlockOverviewWithCarousel'; import { QUOTE_BLOCK_CONFIG } from '~content-blocks/BlockQuote'; import { @@ -87,4 +88,5 @@ export const CONTENT_BLOCK_CONFIG_MAP: Record< [ContentBlockType.Breadcrumbs]: CONTENT_BREADCRUMBS_CONFIG, [ContentBlockType.ScrollDownNudge]: CONTENT_SCROLL_DOWN_NUDGE_CONFIG, [ContentBlockType.OverviewWithCarousel]: CONTENT_OVERVIEW_WITH_CAROUSEL_CONFIG, + [ContentBlockType.OverviewThemes]: OVERVIEW_THEMES_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 a864713a5..a44a6e771 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 @@ -24,6 +24,7 @@ import { INITIAL_KLAAR_COMPONENTS_STATE } from '~content-blocks/BlockKlaar'; import { INITIAL_MAINTAINERS_GRID_BLOCK_STATE } from '~content-blocks/BlockMaintainersGrid'; import { INITIAL_MEDIA_GRID_COMPONENTS_STATE } from '~content-blocks/BlockMediaGrid'; import { INITIAL_OVERVIEW_NEWSPAPER_TITLES_BLOCK_STATE } from '~content-blocks/BlockOverviewNewspaperTitles'; +import { INITIAL_OVERVIEW_THEMES_COMPONENTS_STATE } from '~content-blocks/BlockOverviewThemes/BlockOverviewThemes.editorconfig.ts'; import { INITIAL_CONTENT_OVERVIEW_WITH_CAROUSEL_BLOCK_STATE } from '~content-blocks/BlockOverviewWithCarousel/BlockOverviewWithCarousel.editorconfig.ts'; import { INITIAL_PAGE_OVERVIEW_COMPONENTS_STATE } from '~content-blocks/BlockPageOverview'; import { INITIAL_QUOTE_COMPONENTS_STATE } from '~content-blocks/BlockQuote'; @@ -90,4 +91,5 @@ export const CONTENT_BLOCK_INITIAL_STATE_MAP: { [ContentBlockType.Breadcrumbs]: INITIAL_BREADCRUMBS_BLOCK_STATE, [ContentBlockType.ScrollDownNudge]: INITIAL_SCROLL_DOWN_NUDGE_BLOCK_STATE, [ContentBlockType.OverviewWithCarousel]: INITIAL_CONTENT_OVERVIEW_WITH_CAROUSEL_BLOCK_STATE, + [ContentBlockType.OverviewThemes]: INITIAL_OVERVIEW_THEMES_COMPONENTS_STATE, }; diff --git a/ui/src/react-admin/modules/content-page/const/get-color-options.ts b/ui/src/react-admin/modules/content-page/const/get-color-options.ts index d63483f47..2329efa28 100644 --- a/ui/src/react-admin/modules/content-page/const/get-color-options.ts +++ b/ui/src/react-admin/modules/content-page/const/get-color-options.ts @@ -130,18 +130,9 @@ export const GET_BACKGROUND_COLOR_OPTIONS_AVO: () => SelectOption[] = () yellowOption(), ]; -export const GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF: () => SelectOption< +export const GET_SECONDARY_BACKGROUND_COLOR_OPTIONS_ARCHIEF: () => SelectOption< Color | GradientColor | CustomBackground >[] = () => [ - transparentOption(), - whiteOption(), - oceanGreenOption(), - seaGreenOption(), - platinumOption(), - blackOption(), - skyBlueOption(), - meemooLogoOption(), - blackWhiteGradientOption(), oldPinkOption(), lavenderOption(), lilaOption(), @@ -154,6 +145,21 @@ export const GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF: () => SelectOption< mustardOption(), ]; +export const GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF: () => SelectOption< + Color | GradientColor | CustomBackground +>[] = () => [ + transparentOption(), + whiteOption(), + oceanGreenOption(), + seaGreenOption(), + platinumOption(), + blackOption(), + skyBlueOption(), + meemooLogoOption(), + blackWhiteGradientOption(), + ...GET_SECONDARY_BACKGROUND_COLOR_OPTIONS_ARCHIEF(), +]; + export const GET_AVO_HERO_BACKGROUND_COLOR_OPTIONS: () => SelectOption[] = () => [ softBlueOption(), nightBlueOption(), 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 94bcf7c77..5929b9584 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 @@ -184,6 +184,14 @@ export const GET_CONTENT_BLOCK_TYPE_OPTIONS: () => SelectOption[] = () = ), value: ContentBlockType.OverviewWithCarousel, }, + { + label: tText( + "modules/content-page/const/get-content-block-type-options___overzicht-themas", + {}, + [HET_ARCHIEF] + ), + value: ContentBlockType.OverviewThemes, + }, ]; // Only show the content blocks that the client enabled through the config object 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 534dfefaf..4e3cb7ec2 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 @@ -175,6 +175,7 @@ export enum ContentBlockType { AvoImageTextBackground = 'AVO_IMAGE_TEXT_BACKGROUND', // Avo ScrollDownNudge = 'SCROLL_DOWN_NUDGE', OverviewWithCarousel = 'OVERVIEW_WITH_CAROUSEL', + OverviewThemes = 'OVERVIEW_THEMES', } export enum ContentBlockEditor { diff --git a/ui/src/react-admin/modules/shared/components/ContentPicker/ContentPicker.const.ts b/ui/src/react-admin/modules/shared/components/ContentPicker/ContentPicker.const.ts index d1063ec9d..0e0fc5f2d 100644 --- a/ui/src/react-admin/modules/shared/components/ContentPicker/ContentPicker.const.ts +++ b/ui/src/react-admin/modules/shared/components/ContentPicker/ContentPicker.const.ts @@ -11,6 +11,7 @@ import { retrieveContentPages, retrieveProjectContentPages } from './item-provid import { retrieveInternalLinks } from './item-providers/internal-link'; import { retrieveItems } from './item-providers/item'; import { retrieveProfiles } from './item-providers/profile'; +import { retrieveThemes } from './item-providers/theme'; export interface PickerItem { label?: string; @@ -79,6 +80,9 @@ export const GET_CONTENT_TYPE_LABELS: () => Record PickerTypeOption[] = () => { fetch: retrieveIeObjects, picker: 'SELECT', }, + { + value: AvoCoreContentPickerType.IE_OBJECT_THEME, + label: labels[AvoCoreContentPickerType.IE_OBJECT_THEME], + disabled: false, + fetch: retrieveThemes, + picker: 'SELECT', + }, getContentTypeForContentPage(AvoCoreContentPickerType.CONTENT_PAGE_NEWS_ITEM), getContentTypeForContentPage(AvoCoreContentPickerType.CONTENT_PAGE_PAGE), getContentTypeForContentPage(AvoCoreContentPickerType.CONTENT_PAGE_PROJECT), diff --git a/ui/src/react-admin/modules/shared/components/ContentPicker/item-providers/theme.ts b/ui/src/react-admin/modules/shared/components/ContentPicker/item-providers/theme.ts new file mode 100644 index 000000000..398135ad2 --- /dev/null +++ b/ui/src/react-admin/modules/shared/components/ContentPicker/item-providers/theme.ts @@ -0,0 +1,32 @@ +import { AvoCoreContentPickerType } from '@viaa/avo2-types'; +import memoize from 'memoizee'; +import { MEMOIZEE_OPTIONS } from '~shared/consts/memoizee-options'; +import { CustomError } from '~shared/helpers/custom-error'; +import { ThemesService } from '~shared/services/themes-service/themes.service'; +import type { Theme } from '~shared/services/themes-service/themes.types'; +import type { PickerItem } from '~shared/types/content-picker'; +import { parsePickerItem } from '../helpers/parse-picker'; + +export const retrieveThemes = memoize( + async (titleOrId: string | null, limit = 5): Promise => { + try { + const response = await ThemesService.fetchThemes(titleOrId, 0, limit); + return parseThemes(response.items || []); + } catch (err) { + throw new CustomError('Failed to fetch themes for content picker', err, { + titleOrId, + limit, + }); + } + }, + MEMOIZEE_OPTIONS +); + +const parseThemes = (raw: Theme[]): PickerItem[] => { + return raw.map( + (theme: Theme): PickerItem => ({ + label: theme.nameNl, + ...parsePickerItem(AvoCoreContentPickerType.IE_OBJECT_THEME, theme.id), + }) + ); +}; diff --git a/ui/src/react-admin/modules/shared/services/themes-service/themes.service.ts b/ui/src/react-admin/modules/shared/services/themes-service/themes.service.ts new file mode 100644 index 000000000..1553ed269 --- /dev/null +++ b/ui/src/react-admin/modules/shared/services/themes-service/themes.service.ts @@ -0,0 +1,47 @@ +import { stringifyUrl } from 'query-string'; +import { CustomError } from '~shared/helpers/custom-error'; +import { fetchWithLogoutJson } from '~shared/helpers/fetch-with-logout'; +import { getProxyUrl } from '~shared/helpers/get-proxy-url-from-admin-core-config'; +import type { Theme, ThemesResponse } from './themes.types'; + +export class ThemesService { + private static getBaseUrl(): string { + return `${getProxyUrl()}/themes`; + } + + public static async fetchThemes( + searchTerm: string | null = null, + page = 0, + size = 20 + ): Promise { + try { + return await fetchWithLogoutJson( + stringifyUrl({ + url: ThemesService.getBaseUrl(), + query: { + searchTerm: searchTerm || undefined, + page, + size, + }, + }) + ); + } catch (err) { + throw new CustomError('Failed to fetch themes', err, { searchTerm, page, size }); + } + } + + public static async fetchThemesByIds(ids: string[]): Promise { + if (!ids.length) { + return []; + } + try { + // The themes endpoint doesn't support filtering by ids, so we fetch a large page and filter client-side. + // Themes are a bounded editorial taxonomy, so this is expected to stay small. + const response = await ThemesService.fetchThemes(null, 0, 1000); + const idSet = new Set(ids); + return response.items.filter((theme) => idSet.has(theme.id)); + } catch (err) { + throw new CustomError('Failed to fetch themes by ids', err, { ids }); + } + } +} diff --git a/ui/src/react-admin/modules/shared/services/themes-service/themes.types.ts b/ui/src/react-admin/modules/shared/services/themes-service/themes.types.ts new file mode 100644 index 000000000..311f5fe71 --- /dev/null +++ b/ui/src/react-admin/modules/shared/services/themes-service/themes.types.ts @@ -0,0 +1,15 @@ +export interface Theme { + id: string; + slug: string; + nameNl: string; + nameEn: string; + imageUrl: string | null; +} + +export interface ThemesResponse { + items: Theme[]; + total: number; + pages: number; + page: number; + size: number; +} diff --git a/ui/src/react-admin/modules/shared/styles/utilities/_spacer.scss b/ui/src/react-admin/modules/shared/styles/utilities/_spacer.scss index 3f14c38fa..4de544a45 100644 --- a/ui/src/react-admin/modules/shared/styles/utilities/_spacer.scss +++ b/ui/src/react-admin/modules/shared/styles/utilities/_spacer.scss @@ -214,3 +214,17 @@ $g-spacer-unit: 0.8rem !default; .u-padding-bottom-xl { padding-bottom: variables.$g-spacer-unit * 6 !important; } + +// align + +.u-align-right { + margin-left: auto; + margin-right: 0; + display: block !important; +} + +.u-align-left { + margin-left: 0; + margin-right: auto; + display: block !important; +} diff --git a/ui/src/react-admin/modules/shared/types/index.ts b/ui/src/react-admin/modules/shared/types/index.ts index 7e8af39b6..0eb2fc4ab 100644 --- a/ui/src/react-admin/modules/shared/types/index.ts +++ b/ui/src/react-admin/modules/shared/types/index.ts @@ -40,4 +40,6 @@ export enum QUERY_KEYS { GET_IE_OBJECT = 'GET_IE_OBJECT', GET_TABLE_COLUMN_PREFERENCE = 'GET_TABLE_COLUMN_PREFERENCE', GET_NAMES_BY_PROFILE_IDS = 'GET_NAMES_BY_PROFILE_IDS', + GET_THEMES = 'GET_THEMES', + GET_THEMES_BY_IDS = 'GET_THEMES_BY_IDS', } diff --git a/ui/src/shared/helpers/admin-core-config.tsx b/ui/src/shared/helpers/admin-core-config.tsx index c9e68078d..2a59d0a10 100644 --- a/ui/src/shared/helpers/admin-core-config.tsx +++ b/ui/src/shared/helpers/admin-core-config.tsx @@ -97,6 +97,7 @@ export function getAdminCoreConfigForLocalTestApp(navigateFunc: NavigateFunction // ContentBlockType.ContentEncloseGrid, // ContentBlockType.Breadcrumbs, ContentBlockType.OverviewWithCarousel, + ContentBlockType.OverviewThemes, ], defaultPageWidth: ContentPageWidth.LARGE, onSaveContentPage: async (contentPageInfo: ContentPageInfo) => {