From 5b53040f31a4af98a190ea3fbad5663afa15b756 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Wed, 5 Aug 2026 11:07:41 +0200 Subject: [PATCH 1/7] ARC-3788: Start on new search bar --- .../BlockHetArchiefHeaderSearch.tsx | 82 ++++++++----- .../SearchDropdown/SearchDropdown.module.scss | 0 .../SearchDropdown/SearchDropdown.tsx | 112 ++++++++++++++++++ .../hooks/has-access-to-visitor-spaces.ts | 23 ++++ .../services/visit-request.service.ts | 35 ++++++ .../react-admin/modules/shared/types/index.ts | 1 + 6 files changed, 221 insertions(+), 32 deletions(-) create mode 100644 ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.module.scss create mode 100644 ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx create mode 100644 ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/hooks/has-access-to-visitor-spaces.ts create mode 100644 ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/services/visit-request.service.ts diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx index 72ad44495..7c37c1c42 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx @@ -1,8 +1,16 @@ import { TextInput } from '@meemoo/react-components'; +import { AvoSearchOrderDirection } from '@viaa/avo2-types'; import clsx from 'clsx'; import { stringifyUrl } from 'query-string'; -import type { FunctionComponent, KeyboardEvent, ReactElement } from 'react'; -import React, { useEffect, useState } from 'react'; +import React, { + type FunctionComponent, + type KeyboardEvent, + type ReactElement, + useEffect, + useMemo, + useState, +} from 'react'; +import { SearchDropdown } from '~content-blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx'; import { AdminConfigManager } from '~core/config/config.class'; import type { DefaultComponentProps } from '~modules/shared/types/components'; import { Icon } from '~shared/components/Icon/Icon'; @@ -10,6 +18,7 @@ import { navigateFunc } from '~shared/helpers/navigate-fnc'; import { tText } from '~shared/helpers/translation-functions'; import { HET_ARCHIEF } from '~shared/types'; import { BlockHeading } from '../BlockHeading/BlockHeading'; +import { useHasAccessToVisitorSpaces } from './hooks/has-access-to-visitor-spaces.ts'; export interface BlockHetArchiefHeaderSearchProps extends DefaultComponentProps { title: string; @@ -28,6 +37,8 @@ export const BlockHetArchiefHeaderSearch: FunctionComponent(subtitles.length - 1); const [searchTerm, setSearchTerm] = useState(''); + const { data: hasAccessToVisitorSpaces } = useHasAccessToVisitorSpaces(); + useEffect(() => { const timerId = setInterval(() => { // setActive(activeIndex); @@ -73,36 +84,43 @@ export const BlockHetArchiefHeaderSearch: FunctionComponent
- { - if (evt.key === 'Enter') { - await navigateToSearchPage(); - } - }} - type="submit" - aria-label={tText( - 'modules/content-page/components/blocks/block-het-archief-header-search/block-het-archief-header-search___zoek-in-de-publieke-catalogus-input-aria-label', - {}, - [HET_ARCHIEF] - )} - > - - - } - onChange={(evt) => setSearchTerm(evt.target.value)} - onEnter={navigateToSearchPage} - value={searchTerm} - /> +
+ {hasAccessToVisitorSpaces && } + { + if (evt.key === 'Enter') { + await navigateToSearchPage(); + } + }} + type="submit" + aria-label={tText( + 'modules/content-page/components/blocks/block-het-archief-header-search/block-het-archief-header-search___zoek-in-de-publieke-catalogus-input-aria-label', + {}, + [HET_ARCHIEF] + )} + > + + + } + onChange={(evt) => setSearchTerm(evt.target.value)} + onEnter={navigateToSearchPage} + value={searchTerm} + /> +

{textBelowSearch}

diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.module.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx new file mode 100644 index 000000000..59a2fdfb8 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx @@ -0,0 +1,112 @@ +import { keysEnter, onKey } from '@meemoo/react-components'; +import clsx from 'clsx'; +import { type FC, useState } from 'react'; +import { tText } from '~shared/helpers/translation-functions.ts'; +import styles from './SearchDropdown.module.scss'; + +interface SearchDropdownOption { + id: string; + label: string; + selectedLabel: string; +} + +export const SearchDropdown: FC = () => { + const SEARCH_OPTIONS: SearchDropdownOption[] = [ + { + id: 'search-all', + selectedLabel: tText('Alles'), + label: tText('Zoek in alle objecten'), + }, + { + id: 'search-audio', + selectedLabel: tText('Alle audio'), + label: tText('Zoek in audio'), + }, + { + id: 'search-video', + selectedLabel: tText('Alle video'), + label: tText('Zoek in video'), + }, + { + id: 'search-newspapers', + selectedLabel: tText('Alle kranten'), + label: tText('Zoek in kranten'), + }, + ]; + + const [isOpen, setIsOpen] = useState(false); + const [selectedOptionId, setSelectedOptionId] = useState(SEARCH_OPTIONS[0].id); + + const onClickDropdown = (): void => { + setIsOpen((prevIsOpen: boolean) => !prevIsOpen); + }; + + const onSelectOption = (selectedOption: SearchDropdownOption): void => { + setIsOpen(false); + setSelectedOptionId(selectedOption.id); + }; + + const renderSelectedOption = () => { + const selected = SEARCH_OPTIONS.find(({ id }: SearchDropdownOption) => id === selectedOptionId); + + const actionProps = { + tabIndex: 0, + role: 'button', + 'aria-expanded': isOpen, + 'aria-controls': 'list-controls', + onClick: onClickDropdown, + // biome-ignore lint/suspicious/noExplicitAny: No typing yet + onKeyDown: (evt: any) => onKey(evt, [...keysEnter], onClickDropdown), + }; + + return ( +
  • +
    +

    + {selected?.selectedLabel} +

    +
    +
  • + ); + }; + + const renderAllOptions = () => ( +
  • +
      + {SEARCH_OPTIONS.map((option: SearchDropdownOption) => ( + // biome-ignore lint/a11y/useAriaPropsSupportedByRole: because it works? +
    • onSelectOption(option)} + onKeyDown={(e) => onKey(e, [...keysEnter], () => onSelectOption(option))} + className={clsx(styles['c-visitor-spaces-dropdown__option'])} + > +

      + {option.label} +

      +
    • + ))} +
    +
  • + ); + + return ( +
      + {renderSelectedOption()} + {renderAllOptions()} +
    + ); +}; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/hooks/has-access-to-visitor-spaces.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/hooks/has-access-to-visitor-spaces.ts new file mode 100644 index 000000000..5b0ecf8ae --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/hooks/has-access-to-visitor-spaces.ts @@ -0,0 +1,23 @@ +import { keepPreviousData, useQuery, type UseQueryResult } from '@tanstack/react-query'; +import { getCommonUser } from '~core/config/config.selectors.ts'; +import { QUERY_KEYS } from '~shared/types'; +import { VisitRequestService } from '../services/visit-request.service'; + +async function hasAccessToVisitorSpaces(): Promise { + const commonUser = getCommonUser(); + + if (!commonUser?.userGroup?.id) { + return false; + } + + return VisitRequestService.hasAccessToVisitorSpaces(); +} + +export function useHasAccessToVisitorSpaces(enabled?: boolean): UseQueryResult { + return useQuery({ + queryKey: [QUERY_KEYS.GET_VISIT_REQUESTS], + queryFn: () => hasAccessToVisitorSpaces(), + placeholderData: keepPreviousData, + enabled, + }); +} diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/services/visit-request.service.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/services/visit-request.service.ts new file mode 100644 index 000000000..4b7ea8247 --- /dev/null +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/services/visit-request.service.ts @@ -0,0 +1,35 @@ +import type { IPagination } from '@studiohyperdrive/pagination'; +import { AvoSearchOrderDirection } from '@viaa/avo2-types'; +import { stringifyUrl } from 'query-string'; +import { CustomError } from '~shared/helpers/custom-error.ts'; +import { fetchWithLogoutJson } from '~shared/helpers/fetch-with-logout.ts'; +import { getProxyUrl } from '~shared/helpers/get-proxy-url-from-admin-core-config.ts'; + +export class VisitRequestService { + private static getBaseUrl(): string { + return `${getProxyUrl()}/visits`; + } + + public static async hasAccessToVisitorSpaces(): Promise { + try { + const result = await fetchWithLogoutJson>( + stringifyUrl({ + url: `${VisitRequestService.getBaseUrl()}/personal`, + query: { + page: 1, + size: 100, + orderProp: 'startAt', + orderDirection: AvoSearchOrderDirection.DESC, + status: 'APPROVED', + timeframe: 'ACTIVE', + }, + }) + ); + + console.log('result', result); + return (result.items?.length || 0) > 0; + } catch (err) { + throw new CustomError('Failed to fetch visitor spaces', err); + } + } +} diff --git a/ui/src/react-admin/modules/shared/types/index.ts b/ui/src/react-admin/modules/shared/types/index.ts index 6ecd65edc..4e18e6e7a 100644 --- a/ui/src/react-admin/modules/shared/types/index.ts +++ b/ui/src/react-admin/modules/shared/types/index.ts @@ -43,4 +43,5 @@ export enum QUERY_KEYS { GET_THEMES = 'GET_THEMES', GET_THEMES_BY_IDS = 'GET_THEMES_BY_IDS', GET_THEME_WITH_OBJECTS = 'GET_THEME_WITH_OBJECTS', + GET_VISIT_REQUESTS = 'GET_VISIT_REQUESTS', } From a8ed51e5870b0094c6b6a169631fd163e6cc1fc8 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Wed, 5 Aug 2026 12:03:31 +0200 Subject: [PATCH 2/7] ARC-3788: Start implementing the design --- .../SearchDropdown/SearchDropdown.module.scss | 127 ++++++++++++++++++ .../SearchDropdown/SearchDropdown.tsx | 22 +-- 2 files changed, 139 insertions(+), 10 deletions(-) diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.module.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.module.scss index e69de29bb..b60512fb6 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.module.scss +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.module.scss @@ -0,0 +1,127 @@ +@use "../../../../../shared/styles/settings/variables" as variables; +@use "../../../../../shared/styles/settings/colors" as colors; + +$component: 'c-search-dropdown'; +$height: 4.8rem; // matches the height of the joined search input (see hetarchief-client's .c-input__field) +$border-width: 0.2rem; +$border-radius: 4rem; // matches the joined search input's border-radius (desktop, left corners) +$border-radius-sm: 2.4rem; // mobile-only radius for the pill's top corners +$animate-default: 200ms cubic-bezier(0.4, 0, 0.2, 1); + +.#{$component} { + position: relative; + width: 100%; + + @media (min-width: variables.$g-bp2) { + width: 26.5rem; + } + + &__active { + display: flex; + align-items: center; + justify-content: space-between; + gap: variables.$g-spacer-unit * 2; + height: $height; + padding: (variables.$g-spacer-unit * 2) (variables.$g-spacer-unit * 4); + background-color: colors.$color-gray-1000; + color: colors.$white; + border: $border-width solid colors.$color-platinum; + border-bottom: none; + border-radius: $border-radius-sm $border-radius-sm 0 0; + transition: border-radius $animate-default; + + @media (min-width: variables.$g-bp2) { + border-radius: $border-radius 0 0 $border-radius; + border-bottom: $border-width solid colors.$color-platinum; + border-right: none; + } + + &:focus, + &:focus-within { + outline: 0.2rem solid colors.$color-ocean-green; + outline-offset: -0.2rem; + } + + &-content { + overflow: hidden; + } + + &-label { + font-size: 1.8rem; + font-weight: 700; + line-height: 2rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &-icon { + flex-shrink: 0; + font-size: 2.4rem; + transform: rotate(0); + transition: transform $animate-default; + } + } + + &__list { + position: absolute; + top: 100%; + left: 0; + width: 100%; + background-color: colors.$white; + opacity: 0; + box-shadow: 0 0 24px rgb(0 0 0 / 12%); + transition: opacity $animate-default; + pointer-events: none; + z-index: 1; + } + + &__option { + width: 100%; + + &-label { + padding: 1.2rem (variables.$g-spacer-unit * 4); + overflow: hidden; + font-size: 1.6rem; + color: colors.$color-gray-1000; + } + + &:hover, + &:active, + &:focus { + background-color: colors.$color-platinum; + } + + &:focus, + &:focus-within { + outline: 0.2rem solid colors.$color-ocean-green; + } + } + + &--open { + .#{$component} { + &__list { + opacity: 1; + pointer-events: all; + } + + &__active { + border-radius: $border-radius-sm $border-radius-sm 0 0; + + @media (min-width: variables.$g-bp2) { + border-radius: $border-radius 0 0 0; + } + + &-icon { + transform: rotate(-180deg); + } + } + } + } + + &--selectable { + &:hover { + cursor: pointer; + } + } +} diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx index 59a2fdfb8..0f7759018 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx @@ -1,6 +1,7 @@ import { keysEnter, onKey } from '@meemoo/react-components'; import clsx from 'clsx'; import { type FC, useState } from 'react'; +import { Icon } from '~shared/components/Icon/Icon'; import { tText } from '~shared/helpers/translation-functions.ts'; import styles from './SearchDropdown.module.scss'; @@ -60,12 +61,13 @@ export const SearchDropdown: FC = () => { }; return ( -
  • -
    -

    +

  • +
    +

    {selected?.selectedLabel}

    +
  • ); }; @@ -73,8 +75,8 @@ export const SearchDropdown: FC = () => { const renderAllOptions = () => (
    • {SEARCH_OPTIONS.map((option: SearchDropdownOption) => ( @@ -85,10 +87,10 @@ export const SearchDropdown: FC = () => { aria-selected={selectedOptionId === option.id} onClick={() => onSelectOption(option)} onKeyDown={(e) => onKey(e, [...keysEnter], () => onSelectOption(option))} - className={clsx(styles['c-visitor-spaces-dropdown__option'])} + className={clsx(styles['c-search-dropdown__option'])} >

      {option.label}

      @@ -100,9 +102,9 @@ export const SearchDropdown: FC = () => { return (
        {renderSelectedOption()} From 8775fa8c1167dfa35f1ce54bb19e57fcf91ce9b4 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Wed, 5 Aug 2026 13:44:36 +0200 Subject: [PATCH 3/7] ARC-3788: Move styling to client --- .../BlockHetArchiefHeaderSearch.tsx | 6 +- .../{SearchDropdown => }/SearchDropdown.tsx | 28 ++-- .../SearchDropdown/SearchDropdown.module.scss | 127 ------------------ 3 files changed, 12 insertions(+), 149 deletions(-) rename ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/{SearchDropdown => }/SearchDropdown.tsx (72%) delete mode 100644 ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.module.scss diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx index 7c37c1c42..6b55236d1 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx @@ -1,5 +1,4 @@ import { TextInput } from '@meemoo/react-components'; -import { AvoSearchOrderDirection } from '@viaa/avo2-types'; import clsx from 'clsx'; import { stringifyUrl } from 'query-string'; import React, { @@ -7,10 +6,8 @@ import React, { type KeyboardEvent, type ReactElement, useEffect, - useMemo, useState, } from 'react'; -import { SearchDropdown } from '~content-blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx'; import { AdminConfigManager } from '~core/config/config.class'; import type { DefaultComponentProps } from '~modules/shared/types/components'; import { Icon } from '~shared/components/Icon/Icon'; @@ -19,6 +16,7 @@ import { tText } from '~shared/helpers/translation-functions'; import { HET_ARCHIEF } from '~shared/types'; import { BlockHeading } from '../BlockHeading/BlockHeading'; import { useHasAccessToVisitorSpaces } from './hooks/has-access-to-visitor-spaces.ts'; +import { SearchDropdown } from './SearchDropdown.tsx'; export interface BlockHetArchiefHeaderSearchProps extends DefaultComponentProps { title: string; @@ -121,7 +119,7 @@ export const BlockHetArchiefHeaderSearch: FunctionComponent -

        {textBelowSearch}

        + {textBelowSearch &&

        {textBelowSearch}

        } ); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown.tsx similarity index 72% rename from ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx rename to ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown.tsx index 0f7759018..4732836bf 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown.tsx @@ -3,7 +3,6 @@ import clsx from 'clsx'; import { type FC, useState } from 'react'; import { Icon } from '~shared/components/Icon/Icon'; import { tText } from '~shared/helpers/translation-functions.ts'; -import styles from './SearchDropdown.module.scss'; interface SearchDropdownOption { id: string; @@ -61,13 +60,11 @@ export const SearchDropdown: FC = () => { }; return ( -
      • -
        -

        - {selected?.selectedLabel} -

        +
      • +
        +

        {selected?.selectedLabel}

        - +
      • ); }; @@ -75,8 +72,8 @@ export const SearchDropdown: FC = () => { const renderAllOptions = () => (
        • {SEARCH_OPTIONS.map((option: SearchDropdownOption) => ( @@ -87,13 +84,9 @@ export const SearchDropdown: FC = () => { aria-selected={selectedOptionId === option.id} onClick={() => onSelectOption(option)} onKeyDown={(e) => onKey(e, [...keysEnter], () => onSelectOption(option))} - className={clsx(styles['c-search-dropdown__option'])} + className="c-search-dropdown__option" > -

          - {option.label} -

          +

          {option.label}

          ))}
        @@ -102,9 +95,8 @@ export const SearchDropdown: FC = () => { return (
          {renderSelectedOption()} diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.module.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.module.scss deleted file mode 100644 index b60512fb6..000000000 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown/SearchDropdown.module.scss +++ /dev/null @@ -1,127 +0,0 @@ -@use "../../../../../shared/styles/settings/variables" as variables; -@use "../../../../../shared/styles/settings/colors" as colors; - -$component: 'c-search-dropdown'; -$height: 4.8rem; // matches the height of the joined search input (see hetarchief-client's .c-input__field) -$border-width: 0.2rem; -$border-radius: 4rem; // matches the joined search input's border-radius (desktop, left corners) -$border-radius-sm: 2.4rem; // mobile-only radius for the pill's top corners -$animate-default: 200ms cubic-bezier(0.4, 0, 0.2, 1); - -.#{$component} { - position: relative; - width: 100%; - - @media (min-width: variables.$g-bp2) { - width: 26.5rem; - } - - &__active { - display: flex; - align-items: center; - justify-content: space-between; - gap: variables.$g-spacer-unit * 2; - height: $height; - padding: (variables.$g-spacer-unit * 2) (variables.$g-spacer-unit * 4); - background-color: colors.$color-gray-1000; - color: colors.$white; - border: $border-width solid colors.$color-platinum; - border-bottom: none; - border-radius: $border-radius-sm $border-radius-sm 0 0; - transition: border-radius $animate-default; - - @media (min-width: variables.$g-bp2) { - border-radius: $border-radius 0 0 $border-radius; - border-bottom: $border-width solid colors.$color-platinum; - border-right: none; - } - - &:focus, - &:focus-within { - outline: 0.2rem solid colors.$color-ocean-green; - outline-offset: -0.2rem; - } - - &-content { - overflow: hidden; - } - - &-label { - font-size: 1.8rem; - font-weight: 700; - line-height: 2rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &-icon { - flex-shrink: 0; - font-size: 2.4rem; - transform: rotate(0); - transition: transform $animate-default; - } - } - - &__list { - position: absolute; - top: 100%; - left: 0; - width: 100%; - background-color: colors.$white; - opacity: 0; - box-shadow: 0 0 24px rgb(0 0 0 / 12%); - transition: opacity $animate-default; - pointer-events: none; - z-index: 1; - } - - &__option { - width: 100%; - - &-label { - padding: 1.2rem (variables.$g-spacer-unit * 4); - overflow: hidden; - font-size: 1.6rem; - color: colors.$color-gray-1000; - } - - &:hover, - &:active, - &:focus { - background-color: colors.$color-platinum; - } - - &:focus, - &:focus-within { - outline: 0.2rem solid colors.$color-ocean-green; - } - } - - &--open { - .#{$component} { - &__list { - opacity: 1; - pointer-events: all; - } - - &__active { - border-radius: $border-radius-sm $border-radius-sm 0 0; - - @media (min-width: variables.$g-bp2) { - border-radius: $border-radius 0 0 0; - } - - &-icon { - transform: rotate(-180deg); - } - } - } - } - - &--selectable { - &:hover { - cursor: pointer; - } - } -} From ac5f1fcd35d194f80d0c824526c1fee5b9a24ff1 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Wed, 5 Aug 2026 15:02:16 +0200 Subject: [PATCH 4/7] ARC-3788: Pass mediatype when searching --- .../BlockHetArchiefHeaderSearch.tsx | 41 ++++++++++++++-- .../SearchDropdown.tsx | 49 +++++++------------ 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx index 6b55236d1..27a14a2b7 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx @@ -16,7 +16,7 @@ import { tText } from '~shared/helpers/translation-functions'; import { HET_ARCHIEF } from '~shared/types'; import { BlockHeading } from '../BlockHeading/BlockHeading'; import { useHasAccessToVisitorSpaces } from './hooks/has-access-to-visitor-spaces.ts'; -import { SearchDropdown } from './SearchDropdown.tsx'; +import { SearchDropdown, type SearchDropdownOption } from './SearchDropdown.tsx'; export interface BlockHetArchiefHeaderSearchProps extends DefaultComponentProps { title: string; @@ -32,8 +32,32 @@ export const BlockHetArchiefHeaderSearch: FunctionComponent { + const SEARCH_OPTIONS: SearchDropdownOption[] = [ + { + id: 'all', + selectedLabel: tText('Alles'), + label: tText('Zoek in alle objecten'), + }, + { + id: 'audio', + selectedLabel: tText('Alle audio'), + label: tText('Zoek in audio'), + }, + { + id: 'video', + selectedLabel: tText('Alle video'), + label: tText('Zoek in video'), + }, + { + id: 'newspaper', + selectedLabel: tText('Alle kranten'), + label: tText('Zoek in kranten'), + }, + ]; + const [activeIndex, setActiveIndex] = useState(subtitles.length - 1); const [searchTerm, setSearchTerm] = useState(''); + const [mediaType, setMediaType] = useState(SEARCH_OPTIONS[0].id); const { data: hasAccessToVisitorSpaces } = useHasAccessToVisitorSpaces(); @@ -53,7 +77,12 @@ export const BlockHetArchiefHeaderSearch: FunctionComponent { const url = stringifyUrl({ url: AdminConfigManager.getConfig().routes.SEARCH || '/zoeken', - query: searchTerm ? { zoekterm: searchTerm } : {}, + query: searchTerm + ? { + zoekterm: searchTerm, + format: mediaType, + } + : {}, }); await navigateFunc(url); }; @@ -87,7 +116,13 @@ export const BlockHetArchiefHeaderSearch: FunctionComponent - {hasAccessToVisitorSpaces && } + {hasAccessToVisitorSpaces && ( + setMediaType(selectedOption.id)} + /> + )} { - const SEARCH_OPTIONS: SearchDropdownOption[] = [ - { - id: 'search-all', - selectedLabel: tText('Alles'), - label: tText('Zoek in alle objecten'), - }, - { - id: 'search-audio', - selectedLabel: tText('Alle audio'), - label: tText('Zoek in audio'), - }, - { - id: 'search-video', - selectedLabel: tText('Alle video'), - label: tText('Zoek in video'), - }, - { - id: 'search-newspapers', - selectedLabel: tText('Alle kranten'), - label: tText('Zoek in kranten'), - }, - ]; +interface SearchDropdownProps { + options: SearchDropdownOption[]; + selectedOptionId: string; + onSelectOption: (selectedOption: SearchDropdownOption) => void; +} +export const SearchDropdown: FC = ({ + options, + selectedOptionId, + onSelectOption, +}) => { const [isOpen, setIsOpen] = useState(false); - const [selectedOptionId, setSelectedOptionId] = useState(SEARCH_OPTIONS[0].id); const onClickDropdown = (): void => { setIsOpen((prevIsOpen: boolean) => !prevIsOpen); }; - const onSelectOption = (selectedOption: SearchDropdownOption): void => { + const handleSelectOption = (selectedOption: SearchDropdownOption): void => { setIsOpen(false); - setSelectedOptionId(selectedOption.id); + onSelectOption(selectedOption); }; const renderSelectedOption = () => { - const selected = SEARCH_OPTIONS.find(({ id }: SearchDropdownOption) => id === selectedOptionId); + const selected = options.find(({ id }: SearchDropdownOption) => id === selectedOptionId); const actionProps = { tabIndex: 0, @@ -76,14 +61,14 @@ export const SearchDropdown: FC = () => { 'c-search-dropdown__list--open': isOpen, })} > - {SEARCH_OPTIONS.map((option: SearchDropdownOption) => ( + {options.map((option: SearchDropdownOption) => ( // biome-ignore lint/a11y/useAriaPropsSupportedByRole: because it works?
        • onSelectOption(option)} - onKeyDown={(e) => onKey(e, [...keysEnter], () => onSelectOption(option))} + onClick={() => handleSelectOption(option)} + onKeyDown={(e) => onKey(e, [...keysEnter], () => handleSelectOption(option))} className="c-search-dropdown__option" >

          {option.label}

          From bbd927e867b0636e73e4f8f437fdb18fb62b0c3e Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Wed, 5 Aug 2026 17:59:49 +0200 Subject: [PATCH 5/7] ARC-3788: Remove checks on visitor space since the options should always be there --- .../BlockHetArchiefHeaderSearch.tsx | 29 ++++++++------- .../SearchDropdown.tsx | 2 +- .../hooks/has-access-to-visitor-spaces.ts | 23 ------------ .../services/visit-request.service.ts | 35 ------------------- 4 files changed, 15 insertions(+), 74 deletions(-) delete mode 100644 ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/hooks/has-access-to-visitor-spaces.ts delete mode 100644 ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/services/visit-request.service.ts diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx index 27a14a2b7..113888ddb 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx @@ -15,7 +15,6 @@ import { navigateFunc } from '~shared/helpers/navigate-fnc'; import { tText } from '~shared/helpers/translation-functions'; import { HET_ARCHIEF } from '~shared/types'; import { BlockHeading } from '../BlockHeading/BlockHeading'; -import { useHasAccessToVisitorSpaces } from './hooks/has-access-to-visitor-spaces.ts'; import { SearchDropdown, type SearchDropdownOption } from './SearchDropdown.tsx'; export interface BlockHetArchiefHeaderSearchProps extends DefaultComponentProps { @@ -59,8 +58,6 @@ export const BlockHetArchiefHeaderSearch: FunctionComponent(''); const [mediaType, setMediaType] = useState(SEARCH_OPTIONS[0].id); - const { data: hasAccessToVisitorSpaces } = useHasAccessToVisitorSpaces(); - useEffect(() => { const timerId = setInterval(() => { // setActive(activeIndex); @@ -75,14 +72,17 @@ export const BlockHetArchiefHeaderSearch: FunctionComponent { + const baseQuery = { + format: mediaType, + }; const url = stringifyUrl({ url: AdminConfigManager.getConfig().routes.SEARCH || '/zoeken', query: searchTerm ? { + ...baseQuery, zoekterm: searchTerm, - format: mediaType, } - : {}, + : baseQuery, }); await navigateFunc(url); }; @@ -112,17 +112,16 @@ export const BlockHetArchiefHeaderSearch: FunctionComponent
          - {hasAccessToVisitorSpaces && ( - setMediaType(selectedOption.id)} - /> + className={clsx( + 'c-block-het-archief-header-search__searchbar', + 'c-block-het-archief-header-search__searchbar--has-dropdown' )} + > + setMediaType(selectedOption.id)} + /> = ({ {options.map((option: SearchDropdownOption) => ( // biome-ignore lint/a11y/useAriaPropsSupportedByRole: because it works?
        • handleSelectOption(option)} diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/hooks/has-access-to-visitor-spaces.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/hooks/has-access-to-visitor-spaces.ts deleted file mode 100644 index 5b0ecf8ae..000000000 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/hooks/has-access-to-visitor-spaces.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { keepPreviousData, useQuery, type UseQueryResult } from '@tanstack/react-query'; -import { getCommonUser } from '~core/config/config.selectors.ts'; -import { QUERY_KEYS } from '~shared/types'; -import { VisitRequestService } from '../services/visit-request.service'; - -async function hasAccessToVisitorSpaces(): Promise { - const commonUser = getCommonUser(); - - if (!commonUser?.userGroup?.id) { - return false; - } - - return VisitRequestService.hasAccessToVisitorSpaces(); -} - -export function useHasAccessToVisitorSpaces(enabled?: boolean): UseQueryResult { - return useQuery({ - queryKey: [QUERY_KEYS.GET_VISIT_REQUESTS], - queryFn: () => hasAccessToVisitorSpaces(), - placeholderData: keepPreviousData, - enabled, - }); -} diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/services/visit-request.service.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/services/visit-request.service.ts deleted file mode 100644 index 4b7ea8247..000000000 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/services/visit-request.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { IPagination } from '@studiohyperdrive/pagination'; -import { AvoSearchOrderDirection } from '@viaa/avo2-types'; -import { stringifyUrl } from 'query-string'; -import { CustomError } from '~shared/helpers/custom-error.ts'; -import { fetchWithLogoutJson } from '~shared/helpers/fetch-with-logout.ts'; -import { getProxyUrl } from '~shared/helpers/get-proxy-url-from-admin-core-config.ts'; - -export class VisitRequestService { - private static getBaseUrl(): string { - return `${getProxyUrl()}/visits`; - } - - public static async hasAccessToVisitorSpaces(): Promise { - try { - const result = await fetchWithLogoutJson>( - stringifyUrl({ - url: `${VisitRequestService.getBaseUrl()}/personal`, - query: { - page: 1, - size: 100, - orderProp: 'startAt', - orderDirection: AvoSearchOrderDirection.DESC, - status: 'APPROVED', - timeframe: 'ACTIVE', - }, - }) - ); - - console.log('result', result); - return (result.items?.length || 0) > 0; - } catch (err) { - throw new CustomError('Failed to fetch visitor spaces', err); - } - } -} From dc16899e8173ce0da7685e4d688f8f3681f5d984 Mon Sep 17 00:00:00 2001 From: Femke Reunes Date: Mon, 10 Aug 2026 12:52:15 +0200 Subject: [PATCH 6/7] ARC-3799: Fix order of dropdown --- .../BlockHetArchiefHeaderSearch.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx index 113888ddb..7f2baf26d 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/BlockHetArchiefHeaderSearch.tsx @@ -37,16 +37,16 @@ export const BlockHetArchiefHeaderSearch: FunctionComponent Date: Mon, 10 Aug 2026 13:46:10 +0200 Subject: [PATCH 7/7] ARC-3788: Refactor dropdown to be a dropdown --- .../SearchDropdown.tsx | 129 ++++++++++-------- 1 file changed, 71 insertions(+), 58 deletions(-) diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown.tsx index 0338b7093..c5c046564 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefHeaderSearch/SearchDropdown.tsx @@ -1,4 +1,11 @@ -import { keysEnter, onKey } from '@meemoo/react-components'; +import { + Dropdown, + DropdownButton, + DropdownContent, + keysEnter, + keysSpacebar, + onKey, +} from '@meemoo/react-components'; import clsx from 'clsx'; import { type FC, useState } from 'react'; import { Icon } from '~shared/components/Icon/Icon'; @@ -15,6 +22,8 @@ interface SearchDropdownProps { onSelectOption: (selectedOption: SearchDropdownOption) => void; } +const DROPDOWN_ID = 'search-dropdown-options'; + export const SearchDropdown: FC = ({ options, selectedOptionId, @@ -22,70 +31,74 @@ export const SearchDropdown: FC = ({ }) => { const [isOpen, setIsOpen] = useState(false); - const onClickDropdown = (): void => { - setIsOpen((prevIsOpen: boolean) => !prevIsOpen); - }; - const handleSelectOption = (selectedOption: SearchDropdownOption): void => { setIsOpen(false); onSelectOption(selectedOption); }; - const renderSelectedOption = () => { - const selected = options.find(({ id }: SearchDropdownOption) => id === selectedOptionId); - - const actionProps = { - tabIndex: 0, - role: 'button', - 'aria-expanded': isOpen, - 'aria-controls': 'list-controls', - onClick: onClickDropdown, - // biome-ignore lint/suspicious/noExplicitAny: No typing yet - onKeyDown: (evt: any) => onKey(evt, [...keysEnter], onClickDropdown), - }; - - return ( -
        • -
          -

          {selected?.selectedLabel}

          -
          - -
        • - ); - }; - - const renderAllOptions = () => ( -
        • -
            - {options.map((option: SearchDropdownOption) => ( - // biome-ignore lint/a11y/useAriaPropsSupportedByRole: because it works? -
          • handleSelectOption(option)} - onKeyDown={(e) => onKey(e, [...keysEnter], () => handleSelectOption(option))} - className="c-search-dropdown__option" - > -

            {option.label}

            -
          • - ))} -
          -
        • - ); + const selected = options.find(({ id }: SearchDropdownOption) => id === selectedOptionId); return ( -
            setIsOpen(true)} + onClose={() => setIsOpen(false)} + menuClassName="c-search-dropdown" + triggerClassName="c-search-dropdown__trigger" + flyoutClassName="c-search-dropdown__flyout" + menuWidth="fit-trigger" + placement="bottom-start" + offset={0} > - {renderSelectedOption()} - {renderAllOptions()} -
          + + + + + {/* biome-ignore lint/a11y/useSemanticElements: Dropdown options */} + {/* biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: Dropdown options */} +
            + {options.map((option: SearchDropdownOption) => ( +
          • handleSelectOption(option)} + onKeyDown={(e) => + onKey(e, [...keysEnter, ...keysSpacebar], () => { + if (keysSpacebar.includes(e.key)) { + e.preventDefault(); + } + handleSelectOption(option); + }) + } + className="c-search-dropdown__option" + > +

            {option.label}

            +
          • + ))} +
          +
          + ); };