From 72cc774f8e2f082319d8668d791c3f025af709df Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Mar 20 2018 06:45:50 +0000
Subject: Use react for search


---

diff --git a/hubs/templates/master.html b/hubs/templates/master.html
index 3562b91..3b6ae55 100644
--- a/hubs/templates/master.html
+++ b/hubs/templates/master.html
@@ -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 %}
diff --git a/hubs/templates/react.html b/hubs/templates/react.html
index 731b5be..ce3a8fc 100644
--- a/hubs/templates/react.html
+++ b/hubs/templates/react.html
@@ -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 %}
diff --git a/js/app/components/Search/HubResult.js b/js/app/components/Search/HubResult.js
new file mode 100644
index 0000000..1e325b0
--- /dev/null
+++ b/js/app/components/Search/HubResult.js
@@ -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>
+    );
+  }
+
+}
diff --git a/js/app/components/Search/SearchBar.js b/js/app/components/Search/SearchBar.js
new file mode 100644
index 0000000..3b1e34f
--- /dev/null
+++ b/js/app/components/Search/SearchBar.js
@@ -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);
diff --git a/js/app/components/Search/SearchResults.js b/js/app/components/Search/SearchResults.js
new file mode 100644
index 0000000..84d99f7
--- /dev/null
+++ b/js/app/components/Search/SearchResults.js
@@ -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>
+    );
+  }
+
+}
diff --git a/js/app/index.js b/js/app/index.js
index 45664b8..0c33a5a 100644
--- a/js/app/index.js
+++ b/js/app/index.js
@@ -2,12 +2,12 @@ import React from 'react';
 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 @@ export function setupPage(elementId, initialState) {
     document.getElementById(elementId)
   );
 }
+
+export function setupSearch(elementId, searchUrl) {
+  ReactDOM.render(
+    <SearchBar searchUrl={searchUrl}/>,
+    document.getElementById(elementId)
+  );
+}
diff --git a/js/package.json b/js/package.json
index 130fc5d..435904b 100644
--- a/js/package.json
+++ b/js/package.json
@@ -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",

