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 { createSelector } from 'reselect';
|
||||||
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
|
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
|
||||||
|
|
||||||
import WithInstantSearch from '../WithInstantSearch';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isSearchDropdownEnabledSelector,
|
isSearchDropdownEnabledSelector,
|
||||||
isSearchBarFocusedSelector,
|
isSearchBarFocusedSelector,
|
||||||
@ -18,6 +16,8 @@ import {
|
|||||||
toggleSearchFocused,
|
toggleSearchFocused,
|
||||||
updateSearchQuery
|
updateSearchQuery
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
|
import WithInstantSearch from '../with-instant-search';
|
||||||
|
|
||||||
import SearchHits from './search-hits';
|
import SearchHits from './search-hits';
|
||||||
|
|
||||||
import './searchbar-base.css';
|
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',
|
environment: process.env.FREECODECAMP_NODE_ENV || 'development',
|
||||||
algoliaAppId:
|
algoliaAppId:
|
||||||
!algoliaAppId || algoliaAppId === 'app_id_from_algolia_dashboard'
|
!algoliaAppId || algoliaAppId === 'app_id_from_algolia_dashboard'
|
||||||
? null
|
? ''
|
||||||
: algoliaAppId,
|
: algoliaAppId,
|
||||||
algoliaAPIKey:
|
algoliaAPIKey:
|
||||||
!algoliaAPIKey || algoliaAPIKey === 'api_key_from_algolia_dashboard'
|
!algoliaAPIKey || algoliaAPIKey === 'api_key_from_algolia_dashboard'
|
||||||
? null
|
? ''
|
||||||
: algoliaAPIKey,
|
: algoliaAPIKey,
|
||||||
paypalClientId:
|
paypalClientId:
|
||||||
!paypalClientId || paypalClientId === 'id_from_paypal_dashboard'
|
!paypalClientId || paypalClientId === 'id_from_paypal_dashboard'
|
||||||
|
Reference in New Issue
Block a user