#600 Use react for search
Opened 6 years ago by shaily. Modified 6 years ago
shaily/fedora-hubs search_react  into  develop

file modified
+4 -9
@@ -18,7 +18,7 @@ 

  

  <nav class="navbar navbar-expand-md navbar-light" id="navbar-top">

  

-   <a class="navbar-brand col-md-4" href="{{ url_for('index') }}">

+   <a class="navbar-brand col-md-2" href="{{ url_for('index') }}">

      <img src="{{ url_for('static', filename='img/logo-hubs.png') }}" alt="Hubs">

    </a>

  
@@ -26,14 +26,9 @@ 

      <span class="navbar-toggler-icon"></span>

    </button>

  

-   <div class="col-md-4">

-     {% if g.auth.logged_in %}

-     <div class="form mx-auto" autocomplete="off">

-         <form id="bloodhound" class="input-group">

-           <input class="typeahead form-control" name="term" type="text" placeholder="Search hubs..." autocomplete="off"/>

-         </form>

-     </div>

-     {% endif %}

+   <div class="col-md-8">

+     {% block search %}

+     {% endblock %}

    </div>

    <div class="collapse navbar-collapse" id="navbar-top-menu">

      {% if g.auth.logged_in %}

file modified
+7 -1
@@ -6,8 +6,13 @@ 

  <!-- Flash messages handled in React -->

  {% endblock flash_messages %}

  

- {% block fullcontent %}

+ {% block search %}

+ 

+ <div id="searchbar"></div>

  

+ {% endblock %}

+ 

+ {% block fullcontent %}

  <div id="page"></div>

  

  {% endblock fullcontent %}
@@ -20,6 +25,7 @@ 

  <script>

  (function() {

    Hubs.setupPage("page", {{ initial_state|tojson }});

+   Hubs.setupSearch("searchbar", {{ url_for('api_query_hubs', searchterm="")| tojson }});

  })();

  </script>

  {% endblock %}

@@ -0,0 +1,55 @@ 

+ import React from 'react';

+ 

+ 

+ export default class HubResult extends React.Component {

+ 

+   highlightSummary() {

+     let s = this.props.hub.config.summary;

+     let summary = s.toLowerCase();

+     let query = this.props.query.toLowerCase();

+     let p1 = summary.indexOf(query);

+     let p2 = p1 + query.length;

+     if (p1 === -1) {

+       return <div>{s}</div>;

+     }

+     return (

+       <div>

+         {s.substring(0, p1)}<strong>{s.substring(p1, p2)}</strong>{s.substring(p2)}

+       </div>

+     );

+   }

+ 

+   render() {

+     let image = null;

+     if (this.props.hub.config.avatar) {

+       image =

+         <img src={this.props.hub.config.avatar} alt="avatar" width="60px" height="60px" className="rounded my-auto d-block"/>

+     }

+     else {

+       image =

+         <div className={`monogram-avatar bg-fedora-${this.props.hub.monogram_colour} text-fedora-${this.props.hub.monogram_colour}-dark`}>

+           {this.props.hub.name.charAt(0).toUpperCase()}

+         </div>

+     }

+     return (

+       <div className="media">

+         {image}

+         <div className="media-body pl-3">

+           <div>

+             <strong>{this.props.hub.name}</strong>

+           </div>

+           {this.highlightSummary()}

+           <div>

+             {this.props.hub.type === 'user' &&

+             <span className="badge badge-primary">User</span>

+             }

+             {this.props.hub.type === 'team' &&

+             <span className="badge badge-info">Group</span>

+             }

+           </div>

+         </div>

+       </div>

+     );

+   }

+ 

+ }

@@ -0,0 +1,66 @@ 

+ import React from 'react';

+ import { apiCall } from '../../core/utils';

+ import onClickOutside from "react-onclickoutside";

+ import SearchResults from './SearchResults';

+ 

+ 

+ class SearchBar extends React.Component {

+ 

+   constructor(props) {

+     super(props);

+     this.state = {

+       isLoading: false,

+       showResults: false,

+       results: null

+     };

+     this.handleChange = this.handleChange.bind(this);

+   }

+ 

+   handleClickOutside(event) {

+     this.setState({

+       showResults: false

+     })

+   }

+ 

+   handleChange(event) {

+     event.preventDefault();

+     let searchterm = this.refs.query.value;

+     if (!searchterm || searchterm.length < 2) {

+       this.setState({

+         showResults: false

+       });

+       return;

+     }

+     this.setState({ showResults: true, isLoading: true });

+     let url = `${this.props.searchUrl}${searchterm}`;

+     apiCall(url).then(

+       (data) => {

+         this.setState({ isLoading: false, results: data });

+       },

+       (error) => {

+         console.log(error);

+       }

+     )

+   }

+ 

+   render() {

+     return (

+       <div className="form mx-auto">

+         <div className="input-group">

+           <input className="form-control" type="text" placeholder="Search hubs..."

+                  onChange={this.handleChange} onClick={this.handleChange} ref="query"/>

+           <div style={{position: 'absolute', zIndex: 100, color: '#bbb', right: '10px', top: '7px'}}>

+             <i className="fa fa-search"/>

+           </div>

+           {this.state.showResults &&

+           <SearchResults isLoading={this.state.isLoading} results={this.state.results} query={this.refs.query.value}/>

+           }

+         </div>

+       </div>

+     )

+   }

+ 

+ 

+ }

+ 

+ export default onClickOutside(SearchBar);

@@ -0,0 +1,46 @@ 

+ import React from 'react';

+ import Spinner from '../Spinner/index';

+ import HubResult from "./HubResult";

+ 

+ 

+ export default class SearchResults extends React.Component {

+ 

+   render() {

+     let content;

+     if (this.props.isLoading) {

+       content =

+         <div className="text-center">

+           <Spinner/>

+         </div>

+     }

+     else if (this.props.results.length === 0) {

+       content =

+         <div className="text-center">

+           <span className="text-muted align-center">no matching hubs</span>

+         </div>

+     }

+     else {

+       content =

+         <div className="list-group">

+           {this.props.results.map((result) => {

+             return (

+               <a href={result.hub_url} key={result.name} className="list-group-item border-0 list-group-item-action p-1">

+                 <HubResult hub={result} query={this.props.query}/>

+               </a>

+             );

+           })

+           }

+         </div>

+     }

+     return (

+       <div style={{position: 'absolute', top: '100%', zIndex: '100', width: '100%', boxShadow: "0 5px 10px rgba(0, 0, 0, .2)"}}>

+         <div className="card">

+           <div className="card-block pt-1 px-2 pb-2">

+             {content}

+           </div>

+         </div>

+       </div>

+     );

+   }

+ 

+ }

file modified
+9 -2
@@ -2,12 +2,12 @@ 

  import ReactDOM from 'react-dom';

  import getStore from "./core/store";

  import App from "./core/App";

- 

+ import SearchBar from "./components/Search/SearchBar";

  

  // Read the public path from the backend

  // https://webpack.js.org/guides/public-path/

  if (window.resourceBaseUrl) {

-    __webpack_public_path__ = window.resourceBaseUrl;

+   __webpack_public_path__ = window.resourceBaseUrl;

  }

  

  
@@ -18,3 +18,10 @@ 

      document.getElementById(elementId)

    );

  }

+ 

+ export function setupSearch(elementId, searchUrl) {

+   ReactDOM.render(

+     <SearchBar searchUrl={searchUrl}/>,

+     document.getElementById(elementId)

+   );

+ }

file modified
+1
@@ -17,6 +17,7 @@ 

      "react-dom": "^16.0.0",

      "react-intl": "^2.4.0",

      "react-linkify": "^0.2.1",

+     "react-onclickoutside": "^6.7.1",

      "react-redux": "^5.0.6",

      "react-select": "^1.2.1",

      "react-sortable-hoc": "^0.6.7",

This PR tries to reproduce the behavior exhibited by the typeahead.js code using React components.
Arrow key based navigation of search results is currently missing.
Substring highlighting in search results does not work with accents. This is currently unsupported on the backend too, but is taken care of in PR #540.
Whoosh has support for automatically highlighting the matching terms which we can then pass as props to HubResult

Width of search bar has been increased from 4 columns => 8 columns to make room for more detailed widget search results (in progress).

Package 'react-onclickoutside' has also been added, it is confirmed to be working on touch devices.

rebased onto 72cc774

6 years ago