Revamp search filters

## Issue
4945: Simplify / revamp search filters for odysee (and lbry.tv)

## Changes
- Tweaked the "claim type = file | channel | everything" a little to hopefully make it more intuitive.
- Added "Sort By".
- Added "Upload Date".
- Fixed the affected files to handle both Desktop and Odysee.
- Tweaked the layout to be more condensed so that we can see some results as we change the filter.
- Added animations.
- Added "Exact match" option that helps user to surround with quotes.
This commit is contained in:
infinite-persistence 2021-03-24 13:55:46 +08:00 committed by Sean Yesmunt
parent a9cae23d87
commit 5421a64b65
6 changed files with 300 additions and 96 deletions

View file

@ -175,7 +175,6 @@
"These search results are provided by LBRY, Inc.": "These search results are provided by LBRY, Inc.", "These search results are provided by LBRY, Inc.": "These search results are provided by LBRY, Inc.",
"FILTER": "FILTER", "FILTER": "FILTER",
"View file": "View file", "View file": "View file",
"Search For": "Search For",
"Files": "Files", "Files": "Files",
"Channels": "Channels", "Channels": "Channels",
"Everything": "Everything", "Everything": "Everything",
@ -184,6 +183,15 @@
"Other": "Other", "Other": "Other",
"Other Options": "Other Options", "Other Options": "Other Options",
"Returned Results": "Returned Results", "Returned Results": "Returned Results",
"Find results that include all the given words in the exact order.\nThis can also be done by surrounding the search query with quotation marks (e.g. \"hello world\").": "Find results that include all the given words in the exact order.\nThis can also be done by surrounding the search query with quotation marks (e.g. \"hello world\").",
"Last Hour": "Last Hour",
"Last 24 Hours": "Last 24 Hours",
"Relevance": "Relevance",
"Newest first": "Newest first",
"Oldest first": "Oldest first",
"Exact match": "Exact match",
"Upload Date": "Upload Date",
"Sort By": "Sort By",
"Custom Code": "Custom Code", "Custom Code": "Custom Code",
"Are you a supermodel or rockstar that received a custom reward code? Claim it here.": "Are you a supermodel or rockstar that received a custom reward code? Claim it here.", "Are you a supermodel or rockstar that received a custom reward code? Claim it here.": "Are you a supermodel or rockstar that received a custom reward code? Claim it here.",
"Go To Invites": "Go To Invites", "Go To Invites": "Go To Invites",

View file

