From fa91fd6094645d536c5e52c069fb6c10534e1714 Mon Sep 17 00:00:00 2001 From: Inna K Date: Tue, 9 Jun 2026 12:59:33 +0300 Subject: [PATCH 1/5] added solution --- README.md | 2 +- index.html | 3 + package-lock.json | 9 +- package.json | 2 +- src/App.scss | 4 + src/App.tsx | 37 +- src/components/Navbar.tsx | 54 +- src/components/PeopleFilters.tsx | 119 +++-- src/components/PeoplePage.tsx | 33 -- src/components/PeopleTable.tsx | 705 ++++---------------------- src/components/PersonLink.tsx | 15 + src/components/pages/HomePage.tsx | 3 + src/components/pages/NotFoundPage.tsx | 3 + src/components/pages/PeoplePage.tsx | 110 ++++ 14 files changed, 363 insertions(+), 736 deletions(-) delete mode 100644 src/components/PeoplePage.tsx create mode 100644 src/components/PersonLink.tsx create mode 100644 src/components/pages/HomePage.tsx create mode 100644 src/components/pages/NotFoundPage.tsx create mode 100644 src/components/pages/PeoplePage.tsx diff --git a/README.md b/README.md index 064a39440..430a7107c 100644 --- a/README.md +++ b/README.md @@ -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://Inna-code10.github.io/react_people-table-advanced/) and add it to the PR description. diff --git a/index.html b/index.html index 095fb3a45..eac5f9338 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,9 @@ + + + Vite + React + TS diff --git a/package-lock.json b/package-lock.json index df276ce8a..a2d822adb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@cypress/react18": "^2.0.1", - "@mate-academy/scripts": "^1.9.12", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/node": "^20.14.10", @@ -1184,10 +1184,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.9.12", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.9.12.tgz", - "integrity": "sha512-/OcmxMa34lYLFlGx7Ig926W1U1qjrnXbjFJ2TzUcDaLmED+A5se652NcWwGOidXRuMAOYLPU2jNYBEkKyXrFJA==", + "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 919fbd42b..77fc93ccf 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@cypress/react18": "^2.0.1", - "@mate-academy/scripts": "^1.9.12", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/node": "^20.14.10", diff --git a/src/App.scss b/src/App.scss index c17d529f4..c04e1c33f 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,3 +1,7 @@ iframe { display: none; } + +.title { + padding-top: 48px; +} diff --git a/src/App.tsx b/src/App.tsx index adcb8594e..cb492ae4f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,29 @@ -import { PeoplePage } from './components/PeoplePage'; +import { Navigate, Route, Routes } from 'react-router-dom'; + import { Navbar } from './components/Navbar'; +import { HomePage } from './components/pages/HomePage'; +import { PeoplePage } from './components/pages/PeoplePage'; +import { NotFoundPage } from './components/pages/NotFoundPage'; import './App.scss'; -export const App = () => { - return ( -
- +export const App = () => ( +
+ + +
+
+ + } /> + + } /> + + } /> + } /> -
-
-

Home Page

-

Page not found

