From 62549150bf92297c4ddc5909edc066c57d1740b1 Mon Sep 17 00:00:00 2001 From: Roman Dotsenko Date: Tue, 30 Jun 2026 20:04:59 +0300 Subject: [PATCH 1/3] Added filtering and sorting with searchParams --- README.md | 14 +- src/App.tsx | 11 +- src/Root.tsx | 29 ++ src/components/Navbar.tsx | 23 +- src/components/PeopleFilters.tsx | 120 ++--- src/components/PeoplePage.tsx | 33 -- src/components/PeopleTable.tsx | 733 +++++-------------------------- src/components/PersonLink.tsx | 25 ++ src/components/SearchLink.tsx | 2 +- src/components/SortArrows.tsx | 29 ++ src/index.tsx | 9 +- src/pages/404.tsx | 1 + src/pages/HomePage.tsx | 1 + src/pages/PeoplePage.tsx | 84 ++++ src/types/filterField.ts | 1 + src/utils/filterPeople.ts | 31 ++ src/utils/findParent.ts | 7 + src/utils/sortPeople.ts | 24 + src/utils/useProjectParams.ts | 24 + 19 files changed, 457 insertions(+), 744 deletions(-) create mode 100644 src/Root.tsx delete mode 100644 src/components/PeoplePage.tsx create mode 100644 src/components/PersonLink.tsx create mode 100644 src/components/SortArrows.tsx create mode 100644 src/pages/404.tsx create mode 100644 src/pages/HomePage.tsx create mode 100644 src/pages/PeoplePage.tsx create mode 100644 src/types/filterField.ts create mode 100644 src/utils/filterPeople.ts create mode 100644 src/utils/findParent.ts create mode 100644 src/utils/sortPeople.ts create mode 100644 src/utils/useProjectParams.ts diff --git a/README.md b/README.md index 064a39440..019079e1f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# React People Table (Advanced) - Filterging and Sorting +# React People Table (Advanced) - Filtering and Sorting > Here is [the working example](https://mate-academy.github.io/react_people-table-advanced/) @@ -8,14 +8,14 @@ Using code from the [React People Table](https://github.com/mate-academy/react_p implement the ability to filter and sort people in the table. 1. All the filters and sort params should be saved as URL Search Params, so you could share the link to show exactly what you see. -1. Keep search params when navigating within the `People` page (when selecting a person or clicking the `People` link). -1. The sidebar with the filters should appear only when people are loaded. -1. `NameFilter` should update the `query` search param with the text from the input. +2. Keep search params when navigating within the `People` page (when selecting a person or clicking the `People` link). +3. The sidebar with the filters should appear only when people are loaded. +4. `NameFilter` should update the `query` search param with the text from the input. - show only people with the `name`, `motherName` or `fatherName` that match the query case insensitive; - if the input is empty there should not be `query` in the search params. -1. `CenturyFilter` should allow to choose several centuries or all of them. +5. `CenturyFilter` should allow to choose several centuries or all of them. - add `centuries` search params using `append` method `getAll` method; -1. Implement sorting by `name`, `sex`, `born` and `died` by clicking on arrows in a `th`; +6. Implement sorting by `name`, `sex`, `born` and `died` by clicking on arrows in a `th`; - the first click on a column sorts people by the selected field ascending (`a -> z` or `0 -> 9`); - the second click (when people are already sorted ascending by this field) reverses the order of sorting; - the third click (when people are already sorted in reversed order by this field) disables sorting; @@ -28,4 +28,4 @@ implement the ability to filter and sort people in the table. - 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). - Open one more terminal and run tests with `npm test` to ensure your solution is correct. -- Replace `` with your Github username in the [DEMO LINK](https://.github.io/react_people-table-advanced/) and add it to the PR description. +- Replace `` with your Github username in the [DEMO LINK](https://RVDotsenko.github.io/react_people-table-advanced/) and add it to the PR description. diff --git a/src/App.tsx b/src/App.tsx index adcb8594e..a668a97fa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,21 @@ -import { PeoplePage } from './components/PeoplePage'; import { Navbar } from './components/Navbar'; import './App.scss'; +import { Outlet } from 'react-router-dom'; +import { useEffect } from 'react'; export const App = () => { + useEffect(() => { + document.documentElement.classList.add('has-navbar-fixed-top'); + }, []); + return (
-

