diff --git a/client/src/components/layouts/Default.js b/client/src/components/layouts/Default.js
index a4ecd920b2..84ac3d5f52 100644
--- a/client/src/components/layouts/Default.js
+++ b/client/src/components/layouts/Default.js
@@ -17,6 +17,7 @@ import { flashMessagesSelector, removeFlashMessage } from '../Flash/redux';
import { isBrowser } from '../../../utils';
+import WithInstantSearch from '../search/WithInstantSearch';
import OfflineWarning from '../OfflineWarning';
import Flash from '../Flash';
import Header from '../Header';
@@ -70,6 +71,7 @@ const propTypes = {
landingPage: PropTypes.bool,
navigationMenu: PropTypes.element,
onlineStatusChange: PropTypes.func.isRequired,
+ pathname: PropTypes.string.isRequired,
removeFlashMessage: PropTypes.func.isRequired,
showFooter: PropTypes.bool
};
@@ -92,30 +94,22 @@ const mapDispatchToProps = dispatch =>
);
class DefaultLayout extends Component {
- constructor(props) {
- super(props);
-
- this.location = '';
- }
-
componentDidMount() {
- if (!this.props.isSignedIn) {
- this.props.fetchUser();
+ const { isSignedIn, fetchUser, pathname } = this.props;
+ if (!isSignedIn) {
+ fetchUser();
}
- const url = window.location.pathname + window.location.search;
- ga.pageview(url);
+ ga.pageview(pathname);
window.addEventListener('online', this.updateOnlineStatus);
window.addEventListener('offline', this.updateOnlineStatus);
-
- this.location = url;
}
- componentDidUpdate() {
- const url = window.location.pathname + window.location.search;
- if (url !== this.location) {
- ga.pageview(url);
- this.location = url;
+ componentDidUpdate(prevProps) {
+ const { pathname } = this.props;
+ const { pathname: prevPathname } = prevProps;
+ if (pathname !== prevPathname) {
+ ga.pageview(pathname);
}
}
@@ -136,12 +130,13 @@ class DefaultLayout extends Component {
children,
hasMessages,
flashMessages = [],
- removeFlashMessage,
- landingPage,
- showFooter = true,
- navigationMenu,
isOnline,
- isSignedIn
+ isSignedIn,
+ landingPage,
+ navigationMenu,
+ pathname,
+ removeFlashMessage,
+ showFooter = true
} = this.props;
return (
@@ -158,14 +153,21 @@ class DefaultLayout extends Component {
>
-
-
-
- {hasMessages ? (
-
- ) : null}
- {children}
-
+
+
+
+
+ {hasMessages ? (
+
+ ) : null}
+ {children}
+
+
{showFooter && }
);
diff --git a/client/src/components/search/WithInstantSearch.js b/client/src/components/search/WithInstantSearch.js
new file mode 100644
index 0000000000..619e7c9787
--- /dev/null
+++ b/client/src/components/search/WithInstantSearch.js
@@ -0,0 +1,82 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { InstantSearch, Configure } from 'react-instantsearch-dom';
+
+import {
+ isSearchDropdownEnabledSelector,
+ searchQuerySelector,
+ toggleSearchDropdown,
+ updateSearchQuery
+} from './redux';
+
+import { createSelector } from 'reselect';
+
+const propTypes = {
+ children: PropTypes.any,
+ isDropdownEnabled: PropTypes.bool,
+ pathname: PropTypes.string.isRequired,
+ query: PropTypes.string,
+ toggleSearchDropdown: PropTypes.func.isRequired,
+ updateSearchQuery: PropTypes.func.isRequired
+};
+
+const mapStateToProps = createSelector(
+ searchQuerySelector,
+ isSearchDropdownEnabledSelector,
+ (query, isDropdownEnabled) => ({ query, isDropdownEnabled })
+);
+const mapDispatchToProps = {
+ toggleSearchDropdown,
+ updateSearchQuery
+};
+
+class WithInstantSearch extends Component {
+ componentDidMount() {
+ const { toggleSearchDropdown } = this.props;
+ toggleSearchDropdown(this.getSearchEnableDropdown());
+ }
+
+ componentDidUpdate(prevProps) {
+ const { pathname, toggleSearchDropdown, isDropdownEnabled } = this.props;
+ const { pathname: prevPathname } = prevProps;
+ const enableDropdown = this.getSearchEnableDropdown();
+ if (pathname !== prevPathname || isDropdownEnabled !== enableDropdown) {
+ toggleSearchDropdown(enableDropdown);
+ }
+ }
+
+ getSearchEnableDropdown = () => !this.props.pathname.startsWith('/search');
+
+ onSearchStateChange = ({ query }) => {
+ const { updateSearchQuery, query: propsQuery } = this.props;
+ if (propsQuery === query || typeof query === 'undefined') {
+ return null;
+ }
+ return updateSearchQuery(query);
+ };
+
+ render() {
+ const { query } = this.props;
+ return (
+
+
+ {this.props.children}
+
+ );
+ }
+}
+
+WithInstantSearch.displayName = 'WithInstantSearch';
+WithInstantSearch.propTypes = propTypes;
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(WithInstantSearch);
diff --git a/client/src/components/search/redux/index.js b/client/src/components/search/redux/index.js
new file mode 100644
index 0000000000..36ffa195b1
--- /dev/null
+++ b/client/src/components/search/redux/index.js
@@ -0,0 +1,59 @@
+import { createAction, handleActions } from 'redux-actions';
+
+import { createTypes } from '../../../utils/createTypes';
+
+export const ns = 'search';
+
+const initialState = {
+ query: '',
+ indexName: 'query_suggestions',
+ isSearchDropdownEnabled: true,
+ isSearchBarFocused: false
+};
+
+const types = createTypes(
+ [
+ 'toggleSearchDropdown',
+ 'toggleSearchFocused',
+ 'updateSearchIndexName',
+ 'updateSearchQuery'
+ ],
+ ns
+);
+
+export const toggleSearchDropdown = createAction(types.toggleSearchDropdown);
+export const toggleSearchFocused = createAction(types.toggleSearchFocused);
+export const updateSearchIndexName = createAction(types.updateSearchIndexName);
+export const updateSearchQuery = createAction(types.updateSearchQuery);
+
+export const isSearchDropdownEnabledSelector = state =>
+ state[ns].isSearchDropdownEnabled;
+export const isSearchBarFocusedSelector = state => state[ns].isSearchBarFocused;
+export const searchIndexNameSelector = state => state[ns].indexName;
+export const searchQuerySelector = state => state[ns].query;
+
+export const reducer = handleActions(
+ {
+ [types.toggleSearchDropdown]: (state, { payload }) => ({
+ ...state,
+ isSearchDropdownEnabled:
+ typeof payload === 'boolean' ? payload : !state.isSearchDropdownEnabled
+ }),
+ [types.toggleSearchFocused]: (state, { payload }) => ({
+ ...state,
+ isSearchBarFocused: payload
+ }),
+ [types.updateSearchIndexName]: (state, { payload }) => ({
+ ...state,
+ indexName: payload
+ }),
+ [types.updateSearchQuery]: (state, { payload }) => {
+ console.log('query payload', payload);
+ return {
+ ...state,
+ query: payload
+ };
+ }
+ },
+ initialState
+);
diff --git a/client/src/components/search/searchBar/SearchBar.js b/client/src/components/search/searchBar/SearchBar.js
new file mode 100644
index 0000000000..a1a15fb3b8
--- /dev/null
+++ b/client/src/components/search/searchBar/SearchBar.js
@@ -0,0 +1,107 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import { createSelector } from 'reselect';
+import { SearchBox } from 'react-instantsearch-dom';
+import { navigate } from 'gatsby';
+
+import SearchHits from './SearchHits';
+
+import './searchbar.css';
+import {
+ isSearchDropdownEnabledSelector,
+ isSearchBarFocusedSelector,
+ toggleSearchDropdown,
+ toggleSearchFocused
+} from '../redux';
+
+const propTypes = {
+ isDropdownEnabled: PropTypes.bool,
+ isSearchFocused: PropTypes.bool,
+ toggleSearchDropdown: PropTypes.func.isRequired,
+ toggleSearchFocused: PropTypes.func.isRequired
+};
+
+const mapStateToProps = createSelector(
+ isSearchDropdownEnabledSelector,
+ isSearchBarFocusedSelector,
+ (isDropdownEnabled, isSearchFocused) => ({
+ isDropdownEnabled,
+ isSearchFocused
+ })
+);
+
+const mapDispatchToProps = dispatch =>
+ bindActionCreators({ toggleSearchDropdown, toggleSearchFocused }, dispatch);
+
+const placeholder = 'Search 8,000+ lessons, articles, and videos';
+
+class FCCSearchBar extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ dropdownOverride: true
+ };
+ this.searchBarRef = React.createRef();
+ }
+
+ componentDidMount() {
+ const searchInput = document.querySelector('.ais-SearchBox-input');
+ searchInput.id = 'fcc_instantsearch';
+
+ document.addEventListener('click', this.handlePageClick);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('click', this.handlePageClick);
+ }
+
+ handlePageClick = e => {
+ const { toggleSearchFocused } = this.props;
+ const isSearchFocusedClick = this.searchBarRef.current.contains(e.target);
+ return toggleSearchFocused(isSearchFocusedClick);
+ };
+
+ handleSearch = e => {
+ e.preventDefault();
+ const { toggleSearchDropdown } = this.props;
+ // disable the search dropdown
+ toggleSearchDropdown(false);
+ return navigate('/search');
+ };
+
+ render() {
+ const { isDropdownEnabled, isSearchFocused } = this.props;
+ return (
+
+
+
+
+ {isDropdownEnabled && isSearchFocused && (
+
+ )}
+
+
+ );
+ }
+}
+
+FCCSearchBar.displayName = 'FCCSearchBar';
+FCCSearchBar.propTypes = propTypes;
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(FCCSearchBar);
diff --git a/client/src/components/search/searchBar/SearchHits.js b/client/src/components/search/searchBar/SearchHits.js
new file mode 100644
index 0000000000..22d26095bc
--- /dev/null
+++ b/client/src/components/search/searchBar/SearchHits.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import {
+ connectStateResults,
+ connectAutoComplete
+} from 'react-instantsearch-dom';
+import isEmpty from 'lodash/isEmpty';
+import Suggestion from './SearchSuggestion';
+
+const CustomHits = connectAutoComplete(
+ ({ hits, currentRefinement, handleSubmit }) => {
+ const defaultHit = [
+ {
+ objectID: `default-hit-${currentRefinement}`,
+ _highlightResult: {
+ query: {
+ value:
+ 'Search for "' +
+ currentRefinement +
+ '"'
+ }
+ }
+ }
+ ];
+ return (
+
+
+ {defaultHit.concat(hits).map(hit => (
+ -
+
+
+ ))}
+
+
+ );
+ }
+);
+
+const SearchHits = connectStateResults(({ handleSubmit, searchState }) => {
+ return isEmpty(searchState) || !searchState.query ? null : (
+
+ );
+});
+
+export default SearchHits;
diff --git a/client/src/components/search/searchBar/SearchSuggestion.js b/client/src/components/search/searchBar/SearchSuggestion.js
new file mode 100644
index 0000000000..4541a51faf
--- /dev/null
+++ b/client/src/components/search/searchBar/SearchSuggestion.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Highlight } from 'react-instantsearch-dom';
+import { isEmpty } from 'lodash';
+
+const Suggestion = ({ handleSubmit, hit }) => {
+ return isEmpty(hit) ? null : (
+
+
+ {hit.objectID.includes('default-hit-') ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+Suggestion.propTypes = {
+ handleSubmit: PropTypes.func.isRequired,
+ hit: PropTypes.object
+};
+
+export default Suggestion;
diff --git a/client/src/components/search/searchBar/searchbar.css b/client/src/components/search/searchBar/searchbar.css
new file mode 100644
index 0000000000..9d3ae09232
--- /dev/null
+++ b/client/src/components/search/searchBar/searchbar.css
@@ -0,0 +1,722 @@
+.ais-Breadcrumb-list,
+.ais-CurrentRefinements-list,
+.ais-HierarchicalMenu-list,
+.ais-Hits-list,
+.ais-Results-list,
+.ais-InfiniteHits-list,
+.ais-InfiniteResults-list,
+.ais-Menu-list,
+.ais-NumericMenu-list,
+.ais-Pagination-list,
+.ais-RatingMenu-list,
+.ais-RefinementList-list,
+.ais-ToggleRefinement-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+.ais-ClearRefinements-button,
+.ais-CurrentRefinements-delete,
+.ais-CurrentRefinements-reset,
+.ais-HierarchicalMenu-showMore,
+.ais-InfiniteHits-loadMore,
+.ais-InfiniteResults-loadMore,
+.ais-Menu-showMore,
+.ais-RangeInput-submit,
+.ais-RefinementList-showMore,
+.ais-SearchBox-submit,
+.ais-SearchBox-reset {
+ padding: 0;
+ overflow: visible;
+ font: inherit;
+ line-height: normal;
+ color: inherit;
+ background: none;
+ border: 0;
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.ais-ClearRefinements-button::-moz-focus-inner,
+.ais-CurrentRefinements-delete::-moz-focus-inner,
+.ais-CurrentRefinements-reset::-moz-focus-inner,
+.ais-HierarchicalMenu-showMore::-moz-focus-inner,
+.ais-InfiniteHits-loadMore::-moz-focus-inner,
+.ais-InfiniteResults-loadMore::-moz-focus-inner,
+.ais-Menu-showMore::-moz-focus-inner,
+.ais-RangeInput-submit::-moz-focus-inner,
+.ais-RefinementList-showMore::-moz-focus-inner,
+.ais-SearchBox-submit::-moz-focus-inner,
+.ais-SearchBox-reset::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+}
+.ais-ClearRefinements-button[disabled],
+.ais-CurrentRefinements-delete[disabled],
+.ais-CurrentRefinements-reset[disabled],
+.ais-HierarchicalMenu-showMore[disabled],
+.ais-InfiniteHits-loadMore[disabled],
+.ais-InfiniteResults-loadMore[disabled],
+.ais-Menu-showMore[disabled],
+.ais-RangeInput-submit[disabled],
+.ais-RefinementList-showMore[disabled],
+.ais-SearchBox-submit[disabled],
+.ais-SearchBox-reset[disabled] {
+ cursor: default;
+}
+.ais-Breadcrumb-list,
+.ais-Breadcrumb-item,
+.ais-Pagination-list,
+.ais-RangeInput-form,
+.ais-RatingMenu-link,
+.ais-PoweredBy {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+}
+.ais-HierarchicalMenu-list .ais-HierarchicalMenu-list {
+ margin-left: 1em;
+}
+.ais-PoweredBy-logo {
+ display: block;
+ width: 70px;
+ height: auto;
+}
+.ais-RatingMenu-starIcon {
+ display: block;
+ width: 20px;
+ height: 20px;
+}
+.ais-SearchBox-input::-ms-clear,
+.ais-SearchBox-input::-ms-reveal {
+ display: none;
+ width: 0;
+ height: 0;
+}
+.ais-SearchBox-input::-webkit-search-decoration,
+.ais-SearchBox-input::-webkit-search-cancel-button,
+.ais-SearchBox-input::-webkit-search-results-button,
+.ais-SearchBox-input::-webkit-search-results-decoration {
+ display: none;
+}
+.ais-RangeSlider .rheostat {
+ overflow: visible;
+ margin-top: 40px;
+ margin-bottom: 40px;
+}
+.ais-RangeSlider .rheostat-background {
+ height: 6px;
+ top: 0px;
+ width: 100%;
+}
+.ais-RangeSlider .rheostat-handle {
+ margin-left: -12px;
+ top: -7px;
+}
+.ais-RangeSlider .rheostat-background {
+ position: relative;
+ background-color: #ffffff;
+ border: 1px solid #aaa;
+}
+.ais-RangeSlider .rheostat-progress {
+ position: absolute;
+ top: 1px;
+ height: 4px;
+ background-color: #333;
+}
+.rheostat-handle {
+ position: relative;
+ z-index: 1;
+ width: 20px;
+ height: 20px;
+ background-color: #fff;
+ border: 1px solid #333;
+ border-radius: 50%;
+ cursor: -webkit-grab;
+ cursor: grab;
+}
+.rheostat-marker {
+ margin-left: -1px;
+ position: absolute;
+ width: 1px;
+ height: 5px;
+ background-color: #aaa;
+}
+.rheostat-marker--large {
+ height: 9px;
+}
+.rheostat-value {
+ margin-left: 50%;
+ padding-top: 15px;
+ position: absolute;
+ text-align: center;
+ -webkit-transform: translateX(-50%);
+ transform: translateX(-50%);
+}
+.rheostat-tooltip {
+ margin-left: 50%;
+ position: absolute;
+ top: -22px;
+ text-align: center;
+ -webkit-transform: translateX(-50%);
+ transform: translateX(-50%);
+}
+[class^='ais-'] {
+ font-size: 1rem;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+a[class^='ais-'] {
+ text-decoration: none;
+}
+.ais-Breadcrumb,
+.ais-ClearRefinements,
+.ais-CurrentRefinements,
+.ais-HierarchicalMenu,
+.ais-Hits,
+.ais-Results,
+.ais-HitsPerPage,
+.ais-ResultsPerPage,
+.ais-InfiniteHits,
+.ais-InfiniteResults,
+.ais-Menu,
+.ais-MenuSelect,
+.ais-NumericMenu,
+.ais-NumericSelector,
+.ais-Pagination,
+.ais-Panel,
+.ais-PoweredBy,
+.ais-RangeInput,
+.ais-RangeSlider,
+.ais-RatingMenu,
+.ais-RefinementList,
+.ais-SearchBox,
+.ais-SortBy,
+.ais-Stats,
+.ais-ToggleRefinement {
+ color: #3a4570;
+}
+.ais-Breadcrumb-item--selected,
+.ais-HierarchicalMenu-item--selected,
+.ais-Menu-item--selected {
+ font-weight: bold;
+}
+.ais-Breadcrumb-separator {
+ margin: 0 0.3em;
+ font-weight: normal;
+}
+.ais-Breadcrumb-link,
+.ais-HierarchicalMenu-link,
+.ais-Menu-link,
+.ais-Pagination-link,
+.ais-RatingMenu-link {
+ color: #0096db;
+ -webkit-transition: color 0.2s ease-out;
+ transition: color 0.2s ease-out;
+}
+.ais-Breadcrumb-link:hover,
+.ais-Breadcrumb-link:focus,
+.ais-HierarchicalMenu-link:hover,
+.ais-HierarchicalMenu-link:focus,
+.ais-Menu-link:hover,
+.ais-Menu-link:focus,
+.ais-Pagination-link:hover,
+.ais-Pagination-link:focus,
+.ais-RatingMenu-link:hover,
+.ais-RatingMenu-link:focus {
+ color: #0073a8;
+}
+.ais-ClearRefinements-button,
+.ais-CurrentRefinements-reset,
+.ais-HierarchicalMenu-showMore,
+.ais-InfiniteHits-loadMore,
+.ais-InfiniteResults-loadMore,
+.ais-Menu-showMore,
+.ais-RefinementList-showMore {
+ padding: 0.3rem 0.5rem;
+ font-size: 0.8rem;
+ color: #fff;
+ background-color: #0096db;
+ border-radius: 5px;
+ -webkit-transition: background-color 0.2s ease-out;
+ transition: background-color 0.2s ease-out;
+ outline: none;
+}
+.ais-ClearRefinements-button:hover,
+.ais-ClearRefinements-button:focus,
+.ais-CurrentRefinements-reset:hover,
+.ais-CurrentRefinements-reset:focus,
+.ais-HierarchicalMenu-showMore:hover,
+.ais-HierarchicalMenu-showMore:focus,
+.ais-InfiniteHits-loadMore:hover,
+.ais-InfiniteHits-loadMore:focus,
+.ais-InfiniteResults-loadMore:hover,
+.ais-InfiniteResults-loadMore:focus,
+.ais-Menu-showMore:hover,
+.ais-Menu-showMore:focus,
+.ais-RefinementList-showMore:hover,
+.ais-RefinementList-showMore:focus {
+ background-color: #0073a8;
+}
+.ais-ClearRefinements-button--disabled,
+.ais-HierarchicalMenu-showMore--disabled,
+.ais-InfiniteHits-loadMore--disabled,
+.ais-InfiniteResults-loadMore--disabled,
+.ais-Menu-showMore--disabled,
+.ais-RefinementList-showMore--disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+.ais-ClearRefinements-button--disabled:hover,
+.ais-ClearRefinements-button--disabled:focus,
+.ais-HierarchicalMenu-showMore--disabled:hover,
+.ais-HierarchicalMenu-showMore--disabled:focus,
+.ais-InfiniteHits-loadMore--disabled:hover,
+.ais-InfiniteHits-loadMore--disabled:focus,
+.ais-InfiniteResults-loadMore--disabled:hover,
+.ais-InfiniteResults-loadMore--disabled:focus,
+.ais-Menu-showMore--disabled:hover,
+.ais-Menu-showMore--disabled:focus,
+.ais-RefinementList-showMore--disabled:hover,
+.ais-RefinementList-showMore--disabled:focus {
+ background-color: #0096db;
+}
+.ais-CurrentRefinements {
+ margin-top: -0.3rem;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+}
+.ais-CurrentRefinements-list {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+}
+.ais-CurrentRefinements-item {
+ margin-right: 0.3rem;
+ margin-top: 0.3rem;
+ padding: 0.3rem 0.5rem;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ background-color: #495588;
+ border-radius: 5px;
+}
+.ais-CurrentRefinements-category {
+ margin-left: 0.3em;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+}
+.ais-CurrentRefinements-delete {
+ margin-left: 0.3rem;
+}
+.ais-CurrentRefinements-label,
+.ais-CurrentRefinements-categoryLabel,
+.ais-CurrentRefinements-delete {
+ white-space: nowrap;
+ font-size: 0.8rem;
+ color: #fff;
+}
+.ais-CurrentRefinements-reset {
+ margin-top: 0.3rem;
+ white-space: nowrap;
+}
+.ais-CurrentRefinements-reset + .ais-CurrentRefinements-list {
+ margin-left: 0.3rem;
+}
+.ais-HierarchicalMenu-link,
+.ais-Menu-link {
+ display: block;
+ line-height: 1.5;
+}
+.ais-HierarchicalMenu-list,
+.ais-Menu-list,
+.ais-NumericMenu-list,
+.ais-RatingMenu-list,
+.ais-RefinementList-list {
+ font-weight: normal;
+ line-height: 1.5;
+}
+.ais-HierarchicalMenu-link:after {
+ margin-left: 0.3em;
+ content: '';
+ width: 10px;
+ height: 10px;
+ display: none;
+ background-image: url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27%3E%3Cpath d=%27M7.33 24l-2.83-2.829 9.339-9.175-9.339-9.167 2.83-2.829 12.17 11.996z%27 fill%3D%22%233A4570%22 /%3E%3C/svg%3E');
+ background-size: 100% 100%;
+}
+.ais-HierarchicalMenu-item--parent > .ais-HierarchicalMenu-link:after {
+ display: inline-block;
+}
+.ais-HierarchicalMenu-item--selected > .ais-HierarchicalMenu-link:after {
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+.ais-CurrentRefinements-count,
+.ais-RatingMenu-count {
+ font-size: 0.8rem;
+}
+.ais-CurrentRefinements-count:before,
+.ais-RatingMenu-count:before {
+ content: '(';
+}
+.ais-CurrentRefinements-count:after,
+.ais-RatingMenu-count:after {
+ content: ')';
+}
+.ais-HierarchicalMenu-count,
+.ais-Menu-count,
+.ais-RefinementList-count,
+.ais-ToggleRefinement-count {
+ padding: 0.1rem 0.4rem;
+ font-size: 0.8rem;
+ color: #3a4570;
+ background-color: #dfe2ee;
+ border-radius: 8px;
+}
+.ais-HierarchicalMenu-showMore,
+.ais-Menu-showMore,
+.ais-RefinementList-showMore {
+ margin-top: 0.5rem;
+}
+.ais-Highlight-highlighted,
+.ais-Snippet-highlighted {
+ background-color: #ffc168;
+}
+.ais-InfiniteHits-list,
+.ais-InfiniteResults-list,
+.ais-Hits-list,
+.ais-Results-list {
+ margin-top: -1rem;
+ margin-left: -1rem;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+}
+.ais-Panel-body .ais-InfiniteHits-list,
+.ais-Panel-body .ais-InfiniteResults-list,
+.ais-Panel-body .ais-Hits-list,
+.ais-Panel-body .ais-Results-list {
+ margin: 0.5rem 0 0 -1rem;
+}
+.ais-InfiniteHits-item,
+.ais-InfiniteResults-item,
+.ais-Hits-item,
+.ais-Results-item {
+ margin-top: 1rem;
+ margin-left: 1rem;
+ padding: 1rem;
+ width: calc(25% - 1rem);
+ border: 1px solid #c4c8d8;
+ -webkit-box-shadow: 0 2px 5px 0px #e3e5ec;
+ box-shadow: 0 2px 5px 0px #e3e5ec;
+}
+.ais-Panel-body .ais-InfiniteHits-item,
+.ais-Panel-body .ais-InfiniteResults-item,
+.ais-Panel-body .ais-Hits-item,
+.ais-Panel-body .ais-Results-item {
+ margin: 0.5rem 0 0.5rem 1rem;
+}
+.ais-InfiniteHits-loadMore,
+.ais-InfiniteResults-loadMore {
+ margin-top: 1rem;
+}
+.ais-MenuSelect-select,
+.ais-NumericSelector-select,
+.ais-HitsPerPage-select,
+.ais-ResultsPerPage-select,
+.ais-SortBy-select {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ padding: 0.3rem 2rem 0.3rem 0.3rem;
+ background-color: #fff;
+ background-image: url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27%3E%3Cpath d=%27M0 7.33l2.829-2.83 9.175 9.339 9.167-9.339 2.829 2.83-11.996 12.17z%27 fill%3D%22%233A4570%22 /%3E%3C/svg%3E');
+ background-repeat: no-repeat;
+ background-size: 10px 10px;
+ background-position: 92% 50%;
+ border: 1px solid #c4c8d8;
+ border-radius: 5px;
+}
+.ais-Panel-header {
+ margin-bottom: 0.5rem;
+ padding-bottom: 0.5rem;
+ font-size: 0.8rem;
+ font-weight: bold;
+ text-transform: uppercase;
+ border-bottom: 1px solid #c4c8d8;
+}
+.ais-Panel-footer {
+ margin-top: 0.5rem;
+ font-size: 0.8rem;
+}
+.ais-RangeInput-input {
+ padding: 0 0.2rem;
+ width: 5rem;
+ height: 1.5rem;
+ line-height: 1.5rem;
+}
+.ais-RangeInput-separator {
+ margin: 0 0.3rem;
+}
+.ais-RangeInput-submit {
+ margin-left: 0.3rem;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ padding: 0 0.5rem;
+ height: 1.5rem;
+ line-height: 1.5rem;
+ font-size: 0.8rem;
+ color: #fff;
+ background-color: #0096db;
+ border: none;
+ border-radius: 5px;
+ -webkit-transition: 0.2s ease-out;
+ transition: 0.2s ease-out;
+ outline: none;
+}
+.ais-RangeInput-submit:hover,
+.ais-RangeInput-submit:focus {
+ background-color: #0073a8;
+}
+.ais-RatingMenu-count {
+ color: #3a4570;
+}
+.ais-Pagination-list {
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+}
+.ais-Pagination-item + .ais-Pagination-item {
+ margin-left: 0.3rem;
+}
+.ais-Pagination-link {
+ padding: 0.3rem 0.6rem;
+ display: block;
+ border: 1px solid #c4c8d8;
+ border-radius: 5px;
+ -webkit-transition: background-color 0.2s ease-out;
+ transition: background-color 0.2s ease-out;
+}
+.ais-Pagination-link:hover,
+.ais-Pagination-link:focus {
+ background-color: #e3e5ec;
+}
+.ais-Pagination-item--disabled .ais-Pagination-link {
+ opacity: 0.6;
+ cursor: not-allowed;
+ color: #a5abc4;
+}
+.ais-Pagination-item--disabled .ais-Pagination-link:hover,
+.ais-Pagination-item--disabled .ais-Pagination-link:focus {
+ color: #a5abc4;
+ background-color: #fff;
+}
+.ais-Pagination-item--selected .ais-Pagination-link {
+ color: #fff;
+ background-color: #0096db;
+ border-color: #0096db;
+}
+.ais-Pagination-item--selected .ais-Pagination-link:hover,
+.ais-Pagination-item--selected .ais-Pagination-link:focus {
+ color: #fff;
+}
+.ais-PoweredBy-text,
+.rheostat-tooltip,
+.rheostat-value,
+.ais-Stats-text {
+ font-size: 0.8rem;
+}
+.ais-PoweredBy-logo {
+ margin-left: 0.3rem;
+}
+.ais-RangeSlider .rheostat-progress {
+ background-color: #495588;
+}
+.ais-RangeSlider .rheostat-background {
+ border-color: #878faf;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.ais-RangeSlider .rheostat-handle {
+ border-color: #878faf;
+}
+.ais-RangeSlider .rheostat-marker {
+ background-color: #878faf;
+}
+.ais-Panel-body .ais-RangeSlider {
+ margin: 2rem 0;
+}
+.ais-RatingMenu-item--disabled .ais-RatingMenu-count,
+.ais-RatingMenu-item--disabled .ais-RatingMenu-label {
+ color: #c4c8d8;
+}
+.ais-RatingMenu-item--selected {
+ font-weight: bold;
+}
+.ais-RatingMenu-link {
+ line-height: 1.5;
+}
+.ais-RatingMenu-link > * + * {
+ margin-left: 0.3rem;
+}
+.ais-RatingMenu-starIcon {
+ position: relative;
+ top: -1px;
+ width: 15px;
+ fill: #ffc168;
+}
+.ais-RatingMenu-item--disabled .ais-RatingMenu-starIcon {
+ fill: #c4c8d8;
+}
+.ais-HierarchicalMenu-searchBox > *,
+.ais-Menu-searchBox > *,
+.ais-RefinementList-searchBox > * {
+ margin-bottom: 0.5rem;
+}
+.ais-SearchBox-form {
+ display: block;
+ position: relative;
+}
+.ais-SearchBox-input {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ padding: 0.3rem 1.7rem;
+ width: 100%;
+ position: relative;
+ background-color: #fff;
+ border: 1px solid #c4c8d8;
+ border-radius: 5px;
+}
+.ais-SearchBox-input::-webkit-input-placeholder {
+ color: #a5aed1;
+}
+.ais-SearchBox-input::-moz-placeholder {
+ color: #a5aed1;
+}
+.ais-SearchBox-input:-ms-input-placeholder {
+ color: #a5aed1;
+}
+.ais-SearchBox-input:-moz-placeholder {
+ color: #a5aed1;
+}
+.ais-SearchBox-submit,
+.ais-SearchBox-reset,
+.ais-SearchBox-loadingIndicator {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ position: absolute;
+ z-index: 1;
+ width: 20px;
+ height: 20px;
+ top: 50%;
+ right: 0.3rem;
+ -webkit-transform: translateY(-50%);
+ transform: translateY(-50%);
+}
+.ais-SearchBox-submit {
+ left: 0.3rem;
+}
+.ais-SearchBox-reset {
+ right: 0.3rem;
+}
+.ais-SearchBox-submitIcon,
+.ais-SearchBox-resetIcon,
+.ais-SearchBox-loadingIcon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ -webkit-transform: translateX(-50%) translateY(-50%);
+ transform: translateX(-50%) translateY(-50%);
+}
+.ais-SearchBox-submitIcon path,
+.ais-SearchBox-resetIcon path {
+ fill: #495588;
+}
+.ais-SearchBox-submitIcon {
+ width: 14px;
+ height: 14px;
+}
+.ais-SearchBox-resetIcon {
+ width: 12px;
+ height: 12px;
+}
+.ais-SearchBox-loadingIcon {
+ width: 16px;
+ height: 16px;
+}
+
+.ais-SearchBox-input {
+ padding-left: 25px;
+ font-size: 14px;
+}
+.ais-SearchBox-submitIcon > path {
+ fill: #006400;
+}
+.ais-Hits {
+ position: absolute;
+ width: 90%;
+ background-color: #fff;
+}
+.ais-Hits-item {
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+.ais-Hits-list {
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border: 1px solid #c4c8d8;
+ -webkit-box-shadow: 0 2px 5px 0px #e3e5ec;
+ box-shadow: 0 2px 5px 0px #e3e5ec;
+}
+strong.ais-Highlight-highlighted {
+ background-color: transparent;
+}
+.fcc_hits_wrapper {
+ display: flex;
+ justify-content: center;
+}
+.fcc_suggestion_item {
+ padding: 1rem;
+ color: #333;
+}
+.fcc_suggestion_item [class^='ais-'] {
+ font-size: 17px;
+}
+.fcc_suggestion_item:hover {
+ background-color: rgba(0, 100, 0, 0.4);
+ color: white;
+ cursor: pointer;
+}
+.fcc_sr_only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0;
+}
diff --git a/client/src/components/search/searchPage/EmptySearch.js b/client/src/components/search/searchPage/EmptySearch.js
new file mode 100644
index 0000000000..a6647dc912
--- /dev/null
+++ b/client/src/components/search/searchPage/EmptySearch.js
@@ -0,0 +1,11 @@
+import React from 'react';
+
+import './empty-search.css';
+
+function EmptySearch() {
+ return Empty Search
;
+}
+
+EmptySearch.displayName = 'EmptySearch';
+
+export default EmptySearch;
diff --git a/client/src/components/search/searchPage/NoResults.js b/client/src/components/search/searchPage/NoResults.js
new file mode 100644
index 0000000000..56df0ae46c
--- /dev/null
+++ b/client/src/components/search/searchPage/NoResults.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ query: PropTypes.string
+};
+
+function NoResults({ query }) {
+ return (
+
+
+ We could not find anything relating to {query}
+
+
+ );
+}
+
+NoResults.displayName = 'NoResults';
+NoResults.propTypes = propTypes;
+
+export default NoResults;
diff --git a/client/src/components/search/searchPage/SearchPageHits.js b/client/src/components/search/searchPage/SearchPageHits.js
new file mode 100644
index 0000000000..6bb807ed3d
--- /dev/null
+++ b/client/src/components/search/searchPage/SearchPageHits.js
@@ -0,0 +1,84 @@
+import React from 'react';
+
+import {
+ Highlight,
+ connectStateResults,
+ connectAutoComplete
+} from 'react-instantsearch-dom';
+import { isEmpty } from 'lodash';
+
+import EmptySearch from './EmptySearch';
+import NoResults from './NoResults';
+import { homeLocation } from '../../../../config/env.json';
+
+import './search-page-hits.css';
+
+const indexMap = {
+ challenges: {
+ title: 'Lesson',
+ url: `${homeLocation}/learn`
+ },
+ guides: {
+ title: 'Guide',
+ url: `${homeLocation}/guide`
+ },
+ youtube: {
+ title: 'YouTube',
+ url: 'https://www.youtube.com/watch?v='
+ }
+};
+
+const buildUrl = (index, result) =>
+ `${indexMap[index].url}${'videoId' in result ? result.videoId : result.url}`;
+
+const AllHits = connectAutoComplete(({ hits, currentRefinement }) => {
+ if (hits.some(hit => isEmpty(hit.index))) {
+ return null;
+ }
+ const nonQuerySuggestionHits = hits.filter(
+ ({ index }) => index !== 'query_suggestions'
+ );
+ const isHitsEmpty = nonQuerySuggestionHits.every(({ hits }) => !hits.length);
+
+ return currentRefinement && !isHitsEmpty ? (
+
+ ) : (
+
+ );
+});
+
+AllHits.displayName = 'AllHits';
+
+const SearchHits = connectStateResults(({ handleClick, searchState }) => {
+ const isSearchEmpty = isEmpty(searchState) || isEmpty(searchState.query);
+
+ return isSearchEmpty ? (
+
+ ) : (
+
+ );
+});
+
+SearchHits.displayName = 'SearchHits';
+
+export default SearchHits;
diff --git a/client/src/components/search/searchPage/empty-search.css b/client/src/components/search/searchPage/empty-search.css
new file mode 100644
index 0000000000..ce19aedfcc
--- /dev/null
+++ b/client/src/components/search/searchPage/empty-search.css
@@ -0,0 +1,6 @@
+.empty-search-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 80vh;
+}
diff --git a/client/src/components/search/searchPage/no-results.css b/client/src/components/search/searchPage/no-results.css
new file mode 100644
index 0000000000..339dc067d3
--- /dev/null
+++ b/client/src/components/search/searchPage/no-results.css
@@ -0,0 +1,6 @@
+.no-results-wrapper {
+ height: 80vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
diff --git a/client/src/components/search/searchPage/search-page-hits.css b/client/src/components/search/searchPage/search-page-hits.css
new file mode 100644
index 0000000000..d7f2bc0a61
--- /dev/null
+++ b/client/src/components/search/searchPage/search-page-hits.css
@@ -0,0 +1,40 @@
+.ais-Hits {
+ margin-top: 36px;
+ z-index: 100;
+ position: relative;
+ background: #fefefe;
+}
+
+.ais-Hits-list {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-left: 0;
+ border: 1px solid #efefef;
+}
+
+.ais-Hits-list > a {
+ width: 100%;
+ text-decoration: none;
+}
+
+.ais-Hits-list > a:nth-child(odd) {
+ background-color: #efefef;
+}
+
+.ais-Hits-item {
+ width: 100%;
+ margin: 0;
+ border-color: transparent;
+ color: #333;
+ padding: 15px;
+ box-shadow: none;
+}
+
+.ais-Hits-item:hover {
+ background-color: rgba(0, 100, 0, 0.4);
+ color: white;
+}
+.ais-Hits-item:hover em {
+ color: #333;
+}
diff --git a/client/src/pages/search.js b/client/src/pages/search.js
new file mode 100644
index 0000000000..6b661e7f68
--- /dev/null
+++ b/client/src/pages/search.js
@@ -0,0 +1,22 @@
+import React, { Fragment } from 'react';
+import { Index, PoweredBy } from 'react-instantsearch-dom';
+
+import SearchPageHits from '../components/search/searchPage/SearchPageHits';
+
+function SearchPage() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+SearchPage.displayName = 'SearchPage';
+
+export default SearchPage;
diff --git a/client/src/redux/rootReducer.js b/client/src/redux/rootReducer.js
index b0194993c1..cdfb0346fb 100644
--- a/client/src/redux/rootReducer.js
+++ b/client/src/redux/rootReducer.js
@@ -19,7 +19,20 @@ import {
reducer as challenge,
ns as challengeNameSpace
} from '../templates/Challenges/redux';
+import {
+ reducer as search,
+ ns as searchNameSpace
+} from '../components/search/redux';
+// console.log({
+// [appNameSpace]: app,
+// [challengeNameSpace]: challenge,
+// [curriculumMapNameSpace]: curriculumMap,
+// [flashNameSpace]: flash,
+// form: formReducer,
+// [searchNameSpace]: search,
+// [settingsNameSpace]: settings
+// });
export default combineReducers({
[appNameSpace]: app,
[challengeNameSpace]: challenge,
@@ -27,5 +40,6 @@ export default combineReducers({
[flashNameSpace]: flash,
[guideNavNameSpace]: guideNav,
form: formReducer,
+ [searchNameSpace]: search,
[settingsNameSpace]: settings
});