Search component
This commit is contained in:
		
					parent
					
						
							
								8152584cf5
							
						
					
				
			
			
				commit
				
					
						f0bdfadab7
					
				
			
		
					 8 changed files with 291 additions and 4 deletions
				
			
		
							
								
								
									
										51
									
								
								app/assets/javascripts/components/actions/search.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/assets/javascripts/components/actions/search.jsx
									
										
									
									
									
										Normal 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 | ||||
|   }; | ||||
| }; | ||||
|  | @ -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); | ||||
|  | @ -5,6 +5,7 @@ import UploadFormContainer  from '../ui/containers/upload_form_container'; | |||
| import NavigationContainer  from '../ui/containers/navigation_container'; | ||||
| import PureRenderMixin      from 'react-addons-pure-render-mixin'; | ||||
| import SuggestionsContainer from './containers/suggestions_container'; | ||||
| import SearchContainer      from './containers/search_container'; | ||||
| import { fetchSuggestions } from '../../actions/suggestions'; | ||||
| import { connect }          from 'react-redux'; | ||||
| 
 | ||||
|  | @ -24,13 +25,13 @@ const Compose = React.createClass({ | |||
|     return ( | ||||
|       <Drawer> | ||||
|         <div style={{ flex: '1 1 auto' }}> | ||||
|           <SearchContainer /> | ||||
|           <NavigationContainer /> | ||||
|           <ComposeFormContainer /> | ||||
|           <UploadFormContainer /> | ||||
|         </div> | ||||
| 
 | ||||
|         <SuggestionsContainer /> | ||||
|         <FollowFormContainer /> | ||||
|       </Drawer> | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
|  | @ -26,6 +26,7 @@ import { | |||
|   STATUS_FETCH_SUCCESS, | ||||
|   CONTEXT_FETCH_SUCCESS | ||||
| } from '../actions/statuses'; | ||||
| import { SEARCH_SUGGESTIONS_READY } from '../actions/search'; | ||||
| import Immutable from 'immutable'; | ||||
| 
 | ||||
| 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 FAVOURITES_FETCH_SUCCESS: | ||||
|     case COMPOSE_SUGGESTIONS_READY: | ||||
|     case SEARCH_SUGGESTIONS_READY: | ||||
|       return normalizeAccounts(state, action.accounts); | ||||
|     case TIMELINE_REFRESH_SUCCESS: | ||||
|     case TIMELINE_EXPAND_SUCCESS: | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import user_lists            from './user_lists'; | |||
| import accounts              from './accounts'; | ||||
| import statuses              from './statuses'; | ||||
| import relationships         from './relationships'; | ||||
| import search                from './search'; | ||||
| 
 | ||||
| export default combineReducers({ | ||||
|   timelines, | ||||
|  | @ -22,5 +23,6 @@ export default combineReducers({ | |||
|   user_lists, | ||||
|   accounts, | ||||
|   statuses, | ||||
|   relationships | ||||
|   relationships, | ||||
|   search | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										60
									
								
								app/assets/javascripts/components/reducers/search.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								app/assets/javascripts/components/reducers/search.jsx
									
										
									
									
									
										Normal 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; | ||||
|   } | ||||
| }; | ||||
|  | @ -325,12 +325,22 @@ | |||
|   top: 100%; | ||||
|   width: 100%; | ||||
|   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 { | ||||
|   background: #9baec8; | ||||
|   background: #d9e1e8; | ||||
|   color: #282c37; | ||||
|   box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); | ||||
|   font-size: 14px; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue