parent
8152584cf5
commit
f0bdfadab7
@ -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 |
||||
}; |
||||
}; |
@ -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; |
@ -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); |
@ -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; |
||||
} |
||||
}; |
Loading…
Reference in new issue