From 5a4ef52e3f81ad299153772d977565ce28494bc7 Mon Sep 17 00:00:00 2001 From: Anna Rodina Date: Thu, 30 Apr 2026 13:40:46 +0300 Subject: [PATCH 1/2] Solution --- .eslintrc.cjs | 4 +- README.md | 2 +- package-lock.json | 12 ++--- package.json | 2 +- src/App.tsx | 81 +++++++++------------------------ src/Autocomplete.tsx | 105 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 138 insertions(+), 68 deletions(-) create mode 100644 src/Autocomplete.tsx diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b51149cf5..39a7c6f5f 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -89,8 +89,8 @@ module.exports = { // Typescript '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-unused-vars': ['error'], - '@typescript-eslint/indent': ['error', 2], + '@typescript-eslint/no-unused-vars': ['error'], + '@typescript-eslint/indent': 'off', '@typescript-eslint/ban-types': ['error', { extendDefaults: true, types: { diff --git a/README.md b/README.md index b8897503d..8cff6301e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ that will suggest people matching an entered text. - Install Prettier Extention and use this [VSCode settings](https://mate-academy.github.io/fe-program/tools/vscode/settings.json) to enable format on save. - Implement a solution following the [React task guideline](https://github.com/mate-academy/react_task-guideline#react-tasks-guideline). - Use the [React TypeScript cheat sheet](https://mate-academy.github.io/fe-program/js/extra/react-typescript). -- Replace `` with your Github username in the [DEMO LINK](https://.github.io/react_autocomplete/) and add it to the PR description. +- Replace `` with your Github username in the [DEMO LINK](https://tavokina.github.io/react_autocomplete/) and add it to the PR description. - Don't remove the `data-qa` attributes. It is required for tests. ## Troubleshooting diff --git a/package-lock.json b/package-lock.json index a57a24cb7..62b57e2f0 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", @@ -6800,7 +6801,8 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" }, "node_modules/lodash.get": { "version": "4.4.2", 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..54290bac6 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 './Autocomplete'; +import { Person } from './types/Person'; export const App: React.FC = () => { - const { name, born, died } = peopleFromServer[0]; + const [selectedPerson, setSelectedPerson] = useState(null); + const [query, setQuery] = useState(''); + const delayValue = 300; 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

-
+ { + setSelectedPerson(person); + setQuery(person.name); + }} + delay={delayValue} + query={query} + onQueryChange={value => { + setQuery(value); + setSelectedPerson(null); + }} + />
); diff --git a/src/Autocomplete.tsx b/src/Autocomplete.tsx new file mode 100644 index 000000000..1793a8685 --- /dev/null +++ b/src/Autocomplete.tsx @@ -0,0 +1,105 @@ +import { useCallback, useRef, useState } from 'react'; +import { Person } from './types/Person'; +import debounce from 'lodash.debounce'; +import classNames from 'classnames'; + +type Props = { + people: Person[]; + onSelected: (person: Person) => void; + delay?: number; + query: string; + onQueryChange: (value: string) => void; +}; +export const Autocomplete: React.FC = ({ + people, + onSelected, + delay, + query, + onQueryChange, +}) => { + const [appliedQuery, setAppliedQuery] = useState(''); + const [isOpen, setIsOpen] = useState(false); + + const lastAppliedQuery = useRef(''); + + const isAppliedQuery = (value: string) => { + if (lastAppliedQuery.current === value) { + return; + } + + lastAppliedQuery.current = value; + setAppliedQuery(value); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + const applyQuery = useCallback(debounce(isAppliedQuery, delay), [delay]); + + const handleQueryChange = (event: React.ChangeEvent) => { + onQueryChange(event.target.value); + applyQuery(event.target.value); + }; + + const normalizedQuery = appliedQuery.trim().toLowerCase(); + + const filteredPeople = appliedQuery + ? people.filter(person => + person.name.toLowerCase().includes(normalizedQuery)) + : people; + + return ( +
+
+ { + handleQueryChange(e); + }} + onFocus={() => { + setIsOpen(true); + }} + /> +
+ +
+
+ {filteredPeople.map(person => ( +
{ + onSelected(person); + setIsOpen(false); + }} + > +

{person.name}

+
+ ))} +
+
+ + {isOpen && appliedQuery && filteredPeople.length === 0 && ( +
+

No matching suggestions

+
+ )} +
+ ); +}; From 9bbfe24a806542353824aeb74a4b1492f66b1919 Mon Sep 17 00:00:00 2001 From: Anna Rodina Date: Fri, 1 May 2026 10:58:42 +0300 Subject: [PATCH 2/2] Fixed --- src/Autocomplete.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Autocomplete.tsx b/src/Autocomplete.tsx index 1793a8685..0fea589ac 100644 --- a/src/Autocomplete.tsx +++ b/src/Autocomplete.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Person } from './types/Person'; import debounce from 'lodash.debounce'; import classNames from 'classnames'; @@ -34,6 +34,12 @@ export const Autocomplete: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps const applyQuery = useCallback(debounce(isAppliedQuery, delay), [delay]); + useEffect(() => { + return () => { + applyQuery.cancel(); + }; +}, [applyQuery]); + const handleQueryChange = (event: React.ChangeEvent) => { onQueryChange(event.target.value); applyQuery(event.target.value); @@ -41,9 +47,10 @@ export const Autocomplete: React.FC = ({ const normalizedQuery = appliedQuery.trim().toLowerCase(); - const filteredPeople = appliedQuery + const filteredPeople = normalizedQuery ? people.filter(person => - person.name.toLowerCase().includes(normalizedQuery)) + person.name.toLowerCase().includes(normalizedQuery), + ) : people; return (