diff --git a/README.md b/README.md index 064a39440..4b15a1d57 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://whomngmnt.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..055dfd648 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,26 @@ -import { PeoplePage } from './components/PeoplePage'; +import { Navigate, Route, Routes } from 'react-router-dom'; import { Navbar } from './components/Navbar'; +import { HomePage } from './pages/HomePage'; +import { NotFoundPage } from './pages/NotFoundPage'; +import { PeoplePage } from './components/PeoplePage'; import './App.scss'; -export const App = () => { - return ( -
- +export const App = () => ( +
+ -
-
-

Home Page

-

Page not found

- -
+
+
+ + } /> + } /> + } /> + } /> + } /> + } /> +
-
- ); -}; + +
+); diff --git a/src/api.ts b/src/api.ts index dafca7c8d..d886af414 100644 --- a/src/api.ts +++ b/src/api.ts @@ -9,7 +9,6 @@ function wait(delay: number) { } export async function getPeople(): Promise { - // keep this delay for testing purpose return wait(500) .then(() => fetch(API_URL)) .then(response => response.json()); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3f63898b2..1d0b7fd36 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,4 +1,9 @@ +import classNames from 'classnames'; +import { NavLink, useLocation } from 'react-router-dom'; + export const Navbar = () => { + const { search } = useLocation(); + return ( diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index c9c819cd3..5c32b0637 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,18 +1,61 @@ +import classNames from 'classnames'; +import { ChangeEvent } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { SearchLink } from './SearchLink'; +import { getSearchWith } from '../utils/searchHelper'; + +const centuryOptions = ['16', '17', '18', '19', '20']; + export const PeopleFilters = () => { + const [searchParams, setSearchParams] = useSearchParams(); + + const currentSex = searchParams.get('sex'); + const query = searchParams.get('query') || ''; + const selectedCenturies = searchParams.getAll('centuries'); + + const handleQueryChange = (event: ChangeEvent) => { + const newQuery = event.target.value; + + setSearchParams( + getSearchWith(searchParams, { + query: newQuery || null, + }), + ); + }; + + const getToggledCenturies = (century: string) => { + if (selectedCenturies.includes(century)) { + return selectedCenturies.filter(item => item !== century); + } + + return [...selectedCenturies, century]; + }; + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx index b682bad9b..f1d95ba2f 100644 --- a/src/components/PeoplePage.tsx +++ b/src/components/PeoplePage.tsx @@ -1,29 +1,137 @@ -import { PeopleFilters } from './PeopleFilters'; +/* eslint-disable @typescript-eslint/indent */ +import { useEffect, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { getPeople } from '../api'; +import { Person } from '../types'; import { Loader } from './Loader'; +import { PeopleFilters } from './PeopleFilters'; import { PeopleTable } from './PeopleTable'; +const getPeopleWithParents = (people: Person[]) => { + return people.map(person => ({ + ...person, + mother: people.find(parent => parent.name === person.motherName), + father: people.find(parent => parent.name === person.fatherName), + })); +}; + export const PeoplePage = () => { + const [people, setPeople] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [hasError, setHasError] = useState(false); + const [searchParams] = useSearchParams(); + + useEffect(() => { + setIsLoading(true); + setHasError(false); + + getPeople() + .then(loadedPeople => { + setPeople(getPeopleWithParents(loadedPeople)); + }) + .catch(() => { + setHasError(true); + }) + .finally(() => { + setIsLoading(false); + }); + }, []); + + const hasPeople = useMemo(() => people.length > 0, [people]); + + const visiblePeople = useMemo(() => { + const sex = searchParams.get('sex'); + const query = searchParams.get('query')?.trim().toLowerCase() || ''; + const centuries = searchParams.getAll('centuries'); + const sort = searchParams.get('sort'); + const order = searchParams.get('order'); + + const filteredPeople = people.filter(person => { + const matchesSex = !sex || person.sex === sex; + const matchesQuery = + !query || + person.name.toLowerCase().includes(query) || + person.motherName?.toLowerCase().includes(query) || + person.fatherName?.toLowerCase().includes(query); + const personCentury = Math.ceil(person.born / 100).toString(); + const matchesCentury = + centuries.length === 0 || centuries.includes(personCentury); + + return matchesSex && matchesQuery && matchesCentury; + }); + + if (!sort) { + return filteredPeople; + } + + return [...filteredPeople].sort((person1, person2) => { + let result = 0; + + switch (sort) { + case 'name': + result = person1.name.localeCompare(person2.name); + break; + + case 'sex': + result = person1.sex.localeCompare(person2.sex); + break; + + case 'born': + result = person1.born - person2.born; + break; + + case 'died': + result = person1.died - person2.died; + break; + + default: + result = 0; + } + + return order === 'desc' ? -result : result; + }); + }, [people, searchParams]); + return ( <>

People Page

-
- -
+ {hasPeople && !hasError && !isLoading && ( +
+ +
+ )}
- + {isLoading && } -

Something went wrong

+ {hasError && ( +

+ Something went wrong +

+ )} -

There are no people on the server

+ {!isLoading && !hasError && !hasPeople && ( +

+ There are no people on the server +

+ )} -

There are no people matching the current search criteria

+ {!isLoading && + !hasError && + hasPeople && + visiblePeople.length === 0 && ( +

+ There are no people matching the current search criteria +

+ )} - + {!isLoading && !hasError && visiblePeople.length > 0 && ( + + )}
diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index fdd814b4a..957ce7548 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,5 +1,49 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ -export const PeopleTable = () => { +import classNames from 'classnames'; +import { useParams, useSearchParams } from 'react-router-dom'; +import { Person } from '../types'; +import { PersonLink } from './PersonLink'; +import { SearchLink } from './SearchLink'; + +type Props = { + people: Person[]; +}; + +export const PeopleTable = ({ people }: Props) => { + const { slug } = useParams(); + const [searchParams] = useSearchParams(); + const sort = searchParams.get('sort'); + const order = searchParams.get('order'); + + const getSortParams = (field: string) => { + if (sort !== field) { + return { + sort: field, + order: null, + }; + } + + if (order !== 'desc') { + return { + sort: field, + order: 'desc', + }; + } + + return { + sort: null, + order: null, + }; + }; + + const getSortIcon = (field: string) => { + if (sort !== field) { + return 'fas fa-sort'; + } + + return order === 'desc' ? 'fas fa-sort-down' : 'fas fa-sort-up'; + }; + return ( { @@ -57,588 +101,39 @@ 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 && '-'} + {person.motherName && person.mother && ( + + )} + {person.motherName && !person.mother && person.motherName} + + {!person.fatherName && '-'} + {person.fatherName && person.father && ( + + )} + {person.fatherName && !person.father && person.fatherName} +
); diff --git a/src/components/PersonLink.tsx b/src/components/PersonLink.tsx new file mode 100644 index 000000000..400bf8664 --- /dev/null +++ b/src/components/PersonLink.tsx @@ -0,0 +1,25 @@ +import classNames from 'classnames'; +import { Link, useLocation } from 'react-router-dom'; +import { Person } from '../types'; + +type Props = { + person: Person; +}; + +export const PersonLink = ({ person }: Props) => { + const { search } = useLocation(); + + return ( + + {person.name} + + ); +}; diff --git a/src/components/SearchLink.tsx b/src/components/SearchLink.tsx index f78b83cbc..d4aab50e2 100644 --- a/src/components/SearchLink.tsx +++ b/src/components/SearchLink.tsx @@ -1,34 +1,19 @@ 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` - * along with the custom `params` prop that we use for updating the search - */ type Props = Omit & { params: SearchParams; }; -/** - * SearchLink updates the given `params` in the search keeping the `pathname` - * and the other existing search params (see `getSearchWith`) - */ -export const SearchLink: React.FC = ({ - 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` -}) => { +export const SearchLink: React.FC = ({ children, params, ...props }) => { const [searchParams] = useSearchParams(); return ( {children} 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/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx new file mode 100644 index 000000000..a7c487626 --- /dev/null +++ b/src/pages/NotFoundPage.tsx @@ -0,0 +1 @@ +export const NotFoundPage = () =>

Page not found

; diff --git a/src/utils/searchHelper.ts b/src/utils/searchHelper.ts index f11efd150..513c369fe 100644 --- a/src/utils/searchHelper.ts +++ b/src/utils/searchHelper.ts @@ -2,35 +2,17 @@ export type SearchParams = { [key: string]: string | string[] | null; }; -/** - * This function prepares a correct search string - * from a given currentParams and paramsToUpdate. - */ export function getSearchWith( currentParams: URLSearchParams, - paramsToUpdate: SearchParams, // it's our custom type + paramsToUpdate: SearchParams, ): string { - // copy currentParams by creating new object from a string const newParams = new URLSearchParams(currentParams.toString()); - // Here is the example of paramsToUpdate - // { - // sex: 'm', ['sex', 'm'] - // order: null, ['order', null] - // centuries: ['16', '19'], ['centuries', ['16', '19']] - // } - // - // - params with the `null` value are deleted; - // - string value is set to given param key; - // - array of strings adds several params with the same key; - Object.entries(paramsToUpdate).forEach(([key, value]) => { if (value === null) { newParams.delete(key); } else if (Array.isArray(value)) { - // we delete the key to remove old values newParams.delete(key); - value.forEach(part => { newParams.append(key, part); }); @@ -39,6 +21,5 @@ export function getSearchWith( } }); - // we return a string to use it inside links return newParams.toString(); }