From cb294938ef30657df2ff39941db805293f1a25ba Mon Sep 17 00:00:00 2001 From: Tim Hostetler <6970899+thostetler@users.noreply.github.com> Date: Thu, 21 May 2026 12:56:11 -0400 Subject: [PATCH 1/2] feat: add SearchModifierBar with ADS Compatibility mode Adds a search mode selector that scopes results to astronomy and physics content (fq_database filter + date sort) when ADS Compatibility mode is active. Mode persists in a scix_prefs cookie seeded by middleware on legacy ADS referrer detection. - SearchModifierBar component (landing page only, ASTROPHYSICS mode gated) - SearchMode enum, applySearchModeDefaults, buildSearchOutgoing helpers - scix_prefs cookie read/write utilities (prefs-cookie.ts) - useSearchMode hook backed by Zustand appMode slice - Middleware seeds cookie on /astrophysics discipline routes and ADS referrer - SSR reads cookie to hydrate mode/searchMode before first render - ADS referrer redirects to /?fromADS=true; landing page shows info toast - store.ts: synchronous mode sync on navigation, searchMode excluded from navigation merge to preserve user changes --- .../AbstractSearchForm/AbstractSearchForm.tsx | 18 ++--- src/components/ClassicForm/ClassicForm.tsx | 5 +- .../SearchModifierBar/SearchModifierBar.tsx | 76 +++++++++++++++++++ src/components/SearchModifierBar/index.ts | 1 + .../SearchQueryLink/SearchQueryLink.tsx | 21 +++-- src/config.ts | 2 +- src/lib/useSearchMode.ts | 7 ++ src/middleware.ts | 51 +++++++++++-- src/pages/_app.tsx | 21 ++++- src/pages/_document.tsx | 2 +- src/pages/index.tsx | 32 +++++++- src/pages/search/index.tsx | 27 +++++-- src/providers.tsx | 15 ++-- src/ssr-utils.ts | 59 ++++++-------- src/store/slices/appMode.ts | 20 ++++- src/store/store.ts | 29 ++++++- src/types.ts | 1 - src/utils/common/prefs-cookie.ts | 38 ++++++++++ src/utils/common/searchMode.ts | 62 +++++++++++++++ tsconfig.json | 2 +- 20 files changed, 394 insertions(+), 95 deletions(-) create mode 100644 src/components/SearchModifierBar/SearchModifierBar.tsx create mode 100644 src/components/SearchModifierBar/index.ts create mode 100644 src/lib/useSearchMode.ts create mode 100644 src/utils/common/prefs-cookie.ts create mode 100644 src/utils/common/searchMode.ts diff --git a/src/components/AbstractSearchForm/AbstractSearchForm.tsx b/src/components/AbstractSearchForm/AbstractSearchForm.tsx index 49db29c85..6fbe1879f 100644 --- a/src/components/AbstractSearchForm/AbstractSearchForm.tsx +++ b/src/components/AbstractSearchForm/AbstractSearchForm.tsx @@ -8,17 +8,16 @@ import router from 'next/router'; import { applyFiltersToQuery } from '../SearchFacet/helpers'; import { DatabaseEnum, IADSApiUserDataResponse } from '@/api/user/types'; import { SolrSort } from '@/api/models'; +import { buildSearchOutgoing } from '@/utils/common/searchMode'; +import { useSearchMode } from '@/lib/useSearchMode'; export const AbstractSearchForm = () => { const { settings } = useSettings(); const submitQuery = useStore((state) => state.submitQuery); const sort = [`${settings.preferredSearchSort} desc` as SolrSort]; const query = useStore((state) => state.query.q); + const [searchMode] = useSearchMode(); - /** - * Take in a query object and apply any FQ filters - * These will either be any default ON filters or whatever has been set by the user in the preferences - */ const applyDefaultFilters = useCallback( (query: IADSApiSearchParams) => { const defaultDatabases = getListOfAppliedDefaultDatabases(settings.defaultDatabase); @@ -35,18 +34,12 @@ export const AbstractSearchForm = () => { [settings.defaultDatabase], ); - /** - * Get a list of default databases that have been applied - * @param databases - */ const getListOfAppliedDefaultDatabases = (databases: IADSApiUserDataResponse['defaultDatabase']): Array => { const defaultDatabases = []; for (const db of databases) { - // if All is selected, exit early here and return an empty array (no filters to apply) if (db.name === DatabaseEnum.All && db.value) { return []; } - if (db.value) { defaultDatabases.push(db.name); } @@ -62,13 +55,14 @@ export const AbstractSearchForm = () => { if (query && query.trim().length > 0) { submitQuery(); const defaultedQuery = applyDefaultFilters({ q: query, sort, p: 1 }) as IADSApiSearchParams; + const outgoing = buildSearchOutgoing(defaultedQuery, searchMode); void router.push({ pathname: '/search', - search: makeSearchParams(defaultedQuery), + search: makeSearchParams(outgoing), }); } }, - [applyDefaultFilters, sort, submitQuery], + [applyDefaultFilters, searchMode, sort, submitQuery], ); return ( diff --git a/src/components/ClassicForm/ClassicForm.tsx b/src/components/ClassicForm/ClassicForm.tsx index 4f0d32f92..109a06141 100644 --- a/src/components/ClassicForm/ClassicForm.tsx +++ b/src/components/ClassicForm/ClassicForm.tsx @@ -41,6 +41,7 @@ import { Sort } from '@/components/Sort'; import { Expandable } from '@/components/Expandable'; import { SimpleCopyButton } from '@/components/CopyButton'; import { normalizeSolrSort } from '@/utils/common/search'; +import { ADS_COMPAT_URL_PARAM } from '@/utils/common/searchMode'; import { SolrSort, SolrSortField } from '@/api/models'; const propTypes = { @@ -84,7 +85,9 @@ export const ClassicForm = (props: IClassicFormProps) => { void handleSubmit((params) => { try { const search = getSearchQuery(params, { mode }); - void router.push({ pathname: '/search', search }); + const urlParams = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search); + urlParams.set(ADS_COMPAT_URL_PARAM, '1'); + void router.push({ pathname: '/search', search: '?' + urlParams.toString() }); } catch (e) { setQueryError((e as Error)?.message); } diff --git a/src/components/SearchModifierBar/SearchModifierBar.tsx b/src/components/SearchModifierBar/SearchModifierBar.tsx new file mode 100644 index 000000000..2421e7901 --- /dev/null +++ b/src/components/SearchModifierBar/SearchModifierBar.tsx @@ -0,0 +1,76 @@ +import { Box, BoxProps, Button, Circle, Menu, MenuButton, MenuItem, MenuList, Text, VStack } from '@chakra-ui/react'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import { useRouter } from 'next/router'; +import { omit } from 'ramda'; +import { ADS_COMPAT_URL_PARAM, SEARCH_MODE_OPTIONS, SearchMode } from '@/utils/common/searchMode'; +import { useSearchMode } from '@/lib/useSearchMode'; + +interface SearchModifierBarProps extends BoxProps { + onModeChange?: (mode: SearchMode) => void; + onNavigate?: (mode: SearchMode) => void; + isDisabled?: boolean; +} + +export const SearchModifierBar = ({ onModeChange, onNavigate, isDisabled, ...boxProps }: SearchModifierBarProps) => { + const router = useRouter(); + const [storedMode, setStoredMode] = useSearchMode(); + const urlAdsCompat = router.query[ADS_COMPAT_URL_PARAM] === '1'; + const currentMode = + urlAdsCompat || storedMode === SearchMode.ADS_COMPAT ? SearchMode.ADS_COMPAT : SearchMode.ALL_RELEVANT; + const currentOption = SEARCH_MODE_OPTIONS.find((o) => o.mode === currentMode) ?? SEARCH_MODE_OPTIONS[0]; + + const handleModeChange = (newMode: SearchMode) => { + setStoredMode(newMode); + onModeChange?.(newMode); + if (onNavigate) { + onNavigate(newMode); + } else { + const updatedQuery = + newMode === SearchMode.ADS_COMPAT + ? { ...router.query, [ADS_COMPAT_URL_PARAM]: '1' } + : omit([ADS_COMPAT_URL_PARAM], router.query); + void router.push({ pathname: router.pathname, query: updatedQuery }, undefined, { shallow: true }); + } + }; + + const modeColor = currentMode === SearchMode.ADS_COMPAT ? 'orange.400' : 'teal.400'; + + return ( + + + } + rightIcon={} + aria-label={`Search mode: ${currentOption.label}`} + isDisabled={isDisabled} + > + + Search mode: + + + {currentOption.label} + + + + {SEARCH_MODE_OPTIONS.map((option) => ( + handleModeChange(option.mode)} + fontWeight={option.mode === currentMode ? 'semibold' : 'normal'} + > + + {option.label} + + {option.helperText} + + + + ))} + + + + ); +}; diff --git a/src/components/SearchModifierBar/index.ts b/src/components/SearchModifierBar/index.ts new file mode 100644 index 000000000..0e66847d7 --- /dev/null +++ b/src/components/SearchModifierBar/index.ts @@ -0,0 +1 @@ +export { SearchModifierBar } from './SearchModifierBar'; diff --git a/src/components/SearchQueryLink/SearchQueryLink.tsx b/src/components/SearchQueryLink/SearchQueryLink.tsx index 01be2b53b..7488cfa33 100644 --- a/src/components/SearchQueryLink/SearchQueryLink.tsx +++ b/src/components/SearchQueryLink/SearchQueryLink.tsx @@ -4,22 +4,28 @@ import { useRouter } from 'next/router'; import { ISimpleLinkProps, SimpleLink } from '@/components/SimpleLink'; import { makeSearchParams } from '@/utils/common/search'; import { IADSApiSearchParams } from '@/api/search/types'; +import { useStore } from '@/store'; +import { buildSearchOutgoing } from '@/utils/common/searchMode'; export interface ISearchQueryLinkProps extends Omit { params: IADSApiSearchParams; } -const getSearchUrl = (params: IADSApiSearchParams) => `/search?${makeSearchParams(params)}`; +const getSearchUrl = (params: IADSApiSearchParams, searchMode: string) => + `/search?${makeSearchParams(buildSearchOutgoing(params, searchMode))}`; -/** - * Wrapper around next/link to create a simple link to the search page - * This generates the URL based on the params passed in - */ export const SearchQueryLink = (props: ISearchQueryLinkProps): ReactElement => { const { params, replace = false, shallow = false, prefetch = false, ...linkProps } = props; + const searchMode = useStore((s) => s.searchMode); return ( - + ); }; @@ -29,10 +35,11 @@ export interface ISearchQueryLinkButtonProps extends Omit { const { params, ...buttonProps } = props; const router = useRouter(); + const searchMode = useStore((s) => s.searchMode); const handleClick: MouseEventHandler = (e) => { e.preventDefault(); - void router.push(getSearchUrl(params)); + void router.push(getSearchUrl(params, searchMode)); }; return