From 54cf8d3a37ff8ac6466cc960937f1db53850fe6e Mon Sep 17 00:00:00 2001 From: Artem Date: Sat, 2 May 2026 22:07:35 +0300 Subject: [PATCH 1/3] start --- src/App.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/App.tsx b/src/App.tsx index a88cd7a6d..f1b27ef97 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -70,5 +70,6 @@ export const App: React.FC = () => { + // The above code is a simple React component that displays a title with a person's name and birth/death years, an input field for searching, a dropdown list of suggestions, and a notification message when there are no matching suggestions. The component uses Bulma CSS classes for styling and includes data-cy attributes for testing purposes. ); }; From 701af9e9c71fe1502498b74f8d0a4e4161d606e5 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 4 May 2026 21:09:25 +0300 Subject: [PATCH 2/3] first try --- package-lock.json | 9 +- package.json | 2 +- src/App.tsx | 72 +++------------ src/components/Autocomplete.tsx | 156 ++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 65 deletions(-) create mode 100644 src/components/Autocomplete.tsx 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 f1b27ef97..1cc3ace0a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,75 +1,27 @@ -import React from 'react'; +import React, { useState } from 'react'; import './App.scss'; import { peopleFromServer } from './data/people'; +import { Person } from './types/Person'; +import Autocomplete from './components/Autocomplete'; export const App: React.FC = () => { - const { name, born, died } = peopleFromServer[0]; + const [selected, setSelected] = useState(null); return (

- {`${name} (${born} - ${died})`} + {selected + ? `${selected.name} (${selected.born} - ${selected.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

-
+ setSelected(p)} + />
- // The above code is a simple React component that displays a title with a person's name and birth/death years, an input field for searching, a dropdown list of suggestions, and a notification message when there are no matching suggestions. The component uses Bulma CSS classes for styling and includes data-cy attributes for testing purposes. ); }; diff --git a/src/components/Autocomplete.tsx b/src/components/Autocomplete.tsx new file mode 100644 index 000000000..a12db7651 --- /dev/null +++ b/src/components/Autocomplete.tsx @@ -0,0 +1,156 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import debounce from 'lodash.debounce'; +import { Person } from '../types/Person'; + +type Props = { + people: Person[]; + debounceMs?: number; + onSelected?: (person: Person | null) => void; +}; + +export const Autocomplete: React.FC = ({ + people, + debounceMs = 300, + onSelected, +}) => { + const [inputValue, setInputValue] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const [suggestions, setSuggestions] = useState([]); + + // Keep last queried string to avoid re-filtering when identical + const lastQueryRef = useRef(null); + // Track whether the last action was a selection (so we can avoid clearing it immediately) + const selectedNameRef = useRef(null); + + // Filtering function + const doFilter = useCallback( + (query: string) => { + const q = query.trim().toLowerCase(); + + if (lastQueryRef.current === q) { + return; // don't run again if query hasn't changed + } + + lastQueryRef.current = q; + + if (q === '') { + setSuggestions(people); + + return; + } + + const filtered = people.filter(p => p.name.toLowerCase().includes(q)); + + setSuggestions(filtered); + }, + [people], + ); + + // Debounced version + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedFilter = useCallback( + debounce((q: string) => doFilter(q), debounceMs), + [doFilter, debounceMs], + ); + + useEffect(() => { + return () => { + debouncedFilter.cancel(); + }; + }, [debouncedFilter]); + + const onChange = (e: React.ChangeEvent) => { + const v = e.target.value; + + setInputValue(v); + + // if there was a selected person and the user changed the input, clear selection + if (selectedNameRef.current && v !== selectedNameRef.current) { + selectedNameRef.current = null; + onSelected?.(null); + } + + // open dropdown for new input + setIsOpen(true); + + // Trigger debounce filtering + debouncedFilter(v); + }; + + const onFocus = () => { + // when focused and empty show all + setIsOpen(true); + if (inputValue.trim() === '') { + // show all immediately + lastQueryRef.current = ''; + setSuggestions(people); + } else { + debouncedFilter(inputValue); + } + }; + + const onSuggestionClick = (p: Person) => { + setInputValue(p.name); + setIsOpen(false); + selectedNameRef.current = p.name; + lastQueryRef.current = p.name.trim().toLowerCase(); + setSuggestions([]); + onSelected?.(p); + }; + + const onBlur = () => { + // Delay closing to allow click event on suggestion to fire + requestAnimationFrame(() => { + setIsOpen(false); + }); + }; + + return ( +
+
+ +
+ + {isOpen && ( +
+
+ {suggestions.length > 0 ? ( + suggestions.map(p => ( +
ev.preventDefault()} + onClick={() => onSuggestionClick(p)} + > +

{p.name}

+
+ )) + ) : ( +
+
+

No matching suggestions

+
+
+ )} +
+
+ )} +
+ ); +}; + +export default Autocomplete; From 3e144e31f63b25cd6f69d425fc5c60f7d8f92434 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 5 May 2026 11:34:57 +0300 Subject: [PATCH 3/3] firstt try --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b8897503d..57ca2bdfc 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://flyman13.github.io/react_autocomplete/) and add it to the PR description. - Don't remove the `data-qa` attributes. It is required for tests. ## Troubleshooting