0
0
Fork 0

Rework search

This commit is contained in:
Eugen Rochko 2017-03-31 19:59:54 +02:00
parent 553e6dd07c
commit b4046c5957
13 changed files with 352 additions and 265 deletions

View file

@ -1,44 +0,0 @@
import { Link } from 'react-router';
import { injectIntl, defineMessages } from 'react-intl';
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
});
const Drawer = ({ children, withHeader, intl }) => {
let header = '';
if (withHeader) {
header = (
<div className='drawer__header'>
<Link title={intl.formatMessage(messages.start)} className='drawer__tab' to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link>
<Link title={intl.formatMessage(messages.community)} className='drawer__tab' to='/timelines/public/local'><i className='fa fa-fw fa-users' /></Link>
<Link title={intl.formatMessage(messages.public)} className='drawer__tab' to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link>
<a title={intl.formatMessage(messages.preferences)} className='drawer__tab' href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a>
<a title={intl.formatMessage(messages.logout)} className='drawer__tab' href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a>
</div>
);
}
return (
<div className='drawer'>
{header}
<div className='drawer__inner'>
{children}
</div>
</div>
);
};
Drawer.propTypes = {
withHeader: React.PropTypes.bool,
children: React.PropTypes.node,
intl: React.PropTypes.object
};
export default injectIntl(Drawer);

View file

