diff --git a/package-lock.json b/package-lock.json index a57a24cb7..cceb15e29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "devDependencies": { "@cypress/react18": "^2.0.1", "@faker-js/faker": "^8.4.1", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/lodash.debounce": "^4.0.9", @@ -1189,10 +1189,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.5.tgz", - "integrity": "sha512-mHRY2FkuoYCf5U0ahIukkaRo5LSZsxrTSgMJheFoyf3VXsTvfM9OfWcZIDIDB521kdPrScHHnRp+JRNjCfUO5A==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", diff --git a/package.json b/package.json index cda47c54b..2ae5e8de2 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@cypress/react18": "^2.0.1", "@faker-js/faker": "^8.4.1", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/lodash.debounce": "^4.0.9", diff --git a/src/App.tsx b/src/App.tsx index a88cd7a6d..87c68ff1e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,73 +1,36 @@ -import React from 'react'; +import React, { useState } from 'react'; import './App.scss'; import { peopleFromServer } from './data/people'; +import { Autocomplete } from './components/Autocomplete'; +import { Person } from './types/Person'; export const App: React.FC = () => { - const { name, born, died } = peopleFromServer[0]; + const people = peopleFromServer; + const [selectedPerson, setSelectedPerson] = useState(null); + + const handleSelectPerson = (person: Person) => { + setSelectedPerson(person); + }; + + const handleResetPerson = () => { + setSelectedPerson(null); + }; return (

- {`${name} (${born} - ${died})`} + {selectedPerson + ? `${selectedPerson.name} (${selectedPerson.born} - ${selectedPerson.died})` + : 'No selected person'}

-
-
- -
- -
-
-
-

Pieter Haverbeke

-
- -
-

Pieter Bernard Haverbeke

-
- -
-

Pieter Antone Haverbeke

-
- -
-

Elisabeth Haverbeke

-
- -
-

Pieter de Decker

-
- -
-

Petronella de Decker

-
- -
-

Elisabeth Hercke

-
-
-
-
- -
-

No matching suggestions

-
+
); diff --git a/src/components/Autocomplete/Autocomplete.tsx b/src/components/Autocomplete/Autocomplete.tsx new file mode 100644 index 000000000..e2ff6f64e --- /dev/null +++ b/src/components/Autocomplete/Autocomplete.tsx @@ -0,0 +1,133 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import cn from 'classnames'; +import { Person } from '../../types/Person'; +import { debounce } from '../../utils/debounce'; + +type Props = { + people: Person[]; + onSelect: (person: Person) => void; + onReset: () => void; + selectedPerson: Person | null; + delay?: number; +}; + +export const Autocomplete: React.FC = ({ + people, + onSelect, + onReset, + selectedPerson, + delay = 300, +}) => { + const [inputQuery, setInputQuery] = useState(''); + const [delayedQuery, setDelayedQuery] = useState(''); + const [showAutocomplete, setShowAutocomplete] = useState(false); + + const setQueryDebounced = useCallback(debounce(setDelayedQuery, delay), [ + delay, + ]); + + const handleQueryChange = (event: React.ChangeEvent) => { + const inputValue = event.target.value; + + if (selectedPerson && inputValue !== selectedPerson.name) { + onReset(); + } + + setInputQuery(inputValue); + setQueryDebounced(inputValue); + }; + + const handleReset = () => { + setInputQuery(''); + setDelayedQuery(''); + onReset(); + }; + + const filteredPeople = useMemo(() => { + return [...people].filter(person => { + const personName = person.name.toLowerCase(); + + return personName.includes(delayedQuery.toLowerCase()); + }); + }, [delayedQuery, people]); + + const noMatching = + !selectedPerson && !filteredPeople.length && delayedQuery.trim() !== ''; + + const showDropdownOptions = + !selectedPerson && showAutocomplete && filteredPeople.length > 0; + + return ( +
+
+
+
+ setShowAutocomplete(true)} + onBlur={() => setShowAutocomplete(false)} + type="text" + placeholder="Enter a part of the name" + className="input" + data-cy="search-input" + value={inputQuery} + onChange={handleQueryChange} + /> +
+ +
+ +
+
+
+ + {showDropdownOptions && ( + + )} + + {noMatching && ( +
+

No matching suggestions

+
+ )} +
+ ); +}; diff --git a/src/components/Autocomplete/index.ts b/src/components/Autocomplete/index.ts new file mode 100644 index 000000000..a796c54c6 --- /dev/null +++ b/src/components/Autocomplete/index.ts @@ -0,0 +1 @@ +export * from './Autocomplete'; diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts new file mode 100644 index 000000000..a3ab3a0df --- /dev/null +++ b/src/utils/debounce.ts @@ -0,0 +1,14 @@ +export const debounce = ( + callback: (...args: T) => void, + delay: number, +) => { + let timerId: number = 0; + + return (...args: T) => { + window.clearTimeout(timerId); + + timerId = window.setTimeout(() => { + callback(...args); + }, delay); + }; +};