From 83dca873f8741b56f9111f2affd0177a4e042066 Mon Sep 17 00:00:00 2001 From: Bogdan Fazakas Date: Thu, 21 Jan 2021 11:42:05 +0200 Subject: [PATCH] add sort filter features to search (#322) * sort and filter working added ui element for filter * added UI for filter and sort * made sort and filter work with the new UI * filter and sort inline style * updated styling and linters * added sort direction * apply sort and filter when starting a new search and sort order style update * fixed sort by price * change sort order selector change direction when click on a sort option instead of clicking on the arrow that shows actual sort direction * added default sort to owner and tag search * refactor getSearchQuery method * refactor sort methods * fixed lint errors * updated styling for sort and filter components * fixed lint * apply dynamic filter when sorting by liquidity * interaction & alignment styling * style tweaks, legacy search layout styles removal * refactor sort component and fixed browsing all datasets also aded default sort params when browsing all data sets * fixed lint issues Co-authored-by: Matthias Kretschmann --- src/components/atoms/Publisher/index.tsx | 2 +- src/components/atoms/Tags.tsx | 6 +- src/components/molecules/SearchBar.tsx | 12 +- src/components/pages/Home.tsx | 2 +- .../templates/Search/filterPrice.module.css | 33 ++++++ .../templates/Search/filterPrice.tsx | 57 ++++++++++ .../templates/Search/index.module.css | 30 +++-- src/components/templates/Search/index.tsx | 36 +++++- .../templates/Search/sort.module.css | 48 ++++++++ src/components/templates/Search/sort.tsx | 94 +++++++++++++++ src/components/templates/Search/utils.ts | 107 +++++++++++++++++- 11 files changed, 394 insertions(+), 33 deletions(-) create mode 100644 src/components/templates/Search/filterPrice.module.css create mode 100644 src/components/templates/Search/filterPrice.tsx create mode 100644 src/components/templates/Search/sort.module.css create mode 100644 src/components/templates/Search/sort.tsx diff --git a/src/components/atoms/Publisher/index.tsx b/src/components/atoms/Publisher/index.tsx index 8904c2156..a8c89a0f8 100644 --- a/src/components/atoms/Publisher/index.tsx +++ b/src/components/atoms/Publisher/index.tsx @@ -63,7 +63,7 @@ export default function Publisher({ ) : ( <> {name} diff --git a/src/components/atoms/Tags.tsx b/src/components/atoms/Tags.tsx index 2d5f4ad90..c6950ec87 100644 --- a/src/components/atoms/Tags.tsx +++ b/src/components/atoms/Tags.tsx @@ -15,7 +15,11 @@ const Tag = ({ tag, noLinks }: { tag: string; noLinks?: boolean }) => { return noLinks ? ( {tag} ) : ( - + {tag} ) diff --git a/src/components/molecules/SearchBar.tsx b/src/components/molecules/SearchBar.tsx index 1dadccdb7..c9cb13b1e 100644 --- a/src/components/molecules/SearchBar.tsx +++ b/src/components/molecules/SearchBar.tsx @@ -4,6 +4,7 @@ import styles from './SearchBar.module.css' import Button from '../atoms/Button' import Input from '../atoms/Input' import InputGroup from '../atoms/Input/InputGroup' +import { addExistingParamsToUrl } from '../templates/Search/utils' export default function SearchBar({ placeholder, @@ -23,10 +24,11 @@ export default function SearchBar({ setValue(e.target.value) } - function startSearch(e: FormEvent) { + async function startSearch(e: FormEvent) { e.preventDefault() if (value === '') return - navigate(`/search?text=${value}`) + const url = await addExistingParamsToUrl(location, 'text') + navigate(`${url}&text=${value}`) } return ( @@ -41,7 +43,11 @@ export default function SearchBar({ required size={size} /> - diff --git a/src/components/pages/Home.tsx b/src/components/pages/Home.tsx index 66c1b6cff..2acc3c7fd 100644 --- a/src/components/pages/Home.tsx +++ b/src/components/pages/Home.tsx @@ -112,7 +112,7 @@ export default function HomePage(): ReactElement { title="New Data Sets" query={queryLatest} action={ - } diff --git a/src/components/templates/Search/filterPrice.module.css b/src/components/templates/Search/filterPrice.module.css new file mode 100644 index 000000000..27fb4735f --- /dev/null +++ b/src/components/templates/Search/filterPrice.module.css @@ -0,0 +1,33 @@ +/* .filterList { + display: inline-flex; + float: left; +} */ + +.filter, +button.filter, +.filter:hover, +.filter:active, +.filter:focus { + border: 1px solid var(--border-color); + text-transform: uppercase; + border-radius: var(--border-radius); + margin-right: calc(var(--spacer) / 6); + color: var(--color-secondary); + background: var(--background-body); + + /* the only thing not possible to overwrite button style="text" with more specifity of selectors, so sledgehammer */ + padding: calc(var(--spacer) / 5) !important; +} + +.filter:hover, +.filter:focus { + color: var(--font-color-text); + background: inherit; + transform: none; +} + +.filter.selected { + color: var(--background-body); + background: var(--font-color-text); + border-color: var(--background-body); +} diff --git a/src/components/templates/Search/filterPrice.tsx b/src/components/templates/Search/filterPrice.tsx new file mode 100644 index 000000000..c2b2da8fa --- /dev/null +++ b/src/components/templates/Search/filterPrice.tsx @@ -0,0 +1,57 @@ +import React, { ReactElement } from 'react' +import { useNavigate } from '@reach/router' +import styles from './filterPrice.module.css' +import classNames from 'classnames/bind' +import { addExistingParamsToUrl, FilterByPriceOptions } from './utils' +import Button from '../../atoms/Button' + +const cx = classNames.bind(styles) + +const filterItems = [ + { display: 'all', value: undefined }, + { display: 'fixed price', value: FilterByPriceOptions.Fixed }, + { display: 'dynamic price', value: FilterByPriceOptions.Dynamic } +] + +export default function FilterPrice({ + priceType, + setPriceType +}: { + priceType: string + setPriceType: React.Dispatch> +}): ReactElement { + const navigate = useNavigate() + + async function applyFilter(filterBy: string) { + let urlLocation = await addExistingParamsToUrl(location, 'priceType') + if (filterBy) { + urlLocation = `${urlLocation}&priceType=${filterBy}` + } + setPriceType(filterBy) + navigate(urlLocation) + } + + return ( +
+ {filterItems.map((e, index) => { + const filter = cx({ + [styles.selected]: e.value === priceType, + [styles.filter]: true + }) + return ( + + ) + })} +
+ ) +} diff --git a/src/components/templates/Search/index.module.css b/src/components/templates/Search/index.module.css index 1d48cd0ed..d30c587e0 100644 --- a/src/components/templates/Search/index.module.css +++ b/src/components/templates/Search/index.module.css @@ -1,21 +1,19 @@ -.grid { - display: grid; +.row { + display: inline-flex; + flex-direction: column; + align-items: stretch; + justify-content: space-between; + width: 100%; + white-space: nowrap; + margin-bottom: calc(var(--spacer) / 2); } -@media (min-width: 55rem) { - .grid { - grid-column-gap: calc(var(--spacer) * 3); - /* grid-template-columns: minmax(0, 3fr) 1fr; */ - grid-template-areas: - 'search' - 'results'; - } +.row > div { + margin-bottom: calc(var(--spacer) / 2); +} - .search { - grid-area: search; - } - - .results { - grid-area: results; +@media (min-width: 40rem) { + .row { + flex-direction: row; } } diff --git a/src/components/templates/Search/index.tsx b/src/components/templates/Search/index.tsx index bf14461ef..874bfee99 100644 --- a/src/components/templates/Search/index.tsx +++ b/src/components/templates/Search/index.tsx @@ -4,7 +4,9 @@ import SearchBar from '../../molecules/SearchBar' import AssetQueryList from '../../organisms/AssetQueryList' import styles from './index.module.css' import queryString from 'query-string' -import { getResults } from './utils' +import PriceFilter from './filterPrice' +import Sort from './sort' +import { getResults, addExistingParamsToUrl } from './utils' import Loader from '../../atoms/Loader' import { useOcean } from '@oceanprotocol/react' @@ -17,9 +19,14 @@ export default function SearchPage({ }): ReactElement { const { config } = useOcean() const parsed = queryString.parse(location.search) - const { text, owner, tags, page } = parsed + const { text, owner, tags, page, sort, sortOrder, priceType } = parsed const [queryResult, setQueryResult] = useState() const [loading, setLoading] = useState() + const [price, setPriceType] = useState(priceType as string) + const [sortType, setSortType] = useState(sort as string) + const [sortDirection, setSortDirection] = useState( + sortOrder as string + ) useEffect(() => { if (!config?.metadataCacheUri) return @@ -33,18 +40,37 @@ export default function SearchPage({ setLoading(false) } initSearch() - }, [text, owner, tags, page, config.metadataCacheUri]) + }, [ + text, + owner, + tags, + page, + sort, + priceType, + sortOrder, + config.metadataCacheUri + ]) return ( -
+ <>
{(text || owner) && ( )} +
+ + +
{loading ? : }
-
+ ) } diff --git a/src/components/templates/Search/sort.module.css b/src/components/templates/Search/sort.module.css new file mode 100644 index 000000000..c2c052167 --- /dev/null +++ b/src/components/templates/Search/sort.module.css @@ -0,0 +1,48 @@ +.sortList { + display: flex; + align-items: center; + border-radius: var(--border-radius); + border: 1px solid var(--border-color); + background: var(--background-body); +} + +.sortLabel { + composes: label from '../../atoms/Input/Label.module.css'; + margin-bottom: 0; + margin-left: calc(var(--spacer) / 2); + margin-right: calc(var(--spacer) / 1.5); + text-transform: uppercase; + color: var(--color-secondary); +} + +.sorted { + display: flex; + padding: calc(var(--spacer) / 6) calc(var(--spacer) / 2); + margin-right: calc(var(--spacer) / 4); + color: var(--color-secondary); + text-transform: capitalize; + border-radius: 0; + font-weight: var(--font-weight-base); + background: var(--background-body); + box-shadow: none; +} + +.sorted:hover, +.sorted:focus, +.sorted.selected { + color: var(--font-color-text); + background: inherit; + transform: none; + box-shadow: none; +} + +.direction { + display: flex; + background: transparent; + border: none; + color: inherit; + font-size: 0.75em; + outline: none; + margin-left: calc(var(--spacer) / 8); + margin-top: calc(var(--spacer) / 14); +} diff --git a/src/components/templates/Search/sort.tsx b/src/components/templates/Search/sort.tsx new file mode 100644 index 000000000..5ad939307 --- /dev/null +++ b/src/components/templates/Search/sort.tsx @@ -0,0 +1,94 @@ +import React, { ReactElement } from 'react' +import { useNavigate } from '@reach/router' +import { + addExistingParamsToUrl, + SortTermOptions, + SortValueOptions, + FilterByPriceOptions +} from './utils' +import Button from '../../atoms/Button' +import styles from './sort.module.css' +import classNames from 'classnames/bind' + +const cx = classNames.bind(styles) + +const sortItems = [ + { display: 'Published', value: SortTermOptions.Created }, + { display: 'Liquidity', value: SortTermOptions.Liquidity }, + { display: 'Price', value: SortTermOptions.Price } +] + +export default function Sort({ + sortType, + setSortType, + sortDirection, + setSortDirection, + setPriceType +}: { + sortType: string + setSortType: React.Dispatch> + sortDirection: string + setSortDirection: React.Dispatch> + setPriceType: React.Dispatch> +}): ReactElement { + const navigate = useNavigate() + const directionArrow = String.fromCharCode( + sortDirection === SortValueOptions.Ascending ? 9650 : 9660 + ) + async function sortResults(sortBy?: string, direction?: string) { + let urlLocation: string + if (sortBy) { + urlLocation = await addExistingParamsToUrl(location, 'sort', 'priceType') + urlLocation = `${urlLocation}&sort=${sortBy}` + if (sortBy === SortTermOptions.Liquidity) { + urlLocation = `${urlLocation}&priceType=${FilterByPriceOptions.Dynamic}` + setPriceType(FilterByPriceOptions.Dynamic) + } else { + setPriceType(undefined) + } + setSortType(sortBy) + } else if (direction) { + urlLocation = await addExistingParamsToUrl(location, 'sortOrder') + urlLocation = `${urlLocation}&sortOrder=${direction}` + setSortDirection(direction) + } + navigate(urlLocation) + } + function handleSortButtonClick(value: string) { + if (value === sortType) { + if (sortDirection === SortValueOptions.Descending) { + sortResults(null, SortValueOptions.Ascending) + } else { + sortResults(null, SortValueOptions.Descending) + } + } else { + sortResults(value, null) + } + } + return ( +
+ + {sortItems.map((e, index) => { + const sorted = cx({ + [styles.selected]: e.value === sortType, + [styles.sorted]: true + }) + return ( + + ) + })} +
+ ) +} diff --git a/src/components/templates/Search/utils.ts b/src/components/templates/Search/utils.ts index cdb47f963..914000913 100644 --- a/src/components/templates/Search/utils.ts +++ b/src/components/templates/Search/utils.ts @@ -3,6 +3,52 @@ import { QueryResult } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache' import { MetadataCache, Logger } from '@oceanprotocol/lib' +import queryString from 'query-string' + +export const SortTermOptions = { + Liquidity: 'liquidity', + Price: 'price', + Created: 'created' +} as const +type SortTermOptions = typeof SortTermOptions[keyof typeof SortTermOptions] + +export const SortElasticTerm = { + Liquidity: 'price.ocean', + Price: 'price.value', + Created: 'created' +} as const +type SortElasticTerm = typeof SortElasticTerm[keyof typeof SortElasticTerm] + +export const SortValueOptions = { + Ascending: 'asc', + Descending: 'desc' +} as const +type SortValueOptions = typeof SortValueOptions[keyof typeof SortValueOptions] + +export const FilterByPriceOptions = { + Fixed: 'exchange', + Dynamic: 'pool' +} as const +type FilterByPriceOptions = typeof FilterByPriceOptions[keyof typeof FilterByPriceOptions] + +function addPriceFilterToQuerry(sortTerm: string, priceFilter: string): string { + sortTerm = priceFilter + ? sortTerm === '' + ? `price.type:${priceFilter}` + : `${sortTerm} AND price.type:${priceFilter}` + : sortTerm + return sortTerm +} + +function getSortType(sortParam: string): string { + const sortTerm = + sortParam === SortTermOptions.Liquidity + ? SortElasticTerm.Liquidity + : sortParam === SortTermOptions.Price + ? SortElasticTerm.Price + : SortTermOptions.Created + return sortTerm +} export function getSearchQuery( text?: string, @@ -10,9 +56,14 @@ export function getSearchQuery( tags?: string, categories?: string, page?: string, - offset?: string + offset?: string, + sort?: string, + sortOrder?: string, + priceType?: string ): SearchQuery { - const searchTerm = owner + const sortTerm = getSortType(sort) + const sortValue = sortOrder === SortValueOptions.Ascending ? 1 : -1 + let searchTerm = owner ? `(publicKey.owner:${owner})` : tags ? // eslint-disable-next-line no-useless-escape @@ -21,6 +72,7 @@ export function getSearchQuery( ? // eslint-disable-next-line no-useless-escape `(service.attributes.additionalInformation.categories:\"${categories}\")` : text || '' + searchTerm = addPriceFilterToQuerry(searchTerm, priceType) return { page: Number(page) || 1, @@ -35,7 +87,7 @@ export function getSearchQuery( // ...(categories && { categories: [categories] }) }, sort: { - created: -1 + [sortTerm]: sortValue } // Something in ocean.js is weird when using 'tags: [tag]' @@ -57,11 +109,23 @@ export async function getResults( categories?: string page?: string offset?: string + sort?: string + sortOrder?: string + priceType?: string }, metadataCacheUri: string ): Promise { - const { text, owner, tags, page, offset, categories } = params - + const { + text, + owner, + tags, + page, + offset, + categories, + sort, + sortOrder, + priceType + } = params const metadataCache = new MetadataCache(metadataCacheUri, Logger) const searchQuery = getSearchQuery( text, @@ -69,9 +133,40 @@ export async function getResults( tags, categories, page, - offset + offset, + sort, + sortOrder, + priceType ) const queryResult = await metadataCache.queryMetadata(searchQuery) return queryResult } + +export async function addExistingParamsToUrl( + location: Location, + excludedParam: string, + secondExcludedParam?: string +): Promise { + const parsed = queryString.parse(location.search) + let urlLocation = '/search?' + if (Object.keys(parsed).length > 0) { + for (const querryParam in parsed) { + if ( + querryParam !== excludedParam && + querryParam !== secondExcludedParam + ) { + if (querryParam === 'page' && excludedParam === 'text') { + Logger.log('remove page when starting a new search') + } else { + const value = parsed[querryParam] + urlLocation = `${urlLocation}${querryParam}=${value}&` + } + } + } + } else { + urlLocation = `${urlLocation}sort=${SortTermOptions.Created}&sortOrder=${SortValueOptions.Descending}&` + } + urlLocation = urlLocation.slice(0, -1) + return urlLocation +}