mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
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 <m@kretschmann.io>
This commit is contained in:
parent
0cad79ae97
commit
83dca873f8
@ -63,7 +63,7 @@ export default function Publisher({
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
to={`/search/?owner=${account}`}
|
||||
to={`/search/?owner=${account}&sort=created&sortOrder=desc`}
|
||||
title="Show all data sets created by this account."
|
||||
>
|
||||
{name}
|
||||
|
@ -15,7 +15,11 @@ const Tag = ({ tag, noLinks }: { tag: string; noLinks?: boolean }) => {
|
||||
return noLinks ? (
|
||||
<span className={styles.tag}>{tag}</span>
|
||||
) : (
|
||||
<Link to={`/search?tags=${tag}`} className={styles.tag} title={tag}>
|
||||
<Link
|
||||
to={`/search?tags=${tag}&sort=created&sortOrder=desc`}
|
||||
className={styles.tag}
|
||||
title={tag}
|
||||
>
|
||||
{tag}
|
||||
</Link>
|
||||
)
|
||||
|
@ -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<HTMLButtonElement>) {
|
||||
async function startSearch(e: FormEvent<HTMLButtonElement>) {
|
||||
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}
|
||||
/>
|
||||
<Button onClick={(e: FormEvent<HTMLButtonElement>) => startSearch(e)}>
|
||||
<Button
|
||||
onClick={async (e: FormEvent<HTMLButtonElement>) =>
|
||||
await startSearch(e)
|
||||
}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</InputGroup>
|
||||
|
@ -112,7 +112,7 @@ export default function HomePage(): ReactElement {
|
||||
title="New Data Sets"
|
||||
query={queryLatest}
|
||||
action={
|
||||
<Button style="text" to="/search">
|
||||
<Button style="text" to="/search?sort=created&sortOrder=desc">
|
||||
All data sets →
|
||||
</Button>
|
||||
}
|
||||
|
33
src/components/templates/Search/filterPrice.module.css
Normal file
33
src/components/templates/Search/filterPrice.module.css
Normal file
@ -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);
|
||||
}
|
57
src/components/templates/Search/filterPrice.tsx
Normal file
57
src/components/templates/Search/filterPrice.tsx
Normal file
@ -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<React.SetStateAction<string>>
|
||||
}): 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 (
|
||||
<div>
|
||||
{filterItems.map((e, index) => {
|
||||
const filter = cx({
|
||||
[styles.selected]: e.value === priceType,
|
||||
[styles.filter]: true
|
||||
})
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
style="text"
|
||||
key={index}
|
||||
className={filter}
|
||||
onClick={async () => {
|
||||
await applyFilter(e.value)
|
||||
}}
|
||||
>
|
||||
{e.display}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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<QueryResult>()
|
||||
const [loading, setLoading] = useState<boolean>()
|
||||
const [price, setPriceType] = useState<string>(priceType as string)
|
||||
const [sortType, setSortType] = useState<string>(sort as string)
|
||||
const [sortDirection, setSortDirection] = useState<string>(
|
||||
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 (
|
||||
<section className={styles.grid}>
|
||||
<>
|
||||
<div className={styles.search}>
|
||||
{(text || owner) && (
|
||||
<SearchBar initialValue={(text || owner) as string} />
|
||||
)}
|
||||
<div className={styles.row}>
|
||||
<PriceFilter priceType={price} setPriceType={setPriceType} />
|
||||
<Sort
|
||||
sortType={sortType}
|
||||
sortDirection={sortDirection}
|
||||
setSortType={setSortType}
|
||||
setSortDirection={setSortDirection}
|
||||
setPriceType={setPriceType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.results}>
|
||||
{loading ? <Loader /> : <AssetQueryList queryResult={queryResult} />}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
48
src/components/templates/Search/sort.module.css
Normal file
48
src/components/templates/Search/sort.module.css
Normal file
@ -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);
|
||||
}
|
94
src/components/templates/Search/sort.tsx
Normal file
94
src/components/templates/Search/sort.tsx
Normal file
@ -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<React.SetStateAction<string>>
|
||||
sortDirection: string
|
||||
setSortDirection: React.Dispatch<React.SetStateAction<string>>
|
||||
setPriceType: React.Dispatch<React.SetStateAction<string>>
|
||||
}): 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 (
|
||||
<div className={styles.sortList}>
|
||||
<label className={styles.sortLabel}>Sort</label>
|
||||
{sortItems.map((e, index) => {
|
||||
const sorted = cx({
|
||||
[styles.selected]: e.value === sortType,
|
||||
[styles.sorted]: true
|
||||
})
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
className={sorted}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
handleSortButtonClick(e.value)
|
||||
}}
|
||||
>
|
||||
{e.display}
|
||||
{e.value === sortType ? (
|
||||
<span className={styles.direction}>{directionArrow}</span>
|
||||
) : null}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -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<QueryResult> {
|
||||
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<string> {
|
||||
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user