Search component

main
Eugen Rochko 2016-11-13 13:04:18 +01:00
parent 8152584cf5
commit f0bdfadab7
8 changed files with 291 additions and 4 deletions

View File

@ -0,0 +1,51 @@
import api from '../api'
export const SEARCH_CHANGE = 'SEARCH_CHANGE';
export const SEARCH_SUGGESTIONS_CLEAR = 'SEARCH_SUGGESTIONS_CLEAR';
export const SEARCH_SUGGESTIONS_READY = 'SEARCH_SUGGESTIONS_READY';
export const SEARCH_RESET = 'SEARCH_RESET';
export function changeSearch(value) {
return {
type: SEARCH_CHANGE,
value
};
};
export function clearSearchSuggestions() {
return {
type: SEARCH_SUGGESTIONS_CLEAR
};
};
export function readySearchSuggestions(value, accounts) {
return {
type: SEARCH_SUGGESTIONS_READY,
value,
accounts
};
};
export function fetchSearchSuggestions(value) {
return (dispatch, getState) => {
if (getState().getIn(['search', 'loaded_value']) === value) {
return;
}
api(getState).get('/api/v1/accounts/search', {
params: {
q: value,
resolve: true,
limit: 4
}
}).then(response => {
dispatch(readySearchSuggestions(value, response.data));
});
};
};
export function resetSearch() {
return {
type: SEARCH_RESET
};
};

View File

@ -0,0 +1,126 @@
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';
const getSuggestionValue = suggestion => suggestion.value;
const renderSuggestion = suggestion => {
if (suggestion.type === 'account') {
return <AutosuggestAccountContainer id={suggestion.id} />;
} else {
return <span>#{suggestion.id}</span>
}
};
const renderSectionTitle = section => (
<strong>{section.title}</strong>
);
const getSectionSuggestions = section => section.items;
const outerStyle = {
padding: '10px',
lineHeight: '20px',
position: 'relative'
};
const inputStyle = {
boxSizing: 'border-box',
display: 'block',
width: '100%',
border: 'none',
padding: '10px',
paddingRight: '30px',
fontFamily: 'Roboto',
background: '#282c37',
color: '#9baec8',
fontSize: '14px',
margin: '0'
};
const iconStyle = {
position: 'absolute',
top: '18px',
right: '20px',
color: '#9baec8',
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,
onClear: React.PropTypes.func.isRequired,
onFetch: React.PropTypes.func.isRequired,
onReset: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
onChange (_, { newValue }) {
if (typeof newValue !== 'string') {
return;
}
this.props.onChange(newValue);
},
onSuggestionsClearRequested () {
this.props.onClear();
},
onSuggestionsFetchRequested ({ value }) {
value = value.replace('#', '');
this.props.onFetch(value.trim());
},
onSuggestionSelected (_, { suggestion }) {
if (suggestion.type === 'account') {
this.context.router.push(`/accounts/${suggestion.id}`);
} else {
this.context.router.push(`/statuses/tag/${suggestion.id}`);
}
},
render () {
const inputProps = {
placeholder: 'Search',
value: this.props.value,
onChange: this.onChange,
style: inputStyle
};
return (
<div 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 style={iconStyle}><i className='fa fa-search' /></div>
</div>
);
},
});
export default Search;

View File