Home Page

-

Page not found

- +
diff --git a/src/Root.tsx b/src/Root.tsx new file mode 100644 index 000000000..ea132c06b --- /dev/null +++ b/src/Root.tsx @@ -0,0 +1,29 @@ +import { + HashRouter as Router, + Routes, + Route, + Navigate, +} from 'react-router-dom'; +import { App } from './App'; +import { HomePage } from './pages/HomePage'; +import { PeoplePage } from './pages/PeoplePage'; +import { PeopleTable } from './components/PeopleTable'; +import { PageNotFound } from './pages/404'; + +export const Root = () => ( + + + }> + } /> + } /> + + }> + } /> + } /> + + + } /> + + + +); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3f63898b2..914fa282b 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,3 +1,7 @@ +import { NavLink } from 'react-router-dom'; + +import cn from 'classnames'; + export const Navbar = () => { return ( diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index c9c819cd3..02edf57c1 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,18 +1,40 @@ +import { Link } from 'react-router-dom'; +import { SearchLink } from './SearchLink'; +import cn from 'classnames'; +import { useProjectParams } from '../utils/useProjectParams'; +import { useEffect, useState } from 'react'; +import { getSearchWith } from '../utils/searchHelper'; + +const sexLinks: Record = { + All: null, + Male: 'm', + Female: 'f', +}; + +const centuriesLinks = ['16', '17', '18', '19', '20']; + export const PeopleFilters = () => { + const { search, searchParams, setSearchParams } = useProjectParams(); + const [inputValue, setInputValue] = useState(search.query || ''); + + useEffect(() => { + setInputValue(search.query || ''); + }, [search.query]); + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx deleted file mode 100644 index b682bad9b..000000000 --- a/src/components/PeoplePage.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { PeopleFilters } from './PeopleFilters'; -import { Loader } from './Loader'; -import { PeopleTable } from './PeopleTable'; - -export const PeoplePage = () => { - return ( - <> -

People Page

- -
-
-
- -
- -
-
- - -

Something went wrong

- -

There are no people on the server

- -

There are no people matching the current search criteria

- - -
-
-
-
- - ); -}; diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index fdd814b4a..b8ebae073 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,645 +1,112 @@ +import { useOutletContext } from 'react-router-dom'; +import { Person } from '../types'; +import { findParent } from '../utils/findParent'; +import cn from 'classnames'; +import { PersonLink } from './PersonLink'; +import { useProjectParams } from '../utils/useProjectParams'; +import { SearchLink } from './SearchLink'; +import { sortPeople } from '../utils/sortPeople'; +import { filterPeople } from '../utils/filterPeople'; +import { SortIcon } from './SortArrows'; + /* eslint-disable jsx-a11y/control-has-associated-label */ export const PeopleTable = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + const { people } = useOutletContext<{ people: Person[] }>(); + const { slug, search } = useProjectParams(); + const columnNames = ['Name', 'Sex', 'Born', 'Died', 'Mother', 'Father']; - - - - - - - - + let filteredPeople = [...people]; - - - - - - - - + if (search.centuries?.length) { + filteredPeople = filterPeople(people, 'centuries', search.centuries); + } - - - - - - - - + if (search.sex) { + filteredPeople = filterPeople(filteredPeople, 'sex', search.sex); + } - - - - - - - - + if (search.query) { + const query = search.query.toLowerCase(); - - - - - - - - + filteredPeople = filterPeople(filteredPeople, 'query', query); + } - - - - - - - - + let sortedPeople = filteredPeople; - - - - - - - - + if (search.sort) { + sortedPeople = sortPeople( + filteredPeople, + search.sort as keyof Person, + !!search.order, + ); + } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Name - - - - - - - - - Sex - - - - - - - - - Born - - - - - - - - - Died - - - - - - - MotherFather
- Pieter Haverbeke - m16021642- - - Lieven van Haverbeke - -
- - Anna van Hecke - - f16071670Martijntken BeelaertPaschasius van Hecke
- Lieven Haverbeke - m16311676 - - Anna van Hecke - - - Pieter Haverbeke -
- - Elisabeth Hercke - - f16321674Margriet de BrabanderWillem Hercke
- Daniel Haverbeke - m16521723 - - Elisabeth Hercke - - - Lieven Haverbeke -
- - Joanna de Pape - - f16541723Petronella WautersVincent de Pape
- - Martina de Pape - - f16661727Petronella WautersVincent de Pape
- Willem Haverbeke - m16681731 - - Elisabeth Hercke - - - Lieven Haverbeke -
- Jan Haverbeke - m16711731 - - Elisabeth Hercke - - - Lieven Haverbeke -
- - Maria de Rycke - - f16831724Laurentia van VlaenderenFrederik de Rycke
- - Livina Haverbeke - - f16921743 - - Joanna de Pape - - - Daniel Haverbeke -
- - Pieter Bernard Haverbeke - - m16951762Petronella Wauters - Willem Haverbeke -
- - Lieven de Causmaecker - - m16961724Joanna ClaesCarel de Causmaecker
- - Jacoba Lammens - - f16991740Livina de VriezeLieven Lammens
- Pieter de Decker - m17051780Petronella van de SteeneJoos de Decker
- - Laurentia Haverbeke - - f17101786 - - Maria de Rycke - - - Jan Haverbeke -
- - Elisabeth Haverbeke - - f17111754 - - Maria de Rycke - - - Jan Haverbeke -
- Jan van Brussel - m17141748Joanna van RootenJacobus van Brussel
- - Bernardus de Causmaecker - - m17211789 - - Livina Haverbeke - - - - Lieven de Causmaecker - -
- - Jan Francies Haverbeke - - m17251779Livina de Vrieze - - Pieter Bernard Haverbeke - -
- - Angela Haverbeke - - f17281734Livina de Vrieze - - Pieter Bernard Haverbeke - -
- - Petronella de Decker - - f17311781 - - Livina Haverbeke - - - Pieter de Decker -
- - Jacobus Bernardus van Brussel - - m17361809 - - Elisabeth Haverbeke - - - Jan van Brussel -
- - Pieter Antone Haverbeke - - m17531798 - - Petronella de Decker - - - - Jan Francies Haverbeke - -
- - Jan Frans van Brussel - - m17611833- - - Jacobus Bernardus van Brussel - -
- - Livina Sierens - - f17611826Maria van WaesJan Sierens
- - Joanna de Causmaecker - - f17621807- - - Bernardus de Causmaecker - -
- Carel Haverbeke - m17961837 - - Livina Sierens - - - - Pieter Antone Haverbeke - -
- - Maria van Brussel - - f18011834 - - Joanna de Causmaecker - - - - Jan Frans van Brussel - -
- Carolus Haverbeke - m18321905 - - Maria van Brussel - - - Carel Haverbeke -
- - Maria Sturm - - f18351917Seraphina SpelierCharles Sturm
- - Emma de Milliano - - f18761956Sophia van DammePetrus de Milliano
- Emile Haverbeke - m18771968 - - Maria Sturm - - - Carolus Haverbeke -
+ return ( + <> + {sortedPeople.length === 0 ? ( +

There are no people matching the current search criteria

+ ) : ( + + + + {columnNames.map(columnName => { + const normalizedColumnName = columnName.toLowerCase(); + + const searchParam = + search.sort === normalizedColumnName + ? search.order === 'desc' + ? { sort: null, order: null } + : { sort: normalizedColumnName, order: 'desc' } + : { sort: normalizedColumnName, order: null }; + + return ( + + ); + })} + + + + {sortedPeople.map((person, index) => { + const mother = findParent(person.motherName, people); + const father = findParent(person.fatherName, people); + + const rowKey = `${person.name}-${index}`; + const isRowHighlighted = person.slug === slug; + + return ( + + + + + + + + + + + ); + })} +
+ + {columnName} + + { + + } + + +
{person.sex}{person.born}{person.died}
+ )} + ); }; diff --git a/src/components/PersonLink.tsx b/src/components/PersonLink.tsx new file mode 100644 index 000000000..ceb2e7c61 --- /dev/null +++ b/src/components/PersonLink.tsx @@ -0,0 +1,25 @@ +import { Person } from '../types'; +import cn from 'classnames'; +import { Link } from 'react-router-dom'; + +type Props = { + person: Person | string; +}; + +const getNameClass = (currentPerson: Person) => { + const isWoman = currentPerson.sex === 'f'; + + return cn({ 'has-text-danger': isWoman }); +}; + +export const PersonLink: React.FC = ({ person }) => ( + + {typeof person === 'string' ? ( + person + ) : ( + + {person.name} + + )} + +); diff --git a/src/components/SearchLink.tsx b/src/components/SearchLink.tsx index f78b83cbc..c55dafa0a 100644 --- a/src/components/SearchLink.tsx +++ b/src/components/SearchLink.tsx @@ -2,7 +2,7 @@ import { Link, LinkProps, useSearchParams } from 'react-router-dom'; import { getSearchWith, SearchParams } from '../utils/searchHelper'; /** - * To replace the the standard `Link` we take all it props except for `to` + * To replace the standard `Link` we take all it props except for `to` * along with the custom `params` prop that we use for updating the search */ type Props = Omit & { diff --git a/src/components/SortArrows.tsx b/src/components/SortArrows.tsx new file mode 100644 index 000000000..77f197c7a --- /dev/null +++ b/src/components/SortArrows.tsx @@ -0,0 +1,29 @@ +import cn from 'classnames'; + +export const SortIcon = ({ + columnName, + currentSort, + currentOrder, +}: { + columnName: string; + currentSort: string | null; + currentOrder: string | null; +}) => { + if (columnName === 'Mother' || columnName === 'Father') { + return null; + } + + const normalized = columnName.toLowerCase(); + + return ( + + + + ); +}; diff --git a/src/index.tsx b/src/index.tsx index d72ba5730..0082c2d81 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,8 @@ import { createRoot } from 'react-dom/client'; -import { HashRouter as Router } from 'react-router-dom'; import 'bulma/css/bulma.css'; import '@fortawesome/fontawesome-free/css/all.css'; -import { App } from './App'; +import { Root } from './Root'; -createRoot(document.getElementById('root') as HTMLDivElement).render( - - - , -); +createRoot(document.getElementById('root') as HTMLDivElement).render(); diff --git a/src/pages/404.tsx b/src/pages/404.tsx new file mode 100644 index 000000000..a7b602723 --- /dev/null +++ b/src/pages/404.tsx @@ -0,0 +1 @@ +export const PageNotFound = () =>

Page not found

; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx new file mode 100644 index 000000000..153c06306 --- /dev/null +++ b/src/pages/HomePage.tsx @@ -0,0 +1 @@ +export const HomePage = () =>

Home Page

; diff --git a/src/pages/PeoplePage.tsx b/src/pages/PeoplePage.tsx new file mode 100644 index 000000000..fc6b22794 --- /dev/null +++ b/src/pages/PeoplePage.tsx @@ -0,0 +1,84 @@ +import { useEffect, useState } from 'react'; +import { Loader } from '../components/Loader'; +import { PeopleFilters } from '../components/PeopleFilters'; +import { getPeople } from '../api'; +import { Person } from '../types'; +import { Outlet } from 'react-router-dom'; + +export const PeoplePage = () => { + const [people, setPeople] = useState([]); + + const [isLoading, setIsLoading] = useState(false); + const [errors, setErrors] = useState({ + loading: false, + people: false, + }); + + useEffect(() => { + const fetchPeople = async () => { + setIsLoading(true); + try { + const fetchedPeople = await getPeople(); + + setPeople(fetchedPeople); + if (!fetchedPeople.length) { + setErrors(prev => ({ ...prev, people: true })); + } + } catch { + setErrors(prev => ({ ...prev, loading: true })); + } finally { + setIsLoading(false); + } + }; + + fetchPeople(); + }, []); + + useEffect(() => { + if (errors.loading || errors.people) { + return; + } + + const timerId = setTimeout(() => { + setErrors({ loading: false, people: false }); + }, 2000); + + return () => clearTimeout(timerId); + }, [errors.loading, errors.people]); + + return ( + <> +

People Page

+ +
+ {isLoading ? ( + + ) : ( +
+
+ {isLoading ? : } +
+ +
+
+ {errors.loading && ( +

Something went wrong

+ )} + + {errors.people && ( +

+ There are no people on the server +

+ )} + + {!errors.loading && !errors.people && ( + + )} +
+
+
+ )} +
+ + ); +}; diff --git a/src/types/filterField.ts b/src/types/filterField.ts new file mode 100644 index 000000000..9d81b9bd1 --- /dev/null +++ b/src/types/filterField.ts @@ -0,0 +1 @@ +export type FilterField = 'query' | 'centuries' | 'sex'; diff --git a/src/utils/filterPeople.ts b/src/utils/filterPeople.ts new file mode 100644 index 000000000..455c66d09 --- /dev/null +++ b/src/utils/filterPeople.ts @@ -0,0 +1,31 @@ +import { Person } from '../types'; +import { FilterField } from '../types/filterField'; + +export const filterPeople = ( + people: Person[], + field: FilterField, + value: string | string[], +) => { + if (typeof value === 'string') { + switch (field) { + case 'query': + return [...people].filter( + person => + person.name.toLowerCase().includes(value) || + person.motherName?.toLowerCase().includes(value) || + person.fatherName?.toLowerCase().includes(value), + ); + case 'sex': + return [...people].filter(person => person.sex === value); + + default: + return [...people]; + } + } else { + return [...people].filter(person => { + const personBirthCentury = Math.ceil(person.born / 100); + + return value.includes(personBirthCentury.toString()); + }); + } +}; diff --git a/src/utils/findParent.ts b/src/utils/findParent.ts new file mode 100644 index 000000000..681e68c75 --- /dev/null +++ b/src/utils/findParent.ts @@ -0,0 +1,7 @@ +import { Person } from '../types'; + +export const findParent = (parentName: string | null, people: Person[]) => { + const isParent = people.find(person => person.name === parentName); + + return isParent ? isParent : parentName; +}; diff --git a/src/utils/sortPeople.ts b/src/utils/sortPeople.ts new file mode 100644 index 000000000..14af668ef --- /dev/null +++ b/src/utils/sortPeople.ts @@ -0,0 +1,24 @@ +import { Person } from '../types'; + +export const sortPeople = ( + people: Person[], + sortField: keyof Person, + ordered: boolean, +) => { + const filteredArray = [...people]; + + return filteredArray.sort((a, b) => { + const valueA = a[sortField] ?? ''; + const valueB = b[sortField] ?? ''; + + if (valueA < valueB) { + return ordered ? 1 : -1; + } + + if (valueA > valueB) { + return ordered ? -1 : 1; + } + + return 0; + }); +}; diff --git a/src/utils/useProjectParams.ts b/src/utils/useProjectParams.ts new file mode 100644 index 000000000..613ca2349 --- /dev/null +++ b/src/utils/useProjectParams.ts @@ -0,0 +1,24 @@ +import { useParams, useSearchParams } from 'react-router-dom'; + +type SearchType = { + sort: string | null; + order: string | null; + centuries: string[] | null; + sex: string | null; + query: string | null; +}; + +export const useProjectParams = () => { + const { slug } = useParams(); + const [searchParams, setSearchParams] = useSearchParams(); + + const search: SearchType = { + sort: searchParams.get('sort'), + order: searchParams.get('order'), + centuries: searchParams.getAll('centuries'), + sex: searchParams.get('sex'), + query: searchParams.get('query'), + }; + + return { slug, search, searchParams, setSearchParams }; +}; From bde9bc483182075ab61b26fa6ed06f9cbbe4c08a Mon Sep 17 00:00:00 2001 From: Roman Dotsenko Date: Tue, 30 Jun 2026 20:59:40 +0300 Subject: [PATCH 2/3] Fix navbar, filter links --- src/components/Navbar.tsx | 5 ++++- src/components/PeopleFilters.tsx | 15 ++++++++++++--- src/components/PeopleTable.tsx | 20 +++++++++++--------- src/pages/PeoplePage.tsx | 3 ++- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 914fa282b..e869b0dfa 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,8 +1,11 @@ import { NavLink } from 'react-router-dom'; import cn from 'classnames'; +import { useProjectParams } from '../utils/useProjectParams'; export const Navbar = () => { + const { searchParams } = useProjectParams(); + return (