diff --git a/src/api/gen/api_test.spec.ts b/src/api/gen/api_test.spec.ts new file mode 100644 index 00000000000..7b0c9257f3b --- /dev/null +++ b/src/api/gen/api_test.spec.ts @@ -0,0 +1,3 @@ +test('placeholder test for autogenerated api', () => {}) + +export {} diff --git a/src/features/auth/context/SettingsContext.tsx b/src/features/auth/context/SettingsContext.tsx index 5216e031d7b..23cc3572fb5 100644 --- a/src/features/auth/context/SettingsContext.tsx +++ b/src/features/auth/context/SettingsContext.tsx @@ -5,6 +5,7 @@ import { api } from 'api/api' import { SettingsResponse } from 'api/gen' import { useNetInfoContext } from 'libs/network/NetInfoWrapper' import { QueryKeys } from 'libs/queryKeys' +import { defaultSettings } from 'features/auth/fixtures/fixtures' export interface ISettingsContext { data: UseQueryResult['data'] @@ -12,7 +13,7 @@ export interface ISettingsContext { } const SettingsContext = React.createContext({ - data: undefined, + data: defaultSettings, isLoading: false, }) @@ -25,7 +26,7 @@ export const SettingsWrapper = memo(function SettingsWrapper({ }: { children: React.JSX.Element }) { - const { data, isLoading } = useAppSettings() + const { data = defaultSettings, isLoading } = { data: defaultSettings, isLoading: false } const value = useMemo(() => ({ data, isLoading }), [data, isLoading]) diff --git a/src/features/internal/atoms/OfferCategoryChoices.tsx b/src/features/internal/atoms/OfferCategoryChoices.tsx index 7c3d829832e..cbe5007c143 100644 --- a/src/features/internal/atoms/OfferCategoryChoices.tsx +++ b/src/features/internal/atoms/OfferCategoryChoices.tsx @@ -1,35 +1,37 @@ import React from 'react' import styled from 'styled-components/native' -import { SearchGroupNameEnumv2 } from 'api/gen' import { SelectionLabel } from 'features/search/components/SelectionLabel/SelectionLabel' -import { CategoryCriteria } from 'features/search/enums' -import { - useAvailableCategories, - useAvailableThematicSearchCategories, -} from 'features/search/helpers/useAvailableCategories/useAvailableCategories' -import { useSearchGroupLabelMapping } from 'libs/subcategories/mappings' import { Li } from 'ui/components/Li' import { Ul } from 'ui/components/Ul' import { getSpacing } from 'ui/theme' +import { + CategoryKey, + getTopLevelCategories, +} from 'features/search/helpers/categoriesHelpers/categoriesHelpers' +import { hasAThematicSearch } from 'features/navigation/SearchStackNavigator/types' type CategoryChoicesProps = { - onChange: (selection: SearchGroupNameEnumv2[]) => void - selection: SearchGroupNameEnumv2[] + onChange: (selection: CategoryKey[]) => void + selection: CategoryKey[] } type CategoryChoicesWithCategoryCriteria = CategoryChoicesProps & { - categories: ReadonlyArray + categories: CategoryKey[] } export const OfferCategoryChoices = (props: CategoryChoicesProps) => { - const categories = useAvailableCategories() + const categories = getTopLevelCategories().map((category) => category.key) return } export const ThematicSearchCategoryChoices = (props: CategoryChoicesProps) => { - const categories = useAvailableThematicSearchCategories() - return + return ( + category.valueOf())} + /> + ) } const CategoryChoices = ({ @@ -37,8 +39,6 @@ const CategoryChoices = ({ selection, categories, }: CategoryChoicesWithCategoryCriteria) => { - const searchGroupLabelMapping = useSearchGroupLabelMapping() - if (categories.length === 0) { return null } @@ -46,17 +46,17 @@ const CategoryChoices = ({ return ( - {categories.map((category) => ( -
  • - - onChange(selection?.includes(category.facetFilter) ? [] : [category.facetFilter]) - } - /> -
  • - ))} + {categories.map((categoryKey) => { + return ( +
  • + onChange(selection?.includes(categoryKey) ? [] : [categoryKey])} + /> +
  • + ) + })}
    ) diff --git a/src/features/internal/atoms/OfferNativeCategoryChoices.tsx b/src/features/internal/atoms/OfferNativeCategoryChoices.tsx deleted file mode 100644 index 85410798bef..00000000000 --- a/src/features/internal/atoms/OfferNativeCategoryChoices.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react' -import styled from 'styled-components/native' - -import { - NativeCategoryIdEnumv2, - NativeCategoryResponseModelv2, - SearchGroupNameEnumv2, -} from 'api/gen' -import { SelectionLabel } from 'features/search/components/SelectionLabel/SelectionLabel' -import { getNativeCategories } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' -import { useSubcategories } from 'libs/subcategories/useSubcategories' -import { Li } from 'ui/components/Li' -import { Ul } from 'ui/components/Ul' -import { getSpacing } from 'ui/theme' - -interface Props { - categories: SearchGroupNameEnumv2[] - onChange: (selection: NativeCategoryIdEnumv2[]) => void -} - -export const OfferNativeCategoryChoices = (props: Props) => { - const [selection, setSelection] = useState( - [] as NativeCategoryIdEnumv2[] - ) - - const { data } = useSubcategories() - const { categories, onChange } = props - - const nativeCategories = useMemo(() => { - let nativeCategories: NativeCategoryResponseModelv2[] = [] - categories.forEach((categoryEnum) => { - nativeCategories = [...nativeCategories, ...getNativeCategories(data, categoryEnum)] - }) - return nativeCategories.sort((a, b) => (a?.value ?? '').localeCompare(b?.value ?? '')) - }, [data, categories]) - - const onPress = useCallback( - (nativeCategoryEnum: NativeCategoryIdEnumv2) => { - setSelection((prevSelection) => { - let nextSelection = [...prevSelection] - if (nextSelection.includes(nativeCategoryEnum)) { - nextSelection = [] - } else { - nextSelection = [nativeCategoryEnum] - } - onChange(nextSelection) - return nextSelection - }) - }, - [onChange] - ) - - return ( - - - {nativeCategories.map((nativeCategory) => ( -
  • - onPress(nativeCategory.name)} - /> -
  • - ))} -
    -
    - ) -} - -const BodyContainer = styled.View({ - flexWrap: 'wrap', - flexDirection: 'column', - marginBottom: getSpacing(-3), - marginRight: getSpacing(-3), -}) - -const StyledUl = styled(Ul)({ - flexWrap: 'wrap', -}) diff --git a/src/features/internal/atoms/OfferNativeCategoryChoices.native.test.tsx b/src/features/internal/atoms/OfferSubcategoryChoices.native.test.tsx similarity index 83% rename from src/features/internal/atoms/OfferNativeCategoryChoices.native.test.tsx rename to src/features/internal/atoms/OfferSubcategoryChoices.native.test.tsx index 0043d063647..af389b80e6b 100644 --- a/src/features/internal/atoms/OfferNativeCategoryChoices.native.test.tsx +++ b/src/features/internal/atoms/OfferSubcategoryChoices.native.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import { SearchGroupNameEnumv2 } from 'api/gen' -import { OfferNativeCategoryChoices } from 'features/internal/atoms/OfferNativeCategoryChoices' +import { OfferSubcategoryChoices } from 'features/internal/atoms/OfferSubcategoryChoices' import { render, screen, userEvent } from 'tests/utils' jest.mock('libs/subcategories/useSubcategories') @@ -9,13 +9,13 @@ jest.mock('libs/subcategories/useSubcategories') jest.mock('libs/firebase/analytics/analytics') const user = userEvent.setup() -describe('', () => { +describe('', () => { jest.useFakeTimers() it('should call onChange with proper subcategory when toggling', async () => { const onChange = jest.fn() render( - diff --git a/src/features/internal/atoms/OfferSubcategoryChoices.tsx b/src/features/internal/atoms/OfferSubcategoryChoices.tsx new file mode 100644 index 00000000000..26d53c687ac --- /dev/null +++ b/src/features/internal/atoms/OfferSubcategoryChoices.tsx @@ -0,0 +1,71 @@ +import React, { useCallback, useMemo, useState } from 'react' +import styled from 'styled-components/native' + +import { SelectionLabel } from 'features/search/components/SelectionLabel/SelectionLabel' +import { + CategoryKey, + getCategoryChildren, +} from 'features/search/helpers/categoriesHelpers/categoriesHelpers' +import { useSubcategories } from 'libs/subcategories/useSubcategories' +import { Li } from 'ui/components/Li' +import { Ul } from 'ui/components/Ul' +import { getSpacing } from 'ui/theme' + +interface Props { + categories: CategoryKey[] + onChange: (selection: CategoryKey[]) => void +} + +export const OfferSubcategoryChoices = (props: Props) => { + const [selection, setSelection] = useState([]) + + const { data } = useSubcategories() + const { categories, onChange } = props + + const subcategories = useMemo( + () => + categories + .map((categoryKey) => getCategoryChildren(categoryKey)) + .flat() + .sort((a, b) => a.label.localeCompare(b.label)), + [data, categories] + ) + + const onPress = useCallback( + (subcategoryKey: CategoryKey) => { + setSelection((prevSelection) => { + const nextSelection = prevSelection.includes(subcategoryKey) ? [] : [subcategoryKey] + onChange(nextSelection) + return nextSelection + }) + }, + [onChange] + ) + + return ( + + + {subcategories.map((subcategory) => ( +
  • + onPress(subcategory.label)} + /> +
  • + ))} +
    +
    + ) +} + +const BodyContainer = styled.View({ + flexWrap: 'wrap', + flexDirection: 'column', + marginBottom: getSpacing(-3), + marginRight: getSpacing(-3), +}) + +const StyledUl = styled(Ul)({ + flexWrap: 'wrap', +}) diff --git a/src/features/internal/components/DeeplinksGeneratorForm.tsx b/src/features/internal/components/DeeplinksGeneratorForm.tsx index 0db8e3253cf..8c1ce1998a8 100644 --- a/src/features/internal/components/DeeplinksGeneratorForm.tsx +++ b/src/features/internal/components/DeeplinksGeneratorForm.tsx @@ -2,7 +2,6 @@ import omit from 'lodash/omit' import React, { useMemo, useState } from 'react' import styled, { useTheme } from 'styled-components/native' -import { NativeCategoryIdEnumv2, SearchGroupNameEnumv2 } from 'api/gen' import { generateLongFirebaseDynamicLink } from 'features/deeplinks/helpers' import { ControlledFilterSwitch } from 'features/internal/atoms/ControlledFilterSwitch' import { DateChoice } from 'features/internal/atoms/DateChoice' @@ -11,7 +10,7 @@ import { OfferCategoryChoices, ThematicSearchCategoryChoices, } from 'features/internal/atoms/OfferCategoryChoices' -import { OfferNativeCategoryChoices } from 'features/internal/atoms/OfferNativeCategoryChoices' +import { OfferSubcategoryChoices } from 'features/internal/atoms/OfferSubcategoryChoices' import { FDL_CONFIG, MARKETING_CONFIG, @@ -41,6 +40,7 @@ import { SNACK_BAR_TIME_OUT, useSnackBarContext } from 'ui/components/snackBar/S import { useEnterKeyAction } from 'ui/hooks/useEnterKeyAction' import { Warning as WarningDefault } from 'ui/svg/icons/BicolorWarning' import { getSpacing, Spacer, Typo, TypoDS } from 'ui/theme' +import { CategoryKey } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' export interface GeneratedDeeplink { universalLink: string @@ -157,7 +157,7 @@ export const DeeplinksGeneratorForm = ({ onCreate }: Props) => { ) } - function onChangeOfferCategories(categories: SearchGroupNameEnumv2[]) { + function onChangeOfferCategories(categories: CategoryKey[]) { setScreenParams((prevPageParams) => { return { ...prevPageParams, @@ -167,12 +167,12 @@ export const DeeplinksGeneratorForm = ({ onCreate }: Props) => { }) } - function onChangeOfferNativeCategories(nativeCategories: NativeCategoryIdEnumv2[]) { + function onChangeOfferSubcategories(subcategories: CategoryKey[]) { setScreenParams((prevPageParams) => - nativeCategories.length + subcategories.length ? { ...prevPageParams, - [name]: nativeCategories, + [name]: subcategories, } : omit(prevPageParams, name) ) @@ -238,19 +238,19 @@ export const DeeplinksGeneratorForm = ({ onCreate }: Props) => { {config.type === 'offerCategories' ? ( ) : null} - {config.type === 'offerNativeCategories' && screenParams.offerCategories ? ( - ) : null} {config.type === 'thematicSearchCategories' ? ( ) : null} {config.type === 'date' ? ( diff --git a/src/features/internal/config/deeplinksExportConfig.ts b/src/features/internal/config/deeplinksExportConfig.ts index fd4f6f8dbda..358ca9ab998 100644 --- a/src/features/internal/config/deeplinksExportConfig.ts +++ b/src/features/internal/config/deeplinksExportConfig.ts @@ -25,7 +25,7 @@ export type ParamConfig = { | 'priceRange' | 'boolean' | 'offerCategories' - | 'offerNativeCategories' + | 'offerSubcategories' | 'date' | 'locationFilter' | 'thematicSearchCategories' @@ -46,7 +46,7 @@ export const SCREENS_CONFIG: { type: 'string', required: true, description: 'Identifiant unique de l’offre.', - serverValidator: (value: unknown) => api.getNativeV1OfferofferId(Number(value)), + serverValidator: (value: unknown) => api.getNativeV2OfferofferId(Number(value)), }, }, VenueMap: {}, @@ -96,7 +96,7 @@ export const SCREENS_CONFIG: { description: 'Categories', }, offerNativeCategories: { - type: 'offerNativeCategories', + type: 'offerSubcategories', required: false, description: 'Sous-catégories', }, diff --git a/src/features/navigation/SearchStackNavigator/types.ts b/src/features/navigation/SearchStackNavigator/types.ts index 4df04854351..ba1c2d20b97 100644 --- a/src/features/navigation/SearchStackNavigator/types.ts +++ b/src/features/navigation/SearchStackNavigator/types.ts @@ -1,6 +1,7 @@ import { SearchGroupNameEnumv2 } from 'api/gen' import { DisabilitiesProperties } from 'features/accessibility/types' import { GenericRoute } from 'features/navigation/RootNavigator/types' +import { CategoryKey } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' import { SearchState } from 'features/search/types' export type SearchStackRouteName = keyof SearchStackParamList @@ -14,7 +15,11 @@ export const hasAThematicSearch = [ type HasAThematicSearch = typeof hasAThematicSearch -type ThematicSearchCategories = Extract +export type ThematicSearchCategories = Extract + +export const isThematicSearchCategory = ( + categoryKey: CategoryKey +): categoryKey is ThematicSearchCategories => categoryKey in hasAThematicSearch export type SearchStackParamList = { SearchLanding?: Partial }> diff --git a/src/features/search/components/AutocompleteOffer/AutocompleteOffer.tsx b/src/features/search/components/AutocompleteOffer/AutocompleteOffer.tsx index 4eb07796015..84e57ba0112 100644 --- a/src/features/search/components/AutocompleteOffer/AutocompleteOffer.tsx +++ b/src/features/search/components/AutocompleteOffer/AutocompleteOffer.tsx @@ -11,10 +11,9 @@ import { VerticalUl } from 'ui/components/Ul' import { getSpacing, Typo } from 'ui/theme' import { getHeadingAttrs } from 'ui/theme/typographyAttrs/getHeadingAttrs' -type AutocompleteOfferProps = UseInfiniteHitsProps & { +type AutocompleteOfferProps = UseInfiniteHitsProps & { addSearchHistory: (item: CreateHistoryItem) => void offerCategories?: SearchGroupNameEnumv2[] - shouldShowCategory?: boolean } export function AutocompleteOffer({ @@ -22,7 +21,7 @@ export function AutocompleteOffer({ offerCategories, ...props }: AutocompleteOfferProps) { - const { hits, sendEvent } = useInfiniteHits(props) + const { hits, sendEvent } = useInfiniteHits(props) return hits.length > 0 ? ( @@ -31,11 +30,10 @@ export function AutocompleteOffer({ {hits.map((item) => (
  • ))} diff --git a/src/features/search/components/AutocompleteOfferItem/AutocompleteOfferItem.native.test.tsx b/src/features/search/components/AutocompleteOfferItem/AutocompleteOfferItem.native.test.tsx index 8369bd26652..c6130b656f1 100644 --- a/src/features/search/components/AutocompleteOfferItem/AutocompleteOfferItem.native.test.tsx +++ b/src/features/search/components/AutocompleteOfferItem/AutocompleteOfferItem.native.test.tsx @@ -65,7 +65,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -103,9 +102,8 @@ describe('AutocompleteOfferItem component', () => { , { wrapper: ({ children }) => reactQueryProviderHOC(children), @@ -120,9 +118,8 @@ describe('AutocompleteOfferItem component', () => { , { wrapper: ({ children }) => reactQueryProviderHOC(children), @@ -137,9 +134,8 @@ describe('AutocompleteOfferItem component', () => { , { wrapper: ({ children }) => reactQueryProviderHOC(children), @@ -240,9 +236,8 @@ describe('AutocompleteOfferItem component', () => { , { wrapper: ({ children }) => reactQueryProviderHOC(children), @@ -264,7 +259,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -296,7 +290,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -328,7 +321,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -359,7 +351,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -390,7 +381,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -423,7 +413,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -454,7 +443,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -488,7 +476,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -504,7 +491,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -522,7 +508,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -540,7 +525,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -560,7 +544,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -578,7 +561,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -598,7 +580,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -616,7 +597,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -634,7 +614,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -654,7 +633,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -670,7 +648,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -686,7 +663,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -702,7 +678,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -719,7 +694,6 @@ describe('AutocompleteOfferItem component', () => { , { @@ -737,9 +711,8 @@ describe('AutocompleteOfferItem component', () => { , { wrapper: ({ children }) => reactQueryProviderHOC(children), diff --git a/src/features/search/components/AutocompleteOfferItem/AutocompleteOfferItem.tsx b/src/features/search/components/AutocompleteOfferItem/AutocompleteOfferItem.tsx index f2a115f0947..d31c863dd32 100644 --- a/src/features/search/components/AutocompleteOfferItem/AutocompleteOfferItem.tsx +++ b/src/features/search/components/AutocompleteOfferItem/AutocompleteOfferItem.tsx @@ -1,27 +1,25 @@ -import { useNavigationState } from '@react-navigation/native' import { SendEventForHits } from 'instantsearch.js/es/lib/utils' -import React, { useMemo, FunctionComponent, ReactNode } from 'react' +import React, { FunctionComponent, ReactNode } from 'react' import { Keyboard, Text } from 'react-native' import styled from 'styled-components/native' import { v4 as uuidv4 } from 'uuid' import { NativeCategoryIdEnumv2, SearchGroupNameEnumv2 } from 'api/gen' import { useAccessibilityFiltersContext } from 'features/accessibility/context/AccessibilityFiltersWrapper' +import { useCurrentRoute } from 'features/navigation/helpers/useCurrentRoute' import { Highlight } from 'features/search/components/Highlight/Highlight' import { useSearch } from 'features/search/context/SearchWrapper' import { - getNativeCategoryFromEnum, - getSearchGroupsEnumArrayFromNativeCategoryEnum, - isNativeCategoryOfCategory, - useNativeCategories, + categoryExists, + getCategory, + getCategoryParents, + isChild, + isTopLevelCategory, } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' -import { useAvailableCategories } from 'features/search/helpers/useAvailableCategories/useAvailableCategories' import { useNavigateToSearch } from 'features/search/helpers/useNavigateToSearch/useNavigateToSearch' import { CreateHistoryItem, SearchState, SearchView } from 'features/search/types' import { AlgoliaSuggestionHit } from 'libs/algolia/types' import { env } from 'libs/environment' -import { useSearchGroupLabel } from 'libs/subcategories' -import { useSubcategories } from 'libs/subcategories/useSubcategories' import { MagnifyingGlassFilled } from 'ui/svg/icons/MagnifyingGlassFilled' import { getSpacing, Typo } from 'ui/theme' @@ -29,186 +27,91 @@ type AutocompleteOfferItemProps = { hit: AlgoliaSuggestionHit sendEvent: SendEventForHits addSearchHistory: (item: CreateHistoryItem) => void - shouldShowCategory?: boolean - offerCategories?: SearchGroupNameEnumv2[] + contextCategories?: SearchGroupNameEnumv2[] } export function AutocompleteOfferItem({ hit, sendEvent, addSearchHistory, - shouldShowCategory, - offerCategories = [], + contextCategories = [], }: Readonly) { + const { searchState, dispatch, hideSuggestions } = useSearch() + const currentRoute = useCurrentRoute()?.name + const { navigateToSearch: navigateToSearchResults } = useNavigateToSearch('SearchResults') + const { disabilities } = useAccessibilityFiltersContext() + + // https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/js/#suggestions-with-categories-index-schema const { query, [env.ALGOLIA_OFFERS_INDEX_NAME]: indexInfos } = hit - // https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/how-to/adding-category-suggestions/js/#suggestions-with-categories-index-schema const { ['offer.searchGroupNamev2']: categories = [], ['offer.nativeCategoryId']: nativeCategories = [], } = indexInfos?.facets?.analytics || {} - const mappedNativeCategories = useNativeCategories(offerCategories[0]) - - const { searchState, dispatch, hideSuggestions } = useSearch() - const routes = useNavigationState((state) => state.routes) - const currentRoute = routes?.at(-1)?.name - const { navigateToSearch: navigateToSearchResults } = useNavigateToSearch('SearchResults') - const { disabilities } = useAccessibilityFiltersContext() - const { data } = useSubcategories() - - const availableCategories = useAvailableCategories() - - const availableCategoriesList: SearchGroupNameEnumv2[] = availableCategories.map( - (availableCategory) => availableCategory.facetFilter - ) - - const filteredCategories = categories - .sort((a, b) => b.count - a.count) - .filter((category) => availableCategoriesList.includes(category.value)) - - const searchGroupLabel = useSearchGroupLabel( - filteredCategories[0]?.value ?? SearchGroupNameEnumv2.NONE - ) - - const orderedNativeCategories = nativeCategories.sort((a, b) => b.count - a.count) - - const isQueryFromThematicSearch = !!offerCategories.length - - const isNativeCategoryRelatedToSearchGroup = useMemo(() => { - return mappedNativeCategories.some( - (searchGroupNativeCategory) => - orderedNativeCategories[0] && - searchGroupNativeCategory[0] === orderedNativeCategories[0].value - ) - }, [mappedNativeCategories, orderedNativeCategories]) - - const hasMostPopularHitNativeCategory = - orderedNativeCategories[0]?.value && orderedNativeCategories[0].value in NativeCategoryIdEnumv2 - - const mostPopularNativeCategoryId = hasMostPopularHitNativeCategory - ? orderedNativeCategories[0]?.value - : undefined - - const mostPopularNativeCategoryValue = getNativeCategoryFromEnum( - data, - mostPopularNativeCategoryId - )?.value - - const hasMostPopularHitCategory = - filteredCategories[0]?.value && filteredCategories[0].value in SearchGroupNameEnumv2 - - const searchGroupFromNativeCategory = useMemo( - () => - getSearchGroupsEnumArrayFromNativeCategoryEnum( - data, - mostPopularNativeCategoryId, - availableCategoriesList - ), - [data, mostPopularNativeCategoryId, availableCategoriesList] - ) - const hasSeveralCategoriesFromNativeCategory = searchGroupFromNativeCategory.length > 1 - const isAssociatedMostPopularNativeCategoryToMostPopularCategory = useMemo( - () => - isNativeCategoryOfCategory(data, filteredCategories[0]?.value, mostPopularNativeCategoryId), - [filteredCategories, data, mostPopularNativeCategoryId] - ) - - const shouldDisplayNativeCategory = - hasMostPopularHitNativeCategory && - ((isQueryFromThematicSearch && isNativeCategoryRelatedToSearchGroup) || - !isQueryFromThematicSearch) && - !hasSeveralCategoriesFromNativeCategory - - const isLivresPapierNativeCategory = - orderedNativeCategories[0]?.value == NativeCategoryIdEnumv2.LIVRES_PAPIER - - const shouldUseSearchGroupInsteadOfNativeCategory = - !shouldDisplayNativeCategory || - isLivresPapierNativeCategory || - (isQueryFromThematicSearch && !isNativeCategoryRelatedToSearchGroup) - - const mostPopularCategory = useMemo(() => { - if ( - hasSeveralCategoriesFromNativeCategory || - !hasMostPopularHitNativeCategory || - shouldUseSearchGroupInsteadOfNativeCategory - ) { - return filteredCategories[0]?.value && filteredCategories[0].value in SearchGroupNameEnumv2 - ? [filteredCategories[0].value] - : [] - } else { - return searchGroupFromNativeCategory[0] && - searchGroupFromNativeCategory[0] in SearchGroupNameEnumv2 - ? [searchGroupFromNativeCategory[0]] - : [] - } - }, [ - filteredCategories, - searchGroupFromNativeCategory, - hasMostPopularHitNativeCategory, - hasSeveralCategoriesFromNativeCategory, - shouldUseSearchGroupInsteadOfNativeCategory, - ]) + const topLevelCategorySuggestion = categories + .filter((category) => categoryExists(category.value)) + .toSorted((a, b) => b.count - a.count)[0]?.value + const subcategorySuggestion = nativeCategories.toSorted((a, b) => b.count - a.count)[0]?.value + + if (!topLevelCategorySuggestion && !subcategorySuggestion) return + + const subcategoryParents = subcategorySuggestion ? getCategoryParents(subcategorySuggestion) : [] + const contextCategory = contextCategories[0] + const isSuggestionInContext = + subcategorySuggestion && contextCategory && isChild(subcategorySuggestion, contextCategory) + const shouldDisplaySubcategory = + (!contextCategory || isSuggestionInContext) && + subcategoryParents.length === 1 && + subcategorySuggestion !== NativeCategoryIdEnumv2.LIVRES_PAPIER && + subcategorySuggestion // `shouldDisplaySubcategory` is true already implies `subcategorySuggestion` is true, but here it helps later typing + + const label = shouldDisplaySubcategory + ? getCategory(subcategorySuggestion)?.label + : topLevelCategorySuggestion && getCategory(topLevelCategorySuggestion)?.label + + if (!label) return + + const shouldFilterOnNativeCategory = + shouldDisplaySubcategory && + topLevelCategorySuggestion && + isChild(subcategorySuggestion, topLevelCategorySuggestion) + const subcategoryFilterOnPress = shouldFilterOnNativeCategory ? [subcategorySuggestion] : [] + const topLevelFilterOnPress = + shouldDisplaySubcategory && subcategoryParents[0] && isTopLevelCategory(subcategoryParents[0]) + ? subcategoryParents[0].key + : topLevelCategorySuggestion const onPress = () => { sendEvent('click', hit, 'Suggestion clicked') Keyboard.dismiss() - // When we hit enter, we may have selected a category or a venue on the search landing page - // these are the two potentially 'staged' filters that we want to commit to the global search state. - // We also want to commit the price filter, as beneficiary users may have access to different offer - // price range depending on their available credit. const searchId = uuidv4() - const shouldFilterOnNativeCategory = - shouldShowCategory && - hasMostPopularHitNativeCategory && - !shouldUseSearchGroupInsteadOfNativeCategory && - ((hasSeveralCategoriesFromNativeCategory && - isAssociatedMostPopularNativeCategoryToMostPopularCategory) || - !hasSeveralCategoriesFromNativeCategory) const newSearchState: SearchState = { ...searchState, query, searchId, isAutocomplete: true, offerGenreTypes: undefined, - offerNativeCategories: - shouldFilterOnNativeCategory && orderedNativeCategories[0]?.value - ? [orderedNativeCategories[0].value] - : undefined, - offerCategories: shouldShowCategory ? mostPopularCategory : [], + offerNativeCategories: subcategoryFilterOnPress, + offerCategories: topLevelFilterOnPress ? [topLevelFilterOnPress] : [], isFromHistory: undefined, gtls: [], } + dispatch({ type: 'SET_STATE', payload: newSearchState }) addSearchHistory({ query, - nativeCategory: shouldFilterOnNativeCategory ? orderedNativeCategories[0]?.value : undefined, - category: shouldShowCategory ? mostPopularCategory[0] : undefined, + nativeCategory: shouldFilterOnNativeCategory ? subcategorySuggestion : undefined, + category: topLevelFilterOnPress, }) - dispatch({ type: 'SET_STATE', payload: newSearchState }) - if ( - currentRoute && - [SearchView.Landing, SearchView.Thematic].includes(currentRoute as SearchView) - ) { + if ([SearchView.Landing, SearchView.Thematic].includes(currentRoute as SearchView)) navigateToSearchResults(newSearchState, disabilities) - } hideSuggestions() } - const shouldDisplayCategorySuggestion = - shouldShowCategory && (hasMostPopularHitNativeCategory || hasMostPopularHitCategory) - - const categoryToDisplay = - shouldUseSearchGroupInsteadOfNativeCategory && searchGroupLabel !== 'None' - ? searchGroupLabel - : mostPopularNativeCategoryValue - const testID = `autocompleteOfferItem_${hit.objectID}` return ( - {shouldDisplayCategorySuggestion && categoryToDisplay ? ( - - ) : undefined} + ) } @@ -230,10 +133,10 @@ const SuggestionContainer: FunctionComponent<{ ) -const Suggestion: FunctionComponent<{ categoryToDisplay: string }> = ({ categoryToDisplay }) => ( +const Suggestion: FunctionComponent<{ category: string }> = ({ category }) => ( dans - {categoryToDisplay} + {category} ) diff --git a/src/features/search/components/BookCategoriesSection/BookCategoriesSection.tsx b/src/features/search/components/BookCategoriesSection/BookCategoriesSection.tsx deleted file mode 100644 index d59d3c5daa9..00000000000 --- a/src/features/search/components/BookCategoriesSection/BookCategoriesSection.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react' -import styled from 'styled-components/native' - -import { GenreType, SearchGroupNameEnumv2 } from 'api/gen' -import { - CategoriesMapping, - CategoriesSectionProps, -} from 'features/search/components/CategoriesSection/CategoriesSection' -import { CategoriesSectionItem } from 'features/search/components/CategoriesSectionItem/CategoriesSectionItem' -import { MappingTree } from 'features/search/helpers/categoriesHelpers/mapping-tree' -import { Li } from 'ui/components/Li' -import { RadioButton } from 'ui/components/radioButtons/RadioButton' -import { Separator } from 'ui/components/Separator' -import { VerticalUl } from 'ui/components/Ul' -import { Spacer, TypoDS } from 'ui/theme' - -export function BookCategoriesSection< - T extends CategoriesMapping, - N = T extends MappingTree ? keyof MappingTree : keyof T | null, ->({ - allLabel, - allValue, - itemsMapping, - descriptionContext, - getIcon, - onSelect, - onSubmit, - value, -}: Readonly>) { - const handleGetIcon = (category: SearchGroupNameEnumv2) => { - if (getIcon) { - return getIcon(category) - } - - return undefined - } - - const handleSelect = (key: N) => { - onSelect(key) - if (onSubmit) { - onSubmit() - } - } - - const categories = itemsMapping ? Object.entries(itemsMapping) : [] - const bookCategoriesWithGenre = categories.filter( - ([_k, item]) => item.genreTypeKey === GenreType.BOOK && item.label !== 'Livres papier' - ) - const otherBookCategories = categories.filter( - ([_k, item]) => item.genreTypeKey !== GenreType.BOOK && item.label !== 'Livres papier' - ) - - return ( - - - onSelect(allValue)} - icon={handleGetIcon(SearchGroupNameEnumv2.NONE)} - /> - - - {'Livres papier'} - - {bookCategoriesWithGenre.map(([k, item]) => ( - - ))} - - - - {'Autres'} - - {otherBookCategories.map(([k, item]) => ( - - ))} - - ) -} - -const Title = styled(TypoDS.Title1)({}) - -const ListItem = styled(Li)({ - display: 'flex', -}) diff --git a/src/features/search/components/CategoriesList/CategoriesList.tsx b/src/features/search/components/CategoriesList/CategoriesList.tsx index b1bc1baa7e6..ccb3fe75439 100644 --- a/src/features/search/components/CategoriesList/CategoriesList.tsx +++ b/src/features/search/components/CategoriesList/CategoriesList.tsx @@ -3,7 +3,7 @@ import { Platform } from 'react-native' import { CategoriesListDumb } from 'features/search/components/CategoriesListDumb/CategoriesListDumb' import { useShowResultsForCategory } from 'features/search/helpers/useShowResultsForCategory/useShowResultsForCategory' -import { useSortedSearchCategories } from 'features/search/helpers/useSortedSearchCategories/useSortedSearchCategories' +import { useSearchLandingButtonProps } from 'features/search/helpers/useSortedSearchCategories/useSearchLandingButtonsProps' import { useShouldDisplayVenueMap } from 'features/venueMap/hook/useShouldDisplayVenueMap' import { LocationMode } from 'libs/location/types' import { useModal } from 'ui/components/modals/useModal' @@ -12,7 +12,7 @@ export const CategoriesList = memo(function CategoriesButtons() { const showResultsForCategory = useShowResultsForCategory() const isWeb = Platform.OS === 'web' - const sortedCategories = useSortedSearchCategories(showResultsForCategory) + const searchLandingButtonsProps = useSearchLandingButtonProps(showResultsForCategory) const { shouldDisplayVenueMap, selectedLocationMode } = useShouldDisplayVenueMap() const isLocated = selectedLocationMode !== LocationMode.EVERYWHERE @@ -26,7 +26,7 @@ export const CategoriesList = memo(function CategoriesButtons() { return ( = (props) => ( ) Default.args = { - sortedCategories: [ + categoriesButtonsProps: [ { label: 'CD, vinyles, musique en ligne', Illustration: SearchCategoriesIllustrations.CDVinylsOnlineMusic, diff --git a/src/features/search/components/CategoriesListDumb/CategoriesListDumb.tsx b/src/features/search/components/CategoriesListDumb/CategoriesListDumb.tsx index 68f8fb55556..f97448ffa75 100644 --- a/src/features/search/components/CategoriesListDumb/CategoriesListDumb.tsx +++ b/src/features/search/components/CategoriesListDumb/CategoriesListDumb.tsx @@ -26,7 +26,7 @@ export type CategoryButtonProps = { export type ListCategoryButtonProps = CategoryButtonProps[] type Props = { - sortedCategories: ListCategoryButtonProps + categoriesProps: ListCategoryButtonProps shouldDisplayVenueMap: boolean isMapWithoutPositionAndNotLocated: boolean showVenueMapLocationModal: () => void @@ -41,7 +41,7 @@ const MOBILE_GAPS_AND_PADDINGS = getSpacing(2) const DESKTOP_GAPS_AND_PADDINGS = getSpacing(4) export const CategoriesListDumb: FunctionComponent = ({ - sortedCategories, + categoriesProps, shouldDisplayVenueMap, isMapWithoutPositionAndNotLocated, showVenueMapLocationModal, @@ -67,7 +67,7 @@ export const CategoriesListDumb: FunctionComponent = ({ ) : null} - {sortedCategories.map((item) => ( + {categoriesProps.map((item) => ( ))} diff --git a/src/features/search/components/CategoriesListDumb/CategoriesListDumb.web.test.tsx b/src/features/search/components/CategoriesListDumb/CategoriesListDumb.web.test.tsx index c00691e6131..4c3ff9d501e 100644 --- a/src/features/search/components/CategoriesListDumb/CategoriesListDumb.web.test.tsx +++ b/src/features/search/components/CategoriesListDumb/CategoriesListDumb.web.test.tsx @@ -11,7 +11,7 @@ describe('CategoriesListDumb', () => { it('should not display venue map block when is "web"', () => { render( { - allLabel: string - allValue: N - descriptionContext: DescriptionContext - getIcon?: T extends MappingTree - ? (categoryName: SearchGroupNameEnumv2) => FC | undefined - : undefined - itemsMapping: T - onSelect: (item: N) => void - onSubmit?: () => void - shouldSortItems?: boolean - value: N -} - -export function CategoriesSection< - T extends CategoriesMapping, - N = T extends MappingTree ? keyof MappingTree : keyof T | null, ->({ - allLabel, - allValue, - descriptionContext, - getIcon, - itemsMapping, - onSelect, - onSubmit, - shouldSortItems = true, - value, -}: Readonly>) { - const handleGetIcon = (category: SearchGroupNameEnumv2) => { - if (getIcon) { - return getIcon(category) - } - - return undefined - } - - const handleSelect = (key: N) => { - onSelect(key) - if (onSubmit) { - onSubmit() - } - } - - const entries = Object.entries(itemsMapping) - if (shouldSortItems) entries.sort(([, a], [, b]) => sortCategoriesPredicate(a, b)) - - return ( - - - onSelect(allValue)} - icon={handleGetIcon(SearchGroupNameEnumv2.NONE)} - /> - - {entries.map(([k, item]) => ( - - ))} - - ) -} - -const ListItem = styled(Li)({ - display: 'flex', -}) diff --git a/src/features/search/components/CategoriesSectionBlock/CategoriesSectionBlock.tsx b/src/features/search/components/CategoriesSectionBlock/CategoriesSectionBlock.tsx new file mode 100644 index 00000000000..f76d29fe947 --- /dev/null +++ b/src/features/search/components/CategoriesSectionBlock/CategoriesSectionBlock.tsx @@ -0,0 +1,56 @@ +import React, { FC } from 'react' + +import { CategoriesSectionItem } from 'features/search/components/CategoriesSectionItem/CategoriesSectionItem' +import { BaseCategory } from 'features/search/helpers/categoriesHelpers/categories' +import styled from 'styled-components/native' +import { Li } from 'ui/components/Li' +import { Separator } from 'ui/components/Separator' +import { AccessibleBicolorIcon } from 'ui/svg/icons/types' +import { Spacer, TypoDS } from 'ui/theme' + +interface CategoriesSectionBlockProps { + getIcon: (categoryKey: string) => FC | undefined + items: BaseCategory[] + onSelect: (category: BaseCategory) => void + selectionKey?: string + selectionSubtitle?: string + title: string +} + +export const CategoriesSectionBlock = ({ + getIcon, + items, + onSelect, + selectionKey, + selectionSubtitle, + title, +}: CategoriesSectionBlockProps) => { + return ( + + + + {title} + + + {items.map((item) => ( + onSelect(item)} + icon={getIcon(item.key)} + subtitle={selectionKey === item.key ? selectionSubtitle : undefined} + /> + ))} + + + + + ) +} + +const ListItem = styled(Li)({ + display: 'flex', +}) + +const Title = styled(TypoDS.Title1)({}) diff --git a/src/features/search/components/CategoriesSectionItem/CategoriesSectionItem.tsx b/src/features/search/components/CategoriesSectionItem/CategoriesSectionItem.tsx index 152d9856dce..c68c0dea801 100644 --- a/src/features/search/components/CategoriesSectionItem/CategoriesSectionItem.tsx +++ b/src/features/search/components/CategoriesSectionItem/CategoriesSectionItem.tsx @@ -1,75 +1,61 @@ import React from 'react' import styled from 'styled-components/native' -import { SearchGroupNameEnumv2 } from 'api/gen' -import { CategoriesMapping } from 'features/search/components/CategoriesSection/CategoriesSection' import { FilterRow } from 'features/search/components/FilterRow/FilterRow' -import { - getDescription, - getNbResultsFacetLabel, -} from 'features/search/helpers/categoriesHelpers/categoriesHelpers' -import { DescriptionContext } from 'features/search/types' +import { BaseCategory } from 'features/search/helpers/categoriesHelpers/categories' +import { getNbResultsFacetLabel } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' import { useFeatureFlag } from 'libs/firebase/firestore/featureFlags/useFeatureFlag' import { RemoteStoreFeatureFlags } from 'libs/firebase/firestore/types' -import { useSubcategories } from 'libs/subcategories/useSubcategories' import { Li } from 'ui/components/Li' import { RadioButton } from 'ui/components/radioButtons/RadioButton' import { AccessibleBicolorIcon } from 'ui/svg/icons/types' import { Spacer } from 'ui/theme' -type CategoriesMappingItem = { - label: string - nbResultsFacet?: number - children?: CategoriesMapping +interface CategoriesSectionItemProps { + icon?: React.FC + item: BaseCategory + isSelected: boolean + onSelect: VoidFunction + subtitle?: string } -interface CategoriesSectionItemProps { - value: N - k: string - item: CategoriesMappingItem - descriptionContext: DescriptionContext - handleSelect: (key: N) => void - handleGetIcon: (category: SearchGroupNameEnumv2) => React.FC | undefined -} - -export const CategoriesSectionItem = ({ - value, - k, +export const CategoriesSectionItem = ({ + icon, + isSelected, item, - descriptionContext, - handleSelect, - handleGetIcon, -}: CategoriesSectionItemProps) => { - const { data: subcategoriesData } = useSubcategories() + onSelect, + subtitle, +}: CategoriesSectionItemProps) => { const displaySearchNbFacetResults = useFeatureFlag( RemoteStoreFeatureFlags.WIP_DISPLAY_SEARCH_NB_FACET_RESULTS ) + const nbResultsFacet = displaySearchNbFacetResults + ? getNbResultsFacetLabel(item.nbResultsFacet) + : undefined - const shouldHideArrow = !Object.keys(item.children ?? {})?.length - const itemKey = k as N - const nbResultsFacet = getNbResultsFacetLabel(item.nbResultsFacet) + const shouldHideArrow = !item.children.length return ( {shouldHideArrow ? ( handleSelect(itemKey)} - icon={handleGetIcon(k as SearchGroupNameEnumv2)} - complement={displaySearchNbFacetResults ? nbResultsFacet : undefined} + isSelected={isSelected} + onSelect={onSelect} + icon={icon} + complement={nbResultsFacet} /> ) : ( handleSelect(itemKey)} - captionId={k} - complement={displaySearchNbFacetResults ? nbResultsFacet : undefined} + description={subtitle} + onPress={onSelect} + captionId={item.key} + complement={nbResultsFacet} /> diff --git a/src/features/search/components/SearchBox/SearchBox.tsx b/src/features/search/components/SearchBox/SearchBox.tsx index 70e147bc9a9..388ff2a93a6 100644 --- a/src/features/search/components/SearchBox/SearchBox.tsx +++ b/src/features/search/components/SearchBox/SearchBox.tsx @@ -230,7 +230,6 @@ export const SearchBox: React.FunctionComponent = ({ locationFilter: searchState.locationFilter, venue: searchState.venue, offerCategories: searchState.offerCategories, - offerNativeCategories: searchState.offerNativeCategories, gtls: searchState.gtls, priceRange: searchState.priceRange, searchId, @@ -242,7 +241,6 @@ export const SearchBox: React.FunctionComponent = ({ partialSearchState = { ...partialSearchState, offerCategories, - offerNativeCategories: undefined, gtls: [], } } @@ -264,7 +262,6 @@ export const SearchBox: React.FunctionComponent = ({ searchState.locationFilter, searchState.venue, searchState.offerCategories, - searchState.offerNativeCategories, searchState.gtls, searchState.priceRange, pushWithSearch, diff --git a/src/features/search/components/SearchSuggestions/SearchSuggestions.tsx b/src/features/search/components/SearchSuggestions/SearchSuggestions.tsx index c8636b1a740..69404084e94 100644 --- a/src/features/search/components/SearchSuggestions/SearchSuggestions.tsx +++ b/src/features/search/components/SearchSuggestions/SearchSuggestions.tsx @@ -65,7 +65,7 @@ export const SearchSuggestions = ({ isFromHistory: true, isAutocomplete: undefined, offerGenreTypes: undefined, - offerNativeCategories: item.nativeCategory ? [item.nativeCategory] : undefined, + offerNativeCategories: item.nativeCategory ? [item.nativeCategory] : [], offerCategories: offerCategories ?? (item.category ? [item.category] : []), gtls: [], } diff --git a/src/features/search/components/sections/Category/Category.tsx b/src/features/search/components/sections/Category/Category.tsx index 7c263be5048..dd631858eae 100644 --- a/src/features/search/components/sections/Category/Category.tsx +++ b/src/features/search/components/sections/Category/Category.tsx @@ -1,55 +1,33 @@ -import React, { useCallback, useMemo } from 'react' +import React from 'react' -import { SearchGroupNameEnumv2 } from 'api/gen' import { useSearchResults } from 'features/search/api/useSearchResults/useSearchResults' import { FilterRow } from 'features/search/components/FilterRow/FilterRow' -import { useSearch } from 'features/search/context/SearchWrapper' import { FilterBehaviour } from 'features/search/enums' -import { getDescription } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' -import { CategoriesModal } from 'features/search/pages/modals/CategoriesModal/CategoriesModal' -import { DescriptionContext } from 'features/search/types' -import { useSubcategories } from 'libs/subcategories/useSubcategories' +import { + CategoriesModal, + getPreselectionLabel, +} from 'features/search/pages/modals/CategoriesModal/CategoriesModal' import { useModal } from 'ui/components/modals/useModal' import { Sort } from 'ui/svg/icons/Sort' +import { useSearch } from 'features/search/context/SearchWrapper' type Props = { onClose?: VoidFunction } export const Category = ({ onClose }: Props) => { - const { searchState } = useSearch() - const { data } = useSubcategories() const { facets } = useSearchResults() - const { - visible: categoriesModalVisible, - showModal: showCategoriesModal, - hideModal: hideCategoriesModal, - } = useModal(false) - - const onPress = useCallback(() => { - showCategoriesModal() - }, [showCategoriesModal]) - - const descriptionContext: DescriptionContext = useMemo(() => { - return { - category: searchState.offerCategories[0] ?? SearchGroupNameEnumv2.NONE, - nativeCategory: searchState.offerNativeCategories?.[0] ?? null, - genreType: searchState.offerGenreTypes?.[0]?.name ?? null, - } - }, [searchState.offerCategories, searchState.offerGenreTypes, searchState.offerNativeCategories]) - - const description = useMemo( - () => getDescription(data, descriptionContext), - [data, descriptionContext] - ) + const { visible, showModal, hideModal } = useModal(false) + const { searchState } = useSearch() + const description = getPreselectionLabel(searchState.offerCategories) return ( - + -export type CategoryCriteria = { - icon: React.FC +export type CategoryAppearance = { illustration: React.FC - facetFilter: SearchGroupNameEnumv2 baseColor: ColorsEnum gradients: Gradient - position: number - // v2 App Design fillColor: ColorsEnum borderColor: ColorsEnum textColor: ColorsEnum } -type CategoryCriteriaWithNone = { - [category in SearchGroupNameEnumv2]: category extends SearchGroupNameEnumv2.NONE - ? { - icon: React.FC - illustration: undefined - facetFilter: SearchGroupNameEnumv2 - baseColor: undefined - gradients: undefined - position: undefined - // v2 App Design - fillColor: undefined - borderColor: undefined - textColor: undefined - } - : CategoryCriteria +export const hasIcon = (categoryKey: CategoryKey): categoryKey is SearchGroupNameEnumv2 => + categoryKey in SearchGroupNameEnumv2 + +export const CATEGORY_ICONS: Record> = { + NONE: categoriesIcons.All, + CONCERTS_FESTIVALS: categoriesIcons.Conference, + FILMS_SERIES_CINEMA: categoriesIcons.Cinema, + CINEMA: categoriesIcons.Cinema, + FILMS_DOCUMENTAIRES_SERIES: categoriesIcons.Cinema, + LIVRES: categoriesIcons.Book, + CD_VINYLE_MUSIQUE_EN_LIGNE: categoriesIcons.Disk, + MUSIQUE: categoriesIcons.Disk, + ARTS_LOISIRS_CREATIFS: categoriesIcons.Palette, + SPECTACLES: categoriesIcons.Show, + MUSEES_VISITES_CULTURELLES: categoriesIcons.Museum, + JEUX_JEUX_VIDEOS: categoriesIcons.VideoGame, + INSTRUMENTS: categoriesIcons.Instrument, + MEDIA_PRESSE: categoriesIcons.Press, + CARTES_JEUNES: categoriesIcons.Card, + RENCONTRES_CONFERENCES: categoriesIcons.Microphone, + EVENEMENTS_EN_LIGNE: categoriesIcons.LiveEvent, } -export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { - [SearchGroupNameEnumv2.NONE]: { - icon: categoriesIcons.All, - illustration: undefined, - facetFilter: SearchGroupNameEnumv2.NONE, - baseColor: undefined, - gradients: undefined, - position: undefined, - textColor: undefined, - borderColor: undefined, - fillColor: undefined, - }, +export const CATEGORY_APPEARANCE: Record< + Exclude, + CategoryAppearance +> = { [SearchGroupNameEnumv2.CONCERTS_FESTIVALS]: { - icon: categoriesIcons.Conference, illustration: SearchCategoriesIllustrations.ConcertsFestivals, - facetFilter: SearchGroupNameEnumv2.CONCERTS_FESTIVALS, - position: 1, baseColor: theme.colors.goldDark, gradients: gradientColorsMapping.Gold, textColor: theme.colors.lilacDark, @@ -107,21 +99,15 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.goldLight100, }, [SearchGroupNameEnumv2.FILMS_SERIES_CINEMA]: { - icon: categoriesIcons.Cinema, illustration: SearchCategoriesIllustrations.FilmsSeriesCinema, - facetFilter: SearchGroupNameEnumv2.FILMS_SERIES_CINEMA, - position: 2, - baseColor: theme.colors.skyBlueDark, - gradients: gradientColorsMapping.SkyBlue, - textColor: theme.colors.coralDark, - borderColor: theme.colors.skyBlue, - fillColor: theme.colors.skyBlueLight, + baseColor: theme.colors.aquamarineDark, + gradients: gradientColorsMapping.Aquamarine, + textColor: theme.colors.skyBlueDark, + borderColor: theme.colors.coral, + fillColor: theme.colors.coralLight, }, [SearchGroupNameEnumv2.CINEMA]: { - icon: categoriesIcons.Cinema, illustration: SearchCategoriesIllustrations.FilmsSeriesCinema, - facetFilter: SearchGroupNameEnumv2.CINEMA, - position: 2, baseColor: theme.colors.skyBlueDark, gradients: gradientColorsMapping.SkyBlue, textColor: theme.colors.coralDark, @@ -129,10 +115,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.skyBlueLight, }, [SearchGroupNameEnumv2.FILMS_DOCUMENTAIRES_SERIES]: { - icon: categoriesIcons.Cinema, illustration: SearchCategoriesIllustrations.FilmsSeriesCinema, - facetFilter: SearchGroupNameEnumv2.FILMS_DOCUMENTAIRES_SERIES, - position: 3, baseColor: theme.colors.lilacDark, gradients: gradientColorsMapping.Lilac, textColor: theme.colors.deepPinkDark, @@ -140,10 +123,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.lilacLight, }, [SearchGroupNameEnumv2.LIVRES]: { - icon: categoriesIcons.Book, illustration: SearchCategoriesIllustrations.Books, - facetFilter: SearchGroupNameEnumv2.LIVRES, - position: 4, baseColor: theme.colors.goldDark, gradients: gradientColorsMapping.Gold, textColor: theme.colors.skyBlueDark, @@ -151,10 +131,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.coralLight, }, [SearchGroupNameEnumv2.CD_VINYLE_MUSIQUE_EN_LIGNE]: { - icon: categoriesIcons.Disk, illustration: SearchCategoriesIllustrations.CDVinylsOnlineMusic, - facetFilter: SearchGroupNameEnumv2.CD_VINYLE_MUSIQUE_EN_LIGNE, - position: 5, baseColor: theme.colors.coralDark, gradients: gradientColorsMapping.Coral, textColor: theme.colors.lilacDark, @@ -162,10 +139,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.aquamarineLight, }, [SearchGroupNameEnumv2.MUSIQUE]: { - icon: categoriesIcons.Disk, illustration: SearchCategoriesIllustrations.CDVinylsOnlineMusic, - facetFilter: SearchGroupNameEnumv2.MUSIQUE, - position: 5, baseColor: theme.colors.coralDark, gradients: gradientColorsMapping.Coral, textColor: theme.colors.lilacDark, @@ -173,10 +147,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.aquamarineLight, }, [SearchGroupNameEnumv2.ARTS_LOISIRS_CREATIFS]: { - icon: categoriesIcons.Palette, illustration: SearchCategoriesIllustrations.ArtsCrafts, - facetFilter: SearchGroupNameEnumv2.ARTS_LOISIRS_CREATIFS, - position: 6, baseColor: theme.colors.deepPinkDark, gradients: gradientColorsMapping.DeepPink, textColor: theme.colors.aquamarineDark, @@ -184,10 +155,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.deepPinkLight, }, [SearchGroupNameEnumv2.SPECTACLES]: { - icon: categoriesIcons.Show, illustration: SearchCategoriesIllustrations.Shows, - facetFilter: SearchGroupNameEnumv2.SPECTACLES, - position: 7, baseColor: theme.colors.aquamarineDark, gradients: gradientColorsMapping.Aquamarine, textColor: theme.colors.lilacDark, @@ -195,10 +163,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.goldLight100, }, [SearchGroupNameEnumv2.MUSEES_VISITES_CULTURELLES]: { - icon: categoriesIcons.Museum, illustration: SearchCategoriesIllustrations.MuseumCulturalVisits, - facetFilter: SearchGroupNameEnumv2.MUSEES_VISITES_CULTURELLES, - position: 8, baseColor: theme.colors.skyBlueDark, gradients: gradientColorsMapping.SkyBlue, textColor: theme.colors.coralDark, @@ -206,10 +171,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.skyBlueLight, }, [SearchGroupNameEnumv2.JEUX_JEUX_VIDEOS]: { - icon: categoriesIcons.VideoGame, illustration: SearchCategoriesIllustrations.GamesVideoGames, - facetFilter: SearchGroupNameEnumv2.JEUX_JEUX_VIDEOS, - position: 9, baseColor: theme.colors.lilacDark, gradients: gradientColorsMapping.Lilac, textColor: theme.colors.deepPinkDark, @@ -217,10 +179,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.lilacLight, }, [SearchGroupNameEnumv2.INSTRUMENTS]: { - icon: categoriesIcons.Instrument, illustration: SearchCategoriesIllustrations.MusicalInstruments, - facetFilter: SearchGroupNameEnumv2.INSTRUMENTS, - position: 10, baseColor: theme.colors.goldDark, gradients: gradientColorsMapping.Gold, textColor: theme.colors.skyBlueDark, @@ -228,10 +187,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.coralLight, }, [SearchGroupNameEnumv2.MEDIA_PRESSE]: { - icon: categoriesIcons.Press, illustration: SearchCategoriesIllustrations.MediaPress, - facetFilter: SearchGroupNameEnumv2.MEDIA_PRESSE, - position: 10, baseColor: theme.colors.goldDark, gradients: gradientColorsMapping.Gold, textColor: theme.colors.skyBlueDark, @@ -239,10 +195,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.coralLight, }, [SearchGroupNameEnumv2.CARTES_JEUNES]: { - icon: categoriesIcons.Card, illustration: SearchCategoriesIllustrations.YouthCards, - facetFilter: SearchGroupNameEnumv2.CARTES_JEUNES, - position: 11, baseColor: theme.colors.goldDark, gradients: gradientColorsMapping.Gold, textColor: theme.colors.lilacDark, @@ -250,10 +203,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.aquamarineLight, }, [SearchGroupNameEnumv2.RENCONTRES_CONFERENCES]: { - icon: categoriesIcons.Microphone, illustration: SearchCategoriesIllustrations.ConferencesMeetings, - facetFilter: SearchGroupNameEnumv2.RENCONTRES_CONFERENCES, - position: 12, baseColor: theme.colors.deepPinkDark, gradients: gradientColorsMapping.DeepPink, textColor: theme.colors.aquamarineDark, @@ -261,10 +211,7 @@ export const CATEGORY_CRITERIA: CategoryCriteriaWithNone = { fillColor: theme.colors.deepPinkLight, }, [SearchGroupNameEnumv2.EVENEMENTS_EN_LIGNE]: { - icon: categoriesIcons.LiveEvent, illustration: SearchCategoriesIllustrations.OnlineEvents, - position: 13, - facetFilter: SearchGroupNameEnumv2.EVENEMENTS_EN_LIGNE, baseColor: theme.colors.goldDark, gradients: gradientColorsMapping.Gold, textColor: theme.colors.lilacDark, diff --git a/src/features/search/helpers/availableCategories/availableCategories.test.ts b/src/features/search/helpers/availableCategories/availableCategories.test.ts index ef40a91449c..4a04404b346 100644 --- a/src/features/search/helpers/availableCategories/availableCategories.test.ts +++ b/src/features/search/helpers/availableCategories/availableCategories.test.ts @@ -1,14 +1,14 @@ import omit from 'lodash/omit' import { SearchGroupNameEnumv2 } from 'api/gen' -import { CATEGORY_CRITERIA } from 'features/search/enums' +import { CATEGORY_APPEARANCE } from 'features/search/enums' import { availableCategories } from 'features/search/helpers/availableCategories/availableCategories' describe('availableCategories', () => { it('should return all searchable categories from CATEGORY_CRITERIA - without NONE, FILMS_SERIES_CINEMA, INSTRUMENTS, CD_VINYLE_MUSIQUE_EN_LIGNE', () => { expect(availableCategories).toEqual( omit( - CATEGORY_CRITERIA, + CATEGORY_APPEARANCE, SearchGroupNameEnumv2.NONE, SearchGroupNameEnumv2.FILMS_SERIES_CINEMA, SearchGroupNameEnumv2.INSTRUMENTS, diff --git a/src/features/search/helpers/availableCategories/availableCategories.ts b/src/features/search/helpers/availableCategories/availableCategories.ts index a6920d001af..0c610d2c5ce 100644 --- a/src/features/search/helpers/availableCategories/availableCategories.ts +++ b/src/features/search/helpers/availableCategories/availableCategories.ts @@ -1,11 +1,11 @@ import omit from 'lodash/omit' import { SearchGroupNameEnumv2 } from 'api/gen' -import { CATEGORY_CRITERIA } from 'features/search/enums' +import { CATEGORY_APPEARANCE } from 'features/search/enums' // The available categories are every category expect "None" or "Toutes les catégories" export const availableCategories = omit( - CATEGORY_CRITERIA, + CATEGORY_APPEARANCE, SearchGroupNameEnumv2.NONE, SearchGroupNameEnumv2.FILMS_SERIES_CINEMA, SearchGroupNameEnumv2.INSTRUMENTS, diff --git a/src/features/search/helpers/categoriesHelpers/categories.ts b/src/features/search/helpers/categoriesHelpers/categories.ts new file mode 100644 index 00000000000..d3fc51c87f6 --- /dev/null +++ b/src/features/search/helpers/categoriesHelpers/categories.ts @@ -0,0 +1,109 @@ +import { NativeCategoryIdEnumv2, SearchGroupNameEnumv2 } from 'api/gen' +import { ALL_CATEGORIES_LABEL } from 'features/search/constants' +import { FACETS_FILTERS_ENUM } from 'libs/algolia/enums/facetsEnums' + +export type CategoryKey = NativeCategoryIdEnumv2 | SearchGroupNameEnumv2 | string + +export type BaseCategory = { + children: CategoryKey[] + label: string + key: CategoryKey + position?: number + searchFilter?: FACETS_FILTERS_ENUM + searchValue?: string + nbResultsFacet?: number + showChildren?: boolean +} +export type CategoriesMapping = Record +export type TopLevelCategory = BaseCategory & { + key: Exclude +} + +export const ROOT_ALL: BaseCategory = { + children: [], + label: ALL_CATEGORIES_LABEL, + key: 'NONE', + position: -Infinity, +} +export const ALL: BaseCategory = { + children: [], + label: 'Tout', + key: 'ALL', + position: -Infinity, +} +export const ROOT: BaseCategory = { + children: [], + label: 'Catégories', + key: 'ROOT', + position: -Infinity, +} + +export type CategoryResponseModel = { + key: CategoryKey + label: string + position?: number + children: CategoryKey[] +} + +export const DEFAULT_CATEGORIES: BaseCategory[] = [ + { + key: 'CINEMA', + label: 'Cinéma', + position: 2, + children: ['SEANCE'], + searchFilter: FACETS_FILTERS_ENUM.OFFER_SEARCH_GROUP_NAME, + searchValue: 'CINEMA', + }, + { + key: 'LIVRES', + label: 'Livres', + position: 1, + children: ['BIBLIOTHEQUE', 'LIVRES_PAPIER', 'LIVRES_AUDIO'], + searchFilter: FACETS_FILTERS_ENUM.OFFER_SEARCH_GROUP_NAME, + searchValue: 'LIVRES', + }, + { + key: 'LIVRES_PAPIER', + label: 'Livres papier', + position: 1, + children: ['ROMANS_ET_LITTERATURE', 'MANGAS'], + showChildren: true, + searchFilter: FACETS_FILTERS_ENUM.OFFER_NATIVE_CATEGORY, + searchValue: 'LIVRES_PAPIER', + }, + { + key: 'LIVRES_AUDIO', + label: 'Livres audio', + position: 2, + children: [], + }, + { + key: 'BIBLIOTHEQUE', + label: 'Bibliothèque', + position: 3, + children: [], + }, + { + key: 'ROMANS_ET_LITTERATURE', + label: 'Romans et littérature', + position: 1, + children: ['ROMANCE'], + searchFilter: FACETS_FILTERS_ENUM.OFFER_NATIVE_CATEGORY, + searchValue: 'LIVRES_PAPIER', + }, + { + key: 'MANGAS', + label: 'Mangas', + position: 2, + children: [], + }, + { + key: 'ROMANCE', + label: 'Romance', + position: 1, + children: [], + }, + { key: 'MUSIQUE', label: 'Musique', position: 3, children: ['SEANCE'] }, + { key: 'SEANCE', label: 'Séance de cinéma', position: 1, children: ['THRILLER'] }, + { key: 'THRILLER', label: 'Thriller', position: 1, children: [] }, +] diff --git a/src/features/search/helpers/categoriesHelpers/categoriesHelpers.test.ts b/src/features/search/helpers/categoriesHelpers/categoriesHelpers.test.ts index b9bac5bfa16..1a5eeeeb517 100644 --- a/src/features/search/helpers/categoriesHelpers/categoriesHelpers.test.ts +++ b/src/features/search/helpers/categoriesHelpers/categoriesHelpers.test.ts @@ -1,47 +1,25 @@ import { - GenreType, NativeCategoryIdEnumv2, SearchGroupNameEnumv2, SubcategoriesResponseModelv2, } from 'api/gen' -import { initialSearchState } from 'features/search/context/reducer' -import { CategoriesModalView } from 'features/search/enums' +import { ALL_CATEGORIES_LABEL } from 'features/search/constants' import { - buildBookSearchPayloadValues, - getDefaultFormView, - getFacetTypeFromGenreTypeKey, - getNativeCategories, + ALL, + CategoryResponseModel, + getCategoriesMapping, getNativeCategoryFromEnum, getNbResultsFacetLabel, - getSearchGroupsEnumArrayFromNativeCategoryEnum, - isNativeCategoryOfCategory, isOnlyOnline, - searchGroupOrNativeCategorySortComparator, + ROOT, + ROOT_ALL, sortCategoriesPredicate, useSubcategoryIdsFromSearchGroups, } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' -import { - BaseCategory, - createMappingTree, -} from 'features/search/helpers/categoriesHelpers/mapping-tree' -import { BooksNativeCategoriesEnum, SearchState } from 'features/search/types' -import { FACETS_FILTERS_ENUM } from 'libs/algolia/enums/facetsEnums' -import { - searchGroupsDataTest, - subcategoriesDataTest, -} from 'libs/subcategories/fixtures/subcategoriesResponse' import { PLACEHOLDER_DATA } from 'libs/subcategories/placeholderData' import { reactQueryProviderHOC } from 'tests/reactQueryProviderHOC' import { renderHook } from 'tests/utils' -let mockSearchState: SearchState = { - ...initialSearchState, -} - -const mockedFacets = undefined - -const tree = createMappingTree(subcategoriesDataTest, mockedFacets) - jest.mock('libs/firebase/analytics/analytics') jest.mock('libs/firebase/remoteConfig/remoteConfig.services') @@ -52,158 +30,171 @@ jest.mock('libs/subcategories/useSubcategories', () => ({ }), })) -const mockAvailableCategoriesList: SearchGroupNameEnumv2[] = [ - SearchGroupNameEnumv2.CONCERTS_FESTIVALS, - SearchGroupNameEnumv2.CINEMA, - SearchGroupNameEnumv2.FILMS_DOCUMENTAIRES_SERIES, - SearchGroupNameEnumv2.LIVRES, - SearchGroupNameEnumv2.CD_VINYLE_MUSIQUE_EN_LIGNE, - SearchGroupNameEnumv2.ARTS_LOISIRS_CREATIFS, - SearchGroupNameEnumv2.SPECTACLES, - SearchGroupNameEnumv2.MUSEES_VISITES_CULTURELLES, - SearchGroupNameEnumv2.JEUX_JEUX_VIDEOS, - SearchGroupNameEnumv2.INSTRUMENTS, - SearchGroupNameEnumv2.MEDIA_PRESSE, - SearchGroupNameEnumv2.CARTES_JEUNES, - SearchGroupNameEnumv2.RENCONTRES_CONFERENCES, - SearchGroupNameEnumv2.EVENEMENTS_EN_LIGNE, -] - describe('categoriesHelpers', () => { - it('should sort categories by alphabetical order', () => { - const categories = searchGroupsDataTest - .filter((category) => category.name !== SearchGroupNameEnumv2.NONE) - .sort(searchGroupOrNativeCategorySortComparator) - - expect(categories).toEqual([ - { - name: 'ARTS_LOISIRS_CREATIFS', - value: 'Arts & loisirs créatifs', - }, - { - name: 'CARTES_JEUNES', - value: 'Cartes jeunes', - }, - { - name: 'CD_VINYLE_MUSIQUE_EN_LIGNE', - value: 'CD, vinyles, musique en ligne', - }, - { - name: 'CINEMA', - value: 'Cinéma', - }, - { - name: 'CONCERTS_FESTIVALS', - value: 'Concerts & festivals', - }, - { - name: 'RENCONTRES_CONFERENCES', - value: 'Conférences & rencontres', - }, - { - name: 'EVENEMENTS_EN_LIGNE', - value: 'Évènements en ligne', - }, - { - name: 'FILMS_DOCUMENTAIRES_SERIES', - value: 'Films, séries et documentaires', - }, - { - name: 'INSTRUMENTS', - value: 'Instruments de musique', - }, - { - name: 'JEUX_JEUX_VIDEOS', - value: 'Jeux & jeux vidéos', - }, - { - name: 'LIVRES', - value: 'Livres', - }, - { - name: 'MEDIA_PRESSE', - value: 'Médias & presse', - }, - { - name: 'MUSEES_VISITES_CULTURELLES', - value: 'Musées & visites culturelles', - }, - { - name: 'MUSIQUE', - value: 'Musique', - }, - { - name: 'SPECTACLES', - value: 'Spectacles', - }, - ]) - }) + describe('getCategoriesMapping', () => { + it('should build minimal structure', () => { + const categories: CategoryResponseModel[] = [] + const expectedMapping = { + [ROOT.key]: { + children: [ROOT_ALL.key], + label: 'Catégories', + key: 'ROOT', + position: -Infinity, + }, + [ROOT_ALL.key]: { + children: [], + label: ALL_CATEGORIES_LABEL, + key: 'NONE', + position: -Infinity, + }, + [ALL.key]: { + children: [], + label: 'Tout', + key: 'ALL', + position: -Infinity, + }, + } - it('should sort native subcategories by alphabetical order', () => { - const nativeSubcategories = getNativeCategories( - subcategoriesDataTest, - SearchGroupNameEnumv2.ARTS_LOISIRS_CREATIFS - ) + expect(getCategoriesMapping(categories)).toEqual(expectedMapping) + }) + + it('should handle multiple parents', () => { + const categories: CategoryResponseModel[] = [ + { key: 'CINEMA', label: 'Cinéma', children: ['THRILLER'] }, + { key: 'LIVRES', label: 'Livres', children: ['THRILLER'] }, + ] + const expectedMapping = { + [ROOT.key]: { + children: [ROOT_ALL.key, 'CINEMA', 'LIVRES'], + label: 'Catégories', + key: 'ROOT', + position: -Infinity, + }, + [ROOT_ALL.key]: { + children: [], + label: ALL_CATEGORIES_LABEL, + key: 'NONE', + position: -Infinity, + }, + [ALL.key]: { + children: [], + label: 'Tout', + key: 'ALL', + position: -Infinity, + }, + CINEMA: { + key: 'CINEMA', + label: 'Cinéma', + children: ['ALL', 'THRILLER'], + }, + LIVRES: { + key: 'LIVRES', + label: 'Livres', + children: ['ALL', 'THRILLER'], + }, + } - expect(nativeSubcategories).toEqual([ - { - genreType: null, - parents: ['ARTS_LOISIRS_CREATIFS'], - name: 'ARTS_VISUELS', - value: 'Arts visuels', - }, - { - genreType: null, - parents: ['ARTS_LOISIRS_CREATIFS'], - name: 'MATERIELS_CREATIFS', - value: 'Matériels créatifs', - }, - { - genreType: null, - parents: ['ARTS_LOISIRS_CREATIFS', 'EVENEMENTS_EN_LIGNE'], - name: 'PRATIQUE_ARTISTIQUE_EN_LIGNE', - value: 'Pratique artistique en ligne', - }, - { - genreType: null, - parents: ['ARTS_LOISIRS_CREATIFS'], - name: 'PRATIQUES_ET_ATELIERS_ARTISTIQUES', - value: 'Pratiques & ateliers artistiques', - }, - ]) - }) + expect(getCategoriesMapping(categories)).toEqual(expectedMapping) + }) + + it('should not have ALL as only child', () => { + const categories: CategoryResponseModel[] = [{ key: 'CINEMA', label: 'Cinéma', children: [] }] + const expectedMapping = { + [ROOT.key]: { + children: [ROOT_ALL.key, 'CINEMA'], + label: 'Catégories', + key: 'ROOT', + position: -Infinity, + }, + [ROOT_ALL.key]: { + children: [], + label: ALL_CATEGORIES_LABEL, + key: 'NONE', + position: -Infinity, + }, + [ALL.key]: { + children: [], + label: 'Tout', + key: 'ALL', + position: -Infinity, + }, + CINEMA: { + key: 'CINEMA', + label: 'Cinéma', + children: [], + }, + } + expect(getCategoriesMapping(categories)).toEqual(expectedMapping) + }) + + it('should be idempotent', () => { + const categories: CategoryResponseModel[] = [ + { key: 'CINEMA', label: 'Cinéma', children: ['THRILLER'] }, + { key: 'LIVRES', label: 'Livres', children: ['THRILLER'] }, + ] + const expectedMapping = { + [ROOT.key]: { + children: [ROOT_ALL.key, 'CINEMA', 'LIVRES'], + label: 'Catégories', + key: 'ROOT', + position: -Infinity, + }, + [ROOT_ALL.key]: { + children: [], + label: ALL_CATEGORIES_LABEL, + key: 'NONE', + position: -Infinity, + }, + [ALL.key]: { + children: [], + label: 'Tout', + key: 'ALL', + position: -Infinity, + }, + CINEMA: { + key: 'CINEMA', + label: 'Cinéma', + children: ['ALL', 'THRILLER'], + }, + LIVRES: { + key: 'LIVRES', + label: 'Livres', + children: ['ALL', 'THRILLER'], + }, + } + getCategoriesMapping(categories) + expect(getCategoriesMapping(categories)).toEqual(expectedMapping) + }) + }) describe('isOnlyOnline', () => { it('should return false when category and native category are undefined', () => { - const value = isOnlyOnline(subcategoriesDataTest) + const value = isOnlyOnline([]) expect(value).toEqual(false) }) describe('Category', () => { it('should return true when all native categories of the category are online platform', () => { - const value = isOnlyOnline(subcategoriesDataTest, SearchGroupNameEnumv2.EVENEMENTS_EN_LIGNE) + const value = isOnlyOnline([SearchGroupNameEnumv2.EVENEMENTS_EN_LIGNE]) expect(value).toEqual(true) }) it('should return false when all native categories of the category are offline', () => { - const value = isOnlyOnline(subcategoriesDataTest, SearchGroupNameEnumv2.LIVRES) + const value = isOnlyOnline([SearchGroupNameEnumv2.LIVRES]) expect(value).toEqual(false) }) it('should return false when native categories of the category are online and offline platform', () => { - const value = isOnlyOnline(subcategoriesDataTest, SearchGroupNameEnumv2.SPECTACLES) + const value = isOnlyOnline([SearchGroupNameEnumv2.SPECTACLES]) expect(value).toEqual(false) }) it('should return false when native categories of the category are online, offline and online or offline platform', () => { - const value = isOnlyOnline( - subcategoriesDataTest, - SearchGroupNameEnumv2.ARTS_LOISIRS_CREATIFS - ) + const value = isOnlyOnline([SearchGroupNameEnumv2.ARTS_LOISIRS_CREATIFS]) expect(value).toEqual(false) }) @@ -211,41 +202,25 @@ describe('categoriesHelpers', () => { describe('Native category', () => { it('should return true when all pro subcategories of the native category are online platform', () => { - const value = isOnlyOnline( - subcategoriesDataTest, - undefined, - NativeCategoryIdEnumv2.PRATIQUE_ARTISTIQUE_EN_LIGNE - ) + const value = isOnlyOnline([NativeCategoryIdEnumv2.PRATIQUE_ARTISTIQUE_EN_LIGNE]) expect(value).toEqual(true) }) it('should return false when all pro subcategories of the native category are offline', () => { - const value = isOnlyOnline( - subcategoriesDataTest, - undefined, - NativeCategoryIdEnumv2.ACHAT_LOCATION_INSTRUMENT - ) + const value = isOnlyOnline([NativeCategoryIdEnumv2.ACHAT_LOCATION_INSTRUMENT]) expect(value).toEqual(false) }) it('should return false when pro subcategories of the native category are online and offline platform', () => { - const value = isOnlyOnline( - subcategoriesDataTest, - undefined, - NativeCategoryIdEnumv2.VISITES_CULTURELLES - ) + const value = isOnlyOnline([NativeCategoryIdEnumv2.VISITES_CULTURELLES]) expect(value).toEqual(false) }) it('should return false when pro subcategories of the native category are offline and online or offline platform', () => { - const value = isOnlyOnline( - subcategoriesDataTest, - undefined, - NativeCategoryIdEnumv2.ARTS_VISUELS - ) + const value = isOnlyOnline([NativeCategoryIdEnumv2.ARTS_VISUELS]) expect(value).toEqual(false) }) @@ -260,16 +235,13 @@ describe('categoriesHelpers', () => { }) it('should return undefined when native category id is undefined', () => { - const value = getNativeCategoryFromEnum(subcategoriesDataTest, undefined) + const value = getNativeCategoryFromEnum(PLACEHOLDER_DATA, undefined) expect(value).toEqual(undefined) }) it('should return the native category from native category id', () => { - const value = getNativeCategoryFromEnum( - subcategoriesDataTest, - NativeCategoryIdEnumv2.ARTS_VISUELS - ) + const value = getNativeCategoryFromEnum(PLACEHOLDER_DATA, NativeCategoryIdEnumv2.ARTS_VISUELS) expect(value).toEqual({ genreType: null, @@ -280,159 +252,6 @@ describe('categoriesHelpers', () => { }) }) - describe('getSearchGroupsEnumArrayFromNativeCategoryEnum', () => { - describe('should return an empty array', () => { - it('when no data from backend', () => { - const value = getSearchGroupsEnumArrayFromNativeCategoryEnum() - - expect(value).toEqual([]) - }) - - it('without native category in parameter', () => { - const value = getSearchGroupsEnumArrayFromNativeCategoryEnum(subcategoriesDataTest) - - expect(value).toEqual([]) - }) - }) - - it('should return an array of categories id', () => { - const value = getSearchGroupsEnumArrayFromNativeCategoryEnum( - subcategoriesDataTest, - NativeCategoryIdEnumv2.ARTS_VISUELS, - mockAvailableCategoriesList - ) - - expect(value).toEqual([SearchGroupNameEnumv2.ARTS_LOISIRS_CREATIFS]) - }) - }) - - describe('isNativeCategoryOfCategory', () => { - it('should return false when no data from backend', () => { - const value = isNativeCategoryOfCategory() - - expect(value).toEqual(false) - }) - - it('should return false when native category not associated to category', () => { - const value = isNativeCategoryOfCategory( - subcategoriesDataTest, - SearchGroupNameEnumv2.CONCERTS_FESTIVALS, - NativeCategoryIdEnumv2.ACHAT_LOCATION_INSTRUMENT - ) - - expect(value).toEqual(false) - }) - - it('should return true when native category associated to category', () => { - const value = isNativeCategoryOfCategory( - subcategoriesDataTest, - SearchGroupNameEnumv2.ARTS_LOISIRS_CREATIFS, - NativeCategoryIdEnumv2.ARTS_VISUELS - ) - - expect(value).toEqual(true) - }) - }) - - describe('getDefaultFormView', () => { - describe('should render categories view', () => { - it('by default when no category selected', () => { - mockSearchState = { - ...mockSearchState, - offerCategories: [], - } - - expect(getDefaultFormView(tree, mockSearchState)).toEqual(CategoriesModalView.CATEGORIES) - }) - - it('when category selected is "Cartes jeunes" because it does not native categories', () => { - mockSearchState = { - ...mockSearchState, - offerCategories: [SearchGroupNameEnumv2.CARTES_JEUNES], - } - - expect(getDefaultFormView(tree, mockSearchState)).toEqual(CategoriesModalView.CATEGORIES) - }) - }) - - describe('should render native categories view', () => { - it('when category that is not "Cartes jeunes" because the native category view includes "Tout" choice selected', () => { - mockSearchState = { - ...mockSearchState, - offerCategories: [SearchGroupNameEnumv2.LIVRES], - offerNativeCategories: [], - } - - expect(getDefaultFormView(tree, mockSearchState)).toEqual( - CategoriesModalView.NATIVE_CATEGORIES - ) - }) - - it('when a category that is not "Cartes jeunes" and a native category that it has not genre type selected', () => { - mockSearchState = { - ...mockSearchState, - offerCategories: [SearchGroupNameEnumv2.LIVRES], - offerNativeCategories: [NativeCategoryIdEnumv2.LIVRES_AUDIO_PHYSIQUES], - } - - expect(getDefaultFormView(tree, mockSearchState)).toEqual( - CategoriesModalView.NATIVE_CATEGORIES - ) - }) - }) - - describe('should render genre type categories view', () => { - it('when a category, a native category that it has genre type selected', () => { - mockSearchState = { - ...mockSearchState, - offerCategories: [SearchGroupNameEnumv2.SPECTACLES], - offerNativeCategories: [NativeCategoryIdEnumv2.SPECTACLES_REPRESENTATIONS], - } - - expect(getDefaultFormView(tree, mockSearchState)).toEqual(CategoriesModalView.GENRES) - }) - - it('when a category, a native category, a genre type categories selected', () => { - mockSearchState = { - ...mockSearchState, - offerCategories: [SearchGroupNameEnumv2.LIVRES], - offerNativeCategories: [NativeCategoryIdEnumv2.LIVRES_PAPIER], - offerGenreTypes: [ - { key: GenreType.BOOK, name: 'Bandes dessinées', value: 'Bandes dessinées' }, - ], - } - - expect(getDefaultFormView(tree, mockSearchState)).toEqual(CategoriesModalView.GENRES) - }) - }) - }) - - describe('getFacetTypeFromGenreTypeKey', () => { - it('should return OFFER_BOOK_TYPE for "BOOK"', () => { - const result = getFacetTypeFromGenreTypeKey(GenreType.BOOK) - - expect(result).toEqual(FACETS_FILTERS_ENUM.OFFER_BOOK_TYPE) - }) - - it('should return OFFER_MUSIC_TYPE for "MUSIC"', () => { - const result = getFacetTypeFromGenreTypeKey(GenreType.MUSIC) - - expect(result).toEqual(FACETS_FILTERS_ENUM.OFFER_MUSIC_TYPE) - }) - - it('should return OFFER_SHOW_TYPE for "SHOW"', () => { - const result = getFacetTypeFromGenreTypeKey(GenreType.SHOW) - - expect(result).toEqual(FACETS_FILTERS_ENUM.OFFER_SHOW_TYPE) - }) - - it('should return OFFER_MOVIE_GENRES for "MOVIE"', () => { - const result = getFacetTypeFromGenreTypeKey(GenreType.MOVIE) - - expect(result).toEqual(FACETS_FILTERS_ENUM.OFFER_MOVIE_GENRES) - }) - }) - describe('getNbResultsFacetLabel', () => { it('should display "+10000" when the number of result facets is greater than 10000', () => { const result = getNbResultsFacetLabel(10001) @@ -453,93 +272,12 @@ describe('categoriesHelpers', () => { }) it('should return undefined when the number of result facets is undefined', () => { - const result = getNbResultsFacetLabel(undefined) + const result = getNbResultsFacetLabel() expect(result).toEqual(undefined) }) }) - describe('buildBookSearchPayloadValues', () => { - it('should return search payload for a book native category level', () => { - const mockedForm = { - category: SearchGroupNameEnumv2.LIVRES, - nativeCategory: BooksNativeCategoriesEnum.MANGAS, - currentView: CategoriesModalView.CATEGORIES, - genreType: null, - } - - const result = buildBookSearchPayloadValues(subcategoriesDataTest, mockedForm) - - expect(result).toEqual({ - offerCategories: [SearchGroupNameEnumv2.LIVRES], - offerNativeCategories: [BooksNativeCategoriesEnum.MANGAS], - offerGenreTypes: undefined, - gtls: [ - { - code: '03040300', - label: 'Kodomo', - level: 3, - }, - { - code: '03040400', - label: 'Shôjo', - level: 3, - }, - { - code: '03040500', - label: 'Shonen', - level: 3, - }, - { - code: '03040700', - label: 'Josei', - level: 3, - }, - { - code: '03040800', - label: 'Yaoi', - level: 3, - }, - { - code: '03040900', - label: 'Yuri', - level: 3, - }, - ], - }) - }) - - it('should return search payload for a book genre type level', () => { - const mockedForm = { - category: SearchGroupNameEnumv2.LIVRES, - nativeCategory: BooksNativeCategoriesEnum.MANGAS, - currentView: CategoriesModalView.GENRES, - genreType: 'KODOMO', - } - - const result = buildBookSearchPayloadValues(subcategoriesDataTest, mockedForm) - - expect(result).toEqual({ - offerCategories: [SearchGroupNameEnumv2.LIVRES], - offerNativeCategories: [BooksNativeCategoriesEnum.MANGAS], - offerGenreTypes: [ - { - key: GenreType.BOOK, - name: 'KODOMO', - value: 'Kodomo', - }, - ], - gtls: [ - { - code: '03040300', - label: 'Kodomo', - level: 3, - }, - ], - }) - }) - }) - describe('useSubcategoryIdsFromSearchGroup', () => { it('should return subcategories of one given searchGroup', () => { const searchGroups = [SearchGroupNameEnumv2.LIVRES] @@ -611,8 +349,8 @@ describe('categoriesHelpers', () => { describe('sortCategoriesPredicate', () => { it('should sort following position ascending order', () => { - const lowestPosition: BaseCategory = { label: 'a', position: 0 } - const greatestPosition: BaseCategory = { label: 'b', position: 1 } + const lowestPosition = { label: 'a', position: 0, key: 'a', children: [] } + const greatestPosition = { label: 'b', position: 1, key: 'b', children: [] } expect( [greatestPosition, lowestPosition].sort((a, b) => sortCategoriesPredicate(a, b)) @@ -620,8 +358,8 @@ describe('categoriesHelpers', () => { }) it('should prioritize position over label', () => { - const withoutPosition: BaseCategory = { label: 'a' } - const withPosition: BaseCategory = { label: 'b', position: 1 } + const withoutPosition = { label: 'a', key: 'a', children: [] } + const withPosition = { label: 'b', position: 1, key: 'b', children: [] } expect([withoutPosition, withPosition].sort((a, b) => sortCategoriesPredicate(a, b))).toEqual( [withPosition, withoutPosition] @@ -629,8 +367,8 @@ describe('categoriesHelpers', () => { }) it('should sort following label alphabetical ascending order if no positions', () => { - const firstLabel: BaseCategory = { label: 'a' } - const lastLabel: BaseCategory = { label: 'b' } + const firstLabel = { label: 'a', key: 'a', children: [] } + const lastLabel = { label: 'b', key: 'b', children: [] } expect([lastLabel, firstLabel].sort((a, b) => sortCategoriesPredicate(a, b))).toEqual([ firstLabel, @@ -639,8 +377,8 @@ describe('categoriesHelpers', () => { }) it('should sort following labels if positions are equal', () => { - const firstLabel: BaseCategory = { label: 'a', position: 1 } - const lastLabel: BaseCategory = { label: 'b', position: 1 } + const firstLabel = { label: 'a', position: 1, key: 'a', children: [] } + const lastLabel = { label: 'b', position: 1, key: 'b', children: [] } expect([lastLabel, firstLabel].sort((a, b) => sortCategoriesPredicate(a, b))).toEqual([ firstLabel, diff --git a/src/features/search/helpers/categoriesHelpers/categoriesHelpers.ts b/src/features/search/helpers/categoriesHelpers/categoriesHelpers.ts index b01dc813ac4..4cf0d6d14f7 100644 --- a/src/features/search/helpers/categoriesHelpers/categoriesHelpers.ts +++ b/src/features/search/helpers/categoriesHelpers/categoriesHelpers.ts @@ -1,211 +1,84 @@ import { - BookSubType, BookType, GenreType, NativeCategoryIdEnumv2, - NativeCategoryResponseModelv2, OnlineOfflinePlatformChoicesEnumv2, SearchGroupNameEnumv2, - SearchGroupResponseModelv2, SubcategoriesResponseModelv2, SubcategoryIdEnumv2, } from 'api/gen' -import { useSearchResults } from 'features/search/api/useSearchResults/useSearchResults' -import { ALL_CATEGORIES_LABEL } from 'features/search/constants' -import { CATEGORY_CRITERIA, CategoriesModalView } from 'features/search/enums' import { + ALL, BaseCategory, - MappingTree, - createMappingTree, - getBooksGenreTypes, - getBooksNativeCategories, - getKeyFromStringLabel, -} from 'features/search/helpers/categoriesHelpers/mapping-tree' -import { CategoriesModalFormProps } from 'features/search/pages/modals/CategoriesModal/CategoriesModal' -import { - BooksNativeCategoriesEnum, - DescriptionContext, - NativeCategoryEnum, - SearchState, -} from 'features/search/types' + CategoriesMapping, + CategoryKey, + DEFAULT_CATEGORIES, + ROOT, + ROOT_ALL, + TopLevelCategory, +} from 'features/search/helpers/categoriesHelpers/categories' import { FACETS_FILTERS_ENUM } from 'libs/algolia/enums/facetsEnums' +import { PLACEHOLDER_DATA } from 'libs/subcategories/placeholderData' import { useSubcategories } from 'libs/subcategories/useSubcategories' -type Item = SearchGroupNameEnumv2 | NativeCategoryIdEnumv2 | string | null +export const getCategoriesMapping = (categories: BaseCategory[] = DEFAULT_CATEGORIES) => { + const mapping = categories.reduce((mapping, category) => { + mapping[category.key] = structuredClone(category) + return mapping + }, {} as CategoriesMapping) + mapping[ROOT.key] = structuredClone(ROOT) + mapping[ROOT_ALL.key] = structuredClone(ROOT_ALL) + mapping[ALL.key] = structuredClone(ALL) -function isBookNativeCategory( - category: NativeCategoryEnum | null -): category is BooksNativeCategoriesEnum { - return (category as BooksNativeCategoriesEnum) !== undefined + const childrenCategories = categories.map((category) => category.children).flat() + const rootCategories = categories + .filter((category) => !childrenCategories.includes(category.key)) + .map((category) => category.key) + mapping[ROOT.key]?.children.push(...rootCategories) + return mapping } -function isNativeCategory(category: NativeCategoryEnum | null): category is NativeCategoryIdEnumv2 { - return (category as NativeCategoryIdEnumv2) !== undefined +export const getCategory = (categoryKey: CategoryKey) => { + const mapping = getCategoriesMapping() + return mapping[categoryKey] } -export const buildBookSearchPayloadValues = ( - data: SubcategoriesResponseModelv2, - form: CategoriesModalFormProps -) => { - const bookTrees = data.genreTypes.find((genreType) => genreType.name === GenreType.BOOK) - ?.trees as BookType[] - - const buildNativeCategoryGtls = (nativeCategory: typeof form.nativeCategory) => { - const nativeCat = bookTrees.find( - (category: BookType) => getKeyFromStringLabel(category.label) === nativeCategory - ) - return nativeCat?.gtls - } - - const buildGenreTypeGtls = ( - nativeCategory: typeof form.nativeCategory, - genreType: typeof form.genreType - ) => { - const nativeCat = bookTrees.find( - (category: BookType) => getKeyFromStringLabel(category.label) === nativeCategory - ) - return nativeCat?.children.find((genre) => getKeyFromStringLabel(genre.label) === genreType) - ?.gtls - } - - const buildBookGenreType = (genreType?: BookSubType) => { - const genreKey = getKeyFromStringLabel(genreType?.label) - if (!!genreType && !!genreKey) { - return [ - { - name: genreKey, - value: genreType.label, - key: GenreType.BOOK, - }, - ] - } - return undefined - } - - let gtls - const natCatGtls = buildNativeCategoryGtls(form.nativeCategory) - const genreTypeGtls = buildGenreTypeGtls(form.nativeCategory, form.genreType) - - if (form.genreType) { - gtls = genreTypeGtls?.map((gtl) => gtl) - } else if (form.nativeCategory) { - gtls = natCatGtls?.map((gtl) => gtl) - } - - const nativeCat = bookTrees.find( - (category: BookType) => getKeyFromStringLabel(category.label) === form.nativeCategory - ) - - const genreType = nativeCat?.children.find( - (genre) => getKeyFromStringLabel(genre.label) === form.genreType - ) - - const offerNativeCategories = - form.nativeCategory && isNativeCategory(form.nativeCategory) ? [form.nativeCategory] : [] - const offerBookNativeCategories = - form.nativeCategory && isBookNativeCategory(form.nativeCategory) ? [form.nativeCategory] : [] - - const offerGenericNativeCategories = offerBookNativeCategories ? offerNativeCategories : [] - - return { - offerCategories: [form.category], - offerNativeCategories: offerGenericNativeCategories, - offerGenreTypes: buildBookGenreType(genreType), - gtls, - } +export const getCategoryChildren = (categoryKey: CategoryKey) => { + const mapping = getCategoriesMapping() + const category = getCategory(categoryKey) + return (category?.children ?? []) + .map((category) => mapping[category]) + .filter((category) => !!category) + .sort(sortCategoriesPredicate) } -function buildSearchPayloadValues( - data: SubcategoriesResponseModelv2, - form: CategoriesModalFormProps -) { - const buildGenreType = (genreTypeId: typeof form.genreType) => { - if (genreTypeId === null) return [] - const genreType = getGenreTypeFromEnum(data, genreTypeId) - if (!genreType) return undefined - - const genreTypeKey = data.genreTypes.find((genreType) => - genreType.values.map((v) => v.name).includes(genreTypeId) - )?.name - if (!genreTypeKey) return undefined - - return [{ name: genreTypeId, value: genreType.value, key: genreTypeKey }] - } - - if (form.category === SearchGroupNameEnumv2.LIVRES) { - return buildBookSearchPayloadValues(data, form) - } - - const genreType = buildGenreType(form.genreType) - if (!genreType) return undefined - - const offerNativeCategories = - form.nativeCategory && isNativeCategory(form.nativeCategory) ? [form.nativeCategory] : [] - const offerBookNativeCategories = - form.nativeCategory && isBookNativeCategory(form.nativeCategory) ? [form.nativeCategory] : [] +export const getCategoryParents = (categoryKey: CategoryKey) => { + const mapping = getCategoriesMapping() + return Object.values(mapping).filter((category) => category.children.includes(categoryKey)) +} - const offerGenericNativeCategories = offerBookNativeCategories ? offerNativeCategories : [] +export const isTopLevelCategory = (category: BaseCategory): category is TopLevelCategory => + category.key in SearchGroupNameEnumv2 && category.key !== SearchGroupNameEnumv2.NONE - return { - offerCategories: form.category === SearchGroupNameEnumv2.NONE ? [] : [form.category], - offerNativeCategories: offerGenericNativeCategories, - offerGenreTypes: genreType, - gtls: [], - } +export const getTopLevelCategories = () => { + return getCategoryChildren(ROOT.key).filter(isTopLevelCategory) // `filter` is useless here, but it helps typing } -/** - * Returns unique objects in array distinct by object key - */ -function getUniqueBy(arr: T[], key: keyof T) { - return [...new Map(arr.map((item) => [item[key], item])).values()] +export const categoryExists = (categoryKey: CategoryKey) => { + return Object.keys(getCategoriesMapping()).includes(categoryKey) } -function getCategoryFromEnum(data: undefined, enumValue: undefined): undefined -function getCategoryFromEnum(data: undefined, enumValue: SearchGroupNameEnumv2): undefined -function getCategoryFromEnum(data: SubcategoriesResponseModelv2, enumValue: undefined): undefined -function getCategoryFromEnum( - data: SubcategoriesResponseModelv2, - enumValue: SearchGroupNameEnumv2 -): SearchGroupResponseModelv2 -function getCategoryFromEnum( - data: SubcategoriesResponseModelv2 | undefined, - enumValue?: SearchGroupNameEnumv2 -) { - if (data && enumValue) { - return data.searchGroups.find((category) => category.name === enumValue) - } - - return undefined +export const isChild = (childKey: CategoryKey, parentKey: CategoryKey) => { + return getCategoriesMapping()[parentKey]?.children.includes(childKey) ?? false } -/** - * Returns correct icon for a category. - */ -export function getIcon(item: T) { - return CATEGORY_CRITERIA[item]?.icon -} +export function getBooksNativeCategories(data: SubcategoriesResponseModelv2) { + const bookTree = data.genreTypes.find(({ name }) => name === GenreType.BOOK)?.trees as BookType[] -export function getCategoriesModalTitle( - data: SubcategoriesResponseModelv2, - currentView: CategoriesModalView, - categoryId: SearchGroupNameEnumv2, - nativeCategoryId: NativeCategoryEnum | null -) { - switch (currentView) { - case CategoriesModalView.CATEGORIES: - return 'Catégories' - case CategoriesModalView.NATIVE_CATEGORIES: { - const category = getCategoryFromEnum(data, categoryId) - return category?.value ?? 'Sous-catégories' - } - case CategoriesModalView.GENRES: { - const nativeCategory = getNativeCategoryFromEnum(data, nativeCategoryId ?? undefined) - return nativeCategory?.value ?? 'Genres' - } - default: - return 'Catégories' - } + return bookTree.map((bookCategory) => { + const categoryName = getKeyFromStringLabel(bookCategory.label) + return { name: categoryName, value: bookCategory.label, genreType: GenreType.BOOK } + }) } /** @@ -213,7 +86,7 @@ export function getCategoriesModalTitle( */ export function getNativeCategoryFromEnum( data: SubcategoriesResponseModelv2 | undefined, - enumValue: NativeCategoryIdEnumv2 | BooksNativeCategoriesEnum | undefined + enumValue?: CategoryKey ) { if (data && enumValue) { return ( @@ -225,78 +98,26 @@ export function getNativeCategoryFromEnum( return undefined } -function getGenreTypeFromEnum(data: SubcategoriesResponseModelv2 | undefined, genreType?: string) { - if (data && genreType) { - const genre = data.genreTypes - .map((gt) => gt.values) - .flat() - .find((genreTypeValue) => genreTypeValue.name === genreType) - - const bookGenre = getBooksGenreTypes(data).find( - (genreTypeValue) => genreTypeValue.name === genreType - ) - return genre ?? bookGenre - } - - return undefined -} - -/** - * Returns whether the category or native category is only online or not. - * @param data - * @param categoryId - * @param nativeCategoryId - */ -export function isOnlyOnline( - data: SubcategoriesResponseModelv2, - categoryId?: SearchGroupNameEnumv2, - nativeCategoryId?: NativeCategoryIdEnumv2 | BooksNativeCategoriesEnum -) { - if (!categoryId && !nativeCategoryId) { - return false - } - +export function isOnlyOnline(categoryKeys: CategoryKey[]) { + const { data = PLACEHOLDER_DATA } = useSubcategories() const platforms: OnlineOfflinePlatformChoicesEnumv2[] = [ ...new Set( data.subcategories .filter((subcategory) => - nativeCategoryId - ? subcategory.nativeCategoryId === nativeCategoryId - : subcategory.searchGroupName === categoryId + categoryKeys.some( + (categoryKey) => + subcategory.searchGroupName === categoryKey || + subcategory.nativeCategoryId === categoryKey + ) ) .map((subcategory) => subcategory.onlineOfflinePlatform) ), ] - const isOnlyOnline = + return ( platforms.includes(OnlineOfflinePlatformChoicesEnumv2.ONLINE) && !platforms.includes(OnlineOfflinePlatformChoicesEnumv2.ONLINE_OR_OFFLINE) && !platforms.includes(OnlineOfflinePlatformChoicesEnumv2.OFFLINE) - - return isOnlyOnline -} - -/** - * Sort comparator for `SearchGroupResponseModelv2` and `NativeCategoryResponseModelv2` objects. - */ -export function searchGroupOrNativeCategorySortComparator< - T extends SearchGroupResponseModelv2 | NativeCategoryResponseModelv2, ->(a: T, b: T) { - return (a?.value ?? '').localeCompare(b?.value ?? '') -} - -/** - * Check whether a native category is a subcategory of a category. - */ -export function isNativeCategoryOfCategory( - data?: SubcategoriesResponseModelv2, - searchGroup?: SearchGroupNameEnumv2, - nativeCategory?: NativeCategoryIdEnumv2 -) { - if (!data) return false - return data.subcategories.some( - (subcategory) => - subcategory.searchGroupName === searchGroup && subcategory.nativeCategoryId === nativeCategory ) } @@ -321,53 +142,6 @@ export function getSearchGroupsEnumArrayFromNativeCategoryEnum( return [...new Set(searchGroup)] } -/** - * Return `nativeCategory` array for a `SearchGroupResponseModelv2` category. - */ -export function getNativeCategories( - data: SubcategoriesResponseModelv2 | undefined, - categoryEnum: SearchGroupNameEnumv2 | undefined -) { - if (!data) return [] - if (!categoryEnum) return [] - if (categoryEnum === SearchGroupNameEnumv2.NONE) return [] - - const nativeCategories = data.nativeCategories.filter((nativeCategory) => - nativeCategory.parents.includes(categoryEnum) - ) - - return getUniqueBy(nativeCategories, 'name').sort(searchGroupOrNativeCategorySortComparator) -} - -type Entries = { - [K in keyof T]: [K, T[K]] -}[keyof T][] - -function typedEntries>(obj: T): Entries { - return Object.entries(obj) as Entries -} - -export const useNativeCategories = (searchGroup?: SearchGroupNameEnumv2) => { - const { data: subcategories } = useSubcategories() - const { facets } = useSearchResults() - if (!searchGroup || !subcategories) return [] - - const tree = createMappingTree(subcategories, facets) - if (searchGroup === SearchGroupNameEnumv2.NONE || !tree[searchGroup].children) return [] - - const nativeCategories = typedEntries(tree[searchGroup].children) - if (searchGroup !== SearchGroupNameEnumv2.LIVRES) return nativeCategories - - const bookNativeCategories = nativeCategories.filter( - ([_k, item]) => item.genreTypeKey === GenreType.BOOK && item.label !== 'Livres papier' - ) - const additionalBookNativeCategories = nativeCategories.filter( - ([_k, item]) => item.genreTypeKey !== GenreType.BOOK - ) - - return [...bookNativeCategories, ...additionalBookNativeCategories] -} - export const useSubcategoryIdsFromSearchGroups = ( searchGroups: SearchGroupNameEnumv2[] ): SubcategoryIdEnumv2[] => { @@ -388,164 +162,6 @@ export const useSubcategoryIdsFromSearchGroups = ( .map((filteredSubcategory) => filteredSubcategory.id) } -function getIsCategory(item: Item): item is SearchGroupNameEnumv2 { - return Object.values(SearchGroupNameEnumv2).includes(item as SearchGroupNameEnumv2) -} - -function getIsNativeCategory( - item: Item -): item is NativeCategoryIdEnumv2 | BooksNativeCategoriesEnum { - return ( - Object.values(NativeCategoryIdEnumv2).includes(item as NativeCategoryIdEnumv2) || - Object.values(BooksNativeCategoriesEnum).includes(item as BooksNativeCategoriesEnum) - ) -} - -function getFilterRowDescriptionFromNativeCategoryAndGenre( - data: SubcategoriesResponseModelv2, - nativeCategoryId: NativeCategoryIdEnumv2 | BooksNativeCategoriesEnum | null, - genreTypeId: string -) { - if (genreTypeId && nativeCategoryId) { - const nativeCategory = getNativeCategoryFromEnum(data, nativeCategoryId) - const genreType = getGenreTypeFromEnum(data, genreTypeId) - if (!nativeCategory || !genreType || !nativeCategory.value) return undefined - return `${nativeCategory.value} - ${genreType.value}` - } - - return undefined -} - -function getFilterRowDescriptionFromNativeCategory( - data: SubcategoriesResponseModelv2, - nativeCategoryId: NativeCategoryIdEnumv2 | BooksNativeCategoriesEnum | null -) { - if (nativeCategoryId) { - const nativeCategory = getNativeCategoryFromEnum(data, nativeCategoryId) - if (!nativeCategory) return undefined - return nativeCategory.value ?? undefined - } - - return undefined -} - -function getFilterRowDescriptionFromCategory( - data: SubcategoriesResponseModelv2, - categoryId: SearchGroupNameEnumv2 -) { - if (categoryId === SearchGroupNameEnumv2.NONE) return ALL_CATEGORIES_LABEL - - const category = getCategoryFromEnum(data, categoryId) - if (!category) return undefined - return category.value ?? undefined -} - -function getFilterRowDescription(data: SubcategoriesResponseModelv2, ctx: DescriptionContext) { - const { category: categoryId, nativeCategory: nativeCategoryId, genreType: genreTypeId } = ctx - - if (genreTypeId && nativeCategoryId) { - return getFilterRowDescriptionFromNativeCategoryAndGenre(data, nativeCategoryId, genreTypeId) - } - if (nativeCategoryId) { - return getFilterRowDescriptionFromNativeCategory(data, nativeCategoryId) - } - - if (categoryId) { - return getFilterRowDescriptionFromCategory(data, categoryId) - } - return undefined -} - -function getCategoryDescription( - data: SubcategoriesResponseModelv2, - ctx: DescriptionContext, - item: SearchGroupNameEnumv2 -) { - const { category: categoryId, nativeCategory: nativeCategoryId, genreType: genreTypeId } = ctx - - if (item === SearchGroupNameEnumv2.NONE) return undefined - if (item !== categoryId) return undefined - if (!nativeCategoryId) return 'Tout' - - const nativeCategory = getNativeCategoryFromEnum(data, nativeCategoryId) - if (!nativeCategory) return undefined - if (!nativeCategory.value) return undefined - - if (!genreTypeId) return nativeCategory.value - const genreType = getGenreTypeFromEnum(data, genreTypeId) - - if (!genreType) return nativeCategory.value - return `${nativeCategory.value} - ${genreType.value}` -} - -function getNativeCategoryDescription( - data: SubcategoriesResponseModelv2, - ctx: DescriptionContext, - item: NativeCategoryIdEnumv2 | BooksNativeCategoriesEnum -) { - const { nativeCategory: nativeCategoryId, genreType: genreTypeId } = ctx - - if (!nativeCategoryId) return undefined - if (nativeCategoryId !== item) return undefined - if (!genreTypeId) return 'Tout' - - const genreType = getGenreTypeFromEnum(data, genreTypeId) - return genreType?.value -} - -export function getDescription( - data: SubcategoriesResponseModelv2 | undefined, - ctx: DescriptionContext, - item?: Item -) { - const { category: categoryId } = ctx - if (!categoryId) return undefined - if (!data) return undefined - - if (!item) { - return getFilterRowDescription(data, ctx) - } - - if (getIsCategory(item)) { - return getCategoryDescription(data, ctx, item) - } - - if (getIsNativeCategory(item)) { - return getNativeCategoryDescription(data, ctx, item) - } - - return undefined -} - -export function getDefaultFormView(tree: MappingTree, searchState: SearchState) { - const { offerGenreTypes, offerCategories, offerNativeCategories } = searchState - - if (!offerCategories?.[0]) return CategoriesModalView.CATEGORIES - const category = tree[offerCategories[0]] - const nativeCategories = category?.children - const nativeCategory = - offerNativeCategories?.[0] && nativeCategories - ? nativeCategories[offerNativeCategories[0]] - : undefined - - if (offerGenreTypes?.length || nativeCategory?.children) return CategoriesModalView.GENRES - if (offerNativeCategories?.length || Object.keys(nativeCategories ?? {}).length) - return CategoriesModalView.NATIVE_CATEGORIES - return CategoriesModalView.CATEGORIES -} - -export function getDefaultFormValues( - tree: MappingTree, - searchState: SearchState -): CategoriesModalFormProps { - return { - category: searchState.offerCategories[0] ?? SearchGroupNameEnumv2.NONE, - nativeCategory: searchState.offerNativeCategories?.[0] ?? null, - genreType: searchState.offerGenreTypes?.[0]?.name ?? null, - currentView: getDefaultFormView(tree, searchState), - } -} - export function getFacetTypeFromGenreTypeKey(genreTypeKey: GenreType) { switch (genreTypeKey) { case GenreType.BOOK: @@ -570,22 +186,17 @@ export function getNbResultsFacetLabel(nbResultsFacet?: number) { } } -export const handleCategoriesSearchPress = ( - form: CategoriesModalFormProps, - data: SubcategoriesResponseModelv2 -) => { - const payload = buildSearchPayloadValues(data, form) - if (!payload) return - - let isFullyDigitalOffersCategory = false - if (payload.offerNativeCategories.length > 0) { - isFullyDigitalOffersCategory = isOnlyOnline(data, undefined, payload.offerNativeCategories[0]) - } else if (payload.offerCategories.length > 0) { - isFullyDigitalOffersCategory = isOnlyOnline(data, payload.offerCategories[0]) - } - - return { payload, isFullyDigitalOffersCategory } -} - export const sortCategoriesPredicate = (a: BaseCategory, b: BaseCategory) => (a.position ?? Infinity) - (b.position ?? Infinity) || a.label.localeCompare(b.label) + +export function getKeyFromStringLabel(input?: string | null): string | null { + if (!input) return null + return input + .toUpperCase() + .replace('&', 'ET') + .replace('-', '_') + .replace(',', '') + .replace(/ /g, '_') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') +} diff --git a/src/features/search/helpers/categoriesHelpers/mapping-tree.test.ts b/src/features/search/helpers/categoriesHelpers/mapping-tree.test.ts deleted file mode 100644 index 3c4b157f2c7..00000000000 --- a/src/features/search/helpers/categoriesHelpers/mapping-tree.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createMappingTree } from 'features/search/helpers/categoriesHelpers/mapping-tree' -import { bookTreeResultFixture } from 'features/search/helpers/categoriesHelpers/mappingFixture' -import { PLACEHOLDER_DATA } from 'libs/subcategories/placeholderData' - -const mockedSubcateroriesV2Response = PLACEHOLDER_DATA -const mockedUndefinedFacets = undefined - -jest.mock('libs/firebase/analytics/analytics') -jest.mock('libs/firebase/remoteConfig/remoteConfig.services') - -describe('MappingTree', () => { - it('should return a mapping tree for book category ROMANS_ET_LITTERATURE', () => { - const result = createMappingTree(mockedSubcateroriesV2Response, mockedUndefinedFacets) - - expect(result.LIVRES.children?.ROMANS_ET_LITTERATURE).toEqual( - expect.objectContaining(bookTreeResultFixture.SearchGroup.children.ROMANS_ET_LITTERATURE) - ) - }) -}) diff --git a/src/features/search/helpers/categoriesHelpers/mapping-tree.ts b/src/features/search/helpers/categoriesHelpers/mapping-tree.ts deleted file mode 100644 index 9fb751fbe12..00000000000 --- a/src/features/search/helpers/categoriesHelpers/mapping-tree.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { - BookType, - GenreType, - GTL, - NativeCategoryResponseModelv2, - SearchGroupNameEnumv2, - SubcategoriesResponseModelv2, -} from 'api/gen' -import { ALL_CATEGORIES_LABEL } from 'features/search/constants' -import { CATEGORY_CRITERIA } from 'features/search/enums' -import { availableCategories } from 'features/search/helpers/availableCategories/availableCategories' -import { getNativeCategories } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' -import { OfferGenreType } from 'features/search/types' -import { FACETS_FILTERS_ENUM } from 'libs/algolia/enums/facetsEnums' -import { FacetData, NativeCategoryFacetData } from 'libs/algolia/types' - -export type BaseCategory = { - label: string - position?: number -} -type MappedGenreType = BaseCategory & { - nbResultsFacet?: never - gtls?: GTL[] -} -export type MappedGenreTypes = Record -type MappedNativeCategory = BaseCategory & { - nbResultsFacet?: number - genreTypeKey?: GenreType - children?: MappedGenreTypes - gtls?: GTL[] -} -export type MappedNativeCategories = Record -type MappedCategory = BaseCategory & { - children: MappedNativeCategories -} -export type MappingTree = Record - -function getNativeCategoryGenreTypes( - data: SubcategoriesResponseModelv2, - nativeCategory: NativeCategoryResponseModelv2 -): Omit | undefined { - const genreType = data.genreTypes.find((genreType) => genreType.name === nativeCategory.genreType) - if (!genreType) return undefined - - return { - genreTypeKey: genreType.name, - children: genreType.values.reduce((res, genre) => { - res[genre.name] = { label: genre.value } - return res - }, {}), - } -} - -export function getBooksNativeCategories(data: SubcategoriesResponseModelv2) { - const bookTree = data.genreTypes.find(({ name }) => name === GenreType.BOOK)?.trees as BookType[] - - return bookTree.map((bookCategory) => { - const categoryName = getKeyFromStringLabel(bookCategory.label) - return { name: categoryName, value: bookCategory.label, genreType: GenreType.BOOK } - }) -} - -export function getBooksGenreTypes(data: SubcategoriesResponseModelv2): OfferGenreType[] { - const bookTree = data.genreTypes.find(({ name }) => name === GenreType.BOOK)?.trees as BookType[] - - return bookTree - .map((bookCategory) => - bookCategory.children.map((bookGenre) => { - return { - name: getKeyFromStringLabel(bookGenre.label), - value: bookGenre.label, - key: GenreType.BOOK, - } as OfferGenreType - }) - ) - .flat() -} - -function mapBookCategories(data: SubcategoriesResponseModelv2) { - /* - Here we make fake native categories out of the book genres - The purpose is to be able to display them at the native category level in the `CategoriesModal`. - */ - const bookTree = data.genreTypes.find(({ name }) => name === GenreType.BOOK)?.trees as BookType[] - - return bookTree.reduce((genresMapping, bookGenre) => { - const genreKey = getKeyFromStringLabel(bookGenre.label) - if (!genreKey) return genresMapping - - genresMapping[genreKey] = { - ...bookGenre, - genreTypeKey: GenreType.BOOK, - children: bookGenre.children.reduce((childrenMapping, child) => { - const childKey = getKeyFromStringLabel(child.label) - if (childKey) childrenMapping[childKey] = child - return childrenMapping - }, {}), - } - return genresMapping - }, {}) -} - -export function createMappingTree(data: SubcategoriesResponseModelv2, facetsData?: FacetData) { - /** - * We want to create a mapping tree that looks like this: - * { - * 'SearchGroup': { - * label: 'Tout', - * children: { - * 'NativeCategoryOne': { - * label: 'Tout', - * }, - * 'NativeCategoryTwo': { - * label: 'Évènement', - * genreTypeKey: 'EventType', - * children: { - * 'GenreType': { - * label: 'Cinéma', - * }, - * } - * }, - * } - * } - * } - */ - - return data.searchGroups - .filter( - (searchGroup) => - Object.keys(availableCategories).includes(searchGroup.name) && - Object.keys(CATEGORY_CRITERIA).includes(searchGroup.name) - ) - .sort((a, b) => { - const positionA: number = CATEGORY_CRITERIA[a.name]?.position ?? 0 - const positionB: number = CATEGORY_CRITERIA[b.name]?.position ?? 0 - return positionA - positionB - }) - .reduce((result, searchGroup) => { - const nativeCategories = getNativeCategories(data, searchGroup.name) - const mappedNativeCategories = nativeCategories.length - ? nativeCategories.reduce( - (nativeCategoriesResult, nativeCategory) => { - nativeCategoriesResult[nativeCategory.name] = { - label: nativeCategory.value ?? 'Tout', - nbResultsFacet: - (facetsData as NativeCategoryFacetData)?.[ - FACETS_FILTERS_ENUM.OFFER_NATIVE_CATEGORY - ]?.[nativeCategory.name] ?? 0, - position: nativeCategory.positions?.[searchGroup.name], - ...(getNativeCategoryGenreTypes(data, nativeCategory) || {}), - } - - return nativeCategoriesResult - }, - {} - ) - : {} - - result[searchGroup.name] = { - label: searchGroup.value || ALL_CATEGORIES_LABEL, - children: - searchGroup.name === SearchGroupNameEnumv2.LIVRES - ? { - ...mappedNativeCategories, - ...mapBookCategories(data), - } - : mappedNativeCategories, - } - - return Object.entries(result).reduce((res, [key, value]) => { - res[key as SearchGroupNameEnumv2] = value - return res - }, {} as MappingTree) - }, {} as MappingTree) -} - -export function getKeyFromStringLabel(input?: string | null): string | null { - if (!input) return null - return input - .toUpperCase() - .replace('&', 'ET') - .replace('-', '_') - .replace(',', '') - .replace(/ /g, '_') - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') -} diff --git a/src/features/search/helpers/reducer.helpers.ts b/src/features/search/helpers/reducer.helpers.ts index b676444f46e..b35daf3561f 100644 --- a/src/features/search/helpers/reducer.helpers.ts +++ b/src/features/search/helpers/reducer.helpers.ts @@ -1,11 +1,9 @@ -import { SearchGroupNameEnumv2 } from 'api/gen' import { convertCentsToEuros } from 'libs/parsers/pricesConversion' import { Range } from 'libs/typesUtils/typeHelpers' const MIN_PRICE = 0 export const MAX_PRICE_IN_CENTS = 300_00 export const MAX_RADIUS = 100 -export const DEFAULT_TIME_RANGE = [8, 24] export const clampPrice = (priceRange: Range | null | undefined): Range => { if (!priceRange) return [MIN_PRICE, convertCentsToEuros(MAX_PRICE_IN_CENTS)] @@ -13,6 +11,3 @@ export const clampPrice = (priceRange: Range | null | undefined): Range< const max = Math.min(convertCentsToEuros(MAX_PRICE_IN_CENTS), priceRange[1]) return [min, max] } - -export const sortCategories = (a: SearchGroupNameEnumv2, b: SearchGroupNameEnumv2) => - a.localeCompare(b) diff --git a/src/features/search/helpers/useAvailableCategories/useAvailableCategories.test.tsx b/src/features/search/helpers/useAvailableCategories/useAvailableCategories.test.tsx deleted file mode 100644 index d3566c464f5..00000000000 --- a/src/features/search/helpers/useAvailableCategories/useAvailableCategories.test.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { SearchGroupNameEnumv2 } from 'api/gen' -import { SearchCategoriesIllustrations } from 'features/search/enums' -import { useAvailableCategories } from 'features/search/helpers/useAvailableCategories/useAvailableCategories' -import { PLACEHOLDER_DATA } from 'libs/subcategories/placeholderData' -import { theme } from 'theme' -import { categoriesIcons } from 'ui/svg/icons/bicolor/exports/categoriesIcons' -import { gradientColorsMapping } from 'ui/theme/gradientColorsMapping' - -let mockData = PLACEHOLDER_DATA -jest.mock('libs/subcategories/useSubcategories', () => ({ - useSubcategories: () => ({ - data: mockData, - }), -})) - -describe('useAvailableCategories', () => { - it('should all availables categories', () => { - const categories = useAvailableCategories() - - expect(categories).toHaveLength(13) - expect(categories).toEqual( - expect.arrayContaining([ - { - icon: categoriesIcons.Conference, - illustration: SearchCategoriesIllustrations.ConcertsFestivals, - facetFilter: SearchGroupNameEnumv2.CONCERTS_FESTIVALS, - position: 1, - baseColor: theme.colors.goldDark, - gradients: gradientColorsMapping.Gold, - textColor: theme.colors.lilacDark, - borderColor: theme.colors.goldLight200, - fillColor: theme.colors.goldLight100, - }, - { - icon: categoriesIcons.Cinema, - illustration: SearchCategoriesIllustrations.FilmsSeriesCinema, - facetFilter: SearchGroupNameEnumv2.CINEMA, - position: 2, - baseColor: theme.colors.skyBlueDark, - gradients: gradientColorsMapping.SkyBlue, - textColor: theme.colors.coralDark, - borderColor: theme.colors.skyBlue, - fillColor: theme.colors.skyBlueLight, - }, - { - icon: categoriesIcons.Cinema, - illustration: SearchCategoriesIllustrations.FilmsSeriesCinema, - facetFilter: SearchGroupNameEnumv2.FILMS_DOCUMENTAIRES_SERIES, - position: 3, - baseColor: theme.colors.lilacDark, - gradients: gradientColorsMapping.Lilac, - textColor: theme.colors.deepPinkDark, - borderColor: theme.colors.lilac, - fillColor: theme.colors.lilacLight, - }, - { - icon: categoriesIcons.Book, - illustration: SearchCategoriesIllustrations.Books, - facetFilter: SearchGroupNameEnumv2.LIVRES, - position: 4, - baseColor: theme.colors.goldDark, - gradients: gradientColorsMapping.Gold, - textColor: theme.colors.skyBlueDark, - borderColor: theme.colors.coral, - fillColor: theme.colors.coralLight, - }, - { - icon: categoriesIcons.Disk, - illustration: SearchCategoriesIllustrations.CDVinylsOnlineMusic, - facetFilter: SearchGroupNameEnumv2.MUSIQUE, - position: 5, - baseColor: theme.colors.coralDark, - gradients: gradientColorsMapping.Coral, - textColor: theme.colors.lilacDark, - borderColor: theme.colors.aquamarineDark, - fillColor: theme.colors.aquamarineLight, - }, - { - icon: categoriesIcons.Palette, - illustration: SearchCategoriesIllustrations.ArtsCrafts, - facetFilter: SearchGroupNameEnumv2.ARTS_LOISIRS_CREATIFS, - position: 6, - baseColor: theme.colors.deepPinkDark, - gradients: gradientColorsMapping.DeepPink, - textColor: theme.colors.aquamarineDark, - borderColor: theme.colors.deepPink, - fillColor: theme.colors.deepPinkLight, - }, - { - icon: categoriesIcons.Show, - illustration: SearchCategoriesIllustrations.Shows, - facetFilter: SearchGroupNameEnumv2.SPECTACLES, - position: 7, - baseColor: theme.colors.aquamarineDark, - gradients: gradientColorsMapping.Aquamarine, - textColor: theme.colors.lilacDark, - borderColor: theme.colors.goldLight200, - fillColor: theme.colors.goldLight100, - }, - { - icon: categoriesIcons.Museum, - illustration: SearchCategoriesIllustrations.MuseumCulturalVisits, - facetFilter: SearchGroupNameEnumv2.MUSEES_VISITES_CULTURELLES, - position: 8, - baseColor: theme.colors.skyBlueDark, - gradients: gradientColorsMapping.SkyBlue, - textColor: theme.colors.coralDark, - borderColor: theme.colors.skyBlue, - fillColor: theme.colors.skyBlueLight, - }, - { - icon: categoriesIcons.VideoGame, - illustration: SearchCategoriesIllustrations.GamesVideoGames, - facetFilter: SearchGroupNameEnumv2.JEUX_JEUX_VIDEOS, - position: 9, - baseColor: theme.colors.lilacDark, - gradients: gradientColorsMapping.Lilac, - textColor: theme.colors.deepPinkDark, - borderColor: theme.colors.lilac, - fillColor: theme.colors.lilacLight, - }, - { - icon: categoriesIcons.Press, - illustration: SearchCategoriesIllustrations.MediaPress, - facetFilter: SearchGroupNameEnumv2.MEDIA_PRESSE, - position: 10, - baseColor: theme.colors.goldDark, - gradients: gradientColorsMapping.Gold, - textColor: theme.colors.skyBlueDark, - borderColor: theme.colors.coral, - fillColor: theme.colors.coralLight, - }, - { - icon: categoriesIcons.Card, - illustration: SearchCategoriesIllustrations.YouthCards, - facetFilter: SearchGroupNameEnumv2.CARTES_JEUNES, - position: 11, - baseColor: theme.colors.goldDark, - gradients: gradientColorsMapping.Gold, - textColor: theme.colors.lilacDark, - borderColor: theme.colors.aquamarineDark, - fillColor: theme.colors.aquamarineLight, - }, - { - icon: categoriesIcons.Microphone, - illustration: SearchCategoriesIllustrations.ConferencesMeetings, - facetFilter: SearchGroupNameEnumv2.RENCONTRES_CONFERENCES, - position: 12, - baseColor: theme.colors.deepPinkDark, - gradients: gradientColorsMapping.DeepPink, - textColor: theme.colors.aquamarineDark, - borderColor: theme.colors.deepPink, - fillColor: theme.colors.deepPinkLight, - }, - { - icon: categoriesIcons.LiveEvent, - illustration: SearchCategoriesIllustrations.OnlineEvents, - position: 13, - facetFilter: SearchGroupNameEnumv2.EVENEMENTS_EN_LIGNE, - baseColor: theme.colors.goldDark, - gradients: gradientColorsMapping.Gold, - textColor: theme.colors.lilacDark, - borderColor: theme.colors.goldLight200, - fillColor: theme.colors.goldLight100, - }, - ]) - ) - }) - - it('should only available catégories from backend', () => { - mockData = { - ...mockData, - searchGroups: [ - { name: SearchGroupNameEnumv2.ARTS_LOISIRS_CREATIFS, value: 'Arts & loisirs créatifs' }, - { name: SearchGroupNameEnumv2.CARTES_JEUNES, value: 'Cartes jeunes' }, - ], - } - const categories = useAvailableCategories() - - expect(categories).toEqual([ - { - icon: categoriesIcons.Palette, - illustration: SearchCategoriesIllustrations.ArtsCrafts, - facetFilter: SearchGroupNameEnumv2.ARTS_LOISIRS_CREATIFS, - position: 6, - baseColor: theme.colors.deepPinkDark, - gradients: gradientColorsMapping.DeepPink, - textColor: theme.colors.aquamarineDark, - borderColor: theme.colors.deepPink, - fillColor: theme.colors.deepPinkLight, - }, - { - icon: categoriesIcons.Card, - illustration: SearchCategoriesIllustrations.YouthCards, - facetFilter: SearchGroupNameEnumv2.CARTES_JEUNES, - position: 11, - baseColor: theme.colors.goldDark, - gradients: gradientColorsMapping.Gold, - textColor: theme.colors.lilacDark, - borderColor: theme.colors.aquamarineDark, - fillColor: theme.colors.aquamarineLight, - }, - ]) - }) - - it('should return empty array when no categories from backend', () => { - mockData = { - ...mockData, - searchGroups: [], - } - const categories = useAvailableCategories() - - expect(categories).toEqual([]) - }) -}) diff --git a/src/features/search/helpers/useAvailableCategories/useAvailableCategories.tsx b/src/features/search/helpers/useAvailableCategories/useAvailableCategories.tsx index 25d99f3dd97..fc3c2c238f7 100644 --- a/src/features/search/helpers/useAvailableCategories/useAvailableCategories.tsx +++ b/src/features/search/helpers/useAvailableCategories/useAvailableCategories.tsx @@ -1,9 +1,9 @@ import { hasAThematicSearch } from 'features/navigation/SearchStackNavigator/types' -import { CategoryCriteria } from 'features/search/enums' +import { CategoryAppearance } from 'features/search/enums' import { availableCategories } from 'features/search/helpers/availableCategories/availableCategories' import { useSubcategories } from 'libs/subcategories/useSubcategories' -export const useAvailableCategories = (): CategoryCriteria[] => { +export const useAvailableCategories = (): CategoryAppearance[] => { const { data } = useSubcategories() const searchGroupsEnum = data?.searchGroups.map((searchGroup) => searchGroup.name) ?? [] const categories = Object.values(availableCategories).filter((category) => @@ -13,7 +13,7 @@ export const useAvailableCategories = (): CategoryCriteria[] => { return categories } -export const useAvailableThematicSearchCategories = (): CategoryCriteria[] => { +export const useAvailableThematicSearchCategories = (): CategoryAppearance[] => { return Object.values(availableCategories).filter((category) => hasAThematicSearch.find((thematicSearch) => thematicSearch === category.facetFilter) ) diff --git a/src/features/search/helpers/useFilterCount/useFilterCount.native.test.ts b/src/features/search/helpers/useFilterCount/useFilterCount.native.test.ts index 54e5d05b31f..999a08212be 100644 --- a/src/features/search/helpers/useFilterCount/useFilterCount.native.test.ts +++ b/src/features/search/helpers/useFilterCount/useFilterCount.native.test.ts @@ -1,6 +1,6 @@ import { initialSearchState } from 'features/search/context/reducer' import { DATE_FILTER_OPTIONS } from 'features/search/enums' -import { DEFAULT_TIME_RANGE, MAX_PRICE_IN_CENTS } from 'features/search/helpers/reducer.helpers' +import { MAX_PRICE_IN_CENTS } from 'features/search/helpers/reducer.helpers' import { useMaxPrice } from 'features/search/helpers/useMaxPrice/useMaxPrice' import { SearchState } from 'features/search/types' import { convertCentsToEuros } from 'libs/parsers/pricesConversion' @@ -9,7 +9,7 @@ import { renderHook } from 'tests/utils' import { useFilterCount } from './useFilterCount' const date = { option: DATE_FILTER_OPTIONS.TODAY, selectedDate: new Date() } -const timeRange = DEFAULT_TIME_RANGE +const timeRange = [8, 24] const venueId = 5959 const Kourou = { label: 'Kourou', info: 'Guyane', geolocation: { latitude: 2, longitude: 3 } } diff --git a/src/features/search/helpers/useSearchHistory/useSearchHistory.ts b/src/features/search/helpers/useSearchHistory/useSearchHistory.ts index f5a5a30c2b5..aa85752ba36 100644 --- a/src/features/search/helpers/useSearchHistory/useSearchHistory.ts +++ b/src/features/search/helpers/useSearchHistory/useSearchHistory.ts @@ -2,7 +2,6 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import { useCallback, useEffect, useMemo, useState } from 'react' import { HISTORY_KEY, MAX_HISTORY_RESULTS, MIN_HISTORY_RESULTS } from 'features/search/constants' -import { getNativeCategoryFromEnum } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' import { getHistoryItemLabel } from 'features/search/helpers/getHistoryItemLabel/getHistoryItemLabel' import { getHistoryLessThan30Days } from 'features/search/helpers/useSearchHistory/helpers/getHistoryLessThan30Days' import { CreateHistoryItem, HistoryItem } from 'features/search/types' @@ -12,6 +11,10 @@ import { LogTypeEnum } from 'libs/monitoring/errors' import { useSearchGroupLabelMapping } from 'libs/subcategories/mappings' import { useSubcategories } from 'libs/subcategories/useSubcategories' import { SNACK_BAR_TIME_OUT, useSnackBarContext } from 'ui/components/snackBar/SnackBarContext' +import { + getCategory, + getNativeCategoryFromEnum, +} from 'features/search/helpers/categoriesHelpers/categoriesHelpers' export function useSearchHistory() { const { showErrorSnackBar } = useSnackBarContext() @@ -87,7 +90,7 @@ export function useSearchHistory() { currentHistory = await getHistoryFromStorage() } - const categoryLabel = item.category ? searchGroupLabelMapping[item.category] : undefined + const categoryLabel = item.category ? getCategory(item.category)?.label : undefined const nativeCategoryLabel = getNativeCategoryFromEnum(subcategoriesData, item.nativeCategory)?.value ?? undefined diff --git a/src/features/search/helpers/useShowResultsForCategory/useShowResultsForCategory.ts b/src/features/search/helpers/useShowResultsForCategory/useShowResultsForCategory.ts index 933db3662fc..63dcbc270c1 100644 --- a/src/features/search/helpers/useShowResultsForCategory/useShowResultsForCategory.ts +++ b/src/features/search/helpers/useShowResultsForCategory/useShowResultsForCategory.ts @@ -4,9 +4,8 @@ import { v4 as uuidv4 } from 'uuid' import { SearchGroupNameEnumv2 } from 'api/gen' import { useAccessibilityFiltersContext } from 'features/accessibility/context/AccessibilityFiltersWrapper' import { useSearch } from 'features/search/context/SearchWrapper' -import { isOnlyOnline } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' import { useNavigateToSearch } from 'features/search/helpers/useNavigateToSearch/useNavigateToSearch' -import { OnPressCategory } from 'features/search/helpers/useSortedSearchCategories/useSortedSearchCategories' +import { OnPressCategory } from 'features/search/helpers/useSortedSearchCategories/useSearchLandingButtonsProps' import { useFeatureFlag } from 'libs/firebase/firestore/featureFlags/useFeatureFlag' import { RemoteStoreFeatureFlags } from 'libs/firebase/firestore/types' import { useSubcategories } from 'libs/subcategories/useSubcategories' @@ -29,14 +28,14 @@ export const useShowResultsForCategory = (): OnPressCategory => { const enableWipPageThematicSearchMusic = useFeatureFlag( RemoteStoreFeatureFlags.WIP_THEMATIC_SEARCH_MUSIC ) - const THEMATIC_SEARCH_CATEGORIES: (keyof typeof SearchGroupNameEnumv2 | undefined)[] = useMemo( + const THEMATIC_SEARCH_CATEGORIES: (SearchGroupNameEnumv2 | undefined)[] = useMemo( () => [ - enableWipPageThematicSearchBooks ? 'LIVRES' : undefined, - enableWipPageThematicSearchCinema ? 'CINEMA' : undefined, + enableWipPageThematicSearchBooks ? SearchGroupNameEnumv2.LIVRES : undefined, + enableWipPageThematicSearchCinema ? SearchGroupNameEnumv2.CINEMA : undefined, enableWipPageThematicSearchFilmsDocumentairesEtSeries - ? 'FILMS_DOCUMENTAIRES_SERIES' + ? SearchGroupNameEnumv2.FILMS_DOCUMENTAIRES_SERIES : undefined, - enableWipPageThematicSearchMusic ? 'MUSIQUE' : undefined, + enableWipPageThematicSearchMusic ? SearchGroupNameEnumv2.MUSIQUE : undefined, ], [ enableWipPageThematicSearchBooks, @@ -54,13 +53,14 @@ export const useShowResultsForCategory = (): OnPressCategory => { ...searchState, offerCategories: [pressedCategory], offerSubcategories: [], - offerNativeCategories: undefined, offerGenreTypes: undefined, searchId, - isFullyDigitalOffersCategory: isOnlyOnline(data, pressedCategory) || undefined, isFromHistory: undefined, } - if (THEMATIC_SEARCH_CATEGORIES.includes(pressedCategory)) { + if ( + pressedCategory in SearchGroupNameEnumv2 && + THEMATIC_SEARCH_CATEGORIES.includes(pressedCategory as SearchGroupNameEnumv2) + ) { dispatch({ type: 'SET_STATE', payload: newSearchState, diff --git a/src/features/search/helpers/useSortedSearchCategories/useSortedSearchCategories.native.test.ts b/src/features/search/helpers/useSortedSearchCategories/useSearchLandingButtonsProps.native.test.ts similarity index 84% rename from src/features/search/helpers/useSortedSearchCategories/useSortedSearchCategories.native.test.ts rename to src/features/search/helpers/useSortedSearchCategories/useSearchLandingButtonsProps.native.test.ts index 4ae8c3cb398..d876e541a91 100644 --- a/src/features/search/helpers/useSortedSearchCategories/useSortedSearchCategories.native.test.ts +++ b/src/features/search/helpers/useSortedSearchCategories/useSearchLandingButtonsProps.native.test.ts @@ -4,8 +4,8 @@ import { renderHook } from 'tests/utils' import { categoriesSortPredicate, MappingOutput, - useSortedSearchCategories, -} from './useSortedSearchCategories' + useSearchLandingButtonProps, +} from './useSearchLandingButtonsProps' jest.mock('libs/subcategories/useSubcategories') @@ -13,25 +13,25 @@ describe('useSortedSearchCategories', () => { const options = { initialProps: jest.fn() } it('should return all categories', () => { - const { result } = renderHook(useSortedSearchCategories, options) + const { result } = renderHook(useSearchLandingButtonProps, options) expect(result.current).toHaveLength(13) }) it("should format category's label", () => { - const { result } = renderHook(useSortedSearchCategories, options) + const { result } = renderHook(useSearchLandingButtonProps, options) expect(result.current[0]?.label).toEqual('Concerts & festivals') }) it('should set illustration for category', () => { - const { result } = renderHook(useSortedSearchCategories, options) + const { result } = renderHook(useSearchLandingButtonProps, options) expect(result.current[0]?.Illustration).toEqual(SearchCategoriesIllustrations.ConcertsFestivals) }) it('should sort search group names by the key position', () => { - const { result } = renderHook(useSortedSearchCategories, options) + const { result } = renderHook(useSearchLandingButtonProps, options) const actualCategoriesLabels = result.current.map((category) => category.label) diff --git a/src/features/search/helpers/useSortedSearchCategories/useSearchLandingButtonsProps.ts b/src/features/search/helpers/useSortedSearchCategories/useSearchLandingButtonsProps.ts new file mode 100644 index 00000000000..16a7b57f9c8 --- /dev/null +++ b/src/features/search/helpers/useSortedSearchCategories/useSearchLandingButtonsProps.ts @@ -0,0 +1,38 @@ +import { CategoryButtonProps } from 'features/search/components/CategoriesListDumb/CategoriesListDumb' +import { CATEGORY_APPEARANCE } from 'features/search/enums' +import { + CategoryKey, + getTopLevelCategories, + sortCategoriesPredicate, +} from 'features/search/helpers/categoriesHelpers/categoriesHelpers' + +export type OnPressCategory = (pressedCategory: CategoryKey) => void + +type MappingOutput = (CategoryButtonProps & { position: number | undefined }) | undefined + +export const useSearchLandingButtonProps = ( + onPressCategory: OnPressCategory +): CategoryButtonProps[] => { + const categories = getTopLevelCategories() + + return categories + .toSorted(sortCategoriesPredicate) + .map((category) => { + const appearance = CATEGORY_APPEARANCE[category.key] + if (!appearance) return undefined + return { + label: category.label, + Illustration: appearance.illustration, + onPress() { + onPressCategory(category.key) + }, + baseColor: appearance.baseColor, + gradients: appearance.gradients, + position: category.position, + textColor: appearance.textColor, + borderColor: appearance.borderColor, + fillColor: appearance.fillColor, + } + }) + .filter((mappingOutput) => !!mappingOutput) +} diff --git a/src/features/search/helpers/useSortedSearchCategories/useSortedSearchCategories.ts b/src/features/search/helpers/useSortedSearchCategories/useSortedSearchCategories.ts deleted file mode 100644 index 2ef573b4002..00000000000 --- a/src/features/search/helpers/useSortedSearchCategories/useSortedSearchCategories.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { SearchGroupNameEnumv2 } from 'api/gen' -import { - CategoryButtonProps, - ListCategoryButtonProps, -} from 'features/search/components/CategoriesListDumb/CategoriesListDumb' -import { useAvailableCategories } from 'features/search/helpers/useAvailableCategories/useAvailableCategories' -import { useSearchGroupLabelMapping } from 'libs/subcategories/mappings' - -export type OnPressCategory = (pressedCategory: SearchGroupNameEnumv2) => void - -export type MappingOutput = CategoryButtonProps & { position: number | undefined } - -export function categoriesSortPredicate(a: MappingOutput, b: MappingOutput): number { - const positionA: number = a?.position || 0 - const positionB: number = b?.position || 0 - return positionA - positionB -} - -export const useSortedSearchCategories = ( - onPressCategory: OnPressCategory -): ListCategoryButtonProps => { - const searchGroupLabelMapping = useSearchGroupLabelMapping() - const categories = useAvailableCategories() - - return categories - .map((category) => ({ - label: searchGroupLabelMapping?.[category.facetFilter] || '', - Icon: category.icon, - Illustration: category.illustration, - onPress() { - onPressCategory(category.facetFilter) - }, - baseColor: category.baseColor, - gradients: category.gradients, - position: category.position, - textColor: category.textColor, - borderColor: category.borderColor, - fillColor: category.fillColor, - })) - .sort(categoriesSortPredicate) -} diff --git a/src/features/search/pages/SearchFilter/SearchFilter.tsx b/src/features/search/pages/SearchFilter/SearchFilter.tsx index 40d0013e25b..af64dd25bef 100644 --- a/src/features/search/pages/SearchFilter/SearchFilter.tsx +++ b/src/features/search/pages/SearchFilter/SearchFilter.tsx @@ -86,7 +86,6 @@ export const SearchFilter: React.FC = () => { minPrice: undefined, maxPrice: undefined, offerGenreTypes: undefined, - offerNativeCategories: undefined, beginningDatetime: undefined, date: null, endingDatetime: undefined, diff --git a/src/features/search/pages/ThematicSearch/ThematicSearch.tsx b/src/features/search/pages/ThematicSearch/ThematicSearch.tsx index ccab9198e27..8e185a072c3 100644 --- a/src/features/search/pages/ThematicSearch/ThematicSearch.tsx +++ b/src/features/search/pages/ThematicSearch/ThematicSearch.tsx @@ -9,7 +9,10 @@ import { useAccessibilityFiltersContext } from 'features/accessibility/context/A import { GtlPlaylist } from 'features/gtlPlaylist/components/GtlPlaylist' import { useGTLPlaylists } from 'features/gtlPlaylist/hooks/useGTLPlaylists' import { UseRouteType } from 'features/navigation/RootNavigator/types' -import { SearchStackRouteName } from 'features/navigation/SearchStackNavigator/types' +import { + isThematicSearchCategory, + SearchStackRouteName, +} from 'features/navigation/SearchStackNavigator/types' import { useSearchResults } from 'features/search/api/useSearchResults/useSearchResults' import { VenuePlaylist } from 'features/search/components/VenuePlaylist/VenuePlaylist' import { useSearch } from 'features/search/context/SearchWrapper' @@ -61,8 +64,9 @@ export const ThematicSearch: React.FC = () => { [selectedLocationMode] ) - const offerCategories = params?.offerCategories as SearchGroupNameEnumv2[] - const offerCategory = offerCategories?.[0] || SearchGroupNameEnumv2.LIVRES + const offerCategories = params?.offerCategories + const offerCategory = offerCategories?.[0] ?? SearchGroupNameEnumv2.LIVRES + const isBookCategory = offerCategory === SearchGroupNameEnumv2.LIVRES const isCinemaCategory = offerCategory === SearchGroupNameEnumv2.CINEMA const isFilmsCategory = offerCategory === SearchGroupNameEnumv2.FILMS_DOCUMENTAIRES_SERIES @@ -90,16 +94,18 @@ export const ThematicSearch: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, isWeb, params?.offerCategories]) + if (!isThematicSearchCategory(offerCategory)) return null + return ( {arePlaylistsLoading ? ( ) : ( - + {shouldDisplayVenuesPlaylist ? ( { + const endIndex = + preselections.length >= 2 && preselections.at(-1) === ALL.key // we don't the label to be '`child` - Tout', just 'child' + ? preselections.length - 1 + : preselections.length + + return preselections + .slice(0, endIndex) + .map((categoryKey) => getCategory(categoryKey)?.label) + .join(' - ') } export const CategoriesModal = ({ @@ -57,13 +66,13 @@ export const CategoriesModal = ({ onClose, facets, }: CategoriesModalProps) => { - const { data = PLACEHOLDER_DATA } = useSubcategories() const { modal } = useTheme() const { dispatch, searchState } = useSearch() - const tree = useMemo(() => { - return createMappingTree(data, facets) - }, [data, facets]) + const getDefaultFormValues = (searchState: SearchState): CategoriesModalFormProps => ({ + categoryStack: [ROOT.key, ...searchState.offerCategories.map((category) => category)], + currentIndex: 0, + }) const { formState: { isSubmitting }, @@ -72,68 +81,36 @@ export const CategoriesModal = ({ setValue, watch, } = useForm({ - defaultValues: getDefaultFormValues(tree, searchState), + defaultValues: getDefaultFormValues(searchState), }) - const { category, currentView, nativeCategory, genreType } = watch() - + const { categoryStack, currentIndex } = watch() useEffect(() => { - reset(getDefaultFormValues(tree, searchState)) - }, [reset, searchState, tree]) - - const nativeCategories = useMemo(() => { - return (category && - category !== SearchGroupNameEnumv2.NONE && - tree[category]?.children) as MappedNativeCategories - }, [category, tree]) - - const genreTypes = useMemo(() => { - return (nativeCategory && nativeCategories?.[nativeCategory]?.children) as MappedGenreTypes - }, [nativeCategory, nativeCategories]) + reset(getDefaultFormValues(searchState)) + }, [reset, searchState]) - const handleCategorySelect = useCallback( - (categoryKey: SearchGroupNameEnumv2) => { - setValue('category', categoryKey) - - if (categoryKey !== category) { - setValue('nativeCategory', null) - setValue('genreType', null) - } + const getIcon = (categoryKey: CategoryKey) => { + return hasIcon(categoryKey) ? CATEGORY_ICONS[categoryKey] : undefined + } - if (tree[categoryKey]?.children) { - setValue('currentView', CategoriesModalView.NATIVE_CATEGORIES) - } - }, - [category, setValue, tree] - ) + const isRootLevel = currentIndex === 0 - const handleNativeCategorySelect = useCallback( - (nativeCategoryKey: NativeCategoryEnum | null) => { - if (!nativeCategories) return + const currentItem = + (categoryStack[currentIndex] && getCategory(categoryStack[currentIndex])) || ROOT + const children = getCategoryChildren(currentItem.key) - setValue('nativeCategory', nativeCategoryKey) + const next = currentIndex + 1 // helps typing on next line + const selectedChild = (categoryStack[next] && getCategory(categoryStack[next])) || ALL - if (nativeCategoryKey !== nativeCategory) { - setValue('genreType', null) - } + const preselectionLabel = getPreselectionLabel(categoryStack.slice(currentIndex + 2)) // we want to start at currently selected child's child - if (nativeCategoryKey && nativeCategories[nativeCategoryKey]?.children) { - setValue('currentView', CategoriesModalView.GENRES) - } - }, - [nativeCategories, nativeCategory, setValue] - ) + const shouldRenderBlocks = children.some((child) => child.showChildren) - const handleGenreTypeSelect = useCallback( - (genreTypeKey: string | null) => { - setValue('genreType', genreTypeKey) - }, - [setValue] - ) + const isSelected = (item: BaseCategory) => item.key === selectedChild.key const handleModalClose = useCallback(() => { - reset(getDefaultFormValues(tree, searchState)) + reset(getDefaultFormValues(searchState)) hideModal() - }, [hideModal, reset, searchState, tree]) + }, [hideModal, reset, searchState]) const handleClose = useCallback(() => { handleModalClose() @@ -143,104 +120,126 @@ export const CategoriesModal = ({ }, [handleModalClose, onClose]) const handleGoBack = useCallback(() => { - switch (currentView) { - case CategoriesModalView.CATEGORIES: - handleModalClose() - break - case CategoriesModalView.NATIVE_CATEGORIES: - setValue('currentView', CategoriesModalView.CATEGORIES) - break - case CategoriesModalView.GENRES: - setValue('currentView', CategoriesModalView.NATIVE_CATEGORIES) - break - default: - throw new Error('Unknown current view') + if (isRootLevel) { + handleModalClose() + } else { + const newIndex = Math.max(currentIndex - 1, 0) + setValue('currentIndex', newIndex) } - }, [currentView, handleModalClose, setValue]) + }, [currentIndex, setValue]) const handleSearchPress = useCallback( (form: CategoriesModalFormProps) => { - const searchPressData = handleCategoriesSearchPress(form, data) + const offerCategories = form.categoryStack.filter( + (categoryKey) => ![ROOT.key, ROOT_ALL.key, ALL.key].includes(categoryKey) + ) + const newSearchState = { ...searchState, offerCategories } + dispatch({ type: 'SET_STATE', payload: newSearchState }) + hideModal() + }, + [dispatch, hideModal, searchState] + ) - let additionalSearchState: SearchState = { ...searchState, ...searchPressData?.payload } - additionalSearchState = { - ...additionalSearchState, - isFullyDigitalOffersCategory: searchPressData?.isFullyDigitalOffersCategory || undefined, + const handleSelect = useCallback( + (category: BaseCategory) => { + const hasChildren = category.children.length + if (!hasChildren) { + const newStack = [...categoryStack.slice(0, currentIndex + 1), category.key] + setValue('categoryStack', newStack) + handleSubmit(handleSearchPress) + } else { + const previousSelection = categoryStack[currentIndex + 1] + if (category.key !== previousSelection) { + const newStack = [...categoryStack.slice(0, currentIndex + 1), category.key, ALL.key] + setValue('categoryStack', newStack) + } + const newIndex = currentIndex + 1 + setValue('currentIndex', newIndex) } - - dispatch({ type: 'SET_STATE', payload: additionalSearchState }) - hideModal() }, - [data, dispatch, hideModal, searchState] + [categoryStack, currentIndex, setValue] ) const handleReset = useCallback(() => { - reset({ - category: SearchGroupNameEnumv2.NONE, - nativeCategory: null, - genreType: null, - currentView: CategoriesModalView.CATEGORIES, - }) + reset(getDefaultFormValues(searchState)) }, [reset]) - const descriptionContext = useMemo( - () => ({ - category, - nativeCategory, - genreType, - }), - [category, genreType, nativeCategory] - ) - - const modalTitle = useMemo(() => { - return getCategoriesModalTitle(data, currentView, category, nativeCategory) - }, [category, currentView, data, nativeCategory]) + const renderDefaultItem = () => { + const defaultItem = isRootLevel ? ROOT_ALL : ALL + return ( + handleSelect(defaultItem)} + icon={getIcon(defaultItem.key)} + subtitle={isSelected(defaultItem) ? preselectionLabel : undefined} + /> + ) + } - const shouldDisplayBackButton = useMemo( - () => - currentView !== CategoriesModalView.CATEGORIES || - filterBehaviour === FilterBehaviour.APPLY_WITHOUT_SEARCHING, - [currentView, filterBehaviour] - ) + const renderItems = () => { + const items = children.map((item) => ( + handleSelect(item)} + icon={getIcon(item.key)} + subtitle={isSelected(item) ? preselectionLabel : undefined} + /> + )) + return [renderDefaultItem(), ...items] + } - const getNativeCategoriesSection = () => { - if (category === SearchGroupNameEnumv2.LIVRES) { - return ( - { + const blocks = children + .filter((child) => child.children.length && child.showChildren) + .map((item) => ( + - ) - } - return ( - (!child.children.length || !child.showChildren) && child.key !== ALL.key + ) + const otherItemsBlock = ( + ) + return [renderDefaultItem(), ...blocks, otherItemsBlock] } + console.log(categoryStack, currentIndex) + return ( } - title={modalTitle} + title={currentItem.label} visible={isVisible} isUpToStatusBar noPadding @@ -261,30 +260,7 @@ export const CategoriesModal = ({ }> - {currentView === CategoriesModalView.CATEGORIES ? ( - - ) : null} - {currentView === CategoriesModalView.NATIVE_CATEGORIES && getNativeCategoriesSection()} - {currentView === CategoriesModalView.GENRES ? ( - - ) : null} + {shouldRenderBlocks ? renderBlocks() : renderItems()} ) diff --git a/src/features/search/types.ts b/src/features/search/types.ts index 16b49ea9f09..b3004d3171d 100644 --- a/src/features/search/types.ts +++ b/src/features/search/types.ts @@ -7,7 +7,6 @@ import { GenreTypeContentModel, GTL, NativeCategoryIdEnumv2, - SearchGroupNameEnumv2, SubcategoryIdEnumv2, } from 'api/gen' import { SearchOfferHits } from 'features/search/api/useSearchResults/useSearchResults' @@ -17,6 +16,8 @@ import { LocationMode } from 'libs/location/types' import { SuggestedPlace } from 'libs/place/types' import { Range } from 'libs/typesUtils/typeHelpers' import { Offer } from 'shared/offer/types' +import { CategoryKey } from 'features/search/helpers/categoriesHelpers/categories' + interface SelectedDate { option: DATE_FILTER_OPTIONS selectedDate: string @@ -41,9 +42,9 @@ export interface SearchState { endingDatetime?: string hitsPerPage: number | null locationFilter: LocationFilter - offerCategories: SearchGroupNameEnumv2[] + offerCategories: CategoryKey[] + offerNativeCategories: NativeCategoryIdEnumv2[] offerGenreTypes?: OfferGenreType[] - offerNativeCategories?: NativeCategoryIdEnumv2[] | BooksNativeCategoriesEnum[] offerSubcategories: SubcategoryIdEnumv2[] offerIsDuo: boolean offerIsFree?: boolean @@ -72,9 +73,7 @@ export type UserData = { } export type DescriptionContext = { - category: SearchGroupNameEnumv2 - nativeCategory: NativeCategoryIdEnumv2 | BooksNativeCategoriesEnum | null - genreType: string | null + categories: string[] } type VenueUserTitleRule = { venue_playlist_title: string } @@ -99,8 +98,8 @@ export interface SearchListProps { export type CreateHistoryItem = { query: string - nativeCategory?: NativeCategoryIdEnumv2 - category?: SearchGroupNameEnumv2 + nativeCategory?: CategoryKey + category?: CategoryKey } export type Highlighted = TItem & { diff --git a/src/features/subscription/helpers/mapSubscriptionThemeToIllustration.tsx b/src/features/subscription/helpers/mapSubscriptionThemeToIllustration.tsx index 5e34d7de6ee..a668f5056a6 100644 --- a/src/features/subscription/helpers/mapSubscriptionThemeToIllustration.tsx +++ b/src/features/subscription/helpers/mapSubscriptionThemeToIllustration.tsx @@ -1,4 +1,4 @@ -import { CATEGORY_CRITERIA } from 'features/search/enums' +import { CATEGORY_APPEARANCE } from 'features/search/enums' import { SubscriptionTheme } from 'features/subscription/types' import { AccessibleRectangleIcon } from 'ui/svg/icons/types' @@ -12,16 +12,16 @@ export const mapSubscriptionThemeToIllustration = ( ): IllustrationFeatures => { switch (thematic) { case SubscriptionTheme.CINEMA: - return CATEGORY_CRITERIA.CINEMA + return CATEGORY_APPEARANCE.CINEMA case SubscriptionTheme.LECTURE: - return CATEGORY_CRITERIA.LIVRES + return CATEGORY_APPEARANCE.LIVRES case SubscriptionTheme.MUSIQUE: - return CATEGORY_CRITERIA.CONCERTS_FESTIVALS + return CATEGORY_APPEARANCE.CONCERTS_FESTIVALS case SubscriptionTheme.SPECTACLES: - return CATEGORY_CRITERIA.SPECTACLES + return CATEGORY_APPEARANCE.SPECTACLES case SubscriptionTheme.VISITES: - return CATEGORY_CRITERIA.MUSEES_VISITES_CULTURELLES + return CATEGORY_APPEARANCE.MUSEES_VISITES_CULTURELLES case SubscriptionTheme.ACTIVITES: - return CATEGORY_CRITERIA.ARTS_LOISIRS_CREATIFS + return CATEGORY_APPEARANCE.ARTS_LOISIRS_CREATIFS } } diff --git a/src/libs/algolia/enums/facetsEnums.ts b/src/libs/algolia/enums/facetsEnums.ts index 7ada8e7b7f3..b75fb06e563 100644 --- a/src/libs/algolia/enums/facetsEnums.ts +++ b/src/libs/algolia/enums/facetsEnums.ts @@ -15,7 +15,8 @@ export enum FACETS_FILTERS_ENUM { OFFER_MOVIE_GENRES = 'offer.movieGenres', OFFER_MUSIC_TYPE = 'offer.musicType', OFFER_NATIVE_CATEGORY = 'offer.nativeCategoryId', - OFFER_SEARCH_GROUP_NAME = 'offer.searchGroupNamev2', + OFFER_SEARCH_GROUP_NAME = 'offer.searchGroups', + OFFER_SEARCH_GROUP_NAME_V2 = 'offer.searchGroupNamev2', OFFER_SHOW_TYPE = 'offer.showType', OFFER_SUB_CATEGORY = 'offer.subcategoryId', OFFER_TAGS = 'offer.tags', diff --git a/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/buildFacetFilters.ts b/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/buildFacetFilters.ts index 7d076413136..52d5bc68351 100644 --- a/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/buildFacetFilters.ts +++ b/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/buildFacetFilters.ts @@ -1,4 +1,5 @@ import { DisabilitiesProperties } from 'features/accessibility/types' +import { getCategory } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' import { FACETS_FILTERS_ENUM } from 'libs/algolia/enums/facetsEnums' import { buildAccessibiltyFiltersPredicate, @@ -63,7 +64,11 @@ export const buildFacetFilters = ({ if (isUserUnderage) facetFilters.push(...underageFilter) if (offerCategories.length > 0) { - const categoriesPredicate = buildOfferCategoriesPredicate(offerCategories) + const categoriesPredicate = buildOfferCategoriesPredicate( + offerCategories + .map((categoryKey) => getCategory(categoryKey)) + .filter((category) => !!category) + ) facetFilters.push(categoriesPredicate) } diff --git a/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/getCategoriesFacetFilters.ts b/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/getCategoriesFacetFilters.ts index 292f9ec06e5..f32d2007b85 100644 --- a/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/getCategoriesFacetFilters.ts +++ b/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/getCategoriesFacetFilters.ts @@ -1,5 +1,4 @@ import { SearchGroupNameEnumv2 } from 'api/gen' -import { CATEGORY_CRITERIA } from 'features/search/enums' // Mapping from contentful label to corresponding search group const CONTENTFUL_LABELS: Record = { @@ -21,7 +20,5 @@ const CONTENTFUL_LABELS: Record = { } export const getCategoriesFacetFilters = (categoryLabel: string): SearchGroupNameEnumv2 => { - const searchGroup = CONTENTFUL_LABELS[categoryLabel] - // @ts-expect-error: because of noUncheckedIndexedAccess - return CATEGORY_CRITERIA[searchGroup]?.facetFilter ?? SearchGroupNameEnumv2.NONE + return CONTENTFUL_LABELS[categoryLabel] ?? SearchGroupNameEnumv2.NONE } diff --git a/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/helpers/buildFacetFiltersHelpers/buildFacetFiltersHelpers.ts b/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/helpers/buildFacetFiltersHelpers/buildFacetFiltersHelpers.ts index a9cd1b46195..7438b3be204 100644 --- a/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/helpers/buildFacetFiltersHelpers/buildFacetFiltersHelpers.ts +++ b/src/libs/algolia/fetchAlgolia/buildAlgoliaParameters/helpers/buildFacetFiltersHelpers/buildFacetFiltersHelpers.ts @@ -1,19 +1,16 @@ -import { - GenreType, - GTL, - NativeCategoryIdEnumv2, - SearchGroupNameEnumv2, - SubcategoryIdEnumv2, -} from 'api/gen' +import { GenreType, GTL, NativeCategoryIdEnumv2, SubcategoryIdEnumv2 } from 'api/gen' import { DisabilitiesProperties } from 'features/accessibility/types' +import { BaseCategory } from 'features/search/helpers/categoriesHelpers/categories' import { BooksNativeCategoriesEnum, OfferGenreType } from 'features/search/types' import { FACETS_FILTERS_ENUM } from 'libs/algolia/enums/facetsEnums' import { FiltersArray, SearchQueryParameters } from 'libs/algolia/types' import { eventMonitoring } from 'libs/monitoring' import { GtlLevel } from 'shared/gtl/types' -export const buildOfferCategoriesPredicate = (searchGroups: SearchGroupNameEnumv2[]): string[] => - searchGroups.map((searchGroup) => `${FACETS_FILTERS_ENUM.OFFER_SEARCH_GROUP_NAME}:${searchGroup}`) +export const buildOfferCategoriesPredicate = (categories: BaseCategory[]): string[] => + categories + .filter((category) => !!category.searchFilter && !!category.searchValue) + .map((category) => `${category.searchFilter}:${category.searchValue}`) export const buildOfferSubcategoriesPredicate = (subcategoryIds: SubcategoryIdEnumv2[]): string[] => subcategoryIds.map( diff --git a/src/libs/algolia/fetchAlgolia/fetchMultipleOffers/helpers/adaptOffersPlaylistParameters.ts b/src/libs/algolia/fetchAlgolia/fetchMultipleOffers/helpers/adaptOffersPlaylistParameters.ts index 58862c860ee..028662eb056 100644 --- a/src/libs/algolia/fetchAlgolia/fetchMultipleOffers/helpers/adaptOffersPlaylistParameters.ts +++ b/src/libs/algolia/fetchAlgolia/fetchMultipleOffers/helpers/adaptOffersPlaylistParameters.ts @@ -1,7 +1,6 @@ import { SubcategoryIdEnumv2 } from 'api/gen' import { computeBeginningAndEndingDatetimes } from 'features/home/api/helpers/computeBeginningAndEndingDatetimes' import { OffersModuleParameters } from 'features/home/types' -import { sortCategories } from 'features/search/helpers/reducer.helpers' import { getCategoriesFacetFilters } from 'libs/algolia/fetchAlgolia/buildAlgoliaParameters/getCategoriesFacetFilters' import { buildOfferGenreTypesValues } from 'libs/algolia/fetchAlgolia/fetchMultipleOffers/helpers/buildOfferGenreTypesValues' import { SearchQueryParameters } from 'libs/algolia/types' @@ -25,7 +24,7 @@ export const adaptOffersPlaylistParameters = ( // We receive category labels from contentful. We first have to map to facetFilters used for search const offerCategories = (parameters.categories ?? []) .map(getCategoriesFacetFilters) - .sort(sortCategories) + .sort((a, b) => a.localeCompare(b)) const offerSubcategories = (parameters.subcategories ?? []) .map((subcategoryLabel) => subcategoryLabelMapping[subcategoryLabel]) diff --git a/src/libs/algolia/types.ts b/src/libs/algolia/types.ts index fb5f552ecdd..dca8ded296d 100644 --- a/src/libs/algolia/types.ts +++ b/src/libs/algolia/types.ts @@ -12,7 +12,6 @@ import { VenueResponse, } from 'api/gen' import { DATE_FILTER_OPTIONS } from 'features/search/enums' -import { BooksNativeCategoriesEnum } from 'features/search/types' import { Venue } from 'features/venue/types' import { FACETS_FILTERS_ENUM } from 'libs/algolia/enums/facetsEnums' import { BuildLocationParameterParams } from 'libs/algolia/fetchAlgolia/buildAlgoliaParameters/buildLocationParameter' @@ -20,6 +19,7 @@ import { AlgoliaHit as BaseAlgoliaHit } from 'libs/algolia/types' import { VenueTypeCode } from 'libs/parsers/venueType' import { Range } from 'libs/typesUtils/typeHelpers' import { GtlLevel } from 'shared/gtl/types' +import { CategoryKey } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' interface AlgoliaGeoloc { lat?: number | null @@ -120,13 +120,13 @@ export type SearchQueryParameters = { maxPrice?: string minBookingsThreshold?: number minPrice?: string - offerCategories: SearchGroupNameEnumv2[] + offerCategories: CategoryKey[] offerGenreTypes?: OfferGenreType[] offerGtlLabel?: string offerGtlLevel?: GtlLevel offerIsDuo: boolean offerIsFree?: boolean - offerNativeCategories?: NativeCategoryIdEnumv2[] | BooksNativeCategoriesEnum[] + offerNativeCategories?: CategoryKey[] offerSubcategories: SubcategoryIdEnumv2[] isDigital: boolean page?: number diff --git a/src/libs/analytics/utils.ts b/src/libs/analytics/utils.ts index 427b3b32be8..e189a73de25 100644 --- a/src/libs/analytics/utils.ts +++ b/src/libs/analytics/utils.ts @@ -42,7 +42,7 @@ export const isCloseToBottom = ({ // we just cast it to string. export const prepareLogEventParams = (params: Record) => Object.keys(params).reduce((acc: Record, key) => { - acc[key] = typeof params[key] === 'number' ? (params[key] as number).toString() : params[key] + acc[key] = typeof params[key] === 'number' ? params[key].toString() : params[key] return acc }, {}) @@ -83,7 +83,7 @@ export const buildAccessibilityFilterParam = (disabilities: DisabilitiesProperti const formattedDisability = Object.fromEntries( Object.entries(disabilities).map(([key, value]) => [ formatAccessibilityFilters[key], - value === undefined ? false : value, + value ?? false, ]) ) @@ -132,10 +132,6 @@ export const buildPerformSearchState = ( state.searchOfferIsFree = searchState.offerIsFree } - if (searchState.offerNativeCategories && searchState.offerNativeCategories.length > 0) { - state.searchNativeCategories = JSON.stringify(searchState.offerNativeCategories) - } - if (searchState.query !== '') { state.searchQuery = searchState.query } diff --git a/src/shared/location/LocationSearchFilters.tsx b/src/shared/location/LocationSearchFilters.tsx index 6026bb64f68..e64bc54ed91 100644 --- a/src/shared/location/LocationSearchFilters.tsx +++ b/src/shared/location/LocationSearchFilters.tsx @@ -2,13 +2,11 @@ import React from 'react' import styled from 'styled-components/native' import { v4 as uuidv4 } from 'uuid' +import { MAX_RADIUS } from 'features/search/helpers/reducer.helpers' import { useGetFullscreenModalSliderLength } from 'features/search/helpers/useGetFullscreenModalSliderLength' import { Slider } from 'ui/components/inputs/Slider' import { Spacer, TypoDS } from 'ui/theme' -const MIN_RADIUS = 0 -const MAX_RADIUS = 100 - interface LocationSearchFiltersProps { onValuesChange: (newValues: number[]) => void aroundRadius: number @@ -33,7 +31,7 @@ export const LocationSearchFilters = ({ & { - nativeCategory: NativeCategoryEnum + categoryKey: CategoryKey } type SubcategoryButtonListProps = { subcategoryButtonContent: SubcategoryButtonItem[] - onPress: (nativeCategory: NativeCategoryEnum) => void + onPress: (nativeCategory: CategoryKey) => void } export const SubcategoryButtonList: React.FC = ({ @@ -24,7 +24,7 @@ export const SubcategoryButtonList: React.FC = ({ }) => ( {subcategoryButtonContent.map((item) => ( - onPress(item.nativeCategory)} /> + onPress(item.categoryKey)} /> ))} ) diff --git a/src/ui/components/buttons/SubcategoryButton/SubcategoryButtonListWrapper.native.test.tsx b/src/ui/components/buttons/SubcategoryButton/SubcategoryButtonListWrapper.native.test.tsx index adaa4172e8a..2e4e2381620 100644 --- a/src/ui/components/buttons/SubcategoryButton/SubcategoryButtonListWrapper.native.test.tsx +++ b/src/ui/components/buttons/SubcategoryButton/SubcategoryButtonListWrapper.native.test.tsx @@ -21,7 +21,7 @@ describe('', () => { it('should display "Films à l’affiche" when offerCategory is "Cinema"', async () => { render( reactQueryProviderHOC( - + ) ) @@ -31,7 +31,7 @@ describe('', () => { it('should display "Romans et littérature" when offerCategory is "Livres"', async () => { render( reactQueryProviderHOC( - + ) ) diff --git a/src/ui/components/buttons/SubcategoryButton/SubcategoryButtonListWrapper.tsx b/src/ui/components/buttons/SubcategoryButton/SubcategoryButtonListWrapper.tsx index 5a80e92cc9b..8725031f7db 100644 --- a/src/ui/components/buttons/SubcategoryButton/SubcategoryButtonListWrapper.tsx +++ b/src/ui/components/buttons/SubcategoryButton/SubcategoryButtonListWrapper.tsx @@ -1,96 +1,61 @@ import { useRoute } from '@react-navigation/native' import React, { useMemo } from 'react' -import { ScrollViewProps, ViewStyle } from 'react-native' import { useTheme } from 'styled-components/native' -import { SearchGroupNameEnumv2 } from 'api/gen' import { defaultDisabilitiesProperties } from 'features/accessibility/context/AccessibilityFiltersWrapper' import { UseRouteType } from 'features/navigation/RootNavigator/types' -import { SearchStackRouteName } from 'features/navigation/SearchStackNavigator/types' +import { + SearchStackRouteName, + ThematicSearchCategories, +} from 'features/navigation/SearchStackNavigator/types' import { useSearch } from 'features/search/context/SearchWrapper' -import { CategoriesModalView, CATEGORY_CRITERIA } from 'features/search/enums' +import { CATEGORY_APPEARANCE } from 'features/search/enums' import { - handleCategoriesSearchPress, + CategoryKey, + getCategoryChildren, sortCategoriesPredicate, - useNativeCategories, } from 'features/search/helpers/categoriesHelpers/categoriesHelpers' import { useNavigateToSearch } from 'features/search/helpers/useNavigateToSearch/useNavigateToSearch' -import { CategoriesModalFormProps } from 'features/search/pages/modals/CategoriesModal/CategoriesModal' -import { NativeCategoryEnum, SearchState } from 'features/search/types' -import { useSubcategories } from 'libs/subcategories/useSubcategories' import { SubcategoryButtonItem, SubcategoryButtonList, } from 'ui/components/buttons/SubcategoryButton/SubcategoryButtonList' -type StyledScrollViewProps = ScrollViewProps & { - contentContainerStyle?: ViewStyle -} - type Props = { - offerCategory: SearchGroupNameEnumv2 - scrollViewProps?: StyledScrollViewProps + category: ThematicSearchCategories } -export const SubcategoryButtonListWrapper: React.FC = ({ offerCategory }) => { - const { data: subcategories } = useSubcategories() - +export const SubcategoryButtonListWrapper: React.FC = ({ category }) => { const { colors } = useTheme() - const nativeCategories = useNativeCategories(offerCategory) - const offerCategoryTheme = useMemo( - () => ({ - backgroundColor: CATEGORY_CRITERIA[offerCategory]?.fillColor, - borderColor: CATEGORY_CRITERIA[offerCategory]?.borderColor, - }), - [offerCategory] - ) + const subcategories = getCategoryChildren(category) const { navigateToSearch: navigateToSearchResults } = useNavigateToSearch('SearchResults') const { params } = useRoute>() const { dispatch, searchState } = useSearch() const subcategoryButtonContent = useMemo( () => - nativeCategories + subcategories + .toSorted((a, b) => sortCategoriesPredicate(a, b)) .map( - (nativeCategory): SubcategoryButtonItem => ({ - label: nativeCategory[1].label, - backgroundColor: offerCategoryTheme.backgroundColor || colors.white, - borderColor: offerCategoryTheme.borderColor || colors.black, - nativeCategory: nativeCategory[0] as NativeCategoryEnum, - position: nativeCategory[1].position, + (subcategory): SubcategoryButtonItem => ({ + label: subcategory.label, + backgroundColor: CATEGORY_APPEARANCE[category]?.fillColor ?? colors.white, + borderColor: CATEGORY_APPEARANCE[category]?.borderColor ?? colors.black, + categoryKey: subcategory.key, + position: subcategory.position, }) - ) - .sort((a, b) => sortCategoriesPredicate(a, b)), - [ - colors.black, - colors.white, - nativeCategories, - offerCategoryTheme.backgroundColor, - offerCategoryTheme.borderColor, - ] + ), + [colors.black, colors.white, subcategories] ) if (!subcategories) return null - const handleSubcategoryButtonPress = (nativeCategory: NativeCategoryEnum) => { - const offerCategories = params?.offerCategories as SearchGroupNameEnumv2[] - const form: CategoriesModalFormProps = { - category: offerCategories?.[0] as SearchGroupNameEnumv2, - currentView: CategoriesModalView.GENRES, - genreType: null, - nativeCategory, - } - const searchPayload = handleCategoriesSearchPress(form, subcategories) - - const additionalSearchState: SearchState = { - ...searchState, - ...searchPayload?.payload, - offerCategories, - isFullyDigitalOffersCategory: searchPayload?.isFullyDigitalOffersCategory || undefined, - } + const handleSubcategoryButtonPress = (subcategory: CategoryKey) => { + const offerCategories = [...(params?.offerCategories ?? []), subcategory] + const newSearchState = { ...searchState, offerCategories } - dispatch({ type: 'SET_STATE', payload: additionalSearchState }) - navigateToSearchResults(additionalSearchState, defaultDisabilitiesProperties) + dispatch({ type: 'SET_STATE', payload: newSearchState }) + navigateToSearchResults(newSearchState, defaultDisabilitiesProperties) } return (