@ -0,0 +1,35 @@
import { connect } from 'react-redux';
import {
changeSearch,
clearSearchSuggestions,
fetchSearchSuggestions,
resetSearch
} from '../../../actions/search';
import Search from '../components/search';
const mapStateToProps = state => ({
suggestions: state.getIn(['search', 'suggestions']),
value: state.getIn(['search', 'value'])
});
const mapDispatchToProps = dispatch => ({
onChange (value) {
dispatch(changeSearch(value));
},
onClear () {
dispatch(clearSearchSuggestions());
},
onFetch (value) {
dispatch(fetchSearchSuggestions(value));
},
onReset () {
dispatch(resetSearch());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(Search);

View File

@ -5,6 +5,7 @@ import UploadFormContainer from '../ui/containers/upload_form_container';
import NavigationContainer from '../ui/containers/navigation_container'; import NavigationContainer from '../ui/containers/navigation_container';
import PureRenderMixin from 'react-addons-pure-render-mixin'; import PureRenderMixin from 'react-addons-pure-render-mixin';
import SuggestionsContainer from './containers/suggestions_container'; import SuggestionsContainer from './containers/suggestions_container';
import SearchContainer from './containers/search_container';
import { fetchSuggestions } from '../../actions/suggestions'; import { fetchSuggestions } from '../../actions/suggestions';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -24,13 +25,13 @@ const Compose = React.createClass({
return ( return (
<Drawer> <Drawer>
<div style={{ flex: '1 1 auto' }}> <div style={{ flex: '1 1 auto' }}>
<SearchContainer />
<NavigationContainer /> <NavigationContainer />
<ComposeFormContainer /> <ComposeFormContainer />
<UploadFormContainer /> <UploadFormContainer />
</div> </div>
<SuggestionsContainer /> <SuggestionsContainer />
<FollowFormContainer />
</Drawer> </Drawer>
); );
} }

View File

@ -26,6 +26,7 @@ import {
STATUS_FETCH_SUCCESS, STATUS_FETCH_SUCCESS,
CONTEXT_FETCH_SUCCESS CONTEXT_FETCH_SUCCESS
} from '../actions/statuses'; } from '../actions/statuses';
import { SEARCH_SUGGESTIONS_READY } from '../actions/search';
import Immutable from 'immutable'; import Immutable from 'immutable';
const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS(account)); const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS(account));
@ -70,6 +71,7 @@ export default function accounts(state = initialState, action) {
case REBLOGS_FETCH_SUCCESS: case REBLOGS_FETCH_SUCCESS:
case FAVOURITES_FETCH_SUCCESS: case FAVOURITES_FETCH_SUCCESS:
case COMPOSE_SUGGESTIONS_READY: case COMPOSE_SUGGESTIONS_READY:
case SEARCH_SUGGESTIONS_READY:
return normalizeAccounts(state, action.accounts); return normalizeAccounts(state, action.accounts);
case TIMELINE_REFRESH_SUCCESS: case TIMELINE_REFRESH_SUCCESS:
case TIMELINE_EXPAND_SUCCESS: case TIMELINE_EXPAND_SUCCESS:

View File

@ -10,6 +10,7 @@ import user_lists from './user_lists';
import accounts from './accounts'; import accounts from './accounts';
import statuses from './statuses'; import statuses from './statuses';
import relationships from './relationships'; import relationships from './relationships';
import search from './search';
export default combineReducers({ export default combineReducers({
timelines, timelines,
@ -22,5 +23,6 @@ export default combineReducers({
user_lists, user_lists,
accounts, accounts,
statuses, statuses,
relationships relationships,
search
}); });

View File

@ -0,0 +1,60 @@
import {
SEARCH_CHANGE,
SEARCH_SUGGESTIONS_READY,
SEARCH_RESET
} from '../actions/search';
import Immutable from 'immutable';
const initialState = Immutable.Map({
value: '',
loaded_value: '',
suggestions: []
});
const normalizeSuggestions = (state, value, accounts) => {
let newSuggestions = [
{
title: 'Account',
items: accounts.map(item => ({
type: 'account',
id: item.id,
value: item.acct
}))
}
];
if (value.indexOf('@') === -1) {
newSuggestions.push({
title: 'Hashtag',
items: [
{
type: 'hashtag',
id: value,
value: `#${value}`
}
]
});
}
return state.withMutations(map => {
map.set('suggestions', newSuggestions);
map.set('loaded_value', value);
});
};
export default function search(state = initialState, action) {
switch(action.type) {
case SEARCH_CHANGE:
return state.set('value', action.value);
case SEARCH_SUGGESTIONS_READY:
return normalizeSuggestions(state, action.value, action.accounts);
case SEARCH_RESET:
return state.withMutations(map => {
map.set('suggestions', []);
map.set('value', '');
map.set('loaded_value', '');
});
default:
return state;
}
};

View File

@ -325,12 +325,22 @@
top: 100%; top: 100%;
width: 100%; width: 100%;
z-index: 99; z-index: 99;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.4);
}
.react-autosuggest__section-title {
background: #9baec8;
padding: 4px 10px;
font-weight: 500;
cursor: default;
color: #282c37;
text-transform: uppercase;
font-size: 11px;
} }
.react-autosuggest__suggestions-list { .react-autosuggest__suggestions-list {
background: #9baec8; background: #d9e1e8;
color: #282c37; color: #282c37;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
font-size: 14px; font-size: 14px;
} }