- -
+ } /> +
-
- ); -}; +
+
+); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3f63898b2..96b2560bf 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,26 +1,32 @@ -export const Navbar = () => { - return ( - +); diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index c9c819cd3..68d940591 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,20 +1,49 @@ +import { useSearchParams } from 'react-router-dom'; +import { ChangeEvent } from 'react'; + export const PeopleFilters = () => { + const [searchParams, setSearchParams] = useSearchParams(); + + const query = searchParams.get('query') || ''; + const centuries = searchParams.getAll('centuries'); + + const handleQueryChange = (event: ChangeEvent) => { + const value = event.target.value.trim(); + + const newParams = new URLSearchParams(searchParams); + + if (value) { + newParams.set('query', value); + } else { + newParams.delete('query'); + } + + setSearchParams(newParams); + }; + + const handleCenturyChange = (century: string) => { + const newParams = new URLSearchParams(searchParams); + const current = newParams.getAll('centuries'); + + newParams.delete('centuries'); + + const updated = current.includes(century) + ? current.filter(c => c !== century) + : [...current, century]; + + updated.forEach(c => newParams.append('centuries', c)); + + setSearchParams(newParams); + }; + + const resetAll = () => { + setSearchParams({}); + }; + 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..109e0cb08 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,5 +1,58 @@ -/* eslint-disable jsx-a11y/control-has-associated-label */ -export const PeopleTable = () => { +import { useSearchParams } from 'react-router-dom'; + +import { Person } from '../types/Person'; +import { PersonLink } from './PersonLink'; + +type Props = { + people: Person[]; + selectedSlug?: string; +}; + +type SortField = 'name' | 'sex' | 'born' | 'died'; + +export const PeopleTable = ({ people, selectedSlug }: Props) => { + const [searchParams, setSearchParams] = useSearchParams(); + + const sort = searchParams.get('sort'); + const order = searchParams.get('order'); + + const findPerson = (name?: string | null) => + people.find(person => person.name === name); + + const handleSort = (field: SortField) => { + const params = new URLSearchParams(searchParams); + + if (sort !== field) { + params.set('sort', field); + params.delete('order'); + } else if (!order) { + params.set('order', 'desc'); + } else { + params.delete('sort'); + params.delete('order'); + } + + setSearchParams(params); + }; + + const getSortIcon = (field: string) => { + if (sort !== field) { + return ( + + + + ); + } + + return ( + + + + ); + }; + return ( { @@ -57,588 +94,48 @@ export const PeopleTable = () => { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {people.map(person => { + const mother = findPerson(person.motherName); + const father = findPerson(person.fatherName); + + return ( + + + + + + + + + + + + ); + })}
Name - - - - - + handleSort('name')}>{getSortIcon('name')} Sex - - - - - + handleSort('sex')}>{getSortIcon('sex')} Born - - - - - + handleSort('born')}>{getSortIcon('born')} Died - - - - - + handleSort('died')}>{getSortIcon('died')}
- 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 -
+ + {person.sex}{person.born}{person.died} + {!person.motherName ? ( + '-' + ) : mother ? ( + + ) : ( + person.motherName + )} + + {!person.fatherName ? ( + '-' + ) : father ? ( + + ) : ( + person.fatherName + )} +
); diff --git a/src/components/PersonLink.tsx b/src/components/PersonLink.tsx new file mode 100644 index 000000000..9d0382fba --- /dev/null +++ b/src/components/PersonLink.tsx @@ -0,0 +1,15 @@ +import { Link } from 'react-router-dom'; +import { Person } from '../types/Person'; + +type Props = { + person: Person; +}; + +export const PersonLink = ({ person }: Props) => ( + + {person.name} + +); diff --git a/src/components/pages/HomePage.tsx b/src/components/pages/HomePage.tsx new file mode 100644 index 000000000..e4cd51ac5 --- /dev/null +++ b/src/components/pages/HomePage.tsx @@ -0,0 +1,3 @@ +export const HomePage = () => { + return

Home Page

; +}; diff --git a/src/components/pages/NotFoundPage.tsx b/src/components/pages/NotFoundPage.tsx new file mode 100644 index 000000000..87fe719a7 --- /dev/null +++ b/src/components/pages/NotFoundPage.tsx @@ -0,0 +1,3 @@ +export const NotFoundPage = () => { + return

Page not found

; +}; diff --git a/src/components/pages/PeoplePage.tsx b/src/components/pages/PeoplePage.tsx new file mode 100644 index 000000000..2f142c6be --- /dev/null +++ b/src/components/pages/PeoplePage.tsx @@ -0,0 +1,110 @@ +import { useEffect, useState } from 'react'; +import { useParams, useSearchParams } from 'react-router-dom'; + +import { getPeople } from '../../api'; +import { Loader } from '../../components/Loader/Loader'; +import { PeopleFilters } from '../PeopleFilters'; +import { PeopleTable } from '../PeopleTable'; +import { Person } from '../../types/Person'; + +export const PeoplePage = () => { + const { slug } = useParams(); + + const [people, setPeople] = useState([]); + const [loading, setLoading] = useState(false); + const [hasError, setHasError] = useState(false); + const [searchParams] = useSearchParams(); + + const query = searchParams.get('query')?.toLowerCase() || ''; + const sort = searchParams.get('sort'); + const order = searchParams.get('order'); + const centuries = searchParams.getAll('centuries'); + + const filteredPeople = people.filter(person => { + const matchesQuery = + person.name.toLowerCase().includes(query) || + (person.motherName?.toLowerCase().includes(query) ?? false) || + (person.fatherName?.toLowerCase().includes(query) ?? false); + + const birthCentury = Math.ceil(person.born / 100); + + const matchesCentury = + !centuries.length || centuries.includes(String(birthCentury)); + + return matchesQuery && matchesCentury; + }); + + const sortedPeople = [...filteredPeople]; + + if (sort) { + sortedPeople.sort((a, b) => { + let result = 0; + + switch (sort) { + case 'name': + result = a.name.localeCompare(b.name); + break; + + case 'sex': + result = a.sex.localeCompare(b.sex); + break; + + case 'born': + result = a.born - b.born; + break; + + case 'died': + result = a.died - b.died; + break; + } + + return order === 'desc' ? -result : result; + }); + } + + useEffect(() => { + setLoading(true); + setHasError(false); + + getPeople() + .then(setPeople) + .catch(() => setHasError(true)) + .finally(() => setLoading(false)); + }, []); + + return ( + <> +

People Page

+ +
+ {loading && } + + {!loading && hasError && ( +

+ Something went wrong +

+ )} + + {!loading && !hasError && people.length === 0 && ( +

There are no people on the server

+ )} + + {!loading && !hasError && people.length > 0 && ( +
+
+
+ +
+ +
+
+ +
+
+
+
+ )} +
+ + ); +}; From 3b395880b4875dda8c8a4719ad456bf2f8165653 Mon Sep 17 00:00:00 2001 From: Inna K Date: Tue, 9 Jun 2026 13:43:55 +0300 Subject: [PATCH 2/5] fixed the code --- src/App.scss | 10 ++++++++++ src/components/PersonLink.tsx | 21 ++++++++++++--------- src/components/SearchLink.tsx | 11 ++++++++--- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/App.scss b/src/App.scss index c04e1c33f..94afd154d 100644 --- a/src/App.scss +++ b/src/App.scss @@ -5,3 +5,13 @@ iframe { .title { padding-top: 48px; } + +.navbar { + position: fixed; + top: 0; + z-index: 1000; +} + +.section { + padding-top: 70px; +} diff --git a/src/components/PersonLink.tsx b/src/components/PersonLink.tsx index 9d0382fba..beb656337 100644 --- a/src/components/PersonLink.tsx +++ b/src/components/PersonLink.tsx @@ -1,15 +1,18 @@ -import { Link } from 'react-router-dom'; +import { SearchLink } from './SearchLink'; import { Person } from '../types/Person'; type Props = { person: Person; }; -export const PersonLink = ({ person }: Props) => ( - - {person.name} - -); +export const PersonLink = ({ person }: Props) => { + return ( + + {person.name} + + ); +}; diff --git a/src/components/SearchLink.tsx b/src/components/SearchLink.tsx index f78b83cbc..ac010d156 100644 --- a/src/components/SearchLink.tsx +++ b/src/components/SearchLink.tsx @@ -1,12 +1,15 @@ -import { Link, LinkProps, useSearchParams } from 'react-router-dom'; +import { Link, 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` * along with the custom `params` prop that we use for updating the search */ -type Props = Omit & { +type Props = { + to: string; params: SearchParams; + children: React.ReactNode; + className?: string; }; /** @@ -14,10 +17,11 @@ type Props = Omit & { * and the other existing search params (see `getSearchWith`) */ export const SearchLink: React.FC = ({ + to, children, // this is the content between the open and closing tags params, // the params to be updated in the `search` ...props // all usual Link props like `className`, `style` and `id` -}) => { +}: Props) => { const [searchParams] = useSearchParams(); return ( @@ -26,6 +30,7 @@ export const SearchLink: React.FC = ({ // to={{ search: getSearchWith(searchParams, { query: null }) }} // to={{ search: getSearchWith(searchParams, { centuries: ['16', '18'] }) }} to={{ + pathname: to, search: getSearchWith(searchParams, params), }} {...props} // copy all the other props From 28560d035947cf6f9eecb66608416a022413a9ea Mon Sep 17 00:00:00 2001 From: Inna K Date: Tue, 9 Jun 2026 13:59:26 +0300 Subject: [PATCH 3/5] change App.scss --- src/App.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/App.scss b/src/App.scss index 94afd154d..b41587f53 100644 --- a/src/App.scss +++ b/src/App.scss @@ -15,3 +15,8 @@ iframe { .section { padding-top: 70px; } + +.table-container { + position: relative; + z-index: 1; +} From 17c5ab4e19941deec2b3917f18d55ad46b9abbe1 Mon Sep 17 00:00:00 2001 From: Inna K Date: Tue, 9 Jun 2026 14:14:03 +0300 Subject: [PATCH 4/5] change z-index: 1001 --- src/App.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.scss b/src/App.scss index b41587f53..7d21576f6 100644 --- a/src/App.scss +++ b/src/App.scss @@ -18,5 +18,5 @@ iframe { .table-container { position: relative; - z-index: 1; + z-index: 1001; } From 115a477935a8c61b98219cc708266b556bffea35 Mon Sep 17 00:00:00 2001 From: Inna K Date: Tue, 9 Jun 2026 19:28:29 +0300 Subject: [PATCH 5/5] change App.scss --- src/App.scss | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/App.scss b/src/App.scss index 7d21576f6..20cbedc9b 100644 --- a/src/App.scss +++ b/src/App.scss @@ -2,21 +2,12 @@ iframe { display: none; } -.title { - padding-top: 48px; -} - .navbar { - position: fixed; - top: 0; z-index: 1000; } .section { - padding-top: 70px; + padding-top: 5rem; } -.table-container { - position: relative; - z-index: 1001; -} +