@ -195,6 +195,19 @@ export const icons = {
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" /> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
</g> </g>
), ),
[ICONS.FILTER]: buildIcon(
<g>
<line x1="4" y1="21" x2="4" y2="14" />
<line x1="4" y1="10" x2="4" y2="3" />
<line x1="12" y1="21" x2="12" y2="12" />
<line x1="12" y1="8" x2="12" y2="3" />
<line x1="20" y1="21" x2="20" y2="16" />
<line x1="20" y1="12" x2="20" y2="3" />
<line x1="1" y1="14" x2="7" y2="14" />
<line x1="9" y1="8" x2="15" y2="8" />
<line x1="17" y1="16" x2="23" y2="16" />
</g>
),
[ICONS.ACCOUNT]: buildIcon( [ICONS.ACCOUNT]: buildIcon(
<g> <g>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />

View file

@ -4,16 +4,49 @@ import * as ICONS from 'constants/icons';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import Icon from 'component/common/icon';
import classnames from 'classnames';
const CLAIM_TYPES = {
[SEARCH_OPTIONS.INCLUDE_FILES]: 'Files',
[SEARCH_OPTIONS.INCLUDE_CHANNELS]: 'Channels',
[SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS]: 'Everything',
};
const TYPES_ADVANCED = {
[SEARCH_OPTIONS.MEDIA_VIDEO]: 'Video',
[SEARCH_OPTIONS.MEDIA_AUDIO]: 'Audio',
[SEARCH_OPTIONS.MEDIA_IMAGE]: 'Image',
[SEARCH_OPTIONS.MEDIA_TEXT]: 'Text',
[SEARCH_OPTIONS.MEDIA_APPLICATION]: 'Other',
};
const TIME_FILTER = {
'': 'None',
[SEARCH_OPTIONS.TIME_FILTER_LAST_HOUR]: 'Last Hour',
[SEARCH_OPTIONS.TIME_FILTER_TODAY]: 'Last 24 Hours',
[SEARCH_OPTIONS.TIME_FILTER_THIS_WEEK]: 'This Week',
[SEARCH_OPTIONS.TIME_FILTER_THIS_MONTH]: 'This Month',
[SEARCH_OPTIONS.TIME_FILTER_THIS_YEAR]: 'This Year',
};
const SORT_BY = {
'': 'Relevance',
[SEARCH_OPTIONS.SORT_DESCENDING]: 'Newest first',
[SEARCH_OPTIONS.SORT_ACCENDING]: 'Oldest first',
};
type Props = { type Props = {
setSearchOption: (string, boolean | string | number) => void, setSearchOption: (string, boolean | string | number) => void,
options: {}, options: {},
simple: boolean,
expanded: boolean, expanded: boolean,
toggleSearchExpanded: () => void, toggleSearchExpanded: () => void,
}; };
const SearchOptions = (props: Props) => { const SearchOptions = (props: Props) => {
const { options, setSearchOption, expanded, toggleSearchExpanded } = props; const { options, simple, setSearchOption, expanded, toggleSearchExpanded } = props;
const stringifiedOptions = JSON.stringify(options); const stringifiedOptions = JSON.stringify(options);
const resultCount = options[SEARCH_OPTIONS.RESULT_COUNT]; const resultCount = options[SEARCH_OPTIONS.RESULT_COUNT];
@ -23,99 +56,180 @@ const SearchOptions = (props: Props) => {
return claimType.includes(SEARCH_OPTIONS.INCLUDE_CHANNELS); return claimType.includes(SEARCH_OPTIONS.INCLUDE_CHANNELS);
}, [stringifiedOptions]); }, [stringifiedOptions]);
return ( if (simple) {
<div> delete TYPES_ADVANCED[SEARCH_OPTIONS.MEDIA_APPLICATION];
<Button }
button="alt"
label={__('Filter')}
iconRight={expanded ? ICONS.UP : ICONS.DOWN}
onClick={toggleSearchExpanded}
/>
{expanded && (
<Form className="search__options">
<fieldset>
<legend className="search__legend">{__('Search For')}</legend>
{[
{
option: SEARCH_OPTIONS.INCLUDE_FILES,
label: __('Files'),
},
{
option: SEARCH_OPTIONS.INCLUDE_CHANNELS,
label: __('Channels'),
},
{
option: SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS,
label: __('Everything'),
},
].map(({ option, label }) => (
<FormField
key={option}
name={option}
type="radio"
blockWrap={false}
label={label}
checked={options[SEARCH_OPTIONS.CLAIM_TYPE] === option}
onChange={() => setSearchOption(SEARCH_OPTIONS.CLAIM_TYPE, option)}
/>
))}
</fieldset>
<fieldset disabled={isFilteringByChannel}> function addRow(label: string, value: any) {
<legend className="search__legend">{__('Type')}</legend> return (
{[ <tr>
{ <td>
option: SEARCH_OPTIONS.MEDIA_VIDEO, <legend className="search__legend">{label}</legend>
label: __('Video'), </td>
}, <td>{value}</td>
{ </tr>
option: SEARCH_OPTIONS.MEDIA_AUDIO, );
label: __('Audio'), }
},
{ const OBJ_TO_OPTION_ELEM = (obj) => {
option: SEARCH_OPTIONS.MEDIA_IMAGE, return Object.entries(obj).map((x) => {
label: __('Image'), return (
}, <option key={x[0]} value={x[0]}>
{ {__(String(x[1]))}
option: SEARCH_OPTIONS.MEDIA_TEXT, </option>
label: __('Text'), );
}, });
{ };
option: SEARCH_OPTIONS.MEDIA_APPLICATION,
label: __('Other'), const typeElem = (
}, <>
].map(({ option, label }) => ( <div className="filter-values">
<div>
{Object.entries(CLAIM_TYPES).map((t) => {
const option = t[0];
if (option === SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS) {
return null;
}
return (
<Button
key={option}
button="alt"
label={t[1]}
className={classnames(`button-toggle`, {
'button-toggle--active': options[SEARCH_OPTIONS.CLAIM_TYPE] === option,
})}
onClick={() => setSearchOption(SEARCH_OPTIONS.CLAIM_TYPE, option)}
/>
);
})}
</div>
<Button
button="close"
className={classnames('close-button', {
'close-button--visible': options[SEARCH_OPTIONS.CLAIM_TYPE] !== SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS,
})}
icon={ICONS.REMOVE}
onClick={() => setSearchOption(SEARCH_OPTIONS.CLAIM_TYPE, SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS)}
/>
</div>
<div className="media-types">
{options[SEARCH_OPTIONS.CLAIM_TYPE] === SEARCH_OPTIONS.INCLUDE_FILES &&
Object.entries(TYPES_ADVANCED).map((t) => {
const option = t[0];
return (
<FormField <FormField
key={option} key={option}
name={option} name={option}
type="checkbox" type="checkbox"
blockWrap={false} blockWrap={false}
disabled={options[SEARCH_OPTIONS.CLAIM_TYPE] === SEARCH_OPTIONS.INCLUDE_CHANNELS} disabled={options[SEARCH_OPTIONS.CLAIM_TYPE] !== SEARCH_OPTIONS.INCLUDE_FILES}
label={label} label={t[1]}
checked={!isFilteringByChannel && options[option]} checked={!isFilteringByChannel && options[option]}
onChange={() => setSearchOption(option, !options[option])} onChange={() => setSearchOption(option, !options[option])}
/> />
))} );
</fieldset> })}
</div>
</>
);
<fieldset> const otherOptionsElem = (
<legend className="search__legend">{__('Other Options')}</legend> <>
<FormField <div className="filter-values">
type="select" <FormField
name="result-count" type="checkbox"
value={resultCount} name="exact-match"
onChange={(e) => setSearchOption(SEARCH_OPTIONS.RESULT_COUNT, e.target.value)} checked={options[SEARCH_OPTIONS.EXACT]}
blockWrap={false} onChange={() => setSearchOption(SEARCH_OPTIONS.EXACT, !options[SEARCH_OPTIONS.EXACT])}
label={__('Returned Results')} label={__('Exact match')}
> />
<option value={10}>10</option> <Icon
<option value={30}>30</option> className="icon--help"
<option value={50}>50</option> icon={ICONS.HELP}
<option value={100}>100</option> tooltip
</FormField> size={16}
</fieldset> customTooltipText={__(
</Form> 'Find results that include all the given words in the exact order.\nThis can also be done by surrounding the search query with quotation marks (e.g. "hello world").'
)}
/>
</div>
{!simple && (
<FormField
type="select"
name="result-count"
value={resultCount}
onChange={(e) => setSearchOption(SEARCH_OPTIONS.RESULT_COUNT, e.target.value)}
blockWrap={false}
label={__('Returned Results')}
>
<option value={10}>10</option>
<option value={30}>30</option>
<option value={50}>50</option>
<option value={100}>100</option>
</FormField>
)} )}
</>
);
const uploadDateElem = (
<div className="filter-values">
<FormField
type="select"
name="upload-date"
value={options[SEARCH_OPTIONS.TIME_FILTER]}
onChange={(e) => setSearchOption(SEARCH_OPTIONS.TIME_FILTER, e.target.value)}
blockWrap={false}
>
{OBJ_TO_OPTION_ELEM(TIME_FILTER)}
</FormField>
<Button
button="close"
className={classnames('close-button', {
'close-button--visible': options[SEARCH_OPTIONS.TIME_FILTER] !== '',
})}
icon={ICONS.REMOVE}
onClick={() => setSearchOption(SEARCH_OPTIONS.TIME_FILTER, '')}
/>
</div>
);
const sortByElem = (
<div className="filter-values">
<FormField
type="select"
name="sort-by"
blockWrap={false}
value={options[SEARCH_OPTIONS.SORT]}
onChange={(e) => setSearchOption(SEARCH_OPTIONS.SORT, e.target.value)}
>
{OBJ_TO_OPTION_ELEM(SORT_BY)}
</FormField>
</div>
);
return (
<div>
<Button
button="alt"
label={__('Filter')}
icon={ICONS.FILTER}
iconRight={expanded ? ICONS.UP : ICONS.DOWN}
onClick={toggleSearchExpanded}
/>
<Form
className={classnames('search__options', {
'search__options--expanded': expanded,
})}
>
<table className="table table--condensed">
<tbody>
{addRow(__('Type'), typeElem)}
{addRow(__('Upload Date'), uploadDateElem)}
{addRow(__('Sort By'), sortByElem)}
{addRow(__('Other Options'), otherOptionsElem)}
</tbody>
</table>
</Form>
</div> </div>
); );
}; };

