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:
committed by
Mrugesh Mohapatra
parent
1e86063f04
commit
5ad374cc1a
21641
client/package-lock.json
generated
21641
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,14 +8,13 @@ import { Link, SkeletonSprite } from '../../helpers';
|
|||||||
import NavLogo from './nav-logo';
|
import NavLogo from './nav-logo';
|
||||||
import MenuButton from './menu-button';
|
import MenuButton from './menu-button';
|
||||||
import NavLinks from './nav-links';
|
import NavLinks from './nav-links';
|
||||||
|
import './universalNav.css';
|
||||||
import { isLanding } from '../../../utils/path-parsers';
|
import { isLanding } from '../../../utils/path-parsers';
|
||||||
import Loadable from '@loadable/component';
|
import Loadable from '@loadable/component';
|
||||||
|
|
||||||
import './universal-nav.css';
|
const SearchBar = Loadable(() => import('../../search/searchBar/search-bar'));
|
||||||
|
const SearchBarOptimized = Loadable(() =>
|
||||||
const SearchBar = Loadable(() => import('../../search/searchBar/SearchBar'));
|
import('../../search/searchBar/search-bar-optimized')
|
||||||
const SearchBarOptimized = Loadable(
|
|
||||||
() => import('../../search/searchBar/search-bar-optimized')
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface UniversalNavProps {
|
export interface UniversalNavProps {
|
||||||
|
@ -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;
|
|
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ShallowRenderer from 'react-test-renderer/shallow';
|
import ShallowRenderer from 'react-test-renderer/shallow';
|
||||||
|
|
||||||
import { SearchBar } from './SearchBar';
|
import { SearchBar } from './search-bar';
|
||||||
|
|
||||||
describe('<SearchBar />', () => {
|
describe('<SearchBar />', () => {
|
||||||
it('renders to the DOM', () => {
|
it('renders to the DOM', () => {
|
||||||
const shallow = new ShallowRenderer();
|
const shallow = ShallowRenderer.createRenderer();
|
||||||
shallow.render(<SearchBar {...searchBarProps} />);
|
shallow.render(<SearchBar {...searchBarProps} />);
|
||||||
const result = shallow.getRenderOutput();
|
const result = shallow.getRenderOutput();
|
||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
@ -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;
|
@ -1,7 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { AnyAction, bindActionCreators, Dispatch } from 'redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { SearchBox } from 'react-instantsearch-dom';
|
import { SearchBox } from 'react-instantsearch-dom';
|
||||||
import { HotKeys, ObserveKeys } from 'react-hotkeys';
|
import { HotKeys, ObserveKeys } from 'react-hotkeys';
|
||||||
@ -11,6 +10,7 @@ import { searchPageUrl } from '../../../utils/algolia-locale-setup';
|
|||||||
|
|
||||||
import WithInstantSearch from '../WithInstantSearch';
|
import WithInstantSearch from '../WithInstantSearch';
|
||||||
|
|
||||||
|
import { Hit } from 'react-instantsearch-core';
|
||||||
import {
|
import {
|
||||||
isSearchDropdownEnabledSelector,
|
isSearchDropdownEnabledSelector,
|
||||||
isSearchBarFocusedSelector,
|
isSearchBarFocusedSelector,
|
||||||
@ -18,21 +18,12 @@ import {
|
|||||||
toggleSearchFocused,
|
toggleSearchFocused,
|
||||||
updateSearchQuery
|
updateSearchQuery
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
import SearchHits from './SearchHits';
|
import SearchHits from './search-hits';
|
||||||
|
|
||||||
import './searchbar-base.css';
|
import './searchbar-base.css';
|
||||||
import './searchbar.css';
|
import './searchbar.css';
|
||||||
|
|
||||||
const propTypes = {
|
const searchUrl: string = searchPageUrl as string;
|
||||||
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 mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
isSearchDropdownEnabledSelector,
|
isSearchDropdownEnabledSelector,
|
||||||
isSearchBarFocusedSelector,
|
isSearchBarFocusedSelector,
|
||||||
@ -42,14 +33,29 @@ const mapStateToProps = createSelector(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
{ toggleSearchDropdown, toggleSearchFocused, updateSearchQuery },
|
{ toggleSearchDropdown, toggleSearchFocused, updateSearchQuery },
|
||||||
dispatch
|
dispatch
|
||||||
);
|
);
|
||||||
|
|
||||||
export class SearchBar extends Component {
|
type searchBarPropType = {
|
||||||
constructor(props) {
|
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);
|
super(props);
|
||||||
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
@ -64,15 +70,15 @@ export class SearchBar extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount(): void {
|
||||||
document.addEventListener('click', this.handleFocus);
|
document.addEventListener('click', this.handleFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount(): void {
|
||||||
document.removeEventListener('click', this.handleFocus);
|
document.removeEventListener('click', this.handleFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange() {
|
handleChange = (): void => {
|
||||||
const { isSearchFocused, toggleSearchFocused } = this.props;
|
const { isSearchFocused, toggleSearchFocused } = this.props;
|
||||||
if (!isSearchFocused) {
|
if (!isSearchFocused) {
|
||||||
toggleSearchFocused(true);
|
toggleSearchFocused(true);
|
||||||
@ -81,20 +87,25 @@ export class SearchBar extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
index: -1
|
index: -1
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
handleFocus(e) {
|
handleFocus = (e: React.FocusEvent<Node> | Event): AnyAction | void => {
|
||||||
const { toggleSearchFocused } = this.props;
|
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) {
|
if (!isSearchFocused) {
|
||||||
// Reset if user clicks outside of
|
// Reset if user clicks outside of
|
||||||
// search bar / closes dropdown
|
// search bar / closes dropdown
|
||||||
this.setState({ index: -1 });
|
this.setState({ index: -1 });
|
||||||
}
|
}
|
||||||
return toggleSearchFocused(isSearchFocused);
|
return toggleSearchFocused(isSearchFocused);
|
||||||
}
|
};
|
||||||
|
|
||||||
handleSearch(e, query) {
|
handleSearch = (
|
||||||
|
e: React.SyntheticEvent<HTMLFormElement, Event>,
|
||||||
|
query?: string
|
||||||
|
): boolean | void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { toggleSearchDropdown, updateSearchQuery } = this.props;
|
const { toggleSearchDropdown, updateSearchQuery } = this.props;
|
||||||
const { index, hits } = this.state;
|
const { index, hits } = this.state;
|
||||||
@ -107,7 +118,7 @@ export class SearchBar extends Component {
|
|||||||
return window.location.assign(selectedHit.url);
|
return window.location.assign(selectedHit.url);
|
||||||
} else if (!query) {
|
} else if (!query) {
|
||||||
// Set query to value in search bar if enter is pressed
|
// 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);
|
updateSearchQuery(query);
|
||||||
|
|
||||||
@ -119,12 +130,12 @@ export class SearchBar extends Component {
|
|||||||
// are hits besides the footer
|
// are hits besides the footer
|
||||||
return query && hits.length > 1
|
return query && hits.length > 1
|
||||||
? window.location.assign(
|
? window.location.assign(
|
||||||
`${searchPageUrl}?query=${encodeURIComponent(query)}`
|
`${searchUrl}?query=${encodeURIComponent(query)}`
|
||||||
)
|
)
|
||||||
: false;
|
: false;
|
||||||
}
|
};
|
||||||
|
|
||||||
handleMouseEnter(e) {
|
handleMouseEnter = (e: React.SyntheticEvent<HTMLElement, Event>): void => {
|
||||||
e.persist();
|
e.persist();
|
||||||
const hoveredText = e.currentTarget.innerText;
|
const hoveredText = e.currentTarget.innerText;
|
||||||
|
|
||||||
@ -134,15 +145,15 @@ export class SearchBar extends Component {
|
|||||||
|
|
||||||
return { index: hoveredIndex };
|
return { index: hoveredIndex };
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
handleMouseLeave() {
|
handleMouseLeave = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
index: -1
|
index: -1
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
handleHits(currHits) {
|
handleHits = (currHits: Array<Hit>): void => {
|
||||||
const { hits } = this.state;
|
const { hits } = this.state;
|
||||||
|
|
||||||
if (!isEqual(hits, currHits)) {
|
if (!isEqual(hits, currHits)) {
|
||||||
@ -151,7 +162,7 @@ export class SearchBar extends Component {
|
|||||||
hits: currHits
|
hits: currHits
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
keyMap = {
|
keyMap = {
|
||||||
INDEX_UP: ['up'],
|
INDEX_UP: ['up'],
|
||||||
@ -159,14 +170,14 @@ export class SearchBar extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
keyHandlers = {
|
keyHandlers = {
|
||||||
INDEX_UP: e => {
|
INDEX_UP: (e: KeyboardEvent | undefined): void => {
|
||||||
e.preventDefault();
|
e?.preventDefault();
|
||||||
this.setState(({ index, hits }) => ({
|
this.setState(({ index, hits }) => ({
|
||||||
index: index === -1 ? hits.length - 1 : index - 1
|
index: index === -1 ? hits.length - 1 : index - 1
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
INDEX_DOWN: e => {
|
INDEX_DOWN: (e: KeyboardEvent | undefined): void => {
|
||||||
e.preventDefault();
|
e?.preventDefault();
|
||||||
this.setState(({ index, hits }) => ({
|
this.setState(({ index, hits }) => ({
|
||||||
index: index === hits.length - 1 ? -1 : index + 1
|
index: index === hits.length - 1 ? -1 : index + 1
|
||||||
}));
|
}));
|
||||||
@ -176,7 +187,7 @@ export class SearchBar extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const { isDropdownEnabled, isSearchFocused, innerRef, t } = this.props;
|
const { isDropdownEnabled, isSearchFocused, innerRef, t } = this.props;
|
||||||
const { index } = this.state;
|
const { index } = this.state;
|
||||||
const placeholder = t('search.placeholder');
|
const placeholder = t ? t('search.placeholder') : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WithInstantSearch>
|
<WithInstantSearch>
|
||||||
@ -188,17 +199,20 @@ export class SearchBar extends Component {
|
|||||||
<HotKeys handlers={this.keyHandlers} keyMap={this.keyMap}>
|
<HotKeys handlers={this.keyHandlers} keyMap={this.keyMap}>
|
||||||
<div className='fcc_search_wrapper'>
|
<div className='fcc_search_wrapper'>
|
||||||
<label className='fcc_sr_only' htmlFor='fcc_instantsearch'>
|
<label className='fcc_sr_only' htmlFor='fcc_instantsearch'>
|
||||||
{t('search.label')}
|
{t ? t('search.label') : ''}
|
||||||
</label>
|
</label>
|
||||||
<ObserveKeys except={['Space']}>
|
<ObserveKeys except={['Space']}>
|
||||||
<SearchBox
|
<div onFocus={this.handleFocus} role='textbox'>
|
||||||
focusShortcuts={[83, 191]}
|
<SearchBox
|
||||||
onChange={this.handleChange}
|
focusShortcuts={['83', '191']}
|
||||||
onFocus={this.handleFocus}
|
onChange={this.handleChange}
|
||||||
onSubmit={this.handleSearch}
|
onSubmit={e => {
|
||||||
showLoadingIndicator={false}
|
this.handleSearch(e);
|
||||||
translations={{ placeholder }}
|
}}
|
||||||
/>
|
showLoadingIndicator={false}
|
||||||
|
translations={{ placeholder }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</ObserveKeys>
|
</ObserveKeys>
|
||||||
{isDropdownEnabled && isSearchFocused && (
|
{isDropdownEnabled && isSearchFocused && (
|
||||||
<SearchHits
|
<SearchHits
|
||||||
@ -217,8 +231,6 @@ export class SearchBar extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SearchBar.displayName = 'SearchBar';
|
SearchBar.displayName = 'SearchBar';
|
||||||
SearchBar.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
@ -1,13 +1,28 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connectStateResults, connectHits } from 'react-instantsearch-dom';
|
import { connectStateResults, connectHits } from 'react-instantsearch-dom';
|
||||||
|
import { SearchState, Hit } from 'react-instantsearch-core';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
|
import { searchPageUrl } from '../../../utils/algolia-locale-setup';
|
||||||
|
import Suggestion from './search-suggestion';
|
||||||
|
import NoHitsSuggestion from './no-hits-suggestion';
|
||||||
|
|
||||||
import Suggestion from './SearchSuggestion';
|
const searchUrl = searchPageUrl as string;
|
||||||
import NoHitsSuggestion from './NoHitsSuggestion';
|
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(
|
const CustomHits = connectHits(
|
||||||
({
|
({
|
||||||
hits,
|
hits,
|
||||||
@ -16,7 +31,7 @@ const CustomHits = connectHits(
|
|||||||
handleMouseLeave,
|
handleMouseLeave,
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
handleHits
|
handleHits
|
||||||
}) => {
|
}: customHitsPropTypes) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const noHits = isEmpty(hits);
|
const noHits = isEmpty(hits);
|
||||||
const noHitsTitle = t('search.no-tutorials');
|
const noHitsTitle = t('search.no-tutorials');
|
||||||
@ -26,7 +41,7 @@ const CustomHits = connectHits(
|
|||||||
query: searchQuery,
|
query: searchQuery,
|
||||||
url: noHits
|
url: noHits
|
||||||
? null
|
? null
|
||||||
: `${searchPageUrl}?query=${encodeURIComponent(searchQuery)}`,
|
: `${searchUrl}?query=${encodeURIComponent(searchQuery)}`,
|
||||||
title: t('search.see-results', { searchQuery: searchQuery }),
|
title: t('search.see-results', { searchQuery: searchQuery }),
|
||||||
_highlightResult: {
|
_highlightResult: {
|
||||||
query: {
|
query: {
|
||||||
@ -47,7 +62,7 @@ const CustomHits = connectHits(
|
|||||||
return (
|
return (
|
||||||
<div className='ais-Hits'>
|
<div className='ais-Hits'>
|
||||||
<ul className='ais-Hits-list' data-cy='ais-Hits-list'>
|
<ul className='ais-Hits-list' data-cy='ais-Hits-list'>
|
||||||
{allHits.map((hit, i) => (
|
{allHits.map((hit: Hit, i: number) => (
|
||||||
<li
|
<li
|
||||||
className={
|
className={
|
||||||
!noHits && i === selectedIndex
|
!noHits && i === selectedIndex
|
||||||
@ -85,7 +100,7 @@ const SearchHits = connectStateResults(
|
|||||||
handleMouseLeave,
|
handleMouseLeave,
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
handleHits
|
handleHits
|
||||||
}) => {
|
}: searchHitsPropTypes) => {
|
||||||
return isEmpty(searchState) || !searchState.query ? null : (
|
return isEmpty(searchState) || !searchState.query ? null : (
|
||||||
<CustomHits
|
<CustomHits
|
||||||
handleHits={handleHits}
|
handleHits={handleHits}
|
||||||
@ -98,8 +113,4 @@ const SearchHits = connectStateResults(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
CustomHits.propTypes = {
|
|
||||||
handleHits: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchHits;
|
export default SearchHits;
|
@ -1,8 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Highlight } from 'react-instantsearch-dom';
|
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-');
|
const dropdownFooter = hit.objectID.includes('footer-');
|
||||||
return (
|
return (
|
||||||
<a
|
<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;
|
export default Suggestion;
|
@ -1,16 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
|
|
||||||
const propTypes = {
|
function NoResults({ query }: { query: string }) {
|
||||||
query: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
function NoResults({ query }) {
|
|
||||||
return (
|
return (
|
||||||
<div className='no-results-wrapper'>
|
<div className='no-results-wrapper'>
|
||||||
<p>
|
<p>
|
||||||
<Trans i18nKey='search.no-results' query={query}>
|
<Trans i18nKey='search.no-results' {...{ query: query }}>
|
||||||
<em>{{ query }}</em>
|
<em>{{ query }}</em>
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
@ -19,6 +14,5 @@ function NoResults({ query }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NoResults.displayName = 'NoResults';
|
NoResults.displayName = 'NoResults';
|
||||||
NoResults.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default NoResults;
|
export default NoResults;
|
68
client/src/components/search/searchPage/search-page-hits.tsx
Normal file
68
client/src/components/search/searchPage/search-page-hits.tsx
Normal 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;
|
Reference in New Issue
Block a user