feat(client): ts-migrate for files under client/src/component/search folder (#42327)

* typescript migration for files under search folder

* fixing few more eslint errors and warnings

* reverting changes for redux/index.js

* fixing merge conflicts with next

* deleting redux/index.ts
This commit is contained in:
krishna-saurav
2021-06-25 21:27:46 +05:30
committed by Mrugesh Mohapatra
parent 1e86063f04
commit 5ad374cc1a
12 changed files with 189 additions and 21906 deletions

21641
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,163 +0,0 @@
{
"name": "@freecodecamp/client",
"version": "0.0.1",
"description": "The freeCodeCamp.org open-source codebase and curriculum",
"license": "BSD-3-Clause",
"private": true,
"engines": {
"node": ">= 14.0.0",
"npm": "^6.14.12"
},
"repository": {
"type": "git",
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
},
"bugs": {
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
},
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
"author": "freeCodeCamp <team@freecodecamp.org>",
"main": "none",
"scripts": {
"prebuild": "node ../tools/scripts/build/ensure-env.js && npm run build:workers -- --env production",
"build": "node --max_old_space_size=7168 node_modules/gatsby-cli build --prefix-paths",
"build:workers": "node --max_old_space_size=7168 node_modules/webpack-cli/bin/cli --config ./webpack-workers.js",
"clean": "gatsby clean",
"predevelop": "node ../tools/scripts/build/ensure-env.js && npm run build:workers -- --env development",
"develop": "node --max_old_space_size=4000 node_modules/gatsby-cli develop --inspect=9230",
"format": "npm run format:gatsby && npm run format:src && npm run format:utils",
"format:gatsby": "prettier-eslint --write --trailing-comma none --single-quote './gatsby-*.js'",
"format:src": "prettier-eslint --write --trailing-comma none --single-quote './src/**/*.js'",
"format:utils": "prettier-eslint --write --trailing-comma none --single-quote './utils/**/*.js'",
"lint": "node ./i18n/schema-validation.js",
"serve": "gatsby serve -p 8000",
"prestand-alone": "npm run prebuild",
"stand-alone": "gatsby develop",
"validate-keys": "node ./i18n/validate-keys.js"
},
"dependencies": {
"@babel/plugin-proposal-export-default-from": "7.14.5",
"@babel/plugin-proposal-function-bind": "7.14.5",
"@babel/polyfill": "7.12.1",
"@babel/preset-env": "7.14.7",
"@babel/preset-react": "7.14.5",
"@babel/standalone": "7.14.7",
"@fortawesome/fontawesome": "1.1.8",
"@fortawesome/fontawesome-svg-core": "1.2.35",
"@fortawesome/free-brands-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14",
"@freecodecamp/loop-protect": "2.2.1",
"@freecodecamp/react-bootstrap": "0.32.3",
"@freecodecamp/react-calendar-heatmap": "1.0.0",
"@freecodecamp/strip-comments": "3.0.0",
"@loadable/component": "5.15.0",
"@reach/router": "1.3.4",
"algoliasearch": "4.10.2",
"assert": "2.0.0",
"babel-plugin-preval": "5.0.0",
"babel-plugin-prismjs": "2.0.1",
"bezier-easing": "2.1.0",
"browser-cookies": "1.2.0",
"buffer": "6.0.3",
"chai": "4.3.4",
"crypto-browserify": "3.12.0",
"csrf": "3.1.0",
"date-fns": "2.22.1",
"dedent": "0.7.0",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.6",
"final-form": "4.20.2",
"gatsby": "3.8.1",
"gatsby-cli": "3.8.0",
"gatsby-plugin-advanced-sitemap": "1.6.0",
"gatsby-plugin-create-client-paths": "3.8.0",
"gatsby-plugin-manifest": "3.8.0",
"gatsby-plugin-postcss": "4.8.0",
"gatsby-plugin-react-helmet": "4.8.0",
"gatsby-plugin-remove-serviceworker": "1.0.0",
"gatsby-remark-prismjs": "5.5.0",
"gatsby-source-filesystem": "3.8.0",
"gatsby-transformer-remark": "4.5.0",
"i18next": "20.3.2",
"jquery": "3.6.0",
"lodash": "4.17.21",
"lodash-es": "4.17.21",
"monaco-editor": "0.25.2",
"nanoid": "3.1.23",
"normalize-url": "4.5.1",
"path-browserify": "1.0.1",
"postcss": "8.3.5",
"prismjs": "1.24.0",
"process": "0.11.10",
"prop-types": "15.7.2",
"psl": "1.8.0",
"query-string": "7.0.1",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-final-form": "6.5.3",
"react-ga": "3.3.0",
"react-helmet": "6.1.0",
"react-hotkeys": "2.0.0",
"react-i18next": "11.11.0",
"react-instantsearch-dom": "6.11.2",
"react-lazy-load": "3.1.13",
"react-monaco-editor": "0.43.0",
"react-redux": "5.1.2",
"react-reflex": "4.0.1",
"react-responsive": "6.1.2",
"react-scrollable-anchor": "0.6.1",
"react-spinkit": "3.0.0",
"react-tooltip": "4.2.21",
"react-transition-group": "4.4.2",
"react-youtube": "7.13.1",
"redux": "4.1.0",
"redux-actions": "2.6.5",
"redux-devtools-extension": "2.13.9",
"redux-observable": "1.2.0",
"redux-saga": "1.1.3",
"reselect": "4.0.0",
"rxjs": "6.6.7",
"sanitize-html": "2.4.0",
"sass.js": "0.11.1",
"store": "2.0.12",
"stream-browserify": "3.0.0",
"typescript": "4.3.4",
"uuid": "8.3.2",
"validator": "13.6.0"
},
"devDependencies": {
"@babel/types": "7.14.5",
"@codesee/babel-plugin-instrument": "0.43.1",
"@codesee/tracker": "0.43.1",
"@testing-library/jest-dom": "5.14.1",
"@testing-library/react": "12.0.0",
"@types/jest": "^26.0.23",
"@types/loadable__component": "^5.13.3",
"@types/lodash-es": "^4.17.4",
"@types/react-dom": "^17.0.8",
"@types/react-helmet": "^6.1.1",
"@types/react-instantsearch-dom": "^6.10.0",
"@types/react-monaco-editor": "^0.16.0",
"@types/react-redux": "^7.1.16",
"@types/react-responsive": "^8.0.2",
"@types/react-spinkit": "^3.0.6",
"@types/react-test-renderer": "^17.0.1",
"@types/react-transition-group": "4.4.1",
"@types/redux-actions": "2.6.1",
"@types/sanitize-html": "^2.3.1",
"@types/store": "^2.0.2",
"@types/validator": "^13.1.4",
"autoprefixer": "10.2.6",
"babel-plugin-transform-imports": "2.0.0",
"chokidar": "3.5.2",
"copy-webpack-plugin": "9.0.1",
"gatsby-plugin-webpack-bundle-analyser-v2": "1.1.24",
"jest-json-schema-extended": "1.0.0",
"monaco-editor-webpack-plugin": "4.0.0",
"react-test-renderer": "16.14.0",
"redux-saga-test-plan": "4.0.1",
"webpack": "5.41.1",
"webpack-cli": "4.7.2"
}
}

View File

@ -8,14 +8,13 @@ import { Link, SkeletonSprite } from '../../helpers';
import NavLogo from './nav-logo';
import MenuButton from './menu-button';
import NavLinks from './nav-links';
import './universalNav.css';
import { isLanding } from '../../../utils/path-parsers';
import Loadable from '@loadable/component';
import './universal-nav.css';
const SearchBar = Loadable(() => import('../../search/searchBar/SearchBar'));
const SearchBarOptimized = Loadable(
() => import('../../search/searchBar/search-bar-optimized')
const SearchBar = Loadable(() => import('../../search/searchBar/search-bar'));
const SearchBarOptimized = Loadable(() =>
import('../../search/searchBar/search-bar-optimized')
);
export interface UniversalNavProps {

View File

@ -1,18 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const NoHitsSuggestion = ({ title }) => {
return (
<div className={'no-hits-footer fcc_suggestion_item'} role='region'>
<span className='hit-name'>{title}</span>
</div>
);
};
NoHitsSuggestion.propTypes = {
handleMouseEnter: PropTypes.func.isRequired,
handleMouseLeave: PropTypes.func.isRequired,
title: PropTypes.string
};
export default NoHitsSuggestion;

View File

@ -1,11 +1,11 @@
import React from 'react';
import ShallowRenderer from 'react-test-renderer/shallow';
import { SearchBar } from './SearchBar';
import { SearchBar } from './search-bar';
describe('<SearchBar />', () => {
it('renders to the DOM', () => {
const shallow = new ShallowRenderer();
const shallow = ShallowRenderer.createRenderer();
shallow.render(<SearchBar {...searchBarProps} />);
const result = shallow.getRenderOutput();
expect(result).toBeTruthy();

View File

@ -0,0 +1,17 @@
import React from 'react';
interface noHitsSuggestionPropType {
handleMouseEnter: (e: React.ChangeEvent<HTMLElement>) => void;
handleMouseLeave: (e: React.ChangeEvent<HTMLElement>) => void;
title: string;
}
const NoHitsSuggestion = ({ title }: noHitsSuggestionPropType) => {
return (
<div className={'no-hits-footer fcc_suggestion_item'} role='region'>
<span className='hit-name'>{title}</span>
</div>
);
};
export default NoHitsSuggestion;

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { AnyAction, bindActionCreators, Dispatch } from 'redux';
import { createSelector } from 'reselect';
import { SearchBox } from 'react-instantsearch-dom';
import { HotKeys, ObserveKeys } from 'react-hotkeys';
@ -11,6 +10,7 @@ import { searchPageUrl } from '../../../utils/algolia-locale-setup';
import WithInstantSearch from '../WithInstantSearch';
import { Hit } from 'react-instantsearch-core';
import {
isSearchDropdownEnabledSelector,
isSearchBarFocusedSelector,
@ -18,21 +18,12 @@ import {
toggleSearchFocused,
updateSearchQuery
} from '../redux';
import SearchHits from './SearchHits';
import SearchHits from './search-hits';
import './searchbar-base.css';
import './searchbar.css';
const propTypes = {
innerRef: PropTypes.object,
isDropdownEnabled: PropTypes.bool,
isSearchFocused: PropTypes.bool,
t: PropTypes.func.isRequired,
toggleSearchDropdown: PropTypes.func.isRequired,
toggleSearchFocused: PropTypes.func.isRequired,
updateSearchQuery: PropTypes.func.isRequired
};
const searchUrl: string = searchPageUrl as string;
const mapStateToProps = createSelector(
isSearchDropdownEnabledSelector,
isSearchBarFocusedSelector,
@ -42,14 +33,29 @@ const mapStateToProps = createSelector(
})
);
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) =>
bindActionCreators(
{ toggleSearchDropdown, toggleSearchFocused, updateSearchQuery },
dispatch
);
export class SearchBar extends Component {
constructor(props) {
type searchBarPropType = {
innerRef?: React.RefObject<HTMLDivElement>;
toggleSearchDropdown: typeof toggleSearchDropdown;
toggleSearchFocused: typeof toggleSearchFocused;
updateSearchQuery: typeof updateSearchQuery;
isDropdownEnabled?: boolean;
isSearchFocused?: boolean;
t?: (label: string) => string;
};
type classState = {
index: number;
hits: Array<Hit>;
};
export class SearchBar extends Component<searchBarPropType, classState> {
static displayName: string;
constructor(props: searchBarPropType) {
super(props);
this.handleChange = this.handleChange.bind(this);
@ -64,15 +70,15 @@ export class SearchBar extends Component {
};
}
componentDidMount() {
componentDidMount(): void {
document.addEventListener('click', this.handleFocus);
}
componentWillUnmount() {
componentWillUnmount(): void {
document.removeEventListener('click', this.handleFocus);
}
handleChange() {
handleChange = (): void => {
const { isSearchFocused, toggleSearchFocused } = this.props;
if (!isSearchFocused) {
toggleSearchFocused(true);
@ -81,20 +87,25 @@ export class SearchBar extends Component {
this.setState({
index: -1
});
}
};
handleFocus(e) {
handleFocus = (e: React.FocusEvent<Node> | Event): AnyAction | void => {
const { toggleSearchFocused } = this.props;
const isSearchFocused = this.props.innerRef.current.contains(e.target);
const isSearchFocused = this.props.innerRef?.current?.contains(
e.target as HTMLElement
);
if (!isSearchFocused) {
// Reset if user clicks outside of
// search bar / closes dropdown
this.setState({ index: -1 });
}
return toggleSearchFocused(isSearchFocused);
}
};
handleSearch(e, query) {
handleSearch = (
e: React.SyntheticEvent<HTMLFormElement, Event>,
query?: string
): boolean | void => {
e.preventDefault();
const { toggleSearchDropdown, updateSearchQuery } = this.props;
const { index, hits } = this.state;
@ -107,7 +118,7 @@ export class SearchBar extends Component {
return window.location.assign(selectedHit.url);
} else if (!query) {
// Set query to value in search bar if enter is pressed
query = e.currentTarget.children[0].value;
query = (e.currentTarget?.children?.[0] as HTMLInputElement).value;
}
updateSearchQuery(query);
@ -119,12 +130,12 @@ export class SearchBar extends Component {
// are hits besides the footer
return query && hits.length > 1
? window.location.assign(
`${searchPageUrl}?query=${encodeURIComponent(query)}`
`${searchUrl}?query=${encodeURIComponent(query)}`
)
: false;
}
};
handleMouseEnter(e) {
handleMouseEnter = (e: React.SyntheticEvent<HTMLElement, Event>): void => {
e.persist();
const hoveredText = e.currentTarget.innerText;
@ -134,15 +145,15 @@ export class SearchBar extends Component {
return { index: hoveredIndex };
});
}
};
handleMouseLeave() {
handleMouseLeave = (): void => {
this.setState({
index: -1
});
}
};
handleHits(currHits) {
handleHits = (currHits: Array<Hit>): void => {
const { hits } = this.state;
if (!isEqual(hits, currHits)) {
@ -151,7 +162,7 @@ export class SearchBar extends Component {
hits: currHits
});
}
}
};
keyMap = {
INDEX_UP: ['up'],
@ -159,14 +170,14 @@ export class SearchBar extends Component {
};
keyHandlers = {
INDEX_UP: e => {
e.preventDefault();
INDEX_UP: (e: KeyboardEvent | undefined): void => {
e?.preventDefault();
this.setState(({ index, hits }) => ({
index: index === -1 ? hits.length - 1 : index - 1
}));
},
INDEX_DOWN: e => {
e.preventDefault();
INDEX_DOWN: (e: KeyboardEvent | undefined): void => {
e?.preventDefault();
this.setState(({ index, hits }) => ({
index: index === hits.length - 1 ? -1 : index + 1
}));
@ -176,7 +187,7 @@ export class SearchBar extends Component {
render() {
const { isDropdownEnabled, isSearchFocused, innerRef, t } = this.props;
const { index } = this.state;
const placeholder = t('search.placeholder');
const placeholder = t ? t('search.placeholder') : '';
return (
<WithInstantSearch>
@ -188,17 +199,20 @@ export class SearchBar extends Component {
<HotKeys handlers={this.keyHandlers} keyMap={this.keyMap}>
<div className='fcc_search_wrapper'>
<label className='fcc_sr_only' htmlFor='fcc_instantsearch'>
{t('search.label')}
{t ? t('search.label') : ''}
</label>
<ObserveKeys except={['Space']}>
<SearchBox
focusShortcuts={[83, 191]}
onChange={this.handleChange}
onFocus={this.handleFocus}
onSubmit={this.handleSearch}
showLoadingIndicator={false}
translations={{ placeholder }}
/>
<div onFocus={this.handleFocus} role='textbox'>
<SearchBox
focusShortcuts={['83', '191']}
onChange={this.handleChange}
onSubmit={e => {
this.handleSearch(e);
}}
showLoadingIndicator={false}
translations={{ placeholder }}
/>
</div>
</ObserveKeys>
{isDropdownEnabled && isSearchFocused && (
<SearchHits
@ -217,8 +231,6 @@ export class SearchBar extends Component {
}
SearchBar.displayName = 'SearchBar';
SearchBar.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps

View File

@ -1,13 +1,28 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connectStateResults, connectHits } from 'react-instantsearch-dom';
import { SearchState, Hit } from 'react-instantsearch-core';
import { isEmpty } from 'lodash-es';
import { useTranslation } from 'react-i18next';
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
import Suggestion from './search-suggestion';
import NoHitsSuggestion from './no-hits-suggestion';
import Suggestion from './SearchSuggestion';
import NoHitsSuggestion from './NoHitsSuggestion';
const searchUrl = searchPageUrl as string;
interface customHitsPropTypes {
hits: Array<any>;
searchQuery: string;
handleMouseEnter: (e: React.SyntheticEvent<HTMLElement, Event>) => void;
handleMouseLeave: (e: React.SyntheticEvent<HTMLElement, Event>) => void;
selectedIndex: number;
handleHits: (currHits: Array<Hit>) => void;
}
interface searchHitsPropTypes {
searchState: SearchState;
handleMouseEnter: (e: React.SyntheticEvent<HTMLElement, Event>) => void;
handleMouseLeave: (e: React.SyntheticEvent<HTMLElement, Event>) => void;
selectedIndex: number;
handleHits: (currHits: Array<Hit>) => void;
}
const CustomHits = connectHits(
({
hits,
@ -16,7 +31,7 @@ const CustomHits = connectHits(
handleMouseLeave,
selectedIndex,
handleHits
}) => {
}: customHitsPropTypes) => {
const { t } = useTranslation();
const noHits = isEmpty(hits);
const noHitsTitle = t('search.no-tutorials');
@ -26,7 +41,7 @@ const CustomHits = connectHits(
query: searchQuery,
url: noHits
? null
: `${searchPageUrl}?query=${encodeURIComponent(searchQuery)}`,
: `${searchUrl}?query=${encodeURIComponent(searchQuery)}`,
title: t('search.see-results', { searchQuery: searchQuery }),
_highlightResult: {
query: {
@ -47,7 +62,7 @@ const CustomHits = connectHits(
return (
<div className='ais-Hits'>
<ul className='ais-Hits-list' data-cy='ais-Hits-list'>
{allHits.map((hit, i) => (
{allHits.map((hit: Hit, i: number) => (
<li
className={
!noHits && i === selectedIndex
@ -85,7 +100,7 @@ const SearchHits = connectStateResults(
handleMouseLeave,
selectedIndex,
handleHits
}) => {
}: searchHitsPropTypes) => {
return isEmpty(searchState) || !searchState.query ? null : (
<CustomHits
handleHits={handleHits}
@ -98,8 +113,4 @@ const SearchHits = connectStateResults(
}
);
CustomHits.propTypes = {
handleHits: PropTypes.func.isRequired
};
export default SearchHits;

View File

@ -1,8 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Highlight } from 'react-instantsearch-dom';
import { Hit } from 'react-instantsearch-core';
const Suggestion = ({ hit, handleMouseEnter, handleMouseLeave }) => {
interface suggestionPropTypes {
hit: Hit;
handleMouseEnter: (e: React.SyntheticEvent<HTMLElement, Event>) => void;
handleMouseLeave: (e: React.SyntheticEvent<HTMLElement, Event>) => void;
}
const Suggestion = ({
hit,
handleMouseEnter,
handleMouseLeave
}: suggestionPropTypes) => {
const dropdownFooter = hit.objectID.includes('footer-');
return (
<a
@ -28,10 +38,4 @@ const Suggestion = ({ hit, handleMouseEnter, handleMouseLeave }) => {
);
};
Suggestion.propTypes = {
handleMouseEnter: PropTypes.func.isRequired,
handleMouseLeave: PropTypes.func.isRequired,
hit: PropTypes.object
};
export default Suggestion;

View File

@ -1,16 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans } from 'react-i18next';
const propTypes = {
query: PropTypes.string
};
function NoResults({ query }) {
function NoResults({ query }: { query: string }) {
return (
<div className='no-results-wrapper'>
<p>
<Trans i18nKey='search.no-results' query={query}>
<Trans i18nKey='search.no-results' {...{ query: query }}>
<em>{{ query }}</em>
</Trans>
</p>
@ -19,6 +14,5 @@ function NoResults({ query }) {
}
NoResults.displayName = 'NoResults';
NoResults.propTypes = propTypes;
export default NoResults;

View File

@ -0,0 +1,68 @@
import React, { EventHandler, SyntheticEvent } from 'react';
import { AutocompleteExposed, SearchState } from 'react-instantsearch-core';
import {
Highlight,
connectStateResults,
connectAutoComplete
} from 'react-instantsearch-dom';
import { isEmpty } from 'lodash-es';
import EmptySearch from './empty-search';
import NoResults from './no-results';
import './search-page-hits.css';
type allHitType = {
handleClick?: EventHandler<SyntheticEvent>;
};
const AllHits: React.ComponentClass<AutocompleteExposed & allHitType, any> =
connectAutoComplete(({ hits, currentRefinement }) => {
const isHitsEmpty = !hits.length;
return currentRefinement && !isHitsEmpty ? (
<div className='ais-Hits search-page'>
<ul className='ais-Hits-list'>
{hits.map(result => (
<a
href={result.url}
key={result.objectID}
rel='noopener noreferrer'
target='_blank'
>
<li className='ais-Hits-item dataset-node'>
<p>
<Highlight attribute='title' hit={result} />
</p>
</li>
</a>
))}
</ul>
</div>
) : (
<NoResults query={currentRefinement} />
);
});
AllHits.displayName = 'AllHits';
const SearchHits = connectStateResults(
({
handleClick,
searchState
}: {
handleClick: EventHandler<SyntheticEvent>;
searchState: SearchState;
}) => {
const isSearchEmpty = isEmpty(searchState) || isEmpty(searchState.query);
return isSearchEmpty ? (
<EmptySearch />
) : (
<AllHits handleClick={handleClick} />
);
}
);
SearchHits.displayName = 'SearchHits';
export default SearchHits;