diff --git a/README.md b/README.md index b8897503d..57502a6d0 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,11 @@ that will suggest people matching an entered text. - when the selected person is displayed in the title, but the value in the input changes, the selected person should be cleared and `No selected person` should be shown. ## Instructions + - 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://OlehYuriev.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/src/App.tsx b/src/App.tsx index a88cd7a6d..77ffc37a9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,73 +1,113 @@ -import React from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import './App.scss'; import { peopleFromServer } from './data/people'; +import { Person } from './types/Person'; +import { Dropdown } from './components/Dropdown/Dropdown'; +import { useDebounce } from './hooks/useDebounce'; + +function filteredPerson(people: Person[], query: string): Person[] { + const queryLowerCase = query.trim().toLowerCase(); + + if (query !== '') { + return people.filter(person => + person.name.toLowerCase().includes(queryLowerCase), + ); + } + + return people; +} export const App: React.FC = () => { - const { name, born, died } = peopleFromServer[0]; + const [search, setSearch] = useState(''); + const [selectedPerson, setSelectedPerson] = useState(null); + const [visibleDropdown, setVisibleDropdown] = useState(false); + const debounceValue = useDebounce(search); + const visiblePeople = useMemo(() => { + return filteredPerson(peopleFromServer, debounceValue); + }, [debounceValue]); + const refDropDown = useRef(null); + + const handleChoosePerson = useCallback((person: Person) => { + setSelectedPerson(person); + setSearch(person.name); + setVisibleDropdown(false); + }, []); + + useEffect(() => { + if (!visibleDropdown) { + return; + } + + function handleClick(event: MouseEvent) { + if ( + !refDropDown.current || + refDropDown.current.contains(event.target as Node) + ) { + return; + } + + setVisibleDropdown(false); + } + + document.addEventListener('mousedown', handleClick); + + return () => { + document.removeEventListener('mousedown', handleClick); + }; + }, [visibleDropdown]); + const personData = `${selectedPerson?.name} (${selectedPerson?.born} - ${selectedPerson?.died})`; return (

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

-
+
{ + setSearch(e.target.value); + if (selectedPerson) { + setSelectedPerson(null); + } + }} type="text" placeholder="Enter a part of the name" className="input" data-cy="search-input" + onFocus={() => setVisibleDropdown(true)} />
- -
-
-
-

Pieter Haverbeke

-
- -
-

Pieter Bernard Haverbeke

-
- -
-

Pieter Antone Haverbeke

-
- -
-

Elisabeth Haverbeke

-
- -
-

Pieter de Decker

-
- -
-

Petronella de Decker

-
- -
-

Elisabeth Hercke

-
-
-
+
- -
-

No matching suggestions

-
+ role="alert" + data-cy="no-suggestions-message" + > +

No matching suggestions

+
+ )}
); diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx new file mode 100644 index 000000000..1d2d03a92 --- /dev/null +++ b/src/components/Dropdown/Dropdown.tsx @@ -0,0 +1,40 @@ +import { memo } from 'react'; +import { Person } from '../../types/Person'; + +type Prop = { + visible: boolean; + people: Person[]; + onSelected: (person: Person) => void; +}; +export const Dropdown = memo(function Dropdown({ + visible, + people, + onSelected, +}: Prop) { + return ( + <> + {visible && people.length > 0 && ( +
+
+ {people.map(item => ( + + ))} +
+
+ )} + + ); +}); diff --git a/src/hooks/useDebounce.tsx b/src/hooks/useDebounce.tsx new file mode 100644 index 000000000..69d587b28 --- /dev/null +++ b/src/hooks/useDebounce.tsx @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react'; + +export function useDebounce(value: T, delay: number = 500) { + const [debounceValue, setDebounceValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => { + setDebounceValue(value); + }, delay); + + return () => clearTimeout(timer); + }); + + return debounceValue; +}