diff --git a/README.md b/README.md index 064a39440..3ff9217cb 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://anastasiiadns.github.io/react_people-table-advanced/) and add it to the PR description. 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.tsx b/src/App.tsx index adcb8594e..21dcf2f0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,19 +2,31 @@ import { PeoplePage } from './components/PeoplePage'; import { Navbar } from './components/Navbar'; import './App.scss'; +import { useEffect } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { HomePage } from './components/HomePage'; +import { NotFoundPage } from './components/NotFoundPage'; export const App = () => { + useEffect(() => { + document.documentElement.classList.add('has-navbar-fixed-top'); + }, []); + return (
-
-
-

Home Page

-

Page not found

- -
-
+
+ + } /> + + } /> + } /> + + } /> + } /> + +
); }; diff --git a/src/components/HomePage.tsx b/src/components/HomePage.tsx new file mode 100644 index 000000000..2183e875c --- /dev/null +++ b/src/components/HomePage.tsx @@ -0,0 +1,7 @@ +export const HomePage = () => { + return ( +
+

Home Page

+
+ ); +}; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 3f63898b2..e6344c722 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,4 +1,9 @@ +import { Link, useLocation } from 'react-router-dom'; +import classNames from 'classnames'; + export const Navbar = () => { + const location = useLocation(); + return ( diff --git a/src/components/NotFoundPage.tsx b/src/components/NotFoundPage.tsx new file mode 100644 index 000000000..a7c487626 --- /dev/null +++ b/src/components/NotFoundPage.tsx @@ -0,0 +1 @@ +export const NotFoundPage = () =>

Page not found

; diff --git a/src/components/PeopleFilters.tsx b/src/components/PeopleFilters.tsx index c9c819cd3..d669c07d1 100644 --- a/src/components/PeopleFilters.tsx +++ b/src/components/PeopleFilters.tsx @@ -1,18 +1,58 @@ +import { useSearchParams } from 'react-router-dom'; +import React from 'react'; +import { SearchLink } from './SearchLink'; +import classNames from 'classnames'; + export const PeopleFilters = () => { + const [searchParams, setSearchParams] = useSearchParams(); + + const query = searchParams.get('query') || ''; + const centuries = searchParams.getAll('centuries') || []; + const sex = searchParams.get('sex'); + const allCenturies = ['16', '17', '18', '19', '20']; + + function handleQueryChange(event: React.ChangeEvent) { + const params = new URLSearchParams(searchParams); + const value = event.target.value; + + if (value) { + params.set('query', value); + } else { + params.delete('query'); + } + + setSearchParams(params); + } + + function getToggledCenturies(century: string) { + return centuries.includes(century) + ? centuries.filter(c => c !== century) + : [...centuries, century]; + } + return ( ); diff --git a/src/components/PeoplePage.tsx b/src/components/PeoplePage.tsx index b682bad9b..d1ee9a472 100644 --- a/src/components/PeoplePage.tsx +++ b/src/components/PeoplePage.tsx @@ -1,33 +1,114 @@ -import { PeopleFilters } from './PeopleFilters'; import { Loader } from './Loader'; import { PeopleTable } from './PeopleTable'; +import { useEffect, useState } from 'react'; +import { Person } from '../types'; +import { getPeople } from '../api'; +import { PeopleFilters } from './PeopleFilters'; +import { useSearchParams } from 'react-router-dom'; +import { SortField } from '../types/SortField'; export const PeoplePage = () => { + const [people, setPeople] = useState([]); + const [loading, setLoading] = useState(true); + const [hasError, setHasError] = useState(false); + + const [searchParams] = useSearchParams(); + + const query = searchParams.get('query')?.toLowerCase() || ''; + const sex = searchParams.get('sex'); + const hasPeople = !loading && !hasError && people.length > 0; + const centuries = searchParams.getAll('centuries') || []; + const sort = searchParams.get('sort'); + const order = searchParams.get('order'); + + const sortField = sort as SortField | null; + + useEffect(() => { + setHasError(false); + setLoading(true); + + getPeople() + .then(data => { + setPeople(data); + }) + .catch(() => setHasError(true)) + .finally(() => setLoading(false)); + }, []); + + const visiblePeople = people + .filter(person => { + if (sex && person.sex !== sex) { + return false; + } + + if (centuries.length > 0) { + const century = Math.ceil(person.born / 100); + + if (!centuries.includes(String(century))) { + return false; + } + } + + if (query) { + return ( + person.name.toLowerCase().includes(query) || + (person.motherName || '').toLowerCase().includes(query) || + (person.fatherName || '').toLowerCase().includes(query) + ); + } + + return true; + }) + .sort((a, b) => { + if (!sortField) { + return 0; + } + + const valueA = a[sortField]; + const valueB = b[sortField]; + + if (valueA > valueB) { + return order === 'desc' ? -1 : 1; + } + + if (valueA < valueB) { + return order === 'desc' ? 1 : -1; + } + + return 0; + }); + return ( - <> +

People Page

- + {hasPeople && }
- - -

Something went wrong

+ {loading && } -

There are no people on the server

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

+ Something went wrong +

+ )} -

There are no people matching the current search criteria

+ {!loading && !hasError && visiblePeople.length === 0 && ( +

No people available

+ )} - + {!loading && !hasError && visiblePeople.length > 0 && ( + + )}
- +
); }; diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index fdd814b4a..fc19589b5 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -1,5 +1,33 @@ /* eslint-disable jsx-a11y/control-has-associated-label */ -export const PeopleTable = () => { +import { Person } from '../types'; +import React from 'react'; +import { useParams, useSearchParams } from 'react-router-dom'; +import classNames from 'classnames'; +import { PersonLink } from './PersonLink'; +import { SearchLink } from './SearchLink'; + +interface Props { + people: Person[]; +} + +export const PeopleTable: React.FC = ({ people }) => { + const { slug } = useParams(); + const [searchParams] = useSearchParams(); + const sort = searchParams.get('sort'); + const order = searchParams.get('order'); + + function getNextSort(field: string) { + if (sort !== field) { + return { sort: field, order: null }; + } + + if (!order) { + return { sort: field, order: 'desc' }; + } + + return { sort: null, order: null }; + } + return ( { @@ -55,590 +83,50 @@ export const PeopleTable = () => { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {people.map(person => { + const mother = people.find( + currentPerson => currentPerson.name === person.motherName, + ); + + const father = people.find( + currentPerson => currentPerson.name === person.fatherName, + ); + + return ( + + + + + + + + + + + + ); + })}
Name - + - + Sex - + - + Born - + - + - + Died - + - + Father
- 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} + {mother ? ( + + ) : ( + person.motherName || '-' + )} + + {father ? ( + + ) : ( + person.fatherName || '-' + )} +
); diff --git a/src/components/PersonLink.tsx b/src/components/PersonLink.tsx new file mode 100644 index 000000000..1459681f1 --- /dev/null +++ b/src/components/PersonLink.tsx @@ -0,0 +1,26 @@ +import { Person } from '../types'; +import { NavLink, useLocation } from 'react-router-dom'; +import React from 'react'; +import classNames from 'classnames'; + +interface Props { + person: Person; +} + +export const PersonLink: React.FC = ({ person }) => { + const location = useLocation(); + + return ( + + {person.name} + + ); +}; diff --git a/src/types/SortField.tsx b/src/types/SortField.tsx new file mode 100644 index 000000000..926398790 --- /dev/null +++ b/src/types/SortField.tsx @@ -0,0 +1,6 @@ +export enum SortField { + Name = 'name', + Sex = 'sex', + Born = 'born', + Died = 'died', +}