From 24e5701319a4a83804590effd2d12a68f1e8fcc8 Mon Sep 17 00:00:00 2001 From: Tymur Date: Mon, 11 May 2026 15:36:35 +0300 Subject: [PATCH] add task solution --- README.md | 2 +- src/App.tsx | 15 +- src/components/Navbar.tsx | 28 +- src/components/PeopleFilters.tsx | 160 ++++--- src/components/PeoplePage.tsx | 118 +++++- src/components/PeopleTable.tsx | 688 +++++-------------------------- src/components/PersonLink.tsx | 25 ++ 7 files changed, 372 insertions(+), 664 deletions(-) create mode 100644 src/components/PersonLink.tsx diff --git a/README.md b/README.md index 064a39440..d607b90e1 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://timurradkevic.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..d22d8ce6e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { PeoplePage } from './components/PeoplePage'; import { Navbar } from './components/Navbar'; import './App.scss'; +import { Navigate, Route, Routes } from 'react-router-dom'; export const App = () => { return ( @@ -10,9 +11,17 @@ export const App = () => {
-

Home Page

-

Page not found

- + + } /> + Home Page} /> + + } /> + + Page not found} + /> +
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3f63898b2..598203eb9 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,3 +1,6 @@ +import classNames from 'classnames'; +import { NavLink } from 'react-router-dom'; + export const Navbar = () => { return ( diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index c9c819cd3..15547708c 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,18 +1,80 @@ -export const PeopleFilters = () => { +import classNames from 'classnames'; +import React from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; + +type Param = string | number; +type Params = { + [key: string]: Param[] | Param | null; +}; + +export function getSearchWith( + params: Params, + search?: string | URLSearchParams, +) { + const newParams = new URLSearchParams(search); + + for (const [key, value] of Object.entries(params)) { + if (value === null) { + newParams.delete(key); + } else if (Array.isArray(value)) { + newParams.delete(key); + value.forEach(item => newParams.append(key, item.toString())); + } else { + newParams.set(key, value.toString()); + } + } + + return newParams.toString(); +} + +type Props = { + query: string; + centuries: string[]; + sex: string; +}; + +export const PeopleFilters: React.FC = ({ query, centuries, sex }) => { + const [searchParams, setSearchParams] = useSearchParams(); + + function setSearchWith(params: Params) { + const search = getSearchWith(params, searchParams); + + setSearchParams(search); + } + + function handleQueryChange(event: React.ChangeEvent) { + setSearchWith({ query: event.target.value || null }); + } + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx index b682bad9b..4f5f54d30 100644 --- a/src/components/PeoplePage.tsx +++ b/src/components/PeoplePage.tsx @@ -1,8 +1,99 @@ import { PeopleFilters } from './PeopleFilters'; import { Loader } from './Loader'; import { PeopleTable } from './PeopleTable'; +import { useEffect, useState } from 'react'; +import { getPeople } from '../api'; +import { useParams, useSearchParams } from 'react-router-dom'; +import { Person } from '../types'; + +function preparePeople( + people: Person[], + query: string, + centuries: string[], + sex: string, + sort: string, + sortOrder: string, +) { + const queryPeople = people.filter(person => { + const fixedQuery = query.toLowerCase().trim(); + + return ( + person.name.toLowerCase().includes(fixedQuery) || + person.fatherName?.toLowerCase().includes(fixedQuery) || + person.motherName?.toLowerCase().includes(fixedQuery) + ); + }); + + return queryPeople + .filter(person => { + const personCentury = Math.ceil(person.died / 100); + + if (sex && centuries.length !== 0) { + return person.sex === sex && centuries.includes(String(personCentury)); + } else if (sex) { + return person.sex === sex; + } else if (centuries.length !== 0) { + return centuries.includes(String(personCentury)); + } else { + return true; + } + }) + .toSorted((person1, person2) => { + if (!sort) { + return 0; + } + + const order = sortOrder === 'desc' ? -1 : 1; + + switch (sort) { + case 'born': + case 'died': + return (person1[sort] - person2[sort]) * order; + + case 'sex': + case 'name': + return person1[sort].localeCompare(person2[sort]) * order; + + default: + return 0; + } + }); +} export const PeoplePage = () => { + const [people, setPeople] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(''); + const { slug } = useParams(); + const [searchParams] = useSearchParams(); + const query = searchParams.get('query') || ''; + const centuries = searchParams.getAll('centuries') || []; + const sex = searchParams.get('sex') || ''; + const sort = searchParams.get('sort') || ''; + const order = searchParams.get('order') || ''; + const preparedPeople = preparePeople( + people, + query, + centuries, + sex, + sort, + order, + ); + + function loadPeople() { + setIsLoading(true); + + getPeople() + .then(setPeople) + .catch(error => { + setErrorMessage('Something went wrong'); + throw error; + }) + .finally(() => setIsLoading(false)); + } + + useEffect(loadPeople, []); + return ( <>

People Page

@@ -10,20 +101,35 @@ export const PeoplePage = () => {
- +
- + {isLoading && } -

Something went wrong

+ {errorMessage && ( +

{errorMessage}

+ )} -

There are no people on the server

+ {!isLoading && people.length === 0 && ( +

+ There are no people on the server +

+ )} -

There are no people matching the current search criteria

+ {!isLoading && preparedPeople.length === 0 && ( +

There are no people matching the current search criteria

+ )} - + {!isLoading && preparedPeople.length !== 0 && ( + + )}
diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index fdd814b4a..f09a992c8 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,5 +1,46 @@ +import React from 'react'; +import { Person } from '../types'; +import { PersonLink } from './PersonLink'; +import { Link, useSearchParams } from 'react-router-dom'; +import { getSearchWith } from './PeopleFilters'; + +type Props = { + slug: string | null; + people: Person[]; + sort: string; + order: string; +}; + /* eslint-disable jsx-a11y/control-has-associated-label */ -export const PeopleTable = () => { +export const PeopleTable: React.FC = ({ slug, people, sort, order }) => { + const [searchParams] = useSearchParams(); + + const getSortSearch = (field: string) => { + if (sort !== field) { + return getSearchWith({ sort: field }, searchParams); + + return; + } + + if (order === 'desc') { + return getSearchWith({ sort: null, order: null }, searchParams); + } + + return getSearchWith({ order: 'desc' }, searchParams); + }; + + const handleSortIcon = (field: string) => { + if (sort !== field) { + return 'fas fa-sort'; + } + + if (order === 'desc') { + return 'fas fa-sort-down'; + } + + return 'fas fa-sort-up'; + }; + return ( { @@ -57,588 +98,45 @@ export const PeopleTable = () => { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {people.map(person => ( + + + + + + + + + + ))}
Name - + - + - + Sex - + - + - + Born - + - + - + 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 ? ( + pers.name === person.motherName) || + person.motherName + } + /> + ) : ( + '-' + )} + + {person.fatherName ? ( + pers.name === person.fatherName) || + person.fatherName + } + /> + ) : ( + '-' + )} +
); diff --git a/src/components/PersonLink.tsx b/src/components/PersonLink.tsx new file mode 100644 index 000000000..a135230e1 --- /dev/null +++ b/src/components/PersonLink.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Person } from '../types/Person'; +import { Link } from 'react-router-dom'; +import classNames from 'classnames'; + +type Props = { + person: Person | string; +}; + +export const PersonLink: React.FC = ({ person }) => { + if (typeof person === 'string') { + return

{person}

; + } + + return ( + + {person.name} + + ); +};