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 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> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -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: | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| }); | }); | ||||||
|  |  | ||||||
							
								
								
									
										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%; |   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; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue