feat(client): ts-migrate WithInstantSearch.js (#42923)
* rename to TS * update import * migrate with-instant-search * change falsy values to empty string * update import order Finish resolving conflict from #41824 * update import order for search-bar * update setTimeout() type
This commit is contained in:
@ -1,177 +0,0 @@
|
||||
import { Location } from '@reach/router';
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import { navigate } from 'gatsby';
|
||||
import PropTypes from 'prop-types';
|
||||
import qs from 'query-string';
|
||||
import React, { Component } from 'react';
|
||||
import { InstantSearch, Configure } from 'react-instantsearch-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import Media from 'react-responsive';
|
||||
import { createSelector } from 'reselect';
|
||||
import envData from '../../../../config/env.json';
|
||||
import { newsIndex } from '../../utils/algolia-locale-setup';
|
||||
|
||||
import {
|
||||
isSearchDropdownEnabledSelector,
|
||||
searchQuerySelector,
|
||||
toggleSearchDropdown,
|
||||
updateSearchQuery
|
||||
} from './redux';
|
||||
|
||||
const { algoliaAppId, algoliaAPIKey } = envData;
|
||||
|
||||
const DEBOUNCE_TIME = 100;
|
||||
|
||||
// If a key is missing, searches will fail, but the client will still render.
|
||||
const searchClient =
|
||||
algoliaAppId && algoliaAPIKey
|
||||
? algoliasearch(algoliaAppId, algoliaAPIKey)
|
||||
: { search: () => {} };
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.any,
|
||||
isDropdownEnabled: PropTypes.bool,
|
||||
location: PropTypes.object.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
|
||||
};
|
||||
|
||||
const searchStateToUrl = ({ pathname }, query) =>
|
||||
`${pathname}${query ? `?${qs.stringify({ query })}` : ''}`;
|
||||
|
||||
const urlToSearchState = ({ search }) => qs.parse(search.slice(1));
|
||||
|
||||
class InstantSearchRoot extends Component {
|
||||
componentDidMount() {
|
||||
const { toggleSearchDropdown } = this.props;
|
||||
toggleSearchDropdown(!this.isSearchPage());
|
||||
this.setQueryFromURL();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location, toggleSearchDropdown, isDropdownEnabled } = this.props;
|
||||
|
||||
const enableDropdown = !this.isSearchPage();
|
||||
if (isDropdownEnabled !== enableDropdown) {
|
||||
toggleSearchDropdown(enableDropdown);
|
||||
}
|
||||
|
||||
if (location !== prevProps.location) {
|
||||
const { query, updateSearchQuery } = this.props;
|
||||
if (this.isSearchPage()) {
|
||||
if (
|
||||
location.state &&
|
||||
typeof location.state === 'object' &&
|
||||
location.state.hasOwnProperty('query')
|
||||
) {
|
||||
updateSearchQuery(location.state.query);
|
||||
} else if (location.search) {
|
||||
this.setQueryFromURL();
|
||||
} else {
|
||||
navigate(searchStateToUrl(this.props.location, query), {
|
||||
state: { query },
|
||||
replace: true
|
||||
});
|
||||
}
|
||||
} else if (query) {
|
||||
updateSearchQuery('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isSearchPage = () => this.props.location.pathname.startsWith('/search');
|
||||
|
||||
setQueryFromURL = () => {
|
||||
if (this.isSearchPage()) {
|
||||
const { updateSearchQuery, location, query } = this.props;
|
||||
const { query: queryFromURL } = urlToSearchState(location);
|
||||
if (query !== queryFromURL) {
|
||||
updateSearchQuery(queryFromURL);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onSearchStateChange = ({ query }) => {
|
||||
const { updateSearchQuery, query: propsQuery } = this.props;
|
||||
if (propsQuery === query || typeof query === 'undefined') {
|
||||
return;
|
||||
}
|
||||
updateSearchQuery(query);
|
||||
this.updateBrowserHistory(query);
|
||||
};
|
||||
|
||||
updateBrowserHistory = query => {
|
||||
if (this.isSearchPage()) {
|
||||
clearTimeout(this.debouncedSetState);
|
||||
|
||||
this.debouncedSetState = setTimeout(() => {
|
||||
if (this.isSearchPage()) {
|
||||
navigate(searchStateToUrl(this.props.location, query), {
|
||||
state: { query }
|
||||
});
|
||||
}
|
||||
}, DEBOUNCE_TIME);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { query } = this.props;
|
||||
const MAX_MOBILE_HEIGHT = 768;
|
||||
return (
|
||||
<InstantSearch
|
||||
indexName={newsIndex}
|
||||
onSearchStateChange={this.onSearchStateChange}
|
||||
searchClient={searchClient}
|
||||
searchState={{ query }}
|
||||
>
|
||||
{this.isSearchPage() ? (
|
||||
<Configure hitsPerPage={15} />
|
||||
) : (
|
||||
<>
|
||||
<Media maxHeight={MAX_MOBILE_HEIGHT}>
|
||||
<Configure hitsPerPage={5} />
|
||||
</Media>
|
||||
<Media minHeight={MAX_MOBILE_HEIGHT + 1}>
|
||||
<Configure hitsPerPage={8} />
|
||||
</Media>
|
||||
</>
|
||||
)}
|
||||
{this.props.children}
|
||||
</InstantSearch>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InstantSearchRoot.displayName = 'InstantSearchRoot';
|
||||
InstantSearchRoot.propTypes = propTypes;
|
||||
|
||||
const InstantSearchRootConnected = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(InstantSearchRoot);
|
||||
|
||||
const WithInstantSearch = ({ children }) => (
|
||||
<Location>
|
||||
{({ location }) => (
|
||||
<InstantSearchRootConnected location={location}>
|
||||
{children}
|
||||
</InstantSearchRootConnected>
|
||||
)}
|
||||
</Location>
|
||||
);
|
||||
|
||||
WithInstantSearch.displayName = 'WithInstantSearch';
|
||||
WithInstantSearch.propTypes = { children: PropTypes.any };
|
||||
|
||||
export default WithInstantSearch;
|
@ -9,8 +9,6 @@ import { AnyAction, bindActionCreators, Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
|
||||
|
||||
import WithInstantSearch from '../WithInstantSearch';
|
||||
|
||||
import {
|
||||
isSearchDropdownEnabledSelector,
|
||||
isSearchBarFocusedSelector,
|
||||
@ -18,6 +16,8 @@ import {
|
||||
toggleSearchFocused,
|
||||
updateSearchQuery
|
||||
} from '../redux';
|
||||
import WithInstantSearch from '../with-instant-search';
|
||||
|
||||
import SearchHits from './search-hits';
|
||||
|
||||
import './searchbar-base.css';
|
||||
|
204
client/src/components/search/with-instant-search.tsx
Normal file
204
client/src/components/search/with-instant-search.tsx
Normal file
@ -0,0 +1,204 @@
|
||||
import { Location } from '@reach/router';
|
||||
import type { WindowLocation } from '@reach/router';
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import { navigate } from 'gatsby';
|
||||
import qs from 'query-string';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { InstantSearch, Configure } from 'react-instantsearch-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import Media from 'react-responsive';
|
||||
import { createSelector } from 'reselect';
|
||||
import envData from '../../../../config/env.json';
|
||||
import { newsIndex } from '../../utils/algolia-locale-setup';
|
||||
|
||||
import {
|
||||
isSearchDropdownEnabledSelector,
|
||||
searchQuerySelector,
|
||||
toggleSearchDropdown,
|
||||
updateSearchQuery
|
||||
} from './redux';
|
||||
|
||||
const { algoliaAppId, algoliaAPIKey } = envData;
|
||||
|
||||
const DEBOUNCE_TIME = 100;
|
||||
|
||||
// If a key is missing, searches will fail, but the client will still render.
|
||||
const searchClient =
|
||||
algoliaAppId && algoliaAPIKey
|
||||
? algoliasearch(algoliaAppId, algoliaAPIKey)
|
||||
: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
search: () => {}
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
searchQuerySelector,
|
||||
isSearchDropdownEnabledSelector,
|
||||
function (
|
||||
query: string,
|
||||
isDropdownEnabled: boolean
|
||||
): { query: string; isDropdownEnabled: boolean } {
|
||||
return { query, isDropdownEnabled };
|
||||
}
|
||||
);
|
||||
const mapDispatchToProps = {
|
||||
toggleSearchDropdown,
|
||||
updateSearchQuery
|
||||
};
|
||||
|
||||
function searchStateToUrl(
|
||||
{ pathname }: { pathname: string },
|
||||
query: string
|
||||
): string {
|
||||
return `${pathname}${query ? `?${qs.stringify({ query })}` : ''}`;
|
||||
}
|
||||
function urlToSearchState({ search }: WindowLocation): { query: string } {
|
||||
return qs.parse(search.slice(1)) as { query: string };
|
||||
}
|
||||
|
||||
interface InstantSearchRootProps {
|
||||
isDropdownEnabled: boolean;
|
||||
location: WindowLocation<{ query: string }>;
|
||||
query: string;
|
||||
toggleSearchDropdown: (toggle: boolean) => void;
|
||||
updateSearchQuery: (query: string) => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
function InstantSearchRoot({
|
||||
isDropdownEnabled,
|
||||
location,
|
||||
query,
|
||||
toggleSearchDropdown,
|
||||
updateSearchQuery,
|
||||
children
|
||||
}: InstantSearchRootProps) {
|
||||
function isSearchPage(): boolean {
|
||||
return location.pathname.startsWith('/search');
|
||||
}
|
||||
|
||||
function setQueryFromURL(): void {
|
||||
if (isSearchPage()) {
|
||||
const { query: queryFromURL } = urlToSearchState(location);
|
||||
if (query !== queryFromURL) {
|
||||
updateSearchQuery(queryFromURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
toggleSearchDropdown(!isSearchPage());
|
||||
setQueryFromURL();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
|
||||
const prevLocationRef = useRef<InstantSearchRootProps['location']>();
|
||||
useEffect(() => {
|
||||
prevLocationRef.current = location;
|
||||
});
|
||||
const prevLocation = prevLocationRef.current;
|
||||
useEffect(() => {
|
||||
const enableDropdown = !isSearchPage();
|
||||
if (isDropdownEnabled !== enableDropdown) {
|
||||
toggleSearchDropdown(enableDropdown);
|
||||
}
|
||||
|
||||
if (location !== prevLocation) {
|
||||
if (isSearchPage()) {
|
||||
if (
|
||||
location.state &&
|
||||
typeof location.state === 'object' &&
|
||||
location.state.hasOwnProperty('query')
|
||||
) {
|
||||
updateSearchQuery(location.state.query);
|
||||
} else if (location.search) {
|
||||
setQueryFromURL();
|
||||
} else {
|
||||
void navigate(searchStateToUrl(location, query), {
|
||||
state: { query },
|
||||
replace: true
|
||||
});
|
||||
}
|
||||
} else if (query) {
|
||||
updateSearchQuery('');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const debouncedSetState = useRef<number>();
|
||||
function updateBrowserHistory(query: string): void {
|
||||
if (isSearchPage()) {
|
||||
clearTimeout(debouncedSetState.current);
|
||||
|
||||
debouncedSetState.current = window.setTimeout(() => {
|
||||
if (isSearchPage()) {
|
||||
void navigate(searchStateToUrl(location, query), {
|
||||
state: { query }
|
||||
});
|
||||
}
|
||||
}, DEBOUNCE_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
const propsQuery = query;
|
||||
function onSearchStateChange({ query }: { query: string | undefined }): void {
|
||||
if (propsQuery === query || typeof query === 'undefined') {
|
||||
return;
|
||||
}
|
||||
updateSearchQuery(query);
|
||||
updateBrowserHistory(query);
|
||||
}
|
||||
|
||||
const MAX_MOBILE_HEIGHT = 768;
|
||||
return (
|
||||
<InstantSearch
|
||||
indexName={newsIndex}
|
||||
onSearchStateChange={onSearchStateChange}
|
||||
searchClient={searchClient}
|
||||
searchState={{ query }}
|
||||
>
|
||||
{isSearchPage() ? (
|
||||
<Configure hitsPerPage={15} />
|
||||
) : (
|
||||
<>
|
||||
<Media maxHeight={MAX_MOBILE_HEIGHT}>
|
||||
<Configure hitsPerPage={5} />
|
||||
</Media>
|
||||
<Media minHeight={MAX_MOBILE_HEIGHT + 1}>
|
||||
<Configure hitsPerPage={8} />
|
||||
</Media>
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</InstantSearch>
|
||||
);
|
||||
}
|
||||
|
||||
InstantSearchRoot.displayName = 'InstantSearchRoot';
|
||||
|
||||
const InstantSearchRootConnected = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(InstantSearchRoot);
|
||||
|
||||
interface WithInstantSearchProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
function WithInstantSearch({ children }: WithInstantSearchProps): JSX.Element {
|
||||
return (
|
||||
<Location>
|
||||
{({ location }) => (
|
||||
<InstantSearchRootConnected
|
||||
location={location as InstantSearchRootProps['location']}
|
||||
>
|
||||
{children}
|
||||
</InstantSearchRootConnected>
|
||||
)}
|
||||
</Location>
|
||||
);
|
||||
}
|
||||
|
||||
WithInstantSearch.displayName = 'WithInstantSearch';
|
||||
|
||||
export default WithInstantSearch;
|
@ -46,11 +46,11 @@ module.exports = Object.assign(locations, {
|
||||
environment: process.env.FREECODECAMP_NODE_ENV || 'development',
|
||||
algoliaAppId:
|
||||
!algoliaAppId || algoliaAppId === 'app_id_from_algolia_dashboard'
|
||||
? null
|
||||
? ''
|
||||
: algoliaAppId,
|
||||
algoliaAPIKey:
|
||||
!algoliaAPIKey || algoliaAPIKey === 'api_key_from_algolia_dashboard'
|
||||
? null
|
||||
? ''
|
||||
: algoliaAPIKey,
|
||||
paypalClientId:
|
||||
!paypalClientId || paypalClientId === 'id_from_paypal_dashboard'
|
||||
|
Reference in New Issue
Block a user