@ -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; | |||
} | |||
}; |