@ -1,123 +1,67 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Autosuggest from 'react-autosuggest';
import AutosuggestAccountContainer from '../containers/autosuggest_account_container';
import AutosuggestStatusContainer from '../containers/autosuggest_status_container';
import { debounce } from 'react-decoration';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }
});
const getSuggestionValue = suggestion => suggestion.value;
const renderSuggestion = suggestion => {
if (suggestion.type === 'account') {
return <AutosuggestAccountContainer id={suggestion.id} />;
} else if (suggestion.type === 'hashtag') {
return <span>#{suggestion.id}</span>;
} else {
return <AutosuggestStatusContainer id={suggestion.id} />;
}
};
const renderSectionTitle = section => (
<strong><FormattedMessage id={`search.${section.title}`} defaultMessage={section.title} /></strong>
);
const getSectionSuggestions = section => section.items;
const outerStyle = {
padding: '10px',
lineHeight: '20px',
position: 'relative'
};
const iconStyle = {
position: 'absolute',
top: '18px',
right: '20px',
fontSize: '18px',
pointerEvents: 'none'
};
const Search = React.createClass({
contextTypes: {
router: React.PropTypes.object
},
propTypes: {
suggestions: React.PropTypes.array.isRequired,
value: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
onSubmit: React.PropTypes.func.isRequired,
onClear: React.PropTypes.func.isRequired,
onFetch: React.PropTypes.func.isRequired,
onReset: React.PropTypes.func.isRequired,
onShow: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
onChange (_, { newValue }) {
if (typeof newValue !== 'string') {
return;
}
this.props.onChange(newValue);
handleChange (e) {
this.props.onChange(e.target.value);
},
onSuggestionsClearRequested () {
handleClear (e) {
e.preventDefault();
this.props.onClear();
},
@debounce(500)
onSuggestionsFetchRequested ({ value }) {
value = value.replace('#', '');
this.props.onFetch(value.trim());
},
onSuggestionSelected (_, { suggestion }) {
if (suggestion.type === 'account') {
this.context.router.push(`/accounts/${suggestion.id}`);
} else if(suggestion.type === 'hashtag') {
this.context.router.push(`/timelines/tag/${suggestion.id}`);
} else {
this.context.router.push(`/statuses/${suggestion.id}`);
handleKeyDown (e) {
if (e.key === 'Enter') {
e.preventDefault();
this.props.onSubmit();
}
},
handleFocus () {
this.props.onShow();
},
render () {
const inputProps = {
placeholder: this.props.intl.formatMessage(messages.placeholder),
value: this.props.value,
onChange: this.onChange,
className: 'search__input'
};
const { intl, value } = this.props;
const hasValue = value.length > 0;
return (
<div className='search' style={outerStyle}>
<Autosuggest
multiSection={true}
suggestions={this.props.suggestions}
focusFirstSuggestion={true}
focusInputOnSuggestionClick={false}
alwaysRenderSuggestions={false}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
renderSectionTitle={renderSectionTitle}
getSectionSuggestions={getSectionSuggestions}
inputProps={inputProps}
<div className='search'>
<input
className='search__input'
type='text'
placeholder={intl.formatMessage(messages.placeholder)}
value={value}
onChange={this.handleChange}
onKeyUp={this.handleKeyDown}
onFocus={this.handleFocus}
/>
<div style={iconStyle}><i className='fa fa-search' /></div>
<div className='search__icon'>
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
<i className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} onClick={this.handleClear} />
</div>
</div>
);
},
}
});

View file

@ -0,0 +1,68 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../../containers/account_container';
import StatusContainer from '../../../containers/status_container';
import { Link } from 'react-router';
const SearchResults = React.createClass({
propTypes: {
results: ImmutablePropTypes.map.isRequired
},
mixins: [PureRenderMixin],
render () {
const { results } = this.props;
let accounts, statuses, hashtags;
let count = 0;
if (results.get('accounts') && results.get('accounts').size > 0) {
count += results.get('accounts').size;
accounts = (
<div className='search-results__section'>
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
</div>
);
}
if (results.get('statuses') && results.get('statuses').size > 0) {
count += results.get('statuses').size;
statuses = (
<div className='search-results__section'>
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
</div>
);
}
if (results.get('hashtags') && results.get('hashtags').size > 0) {
count += results.get('hashtags').size;
hashtags = (
<div className='search-results__section'>
{results.get('hashtags').map(hashtag =>
<Link className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}>
#{hashtag}
</Link>
)}
</div>
);
}
return (
<div className='search-results'>
<div className='search-results__header'>
<FormattedMessage id='search_results.total' defaultMessage='{count} {count, plural, one {result} other {results}}' values={{ count }} />
</div>
{accounts}
{statuses}
{hashtags}
</div>
);
}
});
export default SearchResults;

View file

@ -1,31 +0,0 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import { FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import Collapsable from '../../../components/collapsable';
const SensitiveToggle = React.createClass({
propTypes: {
hasMedia: React.PropTypes.bool,
isSensitive: React.PropTypes.bool,
onChange: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
render () {
const { hasMedia, isSensitive, onChange } = this.props;
return (
<Collapsable isVisible={hasMedia} fullHeight={39.5}>
<label className='compose-form__label'>
<Toggle checked={isSensitive} onChange={onChange} />
<span className='compose-form__label__text'><FormattedMessage id='compose_form.sensitive' defaultMessage='Mark media as sensitive' /></span>
</label>
</Collapsable>
);
}
});
export default SensitiveToggle;

View file

@ -1,27 +0,0 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import { FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
const SpoilerToggle = React.createClass({
propTypes: {
isSpoiler: React.PropTypes.bool,
onChange: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
render () {
const { isSpoiler, onChange } = this.props;
return (
<label className='compose-form__label with-border' style={{ marginTop: '10px' }}>
<Toggle checked={isSpoiler} onChange={onChange} />
<span className='compose-form__label__text'><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide text behind warning' /></span>
</label>
);
}
});
export default SpoilerToggle;

View file

@ -1,14 +1,13 @@
import { connect } from 'react-redux';
import {
changeSearch,
clearSearchSuggestions,
fetchSearchSuggestions,
resetSearch
clearSearch,
submitSearch,
showSearch
} from '../../../actions/search';
import Search from '../components/search';
const mapStateToProps = state => ({
suggestions: state.getIn(['search', 'suggestions']),
value: state.getIn(['search', 'value'])
});
@ -19,15 +18,15 @@ const mapDispatchToProps = dispatch => ({
},
onClear () {
dispatch(clearSearchSuggestions());
dispatch(clearSearch());
},
onFetch (value) {
dispatch(fetchSearchSuggestions(value));
onSubmit () {
dispatch(submitSearch());
},
onReset () {
dispatch(resetSearch());
onShow () {
dispatch(showSearch());
}
});

View file

@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import SearchResults from '../components/search_results';
const mapStateToProps = state => ({
results: state.getIn(['search', 'results'])
});
export default connect(mapStateToProps)(SearchResults);

View file

@ -1,17 +1,34 @@
import Drawer from './components/drawer';
import ComposeFormContainer from './containers/compose_form_container';
import UploadFormContainer from './containers/upload_form_container';
import NavigationContainer from './containers/navigation_container';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import SearchContainer from './containers/search_container';
import { connect } from 'react-redux';
import { mountCompose, unmountCompose } from '../../actions/compose';
import { Link } from 'react-router';
import { injectIntl, defineMessages } from 'react-intl';
import SearchContainer from './containers/search_container';
import { Motion, spring } from 'react-motion';
import SearchResultsContainer from './containers/search_results_container';
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
});
const mapStateToProps = state => ({
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden'])
});
const Compose = React.createClass({
propTypes: {
dispatch: React.PropTypes.func.isRequired,
withHeader: React.PropTypes.bool
withHeader: React.PropTypes.bool,
showSearch: React.PropTypes.bool,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
@ -25,15 +42,46 @@ const Compose = React.createClass({
},
render () {
const { withHeader, showSearch, intl } = this.props;
let header = '';
if (withHeader) {
header = (
<div className='drawer__header'>
<Link title={intl.formatMessage(messages.start)} className='drawer__tab' to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link>
<Link title={intl.formatMessage(messages.community)} className='drawer__tab' to='/timelines/public/local'><i className='fa fa-fw fa-users' /></Link>
<Link title={intl.formatMessage(messages.public)} className='drawer__tab' to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link>
<a title={intl.formatMessage(messages.preferences)} className='drawer__tab' href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a>
<a title={intl.formatMessage(messages.logout)} className='drawer__tab' href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a>
</div>
);
}
return (
<Drawer withHeader={this.props.withHeader}>
<div className='drawer'>
{header}
<SearchContainer />
<NavigationContainer />
<ComposeFormContainer />
</Drawer>
<div className='drawer__pager'>
<div className='drawer__inner'>
<NavigationContainer />
<ComposeFormContainer />
</div>
<Motion defaultStyle={{ x: -300 }} style={{ x: spring(showSearch ? 0 : -300, { stiffness: 210, damping: 20 }) }}>
{({ x }) =>
<div className='drawer__inner darker' style={{ transform: `translateX(${x}px)` }}>
<SearchResultsContainer />
</div>
}
</Motion>
</div>
</div>
);
}
});
export default connect()(Compose);
export default connect(mapStateToProps)(injectIntl(Compose));