feat: Port search from 'search-assets'
This commit is contained in:
@ -17,6 +17,7 @@ import { flashMessagesSelector, removeFlashMessage } from '../Flash/redux';
|
|||||||
|
|
||||||
import { isBrowser } from '../../../utils';
|
import { isBrowser } from '../../../utils';
|
||||||
|
|
||||||
|
import WithInstantSearch from '../search/WithInstantSearch';
|
||||||
import OfflineWarning from '../OfflineWarning';
|
import OfflineWarning from '../OfflineWarning';
|
||||||
import Flash from '../Flash';
|
import Flash from '../Flash';
|
||||||
import Header from '../Header';
|
import Header from '../Header';
|
||||||
@ -70,6 +71,7 @@ const propTypes = {
|
|||||||
landingPage: PropTypes.bool,
|
landingPage: PropTypes.bool,
|
||||||
navigationMenu: PropTypes.element,
|
navigationMenu: PropTypes.element,
|
||||||
onlineStatusChange: PropTypes.func.isRequired,
|
onlineStatusChange: PropTypes.func.isRequired,
|
||||||
|
pathname: PropTypes.string.isRequired,
|
||||||
removeFlashMessage: PropTypes.func.isRequired,
|
removeFlashMessage: PropTypes.func.isRequired,
|
||||||
showFooter: PropTypes.bool
|
showFooter: PropTypes.bool
|
||||||
};
|
};
|
||||||
@ -92,30 +94,22 @@ const mapDispatchToProps = dispatch =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
class DefaultLayout extends Component {
|
class DefaultLayout extends Component {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.location = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!this.props.isSignedIn) {
|
const { isSignedIn, fetchUser, pathname } = this.props;
|
||||||
this.props.fetchUser();
|
if (!isSignedIn) {
|
||||||
|
fetchUser();
|
||||||
}
|
}
|
||||||
const url = window.location.pathname + window.location.search;
|
ga.pageview(pathname);
|
||||||
ga.pageview(url);
|
|
||||||
|
|
||||||
window.addEventListener('online', this.updateOnlineStatus);
|
window.addEventListener('online', this.updateOnlineStatus);
|
||||||
window.addEventListener('offline', this.updateOnlineStatus);
|
window.addEventListener('offline', this.updateOnlineStatus);
|
||||||
|
|
||||||
this.location = url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate(prevProps) {
|
||||||
const url = window.location.pathname + window.location.search;
|
const { pathname } = this.props;
|
||||||
if (url !== this.location) {
|
const { pathname: prevPathname } = prevProps;
|
||||||
ga.pageview(url);
|
if (pathname !== prevPathname) {
|
||||||
this.location = url;
|
ga.pageview(pathname);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,12 +130,13 @@ class DefaultLayout extends Component {
|
|||||||
children,
|
children,
|
||||||
hasMessages,
|
hasMessages,
|
||||||
flashMessages = [],
|
flashMessages = [],
|
||||||
removeFlashMessage,
|
|
||||||
landingPage,
|
|
||||||
showFooter = true,
|
|
||||||
navigationMenu,
|
|
||||||
isOnline,
|
isOnline,
|
||||||
isSignedIn
|
isSignedIn,
|
||||||
|
landingPage,
|
||||||
|
navigationMenu,
|
||||||
|
pathname,
|
||||||
|
removeFlashMessage,
|
||||||
|
showFooter = true
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@ -158,14 +153,21 @@ class DefaultLayout extends Component {
|
|||||||
>
|
>
|
||||||
<style>{fontawesome.dom.css()}</style>
|
<style>{fontawesome.dom.css()}</style>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Header disableSettings={landingPage} navigationMenu={navigationMenu} />
|
<WithInstantSearch pathname={pathname}>
|
||||||
<div className={`default-layout ${landingPage ? 'landing-page' : ''}`}>
|
<Header
|
||||||
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
|
disableSettings={landingPage}
|
||||||
{hasMessages ? (
|
navigationMenu={navigationMenu}
|
||||||
<Flash messages={flashMessages} onClose={removeFlashMessage} />
|
/>
|
||||||
) : null}
|
<div
|
||||||
{children}
|
className={`default-layout ${landingPage ? 'landing-page' : ''}`}
|
||||||
</div>
|
>
|
||||||
|
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
|
||||||
|
{hasMessages ? (
|
||||||
|
<Flash messages={flashMessages} onClose={removeFlashMessage} />
|
||||||
|
) : null}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</WithInstantSearch>
|
||||||
{showFooter && <Footer />}
|
{showFooter && <Footer />}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
82
client/src/components/search/WithInstantSearch.js
Normal file
82
client/src/components/search/WithInstantSearch.js
Normal file
@ -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 (
|
||||||
|
<InstantSearch
|
||||||
|
apiKey='4318af87aa3ce128708f1153556c6108'
|
||||||
|
appId='QMJYL5WYTI'
|
||||||
|
indexName='query_suggestions'
|
||||||
|
onSearchStateChange={this.onSearchStateChange}
|
||||||
|
searchState={{ query }}
|
||||||
|
>
|
||||||
|
<Configure hitsPerPage={8} />
|
||||||
|
{this.props.children}
|
||||||
|
</InstantSearch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WithInstantSearch.displayName = 'WithInstantSearch';
|
||||||
|
WithInstantSearch.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(WithInstantSearch);
|
59
client/src/components/search/redux/index.js
Normal file
59
client/src/components/search/redux/index.js
Normal file
@ -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
|
||||||
|
);
|
107
client/src/components/search/searchBar/SearchBar.js
Normal file
107
client/src/components/search/searchBar/SearchBar.js
Normal file
@ -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 (
|
||||||
|
<div
|
||||||
|
className='fcc_searchBar'
|
||||||
|
data-testid='fcc_searchBar'
|
||||||
|
ref={this.searchBarRef}
|
||||||
|
>
|
||||||
|
<div className='fcc_search_wrapper'>
|
||||||
|
<label className='fcc_sr_only' htmlFor='fcc_instantsearch'>
|
||||||
|
Search
|
||||||
|
</label>
|
||||||
|
<SearchBox
|
||||||
|
onSubmit={this.handleSearch}
|
||||||
|
showLoadingIndicator={true}
|
||||||
|
translations={{ placeholder }}
|
||||||
|
/>
|
||||||
|
{isDropdownEnabled && isSearchFocused && (
|
||||||
|
<SearchHits handleSubmit={this.handleSearch} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FCCSearchBar.displayName = 'FCCSearchBar';
|
||||||
|
FCCSearchBar.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(FCCSearchBar);
|
48
client/src/components/search/searchBar/SearchHits.js
Normal file
48
client/src/components/search/searchBar/SearchHits.js
Normal file
@ -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 "<ais-highlight-0000000000>' +
|
||||||
|
currentRefinement +
|
||||||
|
'</ais-highlight-0000000000>"'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className='ais-Hits'>
|
||||||
|
<ul className='ais-Hits-list'>
|
||||||
|
{defaultHit.concat(hits).map(hit => (
|
||||||
|
<li
|
||||||
|
className='ais-Hits-item'
|
||||||
|
data-fccobjectid={hit.objectID}
|
||||||
|
key={hit.objectID}
|
||||||
|
>
|
||||||
|
<Suggestion handleSubmit={handleSubmit} hit={hit} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const SearchHits = connectStateResults(({ handleSubmit, searchState }) => {
|
||||||
|
return isEmpty(searchState) || !searchState.query ? null : (
|
||||||
|
<CustomHits handleSubmit={handleSubmit} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SearchHits;
|
25
client/src/components/search/searchBar/SearchSuggestion.js
Normal file
25
client/src/components/search/searchBar/SearchSuggestion.js
Normal file
@ -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 : (
|
||||||
|
<div className='fcc_suggestion_item' onClickCapture={handleSubmit}>
|
||||||
|
<span className='hit-name'>
|
||||||
|
{hit.objectID.includes('default-hit-') ? (
|
||||||
|
<Highlight attribute='query' hit={hit} tagName='strong' />
|
||||||
|
) : (
|
||||||
|
<Highlight attribute='query' hit={hit} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Suggestion.propTypes = {
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
hit: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Suggestion;
|
722
client/src/components/search/searchBar/searchbar.css
Normal file
722
client/src/components/search/searchBar/searchbar.css
Normal file
@ -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;
|
||||||
|
}
|
11
client/src/components/search/searchPage/EmptySearch.js
Normal file
11
client/src/components/search/searchPage/EmptySearch.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './empty-search.css';
|
||||||
|
|
||||||
|
function EmptySearch() {
|
||||||
|
return <div className='empty-search-wrapper'>Empty Search</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
EmptySearch.displayName = 'EmptySearch';
|
||||||
|
|
||||||
|
export default EmptySearch;
|
21
client/src/components/search/searchPage/NoResults.js
Normal file
21
client/src/components/search/searchPage/NoResults.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
query: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
function NoResults({ query }) {
|
||||||
|
return (
|
||||||
|
<div className='no-results-wrapper'>
|
||||||
|
<p>
|
||||||
|
We could not find anything relating to <em>{query}</em>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoResults.displayName = 'NoResults';
|
||||||
|
NoResults.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default NoResults;
|
84
client/src/components/search/searchPage/SearchPageHits.js
Normal file
84
client/src/components/search/searchPage/SearchPageHits.js
Normal file
@ -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 ? (
|
||||||
|
<div className='ais-Hits'>
|
||||||
|
<ul className='ais-Hits-list'>
|
||||||
|
{nonQuerySuggestionHits.map(({ hits: results, index }) =>
|
||||||
|
results.map(result => (
|
||||||
|
<a
|
||||||
|
href={buildUrl(index, result)}
|
||||||
|
key={result.objectID}
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
<li className='ais-Hits-item dataset-node'>
|
||||||
|
<p>
|
||||||
|
<strong>{indexMap[index].title}:</strong>
|
||||||
|
|
||||||
|
<Highlight attribute='title' hit={result} />
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<NoResults query={currentRefinement} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
AllHits.displayName = 'AllHits';
|
||||||
|
|
||||||
|
const SearchHits = connectStateResults(({ handleClick, searchState }) => {
|
||||||
|
const isSearchEmpty = isEmpty(searchState) || isEmpty(searchState.query);
|
||||||
|
|
||||||
|
return isSearchEmpty ? (
|
||||||
|
<EmptySearch />
|
||||||
|
) : (
|
||||||
|
<AllHits handleClick={handleClick} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
SearchHits.displayName = 'SearchHits';
|
||||||
|
|
||||||
|
export default SearchHits;
|
6
client/src/components/search/searchPage/empty-search.css
Normal file
6
client/src/components/search/searchPage/empty-search.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.empty-search-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 80vh;
|
||||||
|
}
|
6
client/src/components/search/searchPage/no-results.css
Normal file
6
client/src/components/search/searchPage/no-results.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.no-results-wrapper {
|
||||||
|
height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
40
client/src/components/search/searchPage/search-page-hits.css
Normal file
40
client/src/components/search/searchPage/search-page-hits.css
Normal file
@ -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;
|
||||||
|
}
|
22
client/src/pages/search.js
Normal file
22
client/src/pages/search.js
Normal file
@ -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 (
|
||||||
|
<Fragment>
|
||||||
|
<Index indexName='challenges' />
|
||||||
|
<Index indexName='guides' />
|
||||||
|
<Index indexName='youtube' />
|
||||||
|
<main>
|
||||||
|
<SearchPageHits />
|
||||||
|
</main>
|
||||||
|
<PoweredBy />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchPage.displayName = 'SearchPage';
|
||||||
|
|
||||||
|
export default SearchPage;
|
@ -19,7 +19,20 @@ import {
|
|||||||
reducer as challenge,
|
reducer as challenge,
|
||||||
ns as challengeNameSpace
|
ns as challengeNameSpace
|
||||||
} from '../templates/Challenges/redux';
|
} 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({
|
export default combineReducers({
|
||||||
[appNameSpace]: app,
|
[appNameSpace]: app,
|
||||||
[challengeNameSpace]: challenge,
|
[challengeNameSpace]: challenge,
|
||||||
@ -27,5 +40,6 @@ export default combineReducers({
|
|||||||
[flashNameSpace]: flash,
|
[flashNameSpace]: flash,
|
||||||
[guideNavNameSpace]: guideNav,
|
[guideNavNameSpace]: guideNav,
|
||||||
form: formReducer,
|
form: formReducer,
|
||||||
|
[searchNameSpace]: search,
|
||||||
[settingsNameSpace]: settings
|
[settingsNameSpace]: settings
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user