diff --git a/packages/frontend/src/api/hooks/index.ts b/packages/frontend/src/api/hooks/index.ts index e5966993..80070d13 100644 --- a/packages/frontend/src/api/hooks/index.ts +++ b/packages/frontend/src/api/hooks/index.ts @@ -8,6 +8,7 @@ export * from "./useCookieChoice"; export * from "./useIsMobile"; export * from "./useFeatureFlags"; export * from "./useFilters"; +export * from "./useFilterKeywordSearch"; export * from "./useGATracker"; export * from "./useGlobalSearch"; export * from "./useGlobalHooks"; diff --git a/packages/frontend/src/api/hooks/useFilterKeywordSearch.ts b/packages/frontend/src/api/hooks/useFilterKeywordSearch.ts new file mode 100644 index 00000000..c30054d0 --- /dev/null +++ b/packages/frontend/src/api/hooks/useFilterKeywordSearch.ts @@ -0,0 +1,69 @@ +import { useMemo, useState } from "react"; +import { useGetFilterKeywordsQuery } from "src/api/services"; +import { TFilterKeyword, ESupportedFilters } from "src/api/types"; + +interface IFilterKeywordSearch { + searchState: string; + keywordResults: TFilterKeyword[]; + isFetchingKeywordResults: boolean; + setSearchState: (s: string) => void; +} + +/** + * useFilterKeywordSearch. + * Pretty similar to useKeywordSearch, but instead of consuming the keywords endpoint, + * it queries the superfeed/filter_keywords endpoint. + * The main difference is that the results are grouped in categories (concept tags, coins, projects, etc). + * This hook is used in the UserFilter page. + * It is used to search for keywords, display them and add them to the view. + * + * @returns - The search state and results. + */ +export const useFilterKeywordSearch: () => IFilterKeywordSearch = () => { + const [searchState, setSearchState] = useState(""); + + const { data: keywordsData, isFetching: isFetchingKeywordResults } = + useGetFilterKeywordsQuery( + { + filter_text: searchState, + }, + { + skip: searchState === "", + } + ); + + const keywordResults = useMemo(() => { + if (!keywordsData) return []; + return [ + ...keywordsData.conceptTags.map((keyword) => ({ + id: keyword.id, + name: keyword.name, + slug: keyword.tag.slug, + type: ESupportedFilters.ConceptTags, + })), + ...keywordsData.chains.map((keyword) => ({ + id: keyword.id, + name: keyword.name, + slug: keyword.tag.slug, + type: ESupportedFilters.Chains, + })), + ...keywordsData.coins.map((keyword) => ({ + id: keyword.id, + name: keyword.name, + slug: keyword.tag.slug, + type: ESupportedFilters.Coins, + })), + ].filter( + (item, index, self) => + self.findIndex((innerItem) => innerItem.slug === item.slug) === + index + ); + }, [keywordsData]); + + return { + searchState, + setSearchState, + keywordResults, + isFetchingKeywordResults, + }; +}; diff --git a/packages/frontend/src/api/services/superfeed/superfeedEndpoints.ts b/packages/frontend/src/api/services/superfeed/superfeedEndpoints.ts index 9423979d..3be55488 100644 --- a/packages/frontend/src/api/services/superfeed/superfeedEndpoints.ts +++ b/packages/frontend/src/api/services/superfeed/superfeedEndpoints.ts @@ -9,6 +9,9 @@ import { TGetSuperfeedFilterDataRawResponse, TGetSuperfeedFilterDataResponse, TGetSuperfeedFilterDataRequest, + TGetSuperfeedFilterKeywordsResponse, + TGetSuperfeedFilterKeywordsRequest, + TGetSuperfeedFilterKeywordsRawResponse, } from "./types"; const { SUPERFEED } = CONFIG.API.DEFAULT.ROUTES; @@ -89,8 +92,31 @@ export const superfeedApi = alphadayApi.injectEndpoints({ ), }), }), + getFilterKeywords: builder.query< + TGetSuperfeedFilterKeywordsResponse, + TGetSuperfeedFilterKeywordsRequest + >({ + query: (req) => { + const { ...reqParams } = req; + const params: string = queryString.stringify(reqParams); + const path = `${SUPERFEED.BASE}${SUPERFEED.FILTER_KEYWORDS}?${params}`; + Logger.debug("getSuperfeedFilterKeywords: querying", path); + return path; + }, + transformResponse: ( + r: TGetSuperfeedFilterKeywordsRawResponse + ): TGetSuperfeedFilterKeywordsResponse => ({ + coins: r.coin_keywords, + conceptTags: r.concept_keywords, + chains: r.chain_keywords, + }), + }), }), overrideExisting: false, }); -export const { useGetSuperfeedListQuery, useGetFilterDataQuery } = superfeedApi; +export const { + useGetSuperfeedListQuery, + useGetFilterDataQuery, + useGetFilterKeywordsQuery, +} = superfeedApi; diff --git a/packages/frontend/src/api/services/superfeed/types.ts b/packages/frontend/src/api/services/superfeed/types.ts index 1ba3aa87..98ab19c9 100644 --- a/packages/frontend/src/api/services/superfeed/types.ts +++ b/packages/frontend/src/api/services/superfeed/types.ts @@ -44,6 +44,16 @@ export type TTaggedFilterDatum = TBaseFilterItem & { tags: TBaseFilterTag[]; }; +export type TRemoteFilterKeyword = { + id: number; + name: string; + tag: { + id: number; + name: string; + slug: string; + }; +}; + /** * Query types */ @@ -75,3 +85,17 @@ export type TGetSuperfeedFilterDataResponse = { coins: TTaggedFilterDatum[]; chains: TTaggedFilterDatum[]; }; + +export type TGetSuperfeedFilterKeywordsRequest = { + filter_text: string; +}; +export type TGetSuperfeedFilterKeywordsRawResponse = { + concept_keywords: TRemoteFilterKeyword[]; + coin_keywords: TRemoteFilterKeyword[]; + chain_keywords: TRemoteFilterKeyword[]; +}; +export type TGetSuperfeedFilterKeywordsResponse = { + conceptTags: TRemoteFilterKeyword[]; + coins: TRemoteFilterKeyword[]; + chains: TRemoteFilterKeyword[]; +}; diff --git a/packages/frontend/src/api/types/superfeed.ts b/packages/frontend/src/api/types/superfeed.ts index 859a83fa..79c0d911 100644 --- a/packages/frontend/src/api/types/superfeed.ts +++ b/packages/frontend/src/api/types/superfeed.ts @@ -42,6 +42,13 @@ export enum ETimeRange { Last6Months = "last-6-months", } +export type TFilterKeyword = { + id: number; + name: string; + slug: string; + type: ESupportedFilters; +}; + export type TFeedMarketData = { coin: { name: string; diff --git a/packages/frontend/src/config/backend.ts b/packages/frontend/src/config/backend.ts index 1a947d06..862b6b37 100644 --- a/packages/frontend/src/config/backend.ts +++ b/packages/frontend/src/config/backend.ts @@ -123,6 +123,7 @@ const API_V0 = { BASE: "superfeed", DEFAULT: "/", FILTER_DATA: "/filter_data/", + FILTER_KEYWORDS: "/filter_keywords/", }, TVL: { BASE: "tvl", diff --git a/packages/frontend/src/mobile-components/FilterSearchBar.tsx b/packages/frontend/src/mobile-components/FilterSearchBar.tsx index 16141eac..d117c88c 100644 --- a/packages/frontend/src/mobile-components/FilterSearchBar.tsx +++ b/packages/frontend/src/mobile-components/FilterSearchBar.tsx @@ -1,23 +1,22 @@ -import { FC } from "react"; import { SearchBar } from "@alphaday/ui-kit"; import { TBaseFilterItem } from "src/api/services"; import { Logger } from "src/api/utils/logging"; type TOption = TBaseFilterItem; -interface FilterSearchBarProps { +interface FilterSearchBarProps { tags?: string; - tagsList: TOption[]; + tagsList: T[]; setSearchState: (value: string) => void; - onChange: (value: readonly TOption[]) => void; + onChange: (value: readonly T[]) => void; } -const FilterSearchBar: FC = ({ +const FilterSearchBar = ({ onChange, tags, setSearchState, tagsList, -}) => { +}: FilterSearchBarProps) => { const searchValues = tags ?.split(",") .map((tag) => { @@ -31,7 +30,7 @@ const FilterSearchBar: FC = ({ data-testid="header-search-container" > - + showBackdrop onChange={(o) => { Logger.debug("onChange called"); diff --git a/packages/frontend/src/mobile-components/user-filters-modal/UserFiltersModal.tsx b/packages/frontend/src/mobile-components/user-filters-modal/UserFiltersModal.tsx index a5a1c488..168dab78 100644 --- a/packages/frontend/src/mobile-components/user-filters-modal/UserFiltersModal.tsx +++ b/packages/frontend/src/mobile-components/user-filters-modal/UserFiltersModal.tsx @@ -5,9 +5,9 @@ import { Toggle, themeColors, } from "@alphaday/ui-kit"; -import { ESortFeedBy, ESupportedFilters } from "src/api/types"; +import { ESortFeedBy, ESupportedFilters, TFilterKeyword } from "src/api/types"; import { ReactComponent as ChevronSVG } from "src/assets/icons/chevron-down2.svg"; -// import FilterSearchBar from "../FilterSearchBar"; +import FilterSearchBar from "../FilterSearchBar"; import { TFilterOptions } from "./filterOptions"; import { OptionsDisclosure, OptionButton } from "./OptionsDisclosure"; @@ -17,6 +17,8 @@ interface IUserFiltersModalProps { filterOptions: TFilterOptions; isLoading: boolean; onSelectFilter: (slug: string, type: ESupportedFilters) => void; + filterKeywords: TFilterKeyword[]; + onSearchInputChange: (value: string) => void; } const UserFiltersModal: FC = ({ @@ -25,6 +27,8 @@ const UserFiltersModal: FC = ({ filterOptions, isLoading, onSelectFilter, + filterKeywords, + onSearchInputChange, }) => { const [isOpen, setIsOpen] = useState(true); @@ -70,9 +74,20 @@ const UserFiltersModal: FC = ({ Craft your ideal superfeed by customizing the filters below.

- {/* TODO: implement filter search when properly spec-ed */}
- {/* */} + + setSearchState={onSearchInputChange} + tagsList={filterKeywords.map((kw) => ({ + ...kw, + label: kw.name, + value: kw.slug, + }))} + onChange={(values) => + values.forEach((kw) => + onSelectFilter(kw.slug, kw.type) + ) + } + />
diff --git a/packages/frontend/src/mobile-containers/UserFiltersContainer.tsx b/packages/frontend/src/mobile-containers/UserFiltersContainer.tsx index 9e90cba1..90f41f64 100644 --- a/packages/frontend/src/mobile-containers/UserFiltersContainer.tsx +++ b/packages/frontend/src/mobile-containers/UserFiltersContainer.tsx @@ -1,5 +1,5 @@ import { FC } from "react"; -import { useFilters } from "src/api/hooks"; +import { useFilters, useFilterKeywordSearch } from "src/api/hooks"; import { useGetFilterDataQuery, TFilterDatum, @@ -101,6 +101,8 @@ const UserFiltersContainer: FC<{ const { toggleFilter } = useFilters(); + const { setSearchState, keywordResults } = useFilterKeywordSearch(); + const filterOptions: TFilterOptions = { localFilterOptions: updateLocalFilterOptionsState(selectedLocalFilters), syncedFilterOptions: { @@ -146,6 +148,8 @@ const UserFiltersContainer: FC<{ filterOptions={filterOptions} isLoading={isLoading} onSelectFilter={handleSelectFilter} + filterKeywords={keywordResults} + onSearchInputChange={setSearchState} /> ); };