View file

@ -51,6 +51,7 @@ export const MORE = 'More';
export const SHARE_LINK = 'ShareLink'; export const SHARE_LINK = 'ShareLink';
export const ACCOUNT = 'User'; export const ACCOUNT = 'User';
export const SETTINGS = 'Settings'; export const SETTINGS = 'Settings';
export const FILTER = 'Filter';
export const INVITE = 'Users'; export const INVITE = 'Users';
export const FILE = 'File'; export const FILE = 'File';
export const FULLSCREEN = 'Maximize'; export const FULLSCREEN = 'Maximize';

View file

@ -24,8 +24,8 @@ type Props = {
isSearching: boolean, isSearching: boolean,
location: UrlLocation, location: UrlLocation,
uris: Array<string>, uris: Array<string>,
onFeedbackNegative: string => void, onFeedbackNegative: (string) => void,
onFeedbackPositive: string => void, onFeedbackPositive: (string) => void,
showNsfw: boolean, showNsfw: boolean,
isAuthenticated: boolean, isAuthenticated: boolean,
}; };
@ -49,10 +49,7 @@ export default function SearchPage(props: Props) {
additionalOptions['nsfw'] = showNsfw; additionalOptions['nsfw'] = showNsfw;
const modifiedUrlQuery = urlQuery const modifiedUrlQuery = urlQuery.trim().replace(/\s+/g, '').replace(/:/g, '#');
.trim()
.replace(/\s+/g, '')
.replace(/:/g, '#');
const uriFromQuery = `lbry://${modifiedUrlQuery}`; const uriFromQuery = `lbry://${modifiedUrlQuery}`;
let streamName; let streamName;
@ -72,7 +69,7 @@ export default function SearchPage(props: Props) {
try { try {
const dummyUrlForClaimId = `x#${urlQuery}`; const dummyUrlForClaimId = `x#${urlQuery}`;
({ claimId } = parseURI(dummyUrlForClaimId)); ({ claimId } = parseURI(dummyUrlForClaimId));
Lbry.claim_search({ claim_id: claimId }).then(res => { Lbry.claim_search({ claim_id: claimId }).then((res) => {
if (res.items && res.items.length) { if (res.items && res.items.length) {
const claim = res.items[0]; const claim = res.items[0];
const url = formatLbryUrlForWeb(claim.canonical_url); const url = formatLbryUrlForWeb(claim.canonical_url);
@ -100,7 +97,7 @@ export default function SearchPage(props: Props) {
<ClaimList <ClaimList
uris={uris} uris={uris}
loading={isSearching} loading={isSearching}
header={!SIMPLE_SITE && <SearchOptions additionalOptions={additionalOptions} />} header={<SearchOptions simple={SIMPLE_SITE} additionalOptions={additionalOptions} />}
injectedItem={ injectedItem={
SHOW_ADS && IS_WEB ? (SIMPLE_SITE ? false : !isAuthenticated && <Ads small type={'video'} />) : false SHOW_ADS && IS_WEB ? (SIMPLE_SITE ? false : !isAuthenticated && <Ads small type={'video'} />) : false
} }

View file

@ -8,22 +8,87 @@
} }
.search__options { .search__options {
margin-top: var(--spacing-l); margin-top: var(--spacing-s);
padding-left: var(--spacing-m);
transition: all 0.3s ease-out;
// These will be expanded by 'search__options--expanded'.
max-height: 0;
opacity: 0;
visibility: hidden;
.search__legend { .search__legend {
background-color: var(--color-secondary-alt); font-weight: var(--font-weight-bold);
color: var(--color-secondary); font-size: var(--font-small);
margin-bottom: var(--spacing-s); margin-bottom: var(--spacing-s);
padding: var(--spacing-xxs); padding: var(--spacing-xxs);
} }
fieldset:not(:first-child) { fieldset:not(:first-child) {
margin-top: var(--spacing-l); margin-top: var(--spacing-m);
}
table {
line-height: 1.5;
table-layout: fixed;
}
td:nth-of-type(1) {
width: 20%;
vertical-align: top;
}
.claim-type-filter {
display: inline-block;
}
.close-button {
visibility: hidden;
}
.close-button--visible {
visibility: visible;
}
.filter-values {
display: flex;
align-items: flex-start;
.button-toggle {
margin-right: 0;
}
select {
@media (min-width: $breakpoint-small) {
min-width: 200px;
}
}
.icon--help {
margin-top: 4px;
}
.button--close {
position: unset;
align-self: center;
margin-left: var(--spacing-s);
}
}
.media-types {
margin-top: var(--spacing-s);
label {
display: flex;
align-items: center;
}
} }
.radio, .radio,
.checkbox { .checkbox {
display: inline-block; display: inline-block;
margin-top: 0;
&:not(:first-of-type) { &:not(:first-of-type) {
margin-left: var(--spacing-m); margin-left: var(--spacing-m);
@ -31,6 +96,12 @@
} }
} }
.search__options--expanded {
max-height: 400px; // how to not hardcode this :(
opacity: 1;
visibility: visible;
}
.search__top-link { .search__top-link {
margin-top: var(--spacing-s); margin-top: var(--spacing-s);
font-weight: var(--font-weight-body); font-weight: var(--font-weight-body);